feat(viewer): strip scans/guides from the bake, re-add from scene data
Scans (LiDAR meshes) and guides (floorplan images) are heavy reference assets stored elsewhere, not part of the compiled building — and baking them into a public static GLB would also bypass the per-project show_*_public flags. Strip both from the export entirely (previously they leaked in as empty identity nodes, timing-dependent). The GLB viewer re-adds them at runtime from the scene graph, like lights: GlbReferenceNodes resolves each scan/guide's registry renderer (no static nodes import — same nodeRegistry path the viewer already uses) and portals it into its parent level's baked node, so the node's level-local transform resolves to the right world pose and rides level stacking. Uses the same GuideRenderer/ ScanRenderer + asset resolver as the parametric viewer, so http-backed assets show for everyone and local asset:// ones for the owner — exact parity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
df901b1d4e
commit
5938cb6805
@@ -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
|
||||
|
||||
@@ -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<RendererSource<AnyNode>, ComponentType<{ node: AnyNode }>>()
|
||||
|
||||
function getRegistryRenderer(
|
||||
export function getRegistryRenderer(
|
||||
source: RendererSource<AnyNode>,
|
||||
): ComponentType<{ node: AnyNode }> | null {
|
||||
const cached = lazyCache.get(source)
|
||||
|
||||
@@ -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<string, Object3D>
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{nodes.map((node) => {
|
||||
const anchor = node.parentId ? identity.get(node.parentId) : undefined
|
||||
return anchor ? <GlbReferenceNode anchor={anchor} key={node.id} node={node} /> : 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<AnyNode>) : null
|
||||
if (!Renderer) return null
|
||||
return createPortal(
|
||||
<Suspense fallback={null}>
|
||||
<Renderer node={node} />
|
||||
</Suspense>,
|
||||
anchor,
|
||||
)
|
||||
}
|
||||
@@ -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 ? (
|
||||
<GlbReferenceNodes identity={identity} nodes={referenceNodes} />
|
||||
) : 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. */}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user