Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
+345
View File
@@ -0,0 +1,345 @@
/**
* 2D Vessel visualization canvas using Three.js
* Renders the vessel geometry, flow field, and scalar fields
*/
import { useRef, useMemo, useEffect } from "react";
import { Canvas } from "@react-three/fiber";
import { OrthographicCamera, Line } from "@react-three/drei";
import * as THREE from "three";
import type { GridQueryResponse, DisplayMode, ColorMap } from "../lib/types";
import { createColorArray } from "../lib/colors";
import { getRange } from "../lib/simulation";
interface VesselCanvasProps {
gridData: GridQueryResponse | null;
displayMode: DisplayMode;
colorMap: ColorMap;
showVelocityArrows: boolean;
arrowScale: number;
vesselLength: number;
vesselRadius: number;
}
/**
* Main vessel canvas with 2D view
*/
export function VesselCanvas({
gridData,
displayMode,
colorMap,
showVelocityArrows,
arrowScale,
vesselLength,
vesselRadius,
}: VesselCanvasProps) {
return (
<Canvas
orthographic
style={{ background: "#1a1a2e" }}
>
<OrthographicCamera
makeDefault
position={[vesselLength / 2, 0, 10]}
zoom={2000}
near={0.1}
far={1000}
/>
<ambientLight intensity={0.8} />
{/* Vessel boundary outline */}
<VesselBoundary length={vesselLength} radius={vesselRadius} />
{/* Field visualization */}
{gridData && (
<>
<FieldMesh
gridData={gridData}
displayMode={displayMode}
colorMap={colorMap}
/>
{showVelocityArrows && (
<VelocityArrows
gridData={gridData}
scale={arrowScale}
/>
)}
</>
)}
{/* Axis indicators */}
<AxisLabels length={vesselLength} radius={vesselRadius} />
</Canvas>
);
}
/**
* Vessel boundary visualization
*/
function VesselBoundary({
length,
radius,
}: {
length: number;
radius: number;
}) {
const points = useMemo(() => {
const pts: THREE.Vector3[] = [];
const segments = 100;
// Top wall
for (let i = 0; i <= segments; i++) {
const x = (i / segments) * length;
pts.push(new THREE.Vector3(x, radius, 0));
}
// Bottom wall (reverse direction for closed path)
for (let i = segments; i >= 0; i--) {
const x = (i / segments) * length;
pts.push(new THREE.Vector3(x, -radius, 0));
}
// Close the path
pts.push(pts[0].clone());
return pts;
}, [length, radius]);
return (
<Line
points={points}
color="#ffffff"
lineWidth={2}
/>
);
}
/**
* Field mesh visualization using vertex colors
*/
function FieldMesh({
gridData,
displayMode,
colorMap,
}: {
gridData: GridQueryResponse;
displayMode: DisplayMode;
colorMap: ColorMap;
}) {
const meshRef = useRef<THREE.Mesh>(null);
const geometry = useMemo(() => {
const [nx, ny] = gridData.dimensions;
const geo = new THREE.PlaneGeometry(
gridData.dx * (nx - 1),
gridData.dy * (ny - 1),
nx - 1,
ny - 1
);
// Set vertex positions to match grid
const positions = geo.attributes.position.array as Float32Array;
for (let j = 0; j < ny; j++) {
for (let i = 0; i < nx; i++) {
const idx = j * nx + i;
const posIdx = idx * 3;
positions[posIdx] = gridData.x_min + i * gridData.dx;
positions[posIdx + 1] = gridData.y_min + j * gridData.dy;
positions[posIdx + 2] = 0;
}
}
geo.attributes.position.needsUpdate = true;
return geo;
}, [gridData]);
// Update colors based on display mode
const colors = useMemo(() => {
let values: number[];
switch (displayMode) {
case "velocity":
values = gridData.u_field.map((u, i) =>
Math.sqrt(u ** 2 + gridData.v_field[i] ** 2)
);
break;
case "pressure":
values = gridData.p_field;
break;
case "wss":
// WSS only makes sense at boundaries, use velocity magnitude fallback
values = gridData.u_field.map((u, i) =>
Math.sqrt(u ** 2 + gridData.v_field[i] ** 2)
);
break;
default:
values = gridData.p_field;
}
// Mask out exterior points
const maskedValues = values.map((v, i) =>
gridData.mask[i] ? v : 0
);
const [min, max] = getRange(maskedValues.filter((_, i) => gridData.mask[i]));
return createColorArray(maskedValues, min, max, colorMap);
}, [gridData, displayMode, colorMap]);
useEffect(() => {
if (geometry) {
geometry.setAttribute(
"color",
new THREE.BufferAttribute(colors, 3)
);
geometry.attributes.color.needsUpdate = true;
}
}, [geometry, colors]);
return (
<mesh ref={meshRef} geometry={geometry}>
<meshBasicMaterial vertexColors side={THREE.DoubleSide} />
</mesh>
);
}
/**
* Velocity arrows using instanced rendering
*/
function VelocityArrows({
gridData,
scale,
}: {
gridData: GridQueryResponse;
scale: number;
}) {
const meshRef = useRef<THREE.InstancedMesh>(null);
const { count, matrix, colors } = useMemo(() => {
const [nx, ny] = gridData.dimensions;
// Subsample for performance (every 4th point)
const step = 4;
const validIndices: number[] = [];
for (let j = 0; j < ny; j += step) {
for (let i = 0; i < nx; i += step) {
const idx = j * nx + i;
if (gridData.mask[idx]) {
validIndices.push(idx);
}
}
}
const count = validIndices.length;
const matrix = new THREE.Matrix4();
const instanceMatrix = new Float32Array(count * 16);
const instanceColors = new Float32Array(count * 3);
const magnitudes = validIndices.map((idx) =>
Math.sqrt(gridData.u_field[idx] ** 2 + gridData.v_field[idx] ** 2)
);
const [minMag, maxMag] = getRange(magnitudes);
for (let i = 0; i < count; i++) {
const idx = validIndices[i];
const x = gridData.x_min + (idx % nx) * gridData.dx;
const y = gridData.y_min + Math.floor(idx / nx) * gridData.dy;
const u = gridData.u_field[idx];
const v = gridData.v_field[idx];
const mag = magnitudes[i];
// Arrow orientation
const angle = Math.atan2(v, u);
const arrowLength = (mag / (maxMag || 1)) * scale * gridData.dx * 3;
matrix.makeRotationZ(angle);
matrix.scale(new THREE.Vector3(arrowLength, arrowLength * 0.3, 1));
matrix.setPosition(x, y, 0.1);
matrix.toArray(instanceMatrix, i * 16);
// Color based on magnitude
const t = maxMag > minMag ? (mag - minMag) / (maxMag - minMag) : 0;
instanceColors[i * 3] = 1 - t;
instanceColors[i * 3 + 1] = t;
instanceColors[i * 3 + 2] = 0.5;
}
return {
count,
matrix: instanceMatrix,
colors: instanceColors,
};
}, [gridData, scale]);
useEffect(() => {
if (meshRef.current) {
const mesh = meshRef.current;
mesh.instanceMatrix.set(matrix);
mesh.instanceMatrix.needsUpdate = true;
if (mesh.instanceColor) {
mesh.instanceColor.set(colors);
mesh.instanceColor.needsUpdate = true;
}
}
}, [matrix, colors]);
// Arrow geometry (simple triangle)
const arrowGeometry = useMemo(() => {
const shape = new THREE.Shape();
shape.moveTo(0, -0.5);
shape.lineTo(1, 0);
shape.lineTo(0, 0.5);
shape.lineTo(0.3, 0);
shape.closePath();
return new THREE.ShapeGeometry(shape);
}, []);
return (
<instancedMesh
ref={meshRef}
args={[arrowGeometry, undefined, count]}
>
<meshBasicMaterial vertexColors />
</instancedMesh>
);
}
/**
* Axis labels and scale
*/
function AxisLabels({
length,
radius,
}: {
length: number;
radius: number;
}) {
// X-axis line
const xAxisPoints = useMemo(
() => [
new THREE.Vector3(-0.005, -radius * 1.5, 0),
new THREE.Vector3(length + 0.005, -radius * 1.5, 0),
],
[length, radius]
);
// Y-axis line
const yAxisPoints = useMemo(
() => [
new THREE.Vector3(-0.005, -radius * 1.3, 0),
new THREE.Vector3(-0.005, radius * 1.3, 0),
],
[radius]
);
return (
<>
<Line points={xAxisPoints} color="#666666" lineWidth={1} />
<Line points={yAxisPoints} color="#666666" lineWidth={1} />
</>
);
}
export default VesselCanvas;