gitcad-mech 0.7.7__tar.gz

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.
Files changed (30) hide show
  1. gitcad_mech-0.7.7/.gitignore +38 -0
  2. gitcad_mech-0.7.7/PKG-INFO +50 -0
  3. gitcad_mech-0.7.7/README.md +23 -0
  4. gitcad_mech-0.7.7/pyproject.toml +46 -0
  5. gitcad_mech-0.7.7/src/gitcad/analysis.py +181 -0
  6. gitcad_mech-0.7.7/src/gitcad/derive.py +60 -0
  7. gitcad_mech-0.7.7/src/gitcad/document.py +662 -0
  8. gitcad_mech-0.7.7/src/gitcad/drawing/__init__.py +14 -0
  9. gitcad_mech-0.7.7/src/gitcad/drawing/assembly.py +103 -0
  10. gitcad_mech-0.7.7/src/gitcad/drawing/dxf.py +63 -0
  11. gitcad_mech-0.7.7/src/gitcad/drawing/hlr.py +39 -0
  12. gitcad_mech-0.7.7/src/gitcad/drawing/pdf.py +108 -0
  13. gitcad_mech-0.7.7/src/gitcad/drawing/sections.py +137 -0
  14. gitcad_mech-0.7.7/src/gitcad/drawing/sheet.py +286 -0
  15. gitcad_mech-0.7.7/src/gitcad/drawing/svg.py +144 -0
  16. gitcad_mech-0.7.7/src/gitcad/importers/fcstd.py +163 -0
  17. gitcad_mech-0.7.7/src/gitcad/importers/fcstd_tree.py +183 -0
  18. gitcad_mech-0.7.7/src/gitcad/importers/recognize.py +111 -0
  19. gitcad_mech-0.7.7/src/gitcad/importers/step.py +41 -0
  20. gitcad_mech-0.7.7/src/gitcad/kernel/__init__.py +32 -0
  21. gitcad_mech-0.7.7/src/gitcad/kernel/auto.py +96 -0
  22. gitcad_mech-0.7.7/src/gitcad/kernel/null.py +254 -0
  23. gitcad_mech-0.7.7/src/gitcad/kernel/occt.py +719 -0
  24. gitcad_mech-0.7.7/src/gitcad/kernel/ref.py +781 -0
  25. gitcad_mech-0.7.7/src/gitcad/kernel/rustref.py +232 -0
  26. gitcad_mech-0.7.7/src/gitcad/select.py +51 -0
  27. gitcad_mech-0.7.7/src/gitcad/sheetmetal.py +397 -0
  28. gitcad_mech-0.7.7/src/gitcad/sketch.py +164 -0
  29. gitcad_mech-0.7.7/src/gitcad/sketch_solver.py +230 -0
  30. gitcad_mech-0.7.7/src/gitcad/weldment.py +126 -0
