immersive2/src/information/inputTextView.ts
Michael Mainguy e329b95f2f Fix AppConfig persistence and consolidate handle storage
AppConfig Persistence Fixes:
- Fix constructor to properly handle null localStorage values
- Add null check before JSON.parse to prevent errors
- Create fresh config copies with spread operator to avoid reference issues
- Add better error handling and logging for config loading
- Initialize handles array properly

React ConfigModal Improvements:
- Fix config initialization to get fresh values on render instead of stale module-level values
- Separate useEffect hooks for each config property (prevents unnecessary updates)
- Fix SegmentedControl string-to-number conversion (locationSnaps now use "0.01", "0.1" format)
- Enable/disable logic now properly sets values to 0 when disabled

Handle Storage Consolidation:
- Create dynamic HandleConfig type with Vec3 for serializable position/rotation/scale
- Add handles array to AppConfigType for flexible handle storage
- Replace individual localStorage keys with centralized AppConfig storage
- Add handle management methods: getHandleConfig, setHandleConfig, removeHandleConfig, getAllHandleConfigs
- Update Handle class to read from AppConfig instead of direct localStorage
- Update dropMesh to save handles via AppConfig using Vec3 serialization
- Convert between BabylonJS Vector3 and serializable Vec3 at conversion points

Benefits:
- Single source of truth for all configuration
- Proper localStorage persistence across page reloads
- Dynamic handle creation without code changes
- Type-safe configuration with proper JSON serialization
- Consolidated storage (one appConfig key instead of multiple handle-* keys)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 15:34:40 -06:00

123 lines
4.5 KiB
TypeScript

import {AbstractMesh, MeshBuilder, Observable, Scene, TransformNode, Vector3} from "@babylonjs/core";
import log, {Logger} from "loglevel";
import {AdvancedDynamicTexture, Control, InputText, VirtualKeyboard} from "@babylonjs/gui";
import {Handle} from "../objects/handle";
import {DefaultScene} from "../defaultScene";
import {ControllerEvent} from "../controllers/types/controllerEvent";
import {ControllerEventType} from "../controllers/types/controllerEventType";
export type TextEvent = {
id: string;
text: string;
}
export class InputTextView {
private logger: Logger = log.getLogger('InputTextView');
public readonly onTextObservable: Observable<TextEvent> = new Observable<TextEvent>();
private readonly scene: Scene;
private readonly inputMesh: AbstractMesh;
private readonly controllerObservable: Observable<ControllerEvent>;
private readonly handle: Handle;
private inputText: InputText;
private diagramMesh: AbstractMesh;
private keyboard: VirtualKeyboard;
constructor(controllerObservable: Observable<ControllerEvent>) {
this.controllerObservable = controllerObservable;
this.scene = DefaultScene.Scene;
this.inputMesh = MeshBuilder.CreatePlane("input", {width: 1, height: .5}, this.scene);
this.handle = new Handle({
contentMesh: this.inputMesh,
label: 'Input',
defaultPosition: new Vector3(0, .4, .5),
defaultRotation: new Vector3(0, 0, 0)
});
// Position is now controlled by Handle class
this.createKeyboard();
}
public get handleMesh(): TransformNode {
return this.handle.transformNode;
}
public show(mesh: AbstractMesh) {
this.handle.transformNode.setEnabled(true);
if (mesh.metadata?.text) {
this.inputText.text = mesh.metadata?.text;
} else {
this.inputText.text = "";
}
this.diagramMesh = mesh;
this.keyboard.isVisible = true;
this.inputText.focus();
this.logger.debug(mesh.metadata);
}
public createKeyboard() {
const advancedTexture = AdvancedDynamicTexture.CreateForMesh(this.inputMesh, 2048, 1024, false);
const input = new InputText();
input.width = 0.5;
input.maxWidth = 0.5;
input.height = "64px";
input.text = "";
input.fontSize = "32px";
input.color = "white";
input.background = "black";
input.thickness = 3;
input.verticalAlignment = Control.VERTICAL_ALIGNMENT_TOP;
this.inputText = input;
advancedTexture.addControl(this.inputText);
const keyboard = VirtualKeyboard.CreateDefaultLayout();
keyboard.scaleY = 2;
keyboard.scaleX = 2;
keyboard.transformCenterY = 0;
keyboard.transformCenterX = .5;
keyboard.verticalAlignment = Control.VERTICAL_ALIGNMENT_TOP;
keyboard.paddingTop = "70px"
keyboard.height = "768px";
keyboard.fontSizeInPixels = 24;
advancedTexture.addControl(keyboard);
keyboard.connect(input);
keyboard.isVisible = true;
keyboard.isEnabled = true;
keyboard.children.forEach((key) => {
key.onPointerEnterObservable.add((eventData, eventState) => {
this.logger.debug(eventData);
const gripId = eventState?.userInfo?.pickInfo?.gripTransform?.id;
if (gripId) {
this.controllerObservable.notifyObservers({
type: ControllerEventType.PULSE,
gripId: gripId
});
}
}, -1, false, this, false);
});
keyboard.onPointerDownObservable.add(() => {
/*this.sounds.tick.play();*/
});
keyboard.onKeyPressObservable.add((key) => {
if (key === '↵') {
if (this.inputText.text && this.inputText.text.length > 0) {
this.logger.error(this.inputText.text);
this.onTextObservable.notifyObservers({id: this.diagramMesh.id, text: this.inputText.text});
} else {
this.onTextObservable.notifyObservers({id: this.diagramMesh.id, text: null});
}
this.hide();
}
}, -1, false, this, false);
this.keyboard = keyboard;
this.handle.transformNode.setEnabled(false);
}
private hide() {
this.handle.transformNode.setEnabled(false);
this.diagramMesh = null;
}
}