Imported from bonsai/hermes-skills (
software-development/hardware-patch-simulator/SKILL.md). Install upstream withnpx skills add bonsai/hermes-skills --skill hardware-patch-simulator. Copyright stays with the author.
Hardware Patch Simulator
Architecture
Two views share one JSON config:
- SVG schematic view (Pd homage): draggable nodes, port-to-port Bezier cables, rule-based validation.
- Three.js real view (future): meshes generated from the same JSON units array with port transforms.
JSON Config Schema (authoritative)
The config.json lives in public/ and acts as the single source of truth.
{
"version": "1.0",
"layout": "vertical_flow",
"style": {
"deviceFill": "#111",
"deviceStroke": "#444",
"bgColor": "#0a0a0a",
"gridStep": 20,
"gridColor": "#222"
},
"units": [
{
"id": "tt1",
"name": "Turntable L",
"category": "source",
"ports": [
{ "id": "tt1_l", "label": "L", "side": "bottom", "xoff": 50, "yoff": 0, "type": "rca", "signal": "phono", "color": "#2196f3" }
],
"init": { "x": 80, "y": 40, "w": 130, "h": 100 }
}
],
"rules": {
"phono_to_phono": { "ok": true },
"line_to_phono": { "ok": false, "reason": "RIAA補正重複で音が変" }
},
"signal_flow": { "source": [...], "mixer": [...], "amp": [...], "output": [...] }
}
Key fields explained:
units[].ports[].side:"top"|"bottom"|"left"|"right"— determines port placement relative to device rect.xoff/yoff: offset from the device's x/y origin based on side. Fortop/bottom,xoff= horizontal inset;yoff= 0 (ignored). Forleft/right,yoff= vertical inset;xoff= 0.type:"rca"|"xlr"|"jack35"|"terminal"|"speaker"— visual shape.signal:"phono"|"line"|"mic"|"ground"|"speaker"— rule engine category.color: hex color override for port circle.
SVG Schematic View (React/SVG)
Component split
PatchCanvas.jsx— main SVG, event wiring, Bezier path math, cable state.DeviceNode.jsx— one<g>per unit, ports as child<g>with individual event handlers.BezierCable.jsx—<path>with hover state and click-to-delete.
Bezier path formula (vertical-flow layout)
function bezier(a, b) {
const dy = Math.max(Math.abs(b.y - a.y) * 0.5, 30);
return `M${a.x} ${a.y} C${a.x} ${a.y + dy}, ${b.x} ${b.y - dy}, ${b.x} ${b.y}`;
}
Port coordinate math
function getPortXY(dev, port) {
let cx, cy;
switch (port.side) {
case 'left': cx = 0; cy = port.yoff; break;
case 'right': cx = dev.w; cy = port.yoff; break;
case 'top': cx = port.xoff; cy = 0; break;
case 'bottom': cx = port.xoff; cy = dev.h; break;
}
return { x: dev.x + cx, y: dev.y + cy };
}
Device dragging
Store { id, ox, oy, x, y } on mousedown, update dev.x/y by delta in mousemove.
Cable interaction
mousedownon port → start temporary dashed line.mouseupon other port → validate + persist.clickon existing cable → remove it.mousemovewhile dragging temp line → redraw Bezier to cursor.
Validation engine
Two-port lookup by signal pair, then consult rules object:
- Exact signal-pair key (
phono_to_phoeno,line_to_phono, etc.) → return rule. - Same signal (
sf === st) → auto-OK. - Everything else → auto-NG with generic reason.
Include port-type visual restrictions (e.g. XLR male/female shape) as OPTIONAL — the rule engine should protect against port-shape mismatch even if the user force-connects.
Steps (new project)
mkdir audio-patch-simulator && cd $_npm install next react react-dom- Create
next.config.mjswithoutput: 'export',images: { unoptimized: true }. - Put
public/config.jsonwith full units/rules definition. - Create
app/layout.jsx(root layout, dark background). - Create
app/page.jsx(toolbar + PatchCanvas mount). - Create
components/PatchCanvas.jsx,DeviceNode.jsx,BezierCable.jsx. next build→ generatesout/(ordist/per config).- Deploy
dist/to Vercel or host as static.
Extending to Three.js real view
The same config.json can generate a 3D scene:
- Each
unit.id→THREE.Groupwith matching dimensions. - Each
port.id→THREE.Object3Dmarker at computed local position. - Cables →
THREE.CatmullRomCurve3with tube geometry. - Port type → custom GLTF geometry (RCA jack, XLR, etc.).
- Patch state → same JSON exported from the SVG view loaded into Three.js.
See references/ for the full JSON schema and a React implementation snippet.
Pitfalls
- Port visibility and cable filtering: when a source unit is toggled off, HIDE both the device
<g>(e.g.display: 'none') AND any cables connected to its ports (c.elem.style.display = 'none'). Otherwise ghost cables float in space connected to invisible ports. The_hiddenflag on the device object is the simplest gate: filter withdevices.filter(d => !d._hidden)before rendering. useImperativeHandlefor canvas methods: the parentpage.jsxneedsclearCables(),checkAll(),exportPatch(),importPatch(). Wrap the canvas inforwardRefand expose viauseImperativeHandle(ref, () => ({ clearCables: ..., checkAll: ..., exportPatch: ..., importPatch: ... }));. Then consume withconst canvasRef = useRef(null); <PatchCanvas ref={canvasRef} .../>; canvasRef.current?.clearCables().- Event delegation vs direct handlers: SVG event bubbling means
mousedownon a port MUSTe.stopPropagation()or it also triggers device drag. Put the handler on the port<g>group, not the document root.touchstarton ports also needsstopPropagation+preventDefaultto prevent browser scrolling. - Cable color auto-detection by channel: derive L/R/GND color at creation time, not render time. Check
from.includes('_l') || to.includes('_l')for L (blue),'_r'for R (red),signal === 'ground'for GND (grey). Store the resolved color in the cable object soredrawCables()doesn't need to recompute. - PATCH I/O JSON format: export
{ devices: [{id, x, y}], cables: [{from, to}] }. Import reads positions back intodevicesand recreatescablesas new objects (re-run validation on each). Do NOT serialize raw SVG elements or internal React state. - Touch events on SVG: add
{ passive: false }totouchmove/touchstartand callpreventDefault()during cable drag — otherwise the browser scrolls instead of drawing the cable. - Multi-file deploy for static export: ensure
public/config.jsonis copied toout/. Next.js static export handles this automatically, but verifyout/config.jsonexists before deploying. - Vercel deploy from WSL: when
vercelCLI is installed on Windows side (/mnt/c/.../npm/vercel),npx vercelin WSL picks it up correctly; justcdinto the project dir and runnpx vercel. No powershell wrapper needed for simple staticout/deploys. When using Next.js static export (output: 'export'), the deploy target is the generatedout/ordist/directory. If a parent/repo-rootvercel.jsonexists, deploy from the subproject dir withvercel --yes. - Dist artifact hygiene: only ship the static export (
out/ordist/) —_next/staticJS/CSS,index.html,config.json. Do NOT include.next/(build cache),index.txt, or timestamped patch JSONs in the deploy artifact. Keep patch JSONs in repo root for PATCH書出し/PATCH読込 feature.
Templates
templates/config.json — minimal 3-device starter (TT → Mixer → Amp).
templates/PatchCanvas.jsx — bare-bones SVG canvas component without validation, ready to plug config into.
Reference
references/json-schema.md — full config schema with all field types, signal enums, and rule keys.
references/vertical-layout-math.md — port positioning math for top/bottom/left/right sides.
references/threejs-extension-notes.md — notes on using the same JSON to spawn Three.js scene graph.