UNPKG

playcanvas

Version:

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

422 lines (421 loc) 11 kB
import { math } from "../../core/math/math.js"; import { Color } from "../../core/math/color.js"; import { Quat } from "../../core/math/quat.js"; import { Vec3 } from "../../core/math/vec3.js"; import { Ray } from "../../core/shape/ray.js"; import { Plane } from "../../core/shape/plane.js"; import { PROJECTION_PERSPECTIVE } from "../../scene/constants.js"; import { COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_GRAY, color4from3 } from "./color.js"; import { Gizmo } from "./gizmo.js"; const v1 = new Vec3(); const v2 = new Vec3(); const point = new Vec3(); const ray = new Ray(); const plane = new Plane(); const color = new Color(); const AXES = ["x", "y", "z"]; class TransformGizmo extends Gizmo { static EVENT_TRANSFORMSTART = "transform:start"; static EVENT_TRANSFORMMOVE = "transform:move"; static EVENT_TRANSFORMEND = "transform:end"; _theme = { shapeBase: { x: COLOR_RED.clone(), y: COLOR_GREEN.clone(), z: COLOR_BLUE.clone(), xyz: new Color(0.8, 0.8, 0.8, 1), f: new Color(0.8, 0.8, 0.8, 1) }, shapeHover: { x: new Color().lerp(COLOR_RED, Color.WHITE, 0.75), y: new Color().lerp(COLOR_GREEN, Color.WHITE, 0.75), z: new Color().lerp(COLOR_BLUE, Color.WHITE, 0.75), xyz: Color.WHITE.clone(), f: Color.WHITE.clone() }, guideBase: { x: COLOR_RED.clone(), y: COLOR_GREEN.clone(), z: COLOR_BLUE.clone() }, guideOcclusion: 0.8, disabled: COLOR_GRAY.clone() }; _rootStartPos = new Vec3(); _rootStartRot = new Quat(); _shapes = {}; _shapeMap = /* @__PURE__ */ new Map(); _hovering = /* @__PURE__ */ new Set(); _hoverAxis = ""; _hoverIsPlane = false; _selectedAxis = ""; _selectedIsPlane = false; _selectionStartPoint = new Vec3(); snap = false; snapIncrement = 1; dragMode = "selected"; constructor(camera, layer, name = "gizmo:transform") { super(camera, layer, name); this.on(Gizmo.EVENT_POINTERDOWN, (x, y, meshInstance) => { const shape = this._shapeMap.get(meshInstance); if (shape?.disabled) { return; } if (this._dragging) { return; } if (!meshInstance) { return; } this._hoverAxis = ""; this._hoverIsPlane = false; this._selectedAxis = this._getAxis(meshInstance); this._selectedIsPlane = this._getIsPlane(meshInstance); this._rootStartPos.copy(this.root.getLocalPosition()); this._rootStartRot.copy(this.root.getRotation()); const point2 = this._screenToPoint(x, y); this._selectionStartPoint.copy(point2); this.fire(TransformGizmo.EVENT_TRANSFORMSTART, point2, x, y); }); this.on(Gizmo.EVENT_POINTERMOVE, (x, y, meshInstance) => { const shape = this._shapeMap.get(meshInstance); if (shape?.disabled) { return; } this._hover(meshInstance); if (!this._dragging) { return; } const point2 = this._screenToPoint(x, y); this.fire(TransformGizmo.EVENT_TRANSFORMMOVE, point2, x, y); }); this.on(Gizmo.EVENT_POINTERUP, (_x, _y, meshInstance) => { this._hover(meshInstance); if (!this._dragging) { return; } if (meshInstance) { this._hoverAxis = this._selectedAxis; this._hoverIsPlane = this._selectedIsPlane; } this._selectedAxis = ""; this._selectedIsPlane = false; this.fire(TransformGizmo.EVENT_TRANSFORMEND); }); this.on(Gizmo.EVENT_NODESDETACH, () => { this._hoverAxis = ""; this._hoverIsPlane = false; this._hover(); this.fire(Gizmo.EVENT_POINTERUP); }); } get theme() { return this._theme; } set xAxisColor(value) { this.setTheme({ shapeBase: { x: value }, shapeHover: { x: color4from3(value, this.colorAlpha) }, guideBase: { x: value }, guideOcclusion: 1 }); } get xAxisColor() { return this._theme.shapeBase.x; } set yAxisColor(value) { this.setTheme({ shapeBase: { y: value }, shapeHover: { y: color4from3(value, this.colorAlpha) }, guideBase: { y: value }, guideOcclusion: 1 }); } get yAxisColor() { return this._theme.shapeBase.y; } set zAxisColor(value) { this.setTheme({ shapeBase: { z: value }, shapeHover: { z: color4from3(value, this.colorAlpha) }, guideBase: { z: value }, guideOcclusion: 1 }); } get zAxisColor() { return this._theme.shapeBase.z; } set colorAlpha(value) { this.setTheme({ shapeHover: { x: color4from3(this._theme.shapeHover.x, value), y: color4from3(this._theme.shapeHover.y, value), z: color4from3(this._theme.shapeHover.z, value), xyz: color4from3(this._theme.shapeHover.xyz, value), f: color4from3(this._theme.shapeHover.f, value) } }); } get colorAlpha() { return (this._theme.shapeHover.x.a + this._theme.shapeHover.y.a + this._theme.shapeHover.z.a + this._theme.shapeHover.xyz.a + this._theme.shapeHover.f.a) / 5; } get _dragging() { return !this._hoverAxis && !!this._selectedAxis; } _getAxis(meshInstance) { if (!meshInstance) { return ""; } return meshInstance.node.name.split(":")[1]; } _getIsPlane(meshInstance) { if (!meshInstance) { return false; } return meshInstance.node.name.indexOf("plane") !== -1; } _hover(meshInstance) { if (this._dragging) { return; } const remove = new Set(this._hovering); let changed = false; const add = (axis) => { if (remove.has(axis)) { remove.delete(axis); } else { this._hovering.add(axis); this._shapes[axis]?.hover(true); changed = true; } }; this._hoverAxis = this._getAxis(meshInstance); this._hoverIsPlane = this._getIsPlane(meshInstance); if (this._hoverAxis) { if (this._hoverAxis === "xyz") { add("x"); add("y"); add("z"); add("xyz"); } else if (this._hoverIsPlane) { switch (this._hoverAxis) { case "x": add("y"); add("z"); add("yz"); break; case "y": add("x"); add("z"); add("xz"); break; case "z": add("x"); add("y"); add("xy"); break; } } else { add(this._hoverAxis); } } for (const axis of remove) { this._hovering.delete(axis); this._shapes[axis]?.hover(false); changed = true; } if (changed) { this._renderUpdate = true; } } _createRay(mouseWPos) { if (this._camera.projection === PROJECTION_PERSPECTIVE) { ray.origin.copy(this._camera.entity.getPosition()); ray.direction.sub2(mouseWPos, ray.origin).normalize(); return ray; } const orthoDepth = this._camera.farClip - this._camera.nearClip; ray.origin.sub2(mouseWPos, v1.copy(this._camera.entity.forward).mulScalar(orthoDepth)); ray.direction.copy(this._camera.entity.forward); return ray; } _createPlane(axis, isFacing, isLine) { const facingDir = v1.copy(this.facingDir); const normal = plane.normal.set(0, 0, 0); if (isFacing) { normal.copy(this._camera.entity.forward).mulScalar(-1); } else { normal[axis] = 1; this._rootStartRot.transformVector(normal, normal); if (isLine) { v2.cross(normal, facingDir).normalize(); normal.cross(v2, normal).normalize(); } } return plane.setFromPointNormal(this._rootStartPos, normal); } _dirFromAxis(axis, dir) { if (axis === "f") { dir.copy(this._camera.entity.forward).mulScalar(-1); } else { dir.set(0, 0, 0); dir[axis] = 1; } return dir; } _projectToAxis(point2, axis) { v1.set(0, 0, 0); v1[axis] = 1; point2.copy(v1.mulScalar(v1.dot(point2))); const v = point2[axis]; point2.set(0, 0, 0); point2[axis] = v; } _screenToPoint(x, y, isFacing = false, isLine = false) { const mouseWPos = this._camera.screenToWorld(x, y, 1); const axis = this._selectedAxis; const ray2 = this._createRay(mouseWPos); const plane2 = this._createPlane(axis, isFacing, isLine); if (!plane2.intersectsRay(ray2, point)) { return point; } return point; } _drawGuideLines(pos, rot, activeAxis, activeIsPlane) { for (const axis of AXES) { if (activeAxis === "xyz") { this._drawSpanLine(pos, rot, axis); continue; } if (activeIsPlane) { if (axis !== activeAxis) { this._drawSpanLine(pos, rot, axis); } } else { if (axis === activeAxis) { this._drawSpanLine(pos, rot, axis); } } } } _drawSpanLine(pos, rot, axis) { const dir = this._dirFromAxis(axis, v1); const base = this._theme.guideBase[axis]; const from = v1.copy(dir).mulScalar(this._camera.farClip - this._camera.nearClip); const to = v2.copy(from).mulScalar(-1); rot.transformVector(from, from).add(pos); rot.transformVector(to, to).add(pos); if (this._theme.guideOcclusion < 1) { const occluded = color.copy(base); occluded.a *= 1 - this._theme.guideOcclusion; this._app.drawLine(from, to, occluded, false, this._layer); } if (base.a !== 0) { this._app.drawLine(from, to, base, true); } } _createTransform() { for (const key in this._shapes) { const shape = this._shapes[key]; this.root.addChild(shape.entity); this.intersectShapes.push(shape); for (let i = 0; i < shape.meshInstances.length; i++) { this._shapeMap.set(shape.meshInstances[i], shape); } } } enableShape(shapeAxis, enabled) { if (shapeAxis === "face") { shapeAxis = "f"; } const shape = this._shapes[shapeAxis]; if (!shape) { return; } shape.disabled = !enabled; } isShapeEnabled(shapeAxis) { if (shapeAxis === "face") { shapeAxis = "f"; } const shape = this._shapes[shapeAxis]; if (!shape) { return false; } return !shape.disabled; } setTheme(partial) { const theme = { ...this._theme, ...partial }; if (typeof theme !== "object" || typeof theme.shapeBase !== "object" || typeof theme.shapeHover !== "object" || typeof theme.guideBase !== "object") { return; } for (const axis in theme.shapeBase) { if (theme.shapeBase[axis] instanceof Color) { this._theme.shapeBase[axis].copy(theme.shapeBase[axis]); } } for (const axis in theme.shapeHover) { if (theme.shapeHover[axis] instanceof Color) { this._theme.shapeHover[axis].copy(theme.shapeHover[axis]); } } for (const axis in theme.guideBase) { if (theme.guideBase[axis] instanceof Color) { this._theme.guideBase[axis].copy(theme.guideBase[axis]); } } if (typeof theme.guideOcclusion === "number") { this._theme.guideOcclusion = math.clamp(theme.guideOcclusion, 0, 1); } if (theme.disabled instanceof Color) { this._theme.disabled.copy(theme.disabled); } for (const name in this._shapes) { this._shapes[name].hover(!!this._hoverAxis); } } prerender() { super.prerender(); if (!this.enabled) { return; } const gizmoPos = this.root.getLocalPosition(); const gizmoRot = this.root.getRotation(); const activeAxis = this._hoverAxis || this._selectedAxis; const activeIsPlane = this._hoverIsPlane || this._selectedIsPlane; this._drawGuideLines(gizmoPos, gizmoRot, activeAxis, activeIsPlane); } destroy() { super.destroy(); for (const key in this._shapes) { this._shapes[key].destroy(); } } } export { TransformGizmo };