yapCAD 0.5.0__py2.py3-none-any.whl → 0.5.1__py2.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.
- yapcad/boolean/__init__.py +21 -0
- yapcad/boolean/native.py +1012 -0
- yapcad/boolean/trimesh_engine.py +155 -0
- yapcad/combine.py +52 -14
- yapcad/drawable.py +404 -26
- yapcad/geom.py +116 -0
- yapcad/geom3d.py +237 -7
- yapcad/geom3d_util.py +486 -30
- yapcad/geom_util.py +160 -61
- yapcad/io/__init__.py +2 -1
- yapcad/io/step.py +323 -0
- yapcad/spline.py +232 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/METADATA +60 -14
- yapcad-0.5.1.dist-info/RECORD +32 -0
- yapcad-0.5.0.dist-info/RECORD +0 -27
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/WHEEL +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/licenses/AUTHORS.rst +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/licenses/LICENSE.txt +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/top_level.txt +0 -0
yapcad/geom_util.py
CHANGED
@@ -23,6 +23,7 @@
|
|
23
23
|
# SOFTWARE.
|
24
24
|
|
25
25
|
from yapcad.geom import *
|
26
|
+
from yapcad.spline import sample_catmullrom, sample_nurbs
|
26
27
|
import math
|
27
28
|
import random
|
28
29
|
|
@@ -212,86 +213,184 @@ def _convert_geom_sequence(seq, minang, minlen, snap_tol):
|
|
212
213
|
return cleaned, nested_holes
|
213
214
|
|
214
215
|
|
215
|
-
def
|
216
|
-
|
217
|
-
if
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
216
|
+
def _points_to_segments(points, *, closed=False):
|
217
|
+
segments = []
|
218
|
+
if not points:
|
219
|
+
return segments
|
220
|
+
prev = point(points[0])
|
221
|
+
for pt in points[1:]:
|
222
|
+
curr = point(pt)
|
223
|
+
if dist(prev, curr) > epsilon:
|
224
|
+
segments.append(line(prev, curr))
|
225
|
+
prev = curr
|
226
|
+
if closed:
|
227
|
+
start = point(points[0])
|
228
|
+
if dist(prev, start) > epsilon:
|
229
|
+
segments.append(line(prev, start))
|
230
|
+
return segments
|
231
|
+
|
232
|
+
|
233
|
+
def _spline_segments_from_curve(curve, minlen):
|
234
|
+
if iscatmullrom(curve):
|
235
|
+
meta = curve[2]
|
236
|
+
default_segments = max(8, int(round(1.0 / max(minlen, epsilon))))
|
237
|
+
segs = int(meta.get('segments_per_span', default_segments))
|
238
|
+
samples = sample_catmullrom(curve, segments_per_span=max(1, segs))
|
239
|
+
closed = bool(meta.get('closed', False))
|
240
|
+
return _points_to_segments(samples, closed=closed)
|
241
|
+
if isnurbs(curve):
|
242
|
+
meta = curve[2]
|
243
|
+
default = max(32, len(curve[1]) * 8)
|
244
|
+
count = int(meta.get('samples', default))
|
245
|
+
samples = sample_nurbs(curve, samples=max(4, count))
|
246
|
+
return _points_to_segments(samples, closed=False)
|
247
|
+
return []
|
248
|
+
|
249
|
+
def _collect_candidate_polygons(gl, minang, minlen, snap_tol):
|
250
|
+
"""Return raw polygon loops discovered within ``gl``."""
|
224
251
|
|
252
|
+
loops = []
|
225
253
|
segments = []
|
226
254
|
|
227
255
|
for element in gl:
|
228
256
|
if ispoint(element):
|
229
257
|
continue
|
230
|
-
|
258
|
+
if isline(element) or isarc(element):
|
231
259
|
segments.append(element)
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
if area >= 0:
|
244
|
-
positive_candidates.append(outer)
|
245
|
-
else:
|
246
|
-
negative_candidates.append(outer)
|
247
|
-
for h in holes:
|
248
|
-
area = _poly_signed_area(h)
|
249
|
-
if area >= 0:
|
250
|
-
positive_candidates.append(h)
|
251
|
-
else:
|
252
|
-
negative_candidates.append(h)
|
253
|
-
else:
|
254
|
-
raise ValueError(f'bad object in list passed to geomlist2poly: {element}')
|
260
|
+
continue
|
261
|
+
if iscatmullrom(element) or isnurbs(element):
|
262
|
+
segments.extend(_spline_segments_from_curve(element, minlen))
|
263
|
+
continue
|
264
|
+
if ispoly(element):
|
265
|
+
loops.append(_finalize_poly_points(element, snap_tol))
|
266
|
+
continue
|
267
|
+
if isgeomlist(element):
|
268
|
+
loops.extend(_collect_candidate_polygons(element, minang, minlen, snap_tol))
|
269
|
+
continue
|
270
|
+
raise ValueError(f'bad object in list passed to geomlist2poly: {element}')
|
255
271
|
|
256
272
|
if segments:
|
257
|
-
|
258
|
-
for loop in
|
273
|
+
loops_seq, leftovers = _stitch_geomlist(segments, snap_tol)
|
274
|
+
for loop in loops_seq:
|
259
275
|
poly, holes = _convert_geom_sequence(loop, minang, minlen, snap_tol)
|
260
276
|
if poly:
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
277
|
+
loops.append(poly)
|
278
|
+
loops.extend(holes)
|
279
|
+
for leftover in leftovers:
|
280
|
+
if ispoly(leftover):
|
281
|
+
loops.append(_finalize_poly_points(leftover, snap_tol))
|
282
|
+
elif isgeomlist(leftover):
|
283
|
+
loops.extend(_collect_candidate_polygons(leftover, minang, minlen, snap_tol))
|
284
|
+
elif ispoint(leftover):
|
285
|
+
continue
|
286
|
+
elif isline(leftover) or isarc(leftover):
|
287
|
+
poly, holes = _convert_geom_sequence([leftover], minang, minlen, snap_tol)
|
288
|
+
if poly:
|
289
|
+
loops.append(poly)
|
290
|
+
loops.extend(holes)
|
291
|
+
elif iscatmullrom(leftover) or isnurbs(leftover):
|
292
|
+
segments = _spline_segments_from_curve(leftover, minlen)
|
293
|
+
if segments:
|
294
|
+
poly, holes = _convert_geom_sequence(segments, minang, minlen, snap_tol)
|
295
|
+
if poly:
|
296
|
+
loops.append(poly)
|
297
|
+
loops.extend(holes)
|
298
|
+
else:
|
299
|
+
raise ValueError(f'bad object in list passed to geomlist2poly: {leftover}')
|
272
300
|
|
273
|
-
|
274
|
-
return [], []
|
301
|
+
return loops
|
275
302
|
|
276
|
-
all_candidates = positive_candidates + negative_candidates
|
277
|
-
outer = max(all_candidates, key=lambda pts: abs(_poly_signed_area(pts)))
|
278
|
-
outer_area = _poly_signed_area(outer)
|
279
|
-
orientation = 1 if outer_area >= 0 else -1
|
280
303
|
|
281
|
-
|
304
|
+
def _orient_loop(points, want_positive=True):
|
305
|
+
if not points:
|
306
|
+
return []
|
307
|
+
core = points[:-1] if len(points) > 1 else points
|
308
|
+
if len(core) < 3:
|
309
|
+
return [point(p) for p in points]
|
310
|
+
pts = [point(p) for p in core]
|
311
|
+
area = _poly_signed_area(points)
|
312
|
+
should_reverse = (want_positive and area < 0) or (not want_positive and area > 0)
|
313
|
+
if should_reverse:
|
314
|
+
pts.reverse()
|
315
|
+
pts.append(point(pts[0]))
|
316
|
+
return pts
|
317
|
+
|
318
|
+
|
319
|
+
def _assemble_components(loop_points):
|
320
|
+
if not loop_points:
|
321
|
+
return []
|
282
322
|
|
283
|
-
|
284
|
-
|
323
|
+
nodes = []
|
324
|
+
for pts in loop_points:
|
325
|
+
if len(pts) < 3:
|
285
326
|
continue
|
286
327
|
area = _poly_signed_area(pts)
|
287
|
-
if
|
288
|
-
holes.append(pts)
|
328
|
+
if abs(area) <= epsilon:
|
289
329
|
continue
|
290
|
-
|
291
|
-
|
292
|
-
|
330
|
+
nodes.append({
|
331
|
+
'points': pts,
|
332
|
+
'area': area,
|
333
|
+
'abs_area': abs(area),
|
334
|
+
'centroid': _polygon_centroid_xy(pts),
|
335
|
+
'parent': None,
|
336
|
+
'children': [],
|
337
|
+
})
|
338
|
+
|
339
|
+
if not nodes:
|
340
|
+
return []
|
293
341
|
|
294
|
-
|
342
|
+
for idx, node in enumerate(nodes):
|
343
|
+
parent_idx = None
|
344
|
+
for jdx, candidate in enumerate(nodes):
|
345
|
+
if idx == jdx:
|
346
|
+
continue
|
347
|
+
if candidate['abs_area'] <= node['abs_area'] + epsilon:
|
348
|
+
continue
|
349
|
+
if _point_in_polygon_xy(candidate['points'], node['centroid']):
|
350
|
+
if parent_idx is None or candidate['abs_area'] < nodes[parent_idx]['abs_area']:
|
351
|
+
parent_idx = jdx
|
352
|
+
node['parent'] = parent_idx
|
353
|
+
if parent_idx is not None:
|
354
|
+
nodes[parent_idx]['children'].append(idx)
|
355
|
+
|
356
|
+
components = []
|
357
|
+
|
358
|
+
def visit(node_idx):
|
359
|
+
node = nodes[node_idx]
|
360
|
+
outer = _orient_loop(node['points'], want_positive=True)
|
361
|
+
holes = []
|
362
|
+
extras = []
|
363
|
+
for child_idx in node['children']:
|
364
|
+
child = nodes[child_idx]
|
365
|
+
holes.append(_orient_loop(child['points'], want_positive=False))
|
366
|
+
for grand_idx in child['children']:
|
367
|
+
extras.extend(visit(grand_idx))
|
368
|
+
extras.insert(0, (outer, holes))
|
369
|
+
return extras
|
370
|
+
|
371
|
+
for idx, node in enumerate(nodes):
|
372
|
+
if node['parent'] is None:
|
373
|
+
components.extend(visit(idx))
|
374
|
+
|
375
|
+
return components
|
376
|
+
|
377
|
+
|
378
|
+
def geomlist2poly_components(gl, minang=5.0, minlen=0.25, checkcont=False):
|
379
|
+
"""Return a list of ``(outer, holes)`` tuples extracted from ``gl``."""
|
380
|
+
|
381
|
+
if checkcont and not iscontinuousgeomlist(gl):
|
382
|
+
raise ValueError('non-continuous geometry list passed to geomlist2poly')
|
383
|
+
|
384
|
+
snap_tol = max(epsilon * 10, minlen * 0.1)
|
385
|
+
loops = _collect_candidate_polygons(gl, minang, minlen, snap_tol)
|
386
|
+
return _assemble_components(loops)
|
387
|
+
|
388
|
+
|
389
|
+
def geomlist2poly_with_holes(gl, minang=5.0, minlen=0.25, checkcont=False):
|
390
|
+
components = geomlist2poly_components(gl, minang, minlen, checkcont)
|
391
|
+
if not components:
|
392
|
+
return [], []
|
393
|
+
return components[0]
|
295
394
|
|
296
395
|
|
297
396
|
def _polygon_centroid_xy(pts):
|
yapcad/io/__init__.py
CHANGED
yapcad/io/step.py
ADDED
@@ -0,0 +1,323 @@
|
|
1
|
+
"""STEP export utilities for yapCAD surfaces and solids."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from collections import defaultdict
|
6
|
+
from dataclasses import dataclass
|
7
|
+
from typing import Dict, List, Sequence, TextIO, Tuple
|
8
|
+
|
9
|
+
from yapcad.geometry_utils import triangles_from_mesh
|
10
|
+
from yapcad.mesh import mesh_view
|
11
|
+
|
12
|
+
Vec3 = Tuple[float, float, float]
|
13
|
+
|
14
|
+
|
15
|
+
@dataclass(frozen=True)
|
16
|
+
class _Vertex:
|
17
|
+
coords: Vec3
|
18
|
+
point_id: int
|
19
|
+
vertex_id: int
|
20
|
+
|
21
|
+
|
22
|
+
@dataclass(frozen=True)
|
23
|
+
class _EdgeData:
|
24
|
+
edge_curve: int
|
25
|
+
oriented_forward: int
|
26
|
+
oriented_reverse: int
|
27
|
+
start_vertex: int
|
28
|
+
end_vertex: int
|
29
|
+
|
30
|
+
|
31
|
+
@dataclass(frozen=True)
|
32
|
+
class _TriRecord:
|
33
|
+
v0: _Vertex
|
34
|
+
v1: _Vertex
|
35
|
+
v2: _Vertex
|
36
|
+
normal: Vec3
|
37
|
+
|
38
|
+
|
39
|
+
class _EntityWriter:
|
40
|
+
"""Helper to append STEP entities with sequential ids."""
|
41
|
+
|
42
|
+
def __init__(self) -> None:
|
43
|
+
self.entities: List[str] = []
|
44
|
+
|
45
|
+
def add(self, record: str) -> int:
|
46
|
+
idx = len(self.entities) + 1
|
47
|
+
self.entities.append(f"#{idx} = {record};")
|
48
|
+
return idx
|
49
|
+
|
50
|
+
def write(self, stream: TextIO) -> None:
|
51
|
+
for entity in self.entities:
|
52
|
+
stream.write(entity + "\n")
|
53
|
+
|
54
|
+
|
55
|
+
def write_step(obj: Sequence,
|
56
|
+
path_or_file,
|
57
|
+
*,
|
58
|
+
name: str = 'yapCAD',
|
59
|
+
schema: str = 'AUTOMOTIVE_DESIGN_CC2') -> None:
|
60
|
+
"""Export ``obj`` (surface or solid) to a STEP file using a faceted BREP."""
|
61
|
+
|
62
|
+
triangles = list(triangles_from_mesh(mesh_view(obj)))
|
63
|
+
if not triangles:
|
64
|
+
raise ValueError('object produced no triangles for STEP export')
|
65
|
+
writer = _EntityWriter()
|
66
|
+
|
67
|
+
vertex_map: Dict[Vec3, _Vertex] = {}
|
68
|
+
|
69
|
+
for tri in triangles:
|
70
|
+
for coords in (tri.v0, tri.v1, tri.v2):
|
71
|
+
if coords not in vertex_map:
|
72
|
+
pt = writer.add(
|
73
|
+
f"CARTESIAN_POINT('', ({coords[0]:.6f}, {coords[1]:.6f}, {coords[2]:.6f}))"
|
74
|
+
)
|
75
|
+
vert = writer.add(f"VERTEX_POINT('', #{pt})")
|
76
|
+
vertex_map[coords] = _Vertex(coords, pt, vert)
|
77
|
+
|
78
|
+
def _line_for_edge(start: _Vertex, end: _Vertex) -> Tuple[int, int, int]:
|
79
|
+
direction_vec = _normalize((end.coords[0] - start.coords[0],
|
80
|
+
end.coords[1] - start.coords[1],
|
81
|
+
end.coords[2] - start.coords[2]))
|
82
|
+
direction_id = writer.add(
|
83
|
+
f"DIRECTION('', ({direction_vec[0]:.6f}, {direction_vec[1]:.6f}, {direction_vec[2]:.6f}))"
|
84
|
+
)
|
85
|
+
length = _distance(start.coords, end.coords)
|
86
|
+
vector_id = writer.add(f"VECTOR('', #{direction_id}, {length:.6f})")
|
87
|
+
line_id = writer.add(f"LINE('', #{start.point_id}, #{vector_id})")
|
88
|
+
return direction_id, vector_id, line_id
|
89
|
+
|
90
|
+
tri_records: List[_TriRecord] = []
|
91
|
+
edge_to_faces: Dict[Tuple[int, int], List[int]] = defaultdict(list)
|
92
|
+
|
93
|
+
for idx, tri in enumerate(triangles):
|
94
|
+
v0 = vertex_map[tri.v0]
|
95
|
+
v1 = vertex_map[tri.v1]
|
96
|
+
v2 = vertex_map[tri.v2]
|
97
|
+
tri_records.append(_TriRecord(v0=v0, v1=v1, v2=v2, normal=_normalize(tri.normal)))
|
98
|
+
|
99
|
+
for start, end in ((v0, v1), (v1, v2), (v2, v0)):
|
100
|
+
key = _edge_key(start.vertex_id, end.vertex_id)
|
101
|
+
edge_to_faces[key].append(idx)
|
102
|
+
|
103
|
+
neighbors: Dict[int, set[int]] = {i: set() for i in range(len(tri_records))}
|
104
|
+
for faces in edge_to_faces.values():
|
105
|
+
for i in range(len(faces)):
|
106
|
+
for j in range(i + 1, len(faces)):
|
107
|
+
a, b = faces[i], faces[j]
|
108
|
+
neighbors[a].add(b)
|
109
|
+
neighbors[b].add(a)
|
110
|
+
|
111
|
+
tri_component = [-1] * len(tri_records)
|
112
|
+
components: List[List[int]] = []
|
113
|
+
|
114
|
+
for idx in range(len(tri_records)):
|
115
|
+
if tri_component[idx] != -1:
|
116
|
+
continue
|
117
|
+
comp_index = len(components)
|
118
|
+
stack = [idx]
|
119
|
+
tri_component[idx] = comp_index
|
120
|
+
comp_faces: List[int] = []
|
121
|
+
|
122
|
+
while stack:
|
123
|
+
current = stack.pop()
|
124
|
+
comp_faces.append(current)
|
125
|
+
for nb in neighbors[current]:
|
126
|
+
if tri_component[nb] == -1:
|
127
|
+
tri_component[nb] = comp_index
|
128
|
+
stack.append(nb)
|
129
|
+
|
130
|
+
components.append(comp_faces)
|
131
|
+
|
132
|
+
component_closed = [True] * len(components)
|
133
|
+
for faces in edge_to_faces.values():
|
134
|
+
comp_counts: Dict[int, int] = {}
|
135
|
+
for face_idx in faces:
|
136
|
+
comp_idx = tri_component[face_idx]
|
137
|
+
comp_counts[comp_idx] = comp_counts.get(comp_idx, 0) + 1
|
138
|
+
for comp_idx, count in comp_counts.items():
|
139
|
+
if count != 2:
|
140
|
+
component_closed[comp_idx] = False
|
141
|
+
|
142
|
+
edge_store: Dict[Tuple[int, int], _EdgeData] = {}
|
143
|
+
face_ids_by_component: List[List[int]] = [[] for _ in components]
|
144
|
+
|
145
|
+
for tri_idx, tri in enumerate(tri_records):
|
146
|
+
component_index = tri_component[tri_idx]
|
147
|
+
|
148
|
+
loop_edges: List[int] = []
|
149
|
+
for start, end in ((tri.v0, tri.v1), (tri.v1, tri.v2), (tri.v2, tri.v0)):
|
150
|
+
key = _edge_key(start.vertex_id, end.vertex_id)
|
151
|
+
data = edge_store.get(key)
|
152
|
+
if data is None:
|
153
|
+
_, _, line_id = _line_for_edge(start, end)
|
154
|
+
edge_curve = writer.add(
|
155
|
+
f"EDGE_CURVE('', #{start.vertex_id}, #{end.vertex_id}, #{line_id}, .T.)"
|
156
|
+
)
|
157
|
+
oriented_forward = writer.add(
|
158
|
+
f"ORIENTED_EDGE('', *, *, #{edge_curve}, .T.)"
|
159
|
+
)
|
160
|
+
oriented_reverse = writer.add(
|
161
|
+
f"ORIENTED_EDGE('', *, *, #{edge_curve}, .F.)"
|
162
|
+
)
|
163
|
+
data = _EdgeData(edge_curve, oriented_forward, oriented_reverse,
|
164
|
+
start.vertex_id, end.vertex_id)
|
165
|
+
edge_store[key] = data
|
166
|
+
|
167
|
+
if data.start_vertex == start.vertex_id and data.end_vertex == end.vertex_id:
|
168
|
+
loop_edges.append(data.oriented_forward)
|
169
|
+
else:
|
170
|
+
loop_edges.append(data.oriented_reverse)
|
171
|
+
|
172
|
+
edge_loop = writer.add(
|
173
|
+
"EDGE_LOOP('', (" + ", ".join(f"#{eid}" for eid in loop_edges) + "))"
|
174
|
+
)
|
175
|
+
face_outer = writer.add(f"FACE_OUTER_BOUND('', #{edge_loop}, .T.)")
|
176
|
+
|
177
|
+
ref_vec = _perpendicular(tri.normal)
|
178
|
+
normal_id = writer.add(
|
179
|
+
f"DIRECTION('', ({tri.normal[0]:.6f}, {tri.normal[1]:.6f}, {tri.normal[2]:.6f}))"
|
180
|
+
)
|
181
|
+
ref_dir = writer.add(
|
182
|
+
f"DIRECTION('', ({ref_vec[0]:.6f}, {ref_vec[1]:.6f}, {ref_vec[2]:.6f}))"
|
183
|
+
)
|
184
|
+
axis = writer.add(f"AXIS2_PLACEMENT_3D('', #{tri.v0.point_id}, #{normal_id}, #{ref_dir})")
|
185
|
+
plane = writer.add(f"PLANE('', #{axis})")
|
186
|
+
face = writer.add(f"ADVANCED_FACE('', (#{face_outer}), #{plane}, .T.)")
|
187
|
+
face_ids_by_component[component_index].append(face)
|
188
|
+
|
189
|
+
representation_items: List[int] = []
|
190
|
+
for idx, face_ids in enumerate(face_ids_by_component):
|
191
|
+
if not face_ids:
|
192
|
+
continue
|
193
|
+
shell_name = name if len(face_ids_by_component) == 1 else f"{name}_{idx + 1}"
|
194
|
+
if component_closed[idx]:
|
195
|
+
shell_id = writer.add(_shell_record('CLOSED_SHELL', face_ids))
|
196
|
+
brep_id = writer.add(f"MANIFOLD_SOLID_BREP('{shell_name}', #{shell_id})")
|
197
|
+
representation_items.append(brep_id)
|
198
|
+
else:
|
199
|
+
shell_id = writer.add(_shell_record('OPEN_SHELL', face_ids))
|
200
|
+
model_id = writer.add(
|
201
|
+
f"SHELL_BASED_SURFACE_MODEL('{shell_name}', (#{shell_id}))"
|
202
|
+
)
|
203
|
+
representation_items.append(model_id)
|
204
|
+
|
205
|
+
if not representation_items:
|
206
|
+
raise ValueError('STEP export generated no representation items')
|
207
|
+
|
208
|
+
# Representation contexts and product definitions
|
209
|
+
app_context = writer.add("APPLICATION_CONTEXT('mechanical design')")
|
210
|
+
prod_context = writer.add(
|
211
|
+
f"PRODUCT_CONTEXT('part definition', #{app_context}, 'mechanical')"
|
212
|
+
)
|
213
|
+
product = writer.add(
|
214
|
+
f"PRODUCT('{name}', '{name}', '', (#{prod_context}))"
|
215
|
+
)
|
216
|
+
formation = writer.add(f"PRODUCT_DEFINITION_FORMATION('', '', #{product})")
|
217
|
+
pd_context = writer.add(
|
218
|
+
f"PRODUCT_DEFINITION_CONTEXT('design', #{app_context}, 'mechanical')"
|
219
|
+
)
|
220
|
+
product_def = writer.add(
|
221
|
+
f"PRODUCT_DEFINITION('', '', #{formation}, #{pd_context})"
|
222
|
+
)
|
223
|
+
shape = writer.add(f"PRODUCT_DEFINITION_SHAPE('', '', #{product_def})")
|
224
|
+
|
225
|
+
length_unit = writer.add(
|
226
|
+
"( NAMED_UNIT(*) LENGTH_UNIT() SI_UNIT(.MILLI., .METRE.) )"
|
227
|
+
)
|
228
|
+
plane_angle_unit = writer.add(
|
229
|
+
"( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($, .RADIAN.) )"
|
230
|
+
)
|
231
|
+
solid_angle_unit = writer.add(
|
232
|
+
"( NAMED_UNIT(*) SOLID_ANGLE_UNIT() SI_UNIT($, .STERADIAN.) )"
|
233
|
+
)
|
234
|
+
measure_with_unit = writer.add(
|
235
|
+
f"MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-06), #{length_unit})"
|
236
|
+
)
|
237
|
+
uncertainty = writer.add(
|
238
|
+
f"UNCERTAINTY_MEASURE_WITH_UNIT(#{measure_with_unit}, 'distance accuracy value')"
|
239
|
+
)
|
240
|
+
geom_context = writer.add(
|
241
|
+
"( REPRESENTATION_CONTEXT('', '')"
|
242
|
+
" AND GEOMETRIC_REPRESENTATION_CONTEXT(3)"
|
243
|
+
f" AND GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{uncertainty}))"
|
244
|
+
f" AND GLOBAL_UNIT_ASSIGNED_CONTEXT((#{length_unit}, #{plane_angle_unit}, #{solid_angle_unit})) )"
|
245
|
+
)
|
246
|
+
|
247
|
+
items_block = _format_id_list(representation_items)
|
248
|
+
brep_shape = writer.add(
|
249
|
+
"ADVANCED_BREP_SHAPE_REPRESENTATION('', (\n"
|
250
|
+
+ items_block
|
251
|
+
+ f"\n), #{geom_context})"
|
252
|
+
)
|
253
|
+
writer.add(f"SHAPE_DEFINITION_REPRESENTATION(#{shape}, #{brep_shape})")
|
254
|
+
|
255
|
+
close_stream = False
|
256
|
+
if hasattr(path_or_file, 'write'):
|
257
|
+
stream = path_or_file
|
258
|
+
else:
|
259
|
+
stream = open(path_or_file, 'w', encoding='utf-8')
|
260
|
+
close_stream = True
|
261
|
+
|
262
|
+
try:
|
263
|
+
_write_header(stream, name, schema)
|
264
|
+
stream.write("DATA;\n")
|
265
|
+
writer.write(stream)
|
266
|
+
stream.write("ENDSEC;\nEND-ISO-10303-21;\n")
|
267
|
+
finally:
|
268
|
+
if close_stream:
|
269
|
+
stream.close()
|
270
|
+
|
271
|
+
|
272
|
+
def _write_header(stream: TextIO, name: str, schema: str) -> None:
|
273
|
+
stream.write("ISO-10303-21;\n")
|
274
|
+
stream.write("HEADER;\n")
|
275
|
+
stream.write("FILE_DESCRIPTION(('yapCAD export'),'2;1');\n")
|
276
|
+
stream.write(
|
277
|
+
"FILE_NAME('"
|
278
|
+
+ name
|
279
|
+
+ "','2024-01-01T00:00:00',('yapCAD'),('yapCAD organization'), 'yapCAD', 'yapCAD', '');\n"
|
280
|
+
)
|
281
|
+
stream.write("FILE_SCHEMA(('" + schema + "'));\n")
|
282
|
+
stream.write("ENDSEC;\n")
|
283
|
+
|
284
|
+
|
285
|
+
def _normalize(vec: Vec3) -> Vec3:
|
286
|
+
length = (vec[0] ** 2 + vec[1] ** 2 + vec[2] ** 2) ** 0.5
|
287
|
+
if length == 0.0:
|
288
|
+
return (0.0, 0.0, 1.0)
|
289
|
+
return (vec[0] / length, vec[1] / length, vec[2] / length)
|
290
|
+
|
291
|
+
|
292
|
+
def _distance(a: Vec3, b: Vec3) -> float:
|
293
|
+
return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2) ** 0.5
|
294
|
+
|
295
|
+
|
296
|
+
def _perpendicular(normal: Vec3) -> Vec3:
|
297
|
+
x, y, z = normal
|
298
|
+
if abs(x) < abs(z):
|
299
|
+
return _normalize((-z, 0.0, x))
|
300
|
+
return _normalize((0.0, z, -y))
|
301
|
+
|
302
|
+
|
303
|
+
def _edge_key(a: int, b: int) -> Tuple[int, int]:
|
304
|
+
return (a, b) if a <= b else (b, a)
|
305
|
+
|
306
|
+
|
307
|
+
def _shell_record(kind: str, face_ids: Sequence[int]) -> str:
|
308
|
+
block = _format_id_list(face_ids)
|
309
|
+
return f"{kind}('', (\n{block}\n))"
|
310
|
+
|
311
|
+
|
312
|
+
def _format_id_list(ids: Sequence[int], *, indent: str = ' ', per_line: int = 8) -> str:
|
313
|
+
if not ids:
|
314
|
+
return indent
|
315
|
+
lines: List[str] = []
|
316
|
+
total = len(ids)
|
317
|
+
for index, entity in enumerate(ids):
|
318
|
+
suffix = ',' if index + 1 < total else ''
|
319
|
+
lines.append(f"{indent}#{entity}{suffix}")
|
320
|
+
return "\n".join(lines)
|
321
|
+
|
322
|
+
|
323
|
+
__all__ = ['write_step']
|