Skip to content

Commit

Permalink
Merge pull request #42 from yushiang-demo/feature-transform-controls
Browse files Browse the repository at this point in the history
Feature: transform controls
  • Loading branch information
tsengyushiang committed Sep 24, 2023
2 parents 6c6d939 + af388e2 commit 813b3c7
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 57 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
66 changes: 44 additions & 22 deletions apps/editors/decoration/ModeSwitch/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<ToolbarRnd>
<Toolbar>
<Icons.cursor
$highlight={mode === MODE.VIEW}
onClick={changeMode(MODE.VIEW)}
<Toolbar>
{data.map(({ Component, targetMode }, index) => (
<Component
key={index}
$highlight={mode === targetMode}
onClick={changeMode(targetMode)}
/>
<Icons.placeholder
$highlight={mode === MODE.ADD_2D}
onClick={changeMode(MODE.ADD_2D)}
/>
<Icons.box
$highlight={mode === MODE.ADD_3D}
onClick={changeMode(MODE.ADD_3D)}
/>
<Icons.axis
$highlight={mode === MODE.TRANSFORM}
onClick={changeMode(MODE.TRANSFORM)}
/>
</Toolbar>
</ToolbarRnd>
))}
</Toolbar>
);
};

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 <ModeSwitch {...props} data={data} />;
};
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 <ModeSwitch {...props} data={data} />;
};
1 change: 0 additions & 1 deletion apps/editors/decoration/constant.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ export const MODE = {
VIEW: "VIEW",
ADD_2D: "ADD_2D",
ADD_3D: "ADD_3D",
TRANSFORM: "TRANSFORM",
};
63 changes: 56 additions & 7 deletions apps/editors/decoration/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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 (
<>
<ThreeCanvas dev={dev} ref={threeRef} {...eventHandlers}>
Expand All @@ -134,8 +162,29 @@ const Editor = ({ data }) => {
readonly
/>
<MeshIndexMap meshes={meshes} mouse={mouse} ref={mediaIndexMap} />
{focusedMedia && (
<TransformControls
mode={transformMode}
position={focusedMedia.transformation.position}
scale={focusedMedia.transformation.scale}
quaternion={focusedMedia.transformation.quaternion}
onChange={onFocusedMediaChange}
onDraggingChanged={setIsDragging}
/>
)}
</ThreeCanvas>
<ModeSwitch mode={mode} setMode={setMode} />
<ToolbarRnd>
<EditorModeSwitch mode={mode} setMode={setMode} />
{focusedMedia && (
<>
<TransformModeSwitch
mode={transformMode}
setMode={setTransformMode}
/>
<Icons.trash onClick={deleteFocusedMedia} />
</>
)}
</ToolbarRnd>
</>
);
};
Expand Down
7 changes: 5 additions & 2 deletions components/Icon/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ export const Wrapper = styled.div`
padding: 5px;
`;

const Icon = ({ src, ...props }) => {
const Icon = ({ src, onClick, ...props }) => {
return (
<Wrapper {...props}>
<Wrapper onClick={onClick} {...props}>
<Image src={src} {...props} alt={src} />
</Wrapper>
);
Expand All @@ -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) => {
Expand Down
91 changes: 91 additions & 0 deletions packages/three/components/TransformControls/index.js
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions packages/three/core/helpers/CameraControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ function CameraControls(camera, domElement) {
};

return {
domElement,
getCamera: () => controls.object,
setEnable: (data) => {
controls.enabled = data;
Expand Down
38 changes: 13 additions & 25 deletions packages/three/index.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Loading

0 comments on commit 813b3c7

Please sign in to comment.