dazpy 2.6.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.
Files changed (46) hide show
  1. dazpy-2.6.0/PKG-INFO +2642 -0
  2. dazpy-2.6.0/README.md +2627 -0
  3. dazpy-2.6.0/dazpy/__init__.py +158 -0
  4. dazpy-2.6.0/dazpy/_animation.py +402 -0
  5. dazpy-2.6.0/dazpy/_batch.py +107 -0
  6. dazpy-2.6.0/dazpy/_bone.py +87 -0
  7. dazpy-2.6.0/dazpy/_camera.py +188 -0
  8. dazpy-2.6.0/dazpy/_client.py +452 -0
  9. dazpy-2.6.0/dazpy/_element.py +97 -0
  10. dazpy-2.6.0/dazpy/_geometry.py +547 -0
  11. dazpy-2.6.0/dazpy/_interaction.py +2003 -0
  12. dazpy-2.6.0/dazpy/_light.py +87 -0
  13. dazpy-2.6.0/dazpy/_material.py +105 -0
  14. dazpy-2.6.0/dazpy/_modifier.py +40 -0
  15. dazpy-2.6.0/dazpy/_morph.py +43 -0
  16. dazpy-2.6.0/dazpy/_node.py +556 -0
  17. dazpy-2.6.0/dazpy/_polling.py +59 -0
  18. dazpy-2.6.0/dazpy/_pose.py +343 -0
  19. dazpy-2.6.0/dazpy/_property.py +101 -0
  20. dazpy-2.6.0/dazpy/_render.py +185 -0
  21. dazpy-2.6.0/dazpy/_render_api.py +337 -0
  22. dazpy-2.6.0/dazpy/_result.py +24 -0
  23. dazpy-2.6.0/dazpy/_scene.py +658 -0
  24. dazpy-2.6.0/dazpy/_script_builder.py +64 -0
  25. dazpy-2.6.0/dazpy/_skeleton.py +683 -0
  26. dazpy-2.6.0/dazpy/_timeline.py +65 -0
  27. dazpy-2.6.0/dazpy/_undo.py +40 -0
  28. dazpy-2.6.0/dazpy/_viewport.py +190 -0
  29. dazpy-2.6.0/dazpy/exceptions.py +77 -0
  30. dazpy-2.6.0/dazpy/math3.py +611 -0
  31. dazpy-2.6.0/dazpy/py.typed +0 -0
  32. dazpy-2.6.0/dazpy.egg-info/PKG-INFO +2642 -0
  33. dazpy-2.6.0/dazpy.egg-info/SOURCES.txt +44 -0
  34. dazpy-2.6.0/dazpy.egg-info/dependency_links.txt +1 -0
  35. dazpy-2.6.0/dazpy.egg-info/requires.txt +5 -0
  36. dazpy-2.6.0/dazpy.egg-info/top_level.txt +1 -0
  37. dazpy-2.6.0/pyproject.toml +31 -0
  38. dazpy-2.6.0/setup.cfg +4 -0
  39. dazpy-2.6.0/tests/test_api.py +751 -0
  40. dazpy-2.6.0/tests/test_comfyui_client.py +163 -0
  41. dazpy-2.6.0/tests/test_comfyui_enhance_integration.py +124 -0
  42. dazpy-2.6.0/tests/test_dazpy.py +3936 -0
  43. dazpy-2.6.0/tests/test_dazpy_integration.py +1903 -0
  44. dazpy-2.6.0/tests/test_file_watcher.py +113 -0
  45. dazpy-2.6.0/tests/test_performance.py +413 -0
  46. dazpy-2.6.0/tests/test_workflow_builder.py +94 -0
