UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

483 lines (482 loc) 13.1 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { Vec3 } from "../../core/math/vec3.js"; import { Quat } from "../../core/math/quat.js"; import { TransformGizmo } from "./transform-gizmo.js"; import { BoxShape } from "./shape/box-shape.js"; import { PlaneShape } from "./shape/plane-shape.js"; import { BoxLineShape } from "./shape/boxline-shape.js"; const v1 = new Vec3(); const v2 = new Vec3(); const point = new Vec3(); const delta = new Vec3(); const q = new Quat(); const GLANCE_EPSILON = 0.01; class ScaleGizmo extends TransformGizmo { /** * Creates a new ScaleGizmo object. Use {@link Gizmo.createLayer} to create the layer * required to display the gizmo. * * @param {CameraComponent} camera - The camera component. * @param {Layer} layer - The layer responsible for rendering the gizmo. * @example * const gizmo = new ScaleGizmo(camera, layer); */ constructor(camera, layer) { super(camera, layer, "gizmo:scale"); __publicField(this, "_shapes", { xyz: new BoxShape(this._device, { axis: "xyz", layers: [this._layer.id], defaultColor: this._theme.shapeBase.xyz, hoverColor: this._theme.shapeHover.xyz, disabledColor: this._theme.disabled }), yz: new PlaneShape(this._device, { axis: "x", layers: [this._layer.id], rotation: new Vec3(0, 0, -90), defaultColor: this._theme.shapeBase.x, hoverColor: this._theme.shapeHover.x, disabledColor: this._theme.disabled, depth: 1 }), xz: new PlaneShape(this._device, { axis: "y", layers: [this._layer.id], rotation: new Vec3(0, 0, 0), defaultColor: this._theme.shapeBase.y, hoverColor: this._theme.shapeHover.y, disabledColor: this._theme.disabled, depth: 1 }), xy: new PlaneShape(this._device, { axis: "z", layers: [this._layer.id], rotation: new Vec3(90, 0, 0), defaultColor: this._theme.shapeBase.z, hoverColor: this._theme.shapeHover.z, disabledColor: this._theme.disabled, depth: 1 }), x: new BoxLineShape(this._device, { axis: "x", layers: [this._layer.id], rotation: new Vec3(0, 0, -90), defaultColor: this._theme.shapeBase.x, hoverColor: this._theme.shapeHover.x, disabledColor: this._theme.disabled }), y: new BoxLineShape(this._device, { axis: "y", layers: [this._layer.id], rotation: new Vec3(0, 0, 0), defaultColor: this._theme.shapeBase.y, hoverColor: this._theme.shapeHover.y, disabledColor: this._theme.disabled }), z: new BoxLineShape(this._device, { axis: "z", layers: [this._layer.id], rotation: new Vec3(90, 0, 0), defaultColor: this._theme.shapeBase.z, hoverColor: this._theme.shapeHover.z, disabledColor: this._theme.disabled }) }); /** * @type {GizmoSpace} * @protected */ __publicField(this, "_coordSpace", "local"); /** * Internal mapping from each attached node to their starting scale. * * @type {Map<GraphNode, Vec3>} * @private */ __publicField(this, "_nodeScales", /* @__PURE__ */ new Map()); /** * Internal state if transform should use uniform scaling. * * @protected */ __publicField(this, "_uniform", false); /** @override */ __publicField(this, "snapIncrement", 1); /** * Flips the planes to face the camera. */ __publicField(this, "flipPlanes", true); /** * The lower bound for scaling. */ __publicField(this, "lowerBoundScale", new Vec3(-Infinity, -Infinity, -Infinity)); this._createTransform(); this.on(TransformGizmo.EVENT_TRANSFORMSTART, () => { this._storeNodeScales(); this._drag(true); }); this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (point2) => { const scaleDelta = delta.copy(point2).sub(this._selectionStartPoint); if (this.snap) { scaleDelta.mulScalar(1 / this.snapIncrement); scaleDelta.round(); scaleDelta.mulScalar(this.snapIncrement); } scaleDelta.mulScalar(1 / this._scale); this._setNodeScales(scaleDelta.add(Vec3.ONE)); }); this.on(TransformGizmo.EVENT_TRANSFORMEND, () => { this._drag(false); }); this.on(TransformGizmo.EVENT_NODESDETACH, () => { this._nodeScales.clear(); }); } set coordSpace(value) { } get coordSpace() { return this._coordSpace; } /** * Sets the uniform scaling state for planes. * * @type {boolean} */ set uniform(value) { this._uniform = value ?? this._uniform; } /** * Gets the uniform scaling state for planes. * * @type {boolean} */ get uniform() { return this._uniform; } /** * Sets the axis gap. * * @type {number} */ set axisGap(value) { this._setArrowProp("gap", value); } /** * Gets the axis gap. * * @type {number} */ get axisGap() { return this._shapes.x.gap; } /** * Sets the axis line thickness. * * @type {number} */ set axisLineThickness(value) { this._setArrowProp("lineThickness", value); } /** * Gets the axis line thickness. * * @type {number} */ get axisLineThickness() { return this._shapes.x.lineThickness; } /** * Sets the axis line length. * * @type {number} */ set axisLineLength(value) { this._setArrowProp("lineLength", value); } /** * Gets the axis line length. * * @type {number} */ get axisLineLength() { return this._shapes.x.lineLength; } /** * Sets the axis line tolerance. * * @type {number} */ set axisLineTolerance(value) { this._setArrowProp("tolerance", value); } /** * Gets the axis line tolerance. * * @type {number} */ get axisLineTolerance() { return this._shapes.x.tolerance; } /** * Sets the axis box size. * * @type {number} */ set axisBoxSize(value) { this._setArrowProp("boxSize", value); } /** * Gets the axis box size. * * @type {number} */ get axisBoxSize() { return this._shapes.x.boxSize; } /** * Sets the plane size. * * @type {number} */ set axisPlaneSize(value) { this._setPlaneProp("size", value); } /** * Gets the plane size. * * @type {number} */ get axisPlaneSize() { return this._shapes.yz.size; } /** * Sets the plane gap. * * @type {number} */ set axisPlaneGap(value) { this._setPlaneProp("gap", value); } /** * Gets the plane gap. * * @type {number} */ get axisPlaneGap() { return this._shapes.yz.gap; } /** * Sets the axis center size. * * @type {number} */ set axisCenterSize(value) { this._shapes.xyz.size = value; } /** * Gets the axis center size. * * @type {number} */ get axisCenterSize() { return this._shapes.xyz.size; } /** * @type {boolean} * @deprecated Use {@link flipPlanes} instead. * @ignore */ set flipShapes(value) { this.flipPlanes = value; } /** * @type {boolean} * @deprecated Use {@link flipPlanes} instead. * @ignore */ get flipShapes() { return this.flipPlanes; } /** * @param {string} prop - The property name. * @param {any} value - The property value. * @private */ _setArrowProp(prop, value) { this._shapes.x[prop] = value; this._shapes.y[prop] = value; this._shapes.z[prop] = value; } /** * @param {string} prop - The property name. * @param {any} value - The property value. * @private */ _setPlaneProp(prop, value) { this._shapes.yz[prop] = value; this._shapes.xz[prop] = value; this._shapes.xy[prop] = value; } /** @private */ _shapesLookAtCamera() { const cameraDir = this.cameraDir; let changed = false; let dot, enabled; dot = cameraDir.dot(this.root.right); enabled = 1 - Math.abs(dot) > GLANCE_EPSILON; if (this._shapes.x.entity.enabled !== enabled) { this._shapes.x.entity.enabled = enabled; changed = true; } dot = cameraDir.dot(this.root.up); enabled = 1 - Math.abs(dot) > GLANCE_EPSILON; if (this._shapes.y.entity.enabled !== enabled) { this._shapes.y.entity.enabled = enabled; changed = true; } dot = cameraDir.dot(this.root.forward); enabled = 1 - Math.abs(dot) > GLANCE_EPSILON; if (this._shapes.z.entity.enabled !== enabled) { this._shapes.z.entity.enabled = enabled; changed = true; } let flipped; v1.cross(cameraDir, this.root.right); enabled = 1 - v1.length() > GLANCE_EPSILON; if (this._shapes.yz.entity.enabled !== enabled) { this._shapes.yz.entity.enabled = enabled; changed = true; } flipped = this.flipPlanes ? v2.set(0, +(v1.dot(this.root.forward) < 0), +(v1.dot(this.root.up) < 0)) : Vec3.ZERO; if (!this._shapes.yz.flipped.equals(flipped)) { this._shapes.yz.flipped = flipped; changed = true; } v1.cross(cameraDir, this.root.forward); enabled = 1 - v1.length() > GLANCE_EPSILON; if (this._shapes.xy.entity.enabled !== enabled) { this._shapes.xy.entity.enabled = enabled; changed = true; } flipped = this.flipPlanes ? v2.set(+(v1.dot(this.root.up) < 0), +(v1.dot(this.root.right) > 0), 0) : Vec3.ZERO; if (!this._shapes.xy.flipped.equals(flipped)) { this._shapes.xy.flipped = flipped; changed = true; } v1.cross(cameraDir, this.root.up); enabled = 1 - v1.length() > GLANCE_EPSILON; if (this._shapes.xz.entity.enabled !== enabled) { this._shapes.xz.entity.enabled = enabled; changed = true; } flipped = this.flipPlanes ? v2.set(+(v1.dot(this.root.forward) > 0), 0, +(v1.dot(this.root.right) > 0)) : Vec3.ZERO; if (!this._shapes.xz.flipped.equals(flipped)) { this._shapes.xz.flipped = flipped; changed = true; } if (changed) { this._renderUpdate = true; } } /** * @param {boolean} state - The state. * @private */ _drag(state) { for (const axis in this._shapes) { const shape = this._shapes[axis]; switch (this.dragMode) { case "show": { continue; } case "hide": { shape.visible = !state; continue; } case "selected": { if (this._selectedAxis === "xyz") { shape.visible = state ? axis.length === 1 : true; continue; } if (this._selectedIsPlane) { shape.visible = state ? axis.length === 1 && !axis.includes(this._selectedAxis) : true; continue; } shape.visible = state ? axis === this._selectedAxis : true; continue; } } } this._renderUpdate = true; } /** @private */ _storeNodeScales() { for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i]; this._nodeScales.set(node, node.getLocalScale().clone()); } } /** * @param {Vec3} scaleDelta - The point delta. * @private */ _setNodeScales(scaleDelta) { for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i]; const scale = this._nodeScales.get(node); if (!scale) { continue; } node.setLocalScale(v1.copy(scale).mul(scaleDelta).max(this.lowerBoundScale)); } } /** * @param {number} x - The x coordinate. * @param {number} y - The y coordinate. * @returns {Vec3} The point (space is {@link TransformGizmo#coordSpace}). * @protected */ _screenToPoint(x, y) { const gizmoPos = this.root.getLocalPosition(); const mouseWPos = this._camera.screenToWorld(x, y, 1); const axis = this._selectedAxis; const isPlane = this._selectedIsPlane; const ray = this._createRay(mouseWPos); const plane = this._createPlane(axis, axis === "xyz", !isPlane); if (!plane.intersectsRay(ray, point)) { return point; } if (axis === "xyz") { const projDir = v2.add2(this._camera.entity.up, this._camera.entity.right).normalize(); const dir = v1.sub2(point, gizmoPos); const v = dir.length() * dir.normalize().dot(projDir); point.set(v, v, v); return point; } q.copy(this._rootStartRot).invert().transformVector(point, point); if (!isPlane) { this._projectToAxis(point, axis); } if (this._uniform && isPlane) { v1.set(1, 1, 1); v1[axis] = 0; point.copy(v1.mulScalar(v1.dot(point))); point[axis] = 0; } return point; } /** @override */ prerender() { super.prerender(); if (!this.enabled) { return; } this._shapesLookAtCamera(); } } export { ScaleGizmo };