dsh-cad 0.13.2 → 0.14.1

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.
@@ -88,215 +88,215 @@ export function freecadAvailable() {
88
88
  return findFreeCad() !== null;
89
89
  }
90
90
  /** The Python bridge executed inside FreeCAD's console binary. */
91
- const BRIDGE = String.raw `
92
- import json, math, os, struct, traceback
93
-
94
- import FreeCAD
95
- import Part
96
-
97
- BASE = os.path.dirname(os.path.abspath(__file__))
98
- OPS_PATH = os.path.join(BASE, 'ops.json')
99
- RESULT_PATH = os.path.join(BASE, 'result.json')
100
-
101
-
102
- def write(result):
103
- with open(RESULT_PATH, 'w') as handle:
104
- json.dump(result, handle)
105
-
106
-
107
- def vec(value, default=(0.0, 0.0, 0.0)):
108
- if not value:
109
- return FreeCAD.Vector(*default)
110
- return FreeCAD.Vector(float(value[0]), float(value[1]), float(value[2]))
111
-
112
-
113
- def orient(shape, at, axis):
114
- if at:
115
- shape.translate(vec(at))
116
- if axis:
117
- target = vec(axis, (0.0, 0.0, 1.0))
118
- if target.Length < 1e-9:
119
- return shape
120
- target.normalize()
121
- if abs(target.z - 1.0) > 1e-9:
122
- rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), target)
123
- shape.transformShape(rotation.toMatrix(), True)
124
- return shape
125
-
126
-
127
- def make_prim(prim, params):
128
- params = params or {}
129
- if prim == 'box':
130
- shape = Part.makeBox(float(params.get('dx', 10)), float(params.get('dy', 10)), float(params.get('dz', 10)))
131
- elif prim == 'cylinder':
132
- shape = Part.makeCylinder(float(params.get('radius', 5)), float(params.get('height', 10)))
133
- elif prim == 'sphere':
134
- shape = Part.makeSphere(float(params.get('radius', 5)))
135
- elif prim == 'cone':
136
- shape = Part.makeCone(float(params.get('radius1', 5)), float(params.get('radius2', 0)), float(params.get('height', 10)))
137
- elif prim == 'torus':
138
- shape = Part.makeTorus(float(params.get('majorRadius', 10)), float(params.get('minorRadius', 2)))
139
- else:
140
- raise ValueError('unknown primitive: %s' % prim)
141
- return orient(shape, params.get('at'), params.get('axis'))
142
-
143
-
144
- def extrude_profile(points, height, base):
145
- pts = []
146
- for i in range(0, len(points) - 1, 2):
147
- pts.append(FreeCAD.Vector(float(points[i]), float(points[i + 1]), float(base or 0.0)))
148
- if len(pts) < 3:
149
- raise ValueError('profile needs at least 3 points')
150
- if (pts[-1] - pts[0]).Length > 1e-9:
151
- pts.append(pts[0])
152
- return Part.Face(Part.makePolygon(pts)).extrude(FreeCAD.Vector(0, 0, float(height or 10)))
153
-
154
-
155
- def mesh_to_shape(path):
156
- import Mesh, MeshPart
157
- return MeshPart.meshToShape(Mesh.Mesh(str(path)))
158
-
159
-
160
- def flat_normal(a, b, c):
161
- n = (b - a).cross(c - a)
162
- length = n.Length
163
- if length > 1e-12:
164
- n = FreeCAD.Vector(n.x / length, n.y / length, n.z / length)
165
- else:
166
- n = FreeCAD.Vector(0, 0, 1)
167
- return [n.x, n.y, n.z]
168
-
169
-
170
- def tessellate(body_id, shape, name):
171
- verts, facets = shape.tessellate(0.4)
172
- positions = []
173
- normals = []
174
- indices = []
175
- for v in verts:
176
- positions.extend([v.x, v.y, v.z])
177
- for f in facets:
178
- a, b, c = verts[f[0]], verts[f[1]], verts[f[2]]
179
- n = flat_normal(a, b, c)
180
- normals.extend(n + n + n)
181
- indices.extend([f[0], f[1], f[2]])
182
- return {
183
- 'bodyId': body_id,
184
- 'name': name or body_id,
185
- 'positions': positions,
186
- 'normals': normals,
187
- 'indices': indices,
188
- 'vertexCount': len(verts),
189
- 'triangleCount': len(facets),
190
- }
191
-
192
-
193
- def write_stl(shape, path):
194
- verts, facets = shape.tessellate(0.2)
195
- with open(path, 'wb') as handle:
196
- handle.write(b'\0' * 80)
197
- handle.write(struct.pack('<I', len(facets)))
198
- for f in facets:
199
- a, b, c = verts[f[0]], verts[f[1]], verts[f[2]]
200
- n = flat_normal(a, b, c)
201
- handle.write(struct.pack('<3f', n[0], n[1], n[2]))
202
- for v in (a, b, c):
203
- handle.write(struct.pack('<3f', v.x, v.y, v.z))
204
- handle.write(struct.pack('<H', 0))
205
-
206
-
207
- def run(spec):
208
- bodies = {}
209
- volumes = {}
210
- source = spec.get('input')
211
- if source:
212
- fmt = str(source.get('format', 'step')).lower()
213
- body_id = source.get('bodyId', 'input')
214
- if fmt in ('step', 'stp', 'brep'):
215
- bodies[body_id] = Part.read(str(source['path']))
216
- elif fmt == 'stl':
217
- bodies[body_id] = mesh_to_shape(source['path'])
218
- else:
219
- raise ValueError('unsupported input format: %s' % fmt)
220
- for op in spec.get('ops', []):
221
- kind = op['kind']
222
- if kind == 'reset':
223
- bodies = {}
224
- elif kind == 'create_prim':
225
- bodies[op['bodyId']] = make_prim(op['prim'], op.get('params'))
226
- elif kind == 'extrude_profile':
227
- bodies[op['bodyId']] = extrude_profile(op['points'], op.get('height'), op.get('base'))
228
- elif kind == 'boolean':
229
- target = bodies[op['target']]
230
- operation = op['op']
231
- for tool_id in op['tools']:
232
- tool = bodies[tool_id]
233
- if operation == 'cut':
234
- target = target.cut(tool)
235
- elif operation == 'fuse':
236
- target = target.fuse(tool)
237
- elif operation == 'common':
238
- target = target.common(tool)
239
- else:
240
- raise ValueError('unknown boolean: %s' % operation)
241
- del bodies[tool_id]
242
- bodies[op['target']] = target
243
- elif kind == 'fillet':
244
- shape = bodies[op['target']]
245
- bodies[op['target']] = shape.makeFillet(float(op['radius']), shape.Edges)
246
- elif kind == 'transform':
247
- shape = bodies[op['target']]
248
- if op.get('translate'):
249
- shape.translate(vec(op['translate']))
250
- rotate = op.get('rotate')
251
- if rotate:
252
- for axis, angle in ((FreeCAD.Vector(1, 0, 0), rotate[0]), (FreeCAD.Vector(0, 1, 0), rotate[1]), (FreeCAD.Vector(0, 0, 1), rotate[2])):
253
- if abs(float(angle)) > 1e-9:
254
- shape.rotate(FreeCAD.Vector(0, 0, 0), axis, float(angle))
255
- mirror = op.get('mirror')
256
- if mirror:
257
- shape = shape.mirror(FreeCAD.Vector(0, 0, 0), vec(mirror, (0.0, 0.0, 1.0)))
258
- bodies[op['target']] = shape
259
- elif kind == 'volume':
260
- volumes[op['target']] = bodies[op['target']].Volume
261
- elif kind == 'delete':
262
- bodies.pop(op['target'], None)
263
- else:
264
- raise ValueError('unsupported op: %s' % kind)
265
- names = spec.get('names') or {}
266
- result = {'ok': True, 'meshes': [tessellate(i, s, names.get(i)) for i, s in bodies.items()], 'volumes': volumes}
267
- export = spec.get('export')
268
- if export:
269
- fmt = str(export.get('format', 'step')).lower()
270
- target_path = str(export['path'])
271
- shapes = list(bodies.values())
272
- if len(shapes) == 0:
273
- raise ValueError('nothing to export — the program produced no bodies')
274
- if fmt in ('step', 'stp'):
275
- # Part.export() writes files Part.read() loads as null shapes in
276
- # FreeCAD 1.1 — a compound through exportStep() round-trips.
277
- Part.Compound(shapes).exportStep(target_path)
278
- elif fmt == 'stl':
279
- write_stl(shapes[0], target_path)
280
- else:
281
- raise ValueError('unsupported export format: %s' % fmt)
282
- result['exported'] = target_path
283
- if spec.get('display'):
284
- document = FreeCAD.newDocument('dsh-cad')
285
- for body_id, shape in bodies.items():
286
- obj = document.addObject('Part::Feature', body_id)
287
- obj.Label = names.get(body_id, body_id)
288
- obj.Shape = shape
289
- document.recompute()
290
- return result
291
-
292
-
293
- try:
294
- with open(OPS_PATH) as handle:
295
- write(run(json.load(handle)))
296
- print('###DSH-OK###')
297
- except Exception as error:
298
- write({'ok': False, 'error': str(error), 'trace': traceback.format_exc()})
299
- print('###DSH-ERR###')
91
+ const BRIDGE = String.raw `
92
+ import json, math, os, struct, traceback
93
+
94
+ import FreeCAD
95
+ import Part
96
+
97
+ BASE = os.path.dirname(os.path.abspath(__file__))
98
+ OPS_PATH = os.path.join(BASE, 'ops.json')
99
+ RESULT_PATH = os.path.join(BASE, 'result.json')
100
+
101
+
102
+ def write(result):
103
+ with open(RESULT_PATH, 'w') as handle:
104
+ json.dump(result, handle)
105
+
106
+
107
+ def vec(value, default=(0.0, 0.0, 0.0)):
108
+ if not value:
109
+ return FreeCAD.Vector(*default)
110
+ return FreeCAD.Vector(float(value[0]), float(value[1]), float(value[2]))
111
+
112
+
113
+ def orient(shape, at, axis):
114
+ if at:
115
+ shape.translate(vec(at))
116
+ if axis:
117
+ target = vec(axis, (0.0, 0.0, 1.0))
118
+ if target.Length < 1e-9:
119
+ return shape
120
+ target.normalize()
121
+ if abs(target.z - 1.0) > 1e-9:
122
+ rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), target)
123
+ shape.transformShape(rotation.toMatrix(), True)
124
+ return shape
125
+
126
+
127
+ def make_prim(prim, params):
128
+ params = params or {}
129
+ if prim == 'box':
130
+ shape = Part.makeBox(float(params.get('dx', 10)), float(params.get('dy', 10)), float(params.get('dz', 10)))
131
+ elif prim == 'cylinder':
132
+ shape = Part.makeCylinder(float(params.get('radius', 5)), float(params.get('height', 10)))
133
+ elif prim == 'sphere':
134
+ shape = Part.makeSphere(float(params.get('radius', 5)))
135
+ elif prim == 'cone':
136
+ shape = Part.makeCone(float(params.get('radius1', 5)), float(params.get('radius2', 0)), float(params.get('height', 10)))
137
+ elif prim == 'torus':
138
+ shape = Part.makeTorus(float(params.get('majorRadius', 10)), float(params.get('minorRadius', 2)))
139
+ else:
140
+ raise ValueError('unknown primitive: %s' % prim)
141
+ return orient(shape, params.get('at'), params.get('axis'))
142
+
143
+
144
+ def extrude_profile(points, height, base):
145
+ pts = []
146
+ for i in range(0, len(points) - 1, 2):
147
+ pts.append(FreeCAD.Vector(float(points[i]), float(points[i + 1]), float(base or 0.0)))
148
+ if len(pts) < 3:
149
+ raise ValueError('profile needs at least 3 points')
150
+ if (pts[-1] - pts[0]).Length > 1e-9:
151
+ pts.append(pts[0])
152
+ return Part.Face(Part.makePolygon(pts)).extrude(FreeCAD.Vector(0, 0, float(height or 10)))
153
+
154
+
155
+ def mesh_to_shape(path):
156
+ import Mesh, MeshPart
157
+ return MeshPart.meshToShape(Mesh.Mesh(str(path)))
158
+
159
+
160
+ def flat_normal(a, b, c):
161
+ n = (b - a).cross(c - a)
162
+ length = n.Length
163
+ if length > 1e-12:
164
+ n = FreeCAD.Vector(n.x / length, n.y / length, n.z / length)
165
+ else:
166
+ n = FreeCAD.Vector(0, 0, 1)
167
+ return [n.x, n.y, n.z]
168
+
169
+
170
+ def tessellate(body_id, shape, name):
171
+ verts, facets = shape.tessellate(0.4)
172
+ positions = []
173
+ normals = []
174
+ indices = []
175
+ for v in verts:
176
+ positions.extend([v.x, v.y, v.z])
177
+ for f in facets:
178
+ a, b, c = verts[f[0]], verts[f[1]], verts[f[2]]
179
+ n = flat_normal(a, b, c)
180
+ normals.extend(n + n + n)
181
+ indices.extend([f[0], f[1], f[2]])
182
+ return {
183
+ 'bodyId': body_id,
184
+ 'name': name or body_id,
185
+ 'positions': positions,
186
+ 'normals': normals,
187
+ 'indices': indices,
188
+ 'vertexCount': len(verts),
189
+ 'triangleCount': len(facets),
190
+ }
191
+
192
+
193
+ def write_stl(shape, path):
194
+ verts, facets = shape.tessellate(0.2)
195
+ with open(path, 'wb') as handle:
196
+ handle.write(b'\0' * 80)
197
+ handle.write(struct.pack('<I', len(facets)))
198
+ for f in facets:
199
+ a, b, c = verts[f[0]], verts[f[1]], verts[f[2]]
200
+ n = flat_normal(a, b, c)
201
+ handle.write(struct.pack('<3f', n[0], n[1], n[2]))
202
+ for v in (a, b, c):
203
+ handle.write(struct.pack('<3f', v.x, v.y, v.z))
204
+ handle.write(struct.pack('<H', 0))
205
+
206
+
207
+ def run(spec):
208
+ bodies = {}
209
+ volumes = {}
210
+ source = spec.get('input')
211
+ if source:
212
+ fmt = str(source.get('format', 'step')).lower()
213
+ body_id = source.get('bodyId', 'input')
214
+ if fmt in ('step', 'stp', 'brep'):
215
+ bodies[body_id] = Part.read(str(source['path']))
216
+ elif fmt == 'stl':
217
+ bodies[body_id] = mesh_to_shape(source['path'])
218
+ else:
219
+ raise ValueError('unsupported input format: %s' % fmt)
220
+ for op in spec.get('ops', []):
221
+ kind = op['kind']
222
+ if kind == 'reset':
223
+ bodies = {}
224
+ elif kind == 'create_prim':
225
+ bodies[op['bodyId']] = make_prim(op['prim'], op.get('params'))
226
+ elif kind == 'extrude_profile':
227
+ bodies[op['bodyId']] = extrude_profile(op['points'], op.get('height'), op.get('base'))
228
+ elif kind == 'boolean':
229
+ target = bodies[op['target']]
230
+ operation = op['op']
231
+ for tool_id in op['tools']:
232
+ tool = bodies[tool_id]
233
+ if operation == 'cut':
234
+ target = target.cut(tool)
235
+ elif operation == 'fuse':
236
+ target = target.fuse(tool)
237
+ elif operation == 'common':
238
+ target = target.common(tool)
239
+ else:
240
+ raise ValueError('unknown boolean: %s' % operation)
241
+ del bodies[tool_id]
242
+ bodies[op['target']] = target
243
+ elif kind == 'fillet':
244
+ shape = bodies[op['target']]
245
+ bodies[op['target']] = shape.makeFillet(float(op['radius']), shape.Edges)
246
+ elif kind == 'transform':
247
+ shape = bodies[op['target']]
248
+ if op.get('translate'):
249
+ shape.translate(vec(op['translate']))
250
+ rotate = op.get('rotate')
251
+ if rotate:
252
+ for axis, angle in ((FreeCAD.Vector(1, 0, 0), rotate[0]), (FreeCAD.Vector(0, 1, 0), rotate[1]), (FreeCAD.Vector(0, 0, 1), rotate[2])):
253
+ if abs(float(angle)) > 1e-9:
254
+ shape.rotate(FreeCAD.Vector(0, 0, 0), axis, float(angle))
255
+ mirror = op.get('mirror')
256
+ if mirror:
257
+ shape = shape.mirror(FreeCAD.Vector(0, 0, 0), vec(mirror, (0.0, 0.0, 1.0)))
258
+ bodies[op['target']] = shape
259
+ elif kind == 'volume':
260
+ volumes[op['target']] = bodies[op['target']].Volume
261
+ elif kind == 'delete':
262
+ bodies.pop(op['target'], None)
263
+ else:
264
+ raise ValueError('unsupported op: %s' % kind)
265
+ names = spec.get('names') or {}
266
+ result = {'ok': True, 'meshes': [tessellate(i, s, names.get(i)) for i, s in bodies.items()], 'volumes': volumes}
267
+ export = spec.get('export')
268
+ if export:
269
+ fmt = str(export.get('format', 'step')).lower()
270
+ target_path = str(export['path'])
271
+ shapes = list(bodies.values())
272
+ if len(shapes) == 0:
273
+ raise ValueError('nothing to export — the program produced no bodies')
274
+ if fmt in ('step', 'stp'):
275
+ # Part.export() writes files Part.read() loads as null shapes in
276
+ # FreeCAD 1.1 — a compound through exportStep() round-trips.
277
+ Part.Compound(shapes).exportStep(target_path)
278
+ elif fmt == 'stl':
279
+ write_stl(shapes[0], target_path)
280
+ else:
281
+ raise ValueError('unsupported export format: %s' % fmt)
282
+ result['exported'] = target_path
283
+ if spec.get('display'):
284
+ document = FreeCAD.newDocument('dsh-cad')
285
+ for body_id, shape in bodies.items():
286
+ obj = document.addObject('Part::Feature', body_id)
287
+ obj.Label = names.get(body_id, body_id)
288
+ obj.Shape = shape
289
+ document.recompute()
290
+ return result
291
+
292
+
293
+ try:
294
+ with open(OPS_PATH) as handle:
295
+ write(run(json.load(handle)))
296
+ print('###DSH-OK###')
297
+ except Exception as error:
298
+ write({'ok': False, 'error': str(error), 'trace': traceback.format_exc()})
299
+ print('###DSH-ERR###')
300
300
  `;
301
301
  /**
302
302
  * Run a feature program in an external FreeCAD process and return tessellated