Skip to content

Commit 09220bf

Browse files
committed
fix(tessellation): force re-bake only when enabling auto or when caps change while auto is ON; keep manual slider semantics; tests green
1 parent 5921117 commit 09220bf

File tree

3 files changed

+61
-26
lines changed

3 files changed

+61
-26
lines changed

src/App.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -491,15 +491,15 @@ function Demo() {
491491
}, [tessellationSegments])
492492

493493
useEffect(() => {
494-
;(controllerRef.current as any)?.setTessellationAutoEnabled?.(autoTessellation)
494+
controllerRef.current?.setTessellationAutoEnabled?.(autoTessellation)
495495
}, [autoTessellation])
496496

497497
useEffect(() => {
498-
;(controllerRef.current as any)?.setTessellationMinMax?.(tessellationMin, tessellationMax)
498+
controllerRef.current?.setTessellationMinMax?.(tessellationMin, tessellationMax)
499499
}, [tessellationMin, tessellationMax])
500500

501501
useEffect(() => {
502-
;(controllerRef.current as any)?.setWorldSleepGuardEnabled?.(worldSleepGuard)
502+
controllerRef.current?.setWorldSleepGuardEnabled?.(worldSleepGuard)
503503
}, [worldSleepGuard])
504504

505505
useEffect(() => {
@@ -614,7 +614,7 @@ function Demo() {
614614
setSubsteps(1)
615615
setCameraZoom(1)
616616
setPointerColliderVisible(false)
617-
setPinMode("top")
617+
setPinMode("none")
618618
controllerRef.current?.setSleepConfig({ velocityThreshold: 0.001, frameThreshold: 60 })
619619
actionsRef.current?.setSleepConfig(0.001, 60)
620620
}}

src/engine/render/DebugOverlaySystem.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -79,25 +79,38 @@ export class DebugOverlaySystem implements EngineSystem {
7979
if (visible) {
8080
mesh.position.set(this.state.pointer.x, this.state.pointer.y, 0.2)
8181
const r = Math.max(0.0005, Math.min(0.2, this.state.pointerRadius || 0.01))
82-
// Compensate for orthographic anisotropy so the circle renders round on any viewport aspect.
83-
// px/m along X = viewportWidth / worldWidth; px/m along Y = viewportHeight / worldHeight
84-
const cam = this.view.camera
85-
const worldWidth = Math.max(1e-6, (cam.right as number) - (cam.left as number))
86-
const worldHeight = Math.max(1e-6, (cam.top as number) - (cam.bottom as number))
87-
let viewportWidth = 1
88-
let viewportHeight = 1
89-
const anyView = this.view as unknown as { getViewportPixels?: () => { width: number; height: number } }
90-
try {
91-
const vp = anyView.getViewportPixels?.()
92-
if (vp && vp.width && vp.height) {
93-
viewportWidth = vp.width
94-
viewportHeight = vp.height
82+
// Try to compensate for orthographic anisotropy; fall back to uniform if anything is invalid.
83+
const cam = this.view.camera as THREE.Camera & Partial<THREE.OrthographicCamera>
84+
const maybeOrtho = cam as Partial<THREE.OrthographicCamera>
85+
const left = Number((maybeOrtho.left as unknown) ?? NaN)
86+
const right = Number((maybeOrtho.right as unknown) ?? NaN)
87+
const top = Number((maybeOrtho.top as unknown) ?? NaN)
88+
const bottom = Number((maybeOrtho.bottom as unknown) ?? NaN)
89+
const hasOrthoExtents = [left, right, top, bottom].every((v) => Number.isFinite(v))
90+
if (hasOrthoExtents) {
91+
const worldWidth = Math.max(1e-6, right - left)
92+
const worldHeight = Math.max(1e-6, top - bottom)
93+
let viewportWidth = 1
94+
let viewportHeight = 1
95+
const anyView = this.view as unknown as { getViewportPixels?: () => { width: number; height: number } }
96+
try {
97+
const vp = anyView.getViewportPixels?.()
98+
if (vp && Number.isFinite(vp.width) && Number.isFinite(vp.height) && vp.width > 0 && vp.height > 0) {
99+
viewportWidth = vp.width
100+
viewportHeight = vp.height
101+
}
102+
} catch { /* no-op */ }
103+
const pxPerMeterX = viewportWidth / worldWidth
104+
const pxPerMeterY = viewportHeight / worldHeight
105+
const k = pxPerMeterX / pxPerMeterY
106+
if (Number.isFinite(k) && k > 0) {
107+
mesh.scale.set(r, r * k, 1)
108+
} else {
109+
mesh.scale.set(r, r, 1)
95110
}
96-
} catch {}
97-
const pxPerMeterX = viewportWidth / worldWidth
98-
const pxPerMeterY = viewportHeight / worldHeight
99-
const k = pxPerMeterX / pxPerMeterY // scale Y to match X pixels-per-meter
100-
mesh.scale.set(r, r * k, 1)
111+
} else {
112+
mesh.scale.set(r, r, 1)
113+
}
101114
}
102115
// Render other gizmos independently of pointer visibility
103116
this.drawAABBs(!!this.state.drawAABBs)

src/lib/clothSceneController.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -587,10 +587,26 @@ export class ClothSceneController {
587587

588588
// Defensive: recycle static mount before switching the mesh into "cloth" mode to avoid any
589589
// possibility of a duplicate staying behind due to external mounts.
590-
try { this.pool.recycle(element) } catch {}
590+
try {
591+
this.pool.recycle(element)
592+
} catch (e) {
593+
// Non-fatal: log and continue activation path
594+
console.warn('ClothSceneController: failed to recycle static mesh before activation', {
595+
elementId: element.id,
596+
error: e,
597+
})
598+
}
591599
this.pool.resetGeometry(element)
592600
// Re-attach the same mesh explicitly so the scene contains exactly one instance.
593-
try { this.domToWebGL.addMesh(record.mesh) } catch {}
601+
try {
602+
this.domToWebGL.addMesh(record.mesh)
603+
} catch (e) {
604+
// Non-fatal: log and continue; the mesh may already be attached
605+
console.warn('ClothSceneController: failed to add mesh after recycle during activation', {
606+
elementId: element.id,
607+
error: e,
608+
})
609+
}
594610

595611
const cloth = new ClothPhysics(record.mesh, {
596612
damping: 0.985,
@@ -949,11 +965,11 @@ export class ClothSceneController {
949965
}
950966
}
951967

952-
async setTessellationSegments(segments: number) {
968+
async setTessellationSegments(segments: number, force = false) {
953969
const pool = this.pool
954970
if (!pool) return
955971
const clamped = Math.max(1, Math.min(segments, 32))
956-
if (this.debug.tessellationSegments === clamped) return
972+
if (!force && this.debug.tessellationSegments === clamped) return
957973
this.debug.tessellationSegments = clamped
958974

959975
const tasks: Promise<void>[] = []
@@ -982,6 +998,9 @@ export class ClothSceneController {
982998
/** Enables/disables automatic tessellation based on on-screen size. */
983999
setTessellationAutoEnabled(enabled: boolean) {
9841000
this.debug.autoTessellation = !!enabled
1001+
if (this.debug.autoTessellation) {
1002+
void this.setTessellationSegments(this.debug.tessellationSegments, true)
1003+
}
9851004
}
9861005

9871006
/** Sets the min/max caps used by auto tessellation. */
@@ -990,6 +1009,9 @@ export class ClothSceneController {
9901009
const ma = Math.max(mi + 2, Math.min(48, Math.round(max)))
9911010
this.debug.tessellationMin = mi
9921011
this.debug.tessellationMax = ma
1012+
if (this.debug.autoTessellation) {
1013+
void this.setTessellationSegments(this.debug.tessellationSegments, true)
1014+
}
9931015
}
9941016

9951017
// setPointerColliderVisible removed in favour of DebugOverlaySystem

0 commit comments

Comments
 (0)