Source code for dazpy._property

from __future__ import annotations

import json

from ._element import DazElement
from ._script_builder import ScriptBuilder


[docs] class DazProperty(DazElement): """Proxy for a ``DzProperty`` on any ``DzElement``. Provides typed read/write access to a single named property, including keyframe support for animated properties. Args: client: The :class:`~dazpy.DazClient` for remote calls. owner_locator: JavaScript expression that evaluates to the owning ``DzElement``. property_label: The ``getLabel()`` string of the property. """ def __init__(self, client: "DazClient", owner_locator: str, property_label: str): # noqa: F821 locator = ( f"(function(){{" f"var obj = {owner_locator};" f"return obj ? obj.findPropertyByLabel({json.dumps(property_label)}) : null;" f"}})()" ) super().__init__(client, locator) object.__setattr__(self, "_owner_locator", owner_locator) object.__setattr__(self, "_property_label", property_label) @classmethod def _from_locator(cls, client: "DazClient", locator: str) -> "DazProperty": # noqa: F821 """Construct a DazProperty from a pre-built JavaScript locator expression.""" from ._element import DazElement prop = object.__new__(cls) DazElement.__init__(prop, client, locator) return prop @property def value(self) -> object: """Current property value (read/write).""" script = ScriptBuilder.iife( f"var p = {self._locator}; return p ? p.getValue() : null;" ) return self._client.execute(script).value @value.setter def value(self, v: object) -> None: serialized = ScriptBuilder.serialize_arg(v) script = ScriptBuilder.iife( f"var p = {self._locator}; if (p) p.setValue({serialized});" ) self._client.execute(script) @property def raw_value(self) -> object: """This property's own dial value, excluding any DzERCLink contributions. A property driven by one or more ``DzERCLink`` controllers (for example a character's "Scale" dial fed by dozens of linked morphs) computes :attr:`value` as ``raw_value`` plus every controller's contribution. Reading and writing :attr:`value` in a save/restore round trip is therefore *not* idempotent for such properties: the captured (post-ERC) total gets written back into the raw slot on restore, and the ERC links add their contribution again on top, inflating the property a little more on every cycle. Use ``raw_value`` instead of :attr:`value` whenever you need a snapshot/restore round trip to be exact — it reads and writes only the property's own baseline and is unaffected by ERC links either way. For properties without ERC links, ``raw_value`` and :attr:`value` are identical. Falls back to :attr:`value` for property types that don't expose a raw accessor (e.g. non-numeric properties). """ script = ScriptBuilder.iife(f""" var p = {self._locator}; if (!p) return null; return (typeof p.getRawValue === "function") ? p.getRawValue() : p.getValue(); """) return self._client.execute(script).value @raw_value.setter def raw_value(self, v: object) -> None: serialized = ScriptBuilder.serialize_arg(v) script = ScriptBuilder.iife(f""" var p = {self._locator}; if (!p) return; if (typeof p.setRawValue === "function") {{ p.setRawValue({serialized}); }} else {{ p.setValue({serialized}); }} """) self._client.execute(script) @property def label(self) -> str | None: """The display label of this property (read-only).""" script = ScriptBuilder.iife( f"var p = {self._locator}; return p ? p.getLabel() : null;" ) return self._client.execute(script).value @property def min(self) -> float | None: """Minimum allowed value (read-only; ``None`` if not applicable).""" script = ScriptBuilder.iife( f"var p = {self._locator}; return (p && p.getMin) ? p.getMin() : null;" ) return self._client.execute(script).value @property def max(self) -> float | None: """Maximum allowed value (read-only; ``None`` if not applicable).""" script = ScriptBuilder.iife( f"var p = {self._locator}; return (p && p.getMax) ? p.getMax() : null;" ) return self._client.execute(script).value
[docs] def set_key(self, time: float, value: float) -> None: """Set a keyframe for this (numeric) property. Writes via ``DzNumericProperty.setDoubleValue(tm, val)`` — the DazScript keyframe surface confirmed live against a running DAZ Studio instance (see ``dazpy.cinematics.apply_animated_shot``, which uses the same call). ``DzProperty.setKey()``/``addKey()``, which earlier versions of this method called, do not exist on a live property and silently wrote nothing. Args: time: The keyframe time in DAZ ticks. value: The numeric property value at that keyframe. Non-numeric properties (color, string, enum, ...) are not supported — this is a no-op for a property without ``setDoubleValue``. """ script = ScriptBuilder.iife(f""" var p = {self._locator}; if (p && p.setDoubleValue) p.setDoubleValue({time}, {float(value)}); """) self._client.execute(script)
@property def is_animated(self) -> bool | None: """``True`` if this property has keyframe animation data (read-only). Implemented as ``getNumKeys() > 0`` — ``DzProperty.isAnimated()``, which this property called previously, does not exist on a live property and raised a ``ScriptRuntimeError`` on every access. """ script = ScriptBuilder.iife(f""" var p = {self._locator}; if (!p || !p.getNumKeys) return null; return p.getNumKeys() > 0; """) return self._client.execute(script).value
[docs] def get_keys(self) -> list[dict]: """List all keyframes on this property's animation curve. Returns: A list of ``{"time": float, "value": object}`` dicts, one per keyframe, ordered by time. Empty list if the property has no keys or doesn't support keyframing. """ script = ScriptBuilder.iife(f""" var p = {self._locator}; if (!p || !p.getNumKeys) return []; var n = p.getNumKeys(); var keys = []; for (var i = 0; i < n; i++) {{ var t = p.getKeyTime(i); keys.push({{ time: t.valueOf(), value: p.getDoubleValue(t) }}); }} return keys; """) return self._client.execute(script).value
[docs] def remove_key(self, time: float) -> None: """Remove a single keyframe at the given time. Args: time: The keyframe time in DAZ ticks. If no key exists at exactly this time, this is a no-op. """ script = ScriptBuilder.iife(f""" var p = {self._locator}; if (p && p.deleteKeys) p.deleteKeys(new DzTimeRange({time}, {time})); """) self._client.execute(script)
[docs] def clear_keys(self) -> None: """Remove all keyframes from this property's animation curve.""" script = ScriptBuilder.iife(f""" var p = {self._locator}; if (p && p.deleteAllKeys) p.deleteAllKeys(); """) self._client.execute(script)