1use super::*;
4
5pub mod path_smoothing;
6pub mod path_trim;
7pub mod stroke_utils;
8
9#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
11#[serde(crate = "crate::serde")]
12pub enum PathElement {
13 #[default]
15 Empty,
16 Point(Size, Size),
18 Line,
20 Quadratic(Size, Size),
22 Cubic(Size, Size, Size, Size),
24 Close,
26}
27
28impl Interpolatable for PathElement {}
29impl HelperFunctions for PathElement {}
30
31#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
37#[serde(crate = "crate::serde")]
38pub enum PathSmoothing {
39 #[default]
41 None,
42 Light,
44 Strong,
46}
47
48impl Interpolatable for PathSmoothing {
49 fn interpolate(&self, other: &Self, t: f64) -> Self {
50 if t < 1.0 {
51 *self
52 } else {
53 *other
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
63#[serde(crate = "crate::serde")]
64pub enum StrokeCap {
65 #[default]
67 Butt,
68 Round,
70 Square,
72}
73
74impl Interpolatable for StrokeCap {
75 fn interpolate(&self, other: &Self, t: f64) -> Self {
76 if t < 1.0 {
77 self.clone()
78 } else {
79 other.clone()
80 }
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
86#[serde(crate = "crate::serde")]
87pub enum StrokeJoin {
88 #[default]
90 Miter,
91 Round,
93 Bevel,
95}
96
97impl Interpolatable for StrokeJoin {
98 fn interpolate(&self, other: &Self, t: f64) -> Self {
99 if t < 1.0 {
100 self.clone()
101 } else {
102 other.clone()
103 }
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109#[serde(crate = "crate::serde")]
110pub struct Fill {
111 pub paint: Property<Paint>,
113 pub material: Property<Material>,
115 pub opacity: Property<Opacity>,
117 }
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(crate = "crate::serde")]
124pub struct Stroke {
125 pub paint: Property<Paint>,
127 pub material: Property<Material>,
129 pub opacity: Property<Opacity>,
131 pub width: Property<Size>,
133 pub cap: Property<StrokeCap>,
135 pub join: Property<StrokeJoin>,
137 }
139
140impl Default for Fill {
141 fn default() -> Self {
142 Self {
143 paint: Default::default(),
144 material: Default::default(),
145 opacity: Default::default(),
146 }
147 }
148}
149
150#[derive(Clone, Debug, PartialEq)]
153pub struct ResolvedStroke {
154 pub paint: Paint,
155 pub material: ResolvedMaterial,
156 pub opacity: Opacity,
157 pub width: Size,
158 pub cap: StrokeCap,
159 pub join: StrokeJoin,
160}
161
162impl Stroke {
163 pub fn resolve_in_graph(&self) -> ResolvedStroke {
165 ResolvedStroke {
166 paint: self.paint.local().get(),
167 material: self.material.local().get().resolve_in_graph(),
168 opacity: self.opacity.local().get(),
169 width: self.width.local().get(),
170 cap: self.cap.local().get(),
171 join: self.join.local().get(),
172 }
173 }
174
175 pub fn snapshot(&self) -> ResolvedStroke {
178 ResolvedStroke {
179 paint: self.paint.get(),
180 material: self.material.get().snapshot(),
181 opacity: self.opacity.get(),
182 width: self.width.get(),
183 cap: self.cap.get(),
184 join: self.join.get(),
185 }
186 }
187}
188
189impl Default for Stroke {
190 fn default() -> Self {
191 Self {
192 paint: Default::default(),
193 material: Default::default(),
194 opacity: Default::default(),
195 width: Property::new(Size::Pixels(1.into())),
196 cap: Default::default(),
197 join: Default::default(),
198 }
199 }
200}
201
202impl From<Paint> for Fill {
203 fn from(paint: Paint) -> Self {
204 Self {
205 paint: Property::new(paint),
206 ..Self::default()
207 }
208 }
209}
210impl From<Color> for Fill {
211 fn from(color: Color) -> Self {
212 Paint::Solid(color).into()
213 }
214}
215impl From<Paint> for Stroke {
216 fn from(paint: Paint) -> Self {
217 Self {
218 paint: Property::new(paint),
219 ..Self::default()
220 }
221 }
222}
223impl From<Color> for Stroke {
224 fn from(color: Color) -> Self {
225 Paint::Solid(color).into()
226 }
227}
228
229impl Stroke {
230 pub fn width_pixels(&self) -> f64 {
232 match self.width.get() {
233 Size::Pixels(value) if value.to_float().is_finite() => value.to_float().max(0.0),
234 _ => {
235 log::warn!("Stroke width must be finite logical pixels");
236 0.0
237 }
238 }
239 }
240}
241
242impl ResolvedStroke {
243 pub fn width_pixels(&self) -> f64 {
245 match self.width.clone() {
246 Size::Pixels(value) if value.to_float().is_finite() => value.to_float().max(0.0),
247 _ => {
248 log::warn!("Stroke width must be finite logical pixels");
249 0.0
250 }
251 }
252 }
253}
254
255macro_rules! appearance_layer_traits {
256 ($ty:ident, $($field:ident),+) => {
257 impl PartialEq for $ty {
258 fn eq(&self, other: &Self) -> bool { $(self.$field.get() == other.$field.get())&&+ }
259 }
260 impl Hash for $ty {
261 fn hash<H: Hasher>(&self, state: &mut H) { $(self.$field.get().hash(state);)+ }
262 }
263 impl Interpolatable for $ty {
264 fn interpolate(&self, other: &Self, t: f64) -> Self {
265 Self { $($field: Property::new(self.$field.get().interpolate(&other.$field.get(), t))),+ }
266 }
267 }
268 }
269}
270appearance_layer_traits!(Fill, paint, material, opacity);
271appearance_layer_traits!(Stroke, paint, material, opacity, width, cap, join);
272
273pub enum NavigationTarget {
275 Current,
277 New,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
285#[serde(crate = "crate::serde")]
286pub enum Paint {
287 Solid(Color),
289 LinearGradient(LinearGradient),
291 RadialGradient(RadialGradient),
293 Blend(Vec<(Paint, f64)>),
297 MeshGradient(MeshGradient),
299}
300
301impl From<Color> for Paint {
302 fn from(color: Color) -> Self {
303 Self::Solid(color)
304 }
305}
306
307impl Hash for Paint {
308 fn hash<H: Hasher>(&self, state: &mut H) {
309 match self {
310 Paint::Solid(color) => {
311 state.write_u8(0);
312 color.hash(state);
313 }
314 Paint::LinearGradient(linear) => {
315 state.write_u8(1);
316 linear.start.hash(state);
317 linear.end.hash(state);
318 linear.stops.hash(state);
319 }
320 Paint::RadialGradient(radial) => {
321 state.write_u8(2);
322 radial.start.hash(state);
323 radial.end.hash(state);
324 radial.radius.to_bits().hash(state);
325 radial.stops.hash(state);
326 }
327 Paint::Blend(terms) => {
328 state.write_u8(3);
329 terms.len().hash(state);
330 for (fill, weight) in terms {
331 fill.hash(state);
332 weight.to_bits().hash(state);
333 }
334 }
335 Paint::MeshGradient(mesh) => {
336 state.write_u8(4);
337 mesh.hash(state);
338 }
339 }
340 }
341}
342
343#[derive(Debug, Copy, Clone, Serialize, Deserialize, Default, PartialEq, PartialOrd)]
349#[serde(crate = "crate::serde")]
350pub struct Depth(pub Numeric);
351
352impl Depth {
353 pub fn to_float(&self) -> f64 {
355 self.0.to_float()
356 }
357}
358
359impl From<Numeric> for Depth {
360 fn from(value: Numeric) -> Self {
361 Self(value)
362 }
363}
364
365impl From<f64> for Depth {
366 fn from(value: f64) -> Self {
367 Self(Numeric::F64(value))
368 }
369}
370
371impl Hash for Depth {
372 fn hash<H: Hasher>(&self, state: &mut H) {
373 self.0.hash(state);
374 }
375}
376
377impl Interpolatable for Depth {
378 fn interpolate(&self, other: &Self, t: f64) -> Self {
379 Self(self.0.interpolate(&other.0, t))
380 }
381}
382
383impl HelperFunctions for Depth {}
384
385#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
387#[serde(crate = "crate::serde")]
388pub struct Vector3 {
389 pub x: f64,
391 pub y: f64,
393 pub z: f64,
395}
396
397impl Vector3 {
398 pub fn new(x: f64, y: f64, z: f64) -> Self {
400 Self { x, y, z }
401 }
402}
403
404impl Default for Vector3 {
405 fn default() -> Self {
406 Self {
407 x: 0.0,
408 y: 0.0,
409 z: 0.0,
410 }
411 }
412}
413
414impl Hash for Vector3 {
415 fn hash<H: Hasher>(&self, state: &mut H) {
416 self.x.to_bits().hash(state);
417 self.y.to_bits().hash(state);
418 self.z.to_bits().hash(state);
419 }
420}
421
422impl Interpolatable for Vector3 {
423 fn interpolate(&self, other: &Self, t: f64) -> Self {
424 Self {
425 x: self.x.interpolate(&other.x, t),
426 y: self.y.interpolate(&other.y, t),
427 z: self.z.interpolate(&other.z, t),
428 }
429 }
430}
431
432impl HelperFunctions for Vector3 {
433 fn register_all_functions() {
434 register_function(
435 "Vector3".to_string(),
436 "new".to_string(),
437 std::sync::Arc::new(|args| {
438 if args.len() != 3 {
439 return Err("Expected 3 arguments for function Vector3::new".to_string());
440 }
441 let mut itr = args.into_iter();
442 let x = f64::try_coerce(itr.next().unwrap())?;
443 let y = f64::try_coerce(itr.next().unwrap())?;
444 let z = f64::try_coerce(itr.next().unwrap())?;
445 Ok(Vector3::new(x, y, z).to_pax_value())
446 }),
447 );
448 }
449}
450
451#[derive(Debug, Copy, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
453#[serde(crate = "crate::serde")]
454pub enum LightShape {
455 #[default]
457 Point,
458 Directional,
460}
461
462impl Interpolatable for LightShape {}
463impl HelperFunctions for LightShape {}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
467#[serde(crate = "crate::serde")]
468pub struct MaterialParams {
469 pub ambient: Property<f64>,
471 pub diffuse: Property<f64>,
473 pub specular: Property<f64>,
475 pub roughness: Property<f64>,
477 pub metallic: Property<f64>,
479 pub emissive: Property<Color>,
481 pub emissive_intensity: Property<f64>,
483}
484
485impl Default for MaterialParams {
486 fn default() -> Self {
487 Self {
488 ambient: Property::new(1.0),
489 diffuse: Property::new(0.82),
490 specular: Property::new(0.08),
491 roughness: Property::new(0.78),
492 metallic: Property::new(0.0),
493 emissive: Property::new(Color::BLACK),
494 emissive_intensity: Property::new(0.0),
495 }
496 }
497}
498
499impl PartialEq for MaterialParams {
500 fn eq(&self, other: &Self) -> bool {
501 self.ambient.get() == other.ambient.get()
502 && self.diffuse.get() == other.diffuse.get()
503 && self.specular.get() == other.specular.get()
504 && self.roughness.get() == other.roughness.get()
505 && self.metallic.get() == other.metallic.get()
506 && self.emissive.get() == other.emissive.get()
507 && self.emissive_intensity.get() == other.emissive_intensity.get()
508 }
509}
510
511impl Hash for MaterialParams {
512 fn hash<H: Hasher>(&self, state: &mut H) {
513 self.ambient.get().to_bits().hash(state);
514 self.diffuse.get().to_bits().hash(state);
515 self.specular.get().to_bits().hash(state);
516 self.roughness.get().to_bits().hash(state);
517 self.metallic.get().to_bits().hash(state);
518 self.emissive.get().hash(state);
519 self.emissive_intensity.get().to_bits().hash(state);
520 }
521}
522
523impl Interpolatable for MaterialParams {
524 fn interpolate(&self, other: &Self, t: f64) -> Self {
525 Self {
526 ambient: Property::new(self.ambient.get().interpolate(&other.ambient.get(), t)),
527 diffuse: Property::new(self.diffuse.get().interpolate(&other.diffuse.get(), t)),
528 specular: Property::new(self.specular.get().interpolate(&other.specular.get(), t)),
529 roughness: Property::new(self.roughness.get().interpolate(&other.roughness.get(), t)),
530 metallic: Property::new(self.metallic.get().interpolate(&other.metallic.get(), t)),
531 emissive: Property::new(self.emissive.get().interpolate(&other.emissive.get(), t)),
532 emissive_intensity: Property::new(
533 self.emissive_intensity
534 .get()
535 .interpolate(&other.emissive_intensity.get(), t),
536 ),
537 }
538 }
539}
540
541impl HelperFunctions for MaterialParams {}
542
543#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
548#[serde(crate = "crate::serde")]
549pub enum Material {
550 Lit(MaterialParams),
552 Unlit,
554}
555
556#[derive(Clone, Debug, PartialEq)]
558pub struct ResolvedMaterialParams {
559 pub ambient: f64,
560 pub diffuse: f64,
561 pub specular: f64,
562 pub roughness: f64,
563 pub metallic: f64,
564 pub emissive: Color,
565 pub emissive_intensity: f64,
566}
567
568#[derive(Clone, Debug, PartialEq)]
570pub enum ResolvedMaterial {
571 Lit(ResolvedMaterialParams),
572 Unlit,
573}
574
575impl Material {
576 pub fn resolve_in_graph(&self) -> ResolvedMaterial {
578 match self {
579 Self::Unlit => ResolvedMaterial::Unlit,
580 Self::Lit(p) => ResolvedMaterial::Lit(ResolvedMaterialParams {
581 ambient: p.ambient.local().get(),
582 diffuse: p.diffuse.local().get(),
583 specular: p.specular.local().get(),
584 roughness: p.roughness.local().get(),
585 metallic: p.metallic.local().get(),
586 emissive: p.emissive.local().get(),
587 emissive_intensity: p.emissive_intensity.local().get(),
588 }),
589 }
590 }
591
592 pub fn snapshot(&self) -> ResolvedMaterial {
594 match self {
595 Self::Unlit => ResolvedMaterial::Unlit,
596 Self::Lit(p) => ResolvedMaterial::Lit(ResolvedMaterialParams {
597 ambient: p.ambient.get(),
598 diffuse: p.diffuse.get(),
599 specular: p.specular.get(),
600 roughness: p.roughness.get(),
601 metallic: p.metallic.get(),
602 emissive: p.emissive.get(),
603 emissive_intensity: p.emissive_intensity.get(),
604 }),
605 }
606 }
607}
608
609impl Default for Material {
610 fn default() -> Self {
611 Self::matte()
612 }
613}
614
615impl Material {
616 pub fn matte() -> Self {
618 Self::Lit(MaterialParams::default())
619 }
620
621 pub fn glossy(specular: f64) -> Self {
623 let mut params = MaterialParams::default();
624 params.specular = Property::new(specular.clamp(0.0, 1.0));
625 params.roughness = Property::new(0.28);
626 Self::Lit(params)
627 }
628
629 pub fn metallic(metallic: f64) -> Self {
631 let mut params = MaterialParams::default();
632 params.metallic = Property::new(metallic.clamp(0.0, 1.0));
633 params.diffuse = Property::new(0.42);
634 params.specular = Property::new(0.55);
635 params.roughness = Property::new(0.36);
636 Self::Lit(params)
637 }
638
639 pub fn emissive(color: Color, intensity: f64) -> Self {
641 let mut params = MaterialParams::default();
642 params.emissive = Property::new(color);
643 params.emissive_intensity = Property::new(intensity.max(0.0));
644 Self::Lit(params)
645 }
646
647 pub fn custom(params: MaterialParams) -> Self {
649 Self::Lit(params)
650 }
651
652 pub fn unlit() -> Self {
654 Self::Unlit
655 }
656}
657
658impl Hash for Material {
659 fn hash<H: Hasher>(&self, state: &mut H) {
660 match self {
661 Material::Lit(params) => {
662 state.write_u8(0);
663 params.hash(state);
664 }
665 Material::Unlit => {
666 state.write_u8(1);
667 }
668 }
669 }
670}
671
672impl Interpolatable for Material {
673 fn interpolate(&self, other: &Self, t: f64) -> Self {
674 match (self, other) {
675 (Self::Lit(lhs), Self::Lit(rhs)) => Self::Lit(lhs.interpolate(rhs, t)),
676 _ if t < 1.0 => self.clone(),
677 _ => other.clone(),
678 }
679 }
680}
681
682impl HelperFunctions for Material {
683 fn register_all_functions() {
684 register_function(
685 "Material".to_string(),
686 "matte".to_string(),
687 std::sync::Arc::new(|args| {
688 if !args.is_empty() {
689 return Err("Expected 0 arguments for function Material::matte".to_string());
690 }
691 Ok(Material::matte().to_pax_value())
692 }),
693 );
694 register_function(
695 "Material".to_string(),
696 "glossy".to_string(),
697 std::sync::Arc::new(|args| {
698 if args.len() != 1 {
699 return Err("Expected 1 argument for function Material::glossy".to_string());
700 }
701 let specular = f64::try_coerce(args.into_iter().next().unwrap())?;
702 Ok(Material::glossy(specular).to_pax_value())
703 }),
704 );
705 register_function(
706 "Material".to_string(),
707 "metallic".to_string(),
708 std::sync::Arc::new(|args| {
709 if args.len() != 1 {
710 return Err("Expected 1 argument for function Material::metallic".to_string());
711 }
712 let metallic = f64::try_coerce(args.into_iter().next().unwrap())?;
713 Ok(Material::metallic(metallic).to_pax_value())
714 }),
715 );
716 register_function(
717 "Material".to_string(),
718 "emissive".to_string(),
719 std::sync::Arc::new(|args| {
720 if args.len() != 2 {
721 return Err("Expected 2 arguments for function Material::emissive".to_string());
722 }
723 let mut itr = args.into_iter();
724 let color = Color::try_coerce(itr.next().unwrap())?;
725 let intensity = f64::try_coerce(itr.next().unwrap())?;
726 Ok(Material::emissive(color, intensity).to_pax_value())
727 }),
728 );
729 register_function(
730 "Material".to_string(),
731 "custom".to_string(),
732 std::sync::Arc::new(|args| {
733 if args.len() != 1 {
734 return Err("Expected 1 argument for function Material::custom".to_string());
735 }
736 let params = MaterialParams::try_coerce(args.into_iter().next().unwrap())?;
737 Ok(Material::custom(params).to_pax_value())
738 }),
739 );
740 register_function(
741 "Material".to_string(),
742 "unlit".to_string(),
743 std::sync::Arc::new(|args| {
744 if !args.is_empty() {
745 return Err("Expected 0 arguments for function Material::unlit".to_string());
746 }
747 Ok(Material::unlit().to_pax_value())
748 }),
749 );
750 }
751}
752
753#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
755#[serde(crate = "crate::serde")]
756pub struct SceneAmbientLight {
757 pub color: Color,
759 pub intensity: f64,
761}
762
763impl Default for SceneAmbientLight {
764 fn default() -> Self {
765 Self {
766 color: Color::WHITE,
767 intensity: 1.0,
768 }
769 }
770}
771
772impl Interpolatable for SceneAmbientLight {
773 fn interpolate(&self, other: &Self, t: f64) -> Self {
774 Self {
775 color: self.color.interpolate(&other.color, t),
776 intensity: self.intensity.interpolate(&other.intensity, t),
777 }
778 }
779}
780
781impl HelperFunctions for SceneAmbientLight {}
782
783#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
785#[serde(crate = "crate::serde")]
786pub struct SceneLight {
787 pub shape: LightShape,
789 pub position: Vector3,
791 pub direction: Vector3,
793 pub color: Color,
795 pub intensity: f64,
797 pub radius: f64,
799 pub enabled: bool,
801}
802
803impl Default for SceneLight {
804 fn default() -> Self {
805 Self {
806 shape: LightShape::Point,
807 position: Vector3::default(),
808 direction: Vector3::new(0.0, 0.0, -1.0),
809 color: Color::WHITE,
810 intensity: 1.0,
811 radius: 240.0,
812 enabled: true,
813 }
814 }
815}
816
817impl Interpolatable for SceneLight {
818 fn interpolate(&self, other: &Self, t: f64) -> Self {
819 Self {
820 shape: if t < 1.0 { self.shape } else { other.shape },
821 position: self.position.interpolate(&other.position, t),
822 direction: self.direction.interpolate(&other.direction, t),
823 color: self.color.interpolate(&other.color, t),
824 intensity: self.intensity.interpolate(&other.intensity, t),
825 radius: self.radius.interpolate(&other.radius, t),
826 enabled: if t < 1.0 { self.enabled } else { other.enabled },
827 }
828 }
829}
830
831impl HelperFunctions for SceneLight {}
832
833#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
835#[serde(crate = "crate::serde")]
836pub struct SceneLighting {
837 pub active: bool,
839 pub ambient_is_authored: bool,
844 pub ambient: SceneAmbientLight,
846 pub lights: Vec<SceneLight>,
848}
849
850impl Default for SceneLighting {
851 fn default() -> Self {
852 Self {
853 active: false,
854 ambient_is_authored: false,
855 ambient: SceneAmbientLight::default(),
856 lights: vec![],
857 }
858 }
859}
860
861impl SceneLighting {
862 pub const MAX_LIGHTS: usize = 8;
864
865 pub const DEFAULT_AMBIENT_INTENSITY: f64 = 0.35;
867
868 pub fn identity() -> Self {
870 Self::default()
871 }
872
873 pub fn with_default_ambient(lights: Vec<SceneLight>) -> Self {
875 Self {
876 active: !lights.is_empty(),
877 ambient_is_authored: false,
878 ambient: SceneAmbientLight {
879 color: Color::WHITE,
880 intensity: Self::DEFAULT_AMBIENT_INTENSITY,
881 },
882 lights,
883 }
884 }
885}
886
887impl Interpolatable for SceneLighting {}
888impl HelperFunctions for SceneLighting {}
889
890impl Interpolatable for Paint {
891 fn interpolate(&self, other: &Self, t: f64) -> Self {
892 if let (Self::Solid(from), Self::Solid(to)) = (self, other) {
893 return Self::Solid(from.interpolate(to, t));
894 }
895 if t <= 0.0 || self == other {
896 return self.clone();
897 }
898 if t >= 1.0 {
899 return other.clone();
900 }
901 Self::blend(vec![(self.clone(), 1.0 - t), (other.clone(), t)])
902 }
903}
904
905#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Hash)]
910#[serde(crate = "crate::serde")]
911pub struct LinearGradient {
912 pub start: (Size, Size),
914 pub end: (Size, Size),
916 pub stops: Vec<GradientStop>,
918}
919
920#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Hash)]
924#[serde(crate = "crate::serde")]
925pub struct MeshPoint {
926 pub position: (Size, Size),
928 pub color: Color,
930}
931
932impl Interpolatable for MeshPoint {
933 fn interpolate(&self, other: &Self, t: f64) -> Self {
934 Self {
935 position: self.position.interpolate(&other.position, t),
936 color: self.color.interpolate(&other.color, t),
937 }
938 }
939}
940
941#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Hash)]
947#[serde(crate = "crate::serde")]
948pub struct MeshGradient {
949 pub rows: Vec<Vec<MeshPoint>>,
951}
952
953impl Interpolatable for MeshGradient {}
954
955impl MeshGradient {
956 pub fn validate(&self) -> Result<(), String> {
958 let width = self.rows.first().map_or(0, Vec::len);
959 if !(2..=8).contains(&self.rows.len())
960 || !(2..=8).contains(&width)
961 || self.rows.iter().any(|row| row.len() != width)
962 {
963 return Err("mesh gradients require 2–8 equal-length rows of 2–8 anchors".into());
964 }
965 let finite_size = |size: &Size| match size {
966 Size::Pixels(v) | Size::Percent(v) => v.to_float().is_finite(),
967 Size::Combined(px, percent) => {
968 px.to_float().is_finite() && percent.to_float().is_finite()
969 }
970 };
971 if self.rows.iter().flatten().any(|p| {
972 !finite_size(&p.position.0)
973 || !finite_size(&p.position.1)
974 || p.color.to_rgba_0_1().iter().any(|c| !c.is_finite())
975 }) {
976 return Err("mesh gradient positions and colors must be finite".into());
977 }
978 Ok(())
979 }
980}
981
982#[cfg(test)]
983mod mesh_value_tests {
984 use super::*;
985
986 #[test]
987 fn mesh_values_roundtrip_and_opacity_scales_the_sampled_field() {
988 let point = MeshPoint {
989 position: (
990 Size::Combined(5.into(), 40.into()),
991 Size::Percent(70.into()),
992 ),
993 color: Color::CYAN,
994 };
995 let mesh = MeshGradient {
996 rows: vec![vec![point.clone(); 2]; 2],
997 };
998 assert!(mesh.validate().is_ok());
999 let paint = Paint::MeshGradient(mesh.clone());
1000 assert_eq!(
1001 Paint::try_coerce(paint.clone().to_pax_value()).unwrap(),
1002 paint
1003 );
1004 assert_eq!(
1005 MeshPoint::try_coerce(point.clone().to_pax_value()).unwrap(),
1006 point
1007 );
1008 let Paint::Blend(terms) = paint.with_alpha_factor(0.25) else {
1009 panic!("post-sampling opacity")
1010 };
1011 assert_eq!(terms[0], (paint.clone(), 0.25));
1012 assert_eq!(paint.with_alpha_factor(0.0).max_alpha_0_1(), 0.0);
1013 assert_eq!(paint.interpolate(&Paint::Solid(Color::WHITE), 0.0), paint);
1014 assert!(matches!(
1015 paint.interpolate(&Paint::Solid(Color::WHITE), 0.5),
1016 Paint::Blend(_)
1017 ));
1018 let mut invalid = mesh;
1019 invalid.rows[0].pop();
1020 assert!(invalid.validate().is_err());
1021 }
1022
1023 #[test]
1024 fn malformed_mesh_coordinates_never_panic_or_truncate() {
1025 for coordinates in [
1026 vec![],
1027 vec![Size::Percent(0.into())],
1028 vec![Size::Percent(0.into()); 3],
1029 ] {
1030 let value = PaxValue::Object(vec![
1031 ("position".into(), coordinates.to_pax_value()),
1032 ("color".into(), Color::CYAN.to_pax_value()),
1033 ]);
1034 assert!(MeshPoint::try_coerce(value).is_err());
1035 }
1036 }
1037}
1038
1039#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1044#[serde(crate = "crate::serde")]
1045pub struct RadialGradient {
1046 pub end: (Size, Size),
1048 pub start: (Size, Size),
1050 pub radius: f64,
1052 pub stops: Vec<GradientStop>,
1054}
1055
1056#[derive(Debug, Clone, Copy, PartialEq)]
1059pub struct ResolvedRadialGradient {
1060 pub focal_point: kurbo::Point,
1062 pub center: kurbo::Point,
1064 pub radius: f64,
1066}
1067
1068impl RadialGradient {
1069 pub fn resolve_geometry(&self, rect: kurbo::Rect) -> Option<ResolvedRadialGradient> {
1073 let bounds = (rect.width(), rect.height());
1074 let resolve = |p: (Size, Size)| {
1075 kurbo::Point::new(
1076 rect.x0 + p.0.evaluate(bounds, Axis::X),
1077 rect.y0 + p.1.evaluate(bounds, Axis::Y),
1078 )
1079 };
1080 let focal_point = resolve(self.start);
1081 let center = resolve(self.end);
1082 if self.radius <= 0.0
1083 || ![
1084 self.radius,
1085 focal_point.x,
1086 focal_point.y,
1087 center.x,
1088 center.y,
1089 ]
1090 .into_iter()
1091 .all(f64::is_finite)
1092 {
1093 return None;
1094 }
1095 Some(ResolvedRadialGradient {
1096 focal_point,
1097 center,
1098 radius: self.radius,
1099 })
1100 }
1101}
1102
1103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Hash)]
1105#[serde(crate = "crate::serde")]
1106pub struct GradientStop {
1107 pub position: Size,
1109 pub color: Color,
1111}
1112
1113impl GradientStop {
1114 pub fn get(color: Color, position: Size) -> GradientStop {
1116 GradientStop { position, color }
1117 }
1118
1119 pub fn with_alpha_factor(&self, factor: f64) -> GradientStop {
1121 GradientStop {
1122 position: self.position.clone(),
1123 color: self.color.with_alpha_factor(factor),
1124 }
1125 }
1126}
1127
1128impl Default for Paint {
1129 fn default() -> Self {
1130 Self::Solid(Color::default())
1131 }
1132}
1133
1134impl Paint {
1135 pub fn blend(terms: Vec<(Paint, f64)>) -> Self {
1143 fn append(out: &mut Vec<(Paint, f64)>, terms: Vec<(Paint, f64)>, scale: f64) {
1144 let max = terms
1145 .iter()
1146 .map(|(_, w)| *w)
1147 .filter(|w| w.is_finite())
1148 .fold(0.0, f64::max);
1149 if max <= 0.0 {
1150 return;
1151 }
1152 let sum: f64 = terms
1153 .iter()
1154 .map(|(_, w)| *w)
1155 .filter(|w| w.is_finite() && *w > 0.0)
1156 .map(|w| w / max)
1157 .sum();
1158 for (fill, weight) in terms {
1159 if !weight.is_finite() || weight <= 0.0 {
1160 continue;
1161 }
1162 let weight = scale * (weight / max) / sum;
1163 if weight == 0.0 {
1164 continue;
1165 }
1166 if let Paint::Blend(terms) = fill {
1167 append(out, terms, weight);
1168 } else if let Some((_, existing)) = out.iter_mut().find(|(f, _)| *f == fill) {
1169 *existing += weight;
1170 } else {
1171 out.push((fill, weight));
1172 }
1173 }
1174 }
1175 let mut result = Vec::new();
1176 append(&mut result, terms, 1.0);
1177 if result.is_empty() {
1178 return Paint::Solid(Color::from_rgba_0_1([0.0; 4]));
1179 }
1180 if result.len() == 1 {
1181 result.pop().unwrap().0
1182 } else {
1183 Paint::Blend(result)
1184 }
1185 }
1186
1187 pub fn representative_color(&self) -> Color {
1190 match self {
1191 Self::Solid(color) => color.clone(),
1192 Self::LinearGradient(g) => g.stops.first().map(|s| s.color.clone()).unwrap_or_default(),
1193 Self::RadialGradient(g) => g.stops.first().map(|s| s.color.clone()).unwrap_or_default(),
1194 Self::MeshGradient(_) => Color::TRANSPARENT,
1196 Self::Blend(terms) => Color::blend_premultiplied(
1197 terms
1198 .iter()
1199 .map(|(fill, weight)| (fill.representative_color(), *weight)),
1200 ),
1201 }
1202 }
1203
1204 fn gradient_stop_position_0_1(position: &Size) -> Option<f64> {
1205 match position {
1206 Size::Percent(p) => Some((p.to_float() / 100.0).clamp(0.0, 1.0)),
1207 Size::Pixels(_) | Size::Combined(_, _) => None,
1208 }
1209 }
1210
1211 fn integrated_gradient_alpha_0_1(stops: &[GradientStop]) -> f64 {
1212 if stops.is_empty() {
1213 return 0.0;
1214 }
1215
1216 let mut positioned_stops = stops
1217 .iter()
1218 .filter_map(|stop| {
1219 Self::gradient_stop_position_0_1(&stop.position)
1220 .map(|position| (position, stop.color.alpha_0_1()))
1221 })
1222 .collect::<Vec<_>>();
1223
1224 if positioned_stops.is_empty() {
1225 return stops
1226 .iter()
1227 .map(|stop| stop.color.alpha_0_1())
1228 .fold(0.0, f64::max);
1229 }
1230
1231 positioned_stops.sort_by(|lhs, rhs| lhs.0.total_cmp(&rhs.0));
1232
1233 if positioned_stops
1234 .first()
1235 .map(|stop| stop.0 > 0.0)
1236 .unwrap_or(false)
1237 {
1238 let alpha = positioned_stops[0].1;
1239 positioned_stops.insert(0, (0.0, alpha));
1240 }
1241
1242 if positioned_stops
1243 .last()
1244 .map(|stop| stop.0 < 1.0)
1245 .unwrap_or(false)
1246 {
1247 let alpha = positioned_stops.last().map(|stop| stop.1).unwrap_or(0.0);
1248 positioned_stops.push((1.0, alpha));
1249 }
1250
1251 let mut integrated_alpha = 0.0;
1252 for window in positioned_stops.windows(2) {
1253 let [(start_pos, start_alpha), (end_pos, end_alpha)] = window else {
1254 continue;
1255 };
1256 let span = (end_pos - start_pos).clamp(0.0, 1.0);
1257 if span <= f64::EPSILON {
1258 continue;
1259 }
1260 integrated_alpha += (start_alpha + end_alpha) * 0.5 * span;
1261 }
1262
1263 integrated_alpha.clamp(0.0, 1.0)
1264 }
1265
1266 pub fn to_unit_point((x, y): (Size, Size), (width, height): (f64, f64)) -> UnitPoint {
1268 let normalized_x = match x {
1269 Size::Pixels(val) => val.to_float() / width,
1270 Size::Percent(val) => val.to_float() / 100.0,
1271 Size::Combined(pix, per) => (pix.to_float() / width) + (per.to_float() / 100.0),
1272 };
1273
1274 let normalized_y = match y {
1275 Size::Pixels(val) => val.to_float() / height,
1276 Size::Percent(val) => val.to_float() / 100.0,
1277 Size::Combined(pix, per) => (pix.to_float() / width) + (per.to_float() / 100.0),
1278 };
1279 UnitPoint::new(normalized_x, normalized_y)
1280 }
1281
1282 pub fn to_piet_gradient_stops(stops: Vec<GradientStop>) -> Vec<piet::GradientStop> {
1284 let mut ret = Vec::new();
1285 for gradient_stop in stops {
1286 match gradient_stop.position {
1287 Size::Pixels(_) => {
1288 panic!("Gradient stops must be specified in percentages");
1289 }
1290 Size::Percent(p) => {
1291 ret.push(piet::GradientStop {
1292 pos: (p.to_float() / 100.0) as f32,
1293 color: gradient_stop.color.to_piet_color(),
1294 });
1295 }
1296 Size::Combined(_, _) => {
1297 panic!("Gradient stops must be specified in percentages");
1298 }
1299 }
1300 }
1301 ret
1302 }
1303
1304 #[allow(non_snake_case)]
1305 pub fn linearGradient(
1310 start: (Size, Size),
1311 end: (Size, Size),
1312 stops: Vec<GradientStop>,
1313 ) -> Paint {
1314 Paint::LinearGradient(LinearGradient { start, end, stops })
1315 }
1316
1317 pub fn with_alpha_factor(&self, factor: f64) -> Paint {
1319 match self {
1320 Paint::Blend(terms) => Paint::Blend(
1321 terms
1322 .iter()
1323 .map(|(fill, weight)| (fill.with_alpha_factor(factor), *weight))
1324 .collect(),
1325 ),
1326 Paint::Solid(color) => Paint::Solid(color.with_alpha_factor(factor)),
1327 Paint::MeshGradient(_) => {
1328 let factor = factor.clamp(0.0, 1.0);
1331 Paint::blend(vec![
1332 (self.clone(), factor),
1333 (Paint::Solid(Color::TRANSPARENT), 1.0 - factor),
1334 ])
1335 }
1336 Paint::LinearGradient(gradient) => Paint::LinearGradient(LinearGradient {
1337 start: gradient.start.clone(),
1338 end: gradient.end.clone(),
1339 stops: gradient
1340 .stops
1341 .iter()
1342 .map(|stop| stop.with_alpha_factor(factor))
1343 .collect(),
1344 }),
1345 Paint::RadialGradient(gradient) => Paint::RadialGradient(RadialGradient {
1346 start: gradient.start.clone(),
1347 end: gradient.end.clone(),
1348 radius: gradient.radius,
1349 stops: gradient
1350 .stops
1351 .iter()
1352 .map(|stop| stop.with_alpha_factor(factor))
1353 .collect(),
1354 }),
1355 }
1356 }
1357
1358 pub fn max_alpha_0_1(&self) -> f64 {
1361 match self {
1362 Paint::Blend(terms) => terms
1363 .iter()
1364 .map(|(fill, weight)| fill.max_alpha_0_1() * weight)
1365 .sum::<f64>()
1366 .clamp(0.0, 1.0),
1367 Paint::Solid(color) => color.alpha_0_1(),
1368 Paint::MeshGradient(mesh) => {
1369 if mesh
1372 .rows
1373 .iter()
1374 .flatten()
1375 .any(|p| p.color.alpha_0_1() > 0.0)
1376 {
1377 1.0
1378 } else {
1379 0.0
1380 }
1381 }
1382 Paint::LinearGradient(gradient) => gradient
1383 .stops
1384 .iter()
1385 .map(|stop| stop.color.alpha_0_1())
1386 .fold(0.0, f64::max),
1387 Paint::RadialGradient(gradient) => gradient
1388 .stops
1389 .iter()
1390 .map(|stop| stop.color.alpha_0_1())
1391 .fold(0.0, f64::max),
1392 }
1393 }
1394
1395 pub fn coverage_alpha_0_1(&self) -> f64 {
1397 match self {
1398 Paint::Blend(terms) => terms
1399 .iter()
1400 .map(|(fill, weight)| fill.coverage_alpha_0_1() * weight)
1401 .sum::<f64>()
1402 .clamp(0.0, 1.0),
1403 Paint::Solid(color) => color.alpha_0_1(),
1404 Paint::LinearGradient(gradient) => Self::integrated_gradient_alpha_0_1(&gradient.stops),
1405 Paint::RadialGradient(gradient) => Self::integrated_gradient_alpha_0_1(&gradient.stops),
1406 Paint::MeshGradient(_) => self.max_alpha_0_1(),
1407 }
1408 }
1409}
1410
1411#[cfg(test)]
1412mod fill_coverage_tests {
1413 fn gradient(color: Color) -> Paint {
1414 Paint::linearGradient(
1415 (Size::Percent(0.into()), Size::Percent(0.into())),
1416 (Size::Percent(100.into()), Size::Percent(100.into())),
1417 vec![GradientStop::get(color, Size::Percent(0.into()))],
1418 )
1419 }
1420
1421 #[test]
1422 fn transparent_paint_crossfade_uses_premultiplied_color_and_alpha() {
1423 use super::*;
1424 let from = gradient(Color::from_rgba_0_1([1.0, 0.0, 0.0, 0.0]));
1425 let to = Paint::Solid(Color::from_rgba_0_1([0.0, 0.0, 1.0, 0.5]));
1426 let halfway = from.interpolate(&to, 0.5);
1427 assert_eq!(
1428 halfway.representative_color().to_rgba_0_1(),
1429 [0.0, 0.0, 1.0, 0.25]
1430 );
1431 assert_eq!(halfway.max_alpha_0_1(), 0.25);
1432 assert_eq!(halfway.with_alpha_factor(0.5).max_alpha_0_1(), 0.125);
1433 assert_eq!(from.interpolate(&to, 0.0), from);
1434 assert_eq!(from.interpolate(&to, 1.0), to);
1435 }
1436
1437 #[test]
1438 fn interrupted_gradient_reversals_remain_flat_continuous_and_bounded() {
1439 use super::*;
1440 let dark = gradient(Color::BLACK);
1441 let light = gradient(Color::WHITE);
1442 let mut displayed = dark.clone();
1443 for i in 0..1000 {
1444 let target = if i % 2 == 0 { &light } else { &dark };
1445 assert_eq!(displayed.interpolate(target, 0.0), displayed);
1446 let before = displayed.representative_color().to_rgba_0_1();
1447 displayed = displayed.interpolate(target, 0.07);
1448 let after = displayed.representative_color().to_rgba_0_1();
1449 let expected = target.representative_color().to_rgba_0_1();
1450 for channel in 0..4 {
1451 assert!(
1452 (after[channel] - (before[channel] * 0.93 + expected[channel] * 0.07)).abs()
1453 < 1e-12
1454 );
1455 }
1456 let Paint::Blend(terms) = &displayed else {
1457 panic!("expected mixture");
1458 };
1459 assert_eq!(terms.len(), 2);
1460 assert!(terms
1461 .iter()
1462 .all(|(fill, _)| !matches!(fill, Paint::Blend(_))));
1463 let roundtrip = Paint::try_coerce(displayed.clone().to_pax_value()).unwrap();
1464 for (a, b) in roundtrip
1465 .representative_color()
1466 .to_rgba_0_1()
1467 .into_iter()
1468 .zip(after)
1469 {
1470 assert!((a - b).abs() < 1e-12);
1471 }
1472 }
1473 assert_eq!(displayed.interpolate(&light, 1.0), light);
1474 }
1475
1476 #[test]
1477 fn crossfade_keeps_different_gradient_kinds_geometry_and_stop_lists() {
1478 use super::*;
1479 let from = gradient(Color::BLACK);
1480 let to = Paint::RadialGradient(RadialGradient {
1481 start: (Size::Pixels(12.into()), Size::Percent(50.into())),
1482 end: (Size::Percent(100.into()), Size::Percent(50.into())),
1483 radius: 0.5,
1484 stops: vec![
1485 GradientStop::get(Color::WHITE, Size::Percent(20.into())),
1486 GradientStop::get(Color::BLACK, Size::Percent(80.into())),
1487 ],
1488 });
1489 assert_eq!(
1490 from.interpolate(&to, 0.25),
1491 Paint::Blend(vec![(from, 0.75), (to, 0.25)])
1492 );
1493 }
1494
1495 #[test]
1496 fn solid_fill_interpolates_rgba_channels() {
1497 use super::*;
1498 let from = Paint::Solid(Color::BLACK);
1499 let to = Paint::Solid(Color::WHITE);
1500 assert_eq!(
1501 from.interpolate(&to, 0.5),
1502 Paint::Solid(Color::BLACK.interpolate(&Color::WHITE, 0.5))
1503 );
1504 }
1505 use super::{Color, ColorChannel, GradientStop, LinearGradient, Numeric, Paint, Size};
1506
1507 fn rgba_alpha(alpha: u8) -> Color {
1508 Color::rgba(
1509 ColorChannel::Integer(0),
1510 ColorChannel::Integer(0),
1511 ColorChannel::Integer(0),
1512 ColorChannel::Integer(alpha),
1513 )
1514 }
1515
1516 #[test]
1517 fn coverage_alpha_matches_solid_alpha() {
1518 let fill = Paint::Solid(rgba_alpha(128));
1519 assert!((fill.coverage_alpha_0_1() - (128.0 / 255.0)).abs() < 1e-6);
1520 }
1521
1522 #[test]
1523 fn coverage_alpha_integrates_gradient_stops() {
1524 let fill = Paint::LinearGradient(LinearGradient {
1525 start: (
1526 Size::Percent(Numeric::F64(0.0)),
1527 Size::Percent(Numeric::F64(50.0)),
1528 ),
1529 end: (
1530 Size::Percent(Numeric::F64(100.0)),
1531 Size::Percent(Numeric::F64(50.0)),
1532 ),
1533 stops: vec![
1534 GradientStop::get(rgba_alpha(0), Size::Percent(Numeric::F64(0.0))),
1535 GradientStop::get(rgba_alpha(255), Size::Percent(Numeric::F64(50.0))),
1536 GradientStop::get(rgba_alpha(0), Size::Percent(Numeric::F64(100.0))),
1537 ],
1538 });
1539 assert!((fill.coverage_alpha_0_1() - 0.5).abs() < 1e-6);
1540 }
1541}
1542
1543pub fn paint_bounds(mut rect: kurbo::Rect) -> Option<kurbo::Rect> {
1546 if ![rect.x0, rect.y0, rect.x1, rect.y1]
1547 .into_iter()
1548 .all(f64::is_finite)
1549 {
1550 log::warn!("Paint geometry must be finite");
1551 return None;
1552 }
1553 if rect.width() == 0.0 {
1554 rect.x0 -= 0.5;
1555 rect.x1 += 0.5;
1556 }
1557 if rect.height() == 0.0 {
1558 rect.y0 -= 0.5;
1559 rect.y1 += 0.5;
1560 }
1561 Some(rect)
1562}
1563
1564impl Fill {
1565 pub(crate) fn validate_layer(&self) -> Result<(), String> {
1566 Ok(())
1567 }
1568}
1569impl Stroke {
1570 pub(crate) fn validate_layer(&self) -> Result<(), String> {
1571 if matches!(self.width.get(), Size::Pixels(_)) {
1572 Ok(())
1573 } else {
1574 Err("Stroke.width: expected logical pixels; percentage widths are unsupported".into())
1575 }
1576 }
1577}
1578
1579#[cfg(test)]
1580mod appearance_tests {
1581 use super::*;
1582 use crate::properties::drain_effects;
1583
1584 #[test]
1585 fn appearance_defaults_shorthand_and_errors() {
1586 let red = Color::RED.to_pax_value();
1587 let fills = Vec::<Fill>::try_coerce(red.clone()).unwrap();
1588 let strokes = Vec::<Stroke>::try_coerce(red).unwrap();
1589 assert_eq!(fills[0].paint.get(), Paint::Solid(Color::RED));
1590 assert_eq!(strokes[0].width_pixels(), 1.0);
1591 assert_eq!(fills[0].material.get(), Material::default());
1592 assert_eq!(fills[0].opacity.get().to_float_0_1(), 1.0);
1593 assert!(Vec::<Fill>::try_coerce(PaxValue::Vec(vec![]))
1594 .unwrap()
1595 .is_empty());
1596 assert!(Vec::<Numeric>::try_coerce(1.to_pax_value()).is_err());
1597 for invalid in [
1598 Stroke::default().to_pax_value(),
1599 PaxValue::Vec(vec![]),
1600 PaxValue::Option(Box::new(None)),
1601 PaxValue::Object(vec![("color".into(), Color::RED.to_pax_value())]),
1602 ] {
1603 let error =
1604 Vec::<Fill>::try_coerce(PaxValue::Vec(vec![Color::RED.to_pax_value(), invalid]))
1605 .unwrap_err();
1606 assert!(error.contains("[1]"), "{error}");
1607 }
1608 assert!(Stroke::try_coerce(PaxValue::Object(vec![(
1609 "width".into(),
1610 Size::Percent(10.into()).to_pax_value()
1611 )]))
1612 .is_err());
1613 }
1614
1615 #[test]
1616 fn layers_interpolate_by_position_and_switch_lengths_immediately() {
1617 let a = vec![Fill::from(Color::RED)];
1618 let b = vec![Fill::from(Color::BLUE)];
1619 assert_eq!(
1620 a.interpolate(&b, 0.5)[0].paint.get(),
1621 Paint::Solid(Color::RED.interpolate(&Color::BLUE, 0.5))
1622 );
1623 let longer = vec![Fill::from(Color::BLUE), Fill::from(Color::GREEN)];
1624 assert_eq!(a.interpolate(&longer, 0.1), longer);
1625 let a = Stroke::default();
1626 let b = Stroke::default();
1627 b.cap.set(StrokeCap::Round);
1628 assert_eq!(a.interpolate(&b, 0.99).cap.get(), StrokeCap::Butt);
1629 assert_eq!(a.interpolate(&b, 1.0).cap.get(), StrokeCap::Round);
1630 }
1631
1632 #[test]
1633 fn expression_adapter_observes_nested_edits_and_releases_removed_layers() {
1634 let graph = crate::properties::PropertyGraph::new(|| {});
1635 let _entered = graph.enter();
1636 let first = Fill::from(Color::RED);
1637 let source = Property::new(vec![first.clone()]);
1638 let variable = Variable::new_from_typed_property(source.clone());
1639 let read = || Vec::<Fill>::try_coerce(variable.get_as_pax_value()).unwrap();
1640 assert_eq!(read()[0].paint.get(), Paint::Solid(Color::RED));
1641 first.paint.local().set(Paint::Solid(Color::BLUE));
1642 assert!(drain_effects(100) < 100);
1643 assert_eq!(read()[0].paint.get(), Paint::Solid(Color::BLUE));
1644 source.local().set(vec![]);
1645 assert!(read().is_empty());
1646 first.paint.local().set(Paint::Solid(Color::WHITE));
1647 assert!(drain_effects(100) < 100);
1648 assert!(read().is_empty());
1649 }
1650
1651 #[test]
1652 fn nested_appearance_subscriptions_retire_after_replacement_and_disposal() {
1653 let graph = crate::properties::PropertyGraph::new(|| {});
1654 let _entered = graph.enter();
1655 use crate::properties::property_table_total_properties_count;
1656 let baseline = property_table_total_properties_count();
1657 for _ in 0..30 {
1658 let scope = graph.binding_scope();
1659 let _scope = scope.enter();
1660 let source = Property::new(vec![Fill::from(Color::RED)]);
1661 let variable = Variable::new_from_typed_property(source.clone());
1662 variable.get_as_pax_value();
1663 source.local().get()[0]
1664 .opacity
1665 .local()
1666 .set(Opacity::from(0.25));
1667 drain_effects(100);
1668 variable.get_as_pax_value();
1669 source
1670 .local()
1671 .set(vec![Fill::from(Color::BLUE), Fill::from(Color::GREEN)]);
1672 variable.get_as_pax_value();
1673 source.local().set(vec![]);
1674 variable.get_as_pax_value();
1675 scope.close();
1677 }
1678 drain_effects(100);
1679 graph.collect();
1680 assert_eq!(property_table_total_properties_count(), baseline);
1681 }
1682
1683 #[test]
1684 fn degenerate_paint_domain_and_nonfinite_opacity_are_finite() {
1685 let rect = paint_bounds(kurbo::Rect::new(25.0, 40.0, 125.0, 40.0)).unwrap();
1686 assert_eq!(rect, kurbo::Rect::new(25.0, 39.5, 125.0, 40.5));
1687 assert!(paint_bounds(kurbo::Rect::new(f64::NAN, 0.0, 1.0, 1.0)).is_none());
1688 for alpha in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1689 assert_eq!(Opacity::from(alpha).to_float_0_1(), 0.0);
1690 }
1691 }
1692}