@forgent3d/cad-runtime 0.1.2 → 0.1.4
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/README.md +1 -1
- 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 +263 -7
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ This package is intentionally runtime-light:
|
|
|
9
9
|
- no React
|
|
10
10
|
- no database clients
|
|
11
11
|
|
|
12
|
-
`
|
|
12
|
+
`@forgent3d/cloud` and `cf-sandbox` can both depend on this package without coupling their deploy targets together.
|
|
13
13
|
|
|
14
14
|
## Python Runtime Assets
|
|
15
15
|
|
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
|
|
@@ -27,6 +30,58 @@ MODELS_DIR = os.path.join(PROJECT_ROOT, "models")
|
|
|
27
30
|
CACHE_DIR = os.path.join(PROJECT_ROOT, ".cache")
|
|
28
31
|
MODEL_KINDS = ("part",)
|
|
29
32
|
|
|
33
|
+
# Sentinel-prefixed line emitted on stdout when --emit-build-json is passed, so a
|
|
34
|
+
# caller can read the build summary (bbox/anchors/hasResult) from this single run
|
|
35
|
+
# instead of a separate aicad-script build pass. Keep in sync with cf-sandbox rebuild.ts.
|
|
36
|
+
BUILD_JSON_SENTINEL = "@@AICAD_BUILD_JSON@@"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _emit_build_json(payload) -> None:
|
|
40
|
+
print(BUILD_JSON_SENTINEL + json.dumps(payload, ensure_ascii=False, separators=(",", ":")))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _bbox_summary(shape):
|
|
44
|
+
if shape is None or not hasattr(shape, "bounding_box"):
|
|
45
|
+
return None
|
|
46
|
+
try:
|
|
47
|
+
bb = shape.bounding_box()
|
|
48
|
+
return {
|
|
49
|
+
"min": [float(bb.min.X), float(bb.min.Y), float(bb.min.Z)],
|
|
50
|
+
"max": [float(bb.max.X), float(bb.max.Y), float(bb.max.Z)],
|
|
51
|
+
"size": [
|
|
52
|
+
float(bb.max.X - bb.min.X),
|
|
53
|
+
float(bb.max.Y - bb.min.Y),
|
|
54
|
+
float(bb.max.Z - bb.min.Z),
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
except Exception:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _build_summary_payload(model_name, part_name, source_path, ns, result):
|
|
62
|
+
metadata = ns.get("metadata") if isinstance(ns, dict) else None
|
|
63
|
+
anchors = None
|
|
64
|
+
metadata_keys = []
|
|
65
|
+
if isinstance(metadata, dict):
|
|
66
|
+
metadata_keys = sorted(str(k) for k in metadata.keys())
|
|
67
|
+
if isinstance(metadata.get("anchors"), dict):
|
|
68
|
+
try:
|
|
69
|
+
anchors = _json_safe(metadata.get("anchors"))
|
|
70
|
+
except Exception:
|
|
71
|
+
anchors = None
|
|
72
|
+
return {
|
|
73
|
+
"ok": result is not None,
|
|
74
|
+
"script": "build",
|
|
75
|
+
"model": model_name,
|
|
76
|
+
"part": part_name,
|
|
77
|
+
"source": os.path.relpath(source_path, PROJECT_ROOT).replace(os.sep, "/") if source_path else None,
|
|
78
|
+
"resultType": type(result).__name__ if result is not None else None,
|
|
79
|
+
"hasResult": result is not None,
|
|
80
|
+
"bbox": _bbox_summary(result),
|
|
81
|
+
"metadataKeys": metadata_keys,
|
|
82
|
+
"metadataAnchors": anchors,
|
|
83
|
+
}
|
|
84
|
+
|
|
30
85
|
|
|
31
86
|
def _looks_like_build123d(obj) -> bool:
|
|
32
87
|
return any(c.__module__.startswith("build123d") for c in type(obj).__mro__)
|
|
@@ -94,6 +149,159 @@ def _write_stl(shape, path_out):
|
|
|
94
149
|
raise RuntimeError(f"Unable to export STL: {exc}")
|
|
95
150
|
|
|
96
151
|
|
|
152
|
+
def _xml_escape_text(value):
|
|
153
|
+
return (
|
|
154
|
+
str(value if value is not None else "")
|
|
155
|
+
.replace("&", "&")
|
|
156
|
+
.replace("<", "<")
|
|
157
|
+
.replace(">", ">")
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _xml_escape_attr(value):
|
|
162
|
+
return _xml_escape_text(value).replace('"', """).replace("'", "'")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _format_3mf_number(value):
|
|
166
|
+
num = float(value)
|
|
167
|
+
if abs(num) < 1e-9:
|
|
168
|
+
num = 0.0
|
|
169
|
+
if num.is_integer():
|
|
170
|
+
return str(int(num))
|
|
171
|
+
return f"{num:.6f}".rstrip("0").rstrip(".") or "0"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _rgba_to_3mf_color(rgba):
|
|
175
|
+
if not rgba:
|
|
176
|
+
rgba = (0xb0 / 255, 0xb0 / 255, 0xb0 / 255, 1.0)
|
|
177
|
+
values = []
|
|
178
|
+
for i, fallback in enumerate((0.69, 0.69, 0.69, 1.0)):
|
|
179
|
+
try:
|
|
180
|
+
v = float(rgba[i])
|
|
181
|
+
except Exception:
|
|
182
|
+
v = fallback
|
|
183
|
+
values.append(max(0, min(255, round(max(0.0, min(1.0, v)) * 255))))
|
|
184
|
+
return "#" + "".join(f"{v:02X}" for v in values)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _read_stl_triangles(path_in):
|
|
188
|
+
with open(path_in, "rb") as f:
|
|
189
|
+
data = f.read()
|
|
190
|
+
if len(data) >= 84:
|
|
191
|
+
tri_count = struct.unpack("<I", data[80:84])[0]
|
|
192
|
+
expected = 84 + tri_count * 50
|
|
193
|
+
if expected == len(data):
|
|
194
|
+
triangles = []
|
|
195
|
+
offset = 84
|
|
196
|
+
for _ in range(tri_count):
|
|
197
|
+
values = struct.unpack("<12fH", data[offset:offset + 50])
|
|
198
|
+
triangles.append((
|
|
199
|
+
(values[3], values[4], values[5]),
|
|
200
|
+
(values[6], values[7], values[8]),
|
|
201
|
+
(values[9], values[10], values[11]),
|
|
202
|
+
))
|
|
203
|
+
offset += 50
|
|
204
|
+
return triangles
|
|
205
|
+
|
|
206
|
+
text = data.decode("utf-8", errors="ignore")
|
|
207
|
+
vertices = []
|
|
208
|
+
for line in text.splitlines():
|
|
209
|
+
parts = line.strip().split()
|
|
210
|
+
if len(parts) >= 4 and parts[0].lower() == "vertex":
|
|
211
|
+
try:
|
|
212
|
+
vertices.append((float(parts[1]), float(parts[2]), float(parts[3])))
|
|
213
|
+
except ValueError:
|
|
214
|
+
pass
|
|
215
|
+
if len(vertices) < 3 or len(vertices) % 3 != 0:
|
|
216
|
+
raise RuntimeError("Unable to parse STL triangles for mesh export")
|
|
217
|
+
return [(vertices[i], vertices[i + 1], vertices[i + 2]) for i in range(0, len(vertices), 3)]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _write_obj_mesh(triangles, path_out, title="Forgent3D model"):
|
|
221
|
+
if not triangles:
|
|
222
|
+
raise RuntimeError("OBJ export found no triangles")
|
|
223
|
+
with open(path_out, "w", encoding="utf-8", newline="\n") as f:
|
|
224
|
+
f.write("# Forgent3D OBJ export\n")
|
|
225
|
+
f.write(f"o {_xml_escape_text(title or 'model')}\n")
|
|
226
|
+
for tri in triangles:
|
|
227
|
+
for vertex in tri:
|
|
228
|
+
f.write(
|
|
229
|
+
f"v {_format_3mf_number(vertex[0])} "
|
|
230
|
+
f"{_format_3mf_number(vertex[1])} "
|
|
231
|
+
f"{_format_3mf_number(vertex[2])}\n"
|
|
232
|
+
)
|
|
233
|
+
for i in range(0, len(triangles) * 3, 3):
|
|
234
|
+
f.write(f"f {i + 1} {i + 2} {i + 3}\n")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _write_3mf_archive(triangles, path_out, title="Forgent3D model", color=None):
|
|
238
|
+
if not triangles:
|
|
239
|
+
raise RuntimeError("3MF export found no triangles")
|
|
240
|
+
display_color = _rgba_to_3mf_color(color)
|
|
241
|
+
vertices_xml = []
|
|
242
|
+
triangles_xml = []
|
|
243
|
+
vertex_index = 0
|
|
244
|
+
for tri in triangles:
|
|
245
|
+
indices = []
|
|
246
|
+
for vertex in tri:
|
|
247
|
+
indices.append(vertex_index)
|
|
248
|
+
vertices_xml.append(
|
|
249
|
+
f' <vertex x="{_format_3mf_number(vertex[0])}" '
|
|
250
|
+
f'y="{_format_3mf_number(vertex[1])}" '
|
|
251
|
+
f'z="{_format_3mf_number(vertex[2])}" />'
|
|
252
|
+
)
|
|
253
|
+
vertex_index += 1
|
|
254
|
+
triangles_xml.append(
|
|
255
|
+
f' <triangle v1="{indices[0]}" v2="{indices[1]}" v3="{indices[2]}" />'
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
safe_title = _xml_escape_text(str(title or "Forgent3D model")[:128])
|
|
259
|
+
model_xml = "\n".join([
|
|
260
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
261
|
+
'<model unit="millimeter" xml:lang="en-US" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">',
|
|
262
|
+
f' <metadata name="Title">{safe_title}</metadata>',
|
|
263
|
+
' <resources>',
|
|
264
|
+
' <basematerials id="1">',
|
|
265
|
+
f' <base name="Default" displaycolor="{display_color}" />',
|
|
266
|
+
' </basematerials>',
|
|
267
|
+
f' <object id="2" type="model" name="{_xml_escape_attr(title or "model")}" pid="1" pindex="0">',
|
|
268
|
+
' <mesh>',
|
|
269
|
+
' <vertices>',
|
|
270
|
+
*vertices_xml,
|
|
271
|
+
' </vertices>',
|
|
272
|
+
' <triangles>',
|
|
273
|
+
*triangles_xml,
|
|
274
|
+
' </triangles>',
|
|
275
|
+
' </mesh>',
|
|
276
|
+
' </object>',
|
|
277
|
+
' </resources>',
|
|
278
|
+
' <build>',
|
|
279
|
+
' <item objectid="2" />',
|
|
280
|
+
' </build>',
|
|
281
|
+
'</model>',
|
|
282
|
+
'',
|
|
283
|
+
])
|
|
284
|
+
content_types_xml = "\n".join([
|
|
285
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
286
|
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">',
|
|
287
|
+
' <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />',
|
|
288
|
+
' <Override PartName="/3D/3dmodel.model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml" />',
|
|
289
|
+
'</Types>',
|
|
290
|
+
'',
|
|
291
|
+
])
|
|
292
|
+
rels_xml = "\n".join([
|
|
293
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
294
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">',
|
|
295
|
+
' <Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel" />',
|
|
296
|
+
'</Relationships>',
|
|
297
|
+
'',
|
|
298
|
+
])
|
|
299
|
+
with zipfile.ZipFile(path_out, "w", compression=zipfile.ZIP_DEFLATED) as z:
|
|
300
|
+
z.writestr("[Content_Types].xml", content_types_xml)
|
|
301
|
+
z.writestr("_rels/.rels", rels_xml)
|
|
302
|
+
z.writestr("3D/3dmodel.model", model_xml)
|
|
303
|
+
|
|
304
|
+
|
|
97
305
|
# Keep in sync with packages/electron/src/viewer-materials.ts MATERIAL_PRESETS.
|
|
98
306
|
_VIEWER_PRESET_COLORS = {
|
|
99
307
|
"cad_clay": (0xc8 / 255, 0xd0 / 255, 0xdc / 255, 1.0),
|
|
@@ -218,6 +426,44 @@ def _write_glb(shape, path_out, params=None):
|
|
|
218
426
|
raise RuntimeError(f"Unable to export GLB: {exc}")
|
|
219
427
|
|
|
220
428
|
|
|
429
|
+
def _write_3mf(shape, path_out, params=None, title="Forgent3D model"):
|
|
430
|
+
fd, tmp_stl = tempfile.mkstemp(suffix=".stl")
|
|
431
|
+
os.close(fd)
|
|
432
|
+
try:
|
|
433
|
+
_write_stl(shape, tmp_stl)
|
|
434
|
+
triangles = _read_stl_triangles(tmp_stl)
|
|
435
|
+
finally:
|
|
436
|
+
try:
|
|
437
|
+
os.unlink(tmp_stl)
|
|
438
|
+
except OSError:
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
color = None
|
|
442
|
+
if isinstance(params, dict):
|
|
443
|
+
viewer = params.get("__viewer") or params.get("viewer") or {}
|
|
444
|
+
materials = viewer.get("materials") if isinstance(viewer, dict) else None
|
|
445
|
+
if isinstance(materials, dict):
|
|
446
|
+
color = _resolve_material_color(materials.get("default"))
|
|
447
|
+
_write_3mf_archive(triangles, path_out, title=title, color=color)
|
|
448
|
+
return "export_runner.3mf_from_stl_mesh"
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _write_obj(shape, path_out, title="Forgent3D model"):
|
|
452
|
+
fd, tmp_stl = tempfile.mkstemp(suffix=".stl")
|
|
453
|
+
os.close(fd)
|
|
454
|
+
try:
|
|
455
|
+
_write_stl(shape, tmp_stl)
|
|
456
|
+
triangles = _read_stl_triangles(tmp_stl)
|
|
457
|
+
finally:
|
|
458
|
+
try:
|
|
459
|
+
os.unlink(tmp_stl)
|
|
460
|
+
except OSError:
|
|
461
|
+
pass
|
|
462
|
+
|
|
463
|
+
_write_obj_mesh(triangles, path_out, title=title)
|
|
464
|
+
return "export_runner.obj_from_stl_mesh"
|
|
465
|
+
|
|
466
|
+
|
|
221
467
|
def _resolve_model_source(model_name: str, part_name: str, source_override: str = None):
|
|
222
468
|
if source_override:
|
|
223
469
|
candidate = source_override
|
|
@@ -358,7 +604,7 @@ def _build_namespace(model_name: str, part_name: str, source_override: str = Non
|
|
|
358
604
|
return ns, source_path, 0
|
|
359
605
|
|
|
360
606
|
|
|
361
|
-
def build_one(model_name: str, part_name: str = None, export_format: str = "brep", output: str = None, source_override: str = None) -> int:
|
|
607
|
+
def build_one(model_name: str, part_name: str = None, export_format: str = "brep", output: str = None, source_override: str = None, emit_build_json: bool = False) -> int:
|
|
362
608
|
part_name = part_name or model_name
|
|
363
609
|
build_started = time.perf_counter()
|
|
364
610
|
ns, source_path, err = _build_namespace(model_name, part_name, source_override)
|
|
@@ -372,6 +618,8 @@ def build_one(model_name: str, part_name: str = None, export_format: str = "brep
|
|
|
372
618
|
if candidate is not None:
|
|
373
619
|
result = candidate
|
|
374
620
|
if result is None:
|
|
621
|
+
if emit_build_json:
|
|
622
|
+
_emit_build_json(_build_summary_payload(model_name, part_name, source_path, ns, None))
|
|
375
623
|
print(f"[export_runner] {source_path} must define a global result (or assembly) object (build123d).",
|
|
376
624
|
file=sys.stderr)
|
|
377
625
|
return 4
|
|
@@ -381,9 +629,11 @@ def build_one(model_name: str, part_name: str = None, export_format: str = "brep
|
|
|
381
629
|
except Exception as exc:
|
|
382
630
|
print(f"[export_runner] Failed to write metadata.json: {exc}", file=sys.stderr)
|
|
383
631
|
return 8
|
|
632
|
+
if emit_build_json:
|
|
633
|
+
_emit_build_json(_build_summary_payload(model_name, part_name, source_path, ns, result))
|
|
384
634
|
|
|
385
635
|
fmt = (export_format or "brep").strip().lower()
|
|
386
|
-
if fmt not in ("brep", "step", "stl", "glb"):
|
|
636
|
+
if fmt not in ("brep", "step", "stl", "obj", "glb", "3mf"):
|
|
387
637
|
print(f"[export_runner] Unsupported export format: {fmt}", file=sys.stderr)
|
|
388
638
|
return 7
|
|
389
639
|
|
|
@@ -402,6 +652,10 @@ def build_one(model_name: str, part_name: str = None, export_format: str = "brep
|
|
|
402
652
|
method = _write_step(result, out)
|
|
403
653
|
elif fmt == "glb":
|
|
404
654
|
method = _write_glb(result, out, _load_model_params(model_name))
|
|
655
|
+
elif fmt == "obj":
|
|
656
|
+
method = _write_obj(result, out, model_name)
|
|
657
|
+
elif fmt == "3mf":
|
|
658
|
+
method = _write_3mf(result, out, _load_model_params(model_name), model_name)
|
|
405
659
|
else:
|
|
406
660
|
method = _write_stl(result, out)
|
|
407
661
|
export_elapsed = time.perf_counter() - export_started
|
|
@@ -425,8 +679,10 @@ def main() -> int:
|
|
|
425
679
|
parser.add_argument("--part", default=None, help="Legacy alias for --model")
|
|
426
680
|
parser.add_argument("--part-name", default=None, help="Part directory name inside models/<model>/parts/")
|
|
427
681
|
parser.add_argument("--source", default=None, help="Optional project-relative or absolute source file path (overrides default lookup)")
|
|
428
|
-
parser.add_argument("--export-format", default="brep", choices=["brep", "step", "stl", "glb"])
|
|
682
|
+
parser.add_argument("--export-format", default="brep", choices=["brep", "step", "stl", "obj", "glb", "3mf"])
|
|
429
683
|
parser.add_argument("--output", default=None, help="Optional absolute path for exported file")
|
|
684
|
+
parser.add_argument("--emit-build-json", action="store_true",
|
|
685
|
+
help="Emit a sentinel-prefixed build-summary JSON line on stdout (cloud rebuild fast path)")
|
|
430
686
|
args = parser.parse_args()
|
|
431
687
|
global PROJECT_ROOT, MODELS_DIR, CACHE_DIR
|
|
432
688
|
PROJECT_ROOT = os.path.abspath(args.project) if args.project else HERE
|
|
@@ -435,7 +691,7 @@ def main() -> int:
|
|
|
435
691
|
model_name = args.model or args.part
|
|
436
692
|
if not model_name:
|
|
437
693
|
parser.error("one of --model / --part is required")
|
|
438
|
-
return build_one(model_name, args.part_name or model_name, args.export_format, args.output, args.source)
|
|
694
|
+
return build_one(model_name, args.part_name or model_name, args.export_format, args.output, args.source, args.emit_build_json)
|
|
439
695
|
|
|
440
696
|
|
|
441
697
|
if __name__ == "__main__":
|