82d8570767
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>
97 lines
4.0 KiB
Python
97 lines
4.0 KiB
Python
import os
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import FileResponse, JSONResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from sqlalchemy import text, inspect as sa_inspect
|
|
|
|
from .database import Base, engine, SessionLocal
|
|
from .routers import bundles, connectors, diagrams, devices, export, formboard, git_mgr, octopart, views, wires
|
|
|
|
Base.metadata.create_all(bind=engine)
|
|
|
|
# Lightweight column migrations (SQLite ALTER TABLE IF NOT EXISTS unsupported)
|
|
def _migrate():
|
|
import sys
|
|
|
|
# ── Column additions ────────────────────────────────────────────────────────
|
|
try:
|
|
with engine.connect() as conn:
|
|
def _cols(table):
|
|
return {c["name"] for c in sa_inspect(engine).get_columns(table)}
|
|
|
|
if "waypoints" not in _cols("wires"):
|
|
conn.execute(text("ALTER TABLE wires ADD COLUMN waypoints TEXT DEFAULT '[]'"))
|
|
conn.commit()
|
|
if "sheet_id" not in _cols("devices"):
|
|
conn.execute(text("ALTER TABLE devices ADD COLUMN sheet_id INTEGER REFERENCES sheets(id)"))
|
|
conn.commit()
|
|
if "sheet_id" not in _cols("wires"):
|
|
conn.execute(text("ALTER TABLE wires ADD COLUMN sheet_id INTEGER REFERENCES sheets(id)"))
|
|
conn.commit()
|
|
if "twisted_pair" not in _cols("wires"):
|
|
conn.execute(text("ALTER TABLE wires ADD COLUMN twisted_pair INTEGER DEFAULT 0"))
|
|
conn.commit()
|
|
if "twist_pitch" not in _cols("wires"):
|
|
conn.execute(text("ALTER TABLE wires ADD COLUMN twist_pitch REAL DEFAULT 16.0"))
|
|
conn.commit()
|
|
if "shielded" not in _cols("wires"):
|
|
conn.execute(text("ALTER TABLE wires ADD COLUMN shielded INTEGER DEFAULT 0"))
|
|
conn.commit()
|
|
if "bundle_id" not in _cols("wires"):
|
|
conn.execute(text("ALTER TABLE wires ADD COLUMN bundle_id INTEGER REFERENCES wire_bundles(id)"))
|
|
conn.commit()
|
|
if "show_size_label" not in _cols("wires"):
|
|
conn.execute(text("ALTER TABLE wires ADD COLUMN show_size_label INTEGER DEFAULT 0"))
|
|
conn.commit()
|
|
|
|
# diagram_views / view_device_positions / view_wire_waypoints are created
|
|
# via Base.metadata.create_all — no ALTER TABLE needed for new tables
|
|
except Exception as e:
|
|
print(f"[migrate] column error: {e}", file=sys.stderr)
|
|
|
|
_migrate()
|
|
|
|
app = FastAPI(title="Wiring Diagram Maker", version="1.0.0")
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
app.include_router(bundles.router, prefix="/api/bundles")
|
|
app.include_router(diagrams.router, prefix="/api/diagrams")
|
|
app.include_router(devices.router, prefix="/api/devices")
|
|
app.include_router(wires.router, prefix="/api/wires")
|
|
app.include_router(views.router, prefix="/api/views")
|
|
app.include_router(octopart.router, prefix="/api/octopart")
|
|
app.include_router(export.router, prefix="/api/export")
|
|
app.include_router(connectors.router, prefix="/api/connectors")
|
|
app.include_router(formboard.router, prefix="/api/formboard")
|
|
app.include_router(git_mgr.router, prefix="/api/git")
|
|
|
|
FRONTEND_DIR = os.path.normpath(
|
|
os.path.join(os.path.dirname(__file__), "..", "..", "frontend")
|
|
)
|
|
|
|
if os.path.isdir(FRONTEND_DIR):
|
|
for _sub in ("css", "js"):
|
|
_d = os.path.join(FRONTEND_DIR, _sub)
|
|
if os.path.isdir(_d):
|
|
app.mount(f"/{_sub}", StaticFiles(directory=_d), name=_sub)
|
|
|
|
_index = os.path.join(FRONTEND_DIR, "index.html")
|
|
|
|
@app.get("/")
|
|
async def _root():
|
|
return FileResponse(_index)
|
|
|
|
@app.exception_handler(404)
|
|
async def _spa_fallback(request: Request, exc):
|
|
if request.url.path.startswith("/api"):
|
|
return JSONResponse(status_code=404, content={"detail": "Not Found"})
|
|
return FileResponse(_index)
|