dazpy-2.6.0/PKG-INFO ADDED
@@ -0,0 +1,2642 @@
1
+ Metadata-Version: 2.4
2
+ Name: dazpy
3
+ Version: 2.6.0
4
+ Summary: Python SDK for the DAZ Studio Script Server
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/bluemoonfoundry/daz-script-server
7
+ Project-URL: Repository, https://github.com/bluemoonfoundry/daz-script-server
8
+ Project-URL: Bug Tracker, https://github.com/bluemoonfoundry/daz-script-server/issues
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: requests>=2.28
12
+ Provides-Extra: docs
13
+ Requires-Dist: sphinx>=7.0; extra == "docs"
14
+ Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
15
+
16
+ # DazScript Server
17
+
18
+ **Version 2.6.0** | DAZ Studio 4.5+ | DAZ Studio 6.25+ | Windows & macOS
19
+
20
+ [![Docs](https://img.shields.io/badge/docs-dazpy%20SDK-blue)](https://bluemoonfoundry.github.io/daz-script-server/)
21
+ [![HTTP API](https://img.shields.io/badge/docs-HTTP%20API%20reference-blue)](https://bluemoonfoundry.github.io/daz-script-server/api-reference/)
22
+
23
+ A production-ready DAZ Studio plugin that embeds a secure HTTP server inside DAZ Studio, enabling remote execution of DazScript via HTTP POST requests (or an optional Python library that wraps the interface and adds additional features) with JSON responses. Control DAZ Studio programmatically from external tools, automation scripts, and custom applications.
24
+
25
+ > [!NOTE]
26
+ > As of v2.6.0, plugins for DS4 and DS6 on Windows, MacOS Intel, and MacOS Apple Silicon are included as part of the standard release artifacts. Remember that if you want to use a newer version of the plugin you must remove any old DLL or dylib file first!
27
+ ---
28
+
29
+ ## 🚀 Quick Start
30
+
31
+ **Already have the plugin installed?**
32
+
33
+ 1. Open DAZ Studio → **Window → Panes → Daz Script Server**
34
+ 2. Click **Start Server** (default: `127.0.0.1:18811`)
35
+ 3. Click **Copy** to copy your API token (optional, but recommended for public environments)
36
+
37
+ **Option A — dazpy Python SDK (recommended):**
38
+
39
+ ```bash
40
+ pip install dazpy
41
+ ```
42
+
43
+ ```python
44
+ import time
45
+ from dazpy import DazClient, DazScene
46
+
47
+ client = DazClient() # auto-loads token from ~/.daz3d/dazscriptserver_token.txt
48
+ scene = DazScene(client)
49
+
50
+ print(scene.num_nodes(), "nodes in scene")
51
+
52
+ figure = scene.find_skeleton_by_label("Genesis 9")
53
+
54
+ bones = figure.bones()
55
+ print([b._identifier.value for b in bones if "neck" in b._identifier.value.lower()])
56
+
57
+ rots=[0,4,10,15,20]
58
+
59
+ for rot in rots:
60
+ print (f"Set rotation {rot}")
61
+ figure.find_bone("neck1").set_local_rotation(0, rot, 0)
62
+ time.sleep(1)
63
+ ```
64
+
65
+ **Option B — raw HTTP (any language):**
66
+
67
+ ```python
68
+ import requests
69
+
70
+ response = requests.post(
71
+ "http://127.0.0.1:18811/execute",
72
+ json={"script": "(function(){ return 'Hello there from Daz Studio!'; })()"}
73
+ )
74
+ print(response.json())
75
+ ```
76
+
77
+ **Need to get the plugin?** Jump to [Getting the Plugin](#getting-the-plugin) below.
78
+
79
+ ---
80
+
81
+ ## 📋 Table of Contents
82
+
83
+ ### Getting Started
84
+ - [Quick Start](#-quick-start)
85
+ - [Why This Exists](#why-this-exists)
86
+ - [What's New in v2.6.0](#whats-new-in-v260)
87
+ - [What's New in v2.5.0](#whats-new-in-v250)
88
+ - [What's New in v2.4.0](#whats-new-in-v240)
89
+ - [What's New in v2.3.0](#whats-new-in-v230)
90
+ - [What's New in v2.2.0](#whats-new-in-v220)
91
+ - [What's New in v2.0.0](#whats-new-in-v200)
92
+ - [What's New in v1.3.0](#whats-new-in-v130)
93
+ - [What's New in v1.2.0](#whats-new-in-v120)
94
+ - [Requirements](#requirements)
95
+ - [Getting the Plugin](#getting-the-plugin)
96
+
97
+ ### dazpy Python SDK
98
+ - [Overview](#-dazpy-python-sdk)
99
+ - [Installation](#installation)
100
+ - [Jupyter Notebook](#jupyter-notebook)
101
+ - [Connecting to DAZ Studio](#connecting-to-daz-studio)
102
+ - [Scene Graph](#scene-graph)
103
+ - [Figures — Skeletons and Bones](#figures--skeletons-and-bones)
104
+ - [Morphs](#morphs)
105
+ - [Materials](#materials)
106
+ - [Cameras and Lights](#cameras-and-lights)
107
+ - [Scene I/O and Timeline](#scene-io-and-timeline)
108
+ - [Geometry](#geometry)
109
+ - [Advanced: Batch, Undo, Async](#advanced-batch-undo-and-async)
110
+ - [Error Handling](#error-handling)
111
+ - [Testing dazpy](#testing-dazpy)
112
+
113
+ ### Using the Plugin
114
+ - [Starting the Server](#starting-the-server)
115
+ - [Configuration](#-configuration)
116
+ - [API Reference](#-api-reference)
117
+ - [Script Registry](#-script-registry)
118
+ - [Async Operations](#-async-operations)
119
+ - [Scene Events (SSE)](#-scene-events-sse)
120
+ - [Client Examples](#-client-examples)
121
+
122
+ ### Security & Best Practices
123
+ - [Security Features](#-security-features)
124
+ - [Security Best Practices](#-security-best-practices)
125
+ - [Troubleshooting](#-troubleshooting)
126
+
127
+ ### Advanced Topics
128
+ - [Writing Scripts](#writing-scripts)
129
+ - [Performance Tuning](#performance-tuning)
130
+ - [Integration Patterns](#integration-patterns)
131
+ - [Reverse Proxy Setup](#reverse-proxy-setup-https)
132
+
133
+ ### Reference
134
+ - [FAQ](#-frequently-asked-questions)
135
+ - [Contributing](#contributing)
136
+ - [License](#license--attribution)
137
+
138
+ ### Additional Documentation
139
+ - [dazpy SDK Docs](https://bluemoonfoundry.github.io/daz-script-server/) — hosted on GitHub Pages
140
+ - [HTTP API Reference](https://bluemoonfoundry.github.io/daz-script-server/api-reference/) — hosted on GitHub Pages
141
+ - [OpenAPI Specification](openapi.yaml)
142
+ - [Architecture](ARCHITECTURE.md)
143
+ - [Migration Guide (v1.x → v2.0)](MIGRATION.md)
144
+ - [Changelog](CHANGELOG.md)
145
+ - [Contributing Guide](CONTRIBUTING.md)
146
+ - [Interaction Pose Roadmap](INTERACTION_POSE_ROADMAP.md)
147
+
148
+ ---
149
+
150
+ ## Why This Exists
151
+
152
+ DAZ Studio is powerful for 3D content creation, but automation is limited to manually running scripts. This plugin solves that by exposing REST endpoints (HTTP) for remote clients to, among other things, submit inline scripts to execute, which can return log output lines and a return object as JSON.
153
+
154
+ **What You Can Do:**
155
+ - **Remote Automation** - Control DAZ Studio from Python, web apps, CI/CD pipelines
156
+ - **Programmatic Access** - Build custom tools that interact with scenes, assets, and rendering
157
+ - **Integration** - Connect to game engines, asset pipelines, batch processing systems
158
+ - **API-First Development** - Treat DAZ Studio as a service with HTTP APIs
159
+
160
+ **Common Use Cases:**
161
+ - Batch rendering and asset processing
162
+ - Asset management system integration
163
+ - Automated scene generation and testing
164
+ - Custom web-based controllers
165
+ - CI/CD pipelines for 3D content validation
166
+
167
+ ---
168
+
169
+ ## What's New in v2.6.0
170
+
171
+ ### 💾 Scene Save Copy — `POST /scene/save-copy` + `DazScene.save_copy()`
172
+
173
+ Save the current scene to a new path without changing the scene's internal
174
+ filename pointer or dirty flag — the programmatic equivalent of a
175
+ **"Save a Copy As…"** menu option.
176
+
177
+ ```python
178
+ from dazpy import DazClient, DazScene
179
+
180
+ client = DazClient()
181
+ scene = DazScene(client)
182
+
183
+ result = scene.save_copy("C:/backups/scene_v2.duf")
184
+ print(result["method"]) # "copy", "serialize", or "serialize+restore"
185
+ ```
186
+
187
+ For scenes with no unsaved changes the plugin uses `QFile::copy()` — a pure
188
+ file-system copy that produces a byte-identical file with zero DAZ Studio state
189
+ change. For dirty scenes it serialises via `Scene.saveScene()` and restores
190
+ the original filename immediately. The `method` field in the response tells
191
+ you which strategy was used.
192
+
193
+ See `docs/examples/fundamentals/scene_save_copy.py` for a complete example
194
+ with `--compare` and `--dry-run` options.
195
+
196
+ ---
197
+
198
+ ## What's New in v2.5.0
199
+
200
+ ### 🤝 Interaction Posing — Multi-Figure Poses and IK Alignment
201
+
202
+ The dazpy SDK now includes a full **interaction posing system** for placing two
203
+ figures together in physically plausible poses (seated, touching, kissing, combat
204
+ stances, etc.) and aligning individual limbs to world-space targets using an
205
+ iterative IK solver.
206
+
207
+ **Preset recipe builders** generate a ready-to-apply `InteractionRecipe` from a
208
+ pair of figure labels:
209
+
210
+ ```python
211
+ from dazpy import DazClient, DazScene
212
+ from dazpy._interaction import build_sit_recipe, build_touch_recipe
213
+
214
+ client = DazClient()
215
+ scene = DazScene(client)
216
+
217
+ recipe = build_sit_recipe("BobG8", "AliceG8")
218
+ scene.apply_interaction_recipe_to_scene(recipe)
219
+ ```
220
+
221
+ Available presets: `build_sit_recipe`, `build_touch_recipe`, `build_kiss_recipe`,
222
+ `build_fight_recipe`. All presets produce an `InteractionRecipe` that can be
223
+ serialised to JSON, stored, and replayed.
224
+
225
+ **IK limb alignment** moves a hand or foot end-effector to a world-space target
226
+ using a Jacobian-based iterative solver running entirely inside a single batched
227
+ HTTP call sequence:
228
+
229
+ ```python
230
+ from dazpy._interaction import align_hand_target, align_foot_target
231
+
232
+ skel = scene.find_skeleton("BobG8")
233
+ result = align_hand_target(skel, target_position=(12.0, 95.0, 8.0))
234
+ print(f"converged={result.converged} final_error={result.final_error:.3f} cm")
235
+ ```
236
+
237
+ **Bulk scene snapshot** fetches every skeleton's bone world positions in a single
238
+ HTTP call, avoiding per-bone round-trips when building rig data:
239
+
240
+ ```python
241
+ snapshot = scene.scene_snapshot()
242
+ profiles = build_rig_profiles_from_snapshot(snapshot)
243
+ ```
244
+
245
+ **Body measurement accuracy improvements** in `docs/examples/body_measurements.py`:
246
+
247
+ - Slices now use centroid-weighted torso loop selection to filter arm cross-sections, correcting a ~70 cm overestimate on A-pose figures.
248
+ - The heuristic fallback for unlabelled figures (e.g. a character named
249
+ `"MadisonG9"`) now uses robust outlier rejection before selecting the peak
250
+ perimeter slice.
251
+ - Pass `--figure-type G9F` (or `G9M`, `G8F`, etc.) to force a calibration entry
252
+ when the figure's scene label doesn't contain a gender keyword.
253
+
254
+ ---
255
+
256
+ ## What's New in v2.4.0
257
+
258
+ ### 🎬 Render Endpoints — Trigger and Track DAZ Studio Renders
259
+
260
+ Three new HTTP endpoints let you submit renders, stream live progress, and batch
261
+ render variants — all without blocking the server.
262
+
263
+ **`POST /render`** — submit a render job and get back a `request_id`:
264
+
265
+ ```python
266
+ from dazpy import DazClient, render, RenderVariant, FigureMorphs
267
+
268
+ client = DazClient()
269
+
270
+ # Simple render with default settings
271
+ result = render(client, width=1920, height=1080)
272
+ print(result.output_path)
273
+
274
+ # Render with morph overrides
275
+ result = render(client, width=1920, height=1080,
276
+ figure_morphs=[FigureMorphs("Genesis 9", {"eCTRLSmileSimple": 1.0})])
277
+ ```
278
+
279
+ **`POST /render/batch`** — submit multiple variants in one request:
280
+
281
+ ```python
282
+ from dazpy import render_variants, RenderVariant, FigureMorphs
283
+
284
+ variants = [
285
+ RenderVariant(label="neutral", figure_morphs=[FigureMorphs("Genesis 9", {})]),
286
+ RenderVariant(label="smile", figure_morphs=[FigureMorphs("Genesis 9", {"eCTRLSmileSimple": 1.0})]),
287
+ RenderVariant(label="frown", figure_morphs=[FigureMorphs("Genesis 9", {"eCTRLFrownSimple": 0.8})]),
288
+ ]
289
+ results = render_variants(client, variants, width=1920, height=1080)
290
+ ```
291
+
292
+ **`GET /render/:id/progress`** — SSE stream of render progress events:
293
+
294
+ ```
295
+ data: {"stage":"rendering","progress":0.42,"elapsed_ms":4200}
296
+ data: {"stage":"complete","progress":1.0,"output_path":"/path/to/render.png","elapsed_ms":9800}
297
+ ```
298
+
299
+ **`POST /render/:id/cancel`** — cancel a queued or running render job:
300
+
301
+ ```python
302
+ # Submit without waiting, cancel later
303
+ job = render(client, r"C:\tmp\out.png", width=1920, height=1080, wait=False)
304
+ # ...
305
+ client.cancel_render(job.request_id)
306
+ ```
307
+
308
+ Returns 400 if the `request_id` belongs to a non-render job, so you get a clear
309
+ error rather than silently cancelling the wrong request.
310
+
311
+ ### 🎥 Scene Camera and Light Discovery
312
+
313
+ Two new `DazScene` methods make it easy to look up cameras and lights by label —
314
+ the same label string the `render()` `camera` parameter accepts:
315
+
316
+ ```python
317
+ from dazpy import DazClient, DazScene
318
+ from dazpy._render_api import render
319
+
320
+ client = DazClient()
321
+ scene = DazScene(client)
322
+
323
+ cam = scene.find_camera_by_label("Camera 1")
324
+ print(cam.focal_length, cam.pixels_width)
325
+
326
+ # Use the found camera name directly in a render
327
+ render(client, r"C:\tmp\out.png", camera="Camera 1", width=1920, height=1080)
328
+ ```
329
+
330
+ ### ↩️ Programmatic Undo / Redo
331
+
332
+ Step the DAZ Studio undo stack from Python:
333
+
334
+ ```python
335
+ scene.set_frame(10)
336
+ scene.undo_last() # step back (Ctrl+Z equivalent)
337
+ scene.redo_last() # step forward (Ctrl+Y equivalent)
338
+ ```
339
+
340
+ `undo_last()` / `redo_last()` step the global stack directly. Use the existing
341
+ `scene.undo("label")` context manager when you want to group a series of changes
342
+ into a single undoable operation.
343
+
344
+ ### 🔌 Companion Plugin Route Registration
345
+
346
+ Third-party plugins can now register their own HTTP routes into the DazScript
347
+ Server. Routes are resolved via a `DzScriptServerPane` pointer published on
348
+ `qApp`, so companion plugins discovered at DAZ Studio startup are automatically
349
+ wired in. Registered routes support path parameters (e.g. `/export/:id/status`).
350
+
351
+ ### 🐛 Bug Fixes
352
+
353
+ - Fixed CRT heap mismatch crash when a catch-all dispatcher routed to a
354
+ companion plugin (mixed MSVC runtime allocator boundary).
355
+ - Fixed companion plugin routes silently dropped when registered after the
356
+ server had already started listening.
357
+ - Fixed data race in the catch-all route dispatcher under concurrent requests.
358
+
359
+ ---
360
+
361
+ ## What's New in v2.3.0
362
+
363
+ ### 🔔 Scene Events — Real-Time Push Notifications via SSE
364
+
365
+ Clients can now subscribe to live DAZ Studio scene changes over a persistent HTTP connection using **Server-Sent Events (SSE)**. No more polling for scene state.
366
+
367
+ ```python
368
+ import requests, json
369
+
370
+ token = open(os.path.expanduser("~/.daz3d/dazscriptserver_token.txt")).read().strip()
371
+
372
+ with requests.get(
373
+ "http://127.0.0.1:18811/scene/events",
374
+ headers={"X-API-Token": token},
375
+ stream=True
376
+ ) as r:
377
+ for line in r.iter_lines():
378
+ if line.startswith(b"data: "):
379
+ event = json.loads(line[6:])
380
+ print(event["type"], event["data"])
381
+ ```
382
+
383
+ **Event types:** `node.added`, `node.removed`, `skeleton.added/removed`, `light.added/removed`, `camera.added/removed`, `selection.list_changed`, `selection.primary_changed`, `scene.loading`, `scene.loaded`, `scene.saving`, `scene.saved`, `scene.clear_starting`, `scene.cleared`, `time.changed`, `playback.started`, `playback.finished`, `render.started`, `render.finished`
384
+
385
+ **Filter to only the categories you need:**
386
+ ```
387
+ GET /scene/events?filter=node,selection,scene
388
+ ```
389
+
390
+ All auth, IP whitelist, and security checks apply. The connection stays open until the client disconnects or the server stops. See [Scene Events (SSE)](#-scene-events-sse) for full documentation.
391
+
392
+ ---
393
+
394
+ ## What's New in v2.2.0
395
+
396
+ ### 🦴 DazPose — Snapshot and Restore Poses
397
+
398
+ New module **`dazpy.DazPose`** captures a full skeleton pose in one call and
399
+ restores it later, with linear interpolation between any two stored poses.
400
+
401
+ ```python
402
+ from dazpy import DazScene, DazPose
403
+
404
+ scene = DazScene()
405
+ figure = scene.find_skeleton_by_label("Genesis 9")
406
+
407
+ neutral = DazPose.capture(figure) # snapshot current pose
408
+ # ... dial morphs, animate, etc. ...
409
+ DazPose.blend(neutral, other_pose, t=0.5).apply(figure) # 50 % mix
410
+ ```
411
+
412
+ ### 🎞 DazAnimation — Keyframe Read / Write
413
+
414
+ New module **`dazpy.DazAnimation`** exposes per-bone rotation and translation
415
+ tracks, timeline range queries, frame stepping, and pose baking.
416
+
417
+ ```python
418
+ from dazpy import DazAnimation
419
+
420
+ anim = DazAnimation(figure)
421
+ print(anim.frame_range()) # (start, end)
422
+ anim.bake_pose_to_keyframes(start=0, end=60)
423
+ ```
424
+
425
+ ### 📐 math3 — Vec3, Quat, BoundingBox
426
+
427
+ New module **`dazpy.math3`** provides lightweight value types returned throughout
428
+ the SDK: `Vec3` (positions, translations), `Quat` (rotations), and `BoundingBox`
429
+ (geometry bounds). All support arithmetic operators and round-trip through JSON.
430
+
431
+ ### 🌐 Posed Vertex Export & USD Scene Export
432
+
433
+ **`DazGeometry.vertex_positions_posed()`** returns world-space deformed vertex
434
+ positions with skinning and morphs already applied — enabling downstream export
435
+ pipelines to read the final mesh without re-solving the rig.
436
+
437
+ New example **`docs/examples/scene_to_usd.py`** uses this to export a full live
438
+ DAZ Studio scene to Pixar USD: posed meshes, cameras, lights, PBR materials, and
439
+ strand-based hair as `UsdGeom.BasisCurves`. Pass `--morphs` to write blend
440
+ shapes as `UsdSkel` targets.
441
+
442
+ ```bash
443
+ python scene_to_usd.py --output scene.usda
444
+ python scene_to_usd.py --output scene.usda --morphs
445
+ ```
446
+
447
+ ### 🏃 BVH Motion Capture Support
448
+
449
+ Three new interoperable example scripts:
450
+
451
+ | Script | Purpose |
452
+ |---|---|
453
+ | `bvh_import.py` | Parse a `.bvh` file and apply each frame to a DAZ skeleton |
454
+ | `bvh_discover.py` | Print a figure's bone hierarchy to help build bone maps |
455
+ | `bvh_bone_maps.py` | Canonical BVH ↔ DAZ bone-name tables for G8 / G9 |
456
+
457
+ ### 🗂 New Examples
458
+
459
+ Four additional examples expanding SDK coverage:
460
+
461
+ | Script | What it shows |
462
+ |---|---|
463
+ | `animation_mixing.py` | Blend two stored poses at a configurable weight |
464
+ | `batch_operations.py` | Morph dials, material swaps, and camera moves in one batched request |
465
+ | `geometry_analysis.py` | Mesh vertex count, bounding box, and posed positions |
466
+ | `keyframe_baking.py` | Sample procedural animation and write explicit rotation keyframes |
467
+
468
+ ### 🧪 Test Suite
469
+
470
+ `tests_dazpy.py` (unit) and `tests_dazpy_integration.py` (requires a live DAZ
471
+ Studio instance) now ship in the repo root.
472
+
473
+ ### 📖 Docs & Ergonomics
474
+
475
+ - API reference pages for `DazPose`, `DazAnimation`, `Vec3`, `Quat`, and `BoundingBox`
476
+ - Every script in `docs/examples/` has an `if __name__ == "__main__":` guard and
477
+ argparse `--help`, making all examples safe to import and self-documenting
478
+
479
+ ---
480
+
481
+ ## What's New in v2.0.0
482
+
483
+ v2.0 is a **backward-compatible** internal rewrite focused on correctness and reliability.
484
+ The HTTP API, authentication mechanism, and all settings are unchanged.
485
+
486
+ ### dazpy Python SDK
487
+
488
+ A full-featured Python SDK ships alongside the plugin, installable as a wheel from the
489
+ release page or via `pip install dazpy`. It exposes DAZ Studio's scene graph as typed
490
+ Python objects: `DazScene`, `DazSkeleton`, `DazBone`, `DazMaterial`, `DazMorph`, and more.
491
+ See [dazpy Python SDK](#-dazpy-python-sdk) below for the complete reference.
492
+
493
+ ### Bug Fixes
494
+
495
+ - **Concurrent request race condition fixed** — Under heavy load, more requests than the
496
+ configured maximum could slip through. The active-request counter is now atomically
497
+ managed under a `QMutex`.
498
+ - **DzScript memory safety** — Script objects are always destroyed on the main Qt thread,
499
+ eliminating intermittent crashes from cross-thread deletion.
500
+ - **Signal/slot leak** — The `print()` capture connection is now always explicitly
501
+ disconnected on early-return paths (auth failure, rate limit, etc.), preventing stale
502
+ output from appearing in unrelated responses.
503
+
504
+ ### Architecture
505
+
506
+ - Extracted `AuthenticationService`, `RateLimiterService`, `IPWhitelistService`,
507
+ `MetricsCollector`, and `AsyncRequestManager` from the monolithic pane class
508
+ - `RequestValidator` and `RequestProcessor` replace inline dispatch logic
509
+ - `ServerSettings` / `ServerConfig` centralise all defaults and magic numbers
510
+
511
+ ### New Documentation
512
+
513
+ - `openapi.yaml` — machine-readable OpenAPI 3.0 specification
514
+ - `ARCHITECTURE.md` — component and threading diagrams (Mermaid)
515
+ - `MIGRATION.md` — step-by-step upgrade guide from v1.x
516
+ - `CHANGELOG.md` — full version history
517
+ - `CONTRIBUTING.md` — developer guide
518
+
519
+ See the [full migration guide](MIGRATION.md) for upgrade steps and rollback instructions.
520
+
521
+ ---
522
+
523
+ ## What's New in v1.3.0
524
+
525
+ ### ⚡ Async Script Execution
526
+
527
+ Long-running operations (renders, exports, batch jobs) no longer need to block the HTTP connection:
528
+
529
+ - **`POST /execute/async`** — Submit any inline script asynchronously; returns a `request_id` immediately
530
+ - **`POST /scripts/:id/async`** — Submit a registered script asynchronously
531
+ - **`GET /requests/:id/status`** — Poll for progress (`queued`, `running`, `completed`, `failed`, `cancelled`)
532
+ - **`GET /requests/:id/result`** — Fetch the final result; supports `?wait=true` to long-poll until complete
533
+ - **`DELETE /requests/:id`** — Cancel a queued or running request
534
+ - **`GET /requests`** — List all active and recently completed requests
535
+
536
+ **Cancellation:** Queued requests are cancelled immediately. Running requests set a cancel flag and call `killRender()` — DAZ Studio honours the flag when the script returns.
537
+
538
+ **TTL:** Completed, failed, and cancelled requests are automatically purged after 1 hour (cleanup timer fires every 5 minutes).
539
+
540
+ ---
541
+
542
+ ## What's New in v1.2.0
543
+
544
+ ### 🔒 Security Enhancements
545
+ - **IP Whitelist** - Restrict access to specific IP addresses
546
+ - **Per-IP Rate Limiting** - Prevent brute force attacks with sliding window limits
547
+ - **Configurable Limits** - Adjust concurrent requests, body size, and script length
548
+
549
+ ### 📦 Script Registry
550
+ - **Register Once, Call Many** - Upload scripts by name, execute by ID without retransmission
551
+ - **Session-Based Storage** - In-memory registry with register, list, execute, and delete endpoints
552
+ - **Auto-Recovery** - Returns 404 on stale IDs so clients can re-register after restarts
553
+
554
+ ### ✨ Usability Improvements
555
+ - **Active Request Counter** - Real-time display of concurrent requests (X/max)
556
+ - **Auto-Start Option** - Start server automatically when pane opens
557
+ - **Better Error Messages** - Descriptive errors with actionable guidance
558
+ - **More Examples** - Added JavaScript/Node.js and PowerShell client examples
559
+
560
+ ### 💾 Persistent Configuration
561
+ All settings (security, limits, monitoring) are saved via QSettings and restored between sessions.
562
+
563
+ ---
564
+
565
+ ## Requirements
566
+
567
+ - **DAZ Studio 4.5+** (for running the plugin)
568
+ - **DAZ Studio 4.5+ SDK** (for building from source)
569
+ - **CMake 3.5+**
570
+ - **Compiler:**
571
+ - Windows: Visual Studio 2019 or 2022 (MSVC)
572
+ - macOS: Xcode / clang with libc++
573
+
574
+ ---
575
+
576
+ ## 🐍 dazpy Python SDK
577
+
578
+ `dazpy` is a Python SDK that drives DAZ Studio through the Script Server.
579
+ It exposes the DAZ Studio scene graph as typed Python objects so you can write
580
+ automation code without authoring DazScript by hand.
581
+
582
+ ### Installation
583
+
584
+ Download the `.whl` file from the [latest release](https://github.com/bluemoonfoundry/daz-script-server/releases/latest) and install it:
585
+
586
+ ```bash
587
+ pip install dazpy-2.6.0-py3-none-any.whl
588
+ ```
589
+
590
+ Or install directly from the repo for development:
591
+
592
+ ```bash
593
+ pip install -e .
594
+ ```
595
+
596
+ **Requirements:** Python 3.10+, `requests` (installed automatically).
597
+
598
+ ---
599
+
600
+ ### Jupyter Notebook
601
+
602
+ The repo ships an interactive Jupyter notebook (`notebooks/dazpy_intro.ipynb`) that
603
+ covers the full dazpy API with runnable examples. Launcher scripts handle installing
604
+ Jupyter and opening the notebook in one step.
605
+
606
+ **Prerequisites:** DAZ Studio must be running with the DazScriptServer plugin active before opening the notebook.
607
+
608
+ **Launch (macOS / Linux / Git Bash on Windows):**
609
+
610
+ ```bash
611
+ ./notebook.sh
612
+ ```
613
+
614
+ **Launch (Windows PowerShell):**
615
+
616
+ ```powershell
617
+ .\notebook.ps1
618
+ ```
619
+
620
+ Both scripts install `dazpy` (editable) and `notebook` if they are not already present,
621
+ then open Jupyter in the `notebooks/` directory.
622
+
623
+ **Notebook sections:**
624
+
625
+ | Section | What it covers |
626
+ |---|---|
627
+ | 1. Server health | `client.health()` — verify the plugin is reachable |
628
+ | 2. Scene overview | `scene.num_nodes()`, `scene.nodes()` — list everything in the scene |
629
+ | 3. Find a figure / read a bone | `find_skeleton_by_label()`, `find_bone()`, `bone.local_euler` |
630
+ | 4. Raw DazScript | `client.execute()` — run arbitrary DazScript when the SDK doesn't cover it |
631
+ | 5. Error handling | Full exception hierarchy with `try/except` examples |
632
+ | 6. API browser | `dir(obj)` one-liner to list any object's public API surface |
633
+ | 7. Jupyter introspection | `obj?` / `obj??` inline help for any dazpy object |
634
+ | 8. Interactive bone rotator | `ipywidgets` sliders driving `bone.set_local_rotation()` in real time |
635
+ | 9. Morph / property explorer | List, search, and set morphs with `morph_values()` / `set_morph_values()` |
636
+ | 10. Scene tree pretty-printer | `scene.node_tree()` rendered as an indented tree with box-drawing characters |
637
+
638
+ **Interactive bone rotator** (section 8) requires ipywidgets:
639
+
640
+ ```bash
641
+ pip install ipywidgets
642
+ ```
643
+
644
+ ---
645
+
646
+ ### Connecting to DAZ Studio
647
+
648
+ ```python
649
+ from dazpy import DazClient, DazScene
650
+
651
+ # Default: 127.0.0.1:18811, token auto-loaded from ~/.daz3d/dazscriptserver_token.txt
652
+ client = DazClient()
653
+
654
+ # Explicit connection
655
+ client = DazClient(host="127.0.0.1", port=18811, token="your-token", timeout=30.0)
656
+
657
+ scene = DazScene(client) # or just DazScene() to use a default client
658
+ ```
659
+
660
+ `DazClient` also exposes `status()`, `health()`, and `metrics()` for server introspection.
661
+
662
+ ---
663
+
664
+ ### Scene Graph
665
+
666
+ `DazScene` is the primary entry point. All methods execute DazScript on the server and
667
+ return typed Python objects.
668
+
669
+ ```python
670
+ scene = DazScene()
671
+
672
+ # Node counts
673
+ print(scene.num_nodes()) # total nodes
674
+ print(scene.num_skeletons()) # figures only
675
+
676
+ # Get all nodes (returns DazSkeleton / DazCamera / DazLight / DazNode as appropriate)
677
+ for node in scene.nodes():
678
+ print(node.name(), node.label(), node.position())
679
+
680
+ # Find nodes
681
+ node = scene.find_node("Genesis9")
682
+ node = scene.find_node_by_label("Genesis 9")
683
+
684
+ # Scene tree (hierarchy as nested dicts)
685
+ tree = scene.node_tree()
686
+
687
+ # All transforms in one round-trip
688
+ transforms = scene.all_node_transforms()
689
+
690
+ # Selection
691
+ selected = scene.selected_nodes()
692
+ primary = scene.primary_selection()
693
+ scene.set_primary_selection(node)
694
+ scene.select_all(on=False)
695
+ ```
696
+
697
+ #### DazNode
698
+
699
+ Every scene object is a `DazNode`. Common operations:
700
+
701
+ ```python
702
+ node = scene.find_node_by_label("Cube")
703
+
704
+ # Transforms (world-space)
705
+ pos = node.position() # {"x": 0.0, "y": 0.0, "z": 0.0}
706
+ rot = node.rotation() # {"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0}
707
+ node.set_position(10, 0, 0)
708
+ node.set_rotation(0, 45, 0) # degrees
709
+
710
+ # Local-space transforms
711
+ lpos = node.local_position()
712
+ lrot = node.local_rotation()
713
+ node.set_local_position(0, 100, 0)
714
+ node.set_local_rotation(0, 0, 30)
715
+
716
+ # Scale
717
+ gs = node.general_scale() # uniform scale float
718
+ axes = node.scale() # {"x": 1.0, "y": 1.0, "z": 1.0, "general": 1.0}
719
+
720
+ # Visibility and scene membership
721
+ node.is_visible()
722
+ node.is_visible_in_render()
723
+ node.set_visible_in_render(False)
724
+ node.is_visible_in_viewport()
725
+ node.set_visible_in_viewport(True)
726
+ node.is_in_scene()
727
+ node.is_root()
728
+
729
+ # Bounding box
730
+ bb = node.bounding_box() # {"min": {"x":…, "y":…, "z":…}, "max": {…}}
731
+
732
+ # Selection
733
+ node.is_selected()
734
+ node.select(True)
735
+
736
+ # Generic property access (fallback for anything not wrapped)
737
+ val = node.get_property("Some Property Label")
738
+ node.set_property("Some Property Label", 1.0)
739
+ ```
740
+
741
+ ---
742
+
743
+ ### Figures — Skeletons and Bones
744
+
745
+ Figures in DAZ Studio are `DzSkeleton` instances. Posing a figure means setting
746
+ bone rotations.
747
+
748
+ ```python
749
+ # Find a figure
750
+ figure = scene.find_skeleton_by_label("Genesis 9")
751
+ # or: scene.find_skeleton("Genesis9"), scene.skeletons()
752
+
753
+ # Bones
754
+ all_bones = figure.bones() # list[DazBone]
755
+ forearm = figure.find_bone("r_forearm") # Genesis 9; use figure.bones() to list names
756
+ forearm = figure.find_bone_by_label("Right Forearm")
757
+ n = figure.num_bones()
758
+
759
+ # Follow target (clothing/hair fitted to a figure)
760
+ target = figure.follow_target() # DazSkeleton | None
761
+
762
+ # DazBone — pose by setting local Euler angles (degrees)
763
+ rot = forearm.local_rotation() # {"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0}
764
+ forearm.set_local_rotation(0, 0, 45) # bend forearm 45° on Z axis
765
+
766
+ pos = forearm.local_position()
767
+ order = forearm.rotation_order() # "XYZ", "ZYX", etc.
768
+ skel = forearm.get_skeleton() # back-reference to the figure
769
+
770
+ # Example: pose a figure's arm
771
+ with scene.undo("Pose arm"):
772
+ figure.find_bone("lShldrBend").set_local_rotation(0, 0, -60)
773
+ figure.find_bone("lForeArm").set_local_rotation(0, 0, -30)
774
+ ```
775
+
776
+ `scene.nodes()` automatically returns `DazSkeleton` for figure nodes so you can
777
+ iterate and call `bones()` without an extra lookup.
778
+
779
+ ---
780
+
781
+ ### Morphs
782
+
783
+ Morphs control body shapes, facial expressions, and clothing fits.
784
+
785
+ ```python
786
+ figure = scene.find_skeleton_by_label("Genesis 9")
787
+
788
+ # List all modifiers on a node
789
+ for mod in figure.modifiers():
790
+ print(mod.name())
791
+
792
+ # Morphs only (DzMorph subclass)
793
+ for morph in figure.morphs():
794
+ print(morph.name(), morph.value())
795
+
796
+ # Find and adjust a specific morph
797
+ smile = figure.find_modifier("PHMSmileOpen")
798
+ print(smile.value()) # 0.0–1.0
799
+ smile.set_value(0.8)
800
+
801
+ # Or via DazMorph properties
802
+ morph = figure.find_modifier("BodyMorphHeavy")
803
+ morph.value = 0.5 # property-style write
804
+ print(morph.min, morph.max)
805
+ ```
806
+
807
+ ---
808
+
809
+ ### Materials
810
+
811
+ ```python
812
+ node = scene.find_node_by_label("Genesis 9 Skin")
813
+
814
+ # All materials on a node
815
+ for mat in node.materials():
816
+ print(mat.name(), mat.diffuse_color())
817
+
818
+ # Find a specific material
819
+ skin = node.find_material("Torso")
820
+
821
+ # Color and opacity
822
+ color = skin.diffuse_color() # {"r": 255, "g": 220, "b": 200}
823
+ skin.set_diffuse_color(255, 210, 190)
824
+ print(skin.opacity()) # 1.0
825
+ print(skin.color_map()) # texture filename or None
826
+ print(skin.is_opaque())
827
+ print(skin.smoothing_angle())
828
+ print(skin.is_smoothing_on())
829
+
830
+ # Generic property fallback
831
+ skin.set_property("Glossy Reflectivity", 0.3)
832
+ ```
833
+
834
+ ---
835
+
836
+ ### Cameras and Lights
837
+
838
+ ```python
839
+ # Cameras
840
+ for cam in scene.cameras():
841
+ print(cam.name(), cam.focal_length(), cam.is_view_camera())
842
+
843
+ cam = scene.cameras()[0]
844
+ cam.focal_length() # mm
845
+ cam.frame_width()
846
+ cam.focal_distance()
847
+ cam.aspect_width()
848
+ cam.aspect_height()
849
+ cam.pixels_width()
850
+ cam.pixels_height()
851
+ cam.near_clipping_plane()
852
+ cam.far_clipping_plane()
853
+ cam.focal_point() # {"x": …, "y": …, "z": …}
854
+ cam.aim_at(0, 150, 0) # aim camera at world point
855
+
856
+ # Lights
857
+ for light in scene.lights():
858
+ print(light.name(), light.is_on(), light.is_directional())
859
+
860
+ light = scene.lights()[0]
861
+ light.is_on()
862
+ light.is_directional()
863
+ light.is_area_light()
864
+ light.direction() # {"x": …, "y": …, "z": …} (directional lights only)
865
+ light.set_color(1.0, 0.9, 0.8)
866
+ ```
867
+
868
+ ---
869
+
870
+ ### Scene I/O and Timeline
871
+
872
+ ```python
873
+ # Load and save
874
+ scene.load("/path/to/scene.duf") # merge mode (does not clear existing scene)
875
+ scene.save("/path/to/output.duf")
876
+ scene.save_copy("/path/to/backup.duf") # write copy; filename/dirty flag unchanged
877
+ print(scene.filename())
878
+ print(scene.needs_save())
879
+
880
+ # Timeline
881
+ print(scene.frame())
882
+ scene.set_frame(24)
883
+ print(scene.play_range()) # {"start": 0, "end": 240}
884
+ scene.set_play_range(0, 120)
885
+ scene.set_anim_range(0, 240)
886
+ print(scene.is_playing())
887
+ scene.loop_playback(True)
888
+ ```
889
+
890
+ ---
891
+
892
+ ### Geometry
893
+
894
+ `DazGeometry` provides access to the raw mesh data of a node's shape.
895
+
896
+ ```python
897
+ node = scene.find_node_by_label("My Prop")
898
+ geo = node.geometry()
899
+
900
+ print(geo.num_vertices())
901
+ print(geo.num_faces())
902
+ print(geo.subdivision_level())
903
+ print(geo.tris_count(), geo.quads_count())
904
+
905
+ # Vertices (chunked — returns slice starting at offset)
906
+ verts = geo.vertices(start=0, count=1000)
907
+ # {"total": 5000, "start": 0, "vertices": [[x,y,z], …]}
908
+
909
+ # Faces (vertex indices per polygon)
910
+ faces = geo.face_vertex_indices(start=0, count=1000)
911
+ # {"total": 4800, "start": 0, "facets": [[v0,v1,v2,v3], …]}
912
+
913
+ # Normals
914
+ normals = geo.normals(start=0, count=5000)
915
+
916
+ # UV sets
917
+ print(geo.uv_set_count())
918
+ uvs = geo.uv_positions(uv_set=0, start=0, count=5000)
919
+
920
+ # Groups
921
+ print(geo.face_group_names())
922
+ print(geo.material_group_names())
923
+ ```
924
+
925
+ ---
926
+
927
+ ### Advanced: Batch, Undo, and Async
928
+
929
+ #### Undo Groups
930
+
931
+ Wrap multiple changes in a single undo step visible in DAZ Studio's Edit menu:
932
+
933
+ ```python
934
+ with scene.undo("Apply pose"):
935
+ figure.find_bone("lShldrBend").set_local_rotation(0, 0, -60)
936
+ figure.find_bone("rShldrBend").set_local_rotation(0, 0, 60)
937
+ figure.find_modifier("PHMSmileOpen").set_value(0.5)
938
+ ```
939
+
940
+ #### Batch Execution
941
+
942
+ `Batch` groups multiple DazScript snippets into a single HTTP round-trip:
943
+
944
+ ```python
945
+ from dazpy import Batch
946
+
947
+ batch = Batch(client)
948
+ batch.add("(function(){ return Scene.getNumNodes(); })()")
949
+ batch.add("(function(){ return Scene.getNumSkeletons(); })()")
950
+ results = batch.execute() # one HTTP request, list of ExecutionResult
951
+ print(results[0].value, results[1].value)
952
+
953
+ # BatchFuture — fire and collect later
954
+ future = batch.submit_async() # returns BatchFuture
955
+ # ... do other work ...
956
+ results = future.collect()
957
+ ```
958
+
959
+ #### Long-Running Operations
960
+
961
+ `execute_long` wraps the async submit/poll/collect cycle for operations like renders:
962
+
963
+ ```python
964
+ from dazpy import execute_long
965
+
966
+ result = execute_long(
967
+ client,
968
+ script="App.getRenderMgr().doRender(); return 'done';",
969
+ poll_interval=2.0,
970
+ timeout=300.0,
971
+ )
972
+ print(result.value)
973
+ ```
974
+
975
+ ---
976
+
977
+ ### Error Handling
978
+
979
+ All dazpy exceptions are in `dazpy.exceptions`:
980
+
981
+ ```python
982
+ from dazpy import DazClient, DazScene
983
+ from dazpy.exceptions import (
984
+ ConnectionError, # DAZ Studio not reachable
985
+ AuthenticationError, # bad token or IP blocked
986
+ ScriptSyntaxError, # DazScript parse error
987
+ ScriptRuntimeError, # DazScript runtime exception
988
+ TimeoutError, # HTTP request timed out
989
+ NodeNotFoundError, # scene.find_node / find_skeleton raised
990
+ )
991
+
992
+ try:
993
+ scene.find_skeleton_by_label("NonExistent")
994
+ except NodeNotFoundError as e:
995
+ print(e)
996
+
997
+ try:
998
+ client.execute("this is not valid { script")
999
+ except ScriptSyntaxError as e:
1000
+ print(e.request_id) # correlate with server log
1001
+ ```
1002
+
1003
+ `ScriptRuntimeError` and `ScriptSyntaxError` both carry a `request_id` attribute
1004
+ that matches the 8-character ID in the server's request log.
1005
+
1006
+ ---
1007
+
1008
+ ### Testing dazpy
1009
+
1010
+ The SDK ships with two test suites:
1011
+
1012
+ ```bash
1013
+ # Unit tests — no DAZ Studio needed (mock-based)
1014
+ python tests_dazpy.py
1015
+
1016
+ # Integration tests — skip gracefully if server is down
1017
+ python tests_dazpy_integration.py
1018
+ ```
1019
+
1020
+ Tests requiring a live DAZ Studio session are gated with `@skip_no_daz` and
1021
+ silently skipped when the `Scene` global is unavailable.
1022
+
1023
+ ---
1024
+
1025
+ ## Getting the Plugin
1026
+
1027
+ There are two options for getting the plugin. For most users, Option A, downloading a pre-built release, is the easiest route.
1028
+
1029
+ ### A. Download a pre-built release
1030
+
1031
+ 1. Browse to the latest stable release page: https://github.com/bluemoonfoundry/daz-script-server/releases/latest
1032
+ 2. Scroll down to the Assets section. Each release includes:
1033
+ - `DazScriptServer.dll` / `DazScriptServer.dylib` — the plugin
1034
+ - `dazpy-*.whl` — the Python SDK wheel (`pip install dazpy-*.whl`)
1035
+ 3. Download `DazScriptServer.dll` and copy it to the plugins folder in your DAZ Studio installation:
1036
+ - **Windows:** `C:\Program Files\DAZ 3D\DAZStudio4\plugins\`
1037
+ - **macOS:** `/Applications/DAZ 3D/DAZStudio4/plugins/`
1038
+ - Unsure where DAZ Studio is installed? Right-click its icon → Properties → Open File Location.
1039
+
1040
+ ### B. Building it yourself from source
1041
+
1042
+ 1. **Download the DAZ Studio SDK** from the [DAZ Developer portal](https://www.daz3d.com/daz-studio-4-5-sdk)
1043
+
1044
+ 2. **Configure with CMake:**
1045
+ ```bash
1046
+ cmake -B build -S . -DDAZ_SDK_DIR="C:/path/to/DAZStudio4.5+ SDK"
1047
+ ```
1048
+
1049
+ 3. **Build:**
1050
+ ```bash
1051
+ cmake --build build --config Release
1052
+ ```
1053
+
1054
+ Output: `build/plugin/Release/DazScriptServer.dll` (Windows) or `build/plugin/DazScriptServer.dylib` (macOS)
1055
+
1056
+ ### build.sh convenience script
1057
+
1058
+ `build.sh` auto-detects CMake and loads environment variables from `.env`.
1059
+
1060
+ **Commands** (first positional argument; defaults to `build`):
1061
+
1062
+ | Command | Description |
1063
+ |---|---|
1064
+ | `build` | Configure (if needed) and build (default) |
1065
+ | `install` | Build and copy plugin to DAZ Studio plugins folder |
1066
+ | `clean` | Delete the build directory and exit |
1067
+ | `release <tag>` | Build plugin + dazpy wheel, create a GitHub release and attach both |
1068
+
1069
+ **Options** (flags, combinable with any command):
1070
+
1071
+ | Option | Description |
1072
+ |---|---|
1073
+ | `--clean` | Wipe the build directory before building |
1074
+ | `--reconfigure` | Force CMake configure even if a cache already exists |
1075
+ | `--debug` | Build Debug config instead of Release |
1076
+ | `--verbose` | Pass `--verbose` to the CMake build step |
1077
+ | `--title <title>` | Release title (`release` only; defaults to tag) |
1078
+ | `--notes <text>` | Release notes text (`release` only) |
1079
+ | `--update` | Update an existing release instead of creating a new one (`release` only) |
1080
+ | `--no-wheel` | Skip building the dazpy wheel (`release` only) |
1081
+ | `--help` | Show usage and exit |
1082
+
1083
+ ```bash
1084
+ ./build.sh # build
1085
+ ./build.sh build --clean --debug
1086
+ ./build.sh install --clean # DAZ Studio must not be running
1087
+ ./build.sh clean
1088
+ ./build.sh release v1.3.0 --title "v1.3.0" --notes "Bug fixes"
1089
+ ./build.sh release v1.3.0 --update # replace assets on existing release
1090
+ ./build.sh release v1.3.0 --no-wheel # plugin DLL only, skip wheel
1091
+ ```
1092
+
1093
+ ### Installation
1094
+
1095
+ Copy the built plugin to DAZ Studio's plugins folder:
1096
+
1097
+ - **Windows:** `C:\Program Files\DAZ 3D\DAZStudio4\plugins\`
1098
+ - **macOS:** `/Applications/DAZ 3D/DAZStudio4/plugins/`
1099
+
1100
+ **Or build and install in one step** (set `DAZ_STUDIO_EXE_DIR` in `.env` first):
1101
+ ```bash
1102
+ ./build.sh install
1103
+
1104
+ # Clean build + install
1105
+ ./build.sh install --clean
1106
+ ```
1107
+
1108
+ > **Note:** `install` requires DAZ Studio to be closed. The script detects if it is running and exits with a clear error rather than failing at link time.
1109
+
1110
+ ---
1111
+
1112
+ ## Starting the Server
1113
+
1114
+ 1. Open DAZ Studio
1115
+ 2. Go to **Window → Panes → Daz Script Server**
1116
+ 3. Configure settings (see [Configuration](#-configuration) below)
1117
+ 4. Click **Start Server**
1118
+
1119
+ The server status will show "Running" with active requests counter when started successfully.
1120
+
1121
+ ---
1122
+
1123
+ ## ⚙️ Configuration
1124
+
1125
+ All settings are persisted via QSettings and restored between DAZ Studio sessions.
1126
+
1127
+ ### Server Settings
1128
+
1129
+ | Setting | Default | Range | Description |
1130
+ |---------|---------|-------|-------------|
1131
+ | **Host** | `127.0.0.1` | - | IP address to bind to (`127.0.0.1` for localhost only, `0.0.0.0` for all interfaces) |
1132
+ | **Port** | `18811` | 1024-65535 | Port number |
1133
+ | **Timeout** | `30` seconds | 5-300 | Script execution timeout |
1134
+ | **Auto-Start** | Disabled | - | Start server automatically when pane opens |
1135
+
1136
+ ### 🔐 Authentication
1137
+
1138
+ | Setting | Default | Description |
1139
+ |---------|---------|-------------|
1140
+ | **Enable Authentication** | ✅ Enabled | Token-based authentication using cryptographically secure tokens (128-bit) |
1141
+
1142
+ **Token Security:**
1143
+ - Auto-generated using OS crypto APIs (Windows: CryptoAPI, macOS/Linux: `/dev/urandom`)
1144
+ - Stored in `~/.daz3d/dazscriptserver_token.txt`
1145
+ - File permissions automatically set to `chmod 600` (owner-only) on Unix/macOS
1146
+ - Copy token from UI using the **Copy** button
1147
+ - Regenerate with **Regenerate** button if compromised
1148
+ - ⚠️ **Disable at your own risk** - only on trusted networks
1149
+
1150
+ ### 🛡️ IP Whitelist
1151
+
1152
+ | Setting | Default | Description |
1153
+ |---------|---------|-------------|
1154
+ | **Enable IP Whitelist** | ❌ Disabled | Restrict access to specific IP addresses |
1155
+ | **Allowed IPs** | `127.0.0.1` | Comma-separated list (e.g., `127.0.0.1, 192.168.1.100`) |
1156
+
1157
+ - Exact match only (wildcards not supported in v1.2.0)
1158
+ - Blocked IPs receive HTTP 403 Forbidden
1159
+ - Checked before authentication (efficient)
1160
+ - Essential for network-exposed deployments
1161
+
1162
+ ### ⏱️ Rate Limiting
1163
+
1164
+ | Setting | Default | Range | Description |
1165
+ |---------|---------|-------|-------------|
1166
+ | **Enable Rate Limiting** | ❌ Disabled | - | Per-IP rate limiting to prevent abuse |
1167
+ | **Max Requests** | `60` | 10-1000 | Maximum requests per time window |
1168
+ | **Time Window** | `60` seconds | 10-300 | Time window in seconds |
1169
+
1170
+ - Uses sliding window algorithm for accuracy
1171
+ - Separate tracking per IP address
1172
+ - Exceeded IPs receive HTTP 429 Too Many Requests
1173
+ - Logs violations with timestamp and client IP
1174
+
1175
+ ### 🎛️ Advanced Limits
1176
+
1177
+ | Setting | Default | Range | Description |
1178
+ |---------|---------|-------|-------------|
1179
+ | **Max Concurrent Requests** | `10` | 5-50 | Maximum simultaneous requests |
1180
+ | **Max Request Body Size** | `5` MB | 1-50 | Maximum request body size |
1181
+ | **Max Script Text Length** | `1024` KB (1 MB) | 100-10240 | Maximum inline script size |
1182
+
1183
+ **Protection:**
1184
+ - Prevents resource exhaustion
1185
+ - Returns appropriate HTTP errors (413, 429)
1186
+ - Use `scriptFile` for larger scripts
1187
+
1188
+ ### 📊 Monitoring
1189
+
1190
+ - **Active Request Counter** - Real-time display: "Active Requests: 2 / 10"
1191
+ - **Request Log** - Detailed log with:
1192
+ - Timestamps (HH:mm:ss)
1193
+ - Client IP addresses
1194
+ - Status codes (OK, ERR, WARN, AUTH FAILED, BLOCKED, RATE LIMIT)
1195
+ - Duration (milliseconds)
1196
+ - Request IDs (8-character UUID)
1197
+ - Script identifiers
1198
+ - **Log Management** - Maximum 1000 lines, auto-remove old entries, "Clear Log" button
1199
+
1200
+ **Important:** Configuration changes (except auto-start) require stopping and restarting the server to take effect.
1201
+
1202
+ ---
1203
+
1204
+ ## 📡 API Reference
1205
+
1206
+ ### Base URL
1207
+ ```
1208
+ http://127.0.0.1:18811
1209
+ ```
1210
+
1211
+ ### Authentication
1212
+
1213
+ All endpoints except `/status`, `/health`, and `/metrics` require authentication when enabled:
1214
+
1215
+ **Header Options:**
1216
+ - `X-API-Token: YOUR_TOKEN_HERE`
1217
+ - `Authorization: Bearer YOUR_TOKEN_HERE`
1218
+
1219
+ ### HTTP Status Codes
1220
+
1221
+ | Code | Meaning |
1222
+ |------|---------|
1223
+ | `200` | Success (check `success` field in response) |
1224
+ | `400` | Bad Request (malformed JSON, invalid parameters) |
1225
+ | `401` | Unauthorized (missing or invalid token) |
1226
+ | `403` | Forbidden (IP not whitelisted) |
1227
+ | `413` | Payload Too Large (request body exceeds limit) |
1228
+ | `429` | Too Many Requests (concurrent or rate limit exceeded) |
1229
+
1230
+ ---
1231
+
1232
+ ### `GET /status`
1233
+
1234
+ **Purpose:** Check if server is running
1235
+ **Authentication:** Not required
1236
+
1237
+ **Response:**
1238
+ ```json
1239
+ {
1240
+ "running": true,
1241
+ "version": "2.0.0"
1242
+ }
1243
+ ```
1244
+
1245
+ ---
1246
+
1247
+ ### `GET /health`
1248
+
1249
+ **Purpose:** Health check for monitoring and load balancers
1250
+ **Authentication:** Not required
1251
+
1252
+ **Response:**
1253
+ ```json
1254
+ {
1255
+ "status": "ok",
1256
+ "version": "2.0.0",
1257
+ "running": true,
1258
+ "auth_enabled": true,
1259
+ "active_requests": 2,
1260
+ "uptime_seconds": 3600
1261
+ }
1262
+ ```
1263
+
1264
+ ---
1265
+
1266
+ ### `GET /metrics`
1267
+
1268
+ **Purpose:** Request statistics and performance tracking
1269
+ **Authentication:** Not required
1270
+
1271
+ **Response:**
1272
+ ```json
1273
+ {
1274
+ "total_requests": 1523,
1275
+ "successful_requests": 1489,
1276
+ "failed_requests": 28,
1277
+ "auth_failures": 6,
1278
+ "active_requests": 2,
1279
+ "uptime_seconds": 86400,
1280
+ "success_rate_percent": 97.77
1281
+ }
1282
+ ```
1283
+
1284
+ **Note:** Counters persist across DAZ Studio restarts (saved to QSettings).
1285
+
1286
+ ---
1287
+
1288
+ ### `POST /execute`
1289
+
1290
+ **Purpose:** Execute a DazScript and return the result
1291
+ **Authentication:** Required (if enabled)
1292
+
1293
+ **Request Body:**
1294
+
1295
+ **Option 1: Inline script**
1296
+ ```json
1297
+ {
1298
+ "script": "(function(){ return Scene.getNumNodes(); })()",
1299
+ "args": { "key": "value" }
1300
+ }
1301
+ ```
1302
+
1303
+ **Option 2: Script file**
1304
+ ```json
1305
+ {
1306
+ "scriptFile": "/absolute/path/to/script.dsa",
1307
+ "args": { "key": "value" }
1308
+ }
1309
+ ```
1310
+
1311
+ **Parameters:**
1312
+
1313
+ | Field | Type | Required | Description |
1314
+ |-------|------|----------|-------------|
1315
+ | `script` | string | one of | Inline DazScript code (max configurable, default 1MB) |
1316
+ | `scriptFile` | string | one of | Absolute path to `.dsa` file |
1317
+ | `args` | object | optional | Arguments accessible via `getArguments()[0]` |
1318
+
1319
+ **Note:** If both `script` and `scriptFile` are provided, `scriptFile` takes precedence.
1320
+
1321
+ **Response:**
1322
+ ```json
1323
+ {
1324
+ "success": true,
1325
+ "result": 42,
1326
+ "output": ["line 1", "line 2"],
1327
+ "error": null,
1328
+ "request_id": "a3f2b891"
1329
+ }
1330
+ ```
1331
+
1332
+ **Response Fields:**
1333
+
1334
+ | Field | Description |
1335
+ |-------|-------------|
1336
+ | `success` | `true` if script executed without error |
1337
+ | `result` | Script's return value, `null` on error |
1338
+ | `output` | Lines printed via `print()` (max 10,000) |
1339
+ | `error` | Error message with line number, or `null` |
1340
+ | `request_id` | Unique 8-character ID for log correlation |
1341
+
1342
+ **Processing Order:**
1343
+ 1. Concurrent limit check
1344
+ 2. IP whitelist check (if enabled)
1345
+ 3. Rate limit check (if enabled)
1346
+ 4. Body size validation
1347
+ 5. Authentication (if enabled)
1348
+ 6. Input validation
1349
+ 7. Script execution
1350
+
1351
+ ---
1352
+
1353
+ ## 📦 Script Registry
1354
+
1355
+ The script registry allows you to register scripts once and execute them by name/ID on subsequent requests, avoiding retransmission of large script bodies.
1356
+
1357
+ **Key Features:**
1358
+ - Session-only storage (cleared on DAZ Studio restart)
1359
+ - Clients should re-register on HTTP 404
1360
+ - Register, list, execute by ID, and delete operations
1361
+ - Same response format as `/execute`
1362
+
1363
+ ---
1364
+
1365
+ ### `POST /scripts/register`
1366
+
1367
+ **Purpose:** Register or update a named script
1368
+ **Authentication:** Required (if enabled)
1369
+
1370
+ **Request:**
1371
+ ```json
1372
+ {
1373
+ "name": "scene-info",
1374
+ "description": "Return scene node count",
1375
+ "script": "(function(){ return { nodes: Scene.getNumNodes() }; })()"
1376
+ }
1377
+ ```
1378
+
1379
+ **Parameters:**
1380
+
1381
+ | Field | Type | Required | Description |
1382
+ |-------|------|----------|-------------|
1383
+ | `name` | string | yes | Script ID: 1-64 chars, `[A-Za-z0-9_-]` only |
1384
+ | `script` | string | yes | DazScript source code |
1385
+ | `description` | string | no | Human-readable description |
1386
+
1387
+ **Response:**
1388
+ ```json
1389
+ {
1390
+ "success": true,
1391
+ "id": "scene-info",
1392
+ "registered_at": "2026-03-27T10:15:00",
1393
+ "updated": false
1394
+ }
1395
+ ```
1396
+
1397
+ **Note:** Re-registering an existing name overwrites the script and sets `updated: true`.
1398
+
1399
+ ---
1400
+
1401
+ ### `GET /scripts`
1402
+
1403
+ **Purpose:** List all registered scripts
1404
+ **Authentication:** Required (if enabled)
1405
+
1406
+ **Response:**
1407
+ ```json
1408
+ {
1409
+ "scripts": [
1410
+ {
1411
+ "id": "scene-info",
1412
+ "description": "Return scene node count",
1413
+ "registered_at": "2026-03-27T10:15:00"
1414
+ }
1415
+ ],
1416
+ "count": 1
1417
+ }
1418
+ ```
1419
+
1420
+ ---
1421
+
1422
+ ### `POST /scripts/:id/execute`
1423
+
1424
+ **Purpose:** Execute a registered script by ID
1425
+ **Authentication:** Required (if enabled)
1426
+
1427
+ **Request:**
1428
+ ```json
1429
+ {
1430
+ "args": { "label": "FN Ethan" }
1431
+ }
1432
+ ```
1433
+
1434
+ **Response:** Same format as `POST /execute`
1435
+
1436
+ **Error Response (404):**
1437
+ ```json
1438
+ {
1439
+ "success": false,
1440
+ "error": "Script not found: 'scene-info'"
1441
+ }
1442
+ ```
1443
+
1444
+ **Handling 404:** Client should detect 404, re-register scripts, then retry.
1445
+
1446
+ ---
1447
+
1448
+ ### `DELETE /scripts/:id`
1449
+
1450
+ **Purpose:** Remove a script from the registry
1451
+ **Authentication:** Required (if enabled)
1452
+
1453
+ **Response:**
1454
+ ```json
1455
+ {
1456
+ "success": true,
1457
+ "id": "scene-info"
1458
+ }
1459
+ ```
1460
+
1461
+ **Error Response (404):**
1462
+ ```json
1463
+ {
1464
+ "success": false,
1465
+ "error": "Script not found: 'scene-info'"
1466
+ }
1467
+ ```
1468
+
1469
+ ---
1470
+
1471
+ ## ⚡ Async Operations
1472
+
1473
+ For long-running scripts (renders, exports, batch jobs), use the async endpoints to avoid blocking the HTTP connection. Scripts still execute serially on DAZ Studio's main thread — async means the HTTP response is returned immediately while execution is queued.
1474
+
1475
+ **Typical async workflow:**
1476
+
1477
+ ```python
1478
+ import requests, time
1479
+
1480
+ BASE = "http://127.0.0.1:18811"
1481
+ HEADERS = {"X-API-Token": token}
1482
+
1483
+ # 1. Submit asynchronously
1484
+ r = requests.post(f"{BASE}/execute/async", headers=HEADERS,
1485
+ json={"script": "App.getRenderMgr().doRender(); return 'done';"})
1486
+ req_id = r.json()["request_id"]
1487
+
1488
+ # 2. Poll until complete
1489
+ while True:
1490
+ status = requests.get(f"{BASE}/requests/{req_id}/status", headers=HEADERS).json()
1491
+ if status["status"] in ("completed", "failed", "cancelled"):
1492
+ break
1493
+ time.sleep(2)
1494
+
1495
+ # 3. Fetch result
1496
+ result = requests.get(f"{BASE}/requests/{req_id}/result", headers=HEADERS).json()
1497
+ print(result["result"])
1498
+ ```
1499
+
1500
+ **Or use `?wait=true` to long-poll in one step:**
1501
+
1502
+ ```python
1503
+ result = requests.get(f"{BASE}/requests/{req_id}/result?wait=true&timeout=300",
1504
+ headers=HEADERS).json()
1505
+ ```
1506
+
1507
+ ---
1508
+
1509
+ ### `POST /execute/async`
1510
+
1511
+ **Purpose:** Submit an inline script for asynchronous execution
1512
+ **Authentication:** Required (if enabled)
1513
+
1514
+ **Request Body:** Same as `POST /execute`
1515
+
1516
+ **Response (immediate):**
1517
+ ```json
1518
+ {
1519
+ "request_id": "a3f2b891",
1520
+ "status": "queued",
1521
+ "submitted_at": "2026-04-09T10:15:00"
1522
+ }
1523
+ ```
1524
+
1525
+ ---
1526
+
1527
+ ### `POST /scripts/:id/async`
1528
+
1529
+ **Purpose:** Submit a registered script for asynchronous execution
1530
+ **Authentication:** Required (if enabled)
1531
+
1532
+ **Request Body:** Same as `POST /scripts/:id/execute`
1533
+
1534
+ **Response (immediate):** Same shape as `POST /execute/async`
1535
+
1536
+ ---
1537
+
1538
+ ### `GET /requests/:id/status`
1539
+
1540
+ **Purpose:** Poll the status of an async request
1541
+ **Authentication:** Required (if enabled)
1542
+
1543
+ **Response:**
1544
+ ```json
1545
+ {
1546
+ "request_id": "a3f2b891",
1547
+ "status": "running",
1548
+ "progress": 0.0,
1549
+ "elapsed_ms": 1240,
1550
+ "queue_position": 0
1551
+ }
1552
+ ```
1553
+
1554
+ **Status values:** `queued`, `running`, `completed`, `failed`, `cancelled`
1555
+
1556
+ `queue_position` is only meaningful when `status` is `queued`.
1557
+
1558
+ ---
1559
+
1560
+ ### `GET /requests/:id/result`
1561
+
1562
+ **Purpose:** Fetch the final result of an async request
1563
+ **Authentication:** Required (if enabled)
1564
+
1565
+ **Query Parameters:**
1566
+
1567
+ | Parameter | Default | Description |
1568
+ |-----------|---------|-------------|
1569
+ | `wait` | `false` | Block until the request completes |
1570
+ | `timeout` | `300` | Max seconds to wait when `wait=true` |
1571
+
1572
+ **Response (completed):**
1573
+ ```json
1574
+ {
1575
+ "success": true,
1576
+ "result": "done",
1577
+ "output": [],
1578
+ "error": null,
1579
+ "request_id": "a3f2b891",
1580
+ "duration_ms": 45230,
1581
+ "completed_at": "2026-04-09T10:15:47",
1582
+ "status": "completed"
1583
+ }
1584
+ ```
1585
+
1586
+ Returns HTTP 404 if the request ID is unknown or has been purged (TTL: 1 hour).
1587
+
1588
+ ---
1589
+
1590
+ ### `DELETE /requests/:id`
1591
+
1592
+ **Purpose:** Cancel a queued or running async request
1593
+ **Authentication:** Required (if enabled)
1594
+
1595
+ **Cancellation behaviour:**
1596
+ - `queued` → removed from queue immediately
1597
+ - `running` → cancel flag set + `killRender()` called; DAZ Studio honours the flag when the script returns
1598
+
1599
+ **Response:**
1600
+ ```json
1601
+ {
1602
+ "request_id": "a3f2b891",
1603
+ "status": "cancelled",
1604
+ "cancelled_at": "2026-04-09T10:15:05"
1605
+ }
1606
+ ```
1607
+
1608
+ ---
1609
+
1610
+ ### `GET /requests`
1611
+
1612
+ **Purpose:** List all active and recently completed async requests
1613
+ **Authentication:** Required (if enabled)
1614
+
1615
+ **Response:**
1616
+ ```json
1617
+ {
1618
+ "requests": [
1619
+ {
1620
+ "request_id": "a3f2b891",
1621
+ "status": "running",
1622
+ "submitted_at": "2026-04-09T10:15:00",
1623
+ "elapsed_ms": 1240
1624
+ }
1625
+ ],
1626
+ "total": 1,
1627
+ "queued": 0,
1628
+ "running": 1,
1629
+ "completed": 0
1630
+ }
1631
+ ```
1632
+
1633
+ **Note:** Completed, failed, and cancelled requests are automatically purged after 1 hour.
1634
+
1635
+ ---
1636
+
1637
+ ## 🔔 Scene Events (SSE)
1638
+
1639
+ `GET /scene/events` opens a persistent HTTP connection and streams DAZ Studio scene-change notifications as **Server-Sent Events**. Each event is a UTF-8 line in the form:
1640
+
1641
+ ```
1642
+ data: {"type":"node.added","ts":1716307200123,"data":{"node_id":"0x1a2b3c4d","node_name":"Genesis 9","node_type":"DzFigure"}}
1643
+
1644
+ ```
1645
+
1646
+ A `:keepalive` comment is sent every 3 seconds of idle time so clients can detect disconnects without sending an event:
1647
+
1648
+ ```
1649
+ :keepalive
1650
+
1651
+ ```
1652
+
1653
+ The connection stays open until the client disconnects or the server is stopped. All configured security checks (auth, IP whitelist) apply.
1654
+
1655
+ ---
1656
+
1657
+ ### `GET /scene/events`
1658
+
1659
+ **Purpose:** Subscribe to real-time scene-change notifications
1660
+ **Authentication:** Required (if enabled)
1661
+
1662
+ **Query Parameters:**
1663
+
1664
+ | Parameter | Default | Description |
1665
+ |-----------|---------|-------------|
1666
+ | `filter` | _(all)_ | Comma-separated list of event categories to receive. Omit to receive all. |
1667
+
1668
+ **Filter categories:**
1669
+
1670
+ | Value | Events included |
1671
+ |-------|----------------|
1672
+ | `node` | `node.added`, `node.removed` |
1673
+ | `skeleton` | `skeleton.added`, `skeleton.removed` |
1674
+ | `light` | `light.added`, `light.removed` |
1675
+ | `camera` | `camera.added`, `camera.removed` |
1676
+ | `selection` | `selection.list_changed`, `selection.primary_changed` |
1677
+ | `scene` | `scene.loading`, `scene.loaded`, `scene.saving`, `scene.saved`, `scene.clear_starting`, `scene.cleared` |
1678
+ | `time` | `time.changed` _(debounced 150 ms)_, `playback.started`, `playback.finished` |
1679
+ | `render` | `render.started`, `render.finished` |
1680
+
1681
+ **Response headers:**
1682
+ ```
1683
+ Content-Type: text/event-stream
1684
+ Cache-Control: no-cache
1685
+ Transfer-Encoding: chunked
1686
+ ```
1687
+
1688
+ ---
1689
+
1690
+ ### Event reference
1691
+
1692
+ Every event shares the same envelope:
1693
+
1694
+ ```json
1695
+ {
1696
+ "type": "node.added",
1697
+ "ts": 1716307200123,
1698
+ "data": { ... }
1699
+ }
1700
+ ```
1701
+
1702
+ | Field | Type | Description |
1703
+ |-------|------|-------------|
1704
+ | `type` | string | Dot-namespaced event name (see table below) |
1705
+ | `ts` | integer | Unix timestamp in milliseconds |
1706
+ | `data` | object | Event-specific payload (see below) |
1707
+
1708
+ **Node payload** (for `node.*`, `skeleton.*`, `light.*`, `camera.*`):
1709
+
1710
+ ```json
1711
+ {
1712
+ "node_id": "0x1a2b3c4d",
1713
+ "node_name": "Genesis 9",
1714
+ "node_type": "DzFigure"
1715
+ }
1716
+ ```
1717
+
1718
+ `node_id` is a session-stable hex pointer. Use it to correlate add/remove events for the same node within a session.
1719
+
1720
+ **Scene-save payload** (`scene.saving`, `scene.saved`):
1721
+
1722
+ ```json
1723
+ { "filename": "/Users/me/Documents/MyScene.duf" }
1724
+ ```
1725
+
1726
+ **Time payload** (`time.changed`):
1727
+
1728
+ ```json
1729
+ { "time_ticks": 4800, "time_secs": 1.0 }
1730
+ ```
1731
+
1732
+ `time.changed` is debounced — during playback it fires at most once per 150 ms regardless of frame rate.
1733
+
1734
+ **Empty payload** — all other events (`node.removed`, `selection.*`, `scene.loading`, `scene.loaded`, `scene.cleared`, `playback.*`, `render.*`):
1735
+
1736
+ ```json
1737
+ {}
1738
+ ```
1739
+
1740
+ **Full event type table:**
1741
+
1742
+ | `type` | Category | Payload fields |
1743
+ |--------|----------|----------------|
1744
+ | `node.added` | `node` | `node_id`, `node_name`, `node_type` |
1745
+ | `node.removed` | `node` | `node_id`, `node_name`, `node_type` |
1746
+ | `skeleton.added` | `skeleton` | `node_id`, `node_name`, `node_type` |
1747
+ | `skeleton.removed` | `skeleton` | `node_id`, `node_name`, `node_type` |
1748
+ | `light.added` | `light` | `node_id`, `node_name`, `node_type` |
1749
+ | `light.removed` | `light` | `node_id`, `node_name`, `node_type` |
1750
+ | `camera.added` | `camera` | `node_id`, `node_name`, `node_type` |
1751
+ | `camera.removed` | `camera` | `node_id`, `node_name`, `node_type` |
1752
+ | `selection.list_changed` | `selection` | _(empty)_ |
1753
+ | `selection.primary_changed` | `selection` | `node_id`, `node_name`, `node_type` (or `{}` if deselected) |
1754
+ | `scene.loading` | `scene` | _(empty)_ |
1755
+ | `scene.loaded` | `scene` | _(empty)_ |
1756
+ | `scene.saving` | `scene` | `filename` |
1757
+ | `scene.saved` | `scene` | `filename` |
1758
+ | `scene.clear_starting` | `scene` | _(empty)_ |
1759
+ | `scene.cleared` | `scene` | _(empty)_ |
1760
+ | `time.changed` | `time` | `time_ticks`, `time_secs` |
1761
+ | `playback.started` | `time` | _(empty)_ |
1762
+ | `playback.finished` | `time` | _(empty)_ |
1763
+ | `render.started` | `render` | _(empty)_ |
1764
+ | `render.finished` | `render` | _(empty)_ |
1765
+
1766
+ ---
1767
+
1768
+ ### Scene Events — Client Examples
1769
+
1770
+ #### curl
1771
+
1772
+ ```bash
1773
+ TOKEN=$(cat ~/.daz3d/dazscriptserver_token.txt)
1774
+
1775
+ # All events
1776
+ curl -N -H "X-API-Token: $TOKEN" http://127.0.0.1:18811/scene/events
1777
+
1778
+ # Node and scene events only
1779
+ curl -N -H "X-API-Token: $TOKEN" \
1780
+ "http://127.0.0.1:18811/scene/events?filter=node,scene"
1781
+ ```
1782
+
1783
+ #### Python — requests (no extra dependencies)
1784
+
1785
+ ```python
1786
+ import requests, json, os
1787
+
1788
+ token = open(os.path.expanduser("~/.daz3d/dazscriptserver_token.txt")).read().strip()
1789
+
1790
+ with requests.get(
1791
+ "http://127.0.0.1:18811/scene/events",
1792
+ headers={"X-API-Token": token},
1793
+ stream=True,
1794
+ timeout=None,
1795
+ ) as resp:
1796
+ resp.raise_for_status()
1797
+ for raw in resp.iter_lines():
1798
+ if not raw or raw.startswith(b":"): # skip keepalives and blank lines
1799
+ continue
1800
+ if raw.startswith(b"data: "):
1801
+ event = json.loads(raw[6:])
1802
+ print(f"{event['type']}: {event['data']}")
1803
+ ```
1804
+
1805
+ #### Python — sseclient (cleaner API)
1806
+
1807
+ ```bash
1808
+ pip install sseclient-py
1809
+ ```
1810
+
1811
+ ```python
1812
+ import sseclient, requests, os
1813
+
1814
+ token = open(os.path.expanduser("~/.daz3d/dazscriptserver_token.txt")).read().strip()
1815
+
1816
+ response = requests.get(
1817
+ "http://127.0.0.1:18811/scene/events?filter=node,selection,scene",
1818
+ headers={"X-API-Token": token},
1819
+ stream=True,
1820
+ )
1821
+ client = sseclient.SSEClient(response)
1822
+
1823
+ for event in client.events():
1824
+ import json
1825
+ data = json.loads(event.data)
1826
+ print(data["type"], data["data"])
1827
+ ```
1828
+
1829
+ #### JavaScript — EventSource (browser)
1830
+
1831
+ The browser's built-in `EventSource` does not support custom headers, so pass the token as a query parameter instead (requires auth to accept it — which DazScriptServer does not currently support). Use `fetch` with `ReadableStream` for authenticated SSE from a browser:
1832
+
1833
+ ```javascript
1834
+ const token = "your-token-here";
1835
+
1836
+ const response = await fetch(
1837
+ "http://127.0.0.1:18811/scene/events?filter=node,scene",
1838
+ { headers: { "X-API-Token": token } }
1839
+ );
1840
+
1841
+ const reader = response.body.getReader();
1842
+ const decoder = new TextDecoder();
1843
+ let buffer = "";
1844
+
1845
+ while (true) {
1846
+ const { done, value } = await reader.read();
1847
+ if (done) break;
1848
+ buffer += decoder.decode(value, { stream: true });
1849
+
1850
+ const lines = buffer.split("\n");
1851
+ buffer = lines.pop(); // keep incomplete last line
1852
+
1853
+ for (const line of lines) {
1854
+ if (line.startsWith("data: ")) {
1855
+ const event = JSON.parse(line.slice(6));
1856
+ console.log(event.type, event.data);
1857
+ }
1858
+ }
1859
+ }
1860
+ ```
1861
+
1862
+ #### Node.js
1863
+
1864
+ ```javascript
1865
+ const http = require("http");
1866
+ const fs = require("fs");
1867
+ const os = require("os");
1868
+
1869
+ const token = fs.readFileSync(`${os.homedir()}/.daz3d/dazscriptserver_token.txt`, "utf8").trim();
1870
+
1871
+ const req = http.request(
1872
+ { hostname: "127.0.0.1", port: 18811, path: "/scene/events", method: "GET",
1873
+ headers: { "X-API-Token": token, Accept: "text/event-stream" } },
1874
+ (res) => {
1875
+ let buf = "";
1876
+ res.on("data", (chunk) => {
1877
+ buf += chunk.toString();
1878
+ const lines = buf.split("\n");
1879
+ buf = lines.pop();
1880
+ for (const line of lines) {
1881
+ if (line.startsWith("data: ")) {
1882
+ const event = JSON.parse(line.slice(6));
1883
+ console.log(event.type, event.data);
1884
+ }
1885
+ }
1886
+ });
1887
+ }
1888
+ );
1889
+ req.end();
1890
+ ```
1891
+
1892
+ ---
1893
+
1894
+ ### Notes
1895
+
1896
+ - **Multiple concurrent subscribers** are supported — each `GET /scene/events` request is independent.
1897
+ - **High-frequency signals** (`timeChanging` during playback, `nodeSelectionListChanged` during multi-select) are debounced before dispatch so clients are not flooded.
1898
+ - **Server stop** closes all open SSE connections cleanly; clients should reconnect automatically.
1899
+ - **Reverse proxy:** If using nginx in front of the server, add `proxy_buffering off` and `proxy_http_version 1.1` to the SSE location block (see [Reverse Proxy Setup](#reverse-proxy-setup-https)).
1900
+
1901
+ ---
1902
+
1903
+ ## 🔒 Security Features
1904
+
1905
+ ### Built-In Security
1906
+
1907
+ - **Cryptographically Secure Tokens** - 128-bit entropy using OS crypto APIs
1908
+ - **IP Whitelist** - Exact IP matching for access control
1909
+ - **Per-IP Rate Limiting** - Sliding window algorithm prevents brute force
1910
+ - **Input Validation** - Request body, script size, and file path validation
1911
+ - **Audit Logging** - All requests, auth failures, and blocked IPs logged
1912
+ - **Concurrent Request Limiting** - Prevents resource exhaustion attacks
1913
+ - **File Permissions** - Automatic `chmod 600` on token files (Unix/macOS)
1914
+
1915
+ ### What's NOT Included
1916
+
1917
+ - **HTTPS/TLS** - Use a reverse proxy (nginx, Apache) for encryption
1918
+ - **X-Forwarded-For** - IP whitelist uses direct TCP connection IP only
1919
+ - **User Accounts** - Single token for all authenticated access
1920
+ - **Persistent Sessions** - Each request is independently authenticated
1921
+
1922
+ ---
1923
+
1924
+ ## 🛡️ Security Best Practices
1925
+
1926
+ ### Localhost-Only Access (Most Secure)
1927
+
1928
+ For controlling DAZ Studio from the same machine:
1929
+
1930
+ 1. ✅ Keep host set to `127.0.0.1` (default)
1931
+ 2. ✅ Keep authentication enabled (default)
1932
+ 3. ✅ Protect token file (`~/.daz3d/dazscriptserver_token.txt`)
1933
+ 4. ℹ️ IP whitelist and rate limiting are optional
1934
+
1935
+ ### Network Access (Remote Clients)
1936
+
1937
+ For controlling DAZ Studio from other machines:
1938
+
1939
+ 1. ✅ Change host to `0.0.0.0` (accept external connections)
1940
+ 2. ✅ **Required:** Enable IP whitelist with specific allowed IPs
1941
+ 3. ✅ **Required:** Keep authentication enabled
1942
+ 4. ✅ **Recommended:** Enable rate limiting (e.g., 60 requests / 60 seconds)
1943
+ 5. ✅ **Recommended:** Use firewall rules to restrict port access
1944
+ 6. ⚠️ **Never** expose to the public internet without additional security (VPN, reverse proxy with HTTPS)
1945
+
1946
+ ### Token Security
1947
+
1948
+ | Do | Don't |
1949
+ |----|-------|
1950
+ | ✅ Treat token like a password | ❌ Commit to version control |
1951
+ | ✅ Copy from UI or token file | ❌ Share publicly or in logs |
1952
+ | ✅ Use "Regenerate" if compromised | ❌ Email or message token |
1953
+ | ✅ Set `chmod 600` on Unix/macOS | ❌ Use same token across environments |
1954
+ | ✅ Restrict file access on Windows | ❌ Disable auth on untrusted networks |
1955
+
1956
+ **Token File Locations:**
1957
+ - Unix/macOS: `~/.daz3d/dazscriptserver_token.txt`
1958
+ - Windows: `%USERPROFILE%\.daz3d\dazscriptserver_token.txt`
1959
+
1960
+ ### Monitoring & Auditing
1961
+
1962
+ - ✅ Check request log regularly for suspicious activity
1963
+ - ✅ Monitor `/metrics` for high failure rates or auth failures
1964
+ - ✅ Set up alerts for unusual patterns (rate limit violations, auth failures)
1965
+ - ✅ Review BLOCKED and AUTH FAILED entries in the log
1966
+
1967
+ ---
1968
+
1969
+ ## 💻 Client Examples
1970
+
1971
+ The repository includes example clients in multiple languages.
1972
+
1973
+ ### Python — dazpy SDK
1974
+
1975
+ **Install:**
1976
+ ```bash
1977
+ pip install dazpy-*.whl # from the release page
1978
+ # or: pip install -e . # from source
1979
+ ```
1980
+
1981
+ **Files:**
1982
+ - `tests_dazpy.py` — SDK unit tests (mock-based, no server needed)
1983
+ - `tests_dazpy_integration.py` — SDK integration tests (requires running server)
1984
+
1985
+ **Usage:**
1986
+ ```python
1987
+ from dazpy import DazClient, DazScene
1988
+
1989
+ client = DazClient() # auto-loads token
1990
+ scene = DazScene(client)
1991
+
1992
+ # Inspect scene
1993
+ print(scene.num_nodes(), "nodes")
1994
+ for node in scene.nodes():
1995
+ print(node.label(), node.position())
1996
+
1997
+ # Pose a figure
1998
+ figure = scene.find_skeleton_by_label("Genesis 9")
1999
+ with scene.undo("T-pose arms"):
2000
+ figure.find_bone("lShldrBend").set_local_rotation(0, 0, -90)
2001
+ figure.find_bone("rShldrBend").set_local_rotation(0, 0, 90)
2002
+
2003
+ # Adjust morphs
2004
+ smile = figure.find_modifier("PHMSmileOpen")
2005
+ smile.set_value(0.75)
2006
+
2007
+ # Render asynchronously
2008
+ from dazpy import execute_long
2009
+ result = execute_long(client, "App.getRenderMgr().doRender(); return 'done';",
2010
+ timeout=300.0)
2011
+ ```
2012
+
2013
+ ### Python — Raw HTTP
2014
+
2015
+ **Files:**
2016
+ - `test-simple.py` — Basic raw-HTTP client
2017
+ - `tests.py` — Comprehensive server integration test suite (72 tests)
2018
+ - `test-performance.py` — Performance benchmarks and load tests
2019
+
2020
+ **Usage:**
2021
+ ```python
2022
+ import requests
2023
+ import os
2024
+
2025
+ token_path = os.path.expanduser("~/.daz3d/dazscriptserver_token.txt")
2026
+ with open(token_path) as f:
2027
+ token = f.read().strip()
2028
+
2029
+ response = requests.post(
2030
+ "http://127.0.0.1:18811/execute",
2031
+ headers={"X-API-Token": token},
2032
+ json={"script": "(function(){ return 'Hello!'; })()", "args": {}}
2033
+ )
2034
+ print(response.json())
2035
+ ```
2036
+
2037
+ ### JavaScript/Node.js
2038
+
2039
+ **File:** `test-client.js`
2040
+
2041
+ **Requirements:** Node.js 18+ (built-in fetch) or `npm install node-fetch`
2042
+
2043
+ **Usage:**
2044
+ ```javascript
2045
+ const fs = require('fs');
2046
+ const os = require('os');
2047
+
2048
+ const tokenPath = `${os.homedir()}/.daz3d/dazscriptserver_token.txt`;
2049
+ const token = fs.readFileSync(tokenPath, 'utf8').trim();
2050
+
2051
+ const response = await fetch('http://127.0.0.1:18811/execute', {
2052
+ method: 'POST',
2053
+ headers: {
2054
+ 'Content-Type': 'application/json',
2055
+ 'X-API-Token': token
2056
+ },
2057
+ body: JSON.stringify({
2058
+ script: "(function(){ return 'Hello from Node.js!'; })()"
2059
+ })
2060
+ });
2061
+
2062
+ const result = await response.json();
2063
+ console.log(result);
2064
+ ```
2065
+
2066
+ ### PowerShell
2067
+
2068
+ **File:** `test-client.ps1`
2069
+
2070
+ **Compatible:** PowerShell 5.1+ and PowerShell Core 6+
2071
+
2072
+ **Usage:**
2073
+ ```powershell
2074
+ $tokenPath = "$env:USERPROFILE\.daz3d\dazscriptserver_token.txt"
2075
+ $token = Get-Content $tokenPath
2076
+
2077
+ $body = @{
2078
+ script = "(function(){ return 'Hello there from PowerShell!'; })()"
2079
+ } | ConvertTo-Json
2080
+
2081
+ $response = Invoke-RestMethod `
2082
+ -Uri "http://127.0.0.1:18811/execute" `
2083
+ -Method Post `
2084
+ -Headers @{"X-API-Token" = $token} `
2085
+ -ContentType "application/json" `
2086
+ -Body $body
2087
+
2088
+ $response
2089
+ ```
2090
+
2091
+ **Running examples:**
2092
+ ```bash
2093
+ # dazpy unit tests (no server needed)
2094
+ python tests_dazpy.py
2095
+
2096
+ # dazpy integration tests (requires server running)
2097
+ python tests_dazpy_integration.py
2098
+
2099
+ # Server integration tests
2100
+ python tests.py
2101
+
2102
+ # Python smoke tests
2103
+ python test-simple.py
2104
+
2105
+ # Performance benchmarks and load tests
2106
+ python test-performance.py
2107
+ python test-performance.py --quick # fewer iterations, suitable for CI
2108
+
2109
+ # Node.js
2110
+ node test-client.js
2111
+
2112
+ # PowerShell
2113
+ powershell -ExecutionPolicy Bypass -File test-client.ps1
2114
+ # Or PowerShell Core:
2115
+ pwsh test-client.ps1
2116
+ ```
2117
+
2118
+ All examples include error handling, argument passing, and output capture.
2119
+
2120
+ ---
2121
+
2122
+ ## Writing Scripts
2123
+
2124
+ ### Accessing Arguments
2125
+
2126
+ Arguments are available via `getArguments()[0]`:
2127
+
2128
+ ```javascript
2129
+ var args = getArguments()[0];
2130
+ print("Hello, " + args.name);
2131
+ return args.value * 2;
2132
+ ```
2133
+
2134
+ ### Returning Values
2135
+
2136
+ Wrap scripts in an IIFE (Immediately Invoked Function Expression):
2137
+
2138
+ ```javascript
2139
+ (function(){
2140
+ var node = Scene.findNodeByLabel("FN Ethan");
2141
+ return {
2142
+ label: node.getLabel(),
2143
+ position: node.getWSPos()
2144
+ };
2145
+ })()
2146
+ ```
2147
+
2148
+ ### Error Handling
2149
+
2150
+ Throw errors to populate the `error` field:
2151
+
2152
+ ```javascript
2153
+ var args = getArguments()[0];
2154
+ if (!args.label) {
2155
+ throw new Error("label is required");
2156
+ }
2157
+
2158
+ var node = Scene.findNodeByLabel(args.label);
2159
+ if (!node) {
2160
+ throw "Node not found: " + args.label;
2161
+ }
2162
+
2163
+ return node.getLabel();
2164
+ ```
2165
+
2166
+ ### Using Include
2167
+
2168
+ Use `scriptFile` (not inline `script`) for scripts that use `include()`:
2169
+
2170
+ ```javascript
2171
+ // File: /path/to/main.dsa
2172
+ var includeDir = DzFile(getScriptFileName()).path();
2173
+ include(includeDir + "/utils.dsa");
2174
+
2175
+ var args = getArguments()[0];
2176
+ return myUtilFunction(args);
2177
+ ```
2178
+
2179
+ **Request:**
2180
+ ```json
2181
+ {
2182
+ "scriptFile": "/path/to/main.dsa",
2183
+ "args": { "value": 42 }
2184
+ }
2185
+ ```
2186
+
2187
+ ### Script Registry Pattern
2188
+
2189
+ Register once, call many times:
2190
+
2191
+ ```python
2192
+ import requests
2193
+ import os
2194
+
2195
+ token_path = os.path.expanduser("~/.daz3d/dazscriptserver_token.txt")
2196
+ with open(token_path) as f:
2197
+ token = f.read().strip()
2198
+
2199
+ BASE = "http://127.0.0.1:18811"
2200
+ HEADERS = {"X-API-Token": token}
2201
+
2202
+ def register_scripts():
2203
+ """Register all scripts on startup or after 404."""
2204
+ requests.post(f"{BASE}/scripts/register", headers=HEADERS, json={
2205
+ "name": "node-count",
2206
+ "script": "(function(){ return Scene.getNumNodes(); })()"
2207
+ })
2208
+
2209
+ def call_script(name, args=None):
2210
+ """Execute a registered script by name."""
2211
+ r = requests.post(
2212
+ f"{BASE}/scripts/{name}/execute",
2213
+ headers=HEADERS,
2214
+ json={"args": args or {}}
2215
+ )
2216
+
2217
+ # Handle DAZ Studio restart (registry cleared)
2218
+ if r.status_code == 404:
2219
+ register_scripts()
2220
+ r = requests.post(
2221
+ f"{BASE}/scripts/{name}/execute",
2222
+ headers=HEADERS,
2223
+ json={"args": args or {}}
2224
+ )
2225
+
2226
+ r.raise_for_status()
2227
+ return r.json()["result"]
2228
+
2229
+ # Initialize
2230
+ register_scripts()
2231
+
2232
+ # Use
2233
+ node_count = call_script("node-count")
2234
+ print(f"Scene has {node_count} nodes")
2235
+ ```
2236
+
2237
+ ### Rendering Example
2238
+
2239
+ ```javascript
2240
+ (function(){
2241
+ var args = getArguments()[0];
2242
+
2243
+ // Load scene
2244
+ Scene.load(args.scenePath);
2245
+
2246
+ // Configure render settings
2247
+ var renderMgr = App.getRenderMgr();
2248
+ var renderOptions = renderMgr.getRenderOptions();
2249
+ renderOptions.setImageFilename(args.outputPath);
2250
+ renderOptions.setImageSize(args.width, args.height);
2251
+
2252
+ // Trigger render (blocks until complete)
2253
+ renderMgr.doRender(renderOptions);
2254
+
2255
+ return { status: "complete", output: args.outputPath };
2256
+ })()
2257
+ ```
2258
+
2259
+ **Note:** Renders block the request until complete. Use appropriate timeout settings (30-300 seconds).
2260
+
2261
+ ---
2262
+
2263
+ ## 🔧 Troubleshooting
2264
+
2265
+ ### Server Won't Start
2266
+
2267
+ **"Port already in use" or "Failed to bind":**
2268
+ - Another application is using the port
2269
+ - Check if another DAZ Studio instance is running the plugin
2270
+ - Try a different port number
2271
+ - Check what's using the port:
2272
+ - Windows: `netstat -ano | findstr :18811`
2273
+ - macOS/Linux: `lsof -i :18811` or `netstat -an | grep 18811`
2274
+
2275
+ **Server starts but immediately stops:**
2276
+ - Check DAZ Studio log for error messages
2277
+ - Verify permissions to bind to the configured host/port
2278
+ - Ports < 1024 require root privileges (use ports > 1024)
2279
+
2280
+ ### Connection Refused
2281
+
2282
+ **Cannot connect from localhost:**
2283
+ - Verify server is running (check UI status)
2284
+ - Verify correct port (default: 18811)
2285
+ - Check host is `127.0.0.1` or `0.0.0.0`
2286
+ - Firewall may be blocking (add exception for DAZ Studio)
2287
+
2288
+ **Cannot connect from remote machine:**
2289
+ - Verify host is `0.0.0.0` (not `127.0.0.1`)
2290
+ - Check IP whitelist includes client IP
2291
+ - Verify firewall allows incoming connections on the port
2292
+ - Verify network routing between client and server
2293
+
2294
+ ### Authentication Errors (HTTP 401)
2295
+
2296
+ **"Invalid or missing authentication token":**
2297
+ - Verify token file exists: `~/.daz3d/dazscriptserver_token.txt`
2298
+ - Copy token exactly from UI or file (no extra spaces)
2299
+ - Use correct header: `X-API-Token: <token>` or `Authorization: Bearer <token>`
2300
+ - If token file is corrupted, use "Regenerate" button
2301
+ - Verify authentication is enabled in UI
2302
+
2303
+ ### HTTP 403 Forbidden
2304
+
2305
+ **"IP not whitelisted":**
2306
+ - IP whitelist is enabled and your IP is not in the list
2307
+ - Add client IP to whitelist (comma-separated)
2308
+ - Check actual IP (may differ due to NAT/proxy)
2309
+ - Temporarily disable IP whitelist for testing
2310
+
2311
+ ### HTTP 429 Too Many Requests
2312
+
2313
+ **"Rate limit exceeded":**
2314
+ - Per-IP rate limit exceeded
2315
+ - Wait for time window to expire (default: 60 seconds)
2316
+ - Increase max requests or time window
2317
+ - Temporarily disable rate limiting for testing
2318
+
2319
+ **"Maximum concurrent requests limit reached":**
2320
+ - Too many scripts running simultaneously
2321
+ - Wait for requests to complete
2322
+ - Increase max concurrent requests
2323
+ - Optimize scripts for faster execution
2324
+ - Add delays between requests in client
2325
+
2326
+ ### HTTP 413 Payload Too Large
2327
+
2328
+ **"Request body too large":**
2329
+ - Request exceeds configured max (default: 5MB)
2330
+ - Increase max body size in Advanced Limits
2331
+ - For large scripts, use `scriptFile` instead of inline `script`
2332
+
2333
+ ### Script Execution Errors
2334
+
2335
+ **"Script execution failed" or error in response:**
2336
+ - Check `error` field for details (includes line number)
2337
+ - Verify script syntax is valid DazScript
2338
+ - Check referenced files/assets exist
2339
+ - Review `output` field for print statements
2340
+ - Test script manually in DAZ Studio Script IDE first
2341
+
2342
+ **Script times out:**
2343
+ - Script exceeds timeout (default: 30 seconds)
2344
+ - Increase timeout in configuration (max: 300 seconds)
2345
+ - Optimize script performance
2346
+ - Break long operations into multiple smaller requests
2347
+
2348
+ ### Token File Issues
2349
+
2350
+ **Token file not created:**
2351
+ - Plugin may lack write permissions to home directory
2352
+ - Manually create `~/.daz3d/` directory
2353
+ - Windows: `%USERPROFILE%\.daz3d\`
2354
+ - Check DAZ Studio log for permission errors
2355
+
2356
+ **Token file permissions warning (Unix/macOS):**
2357
+ - Plugin automatically sets `chmod 600`
2358
+ - If warning persists: `chmod 600 ~/.daz3d/dazscriptserver_token.txt`
2359
+
2360
+ ---
2361
+
2362
+ ## ❓ Frequently Asked Questions
2363
+
2364
+ ### Is this safe to use?
2365
+
2366
+ The plugin is designed with security in mind:
2367
+ - ✅ Cryptographically secure API tokens
2368
+ - ✅ Optional IP whitelist and rate limiting
2369
+ - ✅ Input validation and size limits
2370
+ - ✅ Audit logging of all requests
2371
+
2372
+ **However:** Any client with a valid token can execute arbitrary DazScript code with full access to your DAZ Studio scene, file system (within script permissions), and system resources. **Treat your API token like a password** and only share it with trusted applications.
2373
+
2374
+ ### Can I use this in production?
2375
+
2376
+ **Yes!** This plugin is production-ready:
2377
+ - Concurrent request limiting prevents resource exhaustion
2378
+ - Rate limiting prevents abuse
2379
+ - Health and metrics endpoints for monitoring
2380
+ - Configurable timeouts and limits
2381
+ - Comprehensive error handling and logging
2382
+
2383
+ Many users run this plugin 24/7 for batch rendering, asset processing, and integration workflows.
2384
+
2385
+ ### What's the performance impact?
2386
+
2387
+ **Idle:** Negligible CPU usage when not processing requests.
2388
+
2389
+ **Under load:** Performance depends on your scripts. The plugin adds < 10ms overhead per request. The limiting factor is usually DazScript execution time and DAZ Studio's single-threaded scene graph operations.
2390
+
2391
+ ### Can I run multiple instances?
2392
+
2393
+ No. Each DAZ Studio process can only load the plugin once.
2394
+
2395
+ **Alternatives:**
2396
+ - Run multiple DAZ Studio instances on different ports (separate processes)
2397
+ - Use concurrent request limit for multiple simultaneous requests in one instance
2398
+
2399
+ ### Does this work with DAZ Studio CLI/headless mode?
2400
+
2401
+ The plugin requires DAZ Studio GUI (it's a pane plugin). For headless automation, run DAZ Studio in a virtual display environment:
2402
+ - Linux: Xvfb
2403
+ - Windows: Hidden window
2404
+
2405
+ ### Can I execute multiple scripts in parallel?
2406
+
2407
+ Yes, up to the configured concurrent request limit (default: 10).
2408
+
2409
+ **Important notes:**
2410
+ - All scripts execute on DAZ Studio's main thread (SDK requirement)
2411
+ - Scripts are executed serially, not truly in parallel
2412
+ - The concurrent limit prevents too many requests from queuing
2413
+ - Heavy scene operations may block other requests
2414
+
2415
+ ### What DazScript features are supported?
2416
+
2417
+ **All standard DazScript features work:**
2418
+ - Scene graph manipulation (load, modify, render)
2419
+ - File I/O operations
2420
+ - Include/import of other scripts (use `scriptFile`)
2421
+ - App objects and APIs
2422
+ - Print statements (captured in `output` array)
2423
+
2424
+ The only difference from manual script execution is that arguments are passed via the `args` JSON object instead of command-line parameters.
2425
+
2426
+ ### How do I debug scripts?
2427
+
2428
+ 1. **Use `print()` statements** - Captured in response `output` array
2429
+ 2. **Check the `error` field** - Includes line numbers
2430
+ 3. **Test manually first** - Run in DAZ Studio Script IDE
2431
+ 4. **Check UI request log** - Shows status and duration
2432
+ 5. **Use `request_id`** - Correlate requests between client and server logs
2433
+
2434
+ ### Can I trigger renders?
2435
+
2436
+ **Yes!** Execute any DazScript that renders:
2437
+
2438
+ ```javascript
2439
+ (function(){
2440
+ var args = getArguments()[0];
2441
+ Scene.load(args.scenePath);
2442
+
2443
+ var renderMgr = App.getRenderMgr();
2444
+ var renderOptions = renderMgr.getRenderOptions();
2445
+ renderOptions.setImageFilename(args.outputPath);
2446
+
2447
+ renderMgr.doRender(renderOptions);
2448
+ return "Render complete";
2449
+ })()
2450
+ ```
2451
+
2452
+ **Note:** Renders block the request until complete. Use appropriate timeout settings.
2453
+
2454
+ ### How do I upgrade to a new version?
2455
+
2456
+ 1. Stop the server in DAZ Studio
2457
+ 2. Close DAZ Studio
2458
+ 3. Replace the plugin DLL/dylib in plugins folder
2459
+ 4. Restart DAZ Studio
2460
+ 5. Start the server
2461
+
2462
+ Your API token and settings persist across upgrades (stored separately).
2463
+
2464
+ ### Where are settings stored?
2465
+
2466
+ **Settings (QSettings):**
2467
+ - Windows: Registry key `HKEY_CURRENT_USER\Software\DAZ 3D\DazScriptServer`
2468
+ - macOS: `~/Library/Preferences/com.daz3d.DazScriptServer.plist`
2469
+ - Linux: `~/.config/DAZ 3D/DazScriptServer.conf`
2470
+
2471
+ **API Token:**
2472
+ - All platforms: `~/.daz3d/dazscriptserver_token.txt`
2473
+
2474
+ ---
2475
+
2476
+ ## Advanced Topics
2477
+
2478
+ ### Performance Tuning
2479
+
2480
+ **For high-throughput scenarios:**
2481
+ - Increase max concurrent requests (default: 10, max: 50)
2482
+ - Increase timeout for long-running scripts
2483
+ - Enable rate limiting to prevent monopolization
2484
+ - Monitor `/metrics` endpoint for bottlenecks
2485
+
2486
+ **For resource-constrained environments:**
2487
+ - Decrease max concurrent requests
2488
+ - Decrease max body size and script length
2489
+ - Decrease timeout to prevent blocking
2490
+
2491
+ ### Integration Patterns
2492
+
2493
+ **Health Check / Polling:**
2494
+ ```python
2495
+ import requests
2496
+ import time
2497
+
2498
+ # Wait for server to be ready
2499
+ while True:
2500
+ try:
2501
+ response = requests.get("http://localhost:18811/health")
2502
+ if response.json()["running"]:
2503
+ break
2504
+ except:
2505
+ time.sleep(1)
2506
+ ```
2507
+
2508
+ **Batch Processing with Retries:**
2509
+ ```python
2510
+ import requests
2511
+ import time
2512
+
2513
+ def process_batch(items, token):
2514
+ for item in items:
2515
+ max_retries = 3
2516
+ for attempt in range(max_retries):
2517
+ try:
2518
+ result = execute_script(item, token)
2519
+ log_success(item, result)
2520
+ break
2521
+ except requests.HTTPError as e:
2522
+ if e.response.status_code == 429:
2523
+ time.sleep(5) # Wait for rate limit
2524
+ elif attempt == max_retries - 1:
2525
+ log_error(item, e)
2526
+ else:
2527
+ continue
2528
+ ```
2529
+
2530
+ **Docker/Kubernetes Health Probe:**
2531
+ ```bash
2532
+ curl -f http://localhost:18811/health || exit 1
2533
+ ```
2534
+
2535
+ ### Reverse Proxy Setup (HTTPS)
2536
+
2537
+ For production deployments requiring HTTPS, use a reverse proxy:
2538
+
2539
+ **nginx example:**
2540
+ ```nginx
2541
+ server {
2542
+ listen 443 ssl;
2543
+ server_name daz-api.example.com;
2544
+
2545
+ ssl_certificate /path/to/cert.pem;
2546
+ ssl_certificate_key /path/to/key.pem;
2547
+
2548
+ # SSE event stream — must disable buffering so events reach clients immediately
2549
+ location /scene/events {
2550
+ proxy_pass http://127.0.0.1:18811;
2551
+ proxy_set_header Host $host;
2552
+ proxy_set_header X-Real-IP $remote_addr;
2553
+ proxy_http_version 1.1; # required for chunked transfer encoding
2554
+ proxy_buffering off; # disable buffering for SSE
2555
+ proxy_cache off;
2556
+ proxy_read_timeout 3600s; # keep connection alive for long-lived streams
2557
+ chunked_transfer_encoding on;
2558
+ }
2559
+
2560
+ location / {
2561
+ proxy_pass http://127.0.0.1:18811;
2562
+ proxy_set_header Host $host;
2563
+ proxy_set_header X-Real-IP $remote_addr;
2564
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
2565
+ proxy_read_timeout 300s; # For long-running scripts
2566
+ }
2567
+ }
2568
+ ```
2569
+
2570
+ **Important:** The current version does not parse `X-Forwarded-For` headers. IP whitelist and rate limiting will see the reverse proxy's IP, not the original client IP. This is a known limitation.
2571
+
2572
+ ### Deployment Checklist
2573
+
2574
+ When deploying to production:
2575
+
2576
+ - [ ] Enable authentication (default)
2577
+ - [ ] Enable IP whitelist with specific allowed IPs
2578
+ - [ ] Enable rate limiting (e.g., 60 requests / 60 seconds)
2579
+ - [ ] Set appropriate concurrent request limit for workload
2580
+ - [ ] Configure timeout based on expected script duration
2581
+ - [ ] Secure token file permissions (`chmod 600` on Unix/macOS)
2582
+ - [ ] Set up monitoring on `/health` and `/metrics` endpoints
2583
+ - [ ] Configure firewall rules to restrict port access
2584
+ - [ ] Test failover behavior (DAZ Studio crashes/restarts)
2585
+ - [ ] Document token rotation procedure for team
2586
+ - [ ] Consider HTTPS via reverse proxy for network exposure
2587
+
2588
+ ---
2589
+
2590
+ ## Contributing
2591
+
2592
+ Contributions are welcome! See areas for improvement:
2593
+
2594
+ **Security:**
2595
+ - X-Forwarded-For support for reverse proxy deployments
2596
+ - Wildcard IP matching (e.g., `192.168.1.*`)
2597
+ - Per-endpoint authentication policies
2598
+ - Token expiration and automatic rotation
2599
+
2600
+ **Features:**
2601
+ - Per-node property/transform change events in SSE stream (currently scene-level only)
2602
+ - Webhook callbacks — register a URL to receive HTTP POST on scene events (alternative to persistent SSE connection)
2603
+ - Script result caching
2604
+ - Request queueing with priorities
2605
+ - Multiple API tokens with labels
2606
+ - CORS header configuration
2607
+
2608
+ **Observability:**
2609
+ - Prometheus metrics endpoint format
2610
+ - Structured logging (JSON)
2611
+ - Distributed tracing support
2612
+
2613
+ **Developer Experience:**
2614
+ - Pre-built binaries for Windows/macOS
2615
+ - Docker image with DAZ Studio and plugin
2616
+ - More example clients (Go, Rust, C#)
2617
+
2618
+ Open a GitHub issue to request features or report bugs.
2619
+
2620
+ ### Development
2621
+
2622
+ For plugin development, see `CLAUDE.md` for detailed architecture notes and development guidelines.
2623
+
2624
+ ---
2625
+
2626
+ ## License & Attribution
2627
+
2628
+ This project is provided under the terms of the AGPL v3 license for use with DAZ Studio.
2629
+
2630
+ **Dependencies:**
2631
+ - [cpp-httplib](https://github.com/yhirose/cpp-httplib) - Header-only HTTP library (AGPL v3 License)
2632
+ - [DAZ Studio SDK](https://www.daz3d.com/daz-studio-4-5-sdk) - Required for building
2633
+
2634
+ **Platform APIs:**
2635
+ - Windows: CryptoAPI for secure random number generation
2636
+ - macOS/Linux: `/dev/urandom` for secure random number generation
2637
+
2638
+ **Authors:**
2639
+ - Original implementation: Blue Moon Foundry
2640
+ - Production-ready improvements: BMF and Community contributors
2641
+
2642
+ For questions, issues, or feature requests, please open an issue on GitHub.