@forgent3d/cad-runtime 0.1.0 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgent3d/cad-runtime",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Shared CAD runtime protocol, types, and pure utilities for Forgent3D cloud services.",
5
5
  "license": "MIT",
6
6
  "author": "Forgent3D",
@@ -94,6 +94,130 @@ def _write_stl(shape, path_out):
94
94
  raise RuntimeError(f"Unable to export STL: {exc}")
95
95
 
96
96
 
97
+ # Keep in sync with packages/electron/src/viewer-materials.ts MATERIAL_PRESETS.
98
+ _VIEWER_PRESET_COLORS = {
99
+ "cad_clay": (0xc8 / 255, 0xd0 / 255, 0xdc / 255, 1.0),
100
+ "matte_plastic": (0xb9 / 255, 0xc2 / 255, 0xd0 / 255, 1.0),
101
+ "gloss_plastic": (0xb9 / 255, 0xc2 / 255, 0xd0 / 255, 1.0),
102
+ "painted_metal": (0x8f / 255, 0xa3 / 255, 0xb8 / 255, 1.0),
103
+ "anodized_aluminum":(0x4f / 255, 0x8f / 255, 0xd8 / 255, 1.0),
104
+ "brushed_steel": (0xa7 / 255, 0xb0 / 255, 0xba / 255, 1.0),
105
+ "dark_steel": (0x4b / 255, 0x55 / 255, 0x63 / 255, 1.0),
106
+ "polished_metal": (0xd0 / 255, 0xd6 / 255, 0xdc / 255, 1.0),
107
+ "rubber": (0x24 / 255, 0x28 / 255, 0x32 / 255, 1.0),
108
+ "glass_clear": (0xd8 / 255, 0xec / 255, 0xff / 255, 0.34),
109
+ }
110
+
111
+
112
+ def _parse_hex_color(text):
113
+ s = str(text or "").strip().lstrip("#")
114
+ if len(s) == 3:
115
+ s = "".join(ch * 2 for ch in s)
116
+ if len(s) not in (6, 8):
117
+ return None
118
+ try:
119
+ r = int(s[0:2], 16) / 255
120
+ g = int(s[2:4], 16) / 255
121
+ b = int(s[4:6], 16) / 255
122
+ a = (int(s[6:8], 16) / 255) if len(s) == 8 else 1.0
123
+ return (r, g, b, a)
124
+ except ValueError:
125
+ return None
126
+
127
+
128
+ def _resolve_material_color(spec):
129
+ """Resolve a __viewer.materials entry to an (r, g, b, a) tuple of floats."""
130
+ if spec is None:
131
+ return None
132
+ if isinstance(spec, bool):
133
+ return None
134
+ if isinstance(spec, int):
135
+ v = spec & 0xffffff
136
+ return (((v >> 16) & 0xff) / 255, ((v >> 8) & 0xff) / 255, (v & 0xff) / 255, 1.0)
137
+ if isinstance(spec, str):
138
+ text = spec.strip()
139
+ if text in _VIEWER_PRESET_COLORS:
140
+ return _VIEWER_PRESET_COLORS[text]
141
+ return _parse_hex_color(text)
142
+ if isinstance(spec, dict):
143
+ explicit = spec.get("color")
144
+ if explicit is not None:
145
+ resolved = _resolve_material_color(explicit)
146
+ if resolved is not None:
147
+ return resolved
148
+ preset_name = str(spec.get("preset") or spec.get("material") or "").strip()
149
+ if preset_name in _VIEWER_PRESET_COLORS:
150
+ return _VIEWER_PRESET_COLORS[preset_name]
151
+ return None
152
+
153
+
154
+ def _load_model_params(model_name: str):
155
+ path = os.path.join(MODELS_DIR, model_name, "params.json")
156
+ if not os.path.isfile(path):
157
+ return None
158
+ try:
159
+ with open(path, "r", encoding="utf-8") as f:
160
+ return json.load(f)
161
+ except Exception:
162
+ return None
163
+
164
+
165
+ def _apply_viewer_colors(shape, params):
166
+ """Stamp build123d .color from params.json __viewer.materials so export_gltf preserves it."""
167
+ if not isinstance(params, dict):
168
+ return
169
+ viewer = params.get("__viewer") or params.get("viewer") or {}
170
+ materials = viewer.get("materials") if isinstance(viewer, dict) else None
171
+ if not isinstance(materials, dict):
172
+ return
173
+ parts_spec = materials.get("parts") if isinstance(materials.get("parts"), dict) else {}
174
+ default_rgba = _resolve_material_color(materials.get("default"))
175
+
176
+ try:
177
+ from build123d import Color # type: ignore
178
+ except Exception:
179
+ return
180
+
181
+ def _set(node, rgba):
182
+ if rgba is None:
183
+ return
184
+ try:
185
+ node.color = Color(float(rgba[0]), float(rgba[1]), float(rgba[2]), float(rgba[3]))
186
+ except Exception:
187
+ pass
188
+
189
+ try:
190
+ children = list(getattr(shape, "children", []) or [])
191
+ except TypeError:
192
+ children = []
193
+
194
+ if not children:
195
+ # Single-shape model: apply default color if configured.
196
+ _set(shape, default_rgba)
197
+ return
198
+
199
+ for child in children:
200
+ label = str(getattr(child, "label", "") or "").strip()
201
+ spec = parts_spec.get(label) if label else None
202
+ rgba = _resolve_material_color(spec) if spec is not None else None
203
+ if rgba is None:
204
+ rgba = default_rgba
205
+ _set(child, rgba)
206
+
207
+
208
+ def _write_glb(shape, path_out, params=None):
209
+ try:
210
+ from build123d import export_gltf, Unit # type: ignore
211
+ except Exception as exc:
212
+ raise RuntimeError(f"build123d.export_gltf unavailable: {exc}")
213
+ try:
214
+ _apply_viewer_colors(shape, params)
215
+ export_gltf(shape, path_out, unit=Unit.MM, binary=True)
216
+ return "build123d.export_gltf"
217
+ except Exception as exc:
218
+ raise RuntimeError(f"Unable to export GLB: {exc}")
219
+
220
+
97
221
  def _resolve_model_source(model_name: str, part_name: str, source_override: str = None):
