diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3ff828..df26d9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://github.com/olivierlacan/keep-a
### Added
- Raycaster to new media. (https://github.com/yushiang-demo/PanoToMesh/pull/38)
+- Add `TransformControls` to edit media in 3d space. (https://github.com/yushiang-demo/pano-to-mesh/pull/42)
### Changed
diff --git a/apps/editors/decoration/ModeSwitch/index.js b/apps/editors/decoration/ModeSwitch/index.js
index 03cd41d..bbaa664 100644
--- a/apps/editors/decoration/ModeSwitch/index.js
+++ b/apps/editors/decoration/ModeSwitch/index.js
@@ -1,38 +1,60 @@
import { useEffect } from "react";
-import ToolbarRnd from "../../../../components/ToolbarRnd";
import Toolbar from "../../../../components/Toolbar";
import Icons from "../../../../components/Icon";
import { MODE } from "../constant";
+import { TRANSFORM_CONTROLS_MODE } from "@pano-to-mesh/three";
-const ModeSwitch = ({ mode, setMode }) => {
+const ModeSwitch = ({ mode, setMode, data }) => {
useEffect(() => {
if (!mode) setMode(MODE.VIEW);
}, [mode, setMode]);
const changeMode = (mode) => () => setMode(mode);
return (
-
-
-
+ {data.map(({ Component, targetMode }, index) => (
+
-
-
-
-
-
+ ))}
+
);
};
-export default ModeSwitch;
+export const EditorModeSwitch = (props) => {
+ const data = [
+ {
+ Component: Icons.cursor,
+ targetMode: MODE.VIEW,
+ },
+ {
+ Component: Icons.placeholder,
+ targetMode: MODE.ADD_2D,
+ },
+ {
+ Component: Icons.box,
+ targetMode: MODE.ADD_3D,
+ },
+ ];
+ return ;
+};
+export const TransformModeSwitch = (props) => {
+ const data = [
+ {
+ Component: Icons.axis,
+ targetMode: TRANSFORM_CONTROLS_MODE.TRANSLATE,
+ },
+ {
+ Component: Icons.scale,
+ targetMode: TRANSFORM_CONTROLS_MODE.SCALE,
+ },
+ {
+ Component: Icons.rotate,
+ targetMode: TRANSFORM_CONTROLS_MODE.ROTATE,
+ },
+ ];
+ return ;
+};
diff --git a/apps/editors/decoration/constant.js b/apps/editors/decoration/constant.js
index cf00962..0667e29 100644
--- a/apps/editors/decoration/constant.js
+++ b/apps/editors/decoration/constant.js
@@ -2,5 +2,4 @@ export const MODE = {
VIEW: "VIEW",
ADD_2D: "ADD_2D",
ADD_3D: "ADD_3D",
- TRANSFORM: "TRANSFORM",
};
diff --git a/apps/editors/decoration/index.js b/apps/editors/decoration/index.js
index 60e5e5e..8185a78 100644
--- a/apps/editors/decoration/index.js
+++ b/apps/editors/decoration/index.js
@@ -12,15 +12,19 @@ import {
ThreeCanvas,
PanoramaProjectionMesh,
MeshIndexMap,
+ TransformControls,
+ TRANSFORM_CONTROLS_MODE,
} from "@pano-to-mesh/three";
import useClick2AddWalls from "../../../hooks/useClick2AddWalls";
import useDragTransformation from "../../../hooks/useDragTransformation";
import { useStoreDataToHash } from "../../../hooks/useHash";
import MediaManager from "../../../components/MediaManager";
import { MODE } from "./constant";
-import ModeSwitch from "./ModeSwitch";
+import { EditorModeSwitch, TransformModeSwitch } from "./ModeSwitch";
import { getNewMedia } from "./media";
import { MEDIA } from "../../../constant/media";
+import ToolbarRnd from "../../../components/ToolbarRnd";
+import Icons from "../../../components/Icon";
const mapMediaToMesh = (media) => {
const { transformation, type } = media;
@@ -45,7 +49,12 @@ const Editor = ({ data }) => {
const [raycasterTarget, setRaycasterTarget] = useState(null);
const [mouse, setMouse] = useState([0, 0]);
const [camera, setCamera] = useState(null);
+ const [transformMode, setTransformMode] = useState(
+ TRANSFORM_CONTROLS_MODE.TRANSLATE
+ );
const [mode, setMode] = useState(null);
+ const [focusedIndex, setFocusedIndex] = useState(null);
+ const [isDragging, setIsDragging] = useState(false);
const [media, setMedia] = useState(data.media || []);
const meshes = media.map(mapMediaToMesh);
const geometryInfo = useMemo(
@@ -81,23 +90,24 @@ const Editor = ({ data }) => {
setMouse([normalizedX, 1 - normalizedY]);
};
- const onMouseUp = ({ normalizedX, normalizedY }) => {
+ const onMouseDown = ({ normalizedX, normalizedY }) => {
const index = mediaIndexMap.current.getIndex(
normalizedX,
1 - normalizedY
);
- console.log(index);
+ setFocusedIndex((prev) => {
+ return prev === index ? null : index;
+ });
};
return {
onMouseMove,
- onMouseUp,
+ onMouseDown,
};
})();
const eventDictionary = {
- [MODE.VIEW]: objectSelectorEventHandlers,
- [MODE.TRANSFORM]: null,
+ [MODE.VIEW]: isDragging ? null : objectSelectorEventHandlers,
[MODE.ADD_3D]: handleAddPlaceholder,
[MODE.ADD_2D]: handleAddPlaceholder,
};
@@ -125,6 +135,24 @@ const Editor = ({ data }) => {
threeRef.current.cameraControls.setEnable(mode === MODE.VIEW);
}, [mode]);
+ const focusedMedia = media[focusedIndex];
+ const onFocusedMediaChange = useCallback(
+ (transformation) => {
+ setMedia((prev) => {
+ const oldMedia = [...prev];
+ oldMedia[focusedIndex].transformation = transformation;
+ return oldMedia;
+ });
+ },
+ [focusedIndex]
+ );
+ const deleteFocusedMedia = useCallback(() => {
+ setMedia((prev) => {
+ return prev.filter((_, index) => index !== focusedIndex);
+ });
+ setFocusedIndex(null);
+ }, [focusedIndex]);
+
return (
<>
@@ -134,8 +162,29 @@ const Editor = ({ data }) => {
readonly
/>
+ {focusedMedia && (
+
+ )}
-
+
+
+ {focusedMedia && (
+ <>
+
+
+ >
+ )}
+
>
);
};
diff --git a/components/Icon/index.js b/components/Icon/index.js
index 691e340..5bbcc16 100644
--- a/components/Icon/index.js
+++ b/components/Icon/index.js
@@ -27,9 +27,9 @@ export const Wrapper = styled.div`
padding: 5px;
`;
-const Icon = ({ src, ...props }) => {
+const Icon = ({ src, onClick, ...props }) => {
return (
-
+
);
@@ -49,6 +49,9 @@ const files = {
placeholder: `${IconFolder}/placeholder.svg`,
box: `${IconFolder}/box.svg`,
axis: `${IconFolder}/axis.svg`,
+ scale: `${IconFolder}/scale.svg`,
+ rotate: `${IconFolder}/rotate.svg`,
+ trash: `${IconFolder}/trash.svg`,
};
const Icons = Object.keys(files).reduce((acc, key) => {
diff --git a/packages/three/components/TransformControls/index.js b/packages/three/components/TransformControls/index.js
new file mode 100644
index 0000000..57d6ef9
--- /dev/null
+++ b/packages/three/components/TransformControls/index.js
@@ -0,0 +1,91 @@
+import { useEffect, useState } from "react";
+import * as THREE from "three";
+import { TransformControls as Controls } from "three/addons/controls/TransformControls.js";
+
+export const TRANSFORM_CONTROLS_MODE = {
+ TRANSLATE: "translate",
+ ROTATE: "rotate",
+ SCALE: "scale",
+};
+
+const TransformControls = (() => {
+ const object = new THREE.Mesh();
+
+ return ({
+ three,
+ position,
+ scale,
+ quaternion,
+ onChange,
+ onDraggingChanged,
+ mode = TRANSFORM_CONTROLS_MODE.TRANSLATE,
+ }) => {
+ const [transformControls, setTransformControls] = useState(null);
+
+ useEffect(() => {
+ const { scene, cameraControls } = three;
+
+ const control = new Controls(
+ cameraControls.getCamera(),
+ cameraControls.domElement
+ );
+ setTransformControls(control);
+ control.setSpace("local");
+ object.frustumCulled = false;
+ scene.add(object);
+
+ control.attach(object);
+ scene.add(control);
+
+ return () => {
+ scene.remove(object);
+ scene.remove(control);
+ control.dispose();
+ };
+ }, [three]);
+
+ useEffect(() => {
+ if (!transformControls) return;
+
+ const { cameraControls } = three;
+ const { onBeforeRender } = object;
+ const draggingChanged = (event) => {
+ const dragging = event.value;
+ onDraggingChanged(dragging);
+ cameraControls.setEnable(!dragging);
+ object.onBeforeRender = dragging
+ ? () => {
+ onChange({
+ position: object.position.toArray(),
+ scale: object.scale.toArray(),
+ quaternion: object.quaternion.toArray(),
+ });
+ }
+ : onBeforeRender;
+ };
+ transformControls.addEventListener("dragging-changed", draggingChanged);
+
+ return () => {
+ transformControls.removeEventListener(
+ "dragging-changed",
+ draggingChanged
+ );
+ };
+ }, [three, transformControls, onChange, onDraggingChanged]);
+
+ useEffect(() => {
+ if (!transformControls) return;
+ transformControls.setMode(mode);
+ }, [three, mode, transformControls]);
+
+ useEffect(() => {
+ if (position) object.position.fromArray(position);
+ if (quaternion) object.quaternion.fromArray(quaternion);
+ if (scale) object.scale.fromArray(scale);
+ }, [position, scale, quaternion]);
+
+ return null;
+ };
+})();
+
+export default TransformControls;
diff --git a/packages/three/core/helpers/CameraControls.js b/packages/three/core/helpers/CameraControls.js
index 918b385..08d144c 100644
--- a/packages/three/core/helpers/CameraControls.js
+++ b/packages/three/core/helpers/CameraControls.js
@@ -108,6 +108,7 @@ function CameraControls(camera, domElement) {
};
return {
+ domElement,
getCamera: () => controls.object,
setEnable: (data) => {
controls.enabled = data;
diff --git a/packages/three/index.js b/packages/three/index.js
index d7751ee..a73e260 100644
--- a/packages/three/index.js
+++ b/packages/three/index.js
@@ -1,34 +1,22 @@
-import hooks from "./hooks";
-import THREEPanoramaOutline from "./components/PanoramaOutline";
-import ProjectionMesh from "./components/PanoramaProjectionMesh";
-import TextureMesh from "./components/PanoramaTextureMesh";
-import Canvas from "./components/ThreeCanvas";
+export { default as Loaders } from "./hooks";
+export { default as PanoramaOutline } from "./components/PanoramaOutline";
+export { default as PanoramaProjectionMesh } from "./components/PanoramaProjectionMesh";
+export { default as PanoramaTextureMesh } from "./components/PanoramaTextureMesh";
+export { default as ThreeCanvas } from "./components/ThreeCanvas";
+import { getEmptyRoomGeometry, downloadMesh } from "./core/RoomGeometry";
+export { default as Css3DObject } from "./components/Css3DObject";
+export { default as TransformControls } from "./components/TransformControls";
+export { TRANSFORM_CONTROLS_MODE } from "./components/TransformControls";
+export { default as Placeholder } from "./components/Placeholder";
+export { default as MeshIndexMap } from "./components/MeshIndexMap";
+export * as Media from "./helpers/MediaLoader";
+
import Math from "./core/helpers/Math";
import {
raycastGeometry,
raycastMeshFromScreen,
} from "./core/helpers/Raycaster";
-import { getEmptyRoomGeometry, downloadMesh } from "./core/RoomGeometry";
-import THREECss3DObject from "./components/Css3DObject";
-import THREEPlaceholder from "./components/Placeholder";
-import THREEMeshIndexMap from "./components/MeshIndexMap";
-import { getBoxMesh, getPlaneMesh } from "./helpers/MediaLoader";
-
-// React components
-export const ThreeCanvas = Canvas;
-export const Css3DObject = THREECss3DObject;
-export const Placeholder = THREEPlaceholder;
-export const MeshIndexMap = THREEMeshIndexMap;
-export const PanoramaOutline = THREEPanoramaOutline;
-export const PanoramaProjectionMesh = ProjectionMesh;
-export const PanoramaTextureMesh = TextureMesh;
-
-// React Hooks
-export const Loaders = hooks;
-
-export const Media = { getBoxMesh, getPlaneMesh };
-// THREE Algorithm
export const Core = {
Math,
raycastGeometry,
diff --git a/public/icons/rotate.svg b/public/icons/rotate.svg
new file mode 100644
index 0000000..09ca49b
--- /dev/null
+++ b/public/icons/rotate.svg
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/public/icons/scale.svg b/public/icons/scale.svg
new file mode 100644
index 0000000..e35ce61
--- /dev/null
+++ b/public/icons/scale.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/public/icons/trash.svg b/public/icons/trash.svg
new file mode 100644
index 0000000..26ad1e2
--- /dev/null
+++ b/public/icons/trash.svg
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file