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.
- package/LICENSE +201 -0
- package/README.md +190 -0
- package/README.zh-CN.md +173 -0
- package/cordis.patch.yml +3 -0
- package/lib/b64.js +12 -0
- package/lib/cad_connector/builtin-executor.js +55 -0
- package/lib/cad_connector/builtin.js +8 -0
- package/lib/cad_connector/executor.js +76 -0
- package/lib/cad_connector/freecad-executor.js +398 -0
- package/lib/cad_connector/freecad.js +8 -0
- package/lib/cad_connector/fusion360-executor.js +398 -0
- package/lib/cad_connector/fusion360.js +8 -0
- package/lib/cad_connector/gstarcad3d.js +8 -0
- package/lib/cad_connector/index.js +20 -0
- package/lib/cad_connector/onshape.js +8 -0
- package/lib/cad_connector/solidworks.js +8 -0
- package/lib/cad_connector/types.js +6 -0
- package/lib/cad_connector/zw3d.js +8 -0
- package/lib/client.js +4117 -0
- package/lib/client.js.map +7 -0
- package/lib/convert/dcprt.js +38 -0
- package/lib/convert/detect.js +22 -0
- package/lib/convert/dxf.js +124 -0
- package/lib/convert/index.js +70 -0
- package/lib/convert/obj.js +106 -0
- package/lib/convert/png.js +114 -0
- package/lib/convert/step-worker.mjs +61 -0
- package/lib/convert/step.js +66 -0
- package/lib/convert/stl.js +110 -0
- package/lib/convert/svg.js +101 -0
- package/lib/demo-bracket.brep +668 -0
- package/lib/demo-flange.brep +709 -0
- package/lib/demo-shaft.brep +516 -0
- package/lib/feature_script/dc_asm.js +17 -0
- package/lib/feature_script/dc_engineering.js +19 -0
- package/lib/feature_script/dc_prt.js +21 -0
- package/lib/feature_script/index.js +7 -0
- package/lib/image/profile.js +218 -0
- package/lib/index.js +77 -0
- package/lib/modeling/assembly.js +63 -0
- package/lib/modeling/bin-format.js +80 -0
- package/lib/modeling/bin-store.js +67 -0
- package/lib/modeling/client.js +117 -0
- package/lib/modeling/document.js +55 -0
- package/lib/modeling/drawing.js +324 -0
- package/lib/modeling/hlr.cjs +328 -0
- package/lib/modeling/modeling-worker.cjs +235 -0
- package/lib/modeling/occt-adapter.cjs +271 -0
- package/lib/routes.js +142 -0
- package/lib/store.js +68 -0
- package/lib/tools/cad-freecad.js +160 -0
- package/lib/tools/cad-fusion.js +138 -0
- package/lib/tools/cad-image.js +121 -0
- package/lib/tools/cad-info.js +92 -0
- package/lib/tools/cad-model.js +816 -0
- package/lib/tools/cad-view.js +112 -0
- package/lib/tools/util.js +25 -0
- package/lib/types/b64.d.ts +8 -0
- package/lib/types/cad/builtin.d.ts +3 -0
- package/lib/types/cad/freecad.d.ts +3 -0
- package/lib/types/cad/fusion360.d.ts +3 -0
- package/lib/types/cad/gstarcad3d.d.ts +3 -0
- package/lib/types/cad/index.d.ts +8 -0
- package/lib/types/cad/onshape.d.ts +3 -0
- package/lib/types/cad/solidworks.d.ts +3 -0
- package/lib/types/cad/types.d.ts +21 -0
- package/lib/types/cad/zw3d.d.ts +3 -0
- package/lib/types/cad_connector/builtin-executor.d.ts +9 -0
- package/lib/types/cad_connector/builtin.d.ts +3 -0
- package/lib/types/cad_connector/executor.d.ts +69 -0
- package/lib/types/cad_connector/freecad-executor.d.ts +55 -0
- package/lib/types/cad_connector/freecad.d.ts +3 -0
- package/lib/types/cad_connector/fusion360-executor.d.ts +11 -0
- package/lib/types/cad_connector/fusion360.d.ts +3 -0
- package/lib/types/cad_connector/gstarcad3d.d.ts +3 -0
- package/lib/types/cad_connector/index.d.ts +8 -0
- package/lib/types/cad_connector/onshape.d.ts +3 -0
- package/lib/types/cad_connector/solidworks.d.ts +3 -0
- package/lib/types/cad_connector/types.d.ts +21 -0
- package/lib/types/cad_connector/zw3d.d.ts +3 -0
- package/lib/types/convert/dcprt.d.ts +14 -0
- package/lib/types/convert/detect.d.ts +7 -0
- package/lib/types/convert/dxf.d.ts +3 -0
- package/lib/types/convert/index.d.ts +9 -0
- package/lib/types/convert/obj.d.ts +7 -0
- package/lib/types/convert/png.d.ts +8 -0
- package/lib/types/convert/step.d.ts +4 -0
- package/lib/types/convert/stl.d.ts +7 -0
- package/lib/types/convert/svg.d.ts +8 -0
- package/lib/types/feature_script/dc_asm.d.ts +40 -0
- package/lib/types/feature_script/dc_engineering.d.ts +55 -0
- package/lib/types/feature_script/dc_prt.d.ts +37 -0
- package/lib/types/feature_script/index.d.ts +7 -0
- package/lib/types/image/profile.d.ts +28 -0
- package/lib/types/index.d.ts +18 -0
- package/lib/types/modeling/assembly.d.ts +17 -0
- package/lib/types/modeling/bin-format.d.ts +42 -0
- package/lib/types/modeling/bin-store.d.ts +20 -0
- package/lib/types/modeling/client.d.ts +125 -0
- package/lib/types/modeling/document.d.ts +26 -0
- package/lib/types/modeling/drawing.d.ts +38 -0
- package/lib/types/routes.d.ts +27 -0
- package/lib/types/store.d.ts +18 -0
- package/lib/types/tools/cad-freecad.d.ts +10 -0
- package/lib/types/tools/cad-fusion.d.ts +8 -0
- package/lib/types/tools/cad-image.d.ts +8 -0
- package/lib/types/tools/cad-info.d.ts +6 -0
- package/lib/types/tools/cad-model.d.ts +13 -0
- package/lib/types/tools/cad-view.d.ts +10 -0
- package/lib/types/tools/util.d.ts +4 -0
- package/lib/types/types.d.ts +142 -0
- package/lib/types.js +25 -0
- 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,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 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
|
+
};
|