Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add duplicate/separate #344

Merged
merged 3 commits into from
Dec 16, 2024
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
27 changes: 26 additions & 1 deletion src/edit-ops.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Color, Mat4 } from 'playcanvas';

import { Pivot } from './pivot';
import { Scene } from './scene';
import { Splat } from './splat';
import { State } from './splat-state';
import { Transform } from './transform';
Expand Down Expand Up @@ -379,6 +380,29 @@ class MultiOp {
}
}

class AddSplatOp {
name: 'addSplat';
scene: Scene;
splat: Splat;

constructor(scene: Scene, splat: Splat) {
this.scene = scene;
this.splat = splat;
}

do() {
this.scene.add(this.splat);
}

undo() {
this.scene.remove(this.splat);
}

destroy() {
this.splat.destroy();
}
}

export {
EditOp,
SelectAllOp,
Expand All @@ -394,5 +418,6 @@ export {
PlacePivotOp,
ColorAdjustment,
SetSplatColorAdjustmentOp,
MultiOp
MultiOp,
AddSplatOp
};
71 changes: 70 additions & 1 deletion src/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import {
} from 'playcanvas';

import { EditHistory } from './edit-history';
import { SelectAllOp, SelectNoneOp, SelectInvertOp, SelectOp, HideSelectionOp, UnhideAllOp, DeleteSelectionOp, ResetOp } from './edit-ops';
import { SelectAllOp, SelectNoneOp, SelectInvertOp, SelectOp, HideSelectionOp, UnhideAllOp, DeleteSelectionOp, ResetOp, MultiOp, AddSplatOp } from './edit-ops';
import { Events } from './events';
import { PngCompressor } from './png-compressor';
import { Scene } from './scene';
import { Splat } from './splat';
import { serializePly } from './splat-serialize';

// register for editor and scene events
const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: Scene) => {
Expand Down Expand Up @@ -187,6 +188,12 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S
scene.camera.ortho = true;
});

// returns true if the selected splat has selected gaussians
events.function('selection.splats', () => {
const splat = events.invoke('selection') as Splat;
return splat?.numSelected > 0;
});

events.on('select.all', () => {
selectedSplats().forEach((splat) => {
events.fire('edit.add', new SelectAllOp(splat));
Expand Down Expand Up @@ -388,6 +395,68 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S
});
});

const performSelectionFunc = async (func: 'duplicate' | 'separate') => {
const splats = selectedSplats();

let data: Uint8Array = null;
let cursor = 0;

const writeFunc = (chunk: Uint8Array, finalWrite?: boolean) => {
if (!data) {
data = finalWrite ? chunk : chunk.slice();
cursor = chunk.byteLength;
} else {
if (data.byteLength < cursor + chunk.byteLength) {
let newSize = data.byteLength * 2;
while (newSize < cursor + chunk.byteLength) {
newSize *= 2;
}
const newData = new Uint8Array(newSize);
newData.set(data);
data = newData;
}
data.set(chunk, cursor);
cursor += chunk.byteLength;
}
};

await serializePly({
splats,
maxSHBands: 3,
selected: true
}, writeFunc);

if (data) {
const splat = splats[0];

// wrap PLY in a blob and load it
const blob = new Blob([data], { type: 'octet/stream' });
const url = URL.createObjectURL(blob);
const { filename } = splat;
const copy = await scene.assetLoader.loadPly({ url, filename });

if (func === 'separate') {
editHistory.add(new MultiOp([
new DeleteSelectionOp(splat),
new AddSplatOp(scene, copy)
]));
} else {
editHistory.add(new AddSplatOp(scene, copy));
}

URL.revokeObjectURL(url);
}
};

// duplicate the current selection
events.on('select.duplicate', async () => {
await performSelectionFunc('duplicate');
});

events.on('select.separate', async () => {
await performSelectionFunc('separate');
});

