Files
Wiring-Designer/frontend/index.html
T
Kyle 82d8570767 Add formboard harness layout designer
Separate read-only canvas for physically laying out wire harnesses.
Syncs connectors from the main diagram (one-way), no changes flow back.

Backend:
- FormboardLayout, FormboardNode, FormboardSegment models
- /api/formboard/ router: get/create layout, sync connectors from diagram,
  full CRUD for nodes and segments

Frontend (formboard.js):
- Konva canvas: drag nodes, click-to-connect segments, zoom/pan
- Node types: Connector (blue rect, synced from diagram), Branch (green
  circle, manual), Splice/joint (orange diamond, manual)
- Segments scale thickness by wire count; fitting overlays for open,
  split loom, conduit, heat shrink, tape wrap
- Right-click context menu for delete on nodes/segments

App integration:
- "⊞ Formboard" tab in view-tab-bar (distinct color, independent of views)
- Sub-toolbar: Select / Branch / Splice / Connect / Sync / Fit / Delete
- Right panel swaps to formboard props when active:
  - Node: label, notes, linked device info
  - Segment: fitting type, fitting color, label, length, wire checklist
    (checkboxes for every wire in the main diagram)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 07:18:43 -04:00

538 lines
26 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WireDraw — Wiring Diagram Maker</title>
<link rel="stylesheet" href="/css/main.css">
</head>
<body>
<div id="app">
<!-- ── Toolbar ──────────────────────────────────────────────────────────── -->
<header id="toolbar">
<div class="toolbar-left">
<span class="app-title">⚡ WireDraw</span>
<input id="diagram-name" type="text" class="diagram-name-input" placeholder="Diagram name…">
</div>
<div class="toolbar-center">
<!-- Mode -->
<button id="btn-select" class="mode-btn active" title="Select mode (S)">▸ Select</button>
<button id="btn-wire" class="mode-btn" title="Draw wire: drag from pin to pin (W)">∿ Wire</button>
<div class="sep"></div>
<!-- Routing -->
<span class="toolbar-label">Route:</span>
<button id="btn-route-ortho" class="route-btn active" title="Orthogonal (right-angle) routing">⌐ Ortho</button>
<button id="btn-route-direct" class="route-btn" title="Direct (straight line) routing">⤢ Direct</button>
<button id="btn-route-curved" class="route-btn" title="Curved (smooth bezier) routing">∿ Curved</button>
<div class="sep"></div>
<!-- Actions -->
<button id="btn-jumps" title="Toggle wire jump arcs at crossings" class="jumps-btn active">⌒ Jumps</button>
<button id="btn-snap" title="Toggle grid snap (G)" class="snap-btn">⊹ Snap</button>
<button id="btn-harness" title="Toggle harness view (H)">⌇ Harness</button>
<button id="btn-fit" title="Fit canvas to devices (F)">⊞ Fit</button>
<button id="btn-duplicate" title="Duplicate selected (Ctrl+D)">⊕ Dupe</button>
<button id="btn-undo" title="Undo (Ctrl+Z)">↩ Undo</button>
<button id="btn-delete" class="danger-btn" title="Delete selected (Del)">✕ Delete</button>
</div>
<div class="toolbar-right">
<button id="btn-git" title="Git version control">⎇ Git</button>
<button id="btn-drc" title="Run design-rule check">⚠ DRC</button>
<div id="saved-indicator">✓ Saved</div>
<div class="export-wrap">
<button id="export-toggle">Export ▾</button>
<div id="export-menu">
<button id="btn-import">📂 Import JSON</button>
<button id="btn-bom">📋 BOM (CSV)</button>
<button id="btn-assembly">📄 Assembly (TXT)</button>
<button id="btn-json">{ } JSON</button>
<button id="btn-img">🖼 Image (PNG)</button>
<button id="btn-formboard">📐 Formboard (1:1)</button>
</div>
</div>
</div>
<input id="import-file-input" type="file" accept=".json" style="display:none">
</header>
<!-- ── Mode bar ─────────────────────────────────────────────────────────── -->
<div id="mode-indicator"></div>
<!-- ── Main ─────────────────────────────────────────────────────────────── -->
<div id="main">
<!-- Left sidebar -->
<aside id="left-sidebar">
<!-- Diagram list -->
<div class="sidebar-section" style="flex-shrink:0">
<div class="section-header">
Diagrams
<button id="btn-new" title="New diagram" style="padding:2px 8px;font-size:16px;line-height:1">+</button>
</div>
<div id="diagram-list" style="overflow-y:auto;max-height:180px"></div>
</div>
<!-- Tabs: Devices / Connectors -->
<div class="tab-bar">
<button class="sidebar-tab active" data-tab="devices">Devices</button>
<button class="sidebar-tab" data-tab="connectors">Connector Library</button>
</div>
<!-- Devices panel -->
<div class="tab-panel" data-panel="devices" style="overflow-y:auto;flex:1">
<p class="muted small" style="padding:5px 10px">Drag to canvas or double-click to add</p>
<div id="device-library"></div>
</div>
<!-- Connector library panel -->
<div class="tab-panel" data-panel="connectors" style="display:none;flex-direction:column;overflow:hidden;flex:1">
<div style="padding:6px 8px;display:flex;flex-direction:column;gap:4px;flex-shrink:0">
<div style="display:flex;gap:4px;align-items:center">
<input id="lib-search" type="text" placeholder="Search connectors…" style="flex:1;background:#0e0e1e;border:1px solid #2a2a44;color:#c0c4e8;padding:5px 8px;border-radius:4px;font-size:12px">
<button id="btn-new-connector" title="Create custom connector" style="padding:4px 8px;font-size:13px;flex-shrink:0">+</button>
</div>
<select id="lib-category" style="background:#0e0e1e;border:1px solid #2a2a44;color:#c0c4e8;padding:4px 6px;border-radius:4px;font-size:11px"></select>
</div>
<p class="muted small" style="padding:0 10px 4px">Drag to canvas or double-click to add</p>
<div id="lib-list" style="overflow-y:auto;flex:1;padding:4px"></div>
</div>
</aside>
<!-- Canvas -->
<main id="canvas-area">
<!-- View tab bar -->
<div id="view-tab-bar" style="display:none">
<button class="view-tab active" data-view-id="main">Main</button>
<button id="btn-add-view" title="Add view">+</button>
<div class="view-tab-sep"></div>
<button id="btn-formboard-tab" class="view-tab fb-tab" title="Formboard harness layout">⊞ Formboard</button>
</div>
<!-- Formboard sub-toolbar (visible only in formboard mode) -->
<div id="fb-toolbar" style="display:none">
<button id="fb-btn-select" class="fb-mode-btn active" title="Select / move">▸ Select</button>
<button id="fb-btn-branch" class="fb-mode-btn" title="Click canvas to add a branch point">⬤ Branch</button>
<button id="fb-btn-splice" class="fb-mode-btn" title="Click canvas to add a splice/joint">◆ Splice</button>
<button id="fb-btn-connect" class="fb-mode-btn" title="Click two nodes to draw a segment">⌒ Connect</button>
<div class="sep"></div>
<button id="fb-btn-sync" title="Import all connectors from main diagram">↻ Sync connectors</button>
<button id="fb-btn-fit" title="Fit view">⊞ Fit</button>
<button id="fb-btn-del" class="danger-btn" title="Delete selected (Del)">✕ Delete</button>
</div>
<div id="canvas-container"></div>
<div id="fb-container" style="display:none;flex:1;width:100%;position:relative">
<div id="fb-canvas" style="width:100%;height:100%"></div>
</div>
<div id="canvas-placeholder">
<div class="placeholder-content">
<h2>⚡ WireDraw</h2>
<p>Create or open a diagram to begin</p>
<button onclick="app.newDiagram()">+ New Diagram</button>
</div>
</div>
</main>
<!-- Right sidebar — Properties -->
<aside id="right-sidebar">
<div class="section-header">Properties</div>
<!-- Empty state -->
<div id="props-empty" style="padding:20px 12px;color:#444466;font-size:12px;text-align:center">
Select a device or wire to edit its properties.
</div>
<!-- Multi-select state -->
<div id="props-multi" style="display:none;padding:24px 12px;text-align:center">
<div id="props-multi-count" style="font-size:13px;color:#c8ccee"></div>
<div style="margin-top:6px;font-size:11px;color:#444466">Drag any to move all · Ctrl+D to duplicate · Del to delete</div>
</div>
<!-- Device properties -->
<div id="props-device" style="display:none">
<div class="prop-row">
<label>Type</label>
<span id="prop-type" class="prop-value"></span>
</div>
<div class="prop-row">
<label>Reference</label>
<input id="prop-reference" type="text" placeholder="J1, TB1, U1…">
</div>
<div class="prop-row">
<label>Label</label>
<input id="prop-label" type="text" placeholder="Friendly name">
</div>
<div class="prop-row">
<label>Part Number</label>
<div style="display:flex;gap:4px">
<input id="prop-partnumber" type="text" placeholder="Mfr part number" style="flex:1;min-width:0">
<button id="btn-octopart-search" title="Search Octopart / Nexar" style="padding:4px 7px;font-size:13px;flex-shrink:0">🔍</button>
</div>
<div id="octopart-results" style="display:none"></div>
<div id="octopart-status-msg" style="display:none;font-size:10px;color:#666688;margin-top:3px"></div>
</div>
<div class="prop-row">
<label>Manufacturer</label>
<input id="prop-manufacturer" type="text" placeholder="Manufacturer">
</div>
<div class="prop-row" id="prop-datasheet-row" style="display:none">
<label>Datasheet</label>
<a id="prop-datasheet-link" href="#" target="_blank" rel="noopener noreferrer"
style="font-size:11px;color:#7788ff;word-break:break-all;display:block">📄 Open datasheet</a>
</div>
<div class="prop-row">
<label>Font Size</label>
<input id="prop-fontsize" type="number" min="6" max="72" step="1" placeholder="12" style="width:64px">
</div>
<!-- Group-specific section (shown only for section box) -->
<div id="group-section" style="display:none">
<div class="prop-row">
<label>Fill Color</label>
<input id="group-fill-color" type="color" value="#2828a0" class="color-swatch">
</div>
<div class="prop-row">
<label>Opacity</label>
<div style="display:flex;gap:6px;align-items:center">
<input id="group-fill-opacity" type="range" min="0.02" max="0.6" step="0.01" value="0.15" style="flex:1">
<span id="group-fill-opacity-val" style="font-size:10px;color:#778;width:28px;text-align:right">15%</span>
</div>
</div>
</div>
<!-- Cable-specific section (shown only for cable devices) -->
<div id="cable-section" style="display:none">
<div class="prop-row">
<label>Jacket Color</label>
<input id="cable-jacket-color" type="color" value="#2a2a2a" class="color-swatch">
</div>
<div class="prop-row">
<label>Sleeve Length</label>
<div style="display:flex;gap:6px;align-items:center">
<input id="cable-sleeve-length" type="range" min="0" max="200" step="5" value="60" style="flex:1">
<span id="cable-sleeve-length-val" style="font-size:10px;color:#778;width:32px;text-align:right">60 px</span>
</div>
</div>
<div class="prop-row">
<label>Conductors</label>
<table class="pin-table">
<thead><tr><th>#</th><th>Name</th><th>Color</th><th></th></tr></thead>
<tbody id="conductor-table-body"></tbody>
</table>
<button id="prop-add-conductor" style="margin-top:5px;width:100%;font-size:11px;padding:3px 6px">+ Add Conductor</button>
</div>
</div>
<div class="prop-row">
<label>Pins <span class="muted small">(rename inline)</span></label>
<table class="pin-table">
<thead><tr><th>ID</th><th>Name</th><th title="L=left R=right T=top B=bottom">Side</th><th></th></tr></thead>
<tbody id="pin-table-body"></tbody>
</table>
<div style="margin-top:5px;display:flex;gap:4px">
<button id="prop-add-pin" style="flex:1;font-size:11px;padding:3px 6px">+ Add Pin</button>
<button id="prop-save-connector" class="btn-primary" style="flex:1;font-size:11px;padding:3px 6px">⬆ Save to Library</button>
</div>
</div>
</div>
<!-- Wire properties -->
<div id="props-wire" style="display:none">
<div class="prop-row">
<label>From</label>
<span id="wire-from" class="prop-value mono"></span>
</div>
<div class="prop-row">
<label>To</label>
<span id="wire-to" class="prop-value mono"></span>
</div>
<div class="prop-row">
<label>Label</label>
<input id="wire-label" type="text" placeholder="Wire label / ID">
</div>
<div class="prop-row">
<label>Primary Color</label>
<div style="display:flex;gap:6px;align-items:center">
<input id="wire-color" type="color" value="#CC0000" class="color-swatch">
<select id="wire-color-preset" style="flex:1;background:#0e0e1e;border:1px solid #2a2a44;color:#c0c4e8;padding:4px 6px;border-radius:4px;font-size:11px">
<option value="">— Preset colors —</option>
</select>
</div>
</div>
<div class="prop-row">
<label id="wire-stripe-lbl">Stripe Color</label>
<div style="display:flex;gap:6px;align-items:center">
<label id="wire-stripe-toggle-lbl" style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:11px;color:#778">
<input id="wire-stripe-enabled" type="checkbox"> stripe
</label>
<input id="wire-stripe" type="color" value="#ffffff" disabled class="color-swatch" style="opacity:0.3">
</div>
</div>
<div class="prop-row">
<label>Wire Gauge</label>
<select id="wire-gauge">
<optgroup label="AWG">
<option>4 AWG</option>
<option>6 AWG</option>
<option>8 AWG</option>
<option>10 AWG</option>
<option>12 AWG</option>
<option>14 AWG</option>
<option>16 AWG</option>
<option selected>18 AWG</option>
<option>20 AWG</option>
<option>22 AWG</option>
<option>24 AWG</option>
<option>26 AWG</option>
</optgroup>
<optgroup label="Metric (mm²)">
<option>0.5 mm²</option>
<option>0.75 mm²</option>
<option>1.0 mm²</option>
<option>1.5 mm²</option>
<option>2.0 mm²</option>
<option>2.5 mm²</option>
<option>4.0 mm²</option>
<option>6.0 mm²</option>
<option>10 mm²</option>
<option>16 mm²</option>
<option>25 mm²</option>
<option>35 mm²</option>
<option>50 mm²</option>
<option>70 mm²</option>
<option>95 mm²</option>
</optgroup>
</select>
</div>
<div class="prop-row" style="display:flex;gap:6px">
<div style="flex:1">
<label>Length</label>
<input id="wire-length" type="number" min="0" step="0.5" placeholder="0.0">
</div>
<div style="width:64px">
<label>Unit</label>
<select id="wire-unit">
<option>in</option><option>cm</option><option>mm</option><option>ft</option>
</select>
</div>
</div>
<div class="prop-row">
<label>Twisted Pair</label>
<div style="display:flex;gap:8px;align-items:center">
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:11px;color:#778">
<input id="wire-twisted" type="checkbox"> twisted pair
</label>
<input id="wire-twist-pitch" type="number" min="8" max="64" step="2" value="16" disabled
style="width:48px;opacity:0.3">
<span id="wire-twist-pitch-label" style="font-size:10px;color:#444466">px pitch</span>
</div>
</div>
<div class="prop-row">
<label>Shielded</label>
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:11px;color:#778">
<input id="wire-shielded" type="checkbox"> shielded cable
</label>
</div>
<div class="prop-row">
<label>Size Label</label>
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:11px;color:#778">
<input id="wire-show-size-label" type="checkbox"> show gauge on canvas
</label>
</div>
<div class="prop-row">
<label>Notes</label>
<textarea id="wire-notes" rows="3" placeholder="Notes…" style="resize:vertical"></textarea>
</div>
<div class="prop-section-header" style="margin-top:10px;margin-bottom:4px;font-size:10px;color:#556;text-transform:uppercase;letter-spacing:.05em">Multi-Core Bundle</div>
<div class="prop-row">
<label>Bundle</label>
<div style="display:flex;gap:4px;flex:1">
<select id="wire-bundle-select" style="flex:1">
<option value="">— none —</option>
</select>
<button id="wire-bundle-new" title="New bundle" style="padding:2px 7px;font-size:13px">+</button>
</div>
</div>
<div id="wire-bundle-edit" style="display:none">
<div class="prop-row">
<label>Label</label>
<input id="wire-bundle-label" type="text" placeholder="e.g. CAN Bus" style="flex:1">
</div>
<div class="prop-row">
<label>Jacket</label>
<input id="wire-bundle-color" type="color" value="#2a2a2a" style="width:50px">
<button id="wire-bundle-delete" style="margin-left:auto;padding:2px 7px;font-size:11px;color:#c66;background:none;border:1px solid #c66;border-radius:3px;cursor:pointer">Delete</button>
</div>
</div>
</div>
<!-- Formboard properties (shown when formboard is active) -->
<div id="props-formboard" style="display:none">
<div id="fb-props-empty" style="padding:20px 12px;color:#444466;font-size:12px;text-align:center">
Select a node or segment.
</div>
<!-- Node props -->
<div id="fb-node-props" style="display:none;padding:8px 10px;display:none">
<div class="cm-label" style="margin-top:4px">Node type</div>
<div id="fb-node-type-badge" style="font-size:11px;color:#8899bb;margin-bottom:8px"></div>
<div class="cm-label">Label</div>
<input id="fb-node-label" class="cm-input" type="text" placeholder="Label…">
<div class="cm-label" style="margin-top:6px">Notes</div>
<textarea id="fb-node-notes" class="cm-input" rows="2" style="resize:vertical" placeholder="Notes…"></textarea>
<div id="fb-connector-info" style="display:none;margin-top:8px;font-size:11px;color:#5566aa;padding:5px 7px;background:#0d0d1e;border-radius:4px;border:1px solid #1a1a33">
<div class="cm-label" style="margin-bottom:2px">Linked device</div>
<div id="fb-connector-dev-label"></div>
</div>
</div>
<!-- Segment props -->
<div id="fb-seg-props" style="display:none;padding:8px 10px">
<div class="cm-label" style="margin-top:4px">Fitting / covering</div>
<select id="fb-seg-fitting" class="cm-input">
<option value="open">Open (no covering)</option>
<option value="loom">Split loom</option>
<option value="conduit">Rigid conduit</option>
<option value="heat_shrink">Heat shrink</option>
<option value="tape">Tape wrap</option>
</select>
<div id="fb-seg-color-row" style="display:none;margin-top:4px">
<div class="cm-label">Fitting color</div>
<input id="fb-seg-fitting-color" type="color" value="#884444" style="width:100%;height:28px;padding:0;border:1px solid #2a2a44;border-radius:4px;background:none;cursor:pointer">
</div>
<div class="cm-label" style="margin-top:6px">Label / notes</div>
<input id="fb-seg-label" class="cm-input" type="text" placeholder="e.g. engine bay trunk">
<div class="cm-label" style="margin-top:6px">Length (mm)</div>
<input id="fb-seg-length" class="cm-input" type="number" min="0" placeholder="e.g. 350">
<div class="cm-label" style="margin-top:10px">Wires through this segment</div>
<div id="fb-wire-list" style="max-height:200px;overflow-y:auto;display:flex;flex-direction:column;gap:2px;font-size:11px"></div>
</div>
</div>
</aside>
</div><!-- #main -->
</div><!-- #app -->
<!-- ── Custom Connector Modal ──────────────────────────────────────────────── -->
<div id="connector-modal" style="display:none">
<div class="modal-box">
<div class="modal-header">
<h3 id="cm-modal-title">New Custom Connector</h3>
</div>
<div class="modal-body">
<div class="cm-row">
<label class="cm-label" for="cm-name">Name <span style="color:#ff6666">*</span></label>
<input id="cm-name" type="text" class="cm-input" placeholder="e.g. AMP Superseal 6P">
</div>
<div class="cm-row cm-row-2col">
<div>
<label class="cm-label" for="cm-category">Category</label>
<input id="cm-category" type="text" class="cm-input" placeholder="Custom">
</div>
<div>
<label class="cm-label" for="cm-pincount">Pin Count <span style="color:#ff6666">*</span></label>
<input id="cm-pincount" type="number" class="cm-input" min="1" max="64" value="4">
</div>
</div>
<div class="cm-row cm-row-2col">
<div>
<label class="cm-label" for="cm-manufacturer">Manufacturer</label>
<input id="cm-manufacturer" type="text" class="cm-input" placeholder="e.g. TE Connectivity">
</div>
<div>
<label class="cm-label" for="cm-partnumber">Part Number</label>
<input id="cm-partnumber" type="text" class="cm-input" placeholder="e.g. 1-1703630-1">
</div>
</div>
<div class="cm-row">
<label class="cm-label" for="cm-description">Description</label>
<input id="cm-description" type="text" class="cm-input" placeholder="Optional description">
</div>
<div class="cm-row">
<label class="cm-label">Pin Labels <span class="muted small">(optional)</span></label>
<div id="cm-pin-labels"></div>
</div>
</div>
<div class="modal-footer">
<button id="cm-delete" class="danger-btn" style="display:none">Delete</button>
<div style="flex:1"></div>
<button id="cm-cancel">Cancel</button>
<button id="cm-save" class="btn-primary">Save</button>
</div>
</div>
</div>
<!-- ── Context Menu ──────────────────────────────────────────────────────────── -->
<div id="ctx-menu">
<!-- populated dynamically by app.js -->
</div>
<!-- ── DRC Modal ──────────────────────────────────────────────────────────────── -->
<div id="drc-modal" style="display:none">
<div class="modal-box" style="min-width:480px;max-width:640px">
<div class="modal-header">
<h3>Design Rule Check</h3>
</div>
<div class="modal-body" id="drc-results" style="max-height:400px;overflow-y:auto;font-size:12px;padding:10px;gap:6px">
</div>
<div class="modal-footer">
<div style="flex:1"></div>
<button id="drc-close" class="btn-primary">Close</button>
</div>
</div>
</div>
<div id="git-modal" style="display:none">
<div class="modal-box" style="min-width:520px;max-width:700px">
<div class="modal-header">
<h3>⎇ Git Version Control</h3>
<span id="git-status-badge" class="git-badge"></span>
</div>
<div class="modal-body" style="padding:12px;gap:10px;display:flex;flex-direction:column">
<!-- Commit section -->
<div class="git-section">
<div style="display:flex;align-items:center;justify-content:space-between">
<label class="git-section-title">Commit diagrams</label>
<label style="font-size:11px;color:#8899bb;display:flex;align-items:center;gap:5px;cursor:pointer">
<input type="checkbox" id="git-scope-current"> Current diagram only
</label>
</div>
<div style="display:flex;gap:6px;align-items:flex-start">
<textarea id="git-commit-msg" placeholder="Describe what changed…" rows="2"
style="flex:1;resize:vertical;font-size:12px;padding:5px;background:#0d0d20;color:#ccd;border:1px solid #2a2a44;border-radius:4px"></textarea>
<div style="display:flex;flex-direction:column;gap:4px">
<button id="btn-git-commit" class="btn-primary" style="white-space:nowrap">⊕ Commit</button>
<button id="btn-git-push" style="white-space:nowrap">⇧ Push</button>
</div>
</div>
<div id="git-op-status" style="font-size:11px;color:#88cc88;min-height:16px"></div>
</div>
<!-- History section -->
<div class="git-section" style="flex:1">
<div style="display:flex;align-items:center;justify-content:space-between">
<label class="git-section-title" id="git-log-title">Commit history</label>
<div style="display:flex;gap:2px">
<button id="btn-git-log-diagram" class="git-scope-btn active" title="Show commits for current diagram only">This diagram</button>
<button id="btn-git-log-all" class="git-scope-btn" title="Show all commits">All</button>
</div>
</div>
<div id="git-log-list" style="max-height:300px;overflow-y:auto;font-size:11px;display:flex;flex-direction:column;gap:1px"></div>
</div>
</div>
<div class="modal-footer">
<div style="flex:1"></div>
<button id="git-close">Close</button>
</div>
</div>
</div>
<script src="https://unpkg.com/konva@9/konva.min.js"></script>
<script src="/js/api.js"></script>
<script src="/js/deviceTypes.js"></script>
<script src="/js/connectorLibrary.js"></script>
<script src="/js/canvas.js"></script>
<script src="/js/formboard.js"></script>
<script src="/js/app.js"></script>
</body>
</html>