Improve ResizeGizmo hover state and handle interaction
Phase 1 & 2: Handle positioning and wireframe improvements - Move handles 5% outward from bounding box (was inward) - Rename boundingBoxPadding → handleOffset for clarity - Add wireframePadding (3% breathing room around mesh) Hover boundary detection (prevent loss in whitespace): - Add isPointerInsideHandleBoundary() with ray-AABB intersection - Use local space transformation for accurate OBB handling - Keep HOVER_MESH state when pointer in handle boundary - Fix: Trust ResizeGizmo state instead of recreating with fake rays Prevent main scene mesh grab during handle interaction: - Add ResizeGizmo state check in pointer observable - Add defense-in-depth guard in grab() method - Prevents controller from grabbing diagram mesh when hovering handle - Two-level protection against race conditions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
43100ad650
commit
c815db4594
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "immersive",
|
"name": "immersive",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.8-23",
|
"version": "0.0.8-24",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@ -73,6 +73,13 @@ export abstract class AbstractController {
|
|||||||
this._meshUnderPointer = null;
|
this._meshUnderPointer = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't set _meshUnderPointer if ResizeGizmo is active
|
||||||
|
// Prevents main scene mesh grab from conflicting with handle interaction
|
||||||
|
if (resizeGizmo.isHoveringHandle() || resizeGizmo.isScaling()) {
|
||||||
|
this._meshUnderPointer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._pickPoint.copyFrom(pointerInfo.pickInfo.pickedPoint);
|
this._pickPoint.copyFrom(pointerInfo.pickInfo.pickedPoint);
|
||||||
@ -289,6 +296,15 @@ export abstract class AbstractController {
|
|||||||
if (viewOnly() || this._meshUnderPointer == null) {
|
if (viewOnly() || this._meshUnderPointer == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defense in depth: Verify ResizeGizmo isn't active
|
||||||
|
// Prevents race conditions where grip press happens during state transitions
|
||||||
|
const resizeGizmo = this.diagramManager?.diagramMenuManager?.resizeGizmo;
|
||||||
|
if (resizeGizmo && (resizeGizmo.isHoveringHandle() || resizeGizmo.isScaling())) {
|
||||||
|
this._logger.debug("ResizeGizmo is active, aborting grab");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
grabbedMesh,
|
grabbedMesh,
|
||||||
grabbedObject,
|
grabbedObject,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity";
|
import {DiagramEntityType, DiagramEvent, DiagramEventType} from "./types/diagramEntity";
|
||||||
import {AbstractMesh, ActionEvent, Observable, Scene, Vector3, WebXRDefaultExperience, WebXRInputSource} from "@babylonjs/core";
|
import {AbstractMesh, ActionEvent, Observable, Ray, Scene, Vector3, WebXRDefaultExperience, WebXRInputSource} from "@babylonjs/core";
|
||||||
import {InputTextView} from "../information/inputTextView";
|
import {InputTextView} from "../information/inputTextView";
|
||||||
import {DefaultScene} from "../defaultScene";
|
import {DefaultScene} from "../defaultScene";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
@ -232,39 +232,25 @@ export class DiagramMenuManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if gizmo should remain active based on pointer position
|
* Check if gizmo should remain active
|
||||||
|
* Trusts ResizeGizmo's internal state management rather than recalculating
|
||||||
*/
|
*/
|
||||||
private shouldKeepGizmoActive(pointerPosition?: Vector3): boolean {
|
private shouldKeepGizmoActive(pointerPosition?: Vector3): boolean {
|
||||||
if (!this._currentHoveredMesh) {
|
if (!this._currentHoveredMesh) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always keep gizmo active if currently scaling
|
// Trust ResizeGizmo's internal state management
|
||||||
if (this.resizeGizmo.isScaling()) {
|
// ResizeGizmo already tracks hover state correctly with proper controller rays
|
||||||
return true;
|
const state = this.resizeGizmo.getInteractionState();
|
||||||
}
|
|
||||||
|
|
||||||
// Keep active if pointer is within bounding box area
|
// Keep active if ResizeGizmo is in any active state:
|
||||||
if (!pointerPosition) {
|
// - ACTIVE_SCALING: User is actively scaling (grip held)
|
||||||
return false;
|
// - HOVER_HANDLE: Pointer is hovering a handle (ready to scale)
|
||||||
}
|
// - HOVER_MESH: Pointer is within handle boundary (grace zone)
|
||||||
|
return state === 'ACTIVE_SCALING' ||
|
||||||
// Get the attached mesh's bounding box
|
state === 'HOVER_HANDLE' ||
|
||||||
const boundingInfo = this._currentHoveredMesh.getBoundingInfo();
|
state === 'HOVER_MESH';
|
||||||
const boundingBox = boundingInfo.boundingBox;
|
|
||||||
|
|
||||||
// Add padding to the bounding box (same as gizmo padding + handle size)
|
|
||||||
const padding = 0.3; // Generous padding to include handles
|
|
||||||
const min = boundingBox.minimumWorld.subtract(new Vector3(padding, padding, padding));
|
|
||||||
const max = boundingBox.maximumWorld.add(new Vector3(padding, padding, padding));
|
|
||||||
|
|
||||||
// Check if pointer is within the padded bounding box
|
|
||||||
const withinBounds =
|
|
||||||
pointerPosition.x >= min.x && pointerPosition.x <= max.x &&
|
|
||||||
pointerPosition.y >= min.y && pointerPosition.y <= max.y &&
|
|
||||||
pointerPosition.z >= min.z && pointerPosition.z <= max.z;
|
|
||||||
|
|
||||||
return withinBounds;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -75,8 +75,8 @@ export class HandleGeometry {
|
|||||||
// Calculate normal from center to corner
|
// Calculate normal from center to corner
|
||||||
const normal = cornerPos.subtract(center).normalize();
|
const normal = cornerPos.subtract(center).normalize();
|
||||||
|
|
||||||
// Apply padding by moving corner inward along the normal
|
// Apply padding by moving corner outward along the normal
|
||||||
const position = cornerPos.subtract(normal.scale(paddingDistance));
|
const position = cornerPos.add(normal.scale(paddingDistance));
|
||||||
|
|
||||||
corners.push({
|
corners.push({
|
||||||
position,
|
position,
|
||||||
@ -139,8 +139,8 @@ export class HandleGeometry {
|
|||||||
// Calculate normal from center to midpoint
|
// Calculate normal from center to midpoint
|
||||||
const normal = midpoint.subtract(center).normalize();
|
const normal = midpoint.subtract(center).normalize();
|
||||||
|
|
||||||
// Apply padding by moving inward along the normal
|
// Apply padding by moving outward along the normal
|
||||||
const position = midpoint.subtract(normal.scale(paddingDistance));
|
const position = midpoint.add(normal.scale(paddingDistance));
|
||||||
|
|
||||||
edges.push({
|
edges.push({
|
||||||
position,
|
position,
|
||||||
@ -195,8 +195,8 @@ export class HandleGeometry {
|
|||||||
// Calculate normal from center to face center
|
// Calculate normal from center to face center
|
||||||
const normal = faceCenter.subtract(center).normalize();
|
const normal = faceCenter.subtract(center).normalize();
|
||||||
|
|
||||||
// Apply padding by moving inward along the normal
|
// Apply padding by moving outward along the normal
|
||||||
const position = faceCenter.subtract(normal.scale(paddingDistance));
|
const position = faceCenter.add(normal.scale(paddingDistance));
|
||||||
|
|
||||||
faces.push({
|
faces.push({
|
||||||
position,
|
position,
|
||||||
|
|||||||
@ -49,10 +49,16 @@ export class ResizeGizmoConfigManager {
|
|||||||
c.handleSize = DEFAULT_RESIZE_GIZMO_CONFIG.handleSize;
|
c.handleSize = DEFAULT_RESIZE_GIZMO_CONFIG.handleSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate bounding box padding
|
// Validate handle offset
|
||||||
if (c.boundingBoxPadding < 0) {
|
if (c.handleOffset < 0) {
|
||||||
console.warn(`[ResizeGizmo] Invalid boundingBoxPadding (${c.boundingBoxPadding}), using 0`);
|
console.warn(`[ResizeGizmo] Invalid handleOffset (${c.handleOffset}), using 0`);
|
||||||
c.boundingBoxPadding = 0;
|
c.handleOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate wireframe padding
|
||||||
|
if (c.wireframePadding < 0) {
|
||||||
|
console.warn(`[ResizeGizmo] Invalid wireframePadding (${c.wireframePadding}), using 0`);
|
||||||
|
c.wireframePadding = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate wireframe alpha
|
// Validate wireframe alpha
|
||||||
|
|||||||
@ -220,6 +220,30 @@ export class ResizeGizmoInteraction {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any XR controller pointer is inside the expanded handle boundary
|
||||||
|
* Used to prevent hover state loss when pointer crosses whitespace between mesh and handles
|
||||||
|
*/
|
||||||
|
private isPointerInsideHandleBoundary(): boolean {
|
||||||
|
// Iterate through registered XR controllers
|
||||||
|
for (const controller of this._xrControllers.values()) {
|
||||||
|
if (!controller.pointer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get controller ray in world space
|
||||||
|
const ray = new Ray(Vector3.Zero(), Vector3.Forward(), 1000);
|
||||||
|
controller.getWorldPointerRayToRef(ray);
|
||||||
|
|
||||||
|
// Check if this ray intersects the handle boundary
|
||||||
|
if (this._visuals.isPointerInsideHandleBoundary(ray)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle mesh hover
|
* Handle mesh hover
|
||||||
*/
|
*/
|
||||||
@ -373,7 +397,19 @@ export class ResizeGizmoInteraction {
|
|||||||
this.onHandleHovered(handlePickResult);
|
this.onHandleHovered(handlePickResult);
|
||||||
} else if (this._state.hoveredHandle) {
|
} else if (this._state.hoveredHandle) {
|
||||||
// Was hovering a handle, but not anymore
|
// Was hovering a handle, but not anymore
|
||||||
this.onHoverExit();
|
// Check if still inside handle boundary before exiting hover (prevents loss in whitespace)
|
||||||
|
const stillInsideBoundary = this.isPointerInsideHandleBoundary();
|
||||||
|
|
||||||
|
if (stillInsideBoundary) {
|
||||||
|
// Keep gizmo active but unhighlight the specific handle
|
||||||
|
this._visuals.unhighlightHandle(this._state.hoveredHandle.id);
|
||||||
|
this._state.hoveredHandle = undefined;
|
||||||
|
// Keep state as HOVER_MESH (don't drop to IDLE)
|
||||||
|
this._state.state = InteractionState.HOVER_MESH;
|
||||||
|
} else {
|
||||||
|
// Pointer left the boundary entirely, exit hover completely
|
||||||
|
this.onHoverExit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -515,6 +551,13 @@ export class ResizeGizmoInteraction {
|
|||||||
return this._state.state === InteractionState.HOVER_HANDLE && this._state.hoveredHandle != null;
|
return this._state.state === InteractionState.HOVER_HANDLE && this._state.hoveredHandle != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current interaction state (for external integration)
|
||||||
|
*/
|
||||||
|
getState(): Readonly<GizmoInteractionState> {
|
||||||
|
return this._state;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispose
|
* Dispose
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -7,7 +7,8 @@ import {
|
|||||||
Scene,
|
Scene,
|
||||||
AbstractMesh,
|
AbstractMesh,
|
||||||
Observable,
|
Observable,
|
||||||
WebXRInputSource
|
WebXRInputSource,
|
||||||
|
Ray
|
||||||
} from "@babylonjs/core";
|
} from "@babylonjs/core";
|
||||||
import {
|
import {
|
||||||
ResizeGizmoMode,
|
ResizeGizmoMode,
|
||||||
@ -258,6 +259,21 @@ export class ResizeGizmoManager {
|
|||||||
return this._interaction.isHoveringHandle();
|
return this._interaction.isHoveringHandle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current interaction state (for external integration)
|
||||||
|
*/
|
||||||
|
getInteractionState(): string {
|
||||||
|
return this._interaction.getState().state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if pointer ray is inside handle boundary (for external integration)
|
||||||
|
* This is used by DiagramMenuManager to determine if gizmo should stay active
|
||||||
|
*/
|
||||||
|
isPointerInsideHandleBoundary(ray: Ray): boolean {
|
||||||
|
return this._visuals.isPointerInsideHandleBoundary(ray);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the utility layer scene (for filtering picks in main scene)
|
* Get the utility layer scene (for filtering picks in main scene)
|
||||||
* This is used to prevent pointer events on gizmo handles from leaking to main scene
|
* This is used to prevent pointer events on gizmo handles from leaking to main scene
|
||||||
|
|||||||
@ -13,7 +13,9 @@ import {
|
|||||||
UtilityLayerRenderer,
|
UtilityLayerRenderer,
|
||||||
LinesMesh,
|
LinesMesh,
|
||||||
Vector3,
|
Vector3,
|
||||||
Quaternion
|
Quaternion,
|
||||||
|
Ray,
|
||||||
|
BoundingBox
|
||||||
} from "@babylonjs/core";
|
} from "@babylonjs/core";
|
||||||
import { HandlePosition, HandleType } from "./types";
|
import { HandlePosition, HandleType } from "./types";
|
||||||
import { ResizeGizmoConfigManager } from "./ResizeGizmoConfig";
|
import { ResizeGizmoConfigManager } from "./ResizeGizmoConfig";
|
||||||
@ -105,7 +107,7 @@ export class ResizeGizmoVisuals {
|
|||||||
// Generate handles based on mode (using OBB)
|
// Generate handles based on mode (using OBB)
|
||||||
return HandleGeometry.generateHandles(
|
return HandleGeometry.generateHandles(
|
||||||
this._targetMesh,
|
this._targetMesh,
|
||||||
this._config.current.boundingBoxPadding,
|
this._config.current.handleOffset,
|
||||||
this._config.usesCornerHandles(),
|
this._config.usesCornerHandles(),
|
||||||
this._config.usesEdgeHandles(),
|
this._config.usesEdgeHandles(),
|
||||||
this._config.usesFaceHandles()
|
this._config.usesFaceHandles()
|
||||||
@ -114,8 +116,9 @@ export class ResizeGizmoVisuals {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the 8 corners of the oriented bounding box (OBB) in world space
|
* Calculate the 8 corners of the oriented bounding box (OBB) in world space
|
||||||
|
* @param paddingFactor Optional padding factor to expand corners outward (0.03 = 3%)
|
||||||
*/
|
*/
|
||||||
private calculateOBBCorners(): Vector3[] {
|
private calculateOBBCorners(paddingFactor: number = 0): Vector3[] {
|
||||||
if (!this._targetMesh) {
|
if (!this._targetMesh) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -144,6 +147,19 @@ export class ResizeGizmoVisuals {
|
|||||||
Vector3.TransformCoordinates(corner, worldMatrix)
|
Vector3.TransformCoordinates(corner, worldMatrix)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Apply padding if specified (expand outward from center)
|
||||||
|
if (paddingFactor > 0) {
|
||||||
|
const center = this._targetMesh.absolutePosition;
|
||||||
|
const size = boundingBox.extendSize;
|
||||||
|
const avgSize = (size.x + size.y + size.z) / 3;
|
||||||
|
const paddingDistance = avgSize * paddingFactor;
|
||||||
|
|
||||||
|
return worldCorners.map(corner => {
|
||||||
|
const normal = corner.subtract(center).normalize();
|
||||||
|
return corner.add(normal.scale(paddingDistance));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return worldCorners;
|
return worldCorners;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,8 +173,8 @@ export class ResizeGizmoVisuals {
|
|||||||
|
|
||||||
this.disposeBoundingBox();
|
this.disposeBoundingBox();
|
||||||
|
|
||||||
// Get OBB corners in world space
|
// Get OBB corners in world space with wireframe padding
|
||||||
const corners = this.calculateOBBCorners();
|
const corners = this.calculateOBBCorners(this._config.current.wireframePadding);
|
||||||
if (corners.length !== 8) {
|
if (corners.length !== 8) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -406,6 +422,73 @@ export class ResizeGizmoVisuals {
|
|||||||
return this._utilityLayer.utilityLayerScene;
|
return this._utilityLayer.utilityLayerScene;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a ray intersects the expanded bounding volume that encompasses all handles
|
||||||
|
* This creates a "grace zone" to prevent hover state loss in whitespace between mesh and handles
|
||||||
|
*
|
||||||
|
* Uses local space transformation for accuracy - transforms ray to mesh local space
|
||||||
|
* and performs AABB intersection test with manual slab method
|
||||||
|
*/
|
||||||
|
isPointerInsideHandleBoundary(ray: Ray): boolean {
|
||||||
|
if (!this._targetMesh || !this._config.current.keepHoverInHandleBoundary) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform ray from world space to mesh local space
|
||||||
|
const worldMatrix = this._targetMesh.computeWorldMatrix(true);
|
||||||
|
const invWorldMatrix = worldMatrix.clone().invert();
|
||||||
|
|
||||||
|
const localOrigin = Vector3.TransformCoordinates(ray.origin, invWorldMatrix);
|
||||||
|
const localDirection = Vector3.TransformNormal(ray.direction, invWorldMatrix);
|
||||||
|
|
||||||
|
// Get local space bounding box
|
||||||
|
const boundingInfo = this._targetMesh.getBoundingInfo();
|
||||||
|
const boundingBox = boundingInfo.boundingBox;
|
||||||
|
const size = boundingBox.extendSize;
|
||||||
|
const avgSize = (size.x + size.y + size.z) / 3;
|
||||||
|
|
||||||
|
// Calculate expanded padding (handleOffset is a fraction, need to scale by avgSize)
|
||||||
|
const handleSize = avgSize * this._config.current.handleSize;
|
||||||
|
const paddingDistance = avgSize * this._config.current.handleOffset;
|
||||||
|
const totalPadding = paddingDistance + (handleSize / 2);
|
||||||
|
|
||||||
|
// Create expanded AABB in local space
|
||||||
|
const paddingVec = new Vector3(totalPadding, totalPadding, totalPadding);
|
||||||
|
const min = boundingBox.minimum.subtract(paddingVec);
|
||||||
|
const max = boundingBox.maximum.add(paddingVec);
|
||||||
|
|
||||||
|
// Ray-AABB intersection test using slab method
|
||||||
|
// https://tavianator.com/2011/ray_box.html
|
||||||
|
const invDir = new Vector3(
|
||||||
|
1 / localDirection.x,
|
||||||
|
1 / localDirection.y,
|
||||||
|
1 / localDirection.z
|
||||||
|
);
|
||||||
|
|
||||||
|
const t1 = (min.x - localOrigin.x) * invDir.x;
|
||||||
|
const t2 = (max.x - localOrigin.x) * invDir.x;
|
||||||
|
const t3 = (min.y - localOrigin.y) * invDir.y;
|
||||||
|
const t4 = (max.y - localOrigin.y) * invDir.y;
|
||||||
|
const t5 = (min.z - localOrigin.z) * invDir.z;
|
||||||
|
const t6 = (max.z - localOrigin.z) * invDir.z;
|
||||||
|
|
||||||
|
const tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6));
|
||||||
|
const tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6));
|
||||||
|
|
||||||
|
// If tmax < 0, ray is intersecting AABB but the box is behind the ray
|
||||||
|
if (tmax < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If tmin > tmax, ray doesn't intersect AABB
|
||||||
|
if (tmin > tmax) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ray intersects the expanded bounding box
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispose all resources
|
* Dispose all resources
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -147,8 +147,11 @@ export interface ResizeGizmoConfig {
|
|||||||
hoverScaleFactor: number;
|
hoverScaleFactor: number;
|
||||||
|
|
||||||
// === Bounding Box ===
|
// === Bounding Box ===
|
||||||
/** Padding around mesh bounding box (0.05 = 5% padding) */
|
/** Handle offset from bounding box surface (0.05 = 5% outward) */
|
||||||
boundingBoxPadding: number;
|
handleOffset: number;
|
||||||
|
|
||||||
|
/** Padding for bounding box wireframe (0.03 = 3% outward breathing room) */
|
||||||
|
wireframePadding: number;
|
||||||
|
|
||||||
/** Bounding box wireframe color */
|
/** Bounding box wireframe color */
|
||||||
boundingBoxColor: Color3;
|
boundingBoxColor: Color3;
|
||||||
@ -159,6 +162,9 @@ export interface ResizeGizmoConfig {
|
|||||||
/** Show bounding box only on hover */
|
/** Show bounding box only on hover */
|
||||||
showBoundingBoxOnHoverOnly: boolean;
|
showBoundingBoxOnHoverOnly: boolean;
|
||||||
|
|
||||||
|
/** Keep hover state when pointer is within handle boundary (prevents loss in whitespace) */
|
||||||
|
keepHoverInHandleBoundary: boolean;
|
||||||
|
|
||||||
// === Snapping ===
|
// === Snapping ===
|
||||||
/** Enable snap-to-grid during scaling */
|
/** Enable snap-to-grid during scaling */
|
||||||
enableSnapping: boolean;
|
enableSnapping: boolean;
|
||||||
@ -232,10 +238,12 @@ export const DEFAULT_RESIZE_GIZMO_CONFIG: ResizeGizmoConfig = {
|
|||||||
hoverScaleFactor: 1.3,
|
hoverScaleFactor: 1.3,
|
||||||
|
|
||||||
// Bounding box
|
// Bounding box
|
||||||
boundingBoxPadding: 0.05,
|
handleOffset: 0.05,
|
||||||
|
wireframePadding: 0.03,
|
||||||
boundingBoxColor: new Color3(1.0, 1.0, 1.0), // White
|
boundingBoxColor: new Color3(1.0, 1.0, 1.0), // White
|
||||||
wireframeAlpha: 0.3,
|
wireframeAlpha: 0.3,
|
||||||
showBoundingBoxOnHoverOnly: false,
|
showBoundingBoxOnHoverOnly: false,
|
||||||
|
keepHoverInHandleBoundary: true,
|
||||||
|
|
||||||
// Snapping
|
// Snapping
|
||||||
enableSnapping: true,
|
enableSnapping: true,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user