From 57f9f9f68cbd09a46a64cffd700c3d434850ab52 Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Tue, 4 Aug 2026 13:05:35 -0400 Subject: [PATCH] perf(nodes): reuse wall and fence draft preview geometry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every `grid:move` built a fresh `BoxGeometry`, disposed the previous one, and reassigned it — allocation and GPU buffer churn on the editor's hottest interaction path. Mount one unit `boxGeometry` and scale the mesh instead. The preview is purely visual (nothing raycasts against it), so scaling is equivalent to rebuilding at the target dimensions. Extends the original wall-only change to the fence tool, which carried a byte-identical copy of the same update function. Co-Authored-By: Claude Opus 5 --- packages/nodes/src/fence/tool.tsx | 19 +++---------------- packages/nodes/src/wall/tool.tsx | 29 +++++++++-------------------- 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/packages/nodes/src/fence/tool.tsx b/packages/nodes/src/fence/tool.tsx index af989dc202..6575945282 100644 --- a/packages/nodes/src/fence/tool.tsx +++ b/packages/nodes/src/fence/tool.tsx @@ -50,15 +50,7 @@ import { import { getSceneTheme, useViewer } from '@pascal-app/viewer' import { useThree } from '@react-three/fiber' import { useEffect, useMemo, useRef, useState } from 'react' -import { - BoxGeometry, - BufferGeometry, - type Camera, - DoubleSide, - type Group, - type Mesh, - Vector3, -} from 'three' +import { BufferGeometry, type Camera, DoubleSide, type Group, type Mesh, Vector3 } from 'three' import { DraftAngleArc, type DraftAngleLabel, @@ -430,16 +422,11 @@ function updateFencePreview( } mesh.visible = true direction.normalize() - const geometry = new BoxGeometry(length, previewHeight, previewThickness) const angle = Math.atan2(direction.z, direction.x) mesh.position.set((start.x + end.x) / 2, start.y + previewHeight / 2, (start.z + end.z) / 2) mesh.rotation.y = -angle - - if (mesh.geometry) { - mesh.geometry.dispose() - } - mesh.geometry = geometry + mesh.scale.set(length, previewHeight, previewThickness) } function getCurrentLevelElements(): { walls: WallNode[]; fences: FenceNode[] } { @@ -762,7 +749,7 @@ const StraightFenceTool: React.FC = () => { /> - + ): WallNode[] { @@ -772,13 +767,9 @@ export const WallTool: React.FC = () => { angleLabel: null, }) triggerSFX('sfx:structure-build-start') - // Visibility is owned by `updateWallPreview` — it flips - // `mesh.visible` based on segment length. Setting it here - // (before any geometry data has been written) draws the - // mesh's empty `` placeholder, which WebGPU - // flags as "Vertex buffer slot 0 ... was not set" on the - // first frame after click. Leaving it false until the next - // `onGridMove` writes a real BoxGeometry skips that frame. + // Visibility is owned by `updateWallPreview`. Leave the + // unit box hidden until the first pointer move scales and + // positions it for the active segment. setDraftMeasurement(null) } else if (buildingState.current === 1) { const angleLocked = isAngleSnapActive() @@ -862,11 +853,9 @@ export const WallTool: React.FC = () => { y: draftY, angleLabel: null, }) - // Hide the preview until the next `onGridMove` writes the - // new segment's geometry. Without this the prior segment's - // BoxGeometry stays visible for a frame on top of the - // freshly-committed real wall, producing a brief - // double-paint at the new wall's position. + // Hide the preview until the next `onGridMove` scales and + // repositions it. Otherwise the prior segment stays visible + // for a frame on top of the freshly committed wall. if (wallPreviewRef.current) { wallPreviewRef.current.visible = false } @@ -908,7 +897,7 @@ export const WallTool: React.FC = () => { /> - +