From d92296754f5013b41234fda0ab315287a85073cf Mon Sep 17 00:00:00 2001 From: Avatarsia Date: Mon, 6 Apr 2026 02:38:30 +0200 Subject: [PATCH] perf: on-demand rendering, dispose leaks, reduce GC pressure - Render loop now only calls renderer.render() when the scene actually changed (needsRender flag + requestRender export). Idle GPU usage drops to near zero. - Disabled shadow map (no receiver in scene, wasted a full render pass). - Reuse overlay materials instead of creating new ones every paint frame. - Dispose CanvasTexture in getEffectiveMapEntry (VRAM leak on every slider change). - Dispose axes/dimension geometry on model reload. - Reuse Vector3/Quaternion temp objects in pointer/touch/wheel handlers instead of allocating ~10 objects per mouse event. - RAF-batch mousemove for hover/cursor, keep paint events immediate. - Reuse faceMask buffer attribute when size matches. - Cache getEffectiveMapEntry result (skip canvas tiling+blur when texture and smoothing haven't changed). - addSmoothNormals: same dedup+flat-array approach as displacement. --- js/main.js | 168 +++++++++++++++++++++++++++++++++------------------ js/viewer.js | 142 ++++++++++++++++++++++++++++--------------- 2 files changed, 203 insertions(+), 107 deletions(-) diff --git a/js/main.js b/js/main.js index 93fc802..44ff2b3 100644 --- a/js/main.js +++ b/js/main.js @@ -1,7 +1,8 @@ import * as THREE from 'three'; import { initViewer, loadGeometry, setMeshMaterial, setMeshGeometry, setWireframe, getControls, getCamera, getCurrentMesh, - setExclusionOverlay, setHoverPreview, setViewerTheme } from './viewer.js'; + setExclusionOverlay, setHoverPreview, setViewerTheme, + requestRender } from './viewer.js'; import { loadModelFile, computeBounds, getTriangleCount } from './stlLoader.js'; import { loadPresets, loadCustomTexture } from './presetTextures.js'; import { createPreviewMaterial, updateMaterial } from './previewMaterial.js'; @@ -25,7 +26,7 @@ let previewDebounce = null; // ── Exclusion state ─────────────────────────────────────────────────────────── let excludedFaces = new Set(); // triangle indices in currentGeometry -let triangleAdjacency = null; // Map from buildAdjacency +let triangleAdjacency = null; // Array from buildAdjacency let triangleCentroids = null; // Float32Array from buildAdjacency let triangleBoundRadii = null; // Float32Array — max vertex-to-centroid dist per tri let exclusionTool = null; // 'brush' | 'bucket' | null @@ -38,6 +39,9 @@ let selectionMode = false; // false = exclude painted faces; true = i let _lastHoverTriIdx = -1; // last triangle index used for hover preview let placeOnFaceActive = false; // true while "Place on Face" mode is active const _raycaster = new THREE.Raycaster(); +let _lastEffectiveTexture = null; +let _effectiveMapCache = null; +let _effectiveMapCacheKey = null; const settings = { mappingMode: 5, // Triplanar default @@ -139,7 +143,7 @@ let precisionEdgeLength = null; // edge length used for current refinement let precisionBusy = false; // true while async subdivision is running let precisionCentroids = null; // Float32Array from buildAdjacency on refined mesh let precisionBoundRadii = null; // Float32Array — max vertex-to-centroid per refined tri -let precisionAdjacency = null; // Map from buildAdjacency on refined mesh +let precisionAdjacency = null; // Array from buildAdjacency on refined mesh let precisionExcludedFaces = new Set(); // precision face indices excluded while precision is active // ── Displacement preview state ──────────────────────────────────────────────── @@ -648,23 +652,41 @@ function wireEvents() { } }); + // RAF-Batching: paint events fire immediately, hover/cursor batched per frame + let _pendingHoverEvent = null; + let _hoverRafId = 0; + canvas.addEventListener('mousemove', (e) => { - if (placeOnFaceActive && currentGeometry) { - updatePlaceOnFaceHover(e); - return; - } - if (exclusionTool === 'brush' && brushIsRadius) { - updateBrushCursor(e); - } + // Paint-Events sofort verarbeiten (jeder Event zaehlt fuer lueckenloses Malen) if (isPainting && exclusionTool === 'brush') { paintAt(e); + // Cursor-Update kann warten + _pendingHoverEvent = e; + if (!_hoverRafId) { + _hoverRafId = requestAnimationFrame(() => { + _hoverRafId = 0; + if (_pendingHoverEvent) updateBrushCursor(_pendingHoverEvent); + _pendingHoverEvent = null; + }); + } return; } - if (!isPainting && exclusionTool === 'brush' && currentGeometry) { - updateBrushHover(e); - } - if (!isPainting && exclusionTool === 'bucket' && currentGeometry) { - updateBucketHover(e); + // Alle anderen Hover-Pfade: RAF-Batching OK + _pendingHoverEvent = e; + if (!_hoverRafId) { + _hoverRafId = requestAnimationFrame(() => { + _hoverRafId = 0; + const ev = _pendingHoverEvent; + if (!ev) return; + _pendingHoverEvent = null; + if (placeOnFaceActive && currentGeometry) { updatePlaceOnFaceHover(ev); return; } + if (exclusionTool === 'brush') { + updateBrushCursor(ev); + if (brushIsRadius && !isPainting && currentGeometry) updateBrushHover(ev); + } else if (exclusionTool === 'bucket' && !isPainting && currentGeometry) { + updateBucketHover(ev); + } + }); } }); @@ -748,12 +770,14 @@ function setExclusionTool(tool) { } } +const _ndcResult = new THREE.Vector2(); function _canvasNDC(e) { const rect = canvas.getBoundingClientRect(); - return new THREE.Vector2( + _ndcResult.set( ((e.clientX - rect.left) / rect.width) * 2 - 1, ((e.clientY - rect.top) / rect.height) * -2 + 1, ); + return _ndcResult; } // The preview material uses THREE.DoubleSide, so the raycaster can return @@ -1437,7 +1461,11 @@ function updateFaceMask(geometry) { if (!geometry) return; const posCount = geometry.attributes.position.count; const triCount = posCount / 3; - const maskArr = new Float32Array(posCount); + + // Reuse existing buffer if length matches exactly, otherwise allocate new + const existing = geometry.getAttribute('faceMask'); + const reuseBuffer = existing && existing.array.length === posCount; + const maskArr = reuseBuffer ? existing.array : new Float32Array(posCount); // Determine which face set to check const isPrecision = (geometry === precisionGeometry && precisionMaskingEnabled); @@ -1461,7 +1489,11 @@ function updateFaceMask(geometry) { } } - geometry.setAttribute('faceMask', new THREE.Float32BufferAttribute(maskArr, 1)); + if (reuseBuffer) { + existing.needsUpdate = true; + } else { + geometry.setAttribute('faceMask', new THREE.Float32BufferAttribute(maskArr, 1)); + } // Ensure faceNormal attribute exists (needed by shader for angle masking). // For the original geometry normal == faceNormal; for subdivided geometry @@ -1470,6 +1502,7 @@ function updateFaceMask(geometry) { if (!geometry.attributes.faceNormal) { addFaceNormals(geometry); } + requestRender(); } /** @@ -1602,8 +1635,16 @@ function buildParentFaceMap(subdivGeo) { } function getEffectiveMapEntry() { - if (!activeMapEntry || settings.textureSmoothing === 0) return activeMapEntry; - const { fullCanvas, width, height } = activeMapEntry; + if (!activeMapEntry || settings.textureSmoothing === 0) { + _effectiveMapCache = null; + _effectiveMapCacheKey = null; + return activeMapEntry; + } + const { fullCanvas, width, height, name } = activeMapEntry; + const cacheKey = `${name}_${width}_${height}_${settings.textureSmoothing}`; + if (_effectiveMapCacheKey === cacheKey && _effectiveMapCache) { + return _effectiveMapCache; + } // Tile the source 3×3 before blurring so edge pixels have correct // neighbours and the blurred centre tile is seamlessly tileable. const tiled = document.createElement('canvas'); @@ -1628,7 +1669,11 @@ function getEffectiveMapEntry() { const imageData = offscreen.getContext('2d').getImageData(0, 0, width, height); const texture = new THREE.CanvasTexture(offscreen); texture.wrapS = texture.wrapT = THREE.RepeatWrapping; - return { ...activeMapEntry, imageData, texture }; + if (_lastEffectiveTexture) _lastEffectiveTexture.dispose(); + _lastEffectiveTexture = texture; + _effectiveMapCache = { ...activeMapEntry, imageData, texture }; + _effectiveMapCacheKey = cacheKey; + return _effectiveMapCache; } function updatePreview() { @@ -1717,19 +1762,28 @@ function addFaceNormals(geometry) { function addSmoothNormals(geometry) { const pos = geometry.attributes.position.array; const count = geometry.attributes.position.count; + const nrm = geometry.attributes.normal.array; + // Vertex-dedup pass: assign a numeric ID to each unique quantised position. const QUANT = 1e4; - const key = (x, y, z) => - `${Math.round(x * QUANT)}_${Math.round(y * QUANT)}_${Math.round(z * QUANT)}`; + const dedupMap = new Map(); + let nextId = 0; + const vertId = new Uint32Array(count); + for (let i = 0; i < count; i++) { + const key = `${Math.round(pos[i*3]*QUANT)}_${Math.round(pos[i*3+1]*QUANT)}_${Math.round(pos[i*3+2]*QUANT)}`; + let id = dedupMap.get(key); + if (id === undefined) { id = nextId++; dedupMap.set(key, id); } + vertId[i] = id; + } - // Accumulate area-weighted buffer normals per unique position. + // Accumulate area-weighted buffer normals per unique position into flat arrays. // The subdivision pipeline splits indexed vertices at sharp dihedral edges - // (>30°) so the interpolated buffer normals are smooth across soft edges + // (>30 deg) so the interpolated buffer normals are smooth across soft edges // (cylinder, sphere) but sharp across hard edges (cube). Using these buffer // normals instead of geometric face normals eliminates visible faceting steps // on round surfaces while still preserving hard edges. - const nrmMap = new Map(); - const nrm = geometry.attributes.normal.array; + const uc = nextId; + const snx = new Float64Array(uc), sny = new Float64Array(uc), snz = new Float64Array(uc); const vA = new THREE.Vector3(), vB = new THREE.Vector3(), vC = new THREE.Vector3(); const e1 = new THREE.Vector3(), e2 = new THREE.Vector3(), fn = new THREE.Vector3(); @@ -1744,32 +1798,24 @@ function addSmoothNormals(geometry) { if (area < 1e-12) continue; for (let v = 0; v < 3; v++) { const vi = i + v; - const nx = nrm[vi * 3], ny = nrm[vi * 3 + 1], nz = nrm[vi * 3 + 2]; - const k = key(pos[vi * 3], pos[vi * 3 + 1], pos[vi * 3 + 2]); - const prev = nrmMap.get(k); - if (prev) { - prev[0] += nx * area; - prev[1] += ny * area; - prev[2] += nz * area; - } else { - nrmMap.set(k, [nx * area, ny * area, nz * area]); - } + const id = vertId[vi]; + snx[id] += nrm[vi * 3] * area; + sny[id] += nrm[vi * 3 + 1] * area; + snz[id] += nrm[vi * 3 + 2] * area; } } // Normalize accumulated normals - for (const n of nrmMap.values()) { - const len = Math.sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]); - if (len > 1e-12) { n[0] /= len; n[1] /= len; n[2] /= len; } + for (let id = 0; id < uc; id++) { + const len = Math.sqrt(snx[id] * snx[id] + sny[id] * sny[id] + snz[id] * snz[id]) || 1; + snx[id] /= len; sny[id] /= len; snz[id] /= len; } - // Write smoothNormal attribute + // Write smoothNormal attribute via vertId lookup const sn = new Float32Array(count * 3); for (let i = 0; i < count; i++) { - const k = key(pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]); - const n = nrmMap.get(k); - if (n) { sn[i * 3] = n[0]; sn[i * 3 + 1] = n[1]; sn[i * 3 + 2] = n[2]; } - else { sn[i * 3] = 0; sn[i * 3 + 1] = 0; sn[i * 3 + 2] = 1; } + const id = vertId[i]; + sn[i * 3] = snx[id]; sn[i * 3 + 1] = sny[id]; sn[i * 3 + 2] = snz[id]; } geometry.setAttribute('smoothNormal', new THREE.Float32BufferAttribute(sn, 3)); } @@ -2193,26 +2239,30 @@ async function handleExport() { // Flat-bottom clamp: when bottom faces are masked (bottomAngleLimit > 0), // any vertex that ended up below the original model's bottom layer gets - // snapped back up to that Z. Only the Z-value is changed. + // snapped back up to that Z. Single pass with selective normal recomputation. if (settings.bottomAngleLimit > 0) { const bottomZ = currentBounds.min.z; - const posArr = finalGeometry.attributes.position.array; - for (let i = 2; i < posArr.length; i += 3) { - if (posArr[i] < bottomZ) posArr[i] = bottomZ; - } - finalGeometry.attributes.position.needsUpdate = true; - // Recompute normals via cross product so they always match winding order. const pa = finalGeometry.attributes.position.array; const na = finalGeometry.attributes.normal ? finalGeometry.attributes.normal.array : new Float32Array(pa.length); + for (let i = 0; i < pa.length; i += 9) { - const ux = pa[i+3]-pa[i], uy = pa[i+4]-pa[i+1], uz = pa[i+5]-pa[i+2]; - const vx = pa[i+6]-pa[i], vy = pa[i+7]-pa[i+1], vz = pa[i+8]-pa[i+2]; - const nx = uy*vz-uz*vy, ny = uz*vx-ux*vz, nz = ux*vy-uy*vx; - const len = Math.sqrt(nx*nx+ny*ny+nz*nz) || 1; - na[i] = na[i+3] = na[i+6] = nx/len; - na[i+1] = na[i+4] = na[i+7] = ny/len; - na[i+2] = na[i+5] = na[i+8] = nz/len; + let dirty = false; + if (pa[i+2] < bottomZ) { pa[i+2] = bottomZ; dirty = true; } + if (pa[i+5] < bottomZ) { pa[i+5] = bottomZ; dirty = true; } + if (pa[i+8] < bottomZ) { pa[i+8] = bottomZ; dirty = true; } + + if (dirty) { + const ux = pa[i+3]-pa[i], uy = pa[i+4]-pa[i+1], uz = pa[i+5]-pa[i+2]; + const vx = pa[i+6]-pa[i], vy = pa[i+7]-pa[i+1], vz = pa[i+8]-pa[i+2]; + const nx = uy*vz-uz*vy, ny = uz*vx-ux*vz, nz = ux*vy-uy*vx; + const len = Math.sqrt(nx*nx+ny*ny+nz*nz) || 1; + na[i] = na[i+3] = na[i+6] = nx/len; + na[i+1] = na[i+4] = na[i+7] = ny/len; + na[i+2] = na[i+5] = na[i+8] = nz/len; + } } + + finalGeometry.attributes.position.needsUpdate = true; if (!finalGeometry.attributes.normal) finalGeometry.setAttribute('normal', new THREE.Float32BufferAttribute(na, 3)); else finalGeometry.attributes.normal.needsUpdate = true; } diff --git a/js/viewer.js b/js/viewer.js index 646b66a..6a6fc34 100644 --- a/js/viewer.js +++ b/js/viewer.js @@ -4,6 +4,14 @@ import { LineSegments2 } from 'three/addons/lines/LineSegments2.js'; import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js'; import { LineMaterial } from 'three/addons/lines/LineMaterial.js'; +// Pre-allocated temp objects for hot-path event handlers (avoid GC pressure) +const _tmpQ1 = new THREE.Quaternion(); +const _tmpQ2 = new THREE.Quaternion(); +const _tmpV1 = new THREE.Vector3(); +const _tmpV2 = new THREE.Vector3(); +const _tmpV3 = new THREE.Vector3(); +const _tmpV4 = new THREE.Vector3(); + let renderer, camera, scene, controls, meshGroup, ambientLight, dirLight1, dirLight2, grid; let currentMesh = null; let axesGroup = null; @@ -12,6 +20,9 @@ let wireframeLines = null; // LineSegments overlay, or null when hidden let wireframeVisible = false; let exclusionMesh = null; // flat orange overlay for user-excluded faces let hoverMesh = null; // semi-transparent yellow bucket-fill preview +let _exclMaterial = null; +let _hoverMaterial = null; +let _needsRender = true; // Build a labelled coordinate axes indicator scaled to `size`. // X = red, Y = green, Z = blue (up). @@ -141,8 +152,7 @@ export function initViewer(canvas) { renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.1; - renderer.shadowMap.enabled = true; - renderer.shadowMap.type = THREE.PCFSoftShadowMap; + renderer.shadowMap.enabled = false; // Scene scene = new THREE.Scene(); @@ -166,8 +176,7 @@ export function initViewer(canvas) { dirLight1 = new THREE.DirectionalLight(0xffffff, 1.2); dirLight1.position.set(80, 120, 60); - dirLight1.castShadow = true; - dirLight1.shadow.mapSize.set(1024, 1024); + dirLight1.castShadow = false; scene.add(dirLight1); dirLight2 = new THREE.DirectionalLight(0x8899ff, 0.4); @@ -229,6 +238,7 @@ export function initViewer(canvas) { const markerScale = (camera.top / camera.zoom) * 0.015; _pivotMarker.scale.setScalar(markerScale); _pivotMarker.visible = true; + _needsRender = true; }); document.addEventListener('pointermove', (e) => { @@ -240,24 +250,24 @@ export function initViewer(canvas) { const rotSpeed = 0.005; // Horizontal: rotate around world Z (up) - const qH = new THREE.Quaternion().setFromAxisAngle( - new THREE.Vector3(0, 0, 1), -dx * rotSpeed); + _tmpQ1.setFromAxisAngle(_tmpV1.set(0, 0, 1), -dx * rotSpeed); // Vertical: rotate around camera's local X (right vector) - const right = new THREE.Vector3().setFromMatrixColumn(camera.matrixWorld, 0).normalize(); - const qV = new THREE.Quaternion().setFromAxisAngle(right, -dy * rotSpeed); - const qTotal = new THREE.Quaternion().multiplyQuaternions(qV, qH); + _tmpV2.setFromMatrixColumn(camera.matrixWorld, 0).normalize(); + _tmpQ2.setFromAxisAngle(_tmpV2, -dy * rotSpeed); + _tmpQ1.premultiply(_tmpQ2); // _tmpQ1 = qV * qH (total rotation) // Rotate camera position around the pivot - const camOff = camera.position.clone().sub(_customPivot); - camOff.applyQuaternion(qTotal); - camera.position.copy(_customPivot).add(camOff); + _tmpV3.copy(camera.position).sub(_customPivot); + _tmpV3.applyQuaternion(_tmpQ1); + camera.position.copy(_customPivot).add(_tmpV3); // Rotate orbit target around the same pivot so OrbitControls stays in sync - const tgtOff = controls.target.clone().sub(_customPivot); - tgtOff.applyQuaternion(qTotal); - controls.target.copy(_customPivot).add(tgtOff); + _tmpV4.copy(controls.target).sub(_customPivot); + _tmpV4.applyQuaternion(_tmpQ1); + controls.target.copy(_customPivot).add(_tmpV4); camera.lookAt(controls.target); + _needsRender = true; }); document.addEventListener('pointerup', () => { @@ -266,6 +276,7 @@ export function initViewer(canvas) { _lastPointer = null; controls.enableRotate = true; _pivotMarker.visible = false; + _needsRender = true; } }); @@ -300,26 +311,27 @@ export function initViewer(canvas) { const curNdcX = ((midX - rect.left) / rect.width) * 2 - 1; const curNdcY = -((midY - rect.top) / rect.height) * 2 + 1; - const prevWorld = new THREE.Vector3(prevNdcX, prevNdcY, 0).unproject(camera); - const curWorld = new THREE.Vector3(curNdcX, curNdcY, 0).unproject(camera); - const panDelta = prevWorld.sub(curWorld); - camera.position.add(panDelta); - controls.target.add(panDelta); + _tmpV1.set(prevNdcX, prevNdcY, 0).unproject(camera); + _tmpV2.set(curNdcX, curNdcY, 0).unproject(camera); + _tmpV1.sub(_tmpV2); // panDelta + camera.position.add(_tmpV1); + controls.target.add(_tmpV1); // ── Zoom: zoom toward the current midpoint ──────────────────────── const factor = newDist / _pinchDist; - const before = new THREE.Vector3(curNdcX, curNdcY, 0).unproject(camera); + _tmpV3.set(curNdcX, curNdcY, 0).unproject(camera); camera.zoom = Math.max(0.05, Math.min(200, camera.zoom * factor)); camera.updateProjectionMatrix(); - const after = new THREE.Vector3(curNdcX, curNdcY, 0).unproject(camera); + _tmpV4.set(curNdcX, curNdcY, 0).unproject(camera); - const zoomDelta = before.clone().sub(after); - camera.position.add(zoomDelta); - controls.target.add(zoomDelta); + _tmpV3.sub(_tmpV4); // zoomDelta + camera.position.add(_tmpV3); + controls.target.add(_tmpV3); _pinchDist = newDist; _pinchMid = { x: midX, y: midY }; controls.update(); + _needsRender = true; }, { passive: false }); renderer.domElement.addEventListener('touchend', (e) => { @@ -338,7 +350,7 @@ export function initViewer(canvas) { const ndcY = -((e.clientY - rect.top) / rect.height) * 2 + 1; // World position under cursor before zoom - const before = new THREE.Vector3(ndcX, ndcY, 0).unproject(camera); + _tmpV1.set(ndcX, ndcY, 0).unproject(camera); // Apply zoom const factor = e.deltaY > 0 ? 1 / 1.1 : 1.1; @@ -346,12 +358,12 @@ export function initViewer(canvas) { camera.updateProjectionMatrix(); // World position under cursor after zoom - const after = new THREE.Vector3(ndcX, ndcY, 0).unproject(camera); + _tmpV2.set(ndcX, ndcY, 0).unproject(camera); // Shift camera + target so the world point stays under the cursor - const delta = before.clone().sub(after); - camera.position.add(delta); - controls.target.add(delta); + _tmpV1.sub(_tmpV2); // delta = before - after + camera.position.add(_tmpV1); + controls.target.add(_tmpV1); controls.update(); }, { passive: false }); @@ -360,12 +372,17 @@ export function initViewer(canvas) { resizeObserver.observe(canvas.parentElement); onResize(); + // Damping needs controls.update() every frame; re-render only when needed + controls.addEventListener('change', () => { _needsRender = true; }); + // Render loop (function animate() { requestAnimationFrame(animate); controls.update(); - - renderer.render(scene, camera); + if (_needsRender) { + _needsRender = false; + renderer.render(scene, camera); + } })(); } @@ -387,6 +404,21 @@ function onResize() { h * renderer.getPixelRatio(), ); } + requestRender(); +} + +function disposeGroup(group) { + group.traverse(obj => { + if (obj.geometry) obj.geometry.dispose(); + if (obj.material) { + if (Array.isArray(obj.material)) { + obj.material.forEach(m => { if (m.map) m.map.dispose(); m.dispose(); }); + } else { + if (obj.material.map) obj.material.map.dispose(); + obj.material.dispose(); + } + } + }); } /** @@ -435,7 +467,7 @@ export function loadGeometry(geometry, material) { fitCamera(sphere); // Place coordinate axes away from the part corner - if (axesGroup) scene.remove(axesGroup); + if (axesGroup) { disposeGroup(axesGroup); scene.remove(axesGroup); } const axisSize = sphere.radius * 0.30; axesGroup = buildAxesIndicator(axisSize); // Offset from the bounding box corner by ~1 axis-length so it doesn't overlap the mesh @@ -444,9 +476,10 @@ export function loadGeometry(geometry, material) { scene.add(axesGroup); // Bounding-box dimension annotations on the ground plane - if (dimensionGroup) scene.remove(dimensionGroup); + if (dimensionGroup) { disposeGroup(dimensionGroup); scene.remove(dimensionGroup); } dimensionGroup = buildDimensions(box, groundZ, sphere.radius); scene.add(dimensionGroup); + requestRender(); } /** @@ -464,6 +497,7 @@ export function setMeshMaterial(material) { metalness: 0.1, side: THREE.DoubleSide, }); + requestRender(); } /** @@ -484,6 +518,7 @@ export function setMeshGeometry(geometry) { wireframeLines = null; } if (wireframeVisible) _buildWireframe(geometry); + requestRender(); } /** @@ -514,6 +549,8 @@ function fitCamera(sphere) { controls.update(); } +export function requestRender() { _needsRender = true; } + export function getRenderer() { return renderer; } export function getCamera() { return camera; } export function getScene() { return scene; } @@ -522,6 +559,7 @@ export function getCurrentMesh() { return currentMesh; } export function setSceneBackground(hexColor) { if (scene) scene.background = new THREE.Color(hexColor); + requestRender(); } export function setViewerTheme(isLight) { @@ -541,6 +579,7 @@ export function setViewerTheme(isLight) { grid.rotation.x = Math.PI / 2; grid.position.z = savedZ; scene.add(grid); + requestRender(); } /** @@ -556,13 +595,11 @@ export function setExclusionOverlay(overlayGeo, color = 0xff6600, opacity = 1.0) if (exclusionMesh) { scene.remove(exclusionMesh); exclusionMesh.geometry.dispose(); - exclusionMesh.material.dispose(); exclusionMesh = null; } - if (!overlayGeo || overlayGeo.attributes.position.count === 0) return; - exclusionMesh = new THREE.Mesh( - overlayGeo, - new THREE.MeshLambertMaterial({ + if (!overlayGeo || overlayGeo.attributes.position.count === 0) { requestRender(); return; } + if (!_exclMaterial) { + _exclMaterial = new THREE.MeshLambertMaterial({ color, side: THREE.DoubleSide, transparent: opacity < 1.0, @@ -570,10 +607,16 @@ export function setExclusionOverlay(overlayGeo, color = 0xff6600, opacity = 1.0) polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1, - }), - ); + }); + } else { + _exclMaterial.color.set(color); + _exclMaterial.opacity = opacity; + _exclMaterial.transparent = opacity < 1.0; + } + exclusionMesh = new THREE.Mesh(overlayGeo, _exclMaterial); exclusionMesh.renderOrder = 1; scene.add(exclusionMesh); + requestRender(); } /** @@ -586,13 +629,11 @@ export function setHoverPreview(overlayGeo, color = 0xffee00) { if (hoverMesh) { scene.remove(hoverMesh); hoverMesh.geometry.dispose(); - hoverMesh.material.dispose(); hoverMesh = null; } - if (!overlayGeo || overlayGeo.attributes.position.count === 0) return; - hoverMesh = new THREE.Mesh( - overlayGeo, - new THREE.MeshBasicMaterial({ + if (!overlayGeo || overlayGeo.attributes.position.count === 0) { requestRender(); return; } + if (!_hoverMaterial) { + _hoverMaterial = new THREE.MeshBasicMaterial({ color, side: THREE.DoubleSide, transparent: true, @@ -600,10 +641,14 @@ export function setHoverPreview(overlayGeo, color = 0xffee00) { polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2, - }), - ); + }); + } else { + _hoverMaterial.color.set(color); + } + hoverMesh = new THREE.Mesh(overlayGeo, _hoverMaterial); hoverMesh.renderOrder = 2; scene.add(hoverMesh); + requestRender(); } /** @@ -618,6 +663,7 @@ export function setWireframe(enabled) { } else { if (wireframeLines) wireframeLines.visible = false; } + requestRender(); } function _buildWireframe(geometry) {