v2.7.2
[2.7.2] - 2026-07-19
No functional changes to dazpy's behavior. 2.7.1's version bump missed
three places still reading 2.7.0: dazpy.__version__ itself (already
baked into the published 2.7.1 wheel, which is why this needed a new
version rather than a doc-only fix), openapi.yaml's info.version plus
two response examples, and the plugin's own DZSRV_VERSION_STR
(common_version.h — what /status//health actually return at
runtime). All three now correctly say 2.7.2.
Actual functional changes added for the 2.7.x release:
DazRenderSettingsIray Canvas support (list_canvases(),
add_canvas(),remove_canvas(),canvases_enabled,
canvas_output_paths()) — enumerate, create, and remove Iray Canvases
(normal/depth/material-ID/etc. passes) and resolve their output file paths
for a given render, closing GitHub issue #19's Canvas gap. Backed by
App.getRenderMgr().getRenderElementObjects()[1], confirmed against a live
DAZ Studio instance — no server-side changes needed.DazScene.create_camera()/DazScene.create_light()— factory
methods to create new camera and light nodes and add them to the scene,
returningDazCamera/DazLightwrappers usable with the rest of the
existing API (set_position,aim_at,set_color, etc).create_light()
accepts"spot","point", or"distant".DazNode.delete()/DazNode.reparent()— remove a node from the
scene entirely, or move it to a new parent in the hierarchy via the
removeNodeChild/addNodeChildpattern, optionally preserving its
world-space transform (preserve_world_transform=Trueby default).DazProperty.raw_value— read/write a property's own dial value,
excluding anyDzERCLinkcontroller contributions. Use this instead of
.valuewhenever a save/restore round trip needs to be exact (see Fixed,
below, for why.valueis unsafe for that).DazScene.overview()— one-call top-level scene snapshot (root
figures, cameras, lights, open scene file, primary selection, node count).DazScene.node_hierarchy(root, max_depth=None)— one-call descendant
tree rooted at a single named node (typically a figure), with atypeon
every entry and an optional recursion-depth limit. Complements the
existingnode_tree(), which always starts from every scene root.DazSceneState— full scene checkpoint: every skeleton's complete
pose (viaDazPose, so bone-level, not just the root transform) plus
camera and light transforms/key properties, captured and restored via
raw_valuethroughout so repeated capture/apply cycles are idempotent.DazCamera.lens_shift_x/.lens_shift_y— read/write properties
backed byDzCamera's "Lens Shift X/Y (mm)" controls, the same values the
render engine uses — needed for depth-based effects (fog, DOF-driven line
weight) to match the actual render.DazScene.export_fbx()/.export_obj()— synchronous export via DAZ
Studio's nativeDzFbxExporter/DzObjExporter, unlike the async job
patternexport_usd_submit()uses — there's no custom pipeline behind
these to poll on.RunSilentis forced on to avoid the native exporter's
modal options dialog hanging the HTTP handler's thread.DazViewport.draw_style()/.set_draw_style()— controls the
viewport's preview quality (Wireframe .. NVIDIA Iray) via
getUserDrawStyle()/setUserDrawStyle(), distinct from
DazRenderSettings.active_engine()below.set_draw_style()reads the
style back after setting it and raisesValueErrorif it didn't take,
sincesetUserDrawStyle()silently no-ops on an unrecognized label.DazRenderSettings.active_engine()/.set_active_engine()— query
and switch the Render Settings "Engine" dropdown. Correctly distinguishes
DzRenderOptions.renderType(which governs "viewport"/
"multi_pass_opengl") fromrenderMgr.getActiveRenderer()(which only
applies in Software mode) — the two are easy to conflate since the UI
presents them as one dropdown.set_active_engine()raisesRenderError
for a pluggable renderer name that isn't registered (e.g. the Filament
plugin not installed).DazRenderSettingsIray quality/samples control —
max_samples/max_time_secs/qualityproperties and
set_quality_preset()(draft/preview/good/final), backed by the active
Iray renderer's property holder since these aren't exposed on
DzRenderOptions. PlusDazClient.list_requests(), wrapping the existing
GET /requestsendpoint to list all tracked async requests by status.- Per-frame animation render submission —
POST /render/animationand
DazClient.render_animation_submit(), mirroring the existing
single-render pattern as one trackable async request that loops a
start/end frame range, writing each frame tooutput_pathwith its
{frame}token substituted (zero-padded). DazNode.fit_to()/.unfit()/.fitted_items()— clothing/prop
fit-to-figure control, matching whatvangard-daz-mcp's hand-rolled
fit/unfit/list-fitted scripts previously did outsidedazpy.DazProperty.get_keys()/.remove_key()/.clear_keys()—
keyframe curve introspection and single-key removal on an animated
property, without hand-rolled DazScript.- dForce simulation control — a
DazDForcemodifier proxy
(freeze_simulation/freeze()/unfreeze(), backed by
DzDForceModifier's "Freeze Simulation" property), wired intoDazNode's
modifier discrimination plus a newdforce_modifiers()filter, and
DazScene.run_dforce_simulation()/.is_simulating()/
.clear_dforce_simulation()driven byDzSimulationMgr. Long-running
simulations use the existing async execute-and-poll path. - General scene-change event stream client —
DazClient. stream_scene_events()plusdazpy/_scene_events.py
(SceneEvent/watch_scene_events()/wait_for_scene_event()), mirroring
the existing render-progress stream so callers can watch or block on
node/light/camera/selection/scene/time/render events over the server's
existingGET /scene/eventsSSE endpoint without hand-rolling SSE
parsing. DazElement.numeric_properties()/.class_name—
numeric_properties()returns{label: value}for every numeric property
on an element in one round trip, instead of needing one round trip per
property afterlist_properties().class_nameexposes the DazScript
class name (e.g."DzFigure") for any element.
Fixed
-
ERC-inflation fix in
DazPosedidn't cover bone rotation channels —
the earlier fix that switched morphs and node properties to
getRawValue()/setRawValue()left boneXRotControl/YRotControl/
ZRotControlchannels on plaingetValue()/setValue(). For rigs where
one bone's rotation is ERC-driven from another (e.g. auto-follow
bend/twist ratios common on Genesis figures), repeated capture/apply
cycles kept inflating that bone's rotation the same way the earlier fix
addressed for morphs/props.capture()/apply()/apply_full()now
feature-detect raw value support on bone rotation controls too. -
DazViewport.capture()left state unrestored if the viewport
disappeared mid-wait — the finish script'sif (!vp) return null;guard
ran before any of the overlay/selection/Tonemapper restore lines, so if
the active viewport became unavailable (closed/changed) during the real
wall-clockconvergence_waitsleep between the prepare and finish scripts,
restoration was skipped entirely -- a state the old atomic single-script
capture could never leave the viewport in. Scene-level restoration
(primary selection, Tonemapper/Environment node visibility) no longer
depends onvpand always runs; only the viewport-specific properties and
the capture itself are skipped (returning no image) ifvpis gone. -
DazViewport.capture()couldn't restore a bone selection — the
two-pass capture's primary-selection restore serialises only the selected
node's name across the prepare/finish round trip and re-resolves it via
Scene.findNode(), which resolves top-level scene nodes but not bones or
other non-node selectable items (e.g. a bone selected via the Joint
Editor), so the original selection was silently lost aftercapture().
Now also captures the owning skeleton's name for bone selections
(isBoneSelectingNode()/getSkeleton()) and falls back to
skeleton.findBone(name)when the directfindNode()lookup misses. -
DazPose.apply_full()didn't zero absent node properties — the
node-property loop only calledsetValue/setRawValueif (v !== undefined), with no else-zero branch, unlike the bones loop (explicit
else setValue(0)) and morphs loop (_v = v !== undefined ? v : 0) right
above it in the same function. This broke the documented contract that
apply_full()drives every channel absent from the pose to zero, and also
affectedDazSceneState.apply()'s "clean baseline" restore, which relies
onapply_full(). Now matches the bones/morphs pattern. -
DazSceneState.apply()didn't isolateapply_full()errors per
skeleton — the per-skeleton try/except only wrapped
scene.find_skeleton(); a followingpose.apply_full(skel)failure
(transient HTTP/DazScript error, stale skeleton reference) propagated
uncaught and aborted restoration of all remaining skeletons, cameras, and
lights, contradicting the docstring's promise that failures are reported
inerrorsrather than raising. Nowapply_full()is wrapped in its own
try/except so a single skeleton's failure is recorded and the loop
continues. -
build_hug_recipe()swapped far-shoulder targets between actors — with
mismatched anchors (e.g.a_anchor="r_hand",b_anchor="l_hand"),
actor_a's hand target used the far shoulder derived from actor_b's anchor
side instead of actor_a's own, producing an anatomically wrong or
self-intersecting embrace. Invisible with the defaultr_hand/r_hand
anchors (both sides happen to compute the same far shoulder), which is all
the prior test covered. Now each actor's far shoulder is derived from its
own anchor's side. -
DazRenderSettings.render()/.render_and_wait()broke truthiness
callers — these switched from returningboolto aRenderOutcome
dataclass with no__bool__, so existing code written against the old
contract (if rs.render():, including downstream projects like
vangard-daz-mcp) always evaluated truthy and silently treated failed
renders as successful.RenderOutcome.__bool__now reflectssuccess, so
if rs.render():behaves the same as before; use.success/.output_path
directly if you need the full outcome. -
DazViewport.capture(..., backdrop_color=...)raisedReferenceError
restoring the background — the two-pass capture's finish script
referenced the JS variableprevBg, butprevBgwas only declared in the
separate prepare-scriptexecute()call (a different HTTP round trip with
no shared JS scope), so restoring the background always threw.prevBgis
now round-tripped through Python (returned from the prepare script as
plain JSON, alongsideaxesOn/selectionName/etc.) and substituted
directly into the finish script. -
DazPose.capture()/.apply()/.apply_full()inflate ERC-driven
properties on every round trip — node-level properties captured via
getValue()(e.g. a "Scale" dial fed by dozens ofDzERCLink-linked
morphs) return the post-ERC computed total, butapply()/apply_full()
wrote that total back viasetValue(), which sets the property's raw
slot — so the ERC links add their contribution again on top. Each
capture/apply cycle compounded the drift (observed inflating a custom
character's Scale dial from 100% to 270% over a handful of cycles in
daz-mcp-server's test suite, via its use ofdaz_save_pose/
daz_load_pose). Now usesgetRawValue()/setRawValue()(see
DazProperty.raw_valueabove) for props and morphs, which round-trip
correctly regardless of ERC links. -
/rendercamera selection — the native render endpoint called
App.getViewportMgr()when acameraNamewas supplied, which is undefined
in this DAZ Studio version and made every camera-targeted render fail
immediately with aTypeError. Switched toMainWindow.getViewportMgr(),
matching the pattern already used bydazpy's own render/viewport helpers. -
/renderblack output withcameraName— after the above fix, renders
submitted with an explicit camera completed without error but produced a
black image, because the render camera actually read bydoRender()comes
fromopts.camera, not the viewport's active camera. The endpoint only
updated the viewport (setActiveCamera) and never setopts.camera, so
the renderer used a stale/null camera. Now sets both, matching
dazpy/_render.pyand this repo's own camera-preset scripts. Affects both
/renderand/render/batch(they sharebuildRenderScript()). -
DazClient.status()/.health()/.metrics()misreported auth
failures — these called the internal_get()helper directly without
the 401/403 checkexecute()/render_submit()/etc. already had, so a 401
with no response body surfaced as a raw JSON-decode error instead of
AuthenticationError. -
ScriptErrordropped captured output on failure — callers building
diagnostics from a failed script (e.g.daz-mcp-server'sdaz_execute)
lost theprint()output that led up to the error.ScriptError/
ScriptRuntimeError/ScriptSyntaxErrornow accept and surface an
optionaloutputlist via.diagnostic. -
Dropped the unsupported
3delightrender engine option — DAZ Studio
no longer ships 3Delight; removed it from the engine allow-list and docs
so a render request naming it fails fast with a clear error instead of
silently mapping to a renderer that isn't there.
Built Artifacts
DAZ Studio 4.5+
- Windows:
DazScriptServer-ds4-windows.dll - macOS Intel:
DazScriptServer-ds4-macos-Intel.dylib - macOS Apple Silicon:
DazScriptServer-ds4-macos-AppleSilicon.dylib
Daz Studio 6.25+
- Windows:
dsp_DazScriptServer-ds6-windows.dll - macOS Intel:
dsp_DazScriptServer-ds6-macos-Intel.dylib - macOS Apple Silicon:
dsp_DazScriptServer-ds6-macos-AppleSilicon.dylib
Python
- Python Package:
dazpy-*.whl