1use 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
12pub const COMPONENT_BUILDER_VIEW_ID: &str = "view:component-builder";
14pub const COMPONENT_PALETTE_VIEW_ID: &str = "view:component-palette";
16pub const COMPONENT_GRAPH_VIEW_ID: &str = "view:component-graph";
18pub const COMPONENT_CORD_VIEW_ID: &str = "view:component-cord-editor";
20pub const COMPONENT_BUILDER_PATCH_FORMAT: &str = "component-builder-patch-v1";
22
23pub 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
41pub 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
51pub const COMPONENT_BUILDER_GRAPH_EDIT_FIXTURE: &str = "graph-edit";
53pub const COMPONENT_BUILDER_CORD_EDIT_FIXTURE: &str = "cord-edit";
55pub const COMPONENT_BUILDER_SECTION_EDIT_FIXTURE: &str = "section-edit";
57pub const COMPONENT_BUILDER_INVALID_PATCH_FIXTURE: &str = "invalid-patch";
59
60#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct BuilderValidation {
63 pub code: Symbol,
65 pub target: Option<Symbol>,
67 pub message: String,
69}
70
71impl BuilderValidation {
72 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
82pub 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
92pub 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 §ions,
108 &validation,
109 ))
110}
111
112pub 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
155pub 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
191pub 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
245pub 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
274pub 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}