Using the NodeTree service¶
The NodeTree service changes the structure of a running application’s node tree from the console. Use it to build a test scene the project does not contain, or to scale one to 10, 100 or 1000 copies with one command. You can also take any node out of the tree, take a node out and put it back later, and move nodes and reorder them.
It is separate from the Overwatch service because the two do different kinds of thing. Overwatch reports on the application, and its writes change values. The NodeTree service changes the application’s structure: destroying a node that the application logic or a binding expects can break the application. That is why it has its own configuration key, ServiceNodeTreeEnabled, and its own command prefix, nodetree.*.
The service is on by default. Set ServiceNodeTreeEnabled = 0 in kanzimonitor.cfg to switch it off; its commands are then not registered. See Configuring Kanzi Monitor.
Everything the service creates exists only in the running application. Nothing is written back to the project.
nodetree.create Kanzi.TextBlock2D "/Screen/RootNode" Label 0
→ {"success":true,"handle":"@48213977","path":"/Screen/RootNode/Label",
"metaclass":"Kanzi.TextBlock2D","createdClass":"Kanzi.TextBlock2D","state":"attached"}
nodetree.instantiate "kzb://myproject/Prefabs/Label" "/Screen/RootNode" 100
→ {"success":true,"url":"kzb://myproject/Prefabs/Label","count":100,"state":"attached",
"nodes":[{"handle":"@48213978","path":"/Screen/RootNode/Label_1"}, … ,
{"handle":"@48214077","path":"/Screen/RootNode/Label_100"}]}
nodetree.held # what the service holds
nodetree.destroy @48213983
→ {"success":true,"path":"/Screen/RootNode/Label_6","handle":"@48213983","destroyed":true}
nodetree.destroycreated # destroy everything this session created
Handles¶
Every node the service creates, detaches or moves gets a handle, such as @48213977, which the reply returns. A handle names the node itself for one run of the application: it does not change when the node’s path does, and it is never reused, so a stale handle can only miss. Handles are numbered across the whole run, so a handle never names two objects, whatever they are. Each run starts numbering at a different, random point, so a handle kept from an earlier run is almost always refused rather than reaching whatever has that number now; that is a safeguard, not a guarantee. Do not keep a handle across a restart: list or find the node again. Every nodetree.* argument that names a node accepts a path, as overwatch.nodes reports it, or a handle, and so do overwatch.props, overwatch.setprop, overwatch.dispatchmessage and debug.breakprop, so a detached node is read and written through its handle. A node has one handle whichever service reports it: overwatch.nodes gives one to every node it lists, and the NodeTree commands report the same one. See Addressing a node. A handle starts with @ because # in a path is Kanzi’s alias syntax.
A path names the first node it finds. Two siblings can have the same name, for example when nodetree.create is given a name that is already taken, and then the second one’s path finds the first. A reply about a node whose path does not reach it says "pathAmbiguous": true: use the handle for that node.
Quote a path that contains spaces: a command given more arguments than it takes, or a quote that is never closed, is refused rather than acting on part of the line. A path segment can be a Kanzi alias, #name; see Aliases in a path.
nodetree.held lists every node the service tracks, attached or detached, with each node’s handle, state (attached or detached), origin (metaclass or prefab with the metaclass name or URL, or existing for the application’s own node) and current path. A detached node also reports its home: the parent’s path, the index, and whether the parent still exists. A tracked node inside a detached node is detached with it: it reports "state": "detached", the path it had in the tree, and inside, the handle of the detached node it goes back with. A node the application destroyed since is reported once under gone, and then dropped. A node the application took out of the tree while keeping it alive, alone or with an ancestor, is reported once under released, and then no longer tracked; nodetree.destroycreated counts such nodes as released.
gone, released and reattachedByApplication are reported to whichever client asks first. With two clients connected, for example the MCP server and a console, one of them may not see a report; its next nodetree.held still lists the current state.
The service never keeps a node alive that is in the tree: while a node is attached, the tree owns it, and the service only watches it. While a node is detached, the service holds it, which is what keeps it alive.
Creating nodes and instantiating prefabs¶
nodetree.create <metaclass> <parent|-> [name] [index]creates the node through theObjectFactory, so an override the application registered for the metaclass applies.createdClassin the reply shows the class actually created. The name defaults to the metaclass name without its namespace, and the index to appending.overwatch.metaclasseslists what exists.nodetree.instantiate <prefabUrl> <parent|-> [count] [rootName]instantiates the prefabcounttimes, 1 to 10000. One copy keepsrootName, or the prefab’s own name. Several are named<name>_1to<name>_<count>, so they can also be addressed by path withsetpropandprops.overwatch.kzbfilesandoverwatch.allresourceslist the prefab URLs.-as the parent creates the node, or every copy, without a parent: the service holds it detached, andnodetree.attachplaces it later. An index needs a parent, sonodetree.createrefuses one with-.The parent must be of the same dimension as the node: a 2D node under a 2D parent, a 3D node such as
Kanzi.TextBlock3Dunder a 3D one such as aScene.The template’s root is attached as it is. The startup prefab’s root, for example, is a
Kanzi.Screen: instantiated under/Screen, it becomes a Screen inside the Screen. It renders, but the tree has one level more than at startup.nodetree.moveputs the content back in its place; see Example: reload the UI from the startup prefab.Bindings resolve against where a copy is placed. A relative binding such as
../../:TextConcept.Textin the prefab follows the copy’s own ancestors, which need not be the ones it has in the project.How long a creation took is the
nodetree.createornodetree.instantiatespan in the trace (see Finding the frame a command ran in). The layout and rendering of what was created follow in the same frame or the next.
Destroying nodes¶
nodetree.destroy <node> takes any node out of the tree, the application’s own included, and releases the service’s reference to it. A node is destroyed when its last reference goes: when the application holds one too, the node leaves the tree but lives on, and the reply says "destroyed": false. Its subtree goes with it.
A node with no parent, such as a screen, is refused: it is the root of its tree. Destroy its children instead.
A Viewport 2D’s scene is refused. Its parent is the viewport, but it is not one of the viewport’s children, so it cannot be taken out like one.
Monitor’s own overlay, and any node that contains it, is refused. Hide the overlay with the
uicommand first.nodetree.destroycreatedremoves every component this session added (see Adding node components), then destroys every node this session created, newest first. A created node that holds Monitor’s overlay is skipped and reported underfailures. It reports how many were destroyed, how many live on because the application holds a reference, and how many were already gone.
Warning
Destroying a node that the application logic or a binding expects can break the application. This is a diagnostic tool. To keep it from being used, set ServiceNodeTreeEnabled = 0.
Refused, with the reason in the reply: an unknown, abstract or non-node metaclass, a 2D/3D mismatch, an unknown prefab URL or a resource that is not a prefab, an unknown path or handle, a parent that is not in the node tree (a detached node, or a node inside one), an index past the last child, a count outside 1 to 10000, a node with no parent, a Viewport 2D’s scene, and Monitor’s overlay.
From a breakpoint. A nodetree.* command can be a breakpoint command. A breakpoint triggers inside the property write, but its command runs after the write has returned, so it can even destroy the node whose property it watches. See When a breakpoint’s command runs.
Detaching and reattaching nodes¶
nodetree.detach <node> takes any node out of the tree without destroying it. The service holds the node, with its subtree, and records where it came from: the parent and the index. A detached node has no path, so the reply’s handle is how it is reached again. nodetree.reattach <@handle> puts it back under the same parent at the same index, and nodetree.reattachall puts every detached node back.
nodetree.detach "/Screen/RootNode/Stack Layout 3D/Sphere_1"
→ {"success":true,"handle":"@48213980","path":"/Screen/RootNode/Stack Layout 3D/Sphere_1",
"parent":"/Screen/RootNode/Stack Layout 3D","index":1}
overwatch.perf # the frame cost without the subtree
nodetree.reattach @48213980
→ {"success":true,"handle":"@48213980","path":"/Screen/RootNode/Stack Layout 3D/Sphere_1","index":1}
Use it to:
attribute render cost to a subtree: read
overwatch.perfwith the subtree detached and again with it back;release a subtree’s resources and load them again: see below;
see what the application does without a node, and return to where it was.
Things to know:
A detach and reattach with nothing else in between restore the tree exactly.
nodetree.reattachallundoes the detaches in reverse, the most recently detached first, so nodes taken from the same parent return to their original order. Reattaching single nodes in another order gives another order.A detached node releases the resources it refers to, even though the service holds it. A node refers to a project resource, such as its mesh, material, texture or render pass, by resource ID. Kanzi resolves that reference when the node joins the tree and drops it when the node leaves. So after a detach,
overwatch.purgereclaims what nothing else uses, andoverwatch.allresourcesshows what went. The reattach then loads it again, which exercises the reload path on demand, without the GPU context loss that otherwise provokes it. A detach does not release a resource held any other way: by another node in the tree, by a resource dictionary or application code, as the node’s own clone of a resource, or because it is marked keep-alive (keepAlivein the resource listings), which a purge never reclaims.The application can change the tree in between. If the parent has fewer children by then, the node goes last, and
detachedFromIndexin the reply says where it was. If the parent no longer exists, the node stays detached and the reply says so; destroy it to release it. If the application holds the node too and puts it in a tree itself, its placement stands: the node is attached from then on, where the application put it, andnodetree.heldreports it once underreattachedByApplication.A node with no parent, such as a screen, and Monitor’s own overlay, or a node that contains it, are refused, as is detaching a node that is already detached or reattaching one that is not.
Warning
Detaching a node that the application logic or a binding expects can break the application, just as destroying it can.
Moving and placing nodes¶
nodetree.move <node> <parent> [index] takes any node in the tree to another parent, or to another position under the same parent, in one step. nodetree.attach <@handle> <parent> [index] puts a detached node anywhere: one taken out with nodetree.detach, or one created with - as its parent.
nodetree.move "/Screen/RootNode/Stack Layout 3D/Sphere_2" "/Screen/RootNode/Stack Layout 3D" 0
→ {"success":true,"handle":"@48213977","path":"/Screen/RootNode/Stack Layout 3D/Sphere_2","index":0,
"from":"/Screen/RootNode/Stack Layout 3D/Sphere_2","fromParent":"/Screen/RootNode/Stack Layout 3D",
"fromIndex":2}
nodetree.create Kanzi.TextBlock2D - Label # held detached, no parent: "handle":"@48213978"
nodetree.attach @48213978 "/Screen/RootNode" 1
The index is the node’s position after the move, from 0 to the number of other children. With the node’s own parent, a move reorders the siblings. Without an index, the node goes last.
A move to the node’s own place does nothing. The reply says
"unchanged": true, and the node does not leave the tree.A refused move changes nothing. Every check runs before the node leaves its parent. If placing the node fails anyway, which takes an exception from Kanzi, Kanzi Monitor takes it out of the new parent if Kanzi left it there, puts it back where it was, and the reply says so. If even that fails, the node is kept detached rather than lost: the error reply gives its
handle,"state": "detached"and its home,nodetree.heldlists it, andnodetree.reattachornodetree.attachputs it in the tree. A failednodetree.attachlikewise leaves the node detached. If Kanzi also fails to take the node out again, it leaves the node out of the tree with its parent still set, and refuses to attach it anywhere after that: after a move as after an attach, the service keeps holding the node, the reply andnodetree.heldsay"attachable": false, andnodetree.destroyreleases it.The reply’s
from(the node’s old path),fromParentandfromIndexsay where the node was, sonodetree.move <node> <fromParent> <fromIndex>moves it back.fromParentis a path, so like any path it finds the first node of that name: if the old parent has a sibling of the same name before it, the path finds that sibling instead, and the reply then carries"fromParentAmbiguous":true. The service does not record moves: no command undoes them.Moving a node makes it leave the tree and join it again, so it releases and reacquires the project resources it refers to, as a detach and reattach do.
The parent must be in the node tree. A detached node, or a node inside one, is refused as a parent, by
nodetree.createandnodetree.instantiatetoo: attach it first, then place nodes under it.Refused, with the reason in the reply: a parent that is the node itself or inside it, a parent that is not in the node tree, a 2D/3D mismatch, an index out of range, a node with no parent, Monitor’s own overlay as the node or the parent, and
nodetree.attachof a node that is in the tree.nodetree.reattachallleaves a node created without a parent where it is, and lists it underwithoutHome: it has never been in the tree, so it has nowhere to go back to.
Cleaning up after a session¶
To take the tree back to where the session started, run nodetree.reattachall first, then nodetree.destroycreated. In the other order, a node detached from under a created node has no parent to go back to once that node is destroyed. Moves are not undone by either command: move each node back with the fromParent and fromIndex its move reply gave, the most recent move first.
Example: reload the UI from the startup prefab¶
The startup prefab’s root is a Kanzi.Screen, which Kanzi makes the root of the tree at startup. To replace the running UI with a fresh copy of it, and keep the tree exactly as it was at startup:
nodetree.destroy "/Screen/RootNode" # the running content
nodetree.instantiate "kzb://<project>/StartupPrefab" "/Screen" 1 Fresh
→ … "nodes":[{"handle":"@48213980","path":"/Screen/Fresh"}] # a Screen under the Screen
nodetree.move "/Screen/Fresh/RootNode" "/Screen" 0 # the content, under the real screen
nodetree.destroy @48213980 # the empty inner Screen
overwatch.nodes then reports the same nodes, names and types as at startup, and the fresh copy’s bindings resolve. The real root Screen itself cannot be replaced: it has no parent, and making another node the root needs the application’s own API. Application code that kept pointers to the original nodes, for example from onProjectLoaded(), still points at the destroyed ones.
Adding node components¶
A node component adds behavior to a node: an Animation Player plays an animation timeline on it, a Property Target Interpolator smooths changes to one of its properties, a value accumulator increments one over time. nodetree.addcomponent adds one of any class to any node, the application’s own nodes included.
nodetree.addcomponent <node> <class> <name> [<property>=<value> ...]
nodetree.addcomponent "/Screen/RootNode" Kanzi.AnimationPlayer Fade AnimationPlayer.Timeline="kzb://<project>/Animation Clips/Fade" AnimationPlayer.AutoplayEnabled=true
→ {"success":true,"node":"/Screen/RootNode","handle":"@48213977","component":"Fade","class":"Kanzi.AnimationPlayer","attached":true,
"properties":[{"name":"AnimationPlayer.Timeline","type":"Resource","value":"kzb://…/Fade"},
{"name":"AnimationPlayer.AutoplayEnabled","type":"bool","value":"true"}]}
nodetree.components "/Screen/RootNode"
nodetree.setcomponentprop "/Screen/RootNode" Fade AnimationPlayer.DurationScale 2
nodetree.removecomponent "/Screen/RootNode" Fade
The class is any concrete class derived from
Kanzi.NodeComponent, created through theObjectFactory:Kanzi.AnimationPlayer,Kanzi.PropertyDrivenAnimationPlayer, the property target interpolators, the value accumulators, the input manipulators, or a plugin’s own.overwatch.metaclasses <class> effectivelists the properties a class declares. An unknown, abstract or non-component class is refused.The name is how the component is addressed: by
nodetree.setcomponentpropandnodetree.removecomponent, and by a message such as an Animation Player’sPlay. It must be unique on the node.The properties are written before the component is added, as
overwatch.setpropwrites them: the type from the registry, the value in that type’s format, a Resource as a resource URL. Use qualified names. A name whose type is known neither from the registry nor from the component itself is refused rather than guessed; an unqualified one is answered with the qualified names it could mean. When any property fails, nothing is added. When the component’s own attach throws, it is taken off the node again and the command reportsCannot add. If taking it off throws too, the reply says the component is left on the node, with the reason undercleanup, andnodetree.destroycreatedretries the removal. If that throws again, the node is listed undercomponentFailuresand the component stays. The order matters because a component can read a property only when it attaches.A trigger (
Kanzi.MessageTrigger,Kanzi.OnPropertyChangedTrigger,Kanzi.TimerTriggerand the like) can be added, but a trigger acts only through its actions and conditions, which the NodeTree service cannot add. Such a trigger does nothing when it fires.nodetree.componentsreports a trigger’sactionsandconditionscounts, so an empty one is visible.Removing.
nodetree.removecomponentremoves any component, one the project gave the node included, asnodetree.destroytakes any node; it reports whether Monitor had added it and whether it was destroyed. A property the component animated returns to its own value.nodetree.destroycreatedremoves every component Monitor added, from created nodes and application nodes alike, and reportscomponentsRemoved. A node whose component removal fails is listed undercomponentFailures, and the rest are still cleaned up. When Monitor’s NodeTree service is torn down, the components it added to nodes that still exist are removed too, so none outlives it.
Playing an animation¶
An Animation Player plays its AnimationPlayer.Timeline, a timeline resource such as an animation clip from the project, set by its URL. Measured on Kanzi 4.1.1:
What you do |
What happens |
|---|---|
Give |
It starts playing when it is added. |
Set |
Nothing. The player reads |
Send |
It plays, however it was set up. |
Change |
The running playback is not affected. The player applies them at the next Play or Resume. |
Send the messages with overwatch.dispatchmessage (see Dispatching a message):
overwatch.dispatchmessage "/Screen/RootNode" Message.AnimationPlayer.Play TargetName=Fade
overwatch.dispatchmessage "/Screen/RootNode" Message.AnimationPlayer.Pause TargetName=Fade
Without TargetName the message reaches every component on the node that handles it. Play also takes PlaybackMode, DurationScale and RepeatCount. The reply lists those you leave out under defaulted. For Play that means the player uses its own properties for them.
Available commands¶
Command |
Description |
|---|---|
|
Creates a node of a metaclass through the |
|
Instantiates a prefab 1 to 10000 times under a parent, and reports every copy’s handle and path. Usage: |
|
Takes any node out of the tree, and reports whether it was destroyed. Usage: |
|
Destroys every node created in this session. |
|
Takes any node out of the tree without destroying it, and holds it to be put back. Usage: |
|
Puts a detached node back under its parent at the index it was taken from. Usage: |
|
Puts every detached node back, the most recently detached first. |
|
Puts a detached node under any parent. Usage: |
|
Moves any node to another parent, or to another position under the same one, in one step. Usage: |
|
Lists every node the service tracks, attached or detached: handle, state, origin, current path and, for a detached node, its home or the node it is inside. |
|
Adds a node component of any class to a node, with its properties written before it attaches. Usage: |
|
Lists a node’s components: name, class, whether Monitor added it, whether it is attached, and a trigger’s action and condition counts. Usage: |
|
Removes a named component from a node, whoever added it. Usage: |
|
Sets a property of a node’s named component. Some take effect only at the component’s next start. Usage: |