Source code for dazpy._node

from __future__ import annotations

import json
from dataclasses import dataclass
from typing import TYPE_CHECKING

from ._element import DazElement
from ._script_builder import ScriptBuilder

if TYPE_CHECKING:
    from ._material import DazMaterial
    from ._modifier import DazModifier
    from ._morph import DazMorph


[docs] @dataclass class NodeIdentifier: """Identifies a scene node by name or label. Attributes: value: The name or label string used to look up the node. kind: Either ``"name"`` (uses ``Scene.findNode()``) or ``"label"`` (uses ``Scene.findNodeByLabel()``). """ value: str kind: str = "name" # "name" or "label"
[docs] class DazNode(DazElement): """Proxy for a ``DzNode`` in the active DAZ Studio scene. Provides access to transforms, hierarchy, visibility, materials, modifiers, and geometry. Instances are typically obtained from :class:`~dazpy.DazScene` rather than constructed directly. All properties that read from the server may return ``None`` if the node no longer exists in the scene. Args: client: The :class:`~dazpy.DazClient` for remote calls. identifier: Identifies the underlying ``DzNode``. """ def __init__(self, client: "DazClient", identifier: NodeIdentifier): # noqa: F821 locator = ScriptBuilder.find_node_expr(identifier) super().__init__(client, locator) object.__setattr__(self, "_identifier", identifier) @property def label(self) -> str | None: """User-visible display label shown in the Scene panel (read/write).""" script = ScriptBuilder.node_body(self._identifier, "return _node.getLabel();") return self._client.execute(script).value @label.setter def label(self, value: str) -> None: script = ScriptBuilder.node_body( self._identifier, f"_node.setLabel({json.dumps(value)});" ) self._client.execute(script) @property def name(self) -> str | None: """Internal node name used to look up the node (read-only).""" script = ScriptBuilder.node_body(self._identifier, "return _node.getName();") return self._client.execute(script).value @property def position(self) -> dict | None: """World-space position as ``{"x": float, "y": float, "z": float}`` (read-only). Use :meth:`set_position` to change. """ script = ScriptBuilder.node_body( self._identifier, "var p = _node.getWSPos(); return {x: p.x, y: p.y, z: p.z};" ) return self._client.execute(script).value
[docs] def set_position(self, x: float, y: float, z: float) -> None: """Set the world-space position of this node. Args: x: X coordinate in DAZ Studio units (centimetres by default). y: Y coordinate. z: Z coordinate. """ script = ScriptBuilder.node_body( self._identifier, f"_node.setWSPos(new DzVec3({x}, {y}, {z}));" ) self._client.execute(script)
@property def rotation(self) -> dict | None: """World-space rotation as ``{"x", "y", "z", "w"}`` quaternion (read-only). Use :meth:`set_rotation` to change (accepts Euler angles in degrees). """ script = ScriptBuilder.node_body( self._identifier, "var r = _node.getWSRot(); return {x: r.x, y: r.y, z: r.z, w: r.w};" ) return self._client.execute(script).value @property def general_scale(self) -> float | None: """Uniform scale factor (read-only).""" script = ScriptBuilder.node_body( self._identifier, "return _node.getScaleControl().getValue();" ) return self._client.execute(script).value @property def scale(self) -> dict | None: """Per-axis and uniform scale as ``{"x", "y", "z", "general"}`` (read-only).""" script = ScriptBuilder.node_body( self._identifier, "return {x: _node.getXScaleControl().getValue(), y: _node.getYScaleControl().getValue(), z: _node.getZScaleControl().getValue(), general: _node.getScaleControl().getValue()};" ) return self._client.execute(script).value @property def visible(self) -> bool | None: """General visibility flag (read/write). Affects both viewport and render visibility unless overridden by the per-channel visibility setters. """ script = ScriptBuilder.node_body(self._identifier, "return _node.isVisible();") return self._client.execute(script).value @visible.setter def visible(self, value: bool) -> None: flag = "true" if value else "false" script = ScriptBuilder.node_body(self._identifier, f"_node.setVisible({flag});") self._client.execute(script) @property def parent(self) -> DazNode | None: """Parent node in the scene hierarchy, or ``None`` for root nodes.""" script = ScriptBuilder.node_body( self._identifier, "var p = _node.getNodeParent(); return p ? p.getName() : null;" ) name = self._client.execute(script).value if name is None: return None return DazNode(self._client, NodeIdentifier(name)) @property def children(self) -> list[DazNode]: """Direct child nodes.""" script = ScriptBuilder.node_body( self._identifier, """ var names = []; for (var i = 0; i < _node.getNumNodeChildren(); i++) { names.push(_node.getNodeChild(i).getName()); } return names; """ ) names = self._client.execute(script).value or [] return [DazNode(self._client, NodeIdentifier(n)) for n in names] def _modifier_locator(self, modifier_name: str) -> str: return ( f"(function(){{" f" var _o = {self._locator};" f" _o = _o ? _o.getObject() : null;" f" return _o ? _o.findModifier({json.dumps(modifier_name)}) : null;" f"}})()" )
[docs] def modifiers(self) -> list["DazModifier"]: """Return all modifiers (morphs, constraints, etc.) on this node. Returns: A list of :class:`~dazpy.DazMorph` and :class:`~dazpy.DazModifier` instances. """ from ._modifier import DazModifier from ._morph import DazMorph script = ScriptBuilder.node_body( self._identifier, """ var obj = _node.getObject(); if (!obj) return []; var mods = []; for (var i = 0; i < obj.getNumModifiers(); i++) { var m = obj.getModifier(i); mods.push({name: m.getName(), className: m.className()}); } return mods; """ ) items = self._client.execute(script).value or [] result = [] for item in items: loc = self._modifier_locator(item["name"]) if item["className"] == "DzMorph": result.append(DazMorph(self._client, loc)) else: result.append(DazModifier(self._client, loc)) return result
[docs] def find_modifier(self, name: str) -> "DazModifier | None": """Find a modifier by internal name. Args: name: The ``getName()`` string of the modifier. Returns: A :class:`~dazpy.DazMorph` or :class:`~dazpy.DazModifier`, or ``None`` if not found. """ from ._modifier import DazModifier from ._morph import DazMorph script = ScriptBuilder.node_body( self._identifier, f""" var obj = _node.getObject(); if (!obj) return null; var m = obj.findModifier({json.dumps(name)}); return m ? {{name: m.getName(), className: m.className()}} : null; """ ) result = self._client.execute(script).value if result is None: return None loc = self._modifier_locator(result["name"]) if result["className"] == "DzMorph": return DazMorph(self._client, loc) return DazModifier(self._client, loc)
def _material_locator(self, material_name: str) -> str: return ( f"(function(){{" f" var _n = {self._locator};" f" if (!_n) return null;" f" var _o = _n.getObject();" f" if (!_o) return null;" f" var _s = _o.getCurrentShape();" f" return _s ? _s.findMaterial({json.dumps(material_name)}) : null;" f"}})()" )
[docs] def materials(self) -> list["DazMaterial"]: """Return all surface materials on this node's current shape.""" from ._material import DazMaterial script = ScriptBuilder.node_body( self._identifier, """ var obj = _node.getObject(); if (!obj) return []; var shape = obj.getCurrentShape(); if (!shape) return []; var names = []; for (var i = 0; i < shape.getNumMaterials(); i++) { names.push(shape.getMaterial(i).getName()); } return names; """ ) names = self._client.execute(script).value or [] return [DazMaterial(self._client, self._material_locator(n)) for n in names]
[docs] def find_material(self, name: str) -> "DazMaterial | None": """Find a surface material by name. Args: name: The material's ``getName()`` string. Returns: A :class:`~dazpy.DazMaterial` proxy, or ``None`` if not found. """ from ._material import DazMaterial script = ScriptBuilder.node_body( self._identifier, f""" var obj = _node.getObject(); if (!obj) return null; var shape = obj.getCurrentShape(); if (!shape) return null; var m = shape.findMaterial({json.dumps(name)}); return m ? m.getName() : null; """ ) result = self._client.execute(script).value if result is None: return None return DazMaterial(self._client, self._material_locator(result))
[docs] def find_modifier_by_label(self, label: str) -> "DazModifier | None": """Find a modifier by its user-visible label (the name shown in the DAZ UI). DAZ Studio morphs have both an internal name (e.g. ``"PHMSmileFull"``) and a display label (e.g. ``"Smile Full Face"``). :meth:`find_modifier` matches the internal name; this method matches the label instead. Args: label: The ``getLabel()`` string shown in the Parameters pane. Returns: A :class:`~dazpy.DazMorph` or :class:`~dazpy.DazModifier`, or ``None`` if no modifier with that label exists. """ from ._modifier import DazModifier from ._morph import DazMorph script = ScriptBuilder.node_body( self._identifier, f""" var obj = _node.getObject(); if (!obj) return null; for (var i = 0; i < obj.getNumModifiers(); i++) {{ var m = obj.getModifier(i); if (m.getLabel() === {json.dumps(label)}) {{ return {{name: m.getName(), className: m.className()}}; }} }} return null; """ ) result = self._client.execute(script).value if result is None: return None loc = self._modifier_locator(result["name"]) if result["className"] == "DzMorph": return DazMorph(self._client, loc) return DazModifier(self._client, loc)
[docs] def find_property(self, name: str) -> "DazProperty | None": # noqa: F821 """Find a node-level property by its internal name. This searches properties directly on the node (via ``DzNode::findProperty``), which covers pose controls, FACS dials, and other parameter channels that are **not** geometry modifiers and therefore invisible to :meth:`find_modifier`. Args: name: The ``getName()`` / ID string of the property (e.g. ``"facs_ctrl_SmileFullFace"``). Returns: A :class:`~dazpy.DazProperty` proxy, or ``None`` if not found. """ from ._property import DazProperty locator = ( f"(function(){{var _n={self._locator};" f"return _n ? _n.findProperty({json.dumps(name)}) : null;}})()" ) exists = self._client.execute( ScriptBuilder.iife(f"return !!({locator});") ).value if not exists: return None return DazProperty._from_locator(self._client, locator)
[docs] def find_property_by_label(self, label: str) -> "DazProperty | None": # noqa: F821 """Find a node-level property by its user-visible label. Equivalent to :meth:`find_property` but matches on ``getLabel()`` instead of ``getName()``. Use this when you know the label shown in the DAZ Studio Parameters pane (e.g. ``"Smile Full Face"``) but not the internal ID. Args: label: The ``getLabel()`` string shown in the Parameters pane. Returns: A :class:`~dazpy.DazProperty` proxy, or ``None`` if not found. """ from ._property import DazProperty locator = ( f"(function(){{var _n={self._locator};" f"return _n ? _n.findPropertyByLabel({json.dumps(label)}) : null;}})()" ) exists = self._client.execute( ScriptBuilder.iife(f"return !!({locator});") ).value if not exists: return None return DazProperty._from_locator(self._client, locator)
[docs] def morphs(self) -> list["DazMorph"]: """Return only the morph modifiers on this node (convenience filter).""" from ._morph import DazMorph return [m for m in self.modifiers() if isinstance(m, DazMorph)]
[docs] def set_rotation(self, x: float, y: float, z: float) -> None: """Set the world-space rotation using Euler angles in degrees. Args: x: Rotation around the X axis in degrees. y: Rotation around the Y axis in degrees. z: Rotation around the Z axis in degrees. """ script = ScriptBuilder.node_body( self._identifier, f"_node.getXRotControl().setValue({x}); _node.getYRotControl().setValue({y}); _node.getZRotControl().setValue({z});" ) self._client.execute(script)
@property def local_position(self) -> dict | None: """Local-space position as ``{"x", "y", "z"}`` (read-only).""" script = ScriptBuilder.node_body( self._identifier, "var p = _node.getLocalPos(); return {x: p.x, y: p.y, z: p.z};" ) return self._client.execute(script).value
[docs] def set_local_position(self, x: float, y: float, z: float) -> None: """Set the local-space position of this node. Args: x: X coordinate relative to the parent. y: Y coordinate. z: Z coordinate. """ script = ScriptBuilder.node_body( self._identifier, f"_node.setLocalPos(new DzVec3({x}, {y}, {z}));" ) self._client.execute(script)
@property def local_euler(self) -> tuple[float, float, float] | None: """Local-space rotation as an ``(x, y, z)`` tuple of Euler angles in degrees. Reads the rotation controls written by :meth:`set_local_rotation`, so the two are exact inverses. Returns: ``(x, y, z)`` in degrees, or ``None`` if the node cannot be found. """ script = ScriptBuilder.node_body( self._identifier, "return [_node.getXRotControl().getValue(), " "_node.getYRotControl().getValue(), " "_node.getZRotControl().getValue()];" ) result = self._client.execute(script).value if result is None: return None return (result[0], result[1], result[2]) @property def local_rotation(self) -> dict | None: """Local-space rotation as ``{"x", "y", "z", "w"}`` quaternion (read-only).""" script = ScriptBuilder.node_body( self._identifier, "var r = _node.getLocalRot(); return {x: r.x, y: r.y, z: r.z, w: r.w};" ) return self._client.execute(script).value
[docs] def set_local_rotation(self, x: float, y: float, z: float) -> None: """Set the local-space rotation using Euler angles in degrees. Args: x: Rotation around the local X axis in degrees. y: Rotation around the local Y axis in degrees. z: Rotation around the local Z axis in degrees. """ script = ScriptBuilder.node_body( self._identifier, f"_node.getXRotControl().setValue({x}); _node.getYRotControl().setValue({y}); _node.getZRotControl().setValue({z});" ) self._client.execute(script)
[docs] def is_selected(self) -> bool: """Return ``True`` if this node is currently selected.""" script = ScriptBuilder.node_body(self._identifier, "return _node.isSelected();") return bool(self._client.execute(script).value)
[docs] def select(self, on: bool = True) -> None: """Select or deselect this node. Args: on: ``True`` to select, ``False`` to deselect. """ flag = "true" if on else "false" script = ScriptBuilder.node_body(self._identifier, f"_node.select({flag});") self._client.execute(script)
[docs] def is_in_scene(self) -> bool: """Return ``True`` if this node is still part of the active scene.""" script = ScriptBuilder.node_body(self._identifier, "return _node.isInScene();") return bool(self._client.execute(script).value)
[docs] def is_root(self) -> bool: """Return ``True`` if this node has no parent (top-level node).""" script = ScriptBuilder.node_body(self._identifier, "return _node.isRootNode();") return bool(self._client.execute(script).value)
[docs] def is_visible_in_render(self) -> bool: """Return ``True`` if this node is visible in render output.""" script = ScriptBuilder.node_body(self._identifier, "return _node.isVisibleInRender();") return bool(self._client.execute(script).value)
[docs] def set_visible_in_render(self, on: bool) -> None: """Set render visibility. Args: on: ``True`` to show in render, ``False`` to hide. """ flag = "true" if on else "false" script = ScriptBuilder.node_body(self._identifier, f"_node.setVisibleInRender({flag});") self._client.execute(script)
[docs] def is_visible_in_viewport(self) -> bool: """Return ``True`` if this node is visible in the 3D viewport.""" script = ScriptBuilder.node_body(self._identifier, "return _node.isVisibleInViewport();") return bool(self._client.execute(script).value)
[docs] def set_visible_in_viewport(self, on: bool) -> None: """Set viewport visibility. Args: on: ``True`` to show, ``False`` to hide. """ flag = "true" if on else "false" script = ScriptBuilder.node_body(self._identifier, f"_node.setVisibleInViewport({flag});") self._client.execute(script)
[docs] def bounding_box(self) -> dict | None: """Return the world-space axis-aligned bounding box. Returns: A dict ``{"min": {"x", "y", "z"}, "max": {"x", "y", "z"}}`` or ``None`` if the node has no geometry. """ script = ScriptBuilder.node_body( self._identifier, "var bb = _node.getWSBoundingBox(); return {min: {x: bb.min.x, y: bb.min.y, z: bb.min.z}, max: {x: bb.max.x, y: bb.max.y, z: bb.max.z}};" ) return self._client.execute(script).value
@property def geometry_vertex_count(self) -> int | None: """Total vertex count of this node's current geometry, or ``None``.""" script = ScriptBuilder.node_body( self._identifier, """ var obj = _node.getObject(); if (!obj) return null; var shape = obj.getCurrentShape(); if (!shape) return null; var geo = shape.getGeometry(); if (!geo) return null; return geo.getNumVertices(); """ ) return self._client.execute(script).value