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>
311 lines
11 KiB
TypeScript
311 lines
11 KiB
TypeScript
import {AbstractMesh, KeyboardEventTypes, Scene} from "@babylonjs/core";
|
|
import {Rigplatform} from "./rigplatform";
|
|
|
|
import {DiagramManager} from "../diagram/diagramManager";
|
|
import {wheelHandler} from "./functions/wheelHandler";
|
|
import log, {Logger} from "loglevel";
|
|
import {DiagramEntityType, DiagramEventType, DiagramTemplates} from "../diagram/types/diagramEntity";
|
|
import {DiagramEventObserverMask} from "../diagram/types/diagramEventObserverMask";
|
|
import {getToolboxColors} from "../toolbox/toolbox";
|
|
|
|
export class WebController {
|
|
private readonly scene: Scene;
|
|
private readonly logger: Logger = log.getLogger('WebController');
|
|
private speed: number = 1;
|
|
private rig: Rigplatform;
|
|
private diagramManager: DiagramManager;
|
|
private mouseDown: boolean = false;
|
|
|
|
private upDownWheel: boolean = false;
|
|
private fowardBackWheel: boolean = false;
|
|
private canvas: HTMLCanvasElement;
|
|
|
|
constructor(scene: Scene,
|
|
rig: Rigplatform,
|
|
diagramManager: DiagramManager,
|
|
) {
|
|
this.scene = scene;
|
|
this.rig = rig;
|
|
this.diagramManager = diagramManager;
|
|
|
|
this.canvas = document.querySelector('#gameCanvas');
|
|
//this.referencePlane = MeshBuilder.CreatePlane('referencePlane', {size: 10}, this.scene);
|
|
//this.referencePlane.setEnabled(false);
|
|
//this.referencePlane.visibility = 0.5;
|
|
/*const material = new GridMaterial('grid', this.scene);
|
|
material.gridRatio = 1;
|
|
material.backFaceCulling = false;
|
|
material.antialias = true;
|
|
this.referencePlane.material = material;
|
|
*/
|
|
|
|
this.scene.onKeyboardObservable.add((kbInfo) => {
|
|
this.logger.debug(kbInfo);
|
|
|
|
|
|
if (this.canvas && kbInfo.event.target != this.canvas) {
|
|
return;
|
|
}
|
|
if (kbInfo.type == KeyboardEventTypes.KEYUP) {
|
|
this.rig.turn(0);
|
|
this.rig.updown(0);
|
|
}
|
|
if (kbInfo.type == 1) {
|
|
switch (kbInfo.event.key) {
|
|
case "W":
|
|
this.rig.updown(-this.speed);
|
|
break;
|
|
case "S":
|
|
this.rig.updown(this.speed);
|
|
break;
|
|
case "ArrowUp":
|
|
case "w":
|
|
this.rig.forwardback(-this.speed);
|
|
break;
|
|
case "ArrowDown":
|
|
case "s":
|
|
this.rig.forwardback(this.speed);
|
|
break;
|
|
case "ArrowLeft":
|
|
case "a":
|
|
this.rig.leftright(-this.speed);
|
|
break;
|
|
case "A":
|
|
this.rig.turn(-this.speed);
|
|
break;
|
|
case "ArrowRight":
|
|
case "d":
|
|
this.rig.leftright(this.speed);
|
|
break;
|
|
case "D":
|
|
this.rig.turn(this.speed);
|
|
break;
|
|
case "]":
|
|
this.speed *= 1.5;
|
|
break;
|
|
case "[":
|
|
this.speed *= .5;
|
|
break;
|
|
case " ":
|
|
/*if (kbInfo.event.ctrlKey) {
|
|
if (this.controllers) {
|
|
this.controllers.controllerObservable.notifyObservers(
|
|
{type: ControllerEventType.X_BUTTON, value: 1}
|
|
)
|
|
}
|
|
}
|
|
|
|
*/
|
|
break;
|
|
case "T":
|
|
// Ctrl+Shift+T: Create test entities (sphere and box)
|
|
if (kbInfo.event.ctrlKey && kbInfo.event.shiftKey) {
|
|
this.createTestEntities();
|
|
}
|
|
break;
|
|
case "X":
|
|
// Ctrl+Shift+X: Clear all entities from diagram
|
|
if (kbInfo.event.ctrlKey && kbInfo.event.shiftKey) {
|
|
this.clearAllEntities();
|
|
}
|
|
break;
|
|
default:
|
|
|
|
this.logger.debug(kbInfo.event);
|
|
}
|
|
|
|
} else {
|
|
this.rig.leftright(0);
|
|
this.rig.forwardback(0);
|
|
}
|
|
if (kbInfo.event.key == "Shift") {
|
|
if (kbInfo.type == 1) {
|
|
//this.referencePlane.setEnabled(true);
|
|
} else {
|
|
/* this.referencePlane.setEnabled(false);
|
|
if (this.pickedMesh) {
|
|
this.pickedMesh.showBoundingBox = false;
|
|
this.pickedMesh = null;
|
|
}
|
|
|
|
*/
|
|
}
|
|
}
|
|
});
|
|
|
|
this.scene.onPointerUp = () => {
|
|
this.mouseDown = false;
|
|
this.rig.turn(0);
|
|
/*if (this.pickedMesh) {
|
|
this.referencePlane.setEnabled(false);
|
|
this.pickedMesh.showBoundingBox = false;
|
|
this.pickedMesh = null;
|
|
}*/
|
|
};
|
|
|
|
|
|
window.addEventListener('wheel', (evt) => {
|
|
if (this.canvas && evt.target != this.canvas) {
|
|
return;
|
|
}
|
|
switch (evt.buttons) {
|
|
case 0:
|
|
if (this.fowardBackWheel == false) {
|
|
this.fowardBackWheel = true;
|
|
const reset = wheelHandler.bind(this);
|
|
setTimeout(reset, 500);
|
|
}
|
|
if (Math.sign(evt.deltaY) != 0) {
|
|
this.rig.forwardback(evt.deltaY / 100);
|
|
}
|
|
break;
|
|
case 1:
|
|
if (this.upDownWheel == false) {
|
|
this.upDownWheel = true;
|
|
const reset = wheelHandler.bind(this);
|
|
setTimeout(reset, 500);
|
|
}
|
|
if (Math.sign(evt.deltaY) != 0) {
|
|
this.rig.updown(evt.deltaY / 100);
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
});
|
|
this.scene.onPointerDown = (evt) => {
|
|
if (evt.pointerType == "mouse") {
|
|
this.mouseDown = true;
|
|
/*if (evt.shiftKey) {
|
|
//setMenuPosition(this.referencePlane, this.scene, new Vector3(0, 0, 5));
|
|
//this.referencePlane.rotation = scene.activeCamera.absoluteRotation.toEulerAngles();
|
|
this.pickedMesh = state.pickedMesh;
|
|
if (this.pickedMesh) {
|
|
this.referencePlane.position = this.pickedMesh.position;
|
|
this.referencePlane.rotation = scene.activeCamera.absoluteRotation.toEulerAngles();
|
|
}
|
|
this.pickedMesh.rotation = scene.activeCamera.absoluteRotation.toEulerAngles();
|
|
this.referencePlane.setEnabled(true);
|
|
} else {
|
|
|
|
|
|
if (state.pickedMesh) {
|
|
|
|
new ClickMenu(state.pickedMesh, state.gripTransform, this.diagramManager.onDiagramEventObservable);
|
|
}
|
|
|
|
|
|
} */
|
|
}
|
|
};
|
|
this.scene.onPointerMove = (evt) => {
|
|
if (evt.pointerType != "mouse") {
|
|
return;
|
|
}
|
|
if (this.mouseDown) {
|
|
this.rig.turn(evt.movementX);
|
|
}
|
|
/*const meshPickInfo = scene.pick(this.scene.pointerX, this.scene.pointerY, (mesh) => {
|
|
return isDiagramEntity(mesh);
|
|
});
|
|
const planePickInfo = scene.pick(this.scene.pointerX, this.scene.pointerY, (mesh) => {
|
|
return mesh.id == this.referencePlane.id;
|
|
});
|
|
|
|
if (meshPickInfo.hit) {
|
|
if (!this._mesh) {
|
|
this.mesh = meshPickInfo.pickedMesh;
|
|
} else {
|
|
if (this._mesh.id != meshPickInfo.pickedMesh.id) {
|
|
this.mesh = meshPickInfo.pickedMesh;
|
|
}
|
|
}
|
|
} else {
|
|
if (this.mesh) {
|
|
this.diagramManager.onDiagramEventObservable.notifyObservers({
|
|
type: DiagramEventType.MODIFY,
|
|
entity: toDiagramEntity(this.mesh)
|
|
}, DiagramEventObserverMask.ALL);
|
|
}
|
|
this.mesh = null;
|
|
}
|
|
if (this.pickedMesh && planePickInfo.hit) {
|
|
this.pickedMesh.position = planePickInfo.pickedPoint;
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
|
|
_mesh: AbstractMesh;
|
|
|
|
get mesh(): AbstractMesh {
|
|
return this._mesh;
|
|
}
|
|
|
|
set mesh(mesh: AbstractMesh) {
|
|
if (mesh) {
|
|
mesh.showBoundingBox = true;
|
|
mesh.outlineWidth = 0.08;
|
|
} else {
|
|
if (this._mesh) {
|
|
this._mesh.showBoundingBox = false;
|
|
}
|
|
}
|
|
this._mesh = mesh;
|
|
}
|
|
|
|
/**
|
|
* Create test entities for testing ResizeGizmo
|
|
* Creates a sphere at (-0.25, 1.5, 4) and a box at (0.25, 1.5, 4)
|
|
*/
|
|
private createTestEntities(): void {
|
|
this.logger.info('Creating test entities (Ctrl+Shift+T)');
|
|
|
|
// Get first color from toolbox colors array
|
|
const firstColor = getToolboxColors()[0];
|
|
const colorHex = firstColor.replace('#', '');
|
|
|
|
// Create sphere
|
|
this.diagramManager.onDiagramEventObservable.notifyObservers({
|
|
type: DiagramEventType.ADD,
|
|
entity: {
|
|
id: `test-sphere-${colorHex}`,
|
|
type: DiagramEntityType.ENTITY,
|
|
template: DiagramTemplates.SPHERE,
|
|
position: { x: -0.25, y: 1.5, z: 4 },
|
|
scale: { x: 0.1, y: 0.1, z: 0.1 },
|
|
color: firstColor
|
|
}
|
|
}, DiagramEventObserverMask.ALL);
|
|
|
|
// Create box
|
|
this.diagramManager.onDiagramEventObservable.notifyObservers({
|
|
type: DiagramEventType.ADD,
|
|
entity: {
|
|
id: `test-box-${colorHex}`,
|
|
type: DiagramEntityType.ENTITY,
|
|
template: DiagramTemplates.BOX,
|
|
position: { x: 0.25, y: 1.5, z: 4 },
|
|
scale: { x: 0.1, y: 0.1, z: 0.1 },
|
|
color: firstColor
|
|
}
|
|
}, DiagramEventObserverMask.ALL);
|
|
|
|
this.logger.info(`Test entities created with color ${firstColor}: test-sphere-${colorHex} at (-0.25, 1.5, 4) and test-box-${colorHex} at (0.25, 1.5, 4)`);
|
|
}
|
|
|
|
/**
|
|
* Clear all entities from the diagram
|
|
*/
|
|
private clearAllEntities(): void {
|
|
this.logger.info('Clearing all entities from diagram (Ctrl+Shift+X)');
|
|
|
|
this.diagramManager.onDiagramEventObservable.notifyObservers({
|
|
type: DiagramEventType.CLEAR
|
|
}, DiagramEventObserverMask.TO_DB);
|
|
|
|
this.logger.info('All entities cleared from diagram');
|
|
}
|
|
} |