dsh-cad 0.13.2 → 0.14.0
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.
- package/LICENSE +201 -201
- package/README.md +3 -3
- package/README.zh-CN.md +1 -1
- package/lib/cad_connector/freecad-executor.js +209 -209
- package/lib/cad_connector/fusion360-executor.js +244 -244
- package/lib/client.js +172 -172
- package/lib/convert/step-worker.mjs +61 -61
- package/lib/demo-bracket.brep +667 -667
- package/lib/demo-flange.brep +708 -708
- package/lib/demo-shaft.brep +515 -515
- package/lib/modeling/modeling-worker.cjs +2 -2
- package/lib/modeling/occt-adapter.cjs +90 -3
- package/lib/modeling/occtts-adapter.cjs +80 -7
- package/lib/routes.js +5 -0
- package/lib/tools/cad-model.js +16 -5
- package/lib/types/modeling/client.d.ts +19 -1
- package/package.json +1 -1
|
@@ -151,248 +151,248 @@ export const FUSION360_EXECUTOR = {
|
|
|
151
151
|
* booleans + transforms; sketch-based extrusion; MeshManager tessellation;
|
|
152
152
|
* STEP/STL export via the ExportManager.
|
|
153
153
|
*/
|
|
154
|
-
const BRIDGE_ADDIN = String.raw `
|
|
155
|
-
import json
|
|
156
|
-
import math
|
|
157
|
-
import os
|
|
158
|
-
import traceback
|
|
159
|
-
|
|
160
|
-
import adsk.core
|
|
161
|
-
import adsk.fusion
|
|
162
|
-
|
|
163
|
-
SPOOL = os.path.join(os.path.expanduser('~'), '.dsh-cad', 'fusion-spool')
|
|
164
|
-
POLL_SECONDS = 0.5
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def log(message):
|
|
168
|
-
try:
|
|
169
|
-
with open(os.path.join(SPOOL, 'bridge.log'), 'a') as handle:
|
|
170
|
-
handle.write(message + '\n')
|
|
171
|
-
except Exception:
|
|
172
|
-
pass
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def vector(values, default=(0.0, 0.0, 0.0)):
|
|
176
|
-
if not values:
|
|
177
|
-
return adsk.core.Vector3D.create(*default)
|
|
178
|
-
return adsk.core.Vector3D.create(float(values[0]), float(values[1]), float(values[2]))
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
class Runner:
|
|
182
|
-
def __init__(self, app):
|
|
183
|
-
self.app = app
|
|
184
|
-
self.temp_brep = adsk.core.TemporaryBRepManager.get()
|
|
185
|
-
self.design = None
|
|
186
|
-
|
|
187
|
-
def document(self):
|
|
188
|
-
doc = self.app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)
|
|
189
|
-
doc.activate()
|
|
190
|
-
self.design = adsk.fusion.Design.cast(doc.products.itemByProductType('DesignProductType'))
|
|
191
|
-
self.design.designType = adsk.fusion.DesignTypes.DirectDesignType
|
|
192
|
-
return doc
|
|
193
|
-
|
|
194
|
-
def create_prim(self, prim, params):
|
|
195
|
-
params = params or {}
|
|
196
|
-
at = params.get('at')
|
|
197
|
-
base = vector(at)
|
|
198
|
-
if prim == 'box':
|
|
199
|
-
dx, dy, dz = float(params.get('dx', 10)), float(params.get('dy', 10)), float(params.get('dz', 10))
|
|
200
|
-
# NOTE: verify the width/length/height argument order against the
|
|
201
|
-
# local Fusion API docs on the first live run.
|
|
202
|
-
return self.temp_brep.createBox(adsk.core.OrientedBoundingBox3D.create(base, adsk.core.Vector3D.create(1, 0, 0), adsk.core.Vector3D.create(0, 1, 0), dx, dy, dz))
|
|
203
|
-
if prim == 'cylinder':
|
|
204
|
-
height = float(params.get('height', 10))
|
|
205
|
-
return self.temp_brep.createCylinder(adsk.core.InputCylinder.create(base, vector(params.get('axis'), (0, 0, 1)), float(params.get('radius', 5)), height))
|
|
206
|
-
if prim == 'sphere':
|
|
207
|
-
return self.temp_brep.createSphere(adsk.core.InputSphere.create(base, float(params.get('radius', 5))))
|
|
208
|
-
if prim == 'cone':
|
|
209
|
-
return self.temp_brep.createCone(adsk.core.InputCone.create(base, vector(params.get('axis'), (0, 0, 1)), float(params.get('radius1', 5)), float(params.get('radius2', 0)), float(params.get('height', 10))))
|
|
210
|
-
if prim == 'torus':
|
|
211
|
-
return self.temp_brep.createTorus(adsk.core.InputTorus.create(base, vector(params.get('axis'), (0, 0, 1)), float(params.get('majorRadius', 10)), float(params.get('minorRadius', 2))))
|
|
212
|
-
raise ValueError('unknown primitive: %s' % prim)
|
|
213
|
-
|
|
214
|
-
def boolean(self, op, target, tool):
|
|
215
|
-
if op == 'cut':
|
|
216
|
-
kind = adsk.core.BooleanTypes.DifferenceBooleanType
|
|
217
|
-
elif op == 'fuse':
|
|
218
|
-
kind = adsk.core.BooleanTypes.UnionBooleanType
|
|
219
|
-
else:
|
|
220
|
-
kind = adsk.core.BooleanTypes.IntersectionBooleanType
|
|
221
|
-
self.temp_brep.booleanOperation(target, tool, kind)
|
|
222
|
-
|
|
223
|
-
def transform(self, body, op):
|
|
224
|
-
if op.get('translate'):
|
|
225
|
-
t = vector(op['translate'])
|
|
226
|
-
m = adsk.core.Matrix3D.create()
|
|
227
|
-
m.translation = t
|
|
228
|
-
self.temp_brep.transform(body, m)
|
|
229
|
-
rotate = op.get('rotate')
|
|
230
|
-
if rotate:
|
|
231
|
-
m = adsk.core.Matrix3D.create()
|
|
232
|
-
for axis, angle in ((adsk.core.Vector3D.create(1, 0, 0), rotate[0]), (adsk.core.Vector3D.create(0, 1, 0), rotate[1]), (adsk.core.Vector3D.create(0, 0, 1), rotate[2])):
|
|
233
|
-
if abs(float(angle)) > 1e-9:
|
|
234
|
-
m.setToRotation(math.radians(float(angle)), axis, adsk.core.Point3D.create(0, 0, 0))
|
|
235
|
-
self.temp_brep.transform(body, m)
|
|
236
|
-
|
|
237
|
-
def extrude(self, points, height):
|
|
238
|
-
root = self.design.rootComponent
|
|
239
|
-
sketch = root.sketches.add(root.xYConstructionPlane)
|
|
240
|
-
pts = []
|
|
241
|
-
for i in range(0, len(points) - 1, 2):
|
|
242
|
-
pts.append(adsk.core.Point3D.create(float(points[i]), float(points[i + 1]), 0))
|
|
243
|
-
if len(pts) < 3:
|
|
244
|
-
raise ValueError('profile needs at least 3 points')
|
|
245
|
-
if (pts[-1].distanceTo(pts[0])) > 1e-9:
|
|
246
|
-
pts.append(pts[0])
|
|
247
|
-
lines = sketch.sketchCurves.sketchLines
|
|
248
|
-
for a, b in zip(pts, pts[1:]):
|
|
249
|
-
lines.addByTwoPoints(a, b)
|
|
250
|
-
profile = sketch.profiles.item(0)
|
|
251
|
-
extrude_input = root.features.extrudeFeatures.createInput(profile, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)
|
|
252
|
-
distance = adsk.fusion.DistanceExtentDefinition.create(adsk.core.ValueInput.createByReal(float(height or 10)))
|
|
253
|
-
extrude_input.setOneSideExtent(distance, adsk.fusion.ExtentDirections.PositiveExtentDirection)
|
|
254
|
-
feature = root.features.extrudeFeatures.add(extrude_input)
|
|
255
|
-
body = feature.bodies.item(0)
|
|
256
|
-
return self.temp_brep.copy(body)
|
|
257
|
-
|
|
258
|
-
def show(self, bodies, names):
|
|
259
|
-
root = self.design.rootComponent
|
|
260
|
-
occurrence = root
|
|
261
|
-
for body_id, body in bodies.items():
|
|
262
|
-
feature = occurrence.features.baseFeatures.add()
|
|
263
|
-
feature.startEdit()
|
|
264
|
-
occurrence.bRepBodies.add(body, feature, names.get(body_id, body_id))
|
|
265
|
-
feature.finishEdit()
|
|
266
|
-
|
|
267
|
-
def tessellate(self, bodies, names):
|
|
268
|
-
root = self.design.rootComponent
|
|
269
|
-
meshes = []
|
|
270
|
-
volumes = {}
|
|
271
|
-
for body_id, body in bodies.items():
|
|
272
|
-
feature = root.features.baseFeatures.add()
|
|
273
|
-
feature.startEdit()
|
|
274
|
-
shown = root.bRepBodies.add(body, feature, names.get(body_id, body_id))
|
|
275
|
-
feature.finishEdit()
|
|
276
|
-
mesh_manager = shown.meshManager
|
|
277
|
-
mesh = mesh_manager.calculateMeshData(adsk.fusion.MeshCalculationOptions.create())
|
|
278
|
-
coordinates = mesh.nodeCoordinatesAsArray
|
|
279
|
-
triangles = mesh.triangleVerticesAsArray
|
|
280
|
-
positions = []
|
|
281
|
-
for point in coordinates:
|
|
282
|
-
positions.extend([point.x, point.y, point.z])
|
|
283
|
-
indices = [int(index) for index in triangles]
|
|
284
|
-
normals = []
|
|
285
|
-
for i in range(0, len(indices), 3):
|
|
286
|
-
a, b, c = coordinates[indices[i]], coordinates[indices[i + 1]], coordinates[indices[i + 2]]
|
|
287
|
-
ux, uy, uz = b.x - a.x, b.y - a.y, b.z - a.z
|
|
288
|
-
vx, vy, vz = c.x - a.x, c.y - a.y, c.z - a.z
|
|
289
|
-
nx, ny, nz = uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx
|
|
290
|
-
length = math.sqrt(nx * nx + ny * ny + nz * nz) or 1.0
|
|
291
|
-
normals.extend([nx / length, ny / length, nz / length] * 3)
|
|
292
|
-
volumes[body_id] = abs(sum(
|
|
293
|
-
(coordinates[indices[i]].x * (coordinates[indices[i + 1]].y * coordinates[indices[i + 2]].z - coordinates[indices[i + 1]].z * coordinates[indices[i + 2]].y)
|
|
294
|
-
- coordinates[indices[i]].y * (coordinates[indices[i + 1]].x * coordinates[indices[i + 2]].z - coordinates[indices[i + 1]].z * coordinates[indices[i + 2]].x)
|
|
295
|
-
+ coordinates[indices[i]].z * (coordinates[indices[i + 1]].x * coordinates[indices[i + 2]].y - coordinates[indices[i + 1]].y * coordinates[indices[i + 2]].x)) / 6.0
|
|
296
|
-
for i in range(0, len(indices), 3)))
|
|
297
|
-
meshes.append({
|
|
298
|
-
'bodyId': body_id,
|
|
299
|
-
'name': names.get(body_id, body_id),
|
|
300
|
-
'positions': positions,
|
|
301
|
-
'normals': normals,
|
|
302
|
-
'indices': indices,
|
|
303
|
-
'vertexCount': len(coordinates),
|
|
304
|
-
'triangleCount': len(indices) // 3,
|
|
305
|
-
})
|
|
306
|
-
return meshes, volumes
|
|
307
|
-
|
|
308
|
-
def export(self, bodies, spec):
|
|
309
|
-
doc = self.app.activeDocument
|
|
310
|
-
if doc.isModified:
|
|
311
|
-
doc.saveAs(spec['path'])
|
|
312
|
-
return spec['path']
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
def execute(app, program):
|
|
316
|
-
runner = Runner(app)
|
|
317
|
-
runner.document()
|
|
318
|
-
bodies = {}
|
|
319
|
-
volumes = {}
|
|
320
|
-
for op in program.get('ops', []):
|
|
321
|
-
kind = op['kind']
|
|
322
|
-
if kind == 'reset':
|
|
323
|
-
bodies = {}
|
|
324
|
-
elif kind == 'create_prim':
|
|
325
|
-
bodies[op['bodyId']] = runner.create_prim(op['prim'], op.get('params'))
|
|
326
|
-
elif kind == 'extrude_profile':
|
|
327
|
-
bodies[op['bodyId']] = runner.extrude(op['points'], op.get('height'))
|
|
328
|
-
elif kind == 'boolean':
|
|
329
|
-
target = bodies[op['target']]
|
|
330
|
-
for tool_id in op['tools']:
|
|
331
|
-
runner.boolean(op['op'], target, bodies[tool_id])
|
|
332
|
-
del bodies[tool_id]
|
|
333
|
-
bodies[op['target']] = target
|
|
334
|
-
elif kind == 'transform':
|
|
335
|
-
runner.transform(bodies[op['target']], op)
|
|
336
|
-
elif kind == 'volume':
|
|
337
|
-
pass # volumes derive from the tessellation below
|
|
338
|
-
elif kind == 'delete':
|
|
339
|
-
bodies.pop(op['target'], None)
|
|
340
|
-
else:
|
|
341
|
-
raise ValueError('unsupported op: %s' % kind)
|
|
342
|
-
names = program.get('names') or {}
|
|
343
|
-
meshes, volumes = runner.tessellate(bodies, names)
|
|
344
|
-
result = {'ok': True, 'meshes': meshes, 'volumes': volumes}
|
|
345
|
-
export = program.get('export')
|
|
346
|
-
if export and meshes:
|
|
347
|
-
result['exported'] = runner.export(bodies, export)
|
|
348
|
-
return result
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
def process_job(app, job_path):
|
|
352
|
-
result_path = job_path.replace('.running', '').replace('.json', '') + '.result.json'
|
|
353
|
-
try:
|
|
354
|
-
with open(job_path) as handle:
|
|
355
|
-
job = json.load(handle)
|
|
356
|
-
# Claim semantics: remove the job file before executing so a spool
|
|
357
|
-
# rename on the host side cannot re-trigger the same job.
|
|
358
|
-
try:
|
|
359
|
-
os.remove(job_path)
|
|
360
|
-
except Exception:
|
|
361
|
-
pass
|
|
362
|
-
result = execute(app, job.get('program', {}))
|
|
363
|
-
except Exception as error:
|
|
364
|
-
result = {'ok': False, 'error': str(error), 'trace': traceback.format_exc()}
|
|
365
|
-
try:
|
|
366
|
-
with open(result_path, 'w') as handle:
|
|
367
|
-
json.dump(result, handle)
|
|
368
|
-
except Exception:
|
|
369
|
-
pass
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
def run(context):
|
|
373
|
-
app = adsk.core.Application.get()
|
|
374
|
-
ui = app.userInterface
|
|
375
|
-
try:
|
|
376
|
-
os.makedirs(SPOOL, exist_ok=True)
|
|
377
|
-
log('bridge loaded')
|
|
378
|
-
|
|
379
|
-
class PollHandler(adsk.core.CustomEventHandler):
|
|
380
|
-
def notify(self, args):
|
|
381
|
-
try:
|
|
382
|
-
for name in sorted(os.listdir(SPOOL)):
|
|
383
|
-
if name.endswith('.json') or name.endswith('.running'):
|
|
384
|
-
process_job(app, os.path.join(SPOOL, name))
|
|
385
|
-
break
|
|
386
|
-
except Exception:
|
|
387
|
-
log(traceback.format_exc())
|
|
388
|
-
|
|
389
|
-
# One recurring timer: its firing CustomEvent drives the spool poll,
|
|
390
|
-
# keeping the bridge resident without any UI.
|
|
391
|
-
timer = adsk.core.Timer.create()
|
|
392
|
-
timer.duration = POLL_SECONDS
|
|
393
|
-
timer.firing.add(PollHandler())
|
|
394
|
-
timer.isRunning = True
|
|
395
|
-
except Exception:
|
|
396
|
-
if ui:
|
|
397
|
-
ui.messageBox('dsh-cad bridge failed to start:\n{}'.format(traceback.format_exc()))
|
|
154
|
+
const BRIDGE_ADDIN = String.raw `
|
|
155
|
+
import json
|
|
156
|
+
import math
|
|
157
|
+
import os
|
|
158
|
+
import traceback
|
|
159
|
+
|
|
160
|
+
import adsk.core
|
|
161
|
+
import adsk.fusion
|
|
162
|
+
|
|
163
|
+
SPOOL = os.path.join(os.path.expanduser('~'), '.dsh-cad', 'fusion-spool')
|
|
164
|
+
POLL_SECONDS = 0.5
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def log(message):
|
|
168
|
+
try:
|
|
169
|
+
with open(os.path.join(SPOOL, 'bridge.log'), 'a') as handle:
|
|
170
|
+
handle.write(message + '\n')
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def vector(values, default=(0.0, 0.0, 0.0)):
|
|
176
|
+
if not values:
|
|
177
|
+
return adsk.core.Vector3D.create(*default)
|
|
178
|
+
return adsk.core.Vector3D.create(float(values[0]), float(values[1]), float(values[2]))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class Runner:
|
|
182
|
+
def __init__(self, app):
|
|
183
|
+
self.app = app
|
|
184
|
+
self.temp_brep = adsk.core.TemporaryBRepManager.get()
|
|
185
|
+
self.design = None
|
|
186
|
+
|
|
187
|
+
def document(self):
|
|
188
|
+
doc = self.app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)
|
|
189
|
+
doc.activate()
|
|
190
|
+
self.design = adsk.fusion.Design.cast(doc.products.itemByProductType('DesignProductType'))
|
|
191
|
+
self.design.designType = adsk.fusion.DesignTypes.DirectDesignType
|
|
192
|
+
return doc
|
|
193
|
+
|
|
194
|
+
def create_prim(self, prim, params):
|
|
195
|
+
params = params or {}
|
|
196
|
+
at = params.get('at')
|
|
197
|
+
base = vector(at)
|
|
198
|
+
if prim == 'box':
|
|
199
|
+
dx, dy, dz = float(params.get('dx', 10)), float(params.get('dy', 10)), float(params.get('dz', 10))
|
|
200
|
+
# NOTE: verify the width/length/height argument order against the
|
|
201
|
+
# local Fusion API docs on the first live run.
|
|
202
|
+
return self.temp_brep.createBox(adsk.core.OrientedBoundingBox3D.create(base, adsk.core.Vector3D.create(1, 0, 0), adsk.core.Vector3D.create(0, 1, 0), dx, dy, dz))
|
|
203
|
+
if prim == 'cylinder':
|
|
204
|
+
height = float(params.get('height', 10))
|
|
205
|
+
return self.temp_brep.createCylinder(adsk.core.InputCylinder.create(base, vector(params.get('axis'), (0, 0, 1)), float(params.get('radius', 5)), height))
|
|
206
|
+
if prim == 'sphere':
|
|
207
|
+
return self.temp_brep.createSphere(adsk.core.InputSphere.create(base, float(params.get('radius', 5))))
|
|
208
|
+
if prim == 'cone':
|
|
209
|
+
return self.temp_brep.createCone(adsk.core.InputCone.create(base, vector(params.get('axis'), (0, 0, 1)), float(params.get('radius1', 5)), float(params.get('radius2', 0)), float(params.get('height', 10))))
|
|
210
|
+
if prim == 'torus':
|
|
211
|
+
return self.temp_brep.createTorus(adsk.core.InputTorus.create(base, vector(params.get('axis'), (0, 0, 1)), float(params.get('majorRadius', 10)), float(params.get('minorRadius', 2))))
|
|
212
|
+
raise ValueError('unknown primitive: %s' % prim)
|
|
213
|
+
|
|
214
|
+
def boolean(self, op, target, tool):
|
|
215
|
+
if op == 'cut':
|
|
216
|
+
kind = adsk.core.BooleanTypes.DifferenceBooleanType
|
|
217
|
+
elif op == 'fuse':
|
|
218
|
+
kind = adsk.core.BooleanTypes.UnionBooleanType
|
|
219
|
+
else:
|
|
220
|
+
kind = adsk.core.BooleanTypes.IntersectionBooleanType
|
|
221
|
+
self.temp_brep.booleanOperation(target, tool, kind)
|
|
222
|
+
|
|
223
|
+
def transform(self, body, op):
|
|
224
|
+
if op.get('translate'):
|
|
225
|
+
t = vector(op['translate'])
|
|
226
|
+
m = adsk.core.Matrix3D.create()
|
|
227
|
+
m.translation = t
|
|
228
|
+
self.temp_brep.transform(body, m)
|
|
229
|
+
rotate = op.get('rotate')
|
|
230
|
+
if rotate:
|
|
231
|
+
m = adsk.core.Matrix3D.create()
|
|
232
|
+
for axis, angle in ((adsk.core.Vector3D.create(1, 0, 0), rotate[0]), (adsk.core.Vector3D.create(0, 1, 0), rotate[1]), (adsk.core.Vector3D.create(0, 0, 1), rotate[2])):
|
|
233
|
+
if abs(float(angle)) > 1e-9:
|
|
234
|
+
m.setToRotation(math.radians(float(angle)), axis, adsk.core.Point3D.create(0, 0, 0))
|
|
235
|
+
self.temp_brep.transform(body, m)
|
|
236
|
+
|
|
237
|
+
def extrude(self, points, height):
|
|
238
|
+
root = self.design.rootComponent
|
|
239
|
+
sketch = root.sketches.add(root.xYConstructionPlane)
|
|
240
|
+
pts = []
|
|
241
|
+
for i in range(0, len(points) - 1, 2):
|
|
242
|
+
pts.append(adsk.core.Point3D.create(float(points[i]), float(points[i + 1]), 0))
|
|
243
|
+
if len(pts) < 3:
|
|
244
|
+
raise ValueError('profile needs at least 3 points')
|
|
245
|
+
if (pts[-1].distanceTo(pts[0])) > 1e-9:
|
|
246
|
+
pts.append(pts[0])
|
|
247
|
+
lines = sketch.sketchCurves.sketchLines
|
|
248
|
+
for a, b in zip(pts, pts[1:]):
|
|
249
|
+
lines.addByTwoPoints(a, b)
|
|
250
|
+
profile = sketch.profiles.item(0)
|
|
251
|
+
extrude_input = root.features.extrudeFeatures.createInput(profile, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)
|
|
252
|
+
distance = adsk.fusion.DistanceExtentDefinition.create(adsk.core.ValueInput.createByReal(float(height or 10)))
|
|
253
|
+
extrude_input.setOneSideExtent(distance, adsk.fusion.ExtentDirections.PositiveExtentDirection)
|
|
254
|
+
feature = root.features.extrudeFeatures.add(extrude_input)
|
|
255
|
+
body = feature.bodies.item(0)
|
|
256
|
+
return self.temp_brep.copy(body)
|
|
257
|
+
|
|
258
|
+
def show(self, bodies, names):
|
|
259
|
+
root = self.design.rootComponent
|
|
260
|
+
occurrence = root
|
|
261
|
+
for body_id, body in bodies.items():
|
|
262
|
+
feature = occurrence.features.baseFeatures.add()
|
|
263
|
+
feature.startEdit()
|
|
264
|
+
occurrence.bRepBodies.add(body, feature, names.get(body_id, body_id))
|
|
265
|
+
feature.finishEdit()
|
|
266
|
+
|
|
267
|
+
def tessellate(self, bodies, names):
|
|
268
|
+
root = self.design.rootComponent
|
|
269
|
+
meshes = []
|
|
270
|
+
volumes = {}
|
|
271
|
+
for body_id, body in bodies.items():
|
|
272
|
+
feature = root.features.baseFeatures.add()
|
|
273
|
+
feature.startEdit()
|
|
274
|
+
shown = root.bRepBodies.add(body, feature, names.get(body_id, body_id))
|
|
275
|
+
feature.finishEdit()
|
|
276
|
+
mesh_manager = shown.meshManager
|
|
277
|
+
mesh = mesh_manager.calculateMeshData(adsk.fusion.MeshCalculationOptions.create())
|
|
278
|
+
coordinates = mesh.nodeCoordinatesAsArray
|
|
279
|
+
triangles = mesh.triangleVerticesAsArray
|
|
280
|
+
positions = []
|
|
281
|
+
for point in coordinates:
|
|
282
|
+
positions.extend([point.x, point.y, point.z])
|
|
283
|
+
indices = [int(index) for index in triangles]
|
|
284
|
+
normals = []
|
|
285
|
+
for i in range(0, len(indices), 3):
|
|
286
|
+
a, b, c = coordinates[indices[i]], coordinates[indices[i + 1]], coordinates[indices[i + 2]]
|
|
287
|
+
ux, uy, uz = b.x - a.x, b.y - a.y, b.z - a.z
|
|
288
|
+
vx, vy, vz = c.x - a.x, c.y - a.y, c.z - a.z
|
|
289
|
+
nx, ny, nz = uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx
|
|
290
|
+
length = math.sqrt(nx * nx + ny * ny + nz * nz) or 1.0
|
|
291
|
+
normals.extend([nx / length, ny / length, nz / length] * 3)
|
|
292
|
+
volumes[body_id] = abs(sum(
|
|
293
|
+
(coordinates[indices[i]].x * (coordinates[indices[i + 1]].y * coordinates[indices[i + 2]].z - coordinates[indices[i + 1]].z * coordinates[indices[i + 2]].y)
|
|
294
|
+
- coordinates[indices[i]].y * (coordinates[indices[i + 1]].x * coordinates[indices[i + 2]].z - coordinates[indices[i + 1]].z * coordinates[indices[i + 2]].x)
|
|
295
|
+
+ coordinates[indices[i]].z * (coordinates[indices[i + 1]].x * coordinates[indices[i + 2]].y - coordinates[indices[i + 1]].y * coordinates[indices[i + 2]].x)) / 6.0
|
|
296
|
+
for i in range(0, len(indices), 3)))
|
|
297
|
+
meshes.append({
|
|
298
|
+
'bodyId': body_id,
|
|
299
|
+
'name': names.get(body_id, body_id),
|
|
300
|
+
'positions': positions,
|
|
301
|
+
'normals': normals,
|
|
302
|
+
'indices': indices,
|
|
303
|
+
'vertexCount': len(coordinates),
|
|
304
|
+
'triangleCount': len(indices) // 3,
|
|
305
|
+
})
|
|
306
|
+
return meshes, volumes
|
|
307
|
+
|
|
308
|
+
def export(self, bodies, spec):
|
|
309
|
+
doc = self.app.activeDocument
|
|
310
|
+
if doc.isModified:
|
|
311
|
+
doc.saveAs(spec['path'])
|
|
312
|
+
return spec['path']
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def execute(app, program):
|
|
316
|
+
runner = Runner(app)
|
|
317
|
+
runner.document()
|
|
318
|
+
bodies = {}
|
|
319
|
+
volumes = {}
|
|
320
|
+
for op in program.get('ops', []):
|
|
321
|
+
kind = op['kind']
|
|
322
|
+
if kind == 'reset':
|
|
323
|
+
bodies = {}
|
|
324
|
+
elif kind == 'create_prim':
|
|
325
|
+
bodies[op['bodyId']] = runner.create_prim(op['prim'], op.get('params'))
|
|
326
|
+
elif kind == 'extrude_profile':
|
|
327
|
+
bodies[op['bodyId']] = runner.extrude(op['points'], op.get('height'))
|
|
328
|
+
elif kind == 'boolean':
|
|
329
|
+
target = bodies[op['target']]
|
|
330
|
+
for tool_id in op['tools']:
|
|
331
|
+
runner.boolean(op['op'], target, bodies[tool_id])
|
|
332
|
+
del bodies[tool_id]
|
|
333
|
+
bodies[op['target']] = target
|
|
334
|
+
elif kind == 'transform':
|
|
335
|
+
runner.transform(bodies[op['target']], op)
|
|
336
|
+
elif kind == 'volume':
|
|
337
|
+
pass # volumes derive from the tessellation below
|
|
338
|
+
elif kind == 'delete':
|
|
339
|
+
bodies.pop(op['target'], None)
|
|
340
|
+
else:
|
|
341
|
+
raise ValueError('unsupported op: %s' % kind)
|
|
342
|
+
names = program.get('names') or {}
|
|
343
|
+
meshes, volumes = runner.tessellate(bodies, names)
|
|
344
|
+
result = {'ok': True, 'meshes': meshes, 'volumes': volumes}
|
|
345
|
+
export = program.get('export')
|
|
346
|
+
if export and meshes:
|
|
347
|
+
result['exported'] = runner.export(bodies, export)
|
|
348
|
+
return result
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def process_job(app, job_path):
|
|
352
|
+
result_path = job_path.replace('.running', '').replace('.json', '') + '.result.json'
|
|
353
|
+
try:
|
|
354
|
+
with open(job_path) as handle:
|
|
355
|
+
job = json.load(handle)
|
|
356
|
+
# Claim semantics: remove the job file before executing so a spool
|
|
357
|
+
# rename on the host side cannot re-trigger the same job.
|
|
358
|
+
try:
|
|
359
|
+
os.remove(job_path)
|
|
360
|
+
except Exception:
|
|
361
|
+
pass
|
|
362
|
+
result = execute(app, job.get('program', {}))
|
|
363
|
+
except Exception as error:
|
|
364
|
+
result = {'ok': False, 'error': str(error), 'trace': traceback.format_exc()}
|
|
365
|
+
try:
|
|
366
|
+
with open(result_path, 'w') as handle:
|
|
367
|
+
json.dump(result, handle)
|
|
368
|
+
except Exception:
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def run(context):
|
|
373
|
+
app = adsk.core.Application.get()
|
|
374
|
+
ui = app.userInterface
|
|
375
|
+
try:
|
|
376
|
+
os.makedirs(SPOOL, exist_ok=True)
|
|
377
|
+
log('bridge loaded')
|
|
378
|
+
|
|
379
|
+
class PollHandler(adsk.core.CustomEventHandler):
|
|
380
|
+
def notify(self, args):
|
|
381
|
+
try:
|
|
382
|
+
for name in sorted(os.listdir(SPOOL)):
|
|
383
|
+
if name.endswith('.json') or name.endswith('.running'):
|
|
384
|
+
process_job(app, os.path.join(SPOOL, name))
|
|
385
|
+
break
|
|
386
|
+
except Exception:
|
|
387
|
+
log(traceback.format_exc())
|
|
388
|
+
|
|
389
|
+
# One recurring timer: its firing CustomEvent drives the spool poll,
|
|
390
|
+
# keeping the bridge resident without any UI.
|
|
391
|
+
timer = adsk.core.Timer.create()
|
|
392
|
+
timer.duration = POLL_SECONDS
|
|
393
|
+
timer.firing.add(PollHandler())
|
|
394
|
+
timer.isRunning = True
|
|
395
|
+
except Exception:
|
|
396
|
+
if ui:
|
|
397
|
+
ui.messageBox('dsh-cad bridge failed to start:\n{}'.format(traceback.format_exc()))
|
|
398
398
|
`;
|