import type { ReactNode } from "react"; import { HAMMER_PRESETS, MATERIAL_PRESETS, PLATE_PRESETS, type ProblemSpec, type ValidationIssue, } from "@plate-dyn/core"; import { NumberField } from "./NumberField"; export type ParameterSection = | "plate" | "material" | "suspension" | "hammer" | "strike" | "probe" | "mic" | "render" | "rr" | "fem"; export const ALL_SECTIONS: ParameterSection[] = [ "plate", "material", "suspension", "hammer", "strike", "probe", "mic", "render", "rr", "fem", ]; export interface ParameterPanelProps { spec: ProblemSpec; onChange: (spec: ProblemSpec) => void; /** Sections to show; default all */ sections?: ParameterSection[]; issues?: ValidationIssue[]; /** Sections open initially; default all */ open?: ParameterSection[]; } /** Controls that edit the spec. Every control maps to one field of the `params` struct. */ export function ParameterPanel(props: ParameterPanelProps) { const { spec, onChange, sections = ALL_SECTIONS, issues = [], open } = props; const set = (patch: Partial) => onChange({ ...spec, ...patch }); const issue = (path: string) => issues.find((i) => i.path === path || i.path.startsWith(path + ".")); const invalid = (path: string) => issue(path)?.severity === "error"; const show = (s: ParameterSection) => sections.includes(s); const isOpen = (s: ParameterSection) => (open ? open.includes(s) : true); const materialPresetId = MATERIAL_PRESETS.find( (p) => p.material.name === spec.material.name && p.material.E === spec.material.E, )?.id; const platePresetId = PLATE_PRESETS.find( (p) => p.plate.width === spec.plate.width && p.plate.height === spec.plate.height && p.plate.thickness === spec.plate.thickness, )?.id; const hammerPresetId = HAMMER_PRESETS.find( (p) => p.hammer.mass === spec.hammer.mass && p.hammer.tip_radius === spec.hammer.tip_radius && p.hammer.E === spec.hammer.E, )?.id; const half = { x: spec.plate.width / 2, y: spec.plate.height / 2 }; return (
{show("plate") && (
{platePresetId && (
{PLATE_PRESETS.find((p) => p.id === platePresetId)?.note}
)} set({ plate: { ...spec.plate, width: v } })} /> set({ plate: { ...spec.plate, height: v } })} /> set({ plate: { ...spec.plate, thickness: v } })} />
)} {show("material") && (
{materialPresetId && (
{MATERIAL_PRESETS.find((p) => p.id === materialPresetId)?.source}
)} set({ material: { ...spec.material, E: v, name: customName(spec.material.name) } }) } /> set({ material: { ...spec.material, nu: v, name: customName(spec.material.name) } }) } /> set({ material: { ...spec.material, rho: v, name: customName(spec.material.name) } }) } /> set({ material: { ...spec.material, Q: v } })} />
)} {show("suspension") && ( set({ hole: { ...spec.hole, diameter: v } })} /> set({ hole: { ...spec.hole, edge_offset: v } })} /> set({ thread: { ...spec.thread, length: v } })} /> set({ thread: { ...spec.thread, dashpot: v } })} />
The thread is a soft point spring at the hole, not a clamp; the plate is free to rotate about it. The dashpot is the loss where the thread rubs the hole.
)} {show("hammer") && (
{hammerPresetId && (
{HAMMER_PRESETS.find((p) => p.id === hammerPresetId)?.note}
)} set({ hammer: { ...spec.hammer, mass: v } })} /> set({ hammer: { ...spec.hammer, tip_radius: v } })} /> set({ hammer: { ...spec.hammer, speed: v } })} /> set({ hammer: { ...spec.hammer, restitution: v } })} /> set({ hammer: { ...spec.hammer, E: v } })} /> set({ hammer: { ...spec.hammer, nu: v } })} />
)} {show("strike") && ( set({ strike: { ...spec.strike, x: v } })} /> set({ strike: { ...spec.strike, y: v } })} />
The centre lies on both symmetry axes and leaves most asymmetric modes silent; a few millimetres off centre wakes them.
)} {show("probe") && ( set({ probe: { ...spec.probe, x: v } })} /> set({ probe: { ...spec.probe, y: v } })} /> )} {show("mic") && ( set({ mic: { ...spec.mic, distance: v } })} /> set({ mic: { ...spec.mic, air_density: v } })} /> set({ mic: { ...spec.mic, sound_speed: v } })} /> )} {show("render") && ( set({ render: { ...spec.render, fs: v } })} /> set({ render: { ...spec.render, duration: v } })} /> set({ render: { ...spec.render, modes: v } })} /> set({ render: { ...spec.render, normalize_dbfs: v } })} /> set({ render: { ...spec.render, nframes: v } })} /> set({ render: { ...spec.render, frame_span: v } })} /> )} {show("rr") && ( set({ rr: { ...spec.rr, basis_order: v } })} /> set({ rr: { ...spec.rr, quadrature_order: v } })} /> set({ rr: { ...spec.rr, grid: v } })} /> )} {show("fem") && ( set({ fem: { ...spec.fem, mesh_x: v } })} /> set({ fem: { ...spec.fem, mesh_y: v } })} />
Full-integration Q4 locks in shear on a thin plate: the reference used 80 × 80, where the main partial is 1 percent above Rayleigh-Ritz; 48 × 48 is 3 percent above and solves in a few seconds, 80 × 80 in tens of seconds.
)}
); } function customName(name: string): string { return name.endsWith(" (edited)") ? name : `${name} (edited)`; } function Group(props: { title: string; prefix: string; issues: ValidationIssue[]; open: boolean; disabled?: boolean; children: ReactNode; }) { const prefixes = props.prefix === "hole" ? ["hole", "thread"] : [props.prefix]; const related = props.issues.filter((i) => prefixes.some((p) => i.path === p || i.path.startsWith(p + ".")), ); return (
{props.title} {related.some((i) => i.severity === "error") && ( check )} {props.children} {related.map((i) => (
{i.path}: {i.message}
))}
); }