UNPKG

@babylonjs/gui

Version:

Babylon.js GUI module =====================

353 lines 17.2 kB
import { Gizmo } from "@babylonjs/core/Gizmos/gizmo.js"; import { Epsilon } from "@babylonjs/core/Maths/math.constants.js"; import { Matrix, Quaternion, TmpVectors, Vector3 } from "@babylonjs/core/Maths/math.vector.js"; import { TransformNode } from "@babylonjs/core/Meshes/transformNode.js"; import { PivotTools } from "@babylonjs/core/Misc/pivotTools.js"; import { CornerHandle, SideHandle } from "./gizmoHandle.js"; /** * Gizmo to resize 2D slates */ export class SlateGizmo extends Gizmo { /** * The slate attached to this gizmo */ set attachedSlate(control) { if (control) { this.attachedMesh = control.mesh; this.updateBoundingBox(); this._pickedPointObserver = control._host.onPickingObservable.add((pickedMesh) => { if (this._handleHovered && (!pickedMesh || pickedMesh.parent !== this._handleHovered.node)) { this._handleHovered.hover = false; this._handleHovered = null; } if (pickedMesh && pickedMesh.parent && pickedMesh.parent.reservedDataStore && pickedMesh.parent.reservedDataStore.handle) { const handle = pickedMesh.parent.reservedDataStore.handle; if (handle.gizmo === this) { this._handleHovered = handle; this._handleHovered.hover = true; } } }); } else if (this._attachedSlate) { this._attachedSlate._host.onPickingObservable.remove(this._pickedPointObserver); } this._attachedSlate = control; } get attachedSlate() { return this._attachedSlate; } constructor(utilityLayer) { super(utilityLayer); this._boundingDimensions = new Vector3(0, 0, 0); this._renderObserver = null; this._tmpQuaternion = new Quaternion(); this._tmpVector = new Vector3(0, 0, 0); // Ordered bl, br, tr, tl this._corners = []; // Ordered left, bottom, right, top this._sides = []; this._boundingBoxGizmo = { min: new Vector3(), max: new Vector3(), }; /** * Value we use to offset handles from mesh */ this._margin = 0.35; this._handleSize = 0.075; this._attachedSlate = null; this._existingSlateScale = new Vector3(); /** * If set, the handles will increase in size based on the distance away from the camera to have a consistent screen size (Default: true) */ this.fixedScreenSize = false; /** * The distance away from the object which the draggable meshes should appear world sized when fixedScreenSize is set to true (default: 10) */ this.fixedScreenSizeDistanceFactor = 10; this._createNode(); this.updateScale = false; this._renderObserver = this.gizmoLayer.originalScene.onBeforeRenderObservable.add(() => { // Only update the bounding box if scaling has changed if (this.attachedMesh && !this._existingSlateScale.equals(this.attachedMesh.scaling)) { this.updateBoundingBox(); } }); } _createNode() { this._handlesParent = new TransformNode("handlesParent", this.gizmoLayer.utilityLayerScene); this._handlesParent.rotationQuaternion = Quaternion.Identity(); const masksCorners = [ { dimensions: new Vector3(-1, -1, 0), origin: new Vector3(1, 0, 0), }, { dimensions: new Vector3(1, -1, 0), origin: new Vector3(0, 0, 0), }, { dimensions: new Vector3(1, 1, 0), origin: new Vector3(0, 1, 0), }, { dimensions: new Vector3(-1, 1, 0), origin: new Vector3(1, 1, 0), }, ]; for (let i = 0; i < 4; i++) { const corner = new CornerHandle(this, this.gizmoLayer.utilityLayerScene); this._corners.push(corner); corner.node.rotation.z = (Math.PI / 2) * i; corner.node.parent = this._handlesParent; this._assignDragBehaviorCorners(corner, (originStart, dimensionsStart, offset, masks) => this._moveHandle(originStart, dimensionsStart, offset, masks, true), masksCorners[i]); } for (let i = 0; i < 4; i++) { const side = new SideHandle(this, this.gizmoLayer.utilityLayerScene); this._sides.push(side); side.node.rotation.z = (Math.PI / 2) * i; side.node.parent = this._handlesParent; this._assignDragBehaviorSides(side, i % 2 === 0 ? new Vector3(0, 1, 0) : new Vector3(1, 0, 0)); } this._handlesParent.parent = this._rootMesh; } _keepAspectRatio(vector, aspectRatio, invertDiagonal = false) { const axis = TmpVectors.Vector3[0]; axis.copyFromFloats(aspectRatio, 1, 0).normalize(); if (invertDiagonal) { axis.y *= -1; } const dot = Vector3.Dot(vector, axis); vector.copyFrom(axis).scaleInPlace(dot); } _clampDimensions(vector, dimensions, mask, keepAspectRatio = false) { const impact = TmpVectors.Vector3[0]; impact.copyFrom(vector).multiplyInPlace(mask); const clampedDimensions = TmpVectors.Vector3[1]; clampedDimensions.copyFromFloats(Math.max(this._attachedSlate.minDimensions.x, impact.x + dimensions.x), Math.max(this._attachedSlate.minDimensions.y, impact.y + dimensions.y), 0); if (keepAspectRatio) { // Extra logic to ensure the ratio is maintained when the vector has been clamped const ratio = dimensions.x / dimensions.y; clampedDimensions.x = Math.max(clampedDimensions.x, clampedDimensions.y * ratio); clampedDimensions.y = Math.max(clampedDimensions.y, clampedDimensions.x / ratio); } // Calculating the real impact of vector on clamped dimensions impact.copyFrom(clampedDimensions).subtractInPlace(dimensions); vector.x = Math.sign(vector.x) * Math.abs(impact.x); vector.y = Math.sign(vector.y) * Math.abs(impact.y); } _moveHandle(originStart, dimensionsStart, offset, masks, isCorner) { if (!this._attachedSlate) { return; } if (isCorner) { const aspectRatio = dimensionsStart.x / dimensionsStart.y; this._keepAspectRatio(offset, aspectRatio, masks.dimensions.x * masks.dimensions.y < 0); } this._clampDimensions(offset, dimensionsStart, masks.dimensions, isCorner); const offsetOriginMasked = TmpVectors.Vector3[0]; const offsetDimensionsMasked = TmpVectors.Vector3[1]; offsetOriginMasked.copyFrom(offset).multiplyInPlace(masks.origin); offsetDimensionsMasked.copyFrom(offset).multiplyInPlace(masks.dimensions); this._attachedSlate.origin.copyFrom(originStart).addInPlace(offsetOriginMasked); this._attachedSlate.dimensions.set(dimensionsStart.x + offsetDimensionsMasked.x, dimensionsStart.y + offsetDimensionsMasked.y); } _assignDragBehaviorCorners(handle, moveFn, masks) { const dimensionsStart = new Vector3(); const originStart = new Vector3(); const dragOrigin = new Vector3(); const toObjectFrame = new Matrix(); const dragPlaneNormal = new Vector3(); const projectToRef = (position, normal, origin, ref) => { // Projects on the plane with its normal and origin position.subtractToRef(origin, TmpVectors.Vector3[0]); const dot = Vector3.Dot(TmpVectors.Vector3[0], normal); TmpVectors.Vector3[1].copyFrom(normal).scaleInPlace(dot); TmpVectors.Vector3[0].subtractInPlace(TmpVectors.Vector3[1]); TmpVectors.Vector3[0].addToRef(origin, ref); }; const dragStart = (event) => { if (this.attachedSlate && this.attachedMesh) { dimensionsStart.set(this.attachedSlate.dimensions.x, this.attachedSlate.dimensions.y, Epsilon); originStart.copyFrom(this.attachedSlate.origin); dragOrigin.copyFrom(event.position); toObjectFrame.copyFrom(this.attachedMesh.computeWorldMatrix(true)); toObjectFrame.invert(); this.attachedSlate._followButton.isToggled = false; Vector3.TransformNormalToRef(Vector3.Forward(), this.attachedMesh.getWorldMatrix(), dragPlaneNormal); dragPlaneNormal.normalize(); if (this._handleHovered) { this._handleDragged = this._handleHovered; this._handleDragged.drag = true; } } }; const dragging = (event) => { if (this.attachedSlate && this.attachedMesh) { projectToRef(event.position, dragPlaneNormal, dragOrigin, this._tmpVector); this._tmpVector.subtractInPlace(dragOrigin); Vector3.TransformNormalToRef(this._tmpVector, toObjectFrame, this._tmpVector); moveFn(originStart, dimensionsStart, this._tmpVector, masks); this.attachedSlate._positionElements(); this.updateBoundingBox(); } }; const dragEnd = () => { if (this.attachedSlate && this.attachedNode) { this.attachedSlate._updatePivot(); if (this._handleDragged) { this._handleDragged.drag = false; this._handleDragged = null; } } }; handle.setDragBehavior(dragStart, dragging, dragEnd); } _assignDragBehaviorSides(handle, dragPlaneNormal) { const quaternionOrigin = new Quaternion(); const dragOrigin = new Vector3(); const directionOrigin = new Vector3(); const worldPivot = new Vector3(); const worldPlaneNormal = new Vector3(); const dragStart = (event) => { if (this.attachedSlate && this.attachedMesh) { quaternionOrigin.copyFrom(this.attachedMesh.rotationQuaternion); dragOrigin.copyFrom(event.position); worldPivot.copyFrom(this.attachedMesh.getAbsolutePivotPoint()); directionOrigin.copyFrom(dragOrigin).subtractInPlace(worldPivot).normalize(); this.attachedSlate._followButton.isToggled = false; Vector3.TransformNormalToRef(dragPlaneNormal, this.attachedMesh.getWorldMatrix(), worldPlaneNormal); worldPlaneNormal.normalize(); if (this._handleHovered) { this._handleDragged = this._handleHovered; this._handleDragged.drag = true; } } }; const dragging = (event) => { if (this.attachedSlate && this.attachedMesh) { this._tmpVector.copyFrom(event.position); this._tmpVector.subtractInPlace(worldPivot); this._tmpVector.normalize(); const angle = -Vector3.GetAngleBetweenVectorsOnPlane(this._tmpVector, directionOrigin, worldPlaneNormal); Quaternion.RotationAxisToRef(dragPlaneNormal, angle, this._tmpQuaternion); quaternionOrigin.multiplyToRef(this._tmpQuaternion, this.attachedMesh.rotationQuaternion); } }; const dragEnd = () => { if (this.attachedSlate && this.attachedNode) { this.attachedSlate._updatePivot(); if (this._handleDragged) { this._handleDragged.drag = false; this._handleDragged = null; } } }; handle.setDragBehavior(dragStart, dragging, dragEnd); } _attachedNodeChanged(value) { if (value) { this.updateBoundingBox(); } } /** * Updates the bounding box information for the gizmo */ updateBoundingBox() { if (this.attachedMesh) { PivotTools._RemoveAndStorePivotPoint(this.attachedMesh); // Store original parent const originalParent = this.attachedMesh.parent; this.attachedMesh.setParent(null); this._update(); // Rotate based on axis if (!this.attachedMesh.rotationQuaternion) { this.attachedMesh.rotationQuaternion = Quaternion.RotationYawPitchRoll(this.attachedMesh.rotation.y, this.attachedMesh.rotation.x, this.attachedMesh.rotation.z); } // Store original position and reset mesh to origin before computing the bounding box this._tmpQuaternion.copyFrom(this.attachedMesh.rotationQuaternion); this._tmpVector.copyFrom(this.attachedMesh.position); this.attachedMesh.rotationQuaternion.set(0, 0, 0, 1); this.attachedMesh.position.set(0, 0, 0); // Update bounding dimensions/positions const boundingMinMax = this.attachedMesh.getHierarchyBoundingVectors(); boundingMinMax.max.subtractToRef(boundingMinMax.min, this._boundingDimensions); this._boundingBoxGizmo.min = boundingMinMax.min; this._boundingBoxGizmo.max = boundingMinMax.max; // Update handles of the gizmo this._updateHandlesPosition(); this._updateHandlesScaling(); // Restore position/rotation values this.attachedMesh.rotationQuaternion.copyFrom(this._tmpQuaternion); this.attachedMesh.position.copyFrom(this._tmpVector); PivotTools._RestorePivotPoint(this.attachedMesh); // Restore original parent this.attachedMesh.setParent(originalParent); this.attachedMesh.computeWorldMatrix(true); this._existingSlateScale.copyFrom(this.attachedMesh.scaling); } } _updateHandlesPosition() { const min = this._boundingBoxGizmo.min.clone(); const max = this._boundingBoxGizmo.max.clone(); const handleScaling = this._corners[0].node.scaling.length(); min.x -= this._margin * handleScaling; min.y -= this._margin * handleScaling; max.x += this._margin * handleScaling; max.y += this._margin * handleScaling; const center = min.add(max).scaleInPlace(0.5); this._corners[0].node.position.copyFromFloats(min.x, min.y, 0); this._corners[1].node.position.copyFromFloats(max.x, min.y, 0); this._corners[2].node.position.copyFromFloats(max.x, max.y, 0); this._corners[3].node.position.copyFromFloats(min.x, max.y, 0); this._sides[0].node.position.copyFromFloats(min.x, center.y, 0); this._sides[1].node.position.copyFromFloats(center.x, min.y, 0); this._sides[2].node.position.copyFromFloats(max.x, center.y, 0); this._sides[3].node.position.copyFromFloats(center.x, max.y, 0); } _updateHandlesScaling() { if (this._attachedSlate && this._attachedSlate.mesh) { const scaledWidth = this._attachedSlate.mesh.scaling.x * this._attachedSlate.dimensions.x; const scaledHeight = this._attachedSlate.mesh.scaling.y * this._attachedSlate.dimensions.y; const scale = Math.min(scaledWidth, scaledHeight) * this._handleSize; for (let index = 0; index < this._corners.length; index++) { this._corners[index].node.scaling.setAll(scale); } for (let index = 0; index < this._sides.length; index++) { this._sides[index].node.scaling.setAll(scale); } } } _update() { super._update(); if (!this.gizmoLayer.utilityLayerScene.activeCamera) { return; } if (this._attachedSlate && this._attachedSlate.mesh) { if (this.fixedScreenSize) { this._attachedSlate.mesh.absolutePosition.subtractToRef(this.gizmoLayer.utilityLayerScene.activeCamera.position, this._tmpVector); const distanceFromCamera = (this._handleSize * this._tmpVector.length()) / this.fixedScreenSizeDistanceFactor; for (let i = 0; i < this._corners.length; i++) { this._corners[i].node.scaling.set(distanceFromCamera, distanceFromCamera, distanceFromCamera); } for (let i = 0; i < this._sides.length; i++) { this._sides[i].node.scaling.set(distanceFromCamera, distanceFromCamera, distanceFromCamera); } } this._updateHandlesPosition(); } } dispose() { this.gizmoLayer.originalScene.onBeforeRenderObservable.remove(this._renderObserver); // Will dispose rootMesh and all descendants super.dispose(); for (const corner of this._corners) { corner.dispose(); } for (const side of this._sides) { side.dispose(); } } } //# sourceMappingURL=slateGizmo.js.map