import { useEffect, useMemo, useState } from "react"; import type { FieldFrames, MethodDefinition, PlateResult, ProblemSpec } from "@plate-lab/core"; import { AudioPlayer, FieldView, FramePlayer, LinePlot, ValidityNotes, useFramePlayer, useSharedAxis, type FieldMarker, } from "@plate-lab/components"; type Quantity = "w" | "vm" | "sp" | "env_vm" | "env_sp"; const QUANTITIES: { id: Quantity; label: string; unit: string; scale: number; symmetric: boolean; }[] = [ { id: "w", label: "Displacement w", unit: "µm", scale: 1e6, symmetric: true }, { id: "vm", label: "Von Mises stress (surface)", unit: "MPa", scale: 1e-6, symmetric: false }, { id: "sp", label: "Principal stress, signed (surface)", unit: "MPa", scale: 1e-6, symmetric: true, }, { id: "env_vm", label: "Peak von Mises over time", unit: "MPa", scale: 1e-6, symmetric: false }, { id: "env_sp", label: "Peak |principal stress| over time", unit: "MPa", scale: 1e-6, symmetric: false, }, ]; export interface ResultsViewProps { result: PlateResult; spec: ProblemSpec; method: MethodDefinition; fieldWidth?: number; } /** Fields, probe series, spectrum, sound and validity for one result. */ export function ResultsView({ result, spec, method, fieldWidth = 460 }: ResultsViewProps) { // Displacement first: it is the primary unknown, and the stresses below are derived from it. const [quantity, setQuantity] = useState("w"); const [rangeMode, setRangeMode] = useState<"global" | "frame">("global"); const q = QUANTITIES.find((x) => x.id === quantity)!; const nf = result.frames.t.length; // The animation waits to be started; a run that finishes should not take over the screen. const player = useFramePlayer(nf, 15, false); // A paused animation would otherwise open on t = 0, which is a blank plate. const startFrame = useMemo(() => peakFrame(result.frames.w), [result]); const setIndex = player.setIndex; useEffect(() => setIndex(startFrame), [startFrame, setIndex]); const frames: FieldFrames | undefined = quantity === "w" ? result.frames.w : quantity === "vm" ? result.frames.vm : quantity === "sp" ? result.frames.sp : undefined; const field = quantity === "env_vm" ? result.envelope.vm : quantity === "env_sp" ? result.envelope.sp : undefined; const tFrame = result.frames.t[Math.min(player.index, nf - 1)] ?? 0; const supportX = result.info.extra.support_x; const supportY = result.info.extra.support_y; const markers: FieldMarker[] = useMemo(() => { const m: FieldMarker[] = [ { x: spec.excitation.x, y: spec.excitation.y, label: "strike", color: "#d1495b" }, { x: result.probe.x, y: result.probe.y, label: "probe", color: "#1f5fa8" }, ]; // a centre-pinned plate reports the node it is held at if (typeof supportX === "number" && typeof supportY === "number") m.push({ x: supportX, y: supportY, label: "support", color: "#2a9d8f" }); return m; }, [spec.excitation.x, spec.excitation.y, result.probe.x, result.probe.y, supportX, supportY]); 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 fNyq = result.info.fs / 2; const axis = useSharedAxis(); const spectrumAxis = useSharedAxis(); const audioT = useMemo( () => Float64Array.from({ length: result.audio.p.length }, (_, i) => i / result.audio.fs), [result.audio], ); return (
{frames && ( )} {!result.info.stable && ( diverged at step {result.info.diverged_step}: frames stop there )}
{spec.plate.shape === "ellipse" && result.method !== "modal" && (
The solver's 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.
)} {frames && }
Frequency resolution {(1 / (result.t[result.t.length - 1] - result.t[0])).toFixed(1)} Hz from a {((result.t[result.t.length - 1] - result.t[0]) * 1000).toFixed(1)} ms record. {result.method === "fd" ? " The finite-difference record is short, so peaks are broad." : ""}
Radiated sound at the listener
Rayleigh integral, listener {spec.listener.z} m above the plate;{" "} {method.supports.audio === "clip" ? "only the simulated window is available, so this is a short clip." : "propagation delay of the nearest patch removed."}
{result.modes && }
What this result idealizes
); } /** * The frame of largest displacement. The player starts here so a run that has just * finished shows the plate in motion rather than the flat state before the strike. */ 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; } function ModesTable({ result }: { result: PlateResult }) { const m = result.modes!; const n = Math.min(12, m.f_hz.length); return (
Modes used: {m.f_hz.length} below {result.info.extra.fmax} Hz {m.truncated ? " (truncated)" : ""} {Array.from({ length: n }, (_, k) => ( ))}
(m, n) f [Hz] ζ = σ/ω T60 [s] force factor G
({m.m[k]}, {m.n[k]}) {m.f_hz[k].toFixed(1)} {(m.sigma[k] / (2 * Math.PI * m.f_hz[k])).toExponential(2)} {(6.91 / m.sigma[k]).toFixed(2)} {m.G[k].toExponential(2)}
First {n} of {m.f_hz.length}. T60 is the time for a mode to decay by 60 dB under this damping model.
); }