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 the ObjectFactory, so an override the application registered for the metaclass applies. createdClass in the reply shows the class actually created. The name defaults to the metaclass name without its namespace, and the index to appending. overwatch.metaclasses lists what exists.

  • nodetree.instantiate <prefabUrl> <parent|-> [count] [rootName] instantiates the prefab count times, 1 to 10000. One copy keeps rootName, or the prefab’s own name. Several are named <name>_1 to <name>_<count>, so they can also be addressed by path with setprop and props. overwatch.kzbfiles and overwatch.allresources list the prefab URLs.

  • - as the parent creates the node, or every copy, without a parent: the service holds it detached, and nodetree.attach places it later. An index needs a parent, so nodetree.create refuses 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.TextBlock3D under a 3D one such as a Scene.

  • 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.move puts 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.Text in 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.create or nodetree.instantiate span 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 ui command first.

  • nodetree.destroycreated removes 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 under failures. 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.perf with 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.reattachall undoes 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.purge reclaims what nothing else uses, and overwatch.allresources shows 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 (keepAlive in 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 detachedFromIndex in 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, and nodetree.held reports it once under reattachedByApplication.

  • 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.held lists it, and nodetree.reattach or nodetree.attach puts it in the tree. A failed nodetree.attach likewise 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 and nodetree.held say "attachable": false, and nodetree.destroy releases it.

  • The reply’s from (the node’s old path), fromParent and fromIndex say where the node was, so nodetree.move <node> <fromParent> <fromIndex> moves it back. fromParent is 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.create and nodetree.instantiate too: 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.attach of a node that is in the tree.

  • nodetree.reattachall leaves a node created without a parent where it is, and lists it under withoutHome: 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 the ObjectFactory: Kanzi.AnimationPlayer, Kanzi.PropertyDrivenAnimationPlayer, the property target interpolators, the value accumulators, the input manipulators, or a plugin’s own. overwatch.metaclasses <class> effective lists the properties a class declares. An unknown, abstract or non-component class is refused.

  • The name is how the component is addressed: by nodetree.setcomponentprop and nodetree.removecomponent, and by a message such as an Animation Player’s Play. It must be unique on the node.

  • The properties are written before the component is added, as overwatch.setprop writes 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 reports Cannot add. If taking it off throws too, the reply says the component is left on the node, with the reason under cleanup, and nodetree.destroycreated retries the removal. If that throws again, the node is listed under componentFailures and the component stays. The order matters because a component can read a property only when it attaches.

  • A trigger (Kanzi.MessageTrigger, Kanzi.OnPropertyChangedTrigger, Kanzi.TimerTrigger and 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.components reports a trigger’s actions and conditions counts, so an empty one is visible.

  • Removing. nodetree.removecomponent removes any component, one the project gave the node included, as nodetree.destroy takes 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.destroycreated removes every component Monitor added, from created nodes and application nodes alike, and reports componentsRemoved. A node whose component removal fails is listed under componentFailures, 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 AnimationPlayer.Timeline=<url> and AnimationPlayer.AutoplayEnabled=true to nodetree.addcomponent

It starts playing when it is added.

Set AutoplayEnabled with nodetree.setcomponentprop after adding it

Nothing. The player reads AutoplayEnabled only when it attaches.

Send Message.AnimationPlayer.Play to the node, with TargetName set to the player’s name

It plays, however it was set up. Pause, Resume and Stop work the same way: Pause holds the animated value, Resume continues from it.

Change DurationScale, RepeatCount, PlaybackMode or Timeline while it plays

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

nodetree.create

Creates a node of a metaclass through the ObjectFactory and adds it under a parent. Usage: nodetree.create <metaclass> <parent|-> [name] [index].

nodetree.instantiate

Instantiates a prefab 1 to 10000 times under a parent, and reports every copy’s handle and path. Usage: nodetree.instantiate <prefabUrl> <parent|-> [count] [rootName].

nodetree.destroy

Takes any node out of the tree, and reports whether it was destroyed. Usage: nodetree.destroy <path|@handle>.

nodetree.destroycreated

Destroys every node created in this session.

nodetree.detach

Takes any node out of the tree without destroying it, and holds it to be put back. Usage: nodetree.detach <path|@handle>.

nodetree.reattach

Puts a detached node back under its parent at the index it was taken from. Usage: nodetree.reattach <@handle>.

nodetree.reattachall

Puts every detached node back, the most recently detached first.

nodetree.attach

Puts a detached node under any parent. Usage: nodetree.attach <@handle> <parent> [index].

nodetree.move

Moves any node to another parent, or to another position under the same one, in one step. Usage: nodetree.move <path|@handle> <parent> [index].

nodetree.held

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.

nodetree.addcomponent

Adds a node component of any class to a node, with its properties written before it attaches. Usage: nodetree.addcomponent <path|@handle> <class> <name> [<property>=<value> ...]. See Adding node components.

nodetree.components

Lists a node’s components: name, class, whether Monitor added it, whether it is attached, and a trigger’s action and condition counts. Usage: nodetree.components <path|@handle>.

nodetree.removecomponent

Removes a named component from a node, whoever added it. Usage: nodetree.removecomponent <path|@handle> <name>.

nodetree.setcomponentprop

Sets a property of a node’s named component. Some take effect only at the component’s next start. Usage: nodetree.setcomponentprop <path|@handle> <name> <property> <value>.

See also

Using the Overwatch service

Using the Kanzi Monitor MCP server

Configuring Kanzi Monitor