Skip to content

Commit

Permalink
feat(Tiles3D): add Tiles3D AssetType and evaluate model type when add…
Browse files Browse the repository at this point in the history
…ing a 3D model to the scene
  • Loading branch information
hwandersman committed Feb 2, 2024
1 parent 6cabfb5 commit eec0f50
Show file tree
Hide file tree
Showing 16 changed files with 267 additions and 32 deletions.
Binary file not shown.
1 change: 1 addition & 0 deletions packages/scene-composer/public/MIXER_Tiles3D/tileset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"asset":{"version":"1.0","extras":{"ion":{"georeferenced":false,"movable":true}}},"geometricError":3.4263414652811504,"root":{"children":[{"geometricError":3.4263414652811504,"boundingVolume":{"box":[0.04932012988307033,0.00410548518149545,1.0065496274953585,1.2187353077585679,0,0,0,0.7779263069343911,0,0,0,0.9998963209502474]},"children":[{"boundingVolume":{"box":[0.04932012988307033,0.00410548518149545,1.0065496274953585,1.2187353077585679,0,0,0,0.7779263069343911,0,0,0,0.9998963209502474]},"geometricError":3.4263414652811504,"children":[{"boundingVolume":{"box":[0.04932012988307033,0.00410548518149545,1.0065496274953585,1.2187353077585679,0,0,0,0.7779263069343911,0,0,0,0.9998963209502474]},"content":{"uri":"0composite0.cmpt"},"geometricError":0}]}],"transform":[1,0,0,0,0,1,0,0,0,0,1,0,-0.04932012988307033,-0.00410548518149545,-0.006653306545111093,1]}],"refine":"ADD","boundingVolume":{"box":[0,0,0.9998963209502474,1.2187353077585679,0,0,0,0.7779263069343911,0,0,0,0.9998963209502474]},"geometricError":3.4263414652811504}}
106 changes: 106 additions & 0 deletions packages/scene-composer/public/Tiles3D.scene.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
{
"specVersion": "1.0",
"version": "1",
"unit": "meters",
"properties": {
"environmentPreset": "neutral",
"componentSettings": {
"Tag": {
"autoRescale": false,
"scale": 1,
"enableOcclusion": false
}
}
},
"nodes": [
{
"name": "MIXER_Tiles3D",
"transform": {
"position": [
0.06654937013597362,
0.9998963209552567,
-0.004778029802959161
],
"rotation": [
0,
0,
0
],
"scale": [
1,
1,
1
]
},
"transformConstraint": {
"snapToFloor": true
},
"components": [
{
"type": "ModelRef",
"uri": "MIXER_Tiles3D/tileset.json",
"modelType": "Tiles3D"
}
],
"properties": {}
}
],
"rootNodeIndexes": [
0
],
"cameras": [],
"rules": {
"sampleAlarmIconRule": {
"statements": [
{
"expression": "alarm_status == 'ACTIVE'",
"target": "iottwinmaker.common.icon:Error"
},
{
"expression": "alarm_status == 'ACKNOWLEDGED'",
"target": "iottwinmaker.common.icon:Warning"
},
{
"expression": "alarm_status == 'SNOOZE_DISABLED'",
"target": "iottwinmaker.common.icon:Warning"
},
{
"expression": "alarm_status == 'NORMAL'",
"target": "iottwinmaker.common.icon:Info"
}
]
},
"sampleTimeSeriesIconRule": {
"statements": [
{
"expression": "temperature >= 40",
"target": "iottwinmaker.common.icon:Error"
},
{
"expression": "temperature >= 20",
"target": "iottwinmaker.common.icon:Warning"
},
{
"expression": "temperature < 20",
"target": "iottwinmaker.common.icon:Info"
}
]
},
"sampleTimeSeriesColorRule": {
"statements": [
{
"expression": "temperature >= 40",
"target": "iottwinmaker.common.color:#FF0000"
},
{
"expression": "temperature >= 20",
"target": "iottwinmaker.common.color:#FFFF00"
},
{
"expression": "temperature < 20",
"target": "iottwinmaker.common.color:#00FF00"
}
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ViewCursorEditSvgString, ViewCursorMoveSvgString } from '../../../../as
export const INIT_SVG_SCALE = 0.003;
export const INIT_SVG_VECTOR = new THREEVector3(INIT_SVG_SCALE, INIT_SVG_SCALE, INIT_SVG_SCALE);

export const ViewCursorWidget = () => {
export const ViewCursorWidget = (): React.JSX.Element => {
const ref = useRef<THREEObject3D>(null);
const { gl } = useThree();
const sceneComposerId = useContext(sceneComposerIdContext);
Expand Down Expand Up @@ -60,11 +60,15 @@ export const ViewCursorWidget = () => {
// Intersections are sorted
const closestIntersection = intersects[0];
const n = getIntersectionTransform(closestIntersection);
shape.lookAt(n.normal as THREEVector3);
shape.position.copy(n.position);
// Set scale based on intersection distance
shape.scale.copy(INIT_SVG_VECTOR);
shape.scale.multiplyScalar(closestIntersection.distance);
const normalVector = n.normal as THREEVector3;
// Intersection surface normal may not be a uniform vector, like for point clouds
if (normalVector) {
shape.lookAt(n.normal as THREEVector3);
shape.position.copy(n.position);
// Set scale based on intersection distance
shape.scale.copy(INIT_SVG_VECTOR);
shape.scale.multiplyScalar(closestIntersection.distance);
}
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { IModelRefComponentInternal, useSceneDocument } from '../../../../../sto
import { findComponentByType } from '../../../../../utils/nodeUtils';
import { sceneComposerIdContext } from '../../../../../common/sceneComposerIdContext';
import { isDynamicNode } from '../../../../../utils/entityModelUtils/sceneUtils';
import { ModelType } from '../../../../../models/SceneModels';

import SceneNodeLabel from './SceneNodeLabel';
import { AcceptableDropTypes, EnhancedTree, EnhancedTreeItem } from './constants';
Expand Down Expand Up @@ -45,8 +46,9 @@ const SceneHierarchyTreeItem: FC<SceneHierarchyTreeItemProps> = ({
const node = getSceneNodeByRef(key);
const component = findComponentByType(node, KnownComponentType.ModelRef) as IModelRefComponentInternal;
const componentRef = component?.ref;
// Show sub models for non Tiles3D models in edit mode
const showSubModel = useMemo(() => {
return component && !!model && !isViewing();
return component && component.modelType !== ModelType.Tiles3D && !!model && !isViewing();
}, [component, model]);
const isSubModel = !!findComponentByType(node, KnownComponentType.SubModelRef);
const isDynamic = isDynamicNode(node);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ jest.doMock('../../../../../src/utils/nodeUtils', () => ({
import { AddObjectMenu } from './AddObjectMenu';
import { IColorOverlayComponentInternal, useStore } from '../../../../store';
import {
AssetType,
COMPOSER_FEATURES,
DEFAULT_CAMERA_OPTIONS,
DEFAULT_LIGHT_SETTINGS_MAP,
Expand All @@ -27,12 +28,9 @@ import { CameraType, Component, LightType } from '../../../../models/SceneModels
import { createNodeWithTransform } from '../../../../utils/nodeUtils';
import { ToolbarOrientation } from '../../common/types';
import { isDynamicScene } from '../../../../utils/entityModelUtils/sceneUtils';
import { TILESET_JSON } from '../../../../utils/sceneAssetUtils';
/* eslint-enable */

jest.mock('../../../../utils/pathUtils', () => ({
extractFileNameExtFromUrl: jest.fn().mockReturnValue(['filename', 'ext']),
}));

jest.mock('../../../../utils/entityModelUtils/sceneUtils', () => ({
isDynamicScene: jest.fn(),
}));
Expand All @@ -42,6 +40,7 @@ describe('AddObjectMenu', () => {
const appendSceneNode = jest.fn();
const showAssetBrowserCallback = jest.fn();
const setAddingWidget = jest.fn();
const addMessages = jest.fn();
const selectedSceneNodeRef = 'test-ref';
const mockMetricRecorder = {
recordClick: jest.fn(),
Expand All @@ -61,6 +60,7 @@ describe('AddObjectMenu', () => {
}),
setAddingWidget,
mainCameraObject,
addMessages,
} as any);
jest.clearAllMocks();

Expand Down Expand Up @@ -177,20 +177,23 @@ describe('AddObjectMenu', () => {
});

it('should call setAddingWidget when adding a model', () => {
const expectedNodeName = 'modelUri';
const filename = `${expectedNodeName}.${AssetType.GLTF}`;
const modelUri = `bucket/assets/${filename}`;
const gltfComponent: IModelRefComponent = {
type: 'ModelRef',
uri: 'modelUri',
modelType: 'EXT',
type: KnownComponentType.ModelRef,
uri: modelUri,
modelType: AssetType.GLTF,
};
showAssetBrowserCallback.mockImplementationOnce((callback) => callback(null, 'modelUri'));
showAssetBrowserCallback.mockImplementationOnce((callback) => callback(null, modelUri));

render(<AddObjectMenu canvasHeight={undefined} toolbarOrientation={ToolbarOrientation.Vertical} />);
const sut = screen.getByTestId('add-object-model');
fireEvent.pointerUp(sut);
expect(setAddingWidget).toBeCalledWith({
type: KnownComponentType.ModelRef,
node: {
name: 'filename',
name: expectedNodeName,
components: [gltfComponent],
parentRef: selectedSceneNodeRef,
},
Expand All @@ -199,6 +202,41 @@ describe('AddObjectMenu', () => {
expect(mockMetricRecorder.recordClick).toBeCalledWith('add-object-model');
});

it('should call setAddingWidget when adding a tiles3d model', () => {
const expectedNodeName = 'MixerTileset';
const modelUri = `bucket/assets/${expectedNodeName}/${TILESET_JSON}`;
const gltfComponent: IModelRefComponent = {
type: KnownComponentType.ModelRef,
uri: modelUri,
modelType: AssetType.TILES_3D,
};
showAssetBrowserCallback.mockImplementationOnce((callback) => callback(null, modelUri));

render(<AddObjectMenu canvasHeight={undefined} toolbarOrientation={ToolbarOrientation.Vertical} />);
const sut = screen.getByTestId('add-object-model');
fireEvent.pointerUp(sut);
expect(setAddingWidget).toBeCalledWith({
type: KnownComponentType.ModelRef,
node: {
name: expectedNodeName,
components: [gltfComponent],
parentRef: selectedSceneNodeRef,
},
});
expect(mockMetricRecorder.recordClick).toBeCalledTimes(1);
expect(mockMetricRecorder.recordClick).toBeCalledWith('add-object-model');
});

it('should ignore action and display a message when adding an invalid model type', () => {
showAssetBrowserCallback.mockImplementationOnce((callback) => callback(null, 'file.invalid'));

render(<AddObjectMenu canvasHeight={undefined} toolbarOrientation={ToolbarOrientation.Vertical} />);
const sut = screen.getByTestId('add-object-model');
fireEvent.pointerUp(sut);
expect(setAddingWidget).toBeCalledTimes(0);
expect(addMessages).toBeCalledTimes(1);
});

it('should call addComponentInternal when adding a model shader', () => {
const colorOverlayComponent: IColorOverlayComponentInternal = {
ref: expect.any(String),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import { sceneComposerIdContext } from '../../../../common/sceneComposerIdContext';
import { Component, LightType } from '../../../../models/SceneModels';
import { IColorOverlayComponentInternal, ISceneNodeInternal, useEditorState, useStore } from '../../../../store';
import { extractFileNameExtFromUrl, parseS3BucketFromArn } from '../../../../utils/pathUtils';
import { parseS3BucketFromArn } from '../../../../utils/pathUtils';
import { ToolbarItem } from '../../common/ToolbarItem';
import { ToolbarItemOptionRaw, ToolbarItemOptions, ToolbarOrientation } from '../../common/types';
import { getGlobalSettings } from '../../../../common/GlobalSettings';
Expand All @@ -31,6 +31,7 @@ import { createNodeWithTransform, findComponentByType } from '../../../../utils/
import { FLOATING_TOOLBAR_VERTICAL_ORIENTATION_BUFFER } from '../FloatingToolbar';
import { TOOLBAR_ITEM_CONTAINER_HEIGHT } from '../../common/styledComponents';
import { isDynamicScene } from '../../../../utils/entityModelUtils/sceneUtils';
import { evaluateModelType } from '../../../../utils/sceneAssetUtils';

// Note: ObjectType String is used to record metric. DO NOT change existing ids unless it's necessary.
enum ObjectTypes {
Expand Down Expand Up @@ -88,6 +89,7 @@ export const AddObjectMenu = ({ canvasHeight, toolbarOrientation }: AddObjectMen
const getSceneNodeByRef = useStore(sceneComposerId)((state) => state.getSceneNodeByRef);
const document = useStore(sceneComposerId)((state) => state.document);
const nodeMap = useStore(sceneComposerId)((state) => state.document.nodeMap);
const addMessages = useStore(sceneComposerId)((state) => state.addMessages);
const { setAddingWidget, getObject3DBySceneNodeRef } = useEditorState(sceneComposerId);
const { enableMatterportViewer } = useMatterportViewer();
const { formatMessage } = useIntl();
Expand Down Expand Up @@ -197,7 +199,7 @@ export const AddObjectMenu = ({ canvasHeight, toolbarOrientation }: AddObjectMen
});
}, [selectedSceneNodeRef]);

const handleAddViewCamera = useCallback(() => {
const handleAddViewCamera = () => {
if (mainCameraObject) {
const cameraComponent: ICameraComponent = {
cameraType: activeCameraSettings.cameraType,
Expand Down Expand Up @@ -237,12 +239,18 @@ export const AddObjectMenu = ({ canvasHeight, toolbarOrientation }: AddObjectMen

appendSceneNode(newNode);
}
}, [selectedSceneNodeRef]);
};

const handleAddModel = (modelType?: string, mustBeRoot = false) => {
const handleAddModel = (mustBeRoot = false) => {
if (showAssetBrowserCallback) {
showAssetBrowserCallback((s3BucketArn, contentLocation) => {
const [filename, ext] = extractFileNameExtFromUrl(contentLocation);
// Check that the file is a valid 3D model type
const result = evaluateModelType(contentLocation, addMessages, formatMessage);

// If the file is not valid to load into the scene then ignore it
if (!result) {
return;
}

let modelUri: string;
if (s3BucketArn === null) {
Expand All @@ -253,13 +261,13 @@ export const AddObjectMenu = ({ canvasHeight, toolbarOrientation }: AddObjectMen
}

const gltfComponent: IModelRefComponent = {
type: 'ModelRef',
type: KnownComponentType.ModelRef,
uri: modelUri,
modelType: modelType ?? ext.toUpperCase(),
modelType: result.modelType,
};

const node = {
name: filename,
name: result.modelName,
components: [gltfComponent],
parentRef: mustBeRoot ? undefined : selectedSceneNodeRef,
} as unknown as ISceneNodeInternal;
Expand Down
3 changes: 2 additions & 1 deletion packages/scene-composer/src/interfaces/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export enum AssetType {
MP4 = 'MP4',
PDF = 'PDF',
PNG = 'PNG',
TILES_3D = 'Tiles3D',
}

export const ModelFileTypeList: AssetType[] = [AssetType.GLB, AssetType.GLTF];
export const ModelFileTypeList: AssetType[] = [AssetType.GLB, AssetType.GLTF, AssetType.TILES_3D];
export const TextureFileTypeList: AssetType[] = [AssetType.JPG, AssetType.JPEG, AssetType.PNG];

0 comments on commit eec0f50

Please sign in to comment.