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/` |