davinci-resolve-mcp 2.23.0
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.
- package/AGENTS.md +85 -0
- package/CHANGELOG.md +802 -0
- package/CLAUDE.md +15 -0
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/SECURITY.md +53 -0
- package/bin/davinci-resolve-mcp.mjs +376 -0
- package/docs/README.md +56 -0
- package/docs/SKILL.md +1145 -0
- package/docs/authoring/fuse-dctl-authoring.md +242 -0
- package/docs/authoring/script-plugin-authoring.md +195 -0
- package/docs/contributing.md +82 -0
- package/docs/guides/color-decision-guide.md +387 -0
- package/docs/guides/editorial-decision-guide.md +136 -0
- package/docs/guides/media-analysis-guide.md +615 -0
- package/docs/guides/multicam-setup-guide.md +138 -0
- package/docs/install.md +198 -0
- package/docs/integrations/workflow-integrations.md +120 -0
- package/docs/kernels/README.md +28 -0
- package/docs/kernels/audio-fairlight-kernel.md +86 -0
- package/docs/kernels/color-grade-kernel.md +103 -0
- package/docs/kernels/extension-authoring-kernel.md +101 -0
- package/docs/kernels/fusion-composition-kernel.md +91 -0
- package/docs/kernels/media-pool-ingest-kernel.md +147 -0
- package/docs/kernels/project-lifecycle-kernel.md +120 -0
- package/docs/kernels/render-deliver-kernel.md +92 -0
- package/docs/kernels/review-annotation-kernel.md +110 -0
- package/docs/kernels/timeline-conform-interchange-kernel.md +99 -0
- package/docs/kernels/timeline-edit-kernel.md +189 -0
- package/docs/notes/codec-plugin-notes.md +136 -0
- package/docs/notes/dctl-notes.md +234 -0
- package/docs/notes/fusion-template-notes.md +136 -0
- package/docs/notes/lut-notes.md +136 -0
- package/docs/notes/openfx-notes.md +120 -0
- package/docs/process/release-process.md +152 -0
- package/docs/reference/api-coverage.md +488 -0
- package/docs/reference/resolve_scripting_api.txt +1012 -0
- package/examples/README.md +53 -0
- package/examples/markers/README.md +81 -0
- package/examples/media/README.md +94 -0
- package/examples/timeline/README.md +98 -0
- package/install.py +1196 -0
- package/package.json +52 -0
- package/scripts/audit_api_parity.py +275 -0
- package/scripts/live_media_analysis_polish_probe.py +65 -0
- package/src/__init__.py +3 -0
- package/src/analysis_dashboard.py +4936 -0
- package/src/control_panel.py +13 -0
- package/src/granular/__init__.py +17 -0
- package/src/granular/common.py +727 -0
- package/src/granular/folder.py +287 -0
- package/src/granular/gallery.py +306 -0
- package/src/granular/graph.py +309 -0
- package/src/granular/media_pool.py +679 -0
- package/src/granular/media_pool_item.py +852 -0
- package/src/granular/media_storage.py +179 -0
- package/src/granular/project.py +1594 -0
- package/src/granular/resolve_control.py +521 -0
- package/src/granular/timeline.py +1074 -0
- package/src/granular/timeline_item.py +2251 -0
- package/src/resolve_mcp_server.py +43 -0
- package/src/server.py +15691 -0
- package/src/utils/__init__.py +3 -0
- package/src/utils/app_control.py +319 -0
- package/src/utils/audio_fairlight_live_probe.py +263 -0
- package/src/utils/cdl.py +20 -0
- package/src/utils/cloud_operations.py +192 -0
- package/src/utils/color_grade_live_probe.py +444 -0
- package/src/utils/dctl_templates.py +368 -0
- package/src/utils/extension_authoring_live_probe.py +292 -0
- package/src/utils/fuse_templates.py +1968 -0
- package/src/utils/fusion_composition_live_probe.py +284 -0
- package/src/utils/layout_presets.py +333 -0
- package/src/utils/mcp_stdio.py +32 -0
- package/src/utils/media_analysis.py +3618 -0
- package/src/utils/media_analysis_jobs.py +796 -0
- package/src/utils/media_pool_ingest_live_probe.py +592 -0
- package/src/utils/multicam.py +393 -0
- package/src/utils/object_inspection.py +287 -0
- package/src/utils/platform.py +157 -0
- package/src/utils/project_lifecycle_live_probe.py +376 -0
- package/src/utils/project_properties.py +601 -0
- package/src/utils/render_deliver_live_probe.py +384 -0
- package/src/utils/resolve_connection.py +77 -0
- package/src/utils/review_annotation_live_probe.py +352 -0
- package/src/utils/script_templates.py +1193 -0
- package/src/utils/sync_detection.py +887 -0
- package/src/utils/timeline_conform_live_probe.py +280 -0
- package/src/utils/timeline_kernel_live_probe.py +1091 -0
- package/src/utils/timeline_kernel_probe.py +185 -0
- package/src/utils/timeline_title_text.py +87 -0
- package/src/utils/update_check.py +610 -0
|
@@ -0,0 +1,1968 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Boilerplate generators for Fusion Fuse plugins.
|
|
3
|
+
|
|
4
|
+
Each generator returns a complete, syntactically-valid .fuse source string ready
|
|
5
|
+
to be written to the Fuses install directory. Generated sources include a
|
|
6
|
+
machine-readable header comment so list() can identify MCP-installed Fuses.
|
|
7
|
+
|
|
8
|
+
Reference: Fusion Fuse SDK (June 2023), Blackmagic Design.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
# Maps the public `type` argument to Fusion's internal ClassType enum.
|
|
14
|
+
CLASS_TYPES = {
|
|
15
|
+
"tool": "CT_Tool",
|
|
16
|
+
"modifier": "CT_Modifier",
|
|
17
|
+
"viewlut": "CT_ViewLUTPlugin",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Linear color ops the color_matrix template knows how to compose.
|
|
21
|
+
COLOR_MATRIX_OPS = ("brightness", "contrast", "gain", "saturation", "invert")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def header(name: str, kind: str, fuse_type: str) -> str:
|
|
25
|
+
"""MCP marker comment placed at the top of generated Fuses."""
|
|
26
|
+
return (
|
|
27
|
+
f"-- @mcp-fuse name={name} kind={kind} type={fuse_type}\n"
|
|
28
|
+
"-- Generated by davinci-resolve-mcp. Edit freely; the marker above\n"
|
|
29
|
+
"-- only affects how the MCP `fuse_plugin list` action reports this file.\n\n"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def color_matrix(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
34
|
+
"""Linear color tool composed from a sequence of matrix operations.
|
|
35
|
+
|
|
36
|
+
options:
|
|
37
|
+
ops: list of operation names from COLOR_MATRIX_OPS.
|
|
38
|
+
Defaults to ['brightness', 'contrast', 'saturation'].
|
|
39
|
+
|
|
40
|
+
The generated Fuse exposes one slider per op and applies them as a single
|
|
41
|
+
ColorMatrixFull, which is faster than chaining separate image operations.
|
|
42
|
+
See Fuse SDK pp. 24 and 102–105.
|
|
43
|
+
"""
|
|
44
|
+
options = options or {}
|
|
45
|
+
ops = options.get("ops") or ["brightness", "contrast", "saturation"]
|
|
46
|
+
invalid = [o for o in ops if o not in COLOR_MATRIX_OPS]
|
|
47
|
+
if invalid:
|
|
48
|
+
raise ValueError(f"Unknown color_matrix ops: {invalid}. "
|
|
49
|
+
f"Valid: {list(COLOR_MATRIX_OPS)}")
|
|
50
|
+
|
|
51
|
+
inputs = []
|
|
52
|
+
matrix_steps = []
|
|
53
|
+
|
|
54
|
+
for op in ops:
|
|
55
|
+
if op == "brightness":
|
|
56
|
+
inputs.append(_slider("InBright", "Brightness", default=0.0,
|
|
57
|
+
min_=-1.0, max_=1.0))
|
|
58
|
+
matrix_steps.append(
|
|
59
|
+
" local b = InBright:GetValue(req).Value\n"
|
|
60
|
+
" m:Offset(b, b, b, 0)"
|
|
61
|
+
)
|
|
62
|
+
elif op == "contrast":
|
|
63
|
+
inputs.append(_slider("InContrast", "Contrast", default=1.0,
|
|
64
|
+
min_=0.0, max_=2.0))
|
|
65
|
+
matrix_steps.append(
|
|
66
|
+
" local c = InContrast:GetValue(req).Value\n"
|
|
67
|
+
" m:Offset(-0.5, -0.5, -0.5, 0)\n"
|
|
68
|
+
" m:Scale(c, c, c, 1)\n"
|
|
69
|
+
" m:Offset(0.5, 0.5, 0.5, 0)"
|
|
70
|
+
)
|
|
71
|
+
elif op == "gain":
|
|
72
|
+
inputs.append(_slider("InGain", "Gain", default=1.0,
|
|
73
|
+
min_=0.0, max_=4.0))
|
|
74
|
+
matrix_steps.append(
|
|
75
|
+
" local g = InGain:GetValue(req).Value\n"
|
|
76
|
+
" m:Scale(g, g, g, 1)"
|
|
77
|
+
)
|
|
78
|
+
elif op == "saturation":
|
|
79
|
+
inputs.append(_slider("InSat", "Saturation", default=1.0,
|
|
80
|
+
min_=0.0, max_=4.0))
|
|
81
|
+
matrix_steps.append(
|
|
82
|
+
" local s = InSat:GetValue(req).Value\n"
|
|
83
|
+
" m:RGBtoYUV()\n"
|
|
84
|
+
" m:Scale(1, s, s, 1)\n"
|
|
85
|
+
" m:YUVtoRGB()"
|
|
86
|
+
)
|
|
87
|
+
elif op == "invert":
|
|
88
|
+
inputs.append(_checkbox("InInvert", "Invert"))
|
|
89
|
+
matrix_steps.append(
|
|
90
|
+
" if InInvert:GetValue(req).Value > 0.5 then\n"
|
|
91
|
+
" m:Scale(-1, -1, -1, 1)\n"
|
|
92
|
+
" m:Offset(1, 1, 1, 0)\n"
|
|
93
|
+
" end"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
inputs_block = "\n".join(inputs)
|
|
97
|
+
process_block = "\n".join(matrix_steps)
|
|
98
|
+
op_list = ", ".join(ops)
|
|
99
|
+
|
|
100
|
+
return header(name, "color_matrix", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
101
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
102
|
+
REGS_OpIconString = "{name[:4]}",
|
|
103
|
+
REGS_OpDescription = "Color matrix tool ({op_list})",
|
|
104
|
+
REG_NoBlendCtrls = false,
|
|
105
|
+
REG_NoMotionBlurCtrls = true,
|
|
106
|
+
}})
|
|
107
|
+
|
|
108
|
+
function Create()
|
|
109
|
+
{inputs_block}
|
|
110
|
+
|
|
111
|
+
InImage = self:AddInput("Input", "Input", {{
|
|
112
|
+
LINKID_DataType = "Image",
|
|
113
|
+
LINK_Main = 1,
|
|
114
|
+
}})
|
|
115
|
+
|
|
116
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
117
|
+
LINKID_DataType = "Image",
|
|
118
|
+
LINK_Main = 1,
|
|
119
|
+
}})
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
function Process(req)
|
|
123
|
+
local img = InImage:GetValue(req)
|
|
124
|
+
local m = ColorMatrixFull()
|
|
125
|
+
|
|
126
|
+
{process_block}
|
|
127
|
+
|
|
128
|
+
local out = img:ApplyMatrixOf(m, {{}})
|
|
129
|
+
OutImage:Set(req, out)
|
|
130
|
+
end
|
|
131
|
+
'''
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def per_pixel(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
135
|
+
"""Per-pixel processing tool using MultiProcessPixels (multithreaded).
|
|
136
|
+
|
|
137
|
+
options:
|
|
138
|
+
inputs: 1 or 2 image inputs (default 1)
|
|
139
|
+
expression: Lua body executed per pixel. Receives (x, y, p1) or
|
|
140
|
+
(x, y, p1, p2). Must mutate and return p1.
|
|
141
|
+
Default: copies p1 unchanged.
|
|
142
|
+
amount: bool, whether to expose an Amount slider as `amt` in the
|
|
143
|
+
expression (default True)
|
|
144
|
+
|
|
145
|
+
See Fuse SDK pp. 29-30, 88-89.
|
|
146
|
+
"""
|
|
147
|
+
options = options or {}
|
|
148
|
+
n_inputs = int(options.get("inputs", 1))
|
|
149
|
+
if n_inputs not in (1, 2):
|
|
150
|
+
raise ValueError("per_pixel: inputs must be 1 or 2")
|
|
151
|
+
expression = options.get("expression", " return p1")
|
|
152
|
+
expose_amount = bool(options.get("amount", True))
|
|
153
|
+
|
|
154
|
+
pixel_args = "x, y, p1, p2" if n_inputs == 2 else "x, y, p1"
|
|
155
|
+
|
|
156
|
+
extra_input = ""
|
|
157
|
+
fetch_extra = ""
|
|
158
|
+
pass_extra = "img1, img1" # MultiProcessPixels needs 2 images even with 1 input
|
|
159
|
+
if n_inputs == 2:
|
|
160
|
+
extra_input = '''
|
|
161
|
+
InImage2 = self:AddInput("Input 2", "Input2", {
|
|
162
|
+
LINKID_DataType = "Image",
|
|
163
|
+
LINK_Main = 2,
|
|
164
|
+
INP_Required = false,
|
|
165
|
+
})
|
|
166
|
+
'''
|
|
167
|
+
fetch_extra = " local img2 = InImage2:GetValue(req)\n if img2 == nil then img2 = img1 end"
|
|
168
|
+
pass_extra = "img1, img2"
|
|
169
|
+
|
|
170
|
+
amount_input = ""
|
|
171
|
+
amount_pass = ""
|
|
172
|
+
if expose_amount:
|
|
173
|
+
amount_input = _slider("InAmount", "Amount", default=1.0, min_=0.0, max_=2.0)
|
|
174
|
+
amount_pass = "{ amt = InAmount:GetValue(req).Value }"
|
|
175
|
+
else:
|
|
176
|
+
amount_pass = "{}"
|
|
177
|
+
|
|
178
|
+
return header(name, "per_pixel", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
179
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
180
|
+
REGS_OpIconString = "{name[:4]}",
|
|
181
|
+
REGS_OpDescription = "Per-pixel custom op",
|
|
182
|
+
}})
|
|
183
|
+
|
|
184
|
+
-- Pixel function: must be defined before Process() (no header files in Lua).
|
|
185
|
+
-- p1 and p2 are Pixel structs with members R, G, B, A and aux channels.
|
|
186
|
+
local function pixel_op({pixel_args})
|
|
187
|
+
{expression}
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
function Create()
|
|
191
|
+
{amount_input}{extra_input}
|
|
192
|
+
InImage = self:AddInput("Input", "Input", {{
|
|
193
|
+
LINKID_DataType = "Image",
|
|
194
|
+
LINK_Main = 1,
|
|
195
|
+
}})
|
|
196
|
+
|
|
197
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
198
|
+
LINKID_DataType = "Image",
|
|
199
|
+
LINK_Main = 1,
|
|
200
|
+
}})
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
function Process(req)
|
|
204
|
+
local img1 = InImage:GetValue(req)
|
|
205
|
+
{fetch_extra}
|
|
206
|
+
|
|
207
|
+
local out = Image({{ IMG_Like = img1 }})
|
|
208
|
+
out:MultiProcessPixels(nil, {amount_pass},
|
|
209
|
+
0, 0, img1.Width, img1.Height,
|
|
210
|
+
{pass_extra}, pixel_op)
|
|
211
|
+
OutImage:Set(req, out)
|
|
212
|
+
end
|
|
213
|
+
'''
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def view_lut(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
217
|
+
"""View LUT plugin: GLSL shader applied during display only.
|
|
218
|
+
|
|
219
|
+
options:
|
|
220
|
+
shader: GLSL body for `void ShadePixel(inout FuPixel f)`.
|
|
221
|
+
Default: a simple gamma adjustment driven by InGamma.
|
|
222
|
+
params: list of {name, type='float', default=1.0, min=0.0, max=5.0}
|
|
223
|
+
Each becomes a slider in the Inspector AND a uniform in the
|
|
224
|
+
shader. Default: one InGamma slider.
|
|
225
|
+
|
|
226
|
+
FuPixel members available in the shader:
|
|
227
|
+
Color vec4 — source pixel color
|
|
228
|
+
TexCoord0 vec4 — pixel coordinates [0..w-1, 0..h-1]
|
|
229
|
+
TexCoord1 vec4 — normalized coordinates [0..1, 0..1]
|
|
230
|
+
TexCoord2 vec4 — destination screen coordinates
|
|
231
|
+
|
|
232
|
+
See Fuse SDK pp. 118–124.
|
|
233
|
+
"""
|
|
234
|
+
options = options or {}
|
|
235
|
+
params = options.get("params") or [
|
|
236
|
+
{"name": "Gamma", "type": "float", "default": 1.0, "min": 0.0, "max": 5.0}
|
|
237
|
+
]
|
|
238
|
+
shader_body = options.get("shader") or (
|
|
239
|
+
" EvalShadePixel(f);\n"
|
|
240
|
+
" f.Color.rgb = sign(f.Color.rgb) * pow(abs(f.Color.rgb), vec3(Gamma));"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
valid_types = ("float", "vec2", "vec3_rgb", "vec4_rgba")
|
|
244
|
+
inputs = []
|
|
245
|
+
setparams = []
|
|
246
|
+
shader_params = []
|
|
247
|
+
for i, prm in enumerate(params, start=1):
|
|
248
|
+
pname = prm["name"]
|
|
249
|
+
ptype = prm.get("type", "float")
|
|
250
|
+
if ptype not in valid_types:
|
|
251
|
+
raise ValueError(f"Invalid view_lut param type '{ptype}'. "
|
|
252
|
+
f"Valid: {list(valid_types)}")
|
|
253
|
+
if ptype == "float":
|
|
254
|
+
default = prm.get("default", 1.0)
|
|
255
|
+
pmin = prm.get("min", 0.0)
|
|
256
|
+
pmax = prm.get("max", 5.0)
|
|
257
|
+
inputs.append(_slider(f"In{pname}", pname, default=default,
|
|
258
|
+
min_=pmin, max_=pmax))
|
|
259
|
+
setparams.append(
|
|
260
|
+
f" vsnode:Set({i}, In{pname}:GetValue(req).Value)"
|
|
261
|
+
)
|
|
262
|
+
shader_params.append(f"float {pname}")
|
|
263
|
+
elif ptype == "vec2":
|
|
264
|
+
inputs.append(
|
|
265
|
+
f' In{pname} = self:AddInput("{pname}", "{pname}", {{\n'
|
|
266
|
+
f' LINKID_DataType = "Point",\n'
|
|
267
|
+
f' INPID_InputControl = "OffsetControl",\n'
|
|
268
|
+
f' INPID_PreviewControl = "CrosshairControl",\n'
|
|
269
|
+
f' INP_DefaultX = 0.5, INP_DefaultY = 0.5,\n'
|
|
270
|
+
f' }})\n'
|
|
271
|
+
)
|
|
272
|
+
setparams.append(
|
|
273
|
+
f" local p_{pname} = In{pname}:GetValue(req)\n"
|
|
274
|
+
f" vsnode:Set({i}, p_{pname}.X, p_{pname}.Y)"
|
|
275
|
+
)
|
|
276
|
+
shader_params.append(f"vec2 {pname}")
|
|
277
|
+
elif ptype in ("vec3_rgb", "vec4_rgba"):
|
|
278
|
+
count = 3 if ptype == "vec3_rgb" else 4
|
|
279
|
+
channel_names = ["Red", "Green", "Blue", "Alpha"][:count]
|
|
280
|
+
for j, chan in enumerate(channel_names):
|
|
281
|
+
inputs.append(
|
|
282
|
+
f' In{pname}{chan} = self:AddInput('
|
|
283
|
+
f'"{pname} {chan}", "{pname}{chan}", {{\n'
|
|
284
|
+
f' LINKID_DataType = "Number",\n'
|
|
285
|
+
f' INPID_InputControl = "ColorControl",\n'
|
|
286
|
+
f' IC_ControlGroup = {100 + i},\n'
|
|
287
|
+
f' IC_ControlID = {j},\n'
|
|
288
|
+
f' INP_Default = 1.0,\n'
|
|
289
|
+
f' }})\n'
|
|
290
|
+
)
|
|
291
|
+
getters = ", ".join(
|
|
292
|
+
f"In{pname}{chan}:GetValue(req).Value"
|
|
293
|
+
for chan in channel_names
|
|
294
|
+
)
|
|
295
|
+
setparams.append(f" vsnode:Set({i}, {getters})")
|
|
296
|
+
glsl_type = "vec3" if ptype == "vec3_rgb" else "vec4"
|
|
297
|
+
shader_params.append(f"{glsl_type} {pname}")
|
|
298
|
+
|
|
299
|
+
inputs_block = "\n".join(inputs)
|
|
300
|
+
setparams_block = "\n".join(setparams)
|
|
301
|
+
params_string = "\\n".join(shader_params)
|
|
302
|
+
|
|
303
|
+
return header(name, "view_lut", "viewlut") + f'''FuRegisterClass("{name}", CT_ViewLUTPlugin, {{
|
|
304
|
+
REGS_Name = "{name}",
|
|
305
|
+
REGS_Category = "ViewShaders",
|
|
306
|
+
}})
|
|
307
|
+
|
|
308
|
+
function Create()
|
|
309
|
+
{inputs_block}
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
local params = [[
|
|
313
|
+
{chr(10).join(shader_params)}
|
|
314
|
+
]]
|
|
315
|
+
|
|
316
|
+
local shader = [[
|
|
317
|
+
void ShadePixel(inout FuPixel f)
|
|
318
|
+
{{
|
|
319
|
+
{shader_body}
|
|
320
|
+
}}
|
|
321
|
+
]]
|
|
322
|
+
|
|
323
|
+
function SetupShadeNode(group, req, img)
|
|
324
|
+
return ViewShadeNode(group, "{name}", params, shader)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
function SetupParams(req, vsnode, img)
|
|
328
|
+
{setparams_block}
|
|
329
|
+
return true
|
|
330
|
+
end
|
|
331
|
+
'''
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def transform(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
335
|
+
"""Geometric warp tool with on-screen Center crosshair, Angle, and Size.
|
|
336
|
+
|
|
337
|
+
options:
|
|
338
|
+
edge_mode: 'Black' | 'Canvas' | 'Wrap' | 'Duplicate' (default 'Black')
|
|
339
|
+
|
|
340
|
+
The generated Fuse uses Image:Transform under the hood, with the OffsetControl
|
|
341
|
+
Y-coordinate squashed conversion baked in so the on-screen crosshair lands
|
|
342
|
+
where the user clicks regardless of image aspect ratio. See Fuse SDK p. 115.
|
|
343
|
+
"""
|
|
344
|
+
options = options or {}
|
|
345
|
+
edge_mode = options.get("edge_mode", "Black")
|
|
346
|
+
if edge_mode not in ("Black", "Canvas", "Wrap", "Duplicate"):
|
|
347
|
+
raise ValueError(f"Invalid edge_mode '{edge_mode}'. "
|
|
348
|
+
"Valid: Black, Canvas, Wrap, Duplicate")
|
|
349
|
+
|
|
350
|
+
return header(name, "transform", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
351
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
352
|
+
REGS_OpIconString = "{name[:4]}",
|
|
353
|
+
REGS_OpDescription = "Transform tool (warp/rotate/scale)",
|
|
354
|
+
}})
|
|
355
|
+
|
|
356
|
+
function Create()
|
|
357
|
+
InCenter = self:AddInput("Center", "Center", {{
|
|
358
|
+
LINKID_DataType = "Point",
|
|
359
|
+
INPID_InputControl = "OffsetControl",
|
|
360
|
+
INPID_PreviewControl = "CrosshairControl",
|
|
361
|
+
INP_DefaultX = 0.5,
|
|
362
|
+
INP_DefaultY = 0.5,
|
|
363
|
+
}})
|
|
364
|
+
|
|
365
|
+
InAngle = self:AddInput("Angle", "Angle", {{
|
|
366
|
+
LINKID_DataType = "Number",
|
|
367
|
+
INPID_InputControl = "ScrewControl",
|
|
368
|
+
INP_Default = 0.0,
|
|
369
|
+
INP_MinScale = -180.0,
|
|
370
|
+
INP_MaxScale = 180.0,
|
|
371
|
+
}})
|
|
372
|
+
|
|
373
|
+
InSize = self:AddInput("Size", "Size", {{
|
|
374
|
+
LINKID_DataType = "Number",
|
|
375
|
+
INPID_InputControl = "SliderControl",
|
|
376
|
+
INP_Default = 1.0,
|
|
377
|
+
INP_MinScale = 0.0,
|
|
378
|
+
INP_MaxScale = 4.0,
|
|
379
|
+
}})
|
|
380
|
+
|
|
381
|
+
InImage = self:AddInput("Input", "Input", {{
|
|
382
|
+
LINKID_DataType = "Image",
|
|
383
|
+
LINK_Main = 1,
|
|
384
|
+
}})
|
|
385
|
+
|
|
386
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
387
|
+
LINKID_DataType = "Image",
|
|
388
|
+
LINK_Main = 1,
|
|
389
|
+
}})
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
function Process(req)
|
|
393
|
+
local img = InImage:GetValue(req)
|
|
394
|
+
local center = InCenter:GetValue(req)
|
|
395
|
+
local angle = InAngle:GetValue(req).Value
|
|
396
|
+
local size = InSize:GetValue(req).Value
|
|
397
|
+
|
|
398
|
+
local out = img:Transform(nil, {{
|
|
399
|
+
XF_XOffset = center.X,
|
|
400
|
+
XF_YOffset = center.Y,
|
|
401
|
+
XF_XAxis = 0.5,
|
|
402
|
+
XF_YAxis = 0.5,
|
|
403
|
+
XF_XSize = size,
|
|
404
|
+
XF_YSize = size,
|
|
405
|
+
XF_Angle = angle,
|
|
406
|
+
XF_EdgeMode = "{edge_mode}",
|
|
407
|
+
}})
|
|
408
|
+
|
|
409
|
+
OutImage:Set(req, out)
|
|
410
|
+
end
|
|
411
|
+
'''
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def text_overlay(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
415
|
+
"""Renders styled text into the input image using Fusion's font shape system.
|
|
416
|
+
|
|
417
|
+
options:
|
|
418
|
+
default_text: str (default '')
|
|
419
|
+
justify: 'left' | 'center' | 'right' (default 'center')
|
|
420
|
+
|
|
421
|
+
The generated Fuse exposes Styled Text, Font, Style, Size, Position, Color,
|
|
422
|
+
and a justify dropdown. Internally uses Fontmetrics + ImageChannel + Shape +
|
|
423
|
+
PutToImage with CM_Merge so text overlays the input image.
|
|
424
|
+
See Fuse SDK pp. 33-36 (Example6_Text reference implementation).
|
|
425
|
+
"""
|
|
426
|
+
options = options or {}
|
|
427
|
+
default_text = options.get("default_text", "")
|
|
428
|
+
justify_label = options.get("justify", "center")
|
|
429
|
+
justify_index = {"left": 0, "center": 1, "right": 2}.get(justify_label)
|
|
430
|
+
if justify_index is None:
|
|
431
|
+
raise ValueError(f"Invalid justify '{justify_label}'. "
|
|
432
|
+
"Valid: left, center, right")
|
|
433
|
+
|
|
434
|
+
return header(name, "text_overlay", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
435
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
436
|
+
REGS_OpIconString = "{name[:4]}",
|
|
437
|
+
REGS_OpDescription = "Text overlay",
|
|
438
|
+
}})
|
|
439
|
+
|
|
440
|
+
function Create()
|
|
441
|
+
InText = self:AddInput("Styled Text", "StyledText", {{
|
|
442
|
+
LINKID_DataType = "Text",
|
|
443
|
+
INPID_InputControl = "TextEditControl",
|
|
444
|
+
TEC_Lines = 3,
|
|
445
|
+
INPS_DefaultText = "{default_text}",
|
|
446
|
+
}})
|
|
447
|
+
|
|
448
|
+
self:BeginControlNest("Font", "FontNest", true, {{}})
|
|
449
|
+
InFont = self:AddInput("Font", "Font", {{
|
|
450
|
+
LINKID_DataType = "Text",
|
|
451
|
+
INPID_InputControl = "FontFileControl",
|
|
452
|
+
IC_ControlGroup = 1,
|
|
453
|
+
IC_ControlID = 0,
|
|
454
|
+
INP_DoNotifyChanged = true,
|
|
455
|
+
}})
|
|
456
|
+
|
|
457
|
+
InStyle = self:AddInput("Style", "Style", {{
|
|
458
|
+
LINKID_DataType = "Text",
|
|
459
|
+
INPID_InputControl = "FontFileControl",
|
|
460
|
+
IC_ControlGroup = 1,
|
|
461
|
+
IC_ControlID = 1,
|
|
462
|
+
INP_DoNotifyChanged = true,
|
|
463
|
+
}})
|
|
464
|
+
|
|
465
|
+
InSize = self:AddInput("Size", "Size", {{
|
|
466
|
+
LINKID_DataType = "Number",
|
|
467
|
+
INPID_InputControl = "SliderControl",
|
|
468
|
+
INP_Default = 0.08,
|
|
469
|
+
INP_MinScale = 0.0,
|
|
470
|
+
INP_MaxScale = 1.0,
|
|
471
|
+
}})
|
|
472
|
+
self:EndControlNest()
|
|
473
|
+
|
|
474
|
+
self:BeginControlNest("Layout", "LayoutNest", true, {{}})
|
|
475
|
+
InPosition = self:AddInput("Position", "Position", {{
|
|
476
|
+
LINKID_DataType = "Point",
|
|
477
|
+
INPID_InputControl = "OffsetControl",
|
|
478
|
+
INPID_PreviewControl = "CrosshairControl",
|
|
479
|
+
INP_DefaultX = 0.5,
|
|
480
|
+
INP_DefaultY = 0.5,
|
|
481
|
+
}})
|
|
482
|
+
|
|
483
|
+
InJustify = self:AddInput("Justify", "Justify", {{
|
|
484
|
+
LINKID_DataType = "Number",
|
|
485
|
+
INPID_InputControl = "ComboControl",
|
|
486
|
+
INP_Default = {justify_index},
|
|
487
|
+
INP_Integer = true,
|
|
488
|
+
{{ CCS_AddString = "Left", }},
|
|
489
|
+
{{ CCS_AddString = "Center", }},
|
|
490
|
+
{{ CCS_AddString = "Right", }},
|
|
491
|
+
}})
|
|
492
|
+
self:EndControlNest()
|
|
493
|
+
|
|
494
|
+
self:BeginControlNest("Color", "ColorNest", false, {{}})
|
|
495
|
+
InR = self:AddInput("Red", "Red", {{
|
|
496
|
+
LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
|
|
497
|
+
IC_ControlGroup = 2, IC_ControlID = 0, INP_Default = 1.0,
|
|
498
|
+
}})
|
|
499
|
+
InG = self:AddInput("Green", "Green", {{
|
|
500
|
+
LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
|
|
501
|
+
IC_ControlGroup = 2, IC_ControlID = 1, INP_Default = 1.0,
|
|
502
|
+
}})
|
|
503
|
+
InB = self:AddInput("Blue", "Blue", {{
|
|
504
|
+
LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
|
|
505
|
+
IC_ControlGroup = 2, IC_ControlID = 2, INP_Default = 1.0,
|
|
506
|
+
}})
|
|
507
|
+
InA = self:AddInput("Alpha", "Alpha", {{
|
|
508
|
+
LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
|
|
509
|
+
IC_ControlGroup = 2, IC_ControlID = 3, INP_Default = 1.0,
|
|
510
|
+
}})
|
|
511
|
+
self:EndControlNest()
|
|
512
|
+
|
|
513
|
+
InImage = self:AddInput("Input", "Input", {{
|
|
514
|
+
LINKID_DataType = "Image",
|
|
515
|
+
LINK_Main = 1,
|
|
516
|
+
}})
|
|
517
|
+
|
|
518
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
519
|
+
LINKID_DataType = "Image",
|
|
520
|
+
LINK_Main = 1,
|
|
521
|
+
}})
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
-- drawstring renders one or more lines of text into ImageChannel `ic`.
|
|
525
|
+
-- Adapted from the SDK's Example6_Text Fuse.
|
|
526
|
+
local function drawstring(img, font_name, style, size, justify, quality,
|
|
527
|
+
x, y, colour, text)
|
|
528
|
+
local ic = ImageChannel(img, quality)
|
|
529
|
+
local fs = FillStyle()
|
|
530
|
+
local cs = ChannelStyle()
|
|
531
|
+
cs.Color = colour
|
|
532
|
+
ic:SetStyleFill(fs)
|
|
533
|
+
|
|
534
|
+
local font = TextStyleFont(font_name, style)
|
|
535
|
+
local tfm = TextStyleFontMetrics(font)
|
|
536
|
+
local line_height = (tfm.TextAscent + tfm.TextDescent +
|
|
537
|
+
tfm.TextExternalLeading) * 10 * size
|
|
538
|
+
|
|
539
|
+
local mat = Matrix4()
|
|
540
|
+
mat:Scale(1.0 / tfm.Scale, 1.0 / tfm.Scale, 1.0)
|
|
541
|
+
mat:Scale(size, size, 1)
|
|
542
|
+
mat:Move(x, y, 0)
|
|
543
|
+
|
|
544
|
+
local shape = Shape()
|
|
545
|
+
|
|
546
|
+
for line in string.gmatch(text, "%C+") do
|
|
547
|
+
local line_width = 0
|
|
548
|
+
for i = 1, #line do
|
|
549
|
+
local ch = line:sub(i, i):byte()
|
|
550
|
+
line_width = line_width + tfm:CharacterWidth(ch) * 10 * size
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
if justify == 1 then
|
|
554
|
+
mat:Move(-line_width / 2, 0, 0)
|
|
555
|
+
elseif justify == 2 then
|
|
556
|
+
mat:Move(-line_width, 0, 0)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
local prevch
|
|
560
|
+
local x_move = 0
|
|
561
|
+
for i = 1, #line do
|
|
562
|
+
local ch = line:sub(i, i):byte()
|
|
563
|
+
local cw = tfm:CharacterWidth(ch) * 10 * size
|
|
564
|
+
|
|
565
|
+
if prevch then
|
|
566
|
+
local x_offset = tfm:CharacterKerning(prevch, ch) * 10 * size
|
|
567
|
+
x_move = x_move + x_offset
|
|
568
|
+
mat:Move(x_offset, 0, 0)
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
mat:Move(cw / 2, 0, 0)
|
|
572
|
+
local sh = tfm:GetCharacterShape(ch, false)
|
|
573
|
+
sh = sh:TransformOfShape(mat)
|
|
574
|
+
mat:Move(cw / 2, 0, 0)
|
|
575
|
+
x_move = x_move + cw
|
|
576
|
+
|
|
577
|
+
shape:AddShape(sh)
|
|
578
|
+
prevch = ch
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
if justify == 0 then
|
|
582
|
+
mat:Move(-x_move, -line_height, 0)
|
|
583
|
+
elseif justify == 1 then
|
|
584
|
+
mat:Move(-x_move / 2, -line_height, 0)
|
|
585
|
+
else
|
|
586
|
+
mat:Move(0, -line_height, 0)
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
ic:ShapeFill(shape)
|
|
591
|
+
ic:PutToImage("CM_Merge", cs)
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
function Process(req)
|
|
595
|
+
local img = InImage:GetValue(req)
|
|
596
|
+
local out = img:CopyOf()
|
|
597
|
+
|
|
598
|
+
local text = InText:GetValue(req).Value or ""
|
|
599
|
+
if text == "" then
|
|
600
|
+
OutImage:Set(req, out)
|
|
601
|
+
return
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
local font = InFont:GetValue(req).Value or "Open Sans"
|
|
605
|
+
local style = InStyle:GetValue(req).Value or "Regular"
|
|
606
|
+
local size = InSize:GetValue(req).Value
|
|
607
|
+
local pos = InPosition:GetValue(req)
|
|
608
|
+
local justify = InJustify:GetValue(req).Value
|
|
609
|
+
local r = InR:GetValue(req).Value
|
|
610
|
+
local g = InG:GetValue(req).Value
|
|
611
|
+
local b = InB:GetValue(req).Value
|
|
612
|
+
local a = InA:GetValue(req).Value
|
|
613
|
+
|
|
614
|
+
-- Squash-correction so the on-screen point lands where the user clicks.
|
|
615
|
+
local cx = pos.X
|
|
616
|
+
local cy = pos.Y * (out.Height * out.YScale) / (out.Width * out.XScale)
|
|
617
|
+
|
|
618
|
+
if FontManager and not next(FontManager:GetFontList()) then
|
|
619
|
+
FontManager:ScanDir()
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
local quality = 32
|
|
623
|
+
if req:IsQuick() then quality = 1 end
|
|
624
|
+
|
|
625
|
+
drawstring(out, font, style, size, justify, quality, cx, cy,
|
|
626
|
+
Pixel({{R = r, G = g, B = b, A = a}}), text)
|
|
627
|
+
|
|
628
|
+
OutImage:Set(req, out)
|
|
629
|
+
end
|
|
630
|
+
'''
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def modifier(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
634
|
+
"""Modifier plugin: drives a Number input on another tool with a function of time.
|
|
635
|
+
|
|
636
|
+
options:
|
|
637
|
+
kind: 'sine' | 'noise' | 'ramp' (default 'sine')
|
|
638
|
+
|
|
639
|
+
Modifiers have no image inputs; they expose a Number output. To use the
|
|
640
|
+
generated modifier, right-click any slider in the Inspector → Modify with →
|
|
641
|
+
[name]. See Fuse SDK p. 9 (Modifier description) and p. 40 (CT_Modifier).
|
|
642
|
+
"""
|
|
643
|
+
options = options or {}
|
|
644
|
+
kind = options.get("kind", "sine")
|
|
645
|
+
if kind not in ("sine", "noise", "ramp"):
|
|
646
|
+
raise ValueError(f"Invalid modifier kind '{kind}'. "
|
|
647
|
+
"Valid: sine, noise, ramp")
|
|
648
|
+
|
|
649
|
+
if kind == "sine":
|
|
650
|
+
formula = "math.sin(2 * math.pi * (t * freq + phase)) * amp + offset"
|
|
651
|
+
elif kind == "ramp":
|
|
652
|
+
formula = "((t * freq + phase) % 1.0) * amp + offset"
|
|
653
|
+
else: # noise
|
|
654
|
+
formula = ("amp * (math.random() - 0.5) * 2 + offset")
|
|
655
|
+
|
|
656
|
+
return header(name, "modifier", "modifier") + f'''FuRegisterClass("{name}", CT_Modifier, {{
|
|
657
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
658
|
+
REGS_OpIconString = "{name[:4]}",
|
|
659
|
+
REGS_OpDescription = "Number modifier ({kind})",
|
|
660
|
+
}})
|
|
661
|
+
|
|
662
|
+
function Create()
|
|
663
|
+
InFreq = self:AddInput("Frequency", "Frequency", {{
|
|
664
|
+
LINKID_DataType = "Number",
|
|
665
|
+
INPID_InputControl = "SliderControl",
|
|
666
|
+
INP_Default = 1.0,
|
|
667
|
+
INP_MinScale = 0.0,
|
|
668
|
+
INP_MaxScale = 10.0,
|
|
669
|
+
}})
|
|
670
|
+
|
|
671
|
+
InAmp = self:AddInput("Amplitude", "Amplitude", {{
|
|
672
|
+
LINKID_DataType = "Number",
|
|
673
|
+
INPID_InputControl = "SliderControl",
|
|
674
|
+
INP_Default = 1.0,
|
|
675
|
+
INP_MinScale = 0.0,
|
|
676
|
+
INP_MaxScale = 10.0,
|
|
677
|
+
}})
|
|
678
|
+
|
|
679
|
+
InPhase = self:AddInput("Phase", "Phase", {{
|
|
680
|
+
LINKID_DataType = "Number",
|
|
681
|
+
INPID_InputControl = "SliderControl",
|
|
682
|
+
INP_Default = 0.0,
|
|
683
|
+
INP_MinScale = -1.0,
|
|
684
|
+
INP_MaxScale = 1.0,
|
|
685
|
+
}})
|
|
686
|
+
|
|
687
|
+
InOffset = self:AddInput("Offset", "Offset", {{
|
|
688
|
+
LINKID_DataType = "Number",
|
|
689
|
+
INPID_InputControl = "SliderControl",
|
|
690
|
+
INP_Default = 0.0,
|
|
691
|
+
INP_MinScale = -10.0,
|
|
692
|
+
INP_MaxScale = 10.0,
|
|
693
|
+
}})
|
|
694
|
+
|
|
695
|
+
OutValue = self:AddOutput("Value", "Value", {{
|
|
696
|
+
LINKID_DataType = "Number",
|
|
697
|
+
LINK_Main = 1,
|
|
698
|
+
}})
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
function Process(req)
|
|
702
|
+
local t = req.Time
|
|
703
|
+
local freq = InFreq:GetValue(req).Value
|
|
704
|
+
local amp = InAmp:GetValue(req).Value
|
|
705
|
+
local phase = InPhase:GetValue(req).Value
|
|
706
|
+
local offset = InOffset:GetValue(req).Value
|
|
707
|
+
|
|
708
|
+
local v = {formula}
|
|
709
|
+
OutValue:Set(req, Number(v))
|
|
710
|
+
end
|
|
711
|
+
'''
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def dctl_kernel(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
715
|
+
"""Fuse that wraps a DCTL kernel via DVIPComputeNode (GPU compute).
|
|
716
|
+
|
|
717
|
+
options:
|
|
718
|
+
kernel: GPU kernel body. Default is a per-pixel gain shader.
|
|
719
|
+
params: list of {name, type='float', default=1.0, min=0.0, max=4.0}
|
|
720
|
+
Each becomes a Lua slider AND a kernel parameter.
|
|
721
|
+
|
|
722
|
+
The generated Fuse runs on Metal (macOS), CUDA (NVIDIA), or OpenCL (AMD)
|
|
723
|
+
via Resolve's built-in DCTL compiler. See Fuse SDK pp. 130-143 (Circle/
|
|
724
|
+
Gradient example).
|
|
725
|
+
"""
|
|
726
|
+
options = options or {}
|
|
727
|
+
params = options.get("params") or [
|
|
728
|
+
{"name": "Gain", "type": "float", "default": 1.0, "min": 0.0, "max": 4.0}
|
|
729
|
+
]
|
|
730
|
+
kernel = options.get("kernel") or (
|
|
731
|
+
" float4 c = _tex2DVecN(src, x, y, 15);\n"
|
|
732
|
+
" c.x *= params->Gain;\n"
|
|
733
|
+
" c.y *= params->Gain;\n"
|
|
734
|
+
" c.z *= params->Gain;\n"
|
|
735
|
+
" _tex2DVec4Write(dst, x, y, c);"
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
inputs = []
|
|
739
|
+
set_lines = []
|
|
740
|
+
param_struct = []
|
|
741
|
+
for prm in params:
|
|
742
|
+
pname = prm["name"]
|
|
743
|
+
ptype = prm.get("type", "float")
|
|
744
|
+
default = prm.get("default", 1.0)
|
|
745
|
+
pmin = prm.get("min", 0.0)
|
|
746
|
+
pmax = prm.get("max", 4.0)
|
|
747
|
+
inputs.append(_slider(f"In{pname}", pname, default=default,
|
|
748
|
+
min_=pmin, max_=pmax))
|
|
749
|
+
set_lines.append(f" paramblock.{pname} = In{pname}:GetValue(req).Value")
|
|
750
|
+
param_struct.append(f" {ptype} {pname};")
|
|
751
|
+
|
|
752
|
+
inputs_block = "\n".join(inputs)
|
|
753
|
+
set_block = "\n".join(set_lines)
|
|
754
|
+
struct_body = "\n".join(param_struct)
|
|
755
|
+
|
|
756
|
+
return header(name, "dctl_kernel", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
757
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
758
|
+
REGS_OpIconString = "{name[:4]}",
|
|
759
|
+
REGS_OpDescription = "DCTL GPU kernel",
|
|
760
|
+
}})
|
|
761
|
+
|
|
762
|
+
KernelParams = [[
|
|
763
|
+
{struct_body}
|
|
764
|
+
int srcsize[2];
|
|
765
|
+
]]
|
|
766
|
+
|
|
767
|
+
KernelSource = [[
|
|
768
|
+
__KERNEL__ void {name}Kernel(__CONSTANTREF__ KernelParams *params,
|
|
769
|
+
__TEXTURE2D__ src,
|
|
770
|
+
__TEXTURE2D_WRITE__ dst)
|
|
771
|
+
{{
|
|
772
|
+
DEFINE_KERNEL_ITERATORS_XY(x, y);
|
|
773
|
+
if (x < params->srcsize[0] && y < params->srcsize[1])
|
|
774
|
+
{{
|
|
775
|
+
{kernel}
|
|
776
|
+
}}
|
|
777
|
+
}}
|
|
778
|
+
]]
|
|
779
|
+
|
|
780
|
+
function Create()
|
|
781
|
+
{inputs_block}
|
|
782
|
+
|
|
783
|
+
InImage = self:AddInput("Input", "Input", {{
|
|
784
|
+
LINKID_DataType = "Image",
|
|
785
|
+
LINK_Main = 1,
|
|
786
|
+
}})
|
|
787
|
+
|
|
788
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
789
|
+
LINKID_DataType = "Image",
|
|
790
|
+
LINK_Main = 1,
|
|
791
|
+
}})
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
function Process(req)
|
|
795
|
+
local img = InImage:GetValue(req)
|
|
796
|
+
local out = Image({{ IMG_Like = img }})
|
|
797
|
+
|
|
798
|
+
local node = DVIPComputeNode(req, "{name}Kernel", KernelSource,
|
|
799
|
+
"KernelParams", KernelParams)
|
|
800
|
+
local paramblock = node:GetParamBlock(KernelParams)
|
|
801
|
+
{set_block}
|
|
802
|
+
paramblock.srcsize[0] = img.DataWindow:Width()
|
|
803
|
+
paramblock.srcsize[1] = img.DataWindow:Height()
|
|
804
|
+
node:SetParamBlock(paramblock)
|
|
805
|
+
|
|
806
|
+
node:AddInput("src", img)
|
|
807
|
+
node:AddOutput("dst", out)
|
|
808
|
+
node:AddSampler("RowSampler", TEX_FILTER_MODE_LINEAR,
|
|
809
|
+
TEX_ADDRESS_MODE_CLAMP, TEX_NORMALIZED_COORDS_FALSE)
|
|
810
|
+
|
|
811
|
+
local ok = node:RunSession(req)
|
|
812
|
+
if not ok then
|
|
813
|
+
print("DCTL kernel run failed for {name}")
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
OutImage:Set(req, out)
|
|
817
|
+
end
|
|
818
|
+
'''
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def source_generator(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
822
|
+
"""Source-only Fuse with no image input. Outputs procedurally-generated content.
|
|
823
|
+
|
|
824
|
+
options:
|
|
825
|
+
kind: 'noise' | 'gradient' | 'checkerboard' | 'solid' (default 'noise')
|
|
826
|
+
width: int (default 1920)
|
|
827
|
+
height: int (default 1080)
|
|
828
|
+
|
|
829
|
+
Uses CT_Tool with REG_Source_*Ctrls registration so Fusion treats this as
|
|
830
|
+
a source generator (no image input slot, sized by the project). See the
|
|
831
|
+
SDK's GPUSampleFuse example (p. 140).
|
|
832
|
+
"""
|
|
833
|
+
options = options or {}
|
|
834
|
+
kind = options.get("kind", "noise")
|
|
835
|
+
if kind not in ("noise", "gradient", "checkerboard", "solid"):
|
|
836
|
+
raise ValueError(f"Invalid source_generator kind '{kind}'. "
|
|
837
|
+
"Valid: noise, gradient, checkerboard, solid")
|
|
838
|
+
width = int(options.get("width", 1920))
|
|
839
|
+
height = int(options.get("height", 1080))
|
|
840
|
+
|
|
841
|
+
if kind == "noise":
|
|
842
|
+
body = ''' local p = Pixel({A = 1})
|
|
843
|
+
for y = 0, out.Height - 1 do
|
|
844
|
+
for x = 0, out.Width - 1 do
|
|
845
|
+
p.R = math.random()
|
|
846
|
+
p.G = math.random()
|
|
847
|
+
p.B = math.random()
|
|
848
|
+
out:SetPixel(x, y, p)
|
|
849
|
+
end
|
|
850
|
+
end'''
|
|
851
|
+
elif kind == "gradient":
|
|
852
|
+
body = ''' local p = Pixel({A = 1})
|
|
853
|
+
for y = 0, out.Height - 1 do
|
|
854
|
+
local v = y / (out.Height - 1)
|
|
855
|
+
for x = 0, out.Width - 1 do
|
|
856
|
+
local u = x / (out.Width - 1)
|
|
857
|
+
p.R = u
|
|
858
|
+
p.G = v
|
|
859
|
+
p.B = 1 - u
|
|
860
|
+
out:SetPixel(x, y, p)
|
|
861
|
+
end
|
|
862
|
+
end'''
|
|
863
|
+
elif kind == "checkerboard":
|
|
864
|
+
body = ''' local p = Pixel({A = 1})
|
|
865
|
+
local cell = 64
|
|
866
|
+
for y = 0, out.Height - 1 do
|
|
867
|
+
for x = 0, out.Width - 1 do
|
|
868
|
+
local on = ((math.floor(x / cell) + math.floor(y / cell)) % 2) == 0
|
|
869
|
+
local v = on and 1 or 0
|
|
870
|
+
p.R, p.G, p.B = v, v, v
|
|
871
|
+
out:SetPixel(x, y, p)
|
|
872
|
+
end
|
|
873
|
+
end'''
|
|
874
|
+
else: # solid
|
|
875
|
+
body = ''' local r = InColorR:GetValue(req).Value
|
|
876
|
+
local g = InColorG:GetValue(req).Value
|
|
877
|
+
local b = InColorB:GetValue(req).Value
|
|
878
|
+
out:Fill(Pixel({R = r, G = g, B = b, A = 1}))'''
|
|
879
|
+
|
|
880
|
+
extra_inputs = ""
|
|
881
|
+
if kind == "solid":
|
|
882
|
+
extra_inputs = (
|
|
883
|
+
_slider("InColorR", "Red", default=1.0, min_=0.0, max_=1.0)
|
|
884
|
+
+ _slider("InColorG", "Green", default=1.0, min_=0.0, max_=1.0)
|
|
885
|
+
+ _slider("InColorB", "Blue", default=1.0, min_=0.0, max_=1.0)
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
return header(name, "source_generator", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
889
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
890
|
+
REGS_OpIconString = "{name[:4]}",
|
|
891
|
+
REGS_OpDescription = "Source generator ({kind})",
|
|
892
|
+
REG_NoBlendCtrls = true,
|
|
893
|
+
REG_NoMotionBlurCtrls = true,
|
|
894
|
+
REG_NoObjMatCtrls = true,
|
|
895
|
+
REG_OpNoMask = true,
|
|
896
|
+
REG_Source_GlobalCtrls = true,
|
|
897
|
+
REG_Source_SizeCtrls = true,
|
|
898
|
+
REG_Source_AspectCtrls = true,
|
|
899
|
+
REG_Source_DepthCtrls = true,
|
|
900
|
+
}})
|
|
901
|
+
|
|
902
|
+
function Create()
|
|
903
|
+
{extra_inputs}
|
|
904
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
905
|
+
LINKID_DataType = "Image",
|
|
906
|
+
LINK_Main = 1,
|
|
907
|
+
}})
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
function Process(req)
|
|
911
|
+
local realwidth = Width
|
|
912
|
+
local realheight = Height
|
|
913
|
+
Width = Width / Scale
|
|
914
|
+
Height = Height / Scale
|
|
915
|
+
Scale = 1
|
|
916
|
+
|
|
917
|
+
local imgattrs = {{
|
|
918
|
+
IMG_Document = self.Comp,
|
|
919
|
+
{{ IMG_Channel = "Red" }},
|
|
920
|
+
{{ IMG_Channel = "Green" }},
|
|
921
|
+
{{ IMG_Channel = "Blue" }},
|
|
922
|
+
{{ IMG_Channel = "Alpha" }},
|
|
923
|
+
IMG_Width = Width,
|
|
924
|
+
IMG_Height = Height,
|
|
925
|
+
IMG_XScale = XAspect,
|
|
926
|
+
IMG_YScale = YAspect,
|
|
927
|
+
IMAT_OriginalWidth = realwidth,
|
|
928
|
+
IMAT_OriginalHeight = realheight,
|
|
929
|
+
IMG_Quality = not req:IsQuick(),
|
|
930
|
+
IMG_MotionBlurQuality = not req:IsNoMotionBlur(),
|
|
931
|
+
}}
|
|
932
|
+
if not req:IsStampOnly() then
|
|
933
|
+
imgattrs.IMG_ProxyScale = 1
|
|
934
|
+
end
|
|
935
|
+
if SourceDepth ~= 0 then
|
|
936
|
+
imgattrs.IMG_Depth = SourceDepth
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
local out = Image(imgattrs)
|
|
940
|
+
{body}
|
|
941
|
+
|
|
942
|
+
OutImage:Set(req, out)
|
|
943
|
+
end
|
|
944
|
+
'''
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def time_displace(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
948
|
+
"""Temporal effect Fuse: outputs the input at a time-shifted frame.
|
|
949
|
+
|
|
950
|
+
options:
|
|
951
|
+
default_offset: integer frame offset (default -1, i.e. previous frame)
|
|
952
|
+
|
|
953
|
+
Uses InImage:GetSource(req.Time + offset) to read a different frame than
|
|
954
|
+
the current request time. Foundation for motion blur, frame averaging,
|
|
955
|
+
deja-vu, and time-displacement effects. See SDK p. 45.
|
|
956
|
+
"""
|
|
957
|
+
options = options or {}
|
|
958
|
+
default_offset = int(options.get("default_offset", -1))
|
|
959
|
+
|
|
960
|
+
return header(name, "time_displace", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
961
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
962
|
+
REGS_OpIconString = "{name[:4]}",
|
|
963
|
+
REGS_OpDescription = "Time displace (temporal frame shift)",
|
|
964
|
+
}})
|
|
965
|
+
|
|
966
|
+
function Create()
|
|
967
|
+
InOffset = self:AddInput("Frame Offset", "FrameOffset", {{
|
|
968
|
+
LINKID_DataType = "Number",
|
|
969
|
+
INPID_InputControl = "SliderControl",
|
|
970
|
+
INP_Integer = true,
|
|
971
|
+
INP_Default = {default_offset},
|
|
972
|
+
INP_MinScale = -30,
|
|
973
|
+
INP_MaxScale = 30,
|
|
974
|
+
}})
|
|
975
|
+
|
|
976
|
+
InImage = self:AddInput("Input", "Input", {{
|
|
977
|
+
LINKID_DataType = "Image",
|
|
978
|
+
LINK_Main = 1,
|
|
979
|
+
INP_SendRequest = false,
|
|
980
|
+
}})
|
|
981
|
+
|
|
982
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
983
|
+
LINKID_DataType = "Image",
|
|
984
|
+
LINK_Main = 1,
|
|
985
|
+
}})
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
function Process(req)
|
|
989
|
+
local offset = InOffset:GetValue(req).Value
|
|
990
|
+
-- GetSource fetches the input at a frame other than the current request time.
|
|
991
|
+
local img = InImage:GetSource(req.Time + offset)
|
|
992
|
+
if img == nil then
|
|
993
|
+
-- No frame available at that time (e.g. start/end of timeline);
|
|
994
|
+
-- fall back to current frame.
|
|
995
|
+
img = InImage:GetValue(req)
|
|
996
|
+
end
|
|
997
|
+
OutImage:Set(req, img)
|
|
998
|
+
end
|
|
999
|
+
'''
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def channel_op(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
1003
|
+
"""Channel operation Fuse with full aux-channel exposure.
|
|
1004
|
+
|
|
1005
|
+
options:
|
|
1006
|
+
operation: 'Copy' | 'Add' | 'Subtract' | 'Multiply' | 'Divide' |
|
|
1007
|
+
'Max' | 'Min' | 'Invert' | 'Difference' | 'SignedAdd' |
|
|
1008
|
+
'Threshold' (default 'Copy')
|
|
1009
|
+
rgba_only: bool — if true, only RGBA controls are exposed
|
|
1010
|
+
(default true). Set false to also expose Z/Object/Position
|
|
1011
|
+
channel mappings.
|
|
1012
|
+
|
|
1013
|
+
Wraps Image:ChannelOpOf (SDK pp. 71-72) which gives fine-grained control
|
|
1014
|
+
over how individual channels combine across two images. Real workflow:
|
|
1015
|
+
multipass / VFX deep-comp where you need to remap aux channels.
|
|
1016
|
+
"""
|
|
1017
|
+
options = options or {}
|
|
1018
|
+
operation = options.get("operation", "Copy")
|
|
1019
|
+
valid_ops = ("Copy", "Add", "Subtract", "Multiply", "Divide", "Max", "Min",
|
|
1020
|
+
"Invert", "Difference", "SignedAdd", "Threshold")
|
|
1021
|
+
if operation not in valid_ops:
|
|
1022
|
+
raise ValueError(f"Invalid channel_op operation '{operation}'. "
|
|
1023
|
+
f"Valid: {list(valid_ops)}")
|
|
1024
|
+
rgba_only = bool(options.get("rgba_only", True))
|
|
1025
|
+
|
|
1026
|
+
aux_channels = ""
|
|
1027
|
+
aux_doc = ""
|
|
1028
|
+
if not rgba_only:
|
|
1029
|
+
aux_doc = (
|
|
1030
|
+
"\n-- Pixel struct also exposes Z, U/V/W, Coverage, ObjectID,\n"
|
|
1031
|
+
"-- MaterialID, NX/NY/NZ, BgR/G/B/A, VectorX/Y, BackVectorX/Y,\n"
|
|
1032
|
+
"-- DisparityX/Y, PositionX/Y/Z. Reference any of these in the\n"
|
|
1033
|
+
"-- options table values (e.g. R = 'Fg.Z' to map Z buffer into Red).\n"
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
return header(name, "channel_op", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
1037
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
1038
|
+
REGS_OpIconString = "{name[:4]}",
|
|
1039
|
+
REGS_OpDescription = "Channel operation ({operation})",
|
|
1040
|
+
}})
|
|
1041
|
+
{aux_doc}
|
|
1042
|
+
function Create()
|
|
1043
|
+
InOperation = self:AddInput("Operation", "Operation", {{
|
|
1044
|
+
LINKID_DataType = "Number",
|
|
1045
|
+
INPID_InputControl = "ComboControl",
|
|
1046
|
+
INP_Integer = true,
|
|
1047
|
+
INP_Default = 0,
|
|
1048
|
+
{{ CCS_AddString = "Copy" }},
|
|
1049
|
+
{{ CCS_AddString = "Add" }},
|
|
1050
|
+
{{ CCS_AddString = "Subtract" }},
|
|
1051
|
+
{{ CCS_AddString = "Multiply" }},
|
|
1052
|
+
{{ CCS_AddString = "Divide" }},
|
|
1053
|
+
{{ CCS_AddString = "Max" }},
|
|
1054
|
+
{{ CCS_AddString = "Min" }},
|
|
1055
|
+
{{ CCS_AddString = "Invert" }},
|
|
1056
|
+
{{ CCS_AddString = "Difference" }},
|
|
1057
|
+
{{ CCS_AddString = "SignedAdd" }},
|
|
1058
|
+
{{ CCS_AddString = "Threshold" }},
|
|
1059
|
+
}})
|
|
1060
|
+
|
|
1061
|
+
InBackground = self:AddInput("Background", "Background", {{
|
|
1062
|
+
LINKID_DataType = "Image",
|
|
1063
|
+
LINK_Main = 1,
|
|
1064
|
+
}})
|
|
1065
|
+
|
|
1066
|
+
InForeground = self:AddInput("Foreground", "Foreground", {{
|
|
1067
|
+
LINKID_DataType = "Image",
|
|
1068
|
+
LINK_Main = 2,
|
|
1069
|
+
INP_Required = false,
|
|
1070
|
+
}})
|
|
1071
|
+
|
|
1072
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
1073
|
+
LINKID_DataType = "Image",
|
|
1074
|
+
LINK_Main = 1,
|
|
1075
|
+
}})
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
local op_table = {{
|
|
1079
|
+
[0] = "Copy", "Add", "Subtract", "Multiply", "Divide",
|
|
1080
|
+
"Max", "Min", "Invert", "Difference", "SignedAdd", "Threshold",
|
|
1081
|
+
}}
|
|
1082
|
+
|
|
1083
|
+
function Process(req)
|
|
1084
|
+
local bg = InBackground:GetValue(req)
|
|
1085
|
+
local fg = InForeground:GetValue(req)
|
|
1086
|
+
if fg == nil then
|
|
1087
|
+
fg = bg
|
|
1088
|
+
end
|
|
1089
|
+
local op = op_table[InOperation:GetValue(req).Value]
|
|
1090
|
+
|
|
1091
|
+
-- Default mapping: copy each channel from foreground.
|
|
1092
|
+
local out = bg:ChannelOpOf(op, fg, {{
|
|
1093
|
+
R = "Fg.R", G = "Fg.G", B = "Fg.B", A = "Fg.A",
|
|
1094
|
+
}})
|
|
1095
|
+
|
|
1096
|
+
OutImage:Set(req, out)
|
|
1097
|
+
end
|
|
1098
|
+
'''
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def spatial_warp(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
1102
|
+
"""Per-pixel spatial warp Fuse. Each output pixel samples a different
|
|
1103
|
+
source location.
|
|
1104
|
+
|
|
1105
|
+
options:
|
|
1106
|
+
edge_mode: 'wrap' | 'clamp' | 'black' (default 'wrap')
|
|
1107
|
+
Selects SamplePixelW / SamplePixelD / SamplePixelB.
|
|
1108
|
+
warp: 'sine' | 'scatter' | 'pinch' (default 'sine')
|
|
1109
|
+
|
|
1110
|
+
Reference implementation: SDK Example7_Sampling.fuse (pp. 37–38).
|
|
1111
|
+
"""
|
|
1112
|
+
options = options or {}
|
|
1113
|
+
edge_mode = options.get("edge_mode", "wrap")
|
|
1114
|
+
if edge_mode not in ("wrap", "clamp", "black"):
|
|
1115
|
+
raise ValueError(f"Invalid edge_mode '{edge_mode}'. "
|
|
1116
|
+
"Valid: wrap, clamp, black")
|
|
1117
|
+
warp = options.get("warp", "sine")
|
|
1118
|
+
if warp not in ("sine", "scatter", "pinch"):
|
|
1119
|
+
raise ValueError(f"Invalid warp '{warp}'. "
|
|
1120
|
+
"Valid: sine, scatter, pinch")
|
|
1121
|
+
sample_fn = {"wrap": "SamplePixelW", "clamp": "SamplePixelD",
|
|
1122
|
+
"black": "SamplePixelB"}[edge_mode]
|
|
1123
|
+
|
|
1124
|
+
if warp == "sine":
|
|
1125
|
+
warp_body = ''' local xt = x - amp * math.sin(y * freq + phase)
|
|
1126
|
+
local yt = y - amp * math.sin(x * freq + phase)
|
|
1127
|
+
img:{sample_fn}(xt, yt, sp)'''
|
|
1128
|
+
elif warp == "scatter":
|
|
1129
|
+
warp_body = ''' img:GetPixel(x, y, sp)
|
|
1130
|
+
local xt = x - amp * 5 * (sp.R - 0.5)
|
|
1131
|
+
local yt = y - amp * 5 * (sp.B - 0.5)
|
|
1132
|
+
img:{sample_fn}(xt, yt, sp)'''
|
|
1133
|
+
else: # pinch
|
|
1134
|
+
warp_body = ''' local dx = x - cx
|
|
1135
|
+
local dy = y - cy
|
|
1136
|
+
local r = math.sqrt(dx*dx + dy*dy)
|
|
1137
|
+
local k = 1 + amp * math.exp(-r / (freq * 100))
|
|
1138
|
+
img:{sample_fn}(cx + dx / k, cy + dy / k, sp)'''
|
|
1139
|
+
warp_body = warp_body.replace("{sample_fn}", sample_fn)
|
|
1140
|
+
|
|
1141
|
+
return header(name, "spatial_warp", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
1142
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
1143
|
+
REGS_OpIconString = "{name[:4]}",
|
|
1144
|
+
REGS_OpDescription = "Spatial warp ({warp}, {edge_mode})",
|
|
1145
|
+
}})
|
|
1146
|
+
|
|
1147
|
+
function Create()
|
|
1148
|
+
InAmp = self:AddInput("Amplitude", "Amplitude", {{
|
|
1149
|
+
LINKID_DataType = "Number",
|
|
1150
|
+
INPID_InputControl = "SliderControl",
|
|
1151
|
+
INP_Default = 5.0,
|
|
1152
|
+
INP_MinScale = 0.0,
|
|
1153
|
+
INP_MaxScale = 50.0,
|
|
1154
|
+
}})
|
|
1155
|
+
|
|
1156
|
+
InFreq = self:AddInput("Frequency", "Frequency", {{
|
|
1157
|
+
LINKID_DataType = "Number",
|
|
1158
|
+
INPID_InputControl = "SliderControl",
|
|
1159
|
+
INP_Default = 0.05,
|
|
1160
|
+
INP_MinScale = 0.0,
|
|
1161
|
+
INP_MaxScale = 1.0,
|
|
1162
|
+
}})
|
|
1163
|
+
|
|
1164
|
+
InPhase = self:AddInput("Phase", "Phase", {{
|
|
1165
|
+
LINKID_DataType = "Number",
|
|
1166
|
+
INPID_InputControl = "ScrewControl",
|
|
1167
|
+
INP_Default = 0.0,
|
|
1168
|
+
INP_MinScale = 0.0,
|
|
1169
|
+
INP_MaxScale = 6.283,
|
|
1170
|
+
}})
|
|
1171
|
+
|
|
1172
|
+
InImage = self:AddInput("Input", "Input", {{
|
|
1173
|
+
LINKID_DataType = "Image",
|
|
1174
|
+
LINK_Main = 1,
|
|
1175
|
+
}})
|
|
1176
|
+
|
|
1177
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
1178
|
+
LINKID_DataType = "Image",
|
|
1179
|
+
LINK_Main = 1,
|
|
1180
|
+
}})
|
|
1181
|
+
end
|
|
1182
|
+
|
|
1183
|
+
function Process(req)
|
|
1184
|
+
local img = InImage:GetValue(req)
|
|
1185
|
+
local amp = InAmp:GetValue(req).Value
|
|
1186
|
+
local freq = InFreq:GetValue(req).Value
|
|
1187
|
+
local phase = InPhase:GetValue(req).Value
|
|
1188
|
+
|
|
1189
|
+
local out = Image({{ IMG_Like = img }})
|
|
1190
|
+
local sp = Pixel()
|
|
1191
|
+
local cx = img.Width / 2
|
|
1192
|
+
local cy = img.Height / 2
|
|
1193
|
+
|
|
1194
|
+
for y = 0, img.Height - 1 do
|
|
1195
|
+
for x = 0, img.Width - 1 do
|
|
1196
|
+
{warp_body}
|
|
1197
|
+
out:SetPixel(x, y, sp)
|
|
1198
|
+
end
|
|
1199
|
+
end
|
|
1200
|
+
|
|
1201
|
+
OutImage:Set(req, out)
|
|
1202
|
+
end
|
|
1203
|
+
'''
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
def builtin_blur(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
1207
|
+
"""Blur Fuse with full BLUR_Type / per-channel-mask exposure.
|
|
1208
|
+
|
|
1209
|
+
options:
|
|
1210
|
+
default_type: 'Box' | 'Soften' | 'Bartlett' | 'Sharpen' | 'Gaussian'
|
|
1211
|
+
| 'Hilight' | 'Blend' | 'Solarise' (default 'Gaussian')
|
|
1212
|
+
|
|
1213
|
+
Wraps Image:Blur with all 8 documented BLUR_Type values + per-channel
|
|
1214
|
+
enable/disable. Stock Blur tool is great but hides MultiBox/Solarise/
|
|
1215
|
+
FastGaussian. SDK pp. 69-70.
|
|
1216
|
+
"""
|
|
1217
|
+
options = options or {}
|
|
1218
|
+
valid_types = ("Box", "Soften", "Bartlett", "Sharpen", "Gaussian",
|
|
1219
|
+
"Hilight", "Blend", "Solarise")
|
|
1220
|
+
default_type = options.get("default_type", "Gaussian")
|
|
1221
|
+
if default_type not in valid_types:
|
|
1222
|
+
raise ValueError(f"Invalid default_type '{default_type}'. "
|
|
1223
|
+
f"Valid: {list(valid_types)}")
|
|
1224
|
+
default_idx = valid_types.index(default_type)
|
|
1225
|
+
|
|
1226
|
+
return header(name, "builtin_blur", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
1227
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
1228
|
+
REGS_OpIconString = "{name[:4]}",
|
|
1229
|
+
REGS_OpDescription = "Configurable blur",
|
|
1230
|
+
}})
|
|
1231
|
+
|
|
1232
|
+
function Create()
|
|
1233
|
+
InType = self:AddInput("Filter", "Filter", {{
|
|
1234
|
+
LINKID_DataType = "Number",
|
|
1235
|
+
INPID_InputControl = "ComboControl",
|
|
1236
|
+
INP_Integer = true,
|
|
1237
|
+
INP_Default = {default_idx},
|
|
1238
|
+
{{ CCS_AddString = "Box" }},
|
|
1239
|
+
{{ CCS_AddString = "Soften" }},
|
|
1240
|
+
{{ CCS_AddString = "Bartlett" }},
|
|
1241
|
+
{{ CCS_AddString = "Sharpen" }},
|
|
1242
|
+
{{ CCS_AddString = "Gaussian" }},
|
|
1243
|
+
{{ CCS_AddString = "Hilight" }},
|
|
1244
|
+
{{ CCS_AddString = "Blend" }},
|
|
1245
|
+
{{ CCS_AddString = "Solarise" }},
|
|
1246
|
+
}})
|
|
1247
|
+
|
|
1248
|
+
InSize = self:AddInput("Size", "Size", {{
|
|
1249
|
+
LINKID_DataType = "Number",
|
|
1250
|
+
INPID_InputControl = "SliderControl",
|
|
1251
|
+
INP_Default = 5.0,
|
|
1252
|
+
INP_MinScale = 0.0,
|
|
1253
|
+
INP_MaxScale = 100.0,
|
|
1254
|
+
}})
|
|
1255
|
+
|
|
1256
|
+
InRed = self:AddInput("Red", "Red", {{
|
|
1257
|
+
LINKID_DataType = "Number", INPID_InputControl = "CheckboxControl",
|
|
1258
|
+
INP_Integer = true, INP_Default = 1.0, ICD_Width = 0.25,
|
|
1259
|
+
}})
|
|
1260
|
+
InGreen = self:AddInput("Green", "Green", {{
|
|
1261
|
+
LINKID_DataType = "Number", INPID_InputControl = "CheckboxControl",
|
|
1262
|
+
INP_Integer = true, INP_Default = 1.0, ICD_Width = 0.25,
|
|
1263
|
+
}})
|
|
1264
|
+
InBlue = self:AddInput("Blue", "Blue", {{
|
|
1265
|
+
LINKID_DataType = "Number", INPID_InputControl = "CheckboxControl",
|
|
1266
|
+
INP_Integer = true, INP_Default = 1.0, ICD_Width = 0.25,
|
|
1267
|
+
}})
|
|
1268
|
+
InAlpha = self:AddInput("Alpha", "Alpha", {{
|
|
1269
|
+
LINKID_DataType = "Number", INPID_InputControl = "CheckboxControl",
|
|
1270
|
+
INP_Integer = true, INP_Default = 0.0, ICD_Width = 0.25,
|
|
1271
|
+
}})
|
|
1272
|
+
|
|
1273
|
+
InImage = self:AddInput("Input", "Input", {{
|
|
1274
|
+
LINKID_DataType = "Image",
|
|
1275
|
+
LINK_Main = 1,
|
|
1276
|
+
}})
|
|
1277
|
+
|
|
1278
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
1279
|
+
LINKID_DataType = "Image",
|
|
1280
|
+
LINK_Main = 1,
|
|
1281
|
+
}})
|
|
1282
|
+
end
|
|
1283
|
+
|
|
1284
|
+
local type_table = {{
|
|
1285
|
+
[0] = "Box", "Soften", "Bartlett", "Sharpen", "Gaussian",
|
|
1286
|
+
"Hilight", "Blend", "Solarise",
|
|
1287
|
+
}}
|
|
1288
|
+
|
|
1289
|
+
function Process(req)
|
|
1290
|
+
local img = InImage:GetValue(req)
|
|
1291
|
+
local size = InSize:GetValue(req).Value
|
|
1292
|
+
local out = Image({{ IMG_Like = img }})
|
|
1293
|
+
|
|
1294
|
+
img:Blur(out, {{
|
|
1295
|
+
BLUR_Type = type_table[InType:GetValue(req).Value],
|
|
1296
|
+
BLUR_Red = InRed:GetValue(req).Value > 0.5,
|
|
1297
|
+
BLUR_Green = InGreen:GetValue(req).Value > 0.5,
|
|
1298
|
+
BLUR_Blue = InBlue:GetValue(req).Value > 0.5,
|
|
1299
|
+
BLUR_Alpha = InAlpha:GetValue(req).Value > 0.5,
|
|
1300
|
+
BLUR_XSize = size / img.OriginalWidth,
|
|
1301
|
+
BLUR_YSize = size / img.OriginalWidth,
|
|
1302
|
+
}})
|
|
1303
|
+
|
|
1304
|
+
OutImage:Set(req, out)
|
|
1305
|
+
end
|
|
1306
|
+
'''
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
def builtin_resize(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
1310
|
+
"""Resize Fuse with all 12 documented filter methods + Lanczos windowing.
|
|
1311
|
+
|
|
1312
|
+
Stock Resize tool exposes a subset; this gives full control including
|
|
1313
|
+
Lanczos+window combinations for production scaling. SDK p. 91.
|
|
1314
|
+
"""
|
|
1315
|
+
options = options or {}
|
|
1316
|
+
return header(name, "builtin_resize", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
1317
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
1318
|
+
REGS_OpIconString = "{name[:4]}",
|
|
1319
|
+
REGS_OpDescription = "Configurable resize",
|
|
1320
|
+
}})
|
|
1321
|
+
|
|
1322
|
+
function Create()
|
|
1323
|
+
InFilter = self:AddInput("Filter", "Filter", {{
|
|
1324
|
+
LINKID_DataType = "Number",
|
|
1325
|
+
INPID_InputControl = "ComboControl",
|
|
1326
|
+
INP_Integer = true,
|
|
1327
|
+
INP_Default = 11, -- Lanczos
|
|
1328
|
+
{{ CCS_AddString = "TopLeft" }},
|
|
1329
|
+
{{ CCS_AddString = "Nearest" }},
|
|
1330
|
+
{{ CCS_AddString = "Box" }},
|
|
1331
|
+
{{ CCS_AddString = "Linear" }},
|
|
1332
|
+
{{ CCS_AddString = "BiLinear" }},
|
|
1333
|
+
{{ CCS_AddString = "Quadratic" }},
|
|
1334
|
+
{{ CCS_AddString = "BiCubic" }},
|
|
1335
|
+
{{ CCS_AddString = "Cubic" }},
|
|
1336
|
+
{{ CCS_AddString = "BSpline" }},
|
|
1337
|
+
{{ CCS_AddString = "CatmullRom" }},
|
|
1338
|
+
{{ CCS_AddString = "Gaussian" }},
|
|
1339
|
+
{{ CCS_AddString = "Mitchell" }},
|
|
1340
|
+
{{ CCS_AddString = "Lanczos" }},
|
|
1341
|
+
{{ CCS_AddString = "Sinc" }},
|
|
1342
|
+
{{ CCS_AddString = "Bessel" }},
|
|
1343
|
+
}})
|
|
1344
|
+
|
|
1345
|
+
InWindow = self:AddInput("Window", "Window", {{
|
|
1346
|
+
LINKID_DataType = "Number",
|
|
1347
|
+
INPID_InputControl = "ComboControl",
|
|
1348
|
+
INP_Integer = true,
|
|
1349
|
+
INP_Default = 0,
|
|
1350
|
+
{{ CCS_AddString = "Hanning" }},
|
|
1351
|
+
{{ CCS_AddString = "Hamming" }},
|
|
1352
|
+
{{ CCS_AddString = "Blackman" }},
|
|
1353
|
+
{{ CCS_AddString = "Kaiser" }},
|
|
1354
|
+
}})
|
|
1355
|
+
|
|
1356
|
+
InWidth = self:AddInput("Width", "Width", {{
|
|
1357
|
+
LINKID_DataType = "Number",
|
|
1358
|
+
INPID_InputControl = "SliderControl",
|
|
1359
|
+
INP_Integer = true,
|
|
1360
|
+
INP_Default = 1920,
|
|
1361
|
+
INP_MinScale = 1, INP_MaxScale = 7680,
|
|
1362
|
+
}})
|
|
1363
|
+
|
|
1364
|
+
InHeight = self:AddInput("Height", "Height", {{
|
|
1365
|
+
LINKID_DataType = "Number",
|
|
1366
|
+
INPID_InputControl = "SliderControl",
|
|
1367
|
+
INP_Integer = true,
|
|
1368
|
+
INP_Default = 1080,
|
|
1369
|
+
INP_MinScale = 1, INP_MaxScale = 4320,
|
|
1370
|
+
}})
|
|
1371
|
+
|
|
1372
|
+
InImage = self:AddInput("Input", "Input", {{
|
|
1373
|
+
LINKID_DataType = "Image",
|
|
1374
|
+
LINK_Main = 1,
|
|
1375
|
+
}})
|
|
1376
|
+
|
|
1377
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
1378
|
+
LINKID_DataType = "Image",
|
|
1379
|
+
LINK_Main = 1,
|
|
1380
|
+
}})
|
|
1381
|
+
end
|
|
1382
|
+
|
|
1383
|
+
local filter_table = {{
|
|
1384
|
+
[0] = "TopLeft", "Nearest", "Box", "Linear", "BiLinear",
|
|
1385
|
+
"Quadratic", "BiCubic", "Cubic", "BSpline", "CatmullRom",
|
|
1386
|
+
"Gaussian", "Mitchell", "Lanczos", "Sinc", "Bessel",
|
|
1387
|
+
}}
|
|
1388
|
+
local window_table = {{ [0] = "Hanning", "Hamming", "Blackman", "Kaiser" }}
|
|
1389
|
+
|
|
1390
|
+
function Process(req)
|
|
1391
|
+
local img = InImage:GetValue(req)
|
|
1392
|
+
local filter = filter_table[InFilter:GetValue(req).Value]
|
|
1393
|
+
local w = InWidth:GetValue(req).Value
|
|
1394
|
+
local h = InHeight:GetValue(req).Value
|
|
1395
|
+
|
|
1396
|
+
local out = Image({{ IMG_Like = img, IMG_Width = w, IMG_Height = h }})
|
|
1397
|
+
img:Resize(out, {{
|
|
1398
|
+
RSZ_Filter = filter,
|
|
1399
|
+
RSZ_Window = window_table[InWindow:GetValue(req).Value],
|
|
1400
|
+
RSZ_Width = w,
|
|
1401
|
+
RSZ_Height = h,
|
|
1402
|
+
}})
|
|
1403
|
+
|
|
1404
|
+
OutImage:Set(req, out)
|
|
1405
|
+
end
|
|
1406
|
+
'''
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
def controls_demo(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
1410
|
+
"""Reference Fuse demonstrating ALL UI control types in nested groups.
|
|
1411
|
+
|
|
1412
|
+
Educational template — when an LLM (or human) wants a worked example of
|
|
1413
|
+
every available UI control, generate this and read the source. Mirrors
|
|
1414
|
+
SDK Example2_UIControls.fuse.
|
|
1415
|
+
"""
|
|
1416
|
+
return header(name, "controls_demo", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
1417
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
1418
|
+
REGS_OpIconString = "{name[:4]}",
|
|
1419
|
+
REGS_OpDescription = "Reference: every UI control type",
|
|
1420
|
+
}})
|
|
1421
|
+
|
|
1422
|
+
function Create()
|
|
1423
|
+
self:AddControlPage("Controls")
|
|
1424
|
+
|
|
1425
|
+
self:BeginControlNest("Sliders", "Sliders", true, {{}})
|
|
1426
|
+
InSliderFloat = self:AddInput("Slider Float", "SliderFloat", {{
|
|
1427
|
+
LINKID_DataType = "Number", INPID_InputControl = "SliderControl",
|
|
1428
|
+
INP_Default = 0.5, INP_MinScale = 0.0, INP_MaxScale = 1.0,
|
|
1429
|
+
}})
|
|
1430
|
+
InSliderInt = self:AddInput("Slider Int", "SliderInt", {{
|
|
1431
|
+
LINKID_DataType = "Number", INPID_InputControl = "SliderControl",
|
|
1432
|
+
INP_Integer = true, INP_Default = 1, INP_MinScale = 0, INP_MaxScale = 10,
|
|
1433
|
+
}})
|
|
1434
|
+
InScrew = self:AddInput("Thumbwheel", "Thumbwheel", {{
|
|
1435
|
+
LINKID_DataType = "Number", INPID_InputControl = "ScrewControl",
|
|
1436
|
+
INP_Default = 0,
|
|
1437
|
+
}})
|
|
1438
|
+
InRangeLow = self:AddInput("Range Low", "RangeLow", {{
|
|
1439
|
+
LINKID_DataType = "Number", INPID_InputControl = "RangeControl",
|
|
1440
|
+
IC_ControlGroup = 1, IC_ControlID = 0, INP_Default = 0.1,
|
|
1441
|
+
}})
|
|
1442
|
+
InRangeHigh = self:AddInput("Range High", "RangeHigh", {{
|
|
1443
|
+
LINKID_DataType = "Number", INPID_InputControl = "RangeControl",
|
|
1444
|
+
IC_ControlGroup = 1, IC_ControlID = 1, INP_Default = 0.9,
|
|
1445
|
+
}})
|
|
1446
|
+
self:EndControlNest()
|
|
1447
|
+
|
|
1448
|
+
self:BeginControlNest("Toggles & Buttons", "Toggles", false, {{}})
|
|
1449
|
+
InCheck = self:AddInput("Checkbox", "Checkbox", {{
|
|
1450
|
+
LINKID_DataType = "Number", INPID_InputControl = "CheckboxControl",
|
|
1451
|
+
INP_Integer = true, INP_Default = 1.0,
|
|
1452
|
+
}})
|
|
1453
|
+
InCombo = self:AddInput("Combo", "Combo", {{
|
|
1454
|
+
LINKID_DataType = "Number", INPID_InputControl = "ComboControl",
|
|
1455
|
+
INP_Integer = true, INP_Default = 0,
|
|
1456
|
+
{{ CCS_AddString = "Option A" }},
|
|
1457
|
+
{{ CCS_AddString = "Option B" }},
|
|
1458
|
+
{{ CCS_AddString = "Option C" }},
|
|
1459
|
+
}})
|
|
1460
|
+
InMulti = self:AddInput("Multi-button", "Multi", {{
|
|
1461
|
+
LINKID_DataType = "Number", INPID_InputControl = "MultiButtonControl",
|
|
1462
|
+
INP_Default = 0, INP_Integer = true,
|
|
1463
|
+
{{ MBTNC_AddButton = "X", MBTNCD_ButtonWidth = 0.33 }},
|
|
1464
|
+
{{ MBTNC_AddButton = "Y", MBTNCD_ButtonWidth = 0.33 }},
|
|
1465
|
+
{{ MBTNC_AddButton = "Z", MBTNCD_ButtonWidth = 0.33 }},
|
|
1466
|
+
}})
|
|
1467
|
+
InButton = self:AddInput("Button", "Button", {{
|
|
1468
|
+
LINKID_DataType = "Text", INPID_InputControl = "ButtonControl",
|
|
1469
|
+
INP_DoNotifyChanged = true, INP_External = false,
|
|
1470
|
+
}})
|
|
1471
|
+
self:EndControlNest()
|
|
1472
|
+
|
|
1473
|
+
self:BeginControlNest("Color & Gradient", "Colors", false, {{}})
|
|
1474
|
+
InGrad = self:AddInput("Gradient", "Gradient", {{
|
|
1475
|
+
LINKID_DataType = "Gradient", INPID_InputControl = "GradientControl",
|
|
1476
|
+
INP_DelayDefault = true,
|
|
1477
|
+
}})
|
|
1478
|
+
InR = self:AddInput("Red", "Red", {{
|
|
1479
|
+
LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
|
|
1480
|
+
IC_ControlGroup = 2, IC_ControlID = 0, INP_Default = 1.0,
|
|
1481
|
+
}})
|
|
1482
|
+
InG = self:AddInput("Green", "Green", {{
|
|
1483
|
+
LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
|
|
1484
|
+
IC_ControlGroup = 2, IC_ControlID = 1, INP_Default = 1.0,
|
|
1485
|
+
}})
|
|
1486
|
+
InB = self:AddInput("Blue", "Blue", {{
|
|
1487
|
+
LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
|
|
1488
|
+
IC_ControlGroup = 2, IC_ControlID = 2, INP_Default = 1.0,
|
|
1489
|
+
}})
|
|
1490
|
+
self:EndControlNest()
|
|
1491
|
+
|
|
1492
|
+
self:BeginControlNest("Text & File", "Text", false, {{}})
|
|
1493
|
+
InText = self:AddInput("Text", "Text", {{
|
|
1494
|
+
LINKID_DataType = "Text", INPID_InputControl = "TextEditControl",
|
|
1495
|
+
TEC_Lines = 2,
|
|
1496
|
+
}})
|
|
1497
|
+
InFile = self:AddInput("File", "File", {{
|
|
1498
|
+
LINKID_DataType = "Text", INPID_InputControl = "FileControl",
|
|
1499
|
+
FC_ClipBrowse = true,
|
|
1500
|
+
}})
|
|
1501
|
+
self:EndControlNest()
|
|
1502
|
+
|
|
1503
|
+
self:BeginControlNest("Onscreen", "Onscreen", false, {{}})
|
|
1504
|
+
InCenter = self:AddInput("Center", "Center", {{
|
|
1505
|
+
LINKID_DataType = "Point", INPID_InputControl = "OffsetControl",
|
|
1506
|
+
INPID_PreviewControl = "CrosshairControl",
|
|
1507
|
+
INP_DefaultX = 0.5, INP_DefaultY = 0.5,
|
|
1508
|
+
}})
|
|
1509
|
+
self:EndControlNest()
|
|
1510
|
+
|
|
1511
|
+
InImage = self:AddInput("Input", "Input", {{
|
|
1512
|
+
LINKID_DataType = "Image", LINK_Main = 1, INP_Required = false,
|
|
1513
|
+
}})
|
|
1514
|
+
|
|
1515
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
1516
|
+
LINKID_DataType = "Image", LINK_Main = 1,
|
|
1517
|
+
}})
|
|
1518
|
+
end
|
|
1519
|
+
|
|
1520
|
+
function NotifyChanged(inp, param, time)
|
|
1521
|
+
if inp == InButton and param.Value == 1 then
|
|
1522
|
+
print("Button pressed at frame " .. tostring(time))
|
|
1523
|
+
end
|
|
1524
|
+
end
|
|
1525
|
+
|
|
1526
|
+
function Process(req)
|
|
1527
|
+
local img = InImage:GetValue(req)
|
|
1528
|
+
if img == nil then
|
|
1529
|
+
-- Stub output when no image connected. Caller should normally connect one.
|
|
1530
|
+
local stub = Image({{
|
|
1531
|
+
IMG_Document = self.Comp,
|
|
1532
|
+
{{IMG_Channel = "Red"}}, {{IMG_Channel = "Green"}},
|
|
1533
|
+
{{IMG_Channel = "Blue"}}, {{IMG_Channel = "Alpha"}},
|
|
1534
|
+
IMG_Width = 64, IMG_Height = 64,
|
|
1535
|
+
IMG_XScale = 1.0, IMG_YScale = 1.0,
|
|
1536
|
+
}})
|
|
1537
|
+
stub:Fill(Pixel({{R = 0, G = 0, B = 0, A = 0}}))
|
|
1538
|
+
OutImage:Set(req, stub)
|
|
1539
|
+
return
|
|
1540
|
+
end
|
|
1541
|
+
OutImage:Set(req, img)
|
|
1542
|
+
end
|
|
1543
|
+
'''
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
def shape_generator(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
1547
|
+
"""Vector shape Fuse: draws a parametric shape (circle/rect/star) as a
|
|
1548
|
+
filled image overlay.
|
|
1549
|
+
|
|
1550
|
+
options:
|
|
1551
|
+
shape: 'circle' | 'rect' | 'star' (default 'circle')
|
|
1552
|
+
|
|
1553
|
+
Demonstrates Shape + AddRectangle / MoveTo / LineTo / OutlineOfShape +
|
|
1554
|
+
ChannelStyle + ImageChannel + PutToImage. See SDK Example5_Shapes.
|
|
1555
|
+
"""
|
|
1556
|
+
options = options or {}
|
|
1557
|
+
shape_kind = options.get("shape", "circle")
|
|
1558
|
+
if shape_kind not in ("circle", "rect", "star"):
|
|
1559
|
+
raise ValueError(f"Invalid shape '{shape_kind}'. "
|
|
1560
|
+
"Valid: circle, rect, star")
|
|
1561
|
+
|
|
1562
|
+
if shape_kind == "circle":
|
|
1563
|
+
# Approximate circle using bezier segments.
|
|
1564
|
+
shape_body = ''' -- Circle approximated with 4 cubic beziers (k = 0.5522847498).
|
|
1565
|
+
local k = 0.5522847498 * radius
|
|
1566
|
+
sh:MoveTo(radius, 0)
|
|
1567
|
+
sh:BezierTo(0, radius, k, 0, radius, k)
|
|
1568
|
+
sh:BezierTo(-radius, 0, 0, radius, -k, 0)
|
|
1569
|
+
sh:BezierTo(0, -radius, -k, 0, -radius, -k)
|
|
1570
|
+
sh:BezierTo(radius, 0, 0, -radius, k, 0)
|
|
1571
|
+
sh:Close()'''
|
|
1572
|
+
elif shape_kind == "rect":
|
|
1573
|
+
shape_body = ''' sh:AddRectangle(-radius, -radius, radius, radius, 0.0, 8)'''
|
|
1574
|
+
else: # star
|
|
1575
|
+
shape_body = ''' -- 5-point star.
|
|
1576
|
+
local n = 5
|
|
1577
|
+
for i = 0, 2 * n - 1 do
|
|
1578
|
+
local a = (i / (2 * n)) * 2 * math.pi - math.pi / 2
|
|
1579
|
+
local r = (i % 2 == 0) and radius or radius * 0.4
|
|
1580
|
+
local x, y = math.cos(a) * r, math.sin(a) * r
|
|
1581
|
+
if i == 0 then sh:MoveTo(x, y) else sh:LineTo(x, y) end
|
|
1582
|
+
end
|
|
1583
|
+
sh:Close()'''
|
|
1584
|
+
|
|
1585
|
+
return header(name, "shape_generator", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
1586
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
1587
|
+
REGS_OpIconString = "{name[:4]}",
|
|
1588
|
+
REGS_OpDescription = "Vector shape ({shape_kind})",
|
|
1589
|
+
}})
|
|
1590
|
+
|
|
1591
|
+
function Create()
|
|
1592
|
+
InCenter = self:AddInput("Center", "Center", {{
|
|
1593
|
+
LINKID_DataType = "Point",
|
|
1594
|
+
INPID_InputControl = "OffsetControl",
|
|
1595
|
+
INPID_PreviewControl = "CrosshairControl",
|
|
1596
|
+
INP_DefaultX = 0.5, INP_DefaultY = 0.5,
|
|
1597
|
+
}})
|
|
1598
|
+
|
|
1599
|
+
InRadius = self:AddInput("Radius", "Radius", {{
|
|
1600
|
+
LINKID_DataType = "Number",
|
|
1601
|
+
INPID_InputControl = "SliderControl",
|
|
1602
|
+
INP_Default = 0.2,
|
|
1603
|
+
INP_MinScale = 0.0, INP_MaxScale = 1.0,
|
|
1604
|
+
}})
|
|
1605
|
+
|
|
1606
|
+
InR = self:AddInput("Red", "Red", {{
|
|
1607
|
+
LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
|
|
1608
|
+
IC_ControlGroup = 1, IC_ControlID = 0, INP_Default = 1.0,
|
|
1609
|
+
}})
|
|
1610
|
+
InG = self:AddInput("Green", "Green", {{
|
|
1611
|
+
LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
|
|
1612
|
+
IC_ControlGroup = 1, IC_ControlID = 1, INP_Default = 1.0,
|
|
1613
|
+
}})
|
|
1614
|
+
InB = self:AddInput("Blue", "Blue", {{
|
|
1615
|
+
LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
|
|
1616
|
+
IC_ControlGroup = 1, IC_ControlID = 2, INP_Default = 1.0,
|
|
1617
|
+
}})
|
|
1618
|
+
InA = self:AddInput("Alpha", "Alpha", {{
|
|
1619
|
+
LINKID_DataType = "Number", INPID_InputControl = "ColorControl",
|
|
1620
|
+
IC_ControlGroup = 1, IC_ControlID = 3, INP_Default = 1.0,
|
|
1621
|
+
}})
|
|
1622
|
+
|
|
1623
|
+
InImage = self:AddInput("Input", "Input", {{
|
|
1624
|
+
LINKID_DataType = "Image",
|
|
1625
|
+
LINK_Main = 1,
|
|
1626
|
+
INP_Required = false,
|
|
1627
|
+
}})
|
|
1628
|
+
|
|
1629
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
1630
|
+
LINKID_DataType = "Image",
|
|
1631
|
+
LINK_Main = 1,
|
|
1632
|
+
}})
|
|
1633
|
+
end
|
|
1634
|
+
|
|
1635
|
+
function Process(req)
|
|
1636
|
+
local img = InImage:GetValue(req)
|
|
1637
|
+
if img == nil then
|
|
1638
|
+
img = Image({{
|
|
1639
|
+
IMG_Document = self.Comp,
|
|
1640
|
+
{{ IMG_Channel = "Red" }}, {{ IMG_Channel = "Green" }},
|
|
1641
|
+
{{ IMG_Channel = "Blue" }}, {{ IMG_Channel = "Alpha" }},
|
|
1642
|
+
IMG_Width = 1920, IMG_Height = 1080,
|
|
1643
|
+
IMG_XScale = 1.0, IMG_YScale = 1.0,
|
|
1644
|
+
}})
|
|
1645
|
+
img:Fill(Pixel({{R = 0, G = 0, B = 0, A = 0}}))
|
|
1646
|
+
end
|
|
1647
|
+
|
|
1648
|
+
local out = img:CopyOf()
|
|
1649
|
+
local center = InCenter:GetValue(req)
|
|
1650
|
+
local radius = InRadius:GetValue(req).Value
|
|
1651
|
+
local r = InR:GetValue(req).Value
|
|
1652
|
+
local g = InG:GetValue(req).Value
|
|
1653
|
+
local b = InB:GetValue(req).Value
|
|
1654
|
+
local a = InA:GetValue(req).Value
|
|
1655
|
+
|
|
1656
|
+
local ic = ImageChannel(out, 8)
|
|
1657
|
+
local fs = FillStyle()
|
|
1658
|
+
local cs = ChannelStyle()
|
|
1659
|
+
cs.Color = Pixel({{R = r, G = g, B = b, A = a}})
|
|
1660
|
+
ic:SetStyleFill(fs)
|
|
1661
|
+
|
|
1662
|
+
local mat = Matrix4()
|
|
1663
|
+
-- Y-squash conversion so on-screen Center lands correctly.
|
|
1664
|
+
local cy = center.Y * (out.Height * out.YScale) / (out.Width * out.XScale)
|
|
1665
|
+
mat:Move(center.X, cy, 0)
|
|
1666
|
+
|
|
1667
|
+
local sh = Shape()
|
|
1668
|
+
{shape_body}
|
|
1669
|
+
sh = sh:TransformOfShape(mat)
|
|
1670
|
+
|
|
1671
|
+
ic:ShapeFill(sh)
|
|
1672
|
+
ic:PutToImage("CM_Merge", cs)
|
|
1673
|
+
|
|
1674
|
+
OutImage:Set(req, out)
|
|
1675
|
+
end
|
|
1676
|
+
'''
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
def notifychanged_demo(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
1680
|
+
"""Demonstrates dynamic UI: NotifyChanged hides/shows controls based on a
|
|
1681
|
+
checkbox state.
|
|
1682
|
+
|
|
1683
|
+
Reference pattern from SDK Example2_UIControls.fuse (pp. 23-24). When the
|
|
1684
|
+
`Show Details` checkbox is toggled, the detail sliders appear or hide.
|
|
1685
|
+
"""
|
|
1686
|
+
return header(name, "notifychanged_demo", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
1687
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
1688
|
+
REGS_OpIconString = "{name[:4]}",
|
|
1689
|
+
REGS_OpDescription = "NotifyChanged dynamic-UI demo",
|
|
1690
|
+
}})
|
|
1691
|
+
|
|
1692
|
+
function Create()
|
|
1693
|
+
InToggle = self:AddInput("Show Details", "ShowDetails", {{
|
|
1694
|
+
LINKID_DataType = "Number",
|
|
1695
|
+
INPID_InputControl = "CheckboxControl",
|
|
1696
|
+
INP_Integer = true,
|
|
1697
|
+
INP_Default = 0.0,
|
|
1698
|
+
INP_DoNotifyChanged = true,
|
|
1699
|
+
}})
|
|
1700
|
+
|
|
1701
|
+
InDetailA = self:AddInput("Detail A", "DetailA", {{
|
|
1702
|
+
LINKID_DataType = "Number",
|
|
1703
|
+
INPID_InputControl = "SliderControl",
|
|
1704
|
+
INP_Default = 0.5,
|
|
1705
|
+
IC_Visible = false,
|
|
1706
|
+
}})
|
|
1707
|
+
|
|
1708
|
+
InDetailB = self:AddInput("Detail B", "DetailB", {{
|
|
1709
|
+
LINKID_DataType = "Number",
|
|
1710
|
+
INPID_InputControl = "SliderControl",
|
|
1711
|
+
INP_Default = 0.5,
|
|
1712
|
+
IC_Visible = false,
|
|
1713
|
+
}})
|
|
1714
|
+
|
|
1715
|
+
InImage = self:AddInput("Input", "Input", {{
|
|
1716
|
+
LINKID_DataType = "Image",
|
|
1717
|
+
LINK_Main = 1,
|
|
1718
|
+
}})
|
|
1719
|
+
|
|
1720
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
1721
|
+
LINKID_DataType = "Image",
|
|
1722
|
+
LINK_Main = 1,
|
|
1723
|
+
}})
|
|
1724
|
+
end
|
|
1725
|
+
|
|
1726
|
+
function NotifyChanged(inp, param, time)
|
|
1727
|
+
if inp == InToggle then
|
|
1728
|
+
local visible = (param.Value > 0.5)
|
|
1729
|
+
InDetailA:SetAttrs({{ IC_Visible = visible }})
|
|
1730
|
+
InDetailB:SetAttrs({{ IC_Visible = visible }})
|
|
1731
|
+
end
|
|
1732
|
+
end
|
|
1733
|
+
|
|
1734
|
+
function Process(req)
|
|
1735
|
+
OutImage:Set(req, InImage:GetValue(req))
|
|
1736
|
+
end
|
|
1737
|
+
'''
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
def point_modifier(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
1741
|
+
"""Modifier producing a Point (X,Y) value over time. Use to drive
|
|
1742
|
+
on-screen positions like a Merge tool's Center input.
|
|
1743
|
+
|
|
1744
|
+
options:
|
|
1745
|
+
kind: 'orbit' | 'figure_eight' | 'spring' (default 'orbit')
|
|
1746
|
+
|
|
1747
|
+
Wires up via Inspector → right-click any Point input → Modify with → [name].
|
|
1748
|
+
Higher-risk than the Number modifier because CT_Modifier with Point output
|
|
1749
|
+
isn't covered by an SDK example.
|
|
1750
|
+
"""
|
|
1751
|
+
options = options or {}
|
|
1752
|
+
kind = options.get("kind", "orbit")
|
|
1753
|
+
if kind not in ("orbit", "figure_eight", "spring"):
|
|
1754
|
+
raise ValueError(f"Invalid point_modifier kind '{kind}'. "
|
|
1755
|
+
"Valid: orbit, figure_eight, spring")
|
|
1756
|
+
|
|
1757
|
+
if kind == "orbit":
|
|
1758
|
+
formula = ''' local x = cx + r * math.cos(2 * math.pi * t * freq + phase)
|
|
1759
|
+
local y = cy + r * math.sin(2 * math.pi * t * freq + phase)'''
|
|
1760
|
+
elif kind == "figure_eight":
|
|
1761
|
+
formula = ''' local angle = 2 * math.pi * t * freq + phase
|
|
1762
|
+
local x = cx + r * math.sin(angle)
|
|
1763
|
+
local y = cy + r * math.sin(angle * 2) / 2'''
|
|
1764
|
+
else: # spring
|
|
1765
|
+
formula = ''' local angle = 2 * math.pi * t * freq + phase
|
|
1766
|
+
local damping = math.exp(-t * 0.1)
|
|
1767
|
+
local x = cx + r * math.cos(angle) * damping
|
|
1768
|
+
local y = cy + r * math.sin(angle) * damping'''
|
|
1769
|
+
|
|
1770
|
+
return header(name, "point_modifier", "modifier") + f'''FuRegisterClass("{name}", CT_Modifier, {{
|
|
1771
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
1772
|
+
REGS_OpIconString = "{name[:4]}",
|
|
1773
|
+
REGS_OpDescription = "Point modifier ({kind})",
|
|
1774
|
+
}})
|
|
1775
|
+
|
|
1776
|
+
function Create()
|
|
1777
|
+
InCenterX = self:AddInput("Center X", "CenterX", {{
|
|
1778
|
+
LINKID_DataType = "Number",
|
|
1779
|
+
INPID_InputControl = "SliderControl",
|
|
1780
|
+
INP_Default = 0.5, INP_MinScale = 0.0, INP_MaxScale = 1.0,
|
|
1781
|
+
}})
|
|
1782
|
+
|
|
1783
|
+
InCenterY = self:AddInput("Center Y", "CenterY", {{
|
|
1784
|
+
LINKID_DataType = "Number",
|
|
1785
|
+
INPID_InputControl = "SliderControl",
|
|
1786
|
+
INP_Default = 0.5, INP_MinScale = 0.0, INP_MaxScale = 1.0,
|
|
1787
|
+
}})
|
|
1788
|
+
|
|
1789
|
+
InRadius = self:AddInput("Radius", "Radius", {{
|
|
1790
|
+
LINKID_DataType = "Number",
|
|
1791
|
+
INPID_InputControl = "SliderControl",
|
|
1792
|
+
INP_Default = 0.2, INP_MinScale = 0.0, INP_MaxScale = 1.0,
|
|
1793
|
+
}})
|
|
1794
|
+
|
|
1795
|
+
InFreq = self:AddInput("Frequency", "Frequency", {{
|
|
1796
|
+
LINKID_DataType = "Number",
|
|
1797
|
+
INPID_InputControl = "SliderControl",
|
|
1798
|
+
INP_Default = 0.05, INP_MinScale = 0.0, INP_MaxScale = 1.0,
|
|
1799
|
+
}})
|
|
1800
|
+
|
|
1801
|
+
InPhase = self:AddInput("Phase", "Phase", {{
|
|
1802
|
+
LINKID_DataType = "Number",
|
|
1803
|
+
INPID_InputControl = "ScrewControl",
|
|
1804
|
+
INP_Default = 0.0, INP_MinScale = 0.0, INP_MaxScale = 6.283,
|
|
1805
|
+
}})
|
|
1806
|
+
|
|
1807
|
+
OutPoint = self:AddOutput("Value", "Value", {{
|
|
1808
|
+
LINKID_DataType = "Point",
|
|
1809
|
+
LINK_Main = 1,
|
|
1810
|
+
}})
|
|
1811
|
+
end
|
|
1812
|
+
|
|
1813
|
+
function Process(req)
|
|
1814
|
+
local t = req.Time
|
|
1815
|
+
local cx = InCenterX:GetValue(req).Value
|
|
1816
|
+
local cy = InCenterY:GetValue(req).Value
|
|
1817
|
+
local r = InRadius:GetValue(req).Value
|
|
1818
|
+
local freq = InFreq:GetValue(req).Value
|
|
1819
|
+
local phase = InPhase:GetValue(req).Value
|
|
1820
|
+
|
|
1821
|
+
{formula}
|
|
1822
|
+
|
|
1823
|
+
OutPoint:Set(req, Point(x, y))
|
|
1824
|
+
end
|
|
1825
|
+
'''
|
|
1826
|
+
|
|
1827
|
+
|
|
1828
|
+
def variable_blur(name: str, options: Optional[Dict[str, Any]] = None) -> str:
|
|
1829
|
+
"""Variable-radius box blur using Summed Area Tables (SAT). Each pixel
|
|
1830
|
+
can have a different blur radius.
|
|
1831
|
+
|
|
1832
|
+
options:
|
|
1833
|
+
radius_source: 'slider' | 'red' | 'alpha' (default 'slider')
|
|
1834
|
+
'slider' = uniform radius from a slider input
|
|
1835
|
+
'red' = per-pixel radius driven by red channel
|
|
1836
|
+
'alpha' = per-pixel radius driven by alpha channel
|
|
1837
|
+
|
|
1838
|
+
Demonstrates UseSAT() + SampleAreaW() + RecycleSAT() pattern (SDK pp. 92-95,
|
|
1839
|
+
99). Full per-pixel implementation — slower than Image:Blur for fixed
|
|
1840
|
+
radii but the right tool for radius-mapped effects (e.g. depth-of-field
|
|
1841
|
+
from a Z buffer).
|
|
1842
|
+
"""
|
|
1843
|
+
options = options or {}
|
|
1844
|
+
radius_source = options.get("radius_source", "slider")
|
|
1845
|
+
if radius_source not in ("slider", "red", "alpha"):
|
|
1846
|
+
raise ValueError(f"Invalid radius_source '{radius_source}'. "
|
|
1847
|
+
"Valid: slider, red, alpha")
|
|
1848
|
+
|
|
1849
|
+
if radius_source == "slider":
|
|
1850
|
+
sample_radius = " local r = max_radius"
|
|
1851
|
+
elif radius_source == "red":
|
|
1852
|
+
sample_radius = ''' img:GetPixel(x, y, ref_pixel)
|
|
1853
|
+
local r = max_radius * ref_pixel.R'''
|
|
1854
|
+
else: # alpha
|
|
1855
|
+
sample_radius = ''' img:GetPixel(x, y, ref_pixel)
|
|
1856
|
+
local r = max_radius * ref_pixel.A'''
|
|
1857
|
+
|
|
1858
|
+
return header(name, "variable_blur", "tool") + f'''FuRegisterClass("{name}", CT_Tool, {{
|
|
1859
|
+
REGS_Category = "Fuses\\\\MCP",
|
|
1860
|
+
REGS_OpIconString = "{name[:4]}",
|
|
1861
|
+
REGS_OpDescription = "Variable-radius blur (SAT-based, {radius_source})",
|
|
1862
|
+
}})
|
|
1863
|
+
|
|
1864
|
+
function Create()
|
|
1865
|
+
InMaxRadius = self:AddInput("Max Radius", "MaxRadius", {{
|
|
1866
|
+
LINKID_DataType = "Number",
|
|
1867
|
+
INPID_InputControl = "SliderControl",
|
|
1868
|
+
INP_Integer = true,
|
|
1869
|
+
INP_Default = 16, INP_MinScale = 1, INP_MaxScale = 100,
|
|
1870
|
+
}})
|
|
1871
|
+
|
|
1872
|
+
InImage = self:AddInput("Input", "Input", {{
|
|
1873
|
+
LINKID_DataType = "Image",
|
|
1874
|
+
LINK_Main = 1,
|
|
1875
|
+
}})
|
|
1876
|
+
|
|
1877
|
+
OutImage = self:AddOutput("Output", "Output", {{
|
|
1878
|
+
LINKID_DataType = "Image",
|
|
1879
|
+
LINK_Main = 1,
|
|
1880
|
+
}})
|
|
1881
|
+
end
|
|
1882
|
+
|
|
1883
|
+
function Process(req)
|
|
1884
|
+
local img = InImage:GetValue(req)
|
|
1885
|
+
local max_radius = InMaxRadius:GetValue(req).Value
|
|
1886
|
+
local out = Image({{ IMG_Like = img }})
|
|
1887
|
+
|
|
1888
|
+
-- Build the Summed Area Table once. SampleArea functions need this.
|
|
1889
|
+
img:UseSAT()
|
|
1890
|
+
|
|
1891
|
+
local sp = Pixel()
|
|
1892
|
+
local ref_pixel = Pixel()
|
|
1893
|
+
|
|
1894
|
+
for y = 0, img.Height - 1 do
|
|
1895
|
+
if self.Status ~= "OK" then break end
|
|
1896
|
+
for x = 0, img.Width - 1 do
|
|
1897
|
+
{sample_radius}
|
|
1898
|
+
r = math.max(1, math.floor(r))
|
|
1899
|
+
-- SampleAreaW expects 4 corner points (rectangle vertices).
|
|
1900
|
+
img:SampleAreaW(x - r, y - r, x + r, y - r,
|
|
1901
|
+
x + r, y + r, x - r, y + r, sp)
|
|
1902
|
+
out:SetPixel(x, y, sp)
|
|
1903
|
+
end
|
|
1904
|
+
end
|
|
1905
|
+
|
|
1906
|
+
-- Release the SAT once we're done with it.
|
|
1907
|
+
img:RecycleSAT()
|
|
1908
|
+
|
|
1909
|
+
OutImage:Set(req, out)
|
|
1910
|
+
end
|
|
1911
|
+
'''
|
|
1912
|
+
|
|
1913
|
+
|
|
1914
|
+
# ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
1915
|
+
|
|
1916
|
+
def _slider(varname: str, label: str, default: float = 0.0,
|
|
1917
|
+
min_: float = -1.0, max_: float = 1.0) -> str:
|
|
1918
|
+
return (
|
|
1919
|
+
f' {varname} = self:AddInput("{label}", "{label}", {{\n'
|
|
1920
|
+
f' LINKID_DataType = "Number",\n'
|
|
1921
|
+
f' INPID_InputControl = "SliderControl",\n'
|
|
1922
|
+
f' INP_Default = {default},\n'
|
|
1923
|
+
f' INP_MinScale = {min_},\n'
|
|
1924
|
+
f' INP_MaxScale = {max_},\n'
|
|
1925
|
+
f' }})\n'
|
|
1926
|
+
)
|
|
1927
|
+
|
|
1928
|
+
|
|
1929
|
+
def _checkbox(varname: str, label: str, default: int = 0) -> str:
|
|
1930
|
+
return (
|
|
1931
|
+
f' {varname} = self:AddInput("{label}", "{label}", {{\n'
|
|
1932
|
+
f' LINKID_DataType = "Number",\n'
|
|
1933
|
+
f' INPID_InputControl = "CheckboxControl",\n'
|
|
1934
|
+
f' INP_Integer = true,\n'
|
|
1935
|
+
f' INP_Default = {default},\n'
|
|
1936
|
+
f' }})\n'
|
|
1937
|
+
)
|
|
1938
|
+
|
|
1939
|
+
|
|
1940
|
+
# Public registry of generators.
|
|
1941
|
+
TEMPLATES = {
|
|
1942
|
+
# Color & per-pixel
|
|
1943
|
+
"color_matrix": color_matrix,
|
|
1944
|
+
"per_pixel": per_pixel,
|
|
1945
|
+
"channel_op": channel_op,
|
|
1946
|
+
# Geometric / spatial
|
|
1947
|
+
"transform": transform,
|
|
1948
|
+
"spatial_warp": spatial_warp,
|
|
1949
|
+
# Text & shapes
|
|
1950
|
+
"text_overlay": text_overlay,
|
|
1951
|
+
"shape_generator": shape_generator,
|
|
1952
|
+
# Source / temporal
|
|
1953
|
+
"source_generator": source_generator,
|
|
1954
|
+
"time_displace": time_displace,
|
|
1955
|
+
# Filters
|
|
1956
|
+
"builtin_blur": builtin_blur,
|
|
1957
|
+
"builtin_resize": builtin_resize,
|
|
1958
|
+
"variable_blur": variable_blur,
|
|
1959
|
+
# Modifiers
|
|
1960
|
+
"modifier": modifier,
|
|
1961
|
+
"point_modifier": point_modifier,
|
|
1962
|
+
# Display / shaders
|
|
1963
|
+
"view_lut": view_lut,
|
|
1964
|
+
"dctl_kernel": dctl_kernel,
|
|
1965
|
+
# Reference / educational
|
|
1966
|
+
"controls_demo": controls_demo,
|
|
1967
|
+
"notifychanged_demo": notifychanged_demo,
|
|
1968
|
+
}
|