Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(document,types): sketch status report && version

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (May 21, 2026, 12:02 PM +0300) commit 64aafd9b parent 31cf26ef change-id lzztpkux
+410 -33
+1 -1
crates/bone-document/src/lib.rs
··· 18 18 ArcData, CircleData, DimensionKind, DimensionRefs, DimensionValue, DimensionValueMismatch, 19 19 EditOutcome, EntityRefs, LineData, PointData, RelationRefs, Sketch, SketchDimension, 20 20 SketchDofReport, SketchEdit, SketchEditError, SketchEntity, SketchEntityKind, SketchParameter, 21 - SketchRelation, SolverError, 21 + SketchRelation, SketchStatusReport, SketchVersion, SolverError, 22 22 }; 23 23 pub use undo::UndoStack; 24 24
+140 -2
crates/bone-document/src/sketch/mod.rs
··· 25 25 }; 26 26 pub use parameter::SketchParameter; 27 27 pub use relation::{RelationRefs, SketchRelation}; 28 - pub use solve::{DragError, Mapping as SketchSolveMapping, SketchDofReport}; 28 + pub use solve::{DragError, Mapping as SketchSolveMapping, SketchDofReport, SketchStatusReport}; 29 + 30 + #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] 31 + pub struct SketchVersion(u64); 32 + 33 + impl SketchVersion { 34 + #[must_use] 35 + pub const fn value(self) -> u64 { 36 + self.0 37 + } 38 + 39 + #[must_use] 40 + fn bump(self) -> Self { 41 + Self(self.0.wrapping_add(1)) 42 + } 43 + } 29 44 30 45 type EntityMap = SlotMap<SketchEntityId, SketchEntity>; 31 46 type RelationMap = SlotMap<SketchRelationId, SketchRelation>; ··· 44 59 dimension_order: Arc<Vec<SketchDimensionId>>, 45 60 parameters: Arc<ParameterMap>, 46 61 parameter_order: Arc<Vec<SketchParameterId>>, 62 + #[serde(skip)] 63 + version: SketchVersion, 47 64 } 48 65 49 66 impl Sketch { ··· 59 76 dimension_order: Arc::new(Vec::new()), 60 77 parameters: Arc::new(SlotMap::with_key()), 61 78 parameter_order: Arc::new(Vec::new()), 79 + version: SketchVersion::default(), 62 80 } 63 81 } 64 82 ··· 107 125 &self.parameter_order 108 126 } 109 127 128 + #[must_use] 129 + pub fn version(&self) -> SketchVersion { 130 + self.version 131 + } 132 + 133 + #[must_use] 134 + pub(crate) fn with_bumped_version(mut self) -> Self { 135 + self.version = self.version.bump(); 136 + self 137 + } 138 + 110 139 fn entries_equal<K, V>(order: &[K], lhs: &SlotMap<K, V>, rhs: &SlotMap<K, V>) -> bool 111 140 where 112 141 K: slotmap::Key, ··· 174 203 if let Ok((ref sketch, _)) = result { 175 204 sketch.assert_invariants(); 176 205 } 177 - result 206 + result.map(|(sketch, outcome)| (sketch.with_bumped_version(), outcome)) 178 207 } 179 208 180 209 fn assert_invariants(&self) { ··· 1599 1628 value: DimensionValue::Angle(angle_deg(45.0)), 1600 1629 }); 1601 1630 assert!(matches!(bad, Err(SketchEditError::DimensionIsDriven(_)))); 1631 + } 1632 + 1633 + #[test] 1634 + fn status_reports_dangling_when_relation_references_missing_entity() { 1635 + use bone_types::SketchStatus; 1636 + 1637 + let (mut sketch, outcomes) = apply_script(&Sketch::new(plane()), rectangle_script()); 1638 + let entity_ids: Vec<SketchEntityId> = outcomes 1639 + .iter() 1640 + .map(|o| match *o { 1641 + EditOutcome::Entity(id) => id, 1642 + _ => panic!("expected entity outcomes"), 1643 + }) 1644 + .collect(); 1645 + let Ok((with_rel, EditOutcome::Relation(rel_id))) = sketch.apply(SketchEdit::AddRelation( 1646 + SketchRelation::Coincident(entity_ids[0], entity_ids[1]), 1647 + )) else { 1648 + panic!("relation should add cleanly"); 1649 + }; 1650 + sketch = with_rel; 1651 + let entities = Arc::make_mut(&mut sketch.entities); 1652 + assert!(entities.remove(entity_ids[1]).is_some()); 1653 + 1654 + let report = sketch.status(); 1655 + assert_eq!(report.status(), SketchStatus::Dangling); 1656 + assert!( 1657 + report 1658 + .offending() 1659 + .contains(&bone_types::SketchItemId::Relation(rel_id)) 1660 + ); 1661 + } 1662 + 1663 + #[test] 1664 + fn status_reports_dangling_when_dimension_references_missing_entity() { 1665 + use bone_types::SketchStatus; 1666 + 1667 + let (mut sketch, outcomes) = apply_script(&Sketch::new(plane()), rectangle_script()); 1668 + let entity_ids: Vec<SketchEntityId> = outcomes 1669 + .iter() 1670 + .map(|o| match *o { 1671 + EditOutcome::Entity(id) => id, 1672 + _ => panic!("expected entity outcomes"), 1673 + }) 1674 + .collect(); 1675 + let Ok((with_dim, EditOutcome::Dimension(dim_id))) = 1676 + sketch.apply(SketchEdit::AddDimension(SketchDimension::Linear { 1677 + a: entity_ids[0], 1678 + b: entity_ids[1], 1679 + value: len_mm(10.0), 1680 + kind: DimensionKind::Driving, 1681 + })) 1682 + else { 1683 + panic!("dimension should add cleanly"); 1684 + }; 1685 + sketch = with_dim; 1686 + let entities = Arc::make_mut(&mut sketch.entities); 1687 + assert!(entities.remove(entity_ids[0]).is_some()); 1688 + 1689 + let report = sketch.status(); 1690 + assert_eq!(report.status(), SketchStatus::Dangling); 1691 + assert!( 1692 + report 1693 + .offending() 1694 + .contains(&bone_types::SketchItemId::Dimension(dim_id)) 1695 + ); 1696 + } 1697 + 1698 + #[test] 1699 + fn status_reports_dangling_when_entity_references_missing_point() { 1700 + use bone_types::SketchStatus; 1701 + 1702 + let (sketch, outcomes) = apply_script(&Sketch::new(plane()), rectangle_script()); 1703 + let entity_ids: Vec<SketchEntityId> = outcomes 1704 + .iter() 1705 + .map(|o| match *o { 1706 + EditOutcome::Entity(id) => id, 1707 + _ => panic!("expected entity outcomes"), 1708 + }) 1709 + .collect(); 1710 + let Ok((mut sketch, EditOutcome::Entity(line_id))) = 1711 + sketch.apply(SketchEdit::AddEntity(SketchEntity::line( 1712 + entity_ids[0], 1713 + entity_ids[1], 1714 + false, 1715 + ))) 1716 + else { 1717 + panic!("line should add cleanly"); 1718 + }; 1719 + let entities = Arc::make_mut(&mut sketch.entities); 1720 + assert!(entities.remove(entity_ids[0]).is_some()); 1721 + 1722 + let report = sketch.status(); 1723 + assert_eq!(report.status(), SketchStatus::Dangling); 1724 + assert!( 1725 + report 1726 + .offending() 1727 + .contains(&bone_types::SketchItemId::Entity(line_id)) 1728 + ); 1729 + } 1730 + 1731 + #[test] 1732 + fn version_is_stable_across_clone_and_bumps_on_edit() { 1733 + let sketch = Sketch::new(plane()); 1734 + let v_a = sketch.version(); 1735 + let v_clone = sketch.clone().version(); 1736 + assert_eq!(v_a, v_clone); 1737 + 1738 + let (after, _) = apply_script(&sketch, rectangle_script()); 1739 + assert_ne!(after.version(), v_a); 1602 1740 } 1603 1741 1604 1742 mod properties {
+238 -14
crates/bone-document/src/sketch/solve.rs
··· 1 - use std::collections::{BTreeMap, HashMap}; 1 + use std::collections::{BTreeMap, BTreeSet, HashMap}; 2 2 use std::time::Instant; 3 3 4 4 use bone_solver::{ ··· 8 8 }; 9 9 use bone_types::{ 10 10 Angle, BudgetCeiling, DegreesOfFreedom, Length, Parameter, ParameterIndex, Point2, 11 - ResidualIndex, SketchDimensionId, SketchEntityId, SketchItemId, SketchRelationId, millimeter, 12 - radian, 11 + ResidualIndex, SketchDimensionId, SketchEntityId, SketchItemId, SketchRelationId, SketchStatus, 12 + millimeter, radian, 13 13 }; 14 14 15 15 use super::{ ··· 268 268 Err(DragError::NotAPoint(*id)) 269 269 } 270 270 })?; 271 + let fixed_points: BTreeSet<SketchEntityId> = self 272 + .relations() 273 + .values() 274 + .filter_map(|rel| match *rel { 275 + SketchRelation::Fix(id) => Some(id), 276 + _ => None, 277 + }) 278 + .collect(); 271 279 let deduped: Vec<(SketchEntityId, Point2)> = pins 272 280 .iter() 273 281 .copied() 274 282 .enumerate() 275 283 .filter(|(i, (id, _))| pins[..*i].iter().all(|(prior, _)| prior != id)) 284 + .filter(|(_, (id, _))| !fixed_points.contains(id)) 276 285 .map(|(_, pair)| pair) 277 286 .collect(); 278 287 let updates: HashMap<SketchEntityId, Point2> = deduped.iter().copied().collect(); ··· 320 329 } 321 330 322 331 #[must_use] 332 + pub fn status(&self) -> SketchStatusReport { 333 + let dangling = collect_dangling(self); 334 + if !dangling.is_empty() { 335 + return SketchStatusReport::dangling(dangling); 336 + } 337 + let (system, mapping) = self.lower(); 338 + let decomposition = decompose(&system); 339 + match solve_newton_decomposed(&system, &decomposition, NewtonConfig::DEFAULT) { 340 + Ok(parameters) => { 341 + let report = analyze_dof_at(&system, &parameters, DofConfig::DEFAULT); 342 + if !report.over_constrained().is_empty() { 343 + return SketchStatusReport::over_defined(narrow_conflicts( 344 + &system, 345 + &mapping, 346 + report.over_constrained(), 347 + )); 348 + } 349 + if report.dof().value() > 0 { 350 + SketchStatusReport::under_defined(report.dof()) 351 + } else { 352 + SketchStatusReport::fully_defined() 353 + } 354 + } 355 + Err(SolverError::OverDefined { conflicts }) => { 356 + SketchStatusReport::over_defined(conflicts) 357 + } 358 + Err(SolverError::NoSolutionFound { .. }) => { 359 + let report = analyze_dof(&system, DofConfig::DEFAULT); 360 + if report.over_constrained().is_empty() { 361 + SketchStatusReport::no_solution_found() 362 + } else { 363 + SketchStatusReport::over_defined(narrow_conflicts( 364 + &system, 365 + &mapping, 366 + report.over_constrained(), 367 + )) 368 + } 369 + } 370 + Err(SolverError::InvalidSolutionFound { .. }) => { 371 + SketchStatusReport::invalid_solution_found() 372 + } 373 + Err(SolverError::Budget { .. }) => SketchStatusReport::no_solution_found(), 374 + } 375 + } 376 + 377 + #[must_use] 323 378 pub fn measure(&self, dim: SketchDimension) -> Option<DimensionValue> { 324 379 measure_dimension(self, dim) 325 380 } ··· 350 405 let next = self 351 406 .with_point_positions(&point_updates) 352 407 .with_circle_radii(&radius_updates); 353 - recompute_driven(next) 408 + recompute_driven(next).with_bumped_version() 354 409 } 355 410 } 356 411 ··· 359 414 mapping: &Mapping, 360 415 over_flagged: &[ResidualIndex], 361 416 ) -> SolverError { 417 + SolverError::OverDefined { 418 + conflicts: narrow_conflicts(system, mapping, over_flagged), 419 + } 420 + } 421 + 422 + fn narrow_conflicts( 423 + system: &ConstraintSystem, 424 + mapping: &Mapping, 425 + over_flagged: &[ResidualIndex], 426 + ) -> Vec<SketchItemId> { 362 427 let mus = minimal_unsatisfiable_subset(system, over_flagged, DofConfig::DEFAULT); 363 428 let rows = if mus.is_empty() { over_flagged } else { &mus }; 364 - SolverError::OverDefined { 365 - conflicts: dedup_preserving_order(mapping.items(rows.iter().copied())), 429 + dedup_preserving_order(mapping.items(rows.iter().copied())) 430 + } 431 + 432 + #[derive(Clone, Debug, PartialEq, Eq)] 433 + pub struct SketchStatusReport { 434 + status: SketchStatus, 435 + offending: Vec<SketchItemId>, 436 + dof: DegreesOfFreedom, 437 + } 438 + 439 + impl SketchStatusReport { 440 + #[must_use] 441 + pub fn status(&self) -> SketchStatus { 442 + self.status 366 443 } 444 + 445 + #[must_use] 446 + pub fn offending(&self) -> &[SketchItemId] { 447 + &self.offending 448 + } 449 + 450 + #[must_use] 451 + pub fn dof(&self) -> DegreesOfFreedom { 452 + self.dof 453 + } 454 + 455 + fn fully_defined() -> Self { 456 + Self { 457 + status: SketchStatus::FullyDefined, 458 + offending: Vec::new(), 459 + dof: DegreesOfFreedom::new(0), 460 + } 461 + } 462 + 463 + fn under_defined(dof: DegreesOfFreedom) -> Self { 464 + Self { 465 + status: SketchStatus::UnderDefined, 466 + offending: Vec::new(), 467 + dof, 468 + } 469 + } 470 + 471 + fn over_defined(offending: Vec<SketchItemId>) -> Self { 472 + Self { 473 + status: SketchStatus::OverDefined, 474 + offending, 475 + dof: DegreesOfFreedom::new(0), 476 + } 477 + } 478 + 479 + fn no_solution_found() -> Self { 480 + Self { 481 + status: SketchStatus::NoSolutionFound, 482 + offending: Vec::new(), 483 + dof: DegreesOfFreedom::new(0), 484 + } 485 + } 486 + 487 + fn invalid_solution_found() -> Self { 488 + Self { 489 + status: SketchStatus::InvalidSolutionFound, 490 + offending: Vec::new(), 491 + dof: DegreesOfFreedom::new(0), 492 + } 493 + } 494 + 495 + fn dangling(offending: Vec<SketchItemId>) -> Self { 496 + Self { 497 + status: SketchStatus::Dangling, 498 + offending, 499 + dof: DegreesOfFreedom::new(0), 500 + } 501 + } 502 + } 503 + 504 + fn collect_dangling(sketch: &Sketch) -> Vec<SketchItemId> { 505 + let entity_exists = |id: SketchEntityId| sketch.entities().contains_key(id); 506 + let dangling_entities = sketch 507 + .entities() 508 + .iter() 509 + .filter(|(_, entity)| !entity.references().into_iter().all(entity_exists)) 510 + .map(|(eid, _)| SketchItemId::Entity(eid)); 511 + let dangling_rels = sketch 512 + .relations() 513 + .iter() 514 + .filter(|(_, rel)| !rel.references().into_iter().all(entity_exists)) 515 + .map(|(rid, _)| SketchItemId::Relation(rid)); 516 + let dangling_dims = sketch 517 + .dimensions() 518 + .iter() 519 + .filter(|(_, dim)| !dim.references().into_iter().all(entity_exists)) 520 + .map(|(did, _)| SketchItemId::Dimension(did)); 521 + dangling_entities 522 + .chain(dangling_rels) 523 + .chain(dangling_dims) 524 + .collect() 367 525 } 368 526 369 527 fn push_parameter(out: &mut Vec<Parameter>, value: f64) -> ParameterIndex { ··· 1799 1957 } 1800 1958 1801 1959 #[test] 1960 + fn solve_with_drag_pins_ignores_drag_on_fixed_point() { 1961 + let base = Sketch::new(xy_plane()); 1962 + let (with_point, outs) = apply( 1963 + &base, 1964 + vec![SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm( 1965 + 1.0, 2.0, 1966 + )))], 1967 + ); 1968 + let p = entity_id(&outs[0]); 1969 + let Ok((sketch, _)) = with_point.apply(SketchEdit::AddRelation(SketchRelation::Fix(p))) 1970 + else { 1971 + panic!("Fix relation should add cleanly"); 1972 + }; 1973 + let pins = [(p, Point2::from_mm(10.0, 20.0))]; 1974 + let Ok(dragged) = sketch.solve_with_drag_pins(&pins, generous_budget()) else { 1975 + panic!("solve should converge even when drag pins are no-ops"); 1976 + }; 1977 + let (x, y) = point_coords(&dragged)[0]; 1978 + assert!( 1979 + (x - 1.0).abs() < 1e-9 && (y - 2.0).abs() < 1e-9, 1980 + "fixed point must not move under drag: ({x}, {y})", 1981 + ); 1982 + } 1983 + 1984 + #[test] 1802 1985 fn solve_with_drag_pins_dedupes_repeated_id() { 1803 1986 let base = Sketch::new(xy_plane()); 1804 1987 let (sketch, outs) = apply( ··· 1927 2110 } 1928 2111 1929 2112 #[test] 1930 - fn solve_with_drag_on_fixed_point_reanchors_to_target() { 2113 + fn solve_with_drag_on_fixed_point_keeps_point_anchored() { 1931 2114 let base = Sketch::new(xy_plane()); 1932 2115 let (with_point, outs) = apply( 1933 2116 &base, ··· 1943 2126 .0; 1944 2127 let target = Point2::from_mm(8.0, 5.0); 1945 2128 let Ok(dragged) = sketch.solve_with_drag(p, target, generous_budget()) else { 1946 - panic!("Fix'd drag re-anchors via warm-start and must converge"); 2129 + panic!("drag on Fix'd point should still solve"); 1947 2130 }; 1948 2131 let (x, y) = point_coords(&dragged)[0]; 1949 2132 assert!( 1950 - (x - 8.0).abs() < 1e-9, 1951 - "warm-start re-anchors Fix's x pin to target: {x}", 1952 - ); 1953 - assert!( 1954 - (y - 5.0).abs() < 1e-9, 1955 - "warm-start re-anchors Fix's y pin to target: {y}", 2133 + x.abs() < 1e-9 && y.abs() < 1e-9, 2134 + "Fix overrides drag: ({x}, {y}) should stay at origin", 1956 2135 ); 1957 2136 } 1958 2137 ··· 2162 2341 assert_eq!(system.row_count(), 1, "one intrinsic row per arc"); 2163 2342 let got = mapping.item_of_residual(ResidualIndex::new(0)); 2164 2343 assert_eq!(got, Some(SketchItemId::Entity(arc_id))); 2344 + } 2345 + 2346 + #[test] 2347 + fn status_fully_constrained_rectangle_is_fully_defined() { 2348 + let sketch = constrained_rectangle(); 2349 + let report = sketch.status(); 2350 + assert_eq!(report.status(), SketchStatus::FullyDefined); 2351 + assert!(report.offending().is_empty()); 2352 + assert_eq!(report.dof().value(), 0); 2353 + } 2354 + 2355 + #[test] 2356 + fn status_free_line_is_under_defined() { 2357 + let base = Sketch::new(xy_plane()); 2358 + let (with_points, outs) = apply( 2359 + &base, 2360 + vec![ 2361 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))), 2362 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(1.0, 3.0))), 2363 + ], 2364 + ); 2365 + let [a, b] = [0_usize, 1].map(|i| entity_id(&outs[i])); 2366 + let sketch = apply( 2367 + &with_points, 2368 + vec![SketchEdit::AddEntity(SketchEntity::line(a, b, false))], 2369 + ) 2370 + .0; 2371 + let report = sketch.status(); 2372 + assert_eq!(report.status(), SketchStatus::UnderDefined); 2373 + assert!(report.offending().is_empty()); 2374 + assert!(report.dof().value() > 0); 2375 + } 2376 + 2377 + #[test] 2378 + fn status_three_conflicting_dims_is_over_defined() { 2379 + let (sketch, dim_ids) = three_conflicting_linear_dims_fixture(); 2380 + let report = sketch.status(); 2381 + assert_eq!(report.status(), SketchStatus::OverDefined); 2382 + assert!(!report.offending().is_empty()); 2383 + let added: BTreeSet<SketchItemId> = dim_ids 2384 + .iter() 2385 + .copied() 2386 + .map(SketchItemId::Dimension) 2387 + .collect(); 2388 + assert!(report.offending().iter().all(|c| added.contains(c))); 2165 2389 } 2166 2390 }
+1 -1
crates/bone-types/src/lib.rs
··· 10 10 pub use schema::{SchemaHeader, SchemaVersion}; 11 11 pub use solver::{ 12 12 BudgetCeiling, DegreesOfFreedom, NewtonDamping, NewtonStepTolerance, Parameter, ParameterIndex, 13 - ParentIndex, ResidualIndex, SketchItemId, SolverResidual, SolverSeed, 13 + ParentIndex, ResidualIndex, SketchItemId, SketchStatus, SolverResidual, SolverSeed, 14 14 }; 15 15 pub use space::{Point2, Point3, SketchPlaneBasis, UnitVec2, UnitVec3, Vec2}; 16 16
+30
crates/bone-types/src/solver.rs
··· 153 153 Entity(crate::SketchEntityId), 154 154 } 155 155 156 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 157 + pub enum SketchStatus { 158 + UnderDefined, 159 + FullyDefined, 160 + OverDefined, 161 + NoSolutionFound, 162 + InvalidSolutionFound, 163 + Dangling, 164 + } 165 + 166 + impl SketchStatus { 167 + #[must_use] 168 + pub const fn label(self) -> &'static str { 169 + match self { 170 + Self::UnderDefined => "Under Defined", 171 + Self::FullyDefined => "Fully Defined", 172 + Self::OverDefined => "Over Defined", 173 + Self::NoSolutionFound => "No Solution Found", 174 + Self::InvalidSolutionFound => "Invalid Solution Found", 175 + Self::Dangling => "Dangling", 176 + } 177 + } 178 + } 179 + 180 + impl core::fmt::Display for SketchStatus { 181 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 182 + f.write_str(self.label()) 183 + } 184 + } 185 + 156 186 impl core::fmt::Display for SketchItemId { 157 187 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 158 188 match self {
-3
crates/bone-ui/src/theme/color.rs
··· 370 370 pub sketch_under_defined: Color, 371 371 pub sketch_fully_defined: Color, 372 372 pub sketch_over_defined: Color, 373 - pub sketch_invalid: Color, 374 373 pub sketch_dangling: Color, 375 374 pub sketch_driven: Color, 376 375 pub selection_primary: Color, ··· 386 385 sketch_under_defined: Color::from_srgb_u8(0x1F, 0x4F, 0xCC), 387 386 sketch_fully_defined: Color::from_srgb_u8(0x10, 0x10, 0x10), 388 387 sketch_over_defined: Color::from_srgb_u8(0xC8, 0x00, 0x1E), 389 - sketch_invalid: Color::from_srgb_u8(0x7A, 0x00, 0x14), 390 388 sketch_dangling: Color::from_srgb_u8(0x80, 0x80, 0x00), 391 389 sketch_driven: Color::from_srgb_u8(0x7E, 0x7E, 0x7E), 392 390 selection_primary: Color::from_srgb_u8(0x2C, 0xCD, 0x33), ··· 579 577 cad.sketch_under_defined, 580 578 cad.sketch_fully_defined, 581 579 cad.sketch_over_defined, 582 - cad.sketch_invalid, 583 580 cad.sketch_dangling, 584 581 cad.sketch_driven, 585 582 cad.selection_primary,
-6
crates/bone-ui/tests/snapshots/theme_snapshot__theme_dark.snap
··· 391 391 b: 0.012983031, 392 392 a: 1.0, 393 393 ), 394 - sketch_invalid: Color( 395 - r: 0.19461781, 396 - g: 0.0, 397 - b: 0.0069954083, 398 - a: 1.0, 399 - ), 400 394 sketch_dangling: Color( 401 395 r: 0.21586053, 402 396 g: 0.21586053,
-6
crates/bone-ui/tests/snapshots/theme_snapshot__theme_light.snap
··· 391 391 b: 0.012983031, 392 392 a: 1.0, 393 393 ), 394 - sketch_invalid: Color( 395 - r: 0.19461781, 396 - g: 0.0, 397 - b: 0.0069954083, 398 - a: 1.0, 399 - ), 400 394 sketch_dangling: Color( 401 395 r: 0.21586053, 402 396 g: 0.21586053,