Skip to content

Commit 573266a

Browse files
authored
Unified Export Dialog (#510)
1 parent afb85fa commit 573266a

9 files changed

Lines changed: 652 additions & 536 deletions

File tree

src/file-handler.ts

Lines changed: 59 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { path, Vec3 } from 'playcanvas';
1+
import { Vec3 } from 'playcanvas';
22

33
import { CreateDropHandler } from './drop-handler';
44
import { ElementType } from './element';
55
import { Events } from './events';
66
import { Scene } from './scene';
7-
import { Writer, DownloadWriter, FileStreamWriter } from './serialize/writer';
7+
import { DownloadWriter, FileStreamWriter } from './serialize/writer';
88
import { Splat } from './splat';
99
import { serializePly, serializePlyCompressed, SerializeSettings, serializeSplat, serializeViewer, ViewerExportSettings } from './splat-serialize';
1010
import { localize } from './ui/localization';
@@ -17,13 +17,20 @@ interface RemoteStorageDetails {
1717
url: string;
1818
}
1919

20-
type ExportType = 'ply' | 'compressed-ply' | 'splat' | 'viewer';
20+
type ExportType = 'ply' | 'splat' | 'viewer';
2121

22-
interface SceneWriteOptions {
23-
type: ExportType;
24-
filename?: string;
25-
stream?: FileSystemWritableFileStream;
26-
viewerExportSettings?: ViewerExportSettings
22+
type FileType = 'ply' | 'compressedPly' | 'splat' | 'htmlViewer' | 'packageViewer';
23+
24+
interface SceneExportOptions {
25+
filename: string;
26+
splatIdx: 'all' | number;
27+
serializeSettings: SerializeSettings;
28+
29+
// ply
30+
compressedPly?: boolean;
31+
32+
// viewer
33+
viewerExportSettings?: ViewerExportSettings;
2734
}
2835

2936
const filePickerTypes: { [key: string]: FilePickerAcceptType } = {
@@ -33,7 +40,7 @@ const filePickerTypes: { [key: string]: FilePickerAcceptType } = {
3340
'application/ply': ['.ply']
3441
}
3542
},
36-
'compressed-ply': {
43+
'compressedPly': {
3744
description: 'Compressed Gaussian Splat PLY File',
3845
accept: {
3946
'application/ply': ['.ply']
@@ -63,30 +70,6 @@ let fileHandle: FileSystemFileHandle = null;
6370

6471
const vec = new Vec3();
6572

66-
// download the data to the given filename
67-
const download = (filename: string, data: Uint8Array) => {
68-
const blob = new Blob([data], { type: 'octet/stream' });
69-
const url = window.URL.createObjectURL(blob);
70-
71-
const lnk = document.createElement('a');
72-
lnk.download = filename;
73-
lnk.href = url;
74-
75-
// create a "fake" click-event to trigger the download
76-
if (document.createEvent) {
77-
const e = document.createEvent('MouseEvents');
78-
e.initMouseEvent('click', true, true, window,
79-
0, 0, 0, 0, 0, false, false, false,
80-
false, 0, null);
81-
lnk.dispatchEvent(e);
82-
} else {
83-
// @ts-ignore
84-
lnk.fireEvent?.('onclick');
85-
}
86-
87-
window.URL.revokeObjectURL(url);
88-
};
89-
9073
const loadCameraPoses = async (url: string, filename: string, events: Events) => {
9174
const response = await fetch(url);
9275
const json = await response.json();
@@ -319,96 +302,42 @@ const initFileHandler = (scene: Scene, events: Events, dropTarget: HTMLElement,
319302
}
320303
});
321304

322-
events.function('scene.export', async (type: ExportType, outputFilename: string = null, exportType: 'export' | 'saveAs' = 'export') => {
323-
const extensions = {
324-
'ply': '.ply',
325-
'compressed-ply': '.compressed.ply',
326-
'splat': '.splat',
327-
'viewer': '-viewer.html'
328-
};
329-
330-
const removeExtension = (filename: string) => {
331-
return filename.substring(0, filename.length - path.getExtension(filename).length);
332-
};
333-
334-
const replaceExtension = (filename: string, extension: string) => {
335-
return `${removeExtension(filename)}${extension}`;
336-
};
337-
305+
events.function('scene.export', async (exportType: 'ply' | 'splat' | 'viewer') => {
338306
const splats = getSplats();
339-
const splat = splats[0];
340-
let filename = outputFilename ?? replaceExtension(splat.filename, extensions[type]);
341307

342-
const hasFilePicker = window.showSaveFilePicker;
308+
const hasFilePicker = !!window.showSaveFilePicker;
343309

344-
let viewerExportSettings;
345-
if (type === 'viewer') {
346-
// show viewer export options
347-
viewerExportSettings = await events.invoke('show.viewerExportPopup', hasFilePicker ? null : filename);
310+
// show viewer export options
311+
const options = await events.invoke('show.exportPopup', exportType, splats.map(s => s.name), !hasFilePicker) as SceneExportOptions;
348312

349-
// return if user cancelled
350-
if (!viewerExportSettings) {
351-
return;
352-
}
353-
354-
if (hasFilePicker) {
355-
filename = replaceExtension(filename, viewerExportSettings.type === 'html' ? '.html' : '.zip');
356-
} else {
357-
filename = viewerExportSettings.filename;
358-
}
313+
// return if user cancelled
314+
if (!options) {
315+
return;
359316
}
360317

318+
const fileType =
319+
(exportType === 'viewer') ? (options.viewerExportSettings.type === 'zip' ? 'packageViewer' : 'htmlViewer') :
320+
(exportType === 'ply') ? (options.compressedPly ? 'compressedPly' : 'ply') : 'splat';
321+
361322
if (hasFilePicker) {
362323
try {
363-
const filePickerType = type === 'viewer' ? (viewerExportSettings.type === 'html' ? filePickerTypes.htmlViewer : filePickerTypes.packageViewer) : filePickerTypes[type];
364-
365324
const fileHandle = await window.showSaveFilePicker({
366325
id: 'SuperSplatFileExport',
367-
types: [filePickerType],
368-
suggestedName: filename
369-
});
370-
await events.invoke('scene.write', {
371-
type,
372-
stream: await fileHandle.createWritable(),
373-
viewerExportSettings
326+
types: [filePickerTypes[fileType]],
327+
suggestedName: options.filename
374328
});
329+
await events.invoke('scene.write', fileType, options, await fileHandle.createWritable());
375330
} catch (error) {
376331
if (error.name !== 'AbortError') {
377332
console.error(error);
378333
}
379334
}
380335
} else {
381-
await events.invoke('scene.write', { type, filename, viewerExportSettings });
336+
await events.invoke('scene.write', fileType, options);
382337
}
383338
});
384339

385-
const writeScene = async (type: ExportType, writer: Writer, viewerExportSettings?: ViewerExportSettings) => {
386-
const splats = getSplats();
387-
const events = splats[0].scene.events;
388-
389-
const serializeSettings: SerializeSettings = {
390-
maxSHBands: events.invoke('view.bands')
391-
};
392-
393-
switch (type) {
394-
case 'ply':
395-
await serializePly(splats, serializeSettings, writer);
396-
break;
397-
case 'compressed-ply':
398-
serializeSettings.minOpacity = 1 / 255;
399-
serializeSettings.removeInvalid = true;
400-
await serializePlyCompressed(splats, serializeSettings, writer);
401-
break;
402-
case 'splat':
403-
await serializeSplat(splats, serializeSettings, writer);
404-
break;
405-
case 'viewer':
406-
await serializeViewer(splats, viewerExportSettings, writer);
407-
break;
408-
}
409-
};
410-
411-
events.function('scene.write', async (options: SceneWriteOptions) => {
340+
events.function('scene.write', async (fileType: FileType, options: SceneExportOptions, stream?: FileSystemWritableFileStream) => {
412341
events.fire('startSpinner');
413342

414343
try {
@@ -417,11 +346,34 @@ const initFileHandler = (scene: Scene, events: Events, dropTarget: HTMLElement,
417346
setTimeout(resolve);
418347
});
419348

420-
const { stream, filename, type, viewerExportSettings } = options;
349+
const { filename, splatIdx, serializeSettings, viewerExportSettings } = options;
350+
421351
const writer = stream ? new FileStreamWriter(stream) : new DownloadWriter(filename);
422352

423-
await writeScene(type, writer, viewerExportSettings);
424-
await writer.close();
353+
try {
354+
const splats = splatIdx === 'all' ? getSplats() : [getSplats()[splatIdx]];
355+
356+
switch (fileType) {
357+
case 'ply':
358+
await serializePly(splats, serializeSettings, writer);
359+
break;
360+
case 'compressedPly':
361+
serializeSettings.minOpacity = 1 / 255;
362+
serializeSettings.removeInvalid = true;
363+
await serializePlyCompressed(splats, serializeSettings, writer);
364+
break;
365+
case 'splat':
366+
await serializeSplat(splats, serializeSettings, writer);
367+
break;
368+
case 'htmlViewer':
369+
case 'packageViewer':
370+
await serializeViewer(splats, serializeSettings, viewerExportSettings, writer);
371+
break;
372+
}
373+
} finally {
374+
await writer.close();
375+
}
376+
425377
} catch (error) {
426378
await events.invoke('showPopup', {
427379
type: 'error',
@@ -434,4 +386,4 @@ const initFileHandler = (scene: Scene, events: Events, dropTarget: HTMLElement,
434386
});
435387
};
436388

437-
export { initFileHandler };
389+
export { initFileHandler, ExportType, SceneExportOptions };

src/splat-serialize.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@ type ExperienceSettings = {
6363

6464
type ViewerExportSettings = {
6565
type: 'html' | 'zip';
66-
filename?: string;
67-
serializeSettings: SerializeSettings;
6866
experienceSettings: ExperienceSettings;
6967
};
7068

@@ -1030,12 +1028,12 @@ const encodeBase64 = (bytes: Uint8Array) => {
10301028
return window.btoa(binary);
10311029
};
10321030

1033-
const serializeViewer = async (splats: Splat[], options: ViewerExportSettings, writer: Writer) => {
1031+
const serializeViewer = async (splats: Splat[], serializeSettings: SerializeSettings, options: ViewerExportSettings, writer: Writer) => {
10341032
const { experienceSettings } = options;
10351033

10361034
// create compressed PLY data
10371035
const plyWriter = new BufferWriter();
1038-
await serializePlyCompressed(splats, options.serializeSettings, plyWriter);
1036+
await serializePlyCompressed(splats, serializeSettings, plyWriter);
10391037
const plyBuffer = plyWriter.close();
10401038

10411039
if (options.type === 'html') {

src/ui/editor.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { DataPanel } from './data-panel';
55
import { Events } from '../events';
66
import { BottomToolbar } from './bottom-toolbar';
77
import { ColorPanel } from './color-panel';
8+
import { ExportPopup } from './export-popup';
89
import { ImageSettingsDialog } from './image-settings-dialog';
910
import { localize, localizeInit } from './localization';
1011
import { Menu } from './menu';
@@ -21,7 +22,6 @@ import { Tooltips } from './tooltips';
2122
import { VideoSettingsDialog } from './video-settings-dialog';
2223
import { ViewCube } from './view-cube';
2324
import { ViewPanel } from './view-panel';
24-
import { ViewerExportPopup } from './viewer-export-popup';
2525
import { version } from '../../package.json';
2626

2727
class EditorUI {
@@ -162,7 +162,7 @@ class EditorUI {
162162
const shortcutsPopup = new ShortcutsPopup();
163163

164164
// export popup
165-
const viewerExportPopup = new ViewerExportPopup(events);
165+
const exportPopup = new ExportPopup(events);
166166

167167
// publish settings
168168
const publishSettingsDialog = new PublishSettingsDialog(events);
@@ -174,7 +174,7 @@ class EditorUI {
174174
const videoSettingsDialog = new VideoSettingsDialog(events);
175175

176176
topContainer.append(popup);
177-
topContainer.append(viewerExportPopup);
177+
topContainer.append(exportPopup);
178178
topContainer.append(publishSettingsDialog);
179179
topContainer.append(imageSettingsDialog);
180180
topContainer.append(videoSettingsDialog);
@@ -198,8 +198,8 @@ class EditorUI {
198198
shortcutsPopup.hidden = false;
199199
});
200200

201-
events.function('show.viewerExportPopup', (filename?: string) => {
202-
return viewerExportPopup.show(filename);
201+
events.function('show.exportPopup', (exportType, splatNames: [string], showFilenameEdit: boolean) => {
202+
return exportPopup.show(exportType, splatNames, showFilenameEdit);
203203
});
204204

205205
events.function('show.publishSettingsDialog', async () => {

0 commit comments

Comments
 (0)