Skip to content

Commit 2261d66

Browse files
authored
gui2dpf templates (#354)
* start working on gui2dpf templates and integration * merge recursive functions * move nanovg render to module * abstract generated send/receive to function call * implement widget generation and some missing gui type features * add @hv_event receiver to DPF * get size from gui_json; add label to canvas object * update pdvg; tweak knob font_height * fix ir_file path; tweak font sizes to match plugdata rendering * escape quotes in comment string * tweak width of comment object to match plugdata rendering * deal with misaligned parameter indices * match on literal empty string; use text.replace to appease mypy * tweak float font height * update changelog * add ADR
1 parent 7a614e9 commit 2261d66

35 files changed

Lines changed: 688 additions & 87 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Features:
1111
* Only parse GUI with `--gui` flag
1212
* Port objects from cyclone
1313
* Port `pdnam~` external using `MicroNAM`
14+
* DPF: support `@hv_event` parameters
15+
* DPF: Generate optional NanoVG UI code from `pd2gui` parser
1416

1517
Bugfixes:
1618

@@ -23,6 +25,7 @@ Bugfixes:
2325
Refactor:
2426

2527
* Migrate to Pathlib (note: Generator signature has changed!)
28+
* GUI IR: stable IDs for graphs/canvas/comments; updated float/number fields; use specific GUI IR filename
2629

2730
Docs:
2831

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# ADR-006: gui2dpf Templates
2+
3+
Date: 2026-05-13
4+
Issue: https://github.com/Wasted-Audio/hvcc/issues/296
5+
6+
## Context
7+
8+
A new [PDVG](https://github.com/Wasted-Audio/PDVG) widget library for DPF was created, which implements Plugdata GUI objects using NanoVG.
9+
10+
We can now use this widget library in combination with the GUI JSON, created by the pd2gui stage in hvcc, to generate an emulated UI for the DPF generator.
11+
12+
## Decision
13+
14+
We create a new c2dpf code-path, with a set of templates, to combine the GUI JSON with the new widget library.
15+
16+
In order to have backwards compatibility with the previous ImGui UI implementation we introduce a new `DPFUIType` in the meta JSON. This allows to keep the old boolean behavior, which selects ImGui as the GUI type, but allows to extend it using integer enumeration. This could allow for other UI extensions in the future as well.
17+
18+
We add the ability to create `@hv_event` based parameters to allow for the same behavior as `[bang]` object. This brings DPF on par with Unity and Javascript implementations.
19+
20+
The GUI JSON will need some minor modifications to better align to the PDVG requirements.
21+
22+
## MVP Definition
23+
24+
The user should be able to generate a GUI for their DPF plugins that matches the basic Plugdata UI as close as possible.
25+
26+
## Future Improvements
27+
28+
More GUI objects and better object behavior may be added in the future. More accuracy in the layout and design may also be required.

docs/generators/custom.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class ExampleHvccGenerator(Generator):
3232
c_src_dir: Path,
3333
out_dir: Path,
3434
externs: ExternInfo,
35-
patch_name: Optional[str] = None,
35+
patch_name: str,
3636
patch_meta: Meta = Meta(),
3737
num_input_channels: int = 0,
3838
num_output_channels: int = 0,

docs/generators/dpf.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ Other special types can give additional information to the host:
4949
* `log` - hints `kParameterIsLogarithmic`
5050
* `log_hz` - unit `Hz` - hints `kParameterIsLogarithmic`
5151

52+
### Events
53+
54+
If you want to receive a bang use `@hv_event` instead of `@hv_param` like `[r banger @hv_event]`.
55+
On the plugin side this behaves like the `trig` type.
56+
5257
## Metadata
5358

5459
An accompanying metadata.json file can be included to set additional plugin settings.

docs/getting-started/patching.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ An intermediate result GUI json is created containing these objects with their l
6666

6767
## Exposing Events
6868

69-
All (control) `[receive]` and `[r]` objects annotated with `@hv_event` will be exposed as events in the Unity and Javascript targets only.
69+
All (control) `[receive]` and `[r]` objects annotated with `@hv_event` will be exposed as events in the Unity, Javascript and DPF targets only.
7070

7171
![events](../img/docs_exposed_events.png)
7272

hvcc/compiler.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,12 +257,15 @@ def compile_dataflow(
257257
search_paths=search_paths,
258258
verbose=verbose)
259259

260+
# ensure that the ir filenames have no funky characters in it
261+
subst_name = re.sub(r'\W', '_', patch_name)
262+
260263
if gui:
261264
if verbose:
262265
print("--> Generating GUI IR")
263266
results.root["pd2gui"] = pd2gui.pd2gui.compile(
264267
pd_path=in_path,
265-
ir_dir=Path(out_dir, "ir"),
268+
ir_file=Path(out_dir, "ir", f"{subst_name}.heavy.gui.json"),
266269
search_paths=search_paths,
267270
verbose=verbose)
268271

@@ -272,7 +275,6 @@ def compile_dataflow(
272275
if response.notifs.has_error:
273276
return results
274277

275-
subst_name = re.sub(r'\W', '_', patch_name)
276278
results.root["hv2ir"] = hv2ir.hv2ir.compile(
277279
hv_file=Path(response.out_dir, response.out_file),
278280
# ensure that the ir filename has no funky characters in it

hvcc/generators/c2daisy/c2daisy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def compile(
5151
c_src_dir: Path,
5252
out_dir: Path,
5353
externs: ExternInfo,
54-
patch_name: Optional[str] = None,
54+
patch_name: str,
5555
patch_meta: Meta = Meta(),
5656
num_input_channels: int = 0,
5757
num_output_channels: int = 0,

hvcc/generators/c2dpf/c2dpf.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525

2626
from hvcc.interpreters.pd2hv.NotificationEnum import NotificationEnum
2727
from hvcc.types.compiler import Generator, CompilerResp, CompilerMsg, CompilerNotif, ExternInfo
28-
from hvcc.types.meta import Meta, DPF
28+
from hvcc.types.meta import Meta, DPF, DPFUIType, DPFUISize
29+
from .nanovg_render import open_gui_json, nanovg_render
2930

3031

3132
class c2dpf(Generator):
@@ -38,7 +39,7 @@ def compile(
3839
c_src_dir: Path,
3940
out_dir: Path,
4041
externs: ExternInfo,
41-
patch_name: Optional[str] = None,
42+
patch_name: str,
4243
patch_meta: Meta = Meta(),
4344
num_input_channels: int = 0,
4445
num_output_channels: int = 0,
@@ -51,6 +52,8 @@ def compile(
5152
out_dir = Path(out_dir, "plugin")
5253
receiver_list = externs.parameters.inParam
5354
sender_list = externs.parameters.outParam
55+
event_list = externs.events.inEvent
56+
out_event_list = externs.events.outEvent
5457

5558
dpf_meta: DPF = patch_meta.dpf
5659
dpf_path = dpf_meta.dpf_path
@@ -71,6 +74,13 @@ def compile(
7174
source_dir = Path(out_dir, "source")
7275
shutil.copytree(c_src_dir, source_dir)
7376

77+
if dpf_meta.enable_ui == DPFUIType.NANOVG:
78+
gui_json = open_gui_json(patch_name, c_src_dir)
79+
dpf_meta.ui_size = DPFUISize(
80+
width=gui_json.size.x,
81+
height=gui_json.size.y
82+
)
83+
7484
# initialize the jinja template environment
7585
env = jinja2.Environment()
7686
env.filters["uniqueid"] = filter_uniqueid
@@ -89,6 +99,8 @@ def compile(
8999
num_output_channels=num_output_channels,
90100
receivers=receiver_list,
91101
senders=sender_list,
102+
events=event_list,
103+
out_events=out_event_list,
92104
copyright=copyright_c))
93105
dpf_cpp_path = Path(source_dir, f"HeavyDPF_{patch_name}.cpp")
94106
with open(dpf_cpp_path, "w") as f:
@@ -100,18 +112,48 @@ def compile(
100112
num_output_channels=num_output_channels,
101113
receivers=receiver_list,
102114
senders=sender_list,
115+
events=event_list,
116+
out_events=out_event_list,
103117
pool_sizes_kb=externs.memoryPoolSizesKb,
104118
copyright=copyright_c))
105-
if dpf_meta.enable_ui:
119+
if dpf_meta.enable_ui == DPFUIType.IMGUI:
106120
dpf_ui_path = Path(source_dir, f"HeavyDPF_{patch_name}_UI.cpp")
107121
with open(dpf_ui_path, "w") as f:
108-
f.write(env.get_template("HeavyDPF_UI.cpp").render(
122+
f.write(env.get_template("HeavyDPF_ImGui_UI.cpp").render(
109123
name=patch_name,
110124
meta=dpf_meta,
111125
class_name=f"HeavyDPF_{patch_name}",
112126
receivers=receiver_list,
113127
senders=sender_list,
114128
copyright=copyright_c))
129+
elif dpf_meta.enable_ui == DPFUIType.NANOVG:
130+
gui_json, widgets, gui_objects_render = nanovg_render(
131+
patch_name, c_src_dir, env, receiver_list, sender_list
132+
)
133+
134+
dpf_ui_header = Path(source_dir, f"HeavyDPF_{patch_name}_UI.hpp")
135+
with open(dpf_ui_header, "w") as f:
136+
f.write(env.get_template("HeavyDPF_NanoVG_UI.hpp").render(
137+
name=patch_name,
138+
meta=dpf_meta,
139+
class_name=f"HeavyDPF_{patch_name}_UI",
140+
gui_json=gui_json,
141+
widgets=widgets,
142+
copyright=copyright_c))
143+
dpf_ui_path = Path(source_dir, f"HeavyDPF_{patch_name}_UI.cpp")
144+
with open(dpf_ui_path, "w") as f:
145+
f.write(env.get_template("HeavyDPF_NanoVG_UI.cpp").render(
146+
name=patch_name,
147+
meta=dpf_meta,
148+
class_name=f"HeavyDPF_{patch_name}_UI",
149+
gui_json=gui_json,
150+
widgets=widgets,
151+
gui_objects=gui_objects_render,
152+
receivers=receiver_list,
153+
senders=sender_list,
154+
events=event_list,
155+
copyright=copyright_c))
156+
115157
dpf_h_path = Path(source_dir, "DistrhoPluginInfo.h")
116158
with open(dpf_h_path, "w") as f:
117159
f.write(env.get_template("DistrhoPluginInfo.h").render(
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import json
2+
3+
from pathlib import Path
4+
5+
import jinja2
6+
7+
from hvcc.types.GUI import Canvas, Comment, GraphRoot, Graph, GUIObjects
8+
9+
10+
def open_gui_json(
11+
patch_name: str,
12+
c_src_dir: Path,
13+
) -> GraphRoot:
14+
15+
# load GUI from json file
16+
gui_json_path = Path(c_src_dir, "../ir/", f"{patch_name}.heavy.gui.json")
17+
with open(gui_json_path, "r") as f:
18+
gui_json = GraphRoot(**json.load(f))
19+
20+
return gui_json
21+
22+
23+
def nanovg_render(
24+
patch_name: str,
25+
c_src_dir: Path,
26+
env: jinja2.Environment,
27+
recv_list: list,
28+
send_list: list
29+
) -> tuple[
30+
GraphRoot,
31+
dict[str, list[str]],
32+
list[str]
33+
]:
34+
""" Generate nanovg components from the GUI json
35+
"""
36+
gui_json = open_gui_json(patch_name, c_src_dir)
37+
38+
# widget overview
39+
widgets: dict[str, list[str]] = {
40+
"graph": [],
41+
"canvas": [],
42+
"comment": [],
43+
"bang": [],
44+
"toggle": [],
45+
"vradio": [],
46+
"hradio": [],
47+
"vslider": [],
48+
"hslider": [],
49+
"knob": [],
50+
"number": [],
51+
"float": []
52+
}
53+
54+
# render gui objects
55+
gui_objects_render = []
56+
57+
def generate_gui_objects(graphs: list[Graph], objects: list[GUIObjects], parent: str):
58+
for w in objects:
59+
widgets[w.type].append(w.id if isinstance(w, (Canvas, Comment)) else w.parameter)
60+
61+
gui_objects_render.append(env.get_template("gui_objects.cpp").render(
62+
parent=parent,
63+
gui_objects=objects,
64+
receivers=recv_list,
65+
senders=send_list
66+
))
67+
68+
for graph in graphs:
69+
widgets["graph"].append(graph.id)
70+
71+
gui_objects_render.append(
72+
f"""
73+
// subpatch
74+
{graph.id} = new PDSubpatch({parent});
75+
{graph.id}->setSize({graph.gop_size.x} * scaleFactor, {graph.gop_size.y} * scaleFactor);
76+
{graph.id}->setAbsolutePos({graph.position.x} * scaleFactor, {graph.position.y} * scaleFactor);
77+
{parent}->addManagedChild({graph.id});
78+
"""
79+
)
80+
generate_gui_objects(graph.graphs, graph.objects, graph.id)
81+
82+
generate_gui_objects(gui_json.graphs, gui_json.objects, "mainPatch")
83+
84+
return gui_json, widgets, gui_objects_render

hvcc/generators/c2dpf/templates/DistrhoPluginInfo.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
#define DISTRHO_PLUGIN_NUM_INPUTS {{num_input_channels}}
3131
#define DISTRHO_PLUGIN_NUM_OUTPUTS {{num_output_channels}}
3232
#define DISTRHO_PLUGIN_IS_SYNTH {{1 if num_output_channels > 0 and meta.midi_input > 0 else 0}}
33-
#define DISTRHO_PLUGIN_HAS_UI {{1 if meta.enable_ui is sameas true else 0}}
33+
#define DISTRHO_PLUGIN_HAS_UI {{1 if meta.enable_ui > 0 else 0}}
3434
#define DISTRHO_PLUGIN_IS_RT_SAFE 1
3535
#define DISTRHO_PLUGIN_WANT_PROGRAMS 0
3636
#define DISTRHO_PLUGIN_WANT_STATE 0
@@ -51,11 +51,16 @@
5151
// for level monitoring
5252
#define DISTRHO_PLUGIN_WANT_DIRECT_ACCESS 0
5353

54-
{% if meta.enable_ui is sameas true %}
54+
{% if meta.enable_ui > 0 %}
5555
// if you are using a UI you'll probably want to modify these settings to your needs
56+
{%- if meta.enable_ui == 1 %}
5657
#define DISTRHO_UI_USE_CUSTOM 1
5758
#define DISTRHO_UI_CUSTOM_INCLUDE_PATH "DearImGui.hpp"
5859
#define DISTRHO_UI_CUSTOM_WIDGET_TYPE DGL_NAMESPACE::ImGuiTopLevelWidget
60+
{%- elif meta.enable_ui == 2 %}
61+
#define DISTRHO_UI_USE_NANOVG 1
62+
{%- endif %}
63+
5964
{%- if meta.ui_size != None %}
6065
#define DISTRHO_UI_DEFAULT_WIDTH {{meta.ui_size.width}}
6166
#define DISTRHO_UI_DEFAULT_HEIGHT {{meta.ui_size.height}}

0 commit comments

Comments
 (0)