From 21693b02f74d0db80463615372f5c8521c95be45 Mon Sep 17 00:00:00 2001 From: KPal <48248865+kpal81xd@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:00:13 +0100 Subject: [PATCH] Gizmos updates v1 (fast forwarding from v2) (#6996) * ff gizmos in v1 to match v2 * added new files * updated all @import to typedef --- examples/src/examples/misc/gizmos.example.mjs | 15 +- .../examples/misc/gizmos.gizmo-handler.mjs | 10 +- src/extras/gizmo/axis-shapes.js | 697 ------------------ .../gizmo/{default-colors.js => color.js} | 14 + src/extras/gizmo/constants.js | 71 ++ src/extras/gizmo/gizmo.js | 277 ++++--- src/extras/gizmo/rotate-gizmo.js | 141 +++- src/extras/gizmo/scale-gizmo.js | 109 ++- src/extras/gizmo/shape/arc-shape.js | 115 +++ src/extras/gizmo/shape/arrow-shape.js | 138 ++++ src/extras/gizmo/shape/box-shape.js | 52 ++ src/extras/gizmo/shape/boxline-shape.js | 129 ++++ src/extras/gizmo/shape/plane-shape.js | 65 ++ src/extras/gizmo/shape/shape.js | 355 +++++++++ src/extras/gizmo/shape/sphere-shape.js | 52 ++ src/extras/gizmo/transform-gizmo.js | 301 ++++---- src/extras/gizmo/translate-gizmo.js | 153 +++- src/extras/gizmo/tri-data.js | 35 +- src/extras/index.js | 14 +- 19 files changed, 1678 insertions(+), 1065 deletions(-) delete mode 100644 src/extras/gizmo/axis-shapes.js rename src/extras/gizmo/{default-colors.js => color.js} (56%) create mode 100644 src/extras/gizmo/constants.js create mode 100644 src/extras/gizmo/shape/arc-shape.js create mode 100644 src/extras/gizmo/shape/arrow-shape.js create mode 100644 src/extras/gizmo/shape/box-shape.js create mode 100644 src/extras/gizmo/shape/boxline-shape.js create mode 100644 src/extras/gizmo/shape/plane-shape.js create mode 100644 src/extras/gizmo/shape/shape.js create mode 100644 src/extras/gizmo/shape/sphere-shape.js diff --git a/examples/src/examples/misc/gizmos.example.mjs b/examples/src/examples/misc/gizmos.example.mjs index 629d992ccde..9cd79c4342a 100644 --- a/examples/src/examples/misc/gizmos.example.mjs +++ b/examples/src/examples/misc/gizmos.example.mjs @@ -137,19 +137,8 @@ light.addComponent('light', { app.root.addChild(light); light.setEulerAngles(0, 0, -60); -// create layers -const gizmoLayer = new pc.Layer({ - name: 'Gizmo', - clearDepthBuffer: true, - opaqueSortMode: pc.SORTMODE_NONE, - transparentSortMode: pc.SORTMODE_NONE -}); -const layers = app.scene.layers; -layers.push(gizmoLayer); -camera.camera.layers = camera.camera.layers.concat(gizmoLayer.id); - // create gizmo -const gizmoHandler = new GizmoHandler(app, camera.camera, gizmoLayer); +const gizmoHandler = new GizmoHandler(app, camera.camera); gizmoHandler.switch('translate'); gizmoHandler.add(box); window.focus(); @@ -257,7 +246,7 @@ data.on('*:set', (/** @type {string} */ path, value) => { // picker const picker = new pc.Picker(app, canvas.clientWidth, canvas.clientHeight); -const worldLayer = layers.getLayerByName('World'); +const worldLayer = app.scene.layers.getLayerByName('World'); const pickerLayers = [worldLayer]; const onPointerDown = (/** @type {PointerEvent} */ e) => { diff --git a/examples/src/examples/misc/gizmos.gizmo-handler.mjs b/examples/src/examples/misc/gizmos.gizmo-handler.mjs index c80ac93aba4..79913168bce 100644 --- a/examples/src/examples/misc/gizmos.gizmo-handler.mjs +++ b/examples/src/examples/misc/gizmos.gizmo-handler.mjs @@ -45,13 +45,13 @@ class GizmoHandler { /** * @param {pc.AppBase} app - The application. * @param {pc.CameraComponent} camera - The camera component. - * @param {pc.Layer} layer - The gizmo layer */ - constructor(app, camera, layer) { + constructor(app, camera) { + const layer = pc.Gizmo.createLayer(app); this._gizmos = { - translate: new pc.TranslateGizmo(app, camera, layer), - rotate: new pc.RotateGizmo(app, camera, layer), - scale: new pc.ScaleGizmo(app, camera, layer) + translate: new pc.TranslateGizmo(camera, layer), + rotate: new pc.RotateGizmo(camera, layer), + scale: new pc.ScaleGizmo(camera, layer) }; for (const type in this._gizmos) { diff --git a/src/extras/gizmo/axis-shapes.js b/src/extras/gizmo/axis-shapes.js deleted file mode 100644 index 4153924f246..00000000000 --- a/src/extras/gizmo/axis-shapes.js +++ /dev/null @@ -1,697 +0,0 @@ -import { Color } from '../../core/math/color.js'; -import { Vec3 } from '../../core/math/vec3.js'; -import { Quat } from '../../core/math/quat.js'; -import { Material } from '../../scene/materials/material.js'; -import { MeshInstance } from '../../scene/mesh-instance.js'; -import { Entity } from '../../framework/entity.js'; -import { CULLFACE_NONE, CULLFACE_BACK, SEMANTIC_POSITION, SEMANTIC_COLOR } from '../../platform/graphics/constants.js'; -import { BLEND_NORMAL } from '../../scene/constants.js'; -import { createShaderFromCode } from '../../scene/shader-lib/utils.js'; - -import { COLOR_GRAY } from './default-colors.js'; -import { TriData } from './tri-data.js'; -import { Mesh } from '../../scene/mesh.js'; -import { BoxGeometry } from '../../scene/geometry/box-geometry.js'; -import { CylinderGeometry } from '../../scene/geometry/cylinder-geometry.js'; -import { ConeGeometry } from '../../scene/geometry/cone-geometry.js'; -import { PlaneGeometry } from '../../scene/geometry/plane-geometry.js'; -import { TorusGeometry } from '../../scene/geometry/torus-geometry.js'; - -// constants -const SHADOW_DAMP_SCALE = 0.25; -const SHADOW_DAMP_OFFSET = 0.75; -const SHADOW_MESH_MAP = new Map(); - -const TORUS_RENDER_SEGMENTS = 80; -const TORUS_INTERSECT_SEGMENTS = 20; - -const LIGHT_DIR = new Vec3(1, 2, 3); - -const GEOMETRIES = { - box: BoxGeometry, - cone: ConeGeometry, - cylinder: CylinderGeometry, - plane: PlaneGeometry, - torus: TorusGeometry -}; - -const SHADER = { - vert: /* glsl */` - attribute vec3 vertex_position; - attribute vec4 vertex_color; - varying vec4 vColor; - varying vec2 vZW; - uniform mat4 matrix_model; - uniform mat4 matrix_viewProjection; - void main(void) { - gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0); - vColor = vertex_color; - #ifdef GL2 - // store z/w for later use in fragment shader - vZW = gl_Position.zw; - // disable depth clipping - // gl_Position.z = 0.0; - #endif - }`, - frag: /* glsl */` - precision highp float; - varying vec4 vColor; - varying vec2 vZW; - void main(void) { - gl_FragColor = vColor; - #ifdef GL2 - // clamp depth in Z to [0, 1] range - gl_FragDepth = max(0.0, min(1.0, (vZW.x / vZW.y + 1.0) * 0.5)); - #endif - }` -}; - -// temporary variables -const tmpV1 = new Vec3(); -const tmpV2 = new Vec3(); -const tmpQ1 = new Quat(); - -function createShadowMesh(device, entity, type, color = Color.WHITE, templateOpts) { - const Geometry = GEOMETRIES[type]; - if (!Geometry) { - throw new Error('Invalid primitive type.'); - } - - const geom = new Geometry(templateOpts); - geom.colors = []; - - const wtm = entity.getWorldTransform().clone().invert(); - tmpV1.copy(LIGHT_DIR); - wtm.transformVector(tmpV1, tmpV1); - tmpV1.normalize(); - const numVertices = geom.positions.length / 3; - const shadow = calculateShadow(tmpV1, numVertices, geom.normals); - for (let i = 0; i < shadow.length; i++) { - geom.colors.push( - shadow[i] * color.r * 0xFF, - shadow[i] * color.g * 0xFF, - shadow[i] * color.b * 0xFF, - color.a * 0xFF - ); - } - - const shadowMesh = Mesh.fromGeometry(device, geom); - SHADOW_MESH_MAP.set(shadowMesh, shadow); - - return shadowMesh; -} - -function calculateShadow(lightDir, numVertices, normals) { - const shadow = []; - for (let i = 0; i < numVertices; i++) { - const x = normals[i * 3]; - const y = normals[i * 3 + 1]; - const z = normals[i * 3 + 2]; - tmpV2.set(x, y, z); - - const dot = lightDir.dot(tmpV2); - shadow.push(dot * SHADOW_DAMP_SCALE + SHADOW_DAMP_OFFSET); - } - - return shadow; -} - -function setShadowMeshColor(mesh, color) { - if (!SHADOW_MESH_MAP.has(mesh)) { - return; - } - const shadow = SHADOW_MESH_MAP.get(mesh); - const colors = []; - for (let i = 0; i < shadow.length; i++) { - colors.push(shadow[i] * color.r * 255, shadow[i] * color.g * 255, shadow[i] * color.b * 255, color.a * 255); - } - mesh.setColors32(colors); - mesh.update(); -} - -class AxisShape { - _position; - - _rotation; - - _scale; - - _layers = []; - - _disabled; - - _defaultColor = Color.WHITE; - - _hoverColor = Color.BLACK; - - _disabledColor = COLOR_GRAY; - - _cull = CULLFACE_BACK; - - device; - - axis; - - entity; - - triData = []; - - meshInstances = []; - - constructor(device, options) { - this.device = device; - this.axis = options.axis ?? 'x'; - this._position = options.position ?? new Vec3(); - this._rotation = options.rotation ?? new Vec3(); - this._scale = options.scale ?? new Vec3(1, 1, 1); - - this._disabled = options.disabled ?? false; - - this._layers = options.layers ?? this._layers; - - if (options.defaultColor instanceof Color) { - this._defaultColor = options.defaultColor; - } - if (options.hoverColor instanceof Color) { - this._hoverColor = options.hoverColor; - } - if (options.disabledColor instanceof Color) { - this._disabledColor = options.disabledColor; - } - } - - set disabled(value) { - for (let i = 0; i < this.meshInstances.length; i++) { - setShadowMeshColor(this.meshInstances[i].mesh, this._disabledColor); - } - this._disabled = value ?? false; - } - - get disabled() { - return this._disabled; - } - - _createRoot(name) { - this.entity = new Entity(`${name}:${this.axis}`); - this._updateRootTransform(); - } - - _updateRootTransform() { - this.entity.setLocalPosition(this._position); - this.entity.setLocalEulerAngles(this._rotation); - this.entity.setLocalScale(this._scale); - } - - _addRenderMeshes(entity, meshes) { - const shader = createShaderFromCode(this.device, SHADER.vert, SHADER.frag, 'axis-shape', { - vertex_position: SEMANTIC_POSITION, - vertex_color: SEMANTIC_COLOR - }); - - const material = new Material(); - material.shader = shader; - material.cull = this._cull; - material.blendType = BLEND_NORMAL; - material.update(); - - const meshInstances = []; - for (let i = 0; i < meshes.length; i++) { - const mi = new MeshInstance(meshes[i], material); - meshInstances.push(mi); - this.meshInstances.push(mi); - } - entity.addComponent('render', { - meshInstances: meshInstances, - layers: this._layers, - castShadows: false - }); - } - - _addRenderShadowMesh(entity, type) { - const color = this._disabled ? this._disabledColor : this._defaultColor; - const mesh = createShadowMesh(this.device, entity, type, color); - this._addRenderMeshes(entity, [mesh]); - } - - hover(state) { - if (this._disabled) { - return; - } - - for (let i = 0; i < this.meshInstances.length; i++) { - const color = state ? this._hoverColor : this._defaultColor; - setShadowMeshColor(this.meshInstances[i].mesh, color); - } - } - - destroy() { - this.entity.destroy(); - } -} - -class AxisArrow extends AxisShape { - _gap = 0; - - _lineThickness = 0.02; - - _lineLength = 0.5; - - _arrowThickness = 0.12; - - _arrowLength = 0.18; - - _tolerance = 0.1; - - constructor(device, options = {}) { - super(device, options); - - this.triData = [ - new TriData(new ConeGeometry()), - new TriData(new CylinderGeometry(), 1) - ]; - - this._createArrow(); - } - - set gap(value) { - this._gap = value ?? 0; - this._updateHead(); - this._updateLine(); - } - - get gap() { - return this._gap; - } - - set lineThickness(value) { - this._lineThickness = value ?? 1; - this._updateHead(); - this._updateLine(); - } - - get lineThickness() { - return this._lineThickness; - } - - set lineLength(value) { - this._lineLength = value ?? 1; - this._updateHead(); - this._updateLine(); - } - - get lineLength() { - return this._lineLength; - } - - set arrowThickness(value) { - this._arrowThickness = value ?? 1; - this._updateHead(); - } - - get arrowThickness() { - return this._arrowThickness; - } - - set arrowLength(value) { - this._arrowLength = value ?? 1; - this._updateHead(); - } - - get arrowLength() { - return this._arrowLength; - } - - set tolerance(value) { - this._tolerance = value; - this._updateLine(); - } - - get tolerance() { - return this._tolerance; - } - - _createArrow() { - this._createRoot('arrow'); - - // head - this._head = new Entity(`head:${this.axis}`); - this.entity.addChild(this._head); - this._updateHead(); - this._addRenderShadowMesh(this._head, 'cone'); - - // line - this._line = new Entity(`line:${this.axis}`); - this.entity.addChild(this._line); - this._updateLine(); - this._addRenderShadowMesh(this._line, 'cylinder'); - } - - _updateHead() { - // intersect - tmpV1.set(0, this._gap + this._arrowLength * 0.5 + this._lineLength, 0); - tmpQ1.set(0, 0, 0, 1); - tmpV2.set(this._arrowThickness, this._arrowLength, this._arrowThickness); - this.triData[0].setTransform(tmpV1, tmpQ1, tmpV2); - - this._head.setLocalPosition(0, this._gap + this._arrowLength * 0.5 + this._lineLength, 0); - this._head.setLocalScale(this._arrowThickness, this._arrowLength, this._arrowThickness); - } - - _updateLine() { - // intersect - tmpV1.set(0, this._gap + this._lineLength * 0.5, 0); - tmpQ1.set(0, 0, 0, 1); - tmpV2.set(this._lineThickness + this._tolerance, this._lineLength, this._lineThickness + this._tolerance); - this.triData[1].setTransform(tmpV1, tmpQ1, tmpV2); - - // render - this._line.setLocalPosition(0, this._gap + this._lineLength * 0.5, 0); - this._line.setLocalScale(this._lineThickness, this._lineLength, this._lineThickness); - } -} - -class AxisBoxCenter extends AxisShape { - _size = 0.12; - - _tolerance = 0.05; - - constructor(device, options = {}) { - super(device, options); - - this.triData = [ - new TriData(new BoxGeometry(), 2) - ]; - - this._createCenter(); - } - - _createCenter() { - this._createRoot('boxCenter'); - this._updateTransform(); - - // box - this._addRenderShadowMesh(this.entity, 'box'); - } - - set size(value) { - this._size = value ?? 1; - this._updateTransform(); - } - - get size() { - return this._size; - } - - set tolerance(value) { - this._tolerance = value; - this._updateTransform(); - } - - get tolerance() { - return this._tolerance; - } - - _updateTransform() { - // intersect/render - this.entity.setLocalScale(this._size, this._size, this._size); - } -} - -class AxisBoxLine extends AxisShape { - _gap = 0; - - _lineThickness = 0.02; - - _lineLength = 0.5; - - _boxSize = 0.12; - - _tolerance = 0.1; - - constructor(device, options = {}) { - super(device, options); - - this.triData = [ - new TriData(new BoxGeometry()), - new TriData(new CylinderGeometry(), 1) - ]; - - this._createBoxLine(); - } - - set gap(value) { - this._gap = value ?? 0; - this._updateLine(); - this._updateBox(); - } - - get gap() { - return this._gap; - } - - set lineThickness(value) { - this._lineThickness = value ?? 1; - this._updateLine(); - this._updateBox(); - } - - get lineThickness() { - return this._lineThickness; - } - - set lineLength(value) { - this._lineLength = value ?? 1; - this._updateLine(); - this._updateBox(); - } - - get lineLength() { - return this._lineLength; - } - - set boxSize(value) { - this._boxSize = value ?? 1; - this._updateBox(); - } - - get boxSize() { - return this._boxSize; - } - - set tolerance(value) { - this._tolerance = value; - this._updateLine(); - } - - get tolerance() { - return this._tolerance; - } - - _createBoxLine() { - this._createRoot('boxLine'); - - // box - this._box = new Entity(`box:${this.axis}`); - this.entity.addChild(this._box); - this._updateBox(); - this._addRenderShadowMesh(this._box, 'box'); - - // line - this._line = new Entity(`line:${this.axis}`); - this.entity.addChild(this._line); - this._updateLine(); - this._addRenderShadowMesh(this._line, 'cylinder'); - - } - - _updateBox() { - // intersect - tmpV1.set(0, this._gap + this._boxSize * 0.5 + this._lineLength, 0); - tmpQ1.set(0, 0, 0, 1); - tmpV2.set(this._boxSize, this._boxSize, this._boxSize); - this.triData[0].setTransform(tmpV1, tmpQ1, tmpV2); - - // render - this._box.setLocalPosition(0, this._gap + this._boxSize * 0.5 + this._lineLength, 0); - this._box.setLocalScale(this._boxSize, this._boxSize, this._boxSize); - } - - _updateLine() { - // intersect - tmpV1.set(0, this._gap + this._lineLength * 0.5, 0); - tmpQ1.set(0, 0, 0, 1); - tmpV2.set(this._lineThickness + this._tolerance, this._lineLength, this._lineThickness + this._tolerance); - this.triData[1].setTransform(tmpV1, tmpQ1, tmpV2); - - // render - this._line.setLocalPosition(0, this._gap + this._lineLength * 0.5, 0); - this._line.setLocalScale(this._lineThickness, this._lineLength, this._lineThickness); - } -} - -class AxisDisk extends AxisShape { - _tubeRadius = 0.01; - - _ringRadius = 0.5; - - _sectorAngle; - - _lightDir; - - _tolerance = 0.05; - - constructor(device, options = {}) { - super(device, options); - - this._tubeRadius = options.tubeRadius ?? this._tubeRadius; - this._ringRadius = options.ringRadius ?? this._ringRadius; - this._sectorAngle = options.sectorAngle ?? this._sectorAngle; - - this.triData = [ - new TriData(this._createTorusGeometry()) - ]; - - this._createDisk(); - } - - _createTorusGeometry() { - return new TorusGeometry({ - tubeRadius: this._tubeRadius + this._tolerance, - ringRadius: this._ringRadius, - sectorAngle: this._sectorAngle, - segments: TORUS_INTERSECT_SEGMENTS - }); - } - - _createTorusMesh(sectorAngle) { - const color = this._disabled ? this._disabledColor : this._defaultColor; - return createShadowMesh(this.device, this.entity, 'torus', color, { - tubeRadius: this._tubeRadius, - ringRadius: this._ringRadius, - sectorAngle: sectorAngle, - segments: TORUS_RENDER_SEGMENTS - }); - } - - _createDisk() { - this._createRoot('disk'); - - // arc/circle - this._addRenderMeshes(this.entity, [ - this._createTorusMesh(this._sectorAngle), - this._createTorusMesh(360) - ]); - this.drag(false); - } - - set tubeRadius(value) { - this._tubeRadius = value ?? 0.1; - this._updateTransform(); - } - - get tubeRadius() { - return this._tubeRadius; - } - - set ringRadius(value) { - this._ringRadius = value ?? 0.1; - this._updateTransform(); - } - - get ringRadius() { - return this._ringRadius; - } - - set tolerance(value) { - this._tolerance = value; - this._updateTransform(); - } - - get tolerance() { - return this._tolerance; - } - - _updateTransform() { - // intersect - this.triData[0].fromGeometry(this._createTorusGeometry()); - - // render - this.meshInstances[0].mesh = this._createTorusMesh(this._sectorAngle); - this.meshInstances[1].mesh = this._createTorusMesh(360); - } - - drag(state) { - this.meshInstances[0].visible = !state; - this.meshInstances[1].visible = state; - } - - hide(state) { - if (state) { - this.meshInstances[0].visible = false; - this.meshInstances[1].visible = false; - return; - } - - this.drag(false); - } -} - -class AxisPlane extends AxisShape { - _cull = CULLFACE_NONE; - - _size = 0.2; - - _gap = 0.1; - - constructor(device, options = {}) { - super(device, options); - - this.triData = [ - new TriData(new PlaneGeometry()) - ]; - - this._createPlane(); - } - - _getPosition() { - const offset = this._size / 2 + this._gap; - const position = new Vec3(offset, offset, offset); - position[this.axis] = 0; - return position; - } - - _createPlane() { - this._createRoot('plane'); - this._updateTransform(); - - // plane - this._addRenderShadowMesh(this.entity, 'plane'); - } - - set size(value) { - this._size = value ?? 1; - this._updateTransform(); - } - - get size() { - return this._size; - } - - set gap(value) { - this._gap = value ?? 0; - this._updateTransform(); - } - - get gap() { - return this._gap; - } - - _updateTransform() { - // intersect/render - this.entity.setLocalPosition(this._getPosition()); - this.entity.setLocalEulerAngles(this._rotation); - this.entity.setLocalScale(this._size, this._size, this._size); - } -} - -export { AxisShape, AxisArrow, AxisBoxCenter, AxisBoxLine, AxisDisk, AxisPlane }; diff --git a/src/extras/gizmo/default-colors.js b/src/extras/gizmo/color.js similarity index 56% rename from src/extras/gizmo/default-colors.js rename to src/extras/gizmo/color.js index f77bc67743c..64d44342483 100644 --- a/src/extras/gizmo/default-colors.js +++ b/src/extras/gizmo/color.js @@ -5,3 +5,17 @@ export const COLOR_GREEN = Object.freeze(new Color(0.3, 1, 0.3)); export const COLOR_BLUE = Object.freeze(new Color(0.3, 0.3, 1)); export const COLOR_YELLOW = Object.freeze(new Color(1, 1, 0.5)); export const COLOR_GRAY = Object.freeze(new Color(0.5, 0.5, 0.5, 0.5)); + +/** + * Converts Color4 to Color3. + * + * @param {Color} color - Color4 + * @returns {Color} - Color3 + */ +export const color3from4 = (color) => { + return new Color(color.r, color.g, color.b); +}; + +export const color4from3 = (color, a) => { + return new Color(color.r, color.g, color.b, a); +}; diff --git a/src/extras/gizmo/constants.js b/src/extras/gizmo/constants.js new file mode 100644 index 00000000000..45cf04f4bdc --- /dev/null +++ b/src/extras/gizmo/constants.js @@ -0,0 +1,71 @@ +/** + * Local coordinate space. + * + * @type {string} + * @category Gizmo + */ +export const GIZMOSPACE_LOCAL = 'local'; + +/** + * World coordinate space. + * + * @type {string} + * @category Gizmo + */ +export const GIZMOSPACE_WORLD = 'world'; + +/** + * Gizmo axis for the line X. + * + * @type {string} + */ +export const GIZMOAXIS_X = 'x'; + +/** + * Gizmo axis for the line Y. + * + * @type {string} + */ +export const GIZMOAXIS_Y = 'y'; + +/** + * Gizmo axis for the line Z. + * + * @type {string} + */ +export const GIZMOAXIS_Z = 'z'; + +/** + * Gizmo axis for the plane YZ. + * + * @type {string} + */ +export const GIZMOAXIS_YZ = 'yz'; + +/** + * Gizmo axis for the plane XZ. + * + * @type {string} + */ +export const GIZMOAXIS_XZ = 'xz'; + +/** + * Gizmo axis for the plane XY. + * + * @type {string} + */ +export const GIZMOAXIS_XY = 'xy'; + +/** + * Gizmo axis for all directions XYZ. + * + * @type {string} + */ +export const GIZMOAXIS_XYZ = 'xyz'; + +/** + * Gizmo axis for facing the camera (facing the camera). + * + * @type {string} + */ +export const GIZMOAXIS_FACE = 'face'; diff --git a/src/extras/gizmo/gizmo.js b/src/extras/gizmo/gizmo.js index 4fe6ffd8088..cbcbeaac640 100644 --- a/src/extras/gizmo/gizmo.js +++ b/src/extras/gizmo/gizmo.js @@ -1,10 +1,23 @@ +import { Debug } from '../../core/debug.js'; import { math } from '../../core/math/math.js'; import { Vec3 } from '../../core/math/vec3.js'; import { Mat4 } from '../../core/math/mat4.js'; import { Ray } from '../../core/shape/ray.js'; import { EventHandler } from '../../core/event-handler.js'; -import { PROJECTION_PERSPECTIVE } from '../../scene/constants.js'; +import { CameraComponent } from '../../framework/components/camera/component.js'; +import { PROJECTION_PERSPECTIVE, SORTMODE_NONE } from '../../scene/constants.js'; import { Entity } from '../../framework/entity.js'; +import { Layer } from '../../scene/layer.js'; + +import { GIZMOSPACE_LOCAL, GIZMOSPACE_WORLD } from './constants.js'; + +/** + * @typedef {import('../../framework/app-base.js').AppBase} AppBase + * @typedef {import('../../scene/graph-node.js').GraphNode} GraphNode + * @typedef {import('../../platform/graphics/graphics-device.js').GraphicsDevice} GraphicsDevice + * @typedef {import('../../scene/mesh-instance.js').MeshInstance} MeshInstance + * @typedef {import('./tri-data.js').TriData} TriData + */ // temporary variables const tmpV1 = new Vec3(); @@ -13,25 +26,11 @@ const tmpM2 = new Mat4(); const tmpR1 = new Ray(); // constants -const MIN_GIZMO_SCALE = 1e-4; +const LAYER_NAME = 'Gizmo'; +const MIN_SCALE = 1e-4; const PERS_SCALE_RATIO = 0.3; const ORTHO_SCALE_RATIO = 0.32; - -/** - * Local coordinate space. - * - * @type {string} - * @category Gizmo - */ -export const GIZMO_LOCAL = 'local'; - -/** - * World coordinate space. - * - * @type {string} - * @category Gizmo - */ -export const GIZMO_WORLD = 'world'; +const UPDATE_EPSILON = 1e-6; /** * The base class for all gizmos. @@ -164,17 +163,17 @@ class Gizmo extends EventHandler { _scale = 1; /** - * Internal version of coordinate space. Defaults to {@link GIZMO_WORLD}. + * Internal version of coordinate space. Defaults to {@link GIZMOSPACE_WORLD}. * * @type {string} * @protected */ - _coordSpace = GIZMO_WORLD; + _coordSpace = GIZMOSPACE_WORLD; /** * Internal reference to the app containing the gizmo. * - * @type {import('../../framework/app-base.js').AppBase} + * @type {AppBase} * @protected */ _app; @@ -182,7 +181,7 @@ class Gizmo extends EventHandler { /** * Internal reference to the graphics device of the app. * - * @type {import('../../platform/graphics/graphics-device.js').GraphicsDevice} + * @type {GraphicsDevice} * @protected */ _device; @@ -190,7 +189,7 @@ class Gizmo extends EventHandler { /** * Internal reference to camera component to view the gizmo. * - * @type {import('../../framework/components/camera/component.js').CameraComponent} + * @type {CameraComponent} * @protected */ _camera; @@ -198,7 +197,7 @@ class Gizmo extends EventHandler { /** * Internal reference to layer to render the gizmo.. * - * @type {import('../../scene/layer.js').Layer} + * @type {Layer} * @protected */ _layer; @@ -206,24 +205,24 @@ class Gizmo extends EventHandler { /** * The graph nodes attached to the gizmo. * - * @type {import('../../scene/graph-node.js').GraphNode[]} + * @type {GraphNode[]} */ nodes = []; /** * The root gizmo entity. * - * @type {import('../../framework/entity.js').Entity} + * @type {Entity} */ root; /** * @typedef IntersectData - * @property {import('./tri-data.js').TriData[]} triData - The array of {@link TriData} - * @property {import('../../scene/graph-node.js').GraphNode} parent - The mesh parent node. - * @property {import('../../scene/mesh-instance.js').MeshInstance[]} meshInstances - - * array of mesh instances for rendering + * @property {TriData[]} triData - The array of {@link TriData}. + * @property {GraphNode} parent - The mesh parent node. + * @property {MeshInstance[]} meshInstances - Array of mesh instances for rendering. */ + /** * The intersection data object. * @@ -231,79 +230,89 @@ class Gizmo extends EventHandler { */ intersectData = []; + /** + * Creates a new gizmo layer and adds it to the scene. + * + * @param {AppBase} app - The app. + * @param {string} [layerName] - The layer name. Defaults to 'Gizmo'. + * @param {number} [layerIndex] - The layer index. Defaults to the end of the layer list. + * @returns {Layer} The new layer. + */ + static createLayer(app, layerName = LAYER_NAME, layerIndex) { + const layer = new Layer({ + name: layerName, + clearDepthBuffer: true, + opaqueSortMode: SORTMODE_NONE, + transparentSortMode: SORTMODE_NONE + }); + app.scene.layers.insert(layer, layerIndex ?? app.scene.layers.layerList.length); + return layer; + } + /** * Creates a new Gizmo object. * - * @param {import('../../framework/app-base.js').AppBase} app - The application instance. - * @param {import('../../framework/components/camera/component.js').CameraComponent} camera - - * The camera component. - * @param {import('../../scene/layer.js').Layer} layer - The render layer. - * @example + * @param {CameraComponent} camera - The camera component. + * @param {Layer} layer - The render layer. This can be provided by the user or will be created + * and added to the scene and camera if not provided. Successive gizmos will share the same layer + * and will be removed from the camera and scene when the last gizmo is destroyed. * const gizmo = new pc.Gizmo(app, camera, layer); */ - constructor(app, camera, layer) { + constructor(camera, layer) { + Debug.assert(camera instanceof CameraComponent, 'Incorrect parameters for Gizmos\'s constructor. Use new Gizmo(camera, layer)'); super(); - this._app = app; - this._device = app.graphicsDevice; - this._deviceStartSize = Math.max(this._device.width, this._device.height); this._camera = camera; + this._app = camera.system.app; + this._device = this._app.graphicsDevice; + this._layer = layer; + camera.layers = camera.layers.concat(layer.id); - this._createGizmo(); + this.root = new Entity('gizmo'); + this._app.root.addChild(this.root); + this.root.enabled = false; this._updateScale(); - this._onPointerDown = (e) => { - if (!this.root.enabled || document.pointerLockElement) { - return; - } - const selection = this._getSelection(e.offsetX, e.offsetY); - if (selection[0]) { - e.preventDefault(); - e.stopPropagation(); - } - this.fire(Gizmo.EVENT_POINTERDOWN, e.offsetX, e.offsetY, selection[0]); - }; - this._onPointerMove = (e) => { - if (!this.root.enabled || document.pointerLockElement) { - return; - } - const selection = this._getSelection(e.offsetX, e.offsetY); - if (selection[0]) { - e.preventDefault(); - e.stopPropagation(); - } - this.fire(Gizmo.EVENT_POINTERMOVE, e.offsetX, e.offsetY, selection[0]); - }; - this._onPointerUp = (e) => { - if (!this.root.enabled || document.pointerLockElement) { - return; - } - this.fire(Gizmo.EVENT_POINTERUP); - }; + this._onPointerDown = this._onPointerDown.bind(this); + this._onPointerMove = this._onPointerMove.bind(this); + this._onPointerUp = this._onPointerUp.bind(this); this._device.canvas.addEventListener('pointerdown', this._onPointerDown, true); this._device.canvas.addEventListener('pointermove', this._onPointerMove, true); this._device.canvas.addEventListener('pointerup', this._onPointerUp); - app.on('update', () => this._updateScale()); + this._app.on('update', () => { + this._updatePosition(); + this._updateRotation(); + this._updateScale(); + }); + + this._app.on('destroy', () => this.destroy()); + } - app.on('destroy', () => this.destroy()); + /** + * Sets the gizmo render layer. + * + * @type {Layer} + */ + get layer() { + return this._layer; } /** * Sets the gizmo coordinate space. Can be: * - * - {@link GIZMO_LOCAL} - * - {@link GIZMO_WORLD} + * - {@link GIZMOSPACE_LOCAL} + * - {@link GIZMOSPACE_WORLD} * - * Defaults to {@link GIZMO_WORLD}. + * Defaults to {@link GIZMOSPACE_WORLD}. * * @type {string} */ set coordSpace(value) { - this._coordSpace = value ?? GIZMO_WORLD; + this._coordSpace = value ?? GIZMOSPACE_WORLD; this._updateRotation(); } @@ -335,12 +344,51 @@ class Gizmo extends EventHandler { return this._size; } - _createGizmo() { - this.root = new Entity('gizmo'); - this._app.root.addChild(this.root); - this.root.enabled = false; + /** + * @param {PointerEvent} e - The pointer event. + * @private + */ + _onPointerDown(e) { + if (!this.root.enabled || document.pointerLockElement) { + return; + } + const selection = this._getSelection(e.offsetX, e.offsetY); + if (selection[0]) { + e.preventDefault(); + e.stopPropagation(); + } + this.fire(Gizmo.EVENT_POINTERDOWN, e.offsetX, e.offsetY, selection[0]); } + /** + * @param {PointerEvent} e - The pointer event. + * @private + */ + _onPointerMove(e) { + if (!this.root.enabled || document.pointerLockElement) { + return; + } + const selection = this._getSelection(e.offsetX, e.offsetY); + if (selection[0]) { + e.preventDefault(); + e.stopPropagation(); + } + this.fire(Gizmo.EVENT_POINTERMOVE, e.offsetX, e.offsetY, selection[0]); + } + + /** + * @private + */ + _onPointerUp() { + if (!this.root.enabled || document.pointerLockElement) { + return; + } + this.fire(Gizmo.EVENT_POINTERUP); + } + + /** + * @protected + */ _updatePosition() { tmpV1.set(0, 0, 0); for (let i = 0; i < this.nodes.length; i++) { @@ -348,21 +396,35 @@ class Gizmo extends EventHandler { tmpV1.add(node.getPosition()); } tmpV1.mulScalar(1.0 / (this.nodes.length || 1)); - this.root.setPosition(tmpV1); + if (tmpV1.distance(this.root.getPosition()) < UPDATE_EPSILON) { + return; + } + + this.root.setPosition(tmpV1); this.fire(Gizmo.EVENT_POSITIONUPDATE, tmpV1); } + /** + * @protected + */ _updateRotation() { tmpV1.set(0, 0, 0); - if (this._coordSpace === GIZMO_LOCAL && this.nodes.length !== 0) { + if (this._coordSpace === GIZMOSPACE_LOCAL && this.nodes.length !== 0) { tmpV1.copy(this.nodes[this.nodes.length - 1].getEulerAngles()); } - this.root.setEulerAngles(tmpV1); + if (tmpV1.distance(this.root.getEulerAngles()) < UPDATE_EPSILON) { + return; + } + + this.root.setEulerAngles(tmpV1); this.fire(Gizmo.EVENT_ROTATIONUPDATE, tmpV1); } + /** + * @protected + */ _updateScale() { if (this._camera.projection === PROJECTION_PERSPECTIVE) { const gizmoPos = this.root.getPosition(); @@ -372,12 +434,22 @@ class Gizmo extends EventHandler { } else { this._scale = this._camera.orthoHeight * ORTHO_SCALE_RATIO; } - this._scale = Math.max(this._scale * this._size, MIN_GIZMO_SCALE); - this.root.setLocalScale(this._scale, this._scale, this._scale); + this._scale = Math.max(this._scale * this._size, MIN_SCALE); + + if (Math.abs(this._scale - this.root.getLocalScale().x) < UPDATE_EPSILON) { + return; + } + this.root.setLocalScale(this._scale, this._scale, this._scale); this.fire(Gizmo.EVENT_SCALEUPDATE, this._scale); } + /** + * @param {number} x - The x coordinate. + * @param {number} y - The y coordinate. + * @returns {MeshInstance[]} - The mesh instances. + * @private + */ _getSelection(x, y) { const start = this._camera.screenToWorld(x, y, this._camera.nearClip); const end = this._camera.screenToWorld(x, y, this._camera.farClip); @@ -386,19 +458,23 @@ class Gizmo extends EventHandler { const selection = []; for (let i = 0; i < this.intersectData.length; i++) { const { triData, parent, meshInstances } = this.intersectData[i]; - const wtm = parent.getWorldTransform(); + const parentTM = parent.getWorldTransform(); for (let j = 0; j < triData.length; j++) { - const { tris, ptm, priority } = triData[j]; - tmpM1.copy(wtm).mul(ptm); - tmpM2.copy(tmpM1).invert(); - tmpM2.transformPoint(start, tmpR1.origin); - tmpM2.transformVector(dir, tmpR1.direction); - tmpR1.direction.normalize(); + const { tris, transform, priority } = triData[j]; + + // combine node world transform with transform of tri relative to parent + const triWTM = tmpM1.copy(parentTM).mul(transform); + const invTriWTM = tmpM2.copy(triWTM).invert(); + + const ray = tmpR1; + invTriWTM.transformPoint(start, ray.origin); + invTriWTM.transformVector(dir, ray.direction); + ray.direction.normalize(); for (let k = 0; k < tris.length; k++) { - if (tris[k].intersectsRay(tmpR1, tmpV1)) { + if (tris[k].intersectsRay(ray, tmpV1)) { selection.push({ - dist: tmpM1.transformPoint(tmpV1).sub(start).length(), + dist: triWTM.transformPoint(tmpV1).sub(start).length(), meshInstances: meshInstances, priority: priority }); @@ -423,19 +499,23 @@ class Gizmo extends EventHandler { /** * Attach an array of graph nodes to the gizmo. * - * @param {import('../../scene/graph-node.js').GraphNode[]} [nodes] - The graph nodes. Defaults to []. + * @param {GraphNode[] | GraphNode} [nodes] - The graph nodes. Defaults to []. * @example * const gizmo = new pc.Gizmo(app, camera, layer); * gizmo.attach([boxA, boxB]); */ attach(nodes = []) { - if (nodes.length === 0) { - return; + if (Array.isArray(nodes)) { + if (nodes.length === 0) { + return; + } + this.nodes = nodes; + } else { + this.nodes = [nodes]; } - - this.nodes = nodes; this._updatePosition(); this._updateRotation(); + this._updateScale(); this.fire(Gizmo.EVENT_NODESATTACH); @@ -462,8 +542,7 @@ class Gizmo extends EventHandler { } /** - * Destroys the gizmo instance; detaches - * all graph nodes. + * Detaches all graph nodes and destroys the gizmo instance. * * @example * const gizmo = new pc.Gizmo(app, camera, layer); diff --git a/src/extras/gizmo/rotate-gizmo.js b/src/extras/gizmo/rotate-gizmo.js index d24f343bd52..b8fe847f97a 100644 --- a/src/extras/gizmo/rotate-gizmo.js +++ b/src/extras/gizmo/rotate-gizmo.js @@ -5,10 +5,16 @@ import { Mat4 } from '../../core/math/mat4.js'; import { Vec3 } from '../../core/math/vec3.js'; import { PROJECTION_ORTHOGRAPHIC, PROJECTION_PERSPECTIVE } from '../../scene/constants.js'; -import { AxisDisk } from './axis-shapes.js'; -import { GIZMO_LOCAL } from './gizmo.js'; +import { ArcShape } from './shape/arc-shape.js'; +import { GIZMOSPACE_LOCAL, GIZMOAXIS_FACE, GIZMOAXIS_X, GIZMOAXIS_Y, GIZMOAXIS_Z } from './constants.js'; import { TransformGizmo } from './transform-gizmo.js'; +/** + * @typedef {import('../../framework/components/camera/component.js').CameraComponent} CameraComponent + * @typedef {import('../../scene/graph-node.js').GraphNode} GraphNode + * @typedef {import('../../scene/layer.js').Layer} Layer + */ + // temporary variables const tmpV1 = new Vec3(); const tmpV2 = new Vec3(); @@ -19,6 +25,7 @@ const tmpQ2 = new Quat(); // constants const FACING_THRESHOLD = 0.9; const ROTATE_SCALE = 900; +const GUIDE_ANGLE_COLOR = new Color(0, 0, 0, 0.3); /** * Rotation gizmo. @@ -27,36 +34,40 @@ const ROTATE_SCALE = 900; */ class RotateGizmo extends TransformGizmo { _shapes = { - z: new AxisDisk(this._device, { - axis: 'z', + z: new ArcShape(this._device, { + axis: GIZMOAXIS_Z, layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(90, 0, 90), defaultColor: this._meshColors.axis.z, hoverColor: this._meshColors.hover.z, sectorAngle: 180 }), - x: new AxisDisk(this._device, { - axis: 'x', + x: new ArcShape(this._device, { + axis: GIZMOAXIS_X, layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(0, 0, -90), defaultColor: this._meshColors.axis.x, hoverColor: this._meshColors.hover.x, sectorAngle: 180 }), - y: new AxisDisk(this._device, { - axis: 'y', + y: new ArcShape(this._device, { + axis: GIZMOAXIS_Y, layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(0, 0, 0), defaultColor: this._meshColors.axis.y, hoverColor: this._meshColors.hover.y, sectorAngle: 180 }), - face: new AxisDisk(this._device, { - axis: 'face', + face: new ArcShape(this._device, { + axis: GIZMOAXIS_FACE, layers: [this._layer.id], + shading: this._shading, rotation: this._getLookAtEulerAngles(this._camera.entity.getPosition()), - defaultColor: this._meshColors.axis.face, - hoverColor: this._meshColors.hover.face, + defaultColor: this._meshColors.axis.f, + hoverColor: this._meshColors.hover.f, ringRadius: 0.55 }) }; @@ -64,7 +75,7 @@ class RotateGizmo extends TransformGizmo { /** * Internal mapping from each attached node to their starting rotation in local space. * - * @type {Map} + * @type {Map} * @private */ _nodeLocalRotations = new Map(); @@ -72,7 +83,7 @@ class RotateGizmo extends TransformGizmo { /** * Internal mapping from each attached node to their starting rotation in world space. * - * @type {Map} + * @type {Map} * @private */ _nodeRotations = new Map(); @@ -80,7 +91,7 @@ class RotateGizmo extends TransformGizmo { /** * Internal mapping from each attached node to their offset position from the gizmo. * - * @type {Map} + * @type {Map} * @private */ _nodeOffsets = new Map(); @@ -91,7 +102,7 @@ class RotateGizmo extends TransformGizmo { * @type {Color} * @private */ - _guideAngleStartColor = new Color(0, 0, 0, 0.3); + _guideAngleStartColor = GUIDE_ANGLE_COLOR.clone(); /** * Internal vector for the start point of the guide line angle. @@ -117,19 +128,17 @@ class RotateGizmo extends TransformGizmo { /** * Creates a new RotateGizmo object. * - * @param {import('../../framework/app-base.js').AppBase} app - The application instance. - * @param {import('../../framework/components/camera/component.js').CameraComponent} camera - - * The camera component. - * @param {import('../../scene/layer.js').Layer} layer - The render layer. + * @param {CameraComponent} camera - The camera component. + * @param {Layer} layer - The render layer. * @example * const gizmo = new pc.RotateGizmo(app, camera, layer); */ - constructor(app, camera, layer) { - super(app, camera, layer); + constructor(camera, layer) { + super(camera, layer); this._createTransform(); - this.on('transform:start', () => { + this.on(TransformGizmo.EVENT_TRANSFORMSTART, () => { this._storeNodeRotations(); // store guide points @@ -139,7 +148,7 @@ class RotateGizmo extends TransformGizmo { this._drag(true); }); - this.on('transform:move', (pointDelta, angleDelta) => { + this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (pointDelta, angleDelta) => { const axis = this._selectedAxis; if (this.snap) { @@ -150,17 +159,17 @@ class RotateGizmo extends TransformGizmo { this._updateGuidePoints(angleDelta); }); - this.on('transform:end', () => { + this.on(TransformGizmo.EVENT_TRANSFORMEND, () => { this._drag(false); }); - this.on('nodes:detach', () => { + this.on(TransformGizmo.EVENT_NODESDETACH, () => { this._nodeLocalRotations.clear(); this._nodeRotations.clear(); this._nodeOffsets.clear(); }); - app.on('update', () => { + this._app.on('update', () => { this._faceAxisLookAtCamera(); this._xyzAxisLookAtCamera(); @@ -264,16 +273,24 @@ class RotateGizmo extends TransformGizmo { return this._shapes.x.tolerance; } + /** + * @param {string} prop - The property. + * @param {any} value - The value. + * @private + */ _setDiskProp(prop, value) { this._shapes.x[prop] = value; this._shapes.y[prop] = value; this._shapes.z[prop] = value; } + /** + * @private + */ _storeGuidePoints() { const gizmoPos = this.root.getPosition(); const axis = this._selectedAxis; - const isFacing = axis === 'face'; + const isFacing = axis === GIZMOAXIS_FACE; const scale = isFacing ? this.faceRingRadius : this.xyzRingRadius; this._guideAngleStart.copy(this._selectionStartPoint).sub(gizmoPos).normalize(); @@ -281,11 +298,15 @@ class RotateGizmo extends TransformGizmo { this._guideAngleEnd.copy(this._guideAngleStart); } + /** + * @param {number} angleDelta - The angle delta. + * @private + */ _updateGuidePoints(angleDelta) { const gizmoPos = this.root.getPosition(); const cameraPos = this._camera.entity.getPosition(); const axis = this._selectedAxis; - const isFacing = axis === 'face'; + const isFacing = axis === GIZMOAXIS_FACE; tmpV1.set(0, 0, 0); if (isFacing) { @@ -298,12 +319,24 @@ class RotateGizmo extends TransformGizmo { tmpQ1.transformVector(this._guideAngleStart, this._guideAngleEnd); } + /** + * @param {Vec3} pos - The position. + * @param {string} axis - The axis. + * @param {Vec3} point - The point. + * @param {Color} [color] - The color. + * @private + */ _drawGuideAngleLine(pos, axis, point, color = this._guideColors[axis]) { tmpV1.set(0, 0, 0); tmpV2.copy(point).mulScalar(this._scale); this._app.drawLine(tmpV1.add(pos), tmpV2.add(pos), color, false, this._layer); } + /** + * @param {Vec3} position - The position. + * @returns {Vec3} The look at euler angles. + * @private + */ _getLookAtEulerAngles(position) { tmpV1.set(0, 0, 0); tmpM1.setLookAt(tmpV1, position, Vec3.UP); @@ -313,6 +346,9 @@ class RotateGizmo extends TransformGizmo { return tmpV1; } + /** + * @private + */ _faceAxisLookAtCamera() { if (this._camera.projection === PROJECTION_PERSPECTIVE) { this._shapes.face.entity.lookAt(this._camera.entity.getPosition()); @@ -325,6 +361,9 @@ class RotateGizmo extends TransformGizmo { } } + /** + * @private + */ _xyzAxisLookAtCamera() { if (this._camera.projection === PROJECTION_PERSPECTIVE) { const gizmoPos = this.root.getPosition(); @@ -342,6 +381,10 @@ class RotateGizmo extends TransformGizmo { this._shapes.z.entity.setLocalEulerAngles(90, 0, angle + 90); } + /** + * @param {boolean} state - The state. + * @private + */ _drag(state) { for (const axis in this._shapes) { const shape = this._shapes[axis]; @@ -351,9 +394,12 @@ class RotateGizmo extends TransformGizmo { shape.hide(state); } } - this.fire('render:update'); + this.fire(TransformGizmo.EVENT_RENDERUPDATE); } + /** + * @private + */ _storeNodeRotations() { const gizmoPos = this.root.getPosition(); for (let i = 0; i < this.nodes.length; i++) { @@ -364,12 +410,21 @@ class RotateGizmo extends TransformGizmo { } } + /** + * @param {string} axis - The axis. + * @param {number} angleDelta - The angle delta. + * @private + */ _setNodeRotations(axis, angleDelta) { const gizmoPos = this.root.getPosition(); const cameraPos = this._camera.entity.getPosition(); - const isFacing = axis === 'face'; + const isFacing = axis === GIZMOAXIS_FACE; for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i]; + const rot = this._nodeRotations.get(node); + if (!rot) { + continue; + } if (isFacing) { tmpV1.copy(cameraPos).sub(gizmoPos).normalize(); @@ -380,13 +435,17 @@ class RotateGizmo extends TransformGizmo { tmpQ1.setFromAxisAngle(tmpV1, angleDelta); - if (!isFacing && this._coordSpace === GIZMO_LOCAL) { - tmpQ2.copy(this._nodeLocalRotations.get(node)).mul(tmpQ1); + if (!isFacing && this._coordSpace === GIZMOSPACE_LOCAL) { + tmpQ2.copy(rot).mul(tmpQ1); node.setLocalRotation(tmpQ2); } else { - tmpV1.copy(this._nodeOffsets.get(node)); + const offset = this._nodeOffsets.get(node); + if (!offset) { + continue; + } + tmpV1.copy(offset); tmpQ1.transformVector(tmpV1, tmpV1); - tmpQ2.copy(tmpQ1).mul(this._nodeRotations.get(node)); + tmpQ2.copy(tmpQ1).mul(rot); // N.B. Rotation via quaternion when scale inverted causes scale warping? node.setEulerAngles(tmpQ2.getEulerAngles()); @@ -394,11 +453,17 @@ class RotateGizmo extends TransformGizmo { } } - if (this._coordSpace === GIZMO_LOCAL) { + if (this._coordSpace === GIZMOSPACE_LOCAL) { this._updateRotation(); } } + /** + * @param {number} x - The x coordinate. + * @param {number} y - The y coordinate. + * @returns {{ point: Vec3, angle: number }} The point and angle. + * @protected + */ _screenToPoint(x, y) { const gizmoPos = this.root.getPosition(); const mouseWPos = this._camera.screenToWorld(x, y, 1); @@ -406,7 +471,7 @@ class RotateGizmo extends TransformGizmo { const axis = this._selectedAxis; const ray = this._createRay(mouseWPos); - const plane = this._createPlane(axis, axis === 'face', false); + const plane = this._createPlane(axis, axis === GIZMOAXIS_FACE, false); const point = new Vec3(); let angle = 0; @@ -416,7 +481,7 @@ class RotateGizmo extends TransformGizmo { // calculate angle const facingDir = tmpV1.sub2(ray.origin, gizmoPos).normalize(); const facingDot = plane.normal.dot(facingDir); - if (axis === 'face' || Math.abs(facingDot) > FACING_THRESHOLD) { + if (axis === GIZMOAXIS_FACE || Math.abs(facingDot) > FACING_THRESHOLD) { // plane facing camera so based on mouse position around gizmo tmpQ1.copy(this._camera.entity.getRotation()).invert(); diff --git a/src/extras/gizmo/scale-gizmo.js b/src/extras/gizmo/scale-gizmo.js index ad6f43ea4d4..7bac51d6faf 100644 --- a/src/extras/gizmo/scale-gizmo.js +++ b/src/extras/gizmo/scale-gizmo.js @@ -1,9 +1,17 @@ import { Vec3 } from '../../core/math/vec3.js'; import { Quat } from '../../core/math/quat.js'; -import { AxisBoxCenter, AxisBoxLine, AxisPlane } from './axis-shapes.js'; -import { GIZMO_LOCAL } from './gizmo.js'; +import { GIZMOSPACE_LOCAL, GIZMOAXIS_X, GIZMOAXIS_XYZ, GIZMOAXIS_Y, GIZMOAXIS_Z } from './constants.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'; + +/** + * @typedef {import('../../framework/components/camera/component.js').CameraComponent} CameraComponent + * @typedef {import('../../scene/graph-node.js').GraphNode} GraphNode + * @typedef {import('../../scene/layer.js').Layer} Layer + */ // temporary variables const tmpV1 = new Vec3(); @@ -17,65 +25,69 @@ const tmpQ1 = new Quat(); */ class ScaleGizmo extends TransformGizmo { _shapes = { - xyz: new AxisBoxCenter(this._device, { - axis: 'xyz', + xyz: new BoxShape(this._device, { + axis: GIZMOAXIS_XYZ, layers: [this._layer.id], + shading: this._shading, defaultColor: this._meshColors.axis.xyz, hoverColor: this._meshColors.hover.xyz }), - yz: new AxisPlane(this._device, { - axis: 'x', - flipAxis: 'y', + yz: new PlaneShape(this._device, { + axis: GIZMOAXIS_X, layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(0, 0, -90), defaultColor: this._meshColors.axis.x, hoverColor: this._meshColors.hover.x }), - xz: new AxisPlane(this._device, { - axis: 'y', - flipAxis: 'z', + xz: new PlaneShape(this._device, { + axis: GIZMOAXIS_Y, layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(0, 0, 0), defaultColor: this._meshColors.axis.y, hoverColor: this._meshColors.hover.y }), - xy: new AxisPlane(this._device, { - axis: 'z', - flipAxis: 'x', + xy: new PlaneShape(this._device, { + axis: GIZMOAXIS_Z, layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(90, 0, 0), defaultColor: this._meshColors.axis.z, hoverColor: this._meshColors.hover.z }), - x: new AxisBoxLine(this._device, { - axis: 'x', + x: new BoxLineShape(this._device, { + axis: GIZMOAXIS_X, layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(0, 0, -90), defaultColor: this._meshColors.axis.x, hoverColor: this._meshColors.hover.x }), - y: new AxisBoxLine(this._device, { - axis: 'y', + y: new BoxLineShape(this._device, { + axis: GIZMOAXIS_Y, layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(0, 0, 0), defaultColor: this._meshColors.axis.y, hoverColor: this._meshColors.hover.y }), - z: new AxisBoxLine(this._device, { - axis: 'z', + z: new BoxLineShape(this._device, { + axis: GIZMOAXIS_Z, layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(90, 0, 0), defaultColor: this._meshColors.axis.z, hoverColor: this._meshColors.hover.z }) }; - _coordSpace = GIZMO_LOCAL; + _coordSpace = GIZMOSPACE_LOCAL; /** * Internal mapping from each attached node to their starting scale. * - * @type {Map} + * @type {Map} * @private */ _nodeScales = new Map(); @@ -96,24 +108,22 @@ class ScaleGizmo extends TransformGizmo { /** * Creates a new ScaleGizmo object. * - * @param {import('../../framework/app-base.js').AppBase} app - The application instance. - * @param {import('../../framework/components/camera/component.js').CameraComponent} camera - - * The camera component. - * @param {import('../../scene/layer.js').Layer} layer - The render layer. + * @param {CameraComponent} camera - The camera component. + * @param {Layer} layer - The render layer. * @example * const gizmo = new pc.ScaleGizmo(app, camera, layer); */ - constructor(app, camera, layer) { - super(app, camera, layer); + constructor(camera, layer) { + super(camera, layer); this._createTransform(); - this.on('transform:start', () => { + this.on(TransformGizmo.EVENT_TRANSFORMSTART, () => { this._selectionStartPoint.sub(Vec3.ONE); this._storeNodeScales(); }); - this.on('transform:move', (pointDelta) => { + this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (pointDelta) => { if (this.snap) { pointDelta.mulScalar(1 / this.snapIncrement); pointDelta.round(); @@ -122,7 +132,7 @@ class ScaleGizmo extends TransformGizmo { this._setNodeScales(pointDelta); }); - this.on('nodes:detach', () => { + this.on(TransformGizmo.EVENT_NODESDETACH, () => { this._nodeScales.clear(); }); } @@ -315,18 +325,31 @@ class ScaleGizmo extends TransformGizmo { return this._shapes.xyz.tolerance; } + /** + * @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 + */ _storeNodeScales() { for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i]; @@ -334,13 +357,27 @@ class ScaleGizmo extends TransformGizmo { } } + /** + * @param {Vec3} pointDelta - The point delta. + * @private + */ _setNodeScales(pointDelta) { for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i]; - node.setLocalScale(this._nodeScales.get(node).clone().mul(pointDelta)); + const scale = this._nodeScales.get(node); + if (!scale) { + continue; + } + node.setLocalScale(scale.clone().mul(pointDelta)); } } + /** + * @param {number} x - The x coordinate. + * @param {number} y - The y coordinate. + * @returns {{ point: Vec3, angle: number }} The point and angle. + * @protected + */ _screenToPoint(x, y) { const gizmoPos = this.root.getPosition(); const mouseWPos = this._camera.screenToWorld(x, y, 1); @@ -348,7 +385,7 @@ class ScaleGizmo extends TransformGizmo { const axis = this._selectedAxis; const isPlane = this._selectedIsPlane; - const isScaleUniform = (this._useUniformScaling && isPlane) || axis === 'xyz'; + const isScaleUniform = (this._useUniformScaling && isPlane) || axis === GIZMOAXIS_XYZ; const ray = this._createRay(mouseWPos); const plane = this._createPlane(axis, isScaleUniform, !isPlane); @@ -361,15 +398,15 @@ class ScaleGizmo extends TransformGizmo { if (isScaleUniform) { // calculate projecion vector for scale direction switch (axis) { - case 'x': + case GIZMOAXIS_X: tmpV1.copy(this.root.up); tmpV2.copy(this.root.forward).mulScalar(-1); break; - case 'y': + case GIZMOAXIS_Y: tmpV1.copy(this.root.right); tmpV2.copy(this.root.forward).mulScalar(-1); break; - case 'z': + case GIZMOAXIS_Z: tmpV1.copy(this.root.up); tmpV2.copy(this.root.right); break; @@ -386,7 +423,7 @@ class ScaleGizmo extends TransformGizmo { point.set(v, v, v); // keep scale of axis constant if not all axes are selected - if (axis !== 'xyz') { + if (axis !== GIZMOAXIS_XYZ) { point[axis] = 1; } diff --git a/src/extras/gizmo/shape/arc-shape.js b/src/extras/gizmo/shape/arc-shape.js new file mode 100644 index 00000000000..273fc42f2ce --- /dev/null +++ b/src/extras/gizmo/shape/arc-shape.js @@ -0,0 +1,115 @@ +import { TorusGeometry } from '../../../scene/geometry/torus-geometry.js'; +import { TriData } from '../tri-data.js'; +import { Shape } from './shape.js'; + +const TORUS_RENDER_SEGMENTS = 80; +const TORUS_INTERSECT_SEGMENTS = 20; + +class ArcShape extends Shape { + _tubeRadius = 0.01; + + _ringRadius = 0.5; + + _sectorAngle; + + _lightDir; + + _tolerance = 0.05; + + constructor(device, options = {}) { + super(device, options); + + this._tubeRadius = options.tubeRadius ?? this._tubeRadius; + this._ringRadius = options.ringRadius ?? this._ringRadius; + this._sectorAngle = options.sectorAngle ?? this._sectorAngle; + + this.triData = [ + new TriData(this._createTorusGeometry()) + ]; + + this._createDisk(); + } + + _createTorusGeometry() { + return new TorusGeometry({ + tubeRadius: this._tubeRadius + this._tolerance, + ringRadius: this._ringRadius, + sectorAngle: this._sectorAngle, + segments: TORUS_INTERSECT_SEGMENTS + }); + } + + _createTorusMesh(sectorAngle) { + const geom = new TorusGeometry({ + tubeRadius: this._tubeRadius, + ringRadius: this._ringRadius, + sectorAngle: sectorAngle, + segments: TORUS_RENDER_SEGMENTS + }); + return this._createMesh(geom, this._shading); + } + + _createDisk() { + this._createRoot('disk'); + + // arc/circle + this._createRenderComponent(this.entity, [ + this._createTorusMesh(this._sectorAngle), + this._createTorusMesh(360) + ]); + this.drag(false); + } + + set tubeRadius(value) { + this._tubeRadius = value ?? 0.1; + this._updateTransform(); + } + + get tubeRadius() { + return this._tubeRadius; + } + + set ringRadius(value) { + this._ringRadius = value ?? 0.1; + this._updateTransform(); + } + + get ringRadius() { + return this._ringRadius; + } + + set tolerance(value) { + this._tolerance = value; + this._updateTransform(); + } + + get tolerance() { + return this._tolerance; + } + + _updateTransform() { + // intersect + this.triData[0].fromGeometry(this._createTorusGeometry()); + + // render + this.meshInstances[0].mesh = this._createTorusMesh(this._sectorAngle); + this.meshInstances[1].mesh = this._createTorusMesh(360); + } + + drag(state) { + this.meshInstances[0].visible = !state; + this.meshInstances[1].visible = state; + } + + hide(state) { + if (state) { + this.meshInstances[0].visible = false; + this.meshInstances[1].visible = false; + return; + } + + this.drag(false); + } +} + +export { ArcShape }; diff --git a/src/extras/gizmo/shape/arrow-shape.js b/src/extras/gizmo/shape/arrow-shape.js new file mode 100644 index 00000000000..78064e102cd --- /dev/null +++ b/src/extras/gizmo/shape/arrow-shape.js @@ -0,0 +1,138 @@ +import { Quat } from '../../../core/math/quat.js'; +import { Vec3 } from '../../../core/math/vec3.js'; +import { Entity } from '../../../framework/entity.js'; +import { ConeGeometry } from '../../../scene/geometry/cone-geometry.js'; +import { CylinderGeometry } from '../../../scene/geometry/cylinder-geometry.js'; +import { TriData } from '../tri-data.js'; +import { Shape } from './shape.js'; + +const tmpV1 = new Vec3(); +const tmpV2 = new Vec3(); +const tmpQ1 = new Quat(); + +class ArrowShape extends Shape { + _gap = 0; + + _lineThickness = 0.02; + + _lineLength = 0.5; + + _arrowThickness = 0.12; + + _arrowLength = 0.18; + + _tolerance = 0.1; + + _head; + + _line; + + constructor(device, options = {}) { + super(device, options); + + this.triData = [ + new TriData(new ConeGeometry()), + new TriData(new CylinderGeometry(), 1) + ]; + + this._createArrow(); + } + + set gap(value) { + this._gap = value ?? 0; + this._updateHead(); + this._updateLine(); + } + + get gap() { + return this._gap; + } + + set lineThickness(value) { + this._lineThickness = value ?? 1; + this._updateHead(); + this._updateLine(); + } + + get lineThickness() { + return this._lineThickness; + } + + set lineLength(value) { + this._lineLength = value ?? 1; + this._updateHead(); + this._updateLine(); + } + + get lineLength() { + return this._lineLength; + } + + set arrowThickness(value) { + this._arrowThickness = value ?? 1; + this._updateHead(); + } + + get arrowThickness() { + return this._arrowThickness; + } + + set arrowLength(value) { + this._arrowLength = value ?? 1; + this._updateHead(); + } + + get arrowLength() { + return this._arrowLength; + } + + set tolerance(value) { + this._tolerance = value; + this._updateLine(); + } + + get tolerance() { + return this._tolerance; + } + + _createArrow() { + this._createRoot('arrow'); + + // head + this._head = new Entity(`head:${this.axis}`); + this.entity.addChild(this._head); + this._updateHead(); + this._addRenderMesh(this._head, 'cone', this._shading); + + // line + this._line = new Entity(`line:${this.axis}`); + this.entity.addChild(this._line); + this._updateLine(); + this._addRenderMesh(this._line, 'cylinder', this._shading); + } + + _updateHead() { + // intersect + tmpV1.set(0, this._gap + this._arrowLength * 0.5 + this._lineLength, 0); + tmpQ1.set(0, 0, 0, 1); + tmpV2.set(this._arrowThickness, this._arrowLength, this._arrowThickness); + this.triData[0].setTransform(tmpV1, tmpQ1, tmpV2); + + this._head.setLocalPosition(0, this._gap + this._arrowLength * 0.5 + this._lineLength, 0); + this._head.setLocalScale(this._arrowThickness, this._arrowLength, this._arrowThickness); + } + + _updateLine() { + // intersect + tmpV1.set(0, this._gap + this._lineLength * 0.5, 0); + tmpQ1.set(0, 0, 0, 1); + tmpV2.set(this._lineThickness + this._tolerance, this._lineLength, this._lineThickness + this._tolerance); + this.triData[1].setTransform(tmpV1, tmpQ1, tmpV2); + + // render + this._line.setLocalPosition(0, this._gap + this._lineLength * 0.5, 0); + this._line.setLocalScale(this._lineThickness, this._lineLength, this._lineThickness); + } +} + +export { ArrowShape }; diff --git a/src/extras/gizmo/shape/box-shape.js b/src/extras/gizmo/shape/box-shape.js new file mode 100644 index 00000000000..5ec923247cc --- /dev/null +++ b/src/extras/gizmo/shape/box-shape.js @@ -0,0 +1,52 @@ +import { BoxGeometry } from '../../../scene/geometry/box-geometry.js'; +import { TriData } from '../tri-data.js'; +import { Shape } from './shape.js'; + +class BoxShape extends Shape { + _size = 0.12; + + _tolerance = 0.05; + + constructor(device, options = {}) { + super(device, options); + + this.triData = [ + new TriData(new BoxGeometry(), 2) + ]; + + this._createCenter(); + } + + _createCenter() { + this._createRoot('boxCenter'); + this._updateTransform(); + + // box + this._addRenderMesh(this.entity, 'box', this._shading); + } + + set size(value) { + this._size = value ?? 1; + this._updateTransform(); + } + + get size() { + return this._size; + } + + set tolerance(value) { + this._tolerance = value; + this._updateTransform(); + } + + get tolerance() { + return this._tolerance; + } + + _updateTransform() { + // intersect/render + this.entity.setLocalScale(this._size, this._size, this._size); + } +} + +export { BoxShape }; diff --git a/src/extras/gizmo/shape/boxline-shape.js b/src/extras/gizmo/shape/boxline-shape.js new file mode 100644 index 00000000000..14ef087f71d --- /dev/null +++ b/src/extras/gizmo/shape/boxline-shape.js @@ -0,0 +1,129 @@ +import { Quat } from '../../../core/math/quat.js'; +import { Vec3 } from '../../../core/math/vec3.js'; +import { Entity } from '../../../framework/entity.js'; +import { BoxGeometry } from '../../../scene/geometry/box-geometry.js'; +import { CylinderGeometry } from '../../../scene/geometry/cylinder-geometry.js'; +import { TriData } from '../tri-data.js'; +import { Shape } from './shape.js'; + +const tmpV1 = new Vec3(); +const tmpV2 = new Vec3(); +const tmpQ1 = new Quat(); + +class BoxLineShape extends Shape { + _gap = 0; + + _lineThickness = 0.02; + + _lineLength = 0.5; + + _boxSize = 0.12; + + _tolerance = 0.1; + + _box; + + _line; + + constructor(device, options = {}) { + super(device, options); + + this.triData = [ + new TriData(new BoxGeometry()), + new TriData(new CylinderGeometry(), 1) + ]; + + this._createBoxLine(); + } + + set gap(value) { + this._gap = value ?? 0; + this._updateLine(); + this._updateBox(); + } + + get gap() { + return this._gap; + } + + set lineThickness(value) { + this._lineThickness = value ?? 1; + this._updateLine(); + this._updateBox(); + } + + get lineThickness() { + return this._lineThickness; + } + + set lineLength(value) { + this._lineLength = value ?? 1; + this._updateLine(); + this._updateBox(); + } + + get lineLength() { + return this._lineLength; + } + + set boxSize(value) { + this._boxSize = value ?? 1; + this._updateBox(); + } + + get boxSize() { + return this._boxSize; + } + + set tolerance(value) { + this._tolerance = value; + this._updateLine(); + } + + get tolerance() { + return this._tolerance; + } + + _createBoxLine() { + this._createRoot('boxLine'); + + // box + this._box = new Entity(`box:${this.axis}`); + this.entity.addChild(this._box); + this._updateBox(); + this._addRenderMesh(this._box, 'box', this._shading); + + // line + this._line = new Entity(`line:${this.axis}`); + this.entity.addChild(this._line); + this._updateLine(); + this._addRenderMesh(this._line, 'cylinder', this._shading); + + } + + _updateBox() { + // intersect + tmpV1.set(0, this._gap + this._boxSize * 0.5 + this._lineLength, 0); + tmpQ1.set(0, 0, 0, 1); + tmpV2.set(this._boxSize, this._boxSize, this._boxSize); + this.triData[0].setTransform(tmpV1, tmpQ1, tmpV2); + + // render + this._box.setLocalPosition(0, this._gap + this._boxSize * 0.5 + this._lineLength, 0); + this._box.setLocalScale(this._boxSize, this._boxSize, this._boxSize); + } + + _updateLine() { + // intersect + tmpV1.set(0, this._gap + this._lineLength * 0.5, 0); + tmpQ1.set(0, 0, 0, 1); + tmpV2.set(this._lineThickness + this._tolerance, this._lineLength, this._lineThickness + this._tolerance); + this.triData[1].setTransform(tmpV1, tmpQ1, tmpV2); + + // render + this._line.setLocalPosition(0, this._gap + this._lineLength * 0.5, 0); + this._line.setLocalScale(this._lineThickness, this._lineLength, this._lineThickness); + } +} + +export { BoxLineShape }; diff --git a/src/extras/gizmo/shape/plane-shape.js b/src/extras/gizmo/shape/plane-shape.js new file mode 100644 index 00000000000..0cb81ece2cb --- /dev/null +++ b/src/extras/gizmo/shape/plane-shape.js @@ -0,0 +1,65 @@ +import { Vec3 } from '../../../core/math/vec3.js'; +import { CULLFACE_NONE } from '../../../platform/graphics/constants.js'; +import { PlaneGeometry } from '../../../scene/geometry/plane-geometry.js'; +import { TriData } from '../tri-data.js'; +import { Shape } from './shape.js'; + +class PlaneShape extends Shape { + _cull = CULLFACE_NONE; + + _size = 0.2; + + _gap = 0.1; + + constructor(device, options = {}) { + super(device, options); + + this.triData = [ + new TriData(new PlaneGeometry()) + ]; + + this._createPlane(); + } + + _getPosition() { + const offset = this._size / 2 + this._gap; + const position = new Vec3(offset, offset, offset); + position[this.axis] = 0; + return position; + } + + _createPlane() { + this._createRoot('plane'); + this._updateTransform(); + + // plane + this._addRenderMesh(this.entity, 'plane', this._shading); + } + + set size(value) { + this._size = value ?? 1; + this._updateTransform(); + } + + get size() { + return this._size; + } + + set gap(value) { + this._gap = value ?? 0; + this._updateTransform(); + } + + get gap() { + return this._gap; + } + + _updateTransform() { + // intersect/render + this.entity.setLocalPosition(this._getPosition()); + this.entity.setLocalEulerAngles(this._rotation); + this.entity.setLocalScale(this._size, this._size, this._size); + } +} + +export { PlaneShape }; diff --git a/src/extras/gizmo/shape/shape.js b/src/extras/gizmo/shape/shape.js new file mode 100644 index 00000000000..0d530c4adb1 --- /dev/null +++ b/src/extras/gizmo/shape/shape.js @@ -0,0 +1,355 @@ +import { Color } from '../../../core/math/color.js'; +import { Vec3 } from '../../../core/math/vec3.js'; +import { MeshInstance } from '../../../scene/mesh-instance.js'; +import { Entity } from '../../../framework/entity.js'; +import { CULLFACE_BACK, SEMANTIC_POSITION, SEMANTIC_COLOR } from '../../../platform/graphics/constants.js'; +import { BLEND_NORMAL } from '../../../scene/constants.js'; + +import { COLOR_GRAY } from '../color.js'; +import { Mesh } from '../../../scene/mesh.js'; +import { Geometry } from '../../../scene/geometry/geometry.js'; +import { BoxGeometry } from '../../../scene/geometry/box-geometry.js'; +import { CylinderGeometry } from '../../../scene/geometry/cylinder-geometry.js'; +import { ConeGeometry } from '../../../scene/geometry/cone-geometry.js'; +import { PlaneGeometry } from '../../../scene/geometry/plane-geometry.js'; +import { SphereGeometry } from '../../../scene/geometry/sphere-geometry.js'; +import { TorusGeometry } from '../../../scene/geometry/torus-geometry.js'; +import { Mat4 } from '../../../core/math/mat4.js'; +import { createShaderFromCode } from '../../../scene/shader-lib/utils.js'; +import { Material } from '../../../scene/materials/material.js'; + +/** + * @typedef {import('../../../platform/graphics/graphics-device.js').GraphicsDevice} GraphicsDevice; + * @typedef {import('../tri-data.js').TriData} TriData; + */ + +// constants +const SHADING_DAMP_SCALE = 0.25; +const SHADING_DAMP_OFFSET = 0.75; + +const LIGHT_DIR = new Vec3(1, 2, 3); + +const GEOMETRIES = { + box: BoxGeometry, + cone: ConeGeometry, + cylinder: CylinderGeometry, + plane: PlaneGeometry, + sphere: SphereGeometry, + torus: TorusGeometry +}; + +const shaderDesc = { + uniqueName: 'axis-shape', + attributes: { + vertex_position: SEMANTIC_POSITION, + vertex_color: SEMANTIC_COLOR + }, + vertexCode: /* glsl */` + attribute vec3 vertex_position; + attribute vec4 vertex_color; + varying vec4 vColor; + varying vec2 vZW; + uniform mat4 matrix_model; + uniform mat4 matrix_viewProjection; + void main(void) { + gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0); + vColor = vertex_color; + } + `, + fragmentCode: /* glsl */` + precision highp float; + varying vec4 vColor; + varying vec2 vZW; + void main(void) { + gl_FragColor = vColor; + } + ` +}; + +const shadingMeshMap = new Map(); + +const tmpV1 = new Vec3(); +const tmpV2 = new Vec3(); +const tmpM1 = new Mat4(); +const tmpG = new Geometry(); +tmpG.positions = []; +tmpG.normals = []; + +/** + * Apply shadow to a geometry. + * + * @param {Geometry} geom - The geometry to apply shadow to. + * @param {Color} color - The color of the geometry. + * @param {Mat4} [transform] - The transform of the geometry. + * @returns {number[]} The shadow data. + */ +const applyShadowColor = (geom, color, transform) => { + if (!geom.normals || !geom.positions) { + return []; + } + + // transform light direction to local space + let localLightDir; + if (transform) { + localLightDir = tmpM1.copy(transform).invert() + .transformVector(tmpV1.copy(LIGHT_DIR), tmpV1) + .normalize(); + } + + // calculate shading intensity and apply to color + geom.colors = []; + const shading = []; + const numVertices = geom.positions.length / 3; + for (let i = 0; i < numVertices; i++) { + let strength = 1; + if (localLightDir) { + const x = geom.normals[i * 3]; + const y = geom.normals[i * 3 + 1]; + const z = geom.normals[i * 3 + 2]; + const normal = tmpV2.set(x, y, z); + const dot = localLightDir.dot(normal); + strength = dot * SHADING_DAMP_SCALE + SHADING_DAMP_OFFSET; + } + shading.push(strength); + geom.colors.push( + strength * color.r * 0xFF, + strength * color.g * 0xFF, + strength * color.b * 0xFF, + color.a * 0xFF + ); + } + + return shading; +}; + +/** + * Set the color of a mesh. + * + * @param {Mesh} mesh - The mesh to set the color of. + * @param {Color} color - The color to set the mesh to. + */ +const setMeshColor = (mesh, color) => { + const shading = shadingMeshMap.get(mesh); + const colors = []; + for (let i = 0; i < shading.length; i++) { + colors.push( + shading[i] * color.r * 0xFF, + shading[i] * color.g * 0xFF, + shading[i] * color.b * 0xFF, + color.a * 0xFF + ); + } + mesh.setColors32(colors); + mesh.update(); +}; + +class Shape { + _position; + + _rotation; + + _scale; + + _layers = []; + + _shading = true; + + _disabled; + + _defaultColor = Color.WHITE; + + _hoverColor = Color.BLACK; + + _disabledColor = COLOR_GRAY; + + _cull = CULLFACE_BACK; + + /** + * The graphics device. + * + * @type {GraphicsDevice} + */ + device; + + /** + * The axis of the shape. + * + * @type {string} + */ + axis; + + /** + * The entity of the shape. + * + * @type {Entity} + */ + entity; + + + /** + * The triangle data of the shape. + * + * @type {TriData[]} + */ + triData = []; + + /** + * The mesh instances of the shape. + * + * @type {MeshInstance[]} + */ + meshInstances = []; + + constructor(device, options) { + this.device = device; + this.axis = options.axis ?? 'x'; + this._position = options.position ?? new Vec3(); + this._rotation = options.rotation ?? new Vec3(); + this._scale = options.scale ?? new Vec3(1, 1, 1); + + this._disabled = options.disabled ?? false; + + this._layers = options.layers ?? this._layers; + this._shading = options.shading ?? this._shading; + + if (options.defaultColor instanceof Color) { + this._defaultColor = options.defaultColor; + } + if (options.hoverColor instanceof Color) { + this._hoverColor = options.hoverColor; + } + if (options.disabledColor instanceof Color) { + this._disabledColor = options.disabledColor; + } + } + + set disabled(value) { + for (let i = 0; i < this.meshInstances.length; i++) { + setMeshColor(this.meshInstances[i].mesh, value ? this._disabledColor : this._defaultColor); + } + this._disabled = value ?? false; + } + + get disabled() { + return this._disabled; + } + + set shading(value) { + this._shading = value ?? true; + + const color = this._disabled ? this._disabledColor : this._defaultColor; + for (let i = 0; i < this.meshInstances.length; i++) { + const mesh = this.meshInstances[i].mesh; + mesh.getPositions(tmpG.positions); + mesh.getNormals(tmpG.normals); + const shadow = applyShadowColor( + tmpG, + color, + this._shading ? this.entity.getWorldTransform() : undefined + ); + shadingMeshMap.set(mesh, shadow); + setMeshColor(mesh, color); + } + } + + get shading() { + return this._shading; + } + + _createRoot(name) { + this.entity = new Entity(`${name}:${this.axis}`); + this.entity.setLocalPosition(this._position); + this.entity.setLocalEulerAngles(this._rotation); + this.entity.setLocalScale(this._scale); + } + + /** + * Create a mesh from a primitive. + * + * @param {Geometry} geom - The geometry to create the mesh from. + * @param {boolean} shading - Whether to apply shading to the primitive. + * @returns {Mesh} The mesh created from the primitive. + * @throws {Error} If the primitive type is invalid. + * @protected + */ + _createMesh(geom, shading = true) { + const color = this._disabled ? this._disabledColor : this._defaultColor; + const shadow = applyShadowColor( + geom, + color, + shading ? this.entity.getWorldTransform() : undefined + ); + const mesh = Mesh.fromGeometry(this.device, geom); + shadingMeshMap.set(mesh, shadow); + + return mesh; + } + + /** + * Create a render component for an entity. + * + * @param {Entity} entity - The entity to create the render component for. + * @param {Mesh[]} meshes - The meshes to create the render component with. + * @protected + */ + _createRenderComponent(entity, meshes) { + const shader = createShaderFromCode(this.device, shaderDesc.vertexCode, shaderDesc.fragmentCode, shaderDesc.uniqueName, { + vertex_position: SEMANTIC_POSITION, + vertex_color: SEMANTIC_COLOR + }); + + const material = new Material(); + material.shader = shader; + material.cull = this._cull; + material.blendType = BLEND_NORMAL; + material.update(); + + const meshInstances = []; + for (let i = 0; i < meshes.length; i++) { + const mi = new MeshInstance(meshes[i], material); + meshInstances.push(mi); + this.meshInstances.push(mi); + } + entity.addComponent('render', { + meshInstances: meshInstances, + layers: this._layers, + castShadows: false + }); + } + + /** + * Add a render mesh to an entity. + * + * @param {Entity} entity - The entity to add the render mesh to. + * @param {string} type - The type of primitive to create. + * @param {boolean} shading - Whether to apply shading to the primitive. + * @throws {Error} If the primitive type is invalid. + * @protected + */ + _addRenderMesh(entity, type, shading) { + const Geometry = GEOMETRIES[type]; + if (!Geometry) { + throw new Error('Invalid primitive type.'); + } + this._createRenderComponent(entity, [ + this._createMesh(new Geometry(), shading) + ]); + } + + hover(state) { + if (this._disabled) { + return; + } + for (let i = 0; i < this.meshInstances.length; i++) { + const color = state ? this._hoverColor : this._defaultColor; + const mesh = this.meshInstances[i].mesh; + setMeshColor(mesh, color); + } + } + + destroy() { + this.entity.destroy(); + } +} + +export { Shape }; diff --git a/src/extras/gizmo/shape/sphere-shape.js b/src/extras/gizmo/shape/sphere-shape.js new file mode 100644 index 00000000000..592482841fb --- /dev/null +++ b/src/extras/gizmo/shape/sphere-shape.js @@ -0,0 +1,52 @@ +import { SphereGeometry } from '../../../scene/geometry/sphere-geometry.js'; +import { TriData } from '../tri-data.js'; +import { Shape } from './shape.js'; + +class SphereShape extends Shape { + _size = 0.12; + + _tolerance = 0.05; + + constructor(device, options = {}) { + super(device, options); + + this.triData = [ + new TriData(new SphereGeometry(), 2) + ]; + + this._createCenter(); + } + + _createCenter() { + this._createRoot('sphereCenter'); + this._updateTransform(); + + // box + this._addRenderMesh(this.entity, 'sphere', this._shading); + } + + set size(value) { + this._size = value ?? 1; + this._updateTransform(); + } + + get size() { + return this._size; + } + + set tolerance(value) { + this._tolerance = value; + this._updateTransform(); + } + + get tolerance() { + return this._tolerance; + } + + _updateTransform() { + // intersect/render + this.entity.setLocalScale(this._size, this._size, this._size); + } +} + +export { SphereShape }; diff --git a/src/extras/gizmo/transform-gizmo.js b/src/extras/gizmo/transform-gizmo.js index 784d5670127..7fa0dd07398 100644 --- a/src/extras/gizmo/transform-gizmo.js +++ b/src/extras/gizmo/transform-gizmo.js @@ -11,10 +11,20 @@ import { COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, - COLOR_GRAY -} from './default-colors.js'; + COLOR_GRAY, + color3from4, + color4from3 +} from './color.js'; +import { GIZMOAXIS_X, GIZMOAXIS_XYZ, GIZMOAXIS_Y, GIZMOAXIS_Z } from './constants.js'; import { Gizmo } from './gizmo.js'; +/** + * @typedef {import('./shape/shape.js').Shape} Shape + * @typedef {import('../../framework/components/camera/component.js').CameraComponent} CameraComponent + * @typedef {import('../../scene/layer.js').Layer} Layer + * @typedef {import('../../scene/mesh-instance.js').MeshInstance} MeshInstance + */ + // temporary variables const tmpV1 = new Vec3(); const tmpV2 = new Vec3(); @@ -22,79 +32,10 @@ const tmpQ1 = new Quat(); const tmpR1 = new Ray(); const tmpP1 = new Plane(); -const pointDelta = new Vec3(); - // constants const VEC3_AXES = Object.keys(tmpV1); const SPANLINE_SIZE = 1e3; -/** - * Shape axis for the line X. - * - * @type {string} - */ -export const SHAPEAXIS_X = 'x'; - -/** - * Shape axis for the line Y. - * - * @type {string} - */ -export const SHAPEAXIS_Y = 'y'; - -/** - * Shape axis for the line Z. - * - * @type {string} - */ -export const SHAPEAXIS_Z = 'z'; - -/** - * Shape axis for the plane YZ. - * - * @type {string} - */ -export const SHAPEAXIS_YZ = 'yz'; - -/** - * Shape axis for the plane XZ. - * - * @type {string} - */ -export const SHAPEAXIS_XZ = 'xz'; - -/** - * Shape axis for the plane XY. - * - * @type {string} - */ -export const SHAPEAXIS_XY = 'xy'; - -/** - * Shape axis for all directions XYZ. - * - * @type {string} - */ -export const SHAPEAXIS_XYZ = 'xyz'; - -/** - * Shape axis for facing the camera. - * - * @type {string} - */ -export const SHAPEAXIS_FACE = 'face'; - - -/** - * Converts Color4 to Color3. - * - * @param {Color} color - Color4 - * @returns {Color} - Color3 - */ -function color3from4(color) { - return new Color(color.r, color.g, color.b); -} - /** * The base class for all transform gizmos. * @@ -146,9 +87,9 @@ class TransformGizmo extends Gizmo { _colorAlpha = 0.6; /** - * Internal color for meshs. + * Internal color for meshes. * - * @type {Object} + * @type {{ axis: Record, hover: Record, disabled: Color }} * @protected */ _meshColors = { @@ -157,14 +98,14 @@ class TransformGizmo extends Gizmo { y: this._colorSemi(COLOR_GREEN), z: this._colorSemi(COLOR_BLUE), xyz: this._colorSemi(Color.WHITE), - face: this._colorSemi(Color.WHITE) + f: this._colorSemi(Color.WHITE) }, hover: { x: COLOR_RED.clone(), y: COLOR_GREEN.clone(), z: COLOR_BLUE.clone(), xyz: Color.WHITE.clone(), - face: COLOR_YELLOW.clone() + f: COLOR_YELLOW.clone() }, disabled: COLOR_GRAY.clone() }; @@ -172,16 +113,33 @@ class TransformGizmo extends Gizmo { /** * Internal version of the guide line color. * - * @type {Object} + * @type {Record} * @protected */ _guideColors = { x: COLOR_RED.clone(), y: COLOR_GREEN.clone(), z: COLOR_BLUE.clone(), - face: COLOR_YELLOW.clone() + f: COLOR_YELLOW.clone() }; + /** + * Internal point delta. + * + * @type {Vec3} + * @private + */ + _pointDelta = new Vec3(); + + /** + * Internal gizmo starting rotation in world space. + * + * @type {Vec3} + * @protected + */ + _rootStartPos = new Vec3(); + + /** * Internal gizmo starting rotation in world space. * @@ -191,17 +149,25 @@ class TransformGizmo extends Gizmo { _rootStartRot = new Quat(); /** - * Internal object containing the axis shapes to render. + * Internal state of if shading is enabled. Defaults to true. + * + * @type {boolean} + * @protected + */ + _shading = false; + + /** + * Internal object containing the gizmo shapes to render. * - * @type {Object.} + * @type {Object.} * @protected */ _shapes = {}; /** - * Internal mapping of mesh instances to axis shapes. + * Internal mapping of mesh instances to gizmo shapes. * - * @type {Map} + * @type {Map} * @private */ _shapeMap = new Map(); @@ -209,7 +175,7 @@ class TransformGizmo extends Gizmo { /** * Internal currently hovered shape. * - * @type {import('./axis-shapes.js').AxisShape | null} + * @type {Shape | null} * @private */ _hoverShape = null; @@ -288,24 +254,22 @@ class TransformGizmo extends Gizmo { /** * Creates a new TransformGizmo object. * - * @param {import('../../framework/app-base.js').AppBase} app - The application instance. - * @param {import('../../framework/components/camera/component.js').CameraComponent} camera - - * The camera component. - * @param {import('../../scene/layer.js').Layer} layer - The render layer. + * @param {CameraComponent} camera - The camera component. + * @param {Layer} layer - The render layer. * @example * const gizmo = new pc.TransformGizmo(app, camera, layer); */ - constructor(app, camera, layer) { - super(app, camera, layer); + constructor(camera, layer) { + super(camera, layer); - app.on('update', () => { + this._app.on('update', () => { if (!this.root.enabled) { return; } this._drawGuideLines(); }); - this.on('pointer:down', (x, y, meshInstance) => { + this.on(Gizmo.EVENT_POINTERDOWN, (x, y, meshInstance) => { const shape = this._shapeMap.get(meshInstance); if (shape?.disabled) { return; @@ -321,6 +285,7 @@ class TransformGizmo extends Gizmo { this._selectedAxis = this._getAxis(meshInstance); this._selectedIsPlane = this._getIsPlane(meshInstance); + this._rootStartPos.copy(this.root.getPosition()); this._rootStartRot.copy(this.root.getRotation()); const pointInfo = this._screenToPoint(x, y); this._selectionStartPoint.copy(pointInfo.point); @@ -329,7 +294,7 @@ class TransformGizmo extends Gizmo { this.fire(TransformGizmo.EVENT_TRANSFORMSTART); }); - this.on('pointer:move', (x, y, meshInstance) => { + this.on(Gizmo.EVENT_POINTERMOVE, (x, y, meshInstance) => { const shape = this._shapeMap.get(meshInstance); if (shape?.disabled) { return; @@ -342,15 +307,15 @@ class TransformGizmo extends Gizmo { } const pointInfo = this._screenToPoint(x, y); - pointDelta.copy(pointInfo.point).sub(this._selectionStartPoint); + this._pointDelta.copy(pointInfo.point).sub(this._selectionStartPoint); const angleDelta = pointInfo.angle - this._selectionStartAngle; - this.fire(TransformGizmo.EVENT_TRANSFORMMOVE, pointDelta, angleDelta); + this.fire(TransformGizmo.EVENT_TRANSFORMMOVE, this._pointDelta, angleDelta); this._hoverAxis = ''; this._hoverIsPlane = false; }); - this.on('pointer:up', () => { + this.on(Gizmo.EVENT_POINTERUP, () => { if (!this._dragging) { return; } @@ -361,15 +326,37 @@ class TransformGizmo extends Gizmo { this._selectedIsPlane = false; }); - this.on('nodes:detach', () => { + this.on(Gizmo.EVENT_NODESDETACH, () => { this.snap = false; this._hoverAxis = ''; this._hoverIsPlane = false; - this._hover(null); - this.fire('pointer:up'); + this._hover(); + this.fire(Gizmo.EVENT_POINTERUP); }); } + /** + * Sets whether shading are enabled. Defaults to true. + * + * @type {boolean} + */ + set shading(value) { + this._shading = this.root.enabled && value; + + for (const name in this._shapes) { + this._shapes[name].shading = this._shading; + } + } + + /** + * Gets whether shading are enabled. Defaults to true. + * + * @type {boolean} + */ + get shading() { + return this._shading; + } + /** * Sets whether snapping is enabled. Defaults to false. * @@ -394,7 +381,7 @@ class TransformGizmo extends Gizmo { * @type {Color} */ set xAxisColor(value) { - this._updateAxisColor('x', value); + this._updateAxisColor(GIZMOAXIS_X, value); } /** @@ -412,7 +399,7 @@ class TransformGizmo extends Gizmo { * @type {Color} */ set yAxisColor(value) { - this._updateAxisColor('y', value); + this._updateAxisColor(GIZMOAXIS_Y, value); } /** @@ -430,7 +417,7 @@ class TransformGizmo extends Gizmo { * @type {Color} */ set zAxisColor(value) { - this._updateAxisColor('z', value); + this._updateAxisColor(GIZMOAXIS_Z, value); } /** @@ -454,7 +441,7 @@ class TransformGizmo extends Gizmo { this._meshColors.axis.y.copy(this._colorSemi(this._meshColors.axis.y)); this._meshColors.axis.z.copy(this._colorSemi(this._meshColors.axis.z)); this._meshColors.axis.xyz.copy(this._colorSemi(this._meshColors.axis.xyz)); - this._meshColors.axis.face.copy(this._colorSemi(this._meshColors.axis.face)); + this._meshColors.axis.f.copy(this._colorSemi(this._meshColors.axis.f)); for (const name in this._shapes) { this._shapes[name].hover(!!this._hoverAxis); @@ -470,12 +457,20 @@ class TransformGizmo extends Gizmo { return this._colorAlpha; } + /** + * @param {Color} color - The color to set. + * @returns {Color} - The color with alpha applied. + * @private + */ _colorSemi(color) { - const clone = color.clone(); - clone.a = this._colorAlpha; - return clone; + return color4from3(color, this._colorAlpha); } + /** + * @param {string} axis - The axis to update. + * @param {any} value - The value to set. + * @private + */ _updateAxisColor(axis, value) { const color3 = color3from4(value); const color4 = this._colorSemi(value); @@ -489,6 +484,11 @@ class TransformGizmo extends Gizmo { } } + /** + * @param {MeshInstance} [meshInstance] - The mesh instance. + * @returns {string} - The axis. + * @private + */ _getAxis(meshInstance) { if (!meshInstance) { return ''; @@ -496,6 +496,11 @@ class TransformGizmo extends Gizmo { return meshInstance.node.name.split(':')[1]; } + /** + * @param {MeshInstance} [meshInstance] - The mesh instance. + * @returns {boolean} - Whether the mesh instance is a plane. + * @private + */ _getIsPlane(meshInstance) { if (!meshInstance) { return false; @@ -503,13 +508,17 @@ class TransformGizmo extends Gizmo { return meshInstance.node.name.indexOf('plane') !== -1; } + /** + * @param {MeshInstance} [meshInstance] - The mesh instance. + * @private + */ _hover(meshInstance) { if (this._dragging) { return; } this._hoverAxis = this._getAxis(meshInstance); this._hoverIsPlane = this._getIsPlane(meshInstance); - const shape = this._shapeMap.get(meshInstance) || null; + const shape = meshInstance ? this._shapeMap.get(meshInstance) ?? null : null; if (shape === this._hoverShape) { return; } @@ -521,9 +530,14 @@ class TransformGizmo extends Gizmo { shape.hover(true); this._hoverShape = shape; } - this.fire('render:update'); + this.fire(Gizmo.EVENT_RENDERUPDATE); } + /** + * @param {Vec3} mouseWPos - The mouse world position. + * @returns {Ray} - The ray. + * @protected + */ _createRay(mouseWPos) { const cameraPos = this._camera.entity.getPosition(); const cameraTransform = this._camera.entity.getWorldTransform(); @@ -541,11 +555,17 @@ class TransformGizmo extends Gizmo { return ray; } + /** + * @param {string} axis - The axis to create the plane for. + * @param {boolean} isFacing - Whether the axis is facing the camera. + * @param {boolean} isLine - Whether the axis is a line. + * @returns {Plane} - The plane. + * @protected + */ _createPlane(axis, isFacing, isLine) { const cameraPos = this._camera.entity.getPosition(); - const gizmoPos = this.root.getPosition(); - const facingDir = tmpV1.sub2(cameraPos, gizmoPos).normalize(); + const facingDir = tmpV1.sub2(cameraPos, this._rootStartPos).normalize(); const normal = tmpP1.normal.set(0, 0, 0); if (isFacing) { @@ -563,9 +583,14 @@ class TransformGizmo extends Gizmo { } } - return tmpP1.setFromPointNormal(gizmoPos, normal); + return tmpP1.setFromPointNormal(this._rootStartPos, normal); } + /** + * @param {Vec3} point - The point to project. + * @param {string} axis - The axis to project to. + * @protected + */ _projectToAxis(point, axis) { // set normal to axis and project position from plane onto normal tmpV1.set(0, 0, 0); @@ -578,6 +603,14 @@ class TransformGizmo extends Gizmo { point[axis] = v; } + /** + * @param {number} x - The x coordinate. + * @param {number} y - The y coordinate. + * @param {boolean} isFacing - Whether the axis is facing the camera. + * @param {boolean} isLine - Whether the axis is a line. + * @returns {{ point: Vec3, angle: number }} - The point and angle. + * @protected + */ _screenToPoint(x, y, isFacing = false, isLine = false) { const mouseWPos = this._camera.screenToWorld(x, y, 1); @@ -593,6 +626,9 @@ class TransformGizmo extends Gizmo { return { point, angle }; } + /** + * @private + */ _drawGuideLines() { const gizmoPos = this.root.getPosition(); const gizmoRot = tmpQ1.copy(this.root.getRotation()); @@ -600,7 +636,7 @@ class TransformGizmo extends Gizmo { const checkIsPlane = this._hoverIsPlane || this._selectedIsPlane; for (let i = 0; i < VEC3_AXES.length; i++) { const axis = VEC3_AXES[i]; - if (checkAxis === 'xyz') { + if (checkAxis === GIZMOAXIS_XYZ) { this._drawSpanLine(gizmoPos, gizmoRot, axis); continue; } @@ -616,6 +652,12 @@ class TransformGizmo extends Gizmo { } } + /** + * @param {Vec3} pos - The position. + * @param {Quat} rot - The rotation. + * @param {string} axis - The axis. + * @private + */ _drawSpanLine(pos, rot, axis) { tmpV1.set(0, 0, 0); tmpV1[axis] = 1; @@ -626,6 +668,9 @@ class TransformGizmo extends Gizmo { this._app.drawLine(tmpV1.add(pos), tmpV2.add(pos), this._guideColors[axis], true); } + /** + * @protected + */ _createTransform() { // shapes for (const key in this._shapes) { @@ -647,14 +692,14 @@ class TransformGizmo extends Gizmo { * * @param {string} shapeAxis - The shape axis. Can be: * - * {@link SHAPEAXIS_X} - * {@link SHAPEAXIS_Y} - * {@link SHAPEAXIS_Z} - * {@link SHAPEAXIS_YZ} - * {@link SHAPEAXIS_XZ} - * {@link SHAPEAXIS_XY} - * {@link SHAPEAXIS_XYZ} - * {@link SHAPEAXIS_FACE} + * - {@link GIZMOAXIS_X} + * - {@link GIZMOAXIS_Y} + * - {@link GIZMOAXIS_Z} + * - {@link GIZMOAXIS_YZ} + * - {@link GIZMOAXIS_XZ} + * - {@link GIZMOAXIS_XY} + * - {@link GIZMOAXIS_XYZ} + * - {@link GIZMOAXIS_FACE} * * @param {boolean} enabled - The enabled state of shape. */ @@ -671,14 +716,14 @@ class TransformGizmo extends Gizmo { * * @param {string} shapeAxis - The shape axis. Can be: * - * {@link SHAPEAXIS_X} - * {@link SHAPEAXIS_Y} - * {@link SHAPEAXIS_Z} - * {@link SHAPEAXIS_YZ} - * {@link SHAPEAXIS_XZ} - * {@link SHAPEAXIS_XY} - * {@link SHAPEAXIS_XYZ} - * {@link SHAPEAXIS_FACE} + * - {@link GIZMOAXIS_X} + * - {@link GIZMOAXIS_Y} + * - {@link GIZMOAXIS_Z} + * - {@link GIZMOAXIS_YZ} + * - {@link GIZMOAXIS_XZ} + * - {@link GIZMOAXIS_XY} + * - {@link GIZMOAXIS_XYZ} + * - {@link GIZMOAXIS_FACE} * * @returns {boolean} - Then enabled state of the shape */ @@ -694,11 +739,11 @@ class TransformGizmo extends Gizmo { * @override */ destroy() { + super.destroy(); + for (const key in this._shapes) { this._shapes[key].destroy(); } - - super.destroy(); } } diff --git a/src/extras/gizmo/translate-gizmo.js b/src/extras/gizmo/translate-gizmo.js index c8623407710..07073364d56 100644 --- a/src/extras/gizmo/translate-gizmo.js +++ b/src/extras/gizmo/translate-gizmo.js @@ -1,9 +1,23 @@ import { Vec3 } from '../../core/math/vec3.js'; import { Quat } from '../../core/math/quat.js'; -import { AxisArrow, AxisPlane } from './axis-shapes.js'; -import { GIZMO_LOCAL } from './gizmo.js'; +import { + GIZMOSPACE_LOCAL, + GIZMOAXIS_FACE, + GIZMOAXIS_X, + GIZMOAXIS_Y, + GIZMOAXIS_Z +} from './constants.js'; import { TransformGizmo } from './transform-gizmo.js'; +import { PlaneShape } from './shape/plane-shape.js'; +import { ArrowShape } from './shape/arrow-shape.js'; +import { SphereShape } from './shape/sphere-shape.js'; + +/** + * @typedef {import('../../framework/components/camera/component.js').CameraComponent} CameraComponent + * @typedef {import('../../scene/graph-node.js').GraphNode} GraphNode + * @typedef {import('../../scene/layer.js').Layer} Layer + */ // temporary variables const tmpV1 = new Vec3(); @@ -17,47 +31,57 @@ const tmpQ1 = new Quat(); */ class TranslateGizmo extends TransformGizmo { _shapes = { - yz: new AxisPlane(this._device, { - axis: 'x', - flipAxis: 'y', + face: new SphereShape(this._device, { + axis: GIZMOAXIS_FACE, layers: [this._layer.id], + shading: this._shading, + defaultColor: this._meshColors.axis.xyz, + hoverColor: this._meshColors.hover.xyz + }), + yz: new PlaneShape(this._device, { + axis: GIZMOAXIS_X, + layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(0, 0, -90), defaultColor: this._meshColors.axis.x, hoverColor: this._meshColors.hover.x }), - xz: new AxisPlane(this._device, { - axis: 'y', - flipAxis: 'z', + xz: new PlaneShape(this._device, { + axis: GIZMOAXIS_Y, layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(0, 0, 0), defaultColor: this._meshColors.axis.y, hoverColor: this._meshColors.hover.y }), - xy: new AxisPlane(this._device, { - axis: 'z', - flipAxis: 'x', + xy: new PlaneShape(this._device, { + axis: GIZMOAXIS_Z, layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(90, 0, 0), defaultColor: this._meshColors.axis.z, hoverColor: this._meshColors.hover.z }), - x: new AxisArrow(this._device, { - axis: 'x', + x: new ArrowShape(this._device, { + axis: GIZMOAXIS_X, layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(0, 0, -90), defaultColor: this._meshColors.axis.x, hoverColor: this._meshColors.hover.x }), - y: new AxisArrow(this._device, { - axis: 'y', + y: new ArrowShape(this._device, { + axis: GIZMOAXIS_Y, layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(0, 0, 0), defaultColor: this._meshColors.axis.y, hoverColor: this._meshColors.hover.y }), - z: new AxisArrow(this._device, { - axis: 'z', + z: new ArrowShape(this._device, { + axis: GIZMOAXIS_Z, layers: [this._layer.id], + shading: this._shading, rotation: new Vec3(90, 0, 0), defaultColor: this._meshColors.axis.z, hoverColor: this._meshColors.hover.z @@ -67,7 +91,7 @@ class TranslateGizmo extends TransformGizmo { /** * Internal mapping from each attached node to their starting position in local space. * - * @type {Map} + * @type {Map} * @private */ _nodeLocalPositions = new Map(); @@ -75,7 +99,7 @@ class TranslateGizmo extends TransformGizmo { /** * Internal mapping from each attached node to their starting position in world space. * - * @type {Map} + * @type {Map} * @private */ _nodePositions = new Map(); @@ -88,23 +112,21 @@ class TranslateGizmo extends TransformGizmo { /** * Creates a new TranslateGizmo object. * - * @param {import('../../framework/app-base.js').AppBase} app - The application instance. - * @param {import('../../framework/components/camera/component.js').CameraComponent} camera - - * The camera component. - * @param {import('../../scene/layer.js').Layer} layer - The render layer. + * @param {CameraComponent} camera - The camera component. + * @param {Layer} layer - The render layer. * @example * const gizmo = new pc.TranslateGizmo(app, camera, layer); */ - constructor(app, camera, layer) { - super(app, camera, layer); + constructor(camera, layer) { + super(camera, layer); this._createTransform(); - this.on('transform:start', () => { + this.on(TransformGizmo.EVENT_TRANSFORMSTART, () => { this._storeNodePositions(); }); - this.on('transform:move', (pointDelta) => { + this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (pointDelta) => { if (this.snap) { pointDelta.mulScalar(1 / this.snapIncrement); pointDelta.round(); @@ -113,7 +135,7 @@ class TranslateGizmo extends TransformGizmo { this._setNodePositions(pointDelta); }); - this.on('nodes:detach', () => { + this.on(TransformGizmo.EVENT_NODESDETACH, () => { this._nodeLocalPositions.clear(); this._nodePositions.clear(); }); @@ -263,18 +285,67 @@ class TranslateGizmo extends TransformGizmo { return this._shapes.yz.gap; } + /** + * Sets the axis center size. + * + * @type {number} + */ + set axisCenterSize(value) { + this._shapes.face.size = value; + } + + /** + * Gets the axis center size. + * + * @type {number} + */ + get axisCenterSize() { + return this._shapes.face.size; + } + + /** + * Sets the axis center tolerance. + * + * @type {number} + */ + set axisCenterTolerance(value) { + this._shapes.face.tolerance = value; + } + + /** + * Gets the axis center tolerance. + * + * @type {number} + */ + get axisCenterTolerance() { + return this._shapes.face.tolerance; + } + + /** + * @param {string} prop - The property to set. + * @param {any} value - The value to set. + * @private + */ _setArrowProp(prop, value) { this._shapes.x[prop] = value; this._shapes.y[prop] = value; this._shapes.z[prop] = value; } + /** + * @param {string} prop - The property to set. + * @param {any} value - The value to set. + * @private + */ _setPlaneProp(prop, value) { this._shapes.yz[prop] = value; this._shapes.xz[prop] = value; this._shapes.xy[prop] = value; } + /** + * @private + */ _storeNodePositions() { for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i]; @@ -283,26 +354,40 @@ class TranslateGizmo extends TransformGizmo { } } + /** + * @param {Vec3} pointDelta - The delta to apply to the node positions. + * @private + */ _setNodePositions(pointDelta) { for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i]; - if (this._coordSpace === GIZMO_LOCAL) { + const pos = this._nodePositions.get(node); + if (!pos) { + continue; + } + if (this._coordSpace === GIZMOSPACE_LOCAL) { tmpV1.copy(pointDelta); - node.parent.getWorldTransform().getScale(tmpV2); + node.parent?.getWorldTransform().getScale(tmpV2); tmpV2.x = 1 / tmpV2.x; tmpV2.y = 1 / tmpV2.y; tmpV2.z = 1 / tmpV2.z; tmpQ1.copy(node.getLocalRotation()).transformVector(tmpV1, tmpV1); tmpV1.mul(tmpV2); - node.setLocalPosition(this._nodeLocalPositions.get(node).clone().add(tmpV1)); + node.setLocalPosition(pos.clone().add(tmpV1)); } else { - node.setPosition(this._nodePositions.get(node).clone().add(pointDelta)); + node.setPosition(pos.clone().add(pointDelta)); } } this._updatePosition(); } + /** + * @param {number} x - The x coordinate. + * @param {number} y - The y coordinate. + * @returns {{ point: Vec3, angle: number }} The point and angle. + * @protected + */ _screenToPoint(x, y) { const mouseWPos = this._camera.screenToWorld(x, y, 1); @@ -310,7 +395,7 @@ class TranslateGizmo extends TransformGizmo { const isPlane = this._selectedIsPlane; const ray = this._createRay(mouseWPos); - const plane = this._createPlane(axis, false, !isPlane); + const plane = this._createPlane(axis, axis === GIZMOAXIS_FACE, !isPlane); const point = new Vec3(); const angle = 0; @@ -320,7 +405,7 @@ class TranslateGizmo extends TransformGizmo { // rotate point back to world coords tmpQ1.copy(this._rootStartRot).invert().transformVector(point, point); - if (!isPlane) { + if (!isPlane && axis !== GIZMOAXIS_FACE) { this._projectToAxis(point, axis); } diff --git a/src/extras/gizmo/tri-data.js b/src/extras/gizmo/tri-data.js index d2365eec844..888825dbb53 100644 --- a/src/extras/gizmo/tri-data.js +++ b/src/extras/gizmo/tri-data.js @@ -29,7 +29,7 @@ class TriData { * * @type {Mat4} */ - _ptm = new Mat4(); + _transform = new Mat4(); /** * The array of triangles for the geometry. @@ -47,21 +47,35 @@ class TriData { this._priority = priority; } - get ptm() { - return this._ptm; + get transform() { + return this._transform; } get priority() { return this._priority; } + /** + * Sets the transform of the triangle data. + * + * @param {Vec3} [pos] - The position of the transform. + * @param {Quat} [rot] - The rotation of the transform. + * @param {Vec3} [scale] - The scale of the transform. + */ setTransform(pos = new Vec3(), rot = new Quat(), scale = new Vec3()) { - this.ptm.setTRS(pos, rot, scale); + this.transform.setTRS(pos, rot, scale); } - calculateTris(geometry) { - const positions = geometry.positions; - const indices = geometry.indices; + /** + * @param {Geometry} geometry - The geometry to create the triangle data from. + */ + fromGeometry(geometry) { + if (!geometry || !(geometry instanceof Geometry)) { + throw new Error('No geometry provided.'); + } + + const positions = geometry.positions ?? []; + const indices = geometry.indices ?? []; this.tris = []; for (let k = 0; k < indices.length; k += 3) { const i1 = indices[k]; @@ -75,13 +89,6 @@ class TriData { this.tris.push(tri); } } - - fromGeometry(geometry) { - if (!geometry || !(geometry instanceof Geometry)) { - throw new Error('No geometry provided.'); - } - this.calculateTris(geometry); - } } export { TriData }; diff --git a/src/extras/index.js b/src/extras/index.js index bd2bc05971e..6a0d7601144 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -19,7 +19,19 @@ export { RenderPassBloom } from './render-passes/render-pass-bloom.js'; export { RenderPassTAA } from './render-passes/render-pass-taa.js'; // GIZMOS -export { Gizmo, GIZMO_LOCAL, GIZMO_WORLD } from './gizmo/gizmo.js'; +export { + GIZMOSPACE_LOCAL, + GIZMOSPACE_WORLD, + GIZMOAXIS_X, + GIZMOAXIS_Y, + GIZMOAXIS_Z, + GIZMOAXIS_XY, + GIZMOAXIS_XZ, + GIZMOAXIS_YZ, + GIZMOAXIS_XYZ, + GIZMOAXIS_FACE +} from './gizmo/constants.js'; +export { Gizmo } from './gizmo/gizmo.js'; export { TransformGizmo } from './gizmo/transform-gizmo.js'; export { TranslateGizmo } from './gizmo/translate-gizmo.js'; export { RotateGizmo } from './gizmo/rotate-gizmo.js';