immersive2/src/diagram/diagramManager.ts
Michael Mainguy ebad30ce4d Implement Virtual Stick scaling with modular ResizeGizmo architecture
Refactored ResizeGizmo into modular structure:
- ResizeGizmo.ts: Main implementation with Virtual Stick scaling
- enums.ts: HandleType and HandleState enums
- types.ts: TypeScript interfaces
- index.ts: Barrel exports

Implemented Virtual Stick scaling approach:
- Fixed-length virtual stick extends from controller forward
- Scaling based on distance ratio in mesh local space
- World-to-local coordinate transforms for proper rotation handling
- Smooth continuous scaling during drag (no rounding)
- Snap to 0.1 increments on grip release
- Face handles: round only scaled axis
- Corner handles: round uniformly on all axes

Fixed scaling oscillation issues:
- Freeze handle position updates during active scaling
- Prevents feedback loop between scaling and handle positioning
- Use absoluteRotationQuaternion for proper handle rotation

Added WebXRDefaultExperience parameter to constructor for proper controller integration with manual ray casting in world space.

Added test shortcuts:
- Ctrl+Shift+T: Create test entities (sphere and box)
- Ctrl+Shift+X: Clear all entities

Wired Close button to dispose active ResizeGizmo.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 11:43:17 -06:00

187 lines
7.4 KiB
TypeScript

import {AbstractActionManager, AbstractMesh, ActionManager, Observable, Scene, WebXRDefaultExperience} from "@babylonjs/core";
import {DiagramEntity, DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity";
import log from "loglevel";
import {AppConfig} from "../util/appConfig";
import {buildEntityActionManager} from "./functions/buildEntityActionManager";
import {DefaultScene} from "../defaultScene";
import {DiagramMenuManager} from "./diagramMenuManager";
import {DiagramEventObserverMask} from "./types/diagramEventObserverMask";
import {DiagramObject} from "./diagramObject";
import {getMe} from "../util/me";
import {UserModelType} from "../users/userTypes";
import {vectoxys} from "./functions/vectorConversion";
import {controllerObservable} from "../controllers/controllers";
import {ControllerEvent} from "../controllers/types/controllerEvent";
export class DiagramManager {
private readonly _logger = log.getLogger('DiagramManager');
public readonly _config: AppConfig;
private readonly _controllerObservable: Observable<ControllerEvent>;
private readonly _diagramEntityActionManager: ActionManager;
public readonly onDiagramEventObservable: Observable<DiagramEvent> = new Observable();
public readonly onUserEventObservable: Observable<UserModelType> = new Observable();
private readonly _diagramMenuManager: DiagramMenuManager;
private readonly _scene: Scene;
private readonly _diagramObjects: Map<string, DiagramObject> = new Map<string, DiagramObject>();
private readonly _me: string;
private _moving: number = 10;
private _i: number = 0;
public get diagramMenuManager(): DiagramMenuManager {
return this._diagramMenuManager;
}
public setXR(xr: WebXRDefaultExperience): void {
this._diagramMenuManager.setXR(xr);
}
constructor(readyObservable: Observable<boolean>) {
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);
this.onUserEventObservable.add((user) => {
if (user.id != this._me) {
this._logger.debug('user event', user);
}
});
window.setInterval(() => {
this._i++;
const platform = this._scene.getMeshByName('platform');
if (!platform || !platform.physicsBody) {
return;
}
if (platform.physicsBody) {
if ((this._i % this._moving) == 0) {
this.onUserEventObservable.notifyObservers(
{
id: this._me,
name: 'me',
type: 'user',
base: {
position: vectoxys(platform.absolutePosition),
rotation: vectoxys(platform.absoluteRotationQuaternion.toEulerAngles()),
velocity: vectoxys(platform.physicsBody.getLinearVelocity())
},
}
);
}
if (platform.physicsBody.getLinearVelocity().length() > 0.01) {
this._moving = 1;
} else {
this._moving = 10;
}
}
}, 100);
document.addEventListener('uploadImage', (event: CustomEvent) => {
const diagramEntity: DiagramEntity = {
template: '#image-template',
image: event.detail.data,
text: event.detail.name,
type: DiagramEntityType.ENTITY,
position: {x: 0, y: 1.6, z: 0},
rotation: {x: 0, y: Math.PI, z: 0},
scale: {x: 1, y: 1, z: 1},
}
const object = new DiagramObject(this._scene, this.onDiagramEventObservable, {diagramEntity: diagramEntity});
this._diagramObjects.set(diagramEntity.id, object);
//const newMesh = buildMeshFromDiagramEntity(diagramEntity, this._scene);
if (this.onDiagramEventObservable) {
this.onDiagramEventObservable.notifyObservers({
type: DiagramEventType.ADD,
entity: diagramEntity
}, DiagramEventObserverMask.TO_DB);
}
});
this._logger.debug("DiagramManager constructed");
}
public get actionManager(): AbstractActionManager {
return this._diagramEntityActionManager;
}
public getDiagramObject(id: string) {
return this._diagramObjects.get(id);
}
public isDiagramObject(mesh: AbstractMesh) {
return this._diagramObjects.has(mesh?.id)
}
public createCopy(id: string): DiagramObject {
const diagramObject = this._diagramObjects.get(id);
if (!diagramObject) {
this._logger.warn('createCopy called with invalid diagram object', id);
return null;
}
const obj = diagramObject.clone();
return obj;
}
public addObject(diagramObject: DiagramObject) {
this._diagramObjects.set(diagramObject.diagramEntity.id, diagramObject);
}
public get config(): AppConfig {
return this._config;
}
private onDiagramEvent(event: DiagramEvent) {
let diagramObject = this._diagramObjects.get(event?.entity?.id);
switch (event.type) {
case DiagramEventType.CLEAR:
this._diagramObjects.forEach((value) => {
value.dispose();
});
this._diagramObjects.clear();
break;
case DiagramEventType.ADD:
if (diagramObject) {
diagramObject.fromDiagramEntity(event.entity);
} else {
diagramObject = DiagramObject.CreateObject(this._scene,
this.onDiagramEventObservable,
{diagramEntity: event.entity, actionManager: this._diagramEntityActionManager});
}
if (diagramObject) {
this._diagramObjects.set(event.entity.id, diagramObject);
} else {
this._logger.error('failed to create diagram object for ', event.entity);
}
break;
case DiagramEventType.REMOVE:
if (diagramObject) {
diagramObject.dispose();
}
this._diagramObjects.delete(event?.entity?.id);
break;
case DiagramEventType.MODIFY:
this._logger.debug(event);
//diagramObject = this._diagramObjects.get(event.entity.id);
if (diagramObject && event.entity.text && event.entity.text != diagramObject.text) {
diagramObject.text = event.entity.text;
} else {
this._logger.warn('Skipping text update for', event);
}
if (event.entity.position) {
//diagramObject.position = event.entity.position;
}
break;
}
}
}