1use std::collections::HashMap;
6
7use flutmax_sema::graph::{PatchGraph, PatchNode};
8use serde_json::{json, Map, Value};
9
10use crate::layout::sugiyama_layout;
11
12pub struct UiData {
14 pub patcher: HashMap<String, Value>,
16 pub entries: HashMap<String, Value>,
18 pub comments: Vec<Value>,
20 pub panels: Vec<Value>,
22 pub images: Vec<Value>,
24}
25
26impl UiData {
27 pub fn from_json(json_str: &str) -> Option<Self> {
30 let root: Value = serde_json::from_str(json_str).ok()?;
31 let obj = root.as_object()?;
32
33 let mut patcher = HashMap::new();
34 let mut entries = HashMap::new();
35
36 let comments = obj
37 .get("_comments")
38 .and_then(|v| v.as_array())
39 .cloned()
40 .unwrap_or_default();
41 let panels = obj
42 .get("_panels")
43 .and_then(|v| v.as_array())
44 .cloned()
45 .unwrap_or_default();
46 let images = obj
47 .get("_images")
48 .and_then(|v| v.as_array())
49 .cloned()
50 .unwrap_or_default();
51
52 for (key, value) in obj {
53 if key == "_patcher" {
54 if let Some(inner) = value.as_object() {
55 for (k, v) in inner {
56 patcher.insert(k.clone(), v.clone());
57 }
58 }
59 } else if key == "_comments" || key == "_panels" || key == "_images" {
60 } else {
62 entries.insert(key.clone(), value.clone());
63 }
64 }
65
66 Some(UiData {
67 patcher,
68 entries,
69 comments,
70 panels,
71 images,
72 })
73 }
74}
75
76#[derive(Debug)]
78pub enum CodegenError {
79 Serialization(String),
81}
82
83impl std::fmt::Display for CodegenError {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 CodegenError::Serialization(msg) => write!(f, "codegen error: {}", msg),
87 }
88 }
89}
90
91impl std::error::Error for CodegenError {}
92
93const LAYOUT_X: f64 = 100.0;
96const LAYOUT_Y_START: f64 = 50.0;
97const LAYOUT_Y_STEP: f64 = 70.0;
98
99const BOX_WIDTH_INLET_OUTLET: f64 = 30.0;
100const BOX_HEIGHT_INLET_OUTLET: f64 = 30.0;
101const BOX_WIDTH_NEWOBJ: f64 = 80.0;
102const BOX_HEIGHT_NEWOBJ: f64 = 22.0;
103const BOX_WIDTH_EZDAC: f64 = 45.0;
104const BOX_HEIGHT_EZDAC: f64 = 45.0;
105
106pub struct GenerateOptions {
108 pub classnamespace: String,
110}
111
112impl Default for GenerateOptions {
113 fn default() -> Self {
114 Self {
115 classnamespace: "box".to_string(),
116 }
117 }
118}
119
120pub fn generate(graph: &PatchGraph) -> Result<String, CodegenError> {
122 generate_with_options(graph, &GenerateOptions::default())
123}
124
125pub fn generate_with_options(
127 graph: &PatchGraph,
128 opts: &GenerateOptions,
129) -> Result<String, CodegenError> {
130 generate_with_ui(graph, opts, None)
131}
132
133pub fn generate_with_ui(
138 graph: &PatchGraph,
139 opts: &GenerateOptions,
140 ui_data: Option<&UiData>,
141) -> Result<String, CodegenError> {
142 let patcher = build_patcher(graph, opts, ui_data)?;
143 let root = json!({ "patcher": patcher });
144 serde_json::to_string_pretty(&root).map_err(|e| CodegenError::Serialization(e.to_string()))
145}
146
147fn build_patcher(
149 graph: &PatchGraph,
150 opts: &GenerateOptions,
151 ui_data: Option<&UiData>,
152) -> Result<Value, CodegenError> {
153 let is_rnbo = opts.classnamespace == "rnbo";
154 let is_gen = opts.classnamespace == "dsp.gen";
155 let needs_port_indices = is_rnbo || is_gen;
156 let ordered_nodes = topological_order(graph);
157
158 let inlet_indices: HashMap<String, usize> = if needs_port_indices {
160 let mut control_idx = 0usize;
161 let mut signal_idx = 0usize;
162 let mut map = HashMap::new();
163 for node in &ordered_nodes {
164 match node.object_name.as_str() {
165 "inlet" => {
166 map.insert(node.id.clone(), control_idx);
167 control_idx += 1;
168 }
169 "inlet~" => {
170 map.insert(node.id.clone(), signal_idx);
171 signal_idx += 1;
172 }
173 _ => {}
174 }
175 }
176 map
177 } else {
178 HashMap::new()
179 };
180
181 let outlet_indices: HashMap<String, usize> = if needs_port_indices {
182 let mut control_idx = 0usize;
183 let mut signal_idx = 0usize;
184 let mut map = HashMap::new();
185 for node in &ordered_nodes {
186 match node.object_name.as_str() {
187 "outlet" => {
188 map.insert(node.id.clone(), control_idx);
189 control_idx += 1;
190 }
191 "outlet~" => {
192 map.insert(node.id.clone(), signal_idx);
193 signal_idx += 1;
194 }
195 _ => {}
196 }
197 }
198 map
199 } else {
200 HashMap::new()
201 };
202
203 let mut id_map: HashMap<String, String> = HashMap::new();
205 for (i, node) in ordered_nodes.iter().enumerate() {
206 id_map.insert(node.id.clone(), format!("obj-{}", i + 1));
207 }
208
209 let layout = sugiyama_layout(graph);
211
212 let classnamespace = opts.classnamespace.as_str();
214 let mut boxes: Vec<Value> = ordered_nodes
215 .iter()
216 .enumerate()
217 .map(|(i, node)| {
218 let mapped_id = format!("obj-{}", i + 1);
219 let (x, y) = layout
220 .positions
221 .get(&node.id)
222 .copied()
223 .unwrap_or((LAYOUT_X, LAYOUT_Y_START + (i as f64) * LAYOUT_Y_STEP));
224 let serial = i + 1; let port_index = inlet_indices
226 .get(&node.id)
227 .or_else(|| outlet_indices.get(&node.id))
228 .copied();
229 build_box(
230 node,
231 &BoxContext {
232 id: &mapped_id,
233 x,
234 y,
235 classnamespace,
236 serial,
237 port_index,
238 ui_data,
239 },
240 )
241 })
242 .collect();
243
244 if let Some(ui) = ui_data {
246 let mut visual_counter = ordered_nodes.len() + 1;
247
248 for comment in &ui.comments {
250 let rect = comment
251 .get("rect")
252 .cloned()
253 .unwrap_or(json!([50, 50, 200, 20]));
254 let text = comment.get("text").and_then(|t| t.as_str()).unwrap_or("");
255 let id = format!("obj-{}", visual_counter);
256 visual_counter += 1;
257 boxes.push(json!({
258 "box": {
259 "id": id,
260 "maxclass": "comment",
261 "text": text,
262 "numinlets": 1,
263 "numoutlets": 0,
264 "outlettype": [],
265 "patching_rect": rect,
266 }
267 }));
268 }
269
270 for panel in &ui.panels {
272 let rect = panel
273 .get("rect")
274 .cloned()
275 .unwrap_or(json!([50, 50, 200, 200]));
276 let id = format!("obj-{}", visual_counter);
277 visual_counter += 1;
278 let mut box_obj = serde_json::Map::new();
279 box_obj.insert("id".into(), json!(id));
280 box_obj.insert("maxclass".into(), json!("panel"));
281 box_obj.insert("numinlets".into(), json!(1));
282 box_obj.insert("numoutlets".into(), json!(0));
283 box_obj.insert("outlettype".into(), json!([]));
284 box_obj.insert("patching_rect".into(), rect);
285 if let Some(obj) = panel.as_object() {
287 for (k, v) in obj {
288 if k != "rect" {
289 box_obj.insert(k.clone(), v.clone());
290 }
291 }
292 }
293 boxes.push(json!({ "box": Value::Object(box_obj) }));
294 }
295
296 for image in &ui.images {
298 let rect = image
299 .get("rect")
300 .cloned()
301 .unwrap_or(json!([50, 50, 200, 200]));
302 let pic = image.get("pic").and_then(|p| p.as_str()).unwrap_or("");
303 let id = format!("obj-{}", visual_counter);
304 visual_counter += 1;
305 let mut box_obj = serde_json::Map::new();
306 box_obj.insert("id".into(), json!(id));
307 box_obj.insert("maxclass".into(), json!("fpic"));
308 box_obj.insert("numinlets".into(), json!(1));
309 box_obj.insert("numoutlets".into(), json!(1));
310 box_obj.insert("outlettype".into(), json!(["jit_matrix"]));
311 box_obj.insert("patching_rect".into(), rect);
312 if !pic.is_empty() {
313 box_obj.insert("pic".into(), json!(pic));
314 }
315 boxes.push(json!({ "box": Value::Object(box_obj) }));
316 }
317
318 let _ = visual_counter;
320 }
321
322 let lines: Vec<Value> = graph
324 .edges
325 .iter()
326 .map(|edge| {
327 let source_id = id_map
328 .get(&edge.source_id)
329 .cloned()
330 .unwrap_or_else(|| edge.source_id.clone());
331 let dest_id = id_map
332 .get(&edge.dest_id)
333 .cloned()
334 .unwrap_or_else(|| edge.dest_id.clone());
335 let mut patchline = serde_json::Map::new();
336 patchline.insert("source".into(), json!([source_id, edge.source_outlet]));
337 patchline.insert("destination".into(), json!([dest_id, edge.dest_inlet]));
338 if let Some(order) = edge.order {
339 patchline.insert("order".into(), json!(order));
340 }
341 json!({ "patchline": Value::Object(patchline) })
342 })
343 .collect();
344
345 let mut patcher = Map::new();
347 patcher.insert("fileversion".into(), json!(1));
348 patcher.insert(
349 "appversion".into(),
350 json!({
351 "major": 8,
352 "minor": 6,
353 "revision": 0,
354 "architecture": "x64",
355 "modernui": 1
356 }),
357 );
358 patcher.insert("classnamespace".into(), json!(&opts.classnamespace));
359 let patcher_rect = ui_data
361 .and_then(|ui| ui.patcher.get("rect"))
362 .cloned()
363 .unwrap_or_else(|| {
364 json!([
365 100.0,
366 100.0,
367 layout.patcher_size.0.max(640.0),
368 layout.patcher_size.1.max(480.0)
369 ])
370 });
371 patcher.insert("rect".into(), patcher_rect);
372 patcher.insert("bglocked".into(), json!(0));
373 patcher.insert("openinpresentation".into(), json!(0));
374 patcher.insert("default_fontsize".into(), json!(12.0));
375 patcher.insert("default_fontface".into(), json!(0));
376 patcher.insert("default_fontname".into(), json!("Arial"));
377 patcher.insert("gridonopen".into(), json!(1));
378 patcher.insert("gridsize".into(), json!([15.0, 15.0]));
379 patcher.insert("gridsnaponopen".into(), json!(1));
380 patcher.insert("objectsnaponopen".into(), json!(1));
381 patcher.insert("statusbarvisible".into(), json!(2));
382 patcher.insert("toolbarvisible".into(), json!(1));
383 patcher.insert("lefttoolbarpinned".into(), json!(0));
384 patcher.insert("toptoolbarpinned".into(), json!(0));
385 patcher.insert("righttoolbarpinned".into(), json!(0));
386 patcher.insert("bottomtoolbarpinned".into(), json!(0));
387 patcher.insert("toolbars_unpinned_last_save".into(), json!(0));
388 patcher.insert("tallnewobj".into(), json!(0));
389 patcher.insert("boxanimatetime".into(), json!(200));
390 patcher.insert("enablehscroll".into(), json!(1));
391 patcher.insert("enablevscroll".into(), json!(1));
392 patcher.insert("devicewidth".into(), json!(0.0));
393 patcher.insert("description".into(), json!(""));
394 patcher.insert("digest".into(), json!(""));
395 patcher.insert("tags".into(), json!(""));
396 patcher.insert("style".into(), json!(""));
397 patcher.insert("subpatcher_template".into(), json!(""));
398 patcher.insert("assistshowspatchername".into(), json!(0));
399 patcher.insert("boxes".into(), Value::Array(boxes));
400 patcher.insert("lines".into(), Value::Array(lines));
401 patcher.insert("dependency_cache".into(), json!([]));
402 patcher.insert("autosave".into(), json!(0));
403
404 Ok(Value::Object(patcher))
405}
406
407struct BoxContext<'a> {
409 id: &'a str,
410 x: f64,
411 y: f64,
412 classnamespace: &'a str,
413 serial: usize,
414 port_index: Option<usize>,
415 ui_data: Option<&'a UiData>,
416}
417
418fn build_box(node: &PatchNode, ctx: &BoxContext) -> Value {
420 let is_rnbo = ctx.classnamespace == "rnbo";
421 let is_gen = ctx.classnamespace == "dsp.gen";
422 let (maxclass, width, height) = classify_maxclass(node, ctx.classnamespace);
423 let outlettype = compute_outlettype(node, is_rnbo, is_gen);
424
425 let effective_num_outlets =
428 if (is_rnbo || is_gen) && matches!(node.object_name.as_str(), "outlet" | "outlet~") {
429 0
430 } else {
431 node.num_outlets
432 };
433
434 let mut box_obj = Map::new();
435 box_obj.insert("id".into(), json!(ctx.id));
436 box_obj.insert("maxclass".into(), json!(maxclass));
437 box_obj.insert("numinlets".into(), json!(node.num_inlets));
438 box_obj.insert("numoutlets".into(), json!(effective_num_outlets));
439
440 if !outlettype.is_empty() {
441 box_obj.insert("outlettype".into(), json!(outlettype));
442 }
443
444 box_obj.insert("patching_rect".into(), json!([ctx.x, ctx.y, width, height]));
445
446 if maxclass == "newobj" {
448 let text = if is_rnbo {
449 match node.object_name.as_str() {
451 "inlet" => {
452 let name = node
453 .varname
454 .clone()
455 .unwrap_or_else(|| format!("port_{}", ctx.port_index.unwrap_or(0)));
456 format!("inport {}", name)
457 }
458 "inlet~" => {
459 let idx = ctx.port_index.unwrap_or(0) + 1; format!("in~ {}", idx)
461 }
462 "outlet" => {
463 let name = node
464 .varname
465 .clone()
466 .unwrap_or_else(|| format!("port_{}", ctx.port_index.unwrap_or(0)));
467 format!("outport {}", name)
468 }
469 "outlet~" => {
470 let idx = ctx.port_index.unwrap_or(0) + 1; format!("out~ {}", idx)
472 }
473 _ => {
474 let mut t = build_object_text(node);
475 if !node.attrs.is_empty() {
476 let attr_str: String = node
477 .attrs
478 .iter()
479 .map(|(k, v)| format!("@{} {}", k, v))
480 .collect::<Vec<_>>()
481 .join(" ");
482 t = format!("{} {}", t, attr_str);
483 }
484 t
485 }
486 }
487 } else if is_gen {
488 match node.object_name.as_str() {
490 "inlet" | "inlet~" => {
491 let idx = ctx.port_index.unwrap_or(0) + 1; format!("in {}", idx)
493 }
494 "outlet" | "outlet~" => {
495 let idx = ctx.port_index.unwrap_or(0) + 1; format!("out {}", idx)
497 }
498 _ => {
499 let mut t = build_object_text(node);
500 if !node.attrs.is_empty() {
501 let attr_str: String = node
502 .attrs
503 .iter()
504 .map(|(k, v)| format!("@{} {}", k, v))
505 .collect::<Vec<_>>()
506 .join(" ");
507 t = format!("{} {}", t, attr_str);
508 }
509 t
510 }
511 }
512 } else {
513 let mut t = build_object_text(node);
514 if !node.attrs.is_empty() {
516 let attr_str: String = node
517 .attrs
518 .iter()
519 .map(|(k, v)| format!("@{} {}", k, v))
520 .collect::<Vec<_>>()
521 .join(" ");
522 t = format!("{} {}", t, attr_str);
523 }
524 t
525 };
526 box_obj.insert("text".into(), json!(text));
527 } else if maxclass == "message" {
528 let text = if node.args.is_empty() {
530 String::new()
531 } else {
532 node.args.join(" ")
533 };
534 box_obj.insert("text".into(), json!(text));
535 }
536
537 if let Some(ref vn) = node.varname {
539 box_obj.insert("varname".into(), json!(vn));
540 }
541
542 if maxclass != "newobj" && !node.attrs.is_empty() {
544 for (key, value) in &node.attrs {
545 if let Ok(f) = value.parse::<f64>() {
547 box_obj.insert(key.clone(), json!(f));
548 } else {
549 box_obj.insert(key.clone(), json!(value));
550 }
551 }
552 }
553
554 if matches!(maxclass, "v8.codebox" | "codebox") {
556 if let Some(ref code) = node.code {
557 box_obj.insert("code".into(), json!(code));
558 }
559 if maxclass == "v8.codebox" {
560 box_obj.insert("filename".into(), json!("none"));
561 if !box_obj.contains_key("text") {
563 box_obj.insert("text".into(), json!(""));
564 }
565 }
566 }
567
568 if let Some(ui_entry) = ctx
570 .ui_data
571 .and_then(|ui| node.varname.as_ref().and_then(|vn| ui.entries.get(vn)))
572 {
573 if let Some(rect) = ui_entry.get("rect") {
575 box_obj.insert("patching_rect".into(), rect.clone());
576 }
577 if let Some(obj) = ui_entry.as_object() {
579 for (k, v) in obj {
580 if k != "rect" {
581 box_obj.insert(k.clone(), v.clone());
582 }
583 }
584 }
585 }
586
587 if is_rnbo {
589 box_obj.insert("rnbo_serial".into(), json!(ctx.serial));
590 box_obj.insert(
591 "rnbo_uniqueid".into(),
592 json!(format!(
593 "{}_{}",
594 node.object_name.replace('~', "_tilde"),
595 ctx.id
596 )),
597 );
598 }
599
600 json!({ "box": Value::Object(box_obj) })
601}
602
603fn classify_maxclass(node: &PatchNode, classnamespace: &str) -> (&'static str, f64, f64) {
606 let is_rnbo = classnamespace == "rnbo";
607 let is_gen = classnamespace == "dsp.gen";
608 match node.object_name.as_str() {
609 "inlet" | "inlet~" if is_rnbo || is_gen => ("newobj", BOX_WIDTH_NEWOBJ, BOX_HEIGHT_NEWOBJ),
610 "outlet" | "outlet~" if is_rnbo || is_gen => {
611 ("newobj", BOX_WIDTH_NEWOBJ, BOX_HEIGHT_NEWOBJ)
612 }
613 "inlet" => ("inlet", BOX_WIDTH_INLET_OUTLET, BOX_HEIGHT_INLET_OUTLET),
614 "inlet~" => ("inlet", BOX_WIDTH_INLET_OUTLET, BOX_HEIGHT_INLET_OUTLET),
615 "outlet" => ("outlet", BOX_WIDTH_INLET_OUTLET, BOX_HEIGHT_INLET_OUTLET),
616 "outlet~" => ("outlet", BOX_WIDTH_INLET_OUTLET, BOX_HEIGHT_INLET_OUTLET),
617 "ezdac~" => ("ezdac~", BOX_WIDTH_EZDAC, BOX_HEIGHT_EZDAC),
618 "message" => ("message", 50.0, 22.0),
619 "button" => ("button", 50.0, 50.0),
620 "flonum" => ("flonum", 80.0, 22.0),
621 "number" => ("number", 50.0, 22.0),
622 "toggle" => ("toggle", 20.0, 20.0),
623 "umenu" => ("umenu", 100.0, 22.0),
624 "panel" => ("panel", 100.0, 50.0),
625 "jsui" => ("jsui", 64.0, 64.0),
626 "textbutton" => ("textbutton", 100.0, 20.0),
628 "live.text" => ("live.text", 44.0, 15.0),
629 "live.dial" => ("live.dial", 47.0, 48.0),
630 "live.toggle" => ("live.toggle", 15.0, 15.0),
631 "live.menu" => ("live.menu", 100.0, 15.0),
632 "live.numbox" => ("live.numbox", 44.0, 15.0),
633 "live.tab" => ("live.tab", 100.0, 20.0),
634 "live.comment" => ("live.comment", 100.0, 18.0),
635 "slider" => ("slider", 20.0, 140.0),
636 "dial" => ("dial", 40.0, 40.0),
637 "multislider" => ("multislider", 120.0, 100.0),
638 "kslider" => ("kslider", 168.0, 53.0),
639 "tab" => ("tab", 200.0, 24.0),
640 "rslider" => ("rslider", 100.0, 22.0),
641 "filtergraph~" => ("filtergraph~", 256.0, 128.0),
642 "spectroscope~" => ("spectroscope~", 300.0, 100.0),
643 "scope~" => ("scope~", 130.0, 130.0),
644 "meter~" => ("meter~", 13.0, 80.0),
645 "gain~" => ("gain~", 22.0, 140.0),
646 "ezadc~" => ("ezadc~", BOX_WIDTH_EZDAC, BOX_HEIGHT_EZDAC),
647 "number~" => ("number~", 56.0, 22.0),
648 "bpatcher" => ("bpatcher", 128.0, 128.0),
649 "fpic" => ("fpic", 100.0, 100.0),
650 "textedit" => ("textedit", 100.0, 22.0),
651 "attrui" => ("attrui", 150.0, 22.0),
652 "nslider" => ("nslider", 50.0, 120.0),
653 "preset" => ("preset", 100.0, 40.0),
654 "v8.codebox" => ("v8.codebox", 200.0, 100.0),
656 "codebox" => ("codebox", 200.0, 100.0),
657 _ => ("newobj", BOX_WIDTH_NEWOBJ, BOX_HEIGHT_NEWOBJ),
658 }
659}
660
661fn compute_outlettype(node: &PatchNode, is_rnbo: bool, is_gen: bool) -> Vec<&'static str> {
663 if (is_rnbo || is_gen) && matches!(node.object_name.as_str(), "outlet" | "outlet~") {
666 return vec![];
667 }
668
669 if node.num_outlets == 0 {
670 return vec![];
671 }
672
673 match node.object_name.as_str() {
674 "inlet" if is_rnbo => vec![""],
676 "inlet~" if is_rnbo => vec!["signal"],
678
679 "inlet" | "inlet~" if is_gen => vec![""],
681
682 "inlet" => vec![""],
684 "inlet~" => vec!["signal"],
685
686 "message" => vec![""],
688
689 "button" => vec!["bang"],
691 "toggle" => vec!["int"],
692 "umenu" => vec!["int", "", ""],
693 "flonum" => vec!["", "bang"],
694 "number" => vec!["", "bang"],
695 "textbutton" => vec!["", "", "int"],
696 "live.text" => vec!["", ""],
697 "live.dial" => vec!["", ""],
698 "live.toggle" => vec![""],
699 "live.menu" => vec!["", "", ""],
700 "live.numbox" => vec!["", ""],
701 "live.tab" => vec!["", "", ""],
702 "live.comment" => vec![],
703 "slider" => vec![""],
704 "dial" => vec![""],
705 "multislider" => vec!["", ""],
706 "kslider" => vec!["", ""],
707 "tab" => vec!["", "", ""],
708 "rslider" => vec!["", ""],
709 "bpatcher" => {
710 vec![""; node.num_outlets as usize]
712 }
713
714 name if name.ends_with('~') => {
716 let mut types = vec!["signal"];
717 if name == "line~" && node.num_outlets >= 2 {
719 types = vec!["signal", "bang"];
720 }
721 while types.len() < node.num_outlets as usize {
723 types.push("signal");
724 }
725 types.truncate(node.num_outlets as usize);
726 types
727 }
728
729 "v8.codebox" | "codebox" => {
731 vec![""; node.num_outlets as usize]
732 }
733
734 "trigger" | "t" => {
736 vec![""; node.num_outlets as usize]
738 }
739
740 _ => {
741 if node.is_signal {
743 vec!["signal"; node.num_outlets as usize]
744 } else {
745 vec![""; node.num_outlets as usize]
747 }
748 }
749 }
750}
751
752fn build_object_text(node: &PatchNode) -> String {
755 if node.args.is_empty() {
756 node.object_name.clone()
757 } else {
758 format!("{} {}", node.object_name, node.args.join(" "))
759 }
760}
761
762fn topological_order(graph: &PatchGraph) -> Vec<&PatchNode> {
766 let mut inlets: Vec<&PatchNode> = Vec::new();
767 let mut outlets: Vec<&PatchNode> = Vec::new();
768 let mut others: Vec<&PatchNode> = Vec::new();
769
770 for node in &graph.nodes {
771 match node.object_name.as_str() {
772 "inlet" | "inlet~" => inlets.push(node),
773 "outlet" | "outlet~" => outlets.push(node),
774 _ => others.push(node),
775 }
776 }
777
778 let mut result = Vec::with_capacity(graph.nodes.len());
780 result.extend(inlets);
781 result.extend(others);
782 result.extend(outlets);
783 result
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789 use flutmax_sema::graph::{NodePurity, PatchEdge, PatchNode};
790
791 fn make_minimal_graph() -> PatchGraph {
793 let mut g = PatchGraph::new();
794 g.add_node(PatchNode {
795 id: "osc".into(),
796 object_name: "cycle~".into(),
797 args: vec!["440".into()],
798 num_inlets: 2,
799 num_outlets: 1,
800 is_signal: true,
801 varname: None,
802 hot_inlets: vec![],
803 purity: NodePurity::Unknown,
804 attrs: vec![],
805 code: None,
806 });
807 g.add_node(PatchNode {
808 id: "dac".into(),
809 object_name: "ezdac~".into(),
810 args: vec![],
811 num_inlets: 2,
812 num_outlets: 0,
813 is_signal: true,
814 varname: None,
815 hot_inlets: vec![],
816 purity: NodePurity::Unknown,
817 attrs: vec![],
818 code: None,
819 });
820 g.add_edge(PatchEdge {
821 source_id: "osc".into(),
822 source_outlet: 0,
823 dest_id: "dac".into(),
824 dest_inlet: 0,
825 is_feedback: false,
826 order: None,
827 });
828 g.add_edge(PatchEdge {
829 source_id: "osc".into(),
830 source_outlet: 0,
831 dest_id: "dac".into(),
832 dest_inlet: 1,
833 is_feedback: false,
834 order: None,
835 });
836 g
837 }
838
839 fn make_l2_graph() -> PatchGraph {
841 let mut g = PatchGraph::new();
842 g.add_node(PatchNode {
843 id: "in_freq".into(),
844 object_name: "inlet".into(),
845 args: vec![],
846 num_inlets: 0,
847 num_outlets: 1,
848 is_signal: false,
849 varname: None,
850 hot_inlets: vec![],
851 purity: NodePurity::Unknown,
852 attrs: vec![],
853 code: None,
854 });
855 g.add_node(PatchNode {
856 id: "cycle".into(),
857 object_name: "cycle~".into(),
858 args: vec![],
859 num_inlets: 2,
860 num_outlets: 1,
861 is_signal: true,
862 varname: None,
863 hot_inlets: vec![],
864 purity: NodePurity::Unknown,
865 attrs: vec![],
866 code: None,
867 });
868 g.add_node(PatchNode {
869 id: "mul".into(),
870 object_name: "*~".into(),
871 args: vec!["0.5".into()],
872 num_inlets: 2,
873 num_outlets: 1,
874 is_signal: true,
875 varname: None,
876 hot_inlets: vec![],
877 purity: NodePurity::Unknown,
878 attrs: vec![],
879 code: None,
880 });
881 g.add_node(PatchNode {
882 id: "out_audio".into(),
883 object_name: "outlet~".into(),
884 args: vec![],
885 num_inlets: 1,
886 num_outlets: 0,
887 is_signal: true,
888 varname: None,
889 hot_inlets: vec![],
890 purity: NodePurity::Unknown,
891 attrs: vec![],
892 code: None,
893 });
894 g.add_edge(PatchEdge {
895 source_id: "in_freq".into(),
896 source_outlet: 0,
897 dest_id: "cycle".into(),
898 dest_inlet: 0,
899 is_feedback: false,
900 order: None,
901 });
902 g.add_edge(PatchEdge {
903 source_id: "cycle".into(),
904 source_outlet: 0,
905 dest_id: "mul".into(),
906 dest_inlet: 0,
907 is_feedback: false,
908 order: None,
909 });
910 g.add_edge(PatchEdge {
911 source_id: "mul".into(),
912 source_outlet: 0,
913 dest_id: "out_audio".into(),
914 dest_inlet: 0,
915 is_feedback: false,
916 order: None,
917 });
918 g
919 }
920
921 #[test]
922 fn test_generate_valid_json() {
923 let graph = make_minimal_graph();
924 let json_str = generate(&graph).unwrap();
925
926 let parsed: Value = serde_json::from_str(&json_str).unwrap();
928 assert!(parsed.is_object());
929 assert!(parsed.get("patcher").is_some());
930 }
931
932 #[test]
933 fn test_patcher_fixed_fields() {
934 let graph = make_minimal_graph();
935 let json_str = generate(&graph).unwrap();
936 let parsed: Value = serde_json::from_str(&json_str).unwrap();
937 let patcher = parsed.get("patcher").unwrap();
938
939 assert_eq!(patcher["fileversion"], 1);
940 assert_eq!(patcher["appversion"]["major"], 8);
941 assert_eq!(patcher["appversion"]["minor"], 6);
942 assert_eq!(patcher["classnamespace"], "box");
943 assert_eq!(patcher["default_fontname"], "Arial");
944 assert_eq!(patcher["autosave"], 0);
945 }
946
947 #[test]
948 fn test_boxes_count() {
949 let graph = make_minimal_graph();
950 let json_str = generate(&graph).unwrap();
951 let parsed: Value = serde_json::from_str(&json_str).unwrap();
952 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
953 assert_eq!(boxes.len(), 2);
954 }
955
956 #[test]
957 fn test_box_structure() {
958 let graph = make_minimal_graph();
959 let json_str = generate(&graph).unwrap();
960 let parsed: Value = serde_json::from_str(&json_str).unwrap();
961 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
962
963 let cycle_box = &boxes[0]["box"];
965 assert_eq!(cycle_box["id"], "obj-1");
966 assert_eq!(cycle_box["maxclass"], "newobj");
967 assert_eq!(cycle_box["numinlets"], 2);
968 assert_eq!(cycle_box["numoutlets"], 1);
969 assert_eq!(cycle_box["text"], "cycle~ 440");
970 let outlettype = cycle_box["outlettype"].as_array().unwrap();
971 assert_eq!(outlettype.len(), 1);
972 assert_eq!(outlettype[0], "signal");
973
974 let dac_box = &boxes[1]["box"];
976 assert_eq!(dac_box["id"], "obj-2");
977 assert_eq!(dac_box["maxclass"], "ezdac~");
978 assert_eq!(dac_box["numinlets"], 2);
979 assert_eq!(dac_box["numoutlets"], 0);
980 assert!(dac_box.get("outlettype").is_none());
982 }
983
984 #[test]
985 fn test_lines_count() {
986 let graph = make_minimal_graph();
987 let json_str = generate(&graph).unwrap();
988 let parsed: Value = serde_json::from_str(&json_str).unwrap();
989 let lines = parsed["patcher"]["lines"].as_array().unwrap();
990 assert_eq!(lines.len(), 2);
991 }
992
993 #[test]
994 fn test_line_structure() {
995 let graph = make_minimal_graph();
996 let json_str = generate(&graph).unwrap();
997 let parsed: Value = serde_json::from_str(&json_str).unwrap();
998 let lines = parsed["patcher"]["lines"].as_array().unwrap();
999
1000 for line in lines {
1002 let patchline = &line["patchline"];
1003 let source = patchline["source"].as_array().unwrap();
1004 let dest = patchline["destination"].as_array().unwrap();
1005
1006 assert_eq!(source[0], "obj-1");
1007 assert_eq!(source[1], 0);
1008 assert_eq!(dest[0], "obj-2");
1009 let inlet = dest[1].as_u64().unwrap();
1011 assert!(inlet == 0 || inlet == 1);
1012 }
1013 }
1014
1015 #[test]
1016 fn test_patching_rect_layout() {
1017 let graph = make_minimal_graph();
1018 let json_str = generate(&graph).unwrap();
1019 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1020 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1021
1022 let rect0 = boxes[0]["box"]["patching_rect"].as_array().unwrap();
1023 let rect1 = boxes[1]["box"]["patching_rect"].as_array().unwrap();
1024
1025 let x0 = rect0[0].as_f64().unwrap();
1027 let x1 = rect1[0].as_f64().unwrap();
1028 assert_eq!(x0, x1, "linear chain nodes should share the same x");
1029
1030 let y0 = rect0[1].as_f64().unwrap();
1032 let y1 = rect1[1].as_f64().unwrap();
1033 assert!(y1 > y0, "downstream node should have larger y");
1034 }
1035
1036 #[test]
1037 fn test_l2_topological_order() {
1038 let graph = make_l2_graph();
1039 let json_str = generate(&graph).unwrap();
1040 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1041 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1042
1043 assert_eq!(boxes[0]["box"]["maxclass"], "inlet");
1045 assert_eq!(boxes[1]["box"]["text"], "cycle~");
1046 assert_eq!(boxes[2]["box"]["text"], "*~ 0.5");
1047 assert_eq!(boxes[3]["box"]["maxclass"], "outlet");
1048 }
1049
1050 #[test]
1051 fn test_inlet_outlettype() {
1052 let graph = make_l2_graph();
1053 let json_str = generate(&graph).unwrap();
1054 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1055 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1056
1057 let inlet_box = &boxes[0]["box"];
1059 assert_eq!(inlet_box["maxclass"], "inlet");
1060 let outlettype = inlet_box["outlettype"].as_array().unwrap();
1061 assert_eq!(outlettype.len(), 1);
1062 assert_eq!(outlettype[0], "");
1063 }
1064
1065 #[test]
1066 fn test_outlet_tilde_maxclass() {
1067 let graph = make_l2_graph();
1068 let json_str = generate(&graph).unwrap();
1069 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1070 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1071
1072 let outlet_box = &boxes[3]["box"];
1074 assert_eq!(outlet_box["maxclass"], "outlet");
1075 assert_eq!(outlet_box["numinlets"], 1);
1076 assert_eq!(outlet_box["numoutlets"], 0);
1077 }
1078
1079 #[test]
1080 fn test_empty_graph() {
1081 let graph = PatchGraph::new();
1082 let json_str = generate(&graph).unwrap();
1083 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1084
1085 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1086 let lines = parsed["patcher"]["lines"].as_array().unwrap();
1087 assert_eq!(boxes.len(), 0);
1088 assert_eq!(lines.len(), 0);
1089 }
1090
1091 #[test]
1092 fn test_dependency_cache_empty() {
1093 let graph = make_minimal_graph();
1094 let json_str = generate(&graph).unwrap();
1095 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1096
1097 let dep_cache = parsed["patcher"]["dependency_cache"].as_array().unwrap();
1098 assert_eq!(dep_cache.len(), 0);
1099 }
1100
1101 #[test]
1102 fn test_build_object_text_no_args() {
1103 let node = PatchNode {
1104 id: "test".into(),
1105 object_name: "cycle~".into(),
1106 args: vec![],
1107 num_inlets: 2,
1108 num_outlets: 1,
1109 is_signal: true,
1110 varname: None,
1111 hot_inlets: vec![],
1112 purity: NodePurity::Unknown,
1113 attrs: vec![],
1114 code: None,
1115 };
1116 assert_eq!(build_object_text(&node), "cycle~");
1117 }
1118
1119 #[test]
1120 fn test_build_object_text_with_args() {
1121 let node = PatchNode {
1122 id: "test".into(),
1123 object_name: "cycle~".into(),
1124 args: vec!["440".into()],
1125 num_inlets: 2,
1126 num_outlets: 1,
1127 is_signal: true,
1128 varname: None,
1129 hot_inlets: vec![],
1130 purity: NodePurity::Unknown,
1131 attrs: vec![],
1132 code: None,
1133 };
1134 assert_eq!(build_object_text(&node), "cycle~ 440");
1135 }
1136
1137 #[test]
1138 fn test_build_object_text_multiple_args() {
1139 let node = PatchNode {
1140 id: "test".into(),
1141 object_name: "trigger".into(),
1142 args: vec!["b".into(), "b".into(), "b".into()],
1143 num_inlets: 1,
1144 num_outlets: 3,
1145 is_signal: false,
1146 varname: None,
1147 hot_inlets: vec![],
1148 purity: NodePurity::Unknown,
1149 attrs: vec![],
1150 code: None,
1151 };
1152 assert_eq!(build_object_text(&node), "trigger b b b");
1153 }
1154
1155 #[test]
1156 fn test_classify_maxclass_inlet() {
1157 let node = PatchNode {
1158 id: "test".into(),
1159 object_name: "inlet".into(),
1160 args: vec![],
1161 num_inlets: 0,
1162 num_outlets: 1,
1163 is_signal: false,
1164 varname: None,
1165 hot_inlets: vec![],
1166 purity: NodePurity::Unknown,
1167 attrs: vec![],
1168 code: None,
1169 };
1170 let (maxclass, _, _) = classify_maxclass(&node, "box");
1171 assert_eq!(maxclass, "inlet");
1172 }
1173
1174 #[test]
1175 fn test_classify_maxclass_inlet_tilde() {
1176 let node = PatchNode {
1179 id: "test".into(),
1180 object_name: "inlet~".into(),
1181 args: vec![],
1182 num_inlets: 1,
1183 num_outlets: 1,
1184 is_signal: true,
1185 varname: None,
1186 hot_inlets: vec![],
1187 purity: NodePurity::Unknown,
1188 attrs: vec![],
1189 code: None,
1190 };
1191 let (maxclass, _, _) = classify_maxclass(&node, "box");
1192 assert_eq!(maxclass, "inlet");
1193 }
1194
1195 #[test]
1196 fn test_classify_maxclass_newobj() {
1197 let node = PatchNode {
1198 id: "test".into(),
1199 object_name: "cycle~".into(),
1200 args: vec!["440".into()],
1201 num_inlets: 2,
1202 num_outlets: 1,
1203 is_signal: true,
1204 varname: None,
1205 hot_inlets: vec![],
1206 purity: NodePurity::Unknown,
1207 attrs: vec![],
1208 code: None,
1209 };
1210 let (maxclass, _, _) = classify_maxclass(&node, "box");
1211 assert_eq!(maxclass, "newobj");
1212 }
1213
1214 #[test]
1215 fn test_compute_outlettype_signal() {
1216 let node = PatchNode {
1217 id: "test".into(),
1218 object_name: "cycle~".into(),
1219 args: vec![],
1220 num_inlets: 2,
1221 num_outlets: 1,
1222 is_signal: true,
1223 varname: None,
1224 hot_inlets: vec![],
1225 purity: NodePurity::Unknown,
1226 attrs: vec![],
1227 code: None,
1228 };
1229 let types = compute_outlettype(&node, false, false);
1230 assert_eq!(types, vec!["signal"]);
1231 }
1232
1233 #[test]
1234 fn test_compute_outlettype_no_outlets() {
1235 let node = PatchNode {
1236 id: "test".into(),
1237 object_name: "ezdac~".into(),
1238 args: vec![],
1239 num_inlets: 2,
1240 num_outlets: 0,
1241 is_signal: true,
1242 varname: None,
1243 hot_inlets: vec![],
1244 purity: NodePurity::Unknown,
1245 attrs: vec![],
1246 code: None,
1247 };
1248 let types = compute_outlettype(&node, false, false);
1249 assert!(types.is_empty());
1250 }
1251
1252 #[test]
1253 fn test_roundtrip_l2() {
1254 use crate::builder::build_graph;
1256 use flutmax_ast::*;
1257
1258 let prog = Program {
1259 in_decls: vec![InDecl {
1260 index: 0,
1261 name: "freq".to_string(),
1262 port_type: PortType::Float,
1263 }],
1264 out_decls: vec![OutDecl {
1265 index: 0,
1266 name: "audio".to_string(),
1267 port_type: PortType::Signal,
1268 value: None,
1269 }],
1270 wires: vec![
1271 Wire {
1272 name: "osc".to_string(),
1273 value: Expr::Call {
1274 object: "cycle~".to_string(),
1275 args: vec![CallArg::positional(Expr::Ref("freq".to_string()))],
1276 },
1277 span: None,
1278 attrs: vec![],
1279 },
1280 Wire {
1281 name: "amp".to_string(),
1282 value: Expr::Call {
1283 object: "mul~".to_string(),
1284 args: vec![
1285 CallArg::positional(Expr::Ref("osc".to_string())),
1286 CallArg::positional(Expr::Lit(LitValue::Float(0.5))),
1287 ],
1288 },
1289 span: None,
1290 attrs: vec![],
1291 },
1292 ],
1293 destructuring_wires: vec![],
1294 msg_decls: vec![],
1295 out_assignments: vec![OutAssignment {
1296 index: 0,
1297 value: Expr::Ref("amp".to_string()),
1298 span: None,
1299 }],
1300 direct_connections: vec![],
1301 feedback_decls: vec![],
1302 feedback_assignments: vec![],
1303 state_decls: vec![],
1304 state_assignments: vec![],
1305 };
1306
1307 let graph = build_graph(&prog).unwrap();
1308 let json_str = generate(&graph).unwrap();
1309 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1310
1311 let patcher = &parsed["patcher"];
1312 let boxes = patcher["boxes"].as_array().unwrap();
1313 let lines = patcher["lines"].as_array().unwrap();
1314
1315 assert_eq!(boxes.len(), 4);
1317 assert_eq!(lines.len(), 3);
1319
1320 assert_eq!(boxes[0]["box"]["maxclass"], "inlet");
1322
1323 assert_eq!(boxes[3]["box"]["maxclass"], "outlet");
1325 }
1326
1327 #[test]
1328 fn test_unique_ids() {
1329 let graph = make_l2_graph();
1330 let json_str = generate(&graph).unwrap();
1331 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1332 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1333
1334 let ids: Vec<&str> = boxes
1335 .iter()
1336 .map(|b| b["box"]["id"].as_str().unwrap())
1337 .collect();
1338
1339 let mut unique_ids = ids.clone();
1341 unique_ids.sort();
1342 unique_ids.dedup();
1343 assert_eq!(ids.len(), unique_ids.len());
1344 }
1345
1346 #[test]
1347 fn test_message_box_output() {
1348 let mut g = PatchGraph::new();
1349 g.add_node(PatchNode {
1350 id: "msg1".into(),
1351 object_name: "message".into(),
1352 args: vec!["bang".into()],
1353 num_inlets: 2,
1354 num_outlets: 1,
1355 is_signal: false,
1356 varname: Some("click".into()),
1357 hot_inlets: vec![true, false],
1358 purity: NodePurity::Stateful,
1359 attrs: vec![],
1360 code: None,
1361 });
1362
1363 let json_str = generate(&g).unwrap();
1364 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1365 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1366
1367 let msg_box = &boxes[0]["box"];
1368 assert_eq!(msg_box["maxclass"], "message");
1369 assert_eq!(msg_box["text"], "bang");
1370 assert_eq!(msg_box["numinlets"], 2);
1371 assert_eq!(msg_box["numoutlets"], 1);
1372 assert_eq!(msg_box["varname"], "click");
1373
1374 let outlettype = msg_box["outlettype"].as_array().unwrap();
1375 assert_eq!(outlettype.len(), 1);
1376 assert_eq!(outlettype[0], "");
1377 }
1378
1379 #[test]
1380 fn test_fanout_patchline_has_order() {
1381 let mut graph = make_minimal_graph();
1383 graph.edges[0].order = Some(0);
1385 graph.edges[1].order = Some(1);
1386
1387 let json_str = generate(&graph).unwrap();
1388 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1389 let lines = parsed["patcher"]["lines"].as_array().unwrap();
1390
1391 for (i, line) in lines.iter().enumerate() {
1393 let patchline = &line["patchline"];
1394 let order = patchline.get("order");
1395 assert!(order.is_some(), "patchline {} should have order field", i);
1396 assert_eq!(order.unwrap().as_u64().unwrap(), i as u64);
1397 }
1398 }
1399
1400 #[test]
1401 fn test_non_fanout_patchline_no_order() {
1402 let graph = make_l2_graph();
1404 let json_str = generate(&graph).unwrap();
1405 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1406 let lines = parsed["patcher"]["lines"].as_array().unwrap();
1407
1408 for (i, line) in lines.iter().enumerate() {
1409 let patchline = &line["patchline"];
1410 assert!(
1411 patchline.get("order").is_none(),
1412 "patchline {} should not have order field",
1413 i
1414 );
1415 }
1416 }
1417
1418 #[test]
1423 fn test_newobj_attrs_in_text() {
1424 let mut g = PatchGraph::new();
1426 g.add_node(PatchNode {
1427 id: "osc".into(),
1428 object_name: "cycle~".into(),
1429 args: vec!["440".into()],
1430 num_inlets: 2,
1431 num_outlets: 1,
1432 is_signal: true,
1433 varname: Some("osc".into()),
1434 hot_inlets: vec![],
1435 purity: NodePurity::Unknown,
1436 attrs: vec![("phase".into(), "0.5".into())],
1437 code: None,
1438 });
1439
1440 let json_str = generate(&g).unwrap();
1441 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1442 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1443
1444 let text = boxes[0]["box"]["text"].as_str().unwrap();
1445 assert_eq!(text, "cycle~ 440 @phase 0.5");
1446 }
1447
1448 #[test]
1449 fn test_newobj_multiple_attrs_in_text() {
1450 let mut g = PatchGraph::new();
1451 g.add_node(PatchNode {
1452 id: "osc".into(),
1453 object_name: "cycle~".into(),
1454 args: vec![],
1455 num_inlets: 2,
1456 num_outlets: 1,
1457 is_signal: true,
1458 varname: None,
1459 hot_inlets: vec![],
1460 purity: NodePurity::Unknown,
1461 attrs: vec![
1462 ("frequency".into(), "440.".into()),
1463 ("phase".into(), "0.5".into()),
1464 ],
1465 code: None,
1466 });
1467
1468 let json_str = generate(&g).unwrap();
1469 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1470 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1471
1472 let text = boxes[0]["box"]["text"].as_str().unwrap();
1473 assert_eq!(text, "cycle~ @frequency 440. @phase 0.5");
1474 }
1475
1476 #[test]
1477 fn test_ui_object_attrs_as_fields() {
1478 let mut g = PatchGraph::new();
1480 g.add_node(PatchNode {
1481 id: "fnum".into(),
1482 object_name: "flonum".into(),
1483 args: vec![],
1484 num_inlets: 1,
1485 num_outlets: 2,
1486 is_signal: false,
1487 varname: Some("w".into()),
1488 hot_inlets: vec![],
1489 purity: NodePurity::Unknown,
1490 attrs: vec![
1491 ("minimum".into(), "0.".into()),
1492 ("maximum".into(), "100.".into()),
1493 ],
1494 code: None,
1495 });
1496
1497 let json_str = generate(&g).unwrap();
1498 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1499 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1500 let box_obj = &boxes[0]["box"];
1501
1502 assert_eq!(box_obj["maxclass"], "flonum");
1503 assert_eq!(box_obj["minimum"], 0.0);
1504 assert_eq!(box_obj["maximum"], 100.0);
1505 assert!(box_obj.get("text").is_none());
1507 }
1508
1509 #[test]
1510 fn test_ui_object_string_attr() {
1511 let mut g = PatchGraph::new();
1512 g.add_node(PatchNode {
1513 id: "dial".into(),
1514 object_name: "live.dial".into(),
1515 args: vec![],
1516 num_inlets: 1,
1517 num_outlets: 2,
1518 is_signal: false,
1519 varname: None,
1520 hot_inlets: vec![],
1521 purity: NodePurity::Unknown,
1522 attrs: vec![("parameter_longname".into(), "Cutoff".into())],
1523 code: None,
1524 });
1525
1526 let json_str = generate(&g).unwrap();
1527 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1528 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1529 let box_obj = &boxes[0]["box"];
1530
1531 assert_eq!(box_obj["maxclass"], "live.dial");
1532 assert_eq!(box_obj["parameter_longname"], "Cutoff");
1533 }
1534
1535 #[test]
1536 fn test_no_attrs_unchanged() {
1537 let mut g = PatchGraph::new();
1539 g.add_node(PatchNode {
1540 id: "osc".into(),
1541 object_name: "cycle~".into(),
1542 args: vec!["440".into()],
1543 num_inlets: 2,
1544 num_outlets: 1,
1545 is_signal: true,
1546 varname: None,
1547 hot_inlets: vec![],
1548 purity: NodePurity::Unknown,
1549 attrs: vec![],
1550 code: None,
1551 });
1552
1553 let json_str = generate(&g).unwrap();
1554 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1555 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1556
1557 let text = boxes[0]["box"]["text"].as_str().unwrap();
1558 assert_eq!(text, "cycle~ 440");
1559 }
1560
1561 fn make_rnbo_graph() -> PatchGraph {
1567 let mut g = PatchGraph::new();
1568 g.add_node(PatchNode {
1569 id: "in_freq".into(),
1570 object_name: "inlet".into(),
1571 args: vec![],
1572 num_inlets: 0,
1573 num_outlets: 1,
1574 is_signal: false,
1575 varname: Some("freq".into()),
1576 hot_inlets: vec![],
1577 purity: NodePurity::Unknown,
1578 attrs: vec![],
1579 code: None,
1580 });
1581 g.add_node(PatchNode {
1582 id: "osc".into(),
1583 object_name: "cycle~".into(),
1584 args: vec!["440".into()],
1585 num_inlets: 2,
1586 num_outlets: 1,
1587 is_signal: true,
1588 varname: Some("osc".into()),
1589 hot_inlets: vec![],
1590 purity: NodePurity::Unknown,
1591 attrs: vec![],
1592 code: None,
1593 });
1594 g.add_node(PatchNode {
1595 id: "out_audio".into(),
1596 object_name: "outlet~".into(),
1597 args: vec![],
1598 num_inlets: 1,
1599 num_outlets: 0,
1600 is_signal: true,
1601 varname: None,
1602 hot_inlets: vec![],
1603 purity: NodePurity::Unknown,
1604 attrs: vec![],
1605 code: None,
1606 });
1607 g.add_edge(PatchEdge {
1608 source_id: "in_freq".into(),
1609 source_outlet: 0,
1610 dest_id: "osc".into(),
1611 dest_inlet: 0,
1612 is_feedback: false,
1613 order: None,
1614 });
1615 g.add_edge(PatchEdge {
1616 source_id: "osc".into(),
1617 source_outlet: 0,
1618 dest_id: "out_audio".into(),
1619 dest_inlet: 0,
1620 is_feedback: false,
1621 order: None,
1622 });
1623 g
1624 }
1625
1626 fn rnbo_opts() -> GenerateOptions {
1627 GenerateOptions {
1628 classnamespace: "rnbo".to_string(),
1629 }
1630 }
1631
1632 #[test]
1633 fn test_generate_rnbo_classnamespace() {
1634 let graph = make_rnbo_graph();
1635 let json_str = generate_with_options(&graph, &rnbo_opts()).unwrap();
1636 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1637 let patcher = parsed.get("patcher").unwrap();
1638
1639 assert_eq!(patcher["classnamespace"], "rnbo");
1640 }
1641
1642 #[test]
1643 fn test_rnbo_inport_outport() {
1644 let graph = make_rnbo_graph();
1645 let json_str = generate_with_options(&graph, &rnbo_opts()).unwrap();
1646 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1647 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1648
1649 let inlet_box = &boxes[0]["box"];
1651 assert_eq!(inlet_box["maxclass"], "newobj");
1652 assert_eq!(inlet_box["text"], "inport freq");
1653
1654 let outlet_box = &boxes[2]["box"];
1656 assert_eq!(outlet_box["maxclass"], "newobj");
1657 assert_eq!(outlet_box["text"], "out~ 1");
1658 }
1659
1660 #[test]
1661 fn test_rnbo_signal_io() {
1662 let mut g = PatchGraph::new();
1664 g.add_node(PatchNode {
1665 id: "in_sig".into(),
1666 object_name: "inlet~".into(),
1667 args: vec![],
1668 num_inlets: 1,
1669 num_outlets: 1,
1670 is_signal: true,
1671 varname: None,
1672 hot_inlets: vec![],
1673 purity: NodePurity::Unknown,
1674 attrs: vec![],
1675 code: None,
1676 });
1677 g.add_node(PatchNode {
1678 id: "out_sig".into(),
1679 object_name: "outlet~".into(),
1680 args: vec![],
1681 num_inlets: 1,
1682 num_outlets: 0,
1683 is_signal: true,
1684 varname: None,
1685 hot_inlets: vec![],
1686 purity: NodePurity::Unknown,
1687 attrs: vec![],
1688 code: None,
1689 });
1690 g.add_edge(PatchEdge {
1691 source_id: "in_sig".into(),
1692 source_outlet: 0,
1693 dest_id: "out_sig".into(),
1694 dest_inlet: 0,
1695 is_feedback: false,
1696 order: None,
1697 });
1698
1699 let json_str = generate_with_options(&g, &rnbo_opts()).unwrap();
1700 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1701 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1702
1703 let inlet_box = &boxes[0]["box"];
1705 assert_eq!(inlet_box["maxclass"], "newobj");
1706 assert_eq!(inlet_box["text"], "in~ 1");
1707 let outlettype = inlet_box["outlettype"].as_array().unwrap();
1708 assert_eq!(outlettype, &[json!("signal")]);
1709
1710 let outlet_box = &boxes[1]["box"];
1712 assert_eq!(outlet_box["maxclass"], "newobj");
1713 assert_eq!(outlet_box["text"], "out~ 1");
1714 assert_eq!(outlet_box["numoutlets"], 0);
1716 assert!(outlet_box.get("outlettype").is_none());
1717 }
1718
1719 #[test]
1720 fn test_rnbo_serial() {
1721 let graph = make_rnbo_graph();
1722 let json_str = generate_with_options(&graph, &rnbo_opts()).unwrap();
1723 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1724 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1725
1726 for (i, boxval) in boxes.iter().enumerate() {
1728 let b = &boxval["box"];
1729 let serial = b["rnbo_serial"].as_u64().unwrap();
1730 assert_eq!(serial, (i + 1) as u64, "rnbo_serial for box {}", i);
1731
1732 let uniqueid = b["rnbo_uniqueid"].as_str().unwrap();
1733 assert!(!uniqueid.is_empty(), "rnbo_uniqueid should not be empty");
1734 }
1735
1736 let inlet_uid = boxes[0]["box"]["rnbo_uniqueid"].as_str().unwrap();
1738 assert_eq!(inlet_uid, "inlet_obj-1");
1739
1740 let cycle_uid = boxes[1]["box"]["rnbo_uniqueid"].as_str().unwrap();
1741 assert_eq!(cycle_uid, "cycle_tilde_obj-2");
1742
1743 let outlet_uid = boxes[2]["box"]["rnbo_uniqueid"].as_str().unwrap();
1744 assert_eq!(outlet_uid, "outlet_tilde_obj-3");
1745 }
1746
1747 #[test]
1748 fn test_standard_unchanged() {
1749 let graph = make_rnbo_graph();
1751 let json_str = generate(&graph).unwrap();
1752 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1753 let patcher = parsed.get("patcher").unwrap();
1754
1755 assert_eq!(patcher["classnamespace"], "box");
1757
1758 let boxes = patcher["boxes"].as_array().unwrap();
1759
1760 let inlet_box = &boxes[0]["box"];
1762 assert_eq!(inlet_box["maxclass"], "inlet");
1763 assert!(inlet_box.get("text").is_none());
1765
1766 let outlet_box = &boxes[2]["box"];
1768 assert_eq!(outlet_box["maxclass"], "outlet");
1769
1770 for boxval in boxes {
1772 let b = &boxval["box"];
1773 assert!(
1774 b.get("rnbo_serial").is_none(),
1775 "standard mode should not have rnbo_serial"
1776 );
1777 assert!(
1778 b.get("rnbo_uniqueid").is_none(),
1779 "standard mode should not have rnbo_uniqueid"
1780 );
1781 }
1782 }
1783
1784 #[test]
1785 fn test_rnbo_control_outlet() {
1786 let mut g = PatchGraph::new();
1788 g.add_node(PatchNode {
1789 id: "out_ctrl".into(),
1790 object_name: "outlet".into(),
1791 args: vec![],
1792 num_inlets: 1,
1793 num_outlets: 1,
1794 is_signal: false,
1795 varname: Some("result".into()),
1796 hot_inlets: vec![],
1797 purity: NodePurity::Unknown,
1798 attrs: vec![],
1799 code: None,
1800 });
1801
1802 let json_str = generate_with_options(&g, &rnbo_opts()).unwrap();
1803 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1804 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1805
1806 let outlet_box = &boxes[0]["box"];
1807 assert_eq!(outlet_box["maxclass"], "newobj");
1808 assert_eq!(outlet_box["text"], "outport result");
1809 assert_eq!(outlet_box["numoutlets"], 0);
1811 }
1812
1813 #[test]
1814 fn test_rnbo_inport_fallback_name() {
1815 let mut g = PatchGraph::new();
1817 g.add_node(PatchNode {
1818 id: "in_unnamed".into(),
1819 object_name: "inlet".into(),
1820 args: vec![],
1821 num_inlets: 0,
1822 num_outlets: 1,
1823 is_signal: false,
1824 varname: None,
1825 hot_inlets: vec![],
1826 purity: NodePurity::Unknown,
1827 attrs: vec![],
1828 code: None,
1829 });
1830
1831 let json_str = generate_with_options(&g, &rnbo_opts()).unwrap();
1832 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1833 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1834
1835 let inlet_box = &boxes[0]["box"];
1836 assert_eq!(inlet_box["text"], "inport port_0");
1837 }
1838
1839 #[test]
1844 fn test_classify_maxclass_codebox() {
1845 let node = PatchNode {
1847 id: "cb1".into(),
1848 object_name: "v8.codebox".into(),
1849 args: vec![],
1850 num_inlets: 1,
1851 num_outlets: 1,
1852 is_signal: false,
1853 varname: None,
1854 hot_inlets: vec![],
1855 purity: NodePurity::Unknown,
1856 attrs: vec![],
1857 code: None,
1858 };
1859 let (maxclass, width, height) = classify_maxclass(&node, "box");
1860 assert_eq!(maxclass, "v8.codebox");
1861 assert_eq!(width, 200.0);
1862 assert_eq!(height, 100.0);
1863
1864 let node2 = PatchNode {
1866 id: "cb2".into(),
1867 object_name: "codebox".into(),
1868 args: vec![],
1869 num_inlets: 1,
1870 num_outlets: 1,
1871 is_signal: false,
1872 varname: None,
1873 hot_inlets: vec![],
1874 purity: NodePurity::Unknown,
1875 attrs: vec![],
1876 code: None,
1877 };
1878 let (maxclass2, _, _) = classify_maxclass(&node2, "box");
1879 assert_eq!(maxclass2, "codebox");
1880 }
1881
1882 #[test]
1883 fn test_build_box_codebox_with_code() {
1884 let node = PatchNode {
1886 id: "cb1".into(),
1887 object_name: "v8.codebox".into(),
1888 args: vec![],
1889 num_inlets: 1,
1890 num_outlets: 1,
1891 is_signal: false,
1892 varname: None,
1893 hot_inlets: vec![],
1894 purity: NodePurity::Unknown,
1895 attrs: vec![],
1896 code: Some("function bang() { outlet(0, 42); }".into()),
1897 };
1898
1899 let box_json = build_box(
1900 &node,
1901 &BoxContext {
1902 id: "obj-1",
1903 x: 100.0,
1904 y: 50.0,
1905 classnamespace: "box",
1906 serial: 1,
1907 port_index: None,
1908 ui_data: None,
1909 },
1910 );
1911 let box_obj = &box_json["box"];
1912
1913 assert_eq!(box_obj["maxclass"], "v8.codebox");
1914 assert_eq!(box_obj["code"], "function bang() { outlet(0, 42); }");
1915 assert_eq!(box_obj["filename"], "none");
1916 assert_eq!(box_obj["text"], "");
1917 }
1918
1919 #[test]
1920 fn test_build_box_codebox_without_code() {
1921 let node = PatchNode {
1923 id: "cb1".into(),
1924 object_name: "codebox".into(),
1925 args: vec![],
1926 num_inlets: 1,
1927 num_outlets: 1,
1928 is_signal: false,
1929 varname: None,
1930 hot_inlets: vec![],
1931 purity: NodePurity::Unknown,
1932 attrs: vec![],
1933 code: None,
1934 };
1935
1936 let box_json = build_box(
1937 &node,
1938 &BoxContext {
1939 id: "obj-1",
1940 x: 100.0,
1941 y: 50.0,
1942 classnamespace: "box",
1943 serial: 1,
1944 port_index: None,
1945 ui_data: None,
1946 },
1947 );
1948 let box_obj = &box_json["box"];
1949
1950 assert_eq!(box_obj["maxclass"], "codebox");
1951 assert!(box_obj.get("code").is_none());
1952 assert!(box_obj.get("filename").is_none());
1953 }
1954
1955 #[test]
1956 fn test_standard_codegen_unchanged() {
1957 let mut g = PatchGraph::new();
1959 g.add_node(PatchNode {
1960 id: "osc".into(),
1961 object_name: "cycle~".into(),
1962 args: vec!["440".into()],
1963 num_inlets: 2,
1964 num_outlets: 1,
1965 is_signal: true,
1966 varname: Some("osc".into()),
1967 hot_inlets: vec![],
1968 purity: NodePurity::Unknown,
1969 attrs: vec![],
1970 code: None,
1971 });
1972
1973 let json_str = generate(&g).unwrap();
1974 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1975 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1976 assert_eq!(boxes.len(), 1);
1977 assert_eq!(boxes[0]["box"]["maxclass"], "newobj");
1978 assert_eq!(boxes[0]["box"]["text"], "cycle~ 440");
1979 assert!(boxes[0]["box"].get("code").is_none());
1981 }
1982
1983 #[test]
1984 fn test_gen_mode_classify_inlet_outlet() {
1985 let inlet_node = PatchNode {
1987 id: "in".into(),
1988 object_name: "inlet~".into(),
1989 args: vec![],
1990 num_inlets: 0,
1991 num_outlets: 1,
1992 is_signal: true,
1993 varname: None,
1994 hot_inlets: vec![],
1995 purity: NodePurity::Unknown,
1996 attrs: vec![],
1997 code: None,
1998 };
1999 let (maxclass, _, _) = classify_maxclass(&inlet_node, "dsp.gen");
2000 assert_eq!(maxclass, "newobj");
2001
2002 let outlet_node = PatchNode {
2003 id: "out".into(),
2004 object_name: "outlet~".into(),
2005 args: vec![],
2006 num_inlets: 1,
2007 num_outlets: 0,
2008 is_signal: true,
2009 varname: None,
2010 hot_inlets: vec![],
2011 purity: NodePurity::Unknown,
2012 attrs: vec![],
2013 code: None,
2014 };
2015 let (maxclass, _, _) = classify_maxclass(&outlet_node, "dsp.gen");
2016 assert_eq!(maxclass, "newobj");
2017 }
2018
2019 #[test]
2020 fn test_gen_mode_build_box_text() {
2021 let inlet_node = PatchNode {
2023 id: "in".into(),
2024 object_name: "inlet~".into(),
2025 args: vec![],
2026 num_inlets: 0,
2027 num_outlets: 1,
2028 is_signal: true,
2029 varname: None,
2030 hot_inlets: vec![],
2031 purity: NodePurity::Unknown,
2032 attrs: vec![],
2033 code: None,
2034 };
2035 let box_json = build_box(
2036 &inlet_node,
2037 &BoxContext {
2038 id: "obj-1",
2039 x: 100.0,
2040 y: 50.0,
2041 classnamespace: "dsp.gen",
2042 serial: 1,
2043 port_index: Some(0),
2044 ui_data: None,
2045 },
2046 );
2047 let box_obj = &box_json["box"];
2048 assert_eq!(box_obj["maxclass"], "newobj");
2049 assert_eq!(box_obj["text"], "in 1");
2050 assert!(box_obj.get("rnbo_serial").is_none());
2052
2053 let outlet_node = PatchNode {
2054 id: "out".into(),
2055 object_name: "outlet~".into(),
2056 args: vec![],
2057 num_inlets: 1,
2058 num_outlets: 0,
2059 is_signal: true,
2060 varname: None,
2061 hot_inlets: vec![],
2062 purity: NodePurity::Unknown,
2063 attrs: vec![],
2064 code: None,
2065 };
2066 let box_json = build_box(
2067 &outlet_node,
2068 &BoxContext {
2069 id: "obj-2",
2070 x: 100.0,
2071 y: 120.0,
2072 classnamespace: "dsp.gen",
2073 serial: 2,
2074 port_index: Some(0),
2075 ui_data: None,
2076 },
2077 );
2078 let box_obj = &box_json["box"];
2079 assert_eq!(box_obj["maxclass"], "newobj");
2080 assert_eq!(box_obj["text"], "out 1");
2081 assert_eq!(box_obj["numoutlets"], 0); }
2083
2084 #[test]
2085 fn test_gen_mode_codegen() {
2086 let mut g = PatchGraph::new();
2088 g.add_node(PatchNode {
2089 id: "in1".into(),
2090 object_name: "inlet~".into(),
2091 args: vec![],
2092 num_inlets: 0,
2093 num_outlets: 1,
2094 is_signal: true,
2095 varname: None,
2096 hot_inlets: vec![],
2097 purity: NodePurity::Unknown,
2098 attrs: vec![],
2099 code: None,
2100 });
2101 g.add_node(PatchNode {
2102 id: "mul".into(),
2103 object_name: "*".into(),
2104 args: vec!["0.5".into()],
2105 num_inlets: 2,
2106 num_outlets: 1,
2107 is_signal: false,
2108 varname: None,
2109 hot_inlets: vec![],
2110 purity: NodePurity::Unknown,
2111 attrs: vec![],
2112 code: None,
2113 });
2114 g.add_node(PatchNode {
2115 id: "out1".into(),
2116 object_name: "outlet~".into(),
2117 args: vec![],
2118 num_inlets: 1,
2119 num_outlets: 0,
2120 is_signal: true,
2121 varname: None,
2122 hot_inlets: vec![],
2123 purity: NodePurity::Unknown,
2124 attrs: vec![],
2125 code: None,
2126 });
2127 g.add_edge(PatchEdge {
2128 source_id: "in1".into(),
2129 source_outlet: 0,
2130 dest_id: "mul".into(),
2131 dest_inlet: 0,
2132 is_feedback: false,
2133 order: None,
2134 });
2135 g.add_edge(PatchEdge {
2136 source_id: "mul".into(),
2137 source_outlet: 0,
2138 dest_id: "out1".into(),
2139 dest_inlet: 0,
2140 is_feedback: false,
2141 order: None,
2142 });
2143
2144 let opts = GenerateOptions {
2145 classnamespace: "dsp.gen".to_string(),
2146 };
2147 let json_str = generate_with_options(&g, &opts).unwrap();
2148 let parsed: Value = serde_json::from_str(&json_str).unwrap();
2149
2150 assert_eq!(parsed["patcher"]["classnamespace"], "dsp.gen");
2151
2152 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
2153 assert_eq!(boxes.len(), 3);
2154
2155 assert_eq!(boxes[0]["box"]["maxclass"], "newobj");
2157 assert_eq!(boxes[0]["box"]["text"], "in 1");
2158
2159 assert_eq!(boxes[1]["box"]["maxclass"], "newobj");
2161 assert_eq!(boxes[1]["box"]["text"], "* 0.5");
2162
2163 assert_eq!(boxes[2]["box"]["maxclass"], "newobj");
2165 assert_eq!(boxes[2]["box"]["text"], "out 1");
2166 assert_eq!(boxes[2]["box"]["numoutlets"], 0);
2167 }
2168
2169 #[test]
2172 fn test_ui_data_from_json_basic() {
2173 let json_str = r#"{
2174 "_patcher": { "rect": [50, 50, 800, 600] },
2175 "osc": { "rect": [100, 200, 80, 22] },
2176 "dac": { "rect": [100, 400, 45, 45], "background": 0 }
2177 }"#;
2178 let ui = UiData::from_json(json_str).unwrap();
2179
2180 assert_eq!(ui.patcher["rect"], json!([50, 50, 800, 600]));
2182
2183 assert!(ui.entries.contains_key("osc"));
2185 assert!(ui.entries.contains_key("dac"));
2186 assert!(!ui.entries.contains_key("_patcher"));
2187 assert_eq!(ui.entries["osc"]["rect"], json!([100, 200, 80, 22]));
2188 assert_eq!(ui.entries["dac"]["background"], json!(0));
2189 }
2190
2191 #[test]
2192 fn test_ui_data_from_json_empty() {
2193 let ui = UiData::from_json("{}").unwrap();
2194 assert!(ui.patcher.is_empty());
2195 assert!(ui.entries.is_empty());
2196 }
2197
2198 #[test]
2199 fn test_ui_data_from_json_invalid() {
2200 assert!(UiData::from_json("not json").is_none());
2201 assert!(UiData::from_json("42").is_none());
2202 assert!(UiData::from_json("[]").is_none());
2203 }
2204
2205 #[test]
2206 fn test_ui_data_from_json_no_patcher() {
2207 let json_str = r#"{ "osc": { "rect": [10, 20, 80, 22] } }"#;
2208 let ui = UiData::from_json(json_str).unwrap();
2209 assert!(ui.patcher.is_empty());
2210 assert_eq!(ui.entries.len(), 1);
2211 }
2212
2213 #[test]
2214 fn test_build_box_with_ui_data_rect_override() {
2215 let node = PatchNode {
2216 id: "osc".into(),
2217 object_name: "cycle~".into(),
2218 args: vec!["440".into()],
2219 num_inlets: 2,
2220 num_outlets: 1,
2221 is_signal: true,
2222 varname: Some("osc".into()),
2223 hot_inlets: vec![],
2224 purity: NodePurity::Unknown,
2225 attrs: vec![],
2226 code: None,
2227 };
2228 let ui = UiData::from_json(r#"{ "osc": { "rect": [250, 350, 90, 24] } }"#).unwrap();
2229
2230 let box_json = build_box(
2231 &node,
2232 &BoxContext {
2233 id: "obj-1",
2234 x: 100.0,
2235 y: 50.0,
2236 classnamespace: "box",
2237 serial: 1,
2238 port_index: None,
2239 ui_data: Some(&ui),
2240 },
2241 );
2242 let rect = box_json["box"]["patching_rect"].as_array().unwrap();
2243
2244 assert_eq!(rect[0], json!(250));
2246 assert_eq!(rect[1], json!(350));
2247 assert_eq!(rect[2], json!(90));
2248 assert_eq!(rect[3], json!(24));
2249 }
2250
2251 #[test]
2252 fn test_build_box_with_ui_data_decorative_attrs() {
2253 let node = PatchNode {
2254 id: "osc".into(),
2255 object_name: "cycle~".into(),
2256 args: vec!["440".into()],
2257 num_inlets: 2,
2258 num_outlets: 1,
2259 is_signal: true,
2260 varname: Some("osc".into()),
2261 hot_inlets: vec![],
2262 purity: NodePurity::Unknown,
2263 attrs: vec![],
2264 code: None,
2265 };
2266 let ui = UiData::from_json(
2267 r#"{
2268 "osc": {
2269 "rect": [250, 350, 90, 24],
2270 "background": 0,
2271 "fontsize": 14
2272 }
2273 }"#,
2274 )
2275 .unwrap();
2276
2277 let box_json = build_box(
2278 &node,
2279 &BoxContext {
2280 id: "obj-1",
2281 x: 100.0,
2282 y: 50.0,
2283 classnamespace: "box",
2284 serial: 1,
2285 port_index: None,
2286 ui_data: Some(&ui),
2287 },
2288 );
2289 let box_obj = &box_json["box"];
2290
2291 assert_eq!(box_obj["background"], json!(0));
2293 assert_eq!(box_obj["fontsize"], json!(14));
2294 }
2295
2296 #[test]
2297 fn test_build_box_without_varname_ignores_ui_data() {
2298 let node = PatchNode {
2299 id: "osc".into(),
2300 object_name: "cycle~".into(),
2301 args: vec!["440".into()],
2302 num_inlets: 2,
2303 num_outlets: 1,
2304 is_signal: true,
2305 varname: None, hot_inlets: vec![],
2307 purity: NodePurity::Unknown,
2308 attrs: vec![],
2309 code: None,
2310 };
2311 let ui = UiData::from_json(r#"{ "osc": { "rect": [250, 350, 90, 24] } }"#).unwrap();
2312
2313 let box_json = build_box(
2314 &node,
2315 &BoxContext {
2316 id: "obj-1",
2317 x: 100.0,
2318 y: 50.0,
2319 classnamespace: "box",
2320 serial: 1,
2321 port_index: None,
2322 ui_data: Some(&ui),
2323 },
2324 );
2325 let rect = box_json["box"]["patching_rect"].as_array().unwrap();
2326
2327 assert_eq!(rect[0], json!(100.0));
2329 assert_eq!(rect[1], json!(50.0));
2330 }
2331
2332 #[test]
2333 fn test_build_patcher_with_ui_data_patcher_rect() {
2334 let mut g = PatchGraph::new();
2335 g.add_node(PatchNode {
2336 id: "osc".into(),
2337 object_name: "cycle~".into(),
2338 args: vec!["440".into()],
2339 num_inlets: 2,
2340 num_outlets: 1,
2341 is_signal: true,
2342 varname: Some("osc".into()),
2343 hot_inlets: vec![],
2344 purity: NodePurity::Unknown,
2345 attrs: vec![],
2346 code: None,
2347 });
2348
2349 let ui = UiData::from_json(
2350 r#"{
2351 "_patcher": { "rect": [50, 50, 800, 600] },
2352 "osc": { "rect": [200, 300, 80, 22] }
2353 }"#,
2354 )
2355 .unwrap();
2356
2357 let json_str = generate_with_ui(&g, &GenerateOptions::default(), Some(&ui)).unwrap();
2358 let parsed: Value = serde_json::from_str(&json_str).unwrap();
2359
2360 assert_eq!(parsed["patcher"]["rect"], json!([50, 50, 800, 600]));
2362
2363 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
2365 assert_eq!(boxes[0]["box"]["patching_rect"], json!([200, 300, 80, 22]));
2366 }
2367
2368 #[test]
2369 fn test_generate_with_ui_none_is_same_as_generate() {
2370 let graph = make_minimal_graph();
2371
2372 let json_without = generate(&graph).unwrap();
2373 let json_with_none = generate_with_ui(&graph, &GenerateOptions::default(), None).unwrap();
2374
2375 assert_eq!(json_without, json_with_none);
2377 }
2378}