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

* fix(viewer): tune shadow biases to stop acne without detaching shadows

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>

* feat(editor): polygon editor handle UX — focus, cursors, visibility

- 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>

* fix(editor): keep ceiling affordances quiet during any interaction

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>

* fix(core): stop room detection resurrecting deleted slabs on load

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>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-07-16 10:55:05 -04:00
committed by GitHub
co-authored by Claude Fable 5
parent 9ca3eaa7fe
commit 22c9472066
5 changed files with 161 additions and 36 deletions
@@ -23,6 +23,17 @@ const SHADOWS_DISABLED =
.map((s) => s.trim()),
).has('shadows')
// Diagnostic toggle: `?debug=shadowcamera` draws a CameraHelper for each
// shadow camera so the building-fit frustum (and thus shadow texel density)
// can be inspected while tuning margins/bias.
const SHADOW_CAMERA_DEBUG =
typeof window !== 'undefined' &&
new Set(
(new URLSearchParams(window.location.search).get('debug') ?? '')
.split(',')
.map((s) => s.trim()),
).has('shadowcamera')
// Shadow darkness for the bright key lights (themes drive most lights past
// intensity 1). Runs high so shadowed areas actually lose the sun's
// contribution — the ambient/hemisphere/IBL stack provides the fill. The old
@@ -32,9 +43,12 @@ const MAX_SHADOW_INTENSITY = 0.75
// `normalBias` is measured in world units. The previous 0.3 moved shadow
// lookups 30 cm off their surfaces, visibly detaching wall shadows at the
// floor. Keep only a small offset for acne, with a tiny depth bias alongside it.
const SHADOW_DEPTH_BIAS = -0.0001
const SHADOW_NORMAL_BIAS = 0.02
// floor; 0.07 and below still acnes on the building-fit 1024 map (large
// texels), so 0.08 is the smallest acne-free value at that resolution — even
// with the depth bias at -0.0005 (which is itself needed: 0.08 alone still
// showed faint acne at -0.0001).
const SHADOW_DEPTH_BIAS = -0.0005
const SHADOW_NORMAL_BIAS = 0.08
// Shadow frustum framing. The frustum is fit to the BUILDING geometry (not the
// camera): we union the bounds of all registered scene nodes, fit a sphere, and
@@ -77,6 +91,7 @@ export function Lights() {
const boundsBox = useRef(new THREE.Box3()) // scratch: union AABB
const boundsSphere = useRef(new THREE.Sphere()) // scratch: fitted sphere
const lastBoundsTime = useRef(-1) // last refresh timestamp (-1 = never)
const shadowHelpers = useRef<Array<THREE.CameraHelper | null>>([])
const hemiRef = useRef<HemisphereLight>(null)
const ambientRef = useRef<AmbientLight>(null)
@@ -170,6 +185,15 @@ export function Lights() {
cam.near = near
cam.far = far
cam.updateProjectionMatrix()
if (SHADOW_CAMERA_DEBUG) {
let helper = shadowHelpers.current[index]
if (!helper) {
helper = new THREE.CameraHelper(cam)
shadowHelpers.current[index] = helper
state.scene.add(helper)
}
helper.update()
}
}
}
}