dazpy 2.6.0__tar.gz → 2.7.0__tar.gz
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.
- {dazpy-2.6.0 → dazpy-2.7.0}/PKG-INFO +1 -1
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/__init__.py +18 -2
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_camera.py +27 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_client.py +141 -9
- dazpy-2.7.0/dazpy/_dforce.py +54 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_element.py +34 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_interaction.py +61 -1
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_node.py +194 -16
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_pose.py +56 -22
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_property.py +81 -0
- dazpy-2.7.0/dazpy/_render.py +573 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_render_api.py +1 -1
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_scene.py +358 -0
- dazpy-2.7.0/dazpy/_scene_events.py +151 -0
- dazpy-2.7.0/dazpy/_scene_state.py +239 -0
- dazpy-2.7.0/dazpy/_viewport.py +352 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/exceptions.py +12 -1
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy.egg-info/PKG-INFO +1 -1
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy.egg-info/SOURCES.txt +4 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/pyproject.toml +1 -1
- {dazpy-2.6.0 → dazpy-2.7.0}/tests/test_dazpy.py +993 -4
- {dazpy-2.6.0 → dazpy-2.7.0}/tests/test_dazpy_integration.py +218 -0
- dazpy-2.7.0/tests/test_dsp_runner.py +137 -0
- dazpy-2.6.0/dazpy/_render.py +0 -185
- dazpy-2.6.0/dazpy/_viewport.py +0 -190
- {dazpy-2.6.0 → dazpy-2.7.0}/README.md +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_animation.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_batch.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_bone.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_geometry.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_light.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_material.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_modifier.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_morph.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_polling.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_result.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_script_builder.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_skeleton.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_timeline.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/_undo.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/math3.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy/py.typed +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy.egg-info/dependency_links.txt +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy.egg-info/requires.txt +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/dazpy.egg-info/top_level.txt +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/setup.cfg +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/tests/test_api.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/tests/test_comfyui_client.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/tests/test_comfyui_enhance_integration.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/tests/test_file_watcher.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/tests/test_performance.py +0 -0
- {dazpy-2.6.0 → dazpy-2.7.0}/tests/test_workflow_builder.py +0 -0
|
@@ -13,7 +13,7 @@ Typical usage::
|
|
|
13
13
|
figure.find_bone("r_forearm").set_local_rotation(0, 0, 45)
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
__version__ = "2.
|
|
16
|
+
__version__ = "2.7.0"
|
|
17
17
|
|
|
18
18
|
from ._client import DazClient
|
|
19
19
|
from ._scene import DazScene
|
|
@@ -25,8 +25,9 @@ from ._light import DazLight
|
|
|
25
25
|
from ._material import DazMaterial
|
|
26
26
|
from ._modifier import DazModifier
|
|
27
27
|
from ._morph import DazMorph
|
|
28
|
+
from ._dforce import DazDForce
|
|
28
29
|
from ._geometry import DazGeometry
|
|
29
|
-
from ._render import DazRenderSettings
|
|
30
|
+
from ._render import DazRenderSettings, Canvas, RenderOutcome
|
|
30
31
|
from ._viewport import DazViewport
|
|
31
32
|
from ._timeline import DazTimeline
|
|
32
33
|
from ._property import DazProperty
|
|
@@ -57,6 +58,9 @@ from ._interaction import (
|
|
|
57
58
|
build_rig_profile,
|
|
58
59
|
build_rig_profiles_from_snapshot,
|
|
59
60
|
build_fight_recipe,
|
|
61
|
+
build_face_each_other_recipe,
|
|
62
|
+
build_handshake_recipe,
|
|
63
|
+
build_hug_recipe,
|
|
60
64
|
build_kiss_recipe,
|
|
61
65
|
build_sit_recipe,
|
|
62
66
|
build_touch_recipe,
|
|
@@ -70,6 +74,7 @@ from ._interaction import (
|
|
|
70
74
|
)
|
|
71
75
|
from ._undo import UndoGroup
|
|
72
76
|
from ._pose import DazPose
|
|
77
|
+
from ._scene_state import DazSceneState
|
|
73
78
|
from ._animation import DazAnimation
|
|
74
79
|
from .math3 import Vec3, Quat, BoundingBox
|
|
75
80
|
from ._result import ExecutionResult
|
|
@@ -83,6 +88,7 @@ from ._render_api import (
|
|
|
83
88
|
render_variants,
|
|
84
89
|
)
|
|
85
90
|
from .exceptions import RenderError
|
|
91
|
+
from ._scene_events import SceneEvent, watch_scene_events, wait_for_scene_event
|
|
86
92
|
from . import exceptions
|
|
87
93
|
|
|
88
94
|
__all__ = [
|
|
@@ -97,8 +103,11 @@ __all__ = [
|
|
|
97
103
|
"DazMaterial",
|
|
98
104
|
"DazModifier",
|
|
99
105
|
"DazMorph",
|
|
106
|
+
"DazDForce",
|
|
100
107
|
"DazGeometry",
|
|
101
108
|
"DazRenderSettings",
|
|
109
|
+
"Canvas",
|
|
110
|
+
"RenderOutcome",
|
|
102
111
|
"DazViewport",
|
|
103
112
|
"DazTimeline",
|
|
104
113
|
"DazProperty",
|
|
@@ -129,6 +138,9 @@ __all__ = [
|
|
|
129
138
|
"build_rig_profile",
|
|
130
139
|
"build_rig_profiles_from_snapshot",
|
|
131
140
|
"build_fight_recipe",
|
|
141
|
+
"build_face_each_other_recipe",
|
|
142
|
+
"build_handshake_recipe",
|
|
143
|
+
"build_hug_recipe",
|
|
132
144
|
"build_kiss_recipe",
|
|
133
145
|
"build_sit_recipe",
|
|
134
146
|
"build_touch_recipe",
|
|
@@ -141,6 +153,7 @@ __all__ = [
|
|
|
141
153
|
"resolve_interaction_target",
|
|
142
154
|
"UndoGroup",
|
|
143
155
|
"DazPose",
|
|
156
|
+
"DazSceneState",
|
|
144
157
|
"DazAnimation",
|
|
145
158
|
"Vec3",
|
|
146
159
|
"Quat",
|
|
@@ -154,5 +167,8 @@ __all__ = [
|
|
|
154
167
|
"RenderError",
|
|
155
168
|
"render",
|
|
156
169
|
"render_variants",
|
|
170
|
+
"SceneEvent",
|
|
171
|
+
"watch_scene_events",
|
|
172
|
+
"wait_for_scene_event",
|
|
157
173
|
"exceptions",
|
|
158
174
|
]
|
|
@@ -45,6 +45,33 @@ class DazCamera(DazNode):
|
|
|
45
45
|
def depth_of_field(self, value: bool) -> None:
|
|
46
46
|
self.set_property("Depth of Field", value)
|
|
47
47
|
|
|
48
|
+
@property
|
|
49
|
+
def lens_shift_x(self) -> float | None:
|
|
50
|
+
"""Horizontal lens shift in millimetres (read/write).
|
|
51
|
+
|
|
52
|
+
Confirmed against a live instance: backed by ``DzCamera``'s
|
|
53
|
+
"Lens Shift X (mm)" property (``getLensShiftXControl()``), the same
|
|
54
|
+
value the render engine uses -- needed to match depth-based effects
|
|
55
|
+
(fog, DOF-driven line weight) to the actual render.
|
|
56
|
+
"""
|
|
57
|
+
return self.get_property("Lens Shift X (mm)")
|
|
58
|
+
|
|
59
|
+
@lens_shift_x.setter
|
|
60
|
+
def lens_shift_x(self, value: float) -> None:
|
|
61
|
+
self.set_property("Lens Shift X (mm)", float(value))
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def lens_shift_y(self) -> float | None:
|
|
65
|
+
"""Vertical lens shift in millimetres (read/write).
|
|
66
|
+
|
|
67
|
+
See :attr:`lens_shift_x`.
|
|
68
|
+
"""
|
|
69
|
+
return self.get_property("Lens Shift Y (mm)")
|
|
70
|
+
|
|
71
|
+
@lens_shift_y.setter
|
|
72
|
+
def lens_shift_y(self, value: float) -> None:
|
|
73
|
+
self.set_property("Lens Shift Y (mm)", float(value))
|
|
74
|
+
|
|
48
75
|
@property
|
|
49
76
|
def frame_width(self) -> float | None:
|
|
50
77
|
"""Sensor / film-gate width in millimetres (read-only)."""
|
|
@@ -37,11 +37,12 @@ def _map_response(resp: _requests.Response, script: str = "") -> ExecutionResult
|
|
|
37
37
|
|
|
38
38
|
if not data.get("success", True):
|
|
39
39
|
error_msg = data.get("error", "Script failed")
|
|
40
|
+
output = data.get("output", [])
|
|
40
41
|
# SyntaxError comes from the parser; runtime errors (TypeError, ReferenceError,
|
|
41
42
|
# Error, etc.) also include "Line N:" but never say "SyntaxError" explicitly.
|
|
42
43
|
if "SyntaxError" in error_msg:
|
|
43
|
-
raise ScriptSyntaxError(error_msg, script=script, request_id=request_id)
|
|
44
|
-
raise ScriptRuntimeError(error_msg, script=script, request_id=request_id)
|
|
44
|
+
raise ScriptSyntaxError(error_msg, script=script, request_id=request_id, output=output)
|
|
45
|
+
raise ScriptRuntimeError(error_msg, script=script, request_id=request_id, output=output)
|
|
45
46
|
|
|
46
47
|
return ExecutionResult(
|
|
47
48
|
value=data.get("result"),
|
|
@@ -228,6 +229,25 @@ class DazClient:
|
|
|
228
229
|
return {"status": "not_found"}
|
|
229
230
|
return resp.json()
|
|
230
231
|
|
|
232
|
+
def list_requests(self, status: str | None = None) -> dict:
|
|
233
|
+
"""List all tracked async requests (script and render) with their status.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
status: Optional filter, one of ``"queued"``, ``"running"``,
|
|
237
|
+
``"completed"``, ``"failed"``, ``"cancelled"``. Omit to list
|
|
238
|
+
requests in every status.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
A dict with a ``"requests"`` list (each item has ``request_id``,
|
|
242
|
+
``status``, ``progress``, ``submitted_at``) plus ``total`` and a
|
|
243
|
+
per-status count for every status value.
|
|
244
|
+
"""
|
|
245
|
+
params = {"status": status} if status else None
|
|
246
|
+
resp = self._get("/requests", params=params)
|
|
247
|
+
if resp.status_code in (401, 403):
|
|
248
|
+
raise AuthenticationError(f"HTTP {resp.status_code}: {resp.text[:200]}")
|
|
249
|
+
return resp.json()
|
|
250
|
+
|
|
231
251
|
def cancel_request(self, request_id: str) -> bool:
|
|
232
252
|
"""Cancel a queued or running async request.
|
|
233
253
|
|
|
@@ -273,7 +293,7 @@ class DazClient:
|
|
|
273
293
|
width: Image width in pixels (must be paired with *height*).
|
|
274
294
|
height: Image height in pixels (must be paired with *width*).
|
|
275
295
|
camera: Camera label to render from.
|
|
276
|
-
engine: Render engine (``"iray"``, ``"
|
|
296
|
+
engine: Render engine (``"iray"``, ``"filament"``).
|
|
277
297
|
iray_samples: iRay sample count (0 = use scene default).
|
|
278
298
|
reset_morphs: If ``True``, reset all morphs to defaults before applying.
|
|
279
299
|
|
|
@@ -331,6 +351,63 @@ class DazClient:
|
|
|
331
351
|
raise AuthenticationError(f"HTTP {resp.status_code}: {resp.text[:200]}")
|
|
332
352
|
return resp.json()
|
|
333
353
|
|
|
354
|
+
def render_animation_submit(
|
|
355
|
+
self,
|
|
356
|
+
output_path: str,
|
|
357
|
+
start_frame: int,
|
|
358
|
+
end_frame: int,
|
|
359
|
+
*,
|
|
360
|
+
frame_padding: int = 4,
|
|
361
|
+
width: int = 0,
|
|
362
|
+
height: int = 0,
|
|
363
|
+
camera: str = "",
|
|
364
|
+
engine: str = "",
|
|
365
|
+
) -> dict:
|
|
366
|
+
"""Submit an animation render job spanning a frame range and return immediately.
|
|
367
|
+
|
|
368
|
+
Renders each frame in ``[start_frame, end_frame]`` to a separate file,
|
|
369
|
+
as a single trackable async request (mirrors :meth:`render_submit`'s
|
|
370
|
+
request-tracking shape, not :meth:`render_batch_submit`'s fan-out).
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
output_path: Output path pattern containing the literal token
|
|
374
|
+
``"{frame}"``, e.g. ``r"C:\\tmp\\anim\\frame_{frame}.png"``.
|
|
375
|
+
The token is replaced with the frame number, zero-padded to
|
|
376
|
+
*frame_padding* digits.
|
|
377
|
+
start_frame: First frame to render (inclusive).
|
|
378
|
+
end_frame: Last frame to render (inclusive).
|
|
379
|
+
frame_padding: Zero-padding width for the frame number (default ``4``).
|
|
380
|
+
width: Image width in pixels (must be paired with *height*).
|
|
381
|
+
height: Image height in pixels (must be paired with *width*).
|
|
382
|
+
camera: Camera label to render from.
|
|
383
|
+
engine: Render engine (``"iray"``, ``"filament"``).
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
A dict with ``request_id``, ``status`` (``"queued"``), and
|
|
387
|
+
``submitted_at`` keys.
|
|
388
|
+
|
|
389
|
+
Raises:
|
|
390
|
+
ConnectionError: If the server cannot be reached.
|
|
391
|
+
AuthenticationError: On HTTP 401/403.
|
|
392
|
+
"""
|
|
393
|
+
payload: dict = {
|
|
394
|
+
"output_path": output_path,
|
|
395
|
+
"start_frame": start_frame,
|
|
396
|
+
"end_frame": end_frame,
|
|
397
|
+
"frame_padding": frame_padding,
|
|
398
|
+
}
|
|
399
|
+
if width and height:
|
|
400
|
+
payload["width"] = width
|
|
401
|
+
payload["height"] = height
|
|
402
|
+
if camera:
|
|
403
|
+
payload["camera"] = camera
|
|
404
|
+
if engine:
|
|
405
|
+
payload["engine"] = engine
|
|
406
|
+
resp = self._post("/render/animation", payload)
|
|
407
|
+
if resp.status_code in (401, 403):
|
|
408
|
+
raise AuthenticationError(f"HTTP {resp.status_code}: {resp.text[:200]}")
|
|
409
|
+
return resp.json()
|
|
410
|
+
|
|
334
411
|
def cancel_render(self, request_id: str) -> bool:
|
|
335
412
|
"""Cancel a queued or running render job.
|
|
336
413
|
|
|
@@ -369,6 +446,40 @@ class DazClient:
|
|
|
369
446
|
except _requests.exceptions.RequestException:
|
|
370
447
|
return None
|
|
371
448
|
|
|
449
|
+
def stream_scene_events(
|
|
450
|
+
self,
|
|
451
|
+
categories: "list[str] | None" = None,
|
|
452
|
+
stream_timeout: "float | None" = None,
|
|
453
|
+
) -> "object | None":
|
|
454
|
+
"""Open the SSE stream for general scene-change events (GET /scene/events).
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
categories: Optional subset of event categories to subscribe to
|
|
458
|
+
(``"node"``, ``"skeleton"``, ``"light"``, ``"camera"``,
|
|
459
|
+
``"selection"``, ``"scene"``, ``"time"``, ``"render"``).
|
|
460
|
+
``None`` (default) subscribes to all categories.
|
|
461
|
+
stream_timeout: Socket timeout in seconds. ``None`` (default)
|
|
462
|
+
waits indefinitely — the server sends a keepalive comment
|
|
463
|
+
every 15 seconds, so the connection never idles out.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
A streaming :class:`requests.Response` on success, or ``None``
|
|
467
|
+
if the endpoint is unavailable. Callers must close the response
|
|
468
|
+
when done (e.g. via a ``with`` statement).
|
|
469
|
+
"""
|
|
470
|
+
try:
|
|
471
|
+
params = {"filter": ",".join(categories)} if categories else None
|
|
472
|
+
resp = _requests.get(
|
|
473
|
+
f"{self._base}/scene/events",
|
|
474
|
+
headers=self._headers,
|
|
475
|
+
params=params,
|
|
476
|
+
stream=True,
|
|
477
|
+
timeout=stream_timeout,
|
|
478
|
+
)
|
|
479
|
+
return resp if resp.status_code == 200 else None
|
|
480
|
+
except _requests.exceptions.RequestException:
|
|
481
|
+
return None
|
|
482
|
+
|
|
372
483
|
# ── USD export ────────────────────────────────────────────────────────────
|
|
373
484
|
|
|
374
485
|
def export_usd_submit(
|
|
@@ -440,13 +551,34 @@ class DazClient:
|
|
|
440
551
|
# ── Server health ─────────────────────────────────────────────────────────
|
|
441
552
|
|
|
442
553
|
def status(self) -> dict:
|
|
443
|
-
"""Return the server status dict from ``GET /status``.
|
|
444
|
-
|
|
554
|
+
"""Return the server status dict from ``GET /status``.
|
|
555
|
+
|
|
556
|
+
Raises:
|
|
557
|
+
AuthenticationError: On HTTP 401/403.
|
|
558
|
+
"""
|
|
559
|
+
resp = self._get("/status")
|
|
560
|
+
if resp.status_code in (401, 403):
|
|
561
|
+
raise AuthenticationError(f"HTTP {resp.status_code}: {resp.text[:200]}")
|
|
562
|
+
return resp.json()
|
|
445
563
|
|
|
446
564
|
def health(self) -> dict:
|
|
447
|
-
"""Return the health check dict from ``GET /health``.
|
|
448
|
-
|
|
565
|
+
"""Return the health check dict from ``GET /health``.
|
|
566
|
+
|
|
567
|
+
Raises:
|
|
568
|
+
AuthenticationError: On HTTP 401/403.
|
|
569
|
+
"""
|
|
570
|
+
resp = self._get("/health")
|
|
571
|
+
if resp.status_code in (401, 403):
|
|
572
|
+
raise AuthenticationError(f"HTTP {resp.status_code}: {resp.text[:200]}")
|
|
573
|
+
return resp.json()
|
|
449
574
|
|
|
450
575
|
def metrics(self) -> dict:
|
|
451
|
-
"""Return the metrics dict from ``GET /metrics``.
|
|
452
|
-
|
|
576
|
+
"""Return the metrics dict from ``GET /metrics``.
|
|
577
|
+
|
|
578
|
+
Raises:
|
|
579
|
+
AuthenticationError: On HTTP 401/403.
|
|
580
|
+
"""
|
|
581
|
+
resp = self._get("/metrics")
|
|
582
|
+
if resp.status_code in (401, 403):
|
|
583
|
+
raise AuthenticationError(f"HTTP {resp.status_code}: {resp.text[:200]}")
|
|
584
|
+
return resp.json()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ._modifier import DazModifier
|
|
4
|
+
from ._script_builder import ScriptBuilder
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DazDForce(DazModifier):
|
|
8
|
+
"""Proxy for a ``DzDForceModifier`` (cloth/hair dForce simulation modifier).
|
|
9
|
+
|
|
10
|
+
Returned by :meth:`DazNode.modifiers`, :meth:`DazNode.find_modifier`,
|
|
11
|
+
:meth:`DazNode.find_modifier_by_label`, and :meth:`DazNode.dforce_modifiers`
|
|
12
|
+
when the underlying DAZ modifier is a ``DzDForceModifier``.
|
|
13
|
+
|
|
14
|
+
Simulation tunables not covered by :attr:`freeze_simulation` (e.g.
|
|
15
|
+
``"Dynamics Strength"``, ``"Contraction-Expansion Ratio"``, the various
|
|
16
|
+
stiffness weight maps) are still reachable through the inherited
|
|
17
|
+
:meth:`~dazpy.DazElement.get_property` / :meth:`~dazpy.DazElement.set_property`,
|
|
18
|
+
which look properties up by their Parameters-pane label.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def freeze_simulation(self) -> bool | None:
|
|
23
|
+
"""Whether the simulated result is frozen onto the mesh (read/write).
|
|
24
|
+
|
|
25
|
+
Freezing detaches the mesh from the live dForce solve so it holds its
|
|
26
|
+
current shape without needing to re-simulate — this is DAZ Studio's
|
|
27
|
+
equivalent of "baking" a dForce result.
|
|
28
|
+
"""
|
|
29
|
+
script = ScriptBuilder.iife(f"""
|
|
30
|
+
var m = {self._locator};
|
|
31
|
+
if (!m) return null;
|
|
32
|
+
var p = m.findPropertyByLabel("Freeze Simulation");
|
|
33
|
+
return p ? p.getValue() : null;
|
|
34
|
+
""")
|
|
35
|
+
return self._client.execute(script).value
|
|
36
|
+
|
|
37
|
+
@freeze_simulation.setter
|
|
38
|
+
def freeze_simulation(self, value: bool) -> None:
|
|
39
|
+
flag = "true" if value else "false"
|
|
40
|
+
script = ScriptBuilder.iife(f"""
|
|
41
|
+
var m = {self._locator};
|
|
42
|
+
if (!m) return;
|
|
43
|
+
var p = m.findPropertyByLabel("Freeze Simulation");
|
|
44
|
+
if (p) p.setValue({flag});
|
|
45
|
+
""")
|
|
46
|
+
self._client.execute(script)
|
|
47
|
+
|
|
48
|
+
def freeze(self) -> None:
|
|
49
|
+
"""Bake the current simulated result onto the mesh (sets ``freeze_simulation`` on)."""
|
|
50
|
+
self.freeze_simulation = True
|
|
51
|
+
|
|
52
|
+
def unfreeze(self) -> None:
|
|
53
|
+
"""Release a frozen simulation so it resumes following the dForce solve."""
|
|
54
|
+
self.freeze_simulation = False
|
|
@@ -78,6 +78,40 @@ class DazElement:
|
|
|
78
78
|
""")
|
|
79
79
|
return self._client.execute(script).value or []
|
|
80
80
|
|
|
81
|
+
def numeric_properties(self) -> dict[str, object]:
|
|
82
|
+
"""Return every numeric property on this element as ``{label: value}``.
|
|
83
|
+
|
|
84
|
+
Unlike :meth:`list_properties`, this fetches labels and current
|
|
85
|
+
values for all numeric (float/int/bool) properties in a single HTTP
|
|
86
|
+
round-trip — use it instead of calling :meth:`get_property` in a
|
|
87
|
+
loop over ``list_properties()`` results.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A dict mapping each numeric property's display label to its
|
|
91
|
+
current value.
|
|
92
|
+
"""
|
|
93
|
+
script = ScriptBuilder.iife(f"""
|
|
94
|
+
var obj = {self._locator};
|
|
95
|
+
if (!obj) return null;
|
|
96
|
+
var result = {{}};
|
|
97
|
+
for (var i = 0; i < obj.getNumProperties(); i++) {{
|
|
98
|
+
var p = obj.getProperty(i);
|
|
99
|
+
if (p.inherits("DzNumericProperty")) {{
|
|
100
|
+
result[p.getLabel()] = p.getValue();
|
|
101
|
+
}}
|
|
102
|
+
}}
|
|
103
|
+
return result;
|
|
104
|
+
""")
|
|
105
|
+
return self._client.execute(script).value or {}
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def class_name(self) -> str | None:
|
|
109
|
+
"""The DazScript class name of this element (e.g. ``"DzFigure"``, ``"DzSpotLight"``)."""
|
|
110
|
+
script = ScriptBuilder.iife(
|
|
111
|
+
f"var obj = {self._locator}; return obj ? obj.className() : null;"
|
|
112
|
+
)
|
|
113
|
+
return self._client.execute(script).value
|
|
114
|
+
|
|
81
115
|
def snapshot(self, fields: list[str]) -> dict:
|
|
82
116
|
"""Read and cache a set of property values in a single call.
|
|
83
117
|
|
|
@@ -27,7 +27,7 @@ AnchorRole = Literal[
|
|
|
27
27
|
"custom",
|
|
28
28
|
]
|
|
29
29
|
InteractionKind = Literal["PoseTarget", "ContactTarget", "LookAtTarget", "BalanceTarget", "HandTarget", "FootTarget"]
|
|
30
|
-
RecipeKind = Literal["sit", "touch", "kiss", "fight", "custom"]
|
|
30
|
+
RecipeKind = Literal["sit", "touch", "kiss", "fight", "handshake", "hug", "face_each_other", "custom"]
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def _normalize_name(value: str | None) -> str:
|
|
@@ -1815,6 +1815,66 @@ def build_kiss_recipe(
|
|
|
1815
1815
|
)
|
|
1816
1816
|
|
|
1817
1817
|
|
|
1818
|
+
def build_handshake_recipe(
|
|
1819
|
+
actor_a: str,
|
|
1820
|
+
actor_b: str,
|
|
1821
|
+
*,
|
|
1822
|
+
a_anchor: str = "r_hand",
|
|
1823
|
+
b_anchor: str = "r_hand",
|
|
1824
|
+
) -> InteractionRecipe:
|
|
1825
|
+
"""Build a mirrored hand-to-hand greeting preset for two figures."""
|
|
1826
|
+
|
|
1827
|
+
return InteractionRecipe(
|
|
1828
|
+
kind="handshake",
|
|
1829
|
+
actors=[actor_a, actor_b],
|
|
1830
|
+
constraints=[
|
|
1831
|
+
LookAtTarget(actor_a, "head", (0.0, 0.0, 0.0)),
|
|
1832
|
+
LookAtTarget(actor_b, "head", (0.0, 0.0, 0.0)),
|
|
1833
|
+
HandTarget(actor_a, a_anchor, target_figure=actor_b, target_anchor=b_anchor),
|
|
1834
|
+
HandTarget(actor_b, b_anchor, target_figure=actor_a, target_anchor=a_anchor),
|
|
1835
|
+
],
|
|
1836
|
+
metadata={"interaction": "handshake"},
|
|
1837
|
+
)
|
|
1838
|
+
|
|
1839
|
+
|
|
1840
|
+
def build_hug_recipe(
|
|
1841
|
+
actor_a: str,
|
|
1842
|
+
actor_b: str,
|
|
1843
|
+
*,
|
|
1844
|
+
a_anchor: str = "r_hand",
|
|
1845
|
+
b_anchor: str = "r_hand",
|
|
1846
|
+
) -> InteractionRecipe:
|
|
1847
|
+
"""Build a mutual arms-around-each-other embrace preset for two figures."""
|
|
1848
|
+
|
|
1849
|
+
a_far_shoulder = "l_shoulder" if _side_from_name(a_anchor) == "right" else "r_shoulder"
|
|
1850
|
+
b_far_shoulder = "l_shoulder" if _side_from_name(b_anchor) == "right" else "r_shoulder"
|
|
1851
|
+
return InteractionRecipe(
|
|
1852
|
+
kind="hug",
|
|
1853
|
+
actors=[actor_a, actor_b],
|
|
1854
|
+
constraints=[
|
|
1855
|
+
LookAtTarget(actor_a, "head", (0.0, 0.0, 0.0)),
|
|
1856
|
+
LookAtTarget(actor_b, "head", (0.0, 0.0, 0.0)),
|
|
1857
|
+
HandTarget(actor_a, a_anchor, target_figure=actor_b, target_anchor=a_far_shoulder),
|
|
1858
|
+
HandTarget(actor_b, b_anchor, target_figure=actor_a, target_anchor=b_far_shoulder),
|
|
1859
|
+
],
|
|
1860
|
+
metadata={"interaction": "hug"},
|
|
1861
|
+
)
|
|
1862
|
+
|
|
1863
|
+
|
|
1864
|
+
def build_face_each_other_recipe(actor_a: str, actor_b: str) -> InteractionRecipe:
|
|
1865
|
+
"""Build a mutual eye-contact preset for two figures, with no limb contact."""
|
|
1866
|
+
|
|
1867
|
+
return InteractionRecipe(
|
|
1868
|
+
kind="face_each_other",
|
|
1869
|
+
actors=[actor_a, actor_b],
|
|
1870
|
+
constraints=[
|
|
1871
|
+
LookAtTarget(actor_a, "head", (0.0, 0.0, 0.0)),
|
|
1872
|
+
LookAtTarget(actor_b, "head", (0.0, 0.0, 0.0)),
|
|
1873
|
+
],
|
|
1874
|
+
metadata={"interaction": "face_each_other"},
|
|
1875
|
+
)
|
|
1876
|
+
|
|
1877
|
+
|
|
1818
1878
|
def build_fight_recipe(
|
|
1819
1879
|
attacker: str,
|
|
1820
1880
|
defender: str,
|