Another project
0

Configure Feed

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

feat(document,render): more drag stuff, dim with_kind/measure, driven parens

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

author
Lewis
date (May 11, 2026, 11:54 PM +0300) commit aaed4759 parent 1ff40b37 change-id zpstspop
+267 -29
+1 -1
crates/bone-document/src/lib.rs
··· 17 17 ArcData, CircleData, DimensionKind, DimensionRefs, DimensionValue, DimensionValueMismatch, 18 18 EditOutcome, EntityRefs, LineData, PointData, RelationRefs, Sketch, SketchDimension, 19 19 SketchDofReport, SketchEdit, SketchEditError, SketchEntity, SketchEntityKind, SketchParameter, 20 - SketchRelation, 20 + SketchRelation, SolverError, 21 21 }; 22 22 pub use undo::UndoStack; 23 23
+18
crates/bone-document/src/sketch/dimension.rs
··· 79 79 } 80 80 } 81 81 82 + #[must_use] 83 + pub const fn with_kind(self, kind: DimensionKind) -> Self { 84 + match self { 85 + Self::Linear { a, b, value, .. } => Self::Linear { a, b, value, kind }, 86 + Self::Radius { target, value, .. } => Self::Radius { 87 + target, 88 + value, 89 + kind, 90 + }, 91 + Self::Diameter { target, value, .. } => Self::Diameter { 92 + target, 93 + value, 94 + kind, 95 + }, 96 + Self::Angular { a, b, value, .. } => Self::Angular { a, b, value, kind }, 97 + } 98 + } 99 + 82 100 pub fn with_value(self, value: DimensionValue) -> Result<Self, DimensionValueMismatch> { 83 101 match (self, value) { 84 102 (Self::Linear { a, b, kind, .. }, DimensionValue::Length(value)) => {
+1
crates/bone-document/src/sketch/mod.rs
··· 15 15 pub mod relation; 16 16 pub mod solve; 17 17 18 + pub use bone_solver::SolverError; 18 19 pub use dimension::{ 19 20 DimensionKind, DimensionRefs, DimensionValue, DimensionValueMismatch, SketchDimension, 20 21 };
+242 -27
crates/bone-document/src/sketch/solve.rs
··· 247 247 target: Point2, 248 248 budget: BudgetCeiling, 249 249 ) -> Result<Sketch, DragError> { 250 + self.solve_with_drag_pins(&[(dragged, target)], budget) 251 + } 252 + 253 + pub fn solve_with_drag_pins( 254 + &self, 255 + pins: &[(SketchEntityId, Point2)], 256 + budget: BudgetCeiling, 257 + ) -> Result<Sketch, DragError> { 250 258 let started = Instant::now(); 251 - let entity = self 252 - .entities() 253 - .get(dragged) 259 + pins.iter().try_for_each(|(id, _)| { 260 + let entity = self 261 + .entities() 262 + .get(*id) 263 + .copied() 264 + .ok_or(DragError::NotFound(*id))?; 265 + if matches!(entity, SketchEntity::Point(_)) { 266 + Ok(()) 267 + } else { 268 + Err(DragError::NotAPoint(*id)) 269 + } 270 + })?; 271 + let deduped: Vec<(SketchEntityId, Point2)> = pins 272 + .iter() 254 273 .copied() 255 - .ok_or(DragError::NotFound(dragged))?; 256 - if !matches!(entity, SketchEntity::Point(_)) { 257 - return Err(DragError::NotAPoint(dragged)); 258 - } 259 - let warm = self 260 - .clone() 261 - .with_point_positions(&HashMap::from([(dragged, target)])); 274 + .enumerate() 275 + .filter(|(i, (id, _))| pins[..*i].iter().all(|(prior, _)| prior != id)) 276 + .map(|(_, pair)| pair) 277 + .collect(); 278 + let updates: HashMap<SketchEntityId, Point2> = deduped.iter().copied().collect(); 279 + let warm = self.clone().with_point_positions(&updates); 262 280 let (system, mapping) = warm.lower(); 263 - let Some(handle) = mapping.point(dragged) else { 264 - unreachable!("Point entity must lower to a PointHandle in the mapping") 265 - }; 266 - let (tx, ty) = target.coords_mm(); 267 - let system = system.with_extra_residuals(vec![ 268 - Residual::Pin { 269 - param: handle.x, 270 - target: tx, 271 - }, 272 - Residual::Pin { 273 - param: handle.y, 274 - target: ty, 275 - }, 276 - ]); 281 + let extra: Vec<Residual> = deduped 282 + .iter() 283 + .flat_map(|(id, target)| { 284 + let Some(handle) = mapping.point(*id) else { 285 + unreachable!("Point entity must lower to a PointHandle in the mapping") 286 + }; 287 + let (tx, ty) = target.coords_mm(); 288 + [ 289 + Residual::Pin { 290 + param: handle.x, 291 + target: tx, 292 + }, 293 + Residual::Pin { 294 + param: handle.y, 295 + target: ty, 296 + }, 297 + ] 298 + }) 299 + .collect(); 300 + let system = system.with_extra_residuals(extra); 277 301 let decomposition = decompose(&system); 278 302 let remaining = BudgetCeiling::new(budget.duration().saturating_sub(started.elapsed())); 279 303 let cfg = NewtonConfig { ··· 293 317 Err(_) => analyze_dof(&system, DofConfig::DEFAULT), 294 318 }; 295 319 SketchDofReport::from_solver_report(&report, &mapping) 320 + } 321 + 322 + #[must_use] 323 + pub fn measure(&self, dim: SketchDimension) -> Option<DimensionValue> { 324 + measure_dimension(self, dim) 296 325 } 297 326 298 327 #[must_use] ··· 745 774 let lb = line_endpoints(sketch, b)?; 746 775 let (d1x, d1y) = (la.1.0 - la.0.0, la.1.1 - la.0.1); 747 776 let (d2x, d2y) = (lb.1.0 - lb.0.0, lb.1.1 - lb.0.1); 748 - let cross = d1x * d2y - d1y * d2x; 749 - let dot = d1x * d2x + d1y * d2y; 750 - Some(cross.atan2(dot)) 777 + let mag = ((d1x * d1x + d1y * d1y) * (d2x * d2x + d2y * d2y)).sqrt(); 778 + if mag <= 0.0 { 779 + return None; 780 + } 781 + let cos = (d1x * d2x + d1y * d2y) / mag; 782 + Some(cos.abs().clamp(0.0, 1.0).acos()) 751 783 } 752 784 753 785 fn line_endpoints(sketch: &Sketch, id: SketchEntityId) -> Option<((f64, f64), (f64, f64))> { ··· 1288 1320 } 1289 1321 1290 1322 #[test] 1323 + fn rectangle_with_conflicting_parallel_edge_dims_reports_over_defined() { 1324 + let (rect, corners) = rectangle_sketch(); 1325 + let [corner_0, corner_1, corner_2, corner_3] = corners; 1326 + let (with_edges, out) = apply( 1327 + &rect, 1328 + vec![ 1329 + SketchEdit::AddEntity(SketchEntity::line(corner_0, corner_1, false)), 1330 + SketchEdit::AddEntity(SketchEntity::line(corner_1, corner_2, false)), 1331 + SketchEdit::AddEntity(SketchEntity::line(corner_2, corner_3, false)), 1332 + SketchEdit::AddEntity(SketchEntity::line(corner_3, corner_0, false)), 1333 + ], 1334 + ); 1335 + let [bottom, right, top, left] = [0_usize, 1, 2, 3].map(|i| entity_id(&out[i])); 1336 + let (sketch, _) = apply( 1337 + &with_edges, 1338 + vec![ 1339 + SketchEdit::AddRelation(SketchRelation::Horizontal(bottom)), 1340 + SketchEdit::AddRelation(SketchRelation::Horizontal(top)), 1341 + SketchEdit::AddRelation(SketchRelation::Vertical(right)), 1342 + SketchEdit::AddRelation(SketchRelation::Vertical(left)), 1343 + SketchEdit::AddRelation(SketchRelation::Fix(corner_0)), 1344 + SketchEdit::AddDimension(SketchDimension::Linear { 1345 + a: corner_0, 1346 + b: corner_1, 1347 + value: mm_val(10.0), 1348 + kind: DimensionKind::Driving, 1349 + }), 1350 + SketchEdit::AddDimension(SketchDimension::Linear { 1351 + a: corner_2, 1352 + b: corner_3, 1353 + value: mm_val(8.0), 1354 + kind: DimensionKind::Driving, 1355 + }), 1356 + ], 1357 + ); 1358 + let Err(err) = sketch.solve() else { 1359 + panic!("rectangle horizontals plus two unequal parallel-edge dims must conflict"); 1360 + }; 1361 + assert!( 1362 + matches!(err, SolverError::OverDefined { .. }), 1363 + "expected OverDefined, got {err:?}", 1364 + ); 1365 + } 1366 + 1367 + #[test] 1368 + fn measure_linear_returns_geometric_distance() { 1369 + let base = Sketch::new(xy_plane()); 1370 + let (sketch, outs) = apply( 1371 + &base, 1372 + vec![ 1373 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))), 1374 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(3.0, 4.0))), 1375 + ], 1376 + ); 1377 + let [a, b] = [0_usize, 1].map(|i| entity_id(&outs[i])); 1378 + let Some(value) = sketch.measure(SketchDimension::Linear { 1379 + a, 1380 + b, 1381 + value: mm_val(0.0), 1382 + kind: DimensionKind::Driving, 1383 + }) else { 1384 + panic!("measure linear yielded None"); 1385 + }; 1386 + let DimensionValue::Length(length) = value else { 1387 + panic!("linear measure must yield Length"); 1388 + }; 1389 + assert!((length.get::<millimeter>() - 5.0).abs() < 1e-9); 1390 + } 1391 + 1392 + #[test] 1291 1393 fn redundant_dimension_does_not_block_solve() { 1292 1394 let base = Sketch::new(xy_plane()); 1293 1395 let (with_points, outs) = apply( ··· 1637 1739 "Fix on a holds: ({ax}, {ay})", 1638 1740 ); 1639 1741 let _ = a; 1742 + } 1743 + 1744 + #[test] 1745 + fn solve_with_drag_pins_translates_line_endpoints() { 1746 + let base = Sketch::new(xy_plane()); 1747 + let (with_points, outs) = apply( 1748 + &base, 1749 + vec![ 1750 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))), 1751 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(4.0, 0.0))), 1752 + ], 1753 + ); 1754 + let [a, b] = [0_usize, 1].map(|i| entity_id(&outs[i])); 1755 + let (sketch, _line_outs) = apply( 1756 + &with_points, 1757 + vec![SketchEdit::AddEntity(SketchEntity::line(a, b, false))], 1758 + ); 1759 + let pins = [ 1760 + (a, Point2::from_mm(3.0, 2.0)), 1761 + (b, Point2::from_mm(7.0, 2.0)), 1762 + ]; 1763 + let Ok(dragged) = sketch.solve_with_drag_pins(&pins, generous_budget()) else { 1764 + panic!("translating both endpoints by a constant delta must converge"); 1765 + }; 1766 + let coords = point_coords(&dragged); 1767 + let (ax, ay) = coords[0]; 1768 + let (bx, by) = coords[1]; 1769 + assert!( 1770 + (ax - 3.0).abs() < 1e-9 && (ay - 2.0).abs() < 1e-9, 1771 + "endpoint a lands at pin: ({ax}, {ay})", 1772 + ); 1773 + assert!( 1774 + (bx - 7.0).abs() < 1e-9 && (by - 2.0).abs() < 1e-9, 1775 + "endpoint b lands at pin: ({bx}, {by})", 1776 + ); 1777 + } 1778 + 1779 + #[test] 1780 + fn solve_with_drag_pins_dedupes_repeated_id() { 1781 + let base = Sketch::new(xy_plane()); 1782 + let (sketch, outs) = apply( 1783 + &base, 1784 + vec![SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm( 1785 + 0.0, 0.0, 1786 + )))], 1787 + ); 1788 + let p = entity_id(&outs[0]); 1789 + let target = Point2::from_mm(2.0, 3.0); 1790 + let pins = [(p, target), (p, target)]; 1791 + let Ok(dragged) = sketch.solve_with_drag_pins(&pins, generous_budget()) else { 1792 + panic!("duplicate pins for the same point must dedupe and converge"); 1793 + }; 1794 + let (x, y) = point_coords(&dragged)[0]; 1795 + assert!( 1796 + (x - 2.0).abs() < 1e-9 && (y - 3.0).abs() < 1e-9, 1797 + "deduped pin lands on target: ({x}, {y})", 1798 + ); 1799 + } 1800 + 1801 + #[test] 1802 + fn measure_angle_returns_acute_for_obtuse_lines() { 1803 + let base = Sketch::new(xy_plane()); 1804 + let (with_points, outs) = apply( 1805 + &base, 1806 + vec![ 1807 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))), 1808 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(1.0, 0.0))), 1809 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))), 1810 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(-1.0, 0.0))), 1811 + ], 1812 + ); 1813 + let [p0, p1, p2, p3] = [0_usize, 1, 2, 3].map(|i| entity_id(&outs[i])); 1814 + let (sketch, line_outs) = apply( 1815 + &with_points, 1816 + vec![ 1817 + SketchEdit::AddEntity(SketchEntity::line(p0, p1, false)), 1818 + SketchEdit::AddEntity(SketchEntity::line(p2, p3, false)), 1819 + ], 1820 + ); 1821 + let [l1, l2] = [0_usize, 1].map(|i| entity_id(&line_outs[i])); 1822 + let Some(value) = sketch.measure(SketchDimension::Angular { 1823 + a: l1, 1824 + b: l2, 1825 + value: Angle::new::<radian>(0.0), 1826 + kind: DimensionKind::Driving, 1827 + }) else { 1828 + panic!("angular measure yielded None"); 1829 + }; 1830 + let DimensionValue::Angle(angle) = value else { 1831 + panic!("angular measure must yield Angle"); 1832 + }; 1833 + assert!(angle.get::<radian>().abs() < 1e-9); 1834 + } 1835 + 1836 + #[test] 1837 + fn solve_with_drag_pins_rejects_non_point_target() { 1838 + let base = Sketch::new(xy_plane()); 1839 + let (with_points, outs) = apply( 1840 + &base, 1841 + vec![ 1842 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))), 1843 + SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(1.0, 0.0))), 1844 + ], 1845 + ); 1846 + let [a, b] = [0_usize, 1].map(|i| entity_id(&outs[i])); 1847 + let (sketch, line_outs) = apply( 1848 + &with_points, 1849 + vec![SketchEdit::AddEntity(SketchEntity::line(a, b, false))], 1850 + ); 1851 + let line = entity_id(&line_outs[0]); 1852 + let result = 1853 + sketch.solve_with_drag_pins(&[(line, Point2::from_mm(0.0, 0.0))], generous_budget()); 1854 + assert!(matches!(result, Err(DragError::NotAPoint(id)) if id == line)); 1640 1855 } 1641 1856 1642 1857 #[test]
+5 -1
crates/bone-render/src/scene.rs
··· 559 559 } 560 560 561 561 fn format_dimension(dim: SketchDimension) -> String { 562 - match (dim, dim.value()) { 562 + let body = match (dim, dim.value()) { 563 563 (SketchDimension::Linear { .. }, DimensionValue::Length(len)) => { 564 564 format!("{:.2} mm", len.get::<millimeter>()) 565 565 } ··· 575 575 _ => unreachable!( 576 576 "SketchDimension::value pins Length for Linear/Radius/Diameter and Angle for Angular" 577 577 ), 578 + }; 579 + match dim.kind() { 580 + bone_document::DimensionKind::Driving => body, 581 + bone_document::DimensionKind::Driven => format!("({body})"), 578 582 } 579 583 } 580 584