Skip to content

viewer: Fix Dutch roof trim artifacts#452

Merged
wass08 merged 60 commits into
pascalorg:mainfrom
sudhir9297:fix/tue-jun-23
Jun 30, 2026
Merged

viewer: Fix Dutch roof trim artifacts#452
wass08 merged 60 commits into
pascalorg:mainfrom
sudhir9297:fix/tue-jun-23

Conversation

@sudhir9297

@sudhir9297 sudhir9297 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes the roof trim workflow around Dutch roof geometry: the trim cutaway now follows the actual roof shell instead of projecting into empty space, Dutch rake/trim materials preserve the roof material during trimming, ridge vent placement and clipping behavior is cleaned up, and roof accessories participate correctly in trim clipping. This branch also includes related roof shape/default updates, floorplan trim helpers, snapping improvements, spline fence work, and Biome cleanup after the main-branch merge.

How to test

  1. Run bun install, then bun dev, and open http://127.0.0.1:3002.
  2. Create or load a Dutch roof, start a trim drag near the gable/rake area, and verify the red trim preview hugs the roof shape without phantom triangles, spikes, or gaps in empty space.
  3. Verify the rake/top roof pieces keep the existing roof material while trimming instead of switching to white.
  4. Add roof accessories/ridge vents, trim through the roof, and verify the trim preview clips the roof/accessories consistently.
  5. Run bun run check-types, bun run check, and bun run build.

Screenshots / screen recording

Visual change; recording will be added separately.

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

Note

High Risk
Large changes to roof mesh generation, trim bounds, and scene node updates (ridge vent regeneration) affect rendering and saved scenes; spline fence schema adds new persisted fields.

Overview
This PR overhauls roof segments for Dutch and complex shapes: new trim fields (including diagonal corners), getRoofSegmentVisibleTopBounds, and a dedicated roof-segment-shape mesh builder with improved Dutch end slopes, gablet rake, and surface-height math. Ridge vents are generated from roof geometry (gable/hip/Dutch/mansard), can auto-refresh when segment dimensions change (but not on trim-only edits), and default vents inherit roof material instead of forced white preset.

Spline fences gain optional path / tangents on the schema, core sampling helpers, handle APIs for control points and tangents, and 3D tangent-line overlay plus floorplan interaction scopes.

The editor shifts floorplan snapping to mode-driven magnetic/grid behavior (removing Shift/Alt bypass in many paths), aligns 2D/3D guide colors, treats pipe-trap as an MEP sub-tool, and hides arc-curve on spline fences. Smaller fixes: integrated spiral stair slab openings no longer add a separate rectangular landing hole; Biome drops an unused nursery rule.

Reviewed by Cursor Bugbot for commit 1d20a74. Bugbot is set up for automated code reviews on this repo. Configure here.

sudhir9297 and others added 30 commits May 19, 2026 02:59
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>
Remove Dutch ridge axis abstraction and rework roof edit system,
ridge vent clipping geometry, and roof surface placement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Track ridge vent auto-generation via an `autoRidgeVent` metadata flag so
geometry changes only regenerate default vents when enabled, treating
legacy segments with generated vents as auto-enabled for back-compat.
Expose a panel toggle to opt in/out per segment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
sudhir9297 and others added 6 commits June 29, 2026 15:42
Child meshes relied on a parent group's layer, which three.js does not
propagate, so the trim section/rail/plane overlays rendered on the scene
layer — getting inked/SSGI-darkened and leaking into thumbnail exports.

Co-Authored-By: Claude <noreply@anthropic.com>
# Conflicts:
#	packages/editor/src/components/editor/floorplan-panel.tsx
#	packages/editor/src/components/tools/roof/roof-tool.tsx
#	packages/editor/src/components/tools/tool-manager.tsx
#	packages/editor/src/components/ui/helpers/helper-manager.tsx
#	packages/editor/src/index.tsx
#	packages/editor/src/store/use-editor.tsx
#	packages/nodes/src/fence/definition.ts
#	packages/nodes/src/wall/tool.tsx
Comment thread packages/core/src/store/actions/node-actions.ts
Comment thread packages/core/src/schema/nodes/roof-segment.ts
Comment thread packages/core/src/schema/nodes/ridge-vent.ts Outdated
Comment thread packages/core/src/schema/nodes/roof-segment-shape.ts Outdated
Comment thread packages/editor/src/components/editor/floorplan-panel.tsx
Comment thread packages/editor/src/components/systems/roof/roof-edit-system.tsx
Comment thread packages/core/src/systems/stair/stair-footprint.ts
@wass08 wass08 self-assigned this Jun 30, 2026

@wass08 wass08 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Architecture review

The PR is overwhelmingly clean on the structural rules — layering, registry composition, schema migration, interaction-scope design, and layer tags all check out. The violations cluster entirely in the new fence-spline reshape feature and one roof-tool snap migration, and they all trace to a single rule: snapping is mode-driven, not a held-modifier bypass.