98
222
  if source_override:
99
223
  candidate = source_override
@@ -259,7 +383,7 @@ def build_one(model_name: str, part_name: str = None, export_format: str = "brep
259
383
  return 8
260
384
 
261
385
  fmt = (export_format or "brep").strip().lower()
262
- if fmt not in ("brep", "step", "stl"):
386
+ if fmt not in ("brep", "step", "stl", "glb"):
263
387
  print(f"[export_runner] Unsupported export format: {fmt}", file=sys.stderr)
264
388
  return 7
265
389
 
@@ -276,6 +400,8 @@ def build_one(model_name: str, part_name: str = None, export_format: str = "brep
276
400
  method = _write_brep(result, out)
277
401
  elif fmt == "step":
278
402
  method = _write_step(result, out)
403
+ elif fmt == "glb":
404
+ method = _write_glb(result, out, _load_model_params(model_name))
279
405
  else:
280
406
  method = _write_stl(result, out)
281
407
  export_elapsed = time.perf_counter() - export_started
@@ -299,7 +425,7 @@ def main() -> int:
299
425
  parser.add_argument("--part", default=None, help="Legacy alias for --model")
300
426
  parser.add_argument("--part-name", default=None, help="Part directory name inside models/<model>/parts/")
301
427
  parser.add_argument("--source", default=None, help="Optional project-relative or absolute source file path (overrides default lookup)")
302
- parser.add_argument("--export-format", default="brep", choices=["brep", "step", "stl"])
428
+ parser.add_argument("--export-format", default="brep", choices=["brep", "step", "stl", "glb"])
303
429
  parser.add_argument("--output", default=None, help="Optional absolute path for exported file")
304
430
  args = parser.parse_args()
305
431
  global PROJECT_ROOT, MODELS_DIR, CACHE_DIR