UNPKG

@shopware-ag/dive

Version:

Shopware Spatial Framework

809 lines (703 loc) 26.1 kB
import { Actions } from './actions/index.ts'; import { generateUUID } from 'three/src/math/MathUtils'; import { isSelectTool } from '../toolbox/select/SelectTool.ts'; import { merge } from 'lodash'; import { DIVEModule } from '../module/Module.ts'; // type imports import { type Color, type MeshStandardMaterial } from 'three'; import { type COMLight, type COMModel, type COMEntity, type COMPov, type COMPrimitive, type COMGroup, } from './types'; import { type DIVEScene } from '../scene/Scene.ts'; import type DIVEToolbox from '../toolbox/Toolbox.ts'; import type DIVEOrbitControls from '../controls/OrbitControls.ts'; import { type DIVEModel } from '../model/Model.ts'; import { type DIVEMediaCreator } from '../mediacreator/MediaCreator.ts'; import { type DIVERenderer } from '../renderer/Renderer.ts'; import { type DIVESelectable } from '../interface/Selectable.ts'; import { type DIVEIO } from '../io/IO.ts'; import { type DIVEAR } from '../ar/AR.ts'; type EventListener<Action extends keyof Actions> = ( payload: Actions[Action]['PAYLOAD'], ) => void; type Unsubscribe = () => boolean; /** * Main class for communicating with DIVE. * * You can subscribe to actions and perform them from outside and inside DIVE. * * ```ts * import { DIVE } from "@shopware-ag/dive"; * * const dive = new DIVE(); * * dive.Communication.Subscribe('GET_ALL_SCENE_DATA', () => { * // do something * })); * * dive.Communication.PerformAction('GET_ALL_SCENE_DATA', {}); * ``` * * @module */ export class DIVECommunication { private static __instances: DIVECommunication[] = []; public static get(id: string): DIVECommunication | undefined { const fromComID = this.__instances.find( (instance) => instance.id === id, ); if (fromComID) return fromComID; return this.__instances.find((instance) => Array.from(instance.registered.values()).find( (object) => object.id === id, ), ); } private _id: string; public get id(): string { return this._id; } private renderer: DIVERenderer; private scene: DIVEScene; private controller: DIVEOrbitControls; private toolbox: DIVEToolbox; private _mediaGenerator: DIVEModule<DIVEMediaCreator> = new DIVEModule( '../mediacreator/MediaCreator.ts', 'DIVEMediaCreator', ); private _io: DIVEModule<DIVEIO> = new DIVEModule('../io/IO.ts', 'DIVEIO'); private _ar: DIVEModule<DIVEAR> = new DIVEModule('../ar/AR.ts', 'DIVEAR'); private registered: Map<string, COMEntity> = new Map(); // private listeners: { [key: string]: EventListener[] } = {}; private listeners: Map<keyof Actions, EventListener<keyof Actions>[]> = new Map(); constructor( renderer: DIVERenderer, scene: DIVEScene, controls: DIVEOrbitControls, toolbox: DIVEToolbox, ) { this._id = generateUUID(); this.renderer = renderer; this.scene = scene; this.controller = controls; this.toolbox = toolbox; DIVECommunication.__instances.push(this); } public DestroyInstance(): boolean { const existingIndex = DIVECommunication.__instances.findIndex( (entry) => entry.id === this.id, ); if (existingIndex === -1) return false; DIVECommunication.__instances.splice(existingIndex, 1); return true; } public PerformAction<Action extends keyof Actions>( action: Action, payload?: Actions[Action]['PAYLOAD'], ): Actions[Action]['RETURN'] { let returnValue: Actions[Action]['RETURN'] = false; switch (action) { case 'START_RENDER': { this.renderer.StartRenderer(this.scene, this.controller.object); returnValue = true; break; } case 'GET_ALL_SCENE_DATA': { returnValue = this.getAllSceneData( payload as Actions['GET_ALL_SCENE_DATA']['PAYLOAD'], ); break; } case 'GET_ALL_OBJECTS': { returnValue = this.getAllObjects( payload as Actions['GET_ALL_OBJECTS']['PAYLOAD'], ); break; } case 'GET_OBJECTS': { returnValue = this.getObjects( payload as Actions['GET_OBJECTS']['PAYLOAD'], ); break; } case 'ADD_OBJECT': { returnValue = this.addObject( payload as Actions['ADD_OBJECT']['PAYLOAD'], ); break; } case 'UPDATE_OBJECT': { returnValue = this.updateObject( payload as Actions['UPDATE_OBJECT']['PAYLOAD'], ); break; } case 'DELETE_OBJECT': { returnValue = this.deleteObject( payload as Actions['DELETE_OBJECT']['PAYLOAD'], ); break; } case 'SELECT_OBJECT': { returnValue = this.selectObject( payload as Actions['SELECT_OBJECT']['PAYLOAD'], ); break; } case 'DESELECT_OBJECT': { returnValue = this.deselectObject( payload as Actions['DESELECT_OBJECT']['PAYLOAD'], ); break; } case 'SET_BACKGROUND': { returnValue = this.setBackground( payload as Actions['SET_BACKGROUND']['PAYLOAD'], ); break; } case 'DROP_IT': { returnValue = this.dropIt( payload as Actions['DROP_IT']['PAYLOAD'], ); break; } case 'PLACE_ON_FLOOR': { returnValue = this.placeOnFloor( payload as Actions['PLACE_ON_FLOOR']['PAYLOAD'], ); break; } case 'SET_CAMERA_TRANSFORM': { returnValue = this.setCameraTransform( payload as Actions['SET_CAMERA_TRANSFORM']['PAYLOAD'], ); break; } case 'GET_CAMERA_TRANSFORM': { returnValue = this.getCameraTransform( payload as Actions['GET_CAMERA_TRANSFORM']['PAYLOAD'], ); break; } case 'MOVE_CAMERA': { returnValue = this.moveCamera( payload as Actions['MOVE_CAMERA']['PAYLOAD'], ); break; } case 'RESET_CAMERA': { returnValue = this.resetCamera( payload as Actions['RESET_CAMERA']['PAYLOAD'], ); break; } case 'COMPUTE_ENCOMPASSING_VIEW': { returnValue = this.computeEncompassingView( payload as Actions['COMPUTE_ENCOMPASSING_VIEW']['PAYLOAD'], ); break; } case 'SET_CAMERA_LAYER': { returnValue = this.setCameraLayer( payload as Actions['SET_CAMERA_LAYER']['PAYLOAD'], ); break; } case 'ZOOM_CAMERA': { returnValue = this.zoomCamera( payload as Actions['ZOOM_CAMERA']['PAYLOAD'], ); break; } case 'SET_GIZMO_MODE': { returnValue = this.setGizmoMode( payload as Actions['SET_GIZMO_MODE']['PAYLOAD'], ); break; } case 'SET_GIZMO_VISIBILITY': { returnValue = this.setGizmoVisibility( payload as Actions['SET_GIZMO_VISIBILITY']['PAYLOAD'], ); break; } case 'SET_GIZMO_SCALE_LINKED': { returnValue = this.setGizmoScaleLinked( payload as Actions['SET_GIZMO_SCALE_LINKED']['PAYLOAD'], ); break; } case 'USE_TOOL': { returnValue = this.useTool( payload as Actions['USE_TOOL']['PAYLOAD'], ); break; } case 'MODEL_LOADED': { returnValue = this.modelLoaded( payload as Actions['MODEL_LOADED']['PAYLOAD'], ); break; } case 'UPDATE_SCENE': { returnValue = this.updateScene( payload as Actions['UPDATE_SCENE']['PAYLOAD'], ); break; } case 'GENERATE_MEDIA': { returnValue = this.generateMedia( payload as Actions['GENERATE_MEDIA']['PAYLOAD'], ); break; } case 'SET_PARENT': { returnValue = this.setParent( payload as Actions['SET_PARENT']['PAYLOAD'], ); break; } case 'EXPORT_SCENE': { returnValue = this.exportScene( payload as Actions['EXPORT_SCENE']['PAYLOAD'], ); break; } case 'LAUNCH_AR': { returnValue = new Promise<void>((resolve, reject) => { this._ar .get() .then((ar) => { resolve( ar.Launch( payload as Actions['LAUNCH_AR']['PAYLOAD'], ), ); }) .catch(reject); }); break; } default: { console.warn( `DIVECommunication.PerformAction: has been executed with unknown Action type ${action}`, ); } } this.dispatch(action, payload); return returnValue; } public Subscribe<Action extends keyof Actions>( type: Action, listener: EventListener<Action>, ): Unsubscribe { if (!this.listeners.get(type)) this.listeners.set(type, []); // casting to any because of typescript not finding between Action and typeof Actions being equal in this case this.listeners .get(type)! .push(listener as EventListener<keyof Actions>); return () => { const listenerArray = this.listeners.get(type); if (!listenerArray) return false; const existingIndex = listenerArray.findIndex( (entry) => entry === listener, ); if (existingIndex === -1) return false; listenerArray.splice(existingIndex, 1); return true; }; } private dispatch<Action extends keyof Actions>( type: Action, payload: Actions[Action]['PAYLOAD'], ): void { const listenerArray = this.listeners.get(type); if (!listenerArray) return; listenerArray.forEach((listener) => listener(payload)); } private getAllSceneData( payload: Actions['GET_ALL_SCENE_DATA']['PAYLOAD'], ): Actions['GET_ALL_SCENE_DATA']['RETURN'] { const sceneData = { name: this.scene.name, mediaItem: null, backgroundColor: '#' + (this.scene.background as Color).getHexString(), floorEnabled: this.scene.Floor.visible, floorColor: '#' + ( this.scene.Floor.material as MeshStandardMaterial ).color.getHexString(), userCamera: { position: this.controller.object.position.clone(), target: this.controller.target.clone(), }, spotmarks: [], lights: Array.from(this.registered.values()).filter( (object) => object.entityType === 'light', ) as COMLight[], objects: Array.from(this.registered.values()).filter( (object) => object.entityType === 'model', ) as COMModel[], cameras: Array.from(this.registered.values()).filter( (object) => object.entityType === 'pov', ) as COMPov[], primitives: Array.from(this.registered.values()).filter( (object) => object.entityType === 'primitive', ) as COMPrimitive[], groups: Array.from(this.registered.values()).filter( (object) => object.entityType === 'group', ) as COMGroup[], }; Object.assign(payload, sceneData); return sceneData; } private getAllObjects( payload: Actions['GET_ALL_OBJECTS']['PAYLOAD'], ): Actions['GET_ALL_OBJECTS']['RETURN'] { Object.assign(payload, this.registered); return this.registered; } private getObjects( payload: Actions['GET_OBJECTS']['PAYLOAD'], ): Actions['GET_OBJECTS']['RETURN'] { if (payload.ids.length === 0) return []; const objects: COMEntity[] = []; this.registered.forEach((object) => { if (!payload.ids.includes(object.id)) return; objects.push(object); }); return objects; } private addObject( payload: Actions['ADD_OBJECT']['PAYLOAD'], ): Actions['ADD_OBJECT']['RETURN'] { if (this.registered.get(payload.id)) return false; if (payload.parentId === undefined) payload.parentId = null; this.registered.set(payload.id, payload); this.scene.AddSceneObject(payload); return true; } private updateObject( payload: Actions['UPDATE_OBJECT']['PAYLOAD'], ): Actions['UPDATE_OBJECT']['RETURN'] { const objectToUpdate = this.registered.get(payload.id); if (!objectToUpdate) return false; this.registered.set(payload.id, merge(objectToUpdate, payload)); const updatedObject = this.registered.get(payload.id)!; this.scene.UpdateSceneObject({ ...payload, id: updatedObject.id, entityType: updatedObject.entityType, }); Object.assign(payload, updatedObject); return true; } private deleteObject( payload: Actions['DELETE_OBJECT']['PAYLOAD'], ): Actions['DELETE_OBJECT']['RETURN'] { const deletedObject = this.registered.get(payload.id); if (!deletedObject) return false; // If the object has a parent, detach it first if (deletedObject.parentId) { // First detach from parent group this.setParent({ object: { id: deletedObject.id }, parent: null, }); } // If deleting a group, update all children to have no parent if (deletedObject.entityType === 'group') { this.registered.forEach((object) => { if (object.parentId === deletedObject.id) { this.updateObject({ id: object.id, parentId: null, }); } }); } // copy object to payload to use later Object.assign(payload, deletedObject); this.registered.delete(payload.id); // detach all children from parent if we delete a group Array.from(this.registered.values()).forEach((object) => { if (!object.parentId) return; if (object.parentId !== payload.id) return; object.parentId = null; }); this.scene.DeleteSceneObject(deletedObject); return true; } private selectObject( payload: Actions['SELECT_OBJECT']['PAYLOAD'], ): Actions['SELECT_OBJECT']['RETURN'] { const object = this.registered.get(payload.id); if (!object) return false; const sceneObject = this.scene.GetSceneObject(object); if (!sceneObject) return false; if (!('isSelectable' in sceneObject)) return false; const activeTool = this.toolbox.GetActiveTool(); if (activeTool && isSelectTool(activeTool)) { activeTool.AttachGizmo(sceneObject as DIVESelectable); } // copy object to payload to use later Object.assign(payload, object); return true; } private deselectObject( payload: Actions['DESELECT_OBJECT']['PAYLOAD'], ): Actions['DESELECT_OBJECT']['RETURN'] { const object = this.registered.get(payload.id); if (!object) return false; const sceneObject = this.scene.GetSceneObject(object); if (!sceneObject) return false; if (!('isSelectable' in sceneObject)) return false; const activeTool = this.toolbox.GetActiveTool(); if (activeTool && isSelectTool(activeTool)) { activeTool.DetachGizmo(); } // copy object to payload to use later Object.assign(payload, object); return true; } private setBackground( payload: Actions['SET_BACKGROUND']['PAYLOAD'], ): Actions['SET_BACKGROUND']['RETURN'] { this.scene.SetBackground(payload.color); return true; } private dropIt( payload: Actions['DROP_IT']['PAYLOAD'], ): Actions['DROP_IT']['RETURN'] { const object = this.registered.get(payload.id); if (!object) return false; const model = this.scene.GetSceneObject(object) as DIVEModel; model.DropIt(); return true; } private placeOnFloor( payload: Actions['PLACE_ON_FLOOR']['PAYLOAD'], ): Actions['PLACE_ON_FLOOR']['RETURN'] { const object = this.registered.get(payload.id); if (!object) return false; this.scene.PlaceOnFloor(object); return true; } private setCameraTransform( payload: Actions['SET_CAMERA_TRANSFORM']['PAYLOAD'], ): Actions['SET_CAMERA_TRANSFORM']['RETURN'] { this.controller.object.position.copy(payload.position); this.controller.target.copy(payload.target); this.controller.update(); return true; } private getCameraTransform( payload: Actions['GET_CAMERA_TRANSFORM']['PAYLOAD'], ): Actions['GET_CAMERA_TRANSFORM']['RETURN'] { const transform = { position: this.controller.object.position.clone(), target: this.controller.target.clone(), }; Object.assign(payload, transform); return transform; } private moveCamera( payload: Actions['MOVE_CAMERA']['PAYLOAD'], ): Actions['MOVE_CAMERA']['RETURN'] { let position = { x: 0, y: 0, z: 0 }; let target = { x: 0, y: 0, z: 0 }; if ('id' in payload) { position = (this.registered.get(payload.id) as COMPov).position; target = (this.registered.get(payload.id) as COMPov).target; } else { position = payload.position; target = payload.target; } this.controller.MoveTo( position, target, payload.duration, payload.locked, ); return true; } private setCameraLayer( payload: Actions['SET_CAMERA_LAYER']['PAYLOAD'], ): Actions['SET_CAMERA_LAYER']['RETURN'] { this.controller.object.SetCameraLayer(payload.layer); return true; } private resetCamera( payload: Actions['RESET_CAMERA']['PAYLOAD'], ): Actions['RESET_CAMERA']['RETURN'] { this.controller.RevertLast(payload.duration); return true; } private computeEncompassingView( payload: Actions['COMPUTE_ENCOMPASSING_VIEW']['PAYLOAD'], ): Actions['COMPUTE_ENCOMPASSING_VIEW']['RETURN'] { const sceneBB = this.scene.ComputeSceneBB(); const transform = this.controller.ComputeEncompassingView(sceneBB); Object.assign(payload, transform); return transform; } private zoomCamera( payload: Actions['ZOOM_CAMERA']['PAYLOAD'], ): Actions['ZOOM_CAMERA']['RETURN'] { if (payload.direction === 'IN') this.controller.ZoomIn(payload.by); if (payload.direction === 'OUT') this.controller.ZoomOut(payload.by); return true; } private setGizmoMode( payload: Actions['SET_GIZMO_MODE']['PAYLOAD'], ): Actions['SET_GIZMO_MODE']['RETURN'] { this.toolbox.SetGizmoMode(payload.mode); return true; } private setGizmoVisibility( payload: Actions['SET_GIZMO_VISIBILITY']['PAYLOAD'], ): Actions['SET_GIZMO_VISIBILITY']['RETURN'] { this.toolbox.SetGizmoVisibility(payload); return payload; } private setGizmoScaleLinked( payload: Actions['SET_GIZMO_SCALE_LINKED']['PAYLOAD'], ): Actions['SET_GIZMO_SCALE_LINKED']['RETURN'] { this.toolbox.SetGizmoScaleLinked(payload); return payload; } private useTool( payload: Actions['USE_TOOL']['PAYLOAD'], ): Actions['USE_TOOL']['RETURN'] { this.toolbox.UseTool(payload.tool); return true; } private modelLoaded( payload: Actions['MODEL_LOADED']['PAYLOAD'], ): Actions['MODEL_LOADED']['RETURN'] { (this.registered.get(payload.id) as COMModel).loaded = true; return true; } private updateScene( payload: Actions['UPDATE_SCENE']['PAYLOAD'], ): Actions['UPDATE_SCENE']['RETURN'] { if (payload.name !== undefined) this.scene.name = payload.name; if (payload.backgroundColor !== undefined) this.scene.SetBackground(payload.backgroundColor); if (payload.gridEnabled !== undefined) this.scene.Grid.SetVisibility(payload.gridEnabled); if (payload.floorEnabled !== undefined) this.scene.Floor.SetVisibility(payload.floorEnabled); if (payload.floorColor !== undefined) this.scene.Floor.SetColor(payload.floorColor); // fill payload with current values // TODO optmize this payload.name = this.scene.name; payload.backgroundColor = '#' + (this.scene.background as Color).getHexString(); payload.gridEnabled = this.scene.Grid.visible; payload.floorEnabled = this.scene.Floor.visible; payload.floorColor = '#' + ( this.scene.Floor.material as MeshStandardMaterial ).color.getHexString(); return true; } private generateMedia( payload: Actions['GENERATE_MEDIA']['PAYLOAD'], ): Actions['GENERATE_MEDIA']['RETURN'] { let position = { x: 0, y: 0, z: 0 }; let target = { x: 0, y: 0, z: 0 }; if ('id' in payload) { position = (this.registered.get(payload.id) as COMPov).position; target = (this.registered.get(payload.id) as COMPov).target; } else { position = payload.position; target = payload.target; } return this._mediaGenerator.get().then((module) => { return module.GenerateMedia( position, target, payload.width, payload.height, ); }); } private setParent( payload: Actions['SET_PARENT']['PAYLOAD'], ): Actions['SET_PARENT']['RETURN'] { const object = this.registered.get(payload.object.id); if (!object) return false; const sceneObject = this.scene.GetSceneObject(object); if (!sceneObject) return false; if (payload.parent === null) { // detach from current parent this.scene.Root.attach(sceneObject); // Update registration to reflect no parent this.updateObject({ id: object.id, parentId: null, }); return true; } if (payload.object.id === payload.parent.id) { // cannot attach object to itself return false; } const parent = this.registered.get(payload.parent.id); if (!parent) { // detach from current parent this.scene.Root.attach(sceneObject); // Update registration to reflect no parent this.updateObject({ id: object.id, parentId: null, }); return true; } // attach to new parent const parentObject = this.scene.GetSceneObject(parent); if (!parentObject) { // detach from current parent this.scene.Root.attach(sceneObject); // Update registration to reflect no parent this.updateObject({ id: object.id, parentId: null, }); return true; } // attach to new parent parentObject.attach(sceneObject); // Update registration to reflect new parent this.updateObject({ id: object.id, parentId: parent.id, }); return true; } private exportScene( payload: Actions['EXPORT_SCENE']['PAYLOAD'], ): Actions['EXPORT_SCENE']['RETURN'] { return new Promise<string | null>((resolve, reject) => { this._io .get() .then((io) => { resolve(io.Export(payload.type)); }) .catch(reject); }); } } export type { Actions } from './actions/index.ts';