Advanced Inheritance

In the Tree and Layers chapter, we learned how inheritance works in Reactor - child layers inherit variables and other tree elements from their parent layers. This is the foundation of how configurations are organized and reused.

However, when you start composing configurations from multiple layer files, you need more control over how variables flow between these separate configuration pieces. This is where Advanced Inheritance features come in. These are optional features that give you fine-grained control over variable scope and definition when building modular, reusable configurations.

Tip

These features are primarily used in SKAARHOJ Default Configurations, where we often need to compose configs from multiple files. Most simple configurations don't need them! Make sure you have a solid understanding of basic inheritance before diving into these advanced topics.

In this chapter, we'll explore three powerful features that modify how variables behave in the layer tree:

  • ExpandScope - Makes variables "bubble up" to parent layers
  • Capture - Intercepts bubbling variables from child layers
  • AlwaysDefine - Forces a variable definition to override previous definitions

Understanding Normal Inheritance

Before we look at these advanced features, let's quickly review normal inheritance behavior:

graph BT
    A[Main Controller Layer<br/>DeviceIndex = 1] --> B[Panel 2 Config Layer<br/>can use DeviceIndex]
    A --> C[Panel 1 Config Layer<br/>can use DeviceIndex]
    B --> D[Panel 2 Menu Layer<br/>can use DeviceIndex]

    style A fill:#5B9BD5,stroke:#2E5C8A,color:#fff
    style B fill:#F39C12,stroke:#C87F0A,color:#fff
    style C fill:#F39C12,stroke:#C87F0A,color:#fff
    style D fill:#B0BEC5,stroke:#78909C,color:#fff
  • Variables defined in a parent layer are inherited by child layers
  • Children can see and use their parents' variables
  • The flow is from root to leaves in the tree
  • If a child defines a variable with the same name, it doesn't redefine it unless explicitly told to do so (with AlwaysDefine)

The advanced inheritance features modify this behavior in specific ways.

ExpandScope - Sending Variables upward through the tree

ExpandScope reverses the normal inheritance direction. When you set ExpandScope: true on a variable, that variable travels outward through the layer tree instead of being confined to its layer and child layers.

How ExpandScope Works

When a variable has ExpandScope: true:

  1. The variable tries to travel toward the root of the tree
  2. It travels through parent layers, making itself available to them
  3. It stops in one of two situations:
    • It reaches the root layer (the top of the tree)
    • It encounters a variable with the same name that has Capture: true
graph BT
    A["Root Layer<br/>(PageVariable available here!)"] --> B[Main Config Layer]
    B --> C[Included Layer]
    C --> D[Sub-Config Layer<br/>Defines: PageVariable<br/>ExpandScope: true<br/> Variables traverses OUTWARD]

    style A fill:#66BB6A,stroke:#43A047,color:#fff
    style B fill:#5B9BD5,stroke:#2E5C8A,color:#fff
    style C fill:#B0BEC5,stroke:#78909C,color:#fff
    style D fill:#F39C12,stroke:#C87F0A,color:#fff

When to Use It

ExpandScope is essential when you're including a configuration file that needs to expose internal variables to the parent configuration. Common scenarios include:

  1. Device configuration files that need to expose their internal page variables to the main controller
  2. Sub-configurations that manage their own state but need that state accessible to the parent
  3. Reusable configuration snippets that define variables which should be controllable from the including layer

Example: Device Configuration with Internal Paging

Imagine you have a reusable PTZ camera configuration saved as a separate layer file. This config has internal paging (preset pages 1-5), and you want to control that paging from your main controller.

PTZ Camera Config (included file):

{
  "Name": "PTZ Camera Config",
  "Variables": {
    "PresetPage": {
      "Name": "Preset Page",
      "Default": ["0"],
      "MinMaxCenterValue": [0, 4], // this is interesting
      "ExpandScope": true
    }
  },
  "HWCBehaviors": {
    "ENC1": {
      "Description": "preset behaviors using PresetPage"
    }
  }
}

Main Controller (includes PTZ config):

{
  "Name": "Main Controller",
  "ImportLayerFiles": [
    "PTZCameraConfig"
  ]
}

With ExpandScope: true, the PresetPage variable bubbles up from the PTZ config to your main controller layer, where you can then map it to a physical button or encoder.

Why is this needed ?

If you don't use ExpandScope, the PresetPage variable stays confined to the PTZ config layer and its children. Your main controller can't access it, making it impossible to create a paging button outside the PTZ config itself.

This would force you to define the page variable in the main controller layer and pass it down (which defeats the purpose of reusable configs) It also means the variable's options or range must be defined in the main controller rather than where they logically belong, making it impossible to have different supconfigs with different menu pages for example.

ExpandScope solves this by letting the included config "publish" variables upward, keeping the variable's options or range defined on the inner layer where they are most relevant.

Capture - Receiving "Expanded" Variables

Capture works hand-in-hand with ExpandScope. When you set Capture: true on a variable definition, it acts as a "receiver" or "blocker" for any ExpandScope variable with the same name coming from child layers.

How Capture Works