@@ -0,0 +1,38 @@
1
+ # Generated geometry is a build artifact, NOT source (ADR-0004). Never commit it.
2
+ *.brep
3
+ *.step
4
+ *.stp
5
+ *.stl
6
+ *.gltf
7
+ *.glb
8
+ *.iges
9
+ *.igs
10
+ # Drawings are generated from the model too.
11
+ /build-artifacts/
12
+ *.dxf
13
+ # PDFs generated from drawings (keep hand-authored docs elsewhere if needed).
14
+ /out/
15
+
16
+ # Python
17
+ __pycache__/
18
+ *.py[cod]
19
+ *.egg-info/
20
+ .eggs/
21
+ build/
22
+ dist/
23
+ .venv/
24
+ venv/
25
+
26
+ # Tooling
27
+ .pytest_cache/
28
+ .coverage
29
+ .coverage.*
30
+ htmlcov/
31
+ .mypy_cache/
32
+ .ruff_cache/
33
+
34
+ # Editors / OS
35
+ .vscode/
36
+ .idea/
37
+ .DS_Store
38
+ Thumbs.db
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitcad-mech
3
+ Version: 0.7.7
4
+ Summary: gitcad mechanical: document model, exact B-rep kernel, sketches, drawings, mech importers
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
+ Project-URL: Kernel (forgekernel), https://github.com/gitcad-xyz/forge
9
+ Author-email: Dan Willis <danielcwillis@gmail.com>
10
+ License-Expression: Apache-2.0
11
+ Keywords: brep,cad,exact-arithmetic,kernel,nurbs,occt,sheet-metal,step
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Manufacturing
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: forgekernel-rs>=0.1.0; platform_machine in 'x86_64 AMD64 aarch64 arm64'
22
+ Requires-Dist: forgekernel>=0.1.0
23
+ Requires-Dist: gitcad-core==0.7.7
24
+ Provides-Extra: occt
25
+ Requires-Dist: cadquery-ocp>=7.7; extra == 'occt'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # gitcad-mech
29
+
30
+ Mechanical domain for [**gitcad**](https://gitcad.xyz) — the parametric document
31
+ model and geometry kernels.
32
+
33
+ - **Document model** — feature tree with stable identity; builds to a solid and a
34
+ reviewable report.
35
+ - **Exact kernel by default** — ships [`forgekernel`](https://pypi.org/project/forgekernel/),
36
+ a from-scratch B-rep kernel whose topological decisions are made in exact
37
+ rational arithmetic (ℚ), never by floating-point epsilon. OCCT
38
+ (`cadquery-ocp`) is available as an independent oracle / fallback.
39
+ - **Sketches, extrude/revolve, loft, sweep, fillet/chamfer, shell, patterns,
40
+ sheet metal, weldments, holes with thread specs.**
41
+ - **Associative drawings** — HLR views, section/detail views, dimensions, GD&T,
42
+ weld & surface-finish symbols → SVG/PDF/DXF.
43
+ - **Importers** — STEP, FCStd (parametric tree), with verified feature recovery.
44
+
45
+ ```bash
46
+ pip install gitcad-mech # exact kernel + native accelerator (default where a wheel exists)
47
+ pip install gitcad-mech[occt] # + OCCT oracle/fallback (cadquery-ocp)
48
+ ```
49
+
50
+ Apache-2.0 · https://github.com/gitcad-xyz/gitcad
@@ -0,0 +1,23 @@
1
+ # gitcad-mech
2
+
3
+ Mechanical domain for [**gitcad**](https://gitcad.xyz) — the parametric document
4
+ model and geometry kernels.
5
+
6
+ - **Document model** — feature tree with stable identity; builds to a solid and a
7
+ reviewable report.
8
+ - **Exact kernel by default** — ships [`forgekernel`](https://pypi.org/project/forgekernel/),
9
+ a from-scratch B-rep kernel whose topological decisions are made in exact
10
+ rational arithmetic (ℚ), never by floating-point epsilon. OCCT
11
+ (`cadquery-ocp`) is available as an independent oracle / fallback.
12
+ - **Sketches, extrude/revolve, loft, sweep, fillet/chamfer, shell, patterns,
13
+ sheet metal, weldments, holes with thread specs.**
14
+ - **Associative drawings** — HLR views, section/detail views, dimensions, GD&T,
15
+ weld & surface-finish symbols → SVG/PDF/DXF.
16
+ - **Importers** — STEP, FCStd (parametric tree), with verified feature recovery.
17
+
18
+ ```bash
19
+ pip install gitcad-mech # exact kernel + native accelerator (default where a wheel exists)
20
+ pip install gitcad-mech[occt] # + OCCT oracle/fallback (cadquery-ocp)
21
+ ```
22
+
23
+ Apache-2.0 · https://github.com/gitcad-xyz/gitcad
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "gitcad-mech"
7
+ version = "0.7.7"
8
+ description = "gitcad mechanical: document model, exact B-rep kernel, sketches, drawings, mech importers"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "Apache-2.0"
12
+ authors = [{name = "Dan Willis", email = "danielcwillis@gmail.com"}]
13
+ keywords = ["cad", "brep", "kernel", "nurbs", "exact-arithmetic", "sheet-metal", "step", "occt"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Manufacturing",
18
+ "License :: OSI Approved :: Apache Software License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Scientific/Engineering",
22
+ "Topic :: Multimedia :: Graphics :: 3D Modeling",
23
+ ]
24
+ # The exact forge kernel is the default backend. forgekernel (pure Python) is
25
+ # the always-present floor; the native forgekernel_rs accelerator installs BY
26
+ # DEFAULT on mainstream architectures (identical results, faster hot paths) and
27
+ # is skipped only on exotic arches with no wheel — where forge auto-falls-back
28
+ # to pure Python at runtime, so nothing breaks.
29
+ dependencies = [
30
+ "gitcad-core==0.7.7",
31
+ "forgekernel>=0.1.0",
32
+ "forgekernel_rs>=0.1.0; platform_machine in 'x86_64 AMD64 aarch64 arm64'",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ # OCCT independent oracle / fallback kernel.
37
+ occt = ["cadquery-ocp>=7.7"]
38
+
39
+ [project.urls]
40
+ Homepage = "https://gitcad.xyz"
41
+ Repository = "https://github.com/gitcad-xyz/gitcad"
42
+ Issues = "https://github.com/gitcad-xyz/gitcad/issues"
43
+ "Kernel (forgekernel)" = "https://github.com/gitcad-xyz/forge"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/gitcad"]
@@ -0,0 +1,181 @@
1
+ """Mechanical analysis: exact inertia through the seam + DFM checks.
2
+
3
+ `inertia` surfaces the exact rational inertia tensor forge computes for
4
+ its solid classes (planar and freeform, via the divergence-theorem flux)
5
+ and falls back to OCCT's float `MatrixOfInertia` for OCCT shapes.
6
+ `draft_analysis` is a moldability check: faces whose draft angle to a
7
+ pull direction is below a minimum can't release from the tool.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ from typing import Any
14
+
15
+
16
+ def inertia(kernel, shape) -> dict[str, Any]:
17
+ """Volume, centroid, and inertia tensor (about the centroid) + the
18
+ three principal moments. Exact rationals when forge built the shape;
19
+ floats via OCCT otherwise. Returns floats for a uniform interface,
20
+ with ``exact=True`` when the underlying tensor is rational."""
21
+ exact = False
22
+ try:
23
+ from forgekernel.bsolid import (PatchSolid, mass_properties,
24
+ polyhedron_mass_properties)
25
+ from forgekernel.brep import Solid as _Solid
26
+
27
+ if isinstance(shape, _Solid):
28
+ mp = polyhedron_mass_properties(shape)
29
+ exact = True
30
+ elif isinstance(shape, PatchSolid):
31
+ mp = mass_properties(shape)
32
+ exact = True
33
+ else:
34
+ mp = None
35
+ except ImportError:
36
+ mp = None
37
+
38
+ if mp is not None:
39
+ V = float(mp["volume"])
40
+ c = tuple(float(x) for x in mp["centroid"])
41
+ I = [[float(mp["inertia"][i][j]) for j in range(3)] for i in range(3)]
42
+ else:
43
+ # any kernel's mass_props returns the tensor about the centroid;
44
+ # route through the seam (no direct OCP import — ADR-0002).
45
+ m = kernel.mass_props(shape)
46
+ V = float(m["volume"])
47
+ c = (m["cx"], m["cy"], m["cz"])
48
+ I = [[m["ixx"], m["ixy"], m["ixz"]],
49
+ [m["ixy"], m["iyy"], m["iyz"]],
50
+ [m["ixz"], m["iyz"], m["izz"]]]
51
+
52
+ principal = sorted(_sym_eigs_3x3(I))
53
+ return {"volume": V, "centroid": c, "inertia": I,
54
+ "principal_moments": principal, "exact": exact}
55
+
56
+
57
+ def _sym_eigs_3x3(A):
58
+ """Eigenvalues of a symmetric 3×3 matrix (closed form; analysis only)."""
59
+ p1 = A[0][1] ** 2 + A[0][2] ** 2 + A[1][2] ** 2
60
+ q = (A[0][0] + A[1][1] + A[2][2]) / 3
61
+ p2 = ((A[0][0] - q) ** 2 + (A[1][1] - q) ** 2 + (A[2][2] - q) ** 2
62
+ + 2 * p1)
63
+ if p2 == 0:
64
+ return [A[0][0], A[1][1], A[2][2]]
65
+ p = math.sqrt(p2 / 6)
66
+ B = [[(A[i][j] - (q if i == j else 0)) / p for j in range(3)]
67
+ for i in range(3)]
68
+ detB = (B[0][0] * (B[1][1] * B[2][2] - B[1][2] * B[2][1])
69
+ - B[0][1] * (B[1][0] * B[2][2] - B[1][2] * B[2][0])
70
+ + B[0][2] * (B[1][0] * B[2][1] - B[1][1] * B[2][0]))
71
+ r = max(-1.0, min(1.0, detB / 2))
72
+ phi = math.acos(r) / 3
73
+ e1 = q + 2 * p * math.cos(phi)
74
+ e3 = q + 2 * p * math.cos(phi + 2 * math.pi / 3)
75
+ e2 = 3 * q - e1 - e3
76
+ return [e1, e2, e3]
77
+
78
+
79
+ def draft_analysis(kernel, shape, pull=(0, 0, 1),
80
+ min_angle_deg: float = 1.0) -> dict[str, Any]:
81
+ """Moldability: for a pull (ejection) direction, report planar faces
82
+ whose draft angle — 90° minus the angle between the face normal and
83
+ the pull — is below ``min_angle_deg``. Zero-draft walls (parallel to
84
+ the pull) fail hardest; floors/ceilings (perpendicular) pass."""
85
+ if math.sqrt(sum(float(c) ** 2 for c in pull)) < 1e-12:
86
+ raise ValueError("draft_analysis: pull direction must be non-zero")
87
+ d = _unit(pull)
88
+ faces = kernel.entities(shape, "face")
89
+ violations = []
90
+ for i, f in enumerate(faces):
91
+ n = f.get("plane")
92
+ if n is None: # curved face: skip (report as such)
93
+ continue
94
+ n = _unit(n)
95
+ dot = abs(sum(n[c] * d[c] for c in range(3)))
96
+ theta = math.degrees(math.acos(max(-1.0, min(1.0, dot))))
97
+ draft = 90.0 - theta
98
+ if draft < min_angle_deg:
99
+ violations.append({"face": i, "draft_deg": round(draft, 4),
100
+ "normal": [round(x, 6) for x in n]})
101
+ return {"pull": list(d), "min_angle_deg": min_angle_deg,
102
+ "faces_checked": len(faces), "violations": violations,
103
+ "ok": not violations}
104
+
105
+
106
+ def _unit(v):
107
+ m = math.sqrt(sum(float(c) ** 2 for c in v)) or 1.0
108
+ return tuple(float(c) / m for c in v)
109
+
110
+
111
+ def thickness_analysis(kernel, shape, min_wall: float = 1.0) -> dict[str, Any]:
112
+ """Min-wall (moldability/printability) estimate for a planar forge
113
+ solid: the smallest separation between an anti-parallel pair of faces
114
+ whose projections overlap. Exact for prismatic parts (a box reports
115
+ min(dx,dy,dz)); a first-order estimate elsewhere (not a full medial
116
+ axis). Raises for non-forge shapes."""
117
+ try:
118
+ from forgekernel.brep import Solid as _Solid
119
+ except ImportError: # pragma: no cover
120
+ _Solid = ()
121
+ if not isinstance(shape, _Solid):
122
+ raise NotImplementedError(
123
+ "thickness_analysis: planar forge solids only")
124
+ # group polygons by (unit normal, plane offset)
125
+ faces = []
126
+ for poly in shape.polys:
127
+ n = _unit(poly.plane.n)
128
+ # a point on the plane → signed offset d = n·p
129
+ v0 = poly.verts[0]
130
+ d = sum(n[c] * float(v0[c]) for c in range(3))
131
+ pts = [tuple(float(x) for x in v) for v in poly.verts]
132
+ faces.append((n, d, pts))
133
+ thin = []
134
+ min_t = float("inf")
135
+ for i in range(len(faces)):
136
+ ni, di, pi = faces[i]
137
+ for j in range(i + 1, len(faces)):
138
+ nj, dj, pj = faces[j]
139
+ # anti-parallel?
140
+ if abs(ni[0] + nj[0]) > 1e-9 or abs(ni[1] + nj[1]) > 1e-9 \
141
+ or abs(ni[2] + nj[2]) > 1e-9:
142
+ continue
143
+ # projected-bbox overlap onto the plane's two in-plane axes
144
+ if not _proj_overlap(ni, pi, pj):
145
+ continue
146
+ # nj = −ni. In the nᵢ coordinate face i sits at di and face j at
147
+ # −dj; SOLID lies between them only when di + dj > 0 (that IS the
148
+ # wall thickness). di + dj < 0 is an EMPTY slot/pocket — not a
149
+ # wall — so the sign must NOT be discarded with abs() (that
150
+ # flagged open slots as thin material, a false DFM failure).
151
+ t = di + dj
152
+ if t <= 1e-12:
153
+ continue
154
+ if t < min_t:
155
+ min_t = t
156
+ if t < min_wall:
157
+ thin.append({"faces": [i, j], "thickness": round(t, 6)})
158
+ return {"min_wall_target": min_wall,
159
+ "min_thickness": None if min_t == float("inf") else round(min_t, 6),
160
+ "thin_regions": thin, "ok": min_t >= min_wall}
161
+
162
+
163
+ def _proj_overlap(n, pa, pb) -> bool:
164
+ """Do two planar point-sets overlap when projected onto the plane ⟂ n?"""
165
+ # two in-plane basis vectors
166
+ ref = (1.0, 0.0, 0.0) if abs(n[0]) < 0.9 else (0.0, 1.0, 0.0)
167
+ dot = sum(ref[c] * n[c] for c in range(3))
168
+ u = tuple(ref[c] - dot * n[c] for c in range(3))
169
+ um = math.sqrt(sum(c * c for c in u)) or 1.0
170
+ u = tuple(c / um for c in u)
171
+ w = (n[1] * u[2] - n[2] * u[1], n[2] * u[0] - n[0] * u[2],
172
+ n[0] * u[1] - n[1] * u[0])
173
+
174
+ def box(pts):
175
+ us = [sum(p[c] * u[c] for c in range(3)) for p in pts]
176
+ ws = [sum(p[c] * w[c] for c in range(3)) for p in pts]
177
+ return min(us), max(us), min(ws), max(ws)
178
+
179
+ au0, au1, aw0, aw1 = box(pa)
180
+ bu0, bu1, bw0, bw1 = box(pb)
181
+ return au0 <= bu1 and bu0 <= au1 and aw0 <= bw1 and bw0 <= aw1
@@ -0,0 +1,60 @@
1
+ """Deriving part interfaces from real domain data (ADR-0008 domain wiring).
2
+
3
+ The board side lives on :meth:`gitcad.ecad.board.Board.to_part`. This module
4
+ covers the mechanical side: build a Document against a kernel and derive the
5
+ envelope from actual geometry. Ports/frames remain *declared* for mech in v1 —
6
+ a solid doesn't announce which faces are mounting bosses; that semantic tag is
7
+ design intent (a follow-up will attach ports to stable entity ids so they
8
+ re-derive positions from geometry too).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from gitcad.document import Document
14
+ from gitcad.part.interface import Frame, Interface, Port
15
+ from gitcad.part.manifest import PartManifest
16
+ from gitcad.seams import Kernel
17
+
18
+
19
+ def model_to_part(
20
+ doc: Document,
21
+ kernel: Kernel,
22
+ *,
23
+ part_id: str,
24
+ name: str,
25
+ version: str = "0.1.0",
26
+ frames: dict[str, Frame] | None = None,
27
+ ports: dict[str, Port] | None = None,
28
+ properties: dict | None = None,
29
+ ) -> PartManifest:
30
+ """Build ``doc`` and derive its ``part.json``: envelope from the built
31
+ geometry's bounding box; declared frames/ports carried through."""
32
+ if not len(doc):
33
+ raise ValueError("document has no features")
34
+ final = doc.build(kernel).final(doc)
35
+ (minx, miny, minz), (maxx, maxy, maxz) = kernel.bbox(final)
36
+ # Round to fixed precision: a derived envelope feeds interface-semver
37
+ # (ADR-0009), so float noise between rebuilds must never register as an
38
+ # interface change. 1e-6 mm is far below manufacturing relevance.
39
+ minx, miny, minz, maxx, maxy, maxz = (round(v, 6) for v in
40
+ (minx, miny, minz, maxx, maxy, maxz))
41
+
42
+ iface_frames = {"origin": Frame()}
43
+ iface_frames.update(frames or {})
44
+ props = {"kernel": kernel.name}
45
+ props.update(properties or {})
46
+ measures = kernel.measure(final)
47
+ if "volume" in measures:
48
+ props["volume_mm3"] = round(measures["volume"], 3)
49
+
50
+ return PartManifest(
51
+ id=part_id, name=name, domain="mech", version=version,
52
+ interface=Interface(
53
+ envelope={"origin": [minx, miny, minz],
54
+ "dx": maxx - minx, "dy": maxy - miny, "dz": maxz - minz},
55
+ frames=iface_frames,
56
+ ports=dict(ports or {}),
57
+ properties=props,
58
+ ),
59
+ body={"model": f"{name}.model"},
60
+ )