import { useEffect, useMemo, useRef, useState } from "react"; import { prepareAudio, rangeOf, symmetricRange, type Field2D, type FieldFrames, type MethodDefinition, type PlateResult, type ProblemSpec, type Range, } from "@plate-dyn/core"; import { AudioPlayer, FieldView, FramePlayer, LinePlot, MovieExport, Plate3DView, ValidityNotes, useFramePlayer, useSharedAxis, type FieldArrow, type FieldMarker, type FieldRect, type MovieOverlay, type Plate3DHandle, } from "@plate-dyn/components"; type Quantity = "w" | "w_total" | "vm" | "sp" | "env_vm" | "env_sp" | "w0" | "nvm"; const QUANTITIES: { id: Quantity; label: string; unit: string; scale: number; symmetric: boolean; frames: boolean; }[] = [ { id: "w", label: "Flexural displacement", unit: "µm", scale: 1e6, symmetric: true, frames: true, }, { id: "w_total", label: "Displacement with the rigid-body motion", unit: "mm", scale: 1e3, symmetric: true, frames: true, }, { id: "vm", label: "Von Mises stress (surface)", unit: "MPa", scale: 1e-6, symmetric: false, frames: true, }, { id: "sp", label: "Principal stress, signed (surface)", unit: "MPa", scale: 1e-6, symmetric: true, frames: true, }, { id: "env_vm", label: "Peak von Mises over time", unit: "MPa", scale: 1e-6, symmetric: false, frames: false, }, { id: "env_sp", label: "Peak |principal stress| over time", unit: "MPa", scale: 1e-6, symmetric: false, frames: false, }, { id: "w0", label: "Static sag under gravity", unit: "mm", scale: 1e3, symmetric: true, frames: false, }, { id: "nvm", label: "Self-weight in-plane stress (von Mises)", unit: "kPa", scale: 1e-3, symmetric: false, frames: false, }, ]; export interface ResultsViewProps { result: PlateResult; spec: ProblemSpec; method: MethodDefinition; fieldWidth?: number; } /** The plate in its mount, the fields, the rigid-body and support series, the probe, the sound. */ export function ResultsView({ result, spec, method, fieldWidth = 460 }: ResultsViewProps) { const [quantity, setQuantity] = useState("w"); const [rangeMode, setRangeMode] = useState<"global" | "frame">("global"); const [flexScale, setFlexScale] = useState(200); const [rigidScale, setRigidScale] = useState(1); const q = QUANTITIES.find((x) => x.id === quantity)!; const nf = result.frames.t.length; const player = useFramePlayer(nf, 30, false); const startFrame = useMemo(() => peakFrame(result.frames.w), [result]); const setIndex = player.setIndex; useEffect(() => setIndex(startFrame), [startFrame, setIndex]); const sceneRef = useRef(null); const nvm = useMemo(() => inPlaneVonMises(result), [result]); const frames: FieldFrames | undefined = quantity === "w" ? result.frames.w : quantity === "w_total" ? result.frames.w_total : quantity === "vm" ? result.frames.vm : quantity === "sp" ? result.frames.sp : undefined; const field: Field2D | undefined = quantity === "env_vm" ? result.envelope.vm : quantity === "env_sp" ? result.envelope.sp : quantity === "w0" ? result.static.w0 : quantity === "nvm" ? nvm : undefined; const tFrame = result.frames.t[Math.min(player.index, nf - 1)] ?? 0; // the 3D surface is coloured by the same quantity when it is a frame stack const colorRange: Range | undefined = useMemo(() => { if (!frames) return undefined; const r = rangeOf(frames.data); return q.symmetric ? symmetricRange(r) : r; }, [frames, q.symmetric]); const markers: FieldMarker[] = useMemo(() => { const m: FieldMarker[] = [ { x: spec.excitation.x, y: spec.excitation.y, label: spec.excitation.type === "impulse" ? "strike" : "pull", color: "#d1495b", }, { x: result.probe.x, y: result.probe.y, label: "probe", color: "#1f5fa8" }, ]; const mt = spec.mount; if (mt.type === "thread") m.push({ x: mt.x, y: mt.y, label: "hole", color: "#2a9d8f" }); if (mt.type === "threads") { m.push({ x: mt.x1, y: mt.y1, label: "hole 1", color: "#2a9d8f" }); m.push({ x: mt.x2, y: mt.y2, label: "hole 2", color: "#2a9d8f" }); } if (mt.type === "pinched") m.push({ x: mt.x, y: mt.y, label: "fingers", color: "#2a9d8f" }); if (mt.type === "cymbal") m.push({ x: spec.plate.Lx / 2, y: spec.plate.Ly / 2, label: "felts", color: "#2a9d8f" }); return m; }, [spec, result.probe.x, result.probe.y]); const rects: FieldRect[] = useMemo(() => { const mt = spec.mount; if (mt.type === "pads") return mt.pads.map((p, k) => ({ x0: p.x - p.rx, y0: p.y - p.ry, x1: p.x + p.rx, y1: p.y + p.ry, label: `pad ${k + 1}`, })); if (mt.type === "vise") { const { Lx, Ly } = spec.plate; const d = mt.depth; const r = mt.edge === "x0" ? { x0: 0, y0: 0, x1: d, y1: Ly } : mt.edge === "x1" ? { x0: Lx - d, y0: 0, x1: Lx, y1: Ly } : mt.edge === "y0" ? { x0: 0, y0: 0, x1: Lx, y1: d } : { x0: 0, y0: Ly - d, x1: Lx, y1: Ly }; return [{ ...r, label: "jaws" }]; } return []; }, [spec]); const arrow: FieldArrow | undefined = useMemo(() => { const s = result.static; if (s.g === 0) return undefined; if (s.gx === 0 && s.gy === 0) return undefined; return { x: s.xc, y: s.yc, dx: s.gx, dy: s.gy, label: "down" }; }, [result.static]); const onPlotClick = (t: number) => { let best = 0; for (let k = 1; k < nf; k++) if (Math.abs(result.frames.t[k] - t) < Math.abs(result.frames.t[best] - t)) best = k; player.setIndex(best); player.setPlaying(false); }; const axis = useSharedAxis(); const spectrumAxis = useSharedAxis(); const audioT = useMemo( () => Float64Array.from({ length: result.audio.p.length }, (_, i) => i / result.audio.fs), [result.audio], ); const rigidUnits = result.rigid.names.map((_, k) => (k === 0 ? "mm" : "mrad")); const rigidColors = ["#1f5fa8", "#d1495b", "#2a9d8f"]; const supportColors = [ "#1f5fa8", "#d1495b", "#2a9d8f", "#e9a300", "#7b4fa6", "#555", "#a0522d", "#008b8b", ]; const orientation = describeOrientation(result, spec); // The movie records the scene as it stands: same orientation, same colouring, // the sound the player and the WAV export use, and a caption that says what // is drawn and by how much it is exaggerated. const audioPrep = useMemo(() => prepareAudio(result.audio.p, result.audio.fs), [result.audio]); const movieSource = useMemo( () => ({ canvas: () => sceneRef.current?.canvas() ?? null, drawFrame: (k: number, size?: { width: number; height: number }) => sceneRef.current?.drawFrame(k, size), }), [], ); const movieOverlay: MovieOverlay = useMemo( () => ({ title: `${describePlate(spec)}, ${MOUNT_TEXT[spec.mount.type]}`, note: `flexure ×${flexScale}, rigid motion ×${rigidScale}`, legend: frames && colorRange ? { label: q.label, unit: q.unit, min: colorRange.min, max: colorRange.max, scale: q.scale, colormap: q.symmetric ? "bwr" : "viridis", } : undefined, }), [spec, flexScale, rigidScale, frames, colorRange, q], ); return (
The plate in its mount
drag to orbit, wheel to zoom
on && player.setPlaying(false)} />
{orientation} The static sag and the flexural field are exaggerated by the first factor, the swing and sway by the second (1 is true scale); the colour is the quantity chosen below.
{frames && }
{frames && ( )} {!result.info.stable && ( diverged at step {result.info.diverged_step}: frames stop there )}
{spec.plate.shape === "ellipse" && (
The grid spans the box around the ellipse; the edge is a staircase of grid nodes, drawn here clipped to the true outline. Stress on the outermost ring is copied from the nearest node with a full stencil.
)} {result.rigid.nr > 0 && ( <> ({ x: result.t, y: s, label: `${result.rigid.names[k]} [${rigidUnits[k]}]`, color: rigidColors[k], }))} yScale={1e3} xLabel="time [s]" yLabel="mm, mrad" yUnit="" cursorX={tFrame} onClick={onPlotClick} height={170} {...axis} />
The translation along the normal at the pivot ( {(result.rigid.pivot.x * 1000).toFixed(0)}, {(result.rigid.pivot.y * 1000).toFixed(0)}) mm in millimetres, the two rotations in milliradians; all measured from the static equilibrium. Rigid-body modes of this mount:{" "} {result.rigid.hz .map( (h, k) => `${Number.isFinite(h) ? h.toFixed(2) + " Hz" : "unstable"} (${modeName(result, k)})`, ) .join(", ")} .
)} {result.supports.names.length > 0 && ( <> ({ x: result.t, y: s, label: result.supports.names[k], color: supportColors[k % supportColors.length], }))} xLabel="time [s]" yLabel="F [N]" yUnit="N" cursorX={tFrame} onClick={onPlotClick} height={150} {...axis} />
Positive pushes the plate towards +z (a pad or the lower felt), negative pulls it back (a thread's transverse spring, the upper felt). At rest:{" "} {result.supports.names .map((n, k) => `${n} ${result.supports.static[k].toFixed(2)} N`) .join(", ")} . A contact at zero has lost touch with the plate.
)}
Frequency resolution {(1 / (result.t[result.t.length - 1] - result.t[0])).toFixed(2)} Hz from a {(result.t[result.t.length - 1] - result.t[0]).toFixed(2)} s record. The pendulum modes sit at the far left; zoom in to see them.
Radiated sound at the listener
{result.audio.model === "unbaffled" ? "Unbaffled estimate: the back face's contribution, taken around the nearest edge, cancels the front at low frequency. " : "Rayleigh integral in an infinite baffle. "} Listener {spec.listener.z} m from the plate; propagation delay of the nearest patch removed.
What this result idealizes
); } const MOUNT_TEXT: Record = { thread: "hanging from one thread", threads: "hanging from two threads", pads: "resting on pads", cymbal: "on a cymbal stand", vise: "one edge in a vise", pinched: "pinched between fingers", }; /** The stock, for the movie's title: "300 × 200 × 2 mm sheet, A36 structural steel". */ function describePlate(spec: ProblemSpec): string { const p = spec.plate; const mm = (x: number) => Number((x * 1000).toPrecision(3)).toString(); const round = p.shape === "ellipse" && p.Lx === p.Ly; const noun = p.shape === "rectangle" ? "sheet" : round ? "disk" : "ellipse"; const dims = round ? `${mm(p.Lx)} × ${mm(p.h)}` : `${mm(p.Lx)} × ${mm(p.Ly)} × ${mm(p.h)}`; return `${dims} mm ${noun}, ${spec.material.name}`; } function captionFor(q: Quantity): string { switch (q) { case "w": return "The flexural part of the departure from rest: the rigid-body swing and sway are taken out. Positive along the plate normal."; case "w_total": return "Flexural field plus the rigid-body motion, measured from the static equilibrium; the swing dominates on a hung plate."; case "vm": case "sp": return "Surface stress at z = h/2 from the curvature of the flexural field; the opposite face carries the opposite sign."; case "env_vm": case "env_sp": return "Peak over the saved frames, so a sample of the true peak. Surface z = h/2."; case "w0": return "Deflection at rest under gravity: the sag of a horizontal plate into its pads or from its clamp. A hanging plate has none."; case "nvm": return "Von Mises equivalent of the in-plane stress the plate's own weight puts in it, from the plane-stress solve; largest near the hole or the jaws."; } } function modeName(result: PlateResult, k: number): string { return result.rigid.modeNames[k] ?? `mode ${k + 1}`; } function describeOrientation(result: PlateResult, spec: ProblemSpec): string { const s = result.static; const m = spec.mount; if (s.g === 0) return "Gravity is off."; if (m.type === "thread" || m.type === "threads") return `The plate hangs vertically; it is turned ${(Math.atan2(s.gx, -s.gy) * (180 / Math.PI)).toFixed(1)}° in its plane so its centre of mass is below the ${m.type === "thread" ? "hole" : "line of the holes"}.`; if (m.type === "pads" || m.type === "cymbal") return "The plate lies horizontal on its supports."; if (m.type === "vise") return `The plate is tilted ${m.tilt}° from hanging straight down out of the jaws.`; return `The plate is held at ${m.tilt}° from hanging straight below the fingers.`; } /** Von Mises equivalent of the self-weight stress resultants, per unit thickness. */ function inPlaneVonMises(result: PlateResult): Field2D | undefined { const s = result.static; if (!s.Nxx || !s.Nyy || !s.Nxy) return undefined; const n = s.Nxx.data.length; const out = new Float64Array(n); for (let i = 0; i < n; i++) { const a = s.Nxx.data[i]; const b = s.Nyy.data[i]; const c = s.Nxy.data[i]; out[i] = Math.sqrt(a * a - a * b + b * b + 3 * c * c); } return { nx: s.Nxx.nx, ny: s.Nxx.ny, data: out }; } /** * The frame of largest flexural displacement. The player starts here so a run * that has just finished shows the plate in motion. */ function peakFrame(f: FieldFrames | undefined): number { if (!f) return 0; const n = f.nx * f.ny; let best = 0; let bestv = -1; for (let k = 0; k < f.nf; k++) { let v = 0; for (let i = k * n; i < (k + 1) * n; i++) { const a = Math.abs(f.data[i]); if (a > v) v = a; } if (v > bestv) { bestv = v; best = k; } } return best; }