e4a80dc0e6
- Roadmap #22: File→Import JSON to restore a diagram (drag-drop or file picker) - Roadmap #23: Duplicate diagram from diagram list - Roadmap #24: DRC panel with collapsible categories (unconnected pins, dup refs, no length, no P/N) - Multi-view tabs: named views per diagram with independent device/wire layouts; snapshot-on-entry prevents bleed - Keyboard arrow nudge for selected device(s) - Undo for move, duplicate, label/property changes - Wire jump arcs toggle (⌒ Jumps button in toolbar) - Shift+click multi-wire selection; right-click bulk clear waypoints or auto-route - Auto-route: obstacle-avoiding ortho routing for single or bulk selected wires - Delete view tab with × button; right-click context menu for rename/delete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
95 lines
3.9 KiB
Python
95 lines
3.9 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, 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")
|
|
|
|
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)
|