From 8ab13db3851f748c55f5c7306a483a38cab5c36f Mon Sep 17 00:00:00 2001 From: Emily Dodds Date: Wed, 5 Oct 2022 16:35:21 -0500 Subject: [PATCH 1/2] Fix: Refactor to improve ViewCursorWidget performance --- packages/scene-composer/package.json | 8 +- .../src/assets/viewpoints/IconSvgs.tsx | 2 +- .../UpdateJsxIntrinsicElements.ts | 8 +- .../three-fiber/anchor/AnchorWidget.tsx | 8 +- .../viewpoint/ViewCursorWidget.spec.tsx | 43 +- .../viewpoint/ViewCursorWidget.tsx | 146 ++-- .../ViewCursorWidget.spec.tsx.snap | 795 ++++++++++++++++++ .../src/augmentations/three/Viewpoint.ts | 73 -- .../src/augmentations/three/index.ts | 3 +- .../components/WebGLCanvasManager.spec.tsx | 18 +- .../src/components/WebGLCanvasManager.tsx | 34 +- .../WebGLCanvasManager.spec.tsx.snap | 49 +- .../panels/SceneNodeInspectorPanel.tsx | 4 - .../three-fiber/EntityGroup/index.tsx | 6 +- .../ModelRefComponent/GLTFModelComponent.tsx | 32 +- .../floatingToolbar/items/AddObjectMenu.tsx | 9 +- .../scene-composer/src/models/SceneModels.ts | 7 - .../src/utils/nodeUtils.spec.ts | 19 - .../scene-composer/src/utils/nodeUtils.ts | 7 +- .../ViewCursorWidget.spec.tsx.snap | 43 - .../augmentations/three/Viewpoint.spec.ts | 87 -- 21 files changed, 956 insertions(+), 445 deletions(-) rename packages/scene-composer/{tests => src}/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx (53%) create mode 100644 packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/__snapshots__/ViewCursorWidget.spec.tsx.snap delete mode 100644 packages/scene-composer/src/augmentations/three/Viewpoint.ts delete mode 100644 packages/scene-composer/tests/augmentations/components/three-fiber/viewpoint/__snapshots__/ViewCursorWidget.spec.tsx.snap delete mode 100644 packages/scene-composer/tests/augmentations/three/Viewpoint.spec.ts diff --git a/packages/scene-composer/package.json b/packages/scene-composer/package.json index 838b3d4ef..5c0a787ca 100644 --- a/packages/scene-composer/package.json +++ b/packages/scene-composer/package.json @@ -154,10 +154,10 @@ "jest": { "coverageThreshold": { "global": { - "lines": 76.64, - "statements": 75.75, - "functions": 76.03, - "branches": 62.22, + "lines": 76.84, + "statements": 75.97, + "functions": 76.29, + "branches": 62.61, "branchesTrue": 100 } } diff --git a/packages/scene-composer/src/assets/viewpoints/IconSvgs.tsx b/packages/scene-composer/src/assets/viewpoints/IconSvgs.tsx index e50f0afd0..4bc04a7b9 100644 --- a/packages/scene-composer/src/assets/viewpoints/IconSvgs.tsx +++ b/packages/scene-composer/src/assets/viewpoints/IconSvgs.tsx @@ -16,7 +16,7 @@ export const SelectedViewpointSvgString = ` `; export const ViewCursorMoveSvgString = ` - + diff --git a/packages/scene-composer/src/augmentations/UpdateJsxIntrinsicElements.ts b/packages/scene-composer/src/augmentations/UpdateJsxIntrinsicElements.ts index e215c2380..24f001763 100644 --- a/packages/scene-composer/src/augmentations/UpdateJsxIntrinsicElements.ts +++ b/packages/scene-composer/src/augmentations/UpdateJsxIntrinsicElements.ts @@ -1,6 +1,6 @@ import { Node, Object3DNode } from '@react-three/fiber'; -import { Anchor, ViewCursor, Viewpoint } from './three'; +import { Anchor } from './three'; import { WidgetSprite, WidgetVisual } from './three/visuals'; export type AnchorProps = Object3DNode; @@ -9,10 +9,6 @@ export type WidgetVisualProps = Node; export type WidgetSpriteProps = Node; -export type ViewpointProps = Object3DNode; - -export type ViewCursorProps = Object3DNode; - /** * Adds the Anchor type and props to the JSX.IntrinsicElements namespace */ @@ -23,8 +19,6 @@ declare global { anchor: AnchorProps; widgetVisual: WidgetVisualProps; widgetSprite: WidgetSpriteProps; - viewpoint: ViewpointProps; - viewCursor: ViewCursorProps; } } } diff --git a/packages/scene-composer/src/augmentations/components/three-fiber/anchor/AnchorWidget.tsx b/packages/scene-composer/src/augmentations/components/three-fiber/anchor/AnchorWidget.tsx index a96eaf451..8fcccbecf 100644 --- a/packages/scene-composer/src/augmentations/components/three-fiber/anchor/AnchorWidget.tsx +++ b/packages/scene-composer/src/augmentations/components/three-fiber/anchor/AnchorWidget.tsx @@ -200,11 +200,15 @@ export function AsyncLoadedAnchorWidget({ }, [linesRef.current]); const parentScale = new THREE.Vector3(1, 1, 1); + let targetParent; if (parent) { - parent.getWorldScale(parentScale); + targetParent = getObject3DFromSceneNodeRef(parent.userData.targetRef); + targetParent + ? targetParent.getWorldScale(parentScale) + : parent.getWorldScale(parentScale) } - const finalScale = parent ? new THREE.Vector3(1, 1, 1).divide(parentScale) : new THREE.Vector3(1, 1, 1); + const finalScale = targetParent ? new THREE.Vector3(1, 1, 1).divide(parentScale) : new THREE.Vector3(1, 1, 1); return ( diff --git a/packages/scene-composer/tests/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx b/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx similarity index 53% rename from packages/scene-composer/tests/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx rename to packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx index 19fedbad9..f8126e556 100644 --- a/packages/scene-composer/tests/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx +++ b/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx @@ -2,15 +2,13 @@ import * as THREE from 'three'; import React from 'react'; import renderer from 'react-test-renderer'; +import { act } from '@testing-library/react'; import { useLoader, useThree } from '@react-three/fiber'; +import { MockTransformControls } from '../../../../../tests/__mocks__/MockTransformControls'; -import { ViewCursorWidget } from '../../../../../src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget'; -import { Viewpoint } from '../../../../../src'; +import { ViewCursorWidget } from '../viewpoint/ViewCursorWidget'; import { useStore } from '../../../../../src/store'; - -jest.mock('../../../../../src/augmentations/components/three-fiber/common/SvgIconToWidgetVisual', () => - jest.fn(((data, name, props) =>
) as any), -); +import SceneLayout from '../../../../../src/layouts/SceneLayout/SceneLayout'; jest.mock('@react-three/fiber', () => { const originalModule = jest.requireActual('@react-three/fiber'); @@ -25,22 +23,21 @@ jest.mock('@react-three/fiber', () => { }; }); -describe('ViewCursorWidget', () => { - const closestViewpoint = new Viewpoint(); - closestViewpoint.position.set(5, 5, 5); - closestViewpoint.userData = { nodeRef: 'closest' }; - - const furthestViewpoint = new Viewpoint(); - furthestViewpoint.position.set(15, 15, 15); - furthestViewpoint.userData = { nodeRef: 'furthest' }; +const Layout: React.FC = () => { + return ( + { }} + LoadingView={
} + isViewing={false} + showMessageModal={false} + > + + + ) +}; +describe('ViewCursorWidget', () => { const scene = new THREE.Scene(); - scene.add(closestViewpoint, furthestViewpoint); - - const baseState: any = { - cursorVisible: true, - cursorStyle: 'move', - }; beforeEach(() => { (useLoader as unknown as jest.Mock).mockReturnValue(['TestSvgData']); @@ -52,7 +49,7 @@ describe('ViewCursorWidget', () => { cursorVisible: true, cursorStyle: 'move', }); - const container = renderer.create(); + const container = renderer.create(); expect(container).toMatchSnapshot(); }); @@ -61,9 +58,7 @@ describe('ViewCursorWidget', () => { cursorVisible: true, cursorStyle: 'edit', }); - const container = renderer.create(); + const container = renderer.create(); expect(container).toMatchSnapshot(); }); - - // TODO: Add tests to send onPointerDown and onPointerUp and verify closet is passed to setViewpointNodeRef }); diff --git a/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.tsx b/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.tsx index c7434633e..fc70d0ea0 100644 --- a/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.tsx +++ b/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.tsx @@ -1,76 +1,108 @@ -import * as THREE from 'three'; -import React, { useContext, useEffect, useMemo, useRef } from 'react'; -import { extend, useLoader, useThree } from '@react-three/fiber'; -import { SVGLoader, SVGResult } from 'three-stdlib'; +import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; +import { useFrame, useLoader, useThree } from '@react-three/fiber'; +import { AlwaysDepth as THREEAlwaysDepth, Box3 as THREEBox3, Color as THREEColor, DoubleSide as THREEDoubleSide, Group as THREEGroup, Mesh as THREEMesh, MeshBasicMaterial as THREEMeshBasicMaterial, Object3D as THREEObject3D, ShapeGeometry as THREEShapeGeometry, Vector3 as THREEVector3 } from 'three'; +import { SVGLoader } from 'three-stdlib'; -import { ViewCursorMoveIcon, ViewCursorEditIcon } from '../../../../assets'; -import svgIconToWidgetVisual from '../common/SvgIconToWidgetVisual'; -import { WidgetVisual } from '../../../three/visuals'; -import { ViewCursor, Viewpoint, ViewpointState } from '../../../three'; +import { ViewCursorEditIcon as THREEViewCursorEditIcon, ViewCursorMoveIcon } from '../../../../assets'; +import { getIntersectionTransform } from '../../../../utils/raycastUtils'; import { sceneComposerIdContext } from '../../../../common/sceneComposerIdContext'; import { useEditorState } from '../../../../store'; -// Adds the custom objects to React Three Fiber -extend({ ViewCursor, Viewpoint, WidgetVisual }); - -const AsyncLoadViewCursorWidget: React.FC = () => { +export const ViewCursorWidget = () => { + const ref = useRef(null); + const { gl } = useThree(); const sceneComposerId = useContext(sceneComposerIdContext); - const { cursorPosition, cursorLookAt, cursorVisible, cursorStyle } = useEditorState(sceneComposerId); - const viewCursorRef = useRef(null); - const { camera } = useThree(); + const { addingWidget, cursorVisible, cursorStyle, setAddingWidget, setCursorVisible, setCursorStyle } = useEditorState(sceneComposerId); + const svg = cursorStyle === 'edit' ? THREEViewCursorEditIcon : ViewCursorMoveIcon; + const data = useLoader(SVGLoader, svg.dataUri); - const moveVisual = useMemo(() => { - const svgData: SVGResult = useLoader(SVGLoader, ViewCursorMoveIcon.dataUri); - return svgIconToWidgetVisual(svgData, ViewpointState.Deselected, false, { userData: { isCursor: true } }); - }, []); + const esc = useCallback(() => { + gl.domElement.addEventListener('keyup', (e: KeyboardEvent) => { + if (e.key === 'Escape' && !!addingWidget) { + setAddingWidget(undefined); + } + }); + return gl.domElement?.removeEventListener('keyup', setAddingWidget as any); + }, [addingWidget]) - const editVisual = useMemo(() => { - const svgData: SVGResult = useLoader(SVGLoader, ViewCursorEditIcon.dataUri); - return svgIconToWidgetVisual(svgData, ViewpointState.Selected, false, { userData: { isCursor: true } }); - }, []); + /* istanbul ignore next */ + const resetObjectCenter = useCallback((object: THREEObject3D) => { + const box = new THREEBox3().setFromObject(object); + box.getCenter(object.position); + object.position.multiplyScalar(-1); + }, [ref]); - useEffect(() => { - if (viewCursorRef.current) { - viewCursorRef.current.lookAt(cursorLookAt); - viewCursorRef.current.rotation.z = camera.rotation.z; // Prevent spinning and always be straight up and down - } - }, [cursorLookAt]); + /* istanbul ignore next */ + const shape = useMemo(() => { + const paths = data.paths; + const svgGroup = new THREEGroup(); - useEffect(() => { - if (viewCursorRef.current) { - viewCursorRef.current.traverse((child) => { - const mesh = child as THREE.Mesh; + paths?.forEach((path) => { + const fillColor = path?.userData?.style.fill; + if (fillColor !== undefined && fillColor !== 'none') { + const material = new THREEMeshBasicMaterial({ + color: new THREEColor().setStyle(fillColor).convertSRGBToLinear(), + opacity: path?.userData?.style.fillOpacity, + transparent: true, + depthFunc: THREEAlwaysDepth, + side: THREEDoubleSide + }); + const shapes = SVGLoader.createShapes(path); + shapes.forEach((line) => { + const geometry = new THREEShapeGeometry(line); + const mesh = new THREEMesh(geometry, material); + resetObjectCenter(mesh); + svgGroup.add(mesh); + }); + } - if (mesh.material) { - if (Array.isArray(mesh.material)) { - mesh.material.forEach((material) => { - material.depthFunc = THREE.AlwaysDepth; - }); - } else { - (mesh.material as THREE.Material).depthFunc = THREE.AlwaysDepth; + const strokeColor = path?.userData?.style.stroke; + if (strokeColor !== undefined && strokeColor !== 'none') { + const material = new THREEMeshBasicMaterial({ + color: new THREEColor().setStyle(strokeColor).convertSRGBToLinear(), + opacity: path?.userData?.style.strokeOpacity, + transparent: true, + depthFunc: THREEAlwaysDepth, + side: THREEDoubleSide + }); + path.subPaths.forEach((childPath) => { + const geometry = SVGLoader.pointsToStroke(childPath.getPoints(), path?.userData?.style); + if (geometry) { + const mesh = new THREEMesh(geometry, material); + resetObjectCenter(mesh); + svgGroup.add(mesh); } - } - }); + }); + } + }); + svgGroup.scale.multiplyScalar(0.005); + return svgGroup; + }, [data]); + + /* istanbul ignore next */ + useFrame(({ raycaster, scene }) => { + const sceneMeshes: THREEObject3D[] = []; + scene.traverse((child) => { + return shape.id !== child.id && (child as THREEMesh).isMesh && child.type !== 'TransformControlsPlane' ? sceneMeshes.push(child as THREEMesh) : null; + }); + const intersects = raycaster.intersectObjects(sceneMeshes, false); + if (intersects.length) { + const n = getIntersectionTransform(intersects[0]); + shape.lookAt(n.normal as THREEVector3); + shape.position.copy(n.position); } - }, [viewCursorRef]); + }); - return ( - - {moveVisual} - {editVisual} - - ); -}; + useEffect(() => { + setCursorVisible(!!addingWidget); + setCursorStyle(addingWidget ? 'edit' : 'move'); + esc() + gl.domElement.style.cursor = addingWidget ? 'none' : 'auto'; + }, [addingWidget]); -export const ViewCursorWidget: React.FC = () => { return ( - + {cursorVisible && } ); }; diff --git a/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/__snapshots__/ViewCursorWidget.spec.tsx.snap b/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/__snapshots__/ViewCursorWidget.spec.tsx.snap new file mode 100644 index 000000000..2425b4962 --- /dev/null +++ b/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/__snapshots__/ViewCursorWidget.spec.tsx.snap @@ -0,0 +1,795 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ViewCursorWidget should render correctly with edit style 1`] = ` +.c13 { + width: 600px; + margin: auto; + -webkit-flex: none; + -ms-flex: none; + flex: none; + z-index: 1000; +} + +.c0 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + width: 100%; + height: 100%; + background-color: colorBackgroundLayoutMain; +} + +.c1 { + background-color: colorBackgroundContainerHeader; + z-index: 100; +} + +.c2 { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + position: relative; + overflow: hidden; +} + +.c8 { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + position: relative; + overflow: hidden; +} + +.c9 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + position: relative; + background-color: colorBackgroundContainerContent; +} + +.c11 { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + overflow: hidden; + padding: 4px; + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c12 { + position: absolute; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + left: 0; + top: 0; + right: 0; + bottom: 0; + z-index: 1000; + background-color: rgba(0,0,0,0.7); +} + +.c3 { + background-color: colorBackgroundContainerContent; + -webkit-flex: none; + -ms-flex: none; + flex: none; + z-index: 99; +} + +.c14 { + background-color: colorBackgroundContainerContent; + -webkit-flex: none; + -ms-flex: none; + flex: none; + height: 100%; +} + +.c10 { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-box-pack: right; + -webkit-justify-content: right; + -ms-flex-pack: right; + justify-content: right; +} + +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + height: 100%; +} + +.c15 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row-reverse; + -ms-flex-direction: row-reverse; + flex-direction: row-reverse; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + height: 100%; +} + +.c5 { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.c7 { + -webkit-flex: none; + -ms-flex: none; + flex: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border-left: 1px solid colorBorderDividerDefault; + border-right: 1px solid colorBorderDividerDefault; +} + +.c7:hover { + background-color: colorBackgroundDropdownItemHover; +} + +.c6 { + width: 400px; + height: 100%; + overflow-y: auto; +} + +.c16 { + width: 400px; + height: 100%; + overflow-y: auto; +} + +
+
+
+
+
+
+
+
, + "id": "Hierarchy", + "label": "Hierarchy", + }, + Object { + "content": , + "id": "Rules", + "label": "Rules", + }, + Object { + "content": , + "id": "Settings", + "label": "Settings", + }, + ] + } + /> +
+
+
+
+
+
+
+
+
+
+
, + "id": "MotionIndicator", + "text": "Motion indicator", + }, + ] + } + onItemClick={[Function]} + > + View Options +
+
+
+
+
+
+
+
+
+ Error +
+
+
+

+ This browser does not support ResizeObserver out of the box. See: https://github.com/react-spring/react-use-measure/#resize-observer-polyfills +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
, + "id": "Inspector", + "label": "Inspector", + }, + ] + } + /> +
+
+
+
+
+
+
+
+
+`; + +exports[`ViewCursorWidget should render correctly with move style 1`] = ` +.c13 { + width: 600px; + margin: auto; + -webkit-flex: none; + -ms-flex: none; + flex: none; + z-index: 1000; +} + +.c0 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + width: 100%; + height: 100%; + background-color: colorBackgroundLayoutMain; +} + +.c1 { + background-color: colorBackgroundContainerHeader; + z-index: 100; +} + +.c2 { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + position: relative; + overflow: hidden; +} + +.c8 { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + position: relative; + overflow: hidden; +} + +.c9 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + position: relative; + background-color: colorBackgroundContainerContent; +} + +.c11 { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + overflow: hidden; + padding: 4px; + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c12 { + position: absolute; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + left: 0; + top: 0; + right: 0; + bottom: 0; + z-index: 1000; + background-color: rgba(0,0,0,0.7); +} + +.c3 { + background-color: colorBackgroundContainerContent; + -webkit-flex: none; + -ms-flex: none; + flex: none; + z-index: 99; +} + +.c14 { + background-color: colorBackgroundContainerContent; + -webkit-flex: none; + -ms-flex: none; + flex: none; + height: 100%; +} + +.c10 { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-box-pack: right; + -webkit-justify-content: right; + -ms-flex-pack: right; + justify-content: right; +} + +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + height: 100%; +} + +.c15 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row-reverse; + -ms-flex-direction: row-reverse; + flex-direction: row-reverse; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + height: 100%; +} + +.c5 { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.c7 { + -webkit-flex: none; + -ms-flex: none; + flex: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border-left: 1px solid colorBorderDividerDefault; + border-right: 1px solid colorBorderDividerDefault; +} + +.c7:hover { + background-color: colorBackgroundDropdownItemHover; +} + +.c6 { + width: 400px; + height: 100%; + overflow-y: auto; +} + +.c16 { + width: 400px; + height: 100%; + overflow-y: auto; +} + +
+
+
+
+
+
+
+
, + "id": "Hierarchy", + "label": "Hierarchy", + }, + Object { + "content": , + "id": "Rules", + "label": "Rules", + }, + Object { + "content": , + "id": "Settings", + "label": "Settings", + }, + ] + } + /> +
+
+
+
+
+
+
+
+
+
+
, + "id": "MotionIndicator", + "text": "Motion indicator", + }, + ] + } + onItemClick={[Function]} + > + View Options +
+
+
+
+
+
+
+
+
+ Error +
+
+
+

+ This browser does not support ResizeObserver out of the box. See: https://github.com/react-spring/react-use-measure/#resize-observer-polyfills +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
, + "id": "Inspector", + "label": "Inspector", + }, + ] + } + /> +
+
+
+
+
+
+
+
+
+`; diff --git a/packages/scene-composer/src/augmentations/three/Viewpoint.ts b/packages/scene-composer/src/augmentations/three/Viewpoint.ts deleted file mode 100644 index 0359d767e..000000000 --- a/packages/scene-composer/src/augmentations/three/Viewpoint.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as THREE from 'three'; - -import DebugLogger from '../../logger/DebugLogger'; - -import { WidgetVisual } from './visuals'; - -export enum ViewpointState { - Deselected = 'Deselected', - Selected = 'Selected', -} - -/** - * The generic Viewpoint object. - */ -export class Viewpoint extends THREE.Object3D { - private log = new DebugLogger('Viewpoint'); - - protected visualMap = new Map(); - protected _visualState: string = ViewpointState.Deselected; - - constructor() { - super(); - this.type = 'Viewpoint'; - this.rotateX(THREE.MathUtils.degToRad(90)); - } - - // @ts-ignore - public add(visualWithStateName: WidgetVisual): this { - const stateName = visualWithStateName.name; - if (!stateName) { - this.log?.error('A name must be provided for the visual. This is used to determine its appearance.'); - return this; - } - - if (!visualWithStateName.visual) { - this.log?.error('A visual must be provided.'); - return this; - } - - // NOTE: Add the states directly, because event bubbling to the anchor level does not seem to occur on click - // and is instead caught by the WidgetVisual - super.add(visualWithStateName.visual); - - this.visualMap.set(stateName, visualWithStateName); - - // Update the visual with the current visual state - this.updateVisualState(); - - return this; - } - - public set isSelected(value: boolean) { - this._visualState = value ? ViewpointState.Selected : ViewpointState.Deselected; - this.updateVisualState(); - } - - public get isSelected(): boolean { - return this._visualState === ViewpointState.Selected; - } - - private updateVisualState() { - this.visualMap.forEach((visual, name) => { - visual.setVisible(name === this._visualState); - }); - } -} - -export class ViewCursor extends Viewpoint { - constructor() { - super(); - this.type = 'ViewCursor'; - } -} diff --git a/packages/scene-composer/src/augmentations/three/index.ts b/packages/scene-composer/src/augmentations/three/index.ts index b6f3ecf08..82943d5a5 100644 --- a/packages/scene-composer/src/augmentations/three/index.ts +++ b/packages/scene-composer/src/augmentations/three/index.ts @@ -1,2 +1 @@ -export * from './Anchor'; -export * from './Viewpoint'; +export * from './Anchor'; \ No newline at end of file diff --git a/packages/scene-composer/src/components/WebGLCanvasManager.spec.tsx b/packages/scene-composer/src/components/WebGLCanvasManager.spec.tsx index 23389b350..041536227 100644 --- a/packages/scene-composer/src/components/WebGLCanvasManager.spec.tsx +++ b/packages/scene-composer/src/components/WebGLCanvasManager.spec.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { useThree } from '@react-three/fiber'; import { BoxGeometry, Mesh, MeshBasicMaterial, Group } from 'three'; -import { render } from '@testing-library/react'; - +import renderer from 'react-test-renderer'; +import { act } from '@testing-library/react'; import { useStore } from '../store'; import { setFeatureConfig } from '../common/GlobalSettings'; - +import { MockTransformControls } from '../../tests/__mocks__/MockTransformControls'; import { WebGLCanvasManager } from './WebGLCanvasManager'; import Mock = jest.Mock; @@ -54,6 +54,14 @@ jest.mock('@react-three/fiber', () => { }; }); +const Layout: React.FC = () => { + return ( + + + + ) +}; + describe('WebGLCanvasManagerSnap', () => { const body = document.createElement('body'); const div = document.createElement('div'); @@ -111,7 +119,7 @@ describe('WebGLCanvasManagerSnap', () => { baseState.isEditing.mockReturnValue(true); baseState.getSceneNodeByRef.mockReturnValue('childNode'); - const { container } = render(); + const container = renderer.create(); expect(container).toMatchSnapshot(); }); @@ -120,7 +128,7 @@ describe('WebGLCanvasManagerSnap', () => { baseState.isEditing.mockReturnValue(false); baseState.getSceneNodeByRef.mockReturnValue('childNode'); - const { container } = render(); + const container = renderer.create(); expect(container).toMatchSnapshot(); }); }); diff --git a/packages/scene-composer/src/components/WebGLCanvasManager.tsx b/packages/scene-composer/src/components/WebGLCanvasManager.tsx index 06bc26d89..069ca68dc 100644 --- a/packages/scene-composer/src/components/WebGLCanvasManager.tsx +++ b/packages/scene-composer/src/components/WebGLCanvasManager.tsx @@ -1,6 +1,6 @@ import * as THREE from 'three'; import * as awsui from '@awsui/design-tokens'; -import React, { useContext, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { GizmoHelper, GizmoViewport } from '@react-three/drei'; import { ThreeEvent, useThree } from '@react-three/fiber'; @@ -36,9 +36,6 @@ export const WebGLCanvasManager: React.FC = () => { const domRef = useRef(gl.domElement.parentElement); const environmentPreset = getSceneProperty(KnownSceneProperty.EnvironmentPreset); const rootNodeRefs = document.rootNodeRefs; - - const { setCursorPosition, setCursorLookAt, setCursorVisible, setCursorStyle } = useEditorState(sceneComposerId); - const [startingPointerPosition, setStartingPointerPosition] = useState(new THREE.Vector2()); const editingTargetPlaneRef = useRef(); @@ -46,36 +43,12 @@ export const WebGLCanvasManager: React.FC = () => { const MAX_CLICK_DISTANCE = 2; - useEffect(() => { - setCursorVisible(!!addingWidget); - setCursorStyle(addingWidget ? 'edit' : 'move'); - }, [addingWidget]); - useEffect(() => { if (!!environmentPreset && !(environmentPreset in presets)) { log?.error('Environment preset must be one of: ' + Object.keys(presets).join(', ')); } }, [environmentPreset]); - useEffect(() => { - window.addEventListener('keyup', (e: KeyboardEvent) => { - if (e.key === 'Escape' && !!addingWidget) { - setAddingWidget(undefined); - } - }); - }, [addingWidget]); - - const onPointerMove = (e: ThreeEvent) => { - // Show only while hidden or adding widget - if (addingWidget) { - if (e.intersections.length > 0) { - const { position, normal } = getIntersectionTransform(e.intersections[0]); - setCursorPosition(position); - setCursorLookAt(normal || new THREE.Vector3(0, 0, 0)); - } - } - }; - const onPointerDown = (e: ThreeEvent) => { setStartingPointerPosition(new THREE.Vector2(e.screenX, e.screenY)); }; @@ -103,10 +76,6 @@ export const WebGLCanvasManager: React.FC = () => { } }, [gridHelperRef.current]); - useEffect(() => { - gl.domElement.style.cursor = addingWidget ? 'none' : 'auto'; - }, [addingWidget]); - return ( @@ -147,7 +116,6 @@ export const WebGLCanvasManager: React.FC = () => { rotation={[THREE.MathUtils.degToRad(270), 0, 0]} onPointerUp={onPointerUp} onPointerDown={onPointerDown} - onPointerMove={onPointerMove} > diff --git a/packages/scene-composer/src/components/__snapshots__/WebGLCanvasManager.spec.tsx.snap b/packages/scene-composer/src/components/__snapshots__/WebGLCanvasManager.spec.tsx.snap index d46c1a69e..71d155459 100644 --- a/packages/scene-composer/src/components/__snapshots__/WebGLCanvasManager.spec.tsx.snap +++ b/packages/scene-composer/src/components/__snapshots__/WebGLCanvasManager.spec.tsx.snap @@ -1,52 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`WebGLCanvasManagerSnap should render editing correctly without immersive view feature 1`] = ` -
- - - - - -
-
-
- - - - - - -
-`; +exports[`WebGLCanvasManagerSnap should render editing correctly without immersive view feature 1`] = `null`; exports[`WebGLCanvasManagerSnap should render viewing correctly without immersive view feature 1`] = ` -
- +Array [ + , - - -
+ , +] `; diff --git a/packages/scene-composer/src/components/panels/SceneNodeInspectorPanel.tsx b/packages/scene-composer/src/components/panels/SceneNodeInspectorPanel.tsx index 3c16d5d21..2946c9bcb 100644 --- a/packages/scene-composer/src/components/panels/SceneNodeInspectorPanel.tsx +++ b/packages/scene-composer/src/components/panels/SceneNodeInspectorPanel.tsx @@ -56,10 +56,6 @@ export const SceneNodeInspectorPanel: React.FC = () => { defaultMessage: 'Motion Indicator', description: 'Expandable Section title', }, - Viewpoint: { - defaultMessage: 'Viewpoint', - description: 'Expandable Section title', - }, }); log?.verbose('render inspect panel with selected scene node ', selectedSceneNodeRef, selectedSceneNode); diff --git a/packages/scene-composer/src/components/three-fiber/EntityGroup/index.tsx b/packages/scene-composer/src/components/three-fiber/EntityGroup/index.tsx index 369b23da8..2d1b763df 100644 --- a/packages/scene-composer/src/components/three-fiber/EntityGroup/index.tsx +++ b/packages/scene-composer/src/components/three-fiber/EntityGroup/index.tsx @@ -99,6 +99,10 @@ const EntityGroup = ({ node }: IEntityGroupProps) => { [node], ); + const targetRef = componentTypes.includes(KnownComponentType.SubModelRef) + ? node.parentRef + : nodeRef + return ( { dispose={null} onPointerDown={onPointerDown} onPointerUp={onPointerUp} - userData={{ nodeRef: !isEnvironmentNode(node) ? nodeRef : undefined, componentTypes }} // Do not add ref for environment nodes + userData={{ nodeRef: !isEnvironmentNode(node) ? nodeRef : undefined, targetRef, componentTypes }} // Do not add ref for environment nodes > diff --git a/packages/scene-composer/src/components/three-fiber/ModelRefComponent/GLTFModelComponent.tsx b/packages/scene-composer/src/components/three-fiber/ModelRefComponent/GLTFModelComponent.tsx index 1d2e6a182..cf048e1f8 100644 --- a/packages/scene-composer/src/components/three-fiber/ModelRefComponent/GLTFModelComponent.tsx +++ b/packages/scene-composer/src/components/three-fiber/ModelRefComponent/GLTFModelComponent.tsx @@ -45,6 +45,8 @@ export const GLTFModelComponent: React.FC = ({ const maxAnisotropy = useMemo(() => gl.capabilities.getMaxAnisotropy(), []); const uriModifier = useStore(sceneComposerId)((state) => state.getEditorConfig().uriModifier); const appendSceneNode = useStore(sceneComposerId)((state) => state.appendSceneNode); + const getObject3DBySceneNodeRef = useStore(sceneComposerId)((state) => state.getObject3DBySceneNodeRef); + const { isEditing, addingWidget, @@ -150,22 +152,6 @@ export const GLTFModelComponent: React.FC = ({ scale = [factor, factor, factor]; } - const onPointerMove = (e: ThreeEvent) => { - // Show only while hidden or adding widget - if (hiddenWhileImmersive || addingWidget) { - setLastPointerMove(Date.now()); - - if (!cursorVisible) setCursorVisible(true); - - if (e.intersections.length > 0) { - const { position, normal } = getIntersectionTransform(e.intersections[0]); - setCursorPosition(position); - setCursorLookAt(normal || new THREE.Vector3(0, 0, 0)); - } - e.stopPropagation(); - } - }; - const onPointerDown = (e: ThreeEvent) => { setStartingPointerPosition(new THREE.Vector2(e.screenX, e.screenY)); }; @@ -174,19 +160,16 @@ export const GLTFModelComponent: React.FC = ({ if (addingWidget) { const parent = findNearestViableParentAncestorNodeRef(e.object) || clonedModelScene; const { position } = getIntersectionTransform(e.intersections[0]); - const newWidgetNode = createNodeWithPositionAndNormal(addingWidget, position, cursorLookAt, parent); - + const targetParent = getObject3DBySceneNodeRef(parent.userData.targetRef); + const newWidgetNode = createNodeWithPositionAndNormal(addingWidget, position, cursorLookAt, targetParent, parent?.userData.nodeRef) appendSceneNode(newWidgetNode); setAddingWidget(undefined); - e.stopPropagation(); } }; const onPointerUp = (e: ThreeEvent) => { const currentPosition = new THREE.Vector2(e.screenX, e.screenY); - - // Check if we treat it as a click if (startingPointerPosition.distanceTo(currentPosition) <= MAX_CLICK_DISTANCE) { if (isEditing() && addingWidget) { handleAddWidget(e); @@ -196,12 +179,7 @@ export const GLTFModelComponent: React.FC = ({ return ( - + ); }; diff --git a/packages/scene-composer/src/components/toolbars/floatingToolbar/items/AddObjectMenu.tsx b/packages/scene-composer/src/components/toolbars/floatingToolbar/items/AddObjectMenu.tsx index 8f7b15db8..ced0c6fdd 100644 --- a/packages/scene-composer/src/components/toolbars/floatingToolbar/items/AddObjectMenu.tsx +++ b/packages/scene-composer/src/components/toolbars/floatingToolbar/items/AddObjectMenu.tsx @@ -31,7 +31,6 @@ enum ObjectTypes { EnvironmentModel = 'add-environment-model', ModelShader = 'add-effect-model-shader', MotionIndicator = 'add-object-motion-indicator', - Viewpoint = 'add-object-viewpoint', Light = 'add-object-light', ViewCamera = 'add-object-view-camera', } @@ -75,7 +74,6 @@ export const AddObjectMenu = () => { const nodeMap = useStore(sceneComposerId)((state) => state.document.nodeMap); const { setAddingWidget, getObject3DBySceneNodeRef } = useEditorState(sceneComposerId); const enhancedEditingEnabled = getGlobalSettings().featureConfig[COMPOSER_FEATURES.ENHANCED_EDITING]; - const { formatMessage } = useIntl(); const { activeCameraSettings, mainCameraObject } = useActiveCamera(); @@ -148,7 +146,6 @@ export const AddObjectMenu = () => { components: [anchorComponent], parentRef: getRefForParenting(), } as ISceneNodeInternal; - if (enhancedEditingEnabled) { setAddingWidget({ type: KnownComponentType.Tag, node }); } else { @@ -250,7 +247,11 @@ export const AddObjectMenu = () => { parentRef: mustBeRoot ? undefined : getRefForParenting(), } as unknown as ISceneNodeInternal; - appendSceneNode(node); + if (enhancedEditingEnabled && !modelType) { + setAddingWidget({ type: KnownComponentType.ModelRef, node }); + } else { + appendSceneNode(node); + } }); } }; diff --git a/packages/scene-composer/src/models/SceneModels.ts b/packages/scene-composer/src/models/SceneModels.ts index cae9a1c4a..77ce6e0ee 100644 --- a/packages/scene-composer/src/models/SceneModels.ts +++ b/packages/scene-composer/src/models/SceneModels.ts @@ -115,7 +115,6 @@ export namespace Component { OpacityFilter = 'OpacityFilter', MotionIndicator = 'MotionIndicator', Space = 'Space', - Viewpoint = 'Viewpoint', } export interface IComponent { @@ -196,12 +195,6 @@ export namespace Component { | ICircularCylinderMotionIndicatorConfig; } - export interface Viewpoint extends IComponent { - skyboxImages: string[]; - cameraPosition: Vector3; - skyboxImageFormat: 'SixSided' | 'CubeMap' | 'Equirectangular'; - cameraRotation?: Vector3; - } export interface ILightShadowSettings { shadowBias?: number; shadowCameraLeft?: number; diff --git a/packages/scene-composer/src/utils/nodeUtils.spec.ts b/packages/scene-composer/src/utils/nodeUtils.spec.ts index 285ce3e5e..a0e43086c 100644 --- a/packages/scene-composer/src/utils/nodeUtils.spec.ts +++ b/packages/scene-composer/src/utils/nodeUtils.spec.ts @@ -8,38 +8,19 @@ describe('nodeUtils', () => { describe('findComponentByType', () => { const node = { components: [ - { - type: KnownComponentType.Viewpoint, - name: 'Viewpoint1', - }, { type: KnownComponentType.Tag, name: 'Tag', }, - { - type: KnownComponentType.Viewpoint, - name: 'Viewpoint2', - }, ], }; - it('should return the first matching component by type', () => { - const component = findComponentByType(node as any, KnownComponentType.Viewpoint); - - expect((component as any)!.name).toEqual('Viewpoint1'); - }); - it('should return undefined if the component is not found', () => { const component = findComponentByType(node as any, KnownComponentType.Light); expect(component).toBeUndefined(); }); - it('should return undefined if the node is undefined', () => { - const component = findComponentByType(undefined, KnownComponentType.Viewpoint); - - expect(component).toBeUndefined(); - }); }); describe('createNodeWithPositionAndNormal', () => { diff --git a/packages/scene-composer/src/utils/nodeUtils.ts b/packages/scene-composer/src/utils/nodeUtils.ts index b7da14f28..3c9c8e35e 100644 --- a/packages/scene-composer/src/utils/nodeUtils.ts +++ b/packages/scene-composer/src/utils/nodeUtils.ts @@ -31,7 +31,7 @@ export const findComponentByType = ( * @returns {boolean} true if the object is a Viable Parent */ const isViableParent = (object: THREE.Object3D): boolean => { - return !!object.userData.nodeRef; + return !!object.userData.targetRef; }; type Transform = { @@ -95,11 +95,12 @@ export const createNodeWithPositionAndNormal = ( position: THREE.Vector3, normal: THREE.Vector3, parent?: THREE.Object3D, -): ISceneNodeInternal => { + targetRef?: string + ): ISceneNodeInternal => { const finalPosition = parent?.worldToLocal(position.clone()) ?? position; return { ...newWidget.node, - parentRef: parent?.userData.nodeRef, + parentRef: targetRef || parent?.userData.nodeRef, transform: { position: finalPosition.toArray(), rotation: [0, 0, 0], // TODO: Find why the normal is producing weird orientations diff --git a/packages/scene-composer/tests/augmentations/components/three-fiber/viewpoint/__snapshots__/ViewCursorWidget.spec.tsx.snap b/packages/scene-composer/tests/augmentations/components/three-fiber/viewpoint/__snapshots__/ViewCursorWidget.spec.tsx.snap deleted file mode 100644 index 7a3c52f43..000000000 --- a/packages/scene-composer/tests/augmentations/components/three-fiber/viewpoint/__snapshots__/ViewCursorWidget.spec.tsx.snap +++ /dev/null @@ -1,43 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ViewCursorWidget should render correctly with edit style 1`] = ` - -
-
- -`; - -exports[`ViewCursorWidget should render correctly with move style 1`] = ` - -
-
- -`; diff --git a/packages/scene-composer/tests/augmentations/three/Viewpoint.spec.ts b/packages/scene-composer/tests/augmentations/three/Viewpoint.spec.ts deleted file mode 100644 index fbc753c82..000000000 --- a/packages/scene-composer/tests/augmentations/three/Viewpoint.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as THREE from 'three'; - -import { ViewCursor, Viewpoint, ViewpointState } from '../../../src'; -import { WidgetVisual } from '../../../src/augmentations/three/visuals'; - -describe('Viewpoint', () => { - let boxGeometry; - let material; - let visual; - let visualContainer; - let widgetVisual; - - beforeEach(() => { - boxGeometry = new THREE.BoxGeometry(1, 1, 1); - material = new THREE.MeshBasicMaterial(); - visual = new THREE.Mesh(boxGeometry, material); - visualContainer = new THREE.Group(); - visualContainer.add(visual); - widgetVisual = new WidgetVisual(); - jest.spyOn(widgetVisual, 'setVisible'); - jest.resetAllMocks(); - }); - - it('should be rotated about the X-Axis 90ยบ by default', () => { - const viewpoint = new Viewpoint(); - viewpoint.add(widgetVisual); - - expect(viewpoint.type).toEqual('Viewpoint'); - expect(viewpoint.rotation.x).not.toEqual(0.0); - }); - - it('should log an error if no name set on widget visual', () => { - const viewpoint = new Viewpoint(); - - const errorLog = jest.spyOn((viewpoint as any).log, 'error'); - - viewpoint.add(widgetVisual); - - expect(errorLog).toHaveBeenCalled(); - }); - - it('should log an error if no visual set on widget visual', () => { - const viewpoint = new Viewpoint(); - widgetVisual.name = 'testName'; - - const errorLog = jest.spyOn((viewpoint as any).log, 'error'); - - viewpoint.add(widgetVisual); - - expect(errorLog).toHaveBeenCalled(); - }); - - it('should add a valid visual to children and map', () => { - const viewpoint = new Viewpoint(); - widgetVisual.name = ViewpointState.Deselected; - widgetVisual.visual = visualContainer; - const visualMap = (viewpoint as any).visualMap; - - viewpoint.add(widgetVisual); - - expect(viewpoint.children).toContain(visualContainer); - expect(visualMap.has(widgetVisual.name)).toEqual(true); - expect(visualMap.get(widgetVisual.name)).toEqual(widgetVisual); - - expect(widgetVisual.setVisible).toBeCalledWith(true); - }); - - it('should update the visibility if it does not match the state', () => { - const viewpoint = new Viewpoint(); - widgetVisual.name = ViewpointState.Deselected; - widgetVisual.visual = visualContainer; - - viewpoint.add(widgetVisual); - expect(widgetVisual.setVisible).toBeCalledWith(true); - - viewpoint.isSelected = true; - - expect(widgetVisual.setVisible).toBeCalledWith(false); - }); - - describe('ViewCursor', () => { - it('should have ViewCursor type', () => { - const viewCursor = new ViewCursor(); - expect(viewCursor.type).toEqual('ViewCursor'); - }); - }); -}); From 26a2e292cc0ab1bfff15d84daee114d8415a6634 Mon Sep 17 00:00:00 2001 From: Emily Dodds Date: Thu, 6 Oct 2022 18:10:13 -0500 Subject: [PATCH 2/2] CR Feedback --- packages/scene-composer/jest.config.ts | 2 + packages/scene-composer/package.json | 8 +- .../three-fiber/anchor/AnchorWidget.tsx | 8 +- .../common/SvgIconToWidgetVisual.tsx | 120 --- .../viewpoint/ViewCursorWidget.spec.tsx | 64 -- .../viewpoint/ViewCursorWidget.tsx | 70 +- .../ViewCursorWidget.spec.tsx.snap | 795 ------------------ .../src/augmentations/three/index.ts | 2 +- .../components/WebGLCanvasManager.spec.tsx | 6 +- .../three-fiber/EntityGroup/index.tsx | 6 +- .../ModelRefComponent/GLTFModelComponent.tsx | 29 +- .../src/utils/nodeUtils.spec.ts | 1 - .../scene-composer/src/utils/nodeUtils.ts | 7 +- .../src/utils/objectThreeUtils.ts | 6 + .../scene-composer/src/utils/svgUtils.spec.ts | 43 + packages/scene-composer/src/utils/svgUtils.ts | 58 ++ 16 files changed, 158 insertions(+), 1067 deletions(-) delete mode 100644 packages/scene-composer/src/augmentations/components/three-fiber/common/SvgIconToWidgetVisual.tsx delete mode 100644 packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx delete mode 100644 packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/__snapshots__/ViewCursorWidget.spec.tsx.snap create mode 100644 packages/scene-composer/src/utils/svgUtils.spec.ts create mode 100644 packages/scene-composer/src/utils/svgUtils.ts diff --git a/packages/scene-composer/jest.config.ts b/packages/scene-composer/jest.config.ts index 6575a604f..1b6fb9ce0 100644 --- a/packages/scene-composer/jest.config.ts +++ b/packages/scene-composer/jest.config.ts @@ -14,10 +14,12 @@ export default merge.recursive(tsPreset, awsuiPreset, { '!src/**/index.ts', '!src/**/*.d.ts', '!src/**/__snapshots__/*', + '!src/assets/**/*', '!src/three/GLTFLoader.js', '!src/three/tiles3d/TilesRenderer.js', '!src/three/tiles3d/TilesRendererBase.js', '!src/utils/sceneDocumentSnapshotCreator.ts', + '!src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.tsx', // Skipping as this is around mouse movement & is interaction based. Should be covered in end-to-end test or manual testing. ], coverageReporters: [ 'json', diff --git a/packages/scene-composer/package.json b/packages/scene-composer/package.json index 5c0a787ca..5e2b4a9ad 100644 --- a/packages/scene-composer/package.json +++ b/packages/scene-composer/package.json @@ -154,10 +154,10 @@ "jest": { "coverageThreshold": { "global": { - "lines": 76.84, - "statements": 75.97, - "functions": 76.29, - "branches": 62.61, + "lines": 77.21, + "statements": 76.35, + "functions": 76.81, + "branches": 63.40, "branchesTrue": 100 } } diff --git a/packages/scene-composer/src/augmentations/components/three-fiber/anchor/AnchorWidget.tsx b/packages/scene-composer/src/augmentations/components/three-fiber/anchor/AnchorWidget.tsx index 8fcccbecf..a96eaf451 100644 --- a/packages/scene-composer/src/augmentations/components/three-fiber/anchor/AnchorWidget.tsx +++ b/packages/scene-composer/src/augmentations/components/three-fiber/anchor/AnchorWidget.tsx @@ -200,15 +200,11 @@ export function AsyncLoadedAnchorWidget({ }, [linesRef.current]); const parentScale = new THREE.Vector3(1, 1, 1); - let targetParent; if (parent) { - targetParent = getObject3DFromSceneNodeRef(parent.userData.targetRef); - targetParent - ? targetParent.getWorldScale(parentScale) - : parent.getWorldScale(parentScale) + parent.getWorldScale(parentScale); } - const finalScale = targetParent ? new THREE.Vector3(1, 1, 1).divide(parentScale) : new THREE.Vector3(1, 1, 1); + const finalScale = parent ? new THREE.Vector3(1, 1, 1).divide(parentScale) : new THREE.Vector3(1, 1, 1); return ( diff --git a/packages/scene-composer/src/augmentations/components/three-fiber/common/SvgIconToWidgetVisual.tsx b/packages/scene-composer/src/augmentations/components/three-fiber/common/SvgIconToWidgetVisual.tsx deleted file mode 100644 index 744834eb0..000000000 --- a/packages/scene-composer/src/augmentations/components/three-fiber/common/SvgIconToWidgetVisual.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import * as THREE from 'three'; -import React from 'react'; -import { SVGLoader, SVGResult } from 'three-stdlib'; - -import { DefaultAnchorStatus } from '../../../../interfaces'; -import { WidgetVisualProps } from '../../../UpdateJsxIntrinsicElements'; -import { RenderOrder } from '../../../../common/constants'; - -function resetObjectCenter(object: THREE.Object3D) { - const box = new THREE.Box3().setFromObject(object); - box.getCenter(object.position); // this re-sets the mesh position - object.position.multiplyScalar(-1); -} - -export default function svgIconToWidgetVisual( - svgData: SVGResult, - key: DefaultAnchorStatus | string, - alwaysVisible, - props?: WidgetVisualProps, -) { - const paths = svgData.paths; - const group = new THREE.Group(); - - group.scale.multiplyScalar(0.01); - - for (let i = 0; i < paths.length; i++) { - const path = paths[i]; - - const strokeColor = path.userData?.style?.stroke; - const opacity = path.userData?.style?.opacity || 1; - - if (strokeColor && strokeColor !== 'none') { - const material = new THREE.MeshBasicMaterial({ - color: new THREE.Color().setStyle(strokeColor).convertSRGBToLinear(), - opacity, - transparent: true, - side: THREE.DoubleSide, - depthWrite: false, - }); - - for (let j = 0, jl = path.subPaths.length; j < jl; j++) { - const subPath = path.subPaths[j]; - const geometry = SVGLoader.pointsToStroke(subPath.getPoints(), path.userData!.style); - if (geometry) { - const mesh = new THREE.Mesh(geometry, material); - - const altMat = material.clone(); - altMat.depthFunc = THREE.GreaterDepth; - altMat.color = altMat.color.lerp(new THREE.Color(0, 0, 0), 0.5); - const altMesh = new THREE.Mesh(geometry.clone(), altMat); - - // This is a render order hack to avoid strange rendering effect when adding new meshes to the scene. - // This render order ensures the anchors are rendered later than the base geometry. - mesh.renderOrder = RenderOrder.DrawLate; - altMesh.renderOrder = RenderOrder.DrawLate; - resetObjectCenter(mesh); - resetObjectCenter(altMesh); - - group.add(mesh); - if (alwaysVisible) { - group.add(altMesh); - } - } - } - } - - const fillColor = path.userData?.style?.fill; - if (fillColor && fillColor !== 'none') { - const material = new THREE.MeshBasicMaterial({ - color: new THREE.Color().setStyle(fillColor).convertSRGBToLinear(), - opacity, - transparent: true, - side: THREE.DoubleSide, - depthWrite: false, - }); - - // TODO: SVGLoader does not support getting smooth path, it uses default number of divisions in the getPoints call - // see: https://github.com/mrdoob/three.js/blob/e62b253081438c030d6af1ee3c3346a89124f277/src/extras/core/CurvePath.js#L155 - // https://github.com/pmndrs/three-stdlib/blob/2815d8e00ce3ae79a9b6891e852e03f18391d60a/src/loaders/SVGLoader.js#L1609 - const shapes = SVGLoader.createShapes(path); - - for (let j = 0; j < shapes.length; j++) { - const shape = shapes[j]; - const geometry = new THREE.ShapeGeometry(shape); - const mesh = new THREE.Mesh(geometry, material); - - const altMat = material.clone(); - altMat.depthFunc = THREE.GreaterDepth; - altMat.color = altMat.color.lerp(new THREE.Color(0, 0, 0), 0.5); - const altMesh = new THREE.Mesh(geometry.clone(), altMat); - - // This is a render order hack to avoid strange rendering effect when adding new meshes to the scene. - // This render order ensures the anchors are rendered later than the base geometry. - mesh.renderOrder = RenderOrder.DrawLate; - altMesh.renderOrder = RenderOrder.DrawLate; - resetObjectCenter(mesh); - resetObjectCenter(altMesh); - - group.add(mesh); - if (alwaysVisible) { - group.add(altMesh); - } - } - - // add a flag to avoid double-centering the group when the scene is - // re-rendered, which will cancel the effect. - if (!group.userData.__centered) { - resetObjectCenter(group); - group.renderOrder = RenderOrder.DrawLate; - group.userData.__centered = true; - } - } - } - - return ( - - - - ); -} diff --git a/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx b/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx deleted file mode 100644 index f8126e556..000000000 --- a/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* eslint-disable import/first */ -import * as THREE from 'three'; -import React from 'react'; -import renderer from 'react-test-renderer'; -import { act } from '@testing-library/react'; -import { useLoader, useThree } from '@react-three/fiber'; -import { MockTransformControls } from '../../../../../tests/__mocks__/MockTransformControls'; - -import { ViewCursorWidget } from '../viewpoint/ViewCursorWidget'; -import { useStore } from '../../../../../src/store'; -import SceneLayout from '../../../../../src/layouts/SceneLayout/SceneLayout'; - -jest.mock('@react-three/fiber', () => { - const originalModule = jest.requireActual('@react-three/fiber'); - - return { - ...originalModule, - useLoader: jest.fn(), - useThree: jest.fn(), - useFrame: jest.fn().mockImplementation((func) => { - func(); - }), - }; -}); - -const Layout: React.FC = () => { - return ( - { }} - LoadingView={
} - isViewing={false} - showMessageModal={false} - > - - - ) -}; - -describe('ViewCursorWidget', () => { - const scene = new THREE.Scene(); - - beforeEach(() => { - (useLoader as unknown as jest.Mock).mockReturnValue(['TestSvgData']); - (useThree as unknown as jest.Mock).mockReturnValue(scene); - }); - - it('should render correctly with move style', () => { - useStore('default').setState({ - cursorVisible: true, - cursorStyle: 'move', - }); - const container = renderer.create(); - expect(container).toMatchSnapshot(); - }); - - it('should render correctly with edit style', () => { - useStore('default').setState({ - cursorVisible: true, - cursorStyle: 'edit', - }); - const container = renderer.create(); - expect(container).toMatchSnapshot(); - }); -}); diff --git a/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.tsx b/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.tsx index fc70d0ea0..1b598e754 100644 --- a/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.tsx +++ b/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.tsx @@ -1,19 +1,21 @@ import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; import { useFrame, useLoader, useThree } from '@react-three/fiber'; -import { AlwaysDepth as THREEAlwaysDepth, Box3 as THREEBox3, Color as THREEColor, DoubleSide as THREEDoubleSide, Group as THREEGroup, Mesh as THREEMesh, MeshBasicMaterial as THREEMeshBasicMaterial, Object3D as THREEObject3D, ShapeGeometry as THREEShapeGeometry, Vector3 as THREEVector3 } from 'three'; +import { Mesh as THREEMesh, Object3D as THREEObject3D, Vector3 as THREEVector3 } from 'three'; import { SVGLoader } from 'three-stdlib'; -import { ViewCursorEditIcon as THREEViewCursorEditIcon, ViewCursorMoveIcon } from '../../../../assets'; +import { convertSvgToMesh } from '../../../../utils/svgUtils'; import { getIntersectionTransform } from '../../../../utils/raycastUtils'; import { sceneComposerIdContext } from '../../../../common/sceneComposerIdContext'; import { useEditorState } from '../../../../store'; +import { ViewCursorEditIcon, ViewCursorMoveIcon } from '../../../../assets'; export const ViewCursorWidget = () => { const ref = useRef(null); const { gl } = useThree(); const sceneComposerId = useContext(sceneComposerIdContext); - const { addingWidget, cursorVisible, cursorStyle, setAddingWidget, setCursorVisible, setCursorStyle } = useEditorState(sceneComposerId); - const svg = cursorStyle === 'edit' ? THREEViewCursorEditIcon : ViewCursorMoveIcon; + const { addingWidget, cursorVisible, cursorStyle, setAddingWidget, setCursorVisible, setCursorStyle } = + useEditorState(sceneComposerId); + const svg = cursorStyle === 'edit' ? ViewCursorEditIcon : ViewCursorMoveIcon; const data = useLoader(SVGLoader, svg.dataUri); const esc = useCallback(() => { @@ -23,67 +25,19 @@ export const ViewCursorWidget = () => { } }); return gl.domElement?.removeEventListener('keyup', setAddingWidget as any); - }, [addingWidget]) - - /* istanbul ignore next */ - const resetObjectCenter = useCallback((object: THREEObject3D) => { - const box = new THREEBox3().setFromObject(object); - box.getCenter(object.position); - object.position.multiplyScalar(-1); - }, [ref]); + }, [addingWidget]); - /* istanbul ignore next */ const shape = useMemo(() => { - const paths = data.paths; - const svgGroup = new THREEGroup(); - - paths?.forEach((path) => { - const fillColor = path?.userData?.style.fill; - if (fillColor !== undefined && fillColor !== 'none') { - const material = new THREEMeshBasicMaterial({ - color: new THREEColor().setStyle(fillColor).convertSRGBToLinear(), - opacity: path?.userData?.style.fillOpacity, - transparent: true, - depthFunc: THREEAlwaysDepth, - side: THREEDoubleSide - }); - const shapes = SVGLoader.createShapes(path); - shapes.forEach((line) => { - const geometry = new THREEShapeGeometry(line); - const mesh = new THREEMesh(geometry, material); - resetObjectCenter(mesh); - svgGroup.add(mesh); - }); - } - - const strokeColor = path?.userData?.style.stroke; - if (strokeColor !== undefined && strokeColor !== 'none') { - const material = new THREEMeshBasicMaterial({ - color: new THREEColor().setStyle(strokeColor).convertSRGBToLinear(), - opacity: path?.userData?.style.strokeOpacity, - transparent: true, - depthFunc: THREEAlwaysDepth, - side: THREEDoubleSide - }); - path.subPaths.forEach((childPath) => { - const geometry = SVGLoader.pointsToStroke(childPath.getPoints(), path?.userData?.style); - if (geometry) { - const mesh = new THREEMesh(geometry, material); - resetObjectCenter(mesh); - svgGroup.add(mesh); - } - }); - } - }); - svgGroup.scale.multiplyScalar(0.005); - return svgGroup; + return convertSvgToMesh(data); }, [data]); /* istanbul ignore next */ useFrame(({ raycaster, scene }) => { const sceneMeshes: THREEObject3D[] = []; scene.traverse((child) => { - return shape.id !== child.id && (child as THREEMesh).isMesh && child.type !== 'TransformControlsPlane' ? sceneMeshes.push(child as THREEMesh) : null; + return shape.id !== child.id && (child as THREEMesh).isMesh && child.type !== 'TransformControlsPlane' + ? sceneMeshes.push(child as THREEMesh) + : null; }); const intersects = raycaster.intersectObjects(sceneMeshes, false); if (intersects.length) { @@ -96,7 +50,7 @@ export const ViewCursorWidget = () => { useEffect(() => { setCursorVisible(!!addingWidget); setCursorStyle(addingWidget ? 'edit' : 'move'); - esc() + esc(); gl.domElement.style.cursor = addingWidget ? 'none' : 'auto'; }, [addingWidget]); diff --git a/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/__snapshots__/ViewCursorWidget.spec.tsx.snap b/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/__snapshots__/ViewCursorWidget.spec.tsx.snap deleted file mode 100644 index 2425b4962..000000000 --- a/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/__snapshots__/ViewCursorWidget.spec.tsx.snap +++ /dev/null @@ -1,795 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ViewCursorWidget should render correctly with edit style 1`] = ` -.c13 { - width: 600px; - margin: auto; - -webkit-flex: none; - -ms-flex: none; - flex: none; - z-index: 1000; -} - -.c0 { - position: relative; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - width: 100%; - height: 100%; - background-color: colorBackgroundLayoutMain; -} - -.c1 { - background-color: colorBackgroundContainerHeader; - z-index: 100; -} - -.c2 { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - position: relative; - overflow: hidden; -} - -.c8 { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - position: relative; - overflow: hidden; -} - -.c9 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - position: relative; - background-color: colorBackgroundContainerContent; -} - -.c11 { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - overflow: hidden; - padding: 4px; - position: relative; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; -} - -.c12 { - position: absolute; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - left: 0; - top: 0; - right: 0; - bottom: 0; - z-index: 1000; - background-color: rgba(0,0,0,0.7); -} - -.c3 { - background-color: colorBackgroundContainerContent; - -webkit-flex: none; - -ms-flex: none; - flex: none; - z-index: 99; -} - -.c14 { - background-color: colorBackgroundContainerContent; - -webkit-flex: none; - -ms-flex: none; - flex: none; - height: 100%; -} - -.c10 { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - -webkit-box-pack: right; - -webkit-justify-content: right; - -ms-flex-pack: right; - justify-content: right; -} - -.c4 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - -webkit-align-items: stretch; - -webkit-box-align: stretch; - -ms-flex-align: stretch; - align-items: stretch; - height: 100%; -} - -.c15 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row-reverse; - -ms-flex-direction: row-reverse; - flex-direction: row-reverse; - -webkit-align-items: stretch; - -webkit-box-align: stretch; - -ms-flex-align: stretch; - align-items: stretch; - height: 100%; -} - -.c5 { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; -} - -.c7 { - -webkit-flex: none; - -ms-flex: none; - flex: none; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - border-left: 1px solid colorBorderDividerDefault; - border-right: 1px solid colorBorderDividerDefault; -} - -.c7:hover { - background-color: colorBackgroundDropdownItemHover; -} - -.c6 { - width: 400px; - height: 100%; - overflow-y: auto; -} - -.c16 { - width: 400px; - height: 100%; - overflow-y: auto; -} - -
-
-
-
-
-
-
-
, - "id": "Hierarchy", - "label": "Hierarchy", - }, - Object { - "content": , - "id": "Rules", - "label": "Rules", - }, - Object { - "content": , - "id": "Settings", - "label": "Settings", - }, - ] - } - /> -
-
-
-
-
-
-
-
-
-
-
, - "id": "MotionIndicator", - "text": "Motion indicator", - }, - ] - } - onItemClick={[Function]} - > - View Options -
-
-
-
-
-
-
-
-
- Error -
-
-
-

