Skip to content

Adjustments pass: shadow bias, polygon editor UX, space-detection load fix#502

Merged
wass08 merged 4 commits into
mainfrom
fix/adjustments-pass-july16
Jul 16, 2026
Merged

Adjustments pass: shadow bias, polygon editor UX, space-detection load fix#502
wass08 merged 4 commits into
mainfrom
fix/adjustments-pass-july16

Conversation

@wass08

@wass08 wass08 commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

A pass of fixes and adjustments across four areas:

  • Shadow acne vs offset (viewer): the previous normalBias drop to 0.02 reintroduced self-shadowing acne on the building-fit 1024 shadow map. Settle on normalBias 0.08 + depth bias -0.0005 — empirically the smallest acne-free combo that keeps wall shadows visually attached (0.07 acnes, 0.1 reads detached). Also adds a ?debug=shadowcamera CameraHelper diagnostic for inspecting the fitted shadow frustum.
  • Polygon editor handle UX (editor): vertex/midpoint handles ignore scene depth like the edge arrows so walls can't hide them; slightly smaller (vertex 0.1→0.08, midpoint 0.06→0.05); midpoints use the brighter arrow shade at rest. During any drag only the active handle stays mounted so the gesture reads clearly. Handles set pointer cursors — move for vertices/midpoints/cross, direction-aware resize cursors for the edge arrows (outward normal projected to screen space).
  • Ceiling affordances during interactions (editor): the ceiling corner brackets caught drag-time hover and flashed the ceiling grid mid-gesture (e.g. while dragging a slab polygon vertex). They now unmount while ANY interaction scope is active, and CeilingSystem's hover-driven grid reveal is gated on idle scope + no inputDragging.
  • Room detection resurrecting deleted slabs (core): two holes in the wall-driven auto slab sync. (1) initSpaceDetectionSync started with an empty baseline, so scene hydration read as "every wall changed" and ran detection on every load, recreating deleted auto slabs. The baseline now seeds from the store and a level's first snapshot is baseline — detection only reacts to in-session wall edits. (2) matchesManualFootprint required mutual coverage, so one manual slab spanning multiple rooms never suppressed those rooms' auto slabs; suppression now asks only whether the room is substantially covered by the union of manual slabs.

How to test

  1. bun dev, open a scene with walls and slabs.
  2. Shadows: orbit around sunlit walls — no acne stripes, and wall shadows stay attached at the floor (no "hole in the wall" gap). Append ?debug=shadowcamera to see the fitted shadow frustum.
  3. Polygon editor: select a slab. Vertex/midpoint handles stay visible through walls; midpoints are the brighter shade. Hover a vertex/midpoint → move cursor; hover an edge arrow → resize cursor matching its screen direction. Drag an edge arrow or a point → every other handle disappears until release.
  4. Ceiling hover: while dragging a slab point/edge, sweep the cursor across a ceiling with corner brackets — the ceiling grid must not flash.
  5. Slab resurrection: delete an auto-created room slab, reload — it stays deleted. Draw a big manual slab covering several rooms, delete those rooms' auto slabs, move a wall endpoint — no auto slabs reappear under the covering slab.
  6. cd packages/core && bun test — 940 tests pass.

Screenshots / screen recording

N/A — will add a recording if reviewers want one; behaviors are quick to reproduce with the steps above.

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

Medium Risk
Space-detection baseline and footprint logic affect auto slab/ceiling lifecycle on every wall edit and scene load; other changes are mostly editor/viewer UX and rendering tuning.

Overview
Fixes wall-driven auto slab sync so deleted auto slabs stay gone after reload and manual coverage suppresses replacement correctly. initSpaceDetectionSync seeds wall snapshots from the loaded store and treats a level’s first snapshot as hydration-only, so detection runs on in-session wall edits, not on setScene. matchesManualFootprint drops the per-manual-slab “mostly inside room” filter and only checks whether the room is substantially covered by all manual polygons, fixing one slab spanning multiple rooms.

Ceiling UI stops flashing mid-gesture: corner brackets unmount whenever interaction scope is not idle, and CeilingSystem only treats ceiling hover as grid-reveal when scope is idle and inputDragging is false.

