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>
123 lines
4.5 KiB
TypeScript
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;
|
|
}
|
|
} |