diff --git a/docs/quantum/README.md b/docs/quantum/README.md new file mode 100644 index 0000000000..87387316af --- /dev/null +++ b/docs/quantum/README.md @@ -0,0 +1,53 @@ +# Quantum — Overview + +Quantum is Stride's graph-based introspection framework. It wraps any .NET object hierarchy into a typed node graph, and that graph is the single source of truth for three things: **property grid display**, **undo/redo**, and **asset override tracking** (the "bold = overridden" behaviour in prefab/archetype workflows). + +## Layers + +```mermaid +flowchart TD + A["Stride.Core.Quantum
sources/presentation/Stride.Core.Quantum/
Live node graph over .NET objects"] + B["Stride.Core.Assets.Quantum
sources/assets/Stride.Core.Assets.Quantum/
Override tracking, base-asset linking"] + C["Stride.Core.Presentation.Quantum
sources/presentation/Stride.Core.Presentation.Quantum/
INodePresenter — property grid view model"] + D["Stride.Core.Assets.Editor
sources/editor/Stride.Core.Assets.Editor/Quantum/
IAssetNodePresenter, AssetNodePresenterUpdaterBase"] + E["Stride.Assets.Presentation
sources/editor/Stride.Assets.Presentation/NodePresenters/
Concrete updaters per asset type"] + + A --> B + B --> C + C --> D + D --> E +``` + +## Data Flow + +1. **Asset opens** — `AssetPropertyGraph` (created via `AssetQuantumRegistry.ConstructPropertyGraph` with the session's `AssetPropertyGraphContainer`) calls `GetOrCreateNode(asset)` on the underlying `NodeContainer` to build the core graph. +2. **Override semantics** — `AssetPropertyGraph` links each node to its counterpart in the base asset graph (if any) and marks inherited vs. overridden values. +3. **Presenter tree** — `AssetNodePresenterFactory` walks the node graph and creates one `IAssetNodePresenter` per node. +4. **Updaters run** — all registered `INodePresenterUpdater` implementations get `UpdateNode()` called per node, then `FinalizeTree()` called once for the full tree. +5. **Property grid binds** — the WPF property grid binds to the presenter tree: `DisplayName` → label, `Value/UpdateValue` → input control, `IsVisible/IsReadOnly` → visibility and editability, `AttachedProperties` → input constraints. + +## When You Need Quantum + +> **Decision tree:** +> +> - Adding a new asset type with default property grid display? +> → **No Quantum code needed.** `[DataMember]` and `[Display]` attributes on the asset class +> are sufficient. See [asset-system/README.md](../asset-system/README.md). +> +> - Customising visibility, display names, numeric constraints, or computed properties +> for a new or existing asset? +> → **Write an `INodePresenterUpdater`.** See [property-grid.md](property-grid.md). +> +> - Asset class holds members that are references to other content objects (Prefabs, Textures)? +> → **Write an `AssetPropertyGraphDefinition`.** See [asset-graph.md](asset-graph.md). +> +> - Custom override or inheritance behaviour (rare — scenes and prefabs already cover this)? +> → **Subclass `AssetPropertyGraph`.** See [asset-graph.md](asset-graph.md). + +## Spoke Files + +| File | Covers | +|---|---| +| [graph-model.md](graph-model.md) | `IGraphNode`, `IObjectNode`, `IMemberNode`, `NodeContainer`, mutations, change listeners | +| [asset-graph.md](asset-graph.md) | `AssetPropertyGraph`, override model, `AssetPropertyGraphDefinition`, `AssetQuantumRegistry` | +| [property-grid.md](property-grid.md) | `INodePresenter`, `IAssetNodePresenter`, updater pipeline, `INodePresenterUpdater` cookbook | diff --git a/docs/quantum/asset-graph.md b/docs/quantum/asset-graph.md new file mode 100644 index 0000000000..ca0c21500d --- /dev/null +++ b/docs/quantum/asset-graph.md @@ -0,0 +1,121 @@ +# Asset Property Graph + +## Role + +`Stride.Core.Assets.Quantum` wraps the core Quantum graph with asset-specific semantics: override tracking, base-asset linking, and archetype/prefab inheritance. When a derived prefab overrides a value from its base, the graph records that override and can reset it. This is the layer that makes "this property is bold because it's overridden" possible in GameStudio. + +## `IAssetNode` Extensions + +`IAssetNode` extends `IGraphNode` with asset-aware members: + +| Member | Type | Purpose | +|---|---|---| +| `PropertyGraph` | `AssetPropertyGraph` | The graph that owns this node | +| `BaseNode` | `IGraphNode` | The corresponding node in the base asset, or `null` if none | +| `OverrideChanging` | event | Raised before override state changes | +| `OverrideChanged` | event | Raised after override state changes | +| `ResetOverrideRecursively()` | method | Resets this node and all descendants to inherited values | +| `SetContent(key, node)` | method | Attaches an auxiliary node (used internally) | +| `GetContent(key)` | method | Retrieves an attached auxiliary node | + +`IAssetMemberNode` (extends `IAssetNode`, `IMemberNode`) and `IAssetObjectNode` (extends `IAssetNode`, `IObjectNode`) are the concrete asset-aware node types. `IAssetObjectNode` adds per-item override tracking for collections (`IsItemInherited`, `IsItemOverridden`, `OverrideItem`, etc.). + +## Override Model + +A property in a derived asset is in one of three states: + +| State | Meaning | GameStudio visual | +|---|---|---| +| **Inherited** | Value comes from the base asset; any change to the base propagates here | Normal weight, italic | +| **Overridden** | Value was explicitly set on this derived asset, shadowing the base | Bold | +| **No base** | Asset has no base (or this property has no base equivalent) | Normal weight | + +**`ResetOverride()`** is a method on `IAssetNodePresenter` (the presenter layer — see `property-grid.md`), not on `IAssetNode` directly. Calling it restores the overridden value to its inherited state, and the graph then re-propagates the base value. The underlying graph node's `ResetOverrideRecursively()` handles the recursive reset; the presenter method is the entry point from the UI. + +When a composite node (an object with children) is reset, all descendant nodes are also reset recursively. + +> [!NOTE] Just adding a new asset type +> If your asset has no base/derived relationship and you are not implementing archetypes or prefab composition, the override model is invisible to you. `IsInherited` will always be `false` and `HasBase` will always be `false`. You do not need to understand this layer to add a new asset type. + +## `AssetPropertyGraph` + +`AssetPropertyGraph` wraps a `NodeContainer` and adds override semantics on top: + +- Created via `AssetQuantumRegistry.ConstructPropertyGraph(container, assetItem, logger)` where `container` is an `AssetPropertyGraphContainer` (not a raw `NodeContainer`) — this is obtained from the editor session; do not instantiate it manually +- Tied to the editor session — created when an asset is opened, disposed when closed +- Links each node to its counterpart in the base asset graph (if the asset has an archetype) +- Propagates base values to all inherited nodes on load + +You rarely interact with `AssetPropertyGraph` directly. The presenter layer reads from it via `IAssetNodePresenter.Asset` and `IAssetNodePresenter.HasBase`. + +## `AssetPropertyGraphDefinition` + +`AssetPropertyGraphDefinition` tells the graph which member values are **object references** (shared identities, loaded separately by `ContentManager`) vs **inline data** (copied into the asset). + +Provide one only when your asset type holds references to other content objects. If you don't, the default definition treats all values as inline — which is correct for most new asset types. + +```csharp +// sources/engine/Stride.Assets/YourFeature/YourAssetPropertyGraphDefinition.cs +using Stride.Core.Assets.Quantum; +using Stride.Core.Quantum; + +namespace Stride.Assets.YourFeature; + +[AssetPropertyGraphDefinition(typeof(YourAsset))] +public class YourAssetPropertyGraphDefinition : AssetPropertyGraphDefinition +{ + // Return true when the value stored in 'member' is an object reference + // (i.e. a handle to a separately-loaded content object, not an inline copy). + public override bool IsMemberTargetObjectReference(IMemberNode member, object? value) + { + // Example: treat any Prefab member as an object reference + if (value is Prefab) + return true; + + return base.IsMemberTargetObjectReference(member, value); + } + + // Return true when a collection item is an object reference. + public override bool IsTargetItemObjectReference(IObjectNode collection, NodeIndex itemIndex, object? value) + { + // Example: treat items in PrefabCollection as object references + if (collection.Descriptor.ElementType == typeof(Prefab)) + return true; + + return base.IsTargetItemObjectReference(collection, itemIndex, value); + } +} +``` + +The `[AssetPropertyGraphDefinition(typeof(YourAsset))]` attribute is discovered automatically when the assembly is registered. No manual registration is needed beyond `AssetQuantumRegistry.RegisterAssembly()` in `Module.cs`. + +> [!NOTE] Just adding a new asset type +> If all your asset's properties are plain data values (numbers, strings, lists of structs), you do not need an `AssetPropertyGraphDefinition`. Only provide one when your asset class has members that hold references to other content objects (Prefabs, Textures, Materials, etc.) that should remain as references rather than be embedded inline. + +## `AssetQuantumRegistry` + +| Method | When to call | +|---|---| +| `AssetQuantumRegistry.RegisterAssembly(assembly)` | From `Module.cs` — call once per assembly containing asset graph types | +| `AssetQuantumRegistry.ConstructPropertyGraph(AssetPropertyGraphContainer, AssetItem, ILogger?)` | Called internally by the editor session; do not call manually | +| `AssetQuantumRegistry.GetDefinition(assetType)` | Called internally; do not call manually | + +`RegisterAssembly` scans the assembly for `AssetPropertyGraph` subclasses (decorated with `[AssetPropertyGraph(typeof(T))]`) and `AssetPropertyGraphDefinition` subclasses (decorated with `[AssetPropertyGraphDefinition(typeof(T))]`) and registers them. + +In `Module.cs` for an assembly that contains both asset classes and graph types: +```csharp +[ModuleInitializer] +public static void Initialize() +{ + // AssemblyRegistry.Register is required for assemblies that contain Asset subclasses. + // If the assembly only contains graph types (no Asset subclasses), omit this line. + AssemblyRegistry.Register(typeof(Module).Assembly, AssemblyCommonCategories.Assets); + AssetQuantumRegistry.RegisterAssembly(typeof(Module).Assembly); +} +``` + +## Assembly Placement + +`Stride.Core.Assets.Quantum` — `sources/assets/Stride.Core.Assets.Quantum/` + +Concrete `AssetPropertyGraphDefinition` subclasses for engine assets live alongside their asset classes (e.g. `sources/engine/Stride.Assets/`). diff --git a/docs/quantum/graph-model.md b/docs/quantum/graph-model.md new file mode 100644 index 0000000000..2518a8fae9 --- /dev/null +++ b/docs/quantum/graph-model.md @@ -0,0 +1,96 @@ +# Graph Model + +## Role + +`Stride.Core.Quantum` builds a live, typed graph over any .NET object hierarchy. Every property and collection item becomes a node. All reads and writes go through the graph, which ensures change notifications fire and undo/redo integrations receive every mutation. This is the foundation layer — it knows nothing about assets, editors, or UI. + +## Node Types + +| Type | What it represents | Key members | +|---|---|---| +| `IGraphNode` | Base interface for all nodes | `Guid`, `Type`, `Descriptor`, `IsReference`, `Retrieve()`, `Retrieve(NodeIndex)` | +| `IObjectNode : IGraphNode` | An object with named members and/or collection items | `Members`, `Indices`, `IsEnumerable`, `this[string name]`, `Update(object?, NodeIndex)`, `Add()`, `Remove()` | +| `IMemberNode : IGraphNode` | A single named property — child of an `IObjectNode` | `Name`, `Parent`, `Target`, `MemberDescriptor`, `Update(object?)` | + +`IObjectNode` is the node for the object itself. `IMemberNode` is the node for each of its properties. Accessing `myObjectNode["MyProperty"]` returns the `IMemberNode` for `MyProperty`. Accessing `memberNode.Target` returns the `IObjectNode` for the referenced object when `IsReference` is true. + +## `NodeContainer` + +`NodeContainer` is the factory and owner of all nodes. Call `GetOrCreateNode(object)` to enter the graph for any object: + +```csharp +var container = new NodeContainer(); +IObjectNode rootNode = container.GetOrCreateNode(myAsset); +``` + +Nodes are keyed by object identity (via `ConditionalWeakTable`). Calling `GetOrCreateNode` on the same object twice returns the same node. Call `GetNode` (non-creating variant) when you only want to look up an existing node. + +**Reference vs. value nodes:** When a member holds a reference to another object, `IMemberNode.IsReference` is `true` and `IMemberNode.Target` returns the `IObjectNode` for that object (creating it if needed). When a member holds a value type or a primitive, `IsReference` is `false` and the value is stored inline. + +## Mutations + +Never set properties on the underlying object directly while the graph is active — change notifications will not fire. Always mutate through the node: + +```csharp +// Update a member value +IMemberNode member = rootNode["MyProperty"]; +member.Update(newValue); + +// Update a collection item +// IMemberNode.Target is non-null when IsReference is true (i.e. the member holds a reference-type collection like List) +IMemberNode listMember = rootNode["MyList"]; +IObjectNode list = listMember.Target!; // valid when listMember.IsReference == true +list.Update(newItem, new NodeIndex(0)); // replace item at index 0 + +// Add to a collection +list.Add(newItem); +list.Add(newItem, new NodeIndex(2)); // insert at index 2 + +// Remove from a collection +list.Remove(existingItem, new NodeIndex(0)); +``` + +## Observing Changes + +`GraphNodeChangeListener` subscribes to all nodes reachable from a root and surfaces four events: + +```csharp +var listener = new GraphNodeChangeListener(rootNode); + +listener.ValueChanging += (sender, e) => { /* MemberNodeChangeEventArgs: e.Member, e.OldValue, e.NewValue */ }; +listener.ValueChanged += (sender, e) => { /* MemberNodeChangeEventArgs: e.Member, e.OldValue, e.NewValue */ }; +listener.ItemChanging += (sender, e) => { /* ItemChangeEventArgs: e.Collection, e.Index, e.OldValue, e.NewValue */ }; +listener.ItemChanged += (sender, e) => { /* ItemChangeEventArgs: e.Collection, e.Index, e.OldValue, e.NewValue */ }; + +listener.Initialize(); // walk the graph after subscribing + +// Dispose to unsubscribe from all nodes +listener.Dispose(); +``` + +`GraphNodeChangeListener` accepts any `IGraphNode` as its root — not just `IObjectNode`. You can start the listener from a member node if needed. + +Call `Initialize()` after subscribing to events — it walks the graph and registers all reachable nodes. Always `Dispose()` the listener when done; failing to do so leaks node subscriptions. + +## `NodeIndex` + +`NodeIndex` is a `readonly struct`. It cannot be `null`; use `NodeIndex.Empty` as the "no index" sentinel. + +`NodeIndex` addresses items in collection nodes: + +```csharp +NodeIndex.Empty // non-collection members (the "no index" sentinel) +new NodeIndex(0) // list item at position 0 +new NodeIndex("key") // dictionary item with key "key" +``` + +```csharp +NodeIndex idx = new NodeIndex(2); +idx.IsEmpty // false +idx.IsInt // true +idx.Int // 2 +``` + +## Assembly Placement + +`Stride.Core.Quantum` — `sources/presentation/Stride.Core.Quantum/` diff --git a/docs/quantum/property-grid.md b/docs/quantum/property-grid.md new file mode 100644 index 0000000000..a4dbf87f1f --- /dev/null +++ b/docs/quantum/property-grid.md @@ -0,0 +1,202 @@ +# Property Grid Presenter Layer + +## Role + +The presenter layer adapts the Quantum node graph for UI. `INodePresenter` is the view model for a single property row in the property grid. `INodePresenterUpdater` implementations customise the presenter tree — showing, hiding, and augmenting nodes — before the property grid binds to it. + +For most new asset types, writing one `INodePresenterUpdater` subclass is all that is needed. The rest of this file explains how. + +## `INodePresenter` Key Members + +| Member | Type | Purpose | +|---|---|---| +| `Name` | `string` | Internal identifier — matches the C# property name | +| `DisplayName` | `string` | Label shown in the property grid (settable) | +| `Type` | `Type` | Property type | +| `Value` | `object` | Current value (read) | +| `UpdateValue(object)` | method | Set a new value (triggers undo/redo, notifications) | +| `IsVisible` | `bool` | Whether this row appears in the property grid (settable) | +| `IsReadOnly` | `bool` | Whether the value can be edited (settable) | +| `Children` | `IReadOnlyList` | Child presenters (nested properties) | +| `Parent` | `INodePresenter?` | Parent presenter | +| `this[string]` | `INodePresenter` | Access a child by name (throws if not found) | +| `TryGetChild(string)` | `INodePresenter?` | Access a child by name without throwing | +| `AttachedProperties` | `PropertyContainerClass` | Bag of UI metadata (min/max, category, etc.) | +| `Commands` | `List` | Commands shown as buttons or context menu entries | +| `Order` | `int?` | Sort order within the parent (settable) | +| `AddDependency(node, bool)` | method | Refresh this node when another node changes | +| `Factory` | `INodePresenterFactory` | Factory for creating virtual presenters | + +## `IAssetNodePresenter` Extensions + +`IAssetNodePresenter` extends `INodePresenter` with asset-aware members: + +| Member | Type | Purpose | +|---|---|---| +| `HasBase` | `bool` | `true` if this property has a counterpart in a base asset | +| `IsInherited` | `bool` | `true` if the value is inherited from the base (not overridden) | +| `IsOverridden` | `bool` | `true` if the value has been explicitly overridden | +| `Asset` | `AssetViewModel` | The asset this presenter belongs to | +| `ResetOverride()` | method | Restores this property to its inherited value | +| `IsObjectReference(value)` | method | Returns `true` if the given value would be an object reference | +| `Factory` | `AssetNodePresenterFactory` | Narrows factory type for asset-aware virtual node creation | + +## Presenter Pipeline + +When a property grid opens for an asset, `AssetNodePresenterFactory` runs the following sequence: + +1. Walks the asset's Quantum node graph depth-first. +2. For each node, creates an `IAssetNodePresenter`. +3. Calls `UpdateNode(presenter)` on every registered `INodePresenterUpdater` — once per node, after all of that node's children have been created. +4. After the full tree is built, calls `FinalizeTree(root)` on every registered `INodePresenterUpdater` — once, with the root presenter. + +The tree is rebuilt whenever a node's value changes (so `UpdateNode` is called again for affected nodes, and `FinalizeTree` is called again for the whole tree). + +**Updater registration:** Updaters are NOT auto-discovered. You must register your updater explicitly in the plugin class for your assembly. For engine assets in `Stride.Assets.Presentation`, register in `StrideDefaultAssetsPlugin` (`sources/editor/Stride.Assets.Presentation/StrideDefaultAssetsPlugin.cs`) inside its constructor: + +```csharp +// In StrideDefaultAssetsPlugin constructor: +RegisterNodePresenterUpdater(new %%AssetName%%AssetNodeUpdater()); +``` + +## `INodePresenterUpdater` — The Main Extension Point + +Subclass `AssetNodePresenterUpdaterBase` and override `UpdateNode` and/or `FinalizeTree`. Note that the public `UpdateNode(INodePresenter)` and `FinalizeTree(INodePresenter)` methods (which the framework calls) are `sealed` in `AssetNodePresenterUpdaterBase`. Only the `protected` overloads that take `IAssetNodePresenter` are open for override — these are what you implement: + +```csharp +// sources/editor/Stride.Assets.Presentation/NodePresenters/Updaters/%%AssetName%%AssetNodeUpdater.cs +using Stride.Core.Assets.Editor.Quantum.NodePresenters; +using Stride.Core.Assets.Editor.Quantum.NodePresenters.Keys; +using Stride.Assets.%%AssetName%%; + +namespace Stride.Assets.Presentation.NodePresenters.Updaters; + +internal sealed class %%AssetName%%AssetNodeUpdater : AssetNodePresenterUpdaterBase +{ + // Called once per node after all of that node's children have been created. + // Safe to: set IsVisible, IsReadOnly, Order, DisplayName; set AttachedProperties; + // create virtual node presenters; add commands. + // Not safe to: navigate to sibling nodes or rely on parent's siblings existing. + protected override void UpdateNode(IAssetNodePresenter node) + { + // Guard: only operate on nodes that belong to %%AssetName%%Asset + if (node.Asset?.Asset is not %%AssetName%%Asset asset) + return; + + // Example: hide a property based on another property's value + if (node.Name == nameof(%%AssetName%%Asset.SomeProperty)) + { + node.IsVisible = asset.SomeFlag; + } + + // Example: clamp a numeric property + if (node.Name == nameof(%%AssetName%%Asset.Iterations)) + { + node.AttachedProperties.Set(NumericData.MinimumKey, 1); + node.AttachedProperties.Set(NumericData.MaximumKey, 64); + node.AttachedProperties.Set(NumericData.DecimalPlacesKey, 0); + } + } + + // Called once after the full presenter tree has been built. + // Safe to: navigate the full tree; add cross-node dependencies. + // Not safe to: add or remove nodes. + protected override void FinalizeTree(IAssetNodePresenter root) + { + if (root.Asset?.Asset is not %%AssetName%%Asset) + return; + + // Example: refresh SomeProperty whenever SomeFlag changes + root[nameof(%%AssetName%%Asset.SomeProperty)] + .AddDependency(root[nameof(%%AssetName%%Asset.SomeFlag)], false); + } +} +``` + +**`UpdateNode` is called for every node in the tree**, including nested nodes. Always guard by checking `node.Asset?.Asset is YourAssetType` before doing anything, and then check `node.Name` to target the specific property you want to modify. + +## `AttachedProperties` + +`AttachedProperties` is a typed property bag for UI metadata. Set values with `node.AttachedProperties.Set(key, value)`. + +Common keys (all in `Stride.Core.Assets.Editor.Quantum.NodePresenters.Keys`): + +| Key | Type | Effect | +|---|---|---| +| `NumericData.MinimumKey` | `object` | Minimum value for numeric inputs | +| `NumericData.MaximumKey` | `object` | Maximum value for numeric inputs | +| `NumericData.DecimalPlacesKey` | `int?` | Number of decimal places shown (`0` for integers) | +| `NumericData.LargeStepKey` | `double?` | Step size for large increments (scroll/drag) | +| `NumericData.SmallStepKey` | `double?` | Step size for small increments | +| `DisplayData.AttributeDisplayNameKey` | `string` | Overrides the display name shown in the property grid | +| `DisplayData.AutoExpandRuleKey` | `ExpandRule` | Controls automatic expand/collapse of object nodes | + +```csharp +// Clamp a float between 0 and 1 +node.AttachedProperties.Set(NumericData.MinimumKey, 0f); +node.AttachedProperties.Set(NumericData.MaximumKey, 1f); +node.AttachedProperties.Set(NumericData.DecimalPlacesKey, 3); + +// Override display name +node.AttachedProperties.Set(DisplayData.AttributeDisplayNameKey, "Radius (units)"); +``` + +## Virtual Node Presenters + +A virtual node presenter is a presenter row **not backed by a real property** on the asset. Use them for computed or derived values that should appear in the property grid. + +```csharp +// Signature (all parameters required): +INodePresenter virtualNode = node.Factory.CreateVirtualNodePresenter( + parent: node.Parent, // where to attach the virtual node + name: "AbsoluteWidth", // unique name within the parent + type: typeof(int), // value type + order: node.Order, // sort order (match adjacent node to appear next to it) + getter: () => node.Value, // reads the display value + setter: node.UpdateValue, // writes back when user edits + hasBase: () => node.HasBase, // for override indicator + isInerited: () => node.IsInherited, // note: misspelled in the API ("isInerited", not "isInherited") + isOverridden:() => node.IsOverridden); +``` + +Virtual nodes are **recreated every time the presenter tree is rebuilt**. Check whether the virtual node already exists before creating it to avoid duplicates: + +```csharp +var existing = node.Parent.TryGetChild("AbsoluteWidth"); +var virtualNode = existing + ?? node.Factory.CreateVirtualNodePresenter(node.Parent, "AbsoluteWidth", typeof(int), node.Order, + () => node.Value, node.UpdateValue, + () => node.HasBase, () => node.IsInherited, () => node.IsOverridden); +``` + +## `AddDependency` + +`AddDependency` makes node A refresh whenever node B changes. Use this in `FinalizeTree` to keep computed/conditional visibility up to date: + +```csharp +// node A refreshes when node B's value changes +nodeA.AddDependency(nodeB, refreshOnNestedNodeChanges: false); + +// node A also refreshes when any child of node B changes +nodeA.AddDependency(nodeB, refreshOnNestedNodeChanges: true); +``` + +Navigating category nodes: if your asset uses `[Display(category: "Size")]`, the category node is named using `CategoryData.ComputeCategoryNodeName("Size")`. Use this helper to build the node name: + +```csharp +// From TextureAssetNodeUpdater: +var sizeCategory = CategoryData.ComputeCategoryNodeName("Size"); +root[sizeCategory][nameof(TextureAsset.Width)] + .AddDependency(root[sizeCategory][nameof(TextureAsset.IsSizeInPercentage)], false); +root[sizeCategory][nameof(TextureAsset.Height)] + .AddDependency(root[sizeCategory][nameof(TextureAsset.IsSizeInPercentage)], false); +``` + +## Assembly Placement + +| Type | Assembly | Location | +|---|---|---| +| `INodePresenter`, `INodePresenterUpdater` interfaces | `Stride.Core.Presentation.Quantum` | `sources/presentation/Stride.Core.Presentation.Quantum/Presenters/` | +| `IAssetNodePresenter`, `AssetNodePresenterUpdaterBase` | `Stride.Core.Assets.Editor` | `sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/` | +| Attached property key classes (`NumericData`, `DisplayData`, `CategoryData`) | `Stride.Core.Assets.Editor` | `sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Keys/` | +| Your `%%AssetName%%AssetNodeUpdater` | `Stride.Assets.Presentation` | `sources/editor/Stride.Assets.Presentation/NodePresenters/Updaters/` |