events.on('scene.reset', () => {
selectedSplats().forEach((splat) => {
editHistory.add(new ResetOp(splat));
Expand Down
2 changes: 1 addition & 1 deletion src/file-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ const initFileHandler = (scene: Scene, events: Events, dropTarget: HTMLElement,
const model = await scene.assetLoader.loadModel({ url, filename });
scene.add(model);
scene.camera.focus();
events.fire('loaded', filename);
return model;
} else {
throw new Error('Unsupported file type');
}
Expand Down
14 changes: 13 additions & 1 deletion src/splat-serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type WriteFunc = (data: Uint8Array, finalWrite?: boolean) => void;
type SerializeOptions = {
splats: Splat[];
maxSHBands: number;
selected?: boolean; // only export selected splats, only used for PLY export
};

type ViewerExportOptions = {
Expand All @@ -38,6 +39,12 @@ const countTotalSplats = (splats: Splat[]) => {
}, 0);
};

const countSelectedSplats = (splats: Splat[]) => {
return splats.reduce((accum, splat) => {
return accum + splat.numSelected;
}, 0);
};

const getVertexProperties = (splatData: GSplatData) => {
return new Set<string>(
splatData.getElement('vertex')
Expand Down Expand Up @@ -309,7 +316,11 @@ const serializePly = async (options: SerializeOptions, write: WriteFunc) => {
const { splats, maxSHBands } = options;

// count the number of non-deleted splats
const totalSplats = countTotalSplats(splats);
const totalSplats = options.selected ? countSelectedSplats(splats) : countTotalSplats(splats);

if (totalSplats === 0) {
return;
}

// this data is filtered out, as it holds internal editor state
const internalProps = ['state', 'transform'];
Expand Down Expand Up @@ -354,6 +365,7 @@ const serializePly = async (options: SerializeOptions, write: WriteFunc) => {

for (let i = 0; i < splatData.numSplats; ++i) {
if ((state[i] & State.deleted) === State.deleted) continue;
if (options.selected && (state[i] & State.selected) === 0) continue;

singleSplat.read(splat, i);

Expand Down
12 changes: 12 additions & 0 deletions src/ui/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const localizeInit = () => {
'select.unlock': 'Sperre aufheben',
'select.delete': 'Selektion aufheben',
'select.reset': 'Splats zurücksetzen',
'select.duplicate': 'Duplizieren',
'select.separate': 'Separieren',

// Help menu
'help': 'Hilfe',
Expand Down Expand Up @@ -204,6 +206,8 @@ const localizeInit = () => {
'select.unlock': 'Unlock All',
'select.delete': 'Delete Selection',
'select.reset': 'Reset Splat',
'select.duplicate': 'Duplicate',
'select.separate': 'Separate',

// Help menu
'help': 'Help',
Expand Down Expand Up @@ -381,6 +385,8 @@ const localizeInit = () => {
'select.unlock': 'Tout débloquer',
'select.delete': 'Supprimer la sélection',
'select.reset': 'Réinitialiser splat',
'select.duplicate': 'Dupliquer',
'select.separate': 'Séparer',

// Help menu
'help': 'Aide',
Expand Down Expand Up @@ -549,6 +555,8 @@ const localizeInit = () => {
'select.unlock': 'ロックを解除',
'select.delete': '選択を削除',
'select.reset': '変更を全てリセット',
'select.duplicate': '複製',
'select.separate': '分離',

// Help menu
'help': 'ヘルプ',
Expand Down Expand Up @@ -717,6 +725,8 @@ const localizeInit = () => {
'select.unlock': '모두 잠금 해제',
'select.delete': '선택 삭제',
'select.reset': 'Splat 재설정',
'select.duplicate': '복제',
'select.separate': '분리',

// Help menu
'help': '도움말',
Expand Down Expand Up @@ -885,6 +895,8 @@ const localizeInit = () => {
'select.unlock': '解锁全部',
'select.delete': '删除选择',
'select.reset': '重置 Splat',
'select.duplicate': '复制',
'select.separate': '分离',

// Help menu
'help': '帮助',
Expand Down
20 changes: 18 additions & 2 deletions src/ui/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import sceneOpen from './svg/open.svg';
import logoSvg from './svg/playcanvas-logo.svg';
import sceneSave from './svg/save.svg';
import selectAll from './svg/select-all.svg';
import selectDuplicate from './svg/select-duplicate.svg';
import selectInverse from './svg/select-inverse.svg';
import selectLock from './svg/select-lock.svg';
import selectNone from './svg/select-none.svg';
import selectSeparate from './svg/select-separate.svg';
import selectUnlock from './svg/select-unlock.svg';

const createSvg = (svgString: string) => {
Expand Down Expand Up @@ -195,7 +197,8 @@ class Menu extends Container {
text: localize('select.lock'),
icon: createSvg(selectLock),
extra: 'H',
onSelect: () => events.fire('select.hide')
onSelect: () => events.fire('select.hide'),
isEnabled: () => events.invoke('selection.splats')
}, {
text: localize('select.unlock'),
icon: createSvg(selectUnlock),
Expand All @@ -205,10 +208,23 @@ class Menu extends Container {
text: localize('select.delete'),
icon: createSvg(selectDelete),
extra: 'Delete',
onSelect: () => events.fire('select.delete')
onSelect: () => events.fire('select.delete'),
isEnabled: () => events.invoke('selection.splats')
}, {
text: localize('select.reset'),
onSelect: () => events.fire('scene.reset')
}, {
// separator
}, {
text: localize('select.duplicate'),
icon: createSvg(selectDuplicate),
onSelect: () => events.fire('select.duplicate'),
isEnabled: () => events.invoke('selection.splats')
}, {
text: localize('select.separate'),
icon: createSvg(selectSeparate),
onSelect: () => events.fire('select.separate'),
isEnabled: () => events.invoke('selection.splats')
}]);

const helpMenuPanel = new MenuPanel([{
Expand Down
4 changes: 4 additions & 0 deletions src/ui/svg/select-duplicate.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/ui/svg/select-separate.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading