perf(editor): stabilize 3D opening-guide rendering, remove per-tick GPU churn

Reuse one THREE.Line + preallocated position buffer per guide slot, mutating
endpoints in place each drag tick instead of rebuilding the geometry, line, and
two Vector3s and re-uploading the GPU buffer every frame. Key guides by a stable
semantic id (sill / head / gap:side / vertical / spacing:i) so a slot that
persists keeps its React element and drei <Html> pill mounted as the guide set
churns, rather than remounting under shifting index keys.

Also: make useOpeningGuides.clear() a no-op when already empty so the common
no-guide hover frame doesn't push a fresh [] and re-render to the same nothing;
dispose the move-tool cursor EdgesGeometry on unmount; and memoize the
placement-tool cursor EdgesGeometry (static fallback dims) so it isn't
reallocated and orphaned on every render during placement.

Reviewed by Codex (peer + adversarial): no correctness, hook-order, or GPU-leak
regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aymeric Rabot
2026-06-15 12:36:06 -04:00
co-authored by Claude Opus 4.8
parent 86e9b3c8bf
commit 69aa272004
7 changed files with 77 additions and 32 deletions
+1
View File
@@ -644,6 +644,7 @@ const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNode }) =>
boxGeo.dispose()
return geo
}, [movingDoorNode])
useEffect(() => () => edgesGeo.dispose(), [edgesGeo])
return (
<group ref={cursorGroupRef} visible={false}>
+11 -5
View File
@@ -22,7 +22,7 @@ import {
useAlignmentGuides,
} from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { useEffect, useRef } from 'react'
import { useEffect, useMemo, useRef } from 'react'
import { BoxGeometry, EdgesGeometry, type Group, type LineSegments, Vector3 } from 'three'
import { LineBasicNodeMaterial } from 'three/webgpu'
import {
@@ -585,10 +585,16 @@ const DoorTool: React.FC = () => {
}
}, [])
// Cursor geometry: door outline.
const boxGeo = new BoxGeometry(FALLBACK_WIDTH, FALLBACK_HEIGHT, 0.07)
const edgesGeo = new EdgesGeometry(boxGeo)
boxGeo.dispose()
// Cursor geometry: door outline. Static dims, so build it once and dispose on
// unmount rather than reallocating (and orphaning) an EdgesGeometry on every
// re-render during placement.
const edgesGeo = useMemo(() => {
const boxGeo = new BoxGeometry(FALLBACK_WIDTH, FALLBACK_HEIGHT, 0.07)
const geo = new EdgesGeometry(boxGeo)
boxGeo.dispose()
return geo
}, [])
useEffect(() => () => edgesGeo.dispose(), [edgesGeo])
return (
<group ref={cursorGroupRef} visible={false}>