Stabilize machine manager WebGL resources
This commit is contained in:
@@ -14,12 +14,21 @@ const zero3 = (): Vec3 => [0, 0, 0];
|
||||
const number = (value: string) => Number.isFinite(Number(value)) ? Number(value) : 0;
|
||||
|
||||
function ManagerPreview({ parts, selectedId, mode, onTransform }: { parts: MachinePartDraft[]; selectedId: string | null; mode: "translate" | "rotate"; onTransform: (id: string, translationMm: Vec3, rotationDeg: Vec3) => void }) {
|
||||
const host = useRef<HTMLDivElement>(null);
|
||||
type PreviewHost = HTMLDivElement & { machineManagerSync?: (parts: MachinePartDraft[], selectedId: string | null, mode: "translate" | "rotate") => void };
|
||||
const host = useRef<PreviewHost>(null);
|
||||
const stateRef = useRef({ parts, selectedId, mode }); stateRef.current = { parts, selectedId, mode };
|
||||
const onTransformRef = useRef(onTransform); onTransformRef.current = onTransform;
|
||||
useEffect(() => { host.current?.machineManagerSync?.(parts, selectedId, mode); }, [parts, selectedId, mode]);
|
||||
useEffect(() => {
|
||||
const element = host.current; if (!element) return;
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(Math.min(devicePixelRatio || 1, 2)); renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.45; renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFShadowMap; renderer.setSize(Math.max(element.clientWidth, 320), Math.max(element.clientHeight, 360), false); element.appendChild(renderer.domElement);
|
||||
// Large industrial GLBs can exceed small-GPU multisample renderbuffer
|
||||
// limits when DevTools or display scaling changes the drawing-buffer size.
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: false, depth: true, stencil: false, powerPreference: "high-performance" });
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.45; renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFShadowMap; element.appendChild(renderer.domElement);
|
||||
const gl = renderer.getContext(), viewportLimit = gl.getParameter(gl.MAX_VIEWPORT_DIMS) as Int32Array;
|
||||
const maxDimension = Math.max(1, Math.floor(Math.min(Number(gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) || 4096), Number(gl.getParameter(gl.MAX_TEXTURE_SIZE) || 4096), Number(viewportLimit?.[0] || 4096), Number(viewportLimit?.[1] || 4096)) * .9));
|
||||
const maxPixels = Math.min(maxDimension * maxDimension, 4096 * 4096);
|
||||
element.dataset.machineManagerWebglAntialias = String(Boolean(gl.getContextAttributes()?.antialias)); element.dataset.machineManagerMaxBuffer = String(maxDimension);
|
||||
const scene = new THREE.Scene(); scene.background = new THREE.Color(0x263438);
|
||||
const studio = new RoomEnvironment(); const pmrem = new THREE.PMREMGenerator(renderer); const environment = pmrem.fromScene(studio, .04); scene.environment = environment.texture; scene.environmentIntensity = 1.25; studio.dispose(); pmrem.dispose();
|
||||
scene.add(new THREE.HemisphereLight(0xffffff, 0x728083, 2.6)); scene.add(new THREE.AmbientLight(0xffffff, .65));
|
||||
@@ -30,30 +39,54 @@ function ManagerPreview({ parts, selectedId, mode, onTransform }: { parts: Machi
|
||||
const camera = new THREE.PerspectiveCamera(42, 1, .005, 100); camera.up.set(0, 0, 1); camera.position.set(2.4, -2.8, 1.8);
|
||||
const orbit = new OrbitControls(camera, renderer.domElement); orbit.enableDamping = true;
|
||||
const root = new THREE.Group(); root.name = "machine-manager-preview"; scene.add(root);
|
||||
const nodes = new Map<string, THREE.Group>(); const helpers: THREE.AxesHelper[] = [];
|
||||
for (const part of parts) {
|
||||
const node = new THREE.Group(); node.name = `draft_${part.id}`; node.visible = part.visible; node.position.fromArray(part.translationMm).multiplyScalar(.001); node.rotation.set(...part.rotationDeg.map(THREE.MathUtils.degToRad) as Vec3);
|
||||
// Preview clones only Object3D transforms/hierarchy. Geometry, material,
|
||||
// texture and source node transforms remain untouched on the imported GLB.
|
||||
const visual = part.scene.clone(true); applyMachineNodeParents(visual, part.nodeParents); visual.position.fromArray(part.originMm).multiplyScalar(-.001); node.add(visual);
|
||||
const axes = new THREE.AxesHelper(.12); helpers.push(axes); axes.name = `origin_${part.id}`; axes.visible = part.showOrigin; node.add(axes); root.add(node); nodes.set(part.id, node);
|
||||
}
|
||||
const cycle = (child: string, parent: string) => { let cursor: string | null = parent; const seen = new Set([child]); while (cursor) { if (seen.has(cursor)) return true; seen.add(cursor); cursor = parts.find((part) => part.id === cursor)?.parentId ?? null; } return false; };
|
||||
for (const part of parts) if (part.parentId && nodes.has(part.parentId) && !cycle(part.id, part.parentId)) nodes.get(part.parentId)!.add(nodes.get(part.id)!);
|
||||
const bounds = new THREE.Box3().setFromObject(root); const center = bounds.isEmpty() ? new THREE.Vector3() : bounds.getCenter(new THREE.Vector3()); const size = bounds.isEmpty() ? 1 : Math.max(bounds.getSize(new THREE.Vector3()).length(), .2);
|
||||
orbit.target.copy(center); camera.position.copy(center).add(new THREE.Vector3(size, size * .7, size * 1.15)); orbit.update();
|
||||
let nodes = new Map<string, THREE.Group>(), helpers: THREE.AxesHelper[] = [], selectedObject: THREE.Group | null = null, selectedPartId: string | null = null, lastParts: MachinePartDraft[] | null = null, fitSignature = "";
|
||||
const transform = new TransformControls(camera, renderer.domElement); transform.setMode(mode); transform.setSpace("local"); scene.add(transform.getHelper());
|
||||
const selected = selectedId ? nodes.get(selectedId) : null; if (selected) transform.attach(selected);
|
||||
transform.addEventListener("dragging-changed", ((event: { value?: boolean }) => { orbit.enabled = !event.value; }) as never);
|
||||
transform.addEventListener("objectChange", (() => {
|
||||
if (!selectedId || !selected) return;
|
||||
onTransformRef.current(selectedId, selected.position.toArray().map((value) => value * 1000) as Vec3, [THREE.MathUtils.radToDeg(selected.rotation.x), THREE.MathUtils.radToDeg(selected.rotation.y), THREE.MathUtils.radToDeg(selected.rotation.z)]);
|
||||
if (!selectedPartId || !selectedObject) return;
|
||||
onTransformRef.current(selectedPartId, selectedObject.position.toArray().map((value) => value * 1000) as Vec3, [THREE.MathUtils.radToDeg(selectedObject.rotation.x), THREE.MathUtils.radToDeg(selectedObject.rotation.y), THREE.MathUtils.radToDeg(selectedObject.rotation.z)]);
|
||||
}) as never);
|
||||
const resize = () => { const width = Math.max(element.clientWidth, 1), height = Math.max(element.clientHeight, 1); camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height, false); };
|
||||
element.machineManagerSync = (nextParts, nextSelectedId, nextMode) => {
|
||||
transform.setMode(nextMode);
|
||||
if (nextParts !== lastParts) {
|
||||
transform.detach(); root.clear(); helpers.forEach((helper) => helper.dispose()); helpers = []; nodes = new Map();
|
||||
for (const part of nextParts) {
|
||||
const node = new THREE.Group(); node.name = `draft_${part.id}`; node.visible = part.visible; node.position.fromArray(part.translationMm).multiplyScalar(.001); node.rotation.set(...part.rotationDeg.map(THREE.MathUtils.degToRad) as Vec3);
|
||||
// Clone transforms/hierarchy only. Geometry, materials, textures and
|
||||
// source-node transforms remain untouched on the imported GLB.
|
||||
const visual = part.scene.clone(true); applyMachineNodeParents(visual, part.nodeParents); visual.position.fromArray(part.originMm).multiplyScalar(-.001); node.add(visual);
|
||||
const axes = new THREE.AxesHelper(.12); helpers.push(axes); axes.name = `origin_${part.id}`; axes.visible = part.showOrigin; node.add(axes); root.add(node); nodes.set(part.id, node);
|
||||
}
|
||||
const cycle = (child: string, parent: string) => { let cursor: string | null = parent; const seen = new Set([child]); while (cursor) { if (seen.has(cursor)) return true; seen.add(cursor); cursor = nextParts.find((part) => part.id === cursor)?.parentId ?? null; } return false; };
|
||||
for (const part of nextParts) if (part.parentId && nodes.has(part.parentId) && !cycle(part.id, part.parentId)) nodes.get(part.parentId)!.add(nodes.get(part.id)!);
|
||||
const signature = nextParts.map((part) => `${part.id}:${part.hash}`).join("|");
|
||||
if (signature !== fitSignature) {
|
||||
const bounds = new THREE.Box3().setFromObject(root), center = bounds.isEmpty() ? new THREE.Vector3() : bounds.getCenter(new THREE.Vector3()), size = bounds.isEmpty() ? 1 : Math.max(bounds.getSize(new THREE.Vector3()).length(), .2);
|
||||
orbit.target.copy(center); camera.position.copy(center).add(new THREE.Vector3(size, size * .7, size * 1.15)); orbit.update(); fitSignature = signature;
|
||||
}
|
||||
lastParts = nextParts;
|
||||
}
|
||||
selectedPartId = nextSelectedId; selectedObject = nextSelectedId ? nodes.get(nextSelectedId) ?? null : null; transform.detach(); if (selectedObject) transform.attach(selectedObject);
|
||||
element.dataset.machineManagerContextGeneration = "1";
|
||||
};
|
||||
element.machineManagerSync(stateRef.current.parts, stateRef.current.selectedId, stateRef.current.mode);
|
||||
let renderable = false, contextLost = false;
|
||||
const resize = () => {
|
||||
const width = element.clientWidth, height = element.clientHeight;
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height) || width < 2 || height < 2) { renderable = false; element.dataset.machineManagerRenderState = "suspended-zero-size"; return; }
|
||||
const requested = Math.min(globalThis.devicePixelRatio || 1, 1.5);
|
||||
const scale = Math.min(1, maxDimension / (width * requested), maxDimension / (height * requested), Math.sqrt(maxPixels / (width * height * requested * requested)));
|
||||
const pixelRatio = Math.max(.5, requested * scale);
|
||||
renderer.setPixelRatio(pixelRatio); camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height, false);
|
||||
renderable = renderer.domElement.width > 0 && renderer.domElement.height > 0 && renderer.domElement.width <= maxDimension && renderer.domElement.height <= maxDimension;
|
||||
element.dataset.machineManagerPixelRatio = pixelRatio.toFixed(3); element.dataset.machineManagerBufferSize = `${renderer.domElement.width}x${renderer.domElement.height}`; element.dataset.machineManagerRenderState = renderable ? "ready" : "suspended-limit";
|
||||
};
|
||||
const contextLostHandler = (event: Event) => { event.preventDefault(); contextLost = true; renderable = false; element.dataset.machineManagerRenderState = "context-lost"; };
|
||||
renderer.domElement.addEventListener("webglcontextlost", contextLostHandler);
|
||||
const observer = new ResizeObserver(resize); observer.observe(element); resize(); let frame = 0;
|
||||
const render = () => { orbit.update(); renderer.render(scene, camera); element.dataset.machineManagerRendered = "true"; frame = requestAnimationFrame(render); }; render();
|
||||
return () => { cancelAnimationFrame(frame); observer.disconnect(); transform.dispose(); orbit.dispose(); helpers.forEach((helper) => helper.dispose()); environment.dispose(); renderer.dispose(); renderer.domElement.remove(); };
|
||||
}, [parts, selectedId, mode]);
|
||||
const render = () => { if (!contextLost && renderable) { orbit.update(); renderer.render(scene, camera); element.dataset.machineManagerRendered = "true"; } frame = contextLost ? 0 : requestAnimationFrame(render); }; render();
|
||||
return () => { if (frame) cancelAnimationFrame(frame); observer.disconnect(); delete element.machineManagerSync; renderer.domElement.removeEventListener("webglcontextlost", contextLostHandler); transform.dispose(); orbit.dispose(); helpers.forEach((helper) => helper.dispose()); environment.dispose(); renderer.dispose(); renderer.domElement.remove(); };
|
||||
}, []);
|
||||
return <div ref={host} className="machine-manager-preview" data-machine-manager-preview data-machine-manager-coordinate-system="machine-z-up" data-machine-manager-axis-map="X:x,Y:y,Z:z" data-machine-manager-camera-up="0,0,1" data-machine-manager-model-transform="none-on-import" data-machine-manager-lighting="studio-pmrem" data-machine-manager-exposure="1.45" data-machine-manager-part-count={parts.length} data-machine-manager-origin-count={parts.filter((part) => part.showOrigin).length} data-transform-mode={mode} aria-label="三维机床装配预览,机床 Z 轴朝上" />;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user