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 =
429 if (is_rnbo || is_gen) && matches!(node.object_name.as_str(), "outlet" | "outlet~") {
430 0
431 } else if is_rnbo && matches!(node.object_name.as_str(), "inlet" | "inlet~") {
432 1
433 } else {
434 node.num_outlets
435 };
436
437 let effective_num_inlets = if is_rnbo
439 && matches!(
440 node.object_name.as_str(),
441 "inlet" | "inlet~" | "outlet" | "outlet~"
442 ) {
443 1
444 } else {
445 node.num_inlets
446 };
447
448 let mut box_obj = Map::new();
449 box_obj.insert("id".into(), json!(ctx.id));
450 box_obj.insert("maxclass".into(), json!(maxclass));
451 box_obj.insert("numinlets".into(), json!(effective_num_inlets));
452 box_obj.insert("numoutlets".into(), json!(effective_num_outlets));
453
454 if !outlettype.is_empty() {
455 box_obj.insert("outlettype".into(), json!(outlettype));
456 }
457
458 box_obj.insert("patching_rect".into(), json!([ctx.x, ctx.y, width, height]));
459
460 if maxclass == "newobj" {
462 let text = if is_rnbo {
463 match node.object_name.as_str() {
465 "inlet" => {
466 let name = node
467 .varname
468 .clone()
469 .unwrap_or_else(|| format!("port_{}", ctx.port_index.unwrap_or(0)));
470 format!("inport {}", name)
471 }
472 "inlet~" => {
473 let idx = ctx.port_index.unwrap_or(0) + 1; format!("in~ {}", idx)
475 }
476 "outlet" => {
477 let name = node
478 .varname
479 .clone()
480 .unwrap_or_else(|| format!("port_{}", ctx.port_index.unwrap_or(0)));
481 format!("outport {}", name)
482 }
483 "outlet~" => {
484 let idx = ctx.port_index.unwrap_or(0) + 1; format!("out~ {}", idx)
486 }
487 _ => {
488 let mut t = build_object_text(node);
489 if !node.attrs.is_empty() {
490 let attr_str: String = node
491 .attrs
492 .iter()
493 .map(|(k, v)| format!("@{} {}", k, v))
494 .collect::<Vec<_>>()
495 .join(" ");
496 t = format!("{} {}", t, attr_str);
497 }
498 t
499 }
500 }
501 } else if is_gen {
502 match node.object_name.as_str() {
504 "inlet" | "inlet~" => {
505 let idx = ctx.port_index.unwrap_or(0) + 1; format!("in {}", idx)
507 }
508 "outlet" | "outlet~" => {
509 let idx = ctx.port_index.unwrap_or(0) + 1; format!("out {}", idx)
511 }
512 "history" => {
513 let first_arg_is_name = node
517 .args
518 .first()
519 .map(|a| a.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_'))
520 .unwrap_or(false);
521 let mut t = if first_arg_is_name {
522 build_object_text(node)
523 } else {
524 let name = node.varname.clone().unwrap_or_else(|| {
525 let suffix = node.id.trim_start_matches(|c: char| !c.is_ascii_digit());
526 if suffix.is_empty() {
527 format!("h_{}", node.id.replace('-', "_"))
528 } else {
529 format!("h_{}", suffix)
530 }
531 });
532 if node.args.is_empty() {
533 format!("history {}", name)
534 } else {
535 format!("history {} {}", name, node.args.join(" "))
536 }
537 };
538 if !node.attrs.is_empty() {
539 let attr_str: String = node
540 .attrs
541 .iter()
542 .map(|(k, v)| format!("@{} {}", k, v))
543 .collect::<Vec<_>>()
544 .join(" ");
545 t = format!("{} {}", t, attr_str);
546 }
547 t
548 }
549 _ => {
550 let mut t = build_object_text(node);
551 if !node.attrs.is_empty() {
552 let attr_str: String = node
553 .attrs
554 .iter()
555 .map(|(k, v)| format!("@{} {}", k, v))
556 .collect::<Vec<_>>()
557 .join(" ");
558 t = format!("{} {}", t, attr_str);
559 }
560 t
561 }
562 }
563 } else {
564 let mut t = build_object_text(node);
565 if !node.attrs.is_empty() {
567 let attr_str: String = node
568 .attrs
569 .iter()
570 .map(|(k, v)| format!("@{} {}", k, v))
571 .collect::<Vec<_>>()
572 .join(" ");
573 t = format!("{} {}", t, attr_str);
574 }
575 t
576 };
577 box_obj.insert("text".into(), json!(text));
578 } else if maxclass == "message" {
579 let text = if node.args.is_empty() {
581 String::new()
582 } else {
583 node.args.join(" ")
584 };
585 box_obj.insert("text".into(), json!(text));
586 }
587
588 if let Some(ref vn) = node.varname {
590 box_obj.insert("varname".into(), json!(vn));
591 }
592
593 if maxclass != "newobj" && !node.attrs.is_empty() {
595 for (key, value) in &node.attrs {
596 if let Ok(f) = value.parse::<f64>() {
598 box_obj.insert(key.clone(), json!(f));
599 } else {
600 box_obj.insert(key.clone(), json!(value));
601 }
602 }
603 }
604
605 if matches!(maxclass, "v8.codebox" | "codebox") {
607 if let Some(ref code) = node.code {
608 box_obj.insert("code".into(), json!(code));
609 }
610 if maxclass == "v8.codebox" {
611 box_obj.insert("filename".into(), json!("none"));
612 if !box_obj.contains_key("text") {
614 box_obj.insert("text".into(), json!(""));
615 }
616 }
617 }
618
619 if let Some(ui_entry) = ctx
621 .ui_data
622 .and_then(|ui| node.varname.as_ref().and_then(|vn| ui.entries.get(vn)))
623 {
624 if let Some(rect) = ui_entry.get("rect") {
626 box_obj.insert("patching_rect".into(), rect.clone());
627 }
628 if let Some(obj) = ui_entry.as_object() {
630 for (k, v) in obj {
631 if k != "rect" {
632 box_obj.insert(k.clone(), v.clone());
633 }
634 }
635 }
636 }
637
638 if is_rnbo {
640 box_obj.insert("rnbo_serial".into(), json!(ctx.serial));
641 box_obj.insert(
642 "rnbo_uniqueid".into(),
643 json!(format!(
644 "{}_{}",
645 node.object_name.replace('~', "_tilde"),
646 ctx.id
647 )),
648 );
649 }
650
651 json!({ "box": Value::Object(box_obj) })
652}
653
654fn classify_maxclass(node: &PatchNode, classnamespace: &str) -> (&'static str, f64, f64) {
657 let is_rnbo = classnamespace == "rnbo";
658 let is_gen = classnamespace == "dsp.gen";
659 match node.object_name.as_str() {
660 "inlet" | "inlet~" if is_rnbo || is_gen => ("newobj", BOX_WIDTH_NEWOBJ, BOX_HEIGHT_NEWOBJ),
661 "outlet" | "outlet~" if is_rnbo || is_gen => {
662 ("newobj", BOX_WIDTH_NEWOBJ, BOX_HEIGHT_NEWOBJ)
663 }
664 "inlet" => ("inlet", BOX_WIDTH_INLET_OUTLET, BOX_HEIGHT_INLET_OUTLET),
665 "inlet~" => ("inlet", BOX_WIDTH_INLET_OUTLET, BOX_HEIGHT_INLET_OUTLET),
666 "outlet" => ("outlet", BOX_WIDTH_INLET_OUTLET, BOX_HEIGHT_INLET_OUTLET),
667 "outlet~" => ("outlet", BOX_WIDTH_INLET_OUTLET, BOX_HEIGHT_INLET_OUTLET),
668 "ezdac~" => ("ezdac~", BOX_WIDTH_EZDAC, BOX_HEIGHT_EZDAC),
669 "message" => ("message", 50.0, 22.0),
670 "button" => ("button", 50.0, 50.0),
671 "flonum" => ("flonum", 80.0, 22.0),
672 "number" => ("number", 50.0, 22.0),
673 "toggle" => ("toggle", 20.0, 20.0),
674 "umenu" => ("umenu", 100.0, 22.0),
675 "panel" => ("panel", 100.0, 50.0),
676 "jsui" => ("jsui", 64.0, 64.0),
677 "textbutton" => ("textbutton", 100.0, 20.0),
679 "live.text" => ("live.text", 44.0, 15.0),
680 "live.dial" => ("live.dial", 47.0, 48.0),
681 "live.toggle" => ("live.toggle", 15.0, 15.0),
682 "live.menu" => ("live.menu", 100.0, 15.0),
683 "live.numbox" => ("live.numbox", 44.0, 15.0),
684 "live.tab" => ("live.tab", 100.0, 20.0),
685 "live.comment" => ("live.comment", 100.0, 18.0),
686 "slider" => ("slider", 20.0, 140.0),
687 "dial" => ("dial", 40.0, 40.0),
688 "multislider" => ("multislider", 120.0, 100.0),
689 "kslider" => ("kslider", 168.0, 53.0),
690 "tab" => ("tab", 200.0, 24.0),
691 "rslider" => ("rslider", 100.0, 22.0),
692 "filtergraph~" => ("filtergraph~", 256.0, 128.0),
693 "spectroscope~" => ("spectroscope~", 300.0, 100.0),
694 "scope~" => ("scope~", 130.0, 130.0),
695 "meter~" => ("meter~", 13.0, 80.0),
696 "gain~" => ("gain~", 22.0, 140.0),
697 "ezadc~" => ("ezadc~", BOX_WIDTH_EZDAC, BOX_HEIGHT_EZDAC),
698 "number~" => ("number~", 56.0, 22.0),
699 "bpatcher" => ("bpatcher", 128.0, 128.0),
700 "fpic" => ("fpic", 100.0, 100.0),
701 "textedit" => ("textedit", 100.0, 22.0),
702 "attrui" => ("attrui", 150.0, 22.0),
703 "nslider" => ("nslider", 50.0, 120.0),
704 "preset" => ("preset", 100.0, 40.0),
705 "v8.codebox" => ("v8.codebox", 200.0, 100.0),
707 "codebox" => ("codebox", 200.0, 100.0),
708 _ => ("newobj", BOX_WIDTH_NEWOBJ, BOX_HEIGHT_NEWOBJ),
709 }
710}
711
712fn compute_outlettype(node: &PatchNode, is_rnbo: bool, is_gen: bool) -> Vec<&'static str> {
714 if (is_rnbo || is_gen) && matches!(node.object_name.as_str(), "outlet" | "outlet~") {
717 return vec![];
718 }
719
720 if node.num_outlets == 0 {
721 return vec![];
722 }
723
724 match node.object_name.as_str() {
725 "inlet" if is_rnbo => vec![""],
727 "inlet~" if is_rnbo => vec!["signal"],
729
730 "inlet" | "inlet~" if is_gen => vec![""],
732
733 "inlet" => vec![""],
735 "inlet~" => vec!["signal"],
736
737 "message" => vec![""],
739
740 "button" => vec!["bang"],
742 "toggle" => vec!["int"],
743 "umenu" => vec!["int", "", ""],
744 "flonum" => vec!["", "bang"],
745 "number" => vec!["", "bang"],
746 "textbutton" => vec!["", "", "int"],
747 "live.text" => vec!["", ""],
748 "live.dial" => vec!["", ""],
749 "live.toggle" => vec![""],
750 "live.menu" => vec!["", "", ""],
751 "live.numbox" => vec!["", ""],
752 "live.tab" => vec!["", "", ""],
753 "live.comment" => vec![],
754 "slider" => vec![""],
755 "dial" => vec![""],
756 "multislider" => vec!["", ""],
757 "kslider" => vec!["", ""],
758 "tab" => vec!["", "", ""],
759 "rslider" => vec!["", ""],
760 "bpatcher" => {
761 vec![""; node.num_outlets as usize]
763 }
764
765 name if name.ends_with('~') => {
767 let mut types = vec!["signal"];
768 if name == "line~" && node.num_outlets >= 2 {
770 types = vec!["signal", "bang"];
771 }
772 while types.len() < node.num_outlets as usize {
774 types.push("signal");
775 }
776 types.truncate(node.num_outlets as usize);
777 types
778 }
779
780 "v8.codebox" | "codebox" => {
782 vec![""; node.num_outlets as usize]
783 }
784
785 "trigger" | "t" => {
787 vec![""; node.num_outlets as usize]
789 }
790
791 _ => {
792 if node.is_signal {
794 vec!["signal"; node.num_outlets as usize]
795 } else {
796 vec![""; node.num_outlets as usize]
798 }
799 }
800 }
801}
802
803fn build_object_text(node: &PatchNode) -> String {
806 if node.args.is_empty() {
807 node.object_name.clone()
808 } else {
809 format!("{} {}", node.object_name, node.args.join(" "))
810 }
811}
812
813fn topological_order(graph: &PatchGraph) -> Vec<&PatchNode> {
817 let mut inlets: Vec<&PatchNode> = Vec::new();
818 let mut outlets: Vec<&PatchNode> = Vec::new();
819 let mut others: Vec<&PatchNode> = Vec::new();
820
821 for node in &graph.nodes {
822 match node.object_name.as_str() {
823 "inlet" | "inlet~" => inlets.push(node),
824 "outlet" | "outlet~" => outlets.push(node),
825 _ => others.push(node),
826 }
827 }
828
829 let mut result = Vec::with_capacity(graph.nodes.len());
831 result.extend(inlets);
832 result.extend(others);
833 result.extend(outlets);
834 result
835}
836
837#[cfg(test)]
838mod tests {
839 use super::*;
840 use flutmax_sema::graph::{NodePurity, PatchEdge, PatchNode};
841
842 fn make_minimal_graph() -> PatchGraph {
844 let mut g = PatchGraph::new();
845 g.add_node(PatchNode {
846 id: "osc".into(),
847 object_name: "cycle~".into(),
848 args: vec!["440".into()],
849 num_inlets: 2,
850 num_outlets: 1,
851 is_signal: true,
852 varname: None,
853 hot_inlets: vec![],
854 purity: NodePurity::Unknown,
855 attrs: vec![],
856 code: None,
857 });
858 g.add_node(PatchNode {
859 id: "dac".into(),
860 object_name: "ezdac~".into(),
861 args: vec![],
862 num_inlets: 2,
863 num_outlets: 0,
864 is_signal: true,
865 varname: None,
866 hot_inlets: vec![],
867 purity: NodePurity::Unknown,
868 attrs: vec![],
869 code: None,
870 });
871 g.add_edge(PatchEdge {
872 source_id: "osc".into(),
873 source_outlet: 0,
874 dest_id: "dac".into(),
875 dest_inlet: 0,
876 is_feedback: false,
877 order: None,
878 });
879 g.add_edge(PatchEdge {
880 source_id: "osc".into(),
881 source_outlet: 0,
882 dest_id: "dac".into(),
883 dest_inlet: 1,
884 is_feedback: false,
885 order: None,
886 });
887 g
888 }
889
890 fn make_l2_graph() -> PatchGraph {
892 let mut g = PatchGraph::new();
893 g.add_node(PatchNode {
894 id: "in_freq".into(),
895 object_name: "inlet".into(),
896 args: vec![],
897 num_inlets: 0,
898 num_outlets: 1,
899 is_signal: false,
900 varname: None,
901 hot_inlets: vec![],
902 purity: NodePurity::Unknown,
903 attrs: vec![],
904 code: None,
905 });
906 g.add_node(PatchNode {
907 id: "cycle".into(),
908 object_name: "cycle~".into(),
909 args: vec![],
910 num_inlets: 2,
911 num_outlets: 1,
912 is_signal: true,
913 varname: None,
914 hot_inlets: vec![],
915 purity: NodePurity::Unknown,
916 attrs: vec![],
917 code: None,
918 });
919 g.add_node(PatchNode {
920 id: "mul".into(),
921 object_name: "*~".into(),
922 args: vec!["0.5".into()],
923 num_inlets: 2,
924 num_outlets: 1,
925 is_signal: true,
926 varname: None,
927 hot_inlets: vec![],
928 purity: NodePurity::Unknown,
929 attrs: vec![],
930 code: None,
931 });
932 g.add_node(PatchNode {
933 id: "out_audio".into(),
934 object_name: "outlet~".into(),
935 args: vec![],
936 num_inlets: 1,
937 num_outlets: 0,
938 is_signal: true,
939 varname: None,
940 hot_inlets: vec![],
941 purity: NodePurity::Unknown,
942 attrs: vec![],
943 code: None,
944 });
945 g.add_edge(PatchEdge {
946 source_id: "in_freq".into(),
947 source_outlet: 0,
948 dest_id: "cycle".into(),
949 dest_inlet: 0,
950 is_feedback: false,
951 order: None,
952 });
953 g.add_edge(PatchEdge {
954 source_id: "cycle".into(),
955 source_outlet: 0,
956 dest_id: "mul".into(),
957 dest_inlet: 0,
958 is_feedback: false,
959 order: None,
960 });
961 g.add_edge(PatchEdge {
962 source_id: "mul".into(),
963 source_outlet: 0,
964 dest_id: "out_audio".into(),
965 dest_inlet: 0,
966 is_feedback: false,
967 order: None,
968 });
969 g
970 }
971
972 #[test]
973 fn test_generate_valid_json() {
974 let graph = make_minimal_graph();
975 let json_str = generate(&graph).unwrap();
976
977 let parsed: Value = serde_json::from_str(&json_str).unwrap();
979 assert!(parsed.is_object());
980 assert!(parsed.get("patcher").is_some());
981 }
982
983 #[test]
984 fn test_patcher_fixed_fields() {
985 let graph = make_minimal_graph();
986 let json_str = generate(&graph).unwrap();
987 let parsed: Value = serde_json::from_str(&json_str).unwrap();
988 let patcher = parsed.get("patcher").unwrap();
989
990 assert_eq!(patcher["fileversion"], 1);
991 assert_eq!(patcher["appversion"]["major"], 8);
992 assert_eq!(patcher["appversion"]["minor"], 6);
993 assert_eq!(patcher["classnamespace"], "box");
994 assert_eq!(patcher["default_fontname"], "Arial");
995 assert_eq!(patcher["autosave"], 0);
996 }
997
998 #[test]
999 fn test_boxes_count() {
1000 let graph = make_minimal_graph();
1001 let json_str = generate(&graph).unwrap();
1002 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1003 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1004 assert_eq!(boxes.len(), 2);
1005 }
1006
1007 #[test]
1008 fn test_box_structure() {
1009 let graph = make_minimal_graph();
1010 let json_str = generate(&graph).unwrap();
1011 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1012 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1013
1014 let cycle_box = &boxes[0]["box"];
1016 assert_eq!(cycle_box["id"], "obj-1");
1017 assert_eq!(cycle_box["maxclass"], "newobj");
1018 assert_eq!(cycle_box["numinlets"], 2);
1019 assert_eq!(cycle_box["numoutlets"], 1);
1020 assert_eq!(cycle_box["text"], "cycle~ 440");
1021 let outlettype = cycle_box["outlettype"].as_array().unwrap();
1022 assert_eq!(outlettype.len(), 1);
1023 assert_eq!(outlettype[0], "signal");
1024
1025 let dac_box = &boxes[1]["box"];
1027 assert_eq!(dac_box["id"], "obj-2");
1028 assert_eq!(dac_box["maxclass"], "ezdac~");
1029 assert_eq!(dac_box["numinlets"], 2);
1030 assert_eq!(dac_box["numoutlets"], 0);
1031 assert!(dac_box.get("outlettype").is_none());
1033 }
1034
1035 #[test]
1036 fn test_lines_count() {
1037 let graph = make_minimal_graph();
1038 let json_str = generate(&graph).unwrap();
1039 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1040 let lines = parsed["patcher"]["lines"].as_array().unwrap();
1041 assert_eq!(lines.len(), 2);
1042 }
1043
1044 #[test]
1045 fn test_line_structure() {
1046 let graph = make_minimal_graph();
1047 let json_str = generate(&graph).unwrap();
1048 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1049 let lines = parsed["patcher"]["lines"].as_array().unwrap();
1050
1051 for line in lines {
1053 let patchline = &line["patchline"];
1054 let source = patchline["source"].as_array().unwrap();
1055 let dest = patchline["destination"].as_array().unwrap();
1056
1057 assert_eq!(source[0], "obj-1");
1058 assert_eq!(source[1], 0);
1059 assert_eq!(dest[0], "obj-2");
1060 let inlet = dest[1].as_u64().unwrap();
1062 assert!(inlet == 0 || inlet == 1);
1063 }
1064 }
1065
1066 #[test]
1067 fn test_patching_rect_layout() {
1068 let graph = make_minimal_graph();
1069 let json_str = generate(&graph).unwrap();
1070 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1071 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1072
1073 let rect0 = boxes[0]["box"]["patching_rect"].as_array().unwrap();
1074 let rect1 = boxes[1]["box"]["patching_rect"].as_array().unwrap();
1075
1076 let x0 = rect0[0].as_f64().unwrap();
1078 let x1 = rect1[0].as_f64().unwrap();
1079 assert_eq!(x0, x1, "linear chain nodes should share the same x");
1080
1081 let y0 = rect0[1].as_f64().unwrap();
1083 let y1 = rect1[1].as_f64().unwrap();
1084 assert!(y1 > y0, "downstream node should have larger y");
1085 }
1086
1087 #[test]
1088 fn test_l2_topological_order() {
1089 let graph = make_l2_graph();
1090 let json_str = generate(&graph).unwrap();
1091 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1092 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1093
1094 assert_eq!(boxes[0]["box"]["maxclass"], "inlet");
1096 assert_eq!(boxes[1]["box"]["text"], "cycle~");
1097 assert_eq!(boxes[2]["box"]["text"], "*~ 0.5");
1098 assert_eq!(boxes[3]["box"]["maxclass"], "outlet");
1099 }
1100
1101 #[test]
1102 fn test_inlet_outlettype() {
1103 let graph = make_l2_graph();
1104 let json_str = generate(&graph).unwrap();
1105 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1106 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1107
1108 let inlet_box = &boxes[0]["box"];
1110 assert_eq!(inlet_box["maxclass"], "inlet");
1111 let outlettype = inlet_box["outlettype"].as_array().unwrap();
1112 assert_eq!(outlettype.len(), 1);
1113 assert_eq!(outlettype[0], "");
1114 }
1115
1116 #[test]
1117 fn test_outlet_tilde_maxclass() {
1118 let graph = make_l2_graph();
1119 let json_str = generate(&graph).unwrap();
1120 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1121 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1122
1123 let outlet_box = &boxes[3]["box"];
1125 assert_eq!(outlet_box["maxclass"], "outlet");
1126 assert_eq!(outlet_box["numinlets"], 1);
1127 assert_eq!(outlet_box["numoutlets"], 0);
1128 }
1129
1130 #[test]
1131 fn test_empty_graph() {
1132 let graph = PatchGraph::new();
1133 let json_str = generate(&graph).unwrap();
1134 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1135
1136 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1137 let lines = parsed["patcher"]["lines"].as_array().unwrap();
1138 assert_eq!(boxes.len(), 0);
1139 assert_eq!(lines.len(), 0);
1140 }
1141
1142 #[test]
1143 fn test_dependency_cache_empty() {
1144 let graph = make_minimal_graph();
1145 let json_str = generate(&graph).unwrap();
1146 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1147
1148 let dep_cache = parsed["patcher"]["dependency_cache"].as_array().unwrap();
1149 assert_eq!(dep_cache.len(), 0);
1150 }
1151
1152 #[test]
1153 fn test_build_object_text_no_args() {
1154 let node = PatchNode {
1155 id: "test".into(),
1156 object_name: "cycle~".into(),
1157 args: vec![],
1158 num_inlets: 2,
1159 num_outlets: 1,
1160 is_signal: true,
1161 varname: None,
1162 hot_inlets: vec![],
1163 purity: NodePurity::Unknown,
1164 attrs: vec![],
1165 code: None,
1166 };
1167 assert_eq!(build_object_text(&node), "cycle~");
1168 }
1169
1170 #[test]
1171 fn test_build_object_text_with_args() {
1172 let node = PatchNode {
1173 id: "test".into(),
1174 object_name: "cycle~".into(),
1175 args: vec!["440".into()],
1176 num_inlets: 2,
1177 num_outlets: 1,
1178 is_signal: true,
1179 varname: None,
1180 hot_inlets: vec![],
1181 purity: NodePurity::Unknown,
1182 attrs: vec![],
1183 code: None,
1184 };
1185 assert_eq!(build_object_text(&node), "cycle~ 440");
1186 }
1187
1188 #[test]
1189 fn test_build_object_text_multiple_args() {
1190 let node = PatchNode {
1191 id: "test".into(),
1192 object_name: "trigger".into(),
1193 args: vec!["b".into(), "b".into(), "b".into()],
1194 num_inlets: 1,
1195 num_outlets: 3,
1196 is_signal: false,
1197 varname: None,
1198 hot_inlets: vec![],
1199 purity: NodePurity::Unknown,
1200 attrs: vec![],
1201 code: None,
1202 };
1203 assert_eq!(build_object_text(&node), "trigger b b b");
1204 }
1205
1206 #[test]
1207 fn test_classify_maxclass_inlet() {
1208 let node = PatchNode {
1209 id: "test".into(),
1210 object_name: "inlet".into(),
1211 args: vec![],
1212 num_inlets: 0,
1213 num_outlets: 1,
1214 is_signal: false,
1215 varname: None,
1216 hot_inlets: vec![],
1217 purity: NodePurity::Unknown,
1218 attrs: vec![],
1219 code: None,
1220 };
1221 let (maxclass, _, _) = classify_maxclass(&node, "box");
1222 assert_eq!(maxclass, "inlet");
1223 }
1224
1225 #[test]
1226 fn test_classify_maxclass_inlet_tilde() {
1227 let node = PatchNode {
1230 id: "test".into(),
1231 object_name: "inlet~".into(),
1232 args: vec![],
1233 num_inlets: 1,
1234 num_outlets: 1,
1235 is_signal: true,
1236 varname: None,
1237 hot_inlets: vec![],
1238 purity: NodePurity::Unknown,
1239 attrs: vec![],
1240 code: None,
1241 };
1242 let (maxclass, _, _) = classify_maxclass(&node, "box");
1243 assert_eq!(maxclass, "inlet");
1244 }
1245
1246 #[test]
1247 fn test_classify_maxclass_newobj() {
1248 let node = PatchNode {
1249 id: "test".into(),
1250 object_name: "cycle~".into(),
1251 args: vec!["440".into()],
1252 num_inlets: 2,
1253 num_outlets: 1,
1254 is_signal: true,
1255 varname: None,
1256 hot_inlets: vec![],
1257 purity: NodePurity::Unknown,
1258 attrs: vec![],
1259 code: None,
1260 };
1261 let (maxclass, _, _) = classify_maxclass(&node, "box");
1262 assert_eq!(maxclass, "newobj");
1263 }
1264
1265 #[test]
1266 fn test_compute_outlettype_signal() {
1267 let node = PatchNode {
1268 id: "test".into(),
1269 object_name: "cycle~".into(),
1270 args: vec![],
1271 num_inlets: 2,
1272 num_outlets: 1,
1273 is_signal: true,
1274 varname: None,
1275 hot_inlets: vec![],
1276 purity: NodePurity::Unknown,
1277 attrs: vec![],
1278 code: None,
1279 };
1280 let types = compute_outlettype(&node, false, false);
1281 assert_eq!(types, vec!["signal"]);
1282 }
1283
1284 #[test]
1285 fn test_compute_outlettype_no_outlets() {
1286 let node = PatchNode {
1287 id: "test".into(),
1288 object_name: "ezdac~".into(),
1289 args: vec![],
1290 num_inlets: 2,
1291 num_outlets: 0,
1292 is_signal: true,
1293 varname: None,
1294 hot_inlets: vec![],
1295 purity: NodePurity::Unknown,
1296 attrs: vec![],
1297 code: None,
1298 };
1299 let types = compute_outlettype(&node, false, false);
1300 assert!(types.is_empty());
1301 }
1302
1303 #[test]
1304 fn test_roundtrip_l2() {
1305 use crate::builder::build_graph;
1307 use flutmax_ast::*;
1308
1309 let prog = Program {
1310 in_decls: vec![InDecl {
1311 index: 0,
1312 name: "freq".to_string(),
1313 port_type: PortType::Float,
1314 }],
1315 out_decls: vec![OutDecl {
1316 index: 0,
1317 name: "audio".to_string(),
1318 port_type: PortType::Signal,
1319 value: None,
1320 }],
1321 wires: vec![
1322 Wire {
1323 name: "osc".to_string(),
1324 value: Expr::Call {
1325 object: "cycle~".to_string(),
1326 args: vec![CallArg::positional(Expr::Ref("freq".to_string()))],
1327 },
1328 span: None,
1329 attrs: vec![],
1330 },
1331 Wire {
1332 name: "amp".to_string(),
1333 value: Expr::Call {
1334 object: "mul~".to_string(),
1335 args: vec![
1336 CallArg::positional(Expr::Ref("osc".to_string())),
1337 CallArg::positional(Expr::Lit(LitValue::Float(0.5))),
1338 ],
1339 },
1340 span: None,
1341 attrs: vec![],
1342 },
1343 ],
1344 destructuring_wires: vec![],
1345 msg_decls: vec![],
1346 out_assignments: vec![OutAssignment {
1347 index: 0,
1348 value: Expr::Ref("amp".to_string()),
1349 span: None,
1350 }],
1351 direct_connections: vec![],
1352 feedback_decls: vec![],
1353 feedback_assignments: vec![],
1354 state_decls: vec![],
1355 state_assignments: vec![],
1356 };
1357
1358 let graph = build_graph(&prog).unwrap();
1359 let json_str = generate(&graph).unwrap();
1360 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1361
1362 let patcher = &parsed["patcher"];
1363 let boxes = patcher["boxes"].as_array().unwrap();
1364 let lines = patcher["lines"].as_array().unwrap();
1365
1366 assert_eq!(boxes.len(), 4);
1368 assert_eq!(lines.len(), 3);
1370
1371 assert_eq!(boxes[0]["box"]["maxclass"], "inlet");
1373
1374 assert_eq!(boxes[3]["box"]["maxclass"], "outlet");
1376 }
1377
1378 #[test]
1379 fn test_unique_ids() {
1380 let graph = make_l2_graph();
1381 let json_str = generate(&graph).unwrap();
1382 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1383 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1384
1385 let ids: Vec<&str> = boxes
1386 .iter()
1387 .map(|b| b["box"]["id"].as_str().unwrap())
1388 .collect();
1389
1390 let mut unique_ids = ids.clone();
1392 unique_ids.sort();
1393 unique_ids.dedup();
1394 assert_eq!(ids.len(), unique_ids.len());
1395 }
1396
1397 #[test]
1398 fn test_message_box_output() {
1399 let mut g = PatchGraph::new();
1400 g.add_node(PatchNode {
1401 id: "msg1".into(),
1402 object_name: "message".into(),
1403 args: vec!["bang".into()],
1404 num_inlets: 2,
1405 num_outlets: 1,
1406 is_signal: false,
1407 varname: Some("click".into()),
1408 hot_inlets: vec![true, false],
1409 purity: NodePurity::Stateful,
1410 attrs: vec![],
1411 code: None,
1412 });
1413
1414 let json_str = generate(&g).unwrap();
1415 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1416 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1417
1418 let msg_box = &boxes[0]["box"];
1419 assert_eq!(msg_box["maxclass"], "message");
1420 assert_eq!(msg_box["text"], "bang");
1421 assert_eq!(msg_box["numinlets"], 2);
1422 assert_eq!(msg_box["numoutlets"], 1);
1423 assert_eq!(msg_box["varname"], "click");
1424
1425 let outlettype = msg_box["outlettype"].as_array().unwrap();
1426 assert_eq!(outlettype.len(), 1);
1427 assert_eq!(outlettype[0], "");
1428 }
1429
1430 #[test]
1431 fn test_fanout_patchline_has_order() {
1432 let mut graph = make_minimal_graph();
1434 graph.edges[0].order = Some(0);
1436 graph.edges[1].order = Some(1);
1437
1438 let json_str = generate(&graph).unwrap();
1439 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1440 let lines = parsed["patcher"]["lines"].as_array().unwrap();
1441
1442 for (i, line) in lines.iter().enumerate() {
1444 let patchline = &line["patchline"];
1445 let order = patchline.get("order");
1446 assert!(order.is_some(), "patchline {} should have order field", i);
1447 assert_eq!(order.unwrap().as_u64().unwrap(), i as u64);
1448 }
1449 }
1450
1451 #[test]
1452 fn test_non_fanout_patchline_no_order() {
1453 let graph = make_l2_graph();
1455 let json_str = generate(&graph).unwrap();
1456 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1457 let lines = parsed["patcher"]["lines"].as_array().unwrap();
1458
1459 for (i, line) in lines.iter().enumerate() {
1460 let patchline = &line["patchline"];
1461 assert!(
1462 patchline.get("order").is_none(),
1463 "patchline {} should not have order field",
1464 i
1465 );
1466 }
1467 }
1468
1469 #[test]
1474 fn test_newobj_attrs_in_text() {
1475 let mut g = PatchGraph::new();
1477 g.add_node(PatchNode {
1478 id: "osc".into(),
1479 object_name: "cycle~".into(),
1480 args: vec!["440".into()],
1481 num_inlets: 2,
1482 num_outlets: 1,
1483 is_signal: true,
1484 varname: Some("osc".into()),
1485 hot_inlets: vec![],
1486 purity: NodePurity::Unknown,
1487 attrs: vec![("phase".into(), "0.5".into())],
1488 code: None,
1489 });
1490
1491 let json_str = generate(&g).unwrap();
1492 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1493 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1494
1495 let text = boxes[0]["box"]["text"].as_str().unwrap();
1496 assert_eq!(text, "cycle~ 440 @phase 0.5");
1497 }
1498
1499 #[test]
1500 fn test_newobj_multiple_attrs_in_text() {
1501 let mut g = PatchGraph::new();
1502 g.add_node(PatchNode {
1503 id: "osc".into(),
1504 object_name: "cycle~".into(),
1505 args: vec![],
1506 num_inlets: 2,
1507 num_outlets: 1,
1508 is_signal: true,
1509 varname: None,
1510 hot_inlets: vec![],
1511 purity: NodePurity::Unknown,
1512 attrs: vec![
1513 ("frequency".into(), "440.".into()),
1514 ("phase".into(), "0.5".into()),
1515 ],
1516 code: None,
1517 });
1518
1519 let json_str = generate(&g).unwrap();
1520 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1521 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1522
1523 let text = boxes[0]["box"]["text"].as_str().unwrap();
1524 assert_eq!(text, "cycle~ @frequency 440. @phase 0.5");
1525 }
1526
1527 #[test]
1528 fn test_ui_object_attrs_as_fields() {
1529 let mut g = PatchGraph::new();
1531 g.add_node(PatchNode {
1532 id: "fnum".into(),
1533 object_name: "flonum".into(),
1534 args: vec![],
1535 num_inlets: 1,
1536 num_outlets: 2,
1537 is_signal: false,
1538 varname: Some("w".into()),
1539 hot_inlets: vec![],
1540 purity: NodePurity::Unknown,
1541 attrs: vec![
1542 ("minimum".into(), "0.".into()),
1543 ("maximum".into(), "100.".into()),
1544 ],
1545 code: None,
1546 });
1547
1548 let json_str = generate(&g).unwrap();
1549 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1550 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1551 let box_obj = &boxes[0]["box"];
1552
1553 assert_eq!(box_obj["maxclass"], "flonum");
1554 assert_eq!(box_obj["minimum"], 0.0);
1555 assert_eq!(box_obj["maximum"], 100.0);
1556 assert!(box_obj.get("text").is_none());
1558 }
1559
1560 #[test]
1561 fn test_ui_object_string_attr() {
1562 let mut g = PatchGraph::new();
1563 g.add_node(PatchNode {
1564 id: "dial".into(),
1565 object_name: "live.dial".into(),
1566 args: vec![],
1567 num_inlets: 1,
1568 num_outlets: 2,
1569 is_signal: false,
1570 varname: None,
1571 hot_inlets: vec![],
1572 purity: NodePurity::Unknown,
1573 attrs: vec![("parameter_longname".into(), "Cutoff".into())],
1574 code: None,
1575 });
1576
1577 let json_str = generate(&g).unwrap();
1578 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1579 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1580 let box_obj = &boxes[0]["box"];
1581
1582 assert_eq!(box_obj["maxclass"], "live.dial");
1583 assert_eq!(box_obj["parameter_longname"], "Cutoff");
1584 }
1585
1586 #[test]
1587 fn test_no_attrs_unchanged() {
1588 let mut g = PatchGraph::new();
1590 g.add_node(PatchNode {
1591 id: "osc".into(),
1592 object_name: "cycle~".into(),
1593 args: vec!["440".into()],
1594 num_inlets: 2,
1595 num_outlets: 1,
1596 is_signal: true,
1597 varname: None,
1598 hot_inlets: vec![],
1599 purity: NodePurity::Unknown,
1600 attrs: vec![],
1601 code: None,
1602 });
1603
1604 let json_str = generate(&g).unwrap();
1605 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1606 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1607
1608 let text = boxes[0]["box"]["text"].as_str().unwrap();
1609 assert_eq!(text, "cycle~ 440");
1610 }
1611
1612 fn make_rnbo_graph() -> PatchGraph {
1618 let mut g = PatchGraph::new();
1619 g.add_node(PatchNode {
1620 id: "in_freq".into(),
1621 object_name: "inlet".into(),
1622 args: vec![],
1623 num_inlets: 0,
1624 num_outlets: 1,
1625 is_signal: false,
1626 varname: Some("freq".into()),
1627 hot_inlets: vec![],
1628 purity: NodePurity::Unknown,
1629 attrs: vec![],
1630 code: None,
1631 });
1632 g.add_node(PatchNode {
1633 id: "osc".into(),
1634 object_name: "cycle~".into(),
1635 args: vec!["440".into()],
1636 num_inlets: 2,
1637 num_outlets: 1,
1638 is_signal: true,
1639 varname: Some("osc".into()),
1640 hot_inlets: vec![],
1641 purity: NodePurity::Unknown,
1642 attrs: vec![],
1643 code: None,
1644 });
1645 g.add_node(PatchNode {
1646 id: "out_audio".into(),
1647 object_name: "outlet~".into(),
1648 args: vec![],
1649 num_inlets: 1,
1650 num_outlets: 0,
1651 is_signal: true,
1652 varname: None,
1653 hot_inlets: vec![],
1654 purity: NodePurity::Unknown,
1655 attrs: vec![],
1656 code: None,
1657 });
1658 g.add_edge(PatchEdge {
1659 source_id: "in_freq".into(),
1660 source_outlet: 0,
1661 dest_id: "osc".into(),
1662 dest_inlet: 0,
1663 is_feedback: false,
1664 order: None,
1665 });
1666 g.add_edge(PatchEdge {
1667 source_id: "osc".into(),
1668 source_outlet: 0,
1669 dest_id: "out_audio".into(),
1670 dest_inlet: 0,
1671 is_feedback: false,
1672 order: None,
1673 });
1674 g
1675 }
1676
1677 fn rnbo_opts() -> GenerateOptions {
1678 GenerateOptions {
1679 classnamespace: "rnbo".to_string(),
1680 }
1681 }
1682
1683 #[test]
1684 fn test_generate_rnbo_classnamespace() {
1685 let graph = make_rnbo_graph();
1686 let json_str = generate_with_options(&graph, &rnbo_opts()).unwrap();
1687 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1688 let patcher = parsed.get("patcher").unwrap();
1689
1690 assert_eq!(patcher["classnamespace"], "rnbo");
1691 }
1692
1693 #[test]
1694 fn test_rnbo_inport_outport() {
1695 let graph = make_rnbo_graph();
1696 let json_str = generate_with_options(&graph, &rnbo_opts()).unwrap();
1697 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1698 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1699
1700 let inlet_box = &boxes[0]["box"];
1702 assert_eq!(inlet_box["maxclass"], "newobj");
1703 assert_eq!(inlet_box["text"], "inport freq");
1704
1705 let outlet_box = &boxes[2]["box"];
1707 assert_eq!(outlet_box["maxclass"], "newobj");
1708 assert_eq!(outlet_box["text"], "out~ 1");
1709 }
1710
1711 #[test]
1712 fn test_rnbo_signal_io() {
1713 let mut g = PatchGraph::new();
1715 g.add_node(PatchNode {
1716 id: "in_sig".into(),
1717 object_name: "inlet~".into(),
1718 args: vec![],
1719 num_inlets: 1,
1720 num_outlets: 1,
1721 is_signal: true,
1722 varname: None,
1723 hot_inlets: vec![],
1724 purity: NodePurity::Unknown,
1725 attrs: vec![],
1726 code: None,
1727 });
1728 g.add_node(PatchNode {
1729 id: "out_sig".into(),
1730 object_name: "outlet~".into(),
1731 args: vec![],
1732 num_inlets: 1,
1733 num_outlets: 0,
1734 is_signal: true,
1735 varname: None,
1736 hot_inlets: vec![],
1737 purity: NodePurity::Unknown,
1738 attrs: vec![],
1739 code: None,
1740 });
1741 g.add_edge(PatchEdge {
1742 source_id: "in_sig".into(),
1743 source_outlet: 0,
1744 dest_id: "out_sig".into(),
1745 dest_inlet: 0,
1746 is_feedback: false,
1747 order: None,
1748 });
1749
1750 let json_str = generate_with_options(&g, &rnbo_opts()).unwrap();
1751 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1752 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1753
1754 let inlet_box = &boxes[0]["box"];
1756 assert_eq!(inlet_box["maxclass"], "newobj");
1757 assert_eq!(inlet_box["text"], "in~ 1");
1758 let outlettype = inlet_box["outlettype"].as_array().unwrap();
1759 assert_eq!(outlettype, &[json!("signal")]);
1760
1761 let outlet_box = &boxes[1]["box"];
1763 assert_eq!(outlet_box["maxclass"], "newobj");
1764 assert_eq!(outlet_box["text"], "out~ 1");
1765 assert_eq!(outlet_box["numoutlets"], 0);
1767 assert!(outlet_box.get("outlettype").is_none());
1768 }
1769
1770 #[test]
1771 fn test_rnbo_serial() {
1772 let graph = make_rnbo_graph();
1773 let json_str = generate_with_options(&graph, &rnbo_opts()).unwrap();
1774 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1775 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1776
1777 for (i, boxval) in boxes.iter().enumerate() {
1779 let b = &boxval["box"];
1780 let serial = b["rnbo_serial"].as_u64().unwrap();
1781 assert_eq!(serial, (i + 1) as u64, "rnbo_serial for box {}", i);
1782
1783 let uniqueid = b["rnbo_uniqueid"].as_str().unwrap();
1784 assert!(!uniqueid.is_empty(), "rnbo_uniqueid should not be empty");
1785 }
1786
1787 let inlet_uid = boxes[0]["box"]["rnbo_uniqueid"].as_str().unwrap();
1789 assert_eq!(inlet_uid, "inlet_obj-1");
1790
1791 let cycle_uid = boxes[1]["box"]["rnbo_uniqueid"].as_str().unwrap();
1792 assert_eq!(cycle_uid, "cycle_tilde_obj-2");
1793
1794 let outlet_uid = boxes[2]["box"]["rnbo_uniqueid"].as_str().unwrap();
1795 assert_eq!(outlet_uid, "outlet_tilde_obj-3");
1796 }
1797
1798 #[test]
1799 fn test_standard_unchanged() {
1800 let graph = make_rnbo_graph();
1802 let json_str = generate(&graph).unwrap();
1803 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1804 let patcher = parsed.get("patcher").unwrap();
1805
1806 assert_eq!(patcher["classnamespace"], "box");
1808
1809 let boxes = patcher["boxes"].as_array().unwrap();
1810
1811 let inlet_box = &boxes[0]["box"];
1813 assert_eq!(inlet_box["maxclass"], "inlet");
1814 assert!(inlet_box.get("text").is_none());
1816
1817 let outlet_box = &boxes[2]["box"];
1819 assert_eq!(outlet_box["maxclass"], "outlet");
1820
1821 for boxval in boxes {
1823 let b = &boxval["box"];
1824 assert!(
1825 b.get("rnbo_serial").is_none(),
1826 "standard mode should not have rnbo_serial"
1827 );
1828 assert!(
1829 b.get("rnbo_uniqueid").is_none(),
1830 "standard mode should not have rnbo_uniqueid"
1831 );
1832 }
1833 }
1834
1835 #[test]
1836 fn test_rnbo_control_outlet() {
1837 let mut g = PatchGraph::new();
1839 g.add_node(PatchNode {
1840 id: "out_ctrl".into(),
1841 object_name: "outlet".into(),
1842 args: vec![],
1843 num_inlets: 1,
1844 num_outlets: 1,
1845 is_signal: false,
1846 varname: Some("result".into()),
1847 hot_inlets: vec![],
1848 purity: NodePurity::Unknown,
1849 attrs: vec![],
1850 code: None,
1851 });
1852
1853 let json_str = generate_with_options(&g, &rnbo_opts()).unwrap();
1854 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1855 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1856
1857 let outlet_box = &boxes[0]["box"];
1858 assert_eq!(outlet_box["maxclass"], "newobj");
1859 assert_eq!(outlet_box["text"], "outport result");
1860 assert_eq!(outlet_box["numoutlets"], 0);
1862 }
1863
1864 #[test]
1865 fn test_rnbo_inport_fallback_name() {
1866 let mut g = PatchGraph::new();
1868 g.add_node(PatchNode {
1869 id: "in_unnamed".into(),
1870 object_name: "inlet".into(),
1871 args: vec![],
1872 num_inlets: 0,
1873 num_outlets: 1,
1874 is_signal: false,
1875 varname: None,
1876 hot_inlets: vec![],
1877 purity: NodePurity::Unknown,
1878 attrs: vec![],
1879 code: None,
1880 });
1881
1882 let json_str = generate_with_options(&g, &rnbo_opts()).unwrap();
1883 let parsed: Value = serde_json::from_str(&json_str).unwrap();
1884 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
1885
1886 let inlet_box = &boxes[0]["box"];
1887 assert_eq!(inlet_box["text"], "inport port_0");
1888 }
1889
1890 #[test]
1895 fn test_classify_maxclass_codebox() {
1896 let node = PatchNode {
1898 id: "cb1".into(),
1899 object_name: "v8.codebox".into(),
1900 args: vec![],
1901 num_inlets: 1,
1902 num_outlets: 1,
1903 is_signal: false,
1904 varname: None,
1905 hot_inlets: vec![],
1906 purity: NodePurity::Unknown,
1907 attrs: vec![],
1908 code: None,
1909 };
1910 let (maxclass, width, height) = classify_maxclass(&node, "box");
1911 assert_eq!(maxclass, "v8.codebox");
1912 assert_eq!(width, 200.0);
1913 assert_eq!(height, 100.0);
1914
1915 let node2 = PatchNode {
1917 id: "cb2".into(),
1918 object_name: "codebox".into(),
1919 args: vec![],
1920 num_inlets: 1,
1921 num_outlets: 1,
1922 is_signal: false,
1923 varname: None,
1924 hot_inlets: vec![],
1925 purity: NodePurity::Unknown,
1926 attrs: vec![],
1927 code: None,
1928 };
1929 let (maxclass2, _, _) = classify_maxclass(&node2, "box");
1930 assert_eq!(maxclass2, "codebox");
1931 }
1932
1933 #[test]
1934 fn test_build_box_codebox_with_code() {
1935 let node = PatchNode {
1937 id: "cb1".into(),
1938 object_name: "v8.codebox".into(),
1939 args: vec![],
1940 num_inlets: 1,
1941 num_outlets: 1,
1942 is_signal: false,
1943 varname: None,
1944 hot_inlets: vec![],
1945 purity: NodePurity::Unknown,
1946 attrs: vec![],
1947 code: Some("function bang() { outlet(0, 42); }".into()),
1948 };
1949
1950 let box_json = build_box(
1951 &node,
1952 &BoxContext {
1953 id: "obj-1",
1954 x: 100.0,
1955 y: 50.0,
1956 classnamespace: "box",
1957 serial: 1,
1958 port_index: None,
1959 ui_data: None,
1960 },
1961 );
1962 let box_obj = &box_json["box"];
1963
1964 assert_eq!(box_obj["maxclass"], "v8.codebox");
1965 assert_eq!(box_obj["code"], "function bang() { outlet(0, 42); }");
1966 assert_eq!(box_obj["filename"], "none");
1967 assert_eq!(box_obj["text"], "");
1968 }
1969
1970 #[test]
1971 fn test_build_box_codebox_without_code() {
1972 let node = PatchNode {
1974 id: "cb1".into(),
1975 object_name: "codebox".into(),
1976 args: vec![],
1977 num_inlets: 1,
1978 num_outlets: 1,
1979 is_signal: false,
1980 varname: None,
1981 hot_inlets: vec![],
1982 purity: NodePurity::Unknown,
1983 attrs: vec![],
1984 code: None,
1985 };
1986
1987 let box_json = build_box(
1988 &node,
1989 &BoxContext {
1990 id: "obj-1",
1991 x: 100.0,
1992 y: 50.0,
1993 classnamespace: "box",
1994 serial: 1,
1995 port_index: None,
1996 ui_data: None,
1997 },
1998 );
1999 let box_obj = &box_json["box"];
2000
2001 assert_eq!(box_obj["maxclass"], "codebox");
2002 assert!(box_obj.get("code").is_none());
2003 assert!(box_obj.get("filename").is_none());
2004 }
2005
2006 #[test]
2007 fn test_standard_codegen_unchanged() {
2008 let mut g = PatchGraph::new();
2010 g.add_node(PatchNode {
2011 id: "osc".into(),
2012 object_name: "cycle~".into(),
2013 args: vec!["440".into()],
2014 num_inlets: 2,
2015 num_outlets: 1,
2016 is_signal: true,
2017 varname: Some("osc".into()),
2018 hot_inlets: vec![],
2019 purity: NodePurity::Unknown,
2020 attrs: vec![],
2021 code: None,
2022 });
2023
2024 let json_str = generate(&g).unwrap();
2025 let parsed: Value = serde_json::from_str(&json_str).unwrap();
2026 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
2027 assert_eq!(boxes.len(), 1);
2028 assert_eq!(boxes[0]["box"]["maxclass"], "newobj");
2029 assert_eq!(boxes[0]["box"]["text"], "cycle~ 440");
2030 assert!(boxes[0]["box"].get("code").is_none());
2032 }
2033
2034 #[test]
2035 fn test_gen_mode_classify_inlet_outlet() {
2036 let inlet_node = PatchNode {
2038 id: "in".into(),
2039 object_name: "inlet~".into(),
2040 args: vec![],
2041 num_inlets: 0,
2042 num_outlets: 1,
2043 is_signal: true,
2044 varname: None,
2045 hot_inlets: vec![],
2046 purity: NodePurity::Unknown,
2047 attrs: vec![],
2048 code: None,
2049 };
2050 let (maxclass, _, _) = classify_maxclass(&inlet_node, "dsp.gen");
2051 assert_eq!(maxclass, "newobj");
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 (maxclass, _, _) = classify_maxclass(&outlet_node, "dsp.gen");
2067 assert_eq!(maxclass, "newobj");
2068 }
2069
2070 #[test]
2071 fn test_gen_mode_build_box_text() {
2072 let inlet_node = PatchNode {
2074 id: "in".into(),
2075 object_name: "inlet~".into(),
2076 args: vec![],
2077 num_inlets: 0,
2078 num_outlets: 1,
2079 is_signal: true,
2080 varname: None,
2081 hot_inlets: vec![],
2082 purity: NodePurity::Unknown,
2083 attrs: vec![],
2084 code: None,
2085 };
2086 let box_json = build_box(
2087 &inlet_node,
2088 &BoxContext {
2089 id: "obj-1",
2090 x: 100.0,
2091 y: 50.0,
2092 classnamespace: "dsp.gen",
2093 serial: 1,
2094 port_index: Some(0),
2095 ui_data: None,
2096 },
2097 );
2098 let box_obj = &box_json["box"];
2099 assert_eq!(box_obj["maxclass"], "newobj");
2100 assert_eq!(box_obj["text"], "in 1");
2101 assert!(box_obj.get("rnbo_serial").is_none());
2103
2104 let outlet_node = PatchNode {
2105 id: "out".into(),
2106 object_name: "outlet~".into(),
2107 args: vec![],
2108 num_inlets: 1,
2109 num_outlets: 0,
2110 is_signal: true,
2111 varname: None,
2112 hot_inlets: vec![],
2113 purity: NodePurity::Unknown,
2114 attrs: vec![],
2115 code: None,
2116 };
2117 let box_json = build_box(
2118 &outlet_node,
2119 &BoxContext {
2120 id: "obj-2",
2121 x: 100.0,
2122 y: 120.0,
2123 classnamespace: "dsp.gen",
2124 serial: 2,
2125 port_index: Some(0),
2126 ui_data: None,
2127 },
2128 );
2129 let box_obj = &box_json["box"];
2130 assert_eq!(box_obj["maxclass"], "newobj");
2131 assert_eq!(box_obj["text"], "out 1");
2132 assert_eq!(box_obj["numoutlets"], 0); }
2134
2135 #[test]
2136 fn test_gen_mode_codegen() {
2137 let mut g = PatchGraph::new();
2139 g.add_node(PatchNode {
2140 id: "in1".into(),
2141 object_name: "inlet~".into(),
2142 args: vec![],
2143 num_inlets: 0,
2144 num_outlets: 1,
2145 is_signal: true,
2146 varname: None,
2147 hot_inlets: vec![],
2148 purity: NodePurity::Unknown,
2149 attrs: vec![],
2150 code: None,
2151 });
2152 g.add_node(PatchNode {
2153 id: "mul".into(),
2154 object_name: "*".into(),
2155 args: vec!["0.5".into()],
2156 num_inlets: 2,
2157 num_outlets: 1,
2158 is_signal: false,
2159 varname: None,
2160 hot_inlets: vec![],
2161 purity: NodePurity::Unknown,
2162 attrs: vec![],
2163 code: None,
2164 });
2165 g.add_node(PatchNode {
2166 id: "out1".into(),
2167 object_name: "outlet~".into(),
2168 args: vec![],
2169 num_inlets: 1,
2170 num_outlets: 0,
2171 is_signal: true,
2172 varname: None,
2173 hot_inlets: vec![],
2174 purity: NodePurity::Unknown,
2175 attrs: vec![],
2176 code: None,
2177 });
2178 g.add_edge(PatchEdge {
2179 source_id: "in1".into(),
2180 source_outlet: 0,
2181 dest_id: "mul".into(),
2182 dest_inlet: 0,
2183 is_feedback: false,
2184 order: None,
2185 });
2186 g.add_edge(PatchEdge {
2187 source_id: "mul".into(),
2188 source_outlet: 0,
2189 dest_id: "out1".into(),
2190 dest_inlet: 0,
2191 is_feedback: false,
2192 order: None,
2193 });
2194
2195 let opts = GenerateOptions {
2196 classnamespace: "dsp.gen".to_string(),
2197 };
2198 let json_str = generate_with_options(&g, &opts).unwrap();
2199 let parsed: Value = serde_json::from_str(&json_str).unwrap();
2200
2201 assert_eq!(parsed["patcher"]["classnamespace"], "dsp.gen");
2202
2203 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
2204 assert_eq!(boxes.len(), 3);
2205
2206 assert_eq!(boxes[0]["box"]["maxclass"], "newobj");
2208 assert_eq!(boxes[0]["box"]["text"], "in 1");
2209
2210 assert_eq!(boxes[1]["box"]["maxclass"], "newobj");
2212 assert_eq!(boxes[1]["box"]["text"], "* 0.5");
2213
2214 assert_eq!(boxes[2]["box"]["maxclass"], "newobj");
2216 assert_eq!(boxes[2]["box"]["text"], "out 1");
2217 assert_eq!(boxes[2]["box"]["numoutlets"], 0);
2218 }
2219
2220 #[test]
2223 fn test_ui_data_from_json_basic() {
2224 let json_str = r#"{
2225 "_patcher": { "rect": [50, 50, 800, 600] },
2226 "osc": { "rect": [100, 200, 80, 22] },
2227 "dac": { "rect": [100, 400, 45, 45], "background": 0 }
2228 }"#;
2229 let ui = UiData::from_json(json_str).unwrap();
2230
2231 assert_eq!(ui.patcher["rect"], json!([50, 50, 800, 600]));
2233
2234 assert!(ui.entries.contains_key("osc"));
2236 assert!(ui.entries.contains_key("dac"));
2237 assert!(!ui.entries.contains_key("_patcher"));
2238 assert_eq!(ui.entries["osc"]["rect"], json!([100, 200, 80, 22]));
2239 assert_eq!(ui.entries["dac"]["background"], json!(0));
2240 }
2241
2242 #[test]
2243 fn test_ui_data_from_json_empty() {
2244 let ui = UiData::from_json("{}").unwrap();
2245 assert!(ui.patcher.is_empty());
2246 assert!(ui.entries.is_empty());
2247 }
2248
2249 #[test]
2250 fn test_ui_data_from_json_invalid() {
2251 assert!(UiData::from_json("not json").is_none());
2252 assert!(UiData::from_json("42").is_none());
2253 assert!(UiData::from_json("[]").is_none());
2254 }
2255
2256 #[test]
2257 fn test_ui_data_from_json_no_patcher() {
2258 let json_str = r#"{ "osc": { "rect": [10, 20, 80, 22] } }"#;
2259 let ui = UiData::from_json(json_str).unwrap();
2260 assert!(ui.patcher.is_empty());
2261 assert_eq!(ui.entries.len(), 1);
2262 }
2263
2264 #[test]
2265 fn test_build_box_with_ui_data_rect_override() {
2266 let node = PatchNode {
2267 id: "osc".into(),
2268 object_name: "cycle~".into(),
2269 args: vec!["440".into()],
2270 num_inlets: 2,
2271 num_outlets: 1,
2272 is_signal: true,
2273 varname: Some("osc".into()),
2274 hot_inlets: vec![],
2275 purity: NodePurity::Unknown,
2276 attrs: vec![],
2277 code: None,
2278 };
2279 let ui = UiData::from_json(r#"{ "osc": { "rect": [250, 350, 90, 24] } }"#).unwrap();
2280
2281 let box_json = build_box(
2282 &node,
2283 &BoxContext {
2284 id: "obj-1",
2285 x: 100.0,
2286 y: 50.0,
2287 classnamespace: "box",
2288 serial: 1,
2289 port_index: None,
2290 ui_data: Some(&ui),
2291 },
2292 );
2293 let rect = box_json["box"]["patching_rect"].as_array().unwrap();
2294
2295 assert_eq!(rect[0], json!(250));
2297 assert_eq!(rect[1], json!(350));
2298 assert_eq!(rect[2], json!(90));
2299 assert_eq!(rect[3], json!(24));
2300 }
2301
2302 #[test]
2303 fn test_build_box_with_ui_data_decorative_attrs() {
2304 let node = PatchNode {
2305 id: "osc".into(),
2306 object_name: "cycle~".into(),
2307 args: vec!["440".into()],
2308 num_inlets: 2,
2309 num_outlets: 1,
2310 is_signal: true,
2311 varname: Some("osc".into()),
2312 hot_inlets: vec![],
2313 purity: NodePurity::Unknown,
2314 attrs: vec![],
2315 code: None,
2316 };
2317 let ui = UiData::from_json(
2318 r#"{
2319 "osc": {
2320 "rect": [250, 350, 90, 24],
2321 "background": 0,
2322 "fontsize": 14
2323 }
2324 }"#,
2325 )
2326 .unwrap();
2327
2328 let box_json = build_box(
2329 &node,
2330 &BoxContext {
2331 id: "obj-1",
2332 x: 100.0,
2333 y: 50.0,
2334 classnamespace: "box",
2335 serial: 1,
2336 port_index: None,
2337 ui_data: Some(&ui),
2338 },
2339 );
2340 let box_obj = &box_json["box"];
2341
2342 assert_eq!(box_obj["background"], json!(0));
2344 assert_eq!(box_obj["fontsize"], json!(14));
2345 }
2346
2347 #[test]
2348 fn test_build_box_without_varname_ignores_ui_data() {
2349 let node = PatchNode {
2350 id: "osc".into(),
2351 object_name: "cycle~".into(),
2352 args: vec!["440".into()],
2353 num_inlets: 2,
2354 num_outlets: 1,
2355 is_signal: true,
2356 varname: None, hot_inlets: vec![],
2358 purity: NodePurity::Unknown,
2359 attrs: vec![],
2360 code: None,
2361 };
2362 let ui = UiData::from_json(r#"{ "osc": { "rect": [250, 350, 90, 24] } }"#).unwrap();
2363
2364 let box_json = build_box(
2365 &node,
2366 &BoxContext {
2367 id: "obj-1",
2368 x: 100.0,
2369 y: 50.0,
2370 classnamespace: "box",
2371 serial: 1,
2372 port_index: None,
2373 ui_data: Some(&ui),
2374 },
2375 );
2376 let rect = box_json["box"]["patching_rect"].as_array().unwrap();
2377
2378 assert_eq!(rect[0], json!(100.0));
2380 assert_eq!(rect[1], json!(50.0));
2381 }
2382
2383 #[test]
2384 fn test_build_patcher_with_ui_data_patcher_rect() {
2385 let mut g = PatchGraph::new();
2386 g.add_node(PatchNode {
2387 id: "osc".into(),
2388 object_name: "cycle~".into(),
2389 args: vec!["440".into()],
2390 num_inlets: 2,
2391 num_outlets: 1,
2392 is_signal: true,
2393 varname: Some("osc".into()),
2394 hot_inlets: vec![],
2395 purity: NodePurity::Unknown,
2396 attrs: vec![],
2397 code: None,
2398 });
2399
2400 let ui = UiData::from_json(
2401 r#"{
2402 "_patcher": { "rect": [50, 50, 800, 600] },
2403 "osc": { "rect": [200, 300, 80, 22] }
2404 }"#,
2405 )
2406 .unwrap();
2407
2408 let json_str = generate_with_ui(&g, &GenerateOptions::default(), Some(&ui)).unwrap();
2409 let parsed: Value = serde_json::from_str(&json_str).unwrap();
2410
2411 assert_eq!(parsed["patcher"]["rect"], json!([50, 50, 800, 600]));
2413
2414 let boxes = parsed["patcher"]["boxes"].as_array().unwrap();
2416 assert_eq!(boxes[0]["box"]["patching_rect"], json!([200, 300, 80, 22]));
2417 }
2418
2419 #[test]
2420 fn test_generate_with_ui_none_is_same_as_generate() {
2421 let graph = make_minimal_graph();
2422
2423 let json_without = generate(&graph).unwrap();
2424 let json_with_none = generate_with_ui(&graph, &GenerateOptions::default(), None).unwrap();
2425
2426 assert_eq!(json_without, json_with_none);
2428 }
2429}