Another project
0

Configure Feed

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

at main 66 kB View raw
1use std::collections::{BTreeSet, HashMap}; 2use std::sync::Arc; 3 4use bone_types::{ 5 Length, Point2, SketchDimensionId, SketchEntityId, SketchParameterId, SketchPlaneBasis, 6 SketchRelationId, 7}; 8use serde::{Deserialize, Serialize}; 9use slotmap::SlotMap; 10 11pub mod dimension; 12pub mod edit; 13pub mod entity; 14pub mod parameter; 15pub mod relation; 16pub mod solve; 17 18pub use bone_solver::SolverError; 19pub use dimension::{ 20 DimensionKind, DimensionRefs, DimensionValue, DimensionValueMismatch, SketchDimension, 21}; 22pub use edit::{EditOutcome, SketchEdit}; 23pub use entity::{ 24 ArcData, CircleData, EntityRefs, LineData, PointData, SketchEntity, SketchEntityKind, 25}; 26pub use parameter::SketchParameter; 27pub use relation::{RelationRefs, SketchRelation}; 28pub use solve::{DragError, Mapping as SketchSolveMapping, SketchDofReport, SketchStatusReport}; 29 30#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] 31pub struct SketchVersion(u64); 32 33impl 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} 44 45type EntityMap = SlotMap<SketchEntityId, SketchEntity>; 46type RelationMap = SlotMap<SketchRelationId, SketchRelation>; 47type DimensionMap = SlotMap<SketchDimensionId, SketchDimension>; 48type ParameterMap = SlotMap<SketchParameterId, SketchParameter>; 49 50#[derive(Clone, Debug, Serialize, Deserialize)] 51#[serde(deny_unknown_fields)] 52pub struct Sketch { 53 plane: SketchPlaneBasis, 54 entities: Arc<EntityMap>, 55 entity_order: Arc<Vec<SketchEntityId>>, 56 relations: Arc<RelationMap>, 57 relation_order: Arc<Vec<SketchRelationId>>, 58 dimensions: Arc<DimensionMap>, 59 dimension_order: Arc<Vec<SketchDimensionId>>, 60 parameters: Arc<ParameterMap>, 61 parameter_order: Arc<Vec<SketchParameterId>>, 62 #[serde(skip)] 63 version: SketchVersion, 64} 65 66impl Sketch { 67 #[must_use] 68 pub fn new(plane: SketchPlaneBasis) -> Self { 69 Self { 70 plane, 71 entities: Arc::new(SlotMap::with_key()), 72 entity_order: Arc::new(Vec::new()), 73 relations: Arc::new(SlotMap::with_key()), 74 relation_order: Arc::new(Vec::new()), 75 dimensions: Arc::new(SlotMap::with_key()), 76 dimension_order: Arc::new(Vec::new()), 77 parameters: Arc::new(SlotMap::with_key()), 78 parameter_order: Arc::new(Vec::new()), 79 version: SketchVersion::default(), 80 } 81 } 82 83 #[must_use] 84 pub fn plane(&self) -> SketchPlaneBasis { 85 self.plane 86 } 87 88 #[must_use] 89 pub fn with_plane(&self, plane: SketchPlaneBasis) -> Self { 90 Self { 91 plane, 92 ..self.clone() 93 } 94 } 95 96 #[must_use] 97 pub fn entities(&self) -> &EntityMap { 98 &self.entities 99 } 100 101 #[must_use] 102 pub fn relations(&self) -> &RelationMap { 103 &self.relations 104 } 105 106 #[must_use] 107 pub fn dimensions(&self) -> &DimensionMap { 108 &self.dimensions 109 } 110 111 #[must_use] 112 pub fn parameters(&self) -> &ParameterMap { 113 &self.parameters 114 } 115 116 #[must_use] 117 pub fn entity_order(&self) -> &[SketchEntityId] { 118 &self.entity_order 119 } 120 121 #[must_use] 122 pub fn relation_order(&self) -> &[SketchRelationId] { 123 &self.relation_order 124 } 125 126 #[must_use] 127 pub fn dimension_order(&self) -> &[SketchDimensionId] { 128 &self.dimension_order 129 } 130 131 #[must_use] 132 pub fn parameter_order(&self) -> &[SketchParameterId] { 133 &self.parameter_order 134 } 135 136 #[must_use] 137 pub fn version(&self) -> SketchVersion { 138 self.version 139 } 140 141 #[must_use] 142 pub(crate) fn with_bumped_version(mut self) -> Self { 143 self.version = self.version.bump(); 144 self 145 } 146 147 fn entries_equal<K, V>(order: &[K], lhs: &SlotMap<K, V>, rhs: &SlotMap<K, V>) -> bool 148 where 149 K: slotmap::Key, 150 V: PartialEq, 151 { 152 order.iter().all(|id| lhs.get(*id) == rhs.get(*id)) 153 } 154 155 pub fn validate(&self) -> Result<(), SketchEditError> { 156 ensure_order_matches("entities", &self.entity_order, &self.entities)?; 157 ensure_order_matches("relations", &self.relation_order, &self.relations)?; 158 ensure_order_matches("dimensions", &self.dimension_order, &self.dimensions)?; 159 ensure_order_matches("parameters", &self.parameter_order, &self.parameters)?; 160 161 self.entity_order.iter().copied().try_for_each(|id| { 162 let entity = *self.require_entity(id)?; 163 validate_entity_shape(entity)?; 164 entity 165 .references() 166 .into_iter() 167 .try_for_each(|reference| self.require_point(reference)) 168 })?; 169 170 self.relation_order.iter().copied().try_for_each(|id| { 171 let rel = *self 172 .relations 173 .get(id) 174 .ok_or(SketchEditError::RelationNotFound(id))?; 175 self.validate_relation(rel) 176 })?; 177 178 self.dimension_order.iter().copied().try_for_each(|id| { 179 let dim = *self 180 .dimensions 181 .get(id) 182 .ok_or(SketchEditError::DimensionNotFound(id))?; 183 self.validate_dimension(dim) 184 }) 185 } 186 187 pub fn apply(self, edit: SketchEdit) -> Result<(Self, EditOutcome), SketchEditError> { 188 let result = match edit { 189 SketchEdit::AddEntity(entity) => { 190 entity 191 .references() 192 .into_iter() 193 .try_for_each(|ref_id| self.require_point(ref_id))?; 194 validate_entity_shape(entity)?; 195 Ok(self.add_entity(entity)) 196 } 197 SketchEdit::AddRelation(rel) => self.add_relation(rel), 198 SketchEdit::AddDimension(dim) => self.add_dimension(dim), 199 SketchEdit::AddParameter(p) => Ok(self.add_parameter(p)), 200 SketchEdit::DeleteEntity(id) => self.delete_entity(id), 201 SketchEdit::DeleteRelation(id) => self.delete_relation(id), 202 SketchEdit::DeleteDimension(id) => self.delete_dimension(id), 203 SketchEdit::DeleteParameter(id) => self.delete_parameter(id), 204 SketchEdit::MovePoint { id, position } => self.move_point(id, position), 205 SketchEdit::SetConstruction { 206 id, 207 for_construction, 208 } => self.set_construction(id, for_construction), 209 SketchEdit::UpdateDimensionValue { id, value } => self.update_dimension(id, value), 210 }; 211 if let Ok((ref sketch, _)) = result { 212 sketch.assert_invariants(); 213 } 214 result.map(|(sketch, outcome)| (sketch.with_bumped_version(), outcome)) 215 } 216 217 fn assert_invariants(&self) { 218 debug_assert_eq!(self.entity_order.len(), self.entities.len()); 219 debug_assert_eq!(self.relation_order.len(), self.relations.len()); 220 debug_assert_eq!(self.dimension_order.len(), self.dimensions.len()); 221 debug_assert_eq!(self.parameter_order.len(), self.parameters.len()); 222 debug_assert!( 223 self.entity_order 224 .iter() 225 .all(|id| self.entities.contains_key(*id)) 226 ); 227 debug_assert!( 228 self.relation_order 229 .iter() 230 .all(|id| self.relations.contains_key(*id)) 231 ); 232 debug_assert!( 233 self.dimension_order 234 .iter() 235 .all(|id| self.dimensions.contains_key(*id)) 236 ); 237 debug_assert!( 238 self.parameter_order 239 .iter() 240 .all(|id| self.parameters.contains_key(*id)) 241 ); 242 } 243 244 pub fn apply_all<I>(&self, edits: I) -> Result<(Self, Vec<EditOutcome>), SketchEditError> 245 where 246 I: IntoIterator<Item = SketchEdit>, 247 { 248 edits.into_iter().try_fold( 249 (self.clone(), Vec::new()), 250 |(sketch, mut outcomes), edit| { 251 let (next, outcome) = sketch.apply(edit)?; 252 outcomes.push(outcome); 253 Ok((next, outcomes)) 254 }, 255 ) 256 } 257 258 fn require_entity(&self, id: SketchEntityId) -> Result<&SketchEntity, SketchEditError> { 259 self.entities 260 .get(id) 261 .ok_or(SketchEditError::EntityNotFound(id)) 262 } 263 264 fn require_point(&self, id: SketchEntityId) -> Result<(), SketchEditError> { 265 if self.require_entity(id)?.is_point() { 266 Ok(()) 267 } else { 268 Err(SketchEditError::ExpectedPoint(id)) 269 } 270 } 271 272 fn kind_of(&self, id: SketchEntityId) -> Result<SketchEntityKind, SketchEditError> { 273 self.require_entity(id).map(SketchEntity::kind) 274 } 275 276 fn validate_relation(&self, rel: SketchRelation) -> Result<(), SketchEditError> { 277 use SketchEntityKind as K; 278 if let Some((a, b)) = rel.pair() 279 && a == b 280 { 281 return Err(SketchEditError::SelfReferencingRelation(rel)); 282 } 283 let ok = match rel { 284 SketchRelation::Coincident(a, b) => { 285 matches!( 286 (self.kind_of(a)?, self.kind_of(b)?), 287 (K::Point, _) | (_, K::Point) 288 ) 289 } 290 SketchRelation::Horizontal(a) | SketchRelation::Vertical(a) => { 291 self.kind_of(a)? == K::Line 292 } 293 SketchRelation::Parallel(a, b) | SketchRelation::Perpendicular(a, b) => { 294 self.kind_of(a)? == K::Line && self.kind_of(b)? == K::Line 295 } 296 SketchRelation::Tangent(a, b) => { 297 let (ka, kb) = (self.kind_of(a)?, self.kind_of(b)?); 298 let curve = |k| matches!(k, K::Line | K::Arc | K::Circle); 299 let round = |k| matches!(k, K::Arc | K::Circle); 300 curve(ka) && curve(kb) && (round(ka) || round(kb)) 301 } 302 SketchRelation::Equal(a, b) => matches!( 303 (self.kind_of(a)?, self.kind_of(b)?), 304 (K::Line, K::Line) | (K::Arc | K::Circle, K::Arc | K::Circle) 305 ), 306 SketchRelation::Concentric(a, b) => { 307 matches!(self.kind_of(a)?, K::Arc | K::Circle) 308 && matches!(self.kind_of(b)?, K::Arc | K::Circle) 309 } 310 SketchRelation::Midpoint { point, line } => match self.require_entity(line)? { 311 SketchEntity::Line(l) => { 312 self.kind_of(point)? == K::Point && point != l.a() && point != l.b() 313 } 314 _ => false, 315 }, 316 SketchRelation::Symmetric { a, b, axis } => { 317 self.kind_of(a)? == K::Point 318 && self.kind_of(b)? == K::Point 319 && self.kind_of(axis)? == K::Line 320 && a != b 321 } 322 SketchRelation::Fix(a) => { 323 self.kind_of(a)?; 324 true 325 } 326 }; 327 if ok { 328 Ok(()) 329 } else { 330 Err(SketchEditError::InvalidRelationOperands(rel)) 331 } 332 } 333 334 fn validate_dimension(&self, dim: SketchDimension) -> Result<(), SketchEditError> { 335 use SketchEntityKind as K; 336 let ok = match dim { 337 SketchDimension::Linear { a, b, .. } => { 338 self.kind_of(a)? == K::Point && self.kind_of(b)? == K::Point 339 } 340 SketchDimension::Radius { target, .. } | SketchDimension::Diameter { target, .. } => { 341 matches!(self.kind_of(target)?, K::Arc | K::Circle) 342 } 343 SketchDimension::Angular { a, b, .. } => { 344 self.kind_of(a)? == K::Line && self.kind_of(b)? == K::Line 345 } 346 }; 347 if ok { 348 Ok(()) 349 } else { 350 Err(SketchEditError::InvalidDimensionOperands(dim)) 351 } 352 } 353 354 fn add_entity(self, entity: SketchEntity) -> (Self, EditOutcome) { 355 let (id, entities) = insert(self.entities, entity); 356 let entity_order = push(self.entity_order, id); 357 let next = Self { 358 entities, 359 entity_order, 360 ..self 361 }; 362 (next, EditOutcome::Entity(id)) 363 } 364 365 fn add_relation(self, rel: SketchRelation) -> Result<(Self, EditOutcome), SketchEditError> { 366 self.validate_relation(rel)?; 367 let (id, relations) = insert(self.relations, rel); 368 let relation_order = push(self.relation_order, id); 369 let next = Self { 370 relations, 371 relation_order, 372 ..self 373 }; 374 Ok((next, EditOutcome::Relation(id))) 375 } 376 377 fn add_dimension(self, dim: SketchDimension) -> Result<(Self, EditOutcome), SketchEditError> { 378 self.validate_dimension(dim)?; 379 let (id, dimensions) = insert(self.dimensions, dim); 380 let dimension_order = push(self.dimension_order, id); 381 let next = Self { 382 dimensions, 383 dimension_order, 384 ..self 385 }; 386 Ok((next, EditOutcome::Dimension(id))) 387 } 388 389 fn add_parameter(self, parameter: SketchParameter) -> (Self, EditOutcome) { 390 let (id, parameters) = insert(self.parameters, parameter); 391 let parameter_order = push(self.parameter_order, id); 392 let next = Self { 393 parameters, 394 parameter_order, 395 ..self 396 }; 397 (next, EditOutcome::Parameter(id)) 398 } 399 400 fn delete_entity(self, id: SketchEntityId) -> Result<(Self, EditOutcome), SketchEditError> { 401 if !self.entities.contains_key(id) { 402 return Err(SketchEditError::EntityNotFound(id)); 403 } 404 let closure = entity_closure(&self.entities, id); 405 let entities = remove_many(self.entities, closure.iter().copied()); 406 let entity_order = retain(&self.entity_order, |eid| !closure.contains(eid)); 407 let touches_closure_rel = 408 |rel: &SketchRelation| rel.references().into_iter().any(|e| closure.contains(&e)); 409 let touches_closure_dim = 410 |dim: &SketchDimension| dim.references().into_iter().any(|e| closure.contains(&e)); 411 let (relations, dropped_relations) = remove_where(self.relations, touches_closure_rel); 412 let relation_order = retain(&self.relation_order, |rid| !dropped_relations.contains(rid)); 413 let (dimensions, dropped_dimensions) = remove_where(self.dimensions, touches_closure_dim); 414 let dimension_order = retain(&self.dimension_order, |did| { 415 !dropped_dimensions.contains(did) 416 }); 417 let next = Self { 418 entities, 419 entity_order, 420 relations, 421 relation_order, 422 dimensions, 423 dimension_order, 424 ..self 425 }; 426 Ok((next, EditOutcome::None)) 427 } 428 429 fn delete_relation(self, id: SketchRelationId) -> Result<(Self, EditOutcome), SketchEditError> { 430 if !self.relations.contains_key(id) { 431 return Err(SketchEditError::RelationNotFound(id)); 432 } 433 let relations = remove_one(self.relations, id); 434 let relation_order = retain(&self.relation_order, |rid| *rid != id); 435 let next = Self { 436 relations, 437 relation_order, 438 ..self 439 }; 440 Ok((next, EditOutcome::None)) 441 } 442 443 fn delete_dimension( 444 self, 445 id: SketchDimensionId, 446 ) -> Result<(Self, EditOutcome), SketchEditError> { 447 if !self.dimensions.contains_key(id) { 448 return Err(SketchEditError::DimensionNotFound(id)); 449 } 450 let dimensions = remove_one(self.dimensions, id); 451 let dimension_order = retain(&self.dimension_order, |did| *did != id); 452 let next = Self { 453 dimensions, 454 dimension_order, 455 ..self 456 }; 457 Ok((next, EditOutcome::None)) 458 } 459 460 fn delete_parameter( 461 self, 462 id: SketchParameterId, 463 ) -> Result<(Self, EditOutcome), SketchEditError> { 464 if !self.parameters.contains_key(id) { 465 return Err(SketchEditError::ParameterNotFound(id)); 466 } 467 let parameters = remove_one(self.parameters, id); 468 let parameter_order = retain(&self.parameter_order, |pid| *pid != id); 469 let next = Self { 470 parameters, 471 parameter_order, 472 ..self 473 }; 474 Ok((next, EditOutcome::None)) 475 } 476 477 fn set_construction( 478 self, 479 id: SketchEntityId, 480 for_construction: bool, 481 ) -> Result<(Self, EditOutcome), SketchEditError> { 482 let entity = *self.require_entity(id)?; 483 let updated = match entity { 484 SketchEntity::Point(_) => return Err(SketchEditError::PointAlwaysConstruction), 485 SketchEntity::Line(l) => SketchEntity::Line(l.with_construction(for_construction)), 486 SketchEntity::Arc(a) => SketchEntity::Arc(a.with_construction(for_construction)), 487 SketchEntity::Circle(c) => SketchEntity::Circle(c.with_construction(for_construction)), 488 }; 489 if updated == entity { 490 return Ok((self, EditOutcome::None)); 491 } 492 let entities = replace(self.entities, id, updated); 493 let next = Self { entities, ..self }; 494 Ok((next, EditOutcome::None)) 495 } 496 497 fn move_point( 498 self, 499 id: SketchEntityId, 500 position: Point2, 501 ) -> Result<(Self, EditOutcome), SketchEditError> { 502 let entity = *self.require_entity(id)?; 503 let SketchEntity::Point(_) = entity else { 504 return Err(SketchEditError::ExpectedPoint(id)); 505 }; 506 let updated = SketchEntity::point(position); 507 if updated == entity { 508 return Ok((self, EditOutcome::None)); 509 } 510 let entities = replace(self.entities, id, updated); 511 let next = Self { entities, ..self }; 512 Ok((next, EditOutcome::None)) 513 } 514 515 fn update_dimension( 516 self, 517 id: SketchDimensionId, 518 value: DimensionValue, 519 ) -> Result<(Self, EditOutcome), SketchEditError> { 520 let existing = *self 521 .dimensions 522 .get(id) 523 .ok_or(SketchEditError::DimensionNotFound(id))?; 524 if existing.kind() == DimensionKind::Driven { 525 return Err(SketchEditError::DimensionIsDriven(id)); 526 } 527 let updated = existing 528 .with_value(value) 529 .map_err(|_| SketchEditError::DimensionValueMismatch { id })?; 530 if updated == existing { 531 return Ok((self, EditOutcome::None)); 532 } 533 let dimensions = replace(self.dimensions, id, updated); 534 let next = Self { dimensions, ..self }; 535 Ok((next, EditOutcome::None)) 536 } 537 538 #[must_use] 539 pub(crate) fn with_point_positions(self, updates: &HashMap<SketchEntityId, Point2>) -> Self { 540 if updates.is_empty() { 541 return self; 542 } 543 let entities = updates 544 .iter() 545 .fold(unwrap_arc(self.entities), |mut acc, (id, pt)| { 546 if let Some(slot @ SketchEntity::Point(_)) = acc.get_mut(*id) { 547 *slot = SketchEntity::Point(PointData::new(*pt)); 548 } 549 acc 550 }); 551 Self { 552 entities: Arc::new(entities), 553 ..self 554 } 555 } 556 557 #[must_use] 558 pub(crate) fn with_circle_radii(self, updates: &HashMap<SketchEntityId, Length>) -> Self { 559 if updates.is_empty() { 560 return self; 561 } 562 let entities = updates 563 .iter() 564 .fold(unwrap_arc(self.entities), |mut acc, (id, r)| { 565 if let Some(SketchEntity::Circle(existing)) = acc.get(*id).copied() 566 && let Some(slot) = acc.get_mut(*id) 567 { 568 *slot = SketchEntity::Circle(CircleData::new( 569 existing.center(), 570 *r, 571 existing.for_construction(), 572 )); 573 } 574 acc 575 }); 576 Self { 577 entities: Arc::new(entities), 578 ..self 579 } 580 } 581 582 #[must_use] 583 pub(crate) fn with_driven_dimension_values( 584 self, 585 updates: &[(SketchDimensionId, DimensionValue)], 586 ) -> Self { 587 if updates.is_empty() { 588 return self; 589 } 590 let dimensions = 591 updates 592 .iter() 593 .copied() 594 .fold(unwrap_arc(self.dimensions), |mut acc, (id, v)| { 595 let existing = acc.get(id).copied(); 596 if let Some(dim) = existing 597 && dim.kind() == DimensionKind::Driven 598 && let Ok(next) = dim.with_value(v) 599 && let Some(slot) = acc.get_mut(id) 600 { 601 *slot = next; 602 } 603 acc 604 }); 605 Self { 606 dimensions: Arc::new(dimensions), 607 ..self 608 } 609 } 610} 611 612impl PartialEq for Sketch { 613 fn eq(&self, other: &Self) -> bool { 614 self.plane == other.plane 615 && *self.entity_order == *other.entity_order 616 && Self::entries_equal(&self.entity_order, &self.entities, &other.entities) 617 && *self.relation_order == *other.relation_order 618 && Self::entries_equal(&self.relation_order, &self.relations, &other.relations) 619 && *self.dimension_order == *other.dimension_order 620 && Self::entries_equal(&self.dimension_order, &self.dimensions, &other.dimensions) 621 && *self.parameter_order == *other.parameter_order 622 && Self::entries_equal(&self.parameter_order, &self.parameters, &other.parameters) 623 } 624} 625 626fn ensure_order_matches<K, V>( 627 container: &'static str, 628 order: &[K], 629 map: &SlotMap<K, V>, 630) -> Result<(), SketchEditError> 631where 632 K: slotmap::Key + Ord, 633{ 634 let order_set: BTreeSet<K> = order.iter().copied().collect(); 635 let has_duplicates = order_set.len() != order.len(); 636 let same_size = order.len() == map.len(); 637 let covers_map = order_set.iter().all(|id| map.contains_key(*id)); 638 if has_duplicates || !same_size || !covers_map { 639 Err(SketchEditError::OrderMismatch { container }) 640 } else { 641 Ok(()) 642 } 643} 644 645fn validate_entity_shape(entity: SketchEntity) -> Result<(), SketchEditError> { 646 let ok = match entity { 647 SketchEntity::Point(_) => true, 648 SketchEntity::Line(l) => l.a() != l.b(), 649 SketchEntity::Arc(a) => { 650 a.center() != a.start() && a.start() != a.end() && a.center() != a.end() 651 } 652 SketchEntity::Circle(c) => c.radius() > Length::default(), 653 }; 654 if ok { 655 Ok(()) 656 } else { 657 Err(SketchEditError::DegenerateEntity(entity)) 658 } 659} 660 661fn insert<K, V>(map: Arc<SlotMap<K, V>>, value: V) -> (K, Arc<SlotMap<K, V>>) 662where 663 K: slotmap::Key, 664 V: Clone, 665{ 666 let mut owned = unwrap_arc(map); 667 let id = owned.insert(value); 668 (id, Arc::new(owned)) 669} 670 671fn push<T: Clone>(list: Arc<Vec<T>>, value: T) -> Arc<Vec<T>> { 672 let mut owned = unwrap_arc(list); 673 owned.push(value); 674 Arc::new(owned) 675} 676 677fn retain<T, F>(list: &Arc<Vec<T>>, keep: F) -> Arc<Vec<T>> 678where 679 T: Clone, 680 F: Fn(&T) -> bool, 681{ 682 Arc::new(list.iter().filter(|t| keep(t)).cloned().collect()) 683} 684 685fn remove_one<K, V>(map: Arc<SlotMap<K, V>>, id: K) -> Arc<SlotMap<K, V>> 686where 687 K: slotmap::Key, 688 V: Clone, 689{ 690 let mut owned = unwrap_arc(map); 691 owned.remove(id); 692 Arc::new(owned) 693} 694 695fn remove_many<K, V, I>(map: Arc<SlotMap<K, V>>, ids: I) -> Arc<SlotMap<K, V>> 696where 697 K: slotmap::Key, 698 V: Clone, 699 I: IntoIterator<Item = K>, 700{ 701 let owned = ids.into_iter().fold(unwrap_arc(map), |mut acc, id| { 702 acc.remove(id); 703 acc 704 }); 705 Arc::new(owned) 706} 707 708fn remove_where<K, V, F>(map: Arc<SlotMap<K, V>>, predicate: F) -> (Arc<SlotMap<K, V>>, BTreeSet<K>) 709where 710 K: slotmap::Key + Ord, 711 V: Clone, 712 F: Fn(&V) -> bool, 713{ 714 let dropped: BTreeSet<K> = map 715 .iter() 716 .filter_map(|(k, v)| predicate(v).then_some(k)) 717 .collect(); 718 let next = remove_many(map, dropped.iter().copied()); 719 (next, dropped) 720} 721 722fn replace<K, V>(map: Arc<SlotMap<K, V>>, id: K, value: V) -> Arc<SlotMap<K, V>> 723where 724 K: slotmap::Key, 725 V: Clone, 726{ 727 let mut owned = unwrap_arc(map); 728 if let Some(slot) = owned.get_mut(id) { 729 *slot = value; 730 } 731 Arc::new(owned) 732} 733 734fn unwrap_arc<T: Clone>(arc: Arc<T>) -> T { 735 Arc::try_unwrap(arc).unwrap_or_else(|shared| (*shared).clone()) 736} 737 738fn entity_closure(entities: &EntityMap, seed: SketchEntityId) -> BTreeSet<SketchEntityId> { 739 grow_closure(entities, BTreeSet::from([seed])) 740} 741 742fn grow_closure( 743 entities: &EntityMap, 744 closure: BTreeSet<SketchEntityId>, 745) -> BTreeSet<SketchEntityId> { 746 let grown: BTreeSet<SketchEntityId> = entities 747 .iter() 748 .filter_map(|(id, entity)| { 749 entity 750 .references() 751 .into_iter() 752 .any(|r| closure.contains(&r)) 753 .then_some(id) 754 }) 755 .chain(closure.iter().copied()) 756 .collect(); 757 if grown == closure { 758 closure 759 } else { 760 grow_closure(entities, grown) 761 } 762} 763 764#[derive(Debug, Clone, PartialEq, thiserror::Error)] 765pub enum SketchEditError { 766 #[error("entity not found: {0:?}")] 767 EntityNotFound(SketchEntityId), 768 #[error("relation not found: {0:?}")] 769 RelationNotFound(SketchRelationId), 770 #[error("dimension not found: {0:?}")] 771 DimensionNotFound(SketchDimensionId), 772 #[error("parameter not found: {0:?}")] 773 ParameterNotFound(SketchParameterId), 774 #[error("expected point entity: {0:?}")] 775 ExpectedPoint(SketchEntityId), 776 #[error("cannot toggle construction on a point")] 777 PointAlwaysConstruction, 778 #[error("dimension {id:?} value kind does not match variant")] 779 DimensionValueMismatch { id: SketchDimensionId }, 780 #[error("cannot update driven dimension: {0:?}")] 781 DimensionIsDriven(SketchDimensionId), 782 #[error("invalid relation operands: {0:?}")] 783 InvalidRelationOperands(SketchRelation), 784 #[error("invalid dimension operands: {0:?}")] 785 InvalidDimensionOperands(SketchDimension), 786 #[error("entity is degenerate: {0:?}")] 787 DegenerateEntity(SketchEntity), 788 #[error("self-referencing relation: {0:?}")] 789 SelfReferencingRelation(SketchRelation), 790 #[error("{container} order list is out of step with its slotmap")] 791 OrderMismatch { container: &'static str }, 792} 793 794#[cfg(test)] 795mod tests { 796 use super::*; 797 use bone_types::{ 798 Angle, Length, Parameter, Point2, Point3, Tolerance, UnitVec3, degree, millimeter, 799 }; 800 801 fn plane() -> SketchPlaneBasis { 802 let Ok(basis) = SketchPlaneBasis::new( 803 Point3::origin(), 804 UnitVec3::x_axis(), 805 UnitVec3::y_axis(), 806 Tolerance::new(1e-9), 807 ) else { 808 panic!("xy plane basis is orthogonal"); 809 }; 810 basis 811 } 812 813 fn len_mm(v: f64) -> Length { 814 Length::new::<millimeter>(v) 815 } 816 817 fn angle_deg(v: f64) -> Angle { 818 Angle::new::<degree>(v) 819 } 820 821 fn rectangle_script() -> Vec<SketchEdit> { 822 vec![ 823 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))), 824 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(10.0, 0.0))), 825 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(10.0, 5.0))), 826 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 5.0))), 827 ] 828 } 829 830 fn apply_script(sketch: &Sketch, edits: Vec<SketchEdit>) -> (Sketch, Vec<EditOutcome>) { 831 let Ok(pair) = sketch.apply_all(edits) else { 832 panic!("fixture edits apply cleanly"); 833 }; 834 pair 835 } 836 837 #[test] 838 fn point_is_always_construction() { 839 let p = SketchEntity::point(Point2::from_mm(0.0, 0.0)); 840 assert!(p.for_construction()); 841 } 842 843 #[test] 844 fn new_sketch_is_empty() { 845 let s = Sketch::new(plane()); 846 assert!(s.entities().is_empty()); 847 assert!(s.relations().is_empty()); 848 assert!(s.dimensions().is_empty()); 849 assert!(s.parameters().is_empty()); 850 assert!(s.entity_order().is_empty()); 851 } 852 853 #[test] 854 fn add_point_records_insertion_order() { 855 let (sketch, outcomes) = apply_script(&Sketch::new(plane()), rectangle_script()); 856 let ids: Vec<_> = outcomes 857 .iter() 858 .map(|o| match *o { 859 EditOutcome::Entity(id) => id, 860 _ => panic!("expected entity outcomes"), 861 }) 862 .collect(); 863 assert_eq!(sketch.entity_order(), ids.as_slice()); 864 assert_eq!(sketch.entities().len(), 4); 865 } 866 867 #[test] 868 fn add_line_requires_point_endpoints() { 869 let mut sketch = Sketch::new(plane()); 870 let Ok((s1, EditOutcome::Entity(a))) = sketch.apply(SketchEdit::AddEntity( 871 SketchEntity::point(Point2::from_mm(0.0, 0.0)), 872 )) else { 873 panic!("add point"); 874 }; 875 let Ok((s2, EditOutcome::Entity(b))) = s1.apply(SketchEdit::AddEntity( 876 SketchEntity::point(Point2::from_mm(1.0, 0.0)), 877 )) else { 878 panic!("add point"); 879 }; 880 let Ok((s3, EditOutcome::Entity(_))) = 881 s2.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) 882 else { 883 panic!("add line"); 884 }; 885 sketch = s3; 886 let bad = sketch.apply(SketchEdit::AddEntity(SketchEntity::line( 887 a, 888 SketchEntityId::default(), 889 false, 890 ))); 891 assert!(matches!(bad, Err(SketchEditError::EntityNotFound(_)))); 892 } 893 894 #[test] 895 fn add_line_rejects_non_point_endpoint() { 896 let Ok((s1, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity( 897 SketchEntity::point(Point2::from_mm(0.0, 0.0)), 898 )) else { 899 panic!("add point"); 900 }; 901 let Ok((s2, EditOutcome::Entity(center))) = s1.apply(SketchEdit::AddEntity( 902 SketchEntity::point(Point2::from_mm(2.0, 0.0)), 903 )) else { 904 panic!("add center"); 905 }; 906 let Ok((s3, EditOutcome::Entity(circle))) = s2.apply(SketchEdit::AddEntity( 907 SketchEntity::circle(center, len_mm(1.0), false), 908 )) else { 909 panic!("add circle"); 910 }; 911 let bad = s3.apply(SketchEdit::AddEntity(SketchEntity::line(a, circle, false))); 912 assert!(matches!(bad, Err(SketchEditError::ExpectedPoint(_)))); 913 } 914 915 #[test] 916 fn delete_entity_cascades_dependents() { 917 let s = Sketch::new(plane()); 918 let Ok((s, EditOutcome::Entity(a))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( 919 Point2::from_mm(0.0, 0.0), 920 ))) else { 921 panic!("a"); 922 }; 923 let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( 924 Point2::from_mm(1.0, 0.0), 925 ))) else { 926 panic!("b"); 927 }; 928 let Ok((s, EditOutcome::Entity(line))) = 929 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) 930 else { 931 panic!("line"); 932 }; 933 let Ok((s, EditOutcome::Relation(_))) = 934 s.apply(SketchEdit::AddRelation(SketchRelation::Horizontal(line))) 935 else { 936 panic!("rel"); 937 }; 938 let Ok((s, EditOutcome::Dimension(_))) = 939 s.apply(SketchEdit::AddDimension(SketchDimension::Linear { 940 a, 941 b, 942 value: len_mm(10.0), 943 kind: DimensionKind::Driving, 944 })) 945 else { 946 panic!("dim"); 947 }; 948 let Ok((after, EditOutcome::None)) = s.apply(SketchEdit::DeleteEntity(a)) else { 949 panic!("delete"); 950 }; 951 assert_eq!(after.entities().len(), 1); 952 assert!(after.entities().contains_key(b)); 953 assert!(!after.entities().contains_key(line)); 954 assert!(after.relations().is_empty()); 955 assert!(after.dimensions().is_empty()); 956 assert_eq!(after.entity_order(), [b]); 957 assert!(after.relation_order().is_empty()); 958 assert!(after.dimension_order().is_empty()); 959 } 960 961 #[test] 962 fn delete_entity_errors_on_unknown() { 963 let s = Sketch::new(plane()); 964 let bad = s.apply(SketchEdit::DeleteEntity(SketchEntityId::default())); 965 assert!(matches!(bad, Err(SketchEditError::EntityNotFound(_)))); 966 } 967 968 #[test] 969 fn move_point_updates_position() { 970 let Ok((s, EditOutcome::Entity(p))) = Sketch::new(plane()).apply(SketchEdit::AddEntity( 971 SketchEntity::point(Point2::from_mm(1.0, 2.0)), 972 )) else { 973 panic!("seed point"); 974 }; 975 let target = Point2::from_mm(7.0, -3.0); 976 let Ok((after, EditOutcome::None)) = s.apply(SketchEdit::MovePoint { 977 id: p, 978 position: target, 979 }) else { 980 panic!("move accepts"); 981 }; 982 let SketchEntity::Point(pt) = after.entities()[p] else { 983 panic!("still a point"); 984 }; 985 assert_eq!(pt.at(), target); 986 } 987 988 #[test] 989 fn move_point_no_op_when_position_unchanged() { 990 let at = Point2::from_mm(4.0, 4.0); 991 let Ok((s, EditOutcome::Entity(p))) = 992 Sketch::new(plane()).apply(SketchEdit::AddEntity(SketchEntity::point(at))) 993 else { 994 panic!("seed point"); 995 }; 996 let Ok((after, EditOutcome::None)) = s.clone().apply(SketchEdit::MovePoint { 997 id: p, 998 position: at, 999 }) else { 1000 panic!("idempotent move accepts"); 1001 }; 1002 assert_eq!(after, s); 1003 } 1004 1005 #[test] 1006 fn move_point_rejects_non_point_entity() { 1007 let s = Sketch::new(plane()); 1008 let Ok((s, EditOutcome::Entity(a))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( 1009 Point2::from_mm(0.0, 0.0), 1010 ))) else { 1011 panic!("a"); 1012 }; 1013 let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( 1014 Point2::from_mm(1.0, 0.0), 1015 ))) else { 1016 panic!("b"); 1017 }; 1018 let Ok((s, EditOutcome::Entity(line))) = 1019 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) 1020 else { 1021 panic!("line"); 1022 }; 1023 let bad = s.apply(SketchEdit::MovePoint { 1024 id: line, 1025 position: Point2::from_mm(2.0, 2.0), 1026 }); 1027 assert!(matches!(bad, Err(SketchEditError::ExpectedPoint(_)))); 1028 } 1029 1030 #[test] 1031 fn move_point_errors_on_unknown_id() { 1032 let s = Sketch::new(plane()); 1033 let bad = s.apply(SketchEdit::MovePoint { 1034 id: SketchEntityId::default(), 1035 position: Point2::from_mm(0.0, 0.0), 1036 }); 1037 assert!(matches!(bad, Err(SketchEditError::EntityNotFound(_)))); 1038 } 1039 1040 #[test] 1041 fn set_construction_refuses_point() { 1042 let Ok((s, EditOutcome::Entity(p))) = Sketch::new(plane()).apply(SketchEdit::AddEntity( 1043 SketchEntity::point(Point2::from_mm(0.0, 0.0)), 1044 )) else { 1045 panic!("p"); 1046 }; 1047 let bad = s.apply(SketchEdit::SetConstruction { 1048 id: p, 1049 for_construction: false, 1050 }); 1051 assert!(matches!(bad, Err(SketchEditError::PointAlwaysConstruction))); 1052 } 1053 1054 #[test] 1055 fn set_construction_toggles_line() { 1056 let s = Sketch::new(plane()); 1057 let Ok((s, EditOutcome::Entity(a))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( 1058 Point2::from_mm(0.0, 0.0), 1059 ))) else { 1060 panic!("a"); 1061 }; 1062 let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( 1063 Point2::from_mm(1.0, 0.0), 1064 ))) else { 1065 panic!("b"); 1066 }; 1067 let Ok((s, EditOutcome::Entity(line))) = 1068 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) 1069 else { 1070 panic!("line"); 1071 }; 1072 assert!(!s.entities()[line].for_construction()); 1073 let Ok((s, _)) = s.apply(SketchEdit::SetConstruction { 1074 id: line, 1075 for_construction: true, 1076 }) else { 1077 panic!("toggle"); 1078 }; 1079 assert!(s.entities()[line].for_construction()); 1080 } 1081 1082 #[test] 1083 fn update_dimension_value_rejects_unit_mismatch() { 1084 let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity( 1085 SketchEntity::point(Point2::from_mm(0.0, 0.0)), 1086 )) else { 1087 panic!("a"); 1088 }; 1089 let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( 1090 Point2::from_mm(1.0, 0.0), 1091 ))) else { 1092 panic!("b"); 1093 }; 1094 let Ok((s, EditOutcome::Dimension(id))) = 1095 s.apply(SketchEdit::AddDimension(SketchDimension::Linear { 1096 a, 1097 b, 1098 value: len_mm(1.0), 1099 kind: DimensionKind::Driving, 1100 })) 1101 else { 1102 panic!("dim"); 1103 }; 1104 let bad = s.apply(SketchEdit::UpdateDimensionValue { 1105 id, 1106 value: DimensionValue::Angle(angle_deg(90.0)), 1107 }); 1108 assert!(matches!( 1109 bad, 1110 Err(SketchEditError::DimensionValueMismatch { .. }) 1111 )); 1112 } 1113 1114 #[test] 1115 fn update_dimension_value_replaces_length() { 1116 let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity( 1117 SketchEntity::point(Point2::from_mm(0.0, 0.0)), 1118 )) else { 1119 panic!("a"); 1120 }; 1121 let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( 1122 Point2::from_mm(1.0, 0.0), 1123 ))) else { 1124 panic!("b"); 1125 }; 1126 let Ok((s, EditOutcome::Dimension(id))) = 1127 s.apply(SketchEdit::AddDimension(SketchDimension::Linear { 1128 a, 1129 b, 1130 value: len_mm(1.0), 1131 kind: DimensionKind::Driving, 1132 })) 1133 else { 1134 panic!("dim"); 1135 }; 1136 let Ok((s, _)) = s.apply(SketchEdit::UpdateDimensionValue { 1137 id, 1138 value: DimensionValue::Length(len_mm(12.5)), 1139 }) else { 1140 panic!("update"); 1141 }; 1142 let SketchDimension::Linear { value, .. } = s.dimensions()[id] else { 1143 panic!("variant unchanged"); 1144 }; 1145 assert!((value.get::<millimeter>() - 12.5).abs() < 1e-12); 1146 } 1147 1148 #[test] 1149 fn identical_scripts_produce_equal_sketches() { 1150 let script = rich_script(); 1151 let a = Sketch::new(plane()) 1152 .apply_all(script.clone()) 1153 .map(|(s, _)| s); 1154 let b = Sketch::new(plane()).apply_all(script).map(|(s, _)| s); 1155 let (Ok(a), Ok(b)) = (a, b) else { 1156 panic!("scripts apply cleanly"); 1157 }; 1158 assert_eq!(a, b); 1159 } 1160 1161 #[test] 1162 fn relation_edit_shares_entity_storage() { 1163 let (base, _) = apply_script(&Sketch::new(plane()), rectangle_script()); 1164 let before = base.clone(); 1165 let Ok((base_with_rel, _)) = base.apply(SketchEdit::AddRelation(SketchRelation::Fix( 1166 before.entity_order()[0], 1167 ))) else { 1168 panic!("relation"); 1169 }; 1170 assert!(Arc::ptr_eq(&before.entities, &base_with_rel.entities)); 1171 assert!(Arc::ptr_eq( 1172 &before.entity_order, 1173 &base_with_rel.entity_order 1174 )); 1175 assert!(Arc::ptr_eq(&before.dimensions, &base_with_rel.dimensions)); 1176 assert!(!Arc::ptr_eq(&before.relations, &base_with_rel.relations)); 1177 } 1178 1179 #[test] 1180 fn add_parameter_roundtrips() { 1181 let s = Sketch::new(plane()); 1182 let Ok((s, EditOutcome::Parameter(id))) = s.apply(SketchEdit::AddParameter( 1183 SketchParameter::new(Parameter::new(0.5)), 1184 )) else { 1185 panic!("param"); 1186 }; 1187 assert!((s.parameters()[id].value().value() - 0.5).abs() < f64::EPSILON); 1188 let Ok((s, _)) = s.apply(SketchEdit::DeleteParameter(id)) else { 1189 panic!("delete param"); 1190 }; 1191 assert!(s.parameters().is_empty()); 1192 assert!(s.parameter_order().is_empty()); 1193 } 1194 1195 fn rich_script() -> Vec<SketchEdit> { 1196 let mut edits = rectangle_script(); 1197 edits.extend([ 1198 SketchEdit::AddParameter(SketchParameter::new(Parameter::new(0.0))), 1199 SketchEdit::AddParameter(SketchParameter::new(Parameter::new(1.0))), 1200 ]); 1201 edits 1202 } 1203 1204 fn two_points() -> (Sketch, SketchEntityId, SketchEntityId) { 1205 let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity( 1206 SketchEntity::point(Point2::from_mm(0.0, 0.0)), 1207 )) else { 1208 panic!("a"); 1209 }; 1210 let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( 1211 Point2::from_mm(1.0, 0.0), 1212 ))) else { 1213 panic!("b"); 1214 }; 1215 (s, a, b) 1216 } 1217 1218 #[test] 1219 fn horizontal_on_point_is_rejected() { 1220 let (s, a, _) = two_points(); 1221 let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Horizontal(a))); 1222 assert!(matches!( 1223 bad, 1224 Err(SketchEditError::InvalidRelationOperands(_)) 1225 )); 1226 } 1227 1228 #[test] 1229 fn tangent_between_two_lines_is_rejected() { 1230 let (s, a, b) = two_points(); 1231 let Ok((s, EditOutcome::Entity(c))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( 1232 Point2::from_mm(0.0, 1.0), 1233 ))) else { 1234 panic!("c"); 1235 }; 1236 let Ok((s, EditOutcome::Entity(l1))) = 1237 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) 1238 else { 1239 panic!("l1"); 1240 }; 1241 let Ok((s, EditOutcome::Entity(l2))) = 1242 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, c, false))) 1243 else { 1244 panic!("l2"); 1245 }; 1246 let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Tangent(l1, l2))); 1247 assert!(matches!( 1248 bad, 1249 Err(SketchEditError::InvalidRelationOperands(_)) 1250 )); 1251 } 1252 1253 #[test] 1254 fn equal_cross_kind_is_rejected() { 1255 let (s, a, b) = two_points(); 1256 let Ok((s, EditOutcome::Entity(line))) = 1257 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) 1258 else { 1259 panic!("line"); 1260 }; 1261 let Ok((s, EditOutcome::Entity(circle))) = s.apply(SketchEdit::AddEntity( 1262 SketchEntity::circle(a, len_mm(1.0), false), 1263 )) else { 1264 panic!("circle"); 1265 }; 1266 let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Equal(line, circle))); 1267 assert!(matches!( 1268 bad, 1269 Err(SketchEditError::InvalidRelationOperands(_)) 1270 )); 1271 } 1272 1273 #[test] 1274 fn coincident_point_on_line_is_accepted() { 1275 let (s, a, b) = two_points(); 1276 let Ok((s, EditOutcome::Entity(line))) = 1277 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) 1278 else { 1279 panic!("line"); 1280 }; 1281 let Ok((s2, _)) = s.apply(SketchEdit::AddRelation(SketchRelation::Coincident(a, line))) 1282 else { 1283 panic!("coincident"); 1284 }; 1285 assert_eq!(s2.relations().len(), 1); 1286 } 1287 1288 #[test] 1289 fn linear_dimension_on_non_point_is_rejected() { 1290 let (s, a, b) = two_points(); 1291 let Ok((s, EditOutcome::Entity(line))) = 1292 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) 1293 else { 1294 panic!("line"); 1295 }; 1296 let bad = s.apply(SketchEdit::AddDimension(SketchDimension::Linear { 1297 a: line, 1298 b: a, 1299 value: len_mm(1.0), 1300 kind: DimensionKind::Driving, 1301 })); 1302 assert!(matches!( 1303 bad, 1304 Err(SketchEditError::InvalidDimensionOperands(_)) 1305 )); 1306 } 1307 1308 #[test] 1309 fn driven_dimension_cannot_be_updated() { 1310 let (s, a, b) = two_points(); 1311 let Ok((s, EditOutcome::Dimension(id))) = 1312 s.apply(SketchEdit::AddDimension(SketchDimension::Linear { 1313 a, 1314 b, 1315 value: len_mm(1.0), 1316 kind: DimensionKind::Driven, 1317 })) 1318 else { 1319 panic!("dim"); 1320 }; 1321 let bad = s.apply(SketchEdit::UpdateDimensionValue { 1322 id, 1323 value: DimensionValue::Length(len_mm(2.0)), 1324 }); 1325 assert!(matches!(bad, Err(SketchEditError::DimensionIsDriven(_)))); 1326 } 1327 1328 #[test] 1329 fn apply_all_leaves_caller_unchanged_on_error() { 1330 let (base, _) = apply_script(&Sketch::new(plane()), rectangle_script()); 1331 let snapshot = base.clone(); 1332 let bad_edit = SketchEdit::AddRelation(SketchRelation::Horizontal(base.entity_order()[0])); 1333 let result = base.apply_all([bad_edit]); 1334 assert!(matches!( 1335 result, 1336 Err(SketchEditError::InvalidRelationOperands(_)) 1337 )); 1338 assert_eq!(base, snapshot); 1339 } 1340 1341 #[test] 1342 fn line_with_same_endpoints_is_rejected() { 1343 let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity( 1344 SketchEntity::point(Point2::from_mm(0.0, 0.0)), 1345 )) else { 1346 panic!("a"); 1347 }; 1348 let bad = s.apply(SketchEdit::AddEntity(SketchEntity::line(a, a, false))); 1349 assert!(matches!(bad, Err(SketchEditError::DegenerateEntity(_)))); 1350 } 1351 1352 #[test] 1353 fn arc_with_repeated_endpoints_is_rejected() { 1354 let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity( 1355 SketchEntity::point(Point2::from_mm(0.0, 0.0)), 1356 )) else { 1357 panic!("a"); 1358 }; 1359 let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( 1360 Point2::from_mm(1.0, 0.0), 1361 ))) else { 1362 panic!("b"); 1363 }; 1364 let bad = s.apply(SketchEdit::AddEntity(SketchEntity::arc(a, b, a, false))); 1365 assert!(matches!(bad, Err(SketchEditError::DegenerateEntity(_)))); 1366 } 1367 1368 #[test] 1369 fn self_referencing_relation_is_rejected() { 1370 let (s, a, b) = two_points(); 1371 let Ok((s, EditOutcome::Entity(line))) = 1372 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) 1373 else { 1374 panic!("line"); 1375 }; 1376 let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Parallel( 1377 line, line, 1378 ))); 1379 assert!(matches!( 1380 bad, 1381 Err(SketchEditError::SelfReferencingRelation(_)) 1382 )); 1383 } 1384 1385 #[test] 1386 fn coincident_self_reference_is_rejected() { 1387 let (s, a, _) = two_points(); 1388 let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Coincident(a, a))); 1389 assert!(matches!( 1390 bad, 1391 Err(SketchEditError::SelfReferencingRelation(_)) 1392 )); 1393 } 1394 1395 fn line_with_third_point( 1396 s: Sketch, 1397 a: SketchEntityId, 1398 b: SketchEntityId, 1399 ) -> (Sketch, SketchEntityId, SketchEntityId) { 1400 let Ok((s, EditOutcome::Entity(line))) = 1401 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) 1402 else { 1403 panic!("line"); 1404 }; 1405 let Ok((s, EditOutcome::Entity(p))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( 1406 Point2::from_mm(0.0, 1.0), 1407 ))) else { 1408 panic!("midpoint"); 1409 }; 1410 (s, line, p) 1411 } 1412 1413 #[test] 1414 fn midpoint_relation_accepts_distinct_point() { 1415 let (s, a, b) = two_points(); 1416 let (s, line, mid) = line_with_third_point(s, a, b); 1417 let Ok((s2, _)) = s.apply(SketchEdit::AddRelation(SketchRelation::Midpoint { 1418 point: mid, 1419 line, 1420 })) else { 1421 panic!("midpoint relation"); 1422 }; 1423 assert_eq!(s2.relations().len(), 1); 1424 } 1425 1426 #[test] 1427 fn midpoint_relation_rejects_endpoint_a_as_point() { 1428 let (s, a, b) = two_points(); 1429 let (s, line, _) = line_with_third_point(s, a, b); 1430 let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Midpoint { 1431 point: a, 1432 line, 1433 })); 1434 assert!(matches!( 1435 bad, 1436 Err(SketchEditError::InvalidRelationOperands(_)) 1437 )); 1438 } 1439 1440 #[test] 1441 fn midpoint_relation_rejects_endpoint_b_as_point() { 1442 let (s, a, b) = two_points(); 1443 let (s, line, _) = line_with_third_point(s, a, b); 1444 let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Midpoint { 1445 point: b, 1446 line, 1447 })); 1448 assert!(matches!( 1449 bad, 1450 Err(SketchEditError::InvalidRelationOperands(_)) 1451 )); 1452 } 1453 1454 #[test] 1455 fn midpoint_relation_rejects_non_line_target() { 1456 let (s, a, _) = two_points(); 1457 let Ok((s, EditOutcome::Entity(circle))) = s.apply(SketchEdit::AddEntity( 1458 SketchEntity::circle(a, len_mm(1.0), false), 1459 )) else { 1460 panic!("circle"); 1461 }; 1462 let Ok((s, EditOutcome::Entity(p))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( 1463 Point2::from_mm(2.0, 0.0), 1464 ))) else { 1465 panic!("p"); 1466 }; 1467 let bad = s.apply(SketchEdit::AddRelation(SketchRelation::Midpoint { 1468 point: p, 1469 line: circle, 1470 })); 1471 assert!(matches!( 1472 bad, 1473 Err(SketchEditError::InvalidRelationOperands(_)) 1474 )); 1475 } 1476 1477 #[test] 1478 fn set_construction_noop_shares_storage() { 1479 let (s, a, b) = two_points(); 1480 let Ok((s, EditOutcome::Entity(line))) = 1481 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, true))) 1482 else { 1483 panic!("line"); 1484 }; 1485 let before = s.clone(); 1486 let Ok((after, _)) = s.apply(SketchEdit::SetConstruction { 1487 id: line, 1488 for_construction: true, 1489 }) else { 1490 panic!("set"); 1491 }; 1492 assert!(Arc::ptr_eq(&before.entities, &after.entities)); 1493 } 1494 1495 #[test] 1496 fn circle_with_non_positive_radius_is_rejected() { 1497 let Ok((s, EditOutcome::Entity(center))) = Sketch::new(plane()).apply( 1498 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))), 1499 ) else { 1500 panic!("center"); 1501 }; 1502 let zero = s.clone().apply(SketchEdit::AddEntity(SketchEntity::circle( 1503 center, 1504 len_mm(0.0), 1505 false, 1506 ))); 1507 assert!(matches!(zero, Err(SketchEditError::DegenerateEntity(_)))); 1508 let negative = s.apply(SketchEdit::AddEntity(SketchEntity::circle( 1509 center, 1510 len_mm(-1.0), 1511 false, 1512 ))); 1513 assert!(matches!( 1514 negative, 1515 Err(SketchEditError::DegenerateEntity(_)) 1516 )); 1517 } 1518 1519 #[test] 1520 fn delete_relation_errors_on_unknown() { 1521 let s = Sketch::new(plane()); 1522 let bad = s.apply(SketchEdit::DeleteRelation(SketchRelationId::default())); 1523 assert!(matches!(bad, Err(SketchEditError::RelationNotFound(_)))); 1524 } 1525 1526 #[test] 1527 fn delete_dimension_errors_on_unknown() { 1528 let s = Sketch::new(plane()); 1529 let bad = s.apply(SketchEdit::DeleteDimension(SketchDimensionId::default())); 1530 assert!(matches!(bad, Err(SketchEditError::DimensionNotFound(_)))); 1531 } 1532 1533 #[test] 1534 fn delete_parameter_errors_on_unknown() { 1535 let s = Sketch::new(plane()); 1536 let bad = s.apply(SketchEdit::DeleteParameter(SketchParameterId::default())); 1537 assert!(matches!(bad, Err(SketchEditError::ParameterNotFound(_)))); 1538 } 1539 1540 #[test] 1541 fn update_dimension_errors_on_unknown() { 1542 let s = Sketch::new(plane()); 1543 let bad = s.apply(SketchEdit::UpdateDimensionValue { 1544 id: SketchDimensionId::default(), 1545 value: DimensionValue::Length(len_mm(1.0)), 1546 }); 1547 assert!(matches!(bad, Err(SketchEditError::DimensionNotFound(_)))); 1548 } 1549 1550 #[test] 1551 fn driven_radius_cannot_be_updated() { 1552 let Ok((s, EditOutcome::Entity(center))) = Sketch::new(plane()).apply( 1553 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))), 1554 ) else { 1555 panic!("center"); 1556 }; 1557 let Ok((s, EditOutcome::Entity(circle))) = s.apply(SketchEdit::AddEntity( 1558 SketchEntity::circle(center, len_mm(1.0), false), 1559 )) else { 1560 panic!("circle"); 1561 }; 1562 let Ok((s, EditOutcome::Dimension(id))) = 1563 s.apply(SketchEdit::AddDimension(SketchDimension::Radius { 1564 target: circle, 1565 value: len_mm(1.0), 1566 kind: DimensionKind::Driven, 1567 })) 1568 else { 1569 panic!("dim"); 1570 }; 1571 let bad = s.apply(SketchEdit::UpdateDimensionValue { 1572 id, 1573 value: DimensionValue::Length(len_mm(2.0)), 1574 }); 1575 assert!(matches!(bad, Err(SketchEditError::DimensionIsDriven(_)))); 1576 } 1577 1578 #[test] 1579 fn driven_diameter_cannot_be_updated() { 1580 let Ok((s, EditOutcome::Entity(center))) = Sketch::new(plane()).apply( 1581 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))), 1582 ) else { 1583 panic!("center"); 1584 }; 1585 let Ok((s, EditOutcome::Entity(circle))) = s.apply(SketchEdit::AddEntity( 1586 SketchEntity::circle(center, len_mm(1.0), false), 1587 )) else { 1588 panic!("circle"); 1589 }; 1590 let Ok((s, EditOutcome::Dimension(id))) = 1591 s.apply(SketchEdit::AddDimension(SketchDimension::Diameter { 1592 target: circle, 1593 value: len_mm(2.0), 1594 kind: DimensionKind::Driven, 1595 })) 1596 else { 1597 panic!("dim"); 1598 }; 1599 let bad = s.apply(SketchEdit::UpdateDimensionValue { 1600 id, 1601 value: DimensionValue::Length(len_mm(4.0)), 1602 }); 1603 assert!(matches!(bad, Err(SketchEditError::DimensionIsDriven(_)))); 1604 } 1605 1606 #[test] 1607 fn driven_angular_cannot_be_updated() { 1608 let (s, a, b) = two_points(); 1609 let Ok((s, EditOutcome::Entity(c))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( 1610 Point2::from_mm(0.0, 1.0), 1611 ))) else { 1612 panic!("c"); 1613 }; 1614 let Ok((s, EditOutcome::Entity(l1))) = 1615 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) 1616 else { 1617 panic!("l1"); 1618 }; 1619 let Ok((s, EditOutcome::Entity(l2))) = 1620 s.apply(SketchEdit::AddEntity(SketchEntity::line(a, c, false))) 1621 else { 1622 panic!("l2"); 1623 }; 1624 let Ok((s, EditOutcome::Dimension(id))) = 1625 s.apply(SketchEdit::AddDimension(SketchDimension::Angular { 1626 a: l1, 1627 b: l2, 1628 value: angle_deg(90.0), 1629 kind: DimensionKind::Driven, 1630 })) 1631 else { 1632 panic!("dim"); 1633 }; 1634 let bad = s.apply(SketchEdit::UpdateDimensionValue { 1635 id, 1636 value: DimensionValue::Angle(angle_deg(45.0)), 1637 }); 1638 assert!(matches!(bad, Err(SketchEditError::DimensionIsDriven(_)))); 1639 } 1640 1641 #[test] 1642 fn status_reports_dangling_when_relation_references_missing_entity() { 1643 use bone_types::SketchStatus; 1644 1645 let (mut sketch, outcomes) = apply_script(&Sketch::new(plane()), rectangle_script()); 1646 let entity_ids: Vec<SketchEntityId> = outcomes 1647 .iter() 1648 .map(|o| match *o { 1649 EditOutcome::Entity(id) => id, 1650 _ => panic!("expected entity outcomes"), 1651 }) 1652 .collect(); 1653 let Ok((with_rel, EditOutcome::Relation(rel_id))) = sketch.apply(SketchEdit::AddRelation( 1654 SketchRelation::Coincident(entity_ids[0], entity_ids[1]), 1655 )) else { 1656 panic!("relation should add cleanly"); 1657 }; 1658 sketch = with_rel; 1659 let entities = Arc::make_mut(&mut sketch.entities); 1660 assert!(entities.remove(entity_ids[1]).is_some()); 1661 1662 let report = sketch.status(); 1663 assert_eq!(report.status(), SketchStatus::Dangling); 1664 assert!( 1665 report 1666 .offending() 1667 .contains(&bone_types::SketchItemId::Relation(rel_id)) 1668 ); 1669 } 1670 1671 #[test] 1672 fn status_reports_dangling_when_dimension_references_missing_entity() { 1673 use bone_types::SketchStatus; 1674 1675 let (mut sketch, outcomes) = apply_script(&Sketch::new(plane()), rectangle_script()); 1676 let entity_ids: Vec<SketchEntityId> = outcomes 1677 .iter() 1678 .map(|o| match *o { 1679 EditOutcome::Entity(id) => id, 1680 _ => panic!("expected entity outcomes"), 1681 }) 1682 .collect(); 1683 let Ok((with_dim, EditOutcome::Dimension(dim_id))) = 1684 sketch.apply(SketchEdit::AddDimension(SketchDimension::Linear { 1685 a: entity_ids[0], 1686 b: entity_ids[1], 1687 value: len_mm(10.0), 1688 kind: DimensionKind::Driving, 1689 })) 1690 else { 1691 panic!("dimension should add cleanly"); 1692 }; 1693 sketch = with_dim; 1694 let entities = Arc::make_mut(&mut sketch.entities); 1695 assert!(entities.remove(entity_ids[0]).is_some()); 1696 1697 let report = sketch.status(); 1698 assert_eq!(report.status(), SketchStatus::Dangling); 1699 assert!( 1700 report 1701 .offending() 1702 .contains(&bone_types::SketchItemId::Dimension(dim_id)) 1703 ); 1704 } 1705 1706 #[test] 1707 fn status_reports_dangling_when_entity_references_missing_point() { 1708 use bone_types::SketchStatus; 1709 1710 let (sketch, outcomes) = apply_script(&Sketch::new(plane()), rectangle_script()); 1711 let entity_ids: Vec<SketchEntityId> = outcomes 1712 .iter() 1713 .map(|o| match *o { 1714 EditOutcome::Entity(id) => id, 1715 _ => panic!("expected entity outcomes"), 1716 }) 1717 .collect(); 1718 let Ok((mut sketch, EditOutcome::Entity(line_id))) = sketch.apply(SketchEdit::AddEntity( 1719 SketchEntity::line(entity_ids[0], entity_ids[1], false), 1720 )) else { 1721 panic!("line should add cleanly"); 1722 }; 1723 let entities = Arc::make_mut(&mut sketch.entities); 1724 assert!(entities.remove(entity_ids[0]).is_some()); 1725 1726 let report = sketch.status(); 1727 assert_eq!(report.status(), SketchStatus::Dangling); 1728 assert!( 1729 report 1730 .offending() 1731 .contains(&bone_types::SketchItemId::Entity(line_id)) 1732 ); 1733 } 1734 1735 #[test] 1736 fn version_is_stable_across_clone_and_bumps_on_edit() { 1737 let sketch = Sketch::new(plane()); 1738 let v_a = sketch.version(); 1739 let v_clone = sketch.clone().version(); 1740 assert_eq!(v_a, v_clone); 1741 1742 let (after, _) = apply_script(&sketch, rectangle_script()); 1743 assert_ne!(after.version(), v_a); 1744 } 1745 1746 mod properties { 1747 use super::*; 1748 use proptest::prelude::*; 1749 1750 #[derive(Copy, Clone, Debug)] 1751 enum Step { 1752 Point(i16, i16), 1753 Parameter(i16), 1754 Line { 1755 ai: u8, 1756 bi: u8, 1757 }, 1758 Circle { 1759 ci: u8, 1760 r: u16, 1761 }, 1762 Horizontal { 1763 li: u8, 1764 }, 1765 LinearDim { 1766 ai: u8, 1767 bi: u8, 1768 v: u16, 1769 driven: bool, 1770 }, 1771 UpdateDim { 1772 di: u8, 1773 v: u16, 1774 }, 1775 Toggle { 1776 ei: u8, 1777 }, 1778 DelEntity { 1779 ei: u8, 1780 }, 1781 DelParam { 1782 pi: u8, 1783 }, 1784 } 1785 1786 fn arb_step() -> impl Strategy<Value = Step> { 1787 prop_oneof![ 1788 (any::<i16>(), any::<i16>()).prop_map(|(x, y)| Step::Point(x, y)), 1789 any::<i16>().prop_map(Step::Parameter), 1790 (any::<u8>(), any::<u8>()).prop_map(|(ai, bi)| Step::Line { ai, bi }), 1791 (any::<u8>(), any::<u16>()).prop_map(|(ci, r)| Step::Circle { ci, r: r % 100 + 1 }), 1792 any::<u8>().prop_map(|li| Step::Horizontal { li }), 1793 (any::<u8>(), any::<u8>(), any::<u16>(), any::<bool>()) 1794 .prop_map(|(ai, bi, v, driven)| Step::LinearDim { ai, bi, v, driven }), 1795 (any::<u8>(), any::<u16>()).prop_map(|(di, v)| Step::UpdateDim { di, v }), 1796 any::<u8>().prop_map(|ei| Step::Toggle { ei }), 1797 any::<u8>().prop_map(|ei| Step::DelEntity { ei }), 1798 any::<u8>().prop_map(|pi| Step::DelParam { pi }), 1799 ] 1800 } 1801 1802 fn entities_of_kind(s: &Sketch, kind: SketchEntityKind) -> Vec<SketchEntityId> { 1803 s.entity_order() 1804 .iter() 1805 .copied() 1806 .filter(|id| s.entities()[*id].kind() == kind) 1807 .collect() 1808 } 1809 1810 fn pick<T: Copy>(xs: &[T], i: u8) -> Option<T> { 1811 if xs.is_empty() { 1812 None 1813 } else { 1814 Some(xs[usize::from(i) % xs.len()]) 1815 } 1816 } 1817 1818 fn pick_two_distinct<T: Copy + Eq>(xs: &[T], ai: u8, bi: u8) -> Option<(T, T)> { 1819 if xs.len() < 2 { 1820 return None; 1821 } 1822 let ai = usize::from(ai) % xs.len(); 1823 let offset = usize::from(bi) % (xs.len() - 1) + 1; 1824 let bi = (ai + offset) % xs.len(); 1825 Some((xs[ai], xs[bi])) 1826 } 1827 1828 fn resolve_step(s: &Sketch, step: Step) -> Option<SketchEdit> { 1829 match step { 1830 Step::Point(x, y) => Some(SketchEdit::AddEntity(SketchEntity::point( 1831 Point2::from_mm(f64::from(x), f64::from(y)), 1832 ))), 1833 Step::Parameter(v) => Some(SketchEdit::AddParameter(SketchParameter::new( 1834 Parameter::new(f64::from(v)), 1835 ))), 1836 Step::Line { ai, bi } => { 1837 let (a, b) = 1838 pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Point), ai, bi)?; 1839 Some(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) 1840 } 1841 Step::Circle { ci, r } => { 1842 let c = pick(&entities_of_kind(s, SketchEntityKind::Point), ci)?; 1843 Some(SketchEdit::AddEntity(SketchEntity::circle( 1844 c, 1845 Length::new::<millimeter>(f64::from(r)), 1846 false, 1847 ))) 1848 } 1849 Step::Horizontal { li } => { 1850 let l = pick(&entities_of_kind(s, SketchEntityKind::Line), li)?; 1851 Some(SketchEdit::AddRelation(SketchRelation::Horizontal(l))) 1852 } 1853 Step::LinearDim { ai, bi, v, driven } => { 1854 let (a, b) = 1855 pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Point), ai, bi)?; 1856 let kind = if driven { 1857 DimensionKind::Driven 1858 } else { 1859 DimensionKind::Driving 1860 }; 1861 Some(SketchEdit::AddDimension(SketchDimension::Linear { 1862 a, 1863 b, 1864 value: Length::new::<millimeter>(f64::from(v)), 1865 kind, 1866 })) 1867 } 1868 Step::UpdateDim { di, v } => { 1869 let id = pick(s.dimension_order(), di)?; 1870 Some(SketchEdit::UpdateDimensionValue { 1871 id, 1872 value: DimensionValue::Length(Length::new::<millimeter>(f64::from(v))), 1873 }) 1874 } 1875 Step::Toggle { ei } => { 1876 let id = pick(s.entity_order(), ei)?; 1877 Some(SketchEdit::SetConstruction { 1878 id, 1879 for_construction: true, 1880 }) 1881 } 1882 Step::DelEntity { ei } => { 1883 let id = pick(s.entity_order(), ei)?; 1884 Some(SketchEdit::DeleteEntity(id)) 1885 } 1886 Step::DelParam { pi } => { 1887 let id = pick(s.parameter_order(), pi)?; 1888 Some(SketchEdit::DeleteParameter(id)) 1889 } 1890 } 1891 } 1892 1893 fn build_script(steps: Vec<Step>) -> Vec<SketchEdit> { 1894 steps 1895 .into_iter() 1896 .fold( 1897 (Sketch::new(plane()), Vec::new()), 1898 |(sk, mut acc), step| match resolve_step(&sk, step) { 1899 Some(edit) => match sk.clone().apply(edit) { 1900 Ok((next, _)) => { 1901 acc.push(edit); 1902 (next, acc) 1903 } 1904 Err(_) => (sk, acc), 1905 }, 1906 None => (sk, acc), 1907 }, 1908 ) 1909 .1 1910 } 1911 1912 proptest! { 1913 #[test] 1914 fn apply_all_is_deterministic(steps in prop::collection::vec(arb_step(), 0..40)) { 1915 let script = build_script(steps); 1916 let Ok((a, _)) = Sketch::new(plane()).apply_all(script.clone()) else { 1917 unreachable!("build_script only emits successfully-applied edits"); 1918 }; 1919 let Ok((b, _)) = Sketch::new(plane()).apply_all(script) else { 1920 unreachable!(); 1921 }; 1922 prop_assert_eq!(a, b); 1923 } 1924 1925 #[test] 1926 fn apply_all_matches_apply_fold(steps in prop::collection::vec(arb_step(), 0..40)) { 1927 let script = build_script(steps); 1928 let Ok((via_all, _)) = Sketch::new(plane()).apply_all(script.clone()) else { 1929 unreachable!(); 1930 }; 1931 let via_fold = script.into_iter().try_fold(Sketch::new(plane()), |acc, e| { 1932 acc.apply(e).map(|(s, _)| s) 1933 }); 1934 let Ok(via_fold) = via_fold else { unreachable!(); }; 1935 prop_assert_eq!(via_all, via_fold); 1936 } 1937 } 1938 } 1939}