- This browser does not support ResizeObserver out of the box. See: https://github.com/react-spring/react-use-measure/#resize-observer-polyfills -

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
, - "id": "Inspector", - "label": "Inspector", - }, - ] - } - /> -
-
-
-
-
-
-
-
-
-`; - -exports[`ViewCursorWidget should render correctly with move style 1`] = ` -.c13 { - width: 600px; - margin: auto; - -webkit-flex: none; - -ms-flex: none; - flex: none; - z-index: 1000; -} - -.c0 { - position: relative; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - width: 100%; - height: 100%; - background-color: colorBackgroundLayoutMain; -} - -.c1 { - background-color: colorBackgroundContainerHeader; - z-index: 100; -} - -.c2 { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - position: relative; - overflow: hidden; -} - -.c8 { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - position: relative; - overflow: hidden; -} - -.c9 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - position: relative; - background-color: colorBackgroundContainerContent; -} - -.c11 { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - overflow: hidden; - padding: 4px; - position: relative; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; -} - -.c12 { - position: absolute; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - left: 0; - top: 0; - right: 0; - bottom: 0; - z-index: 1000; - background-color: rgba(0,0,0,0.7); -} - -.c3 { - background-color: colorBackgroundContainerContent; - -webkit-flex: none; - -ms-flex: none; - flex: none; - z-index: 99; -} - -.c14 { - background-color: colorBackgroundContainerContent; - -webkit-flex: none; - -ms-flex: none; - flex: none; - height: 100%; -} - -.c10 { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - -webkit-box-pack: right; - -webkit-justify-content: right; - -ms-flex-pack: right; - justify-content: right; -} - -.c4 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - -webkit-align-items: stretch; - -webkit-box-align: stretch; - -ms-flex-align: stretch; - align-items: stretch; - height: 100%; -} - -.c15 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row-reverse; - -ms-flex-direction: row-reverse; - flex-direction: row-reverse; - -webkit-align-items: stretch; - -webkit-box-align: stretch; - -ms-flex-align: stretch; - align-items: stretch; - height: 100%; -} - -.c5 { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; -} - -.c7 { - -webkit-flex: none; - -ms-flex: none; - flex: none; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - border-left: 1px solid colorBorderDividerDefault; - border-right: 1px solid colorBorderDividerDefault; -} - -.c7:hover { - background-color: colorBackgroundDropdownItemHover; -} - -.c6 { - width: 400px; - height: 100%; - overflow-y: auto; -} - -.c16 { - width: 400px; - height: 100%; - overflow-y: auto; -} - -
-
-
-
-
-
-
-
, - "id": "Hierarchy", - "label": "Hierarchy", - }, - Object { - "content": , - "id": "Rules", - "label": "Rules", - }, - Object { - "content": , - "id": "Settings", - "label": "Settings", - }, - ] - } - /> -
-
-
-
-
-
-
-
-
-
-
, - "id": "MotionIndicator", - "text": "Motion indicator", - }, - ] - } - onItemClick={[Function]} - > - View Options -
-
-
-
-
-
-
-
-
- Error -
-
-
-

