1#![allow(clippy::result_large_err)]
11
12use std::str::FromStr;
13
14use base64::Engine as _;
15
16use crate::document_ng::{
17 Accents, AutoRoute, Block, BlockLabel, Comment, Decorations, Document, GridPos, GridRect,
18 GridSize, Image, ImageData, LabelSide, LineAnchor, LinearDistance, PinPort, PinSide, PinType,
19 TextBox, Waypoint,
20};
21use crate::schema::SchemaError;
22use crate::schema::enums;
23use crate::schema::loc::{format_loc, parse_loc};
24use crate::schema::model as schema;
25use crate::shape::port::PORT_HEIGHT;
26use crate::store::{
27 CommentId, IdMap, IdMapExt, IdSet, ImageId, PinId, RectId, RouteId, TextId, WaypointId,
28 WireLabelId,
29};
30use crate::widget::auto_route::AutoRouteExt;
31
32impl From<PinSide> for enums::PinSide {
37 fn from(v: PinSide) -> Self {
38 match v {
39 PinSide::East => enums::PinSide::East,
40 PinSide::West => enums::PinSide::West,
41 }
42 }
43}
44impl From<enums::PinSide> for PinSide {
45 fn from(v: enums::PinSide) -> Self {
46 match v {
47 enums::PinSide::East => PinSide::East,
48 enums::PinSide::West => PinSide::West,
49 }
50 }
51}
52
53impl From<PinType> for enums::PinType {
54 fn from(v: PinType) -> Self {
55 match v {
56 PinType::Input => enums::PinType::Input,
57 PinType::Output => enums::PinType::Output,
58 PinType::InOut => enums::PinType::InOut,
59 }
60 }
61}
62impl From<enums::PinType> for PinType {
63 fn from(v: enums::PinType) -> Self {
64 match v {
65 enums::PinType::Input => PinType::Input,
66 enums::PinType::Output => PinType::Output,
67 enums::PinType::InOut => PinType::InOut,
68 }
69 }
70}
71
72impl From<LabelSide> for enums::LabelSide {
73 fn from(v: LabelSide) -> Self {
74 match v {
75 LabelSide::Top => enums::LabelSide::Top,
76 LabelSide::Center => enums::LabelSide::Center,
77 LabelSide::Bottom => enums::LabelSide::Bottom,
78 }
79 }
80}
81impl From<enums::LabelSide> for LabelSide {
82 fn from(v: enums::LabelSide) -> Self {
83 match v {
84 enums::LabelSide::Top => LabelSide::Top,
85 enums::LabelSide::Center => LabelSide::Center,
86 enums::LabelSide::Bottom => LabelSide::Bottom,
87 }
88 }
89}
90
91impl From<&Document> for schema::Document {
94 fn from(doc: &Document) -> Self {
95 schema::Document {
96 top: doc.top_id.to_string(),
97 blocks: doc
98 .blocks
99 .iter()
100 .map(|(&id, b)| schema::Block::from_block(id, b))
101 .collect(),
102 }
103 }
104}
105
106fn format_anchor(anchor: LineAnchor) -> String {
108 match anchor {
109 LineAnchor::Port(p) => p.to_string(),
110 LineAnchor::Pin { block, pin } => format!("{block}:{pin}"),
111 }
112}
113
114fn round1(v: f32) -> f32 {
117 (v * 10.0).round() / 10.0
118}
119
120fn from_label(label: &BlockLabel, default: &BlockLabel) -> Option<schema::Label> {
123 if label == default {
124 return None;
125 }
126 Some(schema::Label {
127 name: label.name.clone(),
128 side: (label.side != default.side).then(|| enums::LabelSide::from(label.side)),
129 offset: round1(label.offset),
130 hidden: label.hidden,
131 })
132}
133
134impl schema::Block {
135 pub fn from_block(id: RectId, b: &Block) -> Self {
136 let auto: std::collections::HashMap<PinId, GridRect> =
139 b.auto_port_rects().into_iter().collect();
140 schema::Block {
141 id: id.to_string(),
142 x: b.inner.min.x,
143 y: b.inner.min.y,
144 w: b.inner.size.w,
145 h: b.inner.size.h,
146 role: b.role,
147 locked: b.locked,
148 title: from_label(&b.decorations.title, &BlockLabel::default()),
149 type_label: from_label(&b.decorations.type_label, &BlockLabel::upper_left()),
150 pins: b
151 .pins
152 .iter()
153 .map(|(&id, p)| schema::Pin::from_pin(id, p, auto.get(&id).copied()))
154 .collect(),
155 routes: b.routes.values().map(schema::Route::from_route).collect(),
156 texts: b.texts.values().map(schema::Text::from_text).collect(),
157 comments: b
158 .comments
159 .values()
160 .map(schema::Comment::from_comment)
161 .collect(),
162 images: b.images.values().map(schema::Image::from_symbol).collect(),
163 icon: b.icon.as_ref().map(schema::Image::from_symbol),
164 children: b.children.iter().map(|&cid| cid.to_string()).collect(),
165 }
166 }
167}
168
169impl schema::Pin {
170 pub fn from_pin(id: PinId, p: &PinPort, auto: Option<GridRect>) -> Self {
171 let is_auto = auto == Some(p.rect);
174 schema::Pin {
175 id: id.to_string(),
176 name: p.name.clone(),
177 type_label: p.type_label.clone(),
178 tag: p.tag.clone(),
179 tag_hidden: p.tag_hidden,
180 loc: Some(format_loc(p.side.into(), p.offset)),
181 x: (!is_auto).then_some(p.rect.min.x),
182 y: (!is_auto).then_some(p.rect.min.y),
183 w: (!is_auto).then_some(p.rect.size.w),
184 dir: (p.kind != PinType::InOut).then(|| p.kind.into()),
185 pin_accent: p.accents.pin_accent,
186 port_accent: p.accents.port_accent,
187 port_pin_accent: p.accents.port_pin_accent,
188 fliplr: p.port_orientation == Some(p.side),
191 }
192 }
193}
194
195impl schema::Route {
196 pub fn from_route(r: &AutoRoute) -> Self {
197 schema::Route {
198 name: r.route_name().to_string(),
199 from: format_anchor(r.start()),
200 to: format_anchor(r.finish()),
201 role: r.role(),
202 waypoints: r
203 .iter_waypoints()
204 .map(|(_, w)| schema::Waypoint {
205 x: w.pos.x,
206 y: w.pos.y,
207 locked: w.locked,
208 })
209 .collect(),
210 labels: r
211 .iter_labels()
212 .map(|(_, &d)| round1(f32::from(d)))
213 .collect(),
214 }
215 }
216}
217
218impl schema::Text {
219 pub fn from_text(t: &TextBox) -> Self {
220 schema::Text {
221 text: t.text.clone(),
222 x: t.anchor.x,
223 y: t.anchor.y,
224 role: t.role,
225 }
226 }
227}
228
229impl schema::Comment {
230 pub fn from_comment(c: &Comment) -> Self {
231 schema::Comment {
232 x: c.inner.min.x,
233 y: c.inner.min.y,
234 w: c.inner.size.w,
235 h: c.inner.size.h,
236 role: c.role,
237 title: from_label(&c.title, &BlockLabel::default()),
238 }
239 }
240}
241
242impl schema::Image {
243 pub fn from_symbol(s: &Image) -> Self {
244 let image = match &s.image {
245 ImageData::Svg(src) => schema::ImageData::Svg(src.clone()),
246 ImageData::Png(bytes) => {
247 schema::ImageData::Png(base64::engine::general_purpose::STANDARD.encode(bytes))
248 }
249 };
250 schema::Image {
251 x: round1(s.inner.min.x),
252 y: round1(s.inner.min.y),
253 w: round1(s.inner.width()),
254 h: round1(s.inner.height()),
255 image,
256 }
257 }
258}
259
260fn parse_id<T: FromStr<Err = String>>(kind: &'static str, s: &str) -> Result<T, SchemaError> {
263 s.parse().map_err(|reason| SchemaError::Id {
264 kind,
265 value: s.to_string(),
266 reason,
267 })
268}
269
270fn parse_anchor(block: &str, s: &str) -> Result<LineAnchor, SchemaError> {
271 let anchor_err = |reason: String| SchemaError::Anchor {
272 block: block.to_string(),
273 value: s.to_string(),
274 reason,
275 };
276 if let Some((b, p)) = s.split_once(':') {
277 Ok(LineAnchor::Pin {
278 block: b.parse().map_err(anchor_err)?,
279 pin: p.parse().map_err(anchor_err)?,
280 })
281 } else {
282 Ok(LineAnchor::Port(s.parse().map_err(anchor_err)?))
283 }
284}
285
286impl schema::Label {
287 fn into_label(self, default: &BlockLabel) -> BlockLabel {
288 BlockLabel {
289 name: self.name,
290 hidden: self.hidden,
291 side: self.side.map_or(default.side, Into::into),
292 offset: self.offset,
293 }
294 }
295}
296
297fn label_or(dto: Option<schema::Label>, default: BlockLabel) -> BlockLabel {
298 match dto {
299 Some(l) => l.into_label(&default),
300 None => default,
301 }
302}
303
304impl schema::Document {
305 pub fn into_document(self) -> Result<Document, SchemaError> {
306 let top_id: RectId = self.top.parse().map_err(|reason| SchemaError::BadTop {
307 value: self.top.clone(),
308 reason,
309 })?;
310 let mut blocks: IdMap<RectId, Block> = IdMap::default();
311 for sb in self.blocks {
312 let (id, block) = sb.into_block()?;
313 blocks.insert(id, block);
314 }
315 if !blocks.contains_key(&top_id) {
316 return Err(SchemaError::BadTop {
317 value: self.top,
318 reason: "no block with that id".into(),
319 });
320 }
321 validate_children(&blocks)?;
322 Ok(Document {
323 top_id,
324 blocks: blocks.into(),
325 })
326 }
327}
328
329fn validate_children(blocks: &IdMap<RectId, Block>) -> Result<(), SchemaError> {
332 for (bid, block) in blocks {
333 for cid in &block.children {
334 if !blocks.contains_key(cid) {
335 return Err(SchemaError::DanglingChild {
336 block: bid.to_string(),
337 child: cid.to_string(),
338 });
339 }
340 }
341 }
342 Ok(())
343}
344
345impl schema::Block {
346 pub fn into_block(self) -> Result<(RectId, Block), SchemaError> {
347 let id: RectId = parse_id("block", &self.id)?;
348 let block_ctx = self.id;
349
350 let decorations = Decorations {
351 title: label_or(self.title, BlockLabel::default()),
352 type_label: label_or(self.type_label, BlockLabel::upper_left()),
353 };
354
355 let mut pins: IdMap<PinId, PinPort> = IdMap::default();
356 for p in self.pins {
357 let (pid, pin) = p.into_pin(&block_ctx)?;
358 pins.insert(pid, pin);
359 }
360
361 let mut routes: IdMap<RouteId, AutoRoute> = IdMap::default();
364 for r in self.routes {
365 routes.insert_value(r.into_route(&block_ctx)?);
366 }
367
368 let mut texts: IdMap<TextId, TextBox> = IdMap::default();
369 for t in self.texts {
370 texts.insert_value(t.into_text());
371 }
372
373 let mut comments: IdMap<CommentId, Comment> = IdMap::default();
374 for c in self.comments {
375 comments.insert_value(c.into_comment());
376 }
377
378 let mut images: IdMap<ImageId, Image> = IdMap::default();
379 for (index, s) in self.images.into_iter().enumerate() {
380 images.insert_value(s.into_symbol(&block_ctx, index)?);
381 }
382 let icon = match self.icon {
383 Some(s) => Some(s.into_symbol(&block_ctx, 0)?),
384 None => None,
385 };
386
387 let mut children: IdSet<RectId> = IdSet::default();
388 for cid in self.children {
389 children.insert(parse_id("block", &cid)?);
390 }
391
392 let block = Block {
393 inner: GridRect::new(GridPos::new(self.x, self.y), GridSize::new(self.w, self.h)),
394 decorations,
395 pins,
396 children,
397 routes,
398 texts,
399 comments,
400 images,
401 icon,
402 role: self.role,
403 locked: self.locked,
404 };
405 let mut block = block;
407 block.fill_missing_port_rects();
408 Ok((id, block))
409 }
410}
411
412impl schema::Pin {
413 pub fn into_pin(self, block: &str) -> Result<(PinId, PinPort), SchemaError> {
414 let id: PinId = parse_id("pin", &self.id)?;
415 let loc = self.loc.ok_or_else(|| SchemaError::MissingPinSide {
416 block: block.to_string(),
417 pin: self.id.clone(),
418 })?;
419 let (side, offset) = parse_loc(&loc).map_err(|reason| SchemaError::BadLoc {
420 block: block.to_string(),
421 pin: self.id.clone(),
422 value: loc.clone(),
423 reason,
424 })?;
425 let side: PinSide = side.into();
426 let pin = PinPort {
427 name: self.name,
428 type_label: self.type_label,
429 tag: self.tag,
430 tag_hidden: self.tag_hidden,
431 side,
432 offset,
433 rect: match (self.x, self.y, self.w) {
436 (Some(x), Some(y), Some(w)) => {
437 GridRect::new(GridPos::new(x, y), GridSize::new(w, PORT_HEIGHT))
438 }
439 _ => GridRect::default(),
440 },
441 kind: self.dir.map(Into::into).unwrap_or_default(),
442 accents: Accents {
443 pin_accent: self.pin_accent,
444 port_accent: self.port_accent,
445 port_pin_accent: self.port_pin_accent,
446 },
447 port_orientation: self.fliplr.then_some(side),
449 };
450 Ok((id, pin))
451 }
452}
453
454impl schema::Route {
455 pub fn into_route(self, block: &str) -> Result<AutoRoute, SchemaError> {
456 let from = parse_anchor(block, &self.from)?;
457 let to = parse_anchor(block, &self.to)?;
458
459 let mut waypoints: IdMap<WaypointId, Waypoint> = IdMap::default();
460 for w in self.waypoints {
461 waypoints.insert_value(Waypoint {
462 pos: GridPos::new(w.x, w.y),
463 locked: w.locked,
464 });
465 }
466 let mut labels: IdMap<WireLabelId, LinearDistance> = IdMap::default();
467 for at in self.labels {
468 labels.insert_value(LinearDistance::from(at));
469 }
470
471 let mut route = AutoRoute::build(from, to, &[], waypoints, labels);
473 route.set_route_name(self.name);
474 route.set_role(self.role);
475 Ok(route)
476 }
477}
478
479impl schema::Text {
480 pub fn into_text(self) -> TextBox {
481 TextBox {
482 text: self.text,
483 anchor: GridPos::new(self.x, self.y),
484 size: None,
485 role: self.role,
486 }
487 }
488}
489
490impl schema::Comment {
491 pub fn into_comment(self) -> Comment {
492 Comment {
493 inner: GridRect::new(GridPos::new(self.x, self.y), GridSize::new(self.w, self.h)),
494 title: label_or(self.title, BlockLabel::default()),
495 role: self.role,
496 }
497 }
498}
499
500impl schema::Image {
501 pub fn into_symbol(self, block: &str, index: usize) -> Result<Image, SchemaError> {
502 let image = match self.image {
503 schema::ImageData::Svg(svg) => ImageData::Svg(svg),
504 schema::ImageData::Png(png) => ImageData::Png(
505 base64::engine::general_purpose::STANDARD
506 .decode(png.as_bytes())
507 .map_err(|e| SchemaError::SymbolPng {
508 block: block.to_string(),
509 index,
510 reason: e.to_string(),
511 })?,
512 ),
513 };
514 Ok(Image {
515 image,
516 inner: egui::Rect::from_min_size(
517 egui::pos2(self.x, self.y),
518 egui::vec2(self.w, self.h),
519 ),
520 })
521 }
522}
523
524pub fn to_json(doc: &Document) -> String {
528 schema::Document::from(doc).to_json()
529}
530
531pub fn from_json(src: &str, src_name: &str) -> miette::Result<Document> {
534 Ok(schema::Document::parse_json(src, src_name)?.into_document()?)
535}
536
537pub fn to_kdl(doc: &Document) -> String {
539 schema::Document::from(doc).to_kdl()
540}
541
542pub fn from_kdl(src: &str, src_name: &str) -> miette::Result<Document> {
544 Ok(schema::Document::parse_kdl(src, src_name)?.into_document()?)
545}
546
547#[cfg(test)]
548mod tests {
549 use super::{from_json, from_kdl, to_json, to_kdl};
550 use crate::document_ng::{
551 Accents, AutoRoute, Block, BlockLabel, Comment, Decorations, Document, GridPos, GridRect,
552 GridSize, Image, ImageData, LabelSide, LineAnchor, LinearDistance, PinPort, PinSide,
553 PinType, TextBox, Waypoint,
554 };
555 use crate::schema::SchemaError;
556 use crate::schema::model;
557 use crate::store::{
558 CommentId, IdMap, IdMapExt, IdSet, ImageId, PinId, RectId, RouteId, TextId, WaypointId,
559 WireLabelId,
560 };
561 use crate::widget::auto_route::AutoRouteExt;
562
563 fn pin(name: &str, side: PinSide, offset: u32, kind: PinType, rect: GridRect) -> PinPort {
564 PinPort {
565 name: name.to_string(),
566 type_label: String::new(),
567 tag: String::new(),
568 tag_hidden: false,
569 side,
570 offset,
571 rect,
572 kind,
573 accents: Accents::default(),
574 port_orientation: None,
575 }
576 }
577
578 fn rect(x: i32, y: i32, w: u32, h: u32) -> GridRect {
579 GridRect::new(GridPos::new(x, y), GridSize::new(w, h))
580 }
581
582 fn block(name: &str) -> Block {
583 Block {
584 decorations: Decorations {
585 title: BlockLabel {
586 name: name.to_string(),
587 ..BlockLabel::default()
588 },
589 ..Decorations::default()
590 },
591 ..Block::default()
592 }
593 }
594
595 fn route(
596 from: LineAnchor,
597 to: LineAnchor,
598 name: &str,
599 wps: &[(i32, i32)],
600 labels: &[f32],
601 ) -> AutoRoute {
602 let mut waypoints: IdMap<WaypointId, Waypoint> = IdMap::default();
603 for &(x, y) in wps {
604 waypoints.insert_value(Waypoint {
605 pos: GridPos::new(x, y),
606 locked: false,
607 });
608 }
609 let mut ld: IdMap<WireLabelId, LinearDistance> = IdMap::default();
610 for &l in labels {
611 ld.insert_value(LinearDistance::from(l));
612 }
613 let mut r = AutoRoute::build(from, to, &[], waypoints, ld);
614 r.set_route_name(name.to_string());
615 r
616 }
617
618 fn demo() -> Document {
622 let top = RectId::nth_default(1);
623 let c1 = RectId::nth_default(2);
624 let c2 = RectId::nth_default(3);
625 let buffer = RectId::nth_default(4);
626
627 let mut t = block("block_1");
628 t.decorations.type_label.name = "Counter".to_string();
629 t.pins.insert(
630 PinId::nth_default(1),
631 pin("clk", PinSide::West, 1, PinType::Input, rect(-11, 28, 4, 2)),
632 );
633 t.pins.insert(
634 PinId::nth_default(2),
635 pin("rst", PinSide::West, 2, PinType::InOut, rect(-11, 32, 4, 2)),
636 );
637 t.children = IdSet::from_iter([c1, c2]);
638 t.role = Some(3);
639 t.locked = true;
640 t.texts.insert(
641 TextId::nth_default(1),
642 TextBox {
643 text: "line one\nline two".to_string(),
644 anchor: GridPos::new(5, 8),
645 size: None,
646 role: Some(2),
647 },
648 );
649 t.comments.insert(
650 CommentId::nth_default(1),
651 Comment {
652 inner: rect(2, 2, 10, 8),
653 title: BlockLabel {
654 name: "Region".to_string(),
655 side: LabelSide::Top,
656 ..BlockLabel::default()
657 },
658 role: None,
659 },
660 );
661 t.images.insert(
662 ImageId::nth_default(1),
663 Image {
664 image: ImageData::Svg(
665 "<svg viewBox=\"0 0 10 10\">\n <path d=\"M0 0 L10 10\"/>\n</svg>".to_string(),
666 ),
667 inner: egui::Rect::from_min_size(egui::pos2(20.0, 15.0), egui::vec2(8.0, 8.0)),
668 },
669 );
670 t.icon = Some(Image {
671 image: ImageData::Svg(
672 "<svg viewBox=\"0 0 4 4\"><rect width=\"4\" height=\"4\"/></svg>".to_string(),
673 ),
674 inner: egui::Rect::from_min_size(egui::pos2(4.0, 4.0), egui::vec2(3.0, 3.0)),
675 });
676 let mut r1 = route(
677 LineAnchor::Pin {
678 block: c1,
679 pin: PinId::nth_default(1),
680 },
681 LineAnchor::Pin {
682 block: c2,
683 pin: PinId::nth_default(1),
684 },
685 "route_1",
686 &[(12, 21), (-16, 19)],
687 &[76.6],
688 );
689 r1.set_role(Some(5));
690 t.routes.insert(RouteId::nth_default(1), r1);
691 t.routes.insert(
692 RouteId::nth_default(2),
693 route(
694 LineAnchor::Pin {
695 block: c2,
696 pin: PinId::nth_default(1),
697 },
698 LineAnchor::Port(PinId::nth_default(1)),
699 "",
700 &[],
701 &[],
702 ),
703 );
704
705 let mut b1 = block("block_1");
706 b1.pins.insert(
707 PinId::nth_default(1),
708 pin(
709 "i.1.write_logic",
710 PinSide::West,
711 1,
712 PinType::InOut,
713 rect(0, 0, 8, 2),
714 ),
715 );
716 let mut fancy = pin(
718 "typed",
719 PinSide::East,
720 3,
721 PinType::Output,
722 rect(20, 6, 5, 2),
723 );
724 fancy.type_label = "Counter".to_string();
725 fancy.tag = "U3".to_string();
726 fancy.tag_hidden = true;
727 fancy.accents = Accents {
728 pin_accent: Some(1),
729 port_accent: Some(2),
730 port_pin_accent: None,
731 };
732 fancy.port_orientation = Some(PinSide::East);
735 b1.pins.insert(PinId::nth_default(2), fancy);
736
737 let mut b2 = block("block_2b");
738 b2.pins.insert(
739 PinId::nth_default(1),
740 pin(
741 "o.0.read_logic",
742 PinSide::East,
743 1,
744 PinType::InOut,
745 rect(30, 0, 8, 2),
746 ),
747 );
748 b2.children = IdSet::from_iter([buffer]);
749
750 let mut buf = block("Internal Buffer");
751 buf.pins.insert(
752 PinId::nth_default(1),
753 pin("Port", PinSide::West, 1, PinType::InOut, rect(22, 5, 4, 2)),
754 );
755
756 let mut blocks: IdMap<RectId, Block> = IdMap::default();
757 blocks.insert(top, t);
758 blocks.insert(c1, b1);
759 blocks.insert(c2, b2);
760 blocks.insert(buffer, buf);
761 Document {
762 top_id: top,
763 blocks: blocks.into(),
764 }
765 }
766
767 #[test]
768 fn json_round_trip_is_idempotent() {
769 let doc = demo();
770 let s1 = to_json(&doc);
771 let back = from_json(&s1, "demo.json").unwrap_or_else(|e| panic!("re-parse:\n{e:?}\n{s1}"));
772 let s2 = to_json(&back);
773 assert_eq!(s1, s2, "JSON must be idempotent\n{s1}");
774 }
775
776 #[test]
777 fn kdl_round_trip_is_idempotent() {
778 let doc = demo();
779 let s1 = to_kdl(&doc);
780 let back = from_kdl(&s1, "demo.kdl").unwrap_or_else(|e| panic!("re-parse:\n{e:?}\n{s1}"));
781 let s2 = to_kdl(&back);
782 assert_eq!(s1, s2, "KDL must be idempotent\n{s1}");
783 }
784
785 #[test]
786 fn json_and_kdl_agree() {
787 let doc = demo();
790 let via_kdl = from_kdl(&to_kdl(&doc), "demo.kdl").unwrap();
791 assert_eq!(to_json(&doc), to_json(&via_kdl));
792 }
793
794 #[test]
795 fn schema_projection_round_trips_by_value() {
796 let doc = demo();
798 let s = model::Document::from(&doc);
799 let via_json = from_json(&to_json(&doc), "d.json").unwrap();
800 let via_kdl = from_kdl(&to_kdl(&doc), "d.kdl").unwrap();
801 assert_eq!(s, model::Document::from(&via_json), "json");
802 assert_eq!(s, model::Document::from(&via_kdl), "kdl");
803 }
804
805 #[test]
806 fn image_fractional_size_round_trips() {
807 let mut doc = Document::default();
810 let top = doc.top_id;
811 let inner = egui::Rect::from_min_size(egui::pos2(3.5, 2.5), egui::vec2(37.5, 12.5));
812 doc.block_mut(top).unwrap().images.insert(
813 ImageId::nth_default(1),
814 Image {
815 image: ImageData::Svg("<svg/>".into()),
816 inner,
817 },
818 );
819 for back in [
820 from_json(&to_json(&doc), "d.json").unwrap(),
821 from_kdl(&to_kdl(&doc), "d.kdl").unwrap(),
822 ] {
823 let img = back.block(top).unwrap().images.values().next().unwrap();
824 assert_eq!(img.inner, inner, "fractional image rect preserved");
825 }
826 }
827
828 #[test]
829 fn ids_preserved_and_child_order_kept() {
830 let back = from_kdl(&to_kdl(&demo()), "d.kdl").unwrap();
831 assert_eq!(back.top_id, RectId::nth_default(1));
832 assert!(back.block(RectId::nth_default(4)).is_some());
833 let top = back.block(back.top_id).unwrap();
834 assert_eq!(
835 top.children.iter().copied().collect::<Vec<_>>(),
836 vec![RectId::nth_default(2), RectId::nth_default(3)]
837 );
838 }
839
840 #[test]
841 fn pin_rect_is_captured_not_recomputed() {
842 let back = from_json(&to_json(&demo()), "d.json").unwrap();
844 let clk = back
845 .block(RectId::nth_default(1))
846 .unwrap()
847 .pins
848 .get(&PinId::nth_default(1))
849 .unwrap();
850 assert_eq!(clk.rect, rect(-11, 28, 4, 2));
851 }
852
853 #[test]
854 fn fliplr_round_trips() {
855 let kdl = to_kdl(&demo());
856 assert!(kdl.contains("fliplr=true"), "{kdl}");
857 assert!(
858 !kdl.contains("facing") && !kdl.contains("port-orientation"),
859 "{kdl}"
860 );
861 let back = from_kdl(&kdl, "d.kdl").unwrap();
862 let fancy = back
863 .block(RectId::nth_default(2))
864 .unwrap()
865 .pins
866 .get(&PinId::nth_default(2))
867 .unwrap();
868 assert_eq!(fancy.port_orientation, Some(PinSide::East));
870 let clk = back
872 .block(RectId::nth_default(1))
873 .unwrap()
874 .pins
875 .get(&PinId::nth_default(1))
876 .unwrap();
877 assert_eq!(clk.port_orientation, None);
878 }
879
880 #[test]
881 fn route_anchors_are_positional_and_id_less() {
882 let kdl = to_kdl(&demo());
883 assert!(
885 kdl.contains(r#"route "b2:p1" "b3:p1" name="route_1""#),
886 "{kdl}"
887 );
888 assert!(!kdl.contains("from=") && !kdl.contains("to="), "{kdl}");
889 }
890
891 #[test]
892 fn label_position_round_trips_exactly() {
893 let doc = demo();
895 let orig = doc
896 .block(RectId::nth_default(1))
897 .unwrap()
898 .routes
899 .get(&RouteId::nth_default(1))
900 .unwrap()
901 .iter_labels()
902 .next()
903 .map(|(_, &d)| d)
904 .unwrap();
905 let back = from_kdl(&to_kdl(&doc), "d.kdl").unwrap();
906 let got = back
907 .block(RectId::nth_default(1))
908 .unwrap()
909 .routes
910 .values()
911 .find(|r| r.route_name() == "route_1")
912 .unwrap()
913 .iter_labels()
914 .next()
915 .map(|(_, &d)| d)
916 .unwrap();
917 assert_eq!(orig, got, "label LinearDistance preserved exactly");
918 }
919
920 #[test]
921 fn enums_are_kebab_case_in_json() {
922 let json = to_json(&demo());
923 assert!(json.contains(r#""dir": "output""#), "{json}");
924 assert!(
925 !json.contains("\"East\"") && !json.contains("\"InOut\""),
926 "{json}"
927 );
928 }
929
930 #[test]
931 fn loc_combines_side_and_offset() {
932 let kdl = to_kdl(&demo());
933 let json = to_json(&demo());
934 assert!(kdl.contains(r#"loc="w1""#), "{kdl}");
936 assert!(json.contains(r#""loc": "w1""#), "{json}");
937 assert!(!kdl.contains(r#""clk" side="#), "{kdl}");
939 let back = from_kdl(&kdl, "d.kdl").unwrap();
940 let clk = back
941 .block(RectId::nth_default(1))
942 .unwrap()
943 .pins
944 .get(&PinId::nth_default(1))
945 .unwrap();
946 assert_eq!((clk.side, clk.offset), (PinSide::West, 1));
947 }
948
949 #[test]
950 fn pin_geometry_omitted_when_auto() {
951 let p = pin("clk", PinSide::West, 1, PinType::InOut, rect(5, 5, 4, 2));
952 let auto = model::Pin::from_pin(PinId::nth_default(1), &p, Some(rect(5, 5, 4, 2)));
954 assert_eq!((auto.x, auto.y, auto.w), (None, None, None));
955 let kept = model::Pin::from_pin(PinId::nth_default(1), &p, Some(rect(9, 9, 4, 2)));
957 assert_eq!((kept.x, kept.y, kept.w), (Some(5), Some(5), Some(4)));
958 }
959
960 #[test]
961 fn hand_authored_pin_without_geometry_gets_auto_placed() {
962 let src =
964 "top \"b1\"\nblock \"b1\" x=0 y=0 w=20 h=8 {\n pin \"p1\" \"clk\" loc=\"w1\"\n}";
965 let back = from_kdl(src, "d.kdl").unwrap();
966 let p = back
967 .block(RectId::nth_default(1))
968 .unwrap()
969 .pins
970 .get(&PinId::nth_default(1))
971 .unwrap();
972 assert!(p.rect.size.w > 0, "port width was auto-filled");
974 assert_eq!(p.rect.size.h, crate::shape::port::PORT_HEIGHT);
975 }
976
977 #[test]
978 fn children_are_one_node() {
979 let kdl = to_kdl(&demo());
980 assert!(kdl.contains(r#"children "b2" "b3""#), "{kdl}");
982 assert!(
983 !kdl.contains("child \"b2\""),
984 "no per-line child nodes:\n{kdl}"
985 );
986 let back = from_kdl(&kdl, "d.kdl").unwrap();
987 assert_eq!(
988 back.block(RectId::nth_default(1))
989 .unwrap()
990 .children
991 .iter()
992 .copied()
993 .collect::<Vec<_>>(),
994 vec![RectId::nth_default(2), RectId::nth_default(3)]
995 );
996 }
997
998 #[test]
999 fn bad_and_missing_loc_errors() {
1000 let missing = "top \"b1\"\nblock \"b1\" x=0 y=0 w=40 h=30 {\n pin \"p1\" \"clk\" x=0 y=0 w=4 h=2\n}";
1002 assert!(matches!(kdl_err(missing), SchemaError::Kdl { .. }));
1003 let bad = "top \"b1\"\nblock \"b1\" x=0 y=0 w=40 h=30 {\n pin \"p1\" \"clk\" loc=\"z9\" x=0 y=0 w=4 h=2\n}";
1004 let err = kdl_err(bad);
1005 assert!(matches!(err, SchemaError::Kdl { .. }));
1006 let (off, len) = kdl_span(&err);
1007 assert_eq!(&bad[off..off + len], "\"z9\"", "span points at the bad loc");
1008 }
1009
1010 #[test]
1011 fn missing_loc_in_json_is_missing_pin_side() {
1012 let json = r#"{"top":"b1","blocks":[{"id":"b1","x":0,"y":0,"w":40,"h":30,
1014 "pins":[{"id":"p1","name":"clk","x":0,"y":0,"w":4,"h":2}]}]}"#;
1015 let err = from_json(json, "e.json")
1016 .unwrap_err()
1017 .downcast::<SchemaError>()
1018 .unwrap();
1019 assert!(matches!(err, SchemaError::MissingPinSide { .. }));
1020 }
1021
1022 #[test]
1023 fn defaults_omitted() {
1024 let kdl = to_kdl(&demo());
1025 assert!(!kdl.contains("in-out"), "default kind omitted:\n{kdl}");
1026 }
1027
1028 fn json_err(src: &str) -> SchemaError {
1031 from_json(src, "e.json")
1032 .unwrap_err()
1033 .downcast::<SchemaError>()
1034 .expect("SchemaError")
1035 }
1036 fn kdl_err(src: &str) -> SchemaError {
1037 from_kdl(src, "e.kdl")
1038 .unwrap_err()
1039 .downcast::<SchemaError>()
1040 .expect("SchemaError")
1041 }
1042
1043 fn kdl_span(err: &SchemaError) -> (usize, usize) {
1045 match err {
1046 SchemaError::Kdl { span, .. } => (span.offset(), span.len()),
1047 other => panic!("expected a Kdl error, got {other:?}"),
1048 }
1049 }
1050
1051 #[test]
1052 fn malformed_is_an_error() {
1053 assert!(matches!(kdl_err("top \"b1\" {"), SchemaError::Kdl { .. }));
1054 assert!(matches!(json_err("not json"), SchemaError::Json { .. }));
1055 }
1056
1057 #[test]
1058 fn bad_id_error_points_at_the_id() {
1059 let src = "top \"b1\"\nblock \"xyz\" x=0 y=0 w=4 h=4";
1060 let err = kdl_err(src);
1061 assert!(matches!(err, SchemaError::Kdl { .. }));
1062 let (off, len) = kdl_span(&err);
1064 assert_eq!(&src[off..off + len], "\"xyz\"");
1065 }
1066
1067 #[test]
1068 fn bad_anchor_error() {
1069 let src = "top \"b1\"\nblock \"b1\" x=0 y=0 w=40 h=30 {\n route \"zzz\" \"p1\"\n}";
1071 assert!(matches!(kdl_err(src), SchemaError::Kdl { .. }));
1072 }
1073
1074 #[test]
1075 fn dangling_child_error() {
1076 let src = "top \"b1\"\nblock \"b1\" x=0 y=0 w=40 h=30 {\n children \"b9\"\n}";
1077 assert!(matches!(kdl_err(src), SchemaError::DanglingChild { .. }));
1078 }
1079
1080 #[test]
1083 fn top_naming_a_missing_block_is_an_error() {
1084 let src = "top \"b9\"\nblock \"b1\" x=0 y=0 w=40 h=30";
1085 assert!(matches!(kdl_err(src), SchemaError::BadTop { .. }));
1086 }
1087
1088 #[test]
1089 fn image_without_data_error() {
1090 let src = "top \"b1\"\nblock \"b1\" x=0 y=0 w=40 h=30 {\n image x=1 y=1 size=10.0\n}";
1091 assert!(matches!(kdl_err(src), SchemaError::Kdl { .. }));
1092 }
1093
1094 #[test]
1095 fn unknown_node_error() {
1096 let src = "top \"b1\"\nblock \"b1\" x=0 y=0 w=40 h=30 {\n widget x=1\n}";
1097 assert!(matches!(kdl_err(src), SchemaError::Kdl { .. }));
1098 }
1099
1100 #[test]
1101 fn svg_with_quotes_round_trips() {
1102 let kdl = to_kdl(&demo());
1103 assert!(kdl.contains("svg r#\""), "{kdl}");
1104 let back = from_kdl(&kdl, "d.kdl").unwrap();
1105 let sym = back
1106 .block(RectId::nth_default(1))
1107 .unwrap()
1108 .images
1109 .get(&ImageId::nth_default(1))
1110 .unwrap();
1111 match &sym.image {
1112 ImageData::Svg(s) => assert!(s.contains("M0 0 L10 10") && s.contains('\n')),
1113 ImageData::Png(_) => panic!("expected svg"),
1114 }
1115 }
1116}