diff --git a/package.json b/package.json index bf3df75..a7a6c69 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "immersive", "private": true, - "version": "0.0.8-27", + "version": "0.0.8-28", "type": "module", "license": "MIT", "engines": { diff --git a/public/api/user/features b/public/api/user/features index a7f36d0..26f3329 100644 --- a/public/api/user/features +++ b/public/api/user/features @@ -14,7 +14,7 @@ "privateDesigns": false, "encryptedDesigns": false, "editData": false, - "config": false, + "config": true, "enterImmersive": true, "launchMetaQuest": true }, diff --git a/src/diagram/diagramManager.ts b/src/diagram/diagramManager.ts index 10cbd79..7406954 100644 --- a/src/diagram/diagramManager.ts +++ b/src/diagram/diagramManager.ts @@ -2,7 +2,7 @@ import {AbstractActionManager, AbstractMesh, ActionManager, Observable, Scene, W import {DiagramEntity, DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity"; import log from "loglevel"; -import {AppConfig} from "../util/appConfig"; +import {appConfigInstance} from "../util/appConfig"; import {buildEntityActionManager} from "./functions/buildEntityActionManager"; import {DefaultScene} from "../defaultScene"; import {DiagramMenuManager} from "./diagramMenuManager"; @@ -17,7 +17,6 @@ import {ControllerEvent} from "../controllers/types/controllerEvent"; export class DiagramManager { private readonly _logger = log.getLogger('DiagramManager'); - public readonly _config: AppConfig; private readonly _controllerObservable: Observable; private readonly _diagramEntityActionManager: ActionManager; public readonly onDiagramEventObservable: Observable = new Observable(); @@ -40,7 +39,6 @@ export class DiagramManager { constructor(readyObservable: Observable) { this._me = getMe(); this._scene = DefaultScene.Scene; - this._config = new AppConfig(); this._diagramMenuManager = new DiagramMenuManager(this.onDiagramEventObservable, controllerObservable, readyObservable); this._diagramEntityActionManager = buildEntityActionManager(controllerObservable); this.onDiagramEventObservable.add(this.onDiagramEvent, DiagramEventObserverMask.FROM_DB, true, this); @@ -133,8 +131,8 @@ export class DiagramManager { this._diagramObjects.set(diagramObject.diagramEntity.id, diagramObject); } - public get config(): AppConfig { - return this._config; + public get config() { + return appConfigInstance; } diff --git a/src/diagram/diagramObject.ts b/src/diagram/diagramObject.ts index 2abe86c..0e1c562 100644 --- a/src/diagram/diagramObject.ts +++ b/src/diagram/diagramObject.ts @@ -25,6 +25,7 @@ import {xyztovec} from "./functions/vectorConversion"; import {AnimatedLineTexture} from "../util/animatedLineTexture"; import {getToolboxColors} from "../toolbox/toolbox"; import {findClosestColor} from "../util/functions/findClosestColor"; +import {appConfigInstance} from "../util/appConfig"; /** * Converts a Color3 to a hex color string @@ -62,6 +63,7 @@ export class DiagramObject { private _fromMesh: AbstractMesh; private _toMesh: AbstractMesh; private _meshRemovedObserver: Observer; + private _configObserver: Observer; // Position caching for connection optimization private _lastFromPosition: Vector3 = null; private _lastToPosition: Vector3 = null; @@ -70,6 +72,12 @@ export class DiagramObject { constructor(scene: Scene, eventObservable: Observable, options?: DiagramObjectOptionsType) { this._eventObservable = eventObservable; this._scene = scene; + + // Subscribe to config changes to update label rendering mode + this._configObserver = appConfigInstance.onConfigChangedObservable.add(() => { + this.updateLabelRenderingMode(); + }); + if (options) { this._logger.debug('DiagramObject constructor called with options', options); if (options.diagramEntity) { @@ -154,37 +162,69 @@ export class DiagramObject { this._labelBack.parent = this._label; this._labelBack.metadata = {exportable: true}; this.updateLabelPosition(); + this.updateLabelRenderingMode(); + } + private updateLabelRenderingMode() { + if (!this._label) { + return; + } + const mode = appConfigInstance.current.labelRenderingMode || 'billboard'; + + // Reset billboard mode first + this._label.billboardMode = Mesh.BILLBOARDMODE_NONE; + if (this._labelBack) { + this._labelBack.billboardMode = Mesh.BILLBOARDMODE_NONE; + } + + switch (mode) { + case 'billboard': + // Billboard mode - labels always face camera (Y-axis only to prevent tilting) + this._label.billboardMode = Mesh.BILLBOARDMODE_Y; + if (this._labelBack) { + this._labelBack.billboardMode = Mesh.BILLBOARDMODE_Y; + } + break; + case 'fixed': + // Fixed mode - no billboard (default state, already set above) + break; + case 'dynamic': + // Dynamic mode - to be implemented in future + // TODO: Implement screen-space positioning + this._logger.warn('Dynamic label rendering mode not yet implemented'); + break; + case 'distance': + // Distance-based mode - to be implemented in future + // TODO: Implement distance-based offset + this._logger.warn('Distance-based label rendering mode not yet implemented'); + break; + } } public updateLabelPosition() { if (this._label) { this._mesh.computeWorldMatrix(true); - this._mesh.refreshBoundingInfo({}); + this._mesh.refreshBoundingInfo(); + if (this._from && this._to) { - //this._label.position.x = .06; - //this._label.position.z = .06; + // Connection labels (arrows/lines) this._label.position.y = .05; this._label.rotation.y = Math.PI / 2; this._labelBack.rotation.y = Math.PI; - this._labelBack.position.z = 0.001 - //this._label.billboardMode = Mesh.BILLBOARDMODE_Y; - //this._label.billboardMode = Mesh.BILLBOARDMODE_Y; - + this._labelBack.position.z = 0.001; } else { - const top = - this._mesh.getBoundingInfo().boundingBox.maximumWorld; + // Standard object labels - convert world space to parent's local space + // This accounts for mesh scaling, which is not included in boundingBox.maximum + const top = this._mesh.getBoundingInfo().boundingBox.maximumWorld; const temp = new TransformNode("temp", this._scene); temp.position = top; temp.setParent(this._baseTransform); const y = temp.position.y; temp.dispose(); - this._label.position.y = y + .06; - //this._labelBack.position.y = y + .06; + this._label.position.y = y + 0.06; this._labelBack.rotation.y = Math.PI; - this._labelBack.position.z = 0.001 - //this._label.billboardMode = Mesh.BILLBOARDMODE_Y; + this._labelBack.position.z = 0.001; } } } @@ -320,6 +360,8 @@ export class DiagramObject { this._logger.debug('DiagramObject dispose called for ', this._diagramEntity?.id) this._scene?.onAfterRenderObservable.remove(this._sceneObserver); this._sceneObserver = null; + appConfigInstance?.onConfigChangedObservable.remove(this._configObserver); + this._configObserver = null; this._mesh?.setParent(null); this._mesh?.dispose(true, false); this._mesh = null; diff --git a/src/diagram/functions/createLabel.ts b/src/diagram/functions/createLabel.ts index c380500..001394c 100644 --- a/src/diagram/functions/createLabel.ts +++ b/src/diagram/functions/createLabel.ts @@ -32,11 +32,12 @@ function createDynamicTexture(text: string, font: string, DTWidth: number, DTHei function createMaterial(dynamicTexture: DynamicTexture): Material { const mat = new StandardMaterial("text-mat", DefaultScene.Scene); //mat.diffuseColor = Color3.Black(); - mat.disableLighting = false; - //mat.backFaceCulling = false; + mat.disableLighting = true; + mat.backFaceCulling = true; mat.emissiveTexture = dynamicTexture; mat.diffuseTexture = dynamicTexture; mat.metadata = {exportable: true}; + //mat.freeze(); return mat; } diff --git a/src/react/pages/configModal.tsx b/src/react/pages/configModal.tsx index 176d886..a9e819b 100644 --- a/src/react/pages/configModal.tsx +++ b/src/react/pages/configModal.tsx @@ -1,6 +1,7 @@ -import {Group, Modal, SegmentedControl, Stack, Switch} from "@mantine/core"; +import {Group, Modal, SegmentedControl, Stack, Switch, Select} from "@mantine/core"; import {useEffect, useState} from "react"; -import {setAppConfig} from "../../util/appConfig"; +import {appConfigInstance} from "../../util/appConfig"; +import {LabelRenderingMode} from "../../util/appConfigType"; const locationSnaps = [ {value: ".01", label: '1cm'}, @@ -16,6 +17,12 @@ const rotationSnaps = [ {value: "180", label: '180°'}, {value: "360", label: '360°'}, ] +const labelRenderingModes = [ + {value: 'fixed', label: 'Fixed'}, + {value: 'billboard', label: 'Billboard (Always Face Camera)'}, + {value: 'dynamic', label: 'Dynamic (Coming Soon)', disabled: true}, + {value: 'distance', label: 'Distance-based (Coming Soon)', disabled: true}, +] let defaultConfig = { locationSnap: '.1', @@ -24,7 +31,8 @@ let defaultConfig = rotationSnapEnabled: true, flyModeEnabled: true, snapTurnSnap: '45', - snapTurnSnapEnabled: false + snapTurnSnapEnabled: false, + labelRenderingMode: 'billboard' as LabelRenderingMode } try { const newConfig = JSON.parse(localStorage.getItem('config')); @@ -42,19 +50,30 @@ export default function ConfigModal({configOpened, closeConfig}) { const [rotationSnap, setRotationSnap] = useState(defaultConfig.rotationSnap); const [rotationSnapEnabled, setRotationSnapEnabled] = useState(defaultConfig.rotationSnapEnabled); const [flyModeEnabled, setFlyModeEnabled] = useState(defaultConfig.flyModeEnabled); + const [labelRenderingMode, setLabelRenderingMode] = useState(defaultConfig.labelRenderingMode); useEffect(() => { - const config = { + // Update AppConfig singleton instance directly + // This triggers Observable notifications to all DiagramObjects + appConfigInstance.setLabelRenderingMode(labelRenderingMode); + appConfigInstance.setFlyMode(flyModeEnabled); + appConfigInstance.setGridSnap(parseFloat(locationSnap)); + appConfigInstance.setRotateSnap(parseFloat(rotationSnap)); + appConfigInstance.setTurnSnap(parseFloat(snapTurnSnap)); + + // Also update legacy config for backward compatibility + const legacyConfig = { locationSnap: locationSnap, locationSnapEnabled: locationSnapEnabled, rotationSnap: rotationSnap, rotationSnapEnabled: rotationSnapEnabled, snapTurnSnap: snapTurnSnap, snapTurnSnapEnabled: snapTurnSnapEnabled, - flyModeEnabled: flyModeEnabled - } - setAppConfig(config); + flyModeEnabled: flyModeEnabled, + labelRenderingMode: labelRenderingMode + }; + localStorage.setItem('config', JSON.stringify(legacyConfig)); - }, [locationSnap, locationSnapEnabled, rotationSnap, rotationSnapEnabled, snapTurnSnap, snapTurnSnapEnabled, flyModeEnabled]); + }, [locationSnap, locationSnapEnabled, rotationSnap, rotationSnapEnabled, snapTurnSnap, snapTurnSnapEnabled, flyModeEnabled, labelRenderingMode]); return (

Configuration

@@ -104,6 +123,16 @@ export default function ConfigModal({configOpened, closeConfig}) { value={snapTurnSnap} onChange={setSnapTurnSnap}/> + + +