@forgent3d/cad-runtime 0.1.1 → 0.1.3
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/dist/index.cjs +1 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/python/__pycache__/export_runner.cpython-313.pyc +0 -0
- package/python/export_runner.py +317 -7
package/dist/index.cjs
CHANGED
|
@@ -31,8 +31,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
31
31
|
var SANDBOX_AUTH_SCHEME = "Bearer";
|
|
32
32
|
var SANDBOX_API_PATHS = {
|
|
33
33
|
resolve: "/v1/sandboxes/resolve",
|
|
34
|
-
runTool: "/v1/tools/run"
|
|
35
|
-
export: "/v1/export"
|
|
34
|
+
runTool: "/v1/tools/run"
|
|
36
35
|
};
|
|
37
36
|
function normalizeSandboxSecret(secret) {
|
|
38
37
|
const normalized = secret?.trim();
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/sandbox-protocol.ts"],"sourcesContent":["export {\n SANDBOX_API_PATHS,\n SANDBOX_AUTH_SCHEME,\n normalizeSandboxSecret,\n sandboxAuthorizationHeader,\n type SandboxApiPath,\n} from \"./sandbox-protocol.js\";\n","export const SANDBOX_AUTH_SCHEME = \"Bearer\";\n\nexport const SANDBOX_API_PATHS = {\n resolve: \"/v1/sandboxes/resolve\",\n runTool: \"/v1/tools/run\",\n
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/sandbox-protocol.ts"],"sourcesContent":["export {\n SANDBOX_API_PATHS,\n SANDBOX_AUTH_SCHEME,\n normalizeSandboxSecret,\n sandboxAuthorizationHeader,\n type SandboxApiPath,\n} from \"./sandbox-protocol.js\";\n","export const SANDBOX_AUTH_SCHEME = \"Bearer\";\n\nexport const SANDBOX_API_PATHS = {\n resolve: \"/v1/sandboxes/resolve\",\n runTool: \"/v1/tools/run\",\n} as const;\n\nexport type SandboxApiPath = (typeof SANDBOX_API_PATHS)[keyof typeof SANDBOX_API_PATHS];\n\nexport function normalizeSandboxSecret(secret: string | undefined): string | undefined {\n const normalized = secret?.trim();\n return normalized || undefined;\n}\n\nexport function sandboxAuthorizationHeader(secret: string | undefined): string | undefined {\n const normalized = normalizeSandboxSecret(secret);\n return normalized ? `${SANDBOX_AUTH_SCHEME} ${normalized}` : undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAO,IAAM,sBAAsB;AAE5B,IAAM,oBAAoB;AAAA,EAC/B,SAAS;AAAA,EACT,SAAS;AACX;AAIO,SAAS,uBAAuB,QAAgD;AACrF,QAAM,aAAa,QAAQ,KAAK;AAChC,SAAO,cAAc;AACvB;AAEO,SAAS,2BAA2B,QAAgD;AACzF,QAAM,aAAa,uBAAuB,MAAM;AAChD,SAAO,aAAa,GAAG,mBAAmB,IAAI,UAAU,KAAK;AAC/D;","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -2,7 +2,6 @@ declare const SANDBOX_AUTH_SCHEME = "Bearer";
|
|
|
2
2
|
declare const SANDBOX_API_PATHS: {
|
|
3
3
|
readonly resolve: "/v1/sandboxes/resolve";
|
|
4
4
|
readonly runTool: "/v1/tools/run";
|
|
5
|
-
readonly export: "/v1/export";
|
|
6
5
|
};
|
|
7
6
|
type SandboxApiPath = (typeof SANDBOX_API_PATHS)[keyof typeof SANDBOX_API_PATHS];
|
|
8
7
|
declare function normalizeSandboxSecret(secret: string | undefined): string | undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ declare const SANDBOX_AUTH_SCHEME = "Bearer";
|
|
|
2
2
|
declare const SANDBOX_API_PATHS: {
|
|
3
3
|
readonly resolve: "/v1/sandboxes/resolve";
|
|
4
4
|
readonly runTool: "/v1/tools/run";
|
|
5
|
-
readonly export: "/v1/export";
|
|
6
5
|
};
|
|
7
6
|
type SandboxApiPath = (typeof SANDBOX_API_PATHS)[keyof typeof SANDBOX_API_PATHS];
|
|
8
7
|
declare function normalizeSandboxSecret(secret: string | undefined): string | undefined;
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sandbox-protocol.ts"],"sourcesContent":["export const SANDBOX_AUTH_SCHEME = \"Bearer\";\n\nexport const SANDBOX_API_PATHS = {\n resolve: \"/v1/sandboxes/resolve\",\n runTool: \"/v1/tools/run\",\n
|
|
1
|
+
{"version":3,"sources":["../src/sandbox-protocol.ts"],"sourcesContent":["export const SANDBOX_AUTH_SCHEME = \"Bearer\";\n\nexport const SANDBOX_API_PATHS = {\n resolve: \"/v1/sandboxes/resolve\",\n runTool: \"/v1/tools/run\",\n} as const;\n\nexport type SandboxApiPath = (typeof SANDBOX_API_PATHS)[keyof typeof SANDBOX_API_PATHS];\n\nexport function normalizeSandboxSecret(secret: string | undefined): string | undefined {\n const normalized = secret?.trim();\n return normalized || undefined;\n}\n\nexport function sandboxAuthorizationHeader(secret: string | undefined): string | undefined {\n const normalized = normalizeSandboxSecret(secret);\n return normalized ? `${SANDBOX_AUTH_SCHEME} ${normalized}` : undefined;\n}\n"],"mappings":";AAAO,IAAM,sBAAsB;AAE5B,IAAM,oBAAoB;AAAA,EAC/B,SAAS;AAAA,EACT,SAAS;AACX;AAIO,SAAS,uBAAuB,QAAgD;AACrF,QAAM,aAAa,QAAQ,KAAK;AAChC,SAAO,cAAc;AACvB;AAEO,SAAS,2BAA2B,QAAgD;AACzF,QAAM,aAAa,uBAAuB,MAAM;AAChD,SAAO,aAAa,GAAG,mBAAmB,IAAI,UAAU,KAAK;AAC/D;","names":[]}
|
package/package.json
CHANGED
|
Binary file
|
package/python/export_runner.py
CHANGED
|
@@ -3,9 +3,9 @@ export_runner.py - Auto-managed by AI CAD Companion Viewer (do not edit manually
|
|
|
3
3
|
----------------------------------------------------------------------------
|
|
4
4
|
Responsibilities:
|
|
5
5
|
* Accept --model <name> (and legacy --part <name>);
|
|
6
|
-
* Optionally support on-demand exports via --export-format/--output (step/stl/brep);
|
|
7
|
-
* Load models/<model>/parts/<part-name>/part.py and read
|
|
8
|
-
* Export build123d geometry to .cache/<name>.brep
|
|
6
|
+
* Optionally support on-demand exports via --export-format/--output (step/stl/obj/brep/glb/3mf);
|
|
7
|
+
* Load models/<model>/assembly.py, part.py, or parts/<part-name>/part.py and read result;
|
|
8
|
+
* Export build123d geometry to .cache/<name>.brep and requested exchange/mesh formats;
|
|
9
9
|
* Let the frontend parse BREP via occt-import-js for geometry inspection.
|
|
10
10
|
"""
|
|
11
11
|
import os
|
|
@@ -18,8 +18,11 @@ if sys.platform == "win32":
|
|
|
18
18
|
|
|
19
19
|
import argparse
|
|
20
20
|
import json
|
|
21
|
+
import struct
|
|
22
|
+
import tempfile
|
|
21
23
|
import time
|
|
22
24
|
import traceback
|
|
25
|
+
import zipfile
|
|
23
26
|
|
|
24
27
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
25
28
|
PROJECT_ROOT = HERE
|
|
@@ -94,18 +97,321 @@ def _write_stl(shape, path_out):
|
|
|
94
97
|
raise RuntimeError(f"Unable to export STL: {exc}")
|
|
95
98
|
|
|
96
99
|
|
|
97
|
-
def
|
|
100
|
+
def _xml_escape_text(value):
|
|
101
|
+
return (
|
|
102
|
+
str(value if value is not None else "")
|
|
103
|
+
.replace("&", "&")
|
|
104
|
+
.replace("<", "<")
|
|
105
|
+
.replace(">", ">")
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _xml_escape_attr(value):
|
|
110
|
+
return _xml_escape_text(value).replace('"', """).replace("'", "'")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _format_3mf_number(value):
|
|
114
|
+
num = float(value)
|
|
115
|
+
if abs(num) < 1e-9:
|
|
116
|
+
num = 0.0
|
|
117
|
+
if num.is_integer():
|
|
118
|
+
return str(int(num))
|
|
119
|
+
return f"{num:.6f}".rstrip("0").rstrip(".") or "0"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _rgba_to_3mf_color(rgba):
|
|
123
|
+
if not rgba:
|
|
124
|
+
rgba = (0xb0 / 255, 0xb0 / 255, 0xb0 / 255, 1.0)
|
|
125
|
+
values = []
|
|
126
|
+
for i, fallback in enumerate((0.69, 0.69, 0.69, 1.0)):
|
|
127
|
+
try:
|
|
128
|
+
v = float(rgba[i])
|
|
129
|
+
except Exception:
|
|
130
|
+
v = fallback
|
|
131
|
+
values.append(max(0, min(255, round(max(0.0, min(1.0, v)) * 255))))
|
|
132
|
+
return "#" + "".join(f"{v:02X}" for v in values)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _read_stl_triangles(path_in):
|
|
136
|
+
with open(path_in, "rb") as f:
|
|
137
|
+
data = f.read()
|
|
138
|
+
if len(data) >= 84:
|
|
139
|
+
tri_count = struct.unpack("<I", data[80:84])[0]
|
|
140
|
+
expected = 84 + tri_count * 50
|
|
141
|
+
if expected == len(data):
|
|
142
|
+
triangles = []
|
|
143
|
+
offset = 84
|
|
144
|
+
for _ in range(tri_count):
|
|
145
|
+
values = struct.unpack("<12fH", data[offset:offset + 50])
|
|
146
|
+
triangles.append((
|
|
147
|
+
(values[3], values[4], values[5]),
|
|
148
|
+
(values[6], values[7], values[8]),
|
|
149
|
+
(values[9], values[10], values[11]),
|
|
150
|
+
))
|
|
151
|
+
offset += 50
|
|
152
|
+
return triangles
|
|
153
|
+
|
|
154
|
+
text = data.decode("utf-8", errors="ignore")
|
|
155
|
+
vertices = []
|
|
156
|
+
for line in text.splitlines():
|
|
157
|
+
parts = line.strip().split()
|
|
158
|
+
if len(parts) >= 4 and parts[0].lower() == "vertex":
|
|
159
|
+
try:
|
|
160
|
+
vertices.append((float(parts[1]), float(parts[2]), float(parts[3])))
|
|
161
|
+
except ValueError:
|
|
162
|
+
pass
|
|
163
|
+
if len(vertices) < 3 or len(vertices) % 3 != 0:
|
|
164
|
+
raise RuntimeError("Unable to parse STL triangles for mesh export")
|
|
165
|
+
return [(vertices[i], vertices[i + 1], vertices[i + 2]) for i in range(0, len(vertices), 3)]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _write_obj_mesh(triangles, path_out, title="Forgent3D model"):
|
|
169
|
+
if not triangles:
|
|
170
|
+
raise RuntimeError("OBJ export found no triangles")
|
|
171
|
+
with open(path_out, "w", encoding="utf-8", newline="\n") as f:
|
|
172
|
+
f.write("# Forgent3D OBJ export\n")
|
|
173
|
+
f.write(f"o {_xml_escape_text(title or 'model')}\n")
|
|
174
|
+
for tri in triangles:
|
|
175
|
+
for vertex in tri:
|
|
176
|
+
f.write(
|
|
177
|
+
f"v {_format_3mf_number(vertex[0])} "
|
|
178
|
+
f"{_format_3mf_number(vertex[1])} "
|
|
179
|
+
f"{_format_3mf_number(vertex[2])}\n"
|
|
180
|
+
)
|
|
181
|
+
for i in range(0, len(triangles) * 3, 3):
|
|
182
|
+
f.write(f"f {i + 1} {i + 2} {i + 3}\n")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _write_3mf_archive(triangles, path_out, title="Forgent3D model", color=None):
|
|
186
|
+
if not triangles:
|
|
187
|
+
raise RuntimeError("3MF export found no triangles")
|
|
188
|
+
display_color = _rgba_to_3mf_color(color)
|
|
189
|
+
vertices_xml = []
|
|
190
|
+
triangles_xml = []
|
|
191
|
+
vertex_index = 0
|
|
192
|
+
for tri in triangles:
|
|
193
|
+
indices = []
|
|
194
|
+
for vertex in tri:
|
|
195
|
+
indices.append(vertex_index)
|
|
196
|
+
vertices_xml.append(
|
|
197
|
+
f' <vertex x="{_format_3mf_number(vertex[0])}" '
|
|
198
|
+
f'y="{_format_3mf_number(vertex[1])}" '
|
|
199
|
+
f'z="{_format_3mf_number(vertex[2])}" />'
|
|
200
|
+
)
|
|
201
|
+
vertex_index += 1
|
|
202
|
+
triangles_xml.append(
|
|
203
|
+
f' <triangle v1="{indices[0]}" v2="{indices[1]}" v3="{indices[2]}" />'
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
safe_title = _xml_escape_text(str(title or "Forgent3D model")[:128])
|
|
207
|
+
model_xml = "\n".join([
|
|
208
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
209
|
+
'<model unit="millimeter" xml:lang="en-US" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">',
|
|
210
|
+
f' <metadata name="Title">{safe_title}</metadata>',
|
|
211
|
+
' <resources>',
|
|
212
|
+
' <basematerials id="1">',
|
|
213
|
+
f' <base name="Default" displaycolor="{display_color}" />',
|
|
214
|
+
' </basematerials>',
|
|
215
|
+
f' <object id="2" type="model" name="{_xml_escape_attr(title or "model")}" pid="1" pindex="0">',
|
|
216
|
+
' <mesh>',
|
|
217
|
+
' <vertices>',
|
|
218
|
+
*vertices_xml,
|
|
219
|
+
' </vertices>',
|
|
220
|
+
' <triangles>',
|
|
221
|
+
*triangles_xml,
|
|
222
|
+
' </triangles>',
|
|
223
|
+
' </mesh>',
|
|
224
|
+
' </object>',
|
|
225
|
+
' </resources>',
|
|
226
|
+
' <build>',
|
|
227
|
+
' <item objectid="2" />',
|
|
228
|
+
' </build>',
|
|
229
|
+
'</model>',
|
|
230
|
+
'',
|
|
231
|
+
])
|
|
232
|
+
content_types_xml = "\n".join([
|
|
233
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
234
|
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">',
|
|
235
|
+
' <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />',
|
|
236
|
+
' <Override PartName="/3D/3dmodel.model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml" />',
|
|
237
|
+
'</Types>',
|
|
238
|
+
'',
|
|
239
|
+
])
|
|
240
|
+
rels_xml = "\n".join([
|
|
241
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
242
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">',
|
|
243
|
+
' <Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel" />',
|
|
244
|
+
'</Relationships>',
|
|
245
|
+
'',
|
|
246
|
+
])
|
|
247
|
+
with zipfile.ZipFile(path_out, "w", compression=zipfile.ZIP_DEFLATED) as z:
|
|
248
|
+
z.writestr("[Content_Types].xml", content_types_xml)
|
|
249
|
+
z.writestr("_rels/.rels", rels_xml)
|
|
250
|
+
z.writestr("3D/3dmodel.model", model_xml)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# Keep in sync with packages/electron/src/viewer-materials.ts MATERIAL_PRESETS.
|
|
254
|
+
_VIEWER_PRESET_COLORS = {
|
|
255
|
+
"cad_clay": (0xc8 / 255, 0xd0 / 255, 0xdc / 255, 1.0),
|
|
256
|
+
"matte_plastic": (0xb9 / 255, 0xc2 / 255, 0xd0 / 255, 1.0),
|
|
257
|
+
"gloss_plastic": (0xb9 / 255, 0xc2 / 255, 0xd0 / 255, 1.0),
|
|
258
|
+
"painted_metal": (0x8f / 255, 0xa3 / 255, 0xb8 / 255, 1.0),
|
|
259
|
+
"anodized_aluminum":(0x4f / 255, 0x8f / 255, 0xd8 / 255, 1.0),
|
|
260
|
+
"brushed_steel": (0xa7 / 255, 0xb0 / 255, 0xba / 255, 1.0),
|
|
261
|
+
"dark_steel": (0x4b / 255, 0x55 / 255, 0x63 / 255, 1.0),
|
|
262
|
+
"polished_metal": (0xd0 / 255, 0xd6 / 255, 0xdc / 255, 1.0),
|
|
263
|
+
"rubber": (0x24 / 255, 0x28 / 255, 0x32 / 255, 1.0),
|
|
264
|
+
"glass_clear": (0xd8 / 255, 0xec / 255, 0xff / 255, 0.34),
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _parse_hex_color(text):
|
|
269
|
+
s = str(text or "").strip().lstrip("#")
|
|
270
|
+
if len(s) == 3:
|
|
271
|
+
s = "".join(ch * 2 for ch in s)
|
|
272
|
+
if len(s) not in (6, 8):
|
|
273
|
+
return None
|
|
274
|
+
try:
|
|
275
|
+
r = int(s[0:2], 16) / 255
|
|
276
|
+
g = int(s[2:4], 16) / 255
|
|
277
|
+
b = int(s[4:6], 16) / 255
|
|
278
|
+
a = (int(s[6:8], 16) / 255) if len(s) == 8 else 1.0
|
|
279
|
+
return (r, g, b, a)
|
|
280
|
+
except ValueError:
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _resolve_material_color(spec):
|
|
285
|
+
"""Resolve a __viewer.materials entry to an (r, g, b, a) tuple of floats."""
|
|
286
|
+
if spec is None:
|
|
287
|
+
return None
|
|
288
|
+
if isinstance(spec, bool):
|
|
289
|
+
return None
|
|
290
|
+
if isinstance(spec, int):
|
|
291
|
+
v = spec & 0xffffff
|
|
292
|
+
return (((v >> 16) & 0xff) / 255, ((v >> 8) & 0xff) / 255, (v & 0xff) / 255, 1.0)
|
|
293
|
+
if isinstance(spec, str):
|
|
294
|
+
text = spec.strip()
|
|
295
|
+
if text in _VIEWER_PRESET_COLORS:
|
|
296
|
+
return _VIEWER_PRESET_COLORS[text]
|
|
297
|
+
return _parse_hex_color(text)
|
|
298
|
+
if isinstance(spec, dict):
|
|
299
|
+
explicit = spec.get("color")
|
|
300
|
+
if explicit is not None:
|
|
301
|
+
resolved = _resolve_material_color(explicit)
|
|
302
|
+
if resolved is not None:
|
|
303
|
+
return resolved
|
|
304
|
+
preset_name = str(spec.get("preset") or spec.get("material") or "").strip()
|
|
305
|
+
if preset_name in _VIEWER_PRESET_COLORS:
|
|
306
|
+
return _VIEWER_PRESET_COLORS[preset_name]
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _load_model_params(model_name: str):
|
|
311
|
+
path = os.path.join(MODELS_DIR, model_name, "params.json")
|
|
312
|
+
if not os.path.isfile(path):
|
|
313
|
+
return None
|
|
314
|
+
try:
|
|
315
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
316
|
+
return json.load(f)
|
|
317
|
+
except Exception:
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _apply_viewer_colors(shape, params):
|
|
322
|
+
"""Stamp build123d .color from params.json __viewer.materials so export_gltf preserves it."""
|
|
323
|
+
if not isinstance(params, dict):
|
|
324
|
+
return
|
|
325
|
+
viewer = params.get("__viewer") or params.get("viewer") or {}
|
|
326
|
+
materials = viewer.get("materials") if isinstance(viewer, dict) else None
|
|
327
|
+
if not isinstance(materials, dict):
|
|
328
|
+
return
|
|
329
|
+
parts_spec = materials.get("parts") if isinstance(materials.get("parts"), dict) else {}
|
|
330
|
+
default_rgba = _resolve_material_color(materials.get("default"))
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
from build123d import Color # type: ignore
|
|
334
|
+
except Exception:
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
def _set(node, rgba):
|
|
338
|
+
if rgba is None:
|
|
339
|
+
return
|
|
340
|
+
try:
|
|
341
|
+
node.color = Color(float(rgba[0]), float(rgba[1]), float(rgba[2]), float(rgba[3]))
|
|
342
|
+
except Exception:
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
children = list(getattr(shape, "children", []) or [])
|
|
347
|
+
except TypeError:
|
|
348
|
+
children = []
|
|
349
|
+
|
|
350
|
+
if not children:
|
|
351
|
+
# Single-shape model: apply default color if configured.
|
|
352
|
+
_set(shape, default_rgba)
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
for child in children:
|
|
356
|
+
label = str(getattr(child, "label", "") or "").strip()
|
|
357
|
+
spec = parts_spec.get(label) if label else None
|
|
358
|
+
rgba = _resolve_material_color(spec) if spec is not None else None
|
|
359
|
+
if rgba is None:
|
|
360
|
+
rgba = default_rgba
|
|
361
|
+
_set(child, rgba)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _write_glb(shape, path_out, params=None):
|
|
98
365
|
try:
|
|
99
366
|
from build123d import export_gltf, Unit # type: ignore
|
|
100
367
|
except Exception as exc:
|
|
101
368
|
raise RuntimeError(f"build123d.export_gltf unavailable: {exc}")
|
|
102
369
|
try:
|
|
370
|
+
_apply_viewer_colors(shape, params)
|
|
103
371
|
export_gltf(shape, path_out, unit=Unit.MM, binary=True)
|
|
104
372
|
return "build123d.export_gltf"
|
|
105
373
|
except Exception as exc:
|
|
106
374
|
raise RuntimeError(f"Unable to export GLB: {exc}")
|
|
107
375
|
|
|
108
376
|
|
|
377
|
+
def _write_3mf(shape, path_out, params=None, title="Forgent3D model"):
|
|
378
|
+
fd, tmp_stl = tempfile.mkstemp(suffix=".stl")
|
|
379
|
+
os.close(fd)
|
|
380
|
+
try:
|
|
381
|
+
_write_stl(shape, tmp_stl)
|
|
382
|
+
triangles = _read_stl_triangles(tmp_stl)
|
|
383
|
+
finally:
|
|
384
|
+
try:
|
|
385
|
+
os.unlink(tmp_stl)
|
|
386
|
+
except OSError:
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
color = None
|
|
390
|
+
if isinstance(params, dict):
|
|
391
|
+
viewer = params.get("__viewer") or params.get("viewer") or {}
|
|
392
|
+
materials = viewer.get("materials") if isinstance(viewer, dict) else None
|
|
393
|
+
if isinstance(materials, dict):
|
|
394
|
+
color = _resolve_material_color(materials.get("default"))
|
|
395
|
+
_write_3mf_archive(triangles, path_out, title=title, color=color)
|
|
396
|
+
return "export_runner.3mf_from_stl_mesh"
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _write_obj(shape, path_out, title="Forgent3D model"):
|
|
400
|
+
fd, tmp_stl = tempfile.mkstemp(suffix=".stl")
|
|
401
|
+
os.close(fd)
|
|
402
|
+
try:
|
|
403
|
+
_write_stl(shape, tmp_stl)
|
|
404
|
+
triangles = _read_stl_triangles(tmp_stl)
|
|
405
|
+
finally:
|
|
406
|
+
try:
|
|
407
|
+
os.unlink(tmp_stl)
|
|
408
|
+
except OSError:
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
_write_obj_mesh(triangles, path_out, title=title)
|
|
412
|
+
return "export_runner.obj_from_stl_mesh"
|
|
413
|
+
|
|
414
|
+
|
|
109
415
|
def _resolve_model_source(model_name: str, part_name: str, source_override: str = None):
|
|
110
416
|
if source_override:
|
|
111
417
|
candidate = source_override
|
|
@@ -271,7 +577,7 @@ def build_one(model_name: str, part_name: str = None, export_format: str = "brep
|
|
|
271
577
|
return 8
|
|
272
578
|
|
|
273
579
|
fmt = (export_format or "brep").strip().lower()
|
|
274
|
-
if fmt not in ("brep", "step", "stl", "glb"):
|
|
580
|
+
if fmt not in ("brep", "step", "stl", "obj", "glb", "3mf"):
|
|
275
581
|
print(f"[export_runner] Unsupported export format: {fmt}", file=sys.stderr)
|
|
276
582
|
return 7
|
|
277
583
|
|
|
@@ -289,7 +595,11 @@ def build_one(model_name: str, part_name: str = None, export_format: str = "brep
|
|
|
289
595
|
elif fmt == "step":
|
|
290
596
|
method = _write_step(result, out)
|
|
291
597
|
elif fmt == "glb":
|
|
292
|
-
method = _write_glb(result, out)
|
|
598
|
+
method = _write_glb(result, out, _load_model_params(model_name))
|
|
599
|
+
elif fmt == "obj":
|
|
600
|
+
method = _write_obj(result, out, model_name)
|
|
601
|
+
elif fmt == "3mf":
|
|
602
|
+
method = _write_3mf(result, out, _load_model_params(model_name), model_name)
|
|
293
603
|
else:
|
|
294
604
|
method = _write_stl(result, out)
|
|
295
605
|
export_elapsed = time.perf_counter() - export_started
|
|
@@ -313,7 +623,7 @@ def main() -> int:
|
|
|
313
623
|
parser.add_argument("--part", default=None, help="Legacy alias for --model")
|
|
314
624
|
parser.add_argument("--part-name", default=None, help="Part directory name inside models/<model>/parts/")
|
|
315
625
|
parser.add_argument("--source", default=None, help="Optional project-relative or absolute source file path (overrides default lookup)")
|
|
316
|
-
parser.add_argument("--export-format", default="brep", choices=["brep", "step", "stl", "glb"])
|
|
626
|
+
parser.add_argument("--export-format", default="brep", choices=["brep", "step", "stl", "obj", "glb", "3mf"])
|
|
317
627
|
parser.add_argument("--output", default=None, help="Optional absolute path for exported file")
|
|
318
628
|
args = parser.parse_args()
|
|
319
629
|
global PROJECT_ROOT, MODELS_DIR, CACHE_DIR
|