dsh-cad 0.2.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.
Files changed (113) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +190 -0
  3. package/README.zh-CN.md +173 -0
  4. package/cordis.patch.yml +3 -0
  5. package/lib/b64.js +12 -0
  6. package/lib/cad_connector/builtin-executor.js +55 -0
  7. package/lib/cad_connector/builtin.js +8 -0
  8. package/lib/cad_connector/executor.js +76 -0
  9. package/lib/cad_connector/freecad-executor.js +398 -0
  10. package/lib/cad_connector/freecad.js +8 -0
  11. package/lib/cad_connector/fusion360-executor.js +398 -0
  12. package/lib/cad_connector/fusion360.js +8 -0
  13. package/lib/cad_connector/gstarcad3d.js +8 -0
  14. package/lib/cad_connector/index.js +20 -0
  15. package/lib/cad_connector/onshape.js +8 -0
  16. package/lib/cad_connector/solidworks.js +8 -0
  17. package/lib/cad_connector/types.js +6 -0
  18. package/lib/cad_connector/zw3d.js +8 -0
  19. package/lib/client.js +4117 -0
  20. package/lib/client.js.map +7 -0
  21. package/lib/convert/dcprt.js +38 -0
  22. package/lib/convert/detect.js +22 -0
  23. package/lib/convert/dxf.js +124 -0
  24. package/lib/convert/index.js +70 -0
  25. package/lib/convert/obj.js +106 -0
  26. package/lib/convert/png.js +114 -0
  27. package/lib/convert/step-worker.mjs +61 -0
  28. package/lib/convert/step.js +66 -0
  29. package/lib/convert/stl.js +110 -0
  30. package/lib/convert/svg.js +101 -0
  31. package/lib/demo-bracket.brep +668 -0
  32. package/lib/demo-flange.brep +709 -0
  33. package/lib/demo-shaft.brep +516 -0
  34. package/lib/feature_script/dc_asm.js +17 -0
  35. package/lib/feature_script/dc_engineering.js +19 -0
  36. package/lib/feature_script/dc_prt.js +21 -0
  37. package/lib/feature_script/index.js +7 -0
  38. package/lib/image/profile.js +218 -0
  39. package/lib/index.js +77 -0
  40. package/lib/modeling/assembly.js +63 -0
  41. package/lib/modeling/bin-format.js +80 -0
  42. package/lib/modeling/bin-store.js +67 -0
  43. package/lib/modeling/client.js +117 -0
  44. package/lib/modeling/document.js +55 -0
  45. package/lib/modeling/drawing.js +324 -0
  46. package/lib/modeling/hlr.cjs +328 -0
  47. package/lib/modeling/modeling-worker.cjs +235 -0
  48. package/lib/modeling/occt-adapter.cjs +271 -0
  49. package/lib/routes.js +142 -0
  50. package/lib/store.js +68 -0
  51. package/lib/tools/cad-freecad.js +160 -0
  52. package/lib/tools/cad-fusion.js +138 -0
  53. package/lib/tools/cad-image.js +121 -0
  54. package/lib/tools/cad-info.js +92 -0
  55. package/lib/tools/cad-model.js +816 -0
  56. package/lib/tools/cad-view.js +112 -0
  57. package/lib/tools/util.js +25 -0
  58. package/lib/types/b64.d.ts +8 -0
  59. package/lib/types/cad/builtin.d.ts +3 -0
  60. package/lib/types/cad/freecad.d.ts +3 -0
  61. package/lib/types/cad/fusion360.d.ts +3 -0
  62. package/lib/types/cad/gstarcad3d.d.ts +3 -0
  63. package/lib/types/cad/index.d.ts +8 -0
  64. package/lib/types/cad/onshape.d.ts +3 -0
  65. package/lib/types/cad/solidworks.d.ts +3 -0
  66. package/lib/types/cad/types.d.ts +21 -0
  67. package/lib/types/cad/zw3d.d.ts +3 -0
  68. package/lib/types/cad_connector/builtin-executor.d.ts +9 -0
  69. package/lib/types/cad_connector/builtin.d.ts +3 -0
  70. package/lib/types/cad_connector/executor.d.ts +69 -0
  71. package/lib/types/cad_connector/freecad-executor.d.ts +55 -0
  72. package/lib/types/cad_connector/freecad.d.ts +3 -0
  73. package/lib/types/cad_connector/fusion360-executor.d.ts +11 -0
  74. package/lib/types/cad_connector/fusion360.d.ts +3 -0
  75. package/lib/types/cad_connector/gstarcad3d.d.ts +3 -0
  76. package/lib/types/cad_connector/index.d.ts +8 -0
  77. package/lib/types/cad_connector/onshape.d.ts +3 -0
  78. package/lib/types/cad_connector/solidworks.d.ts +3 -0
  79. package/lib/types/cad_connector/types.d.ts +21 -0
  80. package/lib/types/cad_connector/zw3d.d.ts +3 -0
  81. package/lib/types/convert/dcprt.d.ts +14 -0
  82. package/lib/types/convert/detect.d.ts +7 -0
  83. package/lib/types/convert/dxf.d.ts +3 -0
  84. package/lib/types/convert/index.d.ts +9 -0
  85. package/lib/types/convert/obj.d.ts +7 -0
  86. package/lib/types/convert/png.d.ts +8 -0
  87. package/lib/types/convert/step.d.ts +4 -0
  88. package/lib/types/convert/stl.d.ts +7 -0
  89. package/lib/types/convert/svg.d.ts +8 -0
  90. package/lib/types/feature_script/dc_asm.d.ts +40 -0
  91. package/lib/types/feature_script/dc_engineering.d.ts +55 -0
  92. package/lib/types/feature_script/dc_prt.d.ts +37 -0
  93. package/lib/types/feature_script/index.d.ts +7 -0
  94. package/lib/types/image/profile.d.ts +28 -0
  95. package/lib/types/index.d.ts +18 -0
  96. package/lib/types/modeling/assembly.d.ts +17 -0
  97. package/lib/types/modeling/bin-format.d.ts +42 -0
  98. package/lib/types/modeling/bin-store.d.ts +20 -0
  99. package/lib/types/modeling/client.d.ts +125 -0
  100. package/lib/types/modeling/document.d.ts +26 -0
  101. package/lib/types/modeling/drawing.d.ts +38 -0
  102. package/lib/types/routes.d.ts +27 -0
  103. package/lib/types/store.d.ts +18 -0
  104. package/lib/types/tools/cad-freecad.d.ts +10 -0
  105. package/lib/types/tools/cad-fusion.d.ts +8 -0
  106. package/lib/types/tools/cad-image.d.ts +8 -0
  107. package/lib/types/tools/cad-info.d.ts +6 -0
  108. package/lib/types/tools/cad-model.d.ts +13 -0
  109. package/lib/types/tools/cad-view.d.ts +10 -0
  110. package/lib/types/tools/util.d.ts +4 -0
  111. package/lib/types/types.d.ts +142 -0
  112. package/lib/types.js +25 -0
  113. package/package.json +91 -0
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Fusion 360 external executor.
3
+ *
4
+ * Fusion has no headless mode: model-interacting code must run inside the
5
+ * application, and the documented automation path is a resident add-in that
6
+ * auto-loads at startup. So this executor is a GUI bridge, structurally the
7
+ * same idea as the FreeCAD detached-GUI mode:
8
+ *
9
+ * 1. (once) install the DshCadBridge add-in into the user's Fusion API
10
+ * AddIns folder — it starts watching a spool directory at launch;
11
+ * 2. run(): write job.json into the spool (and spawn Fusion if it is not
12
+ * running);
13
+ * 3. the bridge picks the job up, executes the op program through the
14
+ * Fusion API (TemporaryBRep primitives/booleans, sketch extrude,
15
+ * MeshManager tessellation, STEP/STL export), writes result.json;
16
+ * 4. run() polls for the result and maps it onto the executor contract.
17
+ *
18
+ * The mesh currency is the same ExecutorMesh every backend produces, so the
19
+ * WebGL display layer never knows which engine ran.
20
+ */
21
+ import { spawn } from 'node:child_process';
22
+ import { existsSync, readdirSync } from 'node:fs';
23
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
24
+ import { homedir } from 'node:os';
25
+ import path from 'node:path';
26
+ const BRIDGE_ID = 'DshCadBridge';
27
+ const SPOOL_DIR = path.join(homedir(), '.dsh-cad', 'fusion-spool');
28
+ let probed;
29
+ function fusionAppCandidates() {
30
+ const apps = [
31
+ '/Applications/Fusion 360.app',
32
+ '/Applications/Autodesk Fusion.app',
33
+ '/Applications/Autodesk Fusion 360.app',
34
+ ];
35
+ // Windows: the Fusion launcher lives under LOCALAPPDATA\Autodesk\webdeploy\production\<hash>\Fusion360.exe
36
+ const localAppData = process.env.LOCALAPPDATA;
37
+ if (localAppData !== undefined) {
38
+ const production = path.join(localAppData, 'Autodesk', 'webdeploy', 'production');
39
+ if (existsSync(production)) {
40
+ try {
41
+ for (const version of readdirSync(production)) {
42
+ apps.push(path.join(production, version, 'Fusion360.exe'));
43
+ }
44
+ }
45
+ catch {
46
+ // unreadable — fall through to the probe miss
47
+ }
48
+ }
49
+ }
50
+ return apps;
51
+ }
52
+ /** Locate a Fusion 360 application bundle/binary, or null when absent. */
53
+ export function findFusion360() {
54
+ if (probed === undefined) {
55
+ probed = fusionAppCandidates().find((candidate) => existsSync(candidate)) ?? null;
56
+ }
57
+ return probed;
58
+ }
59
+ /** Whether the Fusion executor is usable on this machine. */
60
+ export function fusion360Available() {
61
+ return findFusion360() !== null;
62
+ }
63
+ /** The user-level add-in folder Fusion auto-loads at startup. */
64
+ function addInsDirectory() {
65
+ if (process.platform === 'win32') {
66
+ return path.join(process.env.APPDATA ?? path.join(homedir(), 'AppData', 'Roaming'), 'Autodesk', 'Autodesk Fusion 360', 'API', 'AddIns');
67
+ }
68
+ return path.join(homedir(), 'Library', 'Application Support', 'Autodesk', 'Autodesk Fusion 360', 'API', 'AddIns');
69
+ }
70
+ /** Idempotently install the resident bridge add-in next to its manifest. */
71
+ export async function installFusionBridge() {
72
+ const directory = path.join(addInsDirectory(), BRIDGE_ID);
73
+ await mkdir(directory, { recursive: true });
74
+ await writeFile(path.join(directory, `${BRIDGE_ID}.manifest`), JSON.stringify({
75
+ id: BRIDGE_ID,
76
+ name: 'dsh-cad Bridge',
77
+ author: 'dsh-cad',
78
+ description: 'Executes dsh-cad op programs from the local spool directory.',
79
+ }, null, 2));
80
+ await writeFile(path.join(directory, `${BRIDGE_ID}.py`), BRIDGE_ADDIN);
81
+ return directory;
82
+ }
83
+ /** Launch Fusion detached (the bridge add-in auto-loads with it). */
84
+ function launchFusion(app) {
85
+ const child = process.platform === 'win32'
86
+ ? spawn(app, ['--hidden'], { detached: true, stdio: 'ignore', windowsHide: false })
87
+ : spawn('open', ['-a', app], { detached: true, stdio: 'ignore' });
88
+ child.unref();
89
+ }
90
+ /** Write a job, make sure Fusion is running, poll for the bridge result. */
91
+ export async function runFusionProgram(program, options = {}) {
92
+ const timeoutMs = options.timeoutMs ?? 300_000;
93
+ const app = findFusion360();
94
+ if (app === null) {
95
+ throw new Error('Fusion 360 was not found — install Fusion (macOS: /Applications) and try again');
96
+ }
97
+ await installFusionBridge();
98
+ await mkdir(SPOOL_DIR, { recursive: true });
99
+ const jobId = `job-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
100
+ const jobPath = path.join(SPOOL_DIR, `${jobId}.json`);
101
+ const runningPath = path.join(SPOOL_DIR, `${jobId}.running`);
102
+ const resultPath = path.join(SPOOL_DIR, `${jobId}.result.json`);
103
+ await writeFile(jobPath, JSON.stringify({ id: jobId, program }));
104
+ launchFusion(app);
105
+ const deadline = Date.now() + timeoutMs;
106
+ while (Date.now() < deadline) {
107
+ await new Promise((resolve) => setTimeout(resolve, 900));
108
+ if (existsSync(resultPath)) {
109
+ await rm(runningPath, { force: true }).catch(() => undefined);
110
+ await rm(jobPath, { force: true }).catch(() => undefined);
111
+ const parsed = JSON.parse(await readFile(resultPath, 'utf8'));
112
+ await rm(resultPath, { force: true }).catch(() => undefined);
113
+ return mapBridgeResult(parsed);
114
+ }
115
+ // Mark the job running on first sight so stale spool files are visible.
116
+ if (existsSync(jobPath))
117
+ await rename(jobPath, runningPath).catch(() => undefined);
118
+ }
119
+ throw new Error('the Fusion bridge produced no result before the timeout — is Fusion signed in and the DshCadBridge add-in enabled?');
120
+ }
121
+ function mapBridgeResult(parsed) {
122
+ if (!parsed.ok) {
123
+ throw new Error(`Fusion bridge failed: ${parsed.error ?? 'unknown error'}`);
124
+ }
125
+ return {
126
+ meshes: (parsed.meshes ?? []).map((mesh) => ({
127
+ bodyId: mesh.bodyId,
128
+ name: mesh.name === '' ? mesh.bodyId : mesh.name,
129
+ positions: Float32Array.from(mesh.positions),
130
+ normals: Float32Array.from(mesh.normals),
131
+ indices: Uint32Array.from(mesh.indices),
132
+ vertexCount: mesh.vertexCount,
133
+ triangleCount: mesh.triangleCount,
134
+ })),
135
+ volumes: parsed.volumes ?? {},
136
+ exported: parsed.exported,
137
+ };
138
+ }
139
+ /** The GeometryExecutor contract over the Fusion GUI bridge. */
140
+ export const FUSION360_EXECUTOR = {
141
+ id: 'fusion360',
142
+ label: 'Fusion 360',
143
+ available: fusion360Available,
144
+ unavailableReason: () => 'Fusion 360 was not found — install Fusion (native on Apple Silicon) and try again',
145
+ run: runFusionProgram,
146
+ };
147
+ /**
148
+ * The resident add-in auto-loaded by Fusion. Watches the spool directory on a
149
+ * timer, executes one job at a time through the Fusion API, writes the result
150
+ * next to the job. Ops mirror the FreeCAD bridge: TemporaryBRep primitives +
151
+ * booleans + transforms; sketch-based extrusion; MeshManager tessellation;
152
+ * STEP/STL export via the ExportManager.
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()))
398
+ `;
@@ -0,0 +1,8 @@
1
+ export const FUSION360_CONNECTOR = {
2
+ id: 'fusion360',
3
+ label: 'Fusion 360',
4
+ vendor: 'Autodesk',
5
+ language: 'python',
6
+ status: 'experimental',
7
+ binding: 'Resident add-in (auto-loaded) + spool-directory job bridge — no headless mode, the Fusion window doubles as a viewer; native on Apple Silicon macOS and Windows',
8
+ };
@@ -0,0 +1,8 @@
1
+ export const GSTARCAD3D_CONNECTOR = {
2
+ id: 'gstarcad3d',
3
+ label: 'GstarCAD 3D(浩辰3D)',
4
+ vendor: 'Gstarsoft',
5
+ language: 'sdk',
6
+ status: 'planned',
7
+ binding: 'official secondary-development SDK (per Gstarsoft docs)',
8
+ };
@@ -0,0 +1,20 @@
1
+ import { BUILTIN_CONNECTOR } from './builtin.js';
2
+ import { FREECAD_CONNECTOR } from './freecad.js';
3
+ import { SOLIDWORKS_CONNECTOR } from './solidworks.js';
4
+ import { FUSION360_CONNECTOR } from './fusion360.js';
5
+ import { ONSHAPE_CONNECTOR } from './onshape.js';
6
+ import { ZW3D_CONNECTOR } from './zw3d.js';
7
+ import { GSTARCAD3D_CONNECTOR } from './gstarcad3d.js';
8
+ export const CONNECTORS = [
9
+ BUILTIN_CONNECTOR,
10
+ FREECAD_CONNECTOR,
11
+ SOLIDWORKS_CONNECTOR,
12
+ FUSION360_CONNECTOR,
13
+ ONSHAPE_CONNECTOR,
14
+ ZW3D_CONNECTOR,
15
+ GSTARCAD3D_CONNECTOR,
16
+ ];
17
+ export function connectorById(id) {
18
+ return CONNECTORS.find((connector) => connector.id === id);
19
+ }
20
+ export * from './types.js';
@@ -0,0 +1,8 @@
1
+ export const ONSHAPE_CONNECTOR = {
2
+ id: 'onshape',
3
+ label: 'Onshape',
4
+ vendor: 'PTC',
5
+ language: 'rest',
6
+ status: 'planned',
7
+ binding: 'REST API for documents/geometry; FeatureScript for custom features',
8
+ };
@@ -0,0 +1,8 @@
1
+ export const SOLIDWORKS_CONNECTOR = {
2
+ id: 'solidworks',
3
+ label: 'SolidWorks',
4
+ vendor: 'Dassault Systèmes',
5
+ language: 'csharp',
6
+ status: 'planned',
7
+ binding: 'COM/.NET bridge over SldWorks.Application (Windows only) — demo scaffold in scripts/solidworks-bridge/, remote-REST form for mac/linux hosts',
8
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Connector contracts: how dsh-cad describes the CAD engines it can drive.
3
+ * The built-in WebGL/OCCT kernel is one entry; external engines (FreeCAD,
4
+ * SolidWorks, …) are executors reached through the language declared here.
5
+ */
6
+ export {};
@@ -0,0 +1,8 @@
1
+ export const ZW3D_CONNECTOR = {
2
+ id: 'zw3d',
3
+ label: 'ZW3D(中望3D)',
4
+ vendor: 'ZWSOFT',
5
+ language: 'sdk',
6
+ status: 'planned',
7
+ binding: 'official secondary-development SDK (per ZW3D docs)',
8
+ };