Reviewed against origin/main...HEAD (163 files). Note: a local main checkout will be misleading here — it's 219 commits behind origin/main, so git diff main...HEAD reports ~1041 files.


🚫 Blockers

B1 — Shift/Alt used as a snap bypass (regresses the mode-driven snapping convention)

Convention (wiki/architecture/interaction-scope.md §"Snapping mode & modifiers", tools.md §"Snapping is mode-driven"): Shift cycles the snap mode, Alt = force/free; snap state must come from isGridSnapActive() / isMagneticSnapActive() / isAngleSnapActive() — never event.shiftKey. Wall, straight-fence, zone, and slab tools are already migrated to this; these new paths regress it. Five sites:

  1. packages/nodes/src/fence/tool.tsx:727 — new SplineFenceDraft:

    if (shiftPressed.current) return local   // Shift = snap bypass

    The StraightFenceTool in the same file documents "'off' is the bypass — there is no Shift hold-to-bypass." Fix: gate grid via getSegmentGridStep() (already returns 0 when grid mode is off), route magnetic/angle through the mode helpers.

  2. packages/nodes/src/fence/move-control-point-tool.tsx:80 & 3. packages/nodes/src/fence/move-tangent-tool.tsx:83:

    const bypass = shiftPressed || event.nativeEvent?.shiftKey === true

    These run inside a reshaping scope, so a snap context resolves — read isGridSnapActive()/isMagneticSnapActive() instead. The sibling fence/actions/move-endpoint.ts already does this correctly ("No Shift bypass").

  3. packages/nodes/src/fence/floorplan-affordances.ts:50-51,101-102 (2D control-point + tangent):

    const x = modifiers.shiftKey ? planPoint[0] : snapScalarToGrid(planPoint[0], snapStep)
  4. packages/editor/src/components/tools/roof/roof-tool.tsx:266-267 — forwards raw flags into the shared snap pipeline:

    shiftKey: event.nativeEvent?.shiftKey === true,
    altKey:   event.nativeEvent?.altKey === true,

    surface-plan-snap.ts:175 treats shiftKey as a full snap bypass and :216 treats altKey as an alignment bypass. This is a regression: the pre-PR roof-tool read these flags zero times (its own comment at :246 still says "this tool never inspects the flags"), relying only on isGridSnapActive()/isMagneticSnapActive().
    Nuance: the underlying pipeline's flag-bypass is tracked legacy debt (shared with the slab/ceiling callers); the clean fix is to stop forwarding the flags here (and ideally migrate the shared pipeline), not extend it onto a previously-clean tool.

B2 — Per-tick useScene.updateNode during drag in the new fence 3D reshape tools

packages/nodes/src/fence/move-control-point-tool.tsx:57 and move-tangent-tool.tsx:57,102 call useScene.getState().updateNode(fenceId, …) inside onGridMove (every pointer tick). Per tools.md §"Data-driven live drag", a data-driven kind (fence re-meshes from path/tangents) must preview via useLiveNodeOverrides and write the scene once on commit — per-tick updateNode swaps the nodes map ref and re-renders every useScene(s => s.nodes) subscriber each frame.

This is sharpened by a 2D↔3D parity gap (now an explicit rule in CLAUDE.md): the 2D sibling floorplan-affordances.ts does this correctly (useLiveNodeOverrides.set per tick → updateNodes once on commit), while the 3D tools don't. Nuance: the existing fence endpoint move also writes per-tick, so fence-as-a-kind isn't yet migrated to overrides — the clean fix is a fence-wide migration, and these new tools should follow the 2D sibling rather than the legacy endpoint path.


💡 Suggestions

S1 — Leftover diagnostic logging in a hot path

packages/nodes/src/wall/tool.tsx:608-609// TEMP DIAGNOSTIC console.log('[wall-snap]', …) fires on every onMove tick while drafting. Self-marked TEMP; please remove before merge. The wall-snap logic itself is correct (uses isMagneticSnapActive() + angle-lock, no modifier bypass).

S2 — Curved fence shouldn't be reached by cycling the continuation mode

SplineFenceDraft is mounted off continuationByContext.fence === 'curved', i.e. "curved" is reached by cycling the same fence continuation/chain state that straight fence uses. Curved-vs-straight is a different drafting interaction (control points + tangent handles + spline reshape scopes vs. straight segment chaining), and overloading the continuation cycle — whose job is "how the next segment chains" — makes the choice non-discoverable and order-dependent. This doesn't have to be a separate tool: keeping it in the same fence tool is fine, but the straight/curved choice should be its own explicit shortcut/toggle, distinct from the continuation/chain cycle.


