gitcad 0.7.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitcad/bench/__init__.py +0 -0
- gitcad/bench/corpus.py +260 -0
- gitcad/bench/scorecard.py +176 -0
- gitcad/bridge.py +72 -0
- gitcad/convert.py +30 -0
- gitcad/explore.py +99 -0
- gitcad/fasteners.py +112 -0
- gitcad/init.py +134 -0
- gitcad/lots.py +136 -0
- gitcad/mcp/__init__.py +15 -0
- gitcad/mcp/server.py +1065 -0
- gitcad/merge3.py +289 -0
- gitcad/pcba.py +114 -0
- gitcad/release.py +223 -0
- gitcad/render.py +177 -0
- gitcad/requirements.py +204 -0
- gitcad/review.py +230 -0
- gitcad/viewer/__init__.py +19 -0
- gitcad/viewer/boardsvg.py +80 -0
- gitcad/viewer/page.py +577 -0
- gitcad/viewer/server.py +433 -0
- gitcad-0.7.7.dist-info/METADATA +70 -0
- gitcad-0.7.7.dist-info/RECORD +25 -0
- gitcad-0.7.7.dist-info/WHEEL +4 -0
- gitcad-0.7.7.dist-info/entry_points.txt +11 -0
gitcad/viewer/server.py
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""The viewer server — Python stdlib only, one file in, live page out.
|
|
2
|
+
|
|
3
|
+
Serves ``PAGE`` (the self-contained WebGL2 client in :mod:`.page`) plus a
|
|
4
|
+
tiny JSON API. The page polls ``/api/version`` (content hash of the watched
|
|
5
|
+
file) and refetches on change — edit the model text, the view updates.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from gitcad.document import Document
|
|
17
|
+
from gitcad.ecad.board import Board
|
|
18
|
+
from gitcad.kernel import get_kernel
|
|
19
|
+
from gitcad.seams import Kernel
|
|
20
|
+
from gitcad.viewer.boardsvg import board_to_svg
|
|
21
|
+
from gitcad.viewer.page import PAGE
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def detect_kind(text: str) -> str:
|
|
25
|
+
doc = json.loads(text)
|
|
26
|
+
schema = doc.get("schema", "")
|
|
27
|
+
if schema.startswith("gitcad/board"):
|
|
28
|
+
return "board"
|
|
29
|
+
if schema.startswith("gitcad/schematic"):
|
|
30
|
+
return "schematic"
|
|
31
|
+
if schema.startswith("gitcad/document"):
|
|
32
|
+
return "model"
|
|
33
|
+
if schema.startswith("gitcad/part"):
|
|
34
|
+
if doc.get("domain") == "assembly":
|
|
35
|
+
return "assembly"
|
|
36
|
+
if (doc.get("body") or {}).get("kind") == "pcba":
|
|
37
|
+
return "pcba" # the Fusion duality: enter the electrical workflow
|
|
38
|
+
raise ValueError("view a part's MODEL file, not its manifest "
|
|
39
|
+
"(only assembly and pcba manifests are viewable directly)")
|
|
40
|
+
raise ValueError(f"cannot view schema {schema!r}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def mesh_payload(doc: Document, kernel: Kernel) -> dict:
|
|
44
|
+
"""Everything the 3D client needs, JSON-able."""
|
|
45
|
+
result = doc.build(kernel)
|
|
46
|
+
shape = result.final(doc)
|
|
47
|
+
mesh = kernel.tessellate(shape)
|
|
48
|
+
lo, hi = kernel.bbox(shape)
|
|
49
|
+
measures = kernel.measure(shape)
|
|
50
|
+
return {
|
|
51
|
+
"kind": "model",
|
|
52
|
+
"positions": mesh["positions"],
|
|
53
|
+
"indices": mesh["indices"],
|
|
54
|
+
"bbox": [list(lo), list(hi)],
|
|
55
|
+
"stats": {
|
|
56
|
+
"vertices": len(mesh["positions"]) // 3,
|
|
57
|
+
"triangles": len(mesh["indices"]) // 3,
|
|
58
|
+
"volume_mm3": round(measures.get("volume", 0.0), 2),
|
|
59
|
+
"kernel": kernel.name,
|
|
60
|
+
"features": len(doc),
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Distinct-instance palette (dark-theme friendly), cycled.
|
|
66
|
+
_PALETTE = [(0.35, 0.62, 0.85), (0.85, 0.55, 0.35), (0.45, 0.78, 0.55),
|
|
67
|
+
(0.78, 0.45, 0.72), (0.80, 0.75, 0.40), (0.45, 0.72, 0.78),
|
|
68
|
+
(0.65, 0.50, 0.85), (0.60, 0.60, 0.60)]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def resolve_assembly_shapes(manifest_path: Path, kernel: Kernel
|
|
72
|
+
) -> dict[str, tuple]:
|
|
73
|
+
"""Instance name -> (unplaced Shape, translate, rotate_z_deg, part_name).
|
|
74
|
+
|
|
75
|
+
The ONE resolution rule for assemblies on disk (viewer mesh, the
|
|
76
|
+
interference requirements check, exports): part id -> sibling
|
|
77
|
+
.part/.pcba/part.json under the assembly's tree; model-backed parts
|
|
78
|
+
build their .model, board-backed parts extrude POPULATED through the
|
|
79
|
+
bridge (the PCBA's real mechanical envelope)."""
|
|
80
|
+
from gitcad.part import PartManifest
|
|
81
|
+
|
|
82
|
+
manifest = PartManifest.loads(manifest_path.read_text(encoding="utf-8"))
|
|
83
|
+
root = manifest_path.parent
|
|
84
|
+
|
|
85
|
+
by_id: dict[str, tuple[PartManifest, Path]] = {}
|
|
86
|
+
for pj in sorted(list(root.rglob("*part.json")) + list(root.rglob("*.part"))
|
|
87
|
+
+ list(root.rglob("*.pcba"))):
|
|
88
|
+
if pj == manifest_path:
|
|
89
|
+
continue
|
|
90
|
+
try:
|
|
91
|
+
m = PartManifest.loads(pj.read_text(encoding="utf-8"))
|
|
92
|
+
except Exception:
|
|
93
|
+
continue
|
|
94
|
+
by_id.setdefault(m.id, (m, pj))
|
|
95
|
+
|
|
96
|
+
out: dict[str, tuple] = {}
|
|
97
|
+
for name, inst in sorted(manifest.body.get("instances", {}).items()):
|
|
98
|
+
entry = by_id.get(inst["part"])
|
|
99
|
+
if entry is None:
|
|
100
|
+
raise ValueError(f"instance {name!r}: part {inst['part']!r} not found "
|
|
101
|
+
f"next to the assembly (need its part file + model)")
|
|
102
|
+
part, pj_path = entry
|
|
103
|
+
model_name = part.body.get("model")
|
|
104
|
+
board_name = part.body.get("board")
|
|
105
|
+
if model_name:
|
|
106
|
+
model_file = pj_path.parent / model_name
|
|
107
|
+
if not model_file.is_file():
|
|
108
|
+
raise ValueError(f"instance {name!r}: model file {model_file.name!r} "
|
|
109
|
+
f"missing next to {pj_path.name}")
|
|
110
|
+
doc = resolve_import_paths(
|
|
111
|
+
Document.loads(model_file.read_text(encoding="utf-8")),
|
|
112
|
+
model_file.parent)
|
|
113
|
+
elif board_name:
|
|
114
|
+
from gitcad.bridge import board_to_model
|
|
115
|
+
from gitcad.ecad.board import Board as _Board
|
|
116
|
+
|
|
117
|
+
board_file = pj_path.parent / board_name
|
|
118
|
+
if not board_file.is_file():
|
|
119
|
+
raise ValueError(f"instance {name!r}: board file {board_file.name!r} "
|
|
120
|
+
f"missing next to {pj_path.name}")
|
|
121
|
+
doc = board_to_model(_Board.loads(board_file.read_text(encoding="utf-8")))
|
|
122
|
+
else:
|
|
123
|
+
raise ValueError(f"instance {name!r}: part {part.name!r} has neither "
|
|
124
|
+
f"body.model nor body.board — nothing to build")
|
|
125
|
+
shape = doc.build(kernel).final(doc)
|
|
126
|
+
out[name] = (shape, tuple(inst.get("translate", (0, 0, 0))),
|
|
127
|
+
inst.get("rotate_z_deg", 0.0), part.name)
|
|
128
|
+
return out
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def pcba_mesh_payload(board, kernel: Kernel) -> dict:
|
|
132
|
+
"""A board as 3D with PER-COMPONENT groups — the cross-probe substrate:
|
|
133
|
+
clicking R5 on the schematic selects R5's body here and vice versa.
|
|
134
|
+
Group 'board' is the bare slab; every placed component is its own
|
|
135
|
+
group named by ref."""
|
|
136
|
+
from gitcad.bridge import board_to_model, component_envelope
|
|
137
|
+
|
|
138
|
+
positions: list[float] = []
|
|
139
|
+
colors: list[float] = []
|
|
140
|
+
indices: list[int] = []
|
|
141
|
+
groups: list[dict] = []
|
|
142
|
+
los, his = [], []
|
|
143
|
+
|
|
144
|
+
def add_group(name: str, part: str, shape, color) -> None:
|
|
145
|
+
mesh = kernel.tessellate(shape)
|
|
146
|
+
lo, hi = kernel.bbox(shape)
|
|
147
|
+
los.append(lo)
|
|
148
|
+
his.append(hi)
|
|
149
|
+
base = len(positions) // 3
|
|
150
|
+
positions.extend(mesh["positions"])
|
|
151
|
+
colors.extend(color * (len(mesh["positions"]) // 3))
|
|
152
|
+
indices.extend(base + i for i in mesh["indices"])
|
|
153
|
+
groups.append({"name": name, "part": part,
|
|
154
|
+
"triangles": len(mesh["indices"]) // 3,
|
|
155
|
+
"color": list(color)})
|
|
156
|
+
|
|
157
|
+
slab_doc = board_to_model(board, components=False)
|
|
158
|
+
add_group("board", board.name, slab_doc.build(kernel).final(slab_doc),
|
|
159
|
+
(0.18, 0.42, 0.24)) # soldermask green
|
|
160
|
+
for n, comp in enumerate(board.components):
|
|
161
|
+
env = component_envelope(comp)
|
|
162
|
+
if env is None:
|
|
163
|
+
continue
|
|
164
|
+
cw, ch, h = env
|
|
165
|
+
base_z = board.thickness if comp.side == "top" else -h
|
|
166
|
+
body = kernel.transform(
|
|
167
|
+
kernel.box(cw, ch, h),
|
|
168
|
+
translate=(comp.x - cw / 2, comp.y - ch / 2, base_z))
|
|
169
|
+
add_group(comp.ref, comp.footprint.name, body,
|
|
170
|
+
_PALETTE[(n + 1) % len(_PALETTE)])
|
|
171
|
+
|
|
172
|
+
lo = [min(p[i] for p in los) for i in range(3)] if los else [0, 0, 0]
|
|
173
|
+
hi = [max(p[i] for p in his) for i in range(3)] if his else [0, 0, 0]
|
|
174
|
+
return {
|
|
175
|
+
"kind": "assembly",
|
|
176
|
+
"positions": positions, "colors": colors, "indices": indices,
|
|
177
|
+
"bbox": [lo, hi], "groups": groups,
|
|
178
|
+
"stats": {"vertices": len(positions) // 3, "triangles": len(indices) // 3,
|
|
179
|
+
"instances": len(groups), "kernel": kernel.name,
|
|
180
|
+
"volume_mm3": 0, "features": len(groups)},
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def assembly_mesh_payload(manifest_path: Path, kernel: Kernel) -> dict:
|
|
185
|
+
"""Merge every instance's built, placed geometry into one colored mesh."""
|
|
186
|
+
resolved = resolve_assembly_shapes(manifest_path, kernel)
|
|
187
|
+
|
|
188
|
+
positions: list[float] = []
|
|
189
|
+
colors: list[float] = []
|
|
190
|
+
indices: list[int] = []
|
|
191
|
+
groups: list[dict] = []
|
|
192
|
+
los, his = [], []
|
|
193
|
+
|
|
194
|
+
for n, (name, (shape0, translate, rot_z, part_name)) in enumerate(
|
|
195
|
+
sorted(resolved.items())):
|
|
196
|
+
shape = kernel.transform(shape0, translate=translate,
|
|
197
|
+
rotate_axis=(0, 0, 1), rotate_deg=rot_z)
|
|
198
|
+
mesh = kernel.tessellate(shape)
|
|
199
|
+
lo, hi = kernel.bbox(shape)
|
|
200
|
+
los.append(lo)
|
|
201
|
+
his.append(hi)
|
|
202
|
+
base = len(positions) // 3
|
|
203
|
+
color = _PALETTE[n % len(_PALETTE)]
|
|
204
|
+
positions.extend(mesh["positions"])
|
|
205
|
+
colors.extend(color * (len(mesh["positions"]) // 3))
|
|
206
|
+
indices.extend(base + i for i in mesh["indices"])
|
|
207
|
+
groups.append({"name": name, "part": part_name,
|
|
208
|
+
"triangles": len(mesh["indices"]) // 3,
|
|
209
|
+
"color": list(color)})
|
|
210
|
+
|
|
211
|
+
lo = [min(p[i] for p in los) for i in range(3)] if los else [0, 0, 0]
|
|
212
|
+
hi = [max(p[i] for p in his) for i in range(3)] if his else [0, 0, 0]
|
|
213
|
+
return {
|
|
214
|
+
"kind": "assembly",
|
|
215
|
+
"positions": positions, "colors": colors, "indices": indices,
|
|
216
|
+
"bbox": [lo, hi], "groups": groups,
|
|
217
|
+
"stats": {"vertices": len(positions) // 3, "triangles": len(indices) // 3,
|
|
218
|
+
"instances": len(groups), "kernel": kernel.name,
|
|
219
|
+
"volume_mm3": 0, "features": len(groups)},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def resolve_import_paths(doc: Document, base: Path) -> Document:
|
|
224
|
+
"""Import-op file params are project-relative in committed models
|
|
225
|
+
(portability); builds resolve them against the MODEL's directory."""
|
|
226
|
+
for f in doc.features:
|
|
227
|
+
if f.op == "import":
|
|
228
|
+
p = Path(f.params.get("file", ""))
|
|
229
|
+
if p and not p.is_absolute():
|
|
230
|
+
f.params["file"] = str((base / p).resolve())
|
|
231
|
+
return doc
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def run_checks_for(path: Path, kernel: Kernel) -> dict:
|
|
235
|
+
"""The design's live check suite, by kind — the viewer's checks panel.
|
|
236
|
+
|
|
237
|
+
Every result is {check: name, ok, violations}; the rollup is honest
|
|
238
|
+
about coverage (what ran is listed, not implied)."""
|
|
239
|
+
text = path.read_text(encoding="utf-8")
|
|
240
|
+
kind = detect_kind(text)
|
|
241
|
+
results: list[dict] = []
|
|
242
|
+
|
|
243
|
+
def add(name: str, report) -> None:
|
|
244
|
+
results.append({"check": name, "ok": report.ok,
|
|
245
|
+
"violations": list(report.violations)})
|
|
246
|
+
|
|
247
|
+
if kind == "board":
|
|
248
|
+
from gitcad.ecad import Board, check_connectivity
|
|
249
|
+
from gitcad.ecad.drc import run_drc
|
|
250
|
+
|
|
251
|
+
board = Board.loads(text)
|
|
252
|
+
add("validate", board.validate())
|
|
253
|
+
add("drc", run_drc(board))
|
|
254
|
+
add("connectivity", check_connectivity(board))
|
|
255
|
+
elif kind == "schematic":
|
|
256
|
+
from gitcad.ecad import Schematic, check_envelopes
|
|
257
|
+
|
|
258
|
+
sch = Schematic.loads(text)
|
|
259
|
+
add("erc", sch.erc())
|
|
260
|
+
add("envelope", check_envelopes(sch))
|
|
261
|
+
elif kind == "pcba":
|
|
262
|
+
from gitcad.pcba import pcba_verify
|
|
263
|
+
|
|
264
|
+
r = pcba_verify(text, str(path.parent))
|
|
265
|
+
results.append({"check": "pcba", "ok": r["ok"],
|
|
266
|
+
"violations": r["violations"]})
|
|
267
|
+
elif kind == "assembly":
|
|
268
|
+
from gitcad.part.interference import check_interference
|
|
269
|
+
|
|
270
|
+
resolved = resolve_assembly_shapes(path, kernel)
|
|
271
|
+
instances = {n: (s, t, r) for n, (s, t, r, _p) in resolved.items()}
|
|
272
|
+
add("interference", check_interference(kernel, instances))
|
|
273
|
+
elif kind == "model":
|
|
274
|
+
doc = resolve_import_paths(Document.loads(text), path.parent)
|
|
275
|
+
shape = doc.build(kernel).final(doc)
|
|
276
|
+
add("geometry", kernel.validate(shape))
|
|
277
|
+
|
|
278
|
+
ok = all(r["ok"] for r in results)
|
|
279
|
+
return {"kind": kind, "ok": ok, "results": results,
|
|
280
|
+
"total_violations": sum(len(r["violations"]) for r in results)}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def discover_schematics(root: Path, limit: int = 12) -> list[dict]:
|
|
284
|
+
"""The schematics that make up a design, rendered for review.
|
|
285
|
+
|
|
286
|
+
Scans the design's directory tree for schematic sources: ``*.kicad_sch``
|
|
287
|
+
(rendered exactly as drawn via the importer + fidelity renderer) and
|
|
288
|
+
``*.schematic.json`` (canonical gitcad schematics, auto-layout). This is
|
|
289
|
+
the electrical view under a 3D assembly — same sibling-scan rule the
|
|
290
|
+
assembly mesh resolver uses."""
|
|
291
|
+
out: list[dict] = []
|
|
292
|
+
sources = (sorted(root.rglob("*.kicad_sch"))
|
|
293
|
+
+ sorted(root.rglob("*.schematic.json"))
|
|
294
|
+
+ sorted(root.rglob("*.sch.json"))
|
|
295
|
+
+ sorted(p for p in root.rglob("*.sch")
|
|
296
|
+
if not p.name.endswith(".kicad_sch")))
|
|
297
|
+
for src in sources[:limit]:
|
|
298
|
+
try:
|
|
299
|
+
if src.suffix == ".kicad_sch":
|
|
300
|
+
from gitcad.ecad.schsvg import sheet_to_svg
|
|
301
|
+
from gitcad.importers.kicad_sch import import_kicad_sch
|
|
302
|
+
|
|
303
|
+
sch, _report = import_kicad_sch(str(src))
|
|
304
|
+
svg = sheet_to_svg(sch)
|
|
305
|
+
else:
|
|
306
|
+
from gitcad.ecad import Schematic, schematic_to_svg
|
|
307
|
+
|
|
308
|
+
sch = Schematic.loads(src.read_text(encoding="utf-8"))
|
|
309
|
+
svg = schematic_to_svg(sch)
|
|
310
|
+
out.append({"name": sch.name, "file": src.name, "svg": svg})
|
|
311
|
+
except Exception as exc: # a broken sheet must not sink the review
|
|
312
|
+
out.append({"name": src.stem, "file": src.name,
|
|
313
|
+
"error": f"{type(exc).__name__}: {exc}"})
|
|
314
|
+
return out
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class _Handler(BaseHTTPRequestHandler):
|
|
318
|
+
# Set by serve(): watched file path + kernel (+ optional review base ref).
|
|
319
|
+
path_watched: Path
|
|
320
|
+
kernel: Kernel
|
|
321
|
+
review_base: str | None = None
|
|
322
|
+
|
|
323
|
+
def log_message(self, *args) -> None: # quiet
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
def _send(self, status: int, content: bytes, ctype: str) -> None:
|
|
327
|
+
self.send_response(status)
|
|
328
|
+
self.send_header("Content-Type", ctype)
|
|
329
|
+
self.send_header("Content-Length", str(len(content)))
|
|
330
|
+
self.send_header("Cache-Control", "no-store")
|
|
331
|
+
self.end_headers()
|
|
332
|
+
self.wfile.write(content)
|
|
333
|
+
|
|
334
|
+
def do_GET(self) -> None: # noqa: N802 - http.server API
|
|
335
|
+
try:
|
|
336
|
+
text = self.path_watched.read_text(encoding="utf-8")
|
|
337
|
+
if self.path == "/":
|
|
338
|
+
self._send(200, PAGE.encode(), "text/html; charset=utf-8")
|
|
339
|
+
elif self.path == "/api/version":
|
|
340
|
+
digest = hashlib.sha256(text.encode()).hexdigest()
|
|
341
|
+
kind = detect_kind(text)
|
|
342
|
+
self._send(200, json.dumps({"version": digest, "kind": kind,
|
|
343
|
+
"name": self.path_watched.name,
|
|
344
|
+
"review_base": self.review_base}).encode(),
|
|
345
|
+
"application/json")
|
|
346
|
+
elif self.path == "/api/review":
|
|
347
|
+
if not self.review_base:
|
|
348
|
+
self._send(404, b'{"error": "no review base configured"}',
|
|
349
|
+
"application/json")
|
|
350
|
+
return
|
|
351
|
+
import subprocess as _sp
|
|
352
|
+
|
|
353
|
+
from gitcad.review import review_range
|
|
354
|
+
|
|
355
|
+
top = _sp.run(["git", "-C", str(self.path_watched.parent),
|
|
356
|
+
"rev-parse", "--show-toplevel"],
|
|
357
|
+
capture_output=True, text=True)
|
|
358
|
+
if top.returncode != 0:
|
|
359
|
+
raise ValueError("watched file is not inside a git repo")
|
|
360
|
+
report = review_range(top.stdout.strip(), self.review_base)
|
|
361
|
+
self._send(200, json.dumps(report).encode(), "application/json")
|
|
362
|
+
elif self.path == "/api/mesh":
|
|
363
|
+
kind = detect_kind(text)
|
|
364
|
+
if kind == "assembly":
|
|
365
|
+
payload = assembly_mesh_payload(self.path_watched, self.kernel)
|
|
366
|
+
elif kind == "pcba":
|
|
367
|
+
from gitcad.pcba import pcba_sources
|
|
368
|
+
|
|
369
|
+
src = pcba_sources(text, str(self.path_watched.parent))
|
|
370
|
+
board = Board.loads(src["board"].read_text(encoding="utf-8"))
|
|
371
|
+
payload = pcba_mesh_payload(board, self.kernel)
|
|
372
|
+
else:
|
|
373
|
+
payload = mesh_payload(
|
|
374
|
+
resolve_import_paths(Document.loads(text),
|
|
375
|
+
self.path_watched.parent),
|
|
376
|
+
self.kernel)
|
|
377
|
+
self._send(200, json.dumps(payload).encode(), "application/json")
|
|
378
|
+
elif self.path == "/api/checks":
|
|
379
|
+
self._send(200, json.dumps(
|
|
380
|
+
run_checks_for(self.path_watched, self.kernel)).encode(),
|
|
381
|
+
"application/json")
|
|
382
|
+
elif self.path == "/api/schematics":
|
|
383
|
+
sheets = discover_schematics(self.path_watched.parent)
|
|
384
|
+
self._send(200, json.dumps({"sheets": sheets}).encode(),
|
|
385
|
+
"application/json")
|
|
386
|
+
elif self.path == "/api/board.svg":
|
|
387
|
+
kind = detect_kind(text)
|
|
388
|
+
if kind == "schematic":
|
|
389
|
+
from gitcad.ecad import Schematic, schematic_to_svg
|
|
390
|
+
|
|
391
|
+
svg = schematic_to_svg(Schematic.loads(text))
|
|
392
|
+
elif kind == "pcba":
|
|
393
|
+
from gitcad.pcba import pcba_sources
|
|
394
|
+
|
|
395
|
+
src = pcba_sources(text, str(self.path_watched.parent))
|
|
396
|
+
svg = board_to_svg(Board.loads(
|
|
397
|
+
src["board"].read_text(encoding="utf-8")))
|
|
398
|
+
else:
|
|
399
|
+
svg = board_to_svg(Board.loads(text))
|
|
400
|
+
self._send(200, svg.encode(), "image/svg+xml")
|
|
401
|
+
else:
|
|
402
|
+
self._send(404, b"not found", "text/plain")
|
|
403
|
+
except Exception as exc: # surface errors to the page, never crash
|
|
404
|
+
self._send(500, json.dumps({"error": f"{type(exc).__name__}: {exc}"}).encode(),
|
|
405
|
+
"application/json")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def serve(path: str, port: int = 8137, kernel: Kernel | None = None,
|
|
409
|
+
review_base: str | None = None) -> ThreadingHTTPServer:
|
|
410
|
+
"""Start (and return) the server; caller decides whether to block.
|
|
411
|
+
``review_base``: a git ref — the viewer grows a review tab comparing
|
|
412
|
+
the design's repo against it (the gitcad-review report, in-app)."""
|
|
413
|
+
handler = type("Handler", (_Handler,), {
|
|
414
|
+
"path_watched": Path(path),
|
|
415
|
+
"kernel": kernel or get_kernel(),
|
|
416
|
+
"review_base": review_base,
|
|
417
|
+
})
|
|
418
|
+
return ThreadingHTTPServer(("127.0.0.1", port), handler)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def main() -> None: # pragma: no cover - CLI entrypoint
|
|
422
|
+
ap = argparse.ArgumentParser(description="gitcad viewer — live local window on a model or board")
|
|
423
|
+
ap.add_argument("file", help="a .gitcad.json model or board document")
|
|
424
|
+
ap.add_argument("--port", type=int, default=8137)
|
|
425
|
+
ap.add_argument("--review", metavar="BASE",
|
|
426
|
+
help="git ref to review against (adds the review tab)")
|
|
427
|
+
args = ap.parse_args()
|
|
428
|
+
httpd = serve(args.file, args.port, review_base=args.review)
|
|
429
|
+
print(f"gitcad viewer: http://127.0.0.1:{args.port}/ (watching {args.file}, Ctrl+C to stop)")
|
|
430
|
+
try:
|
|
431
|
+
httpd.serve_forever()
|
|
432
|
+
except KeyboardInterrupt:
|
|
433
|
+
pass
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitcad
|
|
3
|
+
Version: 0.7.7
|
|
4
|
+
Summary: Agent-first, headless, git-native CAD (metapackage: core + mech + ecad + MCP + viewer + release)
|
|
5
|
+
Project-URL: Homepage, https://gitcad.xyz
|
|
6
|
+
Project-URL: Repository, https://github.com/gitcad-xyz/gitcad
|
|
7
|
+
Project-URL: Issues, https://github.com/gitcad-xyz/gitcad/issues
|
|
8
|
+
Author-email: Dan Willis <danielcwillis@gmail.com>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Keywords: agent,brep,cad,eda,git,headless,mcp,occt
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Manufacturing
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: gitcad-core==0.7.7
|
|
22
|
+
Requires-Dist: gitcad-ecad==0.7.7
|
|
23
|
+
Requires-Dist: gitcad-mech==0.7.7
|
|
24
|
+
Requires-Dist: mcp>=1.0
|
|
25
|
+
Provides-Extra: occt
|
|
26
|
+
Requires-Dist: gitcad-mech[occt]==0.7.7; extra == 'occt'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# gitcad
|
|
30
|
+
|
|
31
|
+
**Agent-first, headless, git-native CAD — mechanical and electrical, one substrate.**
|
|
32
|
+
|
|
33
|
+
gitcad is a boundary-representation CAD system designed to be driven by agents and
|
|
34
|
+
version-controlled like source. Models are byte-canonical text (geometry is a
|
|
35
|
+
build artifact, never committed); identity is construction-lineage based, never
|
|
36
|
+
ordinal; every build produces a reviewable, diffable report.
|
|
37
|
+
|
|
38
|
+
This metapackage installs the whole system:
|
|
39
|
+
|
|
40
|
+
| package | domain |
|
|
41
|
+
|---|---|
|
|
42
|
+
| [`gitcad-core`](https://pypi.org/project/gitcad-core/) | canonical text, stable identity, the Part standard, report pipeline |
|
|
43
|
+
| [`gitcad-mech`](https://pypi.org/project/gitcad-mech/) | document model, **exact B-rep kernel**, sketches, drawings, importers |
|
|
44
|
+
| [`gitcad-ecad`](https://pypi.org/project/gitcad-ecad/) | schematic + ERC, board + DRC, fab outputs, KiCad import |
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install gitcad # full system: both domains, exact+native kernel, MCP server
|
|
50
|
+
pip install gitcad[occt] # + OCCT kernel oracle/fallback (cadquery-ocp)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`pip install gitcad` is complete and agent-ready out of the box — mechanical and
|
|
54
|
+
electrical, the exact kernel with its native accelerator, and the `gitcad-mcp`
|
|
55
|
+
Model Context Protocol server. Only OCCT (a heavy optional oracle) is an extra.
|
|
56
|
+
|
|
57
|
+
The mechanical kernel is **exact**: topological decisions are made in rational
|
|
58
|
+
arithmetic (ℚ), never by a floating-point tolerance — see
|
|
59
|
+
[`forgekernel`](https://pypi.org/project/forgekernel/) and the
|
|
60
|
+
[benchmarks vs OCCT](https://github.com/gitcad-xyz/gitcad/blob/main/bench/RUST-vs-OCCT.md).
|
|
61
|
+
The compiled Rust build of the hot paths installs **by default** on mainstream
|
|
62
|
+
platforms (identical results, only faster); on an architecture with no wheel it is
|
|
63
|
+
skipped and the kernel runs pure-Python — same answers, nothing breaks.
|
|
64
|
+
|
|
65
|
+
## Console tools
|
|
66
|
+
|
|
67
|
+
`gitcad-init`, `gitcad-render`, `gitcad-view`, `gitcad-review`, `gitcad-merge`,
|
|
68
|
+
`gitcad-verify`, `gitcad-convert`, `gitcad-explore`, `gitcad-mcp`, `gitcad-lot`.
|
|
69
|
+
|
|
70
|
+
Apache-2.0 · https://gitcad.xyz · https://github.com/gitcad-xyz/gitcad
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
gitcad/bridge.py,sha256=ydnRrFruXfpkzpxW9s3fvvZ6Ed-rvkQ03k00707A-VY,3118
|
|
2
|
+
gitcad/convert.py,sha256=N4NgC_7u6B43YkX2x-5CgtB4zDG99N8FIBlOJmRpBHQ,1270
|
|
3
|
+
gitcad/explore.py,sha256=DYssVwSzMaboe7byY4QXmn5Hp49Ez4ROYrjmRvhn810,4057
|
|
4
|
+
gitcad/fasteners.py,sha256=iSuMmNfpJuw_S_S1AZkritWedPiiClqKEnBXh4iFngc,5019
|
|
5
|
+
gitcad/init.py,sha256=bhV8B5PvkPNql0R7L1BIcwY5OR8690wQ1o-foF0qXOU,5227
|
|
6
|
+
gitcad/lots.py,sha256=PzSBRd6v2WY5wzMArxRKy5Dxf9r2N81erHO8HfvM0Ds,5318
|
|
7
|
+
gitcad/merge3.py,sha256=R6EGdHJc-tAmXA8nUfHKRofW1avsM29eT3v7RA_HZdQ,12723
|
|
8
|
+
gitcad/pcba.py,sha256=1qadOC0eOcCgBX4l-MmZ6XbZXfMtYY1Fi9kMrZUJpos,4608
|
|
9
|
+
gitcad/release.py,sha256=LgI3Xizypx3rUQEvv4pKYubd1XxWYFj61AhHKaCUUUs,9544
|
|
10
|
+
gitcad/render.py,sha256=NAIBHtmeIoAYJ7EsP2IlNbH-6untU_dB85y4ALj70Sw,6538
|
|
11
|
+
gitcad/requirements.py,sha256=YFtpRmXigLQth7By1k_Q7Yh1meR-OH2MMlncyPQYKe8,8713
|
|
12
|
+
gitcad/review.py,sha256=GyYCQcgQ15hd0wHI5TVMF1ykLB16uJFaf8qWAd-dxkM,10619
|
|
13
|
+
gitcad/bench/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
gitcad/bench/corpus.py,sha256=vWe1mEJkt3tDGO1dcDDXTbdk5FXnrjjp6_SRAOZHs4U,11160
|
|
15
|
+
gitcad/bench/scorecard.py,sha256=GnaTVsM5ciJXrMG4h669iLM7CmXhCZIeg8LZoxDBYQk,7253
|
|
16
|
+
gitcad/mcp/__init__.py,sha256=XW7uUDunve_DVcKQ0dpKbXqGqwEfgM_bsoABhP-a7JY,634
|
|
17
|
+
gitcad/mcp/server.py,sha256=VfB6_3RtihZ7EJPp41wFY5uMWVWz-LQ6lbPYETIe8Go,46541
|
|
18
|
+
gitcad/viewer/__init__.py,sha256=ExgmL9GO8MGHeVrm58MqVtRBK08RtTprg01mIm6VpgU,833
|
|
19
|
+
gitcad/viewer/boardsvg.py,sha256=s1_Vx1bzT-B6ua67oLuJY7veM7AVR-xykmzdtZ-zEfQ,3533
|
|
20
|
+
gitcad/viewer/page.py,sha256=_YgG3U8F5Tj3Xonxgu7_glFXT_sxU04RvEIQPpHH1ZI,25109
|
|
21
|
+
gitcad/viewer/server.py,sha256=_TkDnclkHclJ2UCIg8adKpDuxsHYWchWfyIOlpdB424,18703
|
|
22
|
+
gitcad-0.7.7.dist-info/METADATA,sha256=PCv1FjKYwG9NfiPCIOjqDMqWo4MmdRBrRk06kJQjdlI,3280
|
|
23
|
+
gitcad-0.7.7.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
24
|
+
gitcad-0.7.7.dist-info/entry_points.txt,sha256=XY43SA_2LlVcBzEmu8PwqzYNovfjPbinfvvOwccb0Y8,374
|
|
25
|
+
gitcad-0.7.7.dist-info/RECORD,,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
[console_scripts]
|
|
2
|
+
gitcad-convert = gitcad.convert:main
|
|
3
|
+
gitcad-explore = gitcad.explore:main
|
|
4
|
+
gitcad-init = gitcad.init:main
|
|
5
|
+
gitcad-lot = gitcad.lots:main
|
|
6
|
+
gitcad-mcp = gitcad.mcp.server:main
|
|
7
|
+
gitcad-merge = gitcad.merge3:main
|
|
8
|
+
gitcad-render = gitcad.render:main
|
|
9
|
+
gitcad-review = gitcad.review:main
|
|
10
|
+
gitcad-verify = gitcad.requirements:main
|
|
11
|
+
gitcad-view = gitcad.viewer.server:main
|