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

Export updates #373

Merged
merged 8 commits into from
Jan 9, 2025
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
3 changes: 1 addition & 2 deletions src/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,7 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S
}
};

await serializePly({
splats,
await serializePly(splats, {
maxSHBands: 3,
selected: true
}, writeFunc);
Expand Down
49 changes: 26 additions & 23 deletions src/file-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ElementType } from './element';
import { Events } from './events';
import { Scene } from './scene';
import { Splat } from './splat';
import { WriteFunc, serializePly, serializePlyCompressed, serializeSplat, serializeViewer, ViewerExportOptions } from './splat-serialize';
import { WriteFunc, serializePly, serializePlyCompressed, serializeSplat, serializeViewer, ViewerExportSettings } from './splat-serialize';
import { localize } from './ui/localization';

// ts compiler and vscode find this type, but eslint does not
Expand All @@ -22,7 +22,7 @@ interface SceneWriteOptions {
type: ExportType;
filename?: string;
stream?: FileSystemWritableFileStream;
viewerExportOptions?: ViewerExportOptions
viewerExportSettings?: ViewerExportSettings
}

const filePickerTypes: { [key: string]: FilePickerAcceptType } = {
Expand Down Expand Up @@ -161,7 +161,7 @@ const initFileHandler = (scene: Scene, events: Events, dropTarget: HTMLElement,
throw new Error('Unsupported file type');
}
} catch (error) {
events.invoke('showPopup', {
await events.invoke('showPopup', {
type: 'error',
header: localize('popup.error-loading'),
message: `${error.message ?? error} while loading '${filename}'`
Expand Down Expand Up @@ -205,7 +205,7 @@ const initFileHandler = (scene: Scene, events: Events, dropTarget: HTMLElement,
});

if (entries.length === 0) {
events.invoke('showPopup', {
await events.invoke('showPopup', {
type: 'error',
header: localize('popup.error-loading'),
message: localize('popup.drop-files')
Expand Down Expand Up @@ -256,6 +256,10 @@ const initFileHandler = (scene: Scene, events: Events, dropTarget: HTMLElement,
.filter(splat => splat.numSplats > 0);
};

events.function('scene.splats', () => {
return getSplats();
});

events.function('scene.empty', () => {
return getSplats().length === 0;
});
Expand Down Expand Up @@ -403,26 +407,26 @@ const initFileHandler = (scene: Scene, events: Events, dropTarget: HTMLElement,

const hasFilePicker = window.showSaveFilePicker;

let viewerExportOptions;
let viewerExportSettings;
if (type === 'viewer') {
// show viewer export options
viewerExportOptions = await events.invoke('show.viewerExportPopup', hasFilePicker ? null : filename);
viewerExportSettings = await events.invoke('show.viewerExportPopup', hasFilePicker ? null : filename);

// return if user cancelled
if (!viewerExportOptions) {
if (!viewerExportSettings) {
return;
}

if (hasFilePicker) {
filename = replaceExtension(filename, viewerExportOptions.type === 'html' ? '.html' : '.zip');
filename = replaceExtension(filename, viewerExportSettings.type === 'html' ? '.html' : '.zip');
} else {
filename = viewerExportOptions.filename;
filename = viewerExportSettings.filename;
}
}

if (hasFilePicker) {
try {
const filePickerType = type === 'viewer' ? (viewerExportOptions.type === 'html' ? filePickerTypes.htmlViewer : filePickerTypes.packageViewer) : filePickerTypes[type];
const filePickerType = type === 'viewer' ? (viewerExportSettings.type === 'html' ? filePickerTypes.htmlViewer : filePickerTypes.packageViewer) : filePickerTypes[type];

const fileHandle = await window.showSaveFilePicker({
id: 'SuperSplatFileExport',
Expand All @@ -432,39 +436,38 @@ const initFileHandler = (scene: Scene, events: Events, dropTarget: HTMLElement,
await events.invoke('scene.write', {
type,
stream: await fileHandle.createWritable(),
viewerExportOptions
viewerExportSettings
});
} catch (error) {
if (error.name !== 'AbortError') {
console.error(error);
}
}
} else {
await events.invoke('scene.write', { type, filename, viewerExportOptions });
await events.invoke('scene.write', { type, filename, viewerExportSettings });
}
});

const writeScene = async (type: ExportType, writeFunc: WriteFunc, viewerExportOptions?: ViewerExportOptions) => {
const writeScene = async (type: ExportType, writeFunc: WriteFunc, viewerExportSettings?: ViewerExportSettings) => {
const splats = getSplats();
const events = splats[0].scene.events;

const options = {
splats: splats,
const serializeSettings = {
maxSHBands: events.invoke('view.bands')
};

switch (type) {
case 'ply':
await serializePly(options, writeFunc);
await serializePly(splats, serializeSettings, writeFunc);
break;
case 'compressed-ply':
await serializePlyCompressed(options, writeFunc);
await serializePlyCompressed(splats, serializeSettings, writeFunc);
break;
case 'splat':
await serializeSplat(options, writeFunc);
await serializeSplat(splats, serializeSettings, writeFunc);
break;
case 'viewer':
await serializeViewer(splats, viewerExportOptions, writeFunc);
await serializeViewer(splats, viewerExportSettings, writeFunc);
break;
}
};
Expand All @@ -478,7 +481,7 @@ const initFileHandler = (scene: Scene, events: Events, dropTarget: HTMLElement,
setTimeout(resolve);
});

const { stream, filename, type, viewerExportOptions } = options;
const { stream, filename, type, viewerExportSettings } = options;

if (stream) {
// writer must keep track of written bytes because JS streams don't
Expand All @@ -489,7 +492,7 @@ const initFileHandler = (scene: Scene, events: Events, dropTarget: HTMLElement,
};

await stream.seek(0);
await writeScene(type, writeFunc, viewerExportOptions);
await writeScene(type, writeFunc, viewerExportSettings);
await stream.truncate(cursor);
await stream.close();
} else if (filename) {
Expand All @@ -515,11 +518,11 @@ const initFileHandler = (scene: Scene, events: Events, dropTarget: HTMLElement,
cursor += chunk.byteLength;
}
};
await writeScene(type, writeFunc, viewerExportOptions);
await writeScene(type, writeFunc, viewerExportSettings);
download(filename, (cursor === data.byteLength) ? data : new Uint8Array(data.buffer, 0, cursor));
}
} catch (error) {
events.invoke('showPopup', {
await events.invoke('showPopup', {
type: 'error',
header: localize('popup.error-loading'),
message: `${error.message ?? error} while saving file`
Expand Down
15 changes: 9 additions & 6 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { EditHistory } from './edit-history';
import { registerEditorEvents } from './editor';
import { Events } from './events';
import { initFileHandler } from './file-handler';
import { initMaterials } from './material';
import { registerPublishEvents } from './publish';
import { Scene } from './scene';
import { getSceneConfig } from './scene-config';
import { registerSelectionEvents } from './selection';
Expand Down Expand Up @@ -94,6 +94,10 @@ const initShortcuts = (events: Events) => {
};

const main = async () => {
// root events object
const events = new Events();

// url
const url = new URL(window.location.href);

// decode remote storage details
Expand All @@ -102,8 +106,9 @@ const main = async () => {
remoteStorageDetails = JSON.parse(decodeURIComponent(url.searchParams.get('remoteStorage')));
} catch (e) { }

// root events object
const events = new Events();
events.function('app.publish', () => {
return url.searchParams.get('publish') !== null;
});

// edit history
const editHistory = new EditHistory(events);
Expand All @@ -121,9 +126,6 @@ const main = async () => {
powerPreference: 'high-performance'
});

// monkey-patch materials for premul alpha rendering
initMaterials();

const overrides = [
getURLArgs()
];
Expand Down Expand Up @@ -244,6 +246,7 @@ const main = async () => {
registerSelectionEvents(events, scene);
registerTransformHandlerEvents(events);
registerAnimationEvents(events);
registerPublishEvents(events);
initShortcuts(events);
initFileHandler(scene, events, editorUI.appContainer.dom, remoteStorageDetails);

Expand Down
39 changes: 0 additions & 39 deletions src/material.ts

This file was deleted.

135 changes: 135 additions & 0 deletions src/publish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Events } from './events';
import { serializePlyCompressed, ViewerSettings, SerializeSettings } from './splat-serialize';
import { localize } from './ui/localization';

type PublishSettings = {
title: string;
description: string;
listed: boolean;
viewerSettings: ViewerSettings;
serializeSettings: SerializeSettings;
};

const origin = location.origin;

// check whether user is logged in
const testUserStatus = async () => {
const urlResponse = await fetch(`${origin}/api/id`);
return urlResponse.ok;
};

const publish = async (data: Uint8Array, publishSettings: PublishSettings) => {
const filename = 'scene.ply';

// get signed url
const urlResponse = await fetch(`${origin}/api/upload/signed-url`, {
method: 'POST',
body: JSON.stringify({ filename }),
headers: {
'Content-Type': 'application/json'
}
});

if (!urlResponse.ok) {
throw new Error(`failed to get signed url (${urlResponse.statusText})`);
}

const json = await urlResponse.json();

// upload the file to S3
const uploadResponse = await fetch(json.signedUrl, {
method: 'PUT',
body: data,
headers: {
'Content-Type': 'binary/octet-stream'
}
});

if (!uploadResponse.ok) {
throw new Error('failed to upload blob');
}

const publishResponse = await fetch(`${origin}/api/splats/publish`, {
method: 'POST',
body: JSON.stringify({
s3_key: json.s3Key,
title: publishSettings.title,
description: publishSettings.description,
listed: publishSettings.listed,
settings: publishSettings.viewerSettings
}),
headers: {
'Content-Type': 'application/json'
}
});

if (!publishResponse.ok) {
throw new Error('failed to publish');
}

return await publishResponse.json();
};

const registerPublishEvents = (events: Events) => {
events.function('scene.publish', async () => {
const userValid = await testUserStatus();

if (!userValid) {
// use must be logged in to publish
await events.invoke('showPopup', {
type: 'error',
header: localize('popup.error'),
message: localize('popup.please-log-in')
});
} else {
// get publish options
const publishSettings: PublishSettings = await events.invoke('show.publishSettingsDialog');

if (!publishSettings) {
return;
}

try {
events.fire('startSpinner');

const splats = events.invoke('scene.splats');

// serialize/compress
let data: Uint8Array = null;
await serializePlyCompressed(splats, publishSettings.serializeSettings, (chunk: Uint8Array) => {
data = chunk;
});

// publish
const response = await publish(data, publishSettings);

events.fire('stopSpinner');

if (!response) {
await events.invoke('showPopup', {
type: 'error',
header: localize('publish.failed'),
message: localize('publish.please-try-again')
});
} else {
await events.invoke('showPopup', {
type: 'info',
header: localize('publish.succeeded'),
message: localize('publish.message'),
link: response.url
});
}
} catch (error) {
events.fire('stopSpinner');

await events.invoke('showPopup', {
type: 'error',
header: localize('publish.failed'),
message: `'${error.message ?? error}'`
});
}
}
});
};

export { PublishSettings, registerPublishEvents };
Loading
Loading