fix viewport
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
import React, { useRef, useEffect, useCallback } from 'react';
|
||||
import React, { useRef, useEffect, useCallback, useState } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
|
||||
const VIEW3D_DIAGNOSTICS_STORAGE_KEY = 'argonode:view3d-diagnostics';
|
||||
const DEFAULT_CAMERA_STATE = {
|
||||
azimuth: 0.0,
|
||||
// A diagonal home view avoids edge-on framing when one lateral axis is much smaller.
|
||||
azimuth: Math.PI / 4,
|
||||
polar: 1.1,
|
||||
distance: 1.8,
|
||||
targetX: 0.0,
|
||||
targetY: 0.0,
|
||||
targetZ: 0.0,
|
||||
};
|
||||
|
||||
function getFiniteNumber(...values) {
|
||||
@@ -21,48 +20,43 @@ function getFiniteNumber(...values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCameraState(meshData, widgetValues, runtimeValues, fallbackTarget = null) {
|
||||
return {
|
||||
azimuth: getFiniteNumber(
|
||||
runtimeValues?.camera_azimuth,
|
||||
widgetValues?.camera_azimuth,
|
||||
meshData?.camera_azimuth,
|
||||
DEFAULT_CAMERA_STATE.azimuth,
|
||||
),
|
||||
polar: getFiniteNumber(
|
||||
runtimeValues?.camera_polar,
|
||||
widgetValues?.camera_polar,
|
||||
meshData?.camera_polar,
|
||||
DEFAULT_CAMERA_STATE.polar,
|
||||
),
|
||||
distance: getFiniteNumber(
|
||||
runtimeValues?.camera_distance,
|
||||
widgetValues?.camera_distance,
|
||||
meshData?.camera_distance,
|
||||
DEFAULT_CAMERA_STATE.distance,
|
||||
),
|
||||
targetX: getFiniteNumber(
|
||||
runtimeValues?.camera_target_x,
|
||||
widgetValues?.camera_target_x,
|
||||
meshData?.camera_target_x,
|
||||
fallbackTarget?.x,
|
||||
DEFAULT_CAMERA_STATE.targetX,
|
||||
),
|
||||
targetY: getFiniteNumber(
|
||||
runtimeValues?.camera_target_y,
|
||||
widgetValues?.camera_target_y,
|
||||
meshData?.camera_target_y,
|
||||
fallbackTarget?.y,
|
||||
DEFAULT_CAMERA_STATE.targetY,
|
||||
),
|
||||
targetZ: getFiniteNumber(
|
||||
runtimeValues?.camera_target_z,
|
||||
widgetValues?.camera_target_z,
|
||||
meshData?.camera_target_z,
|
||||
fallbackTarget?.z,
|
||||
DEFAULT_CAMERA_STATE.targetZ,
|
||||
),
|
||||
};
|
||||
function formatNumber(value, digits = 2) {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? numeric.toFixed(digits) : 'n/a';
|
||||
}
|
||||
|
||||
function formatVector3(value, digits = 2) {
|
||||
if (!value) return 'n/a';
|
||||
return `${formatNumber(value.x, digits)}, ${formatNumber(value.y, digits)}, ${formatNumber(value.z, digits)}`;
|
||||
}
|
||||
|
||||
function areView3dDiagnosticsEnabled() {
|
||||
if (typeof window === 'undefined') return false;
|
||||
try {
|
||||
return window.localStorage?.getItem(VIEW3D_DIAGNOSTICS_STORAGE_KEY) === '1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildGeometrySignature(meshData) {
|
||||
if (!meshData) return '';
|
||||
const positionSource = String(meshData.positions || meshData.z_data || '');
|
||||
const indexSource = String(meshData.indices || '');
|
||||
return [
|
||||
meshData.width,
|
||||
meshData.height,
|
||||
meshData.z_scale,
|
||||
meshData.make_solid ? 1 : 0,
|
||||
meshData.surface_extent_x,
|
||||
meshData.surface_extent_y,
|
||||
positionSource.length,
|
||||
positionSource.slice(0, 24),
|
||||
positionSource.slice(-24),
|
||||
indexSource.length,
|
||||
indexSource.slice(0, 24),
|
||||
indexSource.slice(-24),
|
||||
].join('|');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,19 +66,31 @@ function getCameraState(meshData, widgetValues, runtimeValues, fallbackTarget =
|
||||
* z_min, z_max, z_scale, x_range, y_range }
|
||||
*/
|
||||
export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeValues, onRuntimeValuesChange }) {
|
||||
const [showDiagnostics] = useState(() => areView3dDiagnosticsEnabled());
|
||||
const containerRef = useRef(null);
|
||||
const threeRef = useRef(null); // { renderer, scene, camera, controls, mesh }
|
||||
const meshCenterRef = useRef(new THREE.Vector3());
|
||||
const fitDistanceRef = useRef(DEFAULT_CAMERA_STATE.distance);
|
||||
const lastGeometrySignatureRef = useRef('');
|
||||
const syncTimerRef = useRef(null);
|
||||
const lastSnapshotRef = useRef('');
|
||||
const lastCameraStateRef = useRef({
|
||||
azimuth: null,
|
||||
polar: null,
|
||||
distance: null,
|
||||
targetX: null,
|
||||
targetY: null,
|
||||
targetZ: null,
|
||||
const [diagnostics, setDiagnostics] = useState({
|
||||
status: meshData ? 'initializing' : 'waiting for mesh',
|
||||
webgl: 'pending',
|
||||
canvas: 'n/a',
|
||||
mesh: meshData ? 'pending' : 'none',
|
||||
bounds: 'n/a',
|
||||
camera: 'n/a',
|
||||
target: 'n/a',
|
||||
render: 'n/a',
|
||||
error: '',
|
||||
});
|
||||
|
||||
const updateDiagnostics = useCallback((patch) => {
|
||||
if (!showDiagnostics) return;
|
||||
setDiagnostics((prev) => ({ ...prev, ...patch }));
|
||||
}, [showDiagnostics]);
|
||||
|
||||
// Decode base64 to typed arrays
|
||||
const decode = useCallback((b64, ArrayType) => {
|
||||
const bin = atob(b64);
|
||||
@@ -93,34 +99,42 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
return new ArrayType(bytes.buffer);
|
||||
}, []);
|
||||
|
||||
const captureViewportSnapshot = useCallback(() => {
|
||||
const canvas = threeRef.current?.renderer?.domElement;
|
||||
if (!canvas) return null;
|
||||
try {
|
||||
return canvas.toDataURL('image/png');
|
||||
} catch (error) {
|
||||
console.warn('[argonode] Failed to capture View3D viewport snapshot', error);
|
||||
updateDiagnostics({
|
||||
status: 'snapshot error',
|
||||
error: error?.message || String(error),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}, [updateDiagnostics]);
|
||||
|
||||
const syncViewportState = useCallback((scheduleRun = false) => {
|
||||
const state = threeRef.current;
|
||||
if (!state || !nodeId || !onRuntimeValuesChange) return;
|
||||
const { renderer, controls } = state;
|
||||
const cameraState = {
|
||||
azimuth: Number(controls.getAzimuthalAngle().toFixed(4)),
|
||||
polar: Number(controls.getPolarAngle().toFixed(4)),
|
||||
distance: Number(controls.getDistance().toFixed(4)),
|
||||
targetX: Number(controls.target.x.toFixed(4)),
|
||||
targetY: Number(controls.target.y.toFixed(4)),
|
||||
targetZ: Number(controls.target.z.toFixed(4)),
|
||||
};
|
||||
const snapshot = renderer.domElement.toDataURL('image/png');
|
||||
const previous = lastCameraStateRef.current;
|
||||
if (!state) return;
|
||||
const { controls, camera, renderer } = state;
|
||||
const snapshot = captureViewportSnapshot();
|
||||
updateDiagnostics({
|
||||
camera: `dist ${formatNumber(controls.getDistance())} az ${formatNumber(controls.getAzimuthalAngle())} pol ${formatNumber(controls.getPolarAngle())}`,
|
||||
target: formatVector3(controls.target),
|
||||
canvas: `${renderer.domElement.width}x${renderer.domElement.height} px`,
|
||||
render: `calls ${renderer.info.render.calls} tris ${renderer.info.render.triangles} geo ${renderer.info.memory.geometries}`,
|
||||
});
|
||||
if (!nodeId || !onRuntimeValuesChange) return;
|
||||
const patch = {};
|
||||
if (previous.azimuth !== cameraState.azimuth) patch.camera_azimuth = cameraState.azimuth;
|
||||
if (previous.polar !== cameraState.polar) patch.camera_polar = cameraState.polar;
|
||||
if (previous.distance !== cameraState.distance) patch.camera_distance = cameraState.distance;
|
||||
if (previous.targetX !== cameraState.targetX) patch.camera_target_x = cameraState.targetX;
|
||||
if (previous.targetY !== cameraState.targetY) patch.camera_target_y = cameraState.targetY;
|
||||
if (previous.targetZ !== cameraState.targetZ) patch.camera_target_z = cameraState.targetZ;
|
||||
if (snapshot !== lastSnapshotRef.current) patch.viewport_snapshot = snapshot;
|
||||
if (snapshot && snapshot !== lastSnapshotRef.current) patch.viewport_snapshot = snapshot;
|
||||
if (Object.keys(patch).length > 0) {
|
||||
onRuntimeValuesChange(nodeId, patch, { scheduleRun });
|
||||
lastCameraStateRef.current = cameraState;
|
||||
lastSnapshotRef.current = snapshot;
|
||||
if (snapshot) {
|
||||
lastSnapshotRef.current = snapshot;
|
||||
}
|
||||
}
|
||||
}, [nodeId, onRuntimeValuesChange]);
|
||||
}, [captureViewportSnapshot, nodeId, onRuntimeValuesChange, updateDiagnostics]);
|
||||
|
||||
const scheduleViewportSync = useCallback((delay = 120, scheduleRun = false) => {
|
||||
if (syncTimerRef.current) {
|
||||
@@ -136,13 +150,14 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
const state = threeRef.current;
|
||||
if (!state) return;
|
||||
const { camera, controls } = state;
|
||||
const target = new THREE.Vector3(
|
||||
getFiniteNumber(cameraState.targetX, controls.target.x, DEFAULT_CAMERA_STATE.targetX),
|
||||
getFiniteNumber(cameraState.targetY, controls.target.y, DEFAULT_CAMERA_STATE.targetY),
|
||||
getFiniteNumber(cameraState.targetZ, controls.target.z, DEFAULT_CAMERA_STATE.targetZ),
|
||||
const target = meshCenterRef.current.clone();
|
||||
const distance = THREE.MathUtils.clamp(
|
||||
getFiniteNumber(cameraState.distance, fitDistanceRef.current, DEFAULT_CAMERA_STATE.distance),
|
||||
controls.minDistance,
|
||||
controls.maxDistance,
|
||||
);
|
||||
const spherical = new THREE.Spherical(
|
||||
Math.max(0.3, getFiniteNumber(cameraState.distance, DEFAULT_CAMERA_STATE.distance)),
|
||||
distance,
|
||||
THREE.MathUtils.clamp(
|
||||
getFiniteNumber(cameraState.polar, DEFAULT_CAMERA_STATE.polar),
|
||||
0.01,
|
||||
@@ -153,9 +168,18 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
const offset = new THREE.Vector3().setFromSpherical(spherical);
|
||||
controls.target.copy(target);
|
||||
camera.position.copy(target).add(offset);
|
||||
camera.lookAt(target);
|
||||
controls.update();
|
||||
}, []);
|
||||
|
||||
const resetCamera = useCallback(() => {
|
||||
applyCameraState({
|
||||
azimuth: DEFAULT_CAMERA_STATE.azimuth,
|
||||
polar: DEFAULT_CAMERA_STATE.polar,
|
||||
});
|
||||
scheduleViewportSync(0, true);
|
||||
}, [applyCameraState, scheduleViewportSync]);
|
||||
|
||||
// Initialize Three.js scene once
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
@@ -173,6 +197,29 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setClearColor(0x0f172a);
|
||||
container.appendChild(renderer.domElement);
|
||||
updateDiagnostics({
|
||||
status: meshData ? 'renderer ready' : 'waiting for mesh',
|
||||
webgl: `${renderer.capabilities.isWebGL2 ? 'webgl2' : 'webgl1'} / ${renderer.capabilities.precision}`,
|
||||
canvas: `${renderer.domElement.width}x${renderer.domElement.height} px`,
|
||||
render: `calls ${renderer.info.render.calls} tris ${renderer.info.render.triangles} geo ${renderer.info.memory.geometries}`,
|
||||
error: '',
|
||||
});
|
||||
|
||||
const handleContextLost = (event) => {
|
||||
event.preventDefault();
|
||||
updateDiagnostics({
|
||||
status: 'webgl context lost',
|
||||
error: 'WebGL context lost',
|
||||
});
|
||||
};
|
||||
const handleContextRestored = () => {
|
||||
updateDiagnostics({
|
||||
status: 'webgl context restored',
|
||||
error: '',
|
||||
});
|
||||
};
|
||||
renderer.domElement.addEventListener('webglcontextlost', handleContextLost, false);
|
||||
renderer.domElement.addEventListener('webglcontextrestored', handleContextRestored, false);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
|
||||
@@ -182,27 +229,22 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.1;
|
||||
controls.enablePan = true;
|
||||
controls.enablePan = false;
|
||||
controls.enableZoom = true;
|
||||
controls.screenSpacePanning = true;
|
||||
controls.panSpeed = 1.0;
|
||||
controls.zoomSpeed = 2.2;
|
||||
controls.minDistance = 0.3;
|
||||
controls.maxDistance = 10;
|
||||
controls.mouseButtons = {
|
||||
LEFT: THREE.MOUSE.ROTATE,
|
||||
MIDDLE: THREE.MOUSE.PAN,
|
||||
MIDDLE: THREE.MOUSE.DOLLY,
|
||||
RIGHT: THREE.MOUSE.DOLLY,
|
||||
};
|
||||
controls.touches = {
|
||||
ONE: THREE.TOUCH.ROTATE,
|
||||
TWO: THREE.TOUCH.DOLLY_PAN,
|
||||
TWO: THREE.TOUCH.DOLLY_ROTATE,
|
||||
};
|
||||
if ('zoomToCursor' in controls) {
|
||||
controls.zoomToCursor = true;
|
||||
}
|
||||
renderer.domElement.style.touchAction = 'none';
|
||||
const handleControlsEnd = () => scheduleViewportSync(0, true);
|
||||
const handleControlsEnd = () => scheduleViewportSync(120, true);
|
||||
controls.addEventListener('end', handleControlsEnd);
|
||||
|
||||
// Lighting
|
||||
@@ -225,7 +267,10 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
animate();
|
||||
|
||||
threeRef.current = { renderer, scene, camera, controls, mesh: null, animId };
|
||||
applyCameraState(getCameraState(meshData, widgetValues, runtimeValues));
|
||||
applyCameraState({
|
||||
azimuth: DEFAULT_CAMERA_STATE.azimuth,
|
||||
polar: DEFAULT_CAMERA_STATE.polar,
|
||||
});
|
||||
|
||||
// Resize observer to maintain 1:1 aspect when node width changes
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
@@ -237,6 +282,9 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
r.setSize(w, w);
|
||||
c.aspect = 1;
|
||||
c.updateProjectionMatrix();
|
||||
updateDiagnostics({
|
||||
canvas: `${r.domElement.width}x${r.domElement.height} px`,
|
||||
});
|
||||
});
|
||||
ro.observe(container);
|
||||
|
||||
@@ -245,6 +293,8 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
cancelAnimationFrame(animId);
|
||||
if (syncTimerRef.current) clearTimeout(syncTimerRef.current);
|
||||
controls.removeEventListener('end', handleControlsEnd);
|
||||
renderer.domElement.removeEventListener('webglcontextlost', handleContextLost, false);
|
||||
renderer.domElement.removeEventListener('webglcontextrestored', handleContextRestored, false);
|
||||
controls.dispose();
|
||||
renderer.dispose();
|
||||
if (container.contains(renderer.domElement)) {
|
||||
@@ -252,108 +302,164 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
}
|
||||
threeRef.current = null;
|
||||
};
|
||||
}, [applyCameraState, meshData, runtimeValues, scheduleViewportSync, widgetValues]);
|
||||
}, [scheduleViewportSync, updateDiagnostics]);
|
||||
|
||||
useEffect(() => {
|
||||
if (meshData) {
|
||||
updateDiagnostics({
|
||||
status: 'mesh payload received',
|
||||
mesh: `${meshData.width}x${meshData.height} payload`,
|
||||
error: '',
|
||||
});
|
||||
return;
|
||||
}
|
||||
updateDiagnostics({
|
||||
status: threeRef.current ? 'waiting for mesh' : 'initializing',
|
||||
mesh: 'none',
|
||||
bounds: 'n/a',
|
||||
});
|
||||
}, [meshData, updateDiagnostics]);
|
||||
|
||||
// Update mesh when data changes
|
||||
useEffect(() => {
|
||||
if (!threeRef.current || !meshData) return;
|
||||
try {
|
||||
const { scene, controls, renderer } = threeRef.current;
|
||||
const geometrySignature = buildGeometrySignature(meshData);
|
||||
const geometryChanged = geometrySignature !== lastGeometrySignatureRef.current;
|
||||
const {
|
||||
width: nx, height: ny, z_data, colors, z_min, z_max, z_scale,
|
||||
positions, indices, vertex_colors,
|
||||
surface_extent_x, surface_extent_y,
|
||||
} = meshData;
|
||||
|
||||
const { scene, controls } = threeRef.current;
|
||||
const {
|
||||
width: nx, height: ny, z_data, colors, z_min, z_max, z_scale,
|
||||
positions, indices, vertex_colors,
|
||||
surface_extent_x, surface_extent_y,
|
||||
} = meshData;
|
||||
// Decode arrays
|
||||
const zArr = z_data ? decode(z_data, Float32Array) : null;
|
||||
const colArr = colors ? decode(colors, Uint8Array) : null;
|
||||
const posArr = positions ? decode(positions, Float32Array) : null;
|
||||
const indexArr = indices ? decode(indices, Uint32Array) : null;
|
||||
const vertexColorArr = vertex_colors ? decode(vertex_colors, Uint8Array) : null;
|
||||
|
||||
// Decode arrays
|
||||
const zArr = z_data ? decode(z_data, Float32Array) : null;
|
||||
const colArr = colors ? decode(colors, Uint8Array) : null;
|
||||
const posArr = positions ? decode(positions, Float32Array) : null;
|
||||
const indexArr = indices ? decode(indices, Uint32Array) : null;
|
||||
const vertexColorArr = vertex_colors ? decode(vertex_colors, Uint8Array) : null;
|
||||
// Remove old mesh
|
||||
if (threeRef.current.mesh) {
|
||||
scene.remove(threeRef.current.mesh);
|
||||
threeRef.current.mesh.geometry.dispose();
|
||||
threeRef.current.mesh.material.dispose();
|
||||
}
|
||||
|
||||
// Remove old mesh
|
||||
if (threeRef.current.mesh) {
|
||||
scene.remove(threeRef.current.mesh);
|
||||
threeRef.current.mesh.geometry.dispose();
|
||||
threeRef.current.mesh.material.dispose();
|
||||
}
|
||||
// Build geometry
|
||||
const geom = new THREE.BufferGeometry();
|
||||
const positionsArray = posArr ?? new Float32Array(nx * ny * 3);
|
||||
const colorAttr = new Float32Array((vertexColorArr ? vertexColorArr.length : (nx * ny * 3)));
|
||||
const surfaceExtentX = getFiniteNumber(surface_extent_x, 1.0);
|
||||
const surfaceExtentY = getFiniteNumber(surface_extent_y, 1.0);
|
||||
|
||||
// Build geometry
|
||||
const geom = new THREE.BufferGeometry();
|
||||
const positionsArray = posArr ?? new Float32Array(nx * ny * 3);
|
||||
const colorAttr = new Float32Array((vertexColorArr ? vertexColorArr.length : (nx * ny * 3)));
|
||||
const surfaceExtentX = getFiniteNumber(surface_extent_x, 1.0);
|
||||
const surfaceExtentY = getFiniteNumber(surface_extent_y, 1.0);
|
||||
if (!posArr) {
|
||||
const zRange = z_max - z_min || 1;
|
||||
for (let iy = 0; iy < ny; iy++) {
|
||||
for (let ix = 0; ix < nx; ix++) {
|
||||
const idx = iy * nx + ix;
|
||||
const px = (ix / Math.max(nx - 1, 1) - 0.5) * surfaceExtentX;
|
||||
const py = (iy / Math.max(ny - 1, 1) - 0.5) * surfaceExtentY;
|
||||
const pz = ((zArr[idx] - z_min) / zRange - 0.5) * z_scale;
|
||||
|
||||
if (!posArr) {
|
||||
const zRange = z_max - z_min || 1;
|
||||
for (let iy = 0; iy < ny; iy++) {
|
||||
for (let ix = 0; ix < nx; ix++) {
|
||||
const idx = iy * nx + ix;
|
||||
const px = (ix / Math.max(nx - 1, 1) - 0.5) * surfaceExtentX;
|
||||
const py = (iy / Math.max(ny - 1, 1) - 0.5) * surfaceExtentY;
|
||||
const pz = ((zArr[idx] - z_min) / zRange - 0.5) * z_scale;
|
||||
|
||||
positionsArray[idx * 3] = px;
|
||||
positionsArray[idx * 3 + 1] = pz;
|
||||
positionsArray[idx * 3 + 2] = py;
|
||||
positionsArray[idx * 3] = px;
|
||||
positionsArray[idx * 3 + 1] = pz;
|
||||
positionsArray[idx * 3 + 2] = py;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sourceColors = vertexColorArr ?? colArr;
|
||||
if (sourceColors) {
|
||||
for (let i = 0; i < sourceColors.length; i += 1) {
|
||||
colorAttr[i] = sourceColors[i] / 255;
|
||||
}
|
||||
}
|
||||
|
||||
geom.setAttribute('position', new THREE.BufferAttribute(positionsArray, 3));
|
||||
geom.setAttribute('color', new THREE.BufferAttribute(colorAttr, 3));
|
||||
|
||||
if (indexArr) {
|
||||
geom.setIndex(Array.from(indexArr));
|
||||
} else {
|
||||
const gridIndices = [];
|
||||
for (let iy = 0; iy < ny - 1; iy++) {
|
||||
for (let ix = 0; ix < nx - 1; ix++) {
|
||||
const a = iy * nx + ix;
|
||||
const b = a + 1;
|
||||
const c = a + nx;
|
||||
const d = c + 1;
|
||||
gridIndices.push(a, c, b);
|
||||
gridIndices.push(b, c, d);
|
||||
const sourceColors = vertexColorArr ?? colArr;
|
||||
if (sourceColors) {
|
||||
for (let i = 0; i < sourceColors.length; i += 1) {
|
||||
colorAttr[i] = sourceColors[i] / 255;
|
||||
}
|
||||
}
|
||||
geom.setIndex(gridIndices);
|
||||
|
||||
geom.setAttribute('position', new THREE.BufferAttribute(positionsArray, 3));
|
||||
geom.setAttribute('color', new THREE.BufferAttribute(colorAttr, 3));
|
||||
|
||||
if (indexArr) {
|
||||
geom.setIndex(Array.from(indexArr));
|
||||
} else {
|
||||
const gridIndices = [];
|
||||
for (let iy = 0; iy < ny - 1; iy++) {
|
||||
for (let ix = 0; ix < nx - 1; ix++) {
|
||||
const a = iy * nx + ix;
|
||||
const b = a + 1;
|
||||
const c = a + nx;
|
||||
const d = c + 1;
|
||||
gridIndices.push(a, c, b);
|
||||
gridIndices.push(b, c, d);
|
||||
}
|
||||
}
|
||||
geom.setIndex(gridIndices);
|
||||
}
|
||||
geom.computeVertexNormals();
|
||||
|
||||
const mat = new THREE.MeshPhongMaterial({
|
||||
vertexColors: true,
|
||||
side: THREE.DoubleSide,
|
||||
shininess: 30,
|
||||
flatShading: false,
|
||||
});
|
||||
|
||||
const mesh = new THREE.Mesh(geom, mat);
|
||||
scene.add(mesh);
|
||||
threeRef.current.mesh = mesh;
|
||||
|
||||
const bounds = new THREE.Box3().setFromObject(mesh);
|
||||
const center = bounds.isEmpty() ? new THREE.Vector3() : bounds.getCenter(new THREE.Vector3());
|
||||
const size = bounds.isEmpty() ? new THREE.Vector3(1, 1, 1) : bounds.getSize(new THREE.Vector3());
|
||||
const maxDimension = Math.max(size.x, size.y, size.z, 0.25);
|
||||
const sphere = bounds.isEmpty()
|
||||
? new THREE.Sphere(center.clone(), 0.5)
|
||||
: bounds.getBoundingSphere(new THREE.Sphere());
|
||||
const radius = Math.max(Number(sphere.radius) || 0, 0.125);
|
||||
const fovRadians = THREE.MathUtils.degToRad(threeRef.current.camera.fov || 45);
|
||||
const fitDistance = Math.max(
|
||||
DEFAULT_CAMERA_STATE.distance,
|
||||
(radius / Math.sin(Math.max(fovRadians / 2, 0.01))) * 1.15,
|
||||
);
|
||||
meshCenterRef.current.copy(center);
|
||||
controls.minDistance = Math.max(0.1, maxDimension * 0.35);
|
||||
controls.maxDistance = Math.max(10, fitDistance * 8);
|
||||
fitDistanceRef.current = Math.min(
|
||||
controls.maxDistance,
|
||||
Math.max(controls.minDistance, fitDistance),
|
||||
);
|
||||
const { camera } = threeRef.current;
|
||||
camera.near = Math.max(0.01, fitDistanceRef.current / 100);
|
||||
camera.far = Math.max(1000, fitDistanceRef.current * 20);
|
||||
camera.updateProjectionMatrix();
|
||||
updateDiagnostics({
|
||||
status: 'mesh built',
|
||||
mesh: `${nx}x${ny} / verts ${positionsArray.length / 3} / idx ${geom.index?.count || 0}`,
|
||||
bounds: `center ${formatVector3(center)} size ${formatVector3(size)}`,
|
||||
camera: `fit ${formatNumber(fitDistanceRef.current)} near ${formatNumber(camera.near, 3)} far ${formatNumber(camera.far, 1)}`,
|
||||
render: `calls ${renderer.info.render.calls} tris ${renderer.info.render.triangles} geo ${renderer.info.memory.geometries}`,
|
||||
error: '',
|
||||
});
|
||||
if (geometryChanged) {
|
||||
applyCameraState({
|
||||
azimuth: DEFAULT_CAMERA_STATE.azimuth,
|
||||
polar: DEFAULT_CAMERA_STATE.polar,
|
||||
});
|
||||
lastGeometrySignatureRef.current = geometrySignature;
|
||||
}
|
||||
scheduleViewportSync(0, false);
|
||||
} catch (error) {
|
||||
console.error('[argonode] View3D mesh build failed', error);
|
||||
updateDiagnostics({
|
||||
status: 'mesh build error',
|
||||
error: error?.message || String(error),
|
||||
});
|
||||
}
|
||||
geom.computeVertexNormals();
|
||||
|
||||
const mat = new THREE.MeshPhongMaterial({
|
||||
vertexColors: true,
|
||||
side: THREE.DoubleSide,
|
||||
shininess: 30,
|
||||
flatShading: false,
|
||||
});
|
||||
|
||||
const mesh = new THREE.Mesh(geom, mat);
|
||||
scene.add(mesh);
|
||||
threeRef.current.mesh = mesh;
|
||||
|
||||
const bounds = new THREE.Box3().setFromObject(mesh);
|
||||
const center = bounds.isEmpty() ? new THREE.Vector3() : bounds.getCenter(new THREE.Vector3());
|
||||
const size = bounds.isEmpty() ? new THREE.Vector3(1, 1, 1) : bounds.getSize(new THREE.Vector3());
|
||||
const maxDimension = Math.max(size.x, size.y, size.z, 0.25);
|
||||
controls.minDistance = Math.max(0.1, maxDimension * 0.35);
|
||||
controls.maxDistance = Math.max(10, maxDimension * 14);
|
||||
applyCameraState(getCameraState(meshData, widgetValues, runtimeValues, center));
|
||||
scheduleViewportSync(0, false);
|
||||
}, [meshData, decode, applyCameraState, runtimeValues, scheduleViewportSync, widgetValues]);
|
||||
}, [applyCameraState, decode, meshData, scheduleViewportSync, updateDiagnostics]);
|
||||
|
||||
// Prevent scroll events from propagating to React Flow
|
||||
const onWheel = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
@@ -363,11 +469,38 @@ export default function SurfaceView({ meshData, nodeId, widgetValues, runtimeVal
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="nodrag nowheel surface-view-container"
|
||||
onWheelCapture={onWheel}
|
||||
onContextMenu={onContextMenu}
|
||||
/>
|
||||
<div className="surface-view-shell">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="nodrag nowheel surface-view-container"
|
||||
onWheel={onWheel}
|
||||
onContextMenu={onContextMenu}
|
||||
/>
|
||||
{showDiagnostics ? (
|
||||
<div className="surface-view-diagnostics">
|
||||
<div>{`status: ${diagnostics.status}`}</div>
|
||||
<div>{`webgl: ${diagnostics.webgl}`}</div>
|
||||
<div>{`canvas: ${diagnostics.canvas}`}</div>
|
||||
<div>{`mesh: ${diagnostics.mesh}`}</div>
|
||||
<div>{`bounds: ${diagnostics.bounds}`}</div>
|
||||
<div>{`camera: ${diagnostics.camera}`}</div>
|
||||
<div>{`target: ${diagnostics.target}`}</div>
|
||||
<div>{`render: ${diagnostics.render}`}</div>
|
||||
{diagnostics.error ? <div>{`error: ${diagnostics.error}`}</div> : null}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="surface-view-home nodrag"
|
||||
title="Reset 3D view"
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
resetCamera();
|
||||
}}
|
||||
>
|
||||
Home
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user