Source code for dazpy._pose

from __future__ import annotations

import json
from pathlib import Path
from typing import TYPE_CHECKING

from ._script_builder import ScriptBuilder

if TYPE_CHECKING:
    from ._skeleton import DazSkeleton


[docs] class DazPose: """A snapshot of a figure's complete pose state. Stores bone rotations (Euler XYZ degrees), geometry morph values, and node-level numeric properties. Sparse by default — zero values are omitted so the object stays compact for morph-heavy figures. Typical workflow:: from dazpy import DazScene, DazPose scene = DazScene() figure = scene.find_skeleton_by_label("Genesis 9") neutral = DazPose.capture(figure) neutral.save("neutral.json") smile = DazPose.load("smile.json") neutral.lerp(smile, t=0.5).apply(figure) JSON schema (same as ``character_state.py`` output):: { "figure": "Genesis 9", "bones": {"hip": [0, 2.3, 0], "rForeArm": [0, 0, -45]}, "morphs": {"PHMSmileFull": 0.8}, "props": {"facs_ctrl_SmileFullFace": 0.5} } Args: figure: The label of the figure this pose belongs to. bones: Bone name → ``[x, y, z]`` Euler angles in degrees. morphs: Morph name → blend value (typically 0–1). props: Node property name → numeric value. """ def __init__( self, figure: str, bones: dict[str, list[float]], morphs: dict[str, float], props: dict[str, float], ) -> None: self.figure = figure self.bones = bones self.morphs = morphs self.props = props # ── construction ──────────────────────────────────────────────────────────
[docs] @classmethod def capture(cls, skeleton: "DazSkeleton") -> "DazPose": """Capture the current pose of *skeleton* in a single HTTP call. Records all non-zero bone rotations, morph values, and node-level numeric properties. Zero values are omitted (sparse storage); they are implied as 0.0 during :meth:`lerp` and :meth:`apply_full`. Args: skeleton: The figure to capture. Returns: A new :class:`DazPose`. Raises: ~dazpy.exceptions.NodeNotFoundError: If the skeleton is not found. """ lookup = ScriptBuilder.skeleton_lookup(skeleton._identifier) script = f"""(function(){{ {lookup} if (!_skel) return null; var bones = {{}}; var all = _skel.getAllBones(); for (var i = 0; i < all.length; i++) {{ var b = all[i]; var x = b.getXRotControl().getValue(); var y = b.getYRotControl().getValue(); var z = b.getZRotControl().getValue(); if (Math.abs(x) > 0.0001 || Math.abs(y) > 0.0001 || Math.abs(z) > 0.0001) bones[b.getName()] = [x, y, z]; }} var morphs = {{}}; var obj = _skel.getObject(); if (obj) {{ for (var i = 0; i < obj.getNumModifiers(); i++) {{ var m = obj.getModifier(i); if (m.className() === "DzMorph") {{ var v = m.getValueChannel().getValue(); if (Math.abs(v) > 0.0001) morphs[m.getName()] = v; }} }} }} var props = {{}}; for (var i = 0; i < _skel.getNumProperties(); i++) {{ var p = _skel.getProperty(i); if (p && p.getValue) {{ var v = p.getValue(); if (typeof v === "number" && Math.abs(v) > 0.0001) props[p.getName()] = v; }} }} return {{bones: bones, morphs: morphs, props: props}}; }})()""" result = skeleton._client.execute(script).value if result is None: from .exceptions import NodeNotFoundError raise NodeNotFoundError(f"Skeleton not found: {skeleton._identifier.value!r}") return cls( figure=skeleton._identifier.value, bones=result["bones"], morphs=result["morphs"], props=result["props"], )
[docs] @classmethod def load(cls, path: str | Path) -> "DazPose": """Load a pose from a JSON file. Accepts files produced by :meth:`save` and by the ``character_state.py`` example script. Args: path: Path to the JSON file. Returns: A new :class:`DazPose`. """ with open(path, encoding="utf-8") as f: data = json.load(f) return cls( figure=data.get("figure", ""), bones=data.get("bones", {}), morphs=data.get("morphs", {}), props=data.get("props", {}), )
# ── serialisation ─────────────────────────────────────────────────────────
[docs] def save(self, path: str | Path) -> None: """Write this pose to a JSON file. Args: path: Destination path. Parent directories must exist. """ with open(path, "w", encoding="utf-8") as f: json.dump(self.to_dict(), f, indent=2)
[docs] def to_dict(self) -> dict: """Return the pose as a plain dict (same schema as the JSON file).""" return { "figure": self.figure, "bones": self.bones, "morphs": self.morphs, "props": self.props, }
# ── interpolation ─────────────────────────────────────────────────────────
[docs] def lerp(self, other: "DazPose", t: float) -> "DazPose": """Linearly interpolate between this pose and *other*. Missing keys in either pose are treated as zero. The result uses the figure label from *self*. This is a pure-Python operation — no HTTP round-trip. Args: other: The target pose (``t=1.0`` yields an exact copy of *other*). t: Blend factor. 0.0 = this pose, 1.0 = *other*. Values outside [0, 1] extrapolate. Returns: A new :class:`DazPose` at the interpolated position. """ def _l(a: float, b: float) -> float: return a + (b - a) * t bones = { k: [ _l(self.bones.get(k, _ZERO3)[i], other.bones.get(k, _ZERO3)[i]) for i in range(3) ] for k in set(self.bones) | set(other.bones) } morphs = { k: _l(self.morphs.get(k, 0.0), other.morphs.get(k, 0.0)) for k in set(self.morphs) | set(other.morphs) } props = { k: _l(self.props.get(k, 0.0), other.props.get(k, 0.0)) for k in set(self.props) | set(other.props) } return DazPose(figure=self.figure, bones=bones, morphs=morphs, props=props)
# ── apply ─────────────────────────────────────────────────────────────────
[docs] def apply(self, skeleton: "DazSkeleton") -> None: """Apply this pose to *skeleton* in a single HTTP call. Only channels present in the pose are changed. Bones and morphs not stored in the pose are left at their current values. Use :meth:`apply_full` when you need a clean, authoritative reset to exactly this pose. Args: skeleton: The figure to pose. """ lookup = ScriptBuilder.skeleton_lookup(skeleton._identifier) bones_json = json.dumps({k: [round(v, 6) for v in xyz] for k, xyz in self.bones.items()}) morphs_json = json.dumps({k: round(v, 6) for k, v in self.morphs.items()}) props_json = json.dumps({k: round(v, 6) for k, v in self.props.items()}) script = f"""(function(){{ {lookup} if (!_skel) return false; var _bones = {bones_json}; var _morphs = {morphs_json}; var _props = {props_json}; var all = _skel.getAllBones(); for (var i = 0; i < all.length; i++) {{ var b = all[i]; var xyz = _bones[b.getName()]; if (xyz !== undefined) {{ b.getXRotControl().setValue(xyz[0]); b.getYRotControl().setValue(xyz[1]); b.getZRotControl().setValue(xyz[2]); }} }} var obj = _skel.getObject(); if (obj) {{ for (var i = 0; i < obj.getNumModifiers(); i++) {{ var m = obj.getModifier(i); if (m.className() === "DzMorph") {{ var v = _morphs[m.getName()]; if (v !== undefined) m.getValueChannel().setValue(v); }} }} }} for (var i = 0; i < _skel.getNumProperties(); i++) {{ var p = _skel.getProperty(i); if (p && p.setValue) {{ var v = _props[p.getName()]; if (v !== undefined) p.setValue(v); }} }} return true; }})()""" skeleton._client.execute(script)
[docs] def apply_full(self, skeleton: "DazSkeleton") -> None: """Apply this pose and zero every channel not present in the pose. Unlike :meth:`apply`, every bone rotation, morph, and node property on the skeleton is explicitly set — channels absent from the pose are driven to zero. Use this to restore a known baseline cleanly. Args: skeleton: The figure to pose. """ lookup = ScriptBuilder.skeleton_lookup(skeleton._identifier) bones_json = json.dumps({k: [round(v, 6) for v in xyz] for k, xyz in self.bones.items()}) morphs_json = json.dumps({k: round(v, 6) for k, v in self.morphs.items()}) props_json = json.dumps({k: round(v, 6) for k, v in self.props.items()}) script = f"""(function(){{ {lookup} if (!_skel) return false; var _bones = {bones_json}; var _morphs = {morphs_json}; var _props = {props_json}; var all = _skel.getAllBones(); for (var i = 0; i < all.length; i++) {{ var b = all[i]; var xyz = _bones[b.getName()]; if (xyz !== undefined) {{ b.getXRotControl().setValue(xyz[0]); b.getYRotControl().setValue(xyz[1]); b.getZRotControl().setValue(xyz[2]); }} else {{ b.getXRotControl().setValue(0); b.getYRotControl().setValue(0); b.getZRotControl().setValue(0); }} }} var obj = _skel.getObject(); if (obj) {{ for (var i = 0; i < obj.getNumModifiers(); i++) {{ var m = obj.getModifier(i); if (m.className() === "DzMorph") {{ var v = _morphs[m.getName()]; m.getValueChannel().setValue(v !== undefined ? v : 0); }} }} }} for (var i = 0; i < _skel.getNumProperties(); i++) {{ var p = _skel.getProperty(i); if (p && p.setValue) {{ var v = _props[p.getName()]; if (v !== undefined) p.setValue(v); }} }} return true; }})()""" skeleton._client.execute(script)
# ── dunder ──────────────────────────────────────────────────────────────── def __repr__(self) -> str: return ( f"DazPose(figure={self.figure!r}, " f"bones={len(self.bones)}, morphs={len(self.morphs)}, props={len(self.props)})" )
_ZERO3 = [0.0, 0.0, 0.0]