Support template scans and room preset workflows (#390)

* Allow template forks to preserve scans

* Add room preset save trigger

* Show room preset action for selected zones

* Use standard inspector for selected zones

* Support room preset editor workflows

* Preview room clear-underneath items
This commit is contained in:
Wassim SAMAD
2026-06-09 16:36:31 -04:00
committed by GitHub
parent 555cf06c12
commit 50304d7d10
15 changed files with 450 additions and 57 deletions
+2 -3
View File
@@ -126,9 +126,8 @@ export const MoveCeilingTool: React.FC<{ node: CeilingNode }> = ({ node }) => {
deltaRef.current = [deltaX, deltaZ]
setMeshOffset(ceilingId as AnyNodeId, deltaX, deltaZ, height)
// Aligned with slab/fence: the delta matches the direct mesh
// mutation. CeilingRenderer doesn't bind position via React, so
// this entry isn't consumed for rendering, but kept consistent
// in case other systems read it.
// mutation. CeilingRenderer also consumes this store so external
// movers can preview ceilings without rebuilding the polygon.
useLiveTransforms.getState().set(ceilingId, {
position: [deltaX, 0, deltaZ],
rotation: 0,
+14 -1
View File
@@ -4,6 +4,7 @@ import {
type CeilingNode,
getMaterialPresetByRef,
resolveMaterial,
useLiveTransforms,
useRegistry,
useScene,
} from '@pascal-app/core'
@@ -79,6 +80,13 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
const textures = useViewer((s) => s.textures)
const colorPreset = useViewer((s) => s.colorPreset)
const sceneTheme = useViewer((s) => s.sceneTheme)
const liveTransform = useLiveTransforms((s) => s.get(node.id))
const ceilingY = (node.height ?? 2.5) - 0.01 + (liveTransform?.position[1] ?? 0)
const position: [number, number, number] = [
liveTransform?.position[0] ?? 0,
ceilingY,
liveTransform?.position[2] ?? 0,
]
useEffect(
() => () => {
@@ -124,7 +132,12 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
])
return (
<mesh geometry={placeholderGeometry} material={materials.bottomMaterial} ref={ref}>
<mesh
geometry={placeholderGeometry}
material={materials.bottomMaterial}
position={position}
ref={ref}
>
<mesh
geometry={gridPlaceholderGeometry}
material={materials.topMaterial}
+37 -8
View File
@@ -3,6 +3,7 @@
import {
type AnimationEffect,
type AnyNodeId,
getScaledDimensions,
type Interactive,
type ItemNode,
type LightEffect,
@@ -91,17 +92,25 @@ export const ItemRenderer = ({ node: storeNode }: { node: ItemNode }) => {
() => (liveOverrides ? ({ ...storeNode, ...liveOverrides } as ItemNode) : storeNode),
[storeNode, liveOverrides],
)
const roomClearPreview =
(node as ItemNode & { roomClearPreview?: unknown }).roomClearPreview === true
return (
<group position={node.position} ref={ref} rotation={node.rotation} visible={node.visible}>
<ErrorBoundary fallback={<BrokenItemFallback node={node} />}>
<Suspense fallback={<PreviewModel node={node} />}>
<ModelRenderer node={node} />
</Suspense>
</ErrorBoundary>
{node.children?.map((childId) => (
<NodeRenderer key={childId} nodeId={childId} />
))}
{roomClearPreview ? (
<ClearPreviewModel node={node} />
) : (
<>
<ErrorBoundary fallback={<BrokenItemFallback node={node} />}>
<Suspense fallback={<PreviewModel node={node} />}>
<ModelRenderer node={node} />
</Suspense>
</ErrorBoundary>
{node.children?.map((childId) => (
<NodeRenderer key={childId} nodeId={childId} />
))}
</>
)}
</group>
)
}
@@ -133,6 +142,26 @@ const PreviewModel = ({ node }: { node: ItemNode }) => {
)
}
const ClearPreviewModel = ({ node }: { node: ItemNode }) => {
const shading = useViewer((s) => s.shading)
const [w, h, d] = getScaledDimensions(node)
const material = useMemo(() => {
const next = createDefaultMaterial('#ef4444', 1, shading) as MutableMaterial
next.depthTest = false
next.opacity = 0.35
next.transparent = true
next.wireframe = true
next.needsUpdate = true
return next
}, [shading])
return (
<mesh material={material} position-y={h / 2}>
<boxGeometry args={[w, h, d]} />
</mesh>
)
}
const multiplyScales = (
a: [number, number, number],
b: [number, number, number],