From 880d128b9eb7611d9c9ec3b981d385aee991281d Mon Sep 17 00:00:00 2001 From: Emily Dodds Date: Fri, 7 Oct 2022 16:51:23 -0500 Subject: [PATCH] Iotroci 8819 (#271) * Fix: Refactor to improve ViewCursorWidget performance * CR Feedback * cleanup * Fix from running npm run build Co-authored-by: Emily Dodds --- packages/scene-composer/jest.config.ts | 2 + .../src/assets/viewpoints/IconSvgs.tsx | 2 +- .../UpdateJsxIntrinsicElements.ts | 8 +- .../three-fiber/anchor/AnchorWidget.tsx | 6 +- .../common/SvgIconToWidgetVisual.tsx | 120 ------------------ .../viewpoint/ViewCursorWidget.tsx | 110 +++++++--------- .../src/augmentations/three/Viewpoint.ts | 73 ----------- .../src/augmentations/three/index.ts | 1 - .../components/WebGLCanvasManager.spec.tsx | 14 +- .../src/components/WebGLCanvasManager.tsx | 34 +---- .../WebGLCanvasManager.spec.tsx.snap | 49 +------ .../panels/SceneNodeInspectorPanel.tsx | 4 - .../ModelRefComponent/GLTFModelComponent.tsx | 55 ++++---- .../floatingToolbar/items/AddObjectMenu.tsx | 9 +- .../scene-composer/src/models/SceneModels.ts | 7 - .../src/utils/nodeUtils.spec.ts | 20 --- .../scene-composer/src/utils/nodeUtils.ts | 3 +- .../src/utils/objectThreeUtils.ts | 6 + .../scene-composer/src/utils/svgUtils.spec.ts | 43 +++++++ packages/scene-composer/src/utils/svgUtils.ts | 58 +++++++++ .../viewpoint/ViewCursorWidget.spec.tsx | 69 ---------- .../ViewCursorWidget.spec.tsx.snap | 43 ------- .../augmentations/three/Viewpoint.spec.ts | 87 ------------- .../IotAppKitSceneComposer.en_US.json | 4 - 24 files changed, 214 insertions(+), 613 deletions(-) delete mode 100644 packages/scene-composer/src/augmentations/components/three-fiber/common/SvgIconToWidgetVisual.tsx delete mode 100644 packages/scene-composer/src/augmentations/three/Viewpoint.ts create mode 100644 packages/scene-composer/src/utils/svgUtils.spec.ts create mode 100644 packages/scene-composer/src/utils/svgUtils.ts delete mode 100644 packages/scene-composer/tests/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx 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/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/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..c52e9959d 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,13 @@ 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/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.tsx b/packages/scene-composer/src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.tsx index c7434633e..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,76 +1,62 @@ -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 { Mesh as THREEMesh, Object3D as THREEObject3D, 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 { convertSvgToMesh } from '../../../../utils/svgUtils'; +import { getIntersectionTransform } from '../../../../utils/raycastUtils'; import { sceneComposerIdContext } from '../../../../common/sceneComposerIdContext'; import { useEditorState } from '../../../../store'; +import { ViewCursorEditIcon, ViewCursorMoveIcon } from '../../../../assets'; -// 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 moveVisual = useMemo(() => { - const svgData: SVGResult = useLoader(SVGLoader, ViewCursorMoveIcon.dataUri); - return svgIconToWidgetVisual(svgData, ViewpointState.Deselected, false, { userData: { isCursor: true } }); - }, []); - - const editVisual = useMemo(() => { - const svgData: SVGResult = useLoader(SVGLoader, ViewCursorEditIcon.dataUri); - return svgIconToWidgetVisual(svgData, ViewpointState.Selected, false, { userData: { isCursor: true } }); - }, []); - - useEffect(() => { - if (viewCursorRef.current) { - viewCursorRef.current.lookAt(cursorLookAt); - viewCursorRef.current.rotation.z = camera.rotation.z; // Prevent spinning and always be straight up and down + const { addingWidget, cursorVisible, cursorStyle, setAddingWidget, setCursorVisible, setCursorStyle } = + useEditorState(sceneComposerId); + const svg = cursorStyle === 'edit' ? ViewCursorEditIcon : ViewCursorMoveIcon; + const data = useLoader(SVGLoader, svg.dataUri); + + 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 shape = useMemo(() => { + 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; + }); + 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); } - }, [cursorLookAt]); + }); useEffect(() => { - if (viewCursorRef.current) { - viewCursorRef.current.traverse((child) => { - const mesh = child as THREE.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; - } - } - }); - } - }, [viewCursorRef]); - - return ( - - {moveVisual} - {editVisual} - - ); -}; + 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/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..bf2aea24a 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'; diff --git a/packages/scene-composer/src/components/WebGLCanvasManager.spec.tsx b/packages/scene-composer/src/components/WebGLCanvasManager.spec.tsx index 23389b350..0a5b02306 100644 --- a/packages/scene-composer/src/components/WebGLCanvasManager.spec.tsx +++ b/packages/scene-composer/src/components/WebGLCanvasManager.spec.tsx @@ -1,7 +1,7 @@ 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 { useStore } from '../store'; import { setFeatureConfig } from '../common/GlobalSettings'; @@ -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 de2227c0d..f738b6cdc 100644 --- a/packages/scene-composer/src/components/panels/SceneNodeInspectorPanel.tsx +++ b/packages/scene-composer/src/components/panels/SceneNodeInspectorPanel.tsx @@ -59,10 +59,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/ModelRefComponent/GLTFModelComponent.tsx b/packages/scene-composer/src/components/three-fiber/ModelRefComponent/GLTFModelComponent.tsx index 1d2e6a182..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'; @@ -45,6 +49,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 { getSceneNodeByRef } = useStore(sceneComposerId)((state) => state); const { isEditing, addingWidget, @@ -150,43 +156,37 @@ 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)); }; 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 newWidgetNode = createNodeWithPositionAndNormal(addingWidget, position, cursorLookAt, parent); - + const newWidgetNode = createNodeWithPositionAndNormal( + addingWidget, + position, + cursorLookAt, + physicalParent, + hierarchicalParent?.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 +196,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..1e28657b7 100644 --- a/packages/scene-composer/src/utils/nodeUtils.spec.ts +++ b/packages/scene-composer/src/utils/nodeUtils.spec.ts @@ -8,38 +8,18 @@ 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..547ce8d56 100644 --- a/packages/scene-composer/src/utils/nodeUtils.ts +++ b/packages/scene-composer/src/utils/nodeUtils.ts @@ -95,11 +95,12 @@ export const createNodeWithPositionAndNormal = ( position: THREE.Vector3, normal: THREE.Vector3, parent?: THREE.Object3D, + 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/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; +}; diff --git a/packages/scene-composer/tests/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx b/packages/scene-composer/tests/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx deleted file mode 100644 index 19fedbad9..000000000 --- a/packages/scene-composer/tests/augmentations/components/three-fiber/viewpoint/ViewCursorWidget.spec.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* eslint-disable import/first */ -import * as THREE from 'three'; -import React from 'react'; -import renderer from 'react-test-renderer'; -import { useLoader, useThree } from '@react-three/fiber'; - -import { ViewCursorWidget } from '../../../../../src/augmentations/components/three-fiber/viewpoint/ViewCursorWidget'; -import { Viewpoint } from '../../../../../src'; -import { useStore } from '../../../../../src/store'; - -jest.mock('../../../../../src/augmentations/components/three-fiber/common/SvgIconToWidgetVisual', () => - jest.fn(((data, name, props) =>
) as any), -); - -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(); - }), - }; -}); - -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 scene = new THREE.Scene(); - scene.add(closestViewpoint, furthestViewpoint); - - const baseState: any = { - cursorVisible: true, - cursorStyle: 'move', - }; - - 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(); - }); - - // TODO: Add tests to send onPointerDown and onPointerUp and verify closet is passed to setViewpointNodeRef -}); 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'); - }); - }); -}); diff --git a/packages/scene-composer/translations/IotAppKitSceneComposer.en_US.json b/packages/scene-composer/translations/IotAppKitSceneComposer.en_US.json index 4b0c684ae..5154af0b6 100644 --- a/packages/scene-composer/translations/IotAppKitSceneComposer.en_US.json +++ b/packages/scene-composer/translations/IotAppKitSceneComposer.en_US.json @@ -771,10 +771,6 @@ "note": "Menu Item", "text": "Add empty node" }, - "ygZwns": { - "note": "Expandable Section title", - "text": "Viewpoint" - }, "ylDLGq": { "note": "FormField label", "text": "Arrow color"