editor: improve cabinet resizing and wall alignment (#503)
* Add roof surface placement support for items Items (e.g. solar panels) can now be placed on sloped roof surfaces. The placement system computes euler rotation from the roof surface normal so items sit flush on the slope instead of going inside. - Add roofStrategy to placement-strategies with enter/move/click/leave - Wire roof:enter/move/click/leave events in the placement coordinator - Add calculateRoofRotation in placement-math using surface normals - Support full 3D cursor rotation for sloped surfaces - Items on roofs are parented to the level with world-space rotation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fixed conflict * fix wall treatment miter geometry * fix cabinet group resizing and corner alignment * fix cabinet corner depth resizing * fix(cabinet): stabilize modular preset changes * fix(cabinet): stabilize corner resizing and wall alignment * fix(nodes): stabilize wall cabinet depth resizing * fix(editor): hide cabinet arrows for module selection * feat(cabinet): add individual width resize handles * feat(cabinet): improve wall cabinet editing * fix(cabinet): harden wall cabinet resizing * fix(cabinet): correct wall drag and depth handles * fix(cabinet): refine individual depth resizing * fix(cabinet): preserve context-aware corner depth behavior * fix(editor): respect snapping modes for resize handles * fix(editor): harden cabinet resize interactions --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
Aymeric Rabot
parent
c0a5db935e
commit
6cc10c929e
@@ -10,7 +10,9 @@ import {
|
||||
import { getVisibleWallMaterials, NodeRenderer, useNodeEvents, useViewer } from '@pascal-app/viewer'
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
||||
import type { Mesh } from 'three'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { createPlaceholderGeometry } from '../shared/placeholder-geometry'
|
||||
import { useWallTreatmentLevelData } from './treatment-level-data'
|
||||
import { createWallExtraSlotMaterials, WallTreatments } from './treatments'
|
||||
|
||||
/**
|
||||
@@ -55,13 +57,15 @@ const WallRenderer = ({ node }: { node: WallNode }) => {
|
||||
const textures = useViewer((s) => s.textures)
|
||||
const colorPreset = useViewer((s) => s.colorPreset)
|
||||
const sceneTheme = useViewer((s) => s.sceneTheme)
|
||||
const sceneNodes = useScene((state) => state.nodes)
|
||||
const childNodes = useMemo(
|
||||
() =>
|
||||
const childNodes = useScene(
|
||||
useShallow((state) =>
|
||||
(node.children ?? [])
|
||||
.map((childId) => sceneNodes[childId as AnyNodeId])
|
||||
.map((childId) => state.nodes[childId as AnyNodeId])
|
||||
.filter((child): child is AnyNode => child !== undefined),
|
||||
[node.children, sceneNodes],
|
||||
),
|
||||
)
|
||||
const treatmentLevelData = useWallTreatmentLevelData((state) =>
|
||||
node.parentId ? state.byLevelId.get(node.parentId) : undefined,
|
||||
)
|
||||
// Subscribe to the scene-material palette so editing a `scene:` material a
|
||||
// wall slot references re-renders the wall live (the wall-system geometry
|
||||
@@ -105,7 +109,14 @@ const WallRenderer = ({ node }: { node: WallNode }) => {
|
||||
{...handlers}
|
||||
/>
|
||||
|
||||
<WallTreatments childrenNodes={childNodes} materials={extraMaterials} node={node} />
|
||||
{treatmentLevelData && (
|
||||
<WallTreatments
|
||||
childrenNodes={childNodes}
|
||||
levelData={treatmentLevelData}
|
||||
materials={extraMaterials}
|
||||
node={node}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(node.children ?? []).map((childId) => (
|
||||
<NodeRenderer key={`${node.id}:${childId}`} nodeId={childId} />
|
||||
|
||||
@@ -1,6 +1,43 @@
|
||||
'use client'
|
||||
|
||||
import { type AnyNodeId, useLiveNodeOverrides, useScene, type WallNode } from '@pascal-app/core'
|
||||
import { WallCutout, WallSystem } from '@pascal-app/viewer'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import { buildWallTreatmentLevelData, useWallTreatmentLevelData } from './treatment-level-data'
|
||||
import { wallTreatmentProudOffsets } from './treatments'
|
||||
|
||||
function effectiveWall(wall: WallNode): WallNode {
|
||||
const override = useLiveNodeOverrides.getState().get(wall.id)
|
||||
return override ? ({ ...wall, ...override } as WallNode) : wall
|
||||
}
|
||||
|
||||
const WallTreatmentMiterSystem = () => {
|
||||
useFrame(() => {
|
||||
const { dirtyNodes, nodes } = useScene.getState()
|
||||
if (dirtyNodes.size === 0) return
|
||||
|
||||
const dirtyLevelIds = new Set<string>()
|
||||
for (const id of dirtyNodes) {
|
||||
const node = nodes[id]
|
||||
if (node?.type === 'wall' && node.parentId) dirtyLevelIds.add(node.parentId)
|
||||
}
|
||||
|
||||
for (const levelId of dirtyLevelIds) {
|
||||
const level = nodes[levelId as AnyNodeId]
|
||||
if (level?.type !== 'level') continue
|
||||
const walls = level.children
|
||||
.map((id) => nodes[id])
|
||||
.filter((node): node is WallNode => node?.type === 'wall')
|
||||
.map(effectiveWall)
|
||||
const proudOffsets = walls.flatMap(wallTreatmentProudOffsets)
|
||||
useWallTreatmentLevelData
|
||||
.getState()
|
||||
.setLevelData(levelId, buildWallTreatmentLevelData(walls, proudOffsets))
|
||||
}
|
||||
}, -1)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry-driven wall system bundle.
|
||||
@@ -16,6 +53,7 @@ import { WallCutout, WallSystem } from '@pascal-app/viewer'
|
||||
const WallSystems = () => {
|
||||
return (
|
||||
<>
|
||||
<WallTreatmentMiterSystem />
|
||||
<WallSystem />
|
||||
<WallCutout />
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
calculateLevelMiters,
|
||||
getWallThickness,
|
||||
type WallMiterData,
|
||||
type WallNode,
|
||||
} from '@pascal-app/core'
|
||||
import { create } from 'zustand'
|
||||
|
||||
const PROUD_KEY_PRECISION = 1e6
|
||||
|
||||
function proudKey(proud: number) {
|
||||
return Math.round(proud * PROUD_KEY_PRECISION) / PROUD_KEY_PRECISION
|
||||
}
|
||||
|
||||
export type WallTreatmentLevelData = {
|
||||
walls: readonly WallNode[]
|
||||
miterDataByProud: ReadonlyMap<number, WallMiterData>
|
||||
}
|
||||
|
||||
export function buildWallTreatmentLevelData(
|
||||
walls: readonly WallNode[],
|
||||
proudOffsets: readonly number[],
|
||||
): WallTreatmentLevelData {
|
||||
const uniqueProudOffsets = new Set([0, ...proudOffsets.map(proudKey)])
|
||||
const miterDataByProud = new Map<number, WallMiterData>()
|
||||
|
||||
for (const proud of uniqueProudOffsets) {
|
||||
const adjustedWalls =
|
||||
proud === 0
|
||||
? [...walls]
|
||||
: walls.map((wall) => ({
|
||||
...wall,
|
||||
thickness: getWallThickness(wall) + proud * 2,
|
||||
}))
|
||||
miterDataByProud.set(proud, calculateLevelMiters(adjustedWalls))
|
||||
}
|
||||
|
||||
return { walls, miterDataByProud }
|
||||
}
|
||||
|
||||
export function treatmentMiterDataForProud(
|
||||
levelData: WallTreatmentLevelData,
|
||||
proud: number,
|
||||
): WallMiterData | undefined {
|
||||
return levelData.miterDataByProud.get(proudKey(proud))
|
||||
}
|
||||
|
||||
type WallTreatmentLevelDataState = {
|
||||
byLevelId: ReadonlyMap<string, WallTreatmentLevelData>
|
||||
setLevelData: (levelId: string, data: WallTreatmentLevelData) => void
|
||||
}
|
||||
|
||||
export const useWallTreatmentLevelData = create<WallTreatmentLevelDataState>((set) => ({
|
||||
byLevelId: new Map(),
|
||||
setLevelData: (levelId, data) =>
|
||||
set((state) => {
|
||||
const byLevelId = new Map(state.byLevelId)
|
||||
byLevelId.set(levelId, data)
|
||||
return { byLevelId }
|
||||
}),
|
||||
}))
|
||||
@@ -0,0 +1,149 @@
|
||||
import { describe, expect, mock, test } from 'bun:test'
|
||||
import type { WallNode, WallTrimConfig } from '@pascal-app/core'
|
||||
import { buildWallTreatmentLevelData } from './treatment-level-data'
|
||||
|
||||
mock.module('@pascal-app/viewer', () => ({
|
||||
baseMaterial: () => undefined,
|
||||
createMaterialFromPresetRef: () => undefined,
|
||||
resolveMaterialRef: () => undefined,
|
||||
}))
|
||||
|
||||
const { buildTrimGeometry, wallTreatmentProudOffsets } = await import('./treatments')
|
||||
|
||||
function wall(id: string, start: [number, number], end: [number, number]): WallNode {
|
||||
return {
|
||||
id,
|
||||
type: 'wall',
|
||||
object: 'node',
|
||||
visible: true,
|
||||
parentId: 'level_test',
|
||||
children: [],
|
||||
start,
|
||||
end,
|
||||
thickness: 0.1,
|
||||
height: 2.5,
|
||||
frontSide: 'interior',
|
||||
backSide: 'exterior',
|
||||
metadata: {},
|
||||
} as WallNode
|
||||
}
|
||||
|
||||
const trim: WallTrimConfig = {
|
||||
enabled: true,
|
||||
height: 0.1,
|
||||
proud: 0.02,
|
||||
profile: 'flat',
|
||||
sides: 'both',
|
||||
}
|
||||
|
||||
function treatmentLevelData(walls: WallNode[]) {
|
||||
const treatedWalls = walls.map((entry) => ({
|
||||
...entry,
|
||||
skirting: trim,
|
||||
crown: trim,
|
||||
chairRail: trim,
|
||||
}))
|
||||
return buildWallTreatmentLevelData(treatedWalls, treatedWalls.flatMap(wallTreatmentProudOffsets))
|
||||
}
|
||||
|
||||
function cornerXs(
|
||||
side: 'interior' | 'exterior',
|
||||
kind: 'skirting' | 'crown' | 'chairRail',
|
||||
outerOffset: number,
|
||||
) {
|
||||
const walls = [wall('A', [0, 0], [3, 0]), wall('B', [0, 0], [0, 3])]
|
||||
const geometry = buildTrimGeometry(walls[0]!, side, trim, kind, [], treatmentLevelData(walls))
|
||||
expect(geometry).not.toBeNull()
|
||||
if (!geometry) throw new Error('expected trim geometry')
|
||||
|
||||
const positions = geometry.getAttribute('position')
|
||||
const outerZ = side === 'interior' ? outerOffset : -outerOffset
|
||||
const xs: number[] = []
|
||||
for (let index = 0; index < positions.count; index += 1) {
|
||||
if (Math.abs(positions.getZ(index) - outerZ) < 1e-5) xs.push(positions.getX(index))
|
||||
}
|
||||
geometry.dispose()
|
||||
return xs
|
||||
}
|
||||
|
||||
function allPositions(geometry: NonNullable<ReturnType<typeof buildTrimGeometry>>) {
|
||||
const positions = geometry.getAttribute('position')
|
||||
return Array.from({ length: positions.count }, (_, index) => ({
|
||||
x: positions.getX(index),
|
||||
y: positions.getY(index),
|
||||
z: positions.getZ(index),
|
||||
}))
|
||||
}
|
||||
|
||||
describe('wall treatment miters', () => {
|
||||
test.each([
|
||||
['skirting', 0.0624],
|
||||
['crown', 0.0604],
|
||||
['chairRail', 0.0616],
|
||||
] as const)('preserves the %s outer miter endpoint on both sides', (kind, outerOffset) => {
|
||||
const interiorXs = cornerXs('interior', kind, outerOffset)
|
||||
const exteriorXs = cornerXs('exterior', kind, outerOffset)
|
||||
|
||||
expect(interiorXs.length).toBeGreaterThan(0)
|
||||
expect(exteriorXs.length).toBeGreaterThan(0)
|
||||
expect(Math.min(...interiorXs)).toBeCloseTo(outerOffset, 5)
|
||||
expect(Math.min(...exteriorXs)).toBeCloseTo(-outerOffset, 5)
|
||||
})
|
||||
|
||||
test('keeps each treatment on one physical side of an isolated wall', () => {
|
||||
const node = wall('A', [0, 0], [3, 0])
|
||||
const levelData = treatmentLevelData([node])
|
||||
|
||||
for (const side of ['interior', 'exterior'] as const) {
|
||||
const geometry = buildTrimGeometry(node, side, trim, 'skirting', [], levelData)
|
||||
expect(geometry).not.toBeNull()
|
||||
if (!geometry) throw new Error('expected trim geometry')
|
||||
const positions = allPositions(geometry)
|
||||
|
||||
expect(positions.every((point) => (side === 'interior' ? point.z > 0 : point.z < 0))).toBe(
|
||||
true,
|
||||
)
|
||||
expect(Math.min(...positions.map((point) => point.x))).toBeCloseTo(0, 6)
|
||||
expect(Math.max(...positions.map((point) => point.x))).toBeCloseTo(3, 6)
|
||||
geometry.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
test('joins the outer profile at an end-to-start room corner', () => {
|
||||
const walls = [wall('A', [0, 0], [3, 0]), wall('B', [3, 0], [3, 3])]
|
||||
const levelData = treatmentLevelData(walls)
|
||||
const a = buildTrimGeometry(walls[0]!, 'interior', trim, 'skirting', [], levelData)
|
||||
const b = buildTrimGeometry(walls[1]!, 'interior', trim, 'skirting', [], levelData)
|
||||
expect(a).not.toBeNull()
|
||||
expect(b).not.toBeNull()
|
||||
if (!(a && b)) throw new Error('expected trim geometry')
|
||||
|
||||
const aOuter = allPositions(a).filter((point) => Math.abs(point.z - 0.0624) < 1e-5)
|
||||
const bOuter = allPositions(b).filter((point) => Math.abs(point.z - 0.0624) < 1e-5)
|
||||
expect(Math.max(...aOuter.map((point) => point.x))).toBeCloseTo(2.9376, 5)
|
||||
expect(Math.min(...bOuter.map((point) => point.x))).toBeCloseTo(0.0624, 5)
|
||||
|
||||
a.dispose()
|
||||
b.dispose()
|
||||
})
|
||||
|
||||
test('keeps opening cuts at their local wall positions', () => {
|
||||
const node = wall('A', [0, 0], [3, 0])
|
||||
const geometry = buildTrimGeometry(
|
||||
node,
|
||||
'interior',
|
||||
trim,
|
||||
'skirting',
|
||||
[{ type: 'door', width: 1, height: 2, position: [1.5, 1, 0] }],
|
||||
treatmentLevelData([node]),
|
||||
)
|
||||
expect(geometry).not.toBeNull()
|
||||
if (!geometry) throw new Error('expected trim geometry')
|
||||
|
||||
const xs = allPositions(geometry).map((point) => point.x)
|
||||
expect(xs.some((x) => Math.abs(x - 1) < 1e-6)).toBe(true)
|
||||
expect(xs.some((x) => Math.abs(x - 2) < 1e-6)).toBe(true)
|
||||
expect(xs.every((x) => x <= 1 + 1e-6 || x >= 2 - 1e-6)).toBe(true)
|
||||
geometry.dispose()
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import {
|
||||
getWallCurveFrameAt,
|
||||
getWallMiterBoundaryPoints,
|
||||
getWallThickness,
|
||||
isCurvedWall,
|
||||
type SceneMaterial,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
import { memo, useEffect, useMemo } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import { mergeGeometries as mergeBufferGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
||||
import { treatmentMiterDataForProud, type WallTreatmentLevelData } from './treatment-level-data'
|
||||
|
||||
const CURVE_SEGMENTS = 24
|
||||
const MIN_SLICE_PROUD = 0.0005
|
||||
@@ -257,6 +259,28 @@ function resolveTrimProfile(kind: TrimKind, trim: WallTrimConfig) {
|
||||
)
|
||||
}
|
||||
|
||||
export function wallTreatmentProudOffsets(node: WallNode): number[] {
|
||||
const offsets = new Set<number>()
|
||||
const configs: Array<[TrimKind, WallTrimConfig | undefined]> = [
|
||||
['skirting', node.skirting],
|
||||
['crown', node.crown],
|
||||
['chairRail', node.chairRail],
|
||||
]
|
||||
|
||||
for (const [kind, rawConfig] of configs) {
|
||||
const trim = { ...TRIM_KIND_CONFIG[kind].defaultConfig, ...(rawConfig ?? {}) }
|
||||
if (!trim.enabled) continue
|
||||
const profile = resolveTrimProfile(kind, trim)
|
||||
if (!profile) continue
|
||||
for (let index = 0; index < profile.samples; index += 1) {
|
||||
const t = (index + 0.5) / profile.samples
|
||||
offsets.add(Math.max(MIN_SLICE_PROUD, trim.proud * profile.proudAt(t)))
|
||||
}
|
||||
}
|
||||
|
||||
return [...offsets]
|
||||
}
|
||||
|
||||
function resolveTreatmentSideSign(node: WallNode, side: WallSide) {
|
||||
if (side === 'interior') {
|
||||
if (node.frontSide === 'interior') return 1
|
||||
@@ -300,8 +324,30 @@ function buildSidePolyline(node: WallNode, side: WallSide, offset: number): Poin
|
||||
return points
|
||||
}
|
||||
|
||||
function clipPolyline(points: Point2[], x0: number, x1: number): Point2[] {
|
||||
if (points.length < 2 || x1 - x0 <= EPS) return []
|
||||
function buildMiteredSidePolyline(
|
||||
node: WallNode,
|
||||
levelData: WallTreatmentLevelData,
|
||||
side: WallSide,
|
||||
offset: number,
|
||||
): Point2[] {
|
||||
if (isCurvedWall(node)) return buildSidePolyline(node, side, offset)
|
||||
|
||||
const sideSign = resolveTreatmentSideSign(node, side)
|
||||
const toLocal = wallToLocalTransform(node)
|
||||
const proud = offset - getWallThickness(node) / 2
|
||||
const boundarySource = treatmentMiterDataForProud(levelData, proud)
|
||||
if (!boundarySource) return buildSidePolyline(node, side, offset)
|
||||
const boundary = getWallMiterBoundaryPoints({ ...node, thickness: offset * 2 }, boundarySource)
|
||||
|
||||
if (!boundary) return buildSidePolyline(node, side, offset)
|
||||
|
||||
const start = sideSign > 0 ? boundary.startLeft : boundary.startRight
|
||||
const end = sideSign > 0 ? boundary.endLeft : boundary.endRight
|
||||
return [toLocal(start.x, start.y), toLocal(end.x, end.y)]
|
||||
}
|
||||
|
||||
function clipPolyline(points: Point2[], x0?: number, x1?: number): Point2[] {
|
||||
if (points.length < 2 || (x0 !== undefined && x1 !== undefined && x1 - x0 <= EPS)) return []
|
||||
const out: Point2[] = []
|
||||
for (let index = 0; index < points.length - 1; index += 1) {
|
||||
const a = points[index]
|
||||
@@ -309,7 +355,9 @@ function clipPolyline(points: Point2[], x0: number, x1: number): Point2[] {
|
||||
if (!(a && b)) continue
|
||||
const minX = Math.min(a.x, b.x)
|
||||
const maxX = Math.max(a.x, b.x)
|
||||
if (maxX < x0 - EPS || minX > x1 + EPS) continue
|
||||
if ((x0 !== undefined && maxX < x0 - EPS) || (x1 !== undefined && minX > x1 + EPS)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const pushPointAt = (x: number) => {
|
||||
if (Math.abs(b.x - a.x) <= EPS) {
|
||||
@@ -322,8 +370,8 @@ function clipPolyline(points: Point2[], x0: number, x1: number): Point2[] {
|
||||
}
|
||||
}
|
||||
|
||||
const start = minX < x0 ? pushPointAt(x0) : a
|
||||
const end = maxX > x1 ? pushPointAt(x1) : b
|
||||
const start = x0 !== undefined && minX < x0 ? pushPointAt(x0) : a
|
||||
const end = x1 !== undefined && maxX > x1 ? pushPointAt(x1) : b
|
||||
if (
|
||||
out.length === 0 ||
|
||||
Math.hypot(out[out.length - 1]!.x - start.x, out[out.length - 1]!.z - start.z) > EPS
|
||||
@@ -446,12 +494,13 @@ function mergeGeometries(geometries: THREE.BufferGeometry[]) {
|
||||
return null
|
||||
}
|
||||
|
||||
function buildTrimGeometry(
|
||||
export function buildTrimGeometry(
|
||||
node: WallNode,
|
||||
side: WallSide,
|
||||
trim: WallTrimConfig,
|
||||
kind: TrimKind,
|
||||
childrenNodes: OpeningLike[],
|
||||
levelData: WallTreatmentLevelData,
|
||||
) {
|
||||
const wallHeight = node.height ?? 2.5
|
||||
const height = trim.height
|
||||
@@ -466,11 +515,12 @@ function buildTrimGeometry(
|
||||
: 0
|
||||
|
||||
const thickness = getWallThickness(node)
|
||||
const inner = buildSidePolyline(node, side, thickness / 2)
|
||||
const inner = buildMiteredSidePolyline(node, levelData, side, thickness / 2)
|
||||
if (inner.length < 2) return null
|
||||
|
||||
const wallLength = Math.hypot(node.end[0] - node.start[0], node.end[1] - node.start[1])
|
||||
const openingRanges = trimOpeningRanges(node, childrenNodes, yBottom, height)
|
||||
const fullRanges: Array<[number, number]> = [[inner[0]!.x, inner[inner.length - 1]!.x]]
|
||||
const fullRanges: Array<[number, number]> = [[0, wallLength]]
|
||||
const runs = subtractOpeningRanges(fullRanges, openingRanges)
|
||||
if (runs.length === 0) return null
|
||||
|
||||
@@ -480,13 +530,15 @@ function buildTrimGeometry(
|
||||
const sliceHeight = height / profile.samples
|
||||
|
||||
for (const [runStart, runEnd] of runs) {
|
||||
const innerRun = clipPolyline(inner, runStart, runEnd)
|
||||
const clipStart = runStart > EPS ? runStart : undefined
|
||||
const clipEnd = runEnd < wallLength - EPS ? runEnd : undefined
|
||||
const innerRun = clipPolyline(inner, clipStart, clipEnd)
|
||||
if (innerRun.length < 2) continue
|
||||
for (let index = 0; index < profile.samples; index += 1) {
|
||||
const t = (index + 0.5) / profile.samples
|
||||
const proud = Math.max(MIN_SLICE_PROUD, trim.proud * profile.proudAt(t))
|
||||
const outerRun = buildSidePolyline(node, side, thickness / 2 + proud)
|
||||
const outerClipped = clipPolyline(outerRun, runStart, runEnd)
|
||||
const outerRun = buildMiteredSidePolyline(node, levelData, side, thickness / 2 + proud)
|
||||
const outerClipped = clipPolyline(outerRun, clipStart, clipEnd)
|
||||
if (outerClipped.length < 2) continue
|
||||
const slice = buildTrimSliceGeometry(
|
||||
outerClipped,
|
||||
@@ -538,10 +590,12 @@ export function createWallExtraSlotMaterials(
|
||||
export const WallTreatments = memo(function WallTreatments({
|
||||
node,
|
||||
childrenNodes,
|
||||
levelData,
|
||||
materials,
|
||||
}: {
|
||||
node: WallNode
|
||||
childrenNodes: OpeningLike[]
|
||||
levelData: WallTreatmentLevelData
|
||||
materials: Record<WallTreatmentSlotId, THREE.Material>
|
||||
}) {
|
||||
const fallbackMaterial =
|
||||
@@ -574,7 +628,7 @@ export const WallTreatments = memo(function WallTreatments({
|
||||
? (['interior', 'exterior'] as WallSide[])
|
||||
: ([trim.sides] as WallSide[])
|
||||
for (const side of sides) {
|
||||
const geometry = buildTrimGeometry(node, side, trim, kind, childrenNodes)
|
||||
const geometry = buildTrimGeometry(node, side, trim, kind, childrenNodes, levelData)
|
||||
if (!geometry) continue
|
||||
const slotId = TRIM_KIND_CONFIG[kind].slots[side]
|
||||
out.push({
|
||||
@@ -587,7 +641,7 @@ export const WallTreatments = memo(function WallTreatments({
|
||||
}
|
||||
|
||||
return out
|
||||
}, [childrenNodes, fallbackMaterial, materials, node])
|
||||
}, [childrenNodes, fallbackMaterial, levelData, materials, node])
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
|
||||
Reference in New Issue
Block a user