/ proof-of-concept / plate-mechanics-sim
Sign in
proof-of-concept / plate-mechanics-sim
plate-mechanics-sim / packages / components / src / field / FieldView.tsx
190 lines · 5.8 KBCodeBlameHistory
2546961Plate mechanics lab: Kirchhoff-Love plates solved by numbl in the browserJeremy Magland 1import { useEffect, useMemo, useRef } from "react";
2import {
3 frameOf,
4 getColormap,
5 rangeOf,
6 rasterizeField,
7 symmetricRange,
8 type Field2D,
9 type FieldFrames,
12} from "@plate-lab/core";
13import { Colorbar, fmt } from "./Colorbar";
15export interface FieldMarker {
16 x: number;
17 y: number;
18 label: string;
19 color?: string;
22export interface FieldViewProps {
23 /** Either a single field or a frame stack with an index */
24 field?: Field2D;
25 frames?: FieldFrames;
26 frameIndex?: number;
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 27 /** Physical extents of the bounding box [m] */
29 Ly: number;
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 30 /** Outline of the plate within the box; default rectangle */
31 shape?: PlateShape;
33 unit?: string;
34 /** Display multiplier applied to values in labels (e.g. 1e-6 for MPa) */
35 scale?: number;
36 colormap?: string;
37 /** "frame": rescale per frame; "global": one range over all frames; or a fixed range */
38 range?: "frame" | "global" | Range;
39 /** Force a symmetric range about zero (for signed fields) */
40 symmetric?: boolean;
41 markers?: FieldMarker[];
42 /** Canvas width in CSS pixels; height follows the aspect ratio */
43 width?: number;
44 caption?: string;
47/**
48 * Paints a field on the plate outline. Values pass straight from the solver
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 49 * through a colormap; nothing here changes them. Nodes the solver marked NaN
50 * (outside an elliptical plate) come out transparent, and the image is clipped
51 * to the plate's true outline.
53export function FieldView(props: FieldViewProps) {
55 frames,
56 frameIndex = 0,
57 Lx,
58 Ly,
59 shape = "rectangle",
60 title,
61 unit,
62 scale = 1,
63 colormap,
64 symmetric,
65 markers = [],
66 width = 420,
67 caption,
68 } = props;
69 const field: Field2D | undefined =
70 props.field ?? (frames ? frameOf(frames, Math.min(frameIndex, frames.nf - 1)) : undefined);
2546961Plate mechanics lab: Kirchhoff-Love plates solved by numbl in the browserJeremy Magland 71 const cmId = colormap ?? (symmetric ? "bwr" : "viridis");
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 72 const ellipse = shape === "ellipse";
74 const globalRange = useMemo(() => {
75 const r = frames ? rangeOf(frames.data) : field ? rangeOf(field.data) : { min: 0, max: 1 };
76 return symmetric ? symmetricRange(r) : r;
77 }, [frames, field, symmetric]);
79 const range: Range = useMemo(() => {
80 const mode = props.range ?? "global";
81 if (typeof mode === "object") return mode;
82 if (mode === "global" || !field) return globalRange;
83 const r = rangeOf(field.data);
84 return symmetric ? symmetricRange(r) : r;
85 }, [props.range, field, globalRange, symmetric]);
87 const canvasRef = useRef<HTMLCanvasElement>(null);
88 const offRef = useRef<HTMLCanvasElement | null>(null);
89 const pixRef = useRef<Uint8ClampedArray | undefined>(undefined);
91 const height = Math.max(60, Math.round((width * Ly) / Lx));
93 useEffect(() => {
94 const canvas = canvasRef.current;
95 if (!canvas || !field) return;
96 const ctx = canvas.getContext("2d");
97 if (!ctx) return;
98 const dpr = Math.min(2, window.devicePixelRatio || 1);
99 canvas.width = Math.round(width * dpr);
100 canvas.height = Math.round(height * dpr);
101 // Rasterize at grid resolution, then scale with smoothing onto the plate outline
102 if (!offRef.current) offRef.current = document.createElement("canvas");
103 const off = offRef.current;
104 off.width = field.nx;
105 off.height = field.ny;
106 const pix = rasterizeField(field, range, getColormap(cmId), pixRef.current);
107 pixRef.current = pix;
108 const octx = off.getContext("2d");
109 if (!octx) return;
111 new ImageData(pix as Uint8ClampedArray<ArrayBuffer>, field.nx, field.ny),
112 0,
113 0,
114 );
116 ctx.scale(dpr, dpr);
117 ctx.clearRect(0, 0, width, height);
118 ctx.imageSmoothingEnabled = true;
120 ctx.beginPath();
121 ctx.ellipse(width / 2, height / 2, width / 2 - 0.75, height / 2 - 0.75, 0, 0, 2 * Math.PI);
122 };
123 if (ellipse) {
124 ctx.save();
125 outline();
126 ctx.clip();
127 }
2546961Plate mechanics lab: Kirchhoff-Love plates solved by numbl in the browserJeremy Magland 128 // Grid nodes sit at cell centres of the image; map node (0..nx-1) to the full extent
129 const sx = width / (field.nx - 1);
130 const sy = height / (field.ny - 1);
131 ctx.drawImage(off, -sx / 2, -sy / 2, width + sx, height + sy);
134 ctx.lineWidth = 1;
136 outline();
137 ctx.stroke();
138 } else {
139 ctx.strokeRect(0.5, 0.5, width - 1, height - 1);
140 }
142 const px = (m.x / Lx) * width;
143 const py = height - (m.y / Ly) * height;
144 ctx.beginPath();
145 ctx.arc(px, py, 5, 0, 2 * Math.PI);
146 ctx.fillStyle = "rgba(255,255,255,0.85)";
147 ctx.fill();
148 ctx.strokeStyle = m.color ?? "#000";
149 ctx.lineWidth = 1.5;
150 ctx.stroke();
151 ctx.font = "11px system-ui, sans-serif";
152 ctx.fillStyle = "#000";
153 ctx.strokeStyle = "rgba(255,255,255,0.9)";
154 ctx.lineWidth = 3;
155 ctx.strokeText(m.label, px + 8, py - 6);
156 ctx.fillText(m.label, px + 8, py - 6);
157 }
158 ctx.restore();
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 159 }, [field, range, cmId, width, height, markers, Lx, Ly, ellipse]);
161 const peak = field ? rangeOf(field.data) : null;
163 return (
164 <div className="pl-fieldview">
165 <div className="pl-fieldview-head">
166 <strong>{title}</strong>
167 {peak && (
168 <span className="pl-muted pl-mono">
169 {fmt(peak.min * scale)} to {fmt(peak.max * scale)} {unit}
170 </span>
171 )}
172 </div>
173 <div className="pl-fieldview-body">
175 ref={canvasRef}
176 style={{
177 width,
178 height,
179 aspectRatio: `${Lx} / ${Ly}`,
180 border: ellipse ? "none" : undefined,
181 background: ellipse ? "transparent" : undefined,
182 }}
183 aria-label={title}
184 />
2546961Plate mechanics lab: Kirchhoff-Love plates solved by numbl in the browserJeremy Magland 185 <Colorbar range={range} colormap={cmId} unit={unit} scale={scale} height={height} />
186 </div>
187 {caption && <div className="pl-small pl-muted">{caption}</div>}
188 </div>
189 );
↑↓ move↵openescclose