Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions packages/nodes/src/column/renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type ColumnSlotId,
collectDescendants,
createSceneApi,
getFloorStackedPosition,
useLiveNodeOverrides,
useLiveTransforms,
useRegistry,
Expand All @@ -27,6 +28,7 @@ import {
createSurfaceRoleMaterial,
NodeRenderer,
type RenderShading,
resolveColumnRoofHeight,
resolveMaterialRef,
resolveSlotDefaultMaterial,
useNodeEvents,
Expand Down Expand Up @@ -2246,10 +2248,19 @@ export const ColumnRenderer = ({ node: rawNode }: { node: ColumnNode }) => {
// hearing the commit on release. Subscribes narrowly to this node's
// override entry; unrelated writes don't re-render.
const liveOverride = useLiveNodeOverrides((s) => s.overrides.get(rawNode.id))
const node = useMemo<ColumnNode>(
() => (liveOverride ? ({ ...rawNode, ...liveOverride } as ColumnNode) : rawNode),
[rawNode, liveOverride],
// A post under a roof meets its underside (see `resolveColumnRoofHeight`).
const roofHeight = useScene((s) =>
resolveColumnRoofHeight(
rawNode,
s.nodes,
// The floor-elevation system lifts the column onto its slab; measure from there.
getFloorStackedPosition({ node: rawNode, nodes: s.nodes, position: rawNode.position })[1],
),
)
const node = useMemo<ColumnNode>(() => {
const merged = liveOverride ? ({ ...rawNode, ...liveOverride } as ColumnNode) : rawNode
return roofHeight != null && roofHeight > 0 ? { ...merged, height: roofHeight } : merged
}, [rawNode, liveOverride, roofHeight])
const scriptedColumn = Boolean(node.source)
const modelData = useMemo(() => ({ scriptedColumn }), [scriptedColumn])
const handlers = useNodeEvents(node, 'column')
Expand Down
1 change: 1 addition & 0 deletions packages/viewer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ export {
roofCsgDummyMats,
type SurfaceFrame,
} from './systems/roof/roof-system'
export { resolveColumnRoofHeight } from './systems/roof/roof-underside'
export { ScanSystem } from './systems/scan/scan-system'
// Pure slab geometry generator — composed into the registry-driven slab
// definition's `def.geometry` in `@pascal-app/nodes`.
Expand Down
47 changes: 47 additions & 0 deletions packages/viewer/src/systems/roof/roof-clip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
// depend on @types/bun so the import type is unresolved at compile time.
import { describe, expect, test } from 'bun:test'
import * as THREE from 'three'
import { clipGeometryByRegions } from './roof-clip'

/** Above y = 1 + 0.3 x, inside 0 ≤ x ≤ 4, 0 ≤ z ≤ 4. */
const region = [
new THREE.Plane(new THREE.Vector3(-0.3, 1, 0), -1),
new THREE.Plane(new THREE.Vector3(1, 0, 0), 0),
new THREE.Plane(new THREE.Vector3(-1, 0, 0), 4),
new THREE.Plane(new THREE.Vector3(0, 0, 1), 0),
new THREE.Plane(new THREE.Vector3(0, 0, -1), 4),
]

function post(x: number, height = 3) {
const geometry = new THREE.BoxGeometry(0.2, height, 0.2)
geometry.translate(x, height / 2, 2)
return geometry
}

describe('roof clip', () => {
test('a post under the slope loses everything above the underside', () => {
const clipped = clipGeometryByRegions(post(2), [region])!
expect(clipped).not.toBeNull()
const positions = clipped.getAttribute('position')
for (let i = 0; i < positions.count; i++) {
const x = positions.getX(i)
expect(positions.getY(i)).toBeLessThanOrEqual(1 + 0.3 * x + 1e-5)
}
clipped.computeBoundingBox()
expect(clipped.boundingBox!.max.y).toBeCloseTo(1 + 0.3 * 2.1, 4)
expect(clipped.boundingBox!.min.y).toBeCloseTo(0, 6)
})

test('a post outside the roof outline or below the underside stays as it is', () => {
expect(clipGeometryByRegions(post(6), [region])).toBeNull()
expect(clipGeometryByRegions(post(2, 0.8), [region])).toBeNull()
})

test('material groups survive the cut', () => {
const clipped = clipGeometryByRegions(post(2), [region])!
expect(new Set(clipped.groups.map((group) => group.materialIndex)).size).toBeGreaterThan(1)
const total = clipped.groups.reduce((sum, group) => sum + group.count, 0)
expect(total).toBe(clipped.getAttribute('position').count)
})
})
Loading