Skip to content
Open
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
1 change: 1 addition & 0 deletions src/edit-ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Scene } from './scene';
import { Splat } from './splat';
import { State } from './splat-state';
import { Transform } from './transform';
import { ElementType } from './element';

interface EditOp {
name: string;
Expand Down
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { RotateTool } from './tools/rotate-tool';
import { ScaleTool } from './tools/scale-tool';
import { SphereSelection } from './tools/sphere-selection';
import { ToolManager } from './tools/tool-manager';
import { FlySelectionTool } from './tools/fly-selection-tool';
import { registerTransformHandlerEvents } from './transform-handler';
import { EditorUI } from './ui/editor';

Expand Down Expand Up @@ -81,6 +82,7 @@ const initShortcuts = (events: Events) => {
shortcuts.register(['P', 'p'], { event: 'tool.polygonSelection', sticky: true });
shortcuts.register(['L', 'l'], { event: 'tool.lassoSelection', sticky: true });
shortcuts.register(['B', 'b'], { event: 'tool.brushSelection', sticky: true });
shortcuts.register(['K', 'k'], { event: 'tool.flySelection', sticky: true });
shortcuts.register(['A', 'a'], { event: 'select.all', ctrl: true });
shortcuts.register(['A', 'a'], { event: 'select.none', shift: true });
shortcuts.register(['I', 'i'], { event: 'select.invert', ctrl: true });
Expand Down Expand Up @@ -237,6 +239,7 @@ const main = async () => {
toolManager.register('move', new MoveTool(events, scene));
toolManager.register('rotate', new RotateTool(events, scene));
toolManager.register('scale', new ScaleTool(events, scene));
toolManager.register('flySelection', new FlySelectionTool(events, scene));

editorUI.toolsContainer.dom.appendChild(maskCanvas);

Expand Down
126 changes: 126 additions & 0 deletions src/tools/fly-selection-tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { Ray, Vec3, BoundingSphere, Mat4, Vec4 } from 'playcanvas';
import { Events } from '../events';
import { Scene } from '../scene';
import { Splat } from '../splat';
import { EditOp, SelectOp } from '../edit-ops'; // Import SelectOp
import { ElementType } from '../element';
import { State } from '../splat-state'; // Import State

const ray = new Ray();
const cameraPos = new Vec3();
const splatPos = new Vec3();
const splatSphere = new BoundingSphere();
const gaussianPos = new Vec3(); // For individual gaussian world position
const gaussianVec4 = new Vec4(); // For transform
const mat = new Mat4(); // For transform

class FlySelectionTool {
events: Events;
scene: Scene;
active = false;
// Store splats and the indices of gaussians within them to select
gaussiansToSelect: Map<Splat, number[]> = new Map();
selectionRadius = 0.1; // Radius around camera to check for gaussians

constructor(events: Events, scene: Scene) {
this.events = events;
this.scene = scene;

// Bind the update method to this instance
this.update = this.update.bind(this);
}

activate() {
this.active = true;
// Add update listener when activated
this.scene.app.on('update', this.update);
console.log('Fly Selection Tool Activated');
}

deactivate() {
this.active = false;
// Remove update listener when deactivated
this.scene.app.off('update', this.update);
console.log('Fly Selection Tool Deactivated');
}

update(deltaTime: number) {
if (!this.active) return;

const camera = this.scene.camera.entity;
cameraPos.copy(camera.getPosition());

this.gaussiansToSelect.clear();

const splats = this.scene.getElementsByType(ElementType.splat) as Splat[];
if (!splats || splats.length === 0) return;

for (let i = 0; i < splats.length; ++i) {
const splat = splats[i];
if (!splat.visible || !splat.splatData) continue;

// Broad phase: Check if camera is roughly near the splat
splatPos.copy(splat.worldBound.center);
// Use selectionRadius here
const radius = splat.worldBound.halfExtents.length() + this.selectionRadius;
splatSphere.center.copy(splatPos);
splatSphere.radius = radius;

if (!splatSphere.containsPoint(cameraPos)) {
continue; // Skip splat if camera is too far
}

// Narrow phase: Check individual gaussians
const splatData = splat.splatData;
const x = splatData.getProp('x');
const y = splatData.getProp('y');
const z = splatData.getProp('z');
const state = splatData.getProp('state'); // Get state data
const indicesToSelect: number[] = [];
const worldTransform = splat.worldTransform;

for (let j = 0; j < splatData.numSplats; ++j) {
// Check if gaussian is already deleted, hidden, or selected
const currentState = state[j];
if (currentState & (State.deleted | State.hidden | State.selected)) continue;

// Get gaussian world position
gaussianVec4.set(x[j], y[j], z[j], 1.0);
worldTransform.transformVec4(gaussianVec4, gaussianVec4);
gaussianPos.set(gaussianVec4.x, gaussianVec4.y, gaussianVec4.z);

// Check distance to camera
// Use selectionRadius here
if (gaussianPos.distance(cameraPos) < this.selectionRadius) {
indicesToSelect.push(j);
}
}

if (indicesToSelect.length > 0) {
this.gaussiansToSelect.set(splat, indicesToSelect);
}
}

if (this.gaussiansToSelect.size > 0) {
let totalGaussians = 0;
this.gaussiansToSelect.forEach((indices, splat) => {
totalGaussians += indices.length;
// Create a Set for efficient lookup in the predicate
const indicesSet = new Set(indices);
const predicate = (i: number) => indicesSet.has(i);
// Fire 'select.pred' event with 'add' operation and the predicate
this.events.fire('select.pred', 'add', predicate);
});
console.log(`Selected ${totalGaussians} gaussians across ${this.gaussiansToSelect.size} splats via fly-by`);

// Clear the map after firing the events
this.gaussiansToSelect.clear();

// Selection changes usually trigger renders automatically, so forceRender might not be needed.
// this.scene.forceRender = true;
}
}
}

// Rename the export
export { FlySelectionTool };
11 changes: 11 additions & 0 deletions src/ui/bottom-toolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import pickerSvg from './svg/select-picker.svg';
import polygonSvg from './svg/select-poly.svg';
import sphereSvg from './svg/select-sphere.svg';
import undoSvg from './svg/undo.svg';
import flythroughSvg from './svg/select-flythrough.svg';
import { Tooltips } from './tooltips';
// import cropSvg from './svg/crop.svg';

Expand Down Expand Up @@ -57,6 +58,11 @@ class BottomToolbar extends Container {
class: 'bottom-toolbar-tool'
});

const flySelect = new Button({
id: 'bottom-toolbar-fly-select',
class: 'bottom-toolbar-tool'
});

const lasso = new Button({
id: 'bottom-toolbar-lasso',
class: 'bottom-toolbar-tool'
Expand Down Expand Up @@ -107,6 +113,7 @@ class BottomToolbar extends Container {
picker.dom.appendChild(createSvg(pickerSvg));
polygon.dom.appendChild(createSvg(polygonSvg));
brush.dom.appendChild(createSvg(brushSvg));
flySelect.dom.appendChild(createSvg(flythroughSvg));
sphere.dom.appendChild(createSvg(sphereSvg));
lasso.dom.appendChild(createSvg(lassoSvg));
// crop.dom.appendChild(createSvg(cropSvg));
Expand All @@ -119,6 +126,7 @@ class BottomToolbar extends Container {
this.append(polygon);
this.append(brush);
this.append(new Element({ class: 'bottom-toolbar-separator' }));
this.append(flySelect); // Add the renamed button
this.append(sphere);
// this.append(crop);
this.append(new Element({ class: 'bottom-toolbar-separator' }));
Expand All @@ -133,6 +141,7 @@ class BottomToolbar extends Container {
polygon.dom.addEventListener('click', () => events.fire('tool.polygonSelection'));
lasso.dom.addEventListener('click', () => events.fire('tool.lassoSelection'));
brush.dom.addEventListener('click', () => events.fire('tool.brushSelection'));
flySelect.dom.addEventListener('click', () => events.fire('tool.flySelection'));
picker.dom.addEventListener('click', () => events.fire('tool.rectSelection'));
sphere.dom.addEventListener('click', () => events.fire('tool.sphereSelection'));
translate.dom.addEventListener('click', () => events.fire('tool.move'));
Expand All @@ -151,6 +160,7 @@ class BottomToolbar extends Container {
events.on('tool.activated', (toolName: string) => {
picker.class[toolName === 'rectSelection' ? 'add' : 'remove']('active');
brush.class[toolName === 'brushSelection' ? 'add' : 'remove']('active');
flySelect.class[toolName === 'flySelection' ? 'add' : 'remove']('active');
polygon.class[toolName === 'polygonSelection' ? 'add' : 'remove']('active');
lasso.class[toolName === 'lassoSelection' ? 'add' : 'remove']('active');
sphere.class[toolName === 'sphereSelection' ? 'add' : 'remove']('active');
Expand All @@ -172,6 +182,7 @@ class BottomToolbar extends Container {
tooltips.register(redo, localize('tooltip.redo'));
tooltips.register(picker, localize('tooltip.picker'));
tooltips.register(brush, localize('tooltip.brush'));
tooltips.register(flySelect, localize('tooltip.fly-select'));
tooltips.register(polygon, localize('tooltip.polygon'));
tooltips.register(lasso, 'Lasso Select');
tooltips.register(sphere, localize('tooltip.sphere'));
Expand Down
6 changes: 6 additions & 0 deletions src/ui/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ const localizeInit = () => {
'tooltip.polygon': 'Polygonselektion ( P )',
'tooltip.brush': 'Pinselselektion ( B )',
'tooltip.sphere': 'Kugelselektion',
'tooltip.fly-select': 'Flugauswahl',
'tooltip.translate': 'Verschieben ( 1 )',
'tooltip.rotate': 'Drehen ( 2 )',
'tooltip.scale': 'Skalieren ( 3 )',
Expand Down Expand Up @@ -414,6 +415,7 @@ const localizeInit = () => {
'tooltip.polygon': 'Polygon Select ( P )',
'tooltip.brush': 'Brush Select ( B )',
'tooltip.sphere': 'Sphere Select',
'tooltip.fly-select': 'Fly Select',
'tooltip.translate': 'Translate ( 1 )',
'tooltip.rotate': 'Rotate ( 2 )',
'tooltip.scale': 'Scale ( 3 )',
Expand Down Expand Up @@ -643,6 +645,7 @@ const localizeInit = () => {
'tooltip.polygon': 'Sélection avec polygone ( P )',
'tooltip.brush': 'Sélection avec pinceau ( B )',
'tooltip.sphere': 'Sélection avec sphère',
'tooltip.fly-select': 'Sélection en vol',
'tooltip.translate': 'Translation ( 1 )',
'tooltip.rotate': 'Rotation ( 2 )',
'tooltip.scale': 'Échelle ( 3 )',
Expand Down Expand Up @@ -868,6 +871,7 @@ const localizeInit = () => {
'tooltip.polygon': 'ポリゴン選択 ( P )',
'tooltip.brush': 'ブラシ選択 ( B )',
'tooltip.sphere': '球で選択',
'tooltip.fly-select': '飛行選択',
'tooltip.translate': '移動 ( 1 )',
'tooltip.rotate': '回転 ( 2 )',
'tooltip.scale': 'スケール ( 3 )',
Expand Down Expand Up @@ -1092,6 +1096,7 @@ const localizeInit = () => {
'tooltip.polygon': '다각형 선택 ( P )',
'tooltip.brush': '브러시 선택 ( B )',
'tooltip.sphere': '구 선택',
'tooltip.fly-select': '비행 선택',
'tooltip.translate': '이동 ( 1 )',
'tooltip.rotate': '회전 ( 2 )',
'tooltip.scale': '크기 조정 ( 3 )',
Expand Down Expand Up @@ -1317,6 +1322,7 @@ const localizeInit = () => {
'tooltip.polygon': '多边形选择 ( P )',
'tooltip.brush': '画笔 ( B )',
'tooltip.sphere': '球选择',
'tooltip.fly-select': '飞行选择',
'tooltip.translate': '移动 ( 1 )',
'tooltip.rotate': '旋转 ( 2 )',
'tooltip.scale': '缩放 ( 3 )',
Expand Down
16 changes: 16 additions & 0 deletions src/ui/svg/select-flythrough.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.