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>
187 lines
7.4 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|