- This browser does not support ResizeObserver out of the box. See: https://github.com/react-spring/react-use-measure/#resize-observer-polyfills -

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
, - "id": "Inspector", - "label": "Inspector", - }, - ] - } - /> -
-
-
-
-
-
-
-
-
-`; diff --git a/packages/scene-composer/src/augmentations/three/index.ts b/packages/scene-composer/src/augmentations/three/index.ts index 82943d5a5..bf2aea24a 100644 --- a/packages/scene-composer/src/augmentations/three/index.ts +++ b/packages/scene-composer/src/augmentations/three/index.ts @@ -1 +1 @@ -export * from './Anchor'; \ No newline at end of file +export * from './Anchor'; diff --git a/packages/scene-composer/src/components/WebGLCanvasManager.spec.tsx b/packages/scene-composer/src/components/WebGLCanvasManager.spec.tsx index 041536227..0a5b02306 100644 --- a/packages/scene-composer/src/components/WebGLCanvasManager.spec.tsx +++ b/packages/scene-composer/src/components/WebGLCanvasManager.spec.tsx @@ -2,10 +2,10 @@ import React from 'react'; import { useThree } from '@react-three/fiber'; import { BoxGeometry, Mesh, MeshBasicMaterial, Group } from 'three'; import renderer from 'react-test-renderer'; -import { act } from '@testing-library/react'; + import { useStore } from '../store'; import { setFeatureConfig } from '../common/GlobalSettings'; -import { MockTransformControls } from '../../tests/__mocks__/MockTransformControls'; + import { WebGLCanvasManager } from './WebGLCanvasManager'; import Mock = jest.Mock; @@ -59,7 +59,7 @@ const Layout: React.FC = () => { - ) + ); }; describe('WebGLCanvasManagerSnap', () => { diff --git a/packages/scene-composer/src/components/three-fiber/EntityGroup/index.tsx b/packages/scene-composer/src/components/three-fiber/EntityGroup/index.tsx index 2d1b763df..369b23da8 100644 --- a/packages/scene-composer/src/components/three-fiber/EntityGroup/index.tsx +++ b/packages/scene-composer/src/components/three-fiber/EntityGroup/index.tsx @@ -99,10 +99,6 @@ const EntityGroup = ({ node }: IEntityGroupProps) => { [node], ); - const targetRef = componentTypes.includes(KnownComponentType.SubModelRef) - ? node.parentRef - : nodeRef - return ( { dispose={null} onPointerDown={onPointerDown} onPointerUp={onPointerUp} - userData={{ nodeRef: !isEnvironmentNode(node) ? nodeRef : undefined, targetRef, componentTypes }} // Do not add ref for environment nodes + userData={{ nodeRef: !isEnvironmentNode(node) ? nodeRef : undefined, componentTypes }} // Do not add ref for environment nodes > diff --git a/packages/scene-composer/src/components/three-fiber/ModelRefComponent/GLTFModelComponent.tsx b/packages/scene-composer/src/components/three-fiber/ModelRefComponent/GLTFModelComponent.tsx index cf048e1f8..a10d445b5 100644 --- a/packages/scene-composer/src/components/three-fiber/ModelRefComponent/GLTFModelComponent.tsx +++ b/packages/scene-composer/src/components/three-fiber/ModelRefComponent/GLTFModelComponent.tsx @@ -5,7 +5,7 @@ import { SkeletonUtils } from 'three-stdlib'; import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'; import useLifecycleLogging from '../../../logger/react-logger/hooks/useLifecycleLogging'; -import { Vector3 } from '../../../interfaces'; +import { Vector3, KnownComponentType } from '../../../interfaces'; import { IModelRefComponentInternal, ISceneNodeInternal, useEditorState, useStore } from '../../../store'; import { appendFunction } from '../../../utils/objectUtils'; import { sceneComposerIdContext } from '../../../common/sceneComposerIdContext'; @@ -17,7 +17,11 @@ import { } from '../../../utils/objectThreeUtils'; import { getScaleFactor } from '../../../utils/mathUtils'; import { getIntersectionTransform } from '../../../utils/raycastUtils'; -import { createNodeWithPositionAndNormal, findNearestViableParentAncestorNodeRef } from '../../../utils/nodeUtils'; +import { + createNodeWithPositionAndNormal, + findComponentByType, + findNearestViableParentAncestorNodeRef, +} from '../../../utils/nodeUtils'; import { useGLTF } from './GLTFLoader'; @@ -46,7 +50,7 @@ export const GLTFModelComponent: React.FC = ({ const uriModifier = useStore(sceneComposerId)((state) => state.getEditorConfig().uriModifier); const appendSceneNode = useStore(sceneComposerId)((state) => state.appendSceneNode); const getObject3DBySceneNodeRef = useStore(sceneComposerId)((state) => state.getObject3DBySceneNodeRef); - + const { getSceneNodeByRef } = useStore(sceneComposerId)((state) => state); const { isEditing, addingWidget, @@ -158,10 +162,23 @@ export const GLTFModelComponent: React.FC = ({ const handleAddWidget = (e: ThreeEvent) => { if (addingWidget) { - const parent = findNearestViableParentAncestorNodeRef(e.object) || clonedModelScene; + const hierarchicalParent = findNearestViableParentAncestorNodeRef(e.object); + const hierarchicalParentNode = getSceneNodeByRef(hierarchicalParent?.userData.nodeRef); + let physicalParent = hierarchicalParent; + if (findComponentByType(hierarchicalParentNode, KnownComponentType.SubModelRef)) { + while (physicalParent) { + if (physicalParent.userData.componentTypes?.includes(KnownComponentType.ModelRef)) break; + physicalParent = physicalParent.parent as THREE.Object3D; + } + } const { position } = getIntersectionTransform(e.intersections[0]); - const targetParent = getObject3DBySceneNodeRef(parent.userData.targetRef); - const newWidgetNode = createNodeWithPositionAndNormal(addingWidget, position, cursorLookAt, targetParent, parent?.userData.nodeRef) + const newWidgetNode = createNodeWithPositionAndNormal( + addingWidget, + position, + cursorLookAt, + physicalParent, + hierarchicalParent?.userData.nodeRef, + ); appendSceneNode(newWidgetNode); setAddingWidget(undefined); e.stopPropagation(); diff --git a/packages/scene-composer/src/utils/nodeUtils.spec.ts b/packages/scene-composer/src/utils/nodeUtils.spec.ts index a0e43086c..1e28657b7 100644 --- a/packages/scene-composer/src/utils/nodeUtils.spec.ts +++ b/packages/scene-composer/src/utils/nodeUtils.spec.ts @@ -20,7 +20,6 @@ describe('nodeUtils', () => { expect(component).toBeUndefined(); }); - }); describe('createNodeWithPositionAndNormal', () => { diff --git a/packages/scene-composer/src/utils/nodeUtils.ts b/packages/scene-composer/src/utils/nodeUtils.ts index 3c9c8e35e..b7da14f28 100644 --- a/packages/scene-composer/src/utils/nodeUtils.ts +++ b/packages/scene-composer/src/utils/nodeUtils.ts @@ -31,7 +31,7 @@ export const findComponentByType = ( * @returns {boolean} true if the object is a Viable Parent */ const isViableParent = (object: THREE.Object3D): boolean => { - return !!object.userData.targetRef; + return !!object.userData.nodeRef; }; type Transform = { @@ -95,12 +95,11 @@ export const createNodeWithPositionAndNormal = ( position: THREE.Vector3, normal: THREE.Vector3, parent?: THREE.Object3D, - targetRef?: string - ): ISceneNodeInternal => { +): ISceneNodeInternal => { const finalPosition = parent?.worldToLocal(position.clone()) ?? position; return { ...newWidget.node, - parentRef: targetRef || parent?.userData.nodeRef, + parentRef: parent?.userData.nodeRef, transform: { position: finalPosition.toArray(), rotation: [0, 0, 0], // TODO: Find why the normal is producing weird orientations diff --git a/packages/scene-composer/src/utils/objectThreeUtils.ts b/packages/scene-composer/src/utils/objectThreeUtils.ts index 83e257ba8..d0cdf4f78 100644 --- a/packages/scene-composer/src/utils/objectThreeUtils.ts +++ b/packages/scene-composer/src/utils/objectThreeUtils.ts @@ -67,3 +67,9 @@ export function enableShadow(component: IModelRefComponentInternal, obj: THREE.O if (obj.material.map) obj.material.map.anisotropy = Math.min(16, maxAnisotropy); } } + +export const resetObjectCenter = (object: THREE.Object3D) => { + const box = new THREE.Box3().setFromObject(object); + box.getCenter(object.position); + object.position.multiplyScalar(-1); +}; diff --git a/packages/scene-composer/src/utils/svgUtils.spec.ts b/packages/scene-composer/src/utils/svgUtils.spec.ts new file mode 100644 index 000000000..27a5d4017 --- /dev/null +++ b/packages/scene-composer/src/utils/svgUtils.spec.ts @@ -0,0 +1,43 @@ +import { useLoader } from '@react-three/fiber'; +import { SVGLoader } from 'three-stdlib'; +import { Group as THREEGroup, MeshBasicMaterial as THREEMeshBasicMaterial } from 'three'; +import React from 'react'; + +import { ViewCursorEditIcon } from '../assets'; + +import { convertSvgToMesh, createMesh } from './svgUtils'; + +jest.mock('@react-three/fiber', () => { + const originalModule = jest.requireActual('three-stdlib'); + return { + ...originalModule, + useLoader: jest.fn(), + createShapes: jest.fn(), + SVGLoader: jest.fn(), + }; +}); +describe('svgUtils', () => { + describe('createSvg', () => { + it('creates a mesh to be a type of THREEGroup', () => { + const data = useLoader(SVGLoader, ViewCursorEditIcon.dataUri); + const svgMesh = convertSvgToMesh(data); + expect(svgMesh).toBeInstanceOf(THREEGroup); + }); + it('should silently fail if passed an undefined', () => { + const svgMesh = convertSvgToMesh(undefined); + expect(svgMesh).toBeInstanceOf(THREEGroup); + }); + it('should silently fail if passed a null', () => { + const svgMesh = convertSvgToMesh(null); + expect(svgMesh).toBeInstanceOf(THREEGroup); + }); + }); + describe('createMesh', () => { + it('can create a mesh', () => { + const mesh = createMesh('white', 1); + expect(mesh).toBeInstanceOf(THREEMeshBasicMaterial); + expect(JSON.stringify(mesh.color)).toBe('16777215'); + expect(mesh.opacity).toBe(1); + }); + }); +}); diff --git a/packages/scene-composer/src/utils/svgUtils.ts b/packages/scene-composer/src/utils/svgUtils.ts new file mode 100644 index 000000000..f8792b6ac --- /dev/null +++ b/packages/scene-composer/src/utils/svgUtils.ts @@ -0,0 +1,58 @@ +import { + AlwaysDepth as THREEAlwaysDepth, + Box3 as THREEBox3, + Color as THREEColor, + DoubleSide as THREEDoubleSide, + Group as THREEGroup, + Mesh as THREEMesh, + MeshBasicMaterial as THREEMeshBasicMaterial, + Object3D as THREEObject3D, + ShapeGeometry as THREEShapeGeometry, +} from 'three'; +import { SVGLoader } from 'three-stdlib'; + +import { resetObjectCenter } from './objectThreeUtils'; + +export const createMesh = (color, opacity) => { + return new THREEMeshBasicMaterial({ + color: new THREEColor().setStyle(color).convertSRGBToLinear(), + opacity: opacity, + transparent: true, + depthFunc: THREEAlwaysDepth, + side: THREEDoubleSide, + }); +}; + +export const convertSvgToMesh = (data) => { + const svgGroup = new THREEGroup(); + /* istanbul ignore next */ + data?.paths?.forEach((path) => { + const fillColor = path?.userData?.style.fill; + const fillOpacity = path?.userData?.style.fillOpacity; + if (fillColor !== undefined && fillColor !== 'none') { + const fillMaterial = createMesh(fillColor, fillOpacity); + const shapes = SVGLoader.createShapes(path); + shapes.forEach((line) => { + const geometry = new THREEShapeGeometry(line); + const mesh = new THREEMesh(geometry, fillMaterial); + resetObjectCenter(mesh); + svgGroup.add(mesh); + }); + } + const strokeColor = path?.userData?.style.stroke; + const strokeOpacity = path?.userData?.style.strokeOpacity; + if (strokeColor !== undefined && strokeColor !== 'none') { + const strokeMaterial = createMesh(strokeColor, strokeOpacity); + path.subPaths.forEach((childPath) => { + const geometry = SVGLoader.pointsToStroke(childPath.getPoints(), path?.userData?.style); + if (geometry) { + const mesh = new THREEMesh(geometry, strokeMaterial); + resetObjectCenter(mesh); + svgGroup.add(mesh); + } + }); + } + }); + svgGroup.scale.multiplyScalar(0.005); + return svgGroup; +};