gitcad-mech 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/analysis.py +181 -0
- gitcad/derive.py +60 -0
- gitcad/document.py +662 -0
- gitcad/drawing/__init__.py +14 -0
- gitcad/drawing/assembly.py +103 -0
- gitcad/drawing/dxf.py +63 -0
- gitcad/drawing/hlr.py +39 -0
- gitcad/drawing/pdf.py +108 -0
- gitcad/drawing/sections.py +137 -0
- gitcad/drawing/sheet.py +286 -0
- gitcad/drawing/svg.py +144 -0
- gitcad/importers/fcstd.py +163 -0
- gitcad/importers/fcstd_tree.py +183 -0
- gitcad/importers/recognize.py +111 -0
- gitcad/importers/step.py +41 -0
- gitcad/kernel/__init__.py +32 -0
- gitcad/kernel/auto.py +96 -0
- gitcad/kernel/null.py +254 -0
- gitcad/kernel/occt.py +719 -0
- gitcad/kernel/ref.py +781 -0
- gitcad/kernel/rustref.py +232 -0
- gitcad/select.py +51 -0
- gitcad/sheetmetal.py +397 -0
- gitcad/sketch.py +164 -0
- gitcad/sketch_solver.py +230 -0
- gitcad/weldment.py +126 -0
- gitcad_mech-0.7.7.dist-info/METADATA +50 -0
- gitcad_mech-0.7.7.dist-info/RECORD +29 -0
- gitcad_mech-0.7.7.dist-info/WHEEL +4 -0
gitcad/analysis.py
ADDED
|
@@ -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
|
gitcad/derive.py
ADDED
|
@@ -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
|
+
)
|