Polygon editor polish: vertex/midpoint handles render through geometry (depthTest: false), slightly smaller handles, brighter resting midpoints, only the active handle during drags, and document.body cursors (move / screen-aligned resize on edge arrows).

Viewer shadows: tune normalBias / depth bias for acne vs attachment on the building-fit 1024 map; add ?debug=shadowcamera to visualize shadow frusta.

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

wass08 and others added 4 commits July 16, 2026 10:48
normalBias 0.02 was too small a texel offset for the building-fit 1024
shadow map and brought back self-shadowing acne. Settle on normalBias 0.08
(0.07 and below acnes, 0.1 reads detached) plus depth bias -0.0005 to
suppress the residual acne that a normal bias alone couldn't clear.

Also adds a `?debug=shadowcamera` diagnostic that draws a CameraHelper for
each shadow camera so the fitted frustum can be inspected while tuning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- vertex/midpoint cylinders ignore scene depth like the edge arrows so
  they stay visible through walls and slabs; vertex radius 0.1 -> 0.08,
  midpoint 0.06 -> 0.05, midpoints use the brighter arrow shade at rest
- during any drag only the active handle stays mounted (other arrows,
  vertices, edge bars and the cross disappear) so the gesture reads clearly
- handles set pointer cursors: move for vertices/midpoints/cross, and a
  screen-space direction-aware resize cursor for the edge arrows

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ceiling corner brackets' hit boxes caught drag-time hover (spatial
events keep firing during host drags by design), set hoveredId to the
ceiling and flashed the ceiling grid mid-gesture — e.g. while dragging a
slab polygon vertex. Unmount the brackets while ANY interaction scope is
active, and gate CeilingSystem's hover-driven grid reveal on idle scope +
no inputDragging as a second layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two holes in the wall-driven auto slab sync:

- initSpaceDetectionSync started with an empty baseline, so scene
  hydration (one atomic setScene) read as 'every wall changed' and ran a
  full detection pass on every load, recreating auto slabs the user had
  deleted. Seed the baseline from the store at init and treat a level's
  first snapshot as baseline — detection now only reacts to in-session
  wall edits.

- matchesManualFootprint required mutual coverage, so a single manual
  slab spanning multiple rooms never suppressed those rooms' auto slabs
  (only a fraction of it lies inside each room). Suppression now only
  asks whether the ROOM is substantially covered by the union of manual
  slabs.

Co-Authored-By: Claude Fable 5 <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 4 potential issues.

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 c229c93. Configure here.

// first wall can't close a room anyway. Record it (below) and only
// react to subsequent changes.
const previous = previousSnapshots.get(levelId)
if (previous === undefined) continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Load skips room space sync

High Severity

The initSpaceDetectionSync logic, intended to prevent resurrecting deleted auto slabs, incorrectly skips runSpaceDetection for a level's initial appearance. This prevents spaces from being populated on scene load (affecting features like room painting) and leaves rooms without auto slabs/ceilings when walls are pasted or batch-created onto a new or empty level.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c229c93. Configure here.

onDragCommitRef.current?.()
updatePreviewPolygon(null)
setDragState(null)
}, [onPolygonChange, updatePreviewPolygon])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stuck cursor after drag cancel

Medium Severity

The document.body cursor can remain stuck (e.g., move or resize) if a drag operation is aborted by an external polygon prop update. This occurs because the dragState is nulled without explicitly calling clearBodyCursor in this scenario, unlike on drag commit or component unmount.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c229c93. Configure here.

state.scene.add(helper)
}
helper.update()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Shadow debug helpers never removed

Low Severity

When ?debug=shadowcamera is active, CameraHelper instances are added to the scene but never removed or disposed. This causes them to accumulate in the scene graph, potentially leaking resources, especially when the Lights component unmounts or debug mode changes.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c229c93. Configure here.

onPointerLeave: (e) => {
e.stopPropagation()
setHoveredMidpoint(null)
clearBodyCursor()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Midpoint drag clears move cursor

Medium Severity

Midpoint onPointerLeave always clears the body cursor, and midpoints unmount as soon as any dragState exists. Starting a midpoint insert therefore tears down the hovered handle, fires leave, and drops the move cursor for the rest of the gesture.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c229c93. Configure here.

@wass08
wass08 merged commit 22c9472 into main Jul 16, 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.

1 participant