@forgent3d/cad-runtime 0.1.1 → 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.1",
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,12 +94,124 @@ def _write_stl(shape, path_out):
94
94
  raise RuntimeError(f"Unable to export STL: {exc}")
95
95
 
96
96
 
97
- def _write_glb(shape, path_out):
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):
98
209
  try:
99
210
  from build123d import export_gltf, Unit # type: ignore
100
211
  except Exception as exc:
101
212
  raise RuntimeError(f"build123d.export_gltf unavailable: {exc}")
102
213
  try:
214
+ _apply_viewer_colors(shape, params)
103
215
  export_gltf(shape, path_out, unit=Unit.MM, binary=True)
104
216
  return "build123d.export_gltf"
105
217
  except Exception as exc:
@@ -289,7 +401,7 @@ def build_one(model_name: str, part_name: str = None, export_format: str = "brep
289
401
  elif fmt == "step":
290
402
  method = _write_step(result, out)
291
403
  elif fmt == "glb":
292
- method = _write_glb(result, out)
404
+ method = _write_glb(result, out, _load_model_params(model_name))
293
405
  else:
294
406
  method = _write_stl(result, out)
295
407
  export_elapsed = time.perf_counter() - export_started