diff --git a/packages/editor/src/lib/glb-export.ts b/packages/editor/src/lib/glb-export.ts index e795ca76..bc80a2fb 100644 --- a/packages/editor/src/lib/glb-export.ts +++ b/packages/editor/src/lib/glb-export.ts @@ -97,6 +97,18 @@ export function prepareSceneForExport( const scene = source.clone(true) const cloneByOriginal = pairClones(source, scene) + // Scans (LiDAR meshes) and guides (floorplan images) are heavy reference + // assets stored elsewhere and aren't part of the compiled building. Drop them + // from the artifact entirely — `/viewer` re-adds them from the scene graph, + // gated by the project's public-visibility flags, so they never bloat the + // shared GLB nor slip past those flags into a static public file. + for (const [id, original] of sceneRegistry.nodes) { + const node = nodes[id] + if (node?.type === 'scan' || node?.type === 'guide') { + cloneByOriginal.get(original)?.removeFromParent() + } + } + // Object3Ds that carry node identity — never strip these even when they sit on // a non-scene layer. Some are metadata-only: a zone's visible fill/wall meshes // are stripped, but its identity node stays to carry the polygon that /viewer diff --git a/packages/viewer/src/components/renderers/node-renderer.tsx b/packages/viewer/src/components/renderers/node-renderer.tsx index 75f5afdd..831bf725 100644 --- a/packages/viewer/src/components/renderers/node-renderer.tsx +++ b/packages/viewer/src/components/renderers/node-renderer.tsx @@ -8,7 +8,7 @@ import { ParametricNodeRenderer } from './parametric-node-renderer' // on every render — that would create a new Suspense boundary each time. const lazyCache = new WeakMap, ComponentType<{ node: AnyNode }>>() -function getRegistryRenderer( +export function getRegistryRenderer( source: RendererSource, ): ComponentType<{ node: AnyNode }> | null { const cached = lazyCache.get(source) diff --git a/packages/viewer/src/components/viewer/glb-reference-nodes.tsx b/packages/viewer/src/components/viewer/glb-reference-nodes.tsx new file mode 100644 index 00000000..f1814a2d --- /dev/null +++ b/packages/viewer/src/components/viewer/glb-reference-nodes.tsx @@ -0,0 +1,63 @@ +'use client' + +import { type AnyNode, nodeRegistry, type RendererSource, type SceneGraph } from '@pascal-app/core' +import { createPortal } from '@react-three/fiber' +import { Suspense } from 'react' +import type { Object3D } from 'three' +import { getRegistryRenderer } from '../renderers/node-renderer' + +/** + * Scans (LiDAR meshes) and guides (floorplan images) are stripped from the + * baked GLB — they're heavy reference assets stored elsewhere. The GLB viewer + * re-adds them at runtime from the scene graph, portaled into their parent + * level's baked node so they ride level stacking, using the same registry + * renderers as the parametric viewer. Privacy is enforced upstream: the page + * only includes nodes whose `show_*_public` flag (or owner/admin) allows it, so + * a disallowed asset is never even fetched. + */ +export function buildGlbReferenceNodes( + sceneGraph: SceneGraph | null | undefined, + allow: { scans: boolean; guides: boolean }, +): AnyNode[] { + const nodes = sceneGraph?.nodes + if (!nodes) return [] + const out: AnyNode[] = [] + for (const raw of Object.values(nodes)) { + const node = raw as AnyNode + if (node.type === 'scan' && allow.scans) out.push(node) + else if (node.type === 'guide' && allow.guides) out.push(node) + } + return out +} + +export function GlbReferenceNodes({ + nodes, + identity, +}: { + nodes: AnyNode[] + identity: Map +}) { + return ( + <> + {nodes.map((node) => { + const anchor = node.parentId ? identity.get(node.parentId) : undefined + return anchor ? : null + })} + + ) +} + +/** Render one scan/guide via its registry renderer, portaled into its parent + * level's baked Object3D (so the node's level-local transform resolves to the + * same world pose as the parametric scene). */ +function GlbReferenceNode({ node, anchor }: { node: AnyNode; anchor: Object3D }) { + const source = nodeRegistry.get(node.type)?.renderer + const Renderer = source ? getRegistryRenderer(source as RendererSource) : null + if (!Renderer) return null + return createPortal( + + + , + anchor, + ) +} diff --git a/packages/viewer/src/components/viewer/glb-scene.tsx b/packages/viewer/src/components/viewer/glb-scene.tsx index 7169f4c0..b46dc765 100644 --- a/packages/viewer/src/components/viewer/glb-scene.tsx +++ b/packages/viewer/src/components/viewer/glb-scene.tsx @@ -1,6 +1,6 @@ 'use client' -import type { SurfaceRole } from '@pascal-app/core' +import type { AnyNode, SurfaceRole } from '@pascal-app/core' import { Html, useAnimations } from '@react-three/drei' import { type ThreeEvent, useFrame, useThree } from '@react-three/fiber' import { useCallback, useEffect, useMemo, useRef } from 'react' @@ -13,6 +13,7 @@ import { ZONE_LAYER } from '../../lib/layers' import { createSurfaceRoleMaterial } from '../../lib/materials' import useViewer from '../../store/use-viewer' import { GlbInteractive, type GlbInteractiveItem } from './glb-interactive' +import { GlbReferenceNodes } from './glb-reference-nodes' /** Vertical gap added per floor in `exploded` level mode (matches LevelSystem). */ const EXPLODED_GAP = 5 @@ -281,6 +282,7 @@ function createZoneWallGeometry(polygon: [number, number][]): THREE.BufferGeomet export function GlbScene({ url, interactiveItems, + referenceNodes, onLevelsChange, onIdentityChange, onHoverChange, @@ -290,6 +292,9 @@ export function GlbScene({ /** Light / animation effects + controls recovered from the DB scene graph, * joined to the baked nodes by `pascalId` to re-light + re-animate the GLB. */ interactiveItems?: GlbInteractiveItem[] + /** Scan / guide nodes from the scene graph, re-added at runtime (they're + * stripped from the bake). Already filtered by the privacy flags upstream. */ + referenceNodes?: AnyNode[] onLevelsChange?: (levels: GlbLevel[]) => void onIdentityChange?: (identity: GlbIdentity) => void onHoverChange?: (hover: GlbHover) => void @@ -998,6 +1003,11 @@ export function GlbScene({ zones={zoneEntries} /> ) : null} + {/* Scans + guides, stripped from the bake and re-added from scene data, + anchored to their parent level's baked node. */} + {referenceNodes?.length ? ( + + ) : null} {/* Floating room labels. Each group's matrix is synced to its zone node every frame (above) so the label rides level stacking; the div fades with the room fill via a CSS transition. */} diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts index 2550ac36..498ad03e 100644 --- a/packages/viewer/src/index.ts +++ b/packages/viewer/src/index.ts @@ -23,6 +23,7 @@ export { GlbInteractive, type GlbInteractiveItem, } from './components/viewer/glb-interactive' +export { buildGlbReferenceNodes } from './components/viewer/glb-reference-nodes' export { type GlbHover, type GlbIdentity,