Skip to main content

sim_lib_view_daw/
modular.rs

1//! Modular component builder scenes for synth, patch, and poly workflows.
2
3use sim_kernel::{Expr, Symbol};
4use sim_lib_scene::node;
5use sim_value::build::{int, list, map, sym, text, vector};
6
7use crate::modular_fixture::{
8    fixture_palette, fixture_patch, fixture_sections, invalid_patch_validations,
9};
10use crate::poly::{POLY_SECTION_VIEW_ID, poly_section_view};
11
12/// Stable lens id for the modular component builder.
13pub const COMPONENT_BUILDER_VIEW_ID: &str = "view:component-builder";
14/// Stable lens id for the component palette panel.
15pub const COMPONENT_PALETTE_VIEW_ID: &str = "view:component-palette";
16/// Stable lens id for the component graph panel.
17pub const COMPONENT_GRAPH_VIEW_ID: &str = "view:component-graph";
18/// Stable lens id for the component cord editor.
19pub const COMPONENT_CORD_VIEW_ID: &str = "view:component-cord-editor";
20/// Patch serialization format tag emitted by the builder.
21pub const COMPONENT_BUILDER_PATCH_FORMAT: &str = "component-builder-patch-v1";
22
23/// Action names the component builder accepts.
24pub const COMPONENT_BUILDER_ACTIONS: [&str; 14] = [
25    "connect",
26    "disconnect",
27    "add-module",
28    "duplicate",
29    "delete",
30    "bypass",
31    "reset",
32    "inspect",
33    "route-matrix",
34    "enable-section",
35    "disable-section",
36    "save",
37    "load",
38    "live-preview",
39];
40
41/// Validation codes the component builder can report.
42pub const COMPONENT_BUILDER_VALIDATION_CODES: [&str; 6] = [
43    "missing-input",
44    "cycle",
45    "feedback-delay",
46    "clock-mismatch",
47    "rate-mismatch",
48    "gate-s-trigger-mismatch",
49];
50
51/// Fixture name for a graph-edit builder snapshot.
52pub const COMPONENT_BUILDER_GRAPH_EDIT_FIXTURE: &str = "graph-edit";
53/// Fixture name for a cord-edit builder snapshot.
54pub const COMPONENT_BUILDER_CORD_EDIT_FIXTURE: &str = "cord-edit";
55/// Fixture name for a section-edit builder snapshot.
56pub const COMPONENT_BUILDER_SECTION_EDIT_FIXTURE: &str = "section-edit";
57/// Fixture name for an invalid-patch builder snapshot.
58pub const COMPONENT_BUILDER_INVALID_PATCH_FIXTURE: &str = "invalid-patch";
59
60/// A single validation finding reported by the component builder.
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct BuilderValidation {
63    /// Qualified validation code symbol.
64    pub code: Symbol,
65    /// Module or cord the finding applies to, if any.
66    pub target: Option<Symbol>,
67    /// Human-readable description of the finding.
68    pub message: String,
69}
70
71impl BuilderValidation {
72    /// Builds a validation finding from a code, optional target, and message.
73    pub fn new(code: &str, target: Option<Symbol>, message: impl Into<String>) -> Self {
74        Self {
75            code: Symbol::qualified("component-builder/validation", code),
76            target,
77            message: message.into(),
78        }
79    }
80}
81
82/// Returns the names of the built-in component builder fixtures.
83pub fn component_builder_fixture_names() -> [&'static str; 4] {
84    [
85        COMPONENT_BUILDER_GRAPH_EDIT_FIXTURE,
86        COMPONENT_BUILDER_CORD_EDIT_FIXTURE,
87        COMPONENT_BUILDER_SECTION_EDIT_FIXTURE,
88        COMPONENT_BUILDER_INVALID_PATCH_FIXTURE,
89    ]
90}
91
92/// Renders the named fixture into a component builder Scene snapshot.
93pub fn component_builder_snapshot(name: &str) -> Option<Expr> {
94    let palette = fixture_palette();
95    let patch = fixture_patch();
96    let sections = fixture_sections();
97    let validation = match name {
98        COMPONENT_BUILDER_GRAPH_EDIT_FIXTURE => Vec::new(),
99        COMPONENT_BUILDER_CORD_EDIT_FIXTURE => Vec::new(),
100        COMPONENT_BUILDER_SECTION_EDIT_FIXTURE => Vec::new(),
101        COMPONENT_BUILDER_INVALID_PATCH_FIXTURE => invalid_patch_validations(),
102        _ => return None,
103    };
104    Some(component_builder_view(
105        &patch,
106        &palette,
107        &sections,
108        &validation,
109    ))
110}
111
112/// Builds the full component builder Scene from a patch, palette, sections,
113/// and validation findings.
114pub fn component_builder_view(
115    patch: &Expr,
116    palette: &Expr,
117    sections: &Expr,
118    validation: &[BuilderValidation],
119) -> Expr {
120    let category_filter = field_symbol_named(palette, "category-filter");
121    let capability_filter = field_symbol_named(palette, "capability-filter");
122    let stable_ids = stable_component_ids(patch);
123    node(
124        "stack",
125        vec![
126            ("lens", sym(COMPONENT_BUILDER_VIEW_ID)),
127            ("role", sym("component-builder")),
128            (
129                "patch-format",
130                text(COMPONENT_BUILDER_PATCH_FORMAT.to_owned()),
131            ),
132            ("view-ids", builder_view_ids()),
133            ("action-names", vector(action_names())),
134            ("stable-component-ids", vector(stable_ids.clone())),
135            (
136                "children",
137                list(vec![
138                    action_toolbar(),
139                    component_palette_view(
140                        palette,
141                        category_filter.as_ref(),
142                        capability_filter.as_ref(),
143                    ),
144                    component_graph_view(patch),
145                    component_cord_view(patch),
146                    poly_section_view(sections),
147                    validation_display(validation),
148                    persistence_view(&stable_ids),
149                ]),
150            ),
151        ],
152    )
153}
154
155/// Builds the component palette table Scene, filtered by category and
156/// capability.
157pub fn component_palette_view(
158    inventory: &Expr,
159    category_filter: Option<&Symbol>,
160    capability_filter: Option<&Symbol>,
161) -> Expr {
162    let rows = palette_items(inventory)
163        .into_iter()
164        .filter(|item| item_matches_filters(item, category_filter, capability_filter))
165        .map(|item| palette_row(&item))
166        .collect();
167    node(
168        "table",
169        vec![
170            ("lens", sym(COMPONENT_PALETTE_VIEW_ID)),
171            ("role", sym("component-palette")),
172            (
173                "category-filter",
174                category_filter
175                    .cloned()
176                    .map(Expr::Symbol)
177                    .unwrap_or(Expr::Nil),
178            ),
179            (
180                "capability-filter",
181                capability_filter
182                    .cloned()
183                    .map(Expr::Symbol)
184                    .unwrap_or(Expr::Nil),
185            ),
186            ("rows", vector(rows)),
187        ],
188    )
189}
190
191/// Builds the component graph Scene of modules and cords from a patch.
192pub fn component_graph_view(patch: &Expr) -> Expr {
193    let graph_nodes = patch_modules(patch)
194        .into_iter()
195        .map(|module| {
196            node(
197                "node",
198                vec![
199                    ("id", module.id),
200                    ("title", text(module.label)),
201                    ("module-kind", module.kind),
202                    ("inputs", vector(module.inputs)),
203                    ("outputs", vector(module.outputs)),
204                    (
205                        "actions",
206                        vector(vec![
207                            sym("inspect"),
208                            sym("duplicate"),
209                            sym("bypass"),
210                            sym("delete"),
211                        ]),
212                    ),
213                ],
214            )
215        })
216        .collect();
217    let edges = patch_cords(patch)
218        .into_iter()
219        .map(|cord| {
220            node(
221                "edge",
222                vec![
223                    ("from", text(cord.from)),
224                    ("to", text(cord.to)),
225                    ("action", sym("disconnect")),
226                ],
227            )
228        })
229        .collect();
230    node(
231        "graph",
232        vec![
233            ("lens", sym(COMPONENT_GRAPH_VIEW_ID)),
234            ("role", sym("component-graph")),
235            (
236                "patch-format",
237                text(COMPONENT_BUILDER_PATCH_FORMAT.to_owned()),
238            ),
239            ("nodes", list(graph_nodes)),
240            ("edges", list(edges)),
241        ],
242    )
243}
244
245/// Builds the cord editor table Scene from a patch.
246pub fn component_cord_view(patch: &Expr) -> Expr {
247    let rows = patch_cords(patch)
248        .into_iter()
249        .map(|cord| {
250            map(vec![
251                ("from", text(cord.from)),
252                ("to", text(cord.to)),
253                (
254                    "actions",
255                    vector(vec![sym("connect"), sym("disconnect"), sym("route-matrix")]),
256                ),
257            ])
258        })
259        .collect();
260    node(
261        "table",
262        vec![
263            ("lens", sym(COMPONENT_CORD_VIEW_ID)),
264            ("role", sym("component-cord-editor")),
265            ("rows", vector(rows)),
266            (
267                "actions",
268                vector(vec![sym("connect"), sym("disconnect"), sym("route-matrix")]),
269            ),
270        ],
271    )
272}
273
274/// Builds the validation display Scene from a set of builder findings.
275pub fn validation_display(validation: &[BuilderValidation]) -> Expr {
276    let rows = validation.iter().map(validation_row).collect();
277    let children = validation
278        .iter()
279        .map(|entry| {
280            node(
281                "field",
282                vec![
283                    ("role", sym("builder-validation-entry")),
284                    ("validation", Expr::Symbol(entry.code.clone())),
285                    (
286                        "target",
287                        entry.target.clone().map(Expr::Symbol).unwrap_or(Expr::Nil),
288                    ),
289                    ("message", text(entry.message.clone())),
290                ],
291            )
292        })
293        .collect();
294    node(
295        "box",
296        vec![
297            ("role", sym("builder-validation")),
298            ("rows", vector(rows)),
299            ("children", list(children)),
300        ],
301    )
302}
303
304fn validation_row(entry: &BuilderValidation) -> Expr {
305    map(vec![
306        ("validation", Expr::Symbol(entry.code.clone())),
307        (
308            "target",
309            entry.target.clone().map(Expr::Symbol).unwrap_or(Expr::Nil),
310        ),
311        ("message", text(entry.message.clone())),
312    ])
313}
314
315fn builder_view_ids() -> Expr {
316    map(vec![
317        ("builder", sym(COMPONENT_BUILDER_VIEW_ID)),
318        ("palette", sym(COMPONENT_PALETTE_VIEW_ID)),
319        ("graph", sym(COMPONENT_GRAPH_VIEW_ID)),
320        ("cord", sym(COMPONENT_CORD_VIEW_ID)),
321        ("poly", sym(POLY_SECTION_VIEW_ID)),
322    ])
323}
324
325fn action_toolbar() -> Expr {
326    node(
327        "stack",
328        vec![
329            ("role", sym("component-builder-actions")),
330            ("dir", sym("row")),
331            (
332                "children",
333                list(
334                    COMPONENT_BUILDER_ACTIONS
335                        .iter()
336                        .map(|action| action_button(action))
337                        .collect(),
338                ),
339            ),
340        ],
341    )
342}
343
344fn action_button(action: &str) -> Expr {
345    node(
346        "button",
347        vec![
348            ("role", sym("builder-action")),
349            ("action", sym(action)),
350            ("label", text(action.replace('-', " "))),
351        ],
352    )
353}
354
355fn persistence_view(stable_ids: &[Expr]) -> Expr {
356    node(
357        "box",
358        vec![
359            ("role", sym("builder-persistence")),
360            ("stable-component-ids", vector(stable_ids.to_vec())),
361            (
362                "children",
363                list(vec![
364                    action_button("save"),
365                    action_button("load"),
366                    action_button("live-preview"),
367                ]),
368            ),
369        ],
370    )
371}
372
373fn palette_row(item: &Expr) -> Expr {
374    let id = field_named(item, "id")
375        .cloned()
376        .unwrap_or_else(|| sym("component"));
377    let label = field_str_named(item, "label")
378        .map(text)
379        .unwrap_or_else(|| text(expr_label(&id)));
380    map(vec![
381        ("id", id),
382        ("label", label),
383        (
384            "category",
385            field_named(item, "category")
386                .cloned()
387                .unwrap_or_else(|| sym("unknown")),
388        ),
389        ("capabilities", vector(item_capabilities(item))),
390        (
391            "implemented",
392            field_named(item, "implemented")
393                .cloned()
394                .unwrap_or(Expr::Bool(false)),
395        ),
396        ("actions", vector(vec![sym("add-module"), sym("inspect")])),
397    ])
398}
399
400fn item_matches_filters(
401    item: &Expr,
402    category_filter: Option<&Symbol>,
403    capability_filter: Option<&Symbol>,
404) -> bool {
405    let category_matches = match category_filter {
406        Some(filter) => field_named(item, "category")
407            .map(|value| expr_matches_symbol(value, filter))
408            .unwrap_or(false),
409        None => true,
410    };
411    let capability_matches = match capability_filter {
412        Some(filter) => item_capabilities(item)
413            .iter()
414            .any(|value| expr_matches_symbol(value, filter)),
415        None => true,
416    };
417    category_matches && capability_matches
418}
419
420fn palette_items(inventory: &Expr) -> Vec<Expr> {
421    field_named(inventory, "items")
422        .map(sequence)
423        .unwrap_or_else(|| sequence(inventory))
424}
425
426fn patch_modules(patch: &Expr) -> Vec<ModuleRecord> {
427    if let Some(modules) = field_named(patch, "modules") {
428        return sequence(modules)
429            .into_iter()
430            .enumerate()
431            .map(|(index, module)| module_record(&module, index))
432            .collect();
433    }
434    field_named(patch, "nodes")
435        .map(sequence)
436        .unwrap_or_default()
437        .into_iter()
438        .enumerate()
439        .map(|(index, module)| audio_graph_node_record(&module, index))
440        .collect()
441}
442
443fn module_record(module: &Expr, index: usize) -> ModuleRecord {
444    let id = field_named(module, "id")
445        .cloned()
446        .unwrap_or_else(|| Expr::Symbol(Symbol::new(format!("module-{index}"))));
447    let kind = field_named(module, "module-kind")
448        .or_else(|| field_named(module, "kind"))
449        .cloned()
450        .unwrap_or_else(|| sym("component"));
451    ModuleRecord {
452        label: expr_label(&id),
453        id,
454        kind,
455        inputs: jack_names(field_named(module, "inputs")),
456        outputs: jack_names(field_named(module, "outputs")),
457    }
458}
459
460fn audio_graph_node_record(module: &Expr, index: usize) -> ModuleRecord {
461    let id = field_named(module, "id")
462        .cloned()
463        .unwrap_or_else(|| Expr::String(format!("node-{index}")));
464    ModuleRecord {
465        label: expr_label(&id),
466        id,
467        kind: sym("audio-graph-node"),
468        inputs: vec![
469            field_named(module, "in-channels")
470                .cloned()
471                .unwrap_or_else(|| int(0)),
472        ],
473        outputs: vec![
474            field_named(module, "out-channels")
475                .cloned()
476                .unwrap_or_else(|| int(0)),
477        ],
478    }
479}
480
481fn patch_cords(patch: &Expr) -> Vec<CordRecord> {
482    if let Some(cords) = field_named(patch, "cords") {
483        return sequence(cords)
484            .into_iter()
485            .map(|cord| CordRecord {
486                from: endpoint_label(field_named(&cord, "from")),
487                to: endpoint_label(field_named(&cord, "to")),
488            })
489            .collect();
490    }
491    field_named(patch, "cables")
492        .map(sequence)
493        .unwrap_or_default()
494        .into_iter()
495        .map(|cable| CordRecord {
496            from: field_named(&cable, "from")
497                .map(expr_label)
498                .unwrap_or_else(|| "out".to_owned()),
499            to: field_named(&cable, "to")
500                .map(expr_label)
501                .unwrap_or_else(|| "in".to_owned()),
502        })
503        .collect()
504}
505
506fn stable_component_ids(patch: &Expr) -> Vec<Expr> {
507    patch_modules(patch)
508        .into_iter()
509        .map(|module| module.id)
510        .collect()
511}
512
513fn endpoint_label(endpoint: Option<&Expr>) -> String {
514    let Some(endpoint) = endpoint else {
515        return "unpatched".to_owned();
516    };
517    match endpoint {
518        Expr::Map(_) => {
519            let module = field_named(endpoint, "module")
520                .map(expr_label)
521                .unwrap_or_else(|| "module".to_owned());
522            let jack = field_named(endpoint, "jack")
523                .map(expr_label)
524                .unwrap_or_else(|| "jack".to_owned());
525            format!("{module}:{jack}")
526        }
527        other => expr_label(other),
528    }
529}
530
531fn jack_names(jacks: Option<&Expr>) -> Vec<Expr> {
532    jacks
533        .map(sequence)
534        .unwrap_or_default()
535        .into_iter()
536        .map(|jack| {
537            field_named(&jack, "name")
538                .cloned()
539                .unwrap_or_else(|| sym("jack"))
540        })
541        .collect()
542}
543
544fn sequence(value: &Expr) -> Vec<Expr> {
545    match value {
546        Expr::List(items) | Expr::Vector(items) => items.clone(),
547        Expr::Nil => Vec::new(),
548        other => vec![other.clone()],
549    }
550}
551
552fn item_capabilities(item: &Expr) -> Vec<Expr> {
553    field_named(item, "capabilities")
554        .map(sequence)
555        .unwrap_or_default()
556}
557
558fn field_named<'a>(expr: &'a Expr, name: &str) -> Option<&'a Expr> {
559    let Expr::Map(entries) = expr else {
560        return None;
561    };
562    entries
563        .iter()
564        .find_map(|(key, value)| (key_name(key) == Some(name)).then_some(value))
565}
566
567fn field_symbol_named(expr: &Expr, name: &str) -> Option<Symbol> {
568    match field_named(expr, name) {
569        Some(Expr::Symbol(symbol)) => Some(symbol.clone()),
570        _ => None,
571    }
572}
573
574fn field_str_named<'a>(expr: &'a Expr, name: &str) -> Option<&'a str> {
575    match field_named(expr, name) {
576        Some(Expr::String(text)) => Some(text),
577        _ => None,
578    }
579}
580
581fn key_name(key: &Expr) -> Option<&str> {
582    match key {
583        Expr::Symbol(symbol) => Some(symbol.name.as_ref()),
584        Expr::String(text) => Some(text),
585        _ => None,
586    }
587}
588
589fn expr_matches_symbol(value: &Expr, filter: &Symbol) -> bool {
590    match value {
591        Expr::Symbol(symbol) => {
592            symbol == filter || symbol.as_qualified_str() == filter.as_qualified_str()
593        }
594        Expr::String(text) => text == &filter.as_qualified_str() || text == filter.name.as_ref(),
595        _ => false,
596    }
597}
598
599fn expr_label(expr: &Expr) -> String {
600    match expr {
601        Expr::Symbol(symbol) => symbol.as_qualified_str(),
602        Expr::String(text) => text.clone(),
603        other => format!("{other:?}"),
604    }
605}
606
607fn action_names() -> Vec<Expr> {
608    COMPONENT_BUILDER_ACTIONS
609        .iter()
610        .map(|action| sym(action))
611        .collect()
612}
613
614struct ModuleRecord {
615    id: Expr,
616    kind: Expr,
617    label: String,
618    inputs: Vec<Expr>,
619    outputs: Vec<Expr>,
620}
621
622struct CordRecord {
623    from: String,
624    to: String,
625}