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 ( - + {src} ); @@ -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