diff --git a/packages/editor/src/components/editor/bake-exporter.tsx b/packages/editor/src/components/editor/bake-exporter.tsx new file mode 100644 index 00000000..5771adee --- /dev/null +++ b/packages/editor/src/components/editor/bake-exporter.tsx @@ -0,0 +1,35 @@ +'use client' + +import { useScene } from '@pascal-app/core' +import { useThree } from '@react-three/fiber' +import { useEffect, useRef } from 'react' +import { exportSceneToGlb } from '../../lib/glb-export' + +export function BakeExporter({ + active, + onComplete, + onError, +}: { + active: boolean + onComplete: (buffer: ArrayBuffer) => void + onError: (message: string) => void +}) { + const scene = useThree((s) => s.scene) + const doneRef = useRef(false) + useEffect(() => { + if (!(active && !doneRef.current)) return + doneRef.current = true + const run = async () => { + try { + const sceneGroup = scene.getObjectByName('scene-renderer') + if (!sceneGroup) throw new Error('scene-renderer group not found') + const buffer = await exportSceneToGlb(sceneGroup, useScene.getState().nodes) + onComplete(buffer) + } catch (err) { + onError(err instanceof Error ? err.message : String(err)) + } + } + void run() + }, [active, scene, onComplete, onError]) + return null +} diff --git a/packages/editor/src/components/editor/export-manager.tsx b/packages/editor/src/components/editor/export-manager.tsx index 7f31f7ad..974c164f 100644 --- a/packages/editor/src/components/editor/export-manager.tsx +++ b/packages/editor/src/components/editor/export-manager.tsx @@ -4,11 +4,9 @@ import { emitter, useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useThree } from '@react-three/fiber' import { useEffect } from 'react' -import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js' import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js' import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js' -import * as WebGPUTextureUtils from 'three/examples/jsm/utils/WebGPUTextureUtils.js' -import { prepareSceneForExport } from '../../lib/glb-export' +import { exportSceneToGlb, prepareSceneForExport } from '../../lib/glb-export' export function ExportManager() { const scene = useThree((state) => state.scene) @@ -24,6 +22,14 @@ export function ExportManager() { } const date = new Date().toISOString().split('T')[0] + + if (format === 'glb') { + const buffer = await exportSceneToGlb(sceneGroup, useScene.getState().nodes) + const blob = new Blob([buffer], { type: 'model/gltf-binary' }) + downloadBlob(blob, `model_${date}.glb`) + return + } + // Hide editor affordances that live on the scene layer (selection handles, // ceiling/site brackets) and let wall-cutout reveal all walls — the same // synchronous capture path thumbnails use. We clone the scene inside the @@ -52,30 +58,6 @@ export function ExportManager() { downloadBlob(blob, `model_${date}.obj`) return } - - // Default: GLB export with baked identity + door/window animation clips. - const exporter = new GLTFExporter() - // Painted finishes use KTX2 (GPU-compressed) maps; GLTFExporter can't read - // those directly. WebGPUTextureUtils blits each one to RGBA on its own - // offscreen renderer (passing the live renderer would resize/draw over the - // editor canvas), letting the exporter embed standard textures. - exporter.setTextureUtils(WebGPUTextureUtils) - - return new Promise((resolve, reject) => { - exporter.parse( - exportScene, - (gltf) => { - const blob = new Blob([gltf as ArrayBuffer], { type: 'model/gltf-binary' }) - downloadBlob(blob, `model_${date}.glb`) - resolve() - }, - (error) => { - console.error('Export error:', error) - reject(error) - }, - { binary: true, animations }, - ) - }) } setExportScene(exportFn) diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index 1ef26d77..39b8bbdb 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -11,6 +11,7 @@ export { default as Editor } from './components/editor' // they're referenced throughout the editor's own internals; the public // surface uses the shorter, shell-friendly names from the unified // preset-system spec. +export { BakeExporter } from './components/editor/bake-exporter' export { FloatingActionMenu as FloatingMenu } from './components/editor/floating-action-menu' // Embed surface — the editor's real in-canvas affordances, so a host can mount // authentic selection handles, interactive build tools, and the mover on top @@ -217,6 +218,7 @@ export { resolveCeilingPlanPointSnap, } from './lib/ceiling-plan-snap' export { EDITOR_LAYER } from './lib/constants' +export { exportSceneToGlb } from './lib/glb-export' // Helper libs used by the kind-owned roof / stair / elevator panels. export { resolveCurrentBuildingId, diff --git a/packages/editor/src/lib/glb-export.ts b/packages/editor/src/lib/glb-export.ts index 280bca7c..c8a8703c 100644 --- a/packages/editor/src/lib/glb-export.ts +++ b/packages/editor/src/lib/glb-export.ts @@ -1,5 +1,6 @@ import { type AnyNode, + emitter, getLevelDisplayName, type LevelNode, sceneRegistry, @@ -7,7 +8,10 @@ import { type ZoneNode, } from '@pascal-app/core' import { poseWindowMovingParts, SCENE_LAYER } from '@pascal-app/viewer' +import type { Object3D } from 'three' import * as THREE from 'three' +import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js' +import * as WebGPUTextureUtils from 'three/examples/jsm/utils/WebGPUTextureUtils.js' /** * Two TRS samples (closed vs open) differing by less than this are treated as @@ -27,6 +31,40 @@ export type GlbExport = { animations: THREE.AnimationClip[] } +export async function exportSceneToGlb( + sceneGroup: Object3D, + nodes: Record, +): Promise { + emitter.emit('thumbnail:before-capture', undefined) + let prepared: ReturnType + try { + prepared = prepareSceneForExport(sceneGroup, nodes) + } finally { + emitter.emit('thumbnail:after-capture', undefined) + } + const { scene: exportScene, animations } = prepared + + const exporter = new GLTFExporter() + // Painted finishes use KTX2 (GPU-compressed) maps; GLTFExporter can't read + // those directly. WebGPUTextureUtils blits each one to RGBA on its own + // offscreen renderer (passing the live renderer would resize/draw over the + // editor canvas), letting the exporter embed standard textures. + exporter.setTextureUtils(WebGPUTextureUtils) + + return new Promise((resolve, reject) => { + exporter.parse( + exportScene, + (gltf) => { + resolve(gltf as ArrayBuffer) + }, + (error) => { + reject(error) + }, + { binary: true, animations }, + ) + }) +} + /** * Build an engine-agnostic export tree from the live scene graph. The result is * a standalone three.js scene plus glTF animation clips, ready for