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
6 changes: 6 additions & 0 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). Must be non-negative. */
endHeightOffset: z.number().min(0).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
22 changes: 22 additions & 0 deletions packages/nodes/src/wall/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,14 @@ export default function WallPanel() {
const followsTerrain = node.fillToTerrain === true
const isPlaneBound = node.height == null
const height = node.height ?? resolvedHeightMeters ?? 2.5
const endHeightOffset = node.endHeightOffset ?? 0
const thickness = node.thickness ?? 0.1
const curveOffset = getClampedWallCurveOffset(node)
const maxCurveOffset = getMaxWallCurveOffset(node)
const unitLabel = getLinearUnitLabel(unit)
const displayLength = metersToLinearUnit(length, unit)
const displayHeight = metersToLinearUnit(height, unit)
const displayEndHeightOffset = metersToLinearUnit(endHeightOffset, unit)
const displayThickness = metersToLinearUnit(thickness, unit)
const displayCurveOffset = metersToLinearUnit(curveOffset, unit)
const displayMaxCurveOffset = metersToLinearUnit(maxCurveOffset, unit)
Expand Down Expand Up @@ -300,6 +302,26 @@ export default function WallPanel() {
value={Math.round(displayHeight * 100) / 100}
/>
)}
<SliderControl
label="End height offset"
max={metersToLinearUnit(3, unit)}
min={0}
onChange={(v) => {
handleUpdate({
endHeightOffset: Math.max(
0,
linearControlValueToMeters(v, unit, {
maxMeters: 3,
minMeters: 0,
}),
),
})
}}
precision={2}
step={0.1}
unit={unitLabel}
value={Math.round(displayEndHeightOffset * 100) / 100}
/>
Comment thread
cursor[bot] marked this conversation as resolved.
<div className="px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
Bottom
</div>
Expand Down
31 changes: 31 additions & 0 deletions packages/viewer/src/systems/wall/wall-system.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,36 @@ function mergeWallTerrainFill(
return merged
}

/**
* Tilts a wall's top edge along its length so the `end` side sits taller (or
* shorter) than the `start` side — e.g. a knee wall following a single-pitch
* roof slope — instead of requiring a non-rectangular footprint. Only
* vertices sitting exactly at the flat extruded top (`topY`) move; a
* vertex's local X (0 at `start`, `wallLength` at `end`) linearly
* interpolates the offset from 0 to `wallNode.endHeightOffset`.
*/
function applyWallEndHeightSlope(
geometry: THREE.BufferGeometry,
wallNode: WallNode,
wallLength: number,
topY: number,
): void {
const rawOffset = wallNode.endHeightOffset
console.log('[applyWallEndHeightSlope] called', { rawOffset, wallLength, topY, height: wallNode.height })
if (!rawOffset || wallLength < 1e-9) {
console.log('[applyWallEndHeightSlope] early return', { rawOffset, wallLength })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug logs in slope helper

Medium Severity

applyWallEndHeightSlope still contains temporary console.log calls, including on the early-return path. generateExtrudedWall runs this for every wall rebuild (render and collision), so the console fills with debug output even when endHeightOffset is unset.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f9213c2. Configure here.

return
}
const endHeightOffset = Math.max(0, rawOffset)
const position = geometry.getAttribute('position') as THREE.BufferAttribute
for (let i = 0; i < position.count; i++) {
if (Math.abs(position.getY(i) - topY) > 1e-4) continue
const t = THREE.MathUtils.clamp(position.getX(i) / wallLength, 0, 1)
position.setY(i, topY + endHeightOffset * t)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curved walls warp sloped top

Medium Severity

applyWallEndHeightSlope drives the top height from chord-local X / wallLength, while curved footprints offset the two faces along arc normals. Those opposite-edge vertices get different local X values at the same station, so front and back tops rise by different amounts and the top surface warps whenever curveOffset and endHeightOffset are both set.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 402111e. Configure here.

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Placement height ignores end offset

Medium Severity

Geometry can extend above the flat wall top by endHeightOffset, but height helpers used for wall placement and overlays still resolve only the unsloped top. Near the taller end, wall-mounted items can be snapped or rejected as if the wall stopped at the original height even though the mesh continues higher.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 402111e. Configure here.

position.needsUpdate = true
}

export function generateExtrudedWall(
wallNode: WallNode,
childrenNodes: AnyNode[],
Expand Down Expand Up @@ -1019,6 +1049,7 @@ export function generateExtrudedWall(
// Rotate so extrusion direction (Z) becomes height direction (Y)
geometry.rotateX(-Math.PI / 2)
if (Math.abs(localBottom) > 1e-9) geometry.translate(0, localBottom, 0)
applyWallEndHeightSlope(geometry, wallNode, L, localBottom + height)
geometry.computeVertexNormals()
assignWallMaterialGroups(geometry, wallNode, boundaryEdges, effectiveWallHeight)
ensureRenderableGeometryAttributes(geometry)
Expand Down