Skip to content

Commit

Permalink
Add view cube (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
slimbuck authored Jul 18, 2024
1 parent b7b6d76 commit 5a676da
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 7 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "supersplat",
"version": "0.23.0",
"version": "0.24.0",
"author": "PlayCanvas<support@playcanvas.com>",
"homepage": "https://playcanvas.com/supersplat/editor",
"description": "3D Gaussian Splat Editor",
Expand Down
12 changes: 12 additions & 0 deletions src/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,18 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S
}
});

// handle camera align events
events.on('camera.align', (axis: string) => {
switch (axis) {
case 'px': scene.camera.setAzimElev(90, 0); break;
case 'py': scene.camera.setAzimElev(0, -90); break;
case 'pz': scene.camera.setAzimElev(0, 0); break;
case 'nx': scene.camera.setAzimElev(270, 0); break;
case 'ny': scene.camera.setAzimElev(0, 90); break;
case 'nz': scene.camera.setAzimElev(180, 0); break;
}
});

events.on('select.all', () => {
selectedSplats().forEach((splat) => {
const splatData = splat.splatData;
Expand Down
2 changes: 1 addition & 1 deletion src/scene-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const sceneConfig = {
enableZoom: true,
dampingFactor: 0.2,
minPolarAngle: 0,
maxPolarAngle: 2.8,
maxPolarAngle: Math.PI,
minZoom: 0.001,
maxZoom: 2.0,
initialAzim: -45,
Expand Down
2 changes: 1 addition & 1 deletion src/scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ class Scene {

this.forEachElement(e => e.onPreRender());

this.events.fire('prerender');
this.events.fire('prerender', this.camera.entity.getWorldTransform());

// debug - display scene bound
if (this.config.debug.showBound) {
Expand Down
8 changes: 8 additions & 0 deletions src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,14 @@ body {
height: 100%;
}

#view-cube-container {
position: absolute;
width: 140px;
height: 140px;
right: 0px;
top: 0px;
}

#brush-select-canvas {
display: none;
position: absolute;
Expand Down
6 changes: 6 additions & 0 deletions src/tools/transform-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ class TransformTool {
const w = canvas.clientWidth;
const h = canvas.clientHeight;
this.gizmo.size = 1200 / Math.max(w, h);

// FIXME:
// this is a temporary workaround to undo gizmo's own auto scaling.
// once gizmo's autoscaling code is removed, this line can go too.
// @ts-ignore
this.gizmo._deviceStartSize = Math.min(scene.app.graphicsDevice.width, scene.app.graphicsDevice.height);
}
};

Expand Down
15 changes: 13 additions & 2 deletions src/ui/editor.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Element, Container, Label } from 'pcui';
import { Container, Label } from 'pcui';
import { ControlPanel } from './control-panel';
import { DataPanel } from './data-panel';
import { Toolbar } from './toolbar';
import { Events } from '../events';
import { Popup } from './popup';
import { ViewCube } from './view-cube';
import { Mat4 } from 'playcanvas';
import logo from './playcanvas-logo.png';

class EditorUI {
Expand Down Expand Up @@ -78,6 +80,13 @@ class EditorUI {
canvasContainer.append(filenameLabel);
canvasContainer.append(toolsContainer);

// view axes container
const viewCube = new ViewCube(events);
canvasContainer.append(viewCube);
events.on('prerender', (cameraMatrix: Mat4) => {
viewCube.update(cameraMatrix);
});

// control panel
const controlPanel = new ControlPanel(events, remoteStorageMode);

Expand Down Expand Up @@ -131,7 +140,9 @@ class EditorUI {
canvas.height = Math.ceil(canvasContainer.dom.offsetHeight * pixelRatio);

// disable context menu globally
document.addEventListener('contextmenu', event => event.preventDefault());
document.addEventListener('contextmenu', (event: MouseEvent) => {
event.preventDefault();
}, true);

// whenever the canvas container is clicked, set keyboard focus on the body
canvasContainer.dom.addEventListener('pointerdown', (event: PointerEvent) => {
Expand Down
161 changes: 161 additions & 0 deletions src/ui/view-cube.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { Container } from 'pcui';
import { Mat4, Vec3 } from 'playcanvas';
import { Events } from '../events';

const vecx = new Vec3();
const vecy = new Vec3();
const vecz = new Vec3();
const mat4 = new Mat4();

class ViewCube extends Container {
update: (cameraMatrix: Mat4) => void;

constructor(events: Events, args = {}) {
args = {
...args,
id: 'view-cube-container'
};

super(args);

// 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);

const circle = (color: string, fill: boolean, text?: string) => {
const result = document.createElementNS(svg.namespaceURI, 'g') as SVGElement;

const circle = document.createElementNS(svg.namespaceURI, 'circle') as SVGCircleElement;
circle.setAttribute('fill', color);
circle.setAttribute('fill-opacity', fill ? '1' : '0.14');
circle.setAttribute('stroke', color);
circle.setAttribute('stroke-width', '2');
circle.setAttribute('r', '10');
circle.setAttribute('cx', '0');
circle.setAttribute('cy', '0');

result.appendChild(circle);

if (text) {
const t = document.createElementNS(svg.namespaceURI, 'text') as SVGTextElement;
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.setAttribute('pointer-events', 'none');
t.textContent = text;
result.appendChild(t);
}

result.setAttribute('cursor', 'pointer');

group.appendChild(result);

return result;
};

const line = (color: string) => {
const result = document.createElementNS(svg.namespaceURI, 'line') as SVGLineElement;
result.setAttribute('stroke', color);
result.setAttribute('stroke-width', '2');
group.appendChild(result);
return result;
};

const r = '#f44';
const g = '#4f4';
const b = '#77f';

const shapes = {
nx: circle(r, false),
ny: circle(g, false),
nz: circle(b, false),
xaxis: line(r),
yaxis: line(g),
zaxis: line(b),
px: circle(r, true, 'X'),
py: circle(g, true, 'Y'),
pz: circle(b, true, 'Z'),
};

shapes.px.children[0].addEventListener('pointerdown', () => { events.fire('camera.align', 'px'); });
shapes.py.children[0].addEventListener('pointerdown', () => { events.fire('camera.align', 'py'); });
shapes.pz.children[0].addEventListener('pointerdown', () => { events.fire('camera.align', 'pz'); });
shapes.nx.children[0].addEventListener('pointerdown', () => { events.fire('camera.align', 'nx'); });
shapes.ny.children[0].addEventListener('pointerdown', () => { events.fire('camera.align', 'ny'); });
shapes.nz.children[0].addEventListener('pointerdown', () => { events.fire('camera.align', 'nz'); });

this.dom.appendChild(svg);

let cw = 0;
let ch = 0;

this.update = (cameraMatrix: Mat4) => {
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;
}

mat4.invert(cameraMatrix);
mat4.getX(vecx);
mat4.getY(vecy);
mat4.getZ(vecz);

const transform = (group: SVGElement, x: number, y: number) => {
group.setAttribute('transform', `translate(${x * 40}, ${y * 40})`);
};

const x2y2 = (line: SVGLineElement, x: number, y: number) => {
line.setAttribute('x2', (x * 40).toString());
line.setAttribute('y2', (y * 40).toString());
};

transform(shapes.px, vecx.x, -vecx.y);
transform(shapes.nx, -vecx.x, vecx.y);
transform(shapes.py, vecy.x, -vecy.y);
transform(shapes.ny, -vecy.x, vecy.y);
transform(shapes.pz, vecz.x, -vecz.y);
transform(shapes.nz, -vecz.x, vecz.y);

x2y2(shapes.xaxis, vecx.x, -vecx.y);
x2y2(shapes.yaxis, vecy.x, -vecy.y);
x2y2(shapes.zaxis, vecz.x, -vecz.y);

// reorder dom for the mighty svg painter's algorithm
const order = [
{ n: ['xaxis', 'px'], value: vecx.z },
{ n: ['yaxis', 'py'], value: vecy.z },
{ n: ['zaxis', 'pz'], value: vecz.z },
{ n: ['nx'], value: -vecx.z },
{ n: ['ny'], value: -vecy.z },
{ n: ['nz'], value: -vecz.z }
].sort((a, b) => a.value - b.value);

const fragment = document.createDocumentFragment();

order.forEach((o) => {
o.n.forEach((n) => {
// @ts-ignore
fragment.appendChild(shapes[n]);
});
});

group.appendChild(fragment);
}
};
}
}

export { ViewCube };

0 comments on commit 5a676da

Please # to comment.