Skip to content

Commit

Permalink
feat(composer): add utils to convert all nodes to entities
Browse files Browse the repository at this point in the history
  • Loading branch information
sheilaXu authored and mumanity committed Oct 6, 2023
1 parent 6bbe391 commit 4e305d4
Show file tree
Hide file tree
Showing 20 changed files with 748 additions and 136 deletions.
2 changes: 1 addition & 1 deletion packages/scene-composer/package.json
Expand Up @@ -46,7 +46,7 @@
"convert-svg": "npx @svgr/cli --out-dir src/assets/auto-gen/icons/ --typescript --index-template tools/index-template.js -- src/assets/icons/",
"release": "run-s compile copy-assets",
"copy-assets": "copyfiles -e \"**/*.tsx\" -e \"**/*.ts\" -e \"**/*.snap\" -e \"**/*.js\" -e \"**/*.jsx\" -e \"**/*.json\" \"src/**/*\" dist/",
"lint": "eslint . --max-warnings=575",
"lint": "eslint . --max-warnings=567",
"fix": "eslint --fix .",
"test": "jest --config jest.config.ts --coverage --silent",
"test:dev": "jest --config jest.config.ts --coverage",
Expand Down
Expand Up @@ -2,13 +2,12 @@ import React, { FC, createContext, useContext, useCallback, useState, useEffect,
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { isEmpty } from 'lodash';
import { Euler, Vector3, Quaternion } from 'three';

import { useSceneComposerId } from '../../../common/sceneComposerIdContext';
import { findComponentByType, getFinalTransform, isEnvironmentNode, Transform } from '../../../utils/nodeUtils';
import { findComponentByType, getFinalNodeTransform, isEnvironmentNode } from '../../../utils/nodeUtils';
import { ISceneNodeInternal, useNodeErrorState, useSceneDocument, useStore } from '../../../store';
import useLifecycleLogging from '../../../logger/react-logger/hooks/useLifecycleLogging';
import { KnownComponentType } from '../../../interfaces';
import { ITransform, KnownComponentType } from '../../../interfaces';
import { RecursivePartial } from '../../../utils/typeUtils';

import ISceneHierarchyNode from './model/ISceneHierarchyNode';
Expand Down Expand Up @@ -226,35 +225,16 @@ const SceneHierarchyDataProvider: FC<SceneHierarchyDataProviderProps> = ({ selec
newHierarchyParentObject = getObject3DBySceneNodeRef(hierarchyParent?.ref);
}

let maintainedTransform: Transform | null = null;
let maintainedTransform: ITransform | null = null;
if (originalObject3D) {
const worldPosition = originalObject3D.getWorldPosition(new Vector3());
const worldRotation = new Euler().setFromQuaternion(originalObject3D.getWorldQuaternion(new Quaternion()));
const worldScale = originalObject3D.getWorldScale(new Vector3());
maintainedTransform = getFinalTransform(
{
position: worldPosition,
rotation: worldRotation,
scale: worldScale,
},
newHierarchyParentObject,
);
maintainedTransform = getFinalNodeTransform(originalObject, originalObject3D, newHierarchyParentObject);
}

// Create updates to the moving object
const partial: RecursivePartial<ISceneNodeInternal> = { parentRef: newParentRef };
if (maintainedTransform) {
// Scale of Tag component is independent of its ancestors, therefore keep its original value.
const finalScale = findComponentByType(originalObject, KnownComponentType.Tag)
? originalObject?.transform.scale
: maintainedTransform.scale.toArray();

// Update the node position to remain in its world space
partial.transform = {
position: maintainedTransform.position.toArray(),
rotation: [maintainedTransform.rotation.x, maintainedTransform.rotation.y, maintainedTransform.rotation.z],
scale: finalScale,
};
partial.transform = maintainedTransform;
}

updateSceneNodeInternal(objectRef, partial);
Expand Down
Expand Up @@ -99,7 +99,7 @@ export const SpeedEditor: React.FC<ISpeedEditorProps> = ({ component, onUpdateCa
onChange={(event) => {
const updatedComponent = {
...component,
config: { ...component.config, defaultSpeed: event.target.value },
config: { ...component.config, defaultSpeed: Number(event.target.value) },
};
onUpdateCallback(updatedComponent, true);
}}
Expand Down
2 changes: 1 addition & 1 deletion packages/scene-composer/src/interfaces/interfaces.tsx
Expand Up @@ -48,7 +48,7 @@ export interface ISceneDocument {
rootNodeRefs: string[];
unit?: string;
version: string;
properties?: Record<string, any>;
properties?: Partial<Record<KnownSceneProperty, any>>;
specVersion?: string;
}

Expand Down
Expand Up @@ -12,6 +12,7 @@ jest.mock('../../../utils/mathUtils', () => ({
describe('serializationHelpers', () => {
beforeEach(() => {
jest.clearAllMocks();
(generateUUID as jest.Mock).mockReturnValue('test-uuid');
});

it('should appropriately create model ref componentAs or log errors when calling createModelRefComponent', () => {
Expand Down Expand Up @@ -43,8 +44,6 @@ describe('serializationHelpers', () => {
});

it('should appropriately create tag components or log errors when calling createTagComponent', () => {
(generateUUID as jest.Mock).mockReturnValue('test-uuid');

const component = {
icon: 'testIcon',
ruleBasedMapId: 42,
Expand Down Expand Up @@ -86,8 +85,6 @@ describe('serializationHelpers', () => {
});

it('should appropriately create light components or log errors when calling createLightComponent', () => {
(generateUUID as jest.Mock).mockReturnValueOnce('test-uuid');

const light = { lightType: 'Ambient', lightSettings: { volume: 'full' } };
expect(exportsForTesting.createLightComponent(light as any, [])).toEqual({
ref: 'test-uuid',
Expand All @@ -102,45 +99,39 @@ describe('serializationHelpers', () => {
});

it('should appropriately create model shader component or log errors when calling createModelShaderComponent', () => {
(generateUUID as jest.Mock).mockReturnValueOnce('test-uuid');

const modelShaderComponent = exportsForTesting.createModelShaderComponent({ shader: 'test' } as any, []);
expect(modelShaderComponent).toEqual({ ref: 'test-uuid', shader: 'test' });
});

it('should appropriately create motion indicator components or log errors when calling createMotionIndicatorComponent', () => {
(generateUUID as jest.Mock).mockReturnValueOnce('test-uuid');

const component = {
shape: 'LinearPlane',
const component: Component.MotionIndicator = {
type: KnownComponentType.MotionIndicator,
shape: Component.MotionIndicatorShape.LinearCylinder,
valueDataBindings: {
speed: {
ruleBasedMapId: 42,
valueDataBinding: { dataBindingContext: 'dataBindingContext' },
ruleBasedMapId: 42 as unknown as string,
valueDataBinding: { dataBindingContext: { entityId: 'eid' } },
},
},
config: {
numOfRepeatInY: 2,
backgroundColorOpacity: 1,
defaultSpeed: '0.5' as unknown as number,
},
};
const resolver = jest.fn();
resolver.mockReturnValueOnce('here is a map');
resolver.mockReturnValueOnce(undefined);
const errorCollector = [];
const motionIndicator = exportsForTesting.createMotionIndicatorComponent(
component as any,
resolver,
errorCollector,
);
const error = exportsForTesting.createMotionIndicatorComponent(component as any, resolver, errorCollector);
const motionIndicator = exportsForTesting.createMotionIndicatorComponent(component, resolver, errorCollector);
const error = exportsForTesting.createMotionIndicatorComponent(component, resolver, errorCollector);

expect(motionIndicator).toEqual({
ref: 'test-uuid',
type: 'MotionIndicator',
valueDataBindings: component.valueDataBindings,
shape: component.shape,
config: component.config,
config: { ...component.config, defaultSpeed: 0.5 },
});

expect(error).toEqual(motionIndicator);
Expand All @@ -156,8 +147,6 @@ describe('serializationHelpers', () => {
});

it('should appropriately create data overlay components or log errors when calling createDataOverlayComponent', () => {
(generateUUID as jest.Mock).mockReturnValue('test-uuid');

const component: Component.DataOverlay = {
type: KnownComponentType.DataOverlay,
subType: Component.DataOverlaySubType.OverlayPanel,
Expand Down Expand Up @@ -195,8 +184,6 @@ describe('serializationHelpers', () => {
});

it('should appropriately create entity binding components when calling createEntityBindingComponent', () => {
(generateUUID as jest.Mock).mockReturnValue('test-uuid');

const component: Component.EntityBindingComponent = {
type: KnownComponentType.EntityBinding,
valueDataBinding: {
Expand All @@ -213,8 +200,6 @@ describe('serializationHelpers', () => {
});

it('should create a scene node when calling createSceneNodeInternal', () => {
(generateUUID as jest.Mock).mockReturnValueOnce('test-uuid');

const sceneNode = {
ref: 'a2a91acc-3a47-4875-a146-b95741aedc2a',
name: 'testNode',
Expand Down Expand Up @@ -249,8 +234,6 @@ describe('serializationHelpers', () => {
});

it('should create an indexed object resolver when calling createObjectResolver which validates input', () => {
(generateUUID as jest.Mock).mockReturnValueOnce('test-uuid');

const scene = {
specVersion: '1.0',
version: '1.0',
Expand Down
@@ -1,4 +1,4 @@
import { isEmpty, isNumber } from 'lodash';
import { isNumber } from 'lodash';

import DebugLogger from '../../logger/DebugLogger';
import {
Expand Down Expand Up @@ -188,12 +188,14 @@ function createMotionIndicatorComponent(
validateRuleBasedMapId(map.ruleBasedMapId, resolver, errorCollector);
});

// defaultSpeed was saved as string in older version, convert it to number here
const defaultSpeed = component.config.defaultSpeed !== undefined ? Number(component.config.defaultSpeed) : undefined;
return {
ref: generateUUID(),
type: 'MotionIndicator',
shape: component.shape,
valueDataBindings: component.valueDataBindings,
config: component.config,
config: { ...component.config, defaultSpeed },
};
}

Expand Down
@@ -0,0 +1,102 @@
import { TwinMakerSceneMetadataModule } from '@iot-app-kit/source-iottwinmaker';

import { setTwinMakerSceneMetadataModule } from '../../common/GlobalSettings';
import { defaultNode } from '../../../__mocks__/sceneNode';
import { KnownComponentType } from '../../interfaces';

import { createNodeEntityComponent } from './nodeComponent';
import { createTagEntityComponent } from './tagComponent';
import { createOverlayEntityComponent } from './overlayComponent';
import { createCameraEntityComponent } from './cameraComponent';
import { createMotionIndicatorEntityComponent } from './motionIndicatorComponent';
import { createModelRefComponent } from './modelRefComponent';
import { createNodeEntity } from './createNodeEntity';

jest.mock('./nodeComponent', () => ({
createNodeEntityComponent: jest.fn().mockReturnValue({ componentTypeId: '3d.node' }),
}));
jest.mock('./tagComponent', () => ({
createTagEntityComponent: jest.fn().mockReturnValue({ componentTypeId: '3d.tag' }),
}));
jest.mock('./overlayComponent', () => ({
createOverlayEntityComponent: jest.fn().mockReturnValue({ componentTypeId: '3d.overlay' }),
}));
jest.mock('./cameraComponent', () => ({
createCameraEntityComponent: jest.fn().mockReturnValue({ componentTypeId: '3d.camera' }),
}));
jest.mock('./motionIndicatorComponent', () => ({
createMotionIndicatorEntityComponent: jest.fn().mockReturnValue({ componentTypeId: '3d.motionIndicator' }),
}));
jest.mock('./modelRefComponent', () => ({
createModelRefComponent: jest.fn().mockReturnValue({ componentTypeId: '3d.modelRef' }),
}));

describe('createNodeEntity', () => {
const createSceneEntity = jest.fn();
const mockMetadataModule: Partial<TwinMakerSceneMetadataModule> = {
createSceneEntity,
};

beforeEach(() => {
jest.clearAllMocks();
setTwinMakerSceneMetadataModule(mockMetadataModule as unknown as TwinMakerSceneMetadataModule);
});

it('should call create entity with only node component', async () => {
await createNodeEntity(defaultNode, 'parent', 'layer');

expect(createSceneEntity).toHaveBeenCalledTimes(1);
expect(createSceneEntity).toHaveBeenCalledWith({
workspaceId: undefined,
entityId: defaultNode.ref,
entityName: defaultNode.name + '_' + defaultNode.ref,
parentEntityId: 'parent',
components: {
Node: { componentTypeId: '3d.node' },
},
});

expect(createNodeEntityComponent).toHaveBeenCalledTimes(1);
expect(createNodeEntityComponent).toHaveBeenCalledWith(defaultNode, 'layer');
});

it('should call create entity to create multiple components', async () => {
const tag = { type: KnownComponentType.Tag, ref: 'tag-ref' };
const overlay = { type: KnownComponentType.DataOverlay, ref: 'overlay-ref' };
const camera = { type: KnownComponentType.Camera, ref: 'camera-ref' };
const motionIndicator = { type: KnownComponentType.MotionIndicator, ref: 'indicator-ref' };
const modelRef = { type: KnownComponentType.ModelRef, ref: 'modelref-ref' };
const node = { ...defaultNode, components: [tag, overlay, camera, motionIndicator, modelRef] };

await createNodeEntity(node, 'parent', 'layer');

expect(createSceneEntity).toHaveBeenCalledTimes(1);
expect(createSceneEntity).toHaveBeenCalledWith({
workspaceId: undefined,
entityId: defaultNode.ref,
entityName: defaultNode.name + '_' + defaultNode.ref,
parentEntityId: 'parent',
components: {
Node: { componentTypeId: '3d.node' },
Tag: { componentTypeId: '3d.tag' },
DataOverlay: { componentTypeId: '3d.overlay' },
Camera: { componentTypeId: '3d.camera' },
MotionIndicator: { componentTypeId: '3d.motionIndicator' },
ModelRef: { componentTypeId: '3d.modelRef' },
},
});

expect(createNodeEntityComponent).toHaveBeenCalledTimes(1);
expect(createNodeEntityComponent).toHaveBeenCalledWith(node, 'layer');
expect(createTagEntityComponent).toHaveBeenCalledTimes(1);
expect(createTagEntityComponent).toHaveBeenCalledWith(tag);
expect(createOverlayEntityComponent).toHaveBeenCalledTimes(1);
expect(createOverlayEntityComponent).toHaveBeenCalledWith(overlay);
expect(createCameraEntityComponent).toHaveBeenCalledTimes(1);
expect(createCameraEntityComponent).toHaveBeenCalledWith(camera);
expect(createMotionIndicatorEntityComponent).toHaveBeenCalledTimes(1);
expect(createMotionIndicatorEntityComponent).toHaveBeenCalledWith(motionIndicator);
expect(createModelRefComponent).toHaveBeenCalledTimes(1);
expect(createModelRefComponent).toHaveBeenCalledWith(modelRef);
});
});
@@ -0,0 +1,74 @@
import {
ComponentUpdateRequest,
CreateEntityCommandInput,
CreateEntityCommandOutput,
} from '@aws-sdk/client-iottwinmaker';

import { getGlobalSettings } from '../../common/GlobalSettings';
import {
ICameraComponent,
IDataOverlayComponent,
IModelRefComponent,
IMotionIndicatorComponent,
KnownComponentType,
} from '../../interfaces';
import { ISceneNodeInternal } from '../../store/internalInterfaces';

import { createNodeEntityComponent } from './nodeComponent';
import { createTagEntityComponent } from './tagComponent';
import { createOverlayEntityComponent } from './overlayComponent';
import { createMotionIndicatorEntityComponent } from './motionIndicatorComponent';
import { createCameraEntityComponent } from './cameraComponent';
import { createModelRefComponent } from './modelRefComponent';

export const createNodeEntity = (
node: ISceneNodeInternal,
parentRef: string,
layerId: string,
): Promise<CreateEntityCommandOutput> | undefined => {
const sceneMetadataModule = getGlobalSettings().twinMakerSceneMetadataModule;

const nodecomp = createNodeEntityComponent(node, layerId);

const createEntity: CreateEntityCommandInput = {
workspaceId: undefined,
entityId: node.ref,
entityName: node.name + '_' + node.ref,
parentEntityId: parentRef,
components: {
Node: nodecomp,
},
};

node.components?.forEach((compToBeCreated) => {
if (compToBeCreated?.type !== KnownComponentType.EntityBinding) {
let comp: ComponentUpdateRequest | undefined = undefined;
switch (compToBeCreated?.type) {
case KnownComponentType.Tag:
comp = createTagEntityComponent(compToBeCreated);
break;
case KnownComponentType.DataOverlay:
comp = createOverlayEntityComponent(compToBeCreated as IDataOverlayComponent);
break;
case KnownComponentType.MotionIndicator:
comp = createMotionIndicatorEntityComponent(compToBeCreated as IMotionIndicatorComponent);
break;
case KnownComponentType.Camera:
comp = createCameraEntityComponent(compToBeCreated as ICameraComponent);
break;
case KnownComponentType.ModelRef:
comp = createModelRefComponent(compToBeCreated as IModelRefComponent);
break;

default:
throw new Error('Component type not supported');
}

if (comp) {
createEntity.components![compToBeCreated.type] = comp;
}
}
});

return sceneMetadataModule?.createSceneEntity(createEntity);
};

0 comments on commit 4e305d4

Please sign in to comment.