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
52 changes: 26 additions & 26 deletions packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,36 +352,28 @@ export class SpatialGridManager {
return this.wallGrids.get(levelId)!
}

private getWall(wallId: string): WallNode | undefined {
const fromScene = useScene.getState().nodes[wallId as AnyNodeId]
if (fromScene && fromScene.type === 'wall') {
this.walls.set(wallId, fromScene as WallNode)
return fromScene as WallNode
}
return this.walls.get(wallId)
}

private getWallLength(wallId: string): number {
const wall = this.walls.get(wallId)
const wall = this.getWall(wallId)
if (!wall) return 0
const dx = wall.end[0] - wall.start[0]
const dy = wall.end[1] - wall.start[1]
return Math.sqrt(dx * dx + dy * dy)
return Math.hypot(dx, dy)
}

private getWallHeight(wallId: string): number {
const wall = this.walls.get(wallId)
private getWallHeight(wallId: string, t?: number): number {
const wall = this.getWall(wallId)
if (!wall) return 0
if (wall.height != null) return wall.height

const nodes = useScene.getState().nodes
const levelId = resolveNodeLevelId(wall, nodes)
const support = this.getSlabSupportForWall(
levelId,
wall.start,
wall.end,
wall.curveOffset ?? 0,
wall.thickness,
wall.supportSlabId ?? null,
undefined,
wall.supportOffset,
)
return resolveWallEffectiveHeight(
wall,
getWallPlaneTop(wall, levelId, nodes),
support.elevation,
)
const nodes = useScene.getState().nodes as Record<string, AnyNode>
return getWallEffectiveHeightForNodes(wall, nodes, t)
}

private getCeilingGrid(ceilingId: string): SpatialGrid {
Expand Down Expand Up @@ -772,10 +764,17 @@ export class SpatialGridManager {
if (wallLength === 0) {
return { valid: false, conflictIds: [] }
}
const wallHeight = this.getWallHeight(wallId)
const [itemWidth, itemHeight] = dimensions
// Convert local X position to parametric t (0-1)
const tCenter = localX / wallLength
const [itemWidth, itemHeight] = dimensions
const halfW = itemWidth / wallLength / 2
const tStart = Math.max(0, Math.min(1, tCenter - halfW))
const tEnd = Math.max(0, Math.min(1, tCenter + halfW))
const hStart = this.getWallHeight(wallId, tStart)
const hEnd = this.getWallHeight(wallId, tEnd)
const hCenter = this.getWallHeight(wallId, tCenter)
const wallHeight = Math.min(hStart, hEnd, hCenter)

const baseResult = this.getWallGrid(levelId).canPlaceOnWall(
wallId,
wallLength,
Expand Down Expand Up @@ -1312,8 +1311,9 @@ export function getWallBaseElevationForNodes(
export function getWallEffectiveHeightForNodes(
wall: WallNode,
nodes: Record<string, AnyNode>,
t?: number,
): number {
const levelId = resolveNodeLevelId(wall, nodes)
const baseElevation = getWallBaseElevationForNodes(wall, nodes)
return resolveWallEffectiveHeight(wall, getWallPlaneTop(wall, levelId, nodes), baseElevation)
return resolveWallEffectiveHeight(wall, getWallPlaneTop(wall, levelId, nodes), baseElevation, t)
}
12 changes: 8 additions & 4 deletions packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,15 @@ export function initSpatialGridSync(): () => void {
node.start !== prev.start ||
node.end !== prev.end ||
node.curveOffset !== prev.curveOffset ||
node.thickness !== prev.thickness
node.thickness !== prev.thickness ||
node.height !== prev.height ||
node.endHeightOffset !== prev.endHeightOffset ||
node.supportSlabId !== prev.supportSlabId ||
node.supportOffset !== prev.supportOffset
) {
// Rendered slab polygons adopt wall bands, so a wall reshape
// must reach the manager to refresh its wall map and drop the
// level's rendered-polygon cache.
// Rendered slab polygons adopt wall bands, and wall height/slope
// queries must see the latest node state — reach the manager to
// refresh its wall map and drop the level's rendered-polygon cache.
spatialGridManager.handleNodeUpdated(node, resolveLevelId(node, state.nodes))
}
}
Expand Down
11 changes: 8 additions & 3 deletions packages/core/src/lib/space-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,9 +445,13 @@ function autoRoomVerticalPlacements(
const base = roomFloorPlane(wallBases)
if (base === undefined) continue

const wallTops = boundaryWalls.map((wall, index) =>
resolveWallTop(wall, storeyHeight, wallBases[index] ?? base),
)
const wallTops = boundaryWalls.flatMap((wall, index) => {
const b = wallBases[index] ?? base
return [
resolveWallTop(wall, storeyHeight, b, 0),
resolveWallTop(wall, storeyHeight, b, 1),
]
})
const top = roomCeilingPlane(wallTops)
if (top === undefined) continue

Expand Down Expand Up @@ -1207,6 +1211,7 @@ function wallGeometrySignature(wall: WallNode, nodes: Record<string, any>, level
// value: it resolves to the storey plane, so it must not alias an
// explicit height of the same magnitude in the trigger signature.
wall.height == null ? 'plane' : wall.height.toFixed(4),
(wall.endHeightOffset ?? 0).toFixed(4),
wall.supportSlabId ?? 'elected',
(wall.supportOffset ?? 0).toFixed(4),
getClampedWallCurveOffset(wall).toFixed(4),
Expand Down
13 changes: 10 additions & 3 deletions packages/core/src/schema/nodes/wall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ export const WallNode = BaseNode.extend({
slots: z.record(z.string(), z.string()).optional(),
thickness: z.number().optional(),
height: z.number().optional(),
// Added to the wall's top only at its `end` point (`start` is unaffected),
// tilting the top edge along the wall's length so one side is taller than
// the other — e.g. a knee wall following a single-pitch roof slope.
/** Height offset at the end point (default 0). */
endHeightOffset: z.number().optional(),
curveOffset: z.number().optional(),
// Persisted slab-support host — see ItemNode.supportSlabId for the rules.
supportSlabId: z.string().optional(),
Expand All @@ -174,6 +179,7 @@ export const WallNode = BaseNode.extend({
Wall node - used to represent a wall in the building
- thickness: thickness in meters
- height: height in meters
- endHeightOffset: added to the top only at the wall's end point, tilting the top edge so one side is taller than the other
- fillToTerrain: extends the wall downward to the terrain without changing its authored height
- curveOffset: midpoint sagitta offset used to bend the wall into an arc
- start: start point of the wall in level coordinate system
Expand Down Expand Up @@ -208,10 +214,11 @@ export const WALL_SLOT_DEFAULT: Record<WallSurfaceSide, string> = {
}

export function getWallFaceBandConfig(
wall: Pick<WallNode, 'height' | 'faceBands'>,
wall: Pick<WallNode, 'height' | 'endHeightOffset' | 'faceBands'>,
effectiveWallHeight: number,
) {
const wallHeight = Math.max(0, effectiveWallHeight)
const maxWallHeight = effectiveWallHeight + Math.max(0, wall.endHeightOffset ?? 0)
const wallHeight = Math.max(0, maxWallHeight)
const raw = { ...WALL_FACE_BAND_DEFAULT, ...(wall.faceBands ?? {}) }
const count = raw.enabled ? Math.max(1, Math.min(4, Math.round(raw.count ?? 3))) : 1
const lowerHeight = count >= 2 ? Math.max(0, Math.min(wallHeight, raw.lowerHeight)) : 0
Expand All @@ -233,7 +240,7 @@ export function getWallFaceBandConfig(
}

export function getWallFaceBandForHeight(
wall: Pick<WallNode, 'height' | 'faceBands'>,
wall: Pick<WallNode, 'height' | 'endHeightOffset' | 'faceBands'>,
y: number,
effectiveWallHeight: number,
): WallFaceBand {
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/services/level-height.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ export function deriveLegacyLevelHeight(
slabs,
walls,
).elevation
const top = resolveWallTop(wall, level.height ?? DEFAULT_LEVEL_HEIGHT, electedElevation)
const topStart = resolveWallTop(wall, level.height ?? DEFAULT_LEVEL_HEIGHT, electedElevation, 0)
const topEnd = resolveWallTop(wall, level.height ?? DEFAULT_LEVEL_HEIGHT, electedElevation, 1)
const top = Math.max(topStart, topEnd)
if (top > maxTop) maxTop = top
}
}
Expand Down
26 changes: 20 additions & 6 deletions packages/core/src/systems/wall/wall-top.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,26 @@ export const MIN_WALL_HEIGHT = 0.5
* Returns the top in level-local Y (same frame as `electedBase`).
*/
export function resolveWallTop(
wall: Pick<WallNode, 'height' | 'supportSlabId'>,
wall: Pick<WallNode, 'height' | 'supportSlabId' | 'endHeightOffset'>,
storeyHeight: number,
electedBase: number,
t?: number,
): number {
if (wall.height == null) return storeyHeight
if (wall.supportSlabId === 'ground') return electedBase + wall.height
return electedBase > 0 ? electedBase + wall.height : wall.height
let top: number
if (wall.height == null) {
top = storeyHeight
} else if (wall.supportSlabId === 'ground') {
top = electedBase + wall.height
} else {
top = electedBase > 0 ? electedBase + wall.height : wall.height
}
if (wall.endHeightOffset && t !== undefined) {
const bodyHeight = Math.max(0.01, top - electedBase)
const minEndHeight = 0.01
const clampedOffset = Math.max(wall.endHeightOffset, -(bodyHeight - minEndHeight))
top += clampedOffset * t
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
return top
}

/**
Expand All @@ -48,9 +61,10 @@ export function resolveWallTop(
* policy.
*/
export function resolveWallEffectiveHeight(
wall: Pick<WallNode, 'height' | 'supportSlabId'>,
wall: Pick<WallNode, 'height' | 'supportSlabId' | 'endHeightOffset'>,
storeyHeight: number,
electedBase: number,
t?: number,
): number {
return resolveWallTop(wall, storeyHeight, electedBase) - electedBase
return resolveWallTop(wall, storeyHeight, electedBase, t) - electedBase
}
41 changes: 28 additions & 13 deletions packages/editor/src/components/editor/wall-measurement-label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,16 +315,25 @@ function buildMeasurementGuide(
const measurementPoints = measurementLine ?? fallbackMiddlePoints
if (!measurementPoints) return null

const height = getWallEffectiveHeightForNodes(wall, nodes)
const heightStart = getWallEffectiveHeightForNodes(wall, nodes, 0)
const heightEnd = getWallEffectiveHeightForNodes(wall, nodes, 1)
const startLocal = worldPointToWallLocal(wall, measurementPoints.start)
const endLocal = worldPointToWallLocal(wall, measurementPoints.end)
const curvedMeasurementPath = isCurvedWall(wall)
? getCurvedWallMeasurementPath(wall, miterData, levelWalls)
: null
const dx = wall.end[0] - wall.start[0]
const dz = wall.end[1] - wall.start[1]
const wallChordLength = Math.hypot(dx, dz)
const getChordT = (localX: number) =>
wallChordLength > 1e-6 ? Math.max(0, Math.min(1, localX / wallChordLength)) : 0

const guidePath: Vec3[] = curvedMeasurementPath
? curvedMeasurementPath.map((point) => {
const localPoint = worldPointToWallLocal(wall, point)
return [localPoint[0], height + GUIDE_Y_OFFSET, localPoint[2]]
const t = getChordT(localPoint[0])
const h = getWallEffectiveHeightForNodes(wall, nodes, t)
return [localPoint[0], h + GUIDE_Y_OFFSET, localPoint[2]]
})
: isCurvedWall(wall)
? sampleWallCenterline(wall, 24).map((point, index, points) => {
Expand All @@ -334,12 +343,14 @@ function buildMeasurementGuide(
: index === points.length - 1
? endLocal
: worldPointToWallLocal(wall, point)
const t = getChordT(localPoint[0])
const h = getWallEffectiveHeightForNodes(wall, nodes, t)

return [localPoint[0], height + GUIDE_Y_OFFSET, localPoint[2]]
return [localPoint[0], h + GUIDE_Y_OFFSET, localPoint[2]]
Comment thread
cursor[bot] marked this conversation as resolved.
})
: [
[startLocal[0], height + GUIDE_Y_OFFSET, startLocal[2]],
[endLocal[0], height + GUIDE_Y_OFFSET, endLocal[2]],
[startLocal[0], heightStart + GUIDE_Y_OFFSET, startLocal[2]],
[endLocal[0], heightEnd + GUIDE_Y_OFFSET, endLocal[2]],
]

if (guidePath.length < 2) return null
Expand Down Expand Up @@ -397,26 +408,30 @@ function buildMeasurementGuide(
],
})
const bottomHeightTick = getHorizontalHeightTick(0)
const topHeightTick = getHorizontalHeightTick(height)
const topHeightTick = getHorizontalHeightTick(heightEnd)

return {
guidePath,
extStartStart: [extensionStartBase[0], height, extensionStartBase[2]],
extStartStart: [extensionStartBase[0], heightStart, extensionStartBase[2]],
extStartEnd: [
extensionStartBase[0],
height + GUIDE_Y_OFFSET + extOvershoot,
heightStart + GUIDE_Y_OFFSET + extOvershoot,
extensionStartBase[2],
],
extEndStart: [extensionEndBase[0], height, extensionEndBase[2]],
extEndEnd: [extensionEndBase[0], height + GUIDE_Y_OFFSET + extOvershoot, extensionEndBase[2]],
extEndStart: [extensionEndBase[0], heightEnd, extensionEndBase[2]],
extEndEnd: [
extensionEndBase[0],
heightEnd + GUIDE_Y_OFFSET + extOvershoot,
extensionEndBase[2],
],
labelPosition: [midpoint[0], midpoint[1] + LABEL_LIFT, midpoint[2]],
heightStart: [heightGuidePosition[0], 0, heightGuidePosition[2]],
heightEnd: [heightGuidePosition[0], height, heightGuidePosition[2]],
heightEnd: [heightGuidePosition[0], heightEnd, heightGuidePosition[2]],
heightBottomTickStart: bottomHeightTick.start,
heightBottomTickEnd: bottomHeightTick.end,
heightTopTickStart: topHeightTick.start,
heightTopTickEnd: topHeightTick.end,
heightLabelPosition: [heightGuidePosition[0], height / 2, heightGuidePosition[2]],
heightLabelPosition: [heightGuidePosition[0], heightEnd / 2, heightGuidePosition[2]],
}
}

Expand Down Expand Up @@ -530,7 +545,7 @@ function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
return total
}, [guide, wall])
const label = formatLinearMeasurement(length, unit, metricNotation)
const height = useMemo(() => getWallEffectiveHeightForNodes(wall, nodes), [nodes, wall])
const height = useMemo(() => getWallEffectiveHeightForNodes(wall, nodes, 1), [nodes, wall])
const heightLabel = `H ${formatLinearMeasurement(height, unit, metricNotation)}`

if (!(guide && Number.isFinite(length) && length >= 0.01)) return null
Expand Down
10 changes: 7 additions & 3 deletions packages/editor/src/components/editor/wall-move-side-handles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,11 @@ function WallCornerLeaderHandle({ wall, endpoint }: { wall: WallNode; endpoint:
const corner = endpoint === 'start' ? wall.start : wall.end
const x = corner[0]
const z = corner[1]
const wallHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
const wallHeight = getWallEffectiveHeightForNodes(
wall,
useScene.getState().nodes,
endpoint === 'start' ? 0 : 1,
)

const dashedGeometry = useMemo(() => buildDashedVerticalGeometry(wallHeight), [wallHeight])
const hitGeometry = useMemo(() => createEndpointHitAreaGeometry(CORNER_HEX_RADIUS), [])
Expand Down Expand Up @@ -606,7 +610,7 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
const wallAngle = Math.atan2(-dirZ, dirX)
// `wall` is the override-merged effective wall (see
// WallMoveSideHandlesForWall), so this height is already live during a drag.
const wallHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
const wallHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes, 0.5)
const handleY = wallHeight + HEIGHT_HANDLE_OFFSET

const activateHeightResize = (event: ThreeEvent<PointerEvent>) => {
Expand Down Expand Up @@ -949,7 +953,7 @@ function getWallMoveHandles(wall: WallNode, nodes: Record<string, AnyNode>): Wal
const midpoint: [number, number] = frame
? [frame.point.x, frame.point.y]
: [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
const wallHeight = getWallEffectiveHeightForNodes(wall, nodes)
const wallHeight = getWallEffectiveHeightForNodes(wall, nodes, 0.5)
const handleHeight = Math.max(wallHeight - HANDLE_TOP_INSET, HANDLE_MIN_HEIGHT)
const offset = Math.max(getWallThickness(wall) / 2 + HANDLE_OFFSET, HANDLE_MIN_OFFSET)

Expand Down
Loading