diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-28 10:35:08 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-28 10:35:08 +0100 |
| commit | 1b811693373c20e48160cf2eca5e723727ec6ca5 (patch) | |
| tree | 51eaa145b5ae6303c308c13adda703b16301aeb6 /generat3d/scene.py | |
| parent | 2f037b14ee5fe799b19bfaf838c13dca649574e1 (diff) | |
| download | generat3D-1b811693373c20e48160cf2eca5e723727ec6ca5.tar.gz generat3D-1b811693373c20e48160cf2eca5e723727ec6ca5.zip | |
Harden scene loading: load each file once, fail loudly on malformed DSON
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Diffstat (limited to 'generat3d/scene.py')
| -rw-r--r-- | generat3d/scene.py | 45 |
1 files changed, 36 insertions, 9 deletions
diff --git a/generat3d/scene.py b/generat3d/scene.py index 49cc4c9..08ad4ee 100644 --- a/generat3d/scene.py +++ b/generat3d/scene.py @@ -77,12 +77,20 @@ def _lookup(library, doc, url, section): raise Error(f"{url}: no {section} entry {ident!r}") +def _number(url, v): + if isinstance(v, bool) or not isinstance(v, (int, float)): + raise Error(f"{url}: dial value is not a number") + return v + + class _Loader: def __init__(self, path, library): self.path = Path(path) self.library = library self.doc = read_dson(self.path) self.graph = Graph() + self.loaded = set() + self.hd = 0 def run(self): nodes = self.doc.get("scene", {}).get("nodes", []) @@ -97,9 +105,9 @@ class _Loader: posed = posed or any(v for ch in ("rotation", "translation") for v in _channel(n, ch)) elif kind != "figure": - warn(f"skipped scene node {n.get('name', n['id'])!r} ({kind or 'no type'})") + warn(f"skipped scene node {n.get('name') or n.get('id')!r} ({kind or 'no type'})") elif "conform_target" in n: - warn(f"skipped follower {n.get('name', n['id'])!r} (not handled in v1)") + warn(f"skipped follower {n.get('name') or n.get('id')!r} (not handled in v1)") else: figs.append((n, fpath, asset)) if not figs: @@ -112,6 +120,8 @@ class _Loader: posed = self._dials() or posed if posed: warn("pose in scene ignored: the .dbz holds rest data") + if self.hd: + warn(f"{self.hd} HD morphs: detail ignored (v1)") return Scene(self.path, self.figure, self.graph) def _figure(self, inst, fpath, asset): @@ -171,25 +181,39 @@ class _Loader: morph = mod.get("morph") if morph: if "hd_url" in morph: - warn(f"{path}#{mod['id']}: HD morph detail ignored") + self.hd += 1 d = morph.get("deltas", {}).get("values", []) if morph.get("vertex_count", nverts) != nverts: warn(f"{path}#{mod['id']}: vertex count mismatch, morph skipped") elif d: arr = np.array(d, dtype=float) + if (arr.ndim != 2 or arr.shape[1] != 4 + or (arr[:, 0] != np.floor(arr[:, 0])).any() + or (arr[:, 0] < 0).any() or (arr[:, 0] >= nverts).any()): + raise Error(f"{path}#{mod['id']}: bad morph deltas") self.figure.morphs[k] = (arr[:, 0].astype(int), arr[:, 1:4]) for f in mod.get("formulas", []): self._formula(f, f"{path}#{mod['id']}") + def _load_file(self, path): + if path.lower() in self.loaded: + return + self.loaded.add(path.lower()) + doc = self.library.load(path) + try: + self._modifiers(path, doc) + except (KeyError, TypeError, ValueError, IndexError, AttributeError) as e: + raise Error(f"{path}: malformed morph data: {e!r}") from None + def _morphs(self, fpath): morph_dir = posixpath.join(posixpath.dirname(fpath), "Morphs") for path in self.library.walk(morph_dir): try: - doc = self.library.load(path) + self.library.load(path) except Error as e: warn(f"skipped unreadable morph file: {e}") continue - self._modifiers(path, doc) + self._load_file(path) def _dials(self): """Apply .duf dial values. Returns True if the scene animates a pose channel.""" @@ -199,18 +223,18 @@ class _Loader: if "url" not in m: continue k = key(self.figure.id, parse_url(m["url"])[2], "value") - v = m.get("channel", {}).get("current_value", 0.0) + v = _number(m["url"], m.get("channel", {}).get("current_value", 0.0)) if k not in self.graph.props: path = parse_url(m["url"])[1] if path and self.library.find(path): - self._modifiers(path, self.library.load(path)) + self._load_file(path) if k in self.graph.props: values[k] = v elif v: missing.append(m["url"]) for a in scene.get("animations", []): channel = parse_url(a["url"])[3] - v = a.get("keys", [[0, 0.0]])[0][1] + v = _number(a["url"], a.get("keys", [[0, 0.0]])[0][1]) if channel.split("/")[0] in POSE_REST: posed = posed or bool(v) elif channel == "value": @@ -228,4 +252,7 @@ class _Loader: def load(path, library): """Load a .duf scene and everything its figure needs.""" - return _Loader(path, library).run() + try: + return _Loader(path, library).run() + except (KeyError, TypeError, IndexError, ValueError, AttributeError) as e: + raise Error(f"{path}: malformed scene data: {e!r}") from None |