When a variable has Capture: true:

  1. It "listens" for ExpandScope variables with the same name from deeper in the tree
  2. When it finds one, it intercepts it and stops the upward propagation
  3. The variable is now "owned" by this layer, not the child layer where it originated
graph BT
    A[Root Layer<br/>DevicePage doesn't reach here] --> B[Main Controller Layer<br/>DevicePage: Capture = true<br/>✋ Intercepts all three!]
    B --> D[Camera Config A<br/>DevicePage: ExpandScope<br/>↓ Tries to traverse outward]
    B --> E[Camera Config B<br/>DevicePage: ExpandScope<br/>↓ Tries to traverse outward]
    B --> F[Camera Config C<br/>DevicePage: ExpandScope<br/>↓ Tries to traverse outward]

    style A fill:#B0BEC5,stroke:#78909C,color:#fff
    style B fill:#66BB6A,stroke:#43A047,color:#fff
    style D fill:#F39C12,stroke:#C87F0A,color:#fff
    style E fill:#F39C12,stroke:#C87F0A,color:#fff
    style F fill:#F39C12,stroke:#C87F0A,color:#fff

When to Use Capture

Capture is essential when you're including multiple configuration files that all define the same ExpandScope variable, and you want to coordinate them from a single location. Common scenarios include:

  1. Device selector configurations that include multiple device types, each with their own paging
  2. Multi-device setups where several devices share a common control variable
  3. Modular configurations where you want to centralize control of variables from multiple sub-configs

In our example the variable defined first would win and provide the definition. (So in this case Camera Config A)

Example: Multi-Device Controller

Imagine you're building a controller that can control multiple different devices (ATEM switcher, Canon camera, audio mixer). Each device configuration is a separate file with its own DevicePage variable that uses ExpandScope. You want a single paging control that works across all devices.

Main Controller:

{
  "Name": "Multi-Device Controller",
  "Variables": {
    "DevicePage": {
      "Name": "Device Page",
      "Default": ["0"],
      "MinMaxCenterValue": [0, 5],
      "Capture": true
    },
    "ActiveDevice": {
      "Name": "Selected Device",
      "Default": ["0"],
      "MinMaxCenterValue": [0, 2]
    }
  },
  "ImportLayerFiles": [
    "ATEMConfig",
    "CanonPTZConfig",
    "AudioMixerConfig"
  ]
}

Each Device Config (ATEM, Canon, Audio):

{
  "Variables": {
    "DevicePage": {
      "ExpandScope": true
    }
  }
}

With Capture: true in the main controller, all three device configs' DevicePage variables are intercepted at the main layer. Now you have a single place to map paging controls, and all three device configs respond to the same page variable.

AlwaysDefine - Forcing Variable Redefinition

AlwaysDefine is a different kind of control - it's about precedence rather than scope direction.

How It Works

Normally, when Reactor encounters a variable definition in a child layer that has the same name as a variable already defined in a parent layer, it will not redefine the variable. The parent's definition takes precedence.

graph BT
    A[Main Config<br/>DeviceID = 1] --> B[Device Config<br/>tries to define DeviceID = 2<br/>❌ Ignored - parent wins]
    A --> C[Device Config<br/>tries to define DeviceID = 3<br/>❌ Ignored - parent wins]

    style A fill:#5B9BD5,stroke:#2E5C8A,color:#fff
    style B fill:#EF5350,stroke:#C62828,color:#fff
    style C fill:#EF5350,stroke:#C62828,color:#fff

With AlwaysDefine: true, this behavior is reversed:

  • The variable definition always takes effect at that layer
  • It overrides any previous definition from parent layers
  • From this layer downward, the new definition is used
graph BT
    A[Main Config<br/>DeviceID = 1] --> B[Device Config<br/>DeviceID = 2<br/>AlwaysDefine: true<br/>✅ Takes effect!]
    A --> C[Device Config<br/>DeviceID = 3<br/>AlwaysDefine: true<br/>✅ Takes effect!]

    style A fill:#5B9BD5,stroke:#2E5C8A,color:#fff
    style B fill:#66BB6A,stroke:#43A047,color:#fff
    style C fill:#66BB6A,stroke:#43A047,color:#fff

Tip

Important: ExpandScope takes precedence over AlwaysDefine. If both are set, ExpandScope behavior wins.

When to Use AlwaysDefine

AlwaysDefine is essential when you're including the same configuration file multiple times, and each instance needs its own unique variable values. Common scenarios include:

  • Repeated configuration patterns where each repetition needs unique identifiers
  • Generated or duplicated layers that need independent state

Debugging Tips

If inheritance isn't working as expected:

  1. Check the Tree View - In the configuration view, expand your layer tree and look at the variable definitions. Variables with special properties will show indicators
  2. Use the Variable Inspector - Click on a layer and open "Show More" to see all variables and their flags
  3. Verify Layer Paths - ExpandScope and Capture work based on the layer tree structure. Make sure your ImportLayerFiles statements create the hierarchy you expect
  4. Check Variable Names - Capture only works if the variable keys match exactly (case-sensitive), Names might differ, Keys matter

To deepen your understanding of these advanced features, explore these related chapters: