immersive2/src/gizmos/ResizeGizmo/ResizeGizmoConfig.ts
Michael Mainguy 26b48b26c8 Implement WebXR resize gizmo with virtual stick scaling and extract adapter to integration layer
- Implement comprehensive WebXR resize gizmo system with three handle types:
  - Corner handles: uniform scaling (all axes)
  - Edge handles: two-axis planar scaling
  - Face handles: single-axis scaling
- Use "virtual stick" metaphor for intuitive scaling:
  - Fixed-length projection from controller to handle intersection
  - Distance-ratio based scaling from mesh pivot point
  - Works naturally with controller rotation and movement
- Add world-space coordinate transformations for VR rig parenting
- Implement manual ray picking for utility layer handle detection
- Add motion controller initialization handling for grip button
- Fix color persistence bug in diagram entities:
  - DiagramEntityAdapter now uses toDiagramEntity() converter
  - Store color in mesh metadata for persistence
  - Add dependency injection for loose coupling
- Extract DiagramEntityAdapter to integration layer:
  - Move from src/gizmos/ResizeGizmo/ to src/integration/gizmo/
  - Add dependency injection for mesh-to-entity converter
  - Keep ResizeGizmo pure and reusable without diagram dependencies
- Add closest color matching for missing toolbox colors
- Handle size now relative to bounding box (20% of avg dimension)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 17:52:23 -06:00

167 lines
4.8 KiB
TypeScript

/**
* WebXR Resize Gizmo - Configuration Management
*/
import { Vector3 } from "@babylonjs/core";
import { ResizeGizmoConfig, DEFAULT_RESIZE_GIZMO_CONFIG, ResizeGizmoMode } from "./types";
/**
* Helper class for managing and validating ResizeGizmo configuration
*/
export class ResizeGizmoConfigManager {
private _config: ResizeGizmoConfig;
constructor(userConfig?: Partial<ResizeGizmoConfig>) {
this._config = this.mergeWithDefaults(userConfig);
this.validate();
}
/**
* Merge user config with defaults
*/
private mergeWithDefaults(userConfig?: Partial<ResizeGizmoConfig>): ResizeGizmoConfig {
if (!userConfig) {
return { ...DEFAULT_RESIZE_GIZMO_CONFIG };
}
return {
...DEFAULT_RESIZE_GIZMO_CONFIG,
...userConfig,
// Ensure Vector3 objects are properly cloned
minScale: userConfig.minScale
? userConfig.minScale.clone()
: DEFAULT_RESIZE_GIZMO_CONFIG.minScale.clone(),
maxScale: userConfig.maxScale
? userConfig.maxScale.clone()
: DEFAULT_RESIZE_GIZMO_CONFIG.maxScale?.clone()
};
}
/**
* Validate configuration values
*/
private validate(): void {
const c = this._config;
// Validate handle size
if (c.handleSize <= 0) {
console.warn(`[ResizeGizmo] Invalid handleSize (${c.handleSize}), using default`);
c.handleSize = DEFAULT_RESIZE_GIZMO_CONFIG.handleSize;
}
// Validate bounding box padding
if (c.boundingBoxPadding < 0) {
console.warn(`[ResizeGizmo] Invalid boundingBoxPadding (${c.boundingBoxPadding}), using 0`);
c.boundingBoxPadding = 0;
}
// Validate wireframe alpha
c.wireframeAlpha = Math.max(0, Math.min(1, c.wireframeAlpha));
// Validate snap distances
if (c.snapDistanceX <= 0) c.snapDistanceX = 0.1;
if (c.snapDistanceY <= 0) c.snapDistanceY = 0.1;
if (c.snapDistanceZ <= 0) c.snapDistanceZ = 0.1;
// Validate min scale
if (c.minScale.x <= 0) c.minScale.x = 0.01;
if (c.minScale.y <= 0) c.minScale.y = 0.01;
if (c.minScale.z <= 0) c.minScale.z = 0.01;
// Validate max scale (if set)
if (c.maxScale) {
if (c.maxScale.x < c.minScale.x) c.maxScale.x = c.minScale.x;
if (c.maxScale.y < c.minScale.y) c.maxScale.y = c.minScale.y;
if (c.maxScale.z < c.minScale.z) c.maxScale.z = c.minScale.z;
}
// Validate DiagramEntity integration
if (c.useDiagramEntity && !c.diagramManager) {
console.warn("[ResizeGizmo] useDiagramEntity is true but diagramManager is not provided");
}
// Validate hover scale factor
if (c.hoverScaleFactor <= 0) {
c.hoverScaleFactor = DEFAULT_RESIZE_GIZMO_CONFIG.hoverScaleFactor;
}
}
/**
* Get current configuration (readonly)
*/
get current(): Readonly<ResizeGizmoConfig> {
return this._config;
}
/**
* Update configuration (partial update)
*/
update(updates: Partial<ResizeGizmoConfig>): void {
this._config = this.mergeWithDefaults({
...this._config,
...updates
});
this.validate();
}
/**
* Set mode
*/
setMode(mode: ResizeGizmoMode): void {
this._config.mode = mode;
}
/**
* Get snap distance for axis
*/
getSnapDistance(axis: "X" | "Y" | "Z"): number {
switch (axis) {
case "X": return this._config.snapDistanceX;
case "Y": return this._config.snapDistanceY;
case "Z": return this._config.snapDistanceZ;
}
}
/**
* Get snap vector
*/
getSnapVector(): Vector3 {
return new Vector3(
this._config.snapDistanceX,
this._config.snapDistanceY,
this._config.snapDistanceZ
);
}
/**
* Check if a mode uses corner handles
*/
usesCornerHandles(): boolean {
const mode = this._config.mode;
return mode === ResizeGizmoMode.UNIFORM || mode === ResizeGizmoMode.ALL;
}
/**
* Check if a mode uses edge handles
*/
usesEdgeHandles(): boolean {
const mode = this._config.mode;
return mode === ResizeGizmoMode.TWO_AXIS || mode === ResizeGizmoMode.ALL;
}
/**
* Check if a mode uses face handles
*/
usesFaceHandles(): boolean {
const mode = this._config.mode;
return mode === ResizeGizmoMode.SINGLE_AXIS || mode === ResizeGizmoMode.ALL;
}
/**
* Clone configuration
*/
clone(): ResizeGizmoConfigManager {
return new ResizeGizmoConfigManager(this._config);
}
}