Another project
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}