/* =================================================================== KabelFlux v5 β€” Layout view (consolidated Diagram + Formboard, per Day 2) - Chip toolbar grouped: Wires / Labels / Detail (per DEMO_SCRIPT Part 2) - Pin face Z-order corrected (H2 fix): faces drawn UNDER labels - Reduced motion respected (H5) - Wire mode banner (per Part 4.5) =================================================================== */ const SYM = { Circular: (props) => { const { x, y, pins, gender, sel, showPinNums=true } = props; const r = 38; return ( {Array.from({ length: pins }).map((_, i) => { const a = (i / pins) * Math.PI * 2 - Math.PI / 2; const cx = Math.cos(a) * (r - 14); const cy = Math.sin(a) * (r - 14); return ( {gender === 'M' ? : } {showPinNums && ( {i + 1} )} ); })} ); }, Rect: (props) => { const { x, y, pins, gender, sel, w=78, h=56, showPinNums=true } = props; const rows = Math.min(2, Math.ceil(pins / 4)); const cols = Math.ceil(pins / rows); return ( {Array.from({ length: pins }).map((_, i) => { const col = i % cols, row = Math.floor(i / cols); const cx = -w/2 + 14 + col * ((w - 28) / Math.max(1, cols - 1)); const cy = rows === 1 ? 0 : -h/2 + 14 + row * (h - 28); return ( {gender === 'M' ? : } {showPinNums && ( {i + 1} )} ); })} ); } }; const NODE_GLYPH = { splice: (col) => , fuse: (col) => , ground: (col) => , }; /* β€” Canvas β€” */ const LayoutView = ({ wireMode, setWireMode, renderMode, density, wires=[], connectors=[], nodes=[], bundles=[], loading=false, project, toast }) => { // Analyse β€” consistency check against the backend's audit_project rules. // Same engine as the old "πŸ” Audit" button in the legacy SPA, just // surfaced here next to Wire mode where designers will actually find it. const [analyseRunning, setAnalyseRunning] = useState(false); const [analyseResult, setAnalyseResult] = useState(null); // { issues, projectName } const runAnalyse = async () => { if (!project || !project.realId) { toast && toast.warn && toast.warn('Open a project first to analyse it.'); return; } setAnalyseRunning(true); try { // Call the backend's audit endpoint directly β€” same engine as the // legacy SPA's old "πŸ” Audit" button. const resp = await window.kfxFetch('/projects/' + project.realId + '/audit'); const issues = (resp && Array.isArray(resp.issues)) ? resp.issues : []; setAnalyseResult({ issues: issues, projectName: project.name || '' }); const n = (issues || []).length; const errs = (issues || []).filter(i => i.severity === 'error').length; const warns = (issues || []).filter(i => i.severity === 'warning').length; if (n === 0) toast && toast.success && toast.success('Analyse Β· no issues found.'); else toast && toast.info && toast.info('Analyse Β· ' + errs + ' error' + (errs===1?'':'s') + ', ' + warns + ' warning' + (warns===1?'':'s') + ' found.'); } catch (e) { toast && toast.err && toast.err('Analyse failed: ' + (e.message || e)); } finally { setAnalyseRunning(false); } }; const [tools, setTools] = useState({ nets: true, nodes: true, gauges: false, callouts: true, pinFaces: true, shieldGlow: true, tables: false, dims: true, grid: true, bezier: true, }); const [zoom, setZoom] = useState(1); const [sel, setSel] = useState(null); const [hover, setHover] = useState(null); const reducedMotion = useRef(typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches); const toggle = k => setTools(t => ({ ...t, [k]: !t[k] })); /* Wire paths */ const wirePath = (w) => { const from = connectors.find(c => c.id === w.fromConn) || nodes.find(n => n.id === w.fromConn); const to = connectors.find(c => c.id === w.toConn) || nodes.find(n => n.id === w.toConn); if (!from || !to) return null; if (renderMode === 'formboard') { // 90Β° formboard routing const midX = (from.x + to.x) / 2; return `M ${from.x} ${from.y} L ${midX} ${from.y} L ${midX} ${to.y} L ${to.x} ${to.y}`; } if (!tools.bezier) { // straight line when Bezier is toggled off return `M ${from.x} ${from.y} L ${to.x} ${to.y}`; } // schematic bezier const dx = (to.x - from.x) * 0.4; return `M ${from.x} ${from.y} C ${from.x + dx} ${from.y}, ${to.x - dx} ${to.y}, ${to.x} ${to.y}`; }; return (
{/* Chip toolbar β€” grouped per Day 2 */}
Wires toggle('bezier')} icon="bundle">Bezier toggle('shieldGlow')}>Shield glow
Labels toggle('nets')}>Nets toggle('nodes')}>Nodes toggle('gauges')}>Gauges toggle('callouts')}>Callouts
Detail toggle('pinFaces')}>Pin faces toggle('dims')} icon="ruler">Dimensions toggle('grid')} icon="grid">Grid
{/* Wire mode toggle β€” same format as the other toolbar chips. */}
Action setWireMode(!wireMode)} icon="bolt" ariaLabel="Toggle wire-creation mode"> {wireMode ? 'Wire mode β€’ ON' : 'Wire mode'} {/* Analyse β€” runs the backend consistency audit, formerly the "Audit" button in the legacy SPA. Surfaces unknown connectors, duplicate wires, pin overruns, etc. */} {analyseRunning ? 'Analysing…' : 'Analyse'}
{/* Wire-mode hint banner */} {wireMode && (
Wire mode active β€” click pin A, then pin B to create a wire. Esc to exit.
)}
{/* Left rail: zoom + render mode */}
setZoom(z => Math.min(2, z + .15))}/> setZoom(z => Math.max(.4, z - .15))}/>
{Math.round(zoom * 100)}%
toggle('grid')}/>
{/* The SVG canvas */}
{/* Bundles (drawn first) */} {bundles.map(b => ( ))} {/* Wires */} {wires.map(w => { const hex = COLOR_HEX[w.color] || '#888'; const d = wirePath(w); if (!d) return null; const isOpen = (w.notes || '').toLowerCase().includes('open'); return ( {w.shield && tools.shieldGlow && !reducedMotion.current && ( )} setHover(w.id)} onMouseLeave={() => setHover(null)}/> ); })} {/* Net labels */} {tools.nets && wires.filter((_, i) => i % 3 === 0).map(w => { const from = connectors.find(c => c.id === w.fromConn) || nodes.find(n => n.id === w.fromConn); const to = connectors.find(c => c.id === w.toConn) || nodes.find(n => n.id === w.toConn); if (!from || !to) return null; const mx = (from.x + to.x) / 2, my = (from.y + to.y) / 2 - 8; return ( {w.net} ); })} {/* Gauge labels β€” AWG at wire midpoint, below the wire */} {tools.gauges && wires.map(w => { if (!w.gauge) return null; const from = connectors.find(c => c.id === w.fromConn) || nodes.find(n => n.id === w.fromConn); const to = connectors.find(c => c.id === w.toConn) || nodes.find(n => n.id === w.toConn); if (!from || !to) return null; const mx = (from.x + to.x) / 2, my = (from.y + to.y) / 2 + 10; const lw = Math.max(36, w.gauge.length * 6); return ( {w.gauge} ); })} {/* Dimension arrows (industry-standard, per DEMO_SCRIPT Part 3) */} {tools.dims && bundles.map(b => { const y = Math.min(b.from.y, b.to.y) - 48; const x1 = b.from.x + 8, x2 = b.to.x - 8; return ( {b.label} = {b.length} mm ); })} {/* Callout annotations β€” leader line + text box, from project.callouts */} {tools.callouts && (project?.callouts || []).map((c, i) => { const lx = c.lx != null ? c.lx : c.x + 44; const ly = c.ly != null ? c.ly : c.y - 22; const label = c.text || c.label || ''; const bw = Math.max(52, label.length * 6.2 + 16); return ( {label} ); })} {/* Connectors (drawn AFTER wires; pin faces are INSIDE the symbol β†’ no overlap, H2 fix) */} {connectors.map(c => { const isSel = sel === c.id; const Sym = ['Circular','Deutsch DT'].includes(c.type) ? SYM.Circular : SYM.Rect; return ( setSel(c.id)} style={{ cursor: 'pointer' }}> {c.id} {c.type} Β· {c.gender === 'M' ? 'Male' : 'Female'} Β· {c.pins}P ); })} {/* Nodes */} {tools.nodes && nodes.map(n => { const t = { splice:'#f5a623', fuse:'#ef4e4e', ground:'#9aa3b8' }[n.type] || '#888'; const G = NODE_GLYPH[n.type] || NODE_GLYPH.splice; return ( {G(t)} {n.label}{n.value && ` Β· ${n.value}`} ); })} {/* Selected pin pulse (wire mode) */} {wireMode && ( {!reducedMotion.current && ( <> )} )}
{/* Right inspector */}
Inspector
{sel && {sel}}
{sel ? (
{(() => { const c = connectors.find(x => x.id === sel); if (!c) return null; const wiresOn = wires.filter(w => w.fromConn === sel || w.toConn === sel); return ( <>
{c.label}
{c.type}
{c.gender === 'M' ? 'Male' : 'Female'}
{c.pins}
Pinout Β· {wiresOn.length} wired
{Array.from({ length: c.pins }).map((_, i) => { const w = wiresOn.find(w => (w.fromConn === sel && w.fromPin === i + 1) || (w.toConn === sel && w.toPin === i + 1)); return (
{i + 1} {w ? ( <> {w.net} β†’ {w.fromConn === sel ? `${w.toConn}:${w.toPin}` : `${w.fromConn}:${w.fromPin}`} ) : ( open )}
); })}
Edit Duplicate
); })()}
) : ( )}
{/* Analyse results β€” same modal layout as the legacy SPA's audit modal, restyled to the bundle's design language. */} {analyseResult && ( setAnalyseResult(null)} footer={ setAnalyseResult(null)}>Close}> {analyseResult.issues.length === 0 ? (
βœ…
No issues found
This harness looks clean.
) : (
{/* Severity summary */}
{[['error','danger','πŸ”΄'],['warning','warning','🟑'],['info','info','πŸ”΅']].map(([sev, tone, dot]) => { const n = analyseResult.issues.filter(i => i.severity === sev).length; if (!n) return null; return ( {dot}{n} {sev}{n!==1?'s':''} ); })} {analyseResult.issues.length} total issue{analyseResult.issues.length!==1?'s':''}
{/* Issue list */}
{analyseResult.issues.map((issue, i) => (
{issue.severity === 'error' ? 'ERROR' : issue.severity === 'warning' ? 'WARN' : 'INFO'}
[{issue.category || 'β€”'}] {issue.message}
{issue.fix && (
πŸ’‘ {issue.fix}
)}
))}
)}
)}
); }; /* ---------- Styles ---------- */ const LAYOUT_STYLE = ` .kf-layout { display: flex; flex-direction: column; height: 100%; } .kf-canvas-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 10px 14px; background: var(--bg-2); border-bottom: 1px solid var(--line); } .kf-bar-group { display: inline-flex; align-items: center; gap: 5px; } .kf-bar-label { font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--t-3); margin-right: 4px; } .kf-banner { display: flex; align-items: center; gap: 10px; padding: 8px 14px; font-size: 12.5px; border-bottom: 1px solid var(--line); } .kf-banner b { color: inherit; font-weight: 700; } .kf-banner-success { background: var(--success-soft); color: var(--success); } .kf-banner-warning { background: var(--warning-soft); color: var(--warning); } .kf-banner-info { background: var(--info-soft); color: var(--info); } .kf-canvas-wrap { display: grid; grid-template-columns: 44px 1fr 280px; flex: 1; min-height: 0; } .kf-canvas-rail { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 8px 4px; background: var(--bg-2); border-right: 1px solid var(--line); } .kf-rail-zoom { font-size: 10px; color: var(--t-3); padding: 4px 0; } .kf-rail-sep { width: 24px; height: 1px; background: var(--line); margin: 6px 0; } .kf-canvas { overflow: auto; position: relative; } .kf-canvas-inspector { background: var(--bg-2); border-left: 1px solid var(--line); display: flex; flex-direction: column; overflow: hidden; } .kf-inspector-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; border-bottom: 1px solid var(--line); } .kf-inspector-title { font-size: 11px; font-weight: 700; letter-spacing: .6px; text-transform: uppercase; color: var(--t-3); } .kf-inspector-body { padding: 14px; flex: 1; overflow-y: auto; } .kf-inspector-field { display: flex; flex-direction: column; gap: 3px; margin-bottom: 12px; } .kf-inspector-field.flex { flex: 1; } .kf-inspector-field label { font-size: 9.5px; text-transform: uppercase; letter-spacing: .6px; color: var(--t-3); font-weight: 700; } .kf-inspector-value { font-size: 12.5px; color: var(--t-1); } .kf-inspector-value.num { font-family: var(--f-mono); } .kf-inspector-row { display: flex; gap: 12px; } .kf-inspector-sep { height: 1px; background: var(--line); margin: 6px 0 12px; } .kf-inspector-label { font-size: 10px; text-transform: uppercase; letter-spacing: .8px; color: var(--t-3); font-weight: 700; margin-bottom: 8px; } .kf-inspector-actions { display: flex; gap: 6px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); } .kf-pinout { display: flex; flex-direction: column; gap: 2px; background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--r-2); padding: 4px; } .kf-pinout-row { display: grid; grid-template-columns: 22px 14px 1fr auto; gap: 8px; align-items: center; padding: 4px 6px; border-radius: 3px; font-size: 11px; } .kf-pinout-row:hover { background: var(--bg-3); } .kf-pinout-cav { color: var(--t-3); font-weight: 600; text-align: right; } .kf-pinout-sw { width: 14px; height: 6px; border-radius: 1px; } .kf-pinout-net { color: var(--t-1); font-family: var(--f-mono); font-size: 11px; } .kf-pinout-to { color: var(--t-3); } .kf-pinout-empty{ color: var(--t-4); font-style: italic; font-size: 10.5px; } /* β€” Analyse-results modal β€” */ .kf-analyse-summary { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--bg-3); border: 1px solid var(--line); border-radius: var(--r-3); margin-bottom: 12px; font-size: 12px; font-weight: 600; } .kf-analyse-pill { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: var(--r-pill); font-size: 11px; font-weight: 700; } .kf-analyse-pill-danger { background: rgba(239,78,78,.12); color: var(--danger); } .kf-analyse-pill-warning { background: var(--warning-soft); color: var(--warning); } .kf-analyse-pill-info { background: var(--info-soft); color: var(--info); } .kf-analyse-list { max-height: 50vh; overflow-y: auto; } .kf-analyse-row { display: flex; gap: 10px; align-items: flex-start; padding: 8px 12px; border: 1px solid var(--line); border-left-width: 3px; border-radius: var(--r-2); margin-bottom: 6px; background: var(--bg-2); } .kf-analyse-row-error { border-left-color: var(--danger); background: rgba(239,78,78,.04); } .kf-analyse-row-warning { border-left-color: var(--warning); background: rgba(245,166,35,.05); } .kf-analyse-row-info { border-left-color: var(--info); background: rgba(74,158,255,.04); } .kf-analyse-sev { flex-shrink: 0; font-size: 10px; font-weight: 800; letter-spacing: 0.5px; color: var(--t-3); min-width: 44px; padding-top: 2px; } .kf-analyse-row-error .kf-analyse-sev { color: var(--danger); } .kf-analyse-row-warning .kf-analyse-sev { color: var(--warning); } .kf-analyse-row-info .kf-analyse-sev { color: var(--info); } .kf-analyse-body { flex: 1; min-width: 0; } .kf-analyse-msg { font-size: 12.5px; color: var(--t-1); line-height: 1.4; } .kf-analyse-cat { font-family: var(--f-mono); font-size: 11px; color: var(--t-2); font-weight: 700; } .kf-analyse-fix { font-size: 11.5px; color: var(--t-3); margin-top: 4px; } `; document.head.appendChild(Object.assign(document.createElement('style'), { textContent: LAYOUT_STYLE })); window.LayoutView = LayoutView;