lockwire 0.2.0__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.
- lockwire/__init__.py +9 -0
- lockwire/__main__.py +9 -0
- lockwire/advice.py +119 -0
- lockwire/api.py +1216 -0
- lockwire/api_payload.py +985 -0
- lockwire/bench.py +182 -0
- lockwire/blame.py +100 -0
- lockwire/catalog.py +100 -0
- lockwire/catalog_seed.yaml +545 -0
- lockwire/checks.py +3483 -0
- lockwire/cli.py +1799 -0
- lockwire/cli_line.py +73 -0
- lockwire/constraints.py +313 -0
- lockwire/copy.py +302 -0
- lockwire/describe.py +682 -0
- lockwire/edit.py +2098 -0
- lockwire/emit_ts.py +150 -0
- lockwire/export_mhs.py +115 -0
- lockwire/factdiff.py +254 -0
- lockwire/forces.py +334 -0
- lockwire/gitref.py +188 -0
- lockwire/hw.py +388 -0
- lockwire/mcp_server.py +276 -0
- lockwire/mechanism.py +379 -0
- lockwire/mesh_dae.py +555 -0
- lockwire/mesh_simple.py +198 -0
- lockwire/page.py +1485 -0
- lockwire/plan.py +849 -0
- lockwire/plan_payload.py +151 -0
- lockwire/plugins.py +70 -0
- lockwire/report.py +848 -0
- lockwire/report_html.py +707 -0
- lockwire/report_json.py +168 -0
- lockwire/review_draft.py +315 -0
- lockwire/scan.py +732 -0
- lockwire/scene.py +535 -0
- lockwire/scene_import.py +309 -0
- lockwire/scene_urdf.py +750 -0
- lockwire/scene_view.py +376 -0
- lockwire/schema.py +3616 -0
- lockwire/sdf.py +240 -0
- lockwire/serve.py +1256 -0
- lockwire/start_operation.py +92 -0
- lockwire/statics.py +77 -0
- lockwire/units.py +115 -0
- lockwire/urdf.py +348 -0
- lockwire/vcs.py +402 -0
- lockwire/whatif.py +161 -0
- lockwire-0.2.0.dist-info/METADATA +192 -0
- lockwire-0.2.0.dist-info/RECORD +52 -0
- lockwire-0.2.0.dist-info/WHEEL +4 -0
- lockwire-0.2.0.dist-info/entry_points.txt +3 -0
lockwire/mesh_simple.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Stdlib STL and OBJ mesh readers (`lockwire.scene_urdf`'s `<mesh>` door).
|
|
2
|
+
|
|
3
|
+
Both readers flatten every triangle corner into its own vertex (no index
|
|
4
|
+
sharing) — the simplest thing that is still correct, and it sidesteps OBJ's
|
|
5
|
+
independent position/normal index streams without a join table. Not a
|
|
6
|
+
memory-optimal representation for huge meshes; the largest mesh surveyed was
|
|
7
|
+
~326k triangles (~1M floats), well inside what this project ships pages
|
|
8
|
+
with already.
|
|
9
|
+
|
|
10
|
+
``MeshRefusal`` mirrors ``SceneRefusal``'s posture (message names the defect
|
|
11
|
+
and the remedy); ``scene_urdf.py`` re-wraps it as a ``SceneRefusal``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import struct
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
_STL_HEADER = 80
|
|
18
|
+
_STL_RECORD = 50 # 12 floats (normal + 3 verts) + uint16 attribute byte count
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MeshRefusal(ValueError):
|
|
22
|
+
"""The mesh file is malformed or truncated; message names the defect."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _looks_ascii_stl(raw: bytes) -> bool:
|
|
26
|
+
try:
|
|
27
|
+
head = raw[:512].decode("ascii")
|
|
28
|
+
except UnicodeDecodeError:
|
|
29
|
+
return False
|
|
30
|
+
return head.lstrip().lower().startswith("solid")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def read_stl(path: Path) -> tuple[tuple[float, ...], tuple[float, ...], tuple[int, ...]]:
|
|
34
|
+
"""Binary or ASCII STL -> (positions, normals, indices), flat per-facet."""
|
|
35
|
+
raw = path.read_bytes()
|
|
36
|
+
if not raw:
|
|
37
|
+
raise MeshRefusal(f"{path}: STL file is empty. Re-export it.")
|
|
38
|
+
if len(raw) >= _STL_HEADER + 4:
|
|
39
|
+
(count,) = struct.unpack_from("<I", raw, _STL_HEADER)
|
|
40
|
+
expected = _STL_HEADER + 4 + _STL_RECORD * count
|
|
41
|
+
if len(raw) == expected:
|
|
42
|
+
return _read_stl_binary(raw, count, path)
|
|
43
|
+
if not _looks_ascii_stl(raw):
|
|
44
|
+
raise MeshRefusal(
|
|
45
|
+
f"{path}: STL is {len(raw)} bytes but declares {count} triangles "
|
|
46
|
+
f"(needs {expected}); the file is truncated or corrupt. Re-export it."
|
|
47
|
+
)
|
|
48
|
+
return _read_stl_ascii(raw, path)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _read_stl_binary(
|
|
52
|
+
raw: bytes, count: int, path: Path
|
|
53
|
+
) -> tuple[tuple[float, ...], tuple[float, ...], tuple[int, ...]]:
|
|
54
|
+
positions: list[float] = []
|
|
55
|
+
normals: list[float] = []
|
|
56
|
+
offset = _STL_HEADER + 4
|
|
57
|
+
try:
|
|
58
|
+
for _ in range(count):
|
|
59
|
+
nx, ny, nz, v1x, v1y, v1z, v2x, v2y, v2z, v3x, v3y, v3z, _attr = struct.unpack_from(
|
|
60
|
+
"<12fH", raw, offset
|
|
61
|
+
)
|
|
62
|
+
positions.extend((v1x, v1y, v1z, v2x, v2y, v2z, v3x, v3y, v3z))
|
|
63
|
+
normals.extend((nx, ny, nz) * 3)
|
|
64
|
+
offset += _STL_RECORD
|
|
65
|
+
except struct.error as exc:
|
|
66
|
+
raise MeshRefusal(f"{path}: binary STL is truncated mid-triangle. Re-export it.") from exc
|
|
67
|
+
indices = tuple(range(len(positions) // 3))
|
|
68
|
+
return tuple(positions), tuple(normals), indices
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _read_stl_ascii(
|
|
72
|
+
raw: bytes, path: Path
|
|
73
|
+
) -> tuple[tuple[float, ...], tuple[float, ...], tuple[int, ...]]:
|
|
74
|
+
try:
|
|
75
|
+
text = raw.decode("ascii")
|
|
76
|
+
except UnicodeDecodeError as exc:
|
|
77
|
+
raise MeshRefusal(
|
|
78
|
+
f"{path}: not a recognizable STL — not binary (size mismatch) and not "
|
|
79
|
+
f"ASCII (non-ASCII bytes). Re-export as binary or ASCII STL."
|
|
80
|
+
) from exc
|
|
81
|
+
stripped = text.strip()
|
|
82
|
+
if not stripped.lower().startswith("solid"):
|
|
83
|
+
raise MeshRefusal(
|
|
84
|
+
f"{path}: not a recognizable STL — no binary header match and no leading "
|
|
85
|
+
"'solid' line. Re-export as binary or ASCII STL."
|
|
86
|
+
)
|
|
87
|
+
positions: list[float] = []
|
|
88
|
+
normals: list[float] = []
|
|
89
|
+
cur_normal: tuple[float, float, float] = (0.0, 0.0, 0.0)
|
|
90
|
+
verts_in_facet: list[tuple[float, float, float]] = []
|
|
91
|
+
for lineno, line in enumerate(text.splitlines(), start=1):
|
|
92
|
+
words = line.split()
|
|
93
|
+
if not words:
|
|
94
|
+
continue
|
|
95
|
+
if words[0] == "facet" and len(words) >= 5 and words[1] == "normal":
|
|
96
|
+
try:
|
|
97
|
+
cur_normal = tuple(float(x) for x in words[2:5]) # type: ignore[assignment]
|
|
98
|
+
except ValueError as exc:
|
|
99
|
+
raise MeshRefusal(
|
|
100
|
+
f"{path}:{lineno}: 'facet normal' line has a non-numeric value."
|
|
101
|
+
) from exc
|
|
102
|
+
verts_in_facet = []
|
|
103
|
+
elif words[0] == "vertex" and len(words) >= 4:
|
|
104
|
+
try:
|
|
105
|
+
verts_in_facet.append(tuple(float(x) for x in words[1:4])) # type: ignore[arg-type]
|
|
106
|
+
except ValueError as exc:
|
|
107
|
+
raise MeshRefusal(
|
|
108
|
+
f"{path}:{lineno}: 'vertex' line has a non-numeric value."
|
|
109
|
+
) from exc
|
|
110
|
+
elif words[0] == "endfacet":
|
|
111
|
+
if len(verts_in_facet) != 3:
|
|
112
|
+
raise MeshRefusal(
|
|
113
|
+
f"{path}: ASCII STL facet has {len(verts_in_facet)} vertices, expected 3."
|
|
114
|
+
)
|
|
115
|
+
for v in verts_in_facet:
|
|
116
|
+
positions.extend(v)
|
|
117
|
+
normals.extend(cur_normal)
|
|
118
|
+
verts_in_facet = []
|
|
119
|
+
if not positions:
|
|
120
|
+
raise MeshRefusal(f"{path}: ASCII STL has no facets.")
|
|
121
|
+
indices = tuple(range(len(positions) // 3))
|
|
122
|
+
return tuple(positions), tuple(normals), indices
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def read_obj(path: Path) -> tuple[tuple[float, ...], tuple[float, ...] | None, tuple[int, ...]]:
|
|
126
|
+
"""OBJ (v, vn, f) -> (positions, normals or None, indices), fan-triangulated,
|
|
127
|
+
flat per-corner (no shared vertex/normal index join)."""
|
|
128
|
+
verts: list[tuple[float, float, float]] = []
|
|
129
|
+
vnormals: list[tuple[float, float, float]] = []
|
|
130
|
+
positions: list[float] = []
|
|
131
|
+
normals: list[float] = []
|
|
132
|
+
have_normals = False
|
|
133
|
+
face_count = 0
|
|
134
|
+
|
|
135
|
+
for lineno, line in enumerate(path.read_text(errors="replace").splitlines(), start=1):
|
|
136
|
+
words = line.split()
|
|
137
|
+
if not words:
|
|
138
|
+
continue
|
|
139
|
+
tag = words[0]
|
|
140
|
+
if tag == "v" and len(words) >= 4:
|
|
141
|
+
try:
|
|
142
|
+
verts.append((float(words[1]), float(words[2]), float(words[3])))
|
|
143
|
+
except ValueError as exc:
|
|
144
|
+
raise MeshRefusal(f"{path}:{lineno}: 'v' line has a non-numeric value.") from exc
|
|
145
|
+
elif tag == "vn" and len(words) >= 4:
|
|
146
|
+
try:
|
|
147
|
+
vnormals.append((float(words[1]), float(words[2]), float(words[3])))
|
|
148
|
+
except ValueError as exc:
|
|
149
|
+
raise MeshRefusal(f"{path}:{lineno}: 'vn' line has a non-numeric value.") from exc
|
|
150
|
+
elif tag == "f" and len(words) >= 4:
|
|
151
|
+
corners = words[1:]
|
|
152
|
+
resolved: list[tuple[int, int | None]] = []
|
|
153
|
+
for corner in corners:
|
|
154
|
+
parts = corner.split("/")
|
|
155
|
+
try:
|
|
156
|
+
v_idx = int(parts[0])
|
|
157
|
+
except ValueError as exc:
|
|
158
|
+
raise MeshRefusal(
|
|
159
|
+
f"{path}:{lineno}: face corner {corner!r} has no vertex index."
|
|
160
|
+
) from exc
|
|
161
|
+
v_idx = v_idx - 1 if v_idx > 0 else len(verts) + v_idx
|
|
162
|
+
n_idx = None
|
|
163
|
+
if len(parts) == 3 and parts[2]:
|
|
164
|
+
try:
|
|
165
|
+
n_idx = int(parts[2])
|
|
166
|
+
except ValueError as exc:
|
|
167
|
+
raise MeshRefusal(
|
|
168
|
+
f"{path}:{lineno}: face corner {corner!r} has a "
|
|
169
|
+
"non-numeric normal index."
|
|
170
|
+
) from exc
|
|
171
|
+
n_idx = n_idx - 1 if n_idx > 0 else len(vnormals) + n_idx
|
|
172
|
+
if v_idx < 0 or v_idx >= len(verts):
|
|
173
|
+
raise MeshRefusal(
|
|
174
|
+
f"{path}:{lineno}: face vertex index {parts[0]} is out of range "
|
|
175
|
+
f"({len(verts)} vertices read so far)."
|
|
176
|
+
)
|
|
177
|
+
resolved.append((v_idx, n_idx))
|
|
178
|
+
# Fan-triangulate polygons with >3 corners.
|
|
179
|
+
for i in range(1, len(resolved) - 1):
|
|
180
|
+
for v_idx, n_idx in (resolved[0], resolved[i], resolved[i + 1]):
|
|
181
|
+
positions.extend(verts[v_idx])
|
|
182
|
+
if n_idx is not None:
|
|
183
|
+
if n_idx < 0 or n_idx >= len(vnormals):
|
|
184
|
+
raise MeshRefusal(
|
|
185
|
+
f"{path}:{lineno}: face normal index out of range "
|
|
186
|
+
f"({len(vnormals)} normals read so far)."
|
|
187
|
+
)
|
|
188
|
+
normals.extend(vnormals[n_idx])
|
|
189
|
+
have_normals = True
|
|
190
|
+
else:
|
|
191
|
+
normals.extend((0.0, 0.0, 0.0))
|
|
192
|
+
face_count += 1
|
|
193
|
+
|
|
194
|
+
if not positions:
|
|
195
|
+
raise MeshRefusal(f"{path}: OBJ has no triangulable faces.")
|
|
196
|
+
indices = tuple(range(len(positions) // 3))
|
|
197
|
+
normals_out = tuple(normals) if have_normals else None
|
|
198
|
+
return tuple(positions), normals_out, indices
|