1- import { path , Vec3 } from 'playcanvas' ;
1+ import { Vec3 } from 'playcanvas' ;
22
33import { CreateDropHandler } from './drop-handler' ;
44import { ElementType } from './element' ;
55import { Events } from './events' ;
66import { Scene } from './scene' ;
7- import { Writer , DownloadWriter , FileStreamWriter } from './serialize/writer' ;
7+ import { DownloadWriter , FileStreamWriter } from './serialize/writer' ;
88import { Splat } from './splat' ;
99import { serializePly , serializePlyCompressed , SerializeSettings , serializeSplat , serializeViewer , ViewerExportSettings } from './splat-serialize' ;
1010import { 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
2936const 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
6471const 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-
9073const 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 } ;
0 commit comments