1use crate::ShapesPlan;
8use crate::animation::{
9 sample_hold, sample_insets, sample_length, sample_offset, sample_paint, sample_scalar,
10 sample_size, sample_vec2,
11};
12use alpha2_geometry::StableHash;
13use alpha2_model::{
14 AlignContent, AlignItems, Animated, Display, FlexDirection, FlexWrap, GridAutoFlow,
15 GridPlacement, JustifyContent, LAYOUT_TEXT_DEFAULTS, LayoutLength, LayoutNode, LayoutTextStyle,
16 Overflow, Paint, Position, Rgba, TextAlign, TextShadow, TextStroke, TrackList, Vec2,
17};
18use alpha2_time::MediaTime;
19use std::sync::Arc;
20
21#[derive(Clone, Debug)]
23pub struct LayoutPlan {
24 root: Box<LayoutNode>,
26 shapes: Vec<(Arc<ShapesPlan>, u128)>,
28 shapes_animate: bool,
30 fixed: Option<Arc<EvaluatedLayout>>,
32}
33
34impl PartialEq for LayoutPlan {
35 fn eq(&self, other: &Self) -> bool {
36 self.root == other.root
37 }
38}
39
40impl LayoutPlan {
41 #[must_use]
43 pub fn new(root: &LayoutNode, canvas: (u32, u32)) -> Self {
44 #[allow(clippy::cast_precision_loss)]
45 let shorter_side = canvas.0.min(canvas.1).max(1) as f32;
46 let root = root.without_px(shorter_side);
47 let mut shapes = Vec::new();
48 root.walk(|_, node| {
49 if let Some(shape) = &node.shape {
50 let mut hash = StableHash::new();
51 hash.bytes(format!("{:?}", shape.items).as_bytes());
52 shapes.push((Arc::new(ShapesPlan::new(&shape.items)), hash.finish()));
53 }
54 });
55 let shapes_animate = shapes.iter().any(|(plan, _)| plan.is_animated());
56 let animated = root.is_animated();
57 let mut plan = Self {
58 root: Box::new(root),
59 shapes,
60 shapes_animate,
61 fixed: None,
62 };
63 if !animated {
64 plan.fixed = Some(Arc::new(plan.evaluate_now(MediaTime::ZERO)));
65 }
66 plan
67 }
68
69 #[must_use]
71 pub fn evaluate(&self, time: MediaTime) -> Arc<EvaluatedLayout> {
72 self.fixed
73 .clone()
74 .unwrap_or_else(|| Arc::new(self.evaluate_now(time)))
75 }
76
77 #[must_use]
79 pub fn is_animated(&self) -> bool {
80 self.fixed.is_none()
81 }
82
83 #[must_use]
85 pub const fn root(&self) -> &LayoutNode {
86 &self.root
87 }
88
89 fn evaluate_now(&self, time: MediaTime) -> EvaluatedLayout {
90 let mut shapes = self.shapes.iter();
91 let root = evaluate_node(&self.root, time, &mut shapes, &LAYOUT_TEXT_DEFAULTS);
92 let mut hash = StableHash::new();
93 hash_node(&mut hash, &root, &self.shapes);
94 if self.shapes_animate {
95 hash.u64(time.ticks().cast_unsigned());
96 }
97 EvaluatedLayout {
98 root,
99 time,
100 key: hash.finish(),
101 }
102 }
103}
104
105#[derive(Clone, Debug, PartialEq)]
107pub struct EvaluatedLayout {
108 pub root: EvaluatedNode,
110 pub time: MediaTime,
112 pub key: u128,
114}
115
116#[derive(Clone, Debug, PartialEq)]
118pub struct EvaluatedNode {
119 pub layout: NodeLayout,
121 pub paint: NodePaint,
123 pub visual: Option<NodeVisual>,
125 pub clip: bool,
127 pub content: NodeContent,
129}
130
131impl EvaluatedNode {
132 #[must_use]
134 pub const fn is_isolated(&self) -> bool {
135 self.visual.is_some() || self.clip
136 }
137}
138
139#[derive(Copy, Clone, Debug, Default, PartialEq)]
141#[allow(missing_docs)]
142pub struct Sides {
143 pub top: f32,
144 pub right: f32,
145 pub bottom: f32,
146 pub left: f32,
147}
148
149impl Sides {
150 const fn from_array([top, right, bottom, left]: [f32; 4]) -> Self {
151 Self {
152 top,
153 right,
154 bottom,
155 left,
156 }
157 }
158
159 const fn to_array(self) -> [f32; 4] {
160 [self.top, self.right, self.bottom, self.left]
161 }
162}
163
164#[derive(Clone, Debug, PartialEq)]
166#[allow(missing_docs)]
167pub struct NodeLayout {
168 pub display: Display,
169 pub flex_direction: FlexDirection,
170 pub flex_wrap: FlexWrap,
171 pub grid_template_columns: Option<TrackList>,
172 pub grid_template_rows: Option<TrackList>,
173 pub grid_auto_columns: Option<TrackList>,
174 pub grid_auto_rows: Option<TrackList>,
175 pub grid_auto_flow: GridAutoFlow,
176 pub row_gap: f32,
178 pub column_gap: f32,
180 pub justify_content: JustifyContent,
181 pub align_items: AlignItems,
182 pub align_content: AlignContent,
183 pub justify_items: AlignItems,
184 pub padding: Sides,
185 pub border_width: f32,
187 pub overflow: Overflow,
188 pub width: Option<LayoutLength>,
189 pub height: Option<LayoutLength>,
190 pub min_width: Option<LayoutLength>,
191 pub min_height: Option<LayoutLength>,
192 pub max_width: Option<LayoutLength>,
193 pub max_height: Option<LayoutLength>,
194 pub aspect_ratio: Option<f32>,
195 pub margin: Sides,
196 pub flex_grow: f32,
197 pub flex_shrink: f32,
198 pub flex_basis: Option<LayoutLength>,
199 pub align_self: Option<AlignItems>,
200 pub justify_self: Option<AlignItems>,
201 pub grid_column: GridPlacement,
202 pub grid_row: GridPlacement,
203 pub position: Position,
204 pub inset: [Option<LayoutLength>; 4],
206}
207
208#[derive(Clone, Debug, PartialEq)]
210pub struct NodePaint {
211 pub background: Option<Paint>,
213 pub border: Option<(f32, Paint)>,
215 pub border_radius: f32,
217}
218
219#[derive(Copy, Clone, Debug, PartialEq)]
221pub struct NodeVisual {
222 pub opacity: f32,
224 pub translate: Vec2,
226 pub scale: Vec2,
228 pub rotation_degrees: f32,
230}
231
232#[derive(Clone, Debug, PartialEq)]
234pub enum NodeContent {
235 Empty,
237 Children(Vec<EvaluatedNode>),
239 Text(EvaluatedText),
241 Shape(Arc<ShapesPlan>),
243}
244
245#[derive(Clone, Debug, PartialEq)]
247pub struct EvaluatedText {
248 pub content: String,
250 pub font_family: Option<String>,
252 pub font_size: f32,
254 pub fill: Paint,
256 pub line_height: f32,
258 pub letter_spacing: f32,
260 pub font_weight: u16,
262 pub text_align: TextAlign,
264 pub strokes: Vec<TextStroke>,
266 pub shadows: Vec<TextShadow>,
268 pub animators: Vec<crate::SampledAnimator>,
270}
271
272fn non_negative(animation: Option<&Animated<f32>>, time: MediaTime, default: f32) -> f32 {
273 animation.map_or(default, |value| sample_scalar(value, time).max(0.0))
274}
275
276fn size_or(
277 animation: Option<&Animated<alpha2_model::LayoutSize>>,
278 time: MediaTime,
279 default: f32,
280) -> f32 {
281 animation.map_or(default, |value| sample_size(value, time).max(0.0))
282}
283
284fn length(animation: Option<&Animated<LayoutLength>>, time: MediaTime) -> Option<LayoutLength> {
285 animation.map(|value| sample_length(value, time))
286}
287
288fn size(animation: Option<&Animated<LayoutLength>>, time: MediaTime) -> Option<LayoutLength> {
289 length(animation, time).map(|length| length.with_value(length.value().max(0.0)))
290}
291
292fn node_layout(node: &LayoutNode, time: MediaTime, border_width: f32) -> NodeLayout {
293 let insets = |value: Option<&Animated<alpha2_model::Insets>>| {
294 value.map_or_else(Sides::default, |value| {
295 Sides::from_array(sample_insets(value, time))
296 })
297 };
298 let padding = Sides::from_array(
299 insets(node.padding.as_ref())
300 .to_array()
301 .map(|side| side.max(0.0)),
302 );
303 let inset = node.inset.as_ref().map_or([None; 4], |inset| {
304 [
305 length(inset.left.as_ref(), time),
306 length(inset.top.as_ref(), time),
307 length(inset.right.as_ref(), time),
308 length(inset.bottom.as_ref(), time),
309 ]
310 });
311 let gap = size_or(node.gap.as_ref(), time, 0.0);
312 NodeLayout {
313 display: node.display.unwrap_or_default(),
314 flex_direction: node.flex_direction.unwrap_or_default(),
315 flex_wrap: node.flex_wrap.unwrap_or_default(),
316 grid_template_columns: node.grid_template_columns.clone(),
317 grid_template_rows: node.grid_template_rows.clone(),
318 grid_auto_columns: node.grid_auto_columns.clone(),
319 grid_auto_rows: node.grid_auto_rows.clone(),
320 grid_auto_flow: node.grid_auto_flow.unwrap_or_default(),
321 row_gap: size_or(node.row_gap.as_ref(), time, gap),
322 column_gap: size_or(node.column_gap.as_ref(), time, gap),
323 justify_content: node.justify_content.unwrap_or_default(),
324 align_items: node.align_items.unwrap_or_default(),
325 align_content: node.align_content.unwrap_or_default(),
326 justify_items: node.justify_items.unwrap_or_default(),
327 padding,
328 border_width,
329 overflow: node.overflow.unwrap_or_default(),
330 width: size(node.width.as_ref(), time),
331 height: size(node.height.as_ref(), time),
332 min_width: size(node.min_width.as_ref(), time),
333 min_height: size(node.min_height.as_ref(), time),
334 max_width: size(node.max_width.as_ref(), time),
335 max_height: size(node.max_height.as_ref(), time),
336 aspect_ratio: node.aspect_ratio,
337 margin: insets(node.margin.as_ref()),
338 flex_grow: non_negative(node.flex_grow.as_ref(), time, 0.0),
339 flex_shrink: non_negative(node.flex_shrink.as_ref(), time, 1.0),
340 flex_basis: size(node.flex_basis.as_ref(), time),
341 align_self: node.align_self,
342 justify_self: node.justify_self,
343 grid_column: node.grid_column.unwrap_or_default(),
344 grid_row: node.grid_row.unwrap_or_default(),
345 position: node.position.unwrap_or_default(),
346 inset,
347 }
348}
349
350fn text_style(node: &LayoutNode, time: MediaTime, inherited: &LayoutTextStyle) -> LayoutTextStyle {
352 LayoutTextStyle {
353 font_family: node
354 .font_family
355 .clone()
356 .or_else(|| inherited.font_family.clone()),
357 font_size: node.font_size.as_ref().map_or(inherited.font_size, |size| {
358 sample_size(size, time).max(f32::MIN_POSITIVE)
359 }),
360 fill: node.fill.clone().unwrap_or_else(|| inherited.fill.clone()),
361 line_height: node.line_height.unwrap_or(inherited.line_height),
362 letter_spacing: node.letter_spacing.unwrap_or(inherited.letter_spacing),
363 font_weight: node.font_weight.unwrap_or(inherited.font_weight),
364 text_align: node.text_align.unwrap_or(inherited.text_align),
365 strokes: node
366 .strokes
367 .clone()
368 .unwrap_or_else(|| inherited.strokes.clone()),
369 shadows: node
370 .shadows
371 .clone()
372 .unwrap_or_else(|| inherited.shadows.clone()),
373 }
374}
375
376fn evaluate_node<'a>(
377 node: &LayoutNode,
378 time: MediaTime,
379 shapes: &mut impl Iterator<Item = &'a (Arc<ShapesPlan>, u128)>,
380 inherited: &LayoutTextStyle,
381) -> EvaluatedNode {
382 let border = node.border.as_ref().map(|border| {
383 (
384 sample_size(&border.width, time).max(0.0),
385 sample_paint(&border.paint, time),
386 )
387 });
388 let layout = node_layout(node, time, border.as_ref().map_or(0.0, |(width, _)| *width));
389 let paint = NodePaint {
390 background: node
391 .background
392 .as_ref()
393 .map(|paint| sample_paint(paint, time)),
394 border,
395 border_radius: size_or(node.border_radius.as_ref(), time, 0.0),
396 };
397 let visual = node.has_visual().then(|| NodeVisual {
398 opacity: node
399 .opacity
400 .as_ref()
401 .map_or(1.0, |value| sample_scalar(value, time).clamp(0.0, 1.0)),
402 translate: node
403 .translate
404 .as_ref()
405 .map_or_else(Vec2::default, |value| sample_offset(value, time)),
406 scale: node
407 .scale
408 .as_ref()
409 .map_or(Vec2::new(1.0, 1.0), |value| sample_vec2(value, time)),
410 rotation_degrees: node
411 .rotation_degrees
412 .as_ref()
413 .map_or(0.0, |value| sample_scalar(value, time)),
414 });
415 let style = text_style(node, time, inherited);
416 let content = if let Some(text) = &node.text {
417 NodeContent::Text(EvaluatedText {
418 content: sample_hold(text, time),
419 font_family: style.font_family,
420 font_size: style.font_size,
421 fill: style.fill.clone(),
422 line_height: style.line_height,
423 letter_spacing: style.letter_spacing,
424 font_weight: style.font_weight,
425 text_align: style.text_align,
426 strokes: style.strokes.clone(),
427 shadows: style.shadows.clone(),
428 animators: crate::sample_text_animators(&node.text_animators, time),
429 })
430 } else if node.shape.is_some() {
431 let (plan, _) = shapes.next().expect("one plan per shape node");
432 NodeContent::Shape(Arc::clone(plan))
433 } else if node.children.is_empty() {
434 NodeContent::Empty
435 } else {
436 NodeContent::Children(
437 node.children
438 .iter()
439 .map(|child| evaluate_node(child, time, shapes, &style))
440 .collect(),
441 )
442 };
443 EvaluatedNode {
444 layout,
445 paint,
446 visual,
447 clip: matches!(node.overflow, Some(Overflow::Hidden)),
448 content,
449 }
450}
451
452fn hash_length(hash: &mut StableHash, length: Option<LayoutLength>) {
453 match length {
454 None => hash.u32(0),
455 Some(LayoutLength::Fraction(value)) => {
456 hash.u32(1);
457 hash.f32(value);
458 }
459 Some(LayoutLength::Percent(value)) => {
460 hash.u32(2);
461 hash.f32(value);
462 }
463 Some(LayoutLength::Px(value)) => {
464 hash.u32(3);
465 hash.f32(value);
466 }
467 }
468}
469
470fn hash_sides(hash: &mut StableHash, sides: Sides) {
471 for side in sides.to_array() {
472 hash.f32(side);
473 }
474}
475
476fn hash_color(hash: &mut StableHash, color: Rgba) {
477 for component in [color.red, color.green, color.blue, color.alpha] {
478 hash.f32(component);
479 }
480}
481
482fn hash_paint(hash: &mut StableHash, paint: Option<&Paint>) {
483 let stops = |hash: &mut StableHash, stops: &[alpha2_model::GradientStop]| {
484 hash.u64(stops.len() as u64);
485 for stop in stops {
486 hash.f32(stop.offset);
487 hash_color(hash, stop.color);
488 }
489 };
490 match paint {
491 None => hash.u32(0),
492 Some(Paint::Solid { color }) => {
493 hash.u32(1);
494 hash_color(hash, *color);
495 }
496 Some(Paint::LinearGradient {
497 start,
498 end,
499 stops: list,
500 spread,
501 }) => {
502 hash.u32(2);
503 for value in [start.x, start.y, end.x, end.y] {
504 hash.f32(value);
505 }
506 stops(hash, list);
507 hash.u32(*spread as u32);
508 }
509 Some(Paint::RadialGradient {
510 center,
511 radius,
512 stops: list,
513 spread,
514 }) => {
515 hash.u32(3);
516 for value in [center.x, center.y, *radius] {
517 hash.f32(value);
518 }
519 stops(hash, list);
520 hash.u32(*spread as u32);
521 }
522 Some(Paint::ConicGradient {
523 center,
524 angle_degrees,
525 stops: list,
526 }) => {
527 hash.u32(4);
528 for value in [center.x, center.y, *angle_degrees] {
529 hash.f32(value);
530 }
531 stops(hash, list);
532 }
533 }
534}
535
536fn hash_node(hash: &mut StableHash, node: &EvaluatedNode, shapes: &[(Arc<ShapesPlan>, u128)]) {
537 let mut shape_index = 0;
538 hash_node_in(hash, node, shapes, &mut shape_index);
539}
540
541fn hash_layout(hash: &mut StableHash, layout: &NodeLayout) {
542 for value in [
543 layout.display as u32,
544 layout.flex_direction as u32,
545 layout.flex_wrap as u32,
546 layout.grid_auto_flow as u32,
547 layout.justify_content as u32,
548 layout.align_items as u32,
549 layout.align_content as u32,
550 layout.justify_items as u32,
551 layout.overflow as u32,
552 layout.position as u32,
553 layout.align_self.map_or(u32::MAX, |align| align as u32),
554 layout.justify_self.map_or(u32::MAX, |align| align as u32),
555 ] {
556 hash.u32(value);
557 }
558 for tracks in [
560 &layout.grid_template_columns,
561 &layout.grid_template_rows,
562 &layout.grid_auto_columns,
563 &layout.grid_auto_rows,
564 ] {
565 match tracks {
566 None => hash.u32(0),
567 Some(tracks) => {
568 hash.u32(1);
569 hash.bytes(tracks.to_string().as_bytes());
570 }
571 }
572 }
573 for placement in [layout.grid_column, layout.grid_row] {
574 hash.bytes(placement.to_string().as_bytes());
575 }
576 for value in [
577 layout.row_gap,
578 layout.column_gap,
579 layout.border_width,
580 layout.flex_grow,
581 layout.flex_shrink,
582 layout.aspect_ratio.unwrap_or(-1.0),
583 ] {
584 hash.f32(value);
585 }
586 hash_sides(hash, layout.padding);
587 hash_sides(hash, layout.margin);
588 for length in [
589 layout.width,
590 layout.height,
591 layout.min_width,
592 layout.min_height,
593 layout.max_width,
594 layout.max_height,
595 layout.flex_basis,
596 ]
597 .into_iter()
598 .chain(layout.inset)
599 {
600 hash_length(hash, length);
601 }
602}
603
604fn hash_node_in(
605 hash: &mut StableHash,
606 node: &EvaluatedNode,
607 shapes: &[(Arc<ShapesPlan>, u128)],
608 shape_index: &mut usize,
609) {
610 hash_layout(hash, &node.layout);
611 hash_paint(hash, node.paint.background.as_ref());
612 match &node.paint.border {
613 None => hash.u32(0),
614 Some((width, paint)) => {
615 hash.u32(1);
616 hash.f32(*width);
617 hash_paint(hash, Some(paint));
618 }
619 }
620 hash.f32(node.paint.border_radius);
621 match node.visual {
622 None => hash.u32(0),
623 Some(visual) => {
624 hash.u32(1);
625 for value in [
626 visual.opacity,
627 visual.translate.x,
628 visual.translate.y,
629 visual.scale.x,
630 visual.scale.y,
631 visual.rotation_degrees,
632 ] {
633 hash.f32(value);
634 }
635 }
636 }
637 hash.u32(u32::from(node.clip));
638 match &node.content {
639 NodeContent::Empty => hash.u32(0),
640 NodeContent::Children(children) => {
641 hash.u32(1);
642 hash.u64(children.len() as u64);
643 for child in children {
644 hash_node_in(hash, child, shapes, shape_index);
645 }
646 }
647 NodeContent::Text(text) => {
648 hash.u32(2);
649 hash.bytes(text.content.as_bytes());
650 match &text.font_family {
651 None => hash.u32(0),
652 Some(family) => {
653 hash.u32(1);
654 hash.bytes(family.as_bytes());
655 }
656 }
657 hash.f32(text.font_size);
658 hash_paint(hash, Some(&text.fill));
659 hash.f32(text.line_height);
660 hash.f32(text.letter_spacing);
661 hash.u32(u32::from(text.font_weight));
662 hash.u32(text.text_align as u32);
663 hash.u64(text.strokes.len() as u64);
664 for stroke in &text.strokes {
665 hash_color(hash, stroke.color);
666 hash.f32(stroke.width);
667 }
668 hash.u64(text.shadows.len() as u64);
669 for shadow in &text.shadows {
670 hash_color(hash, shadow.color);
671 hash.f32(shadow.offset.x);
672 hash.f32(shadow.offset.y);
673 hash.f32(shadow.blur);
674 }
675 if !text.animators.is_empty() {
677 crate::hash_sampled_animators(hash, &text.animators);
678 }
679 }
680 NodeContent::Shape(_) => {
681 hash.u32(3);
682 let digest = shapes.get(*shape_index).map_or(0, |(_, digest)| *digest);
683 *shape_index += 1;
684 hash.bytes(&digest.to_le_bytes());
685 }
686 }
687}
688
689#[must_use]
691pub(crate) fn layout_bytes(
692 node: &LayoutNode,
693 shape_items_bytes: &impl Fn(&[alpha2_model::ShapeItem]) -> usize,
694) -> usize {
695 fn keys<T>(animation: Option<&Animated<T>>) -> usize {
696 match animation {
697 Some(Animated::Keyframes { keyframes }) => {
698 keyframes.len() * size_of::<alpha2_model::Keyframe<T>>()
699 }
700 _ => 0,
701 }
702 }
703 let text = node.text.as_ref().map_or(0, |text| match text {
704 Animated::Constant { value } => value.len(),
705 Animated::Keyframes { keyframes } => keyframes
706 .iter()
707 .map(|keyframe| size_of::<alpha2_model::Keyframe<String>>() + keyframe.value.len())
708 .sum(),
709 });
710 let style = node.font_family.as_ref().map_or(0, String::len) + keys(node.font_size.as_ref());
711 let tracks = [
712 &node.grid_template_columns,
713 &node.grid_template_rows,
714 &node.grid_auto_columns,
715 &node.grid_auto_rows,
716 ]
717 .into_iter()
718 .flatten()
719 .map(|tracks| tracks.track_count() * size_of::<alpha2_model::TrackSize>())
720 .sum::<usize>();
721 let paints = |paint: Option<&Animated<Paint>>| match paint {
722 Some(Animated::Keyframes { keyframes }) => {
723 keyframes.len() * size_of::<alpha2_model::Keyframe<Paint>>()
724 }
725 Some(Animated::Constant { .. }) | None => 0,
726 };
727 let inset = node.inset.as_ref().map_or(0, |inset| {
728 [&inset.left, &inset.top, &inset.right, &inset.bottom]
729 .into_iter()
730 .map(|side| keys(side.as_ref()))
731 .sum()
732 });
733 size_of::<LayoutNode>()
734 + text
735 + style
736 + tracks
737 + node
738 .shape
739 .as_ref()
740 .map_or(0, |shape| shape_items_bytes(&shape.items))
741 + [
742 &node.gap,
743 &node.row_gap,
744 &node.column_gap,
745 &node.border_radius,
746 ]
747 .into_iter()
748 .map(|value| keys(value.as_ref()))
749 .sum::<usize>()
750 + keys(node.padding.as_ref())
751 + paints(node.background.as_ref())
752 + node.border.as_ref().map_or(0, |border| {
753 keys(Some(&border.width)) + paints(Some(&border.paint))
754 })
755 + [
756 &node.width,
757 &node.height,
758 &node.min_width,
759 &node.min_height,
760 &node.max_width,
761 &node.max_height,
762 &node.flex_basis,
763 ]
764 .into_iter()
765 .map(|length| keys(length.as_ref()))
766 .sum::<usize>()
767 + keys(node.margin.as_ref())
768 + keys(node.flex_grow.as_ref())
769 + keys(node.flex_shrink.as_ref())
770 + inset
771 + keys(node.opacity.as_ref())
772 + keys(node.translate.as_ref())
773 + keys(node.scale.as_ref())
774 + keys(node.rotation_degrees.as_ref())
775 + node
776 .children
777 .iter()
778 .map(|child| layout_bytes(child, shape_items_bytes))
779 .sum::<usize>()
780}
781
782#[derive(Clone, Debug, PartialEq)]
784pub enum GeneratorPlan {
785 Shapes(ShapesPlan),
787 Layout(LayoutPlan),
789}
790
791impl GeneratorPlan {
792 #[must_use]
794 pub fn new(generator: &alpha2_model::Generator, canvas: (u32, u32)) -> Option<Self> {
795 match generator {
796 alpha2_model::Generator::Shapes { items } => Some(Self::Shapes(ShapesPlan::new(items))),
797 alpha2_model::Generator::Layout { root } => {
798 Some(Self::Layout(LayoutPlan::new(root, canvas)))
799 }
800 alpha2_model::Generator::Fill { .. } | alpha2_model::Generator::Text { .. } => None,
801 }
802 }
803
804 #[must_use]
806 pub const fn shapes(&self) -> Option<&ShapesPlan> {
807 match self {
808 Self::Shapes(plan) => Some(plan),
809 Self::Layout(_) => None,
810 }
811 }
812
813 #[must_use]
815 pub const fn layout(&self) -> Option<&LayoutPlan> {
816 match self {
817 Self::Layout(plan) => Some(plan),
818 Self::Shapes(_) => None,
819 }
820 }
821}
822
823#[cfg(test)]
824#[path = "layout_tests.rs"]
825mod tests;