davinci-resolve-mcp 2.33.1 → 2.33.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,568 @@
1
+ # `.setting` File Format Reference
2
+
3
+ ## 1. Lua-Table Syntax
4
+
5
+ `.setting` files are parsed by Fusion's table reader — a slightly relaxed Lua-table dialect. Key points:
6
+
7
+ - **Tables:** `{ key = value, other = value }` — trailing commas allowed.
8
+ - **Ordered tables:** `ordered() { key = value, ... }` — same as a table but Fusion preserves insertion order. Use this wherever display order matters (`Inputs`, `Tools`, `UserControls`).
9
+ - **Strings:** double-quoted, backslash-escaped. Keys with spaces or dots must be quoted and bracketed: `["Gamut.SLogVersion"] = Input { ... }`.
10
+ - **Numbers:** integers, floats, negative. Scientific notation works.
11
+ - **Nested tables:** freely. Values can be tables, lists, or other tables.
12
+ - **Comments:** not officially supported — Fusion may strip them on round-trip.
13
+
14
+ ## 2. Top-Level Shape
15
+
16
+ Every `.setting` file is a single top-level table:
17
+
18
+ ```lua
19
+ {
20
+ Tools = ordered() {
21
+ <name> = <NodeType> { ... }, -- one or more nodes at the top level
22
+ ...
23
+ },
24
+ ActiveTool = "<name>" -- optional; names the "primary" node
25
+ }
26
+ ```
27
+
28
+ Three legal top-level shapes observed in the stock library:
29
+
30
+ | Shape | Used for | Example |
31
+ |-------|---------|---------|
32
+ | **One GroupOperator** wrapping internal tools | Most Edit effects/transitions/titles/generators, most Fusion macros that want a clean inspector | `Binoculars`, `Block Glitch`, `Background Reveal` |
33
+ | **One raw node** (not wrapped) | Single-node effects that expose the node's native UI | `Lens Flare V01` (HotSpot), `Blowing Leaves` (pEmitter) |
34
+ | **Multiple raw nodes** | Fusion macros that drop a subgraph into the user's comp (user sees all nodes) | `Circle Values` (Merge + Background + BrightnessContrast) |
35
+
36
+ If you're authoring for the Edit page (Effects/Transitions/Titles/Generators), **always use the wrapped GroupOperator form** — the Edit page doesn't know how to display raw node graphs in the inspector.
37
+
38
+ ## 3. The GroupOperator Block
39
+
40
+ ```lua
41
+ MyEffect = GroupOperator {
42
+ CtrlWZoom = false, -- UI detail, harmless; copy from stock files
43
+ NameSet = true, -- optional; marks the name as explicitly set
44
+ Inputs = ordered() { ... exposed controls ... },
45
+ Outputs = { ... MainOutput1, optional extra outputs ... },
46
+ ViewInfo = GroupInfo { Pos = { 0, 0 } }, -- position in the Fusion graph editor
47
+ Tools = ordered() { ... internal nodes ... },
48
+ },
49
+ ```
50
+
51
+ Fields:
52
+
53
+ - **`Inputs`** — InstanceInputs (controls). Order = inspector order.
54
+ - **`Outputs`** — InstanceOutputs. `MainOutput1` is mandatory for anything that renders pixels. You can expose multiple outputs; the first is what downstream Resolve nodes read.
55
+ - **`ViewInfo`** — `GroupInfo { Pos = {x, y}, Flags = {...}, Size = {...}, Direction, PipeStyle, Scale, Offset }`. Only `Pos` is necessary for templates; the rest are cosmetic. Flags like `AllowPan`, `ConnectedSnap`, `AutoSnap`, `RemoveRouters` are all optional booleans.
56
+
57
+ Three `ViewInfo` subtypes exist across `.setting` files:
58
+
59
+ | Type | Used on | Required fields |
60
+ |------|---------|----------------|
61
+ | `GroupInfo` | `GroupOperator` node | `Pos` |
62
+ | `OperatorInfo` | All regular internal nodes | `Pos` |
63
+ | `PipeRouterInfo` | `PipeRouter` nodes only | `Pos` |
64
+
65
+ **`PipeRouter`** is a no-op pass-through node. It carries a single `Input` (a source connection) and forwards the same data downstream, producing no visual change. Its purpose is cosmetic: keeping the Fusion graph editor tidy when a single output must fan out to several nodes. You identify it by its distinct `ViewInfo = PipeRouterInfo { Pos = {x, y} }` — using `OperatorInfo` on a `PipeRouter` is legal but non-standard.
66
+
67
+ From `Digital Glitch.setting`:
68
+
69
+ ```lua
70
+ PipeRouter1_1 = PipeRouter {
71
+ CtrlWShown = false,
72
+ Inputs = {
73
+ Input = Input {
74
+ SourceOp = "ChannelBooleans3_1_1",
75
+ Source = "Output",
76
+ },
77
+ },
78
+ ViewInfo = PipeRouterInfo { Pos = { 1003.44, 58.5639 } },
79
+ }
80
+ ```
81
+
82
+ `PipeRouter` nodes are invisible to the GroupOperator's inspector and don't need to be exposed as `InstanceInput`s.
83
+ - **`Tools`** — the internal graph.
84
+
85
+ ## 4. Node Definitions (Internal `Tools`)
86
+
87
+ Every internal node looks like:
88
+
89
+ ```lua
90
+ NodeName1 = NodeType {
91
+ CtrlWZoom = false, -- optional UI state
92
+ CtrlWShown = false, -- optional UI state
93
+ NameSet = true, -- optional
94
+ Inputs = {
95
+ Param1 = Input { Value = 0.5, },
96
+ Param2 = Input {
97
+ SourceOp = "OtherNode", -- connection to another node's output
98
+ Source = "Output",
99
+ },
100
+ ["Namespaced.Param"] = Input { Value = FuID { "EnumValue" }, },
101
+ },
102
+ ViewInfo = OperatorInfo { Pos = { 304, -52 } },
103
+ UserControls = ordered() { ... optional custom controls ... },
104
+ },
105
+ ```
106
+
107
+ Key points:
108
+
109
+ - **Connections** use `SourceOp` + `Source` inside an `Input {}`. The referenced node must be in the same `Tools` list. Output names depend on node type: image nodes output `"Output"`, masks output `"Mask"`, value-node splines output `"Value"`, TextPlus outputs `"Output"` (pixels) and `"Result"` (stretched keyframes when piped through KeyStretcher).
110
+ - **Scalar values** use `Input { Value = N }`.
111
+ - **Typed enums** use `Input { Value = FuID { "EnumValue" } }` — see "FuID Enums" below.
112
+ - **Gradients** use `Input { Value = Gradient { Colors = { [0] = {r,g,b,a}, [1] = {r,g,b,a}, ... } } }`.
113
+ - **Keyed animation** uses `SourceOp = "SomeBezier"` pointing at a `BezierSpline` defined as a sibling node.
114
+ - **StyledText inputs** (TextPlus `StyledText`, `ManualFontKerningPlacement`) wrap the string in `StyledText { Value = "...", Array = { ... } }`.
115
+
116
+ ### 4a. `Input.Expression` — Live Lua Expressions
117
+
118
+ Any `Input {}` block can carry an `Expression = "..."` field instead of (or alongside) `Value`. Fusion evaluates the string as a Lua expression at render time; the result becomes the parameter's value for that frame. Expressions can reference:
119
+
120
+ - Other inputs on the **same node** by bare name: `"Switch"`, `"Horizontal"`
121
+ - Inputs on **other nodes** with dot notation: `"pEmitter1_3.CubeRgn.Depth"`
122
+ - Constructor functions: `Point(x, y)` for 2-component inputs
123
+ - Built-in variables: `time` (current frame number)
124
+ - Arithmetic: standard Lua operators
125
+
126
+ From `Highlight Stretch.setting` — binding a Point input to two scalar UserControls on the same node:
127
+
128
+ ```lua
129
+ Center = Input { Expression = "Point(Horizontal, Vertical)", },
130
+ ```
131
+
132
+ From `Starfield.setting` — a more complex cross-node expression used as an `Offset` on a `LUTLookup`:
133
+
134
+ ```lua
135
+ Offset = Input {
136
+ Value = 50,
137
+ Expression = "pEmitter1_3.CubeRgn.Depth/2",
138
+ },
139
+ ```
140
+
141
+ And a compound arithmetic expression reading from two separate nodes:
142
+
143
+ ```lua
144
+ FirstOperand = Input {
145
+ Value = -100,
146
+ Expression = "-pEmitter1_3.CubeRgn.Depth",
147
+ },
148
+ SecondOperand = Input {
149
+ Value = 10,
150
+ Expression = "11-CustomTool1_1.Speed",
151
+ },
152
+ ```
153
+
154
+ When `Expression` is present alongside `Value`, `Value` acts as a fallback (used when the expression cannot be evaluated or when Fusion first loads the file). When `Expression` is the only field, the parameter is purely reactive.
155
+
156
+ ## 5. Exposing Controls (`InstanceInput`)
157
+
158
+ Inside `GroupOperator.Inputs`, each entry is an `InstanceInput`:
159
+
160
+ ```lua
161
+ InputN = InstanceInput {
162
+ SourceOp = "InternalNodeName", -- which internal node
163
+ Source = "InternalParamName", -- which parameter
164
+ Name = "Label Shown To User", -- optional; defaults to Source
165
+ Default = 0.5, -- optional; initial value
166
+ MinScale = 0, -- soft slider minimum (value can be below)
167
+ MaxScale = 1, -- soft slider maximum
168
+ MinAllowed = -10, -- hard minimum (optional)
169
+ MaxAllowed = 10, -- hard maximum (optional)
170
+ Page = "Controls", -- inspector tab name
171
+ ControlGroup = 4, -- group id for paired/nested controls
172
+ Width = 0.5, -- width 0-1 for side-by-side layouts
173
+ DefaultX = 0.5, -- for point/size inputs (2-component)
174
+ DefaultY = 0.5,
175
+ IconString = "...", -- rarely used; icon on the control row
176
+ },
177
+ ```
178
+
179
+ **Reserved names:**
180
+
181
+ - **`MainInput1`** — in effects, points at the internal input that should receive the source clip. In transitions, points at Background. In titles/generators, omitted.
182
+ - **`MainInput2`** — transitions: Foreground. Otherwise free to use.
183
+ - **`MainInput3` / `MainInput4` / ...** — additional "main" inputs; Fusion treats them as additional pipeline connections. Rarely used outside of 3D / material tools.
184
+
185
+ Non-reserved keys (`Input1`, `Input2`, `Intensity`, `Blur`, anything) are displayed as normal controls. The key itself is the internal identifier; `Name` is the label.
186
+
187
+ ### 5a. `EffectMask` — Reserved Mask Input
188
+
189
+ `EffectMask` is a reserved input name on most image-processing nodes (Blur, BrightnessContrast, Background, ChannelBoolean, Merge, etc.). When wired to a mask node's `"Mask"` output, it scopes the tool's effect to the masked area. This is the mask equivalent of the `MainInput1`/`MainInput2` pipeline pattern.
190
+
191
+ ```lua
192
+ Background1 = Background {
193
+ Inputs = {
194
+ EffectMask = Input {
195
+ SourceOp = "Rectangle1",
196
+ Source = "Mask", -- mask nodes output "Mask", not "Output"
197
+ },
198
+ TopLeftRed = Input { Value = 1, },
199
+ ...
200
+ },
201
+ },
202
+ ```
203
+
204
+ From `Colored Border.setting`:
205
+
206
+ ```lua
207
+ ChannelBooleans1 = ChannelBoolean {
208
+ Inputs = {
209
+ EffectMask = Input {
210
+ SourceOp = "Instance_Rectangle1",
211
+ Source = "Mask",
212
+ },
213
+ ApplyMaskInverted = Input { Value = 1, },
214
+ ...
215
+ },
216
+ },
217
+ ```
218
+
219
+ The companion input `ApplyMaskInverted = Input { Value = 1 }` inverts the mask region — the tool applies _outside_ the mask shape rather than inside. This pattern (border effect visible only outside the inner rectangle) is a common idiom in stock effects.
220
+
221
+ `ControlGroup` collapses consecutive inputs sharing the same group id into one visual widget — that's how R/G/B/A become one color picker:
222
+
223
+ ```lua
224
+ Input4 = InstanceInput { SourceOp = "BG", Source = "TopLeftRed", Name = "Color", ControlGroup = 4, Default = 1 },
225
+ Input5 = InstanceInput { SourceOp = "BG", Source = "TopLeftGreen", ControlGroup = 4, Default = 1 },
226
+ Input6 = InstanceInput { SourceOp = "BG", Source = "TopLeftBlue", ControlGroup = 4, Default = 1 },
227
+ Input7 = InstanceInput { SourceOp = "BG", Source = "TopLeftAlpha", ControlGroup = 4, Default = 1 },
228
+ ```
229
+
230
+ Result: a single "Color" swatch in the inspector.
231
+
232
+ ## 6. Outputs (`InstanceOutput`)
233
+
234
+ ```lua
235
+ Outputs = {
236
+ MainOutput1 = InstanceOutput {
237
+ SourceOp = "FinalNode", -- which internal node's output
238
+ Source = "Output", -- which output (usually "Output")
239
+ },
240
+ Output2 = InstanceOutput { -- optional additional outputs
241
+ SourceOp = "SomeOtherNode",
242
+ Source = "Value",
243
+ },
244
+ },
245
+ ```
246
+
247
+ `MainOutput1` is mandatory if the effect produces pixels. For Edit effects, transitions, and titles, Resolve reads `MainOutput1` and pipes it to the next node in the timeline.
248
+
249
+ ## 7. FuID Enums
250
+
251
+ Many node parameters are enumerated. Fusion stores them as `FuID { "EnumValue" }`:
252
+
253
+ ```lua
254
+ Filter = Input { Value = FuID { "Fast Gaussian" }, }, -- Blur.Filter
255
+ Operation = Input { Value = FuID { "DFTLumaRamp" }, }, -- Dissolve.Operation
256
+ Channel = Input { Value = FuID { "Luminance" }, }, -- BitmapMask.Channel
257
+ Shape = Input { Value = FuID { "SurfaceCylinderInputs" }, }, -- Shape3D.Shape
258
+ Region = Input { Value = FuID { "CubeRgn" }, }, -- pEmitter.Region
259
+ ```
260
+
261
+ You can't guess these names — inspect the node in Fusion (right-click → Paste Settings / Copy Settings) or read the reference `.setting` files in the stock library.
262
+
263
+ ## 8. `BezierSpline` (Keyframe Animation)
264
+
265
+ Animation curves are sibling nodes, referenced by `SourceOp`:
266
+
267
+ ```lua
268
+ TextAlpha = BezierSpline {
269
+ SplineColor = { Red = 180, Green = 180, Blue = 180 },
270
+ NameSet = true,
271
+ KeyFrames = {
272
+ [13] = { 0, RH = { 20, 0.333 } },
273
+ [34] = { 1, LH = { 27, 0.666 }, RH = { 56, 1 }, Flags = { Linear = true } },
274
+ [101] = { 1, LH = { 78, 1 }, RH = { 105, 0.666 }, Flags = { Linear = true } },
275
+ [115] = { 0, LH = { 110, 0.333 }, Flags = { Linear = true } },
276
+ }
277
+ },
278
+ ```
279
+
280
+ Keyframe format: `[frameNumber] = { value, LH = {tx, ty}, RH = {tx, ty}, Flags = { Linear = true } }`. `LH` / `RH` are left/right handles (time, value). `Flags.Linear` disables Bezier interpolation on that side.
281
+
282
+ ### 8a. `PublishNumber` (Static Shared Values)
283
+
284
+ `PublishNumber` is a sibling node type that hosts a single scalar and exposes it as a named `"Value"` output. Other nodes consume it via the usual `SourceOp` / `Source = "Value"` pattern. Use it when you want one value shared across multiple downstream nodes without the overhead of a keyframe curve — it's a lighter alternative to `BezierSpline` when the value is static or set by an expression.
285
+
286
+ From `Drone Overlay.setting` — several `PublishNumber` nodes each hold one dimension value, and multiple mask nodes share them:
287
+
288
+ ```lua
289
+ Publish1_2 = PublishNumber {
290
+ CtrlWZoom = false,
291
+ Inputs = {
292
+ Value = Input { Value = 0.094, },
293
+ },
294
+ },
295
+ Publish3_2 = PublishNumber {
296
+ CtrlWZoom = false,
297
+ Inputs = {
298
+ Value = Input { Value = 0.14, },
299
+ },
300
+ },
301
+ ```
302
+
303
+ Consuming node:
304
+
305
+ ```lua
306
+ Width = Input {
307
+ SourceOp = "Publish1_2",
308
+ Source = "Value",
309
+ },
310
+ Height = Input {
311
+ SourceOp = "Publish1_2",
312
+ Source = "Value",
313
+ },
314
+ ```
315
+
316
+ This lets you change a shared dimension once (by editing the `PublishNumber`) rather than updating every consumer. `PublishNumber` does not animate; use `BezierSpline` when you need keyframes.
317
+
318
+ ## 9. `LUTLookup` + `LUTBezier` (Time Ramps)
319
+
320
+ Transitions and titles use `LUTLookup` to turn clip-time into a 0→1 ramp that drives whatever parameter should animate over the clip length. Pattern:
321
+
322
+ ```lua
323
+ AnimCurves1 = LUTLookup {
324
+ Inputs = {
325
+ Source = Input { Value = FuID { "Duration" }, }, -- "Duration" = full clip length
326
+ Curve = Input { Value = FuID { "Easing" }, },
327
+ EaseIn = Input { Value = FuID { "Cubic" }, },
328
+ EaseOut = Input { Value = FuID { "Cubic" }, },
329
+ Lookup = Input {
330
+ SourceOp = "AnimCurves1Lookup",
331
+ Source = "Value",
332
+ },
333
+ Scale = Input { Value = 10 }, -- output multiplied by Scale
334
+ Offset = Input { Value = 0 },
335
+ },
336
+ },
337
+ AnimCurves1Lookup = LUTBezier {
338
+ KeyColorSplines = {
339
+ [0] = {
340
+ [0] = { 0, RH = { 0.333, 0.333 }, Flags = { Linear = true } },
341
+ [1] = { 1, LH = { 0.666, 0.666 }, Flags = { Linear = true } }
342
+ }
343
+ },
344
+ SplineColor = { Red = 255, Green = 255, Blue = 255 },
345
+ },
346
+ ```
347
+
348
+ Then any internal node can pull from it:
349
+
350
+ ```lua
351
+ Dissolve1 = Dissolve {
352
+ Inputs = {
353
+ Mix = Input { SourceOp = "AnimCurves1", Source = "Value" },
354
+ ...
355
+ },
356
+ },
357
+ ```
358
+
359
+ That's the entire transition-progress mechanism. Don't expose it to the user. Don't try to find a "Progress" input on the Group — there isn't one.
360
+
361
+ ### 9a. `TimeCurve` Input on `LUTBezier`
362
+
363
+ A `LUTBezier` node accepts an optional `TimeCurve` input that overrides the implicit clip-time source used to index the spline. When wired, the `LUTBezier` samples its curve at the value supplied by the `TimeCurve` connection rather than at the default frame-normalized time. Transitions and stingers use this to anchor animation to clip in/out points relative to a controlling `LUTLookup` or `BezierSpline`.
364
+
365
+ From `Lens Flare V01.setting` — the `LUTBezier` reads its time from a `BezierSpline` that provides a custom timeline offset:
366
+
367
+ ```lua
368
+ HotSpot1Red = LUTBezier {
369
+ Inputs = {
370
+ TimeCurve = Input {
371
+ SourceOp = "HotSpot1ColorChange",
372
+ Source = "Value",
373
+ },
374
+ },
375
+ SplineColor = { 1, 0, 0, },
376
+ KeyColorSplines = {
377
+ [0] = {
378
+ [0] = { 0, RH = { 0.018, 0 } },
379
+ [0.057] = { 0.244, ... },
380
+ [1] = { 1, LH = { 0.652, 1 } },
381
+ },
382
+ },
383
+ },
384
+ ```
385
+
386
+ Without `TimeCurve`, the `LUTBezier` evaluates at normalized clip time (0 = start, 1 = end). With it wired, the curve is driven entirely by the incoming value — useful for effects that must sync to a non-linear timeline or to a master curve shared across several `LUTBezier` nodes.
387
+
388
+ ## 9b. Common Mask Node Types
389
+
390
+ Three mask node types appear throughout the stock library and feed into the `EffectMask` input described above. Their output port is always named `"Mask"` — never `"Output"`.
391
+
392
+ | Node type | Description | Common unique inputs |
393
+ |-----------|-------------|---------------------|
394
+ | `RectangleMask` | Rectangular / border mask | `BorderWidth`, `CornerRadius` |
395
+ | `EllipseMask` | Elliptical mask | (no unique inputs beyond base set) |
396
+ | `PolylineMask` | Freehand polygon / Bezier shape | `Polygon` (point list) |
397
+
398
+ **Common inputs shared by all three:**
399
+
400
+ - `MaskWidth` / `MaskHeight` — pixel dimensions of the mask canvas (typically match the project frame size, e.g. `1920` / `1080`). **Not the same as the visual size of the shape** — see below.
401
+ - `PixelAspect = Input { Value = { 1, 1 } }` — pixel aspect ratio.
402
+ - `ClippingMode = Input { Value = FuID { "None" } }` — standard; leaves mask edges sharp outside the canvas boundary.
403
+ - `Filter = Input { Value = FuID { "Fast Gaussian" } }` — anti-aliasing filter for the mask edge.
404
+ - `Center` — center position of the shape (0–1 normalized, `{0.5, 0.5}` = image center).
405
+ - `Width` / `Height` — **size of the shape** as a fraction of image width. This is frequently confused with `MaskWidth`/`MaskHeight` (the canvas dimensions). `Width = 1.0` means the shape spans the full image width; `Width = 0.5` spans half. See gotcha #21.
406
+ - `Angle` — rotation in degrees.
407
+ - `Invert = Input { Value = 1 }` — inverts mask polarity (effect applies outside the shape).
408
+ - `SoftEdge` — blur applied to the mask boundary.
409
+
410
+ From `Binoculars.setting`:
411
+
412
+ ```lua
413
+ Ellipse1 = EllipseMask {
414
+ Inputs = {
415
+ Filter = Input { Value = FuID { "Fast Gaussian" }, },
416
+ MaskWidth = Input { Value = 1920, },
417
+ MaskHeight = Input { Value = 1080, },
418
+ PixelAspect = Input { Value = { 1, 1 }, },
419
+ ClippingMode = Input { Value = FuID { "None" }, },
420
+ Center = Input { Value = { 0.313210464153306, 0.5 }, },
421
+ Width = Input { Value = 0.5069181538528, }, -- shape size, NOT canvas width
422
+ Height = Input { Value = 0.5069181538528, },
423
+ },
424
+ ViewInfo = OperatorInfo { Pos = { 136.034, -147.182 } },
425
+ }
426
+ ```
427
+
428
+ From `Colored Border.setting` — a `RectangleMask` using `BorderWidth` to create a frame/border shape:
429
+
430
+ ```lua
431
+ Rectangle1 = RectangleMask {
432
+ Inputs = {
433
+ Filter = Input { Value = FuID { "Fast Gaussian" }, },
434
+ BorderWidth = Input { Value = -0.038, },
435
+ Invert = Input { Value = 1, },
436
+ MaskWidth = Input { Value = 3840, },
437
+ MaskHeight = Input { Value = 2160, },
438
+ PixelAspect = Input { Value = { 1, 1 }, },
439
+ ClippingMode = Input { Value = FuID { "None" }, },
440
+ Width = Input { Value = 1, },
441
+ Height = Input { Value = 1, },
442
+ },
443
+ },
444
+ ```
445
+
446
+ Masks can also chain: a second mask node's `EffectMask` receives the output of a first mask, intersecting the two shapes. The `Digital Glitch.setting` uses a chain of three `RectangleMask` nodes each feeding the next's `EffectMask` to build a compound animated region.
447
+
448
+ ## 10. `CustomData` and Preset Variants
449
+
450
+ Many stock effects include a `CustomData` block inside nodes. Two common uses:
451
+
452
+ ```lua
453
+ CustomData = { Settings = { [1] = { Tools = ordered() { ... } }, [2] = { Tools = ordered() { ... } } } }
454
+ ```
455
+
456
+ This stores multiple **preset variations** of the node. The `CurrentSettings = 2` field on the parent node picks which preset is live. You see this a lot on TextPlus-based titles (where each preset is a different style) and on pEmitter-based particle effects.
457
+
458
+ ```lua
459
+ CustomData = { AltSettings9 = { Tools = ordered() { ... } } }
460
+ ```
461
+
462
+ Same idea, different key naming. Lens Flares use this.
463
+
464
+ **For hand-authored templates, you can ignore CustomData entirely** — it's a convenience Fusion writes when you save variations from the UI. Your template will still load and work with just a single variant.
465
+
466
+ ## 11. `UserControls` (Adding Controls Not Tied to a Node Input)
467
+
468
+ Any internal node can add **new** controls that don't map to an existing parameter, via a `UserControls` block. Those controls become addressable as if they were real inputs on that node, and you can then re-expose them via `InstanceInput` up to the GroupOperator.
469
+
470
+ ```lua
471
+ BUTTONS = Fuse.Wireless { -- a fake "no-op" node that hosts the controls
472
+ UserControls = ordered() {
473
+ Button01 = {
474
+ INPID_InputControl = "ButtonControl",
475
+ LINKID_DataType = "Number",
476
+ LINKS_Name = "Top",
477
+ ICS_ControlPage = "Controls",
478
+ ICD_Width = 0.5,
479
+ INP_Integer = false,
480
+ BTNCS_Execute = "tool:SetInput('Input9',{0.5,1})\n tool:SetInput('Input10',{0.5,0})",
481
+ },
482
+ },
483
+ },
484
+ ```
485
+
486
+ `Fuse.Wireless` is the idiomatic carrier for "free-floating" UI controls (buttons that run Lua, labels, etc.) that don't belong to any real node. See `controls.md` for the full list of control types and attributes.
487
+
488
+ ### 11a. Collapsible `LabelControl` Section Headers
489
+
490
+ A `LabelControl` entry in `UserControls` can act as a collapsible disclosure group in the inspector. Set `LBLC_DropDownButton = true` and `LBLC_NumInputs = N` where `N` is the count of immediately following `UserControls` entries that collapse under this header.
491
+
492
+ ```lua
493
+ UserControls = ordered() {
494
+ RightText = {
495
+ LINKS_Name = "Right Text",
496
+ LINKID_DataType = "Number",
497
+ INPID_InputControl = "LabelControl",
498
+ LBLC_DropDownButton = true,
499
+ LBLC_NumInputs = 10, -- next 10 UserControls entries fold under this header
500
+ INP_External = false,
501
+ INP_Integer = false,
502
+ },
503
+ -- ... 10 more entries that collapse under "Right Text" ...
504
+ }
505
+ ```
506
+
507
+ From `CCTV.setting` (on a `TextPlus` node):
508
+
509
+ ```lua
510
+ REC_1 = TextPlus {
511
+ ...
512
+ UserControls = ordered() {
513
+ RightText = {
514
+ INP_Integer = false,
515
+ LBLC_DropDownButton = true,
516
+ LINKID_DataType = "Number",
517
+ LBLC_NumInputs = 10,
518
+ INPID_InputControl = "LabelControl",
519
+ LINKS_Name = "Right Text",
520
+ }
521
+ }
522
+ },
523
+ ```
524
+
525
+ `LBLC_NumInputs` counts entries in the _same_ `UserControls` table, not across multiple nodes. `INP_External = false` prevents the label itself from showing up as a connectable input in the graph. If you omit `LBLC_DropDownButton`, the label renders as a static section divider with no collapse behavior.
526
+
527
+ ## 12. Minimum Viable File
528
+
529
+ The smallest file Fusion will accept as an Edit effect:
530
+
531
+ ```lua
532
+ {
533
+ Tools = ordered() {
534
+ MinimalEffect = GroupOperator {
535
+ Inputs = ordered() {
536
+ MainInput1 = InstanceInput {
537
+ SourceOp = "Blur1",
538
+ Source = "Input",
539
+ },
540
+ Strength = InstanceInput {
541
+ SourceOp = "Blur1",
542
+ Source = "XBlurSize",
543
+ Default = 5,
544
+ MinScale = 0,
545
+ MaxScale = 50,
546
+ },
547
+ },
548
+ Outputs = {
549
+ MainOutput1 = InstanceOutput {
550
+ SourceOp = "Blur1",
551
+ Source = "Output",
552
+ },
553
+ },
554
+ Tools = ordered() {
555
+ Blur1 = Blur {
556
+ Inputs = {
557
+ Filter = Input { Value = FuID { "Fast Gaussian" } },
558
+ XBlurSize = Input { Value = 5 },
559
+ },
560
+ },
561
+ },
562
+ },
563
+ },
564
+ ActiveTool = "MinimalEffect",
565
+ }
566
+ ```
567
+
568
+ Drop that into `Templates/Edit/Effects/MyBlur.setting`, restart Resolve, and it works.