Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 103 additions & 35 deletions src/ui/camera-info-overlay.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
import { Container, Label } from '@playcanvas/pcui';
import { Vec3 } from 'playcanvas';

import { Events } from '../events';
import { i18n } from './localization';
import { Tooltips } from './tooltips';

// Accepts "1,2,3", "1, 2, 3", "1 2 3", with or without trailing whitespace.
const parseVector = (text: string): [number, number, number] | null => {
const parts = text.trim().split(/[\s,]+/).filter(p => p.length > 0);
if (parts.length !== 3) return null;
const nums = parts.map(Number);
if (nums.some(n => !Number.isFinite(n))) return null;
return [nums[0], nums[1], nums[2]];
};

const flash = (dom: HTMLElement, cls: string) => {
dom.classList.add(cls);
setTimeout(() => dom.classList.remove(cls), 250);
};

// round to 2 decimals and drop trailing zeros ("0.00" -> "0", "0.50" -> "0.5")
const fmt = (n: number) => `${parseFloat(n.toFixed(2))}`;

class CameraInfoOverlay extends Container {
constructor(events: Events, tooltips: Tooltips) {
super({
Expand All @@ -16,15 +34,15 @@ class CameraInfoOverlay extends Container {
this.dom.addEventListener(eventName, (event: Event) => event.stopPropagation());
});

const createRow = (localeKey: string) => {
const createRow = (letter: string, tooltipKey: string, apply: (value: [number, number, number]) => void) => {
const row = new Container({
class: 'camera-info-row'
});

const key = new Label({
class: 'camera-info-key'
class: 'camera-info-key',
text: letter
});
i18n.bindText(key, localeKey);

const value = new Label({
class: 'camera-info-value'
Expand All @@ -34,40 +52,96 @@ class CameraInfoOverlay extends Container {
row.append(value);
this.append(row);

// display holds the latest formatted value, copyText the full
// precision equivalent. copiedUntil suppresses live updates while
// the 'Copied!' feedback is showing.
const state = { copyText: '', display: '', copiedUntil: 0 };

row.dom.addEventListener('pointerdown', () => {
navigator.clipboard.writeText(state.copyText);
tooltips.register(row, () => i18n.t(tooltipKey), 'top');

const dom = value.dom;
dom.setAttribute('contenteditable', 'plaintext-only');
dom.setAttribute('spellcheck', 'false');

// display holds the latest formatted value; live updates are
// suppressed while the user is editing so their input isn't
// overwritten by the per-frame refresh.
let display = '';
let editing = false;
let canceled = false;

dom.addEventListener('focus', () => {
editing = true;
canceled = false;
// select all text so the user can start typing to replace
const range = document.createRange();
range.selectNodeContents(dom);
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
});

state.copiedUntil = performance.now() + 1000;
value.text = i18n.t('cursor.copied');
setTimeout(() => {
if (performance.now() >= state.copiedUntil) {
value.text = state.display;
}
}, 1000);
// stop key events reaching the editor's global shortcut handlers
const stopKey = (e: KeyboardEvent) => e.stopPropagation();
dom.addEventListener('keydown', (e: KeyboardEvent) => {
stopKey(e);
if (e.key === 'Enter') {
e.preventDefault();
dom.blur();
} else if (e.key === 'Escape') {
e.preventDefault();
canceled = true;
dom.blur();
}
});
dom.addEventListener('keyup', stopKey);
dom.addEventListener('keypress', stopKey);

dom.addEventListener('blur', () => {
const wasCanceled = canceled;
editing = false;
canceled = false;
// clear any leftover selection
window.getSelection()?.removeAllRanges();

if (!wasCanceled) {
const parsed = parseVector(dom.textContent ?? '');
if (parsed) {
apply(parsed);
flash(dom, 'flash-ok');
} else {
flash(dom, 'flash-bad');
}
}

tooltips.register(row, () => i18n.t('cursor.click-to-copy'), 'right');
// restore the live display; the next prerender picks up any
// pose change resulting from the edit
dom.textContent = display;
});

return {
update: (display: string, copyText: string) => {
state.copyText = copyText;
if (state.display !== display) {
state.display = display;
if (performance.now() >= state.copiedUntil) {
value.text = display;
}
update: (text: string) => {
if (editing) {
return;
}
if (display !== text) {
display = text;
dom.textContent = text;
}
}
};
};

const positionRow = createRow('camera-info.position');
const targetRow = createRow('camera-info.target');
const positionRow = createRow('P', 'camera-info.position', (v) => {
const { target } = events.invoke('camera.getPose');
events.fire('camera.setPose', {
position: new Vec3(v[0], v[1], v[2]),
target: new Vec3(target.x, target.y, target.z)
});
});

const targetRow = createRow('T', 'camera-info.target', (v) => {
const { position } = events.invoke('camera.getPose');
events.fire('camera.setPose', {
position: new Vec3(position.x, position.y, position.z),
target: new Vec3(v[0], v[1], v[2])
});
});

events.on('camera.showInfo', (visible: boolean) => {
this.hidden = !visible;
Expand All @@ -80,14 +154,8 @@ class CameraInfoOverlay extends Container {

const { position, target } = events.invoke('camera.getPose');

positionRow.update(
`${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}`,
`${position.x},${position.y},${position.z}`
);
targetRow.update(
`${target.x.toFixed(2)}, ${target.y.toFixed(2)}, ${target.z.toFixed(2)}`,
`${target.x},${target.y},${target.z}`
);
positionRow.update(`${fmt(position.x)}, ${fmt(position.y)}, ${fmt(position.z)}`);
targetRow.update(`${fmt(target.x)}, ${fmt(target.y)}, ${fmt(target.z)}`);
});
}
}
Expand Down
37 changes: 1 addition & 36 deletions src/ui/editor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Container, Label } from '@playcanvas/pcui';
import { Mat4, path, Vec3 } from 'playcanvas';
import { Mat4, path } from 'playcanvas';

import { DataPanel } from './data-panel';
import { Events } from '../events';
Expand Down Expand Up @@ -80,38 +80,6 @@ class EditorUI {
text: `SUPERSPLAT v${version}`
});

// cursor label
const cursorLabel = new Label({
id: 'cursor-label'
});

let fullprecision = '';

events.on('camera.focalPointPicked', (details: { position: Vec3 }) => {
cursorLabel.text = `${details.position.x.toFixed(2)}, ${details.position.y.toFixed(2)}, ${details.position.z.toFixed(2)}`;
fullprecision = `${details.position.x}, ${details.position.y}, ${details.position.z}`;
});

['pointerdown', 'pointerup', 'pointermove', 'wheel', 'dblclick'].forEach((eventName) => {
cursorLabel.dom.addEventListener(eventName, (event: Event) => event.stopPropagation());
});

cursorLabel.dom.addEventListener('pointerdown', () => {
navigator.clipboard.writeText(fullprecision);

const orig = cursorLabel.text;
cursorLabel.text = i18n.t('cursor.copied');
setTimeout(() => {
cursorLabel.text = orig;
}, 1000);
});

// the camera info overlay occupies the same corner and its target row
// shows the focal point live, so hide the cursor label while it's visible
events.on('camera.showInfo', (visible: boolean) => {
cursorLabel.hidden = visible;
});

// canvas container
const canvasContainer = new Container({
id: 'canvas-container'
Expand All @@ -138,7 +106,6 @@ class EditorUI {

canvasContainer.dom.appendChild(canvas);
canvasContainer.append(appLabel);
canvasContainer.append(cursorLabel);
canvasContainer.append(cameraInfoOverlay);
canvasContainer.append(toolsContainer);
canvasContainer.append(scenePanel);
Expand Down Expand Up @@ -180,8 +147,6 @@ class EditorUI {

editorContainer.append(mainContainer);

tooltips.register(cursorLabel, () => i18n.t('cursor.click-to-copy'), 'top');

// message popup
const popup = new Popup(tooltips);

Expand Down
2 changes: 1 addition & 1 deletion src/ui/publish-settings-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ class PublishSettingsDialog extends Container {
overrideModelToggle.value = true;
overrideAnimationToggle.value = hasPoses;
animationToggle.value = hasPoses;
loopSelect.value = 'repeat';
loopSelect.value = events.invoke('timeline.loop') ? 'repeat' : 'none';
colorPicker.value = [bgClr.r, bgClr.g, bgClr.b];
fovSlider.value = events.invoke('camera.fov');
generateLodsToggle.value = totalSplats >= 1_000_000 && isLargeScene;
Expand Down
68 changes: 36 additions & 32 deletions src/ui/scss/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -107,55 +107,59 @@ body {
text-shadow: 1px 1px 4px black;
}

#cursor-label {
position: absolute;
left: 12px;
bottom: 12px;
color: $text-primary;
text-shadow: 1px 1px 4px black;

padding: 8px;
border-radius: 4px;
border: 1px solid transparent;

cursor: pointer;

&:hover {
border: 1px solid $clr-hilight;
background-color: rgba(0, 0, 0, 0.25);
}
}

#camera-info-overlay {
position: absolute;
left: 12px;
bottom: 17px;
color: $text-primary;
text-shadow: 1px 1px 4px black;
font-family: ui-monospace, Menlo, Consolas, monospace;

.camera-info-row {
display: flex;
flex-direction: row;
padding: 2px 8px;
align-items: center;
padding: 1px 0;
font-family: inherit;
}

.camera-info-key {
margin: 0;
color: $clr-icon-hilight;
font-family: inherit;
font-weight: bold;
overflow: visible;
}

.camera-info-value {
margin: 0 0 0 8px;
color: $text-primary;
font-family: inherit;
font-variant-numeric: tabular-nums;
overflow: visible;
cursor: text;
user-select: text;
padding: 0 4px;
border-radius: 4px;
border: 1px solid transparent;
cursor: pointer;
outline: none;
transition: background-color 0.15s ease;

&:hover {
border: 1px solid $clr-hilight;
background-color: rgba(0, 0, 0, 0.25);
}
}

.camera-info-key {
min-width: 56px;
margin-right: 0;
color: $text-secondary;
}
&:focus {
border-color: $clr-hilight;
background-color: rgba(0, 0, 0, 0.25);
}

.camera-info-value {
margin-left: 0;
font-variant-numeric: tabular-nums;
&.flash-ok {
background-color: rgba(120, 220, 140, 0.35);
}

&.flash-bad {
background-color: rgba(220, 100, 100, 0.45);
}
}
}

Expand Down
6 changes: 2 additions & 4 deletions static/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,8 @@
"popup.publish.override-model": "Modell überschreiben",
"popup.publish.override-animation": "Animation überschreiben",
"popup.publish.generate-lods": "LODs erzeugen",
"cursor.click-to-copy": "Klicken zum Kopieren",
"cursor.copied": "Kopiert!",
"camera-info.position": "Position:",
"camera-info.target": "Ziel:",
"camera-info.position": "Kameraposition",
"camera-info.target": "Kameraziel",
"doc.reset": "Szene Zurücksetzen",
"doc.unsaved-message": "Sie haben ungespeicherte Änderungen. Möchten Sie die Szene wirklich zurücksetzen?",
"doc.reset-message": "Möchten Sie die Szene wirklich zurücksetzen?",
Expand Down
6 changes: 2 additions & 4 deletions static/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,8 @@
"popup.publish.override-model": "Override Model",
"popup.publish.override-animation": "Override Animation",
"popup.publish.generate-lods": "Generate LODs",
"cursor.click-to-copy": "Click to Copy",
"cursor.copied": "Copied!",
"camera-info.position": "Position:",
"camera-info.target": "Target:",
"camera-info.position": "Camera Position",
"camera-info.target": "Camera Target",
"doc.reset": "Reset Scene",
"doc.unsaved-message": "You have unsaved changes. Are you sure you want to reset the scene?",
"doc.reset-message": "Are you sure you want to reset the scene?",
Expand Down
6 changes: 2 additions & 4 deletions static/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,8 @@
"popup.publish.override-model": "Reemplazar modelo",
"popup.publish.override-animation": "Reemplazar animación",
"popup.publish.generate-lods": "Generar LOD",
"cursor.click-to-copy": "Haga clic para copiar",
"cursor.copied": "¡Copiado!",
"camera-info.position": "Posición:",
"camera-info.target": "Objetivo:",
"camera-info.position": "Posición de la cámara",
"camera-info.target": "Objetivo de la cámara",
"doc.reset": "Restablecer escena",
"doc.unsaved-message": "Tiene cambios sin guardar. ¿Está seguro de que desea restablecer la escena?",
"doc.reset-message": "¿Está seguro de que desea restablecer la escena?",
Expand Down
Loading