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>
This commit is contained in:
2026-04-25 07:18:43 -04:00
parent 0bbcaa8ee5
commit 82d8570767
8 changed files with 985 additions and 1 deletions
+39
View File
@@ -124,6 +124,45 @@ class ViewWireWaypoints(Base):
view = relationship("DiagramView", back_populates="wire_waypoints")
class FormboardLayout(Base):
__tablename__ = "formboard_layouts"
id = Column(Integer, primary_key=True, index=True)
diagram_id = Column(Integer, ForeignKey("diagrams.id", ondelete="CASCADE"), nullable=False, unique=True)
nodes = relationship("FormboardNode", back_populates="layout", cascade="all, delete-orphan")
segments = relationship("FormboardSegment", back_populates="layout", cascade="all, delete-orphan")
class FormboardNode(Base):
__tablename__ = "formboard_nodes"
id = Column(Integer, primary_key=True, index=True)
layout_id = Column(Integer, ForeignKey("formboard_layouts.id", ondelete="CASCADE"), nullable=False)
node_type = Column(String, default="branch") # connector | branch | splice
device_id = Column(Integer, ForeignKey("devices.id", ondelete="SET NULL"), nullable=True)
x = Column(Float, default=100)
y = Column(Float, default=100)
label = Column(String, default="")
notes = Column(Text, default="")
layout = relationship("FormboardLayout", back_populates="nodes")
class FormboardSegment(Base):
__tablename__ = "formboard_segments"
id = Column(Integer, primary_key=True, index=True)
layout_id = Column(Integer, ForeignKey("formboard_layouts.id", ondelete="CASCADE"), nullable=False)
from_node_id = Column(Integer, ForeignKey("formboard_nodes.id", ondelete="CASCADE"), nullable=False)
to_node_id = Column(Integer, ForeignKey("formboard_nodes.id", ondelete="CASCADE"), nullable=False)
waypoints = Column(JSON, default=[])
wire_ids = Column(JSON, default=[]) # wire IDs from main diagram
fitting_type = Column(String, default="open") # open | loom | conduit | heat_shrink | tape
fitting_color = Column(String, default="#888888")
label = Column(String, default="")
length_mm = Column(Float, nullable=True)
layout = relationship("FormboardLayout", back_populates="segments")
class CustomConnector(Base):
__tablename__ = "custom_connectors"
id = Column(Integer, primary_key=True, index=True)