From 20e8adf61105fe43a7527285a2d039bd543cf515 Mon Sep 17 00:00:00 2001 From: kpal Date: Thu, 31 Oct 2024 11:49:09 +0000 Subject: [PATCH 1/9] Ported view cube to extras from supersplat --- .../gizmos/transform-rotate.example.mjs | 7 + .../gizmos/transform-scale.example.mjs | 7 + .../gizmos/transform-translate.example.mjs | 7 + examples/src/examples/misc/editor.example.mjs | 7 + src/extras/gizmo/view-cube.js | 319 ++++++++++++++++++ src/extras/index.js | 1 + 6 files changed, 348 insertions(+) create mode 100644 src/extras/gizmo/view-cube.js diff --git a/examples/src/examples/gizmos/transform-rotate.example.mjs b/examples/src/examples/gizmos/transform-rotate.example.mjs index 4f43bf31501..80de121dc15 100644 --- a/examples/src/examples/gizmos/transform-rotate.example.mjs +++ b/examples/src/examples/gizmos/transform-rotate.example.mjs @@ -164,6 +164,13 @@ const lines = createGridLines(2); const gridCol = new pc.Color(1, 1, 1, 0.5); app.on('update', () => app.drawLines(lines, gridCol)); +// view cube +const viewCube = new pc.ViewCube(); +viewCube.anchor = new pc.Vec4(0, 1, 1, 0); +app.on('prerender', () => { + viewCube.update(camera.getWorldTransform()); +}); + app.on('destroy', () => { window.removeEventListener('resize', resize); }); diff --git a/examples/src/examples/gizmos/transform-scale.example.mjs b/examples/src/examples/gizmos/transform-scale.example.mjs index 87eefeeb4ac..9dfc5f1284b 100644 --- a/examples/src/examples/gizmos/transform-scale.example.mjs +++ b/examples/src/examples/gizmos/transform-scale.example.mjs @@ -168,6 +168,13 @@ const lines = createGridLines(2); const gridCol = new pc.Color(1, 1, 1, 0.5); app.on('update', () => app.drawLines(lines, gridCol)); +// view cube +const viewCube = new pc.ViewCube(); +viewCube.anchor = new pc.Vec4(0, 1, 1, 0); +app.on('prerender', () => { + viewCube.update(camera.getWorldTransform()); +}); + app.on('destroy', () => { window.removeEventListener('resize', resize); }); diff --git a/examples/src/examples/gizmos/transform-translate.example.mjs b/examples/src/examples/gizmos/transform-translate.example.mjs index 956e9cdd0e2..7050ff6b188 100644 --- a/examples/src/examples/gizmos/transform-translate.example.mjs +++ b/examples/src/examples/gizmos/transform-translate.example.mjs @@ -169,6 +169,13 @@ const lines = createGridLines(2); const gridCol = new pc.Color(1, 1, 1, 0.5); app.on('update', () => app.drawLines(lines, gridCol)); +// view cube +const viewCube = new pc.ViewCube(); +viewCube.anchor = new pc.Vec4(0, 1, 1, 0); +app.on('prerender', () => { + viewCube.update(camera.getWorldTransform()); +}); + app.on('destroy', () => { window.removeEventListener('resize', resize); }); diff --git a/examples/src/examples/misc/editor.example.mjs b/examples/src/examples/misc/editor.example.mjs index 851959c20b2..3b5c6a2c431 100644 --- a/examples/src/examples/misc/editor.example.mjs +++ b/examples/src/examples/misc/editor.example.mjs @@ -269,6 +269,13 @@ app.on('update', (/** @type {number} */ dt) => { grid.draw(app); }); +// view cube +const viewCube = new pc.ViewCube(); +viewCube.anchor = new pc.Vec4(0, 1, 1, 0); +app.on('prerender', () => { + viewCube.update(camera.getWorldTransform()); +}); + app.on('destroy', () => { gizmoHandler.destroy(); selector.destroy(); diff --git a/src/extras/gizmo/view-cube.js b/src/extras/gizmo/view-cube.js new file mode 100644 index 00000000000..a95e35bd539 --- /dev/null +++ b/src/extras/gizmo/view-cube.js @@ -0,0 +1,319 @@ +import { Color } from '../../core/math/color.js'; +import { EventHandler } from '../../core/event-handler.js'; +import { Mat4 } from '../../core/math/mat4.js'; +import { Vec3 } from '../../core/math/vec3.js'; +import { Vec4 } from '../../core/math/vec4.js'; + +const tmpV1 = new Vec3(); +const tmpV2 = new Vec3(); +const tmpV3 = new Vec3(); +const tmpM1 = new Mat4(); + +class ViewCube extends EventHandler { + /** + * Fired when the user clicks on a face of the view cube. + * + * @event + * @example + * const viewCube = new ViewCube() + * viewCube.on(ViewCube.EVENT_CAMERAALIGN, function (face) { + * console.log('Camera aligned to face: ' + face); + * }); + */ + static EVENT_CAMERAALIGN = 'camera:align'; + + /** + * @type {Vec4} + */ + _anchor = new Vec4(1, 1, 1, 1); + + /** + * @type {Color} + */ + _colorX = new Color(1, 0.3, 0.3); + + /** + * @type {Color} + */ + _colorY = new Color(0.3, 1, 0.3); + + /** + * @type {Color} + */ + _colorZ = new Color(0.3, 0.3, 1); + + /** + * @type {{ + * nx: SVGAElement, + * ny: SVGAElement, + * nz: SVGAElement, + * xaxis: SVGLineElement, + * yaxis: SVGLineElement, + * zaxis: SVGLineElement, + * px: SVGAElement, + * py: SVGAElement, + * pz: SVGAElement + * }} + */ + _shapes; + + /** + * @type {(cameraMatrix: Mat4) => void} + */ + update; + + constructor() { + super(); + this.dom = document.createElement('div'); + this.dom.id = 'view-cube-container'; + this.dom.style.cssText = [ + 'position: absolute', + 'width: 140px', + 'height: 140px', + `top: ${this._anchor.x ? '0px' : 'auto'}`, + `right: ${this._anchor.y ? '0px' : 'auto'}`, + `bottom: ${this._anchor.z ? '0px' : 'auto'}`, + `left: ${this._anchor.w ? '0px' : 'auto'}`, + 'margin: auto', + 'pointer-events: none' + ].join(';'); + document.body.appendChild(this.dom); + + // construct svg elements + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.id = 'view-cube-svg'; + + const group = document.createElementNS(svg.namespaceURI, 'g'); + svg.appendChild(group); + + /** + * @param {string} color - The color. + * @param {boolean} [fill] - The fill. + * @param {string} [text] - The text. + * @returns {SVGAElement} - The circle. + */ + const circle = (color, fill = false, text) => { + const result = /** @type {SVGAElement} */ (document.createElementNS(svg.namespaceURI, 'g')); + + const circle = /** @type {SVGCircleElement} */ (document.createElementNS(svg.namespaceURI, 'circle')); + circle.setAttribute('fill', fill ? color : '#555'); + circle.setAttribute('stroke', color); + circle.setAttribute('stroke-width', '2'); + circle.setAttribute('r', '10'); + circle.setAttribute('cx', '0'); + circle.setAttribute('cy', '0'); + circle.setAttribute('pointer-events', 'all'); + + result.appendChild(circle); + + if (text) { + const t = /** @type {SVGTextElement} */ (document.createElementNS(svg.namespaceURI, 'text')); + t.setAttribute('font-size', '10'); + t.setAttribute('font-family', 'Arial'); + t.setAttribute('font-weight', 'bold'); + t.setAttribute('text-anchor', 'middle'); + t.setAttribute('alignment-baseline', 'central'); + t.textContent = text; + result.appendChild(t); + } + + result.setAttribute('cursor', 'pointer'); + + group.appendChild(result); + + return result; + }; + + /** + * @param {string} color - The color. + * @returns {SVGLineElement} - The line. + */ + const line = (color) => { + const result = /** @type {SVGLineElement} */ (document.createElementNS(svg.namespaceURI, 'line')); + result.setAttribute('stroke', color); + result.setAttribute('stroke-width', '2'); + group.appendChild(result); + return result; + }; + + const colX = this._colorX.toString(false); + const colY = this._colorY.toString(false); + const colZ = this._colorZ.toString(false); + + this._shapes = { + nx: circle(colX), + ny: circle(colY), + nz: circle(colZ), + xaxis: line(colX), + yaxis: line(colY), + zaxis: line(colZ), + px: circle(colX, true, 'X'), + py: circle(colY, true, 'Y'), + pz: circle(colZ, true, 'Z') + }; + + this._shapes.px.children[0].addEventListener('pointerdown', () => { + this.fire(ViewCube.EVENT_CAMERAALIGN, 'px'); + }); + this._shapes.py.children[0].addEventListener('pointerdown', () => { + this.fire(ViewCube.EVENT_CAMERAALIGN, 'py'); + }); + this._shapes.pz.children[0].addEventListener('pointerdown', () => { + this.fire(ViewCube.EVENT_CAMERAALIGN, 'pz'); + }); + this._shapes.nx.children[0].addEventListener('pointerdown', () => { + this.fire(ViewCube.EVENT_CAMERAALIGN, 'nx'); + }); + this._shapes.ny.children[0].addEventListener('pointerdown', () => { + this.fire(ViewCube.EVENT_CAMERAALIGN, 'ny'); + }); + this._shapes.nz.children[0].addEventListener('pointerdown', () => { + this.fire(ViewCube.EVENT_CAMERAALIGN, 'nz'); + }); + + this.dom.appendChild(svg); + + let cw = 0; + let ch = 0; + + /** + * @param {Mat4} cameraMatrix - The camera matrix. + */ + this.update = (cameraMatrix) => { + const w = this.dom.clientWidth; + const h = this.dom.clientHeight; + + if (w && h) { + if (w !== cw || h !== ch) { + // resize elements + svg.setAttribute('width', w.toString()); + svg.setAttribute('height', h.toString()); + group.setAttribute('transform', `translate(${w * 0.5}, ${h * 0.5})`); + cw = w; + ch = h; + } + + tmpM1.invert(cameraMatrix); + tmpM1.getX(tmpV1); + tmpM1.getY(tmpV2); + tmpM1.getZ(tmpV3); + + /** + * @param {SVGAElement} group - The group. + * @param {number} x - The x. + * @param {number} y - The y. + */ + const transform = (group, x, y) => { + group.setAttribute('transform', `translate(${x * 40}, ${y * 40})`); + }; + + /** + * @param {SVGLineElement} line - The line. + * @param {number} x - The x. + * @param {number} y - The y. + */ + const x2y2 = (line, x, y) => { + line.setAttribute('x2', (x * 40).toString()); + line.setAttribute('y2', (y * 40).toString()); + }; + + transform(this._shapes.px, tmpV1.x, -tmpV1.y); + transform(this._shapes.nx, -tmpV1.x, tmpV1.y); + transform(this._shapes.py, tmpV2.x, -tmpV2.y); + transform(this._shapes.ny, -tmpV2.x, tmpV2.y); + transform(this._shapes.pz, tmpV3.x, -tmpV3.y); + transform(this._shapes.nz, -tmpV3.x, tmpV3.y); + + x2y2(this._shapes.xaxis, tmpV1.x, -tmpV1.y); + x2y2(this._shapes.yaxis, tmpV2.x, -tmpV2.y); + x2y2(this._shapes.zaxis, tmpV3.x, -tmpV3.y); + + // reorder dom for the mighty svg painter's algorithm + const order = [ + { n: ['xaxis', 'px'], value: tmpV1.z }, + { n: ['yaxis', 'py'], value: tmpV2.z }, + { n: ['zaxis', 'pz'], value: tmpV3.z }, + { n: ['nx'], value: -tmpV1.z }, + { n: ['ny'], value: -tmpV2.z }, + { n: ['nz'], value: -tmpV3.z } + ].sort((a, b) => a.value - b.value); + + const fragment = document.createDocumentFragment(); + + order.forEach((o) => { + o.n.forEach((n) => { + fragment.appendChild(this._shapes[n]); + }); + }); + + group.appendChild(fragment); + } + }; + } + + set anchor(value) { + this._anchor.copy(value); + + this.dom.style.top = this._anchor.x ? '0px' : 'auto'; + this.dom.style.right = this._anchor.y ? '0px' : 'auto'; + this.dom.style.bottom = this._anchor.z ? '0px' : 'auto'; + this.dom.style.left = this._anchor.w ? '0px' : 'auto'; + } + + get anchor() { + return this._anchor; + } + + /** + * @attribute + * @type {Color} + */ + set colorX(value) { + this._colorX.copy(value); + + this._shapes.px.children[0].setAttribute('fill', this._colorX.toString(false)); + this._shapes.px.children[0].setAttribute('stroke', this._colorX.toString(false)); + this._shapes.nx.children[0].setAttribute('stroke', this._colorX.toString(false)); + this._shapes.xaxis.setAttribute('stroke', this._colorX.toString(false)); + } + + get colorX() { + return this._colorX; + } + + /** + * @attribute + * @type {Color} + */ + set colorY(value) { + this._colorY.copy(value); + + this._shapes.py.children[0].setAttribute('fill', this._colorY.toString(false)); + this._shapes.py.children[0].setAttribute('stroke', this._colorY.toString(false)); + this._shapes.ny.children[0].setAttribute('stroke', this._colorY.toString(false)); + this._shapes.yaxis.setAttribute('stroke', this._colorY.toString(false)); + } + + get colorY() { + return this._colorY; + } + + /** + * @attribute + * @type {Color} + */ + set colorZ(value) { + this._colorZ.copy(value); + + this._shapes.pz.children[0].setAttribute('fill', this._colorZ.toString(false)); + this._shapes.pz.children[0].setAttribute('stroke', this._colorZ.toString(false)); + this._shapes.nz.children[0].setAttribute('stroke', this._colorZ.toString(false)); + this._shapes.zaxis.setAttribute('stroke', this._colorZ.toString(false)); + } + + get colorZ() { + return this._colorZ; + } +} + +export { ViewCube }; diff --git a/src/extras/index.js b/src/extras/index.js index c6df3f3165f..c9ed50a2efd 100644 --- a/src/extras/index.js +++ b/src/extras/index.js @@ -42,3 +42,4 @@ export { TransformGizmo } from './gizmo/transform-gizmo.js'; export { TranslateGizmo } from './gizmo/translate-gizmo.js'; export { RotateGizmo } from './gizmo/rotate-gizmo.js'; export { ScaleGizmo } from './gizmo/scale-gizmo.js'; +export { ViewCube } from './gizmo/view-cube.js'; From 59401eb37d874858049944e227b63c33c28789bf Mon Sep 17 00:00:00 2001 From: kpal Date: Mon, 27 Jan 2025 14:33:45 +0000 Subject: [PATCH 2/9] Cleaned up view cube class --- examples/src/examples/misc/editor.example.mjs | 7 + src/extras/gizmo/view-cube.js | 331 ++++++++++-------- 2 files changed, 186 insertions(+), 152 deletions(-) diff --git a/examples/src/examples/misc/editor.example.mjs b/examples/src/examples/misc/editor.example.mjs index 92ecb295ba3..d98eb9dd31c 100644 --- a/examples/src/examples/misc/editor.example.mjs +++ b/examples/src/examples/misc/editor.example.mjs @@ -182,6 +182,13 @@ setGizmoControls(); gizmoHandler.add(box); window.focus(); +// view cube +const viewCube = new pc.ViewCube(); +viewCube.anchor = new pc.Vec4(0, 1, 1, 0); +app.on('prerender', () => { + viewCube.update(camera.getWorldTransform()); +}); + // selector const layers = app.scene.layers; const selector = new Selector(app, camera.camera, [layers.getLayerByName('World')]); diff --git a/src/extras/gizmo/view-cube.js b/src/extras/gizmo/view-cube.js index a95e35bd539..a95ab27530f 100644 --- a/src/extras/gizmo/view-cube.js +++ b/src/extras/gizmo/view-cube.js @@ -1,8 +1,10 @@ -import { Color } from '../../core/math/color.js'; import { EventHandler } from '../../core/event-handler.js'; import { Mat4 } from '../../core/math/mat4.js'; import { Vec3 } from '../../core/math/vec3.js'; import { Vec4 } from '../../core/math/vec4.js'; +import { COLOR_BLUE, COLOR_GREEN, COLOR_RED } from './color.js'; + +/** @import { Color } from '../../core/math/color.js' */ const tmpV1 = new Vec3(); const tmpV2 = new Vec3(); @@ -22,25 +24,53 @@ class ViewCube extends EventHandler { */ static EVENT_CAMERAALIGN = 'camera:align'; + /** + * @type {SVGSVGElement} + * @private + */ + _svg; + + /** + * @type {Element} + * @private + */ + _group; + /** * @type {Vec4} + * @private */ _anchor = new Vec4(1, 1, 1, 1); /** * @type {Color} + * @private */ - _colorX = new Color(1, 0.3, 0.3); + _colorX = COLOR_RED.clone(); /** * @type {Color} + * @private */ - _colorY = new Color(0.3, 1, 0.3); + _colorY = COLOR_GREEN.clone(); /** * @type {Color} + * @private */ - _colorZ = new Color(0.3, 0.3, 1); + _colorZ = COLOR_BLUE.clone(); + + /** + * @type {number} + * @private + */ + _width = 0; + + /** + * @type {number} + * @private + */ + _height = 0; /** * @type {{ @@ -57,13 +87,10 @@ class ViewCube extends EventHandler { */ _shapes; - /** - * @type {(cameraMatrix: Mat4) => void} - */ - update; - constructor() { super(); + + // container this.dom = document.createElement('div'); this.dom.id = 'view-cube-container'; this.dom.style.cssText = [ @@ -79,77 +106,26 @@ class ViewCube extends EventHandler { ].join(';'); document.body.appendChild(this.dom); - // construct svg elements - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.id = 'view-cube-svg'; - - const group = document.createElementNS(svg.namespaceURI, 'g'); - svg.appendChild(group); - - /** - * @param {string} color - The color. - * @param {boolean} [fill] - The fill. - * @param {string} [text] - The text. - * @returns {SVGAElement} - The circle. - */ - const circle = (color, fill = false, text) => { - const result = /** @type {SVGAElement} */ (document.createElementNS(svg.namespaceURI, 'g')); - - const circle = /** @type {SVGCircleElement} */ (document.createElementNS(svg.namespaceURI, 'circle')); - circle.setAttribute('fill', fill ? color : '#555'); - circle.setAttribute('stroke', color); - circle.setAttribute('stroke-width', '2'); - circle.setAttribute('r', '10'); - circle.setAttribute('cx', '0'); - circle.setAttribute('cy', '0'); - circle.setAttribute('pointer-events', 'all'); - - result.appendChild(circle); - - if (text) { - const t = /** @type {SVGTextElement} */ (document.createElementNS(svg.namespaceURI, 'text')); - t.setAttribute('font-size', '10'); - t.setAttribute('font-family', 'Arial'); - t.setAttribute('font-weight', 'bold'); - t.setAttribute('text-anchor', 'middle'); - t.setAttribute('alignment-baseline', 'central'); - t.textContent = text; - result.appendChild(t); - } - - result.setAttribute('cursor', 'pointer'); - - group.appendChild(result); - - return result; - }; - - /** - * @param {string} color - The color. - * @returns {SVGLineElement} - The line. - */ - const line = (color) => { - const result = /** @type {SVGLineElement} */ (document.createElementNS(svg.namespaceURI, 'line')); - result.setAttribute('stroke', color); - result.setAttribute('stroke-width', '2'); - group.appendChild(result); - return result; - }; + // construct svg root and group + this._svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + this._svg.id = 'view-cube-svg'; + this._group = document.createElementNS(this._svg.namespaceURI, 'g'); + this._svg.appendChild(this._group); const colX = this._colorX.toString(false); const colY = this._colorY.toString(false); const colZ = this._colorZ.toString(false); this._shapes = { - nx: circle(colX), - ny: circle(colY), - nz: circle(colZ), - xaxis: line(colX), - yaxis: line(colY), - zaxis: line(colZ), - px: circle(colX, true, 'X'), - py: circle(colY, true, 'Y'), - pz: circle(colZ, true, 'Z') + nx: this._circle(colX), + ny: this._circle(colY), + nz: this._circle(colZ), + xaxis: this._line(colX), + yaxis: this._line(colY), + zaxis: this._line(colZ), + px: this._circle(colX, true, 'X'), + py: this._circle(colY, true, 'Y'), + pz: this._circle(colZ, true, 'Z') }; this._shapes.px.children[0].addEventListener('pointerdown', () => { @@ -171,84 +147,7 @@ class ViewCube extends EventHandler { this.fire(ViewCube.EVENT_CAMERAALIGN, 'nz'); }); - this.dom.appendChild(svg); - - let cw = 0; - let ch = 0; - - /** - * @param {Mat4} cameraMatrix - The camera matrix. - */ - this.update = (cameraMatrix) => { - const w = this.dom.clientWidth; - const h = this.dom.clientHeight; - - if (w && h) { - if (w !== cw || h !== ch) { - // resize elements - svg.setAttribute('width', w.toString()); - svg.setAttribute('height', h.toString()); - group.setAttribute('transform', `translate(${w * 0.5}, ${h * 0.5})`); - cw = w; - ch = h; - } - - tmpM1.invert(cameraMatrix); - tmpM1.getX(tmpV1); - tmpM1.getY(tmpV2); - tmpM1.getZ(tmpV3); - - /** - * @param {SVGAElement} group - The group. - * @param {number} x - The x. - * @param {number} y - The y. - */ - const transform = (group, x, y) => { - group.setAttribute('transform', `translate(${x * 40}, ${y * 40})`); - }; - - /** - * @param {SVGLineElement} line - The line. - * @param {number} x - The x. - * @param {number} y - The y. - */ - const x2y2 = (line, x, y) => { - line.setAttribute('x2', (x * 40).toString()); - line.setAttribute('y2', (y * 40).toString()); - }; - - transform(this._shapes.px, tmpV1.x, -tmpV1.y); - transform(this._shapes.nx, -tmpV1.x, tmpV1.y); - transform(this._shapes.py, tmpV2.x, -tmpV2.y); - transform(this._shapes.ny, -tmpV2.x, tmpV2.y); - transform(this._shapes.pz, tmpV3.x, -tmpV3.y); - transform(this._shapes.nz, -tmpV3.x, tmpV3.y); - - x2y2(this._shapes.xaxis, tmpV1.x, -tmpV1.y); - x2y2(this._shapes.yaxis, tmpV2.x, -tmpV2.y); - x2y2(this._shapes.zaxis, tmpV3.x, -tmpV3.y); - - // reorder dom for the mighty svg painter's algorithm - const order = [ - { n: ['xaxis', 'px'], value: tmpV1.z }, - { n: ['yaxis', 'py'], value: tmpV2.z }, - { n: ['zaxis', 'pz'], value: tmpV3.z }, - { n: ['nx'], value: -tmpV1.z }, - { n: ['ny'], value: -tmpV2.z }, - { n: ['nz'], value: -tmpV3.z } - ].sort((a, b) => a.value - b.value); - - const fragment = document.createDocumentFragment(); - - order.forEach((o) => { - o.n.forEach((n) => { - fragment.appendChild(this._shapes[n]); - }); - }); - - group.appendChild(fragment); - } - }; + this.dom.appendChild(this._svg); } set anchor(value) { @@ -314,6 +213,134 @@ class ViewCube extends EventHandler { get colorZ() { return this._colorZ; } + + /** + * @private + * @param {string} color - The color. + * @returns {SVGLineElement} - The line. + */ + _line(color) { + const result = /** @type {SVGLineElement} */ (document.createElementNS(this._svg.namespaceURI, 'line')); + result.setAttribute('stroke', color); + result.setAttribute('stroke-width', '2'); + this._group.appendChild(result); + return result; + } + + /** + * @private + * @param {string} color - The color. + * @param {boolean} [fill] - The fill. + * @param {string} [text] - The text. + * @returns {SVGAElement} - The circle. + */ + _circle(color, fill = false, text) { + const result = /** @type {SVGAElement} */ (document.createElementNS(this._svg.namespaceURI, 'g')); + + const circle = /** @type {SVGCircleElement} */ (document.createElementNS(this._svg.namespaceURI, 'circle')); + circle.setAttribute('fill', fill ? color : '#555'); + circle.setAttribute('stroke', color); + circle.setAttribute('stroke-width', '2'); + circle.setAttribute('r', '10'); + circle.setAttribute('cx', '0'); + circle.setAttribute('cy', '0'); + circle.setAttribute('pointer-events', 'all'); + + result.appendChild(circle); + + if (text) { + const t = /** @type {SVGTextElement} */ (document.createElementNS(this._svg.namespaceURI, 'text')); + t.setAttribute('font-size', '10'); + t.setAttribute('font-family', 'Arial'); + t.setAttribute('font-weight', 'bold'); + t.setAttribute('text-anchor', 'middle'); + t.setAttribute('alignment-baseline', 'central'); + t.textContent = text; + result.appendChild(t); + } + + result.setAttribute('cursor', 'pointer'); + + this._group.appendChild(result); + + return result; + } + + /** + * @param {Mat4} cameraMatrix - The camera matrix. + */ + update(cameraMatrix) { + const w = this.dom.clientWidth; + const h = this.dom.clientHeight; + + // skip if the container is not visible + if (!w || !h) { + return; + } + + // skip if the size has not changed + if (w !== this._width || h !== this._height) { + // resize elements + this._svg.setAttribute('width', w.toString()); + this._svg.setAttribute('height', h.toString()); + this._group.setAttribute('transform', `translate(${w * 0.5}, ${h * 0.5})`); + this._width = w; + this._height = h; + } + + tmpM1.invert(cameraMatrix); + tmpM1.getX(tmpV1); + tmpM1.getY(tmpV2); + tmpM1.getZ(tmpV3); + + /** + * @param {SVGAElement} group - The group. + * @param {number} x - The x. + * @param {number} y - The y. + */ + const transform = (group, x, y) => { + group.setAttribute('transform', `translate(${x * 40}, ${y * 40})`); + }; + + /** + * @param {SVGLineElement} line - The line. + * @param {number} x - The x. + * @param {number} y - The y. + */ + const x2y2 = (line, x, y) => { + line.setAttribute('x2', (x * 40).toString()); + line.setAttribute('y2', (y * 40).toString()); + }; + + transform(this._shapes.px, tmpV1.x, -tmpV1.y); + transform(this._shapes.nx, -tmpV1.x, tmpV1.y); + transform(this._shapes.py, tmpV2.x, -tmpV2.y); + transform(this._shapes.ny, -tmpV2.x, tmpV2.y); + transform(this._shapes.pz, tmpV3.x, -tmpV3.y); + transform(this._shapes.nz, -tmpV3.x, tmpV3.y); + + x2y2(this._shapes.xaxis, tmpV1.x, -tmpV1.y); + x2y2(this._shapes.yaxis, tmpV2.x, -tmpV2.y); + x2y2(this._shapes.zaxis, tmpV3.x, -tmpV3.y); + + // reorder dom for the mighty svg painter's algorithm + const order = [ + { n: ['xaxis', 'px'], value: tmpV1.z }, + { n: ['yaxis', 'py'], value: tmpV2.z }, + { n: ['zaxis', 'pz'], value: tmpV3.z }, + { n: ['nx'], value: -tmpV1.z }, + { n: ['ny'], value: -tmpV2.z }, + { n: ['nz'], value: -tmpV3.z } + ].sort((a, b) => a.value - b.value); + const fragment = document.createDocumentFragment(); + order.forEach((o) => { + o.n.forEach((n) => { + fragment.appendChild(this._shapes[n]); + }); + }); + + this._group.appendChild(fragment); + } } export { ViewCube }; From 43060a7d899928f46a346af569f2f346b7680931 Mon Sep 17 00:00:00 2001 From: kpal Date: Mon, 27 Jan 2025 14:36:24 +0000 Subject: [PATCH 3/9] Added ctor argument to viewcube --- examples/src/examples/misc/editor.example.mjs | 3 +-- src/extras/gizmo/view-cube.js | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/src/examples/misc/editor.example.mjs b/examples/src/examples/misc/editor.example.mjs index d98eb9dd31c..69b2d373cf5 100644 --- a/examples/src/examples/misc/editor.example.mjs +++ b/examples/src/examples/misc/editor.example.mjs @@ -183,8 +183,7 @@ gizmoHandler.add(box); window.focus(); // view cube -const viewCube = new pc.ViewCube(); -viewCube.anchor = new pc.Vec4(0, 1, 1, 0); +const viewCube = new pc.ViewCube(new pc.Vec4(0, 1, 1, 0)); app.on('prerender', () => { viewCube.update(camera.getWorldTransform()); }); diff --git a/src/extras/gizmo/view-cube.js b/src/extras/gizmo/view-cube.js index a95ab27530f..969a727592f 100644 --- a/src/extras/gizmo/view-cube.js +++ b/src/extras/gizmo/view-cube.js @@ -87,9 +87,14 @@ class ViewCube extends EventHandler { */ _shapes; - constructor() { + /** + * @param {Vec4} [anchor] - The anchor. + */ + constructor(anchor) { super(); + this._anchor = anchor ?? this._anchor; + // container this.dom = document.createElement('div'); this.dom.id = 'view-cube-container'; From a6f567dfb186bc8d1eeace74a556e6e7abf67451 Mon Sep 17 00:00:00 2001 From: kpal Date: Mon, 27 Jan 2025 16:26:33 +0000 Subject: [PATCH 4/9] More refactors to view cube --- src/extras/gizmo/view-cube.js | 66 +++++++++++++++++------------------ 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/src/extras/gizmo/view-cube.js b/src/extras/gizmo/view-cube.js index 969a727592f..5ac1ff83f9a 100644 --- a/src/extras/gizmo/view-cube.js +++ b/src/extras/gizmo/view-cube.js @@ -93,8 +93,6 @@ class ViewCube extends EventHandler { constructor(anchor) { super(); - this._anchor = anchor ?? this._anchor; - // container this.dom = document.createElement('div'); this.dom.id = 'view-cube-container'; @@ -102,15 +100,13 @@ class ViewCube extends EventHandler { 'position: absolute', 'width: 140px', 'height: 140px', - `top: ${this._anchor.x ? '0px' : 'auto'}`, - `right: ${this._anchor.y ? '0px' : 'auto'}`, - `bottom: ${this._anchor.z ? '0px' : 'auto'}`, - `left: ${this._anchor.w ? '0px' : 'auto'}`, 'margin: auto', 'pointer-events: none' ].join(';'); document.body.appendChild(this.dom); + this.anchor = anchor ?? this._anchor; + // construct svg root and group this._svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this._svg.id = 'view-cube-svg'; @@ -219,6 +215,27 @@ class ViewCube extends EventHandler { return this._colorZ; } + /** + * @private + * @param {SVGAElement} group - The group. + * @param {number} x - The x. + * @param {number} y - The y. + */ + _transform(group, x, y) { + group.setAttribute('transform', `translate(${x * 40}, ${y * 40})`); + } + + /** + * @private + * @param {SVGLineElement} line - The line. + * @param {number} x - The x. + * @param {number} y - The y. + */ + _x2y2(line, x, y) { + line.setAttribute('x2', `${x * 40}`); + line.setAttribute('y2', `${y * 40}`); + } + /** * @private * @param {string} color - The color. @@ -298,35 +315,16 @@ class ViewCube extends EventHandler { tmpM1.getY(tmpV2); tmpM1.getZ(tmpV3); - /** - * @param {SVGAElement} group - The group. - * @param {number} x - The x. - * @param {number} y - The y. - */ - const transform = (group, x, y) => { - group.setAttribute('transform', `translate(${x * 40}, ${y * 40})`); - }; - - /** - * @param {SVGLineElement} line - The line. - * @param {number} x - The x. - * @param {number} y - The y. - */ - const x2y2 = (line, x, y) => { - line.setAttribute('x2', (x * 40).toString()); - line.setAttribute('y2', (y * 40).toString()); - }; - - transform(this._shapes.px, tmpV1.x, -tmpV1.y); - transform(this._shapes.nx, -tmpV1.x, tmpV1.y); - transform(this._shapes.py, tmpV2.x, -tmpV2.y); - transform(this._shapes.ny, -tmpV2.x, tmpV2.y); - transform(this._shapes.pz, tmpV3.x, -tmpV3.y); - transform(this._shapes.nz, -tmpV3.x, tmpV3.y); + this._transform(this._shapes.px, tmpV1.x, -tmpV1.y); + this._transform(this._shapes.nx, -tmpV1.x, tmpV1.y); + this._transform(this._shapes.py, tmpV2.x, -tmpV2.y); + this._transform(this._shapes.ny, -tmpV2.x, tmpV2.y); + this._transform(this._shapes.pz, tmpV3.x, -tmpV3.y); + this._transform(this._shapes.nz, -tmpV3.x, tmpV3.y); - x2y2(this._shapes.xaxis, tmpV1.x, -tmpV1.y); - x2y2(this._shapes.yaxis, tmpV2.x, -tmpV2.y); - x2y2(this._shapes.zaxis, tmpV3.x, -tmpV3.y); + this._x2y2(this._shapes.xaxis, tmpV1.x, -tmpV1.y); + this._x2y2(this._shapes.yaxis, tmpV2.x, -tmpV2.y); + this._x2y2(this._shapes.zaxis, tmpV3.x, -tmpV3.y); // reorder dom for the mighty svg painter's algorithm const order = [ From 62c13665d7ca743580d6d15bfddcf2061c54fade Mon Sep 17 00:00:00 2001 From: kpal Date: Mon, 27 Jan 2025 17:37:47 +0000 Subject: [PATCH 5/9] Exposed color neg line thickness line length radius and text size --- .../src/examples/misc/editor.controls.mjs | 78 ++++++++ examples/src/examples/misc/editor.example.mjs | 42 +++- src/extras/gizmo/view-cube.js | 185 ++++++++++++++---- 3 files changed, 269 insertions(+), 36 deletions(-) diff --git a/examples/src/examples/misc/editor.controls.mjs b/examples/src/examples/misc/editor.controls.mjs index 4ae549b9333..1190246f0ab 100644 --- a/examples/src/examples/misc/editor.controls.mjs +++ b/examples/src/examples/misc/editor.controls.mjs @@ -147,6 +147,84 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { link: { observer, path: 'grid.colorZ' } }) ) + ), + jsx( + Panel, + { headerText: 'View Cube' }, + jsx( + LabelGroup, + { text: 'Size' }, + jsx(SliderInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'viewCube.size' }, + min: 10, + max: 200 + }) + ), + jsx( + LabelGroup, + { text: 'Color X' }, + jsx(ColorPicker, { + binding: new BindingTwoWay(), + link: { observer, path: 'viewCube.colorX' } + }) + ), + jsx( + LabelGroup, + { text: 'Color Y' }, + jsx(ColorPicker, { + binding: new BindingTwoWay(), + link: { observer, path: 'viewCube.colorY' } + }) + ), + jsx( + LabelGroup, + { text: 'Color Z' }, + jsx(ColorPicker, { + binding: new BindingTwoWay(), + link: { observer, path: 'viewCube.colorZ' } + }) + ), + jsx( + LabelGroup, + { text: 'Radius' }, + jsx(SliderInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'viewCube.radius' }, + min: 10, + max: 50 + }) + ), + jsx( + LabelGroup, + { text: 'Text Size' }, + jsx(SliderInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'viewCube.textSize' }, + min: 1, + max: 30 + }) + ), + jsx( + LabelGroup, + { text: 'Line Thickness' }, + jsx(SliderInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'viewCube.lineThickness' }, + min: 1, + max: 10 + }) + ), + jsx( + LabelGroup, + { text: 'Line Length' }, + jsx(SliderInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'viewCube.lineLength' }, + min: 10, + max: 100 + }) + ) ) ); }; diff --git a/examples/src/examples/misc/editor.example.mjs b/examples/src/examples/misc/editor.example.mjs index 69b2d373cf5..039e2e186c4 100644 --- a/examples/src/examples/misc/editor.example.mjs +++ b/examples/src/examples/misc/editor.example.mjs @@ -183,7 +183,17 @@ gizmoHandler.add(box); window.focus(); // view cube -const viewCube = new pc.ViewCube(new pc.Vec4(0, 1, 1, 0)); +const viewCube = new pc.ViewCube(new pc.Vec4(1, 1, 1, 1)); +data.set('viewCube', { + size: 140, + colorX: Object.values(viewCube.colorX), + colorY: Object.values(viewCube.colorY), + colorZ: Object.values(viewCube.colorZ), + radius: viewCube.radius, + textSize: viewCube.textSize, + lineThickness: viewCube.lineThickness, + lineLength: viewCube.lineLength +}); app.on('prerender', () => { viewCube.update(camera.getWorldTransform()); }); @@ -294,6 +304,36 @@ data.on('*:set', (/** @type {string} */ path, /** @type {any} */ value) => { } break; } + case 'viewCube': { + switch (key) { + case 'size': + viewCube.dom.style.width = `${value}px`; + viewCube.dom.style.height = `${value}px`; + break; + case 'colorX': + viewCube.colorX = tmpC1.set(value[0], value[1], value[2]); + break; + case 'colorY': + viewCube.colorY = tmpC1.set(value[0], value[1], value[2]); + break; + case 'colorZ': + viewCube.colorZ = tmpC1.set(value[0], value[1], value[2]); + break; + case 'radius': + viewCube.radius = value; + break; + case 'textSize': + viewCube.textSize = value; + break; + case 'lineThickness': + viewCube.lineThickness = value; + break; + case 'lineLength': + viewCube.lineLength = value; + break; + } + break; + } } }); diff --git a/src/extras/gizmo/view-cube.js b/src/extras/gizmo/view-cube.js index 5ac1ff83f9a..449557919ee 100644 --- a/src/extras/gizmo/view-cube.js +++ b/src/extras/gizmo/view-cube.js @@ -1,11 +1,10 @@ import { EventHandler } from '../../core/event-handler.js'; +import { Color } from '../../core/math/color.js'; import { Mat4 } from '../../core/math/mat4.js'; import { Vec3 } from '../../core/math/vec3.js'; import { Vec4 } from '../../core/math/vec4.js'; import { COLOR_BLUE, COLOR_GREEN, COLOR_RED } from './color.js'; -/** @import { Color } from '../../core/math/color.js' */ - const tmpV1 = new Vec3(); const tmpV2 = new Vec3(); const tmpV3 = new Vec3(); @@ -24,6 +23,18 @@ class ViewCube extends EventHandler { */ static EVENT_CAMERAALIGN = 'camera:align'; + /** + * @type {number} + * @private + */ + _width = 140; + + /** + * @type {number} + * @private + */ + _height = 140; + /** * @type {SVGSVGElement} * @private @@ -60,29 +71,47 @@ class ViewCube extends EventHandler { */ _colorZ = COLOR_BLUE.clone(); + /** + * @type {Color} + * @private + */ + _colorNeg = new Color(0.3, 0.3, 0.3); + + /** + * @type {number} + * @private + */ + _radius = 10; + + /** + * @type {number} + * @private + */ + _textSize = 10; + /** * @type {number} * @private */ - _width = 0; + _lineThickness = 2; /** * @type {number} * @private */ - _height = 0; + _lineLength = 40; /** * @type {{ * nx: SVGAElement, * ny: SVGAElement, * nz: SVGAElement, - * xaxis: SVGLineElement, - * yaxis: SVGLineElement, - * zaxis: SVGLineElement, * px: SVGAElement, * py: SVGAElement, - * pz: SVGAElement + * pz: SVGAElement, + * xaxis: SVGLineElement, + * yaxis: SVGLineElement, + * zaxis: SVGLineElement * }} */ _shapes; @@ -98,8 +127,8 @@ class ViewCube extends EventHandler { this.dom.id = 'view-cube-container'; this.dom.style.cssText = [ 'position: absolute', - 'width: 140px', - 'height: 140px', + `width: ${this._width}px`, + `height: ${this._height}px`, 'margin: auto', 'pointer-events: none' ].join(';'); @@ -112,6 +141,7 @@ class ViewCube extends EventHandler { this._svg.id = 'view-cube-svg'; this._group = document.createElementNS(this._svg.namespaceURI, 'g'); this._svg.appendChild(this._group); + this._resize(this._width, this._height); const colX = this._colorX.toString(false); const colY = this._colorY.toString(false); @@ -121,12 +151,12 @@ class ViewCube extends EventHandler { nx: this._circle(colX), ny: this._circle(colY), nz: this._circle(colZ), - xaxis: this._line(colX), - yaxis: this._line(colY), - zaxis: this._line(colZ), px: this._circle(colX, true, 'X'), py: this._circle(colY, true, 'Y'), - pz: this._circle(colZ, true, 'Z') + pz: this._circle(colZ, true, 'Z'), + xaxis: this._line(colX), + yaxis: this._line(colY), + zaxis: this._line(colZ) }; this._shapes.px.children[0].addEventListener('pointerdown', () => { @@ -165,7 +195,6 @@ class ViewCube extends EventHandler { } /** - * @attribute * @type {Color} */ set colorX(value) { @@ -182,7 +211,6 @@ class ViewCube extends EventHandler { } /** - * @attribute * @type {Color} */ set colorY(value) { @@ -199,7 +227,6 @@ class ViewCube extends EventHandler { } /** - * @attribute * @type {Color} */ set colorZ(value) { @@ -215,6 +242,97 @@ class ViewCube extends EventHandler { return this._colorZ; } + /** + * @type {Color} + */ + set colorNeg(value) { + this._colorNeg.copy(value); + + this._shapes.px.children[0].setAttribute('fill', this._colorNeg.toString(false)); + this._shapes.py.children[0].setAttribute('fill', this._colorNeg.toString(false)); + this._shapes.pz.children[0].setAttribute('fill', this._colorNeg.toString(false)); + } + + get colorNeg() { + return this._colorNeg; + } + + /** + * @type {number} + */ + set radius(value) { + this._radius = value; + + this._shapes.px.children[0].setAttribute('r', `${value}`); + this._shapes.py.children[0].setAttribute('r', `${value}`); + this._shapes.pz.children[0].setAttribute('r', `${value}`); + this._shapes.nx.children[0].setAttribute('r', `${value}`); + this._shapes.ny.children[0].setAttribute('r', `${value}`); + this._shapes.nz.children[0].setAttribute('r', `${value}`); + } + + get radius() { + return this._radius; + } + + /** + * @type {number} + */ + set textSize(value) { + this._textSize = value; + + this._shapes.px.children[1].setAttribute('font-size', `${value}`); + this._shapes.py.children[1].setAttribute('font-size', `${value}`); + this._shapes.pz.children[1].setAttribute('font-size', `${value}`); + } + + get textSize() { + return this._textSize; + } + + /** + * @type {number} + */ + set lineThickness(value) { + this._lineThickness = value; + + this._shapes.xaxis.setAttribute('stroke-width', `${value}`); + this._shapes.yaxis.setAttribute('stroke-width', `${value}`); + this._shapes.zaxis.setAttribute('stroke-width', `${value}`); + this._shapes.px.children[0].setAttribute('stroke-width', `${value}`); + this._shapes.py.children[0].setAttribute('stroke-width', `${value}`); + this._shapes.pz.children[0].setAttribute('stroke-width', `${value}`); + this._shapes.nx.children[0].setAttribute('stroke-width', `${value}`); + this._shapes.ny.children[0].setAttribute('stroke-width', `${value}`); + this._shapes.nz.children[0].setAttribute('stroke-width', `${value}`); + } + + get lineThickness() { + return this._lineThickness; + } + + /** + * @type {number} + */ + set lineLength(value) { + this._lineLength = value; + } + + get lineLength() { + return this._lineLength; + } + + /** + * @private + * @param {number} x - The x. + * @param {number} y - The y. + */ + _resize(x, y) { + this._svg.setAttribute('width', `${x}`); + this._svg.setAttribute('height', `${y}`); + this._group.setAttribute('transform', `translate(${x * 0.5}, ${y * 0.5})`); + } + /** * @private * @param {SVGAElement} group - The group. @@ -222,7 +340,7 @@ class ViewCube extends EventHandler { * @param {number} y - The y. */ _transform(group, x, y) { - group.setAttribute('transform', `translate(${x * 40}, ${y * 40})`); + group.setAttribute('transform', `translate(${x * this._lineLength}, ${y * this._lineLength})`); } /** @@ -232,8 +350,8 @@ class ViewCube extends EventHandler { * @param {number} y - The y. */ _x2y2(line, x, y) { - line.setAttribute('x2', `${x * 40}`); - line.setAttribute('y2', `${y * 40}`); + line.setAttribute('x2', `${x * this._lineLength}`); + line.setAttribute('y2', `${y * this._lineLength}`); } /** @@ -244,7 +362,7 @@ class ViewCube extends EventHandler { _line(color) { const result = /** @type {SVGLineElement} */ (document.createElementNS(this._svg.namespaceURI, 'line')); result.setAttribute('stroke', color); - result.setAttribute('stroke-width', '2'); + result.setAttribute('stroke-width', `${this._lineThickness}`); this._group.appendChild(result); return result; } @@ -257,35 +375,35 @@ class ViewCube extends EventHandler { * @returns {SVGAElement} - The circle. */ _circle(color, fill = false, text) { - const result = /** @type {SVGAElement} */ (document.createElementNS(this._svg.namespaceURI, 'g')); + const group = /** @type {SVGAElement} */ (document.createElementNS(this._svg.namespaceURI, 'g')); const circle = /** @type {SVGCircleElement} */ (document.createElementNS(this._svg.namespaceURI, 'circle')); - circle.setAttribute('fill', fill ? color : '#555'); + circle.setAttribute('fill', fill ? color : this._colorNeg.toString(false)); circle.setAttribute('stroke', color); - circle.setAttribute('stroke-width', '2'); - circle.setAttribute('r', '10'); + circle.setAttribute('stroke-width', `${this._lineThickness}`); + circle.setAttribute('r', `${this._radius}`); circle.setAttribute('cx', '0'); circle.setAttribute('cy', '0'); circle.setAttribute('pointer-events', 'all'); - result.appendChild(circle); + group.appendChild(circle); if (text) { const t = /** @type {SVGTextElement} */ (document.createElementNS(this._svg.namespaceURI, 'text')); - t.setAttribute('font-size', '10'); + t.setAttribute('font-size', `${this._textSize}`); t.setAttribute('font-family', 'Arial'); t.setAttribute('font-weight', 'bold'); t.setAttribute('text-anchor', 'middle'); t.setAttribute('alignment-baseline', 'central'); t.textContent = text; - result.appendChild(t); + group.appendChild(t); } - result.setAttribute('cursor', 'pointer'); + group.setAttribute('cursor', 'pointer'); - this._group.appendChild(result); + this._group.appendChild(group); - return result; + return group; } /** @@ -302,12 +420,9 @@ class ViewCube extends EventHandler { // skip if the size has not changed if (w !== this._width || h !== this._height) { - // resize elements - this._svg.setAttribute('width', w.toString()); - this._svg.setAttribute('height', h.toString()); - this._group.setAttribute('transform', `translate(${w * 0.5}, ${h * 0.5})`); this._width = w; this._height = h; + this._resize(w, h); } tmpM1.invert(cameraMatrix); From 95ffef6961482ef088db34567af6cb02f987da7c Mon Sep 17 00:00:00 2001 From: kpal Date: Mon, 27 Jan 2025 17:53:37 +0000 Subject: [PATCH 6/9] Replaced width and height with internal sizing --- .../src/examples/misc/editor.controls.mjs | 16 ++----- examples/src/examples/misc/editor.example.mjs | 1 - src/extras/gizmo/view-cube.js | 47 ++++++++----------- 3 files changed, 23 insertions(+), 41 deletions(-) diff --git a/examples/src/examples/misc/editor.controls.mjs b/examples/src/examples/misc/editor.controls.mjs index 1190246f0ab..4be9ebd2448 100644 --- a/examples/src/examples/misc/editor.controls.mjs +++ b/examples/src/examples/misc/editor.controls.mjs @@ -151,16 +151,6 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { jsx( Panel, { headerText: 'View Cube' }, - jsx( - LabelGroup, - { text: 'Size' }, - jsx(SliderInput, { - binding: new BindingTwoWay(), - link: { observer, path: 'viewCube.size' }, - min: 10, - max: 200 - }) - ), jsx( LabelGroup, { text: 'Color X' }, @@ -202,7 +192,7 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { binding: new BindingTwoWay(), link: { observer, path: 'viewCube.textSize' }, min: 1, - max: 30 + max: 50 }) ), jsx( @@ -212,7 +202,7 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { binding: new BindingTwoWay(), link: { observer, path: 'viewCube.lineThickness' }, min: 1, - max: 10 + max: 20 }) ), jsx( @@ -222,7 +212,7 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { binding: new BindingTwoWay(), link: { observer, path: 'viewCube.lineLength' }, min: 10, - max: 100 + max: 200 }) ) ) diff --git a/examples/src/examples/misc/editor.example.mjs b/examples/src/examples/misc/editor.example.mjs index 039e2e186c4..1e30fb27a0c 100644 --- a/examples/src/examples/misc/editor.example.mjs +++ b/examples/src/examples/misc/editor.example.mjs @@ -185,7 +185,6 @@ window.focus(); // view cube const viewCube = new pc.ViewCube(new pc.Vec4(1, 1, 1, 1)); data.set('viewCube', { - size: 140, colorX: Object.values(viewCube.colorX), colorY: Object.values(viewCube.colorY), colorZ: Object.values(viewCube.colorZ), diff --git a/src/extras/gizmo/view-cube.js b/src/extras/gizmo/view-cube.js index 449557919ee..73b7e85f3d6 100644 --- a/src/extras/gizmo/view-cube.js +++ b/src/extras/gizmo/view-cube.js @@ -27,13 +27,7 @@ class ViewCube extends EventHandler { * @type {number} * @private */ - _width = 140; - - /** - * @type {number} - * @private - */ - _height = 140; + _size = 0; /** * @type {SVGSVGElement} @@ -127,8 +121,6 @@ class ViewCube extends EventHandler { this.dom.id = 'view-cube-container'; this.dom.style.cssText = [ 'position: absolute', - `width: ${this._width}px`, - `height: ${this._height}px`, 'margin: auto', 'pointer-events: none' ].join(';'); @@ -141,7 +133,9 @@ class ViewCube extends EventHandler { this._svg.id = 'view-cube-svg'; this._group = document.createElementNS(this._svg.namespaceURI, 'g'); this._svg.appendChild(this._group); - this._resize(this._width, this._height); + + // size + this._resize(); const colX = this._colorX.toString(false); const colY = this._colorY.toString(false); @@ -269,6 +263,8 @@ class ViewCube extends EventHandler { this._shapes.nx.children[0].setAttribute('r', `${value}`); this._shapes.ny.children[0].setAttribute('r', `${value}`); this._shapes.nz.children[0].setAttribute('r', `${value}`); + + this._resize(); } get radius() { @@ -305,6 +301,8 @@ class ViewCube extends EventHandler { this._shapes.nx.children[0].setAttribute('stroke-width', `${value}`); this._shapes.ny.children[0].setAttribute('stroke-width', `${value}`); this._shapes.nz.children[0].setAttribute('stroke-width', `${value}`); + + this._resize(); } get lineThickness() { @@ -316,6 +314,8 @@ class ViewCube extends EventHandler { */ set lineLength(value) { this._lineLength = value; + + this._resize(); } get lineLength() { @@ -324,13 +324,16 @@ class ViewCube extends EventHandler { /** * @private - * @param {number} x - The x. - * @param {number} y - The y. */ - _resize(x, y) { - this._svg.setAttribute('width', `${x}`); - this._svg.setAttribute('height', `${y}`); - this._group.setAttribute('transform', `translate(${x * 0.5}, ${y * 0.5})`); + _resize() { + this._size = 2 * (this.lineLength + this.radius + this.lineThickness); + + this.dom.style.width = `${this._size}px`; + this.dom.style.height = `${this._size}px`; + + this._svg.setAttribute('width', `${this._size}`); + this._svg.setAttribute('height', `${this._size}`); + this._group.setAttribute('transform', `translate(${this._size * 0.5}, ${this._size * 0.5})`); } /** @@ -410,21 +413,11 @@ class ViewCube extends EventHandler { * @param {Mat4} cameraMatrix - The camera matrix. */ update(cameraMatrix) { - const w = this.dom.clientWidth; - const h = this.dom.clientHeight; - // skip if the container is not visible - if (!w || !h) { + if (!this._size) { return; } - // skip if the size has not changed - if (w !== this._width || h !== this._height) { - this._width = w; - this._height = h; - this._resize(w, h); - } - tmpM1.invert(cameraMatrix); tmpM1.getX(tmpV1); tmpM1.getY(tmpV2); From 04c3b65aed090a8543daabaf39e139fec9508842 Mon Sep 17 00:00:00 2001 From: kpal Date: Mon, 27 Jan 2025 18:00:30 +0000 Subject: [PATCH 7/9] Removed unused size --- examples/src/examples/misc/editor.example.mjs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/examples/src/examples/misc/editor.example.mjs b/examples/src/examples/misc/editor.example.mjs index 1e30fb27a0c..3079e71701e 100644 --- a/examples/src/examples/misc/editor.example.mjs +++ b/examples/src/examples/misc/editor.example.mjs @@ -305,10 +305,6 @@ data.on('*:set', (/** @type {string} */ path, /** @type {any} */ value) => { } case 'viewCube': { switch (key) { - case 'size': - viewCube.dom.style.width = `${value}px`; - viewCube.dom.style.height = `${value}px`; - break; case 'colorX': viewCube.colorX = tmpC1.set(value[0], value[1], value[2]); break; From 1f52e1b0dc1475fcdd63cd8de5e07f08aa30c0e2 Mon Sep 17 00:00:00 2001 From: kpal Date: Mon, 27 Jan 2025 18:30:41 +0000 Subject: [PATCH 8/9] Connected view cube to camera controls --- examples/src/examples/misc/editor.example.mjs | 10 +++++++++- src/extras/gizmo/view-cube.js | 12 ++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/examples/src/examples/misc/editor.example.mjs b/examples/src/examples/misc/editor.example.mjs index 3079e71701e..19e36cc5715 100644 --- a/examples/src/examples/misc/editor.example.mjs +++ b/examples/src/examples/misc/editor.example.mjs @@ -183,7 +183,7 @@ gizmoHandler.add(box); window.focus(); // view cube -const viewCube = new pc.ViewCube(new pc.Vec4(1, 1, 1, 1)); +const viewCube = new pc.ViewCube(new pc.Vec4(0, 1, 1, 0)); data.set('viewCube', { colorX: Object.values(viewCube.colorX), colorY: Object.values(viewCube.colorY), @@ -193,6 +193,14 @@ data.set('viewCube', { lineThickness: viewCube.lineThickness, lineLength: viewCube.lineLength }); +const tmpV1 = new pc.Vec3(); +viewCube.on(pc.ViewCube.EVENT_CAMERAALIGN, (/** @type {pc.Vec3} */ dir) => { + const cameraPos = camera.getPosition(); + const focusPoint = cameraControls.focusPoint; + const cameraDist = focusPoint.distance(cameraPos); + const cameraStart = tmpV1.copy(dir).mulScalar(cameraDist).add(focusPoint); + cameraControls.refocus(focusPoint, cameraStart); +}); app.on('prerender', () => { viewCube.update(camera.getWorldTransform()); }); diff --git a/src/extras/gizmo/view-cube.js b/src/extras/gizmo/view-cube.js index 73b7e85f3d6..b4f5646fcec 100644 --- a/src/extras/gizmo/view-cube.js +++ b/src/extras/gizmo/view-cube.js @@ -154,22 +154,22 @@ class ViewCube extends EventHandler { }; this._shapes.px.children[0].addEventListener('pointerdown', () => { - this.fire(ViewCube.EVENT_CAMERAALIGN, 'px'); + this.fire(ViewCube.EVENT_CAMERAALIGN, Vec3.RIGHT); }); this._shapes.py.children[0].addEventListener('pointerdown', () => { - this.fire(ViewCube.EVENT_CAMERAALIGN, 'py'); + this.fire(ViewCube.EVENT_CAMERAALIGN, Vec3.UP); }); this._shapes.pz.children[0].addEventListener('pointerdown', () => { - this.fire(ViewCube.EVENT_CAMERAALIGN, 'pz'); + this.fire(ViewCube.EVENT_CAMERAALIGN, Vec3.BACK); }); this._shapes.nx.children[0].addEventListener('pointerdown', () => { - this.fire(ViewCube.EVENT_CAMERAALIGN, 'nx'); + this.fire(ViewCube.EVENT_CAMERAALIGN, Vec3.LEFT); }); this._shapes.ny.children[0].addEventListener('pointerdown', () => { - this.fire(ViewCube.EVENT_CAMERAALIGN, 'ny'); + this.fire(ViewCube.EVENT_CAMERAALIGN, Vec3.DOWN); }); this._shapes.nz.children[0].addEventListener('pointerdown', () => { - this.fire(ViewCube.EVENT_CAMERAALIGN, 'nz'); + this.fire(ViewCube.EVENT_CAMERAALIGN, Vec3.FORWARD); }); this.dom.appendChild(this._svg); From 0dcf4c568b47d754be6cc6bc75a9a3fbd6622a06 Mon Sep 17 00:00:00 2001 From: kpal Date: Mon, 27 Jan 2025 18:34:31 +0000 Subject: [PATCH 9/9] Added margin around view cube --- examples/src/examples/misc/editor.example.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/src/examples/misc/editor.example.mjs b/examples/src/examples/misc/editor.example.mjs index 19e36cc5715..12b4c7ab7f6 100644 --- a/examples/src/examples/misc/editor.example.mjs +++ b/examples/src/examples/misc/editor.example.mjs @@ -184,6 +184,7 @@ window.focus(); // view cube const viewCube = new pc.ViewCube(new pc.Vec4(0, 1, 1, 0)); +viewCube.dom.style.margin = '20px'; data.set('viewCube', { colorX: Object.values(viewCube.colorX), colorY: Object.values(viewCube.colorY),