✅ Verified compliant (no action)

  • packages/core — no Three.js/viewer/editor/nodes imports; all schema additions (roof-segment.trim, Dutch fields, fence.path/tangents) are .default()/.optional(), and ridge-vent.materialPreset is widened stringoptional (old 'preset-white' scenes still parse) — load-safe, no migration gap. No host-kind-shaped capabilities.
  • packages/viewer — viewer isolation clean; the large Dutch-roof / ridge-vent / trim rework delegates all shape math to core helpers and adds no new kind switches or editor vocabulary; roofAccessory use is the documented tech debt. No new untagged overlay primitives.
  • packages/editor — every new overlay primitive (fence-tangent-lines-3d, wall-snap-beacon-layer, alignment-3d-guide-layer, cursor-sphere, roof-edit-system) sets layers={EDITOR_LAYER}; the new control-point/tangent reshapes are added as sub-states of the existing reshaping interaction scope (not new useEditor flags) — exemplary; no whole-Map selector subscriptions added; no @pascal-app/nodes import.

Summary

  • Blockers: 2 classes / 7 sites — B1 Shift/Alt snap-bypass regression (fence spline ×4 + roof-tool ×1), B2 per-tick updateNode in fence 3D reshape tools (×2).
  • Suggestions: 2 — leftover console.log; curved fence selected via its own shortcut/toggle rather than by cycling the continuation mode (can stay in the same fence tool).

Verdict: needs changes. The structural architecture is sound; all blockers are localized to the new fence-spline reshape feature plus the roof-tool snap migration, and all trace to one rule — snapping is mode-driven, not a held-modifier bypass.

🤖 Architecture review via the review-architecture skill.

Comment thread packages/nodes/src/fence/floorplan-move.ts
Comment thread packages/core/src/schema/nodes/ridge-vent.ts Outdated
@sudhir9297 sudhir9297 requested a review from wass08 June 30, 2026 19:27
Comment thread packages/core/src/store/actions/node-actions.ts
Comment thread packages/core/src/schema/nodes/roof-segment.ts
const isOpeningMoveActive = movingOpeningType !== null
const isOpeningPlacementActive = isOpeningBuildActive || isOpeningMoveActive
const isFenceBuildActive = phase === 'structure' && mode === 'build' && tool === 'fence'
const fenceContinuation = useEditor((state) => state.continuationByContext.fence)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unused fence continuation selector

Low Severity

fenceContinuation is subscribed from the editor store but never read in FloorplanPanel, so it triggers extra re-renders without affecting fence build or continuation behavior.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0620d9d. Configure here.

],
} as AnyNode

return nextVents.map((vent) => vent.id as AnyNodeId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Default vent refresh drops materials

Medium Severity

When a roof-segment geometry field triggers default ridge-vent refresh, existing generator vents are deleted and replaced with newly parsed nodes. Paint or material choices on those default vents are not carried over, so a width or pitch tweak can silently reset customized vent appearance.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0620d9d. Configure here.

Comment thread packages/core/src/store/actions/node-actions.ts
…aft start

Remove the leftover TEMP DIAGNOSTIC console.log in the wall tool's onMove
hot path.

Curved fences commit on a closing gesture (double-click / Enter) rather than
per-click, so surface a 'Finish curve' hint in the fence HUD — but only once a
point has been placed and a curve is actually in flight. The draft point count
is published from SplineFenceDraft into a small ephemeral editor store
(useFenceCurveDraft) that the contextual helper reads, mirroring the existing
useSegmentDraftChain pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1d20a74. Configure here.

start,
end,
path: translatePath(snapshot.path, dx, dz),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Linked spline fence endpoints desync

High Severity

When a corner-linked fence is moved on the floorplan, projectLinked shifts the whole path by the drag delta but leaves start/end at the old coordinates whenever that endpoint is not tied to the mover’s corner. Committed nodes then disagree with their spline, breaking rendering, handles, and length math.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1d20a74. Configure here.

const zProgressDenom = Math.max(0.0001, node.depth / 2 - metrics.waistHalfZ)
const xProgress = Math.max(0, Math.abs(localX) - waistHalfX) / xProgressDenom
const zProgress = Math.max(0, Math.abs(localZ) - metrics.waistHalfZ) / zProgressDenom
return node.wallHeight + lowerRise * (1 - Math.min(1, Math.max(xProgress, zProgress)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dutch rake height normal mismatch

Medium Severity

Dutch getRoofSegmentSurfaceY now treats the gablet rake band using upperHalfX / upperHalfZ, but getAnalyticalNormal still classifies the upper slope only out to the inner waist. Accessories that seat with the analytical normal (e.g. box vents) get the wrong tilt on rake even though height follows the updated surface.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1d20a74. Configure here.

@wass08 wass08 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blockers from the earlier review are resolved (mode-driven snapping across the fence spline tools + roof-tool, fence reshape now previews via useLiveNodeOverrides and commits once, curved-fence selected via the T shortcut). Pushed two follow-up cleanups: removed the leftover [wall-snap] diagnostic log and gated the curved-fence "Finish curve" hint on a draft being in progress. ci + quality green. 🟢

@wass08 wass08 merged commit bf25af6 into pascalorg:main Jun 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants