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
2 changes: 2 additions & 0 deletions packages/nodes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,5 +235,7 @@ export { structuralGridDefinition } from './structural-grid'
export { turbineVentDefinition } from './turbine-vent'
export { unitDefinition } from './unit'
export { wallDefinition } from './wall'
export { getCurtainAwareWallMaterials } from './wall/curtain-wall-materials'
export { setWallMaterialsResolver } from './wall/materials-resolver'
export { windowDefinition } from './window'
export { zoneDefinition } from './zone'
27 changes: 27 additions & 0 deletions packages/nodes/src/wall/materials-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { WallMaterialsResolver } from '@pascal-app/viewer'
import { getCurtainAwareWallMaterials } from './curtain-wall-materials'

let resolver: WallMaterialsResolver = getCurtainAwareWallMaterials
const listeners = new Set<() => void>()

/**
* Replaces the resolver `WallCutout` uses for every wall's material set
* (visible / cut-away / translucent / delete variants). Hosts wrap
* `getCurtainAwareWallMaterials` to restyle a variant, e.g. the cut-away look;
* `null` restores the default.
*/
export function setWallMaterialsResolver(next: WallMaterialsResolver | null): void {
resolver = next ?? getCurtainAwareWallMaterials
for (const listener of listeners) listener()
}

export function getWallMaterialsResolver(): WallMaterialsResolver {
return resolver
}

export function subscribeWallMaterialsResolver(listener: () => void): () => void {
listeners.add(listener)
return () => {
listeners.delete(listener)
}
}
11 changes: 8 additions & 3 deletions packages/nodes/src/wall/system.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import {
} from '@pascal-app/core'
import { timeSpan, WallCutout, WallSystem } from '@pascal-app/viewer'
import { useFrame } from '@react-three/fiber'
import { useEffect } from 'react'
import { useEffect, useSyncExternalStore } from 'react'
import { curtainWallGeometryAdapter } from './curtain-wall-adapter'
import { getCurtainAwareWallMaterials } from './curtain-wall-materials'
import { getWallMaterialsResolver, subscribeWallMaterialsResolver } from './materials-resolver'
import {
buildWallTreatmentLevelData,
clearWallTreatmentMiterCache,
Expand Down Expand Up @@ -176,11 +176,16 @@ const WallTreatmentMiterSystem = () => {
* draw calls instead of one per wall face run.
*/
const WallSystems = () => {
const materialResolver = useSyncExternalStore(
subscribeWallMaterialsResolver,
getWallMaterialsResolver,
getWallMaterialsResolver,
)
return (
<>
<WallTreatmentMiterSystem />
<WallSystem geometryAdapter={curtainWallGeometryAdapter} />
<WallCutout materialResolver={getCurtainAwareWallMaterials} />
<WallCutout materialResolver={materialResolver} />
Comment thread
cursor[bot] marked this conversation as resolved.
<WallBatchSystem />
</>
)
Expand Down
24 changes: 24 additions & 0 deletions packages/nodes/src/wall/wall-batch-system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Object3D,
} from 'three'
import { revealAllBatchedHolds } from '../shared/node-batch/candidates'
import { setWallMaterialsResolver } from './materials-resolver'
import {
collectTintedWalls,
collectWallBatchCandidates,
Expand Down Expand Up @@ -265,6 +266,29 @@ test('merged wall batches are stripped from GLB exports', () => {
expect(batch.userData.pascalExport).toBe('strip')
})

test('a host resolver swap re-sews a batched level from the new materials', () => {
const { batch, root, walls } = setupBatchedLevel()
const restyled = new MeshBasicMaterial()
try {
// The swap re-renders each wall's cutout with the new set; the merged mesh
// still holds the old one until the level is sewn again.
setWallMaterialsResolver(viewerExports.getMaterialsForWall)
for (const wall of walls) wall.material = [restyled]
nowMs = 200
runFrame()
nowMs = 381
runFrame()

const sewn = root.children.find((child) => child.name === 'wall-batch') as Mesh | undefined
expect(sewn).toBeDefined()
expect(sewn).not.toBe(batch)
expect([sewn!.material].flat()).toContain(restyled)
} finally {
setWallMaterialsResolver(null)
restyled.dispose()
}
})

test('wall batches keep waiting for pending neighbours even after the dirty census is clean', () => {
const { root, walls } = setupBatchedLevel()
let pending = 1
Expand Down
21 changes: 14 additions & 7 deletions packages/nodes/src/wall/wall-batch-system.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import {
isNodeRevealing,
SCENE_LAYER,
useViewer,
type WallMaterialsResolver,
type WallMode,
} from '@pascal-app/viewer'
import { useFrame, useThree } from '@react-three/fiber'
import { useEffect, useRef } from 'react'
import { type Material, Matrix4, Mesh, type Object3D } from 'three'
import { isSlotPaintPreviewActive } from '../shared/slot-paint'
import { getWallMaterialsResolver } from './materials-resolver'
import {
applyWallBatchGroups,
buildWallBatch,
Expand Down Expand Up @@ -49,12 +51,12 @@ type BatchRecord = {
* node.
*
* A wall whose own definition changes is marked dirty and leaves its batch on
* that signal. These four do not go through a node at all — they are viewer
* toggles and the scene's material library — yet the cutaway pass rebuilds
* every wall's material set from them, so a merged mesh holding the old set
* would keep a whole floor looking the way it did before the switch. They
* change only when someone deliberately flips a switch, so re-sewing the
* scene on them is cheap.
* that signal. These do not go through a node at all — viewer toggles, the
* scene's material library and the host's wall materials resolver — yet the
* cutaway pass rebuilds every wall's material set from them, so a merged mesh
* holding the old set would keep a whole floor looking the way it did before
* the switch. They change only when someone deliberately flips a switch, so
* re-sewing the scene on them is cheap.
*/
type AppearanceInputs = {
shading: unknown
Expand All @@ -63,6 +65,7 @@ type AppearanceInputs = {
sceneTheme: unknown
materials: object | null
libraryVersion: number
resolver: WallMaterialsResolver | null
}

const lastAppearance: AppearanceInputs = {
Expand All @@ -72,20 +75,23 @@ const lastAppearance: AppearanceInputs = {
sceneTheme: undefined,
materials: null,
libraryVersion: -1,
resolver: null,
}

function appearanceChanged(): boolean {
const viewer = useViewer.getState()
const materials = useScene.getState().materials as object
const libraryVersion = getLibraryMaterialsVersion()
const resolver = getWallMaterialsResolver()

if (
lastAppearance.shading === viewer.shading &&
lastAppearance.textures === viewer.textures &&
lastAppearance.colorPreset === viewer.colorPreset &&
lastAppearance.sceneTheme === viewer.sceneTheme &&
lastAppearance.materials === materials &&
lastAppearance.libraryVersion === libraryVersion
lastAppearance.libraryVersion === libraryVersion &&
lastAppearance.resolver === resolver
) {
return false
}
Expand All @@ -96,6 +102,7 @@ function appearanceChanged(): boolean {
lastAppearance.sceneTheme = viewer.sceneTheme
lastAppearance.materials = materials
lastAppearance.libraryVersion = libraryVersion
lastAppearance.resolver = resolver
return true
}

Expand Down