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:
co-authored by
Claude Opus 4.8
parent
86e9b3c8bf
commit
69aa272004
@@ -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}>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -105,10 +105,14 @@ export function publishOpeningGuides3D(args: {
|
||||
|
||||
const out: OpeningGuide3D[] = []
|
||||
|
||||
// Stable `id`s keyed on the guide's semantic role (not list position) so the
|
||||
// 3D layer can keep a persisting slot's element + `<Html>` pill mounted as the
|
||||
// set churns each tick — see `OpeningGuide3D`.
|
||||
if (guides.sillHead) {
|
||||
if (guides.sillHead.sill > MIN_DIMENSION_M) {
|
||||
out.push({
|
||||
kind: 'dimension',
|
||||
id: 'sill',
|
||||
from: toWorld(centerS, 0),
|
||||
to: toWorld(centerS, guides.sillHead.bottomY),
|
||||
value: guides.sillHead.sill,
|
||||
@@ -117,6 +121,7 @@ export function publishOpeningGuides3D(args: {
|
||||
if (guides.sillHead.head > MIN_DIMENSION_M) {
|
||||
out.push({
|
||||
kind: 'dimension',
|
||||
id: 'head',
|
||||
from: toWorld(centerS, guides.sillHead.topY),
|
||||
to: toWorld(centerS, wallHeight),
|
||||
value: guides.sillHead.head,
|
||||
@@ -127,6 +132,7 @@ export function publishOpeningGuides3D(args: {
|
||||
for (const gap of guides.gaps) {
|
||||
out.push({
|
||||
kind: 'dimension',
|
||||
id: `gap:${gap.side}`,
|
||||
from: toWorld(gap.fromS, centerY),
|
||||
to: toWorld(gap.toS, centerY),
|
||||
value: gap.distance,
|
||||
@@ -140,6 +146,7 @@ export function publishOpeningGuides3D(args: {
|
||||
const hi = Math.max(centerS + width / 2, target.centerS + target.width / 2)
|
||||
out.push({
|
||||
kind: 'align-line',
|
||||
id: 'vertical',
|
||||
from: toWorld(lo, guides.vertical.y),
|
||||
to: toWorld(hi, guides.vertical.y),
|
||||
})
|
||||
@@ -147,13 +154,15 @@ export function publishOpeningGuides3D(args: {
|
||||
}
|
||||
|
||||
if (guides.equalSpacing) {
|
||||
for (const seg of guides.equalSpacing.segments) {
|
||||
const { gap, segments } = guides.equalSpacing
|
||||
segments.forEach((seg, i) => {
|
||||
out.push({
|
||||
kind: 'badge',
|
||||
id: `spacing:${i}`,
|
||||
at: toWorld((seg.fromS + seg.toS) / 2, centerY),
|
||||
value: guides.equalSpacing.gap,
|
||||
value: gap,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useOpeningGuides.getState().set(out)
|
||||
|
||||
@@ -702,6 +702,7 @@ const MoveWindowTool: React.FC<{ node: WindowNode }> = ({ node: movingWindowNode
|
||||
boxGeo.dispose()
|
||||
return geo
|
||||
}, [movingWindowNode])
|
||||
useEffect(() => () => edgesGeo.dispose(), [edgesGeo])
|
||||
|
||||
return (
|
||||
<group ref={cursorGroupRef} visible={false}>
|
||||
|
||||
@@ -23,7 +23,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 {
|
||||
@@ -631,10 +631,16 @@ const WindowTool: React.FC = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Cursor geometry: window outline rectangle.
|
||||
const boxGeo = new BoxGeometry(FALLBACK_WIDTH, FALLBACK_HEIGHT, 0.07)
|
||||
const edgesGeo = new EdgesGeometry(boxGeo)
|
||||
boxGeo.dispose()
|
||||
// Cursor geometry: window outline rectangle. 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}>
|
||||
|
||||
Reference in New Issue
Block a user