UNPKG

playcanvas

Version:

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

448 lines (447 loc) 13.6 kB
import { math } from "../../core/math/math.js"; import { Color } from "../../core/math/color.js"; import { Quat } from "../../core/math/quat.js"; import { Vec2 } from "../../core/math/vec2.js"; import { Vec3 } from "../../core/math/vec3.js"; import { PROJECTION_PERSPECTIVE } from "../../scene/constants.js"; import { ArcShape } from "./shape/arc-shape.js"; import { TransformGizmo } from "./transform-gizmo.js"; import { MeshLine } from "./mesh-line.js"; import { SphereShape } from "./shape/sphere-shape.js"; const point = new Vec3(); const v1 = new Vec3(); const v2 = new Vec3(); const v3 = new Vec3(); const q1 = new Quat(); const q2 = new Quat(); const color = new Color(); const RING_FACING_EPSILON = 1e-4; const UPDATE_EPSILON = 1e-6; const AXES = ["x", "y", "z"]; class RotateGizmo extends TransformGizmo { _shapes = { z: new ArcShape(this._device, { axis: "z", layers: [this._layer.id], rotation: new Vec3(90, 0, 90), defaultColor: this._theme.shapeBase.z, hoverColor: this._theme.shapeHover.z, disabledColor: this._theme.disabled, sectorAngle: 180 }), x: new ArcShape(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, sectorAngle: 180 }), y: new ArcShape(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, sectorAngle: 180 }), f: new ArcShape(this._device, { axis: "f", layers: [this._layer.id], defaultColor: this._theme.shapeBase.f, hoverColor: this._theme.shapeHover.f, disabledColor: this._theme.disabled, ringRadius: 0.55 }), xyz: new SphereShape(this._device, { axis: "xyz", layers: [this._layer.id], defaultColor: this._theme.shapeBase.xyz, hoverColor: this._theme.shapeHover.xyz, disabledColor: this._theme.disabled, radius: 0.5 }) }; _selectionStartAngle = 0; _nodeLocalRotations = /* @__PURE__ */ new Map(); _nodeRotations = /* @__PURE__ */ new Map(); _nodeOffsets = /* @__PURE__ */ new Map(); _screenPos = new Vec2(); _screenStartPos = new Vec2(); _guideAngleStart = new Vec3(); _guideAngleEnd = new Vec3(); _guideAngleLines; _facingDir = new Vec3(); snapIncrement = 5; rotationMode = "absolute"; constructor(camera, layer) { super(camera, layer, "gizmo:rotate"); this.setTheme({ shapeBase: { xyz: new Color(0, 0, 0, 0) }, shapeHover: { xyz: new Color(1, 1, 1, 0.2) } }); this._createTransform(); this._guideAngleLines = [ new MeshLine(this._app, this._layer), new MeshLine(this._app, this._layer) ]; this._guideAngleLines.forEach((line) => { this._app.root.addChild(line.entity); line.entity.enabled = false; }); this.on(TransformGizmo.EVENT_TRANSFORMSTART, (point2, x, y) => { this._screenPos.set(x, y); this._screenStartPos.set(x, y); this._selectionStartAngle = this._calculateArcAngle(point2, x, y); this._storeNodeRotations(); this._storeGuidePoints(); this._drag(true); this._angleGuide(true); }); this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (point2, x, y) => { const axis = this._selectedAxis; if (!axis) { return; } this._screenPos.set(x, y); if (axis === "xyz") { const facingDir = v1.copy(this.facingDir); const delta = v2.copy(point2).sub(this._selectionStartPoint); const angleAxis = v1.cross(facingDir, delta).normalize(); const angleDelta = this._screenPos.distance(this._screenStartPos); this._setNodeRotations(axis, angleAxis, angleDelta); } else { let angleDelta = this._calculateArcAngle(point2, x, y) - this._selectionStartAngle; if (this.snap) { angleDelta = Math.round(angleDelta / this.snapIncrement) * this.snapIncrement; } const angleAxis = this._dirFromAxis(axis, v1); this._setNodeRotations(axis, angleAxis, angleDelta); this._updateGuidePoints(angleDelta); this._angleGuide(true); } }); this.on(TransformGizmo.EVENT_TRANSFORMEND, () => { this._drag(false); this._angleGuide(false); }); this.on(TransformGizmo.EVENT_NODESDETACH, () => { this._nodeLocalRotations.clear(); this._nodeRotations.clear(); this._nodeOffsets.clear(); }); } set xyzTubeRadius(value) { this._setDiskProp("tubeRadius", value); } get xyzTubeRadius() { return this._shapes.x.tubeRadius; } set xyzRingRadius(value) { this._setDiskProp("ringRadius", value); } get xyzRingRadius() { return this._shapes.x.ringRadius; } set faceTubeRadius(value) { this._shapes.f.tubeRadius = value; } get faceTubeRadius() { return this._shapes.f.tubeRadius; } set faceRingRadius(value) { this._shapes.f.ringRadius = value; } get faceRingRadius() { return this._shapes.f.ringRadius; } set centerRadius(value) { this._shapes.xyz.radius = value; } get centerRadius() { return this._shapes.xyz.radius; } set ringTolerance(value) { this._setDiskProp("tolerance", value); this._shapes.f.tolerance = value; } get ringTolerance() { return this._shapes.x.tolerance; } set angleGuideThickness(value) { this._guideAngleLines[0].thickness = value; this._guideAngleLines[1].thickness = value; } get angleGuideThickness() { return this._guideAngleLines[0].thickness; } set orbitRotation(value) { this.rotationMode = value ? "orbit" : "absolute"; } get orbitRotation() { return this.rotationMode === "orbit"; } _setDiskProp(prop, value) { this._shapes.x[prop] = value; this._shapes.y[prop] = value; this._shapes.z[prop] = value; } _storeGuidePoints() { const gizmoPos = this.root.getLocalPosition(); const axis = this._selectedAxis; const isFacing = axis === "f"; const scale = isFacing ? this.faceRingRadius : this.xyzRingRadius; this._guideAngleStart.copy(this._selectionStartPoint).sub(gizmoPos).normalize(); this._guideAngleStart.mulScalar(scale); this._guideAngleEnd.copy(this._guideAngleStart); } _updateGuidePoints(angleDelta) { const axis = this._selectedAxis; const isFacing = axis === "f"; if (isFacing) { v1.copy(this.facingDir); } else { v1.set(0, 0, 0); v1[axis] = 1; this._rootStartRot.transformVector(v1, v1); } q1.setFromAxisAngle(v1, angleDelta); q1.transformVector(this._guideAngleStart, this._guideAngleEnd); this._renderUpdate = true; } _angleGuide(state) { const axis = this._selectedAxis; if (state && this.dragMode !== "show" && axis !== "xyz") { const gizmoPos = this.root.getLocalPosition(); const baseColor = this._theme.shapeHover[axis]; const startColor = color.copy(baseColor); startColor.a *= 0.3; this._guideAngleLines[0].draw( gizmoPos, v1.copy(this._guideAngleStart).add(gizmoPos), this._scale, startColor ); this._guideAngleLines[1].draw( gizmoPos, v1.copy(this._guideAngleEnd).add(gizmoPos), this._scale, baseColor ); this._guideAngleLines[0].entity.enabled = true; this._guideAngleLines[1].entity.enabled = true; } else { this._guideAngleLines[0].entity.enabled = false; this._guideAngleLines[1].entity.enabled = false; } } _shapesLookAtCamera() { if (this._camera.projection === PROJECTION_PERSPECTIVE) { const dir = this._camera.entity.getPosition().sub(this.root.getPosition()).normalize(); const elev = Math.atan2(-dir.y, Math.sqrt(dir.x * dir.x + dir.z * dir.z)) * math.RAD_TO_DEG; const azim = Math.atan2(-dir.x, -dir.z) * math.RAD_TO_DEG; this._shapes.f.entity.setEulerAngles(-elev + 90, azim, 0); } else { q1.copy(this._camera.entity.getRotation()).getEulerAngles(v1); this._shapes.f.entity.setEulerAngles(v1); this._shapes.f.entity.rotateLocal(-90, 0, 0); } let angle, dot, sector; const facingDir = v1.copy(this.facingDir); q1.copy(this.root.getRotation()).invert().transformVector(facingDir, facingDir); angle = Math.atan2(facingDir.z, facingDir.y) * math.RAD_TO_DEG; this._shapes.x.entity.setLocalEulerAngles(0, angle - 90, -90); angle = Math.atan2(facingDir.x, facingDir.z) * math.RAD_TO_DEG; this._shapes.y.entity.setLocalEulerAngles(0, angle, 0); angle = Math.atan2(facingDir.y, facingDir.x) * math.RAD_TO_DEG; this._shapes.z.entity.setLocalEulerAngles(90, 0, angle + 90); if (!this._dragging) { dot = facingDir.dot(this.root.right); sector = 1 - Math.abs(dot) > RING_FACING_EPSILON; this._shapes.x.show(sector ? "sector" : "ring"); dot = facingDir.dot(this.root.up); sector = 1 - Math.abs(dot) > RING_FACING_EPSILON; this._shapes.y.show(sector ? "sector" : "ring"); dot = facingDir.dot(this.root.forward); sector = 1 - Math.abs(dot) > RING_FACING_EPSILON; this._shapes.z.show(sector ? "sector" : "ring"); } if (!facingDir.equalsApprox(this._facingDir, UPDATE_EPSILON)) { this._facingDir.copy(facingDir); this._renderUpdate = true; } } _drag(state) { for (const axis in this._shapes) { const shape = this._shapes[axis]; if (!(shape instanceof ArcShape)) { continue; } switch (this.dragMode) { case "show": { break; } case "hide": { shape.show(state ? axis === this._selectedAxis ? "ring" : "none" : "sector"); continue; } case "selected": { shape.show(state ? axis === this._selectedAxis ? "ring" : "sector" : "sector"); break; } } } this._renderUpdate = true; } _storeNodeRotations() { const gizmoPos = this.root.getLocalPosition(); for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i]; this._nodeLocalRotations.set(node, node.getLocalRotation().clone()); this._nodeRotations.set(node, node.getRotation().clone()); this._nodeOffsets.set(node, node.getPosition().clone().sub(gizmoPos)); } } _setNodeRotations(axis, angleAxis, angleDelta) { const gizmoPos = this.root.getLocalPosition(); q1.setFromAxisAngle(angleAxis, angleDelta); for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i]; if ((axis === "x" || axis === "y" || axis === "z") && this._coordSpace === "local") { const rot = this._nodeLocalRotations.get(node); if (!rot) { continue; } q2.copy(rot).mul(q1); node.setLocalRotation(q2); } else { const rot = this._nodeRotations.get(node); if (!rot) { continue; } const offset = this._nodeOffsets.get(node); if (!offset) { continue; } v1.copy(offset); q1.transformVector(v1, v1); q2.copy(q1).mul(rot); node.setRotation(q2); node.setPosition(v1.add(gizmoPos)); } } if (this._coordSpace === "local") { this._updateRotation(); } } _screenToPoint(x, y) { const mouseWPos = this._camera.screenToWorld(x, y, 1); const axis = this._selectedAxis; const ray = this._createRay(mouseWPos); const plane = this._createPlane(axis, axis === "f" || axis === "xyz", false); if (!plane.intersectsRay(ray, point)) { ray.direction.mulScalar(-1); const intersection = plane.intersectsRay(ray, point); ray.direction.mulScalar(-1); if (!intersection) { return point.copy(this.root.getLocalPosition()); } } return point; } _calculateArcAngle(point2, x, y) { const gizmoPos = this.root.getLocalPosition(); const axis = this._selectedAxis; const plane = this._createPlane(axis, axis === "f", false); let angle = 0; const facingDir = this.facingDir; const facingDot = plane.normal.dot(facingDir); switch (this.rotationMode) { case "absolute": { this._camera.worldToScreen(gizmoPos, v2); if (axis === "f" || facingDot > 1 - RING_FACING_EPSILON) { v1.set( this._screenStartPos.y >= v2.y ? 1 : -1, this._screenStartPos.x >= v2.x ? -1 : 1, 0 ).normalize(); } else { const projDir = v1.cross(plane.normal, facingDir).normalize(); this._camera.worldToScreen(projDir.add(gizmoPos), v3); v1.sub2(v3, v2).normalize(); } v2.set(x, y, 0); angle = v1.dot(v2); break; } case "orbit": { v1.sub2(point2, gizmoPos); switch (axis) { case "x": { q1.copy(this._rootStartRot).invert().transformVector(v1, v1); angle = Math.atan2(v1.z, v1.y) * math.RAD_TO_DEG; break; } case "y": { q1.copy(this._rootStartRot).invert().transformVector(v1, v1); angle = Math.atan2(v1.x, v1.z) * math.RAD_TO_DEG; break; } case "z": { q1.copy(this._rootStartRot).invert().transformVector(v1, v1); angle = Math.atan2(v1.y, v1.x) * math.RAD_TO_DEG; break; } case "f": { q1.copy(this._camera.entity.getRotation()).invert().transformVector(v1, v1); angle = Math.sign(facingDot) * Math.atan2(v1.y, v1.x) * math.RAD_TO_DEG; break; } } const dir = v1.sub2(point2, this._camera.entity.getPosition()).normalize(); const dot = dir.dot(this._camera.entity.forward); if (dot < 0) { angle += 180; } break; } } return angle; } _drawGuideLines(pos, rot, activeAxis, activeIsPlane) { for (const axis of AXES) { if (activeAxis === "xyz") { continue; } if (activeIsPlane) { if (axis !== activeAxis) { this._drawSpanLine(pos, rot, axis); } } else { if (axis === activeAxis) { this._drawSpanLine(pos, rot, axis); } } } } prerender() { super.prerender(); if (!this.enabled) { return; } this._shapesLookAtCamera(); } destroy() { this._guideAngleLines.forEach((line) => line.destroy()); super.destroy(); } } export { RotateGizmo };