Skip to content

Commit

Permalink
feat(3D Knowledge Graph): add scene node highlighting
Browse files Browse the repository at this point in the history
  • Loading branch information
haweston authored and mumanity committed May 26, 2023
1 parent fc17202 commit ef5c71c
Show file tree
Hide file tree
Showing 15 changed files with 758 additions and 35 deletions.
24 changes: 24 additions & 0 deletions packages/scene-composer/public/CookieFactoryWaterTank.scene.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@
"uri": "CookieFactoryWaterTank.glb",
"modelType": "GLB",
"unitOfMeasure": "meters"
},
{
"type": "DataBinding",
"valueDataBindings":[
{
"valueDataBinding": {
"dataBindingContext":{
"entityId": "WaterTank"
}
}
}
]
}
],
"properties": {}
Expand Down Expand Up @@ -190,6 +202,18 @@
"parentRef": "9A99B442-CBE4-4E34-836D-97B75A14D3AE",
"selector": "water-pipe1_2",
"type": "SubModelRef"
},
{
"type": "DataBinding",
"valueDataBindings":[
{
"valueDataBinding": {
"dataBindingContext":{
"entityId": "MainPipe"
}
}
}
]
}
],
"properties": []
Expand Down
85 changes: 78 additions & 7 deletions packages/scene-composer/src/components/SceneComposerInternal.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import React, { useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
import { ThemeProvider } from 'styled-components';
import { applyMode, Mode } from '@awsui/global-styles';
import { cloneDeep } from 'lodash';

import { SCENE_BODY_CLASS } from '../common/constants';
import { sceneComposerIdContext } from '../common/sceneComposerIdContext';
import LogProvider from '../logger/react-logger/log-provider';
import { darkTheme, lightTheme } from '../theme';
import { GlobalStyles } from '../GlobalStyles';
import { useStore } from '../store';
import { sceneComposerIdContext } from '../common/sceneComposerIdContext';
import { KnownComponentType, SceneComposerInternalProps, StyleTarget } from '../interfaces';
import {
materialReducer,
initialMaterialMaps,
addMaterial,
removeMaterial,
backUpOriginalMaterial,
} from '../reducers/materialReducer';
import { IDataBindingComponentInternal, useStore } from '../store';
import { darkTheme, lightTheme } from '../theme';
import { containsMatchingEntityComponent } from '../utils/dataBindingUtils';
import { generateUUID } from '../utils/mathUtils';
import { SCENE_BODY_CLASS } from '../common/constants';
import { SceneComposerInternalProps } from '../interfaces';
import { createMaterialFromStyle } from '../utils/objectThreeStyleUtils';

import StateManager from './StateManager';
import DefaultErrorFallback from './DefaultErrorFallback';
Expand Down Expand Up @@ -57,14 +66,76 @@ export const SceneComposerInternal: React.FC<SceneComposerInternalProps> = ({

export function useSceneComposerApi(sceneComposerId: string) {
const store = useStore(sceneComposerId);
const state = store.getState();
const state = store.getState(); //This should likely be a useEffect updated by store instead!
const [materialMaps, dispatch] = useReducer(materialReducer, initialMaterialMaps);

const highlights = useCallback((decorations: StyleTarget[]) => {
console.log('trying to apply decorations: ', decorations)
const bindingComponentTypeFilter = [KnownComponentType.DataBinding];
const nodeList = Object.values(store.getState().document.nodeMap);
decorations.forEach((styleTarget) => {
nodeList.forEach((node) => {
const bindingComponent = node.components.find((component) => {
if (bindingComponentTypeFilter.includes(component.type as KnownComponentType)) {
const dataBoundComponent = component as IDataBindingComponentInternal;
//TODO this should get changed to not be an array soon
const boundContext = dataBoundComponent?.valueDataBindings?.at(0)?.valueDataBinding?.dataBindingContext;
return containsMatchingEntityComponent(styleTarget.dataBindingContext, boundContext);
} else {
return false;
}
});
if (bindingComponent) {
const object3D = store.getState().getObject3DBySceneNodeRef(node.ref);
if (object3D) {
object3D?.traverse((o) => {
const material = createMaterialFromStyle(o, styleTarget.style);
if (material) {
//backup original
backUpOriginalMaterial(o, materialMaps, dispatch);
addMaterial(o, material, 'highlights', materialMaps, dispatch);
}
});
}
}
});
});
},[store, dispatch, materialMaps]);

const clearHighlights = useCallback((dataBindingContexts: unknown[]) => {
const bindingComponentTypeFilter = [KnownComponentType.DataBinding];
const nodeList = Object.values(store.getState().document.nodeMap);
dataBindingContexts.forEach((dataBindingContext) => {
nodeList.forEach((node) => {
const bindingComponent = node.components.find((component) => {
if (bindingComponentTypeFilter.includes(component.type as KnownComponentType)) {
const dataBoundComponent = component as IDataBindingComponentInternal;
const boundContext = dataBoundComponent?.valueDataBindings?.at(0)?.valueDataBinding?.dataBindingContext;
return containsMatchingEntityComponent(dataBindingContext, boundContext);
} else {
return false;
}
});
if (bindingComponent) {
const object3D = store.getState().getObject3DBySceneNodeRef(node.ref);
if (object3D) {
object3D?.traverse((o) => {
removeMaterial(o, 'highlights', materialMaps, dispatch);
});
}
}
});
});
},[[store, dispatch, materialMaps]]);

return {
findSceneNodeRefBy: state.findSceneNodeRefBy,
setCameraTarget: state.setCameraTarget,
getSceneNodeByRef: (ref: string) => cloneDeep(state.getSceneNodeByRef(ref)),
getSelectedSceneNodeRef: () => store.getState().selectedSceneNodeRef,
setSelectedSceneNodeRef: state.setSelectedSceneNodeRef,
highlights: highlights,
clearHighlights: clearHighlights,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@ const SubModelTree: FC<SubModelTreeProps> = ({
const [transform, restore] = useMaterialEffect(
/* istanbul ignore next */ (o) => {
if (o instanceof Mesh && o.material && o.material.color) {
o.material.color = hoverColor;
const newMaterial = o.material.clone();
newMaterial.color = hoverColor;
return newMaterial;
}
return null;
},
'subModel',
object3D,
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useMemo, useEffect } from 'react';
import { Mesh } from 'three';
import { Material, Mesh } from 'three';
import { isEmpty } from 'lodash';

import { COMPOSER_FEATURES, SceneResourceType } from '../../../interfaces';
Expand Down Expand Up @@ -44,19 +44,23 @@ const ColorOverlayComponent: React.FC<IColorOverlayComponentProps> = ({
const [transform, restore] = useMaterialEffect(
/* istanbul ignore next */ (obj) => {
if (obj instanceof Mesh && ruleColor) {
if ('color' in obj.material) {
const newMaterial: Material = obj.material.clone();
if ('color' in newMaterial) {
if (ruleColor) {
if (ruleColor.color) {
obj.material.color = ruleColor.color.clone().convertSRGBToLinear();
newMaterial.color = ruleColor.color.clone().convertSRGBToLinear();
}
if ((ruleColor.alpha || ruleColor.alpha === 0) && ruleColor?.alpha !== 1) {
obj.material.transparent = true;
obj.material.opacity = ruleColor.alpha;
newMaterial.transparent = true;
newMaterial.opacity = ruleColor.alpha;
}
return newMaterial;
}
}
}
return null;
},
'rules',
entityObject3D,
);

Expand All @@ -65,7 +69,9 @@ const ColorOverlayComponent: React.FC<IColorOverlayComponentProps> = ({
transform();
}

return () => restore();
return () => {
restore();
};
}, [ruleResult, entityObject3D, opacityRuleEnabled]);

// This component relies on side effects to update the rendering of the entity's mesh. Returning an empty fragment.
Expand Down
22 changes: 17 additions & 5 deletions packages/scene-composer/src/hooks/useMaterialEffect.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect } from 'react';
import { cleanup, renderHook } from '@testing-library/react-hooks';
import { Object3D, Event, Mesh, MeshBasicMaterial, Color } from 'three';

import useMaterialEffect from './useMaterialEffect';
Expand Down Expand Up @@ -30,11 +31,20 @@ describe('useMaterialEffect', () => {
object.children.push(mesh);
});

const [transform, restore] = useMaterialEffect((obj) => {
if (obj instanceof Mesh) {
obj.material.color = transformedColor;
}
}, object);
const [transform, restore] = renderHook(() =>
useMaterialEffect(
(obj) => {
if (obj instanceof Mesh) {
const newMaterial = obj.material.clone();
newMaterial.color = transformedColor;
return newMaterial;
}
return null;
},
'rules',
object,
),
).result.current;

transform();

Expand Down Expand Up @@ -63,5 +73,7 @@ describe('useMaterialEffect', () => {
65280,
]
`);

cleanup();
});
});
39 changes: 28 additions & 11 deletions packages/scene-composer/src/hooks/useMaterialEffect.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,49 @@
import { useRef, useCallback, useEffect } from 'react';
import { useCallback, useEffect, useReducer } from 'react';
import { Object3D, Mesh } from 'three';

const useMaterialEffect = (callback: (object: Object3D) => void, object?: Object3D) => {
const originalMaterialMap = useRef({});
import {
addMaterial,
backUpOriginalMaterial,
initialMaterialMaps,
materialReducer,
MaterialMapLayer,
removeMaterial,
} from '../reducers/materialReducer';

const useMaterialEffect = (
callback: (object: Object3D) => THREE.Material | null,
layer: MaterialMapLayer,
object?: Object3D,
) => {
const [materialMaps, dispatch] = useReducer(materialReducer, initialMaterialMaps);

const restore = useCallback(() => {
object?.traverse((o) => {
if (o instanceof Mesh && o.userData?.isOriginal) {
const original = originalMaterialMap.current[o.uuid];
o.material = original ? original.clone() : o.material;
removeMaterial(o, layer, materialMaps, dispatch);
}
});
}, [object]);
}, [object, layer]);

useEffect(() => {
object?.traverse((o) => {
if (o instanceof Mesh) {
if (!originalMaterialMap.current[o.uuid]) {
originalMaterialMap.current[o.uuid] = o.material.clone();
}
if (o instanceof Mesh && !materialMaps.original[o.uuid]) {
backUpOriginalMaterial(o, materialMaps, dispatch);
}
});
}, [object]);

const transform = () => {
// Currently can't think of a use case where we'd want to use this to transform a material on a component we own
// isOriginal could be an argument in the future.
object?.traverse((o) => o.userData?.isOriginal && callback(o));
object?.traverse((o) => {
if (o.userData?.isOriginal && o instanceof Mesh) {
const newMaterial = callback(o);
if (newMaterial) {
addMaterial(o, newMaterial, layer, materialMaps, dispatch);
}
}
});
};

return [transform, restore];
Expand Down
11 changes: 11 additions & 0 deletions packages/scene-composer/src/interfaces/interfaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,14 @@ export interface AddingWidgetInfo {
type?: KnownComponentType;
node: ISceneNodeInternal;
}

export interface MeshStyle {
color?: string | number;
opacity?: number;
transparent?: boolean;
}

export interface StyleTarget {
dataBindingContext: unknown;
style: MeshStyle;
}

0 comments on commit ef5c71c

Please sign in to comment.