Another project
0

Configure Feed

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

feat(app): clickanchor enum, tools module surface, place ops

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

author
Lewis
date (May 10, 2026, 12:10 PM +0300) commit b161edf0 parent 3f86d7f8 change-id wltlyzmw
+684 -5
+21 -5
crates/bone-app/src/sketch_mode.rs
··· 71 71 pub const REDO_ACTION: ActionId = action_id(3); 72 72 73 73 #[derive(Copy, Clone, Debug, PartialEq)] 74 - pub enum Pending { 74 + pub enum ClickAnchor { 75 75 Position(Point2), 76 76 Endpoint(SketchEntityId), 77 + MidpointOf { 78 + line: SketchEntityId, 79 + position: Point2, 80 + }, 81 + } 82 + 83 + #[derive(Copy, Clone, Debug, PartialEq)] 84 + pub enum Pending { 85 + First(ClickAnchor), 86 + Second(ClickAnchor, ClickAnchor), 77 87 } 78 88 79 89 #[derive(Copy, Clone, Debug, Default, PartialEq)] ··· 189 199 190 200 #[cfg(test)] 191 201 mod tests { 192 - use super::{Mode, Pending, Plane, SketchSession, SketchTool}; 202 + use super::{ClickAnchor, Mode, Pending, Plane, SketchSession, SketchTool}; 193 203 use bone_types::{Point2, SketchEntityId, SketchId}; 194 204 195 205 #[test] ··· 210 220 fn arm_tool_clears_pending() { 211 221 let session = SketchSession { 212 222 tool: Some(SketchTool::Line), 213 - pending: Some(Pending::Position(Point2::from_mm(1.0, 2.0))), 223 + pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 224 + 1.0, 2.0, 225 + )))), 214 226 drag: None, 215 227 }; 216 228 let mode = Mode::Sketch { ··· 229 241 fn clear_pending_in_sketch_drops_pending_keeps_tool() { 230 242 let session = SketchSession { 231 243 tool: Some(SketchTool::Line), 232 - pending: Some(Pending::Position(Point2::from_mm(3.0, 4.0))), 244 + pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 245 + 3.0, 4.0, 246 + )))), 233 247 drag: None, 234 248 }; 235 249 let mode = Mode::Sketch { ··· 256 270 sketch_id: SketchId::default(), 257 271 session: SketchSession { 258 272 tool: Some(SketchTool::Line), 259 - pending: Some(Pending::Position(Point2::from_mm(0.0, 0.0))), 273 + pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 274 + 0.0, 0.0, 275 + )))), 260 276 drag: None, 261 277 }, 262 278 }
+167
crates/bone-app/src/tools/mod.rs
··· 1 + use bone_document::{EditOutcome, Sketch, SketchEdit, SketchEntity, SketchRelation}; 2 + use bone_render::SketchPreview; 3 + use bone_types::{Length, Point2, SketchEntityId}; 4 + 5 + use crate::sketch_mode::{ClickAnchor, Pending, SketchTool}; 6 + use crate::snap::{SnapHit, SnapKind}; 7 + 8 + mod geometry; 9 + mod place; 10 + mod preview; 11 + 12 + #[cfg(test)] 13 + mod tests; 14 + 15 + pub(super) type PlaceResult = (Option<Sketch>, Option<Pending>); 16 + 17 + fn add_entity(sketch: Sketch, entity: SketchEntity) -> (Sketch, SketchEntityId) { 18 + let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity(entity)) else { 19 + unreachable!("AddEntity yields Entity outcome on a well-formed sketch"); 20 + }; 21 + (next, id) 22 + } 23 + 24 + pub fn add_point(sketch: Sketch, at: Point2) -> (Sketch, SketchEntityId) { 25 + add_entity(sketch, SketchEntity::point(at)) 26 + } 27 + 28 + pub fn add_line( 29 + sketch: Sketch, 30 + a: SketchEntityId, 31 + b: SketchEntityId, 32 + for_construction: bool, 33 + ) -> (Sketch, SketchEntityId) { 34 + add_entity(sketch, SketchEntity::line(a, b, for_construction)) 35 + } 36 + 37 + pub fn add_circle( 38 + sketch: Sketch, 39 + center: SketchEntityId, 40 + radius: Length, 41 + for_construction: bool, 42 + ) -> (Sketch, SketchEntityId) { 43 + add_entity( 44 + sketch, 45 + SketchEntity::circle(center, radius, for_construction), 46 + ) 47 + } 48 + 49 + pub fn add_arc( 50 + sketch: Sketch, 51 + center: SketchEntityId, 52 + start: SketchEntityId, 53 + end: SketchEntityId, 54 + for_construction: bool, 55 + ) -> (Sketch, SketchEntityId) { 56 + add_entity( 57 + sketch, 58 + SketchEntity::arc(center, start, end, for_construction), 59 + ) 60 + } 61 + 62 + pub fn add_relation(sketch: Sketch, relation: SketchRelation) -> Sketch { 63 + match sketch.clone().apply(SketchEdit::AddRelation(relation)) { 64 + Ok((next, _)) => next, 65 + Err(e) => { 66 + tracing::warn!(error = %e, ?relation, "auto-relation rejected; skipping"); 67 + sketch 68 + } 69 + } 70 + } 71 + 72 + pub fn click_position(sketch: &Sketch, click: ClickAnchor) -> Option<Point2> { 73 + match click { 74 + ClickAnchor::Position(p) | ClickAnchor::MidpointOf { position: p, .. } => Some(p), 75 + ClickAnchor::Endpoint(id) => geometry::endpoint_position(sketch, id), 76 + } 77 + } 78 + 79 + pub(super) fn materialize(sketch: Sketch, click: ClickAnchor) -> (Sketch, SketchEntityId) { 80 + match click { 81 + ClickAnchor::Position(p) => add_point(sketch, p), 82 + ClickAnchor::Endpoint(id) => (sketch, id), 83 + ClickAnchor::MidpointOf { line, position } => { 84 + let (sketch, id) = add_point(sketch, position); 85 + let sketch = add_relation(sketch, SketchRelation::Midpoint { point: id, line }); 86 + (sketch, id) 87 + } 88 + } 89 + } 90 + 91 + pub(super) fn anchor_at(pos: Point2, snap: Option<SnapHit>) -> ClickAnchor { 92 + match snap.map(|s| s.kind) { 93 + Some(SnapKind::Endpoint(id)) => ClickAnchor::Endpoint(id), 94 + Some(SnapKind::Midpoint(line)) => ClickAnchor::MidpointOf { 95 + line, 96 + position: pos, 97 + }, 98 + _ => ClickAnchor::Position(pos), 99 + } 100 + } 101 + 102 + pub(super) fn anchor_from(world: Point2, snap: Option<SnapHit>) -> ClickAnchor { 103 + anchor_at(snap.map_or(world, |s| s.world), snap) 104 + } 105 + 106 + pub(super) fn anchors_collide(a: ClickAnchor, b: ClickAnchor) -> bool { 107 + matches!((a, b), (ClickAnchor::Endpoint(x), ClickAnchor::Endpoint(y)) if x == y) 108 + } 109 + 110 + pub(super) fn snap_world(world: Point2, snap: Option<SnapHit>) -> Point2 { 111 + snap.map_or(world, |s| s.world) 112 + } 113 + 114 + pub fn place( 115 + sketch: Sketch, 116 + tool: SketchTool, 117 + world: Point2, 118 + pending: Option<Pending>, 119 + snap: Option<SnapHit>, 120 + ) -> PlaceResult { 121 + match tool { 122 + SketchTool::Point => place::place_point(sketch, world, snap), 123 + SketchTool::Line => place::place_line(sketch, world, pending, snap), 124 + SketchTool::Circle => place::place_circle(sketch, world, pending, snap), 125 + SketchTool::PerimeterCircle => place::place_perimeter_circle(sketch, world, pending, snap), 126 + SketchTool::CenterpointArc => place::place_centerpoint_arc(sketch, world, pending, snap), 127 + SketchTool::TangentArc => place::place_tangent_arc(sketch, world, pending, snap), 128 + SketchTool::ThreePointArc => place::place_three_point_arc(sketch, world, pending, snap), 129 + SketchTool::CornerRectangle => place::place_corner_rectangle(sketch, world, pending, snap), 130 + SketchTool::CenterRectangle => place::place_center_rectangle(sketch, world, pending, snap), 131 + SketchTool::ThreePointCornerRectangle => { 132 + place::place_three_point_corner_rectangle(sketch, world, pending, snap) 133 + } 134 + SketchTool::ThreePointCenterRectangle => { 135 + place::place_three_point_center_rectangle(sketch, world, pending, snap) 136 + } 137 + SketchTool::Parallelogram => place::place_parallelogram(sketch, world, pending, snap), 138 + SketchTool::SmartDimension => (None, pending), 139 + } 140 + } 141 + 142 + #[must_use] 143 + pub fn preview_anchors_only(sketch: &Sketch, pending: Option<Pending>) -> SketchPreview { 144 + let anchors: Vec<Point2> = match pending { 145 + None => Vec::new(), 146 + Some(Pending::First(a)) => click_position(sketch, a).into_iter().collect(), 147 + Some(Pending::Second(a, b)) => [a, b] 148 + .into_iter() 149 + .filter_map(|c| click_position(sketch, c)) 150 + .collect(), 151 + }; 152 + SketchPreview { 153 + anchors, 154 + ..SketchPreview::empty() 155 + } 156 + } 157 + 158 + #[must_use] 159 + pub fn preview( 160 + sketch: &Sketch, 161 + tool: SketchTool, 162 + cursor: Point2, 163 + pending: Option<Pending>, 164 + snap: Option<SnapHit>, 165 + ) -> SketchPreview { 166 + preview::preview(sketch, tool, cursor, pending, snap) 167 + }
+496
crates/bone-app/src/tools/place.rs
··· 1 + use bone_document::{Sketch, SketchRelation}; 2 + use bone_types::{Length, Point2}; 3 + use uom::si::length::millimeter; 4 + 5 + use crate::sketch_mode::{ClickAnchor, Pending}; 6 + use crate::snap::{SnapHit, SnapKind}; 7 + 8 + use super::geometry::{ 9 + axes_distinct, ccw_through, circumcircle, degenerate_triangle, distance, minor_arc_ccw, 10 + on_circle_endpoint, project_to_circle, tangent_arc_ccw_natural, tangent_arc_center, 11 + tangent_at_point, three_point_center_corners, three_point_corner_extents, 12 + }; 13 + use super::{ 14 + PlaceResult, add_arc, add_circle, add_line, add_point, add_relation, anchor_at, anchor_from, 15 + anchors_collide, click_position, materialize, snap_world, 16 + }; 17 + 18 + pub(super) fn dispatch_two<F>( 19 + pending: Option<Pending>, 20 + world: Point2, 21 + snap: Option<SnapHit>, 22 + commit: F, 23 + ) -> PlaceResult 24 + where 25 + F: FnOnce(ClickAnchor) -> PlaceResult, 26 + { 27 + match pending { 28 + None => (None, Some(Pending::First(anchor_from(world, snap)))), 29 + Some(Pending::First(a)) => commit(a), 30 + Some(Pending::Second(_, _)) => (None, pending), 31 + } 32 + } 33 + 34 + pub(super) fn dispatch_three<F>( 35 + pending: Option<Pending>, 36 + world: Point2, 37 + snap: Option<SnapHit>, 38 + commit: F, 39 + ) -> PlaceResult 40 + where 41 + F: FnOnce(ClickAnchor, ClickAnchor) -> PlaceResult, 42 + { 43 + match pending { 44 + None => (None, Some(Pending::First(anchor_from(world, snap)))), 45 + Some(Pending::First(a)) => (None, Some(Pending::Second(a, anchor_from(world, snap)))), 46 + Some(Pending::Second(a, b)) => commit(a, b), 47 + } 48 + } 49 + 50 + pub(super) fn place_point(sketch: Sketch, world: Point2, snap: Option<SnapHit>) -> PlaceResult { 51 + if matches!(snap.map(|s| s.kind), Some(SnapKind::Endpoint(_))) { 52 + return (None, None); 53 + } 54 + let (next, _) = add_point(sketch, snap_world(world, snap)); 55 + (Some(next), None) 56 + } 57 + 58 + pub(super) fn place_line( 59 + sketch: Sketch, 60 + world: Point2, 61 + pending: Option<Pending>, 62 + snap: Option<SnapHit>, 63 + ) -> PlaceResult { 64 + dispatch_two(pending, world, snap, move |prev| { 65 + commit_line(sketch, world, prev, snap) 66 + }) 67 + } 68 + 69 + fn commit_line( 70 + sketch: Sketch, 71 + world: Point2, 72 + prev: ClickAnchor, 73 + snap: Option<SnapHit>, 74 + ) -> PlaceResult { 75 + if click_position(&sketch, prev).is_none() { 76 + return (None, None); 77 + } 78 + let end_anchor = anchor_from(world, snap); 79 + if anchors_collide(prev, end_anchor) { 80 + return (None, Some(Pending::First(prev))); 81 + } 82 + let (sketch, prev_id) = materialize(sketch, prev); 83 + let (sketch, end_id) = materialize(sketch, end_anchor); 84 + let (sketch, line_id) = add_line(sketch, prev_id, end_id, false); 85 + let sketch = match snap.map(|s| s.kind) { 86 + Some(SnapKind::Tangent(curve)) => { 87 + add_relation(sketch, SketchRelation::Tangent(line_id, curve)) 88 + } 89 + Some(SnapKind::Horizontal) => add_relation(sketch, SketchRelation::Horizontal(line_id)), 90 + Some(SnapKind::Vertical) => add_relation(sketch, SketchRelation::Vertical(line_id)), 91 + Some(SnapKind::Midpoint(_) | SnapKind::Endpoint(_)) | None => sketch, 92 + }; 93 + ( 94 + Some(sketch), 95 + Some(Pending::First(ClickAnchor::Endpoint(end_id))), 96 + ) 97 + } 98 + 99 + pub(super) fn place_circle( 100 + sketch: Sketch, 101 + world: Point2, 102 + pending: Option<Pending>, 103 + snap: Option<SnapHit>, 104 + ) -> PlaceResult { 105 + dispatch_two(pending, world, snap, move |center_anchor| { 106 + let Some(center_pos) = click_position(&sketch, center_anchor) else { 107 + return (None, None); 108 + }; 109 + let radius_mm = distance(center_pos, snap_world(world, snap)); 110 + if !(radius_mm.is_finite() && radius_mm > 0.0) { 111 + return (None, Some(Pending::First(center_anchor))); 112 + } 113 + let (sketch, center_id) = materialize(sketch, center_anchor); 114 + let (sketch, _) = add_circle( 115 + sketch, 116 + center_id, 117 + Length::new::<millimeter>(radius_mm), 118 + false, 119 + ); 120 + (Some(sketch), None) 121 + }) 122 + } 123 + 124 + pub(super) fn place_perimeter_circle( 125 + sketch: Sketch, 126 + world: Point2, 127 + pending: Option<Pending>, 128 + snap: Option<SnapHit>, 129 + ) -> PlaceResult { 130 + dispatch_three(pending, world, snap, move |a1, a2| { 131 + let Some(p1) = click_position(&sketch, a1) else { 132 + return (None, None); 133 + }; 134 + let Some(p2) = click_position(&sketch, a2) else { 135 + return (None, None); 136 + }; 137 + let Some((center, radius_mm)) = circumcircle(p1, p2, snap_world(world, snap)) else { 138 + return (None, Some(Pending::Second(a1, a2))); 139 + }; 140 + let (sketch, center_id) = add_point(sketch, center); 141 + let (sketch, _) = add_circle( 142 + sketch, 143 + center_id, 144 + Length::new::<millimeter>(radius_mm), 145 + false, 146 + ); 147 + (Some(sketch), None) 148 + }) 149 + } 150 + 151 + pub(super) fn place_centerpoint_arc( 152 + sketch: Sketch, 153 + world: Point2, 154 + pending: Option<Pending>, 155 + snap: Option<SnapHit>, 156 + ) -> PlaceResult { 157 + dispatch_three(pending, world, snap, move |center, start| { 158 + let Some(center_pos) = click_position(&sketch, center) else { 159 + return (None, None); 160 + }; 161 + let Some(start_pos) = click_position(&sketch, start) else { 162 + return (None, None); 163 + }; 164 + let radius_mm = distance(center_pos, start_pos); 165 + if !(radius_mm.is_finite() && radius_mm > 0.0) { 166 + return (None, Some(Pending::Second(center, start))); 167 + } 168 + let end_pos = project_to_circle(center_pos, radius_mm, snap_world(world, snap)); 169 + let end_anchor = on_circle_endpoint(snap, center_pos, radius_mm) 170 + .map_or(ClickAnchor::Position(end_pos), ClickAnchor::Endpoint); 171 + if anchors_collide(center, start) 172 + || anchors_collide(center, end_anchor) 173 + || anchors_collide(start, end_anchor) 174 + || distance(start_pos, end_pos) < 1e-9 175 + { 176 + return (None, Some(Pending::Second(center, start))); 177 + } 178 + let (sketch, center_id) = materialize(sketch, center); 179 + let (sketch, start_id) = materialize(sketch, start); 180 + let (sketch, end_id) = materialize(sketch, end_anchor); 181 + let (start_arc, end_arc) = if minor_arc_ccw(center_pos, start_pos, end_pos) { 182 + (start_id, end_id) 183 + } else { 184 + (end_id, start_id) 185 + }; 186 + let (sketch, _) = add_arc(sketch, center_id, start_arc, end_arc, false); 187 + (Some(sketch), None) 188 + }) 189 + } 190 + 191 + pub(super) fn place_tangent_arc( 192 + sketch: Sketch, 193 + world: Point2, 194 + pending: Option<Pending>, 195 + snap: Option<SnapHit>, 196 + ) -> PlaceResult { 197 + match pending { 198 + None => match snap.map(|s| s.kind) { 199 + Some(SnapKind::Endpoint(id)) if tangent_at_point(&sketch, id).is_some() => { 200 + (None, Some(Pending::First(ClickAnchor::Endpoint(id)))) 201 + } 202 + _ => (None, None), 203 + }, 204 + Some(Pending::First(start)) => commit_tangent_arc(sketch, world, start, snap), 205 + Some(Pending::Second(_, _)) => (None, pending), 206 + } 207 + } 208 + 209 + fn commit_tangent_arc( 210 + sketch: Sketch, 211 + world: Point2, 212 + start: ClickAnchor, 213 + snap: Option<SnapHit>, 214 + ) -> PlaceResult { 215 + let ClickAnchor::Endpoint(start_id) = start else { 216 + return (None, None); 217 + }; 218 + let Some(start_pos) = click_position(&sketch, start) else { 219 + return (None, None); 220 + }; 221 + let Some((host, tangent)) = tangent_at_point(&sketch, start_id) else { 222 + return (None, Some(Pending::First(start))); 223 + }; 224 + let end_pos = snap_world(world, snap); 225 + let Some(center_pos) = tangent_arc_center(start_pos, end_pos, tangent) else { 226 + return (None, Some(Pending::First(start))); 227 + }; 228 + let end_anchor = anchor_at(end_pos, snap); 229 + if anchors_collide(start, end_anchor) { 230 + return (None, Some(Pending::First(start))); 231 + } 232 + let (sketch, end_id) = materialize(sketch, end_anchor); 233 + let (sketch, center_id) = add_point(sketch, center_pos); 234 + let (start_arc, end_arc) = if tangent_arc_ccw_natural(start_pos, center_pos, tangent) { 235 + (start_id, end_id) 236 + } else { 237 + (end_id, start_id) 238 + }; 239 + let (sketch, arc_id) = add_arc(sketch, center_id, start_arc, end_arc, false); 240 + let sketch = add_relation(sketch, SketchRelation::Tangent(arc_id, host)); 241 + (Some(sketch), None) 242 + } 243 + 244 + pub(super) fn place_three_point_arc( 245 + sketch: Sketch, 246 + world: Point2, 247 + pending: Option<Pending>, 248 + snap: Option<SnapHit>, 249 + ) -> PlaceResult { 250 + dispatch_three(pending, world, snap, move |start, end| { 251 + let Some(start_pos) = click_position(&sketch, start) else { 252 + return (None, None); 253 + }; 254 + let Some(end_pos) = click_position(&sketch, end) else { 255 + return (None, None); 256 + }; 257 + let mid_pos = snap_world(world, snap); 258 + let Some((center_pos, _)) = circumcircle(start_pos, end_pos, mid_pos) else { 259 + return (None, Some(Pending::Second(start, end))); 260 + }; 261 + let (start, end) = if ccw_through(center_pos, start_pos, end_pos, mid_pos) { 262 + (start, end) 263 + } else { 264 + (end, start) 265 + }; 266 + let (sketch, center_id) = add_point(sketch, center_pos); 267 + let (sketch, start_id) = materialize(sketch, start); 268 + let (sketch, end_id) = materialize(sketch, end); 269 + let (sketch, _) = add_arc(sketch, center_id, start_id, end_id, false); 270 + (Some(sketch), None) 271 + }) 272 + } 273 + 274 + pub(super) fn place_corner_rectangle( 275 + sketch: Sketch, 276 + world: Point2, 277 + pending: Option<Pending>, 278 + snap: Option<SnapHit>, 279 + ) -> PlaceResult { 280 + dispatch_two(pending, world, snap, move |corner| { 281 + let Some(corner_pos) = click_position(&sketch, corner) else { 282 + return (None, None); 283 + }; 284 + let opposite = anchor_from(world, snap); 285 + let Some(opposite_pos) = click_position(&sketch, opposite) else { 286 + return (None, Some(Pending::First(corner))); 287 + }; 288 + let (cx, cy) = corner_pos.coords_mm(); 289 + let (ox, oy) = opposite_pos.coords_mm(); 290 + if !axes_distinct((cx, cy), (ox, oy)) || anchors_collide(corner, opposite) { 291 + return (None, Some(Pending::First(corner))); 292 + } 293 + let (sketch, p1_id) = materialize(sketch, corner); 294 + let (sketch, p2_id) = add_point(sketch, Point2::from_mm(ox, cy)); 295 + let (sketch, p3_id) = materialize(sketch, opposite); 296 + let (sketch, p4_id) = add_point(sketch, Point2::from_mm(cx, oy)); 297 + let (sketch, bottom) = add_line(sketch, p1_id, p2_id, false); 298 + let (sketch, right) = add_line(sketch, p2_id, p3_id, false); 299 + let (sketch, top) = add_line(sketch, p3_id, p4_id, false); 300 + let (sketch, left) = add_line(sketch, p4_id, p1_id, false); 301 + let sketch = [ 302 + SketchRelation::Horizontal(bottom), 303 + SketchRelation::Horizontal(top), 304 + SketchRelation::Vertical(right), 305 + SketchRelation::Vertical(left), 306 + ] 307 + .into_iter() 308 + .fold(sketch, add_relation); 309 + (Some(sketch), None) 310 + }) 311 + } 312 + 313 + pub(super) fn place_center_rectangle( 314 + sketch: Sketch, 315 + world: Point2, 316 + pending: Option<Pending>, 317 + snap: Option<SnapHit>, 318 + ) -> PlaceResult { 319 + dispatch_two(pending, world, snap, move |center| { 320 + let Some(center_pos) = click_position(&sketch, center) else { 321 + return (None, None); 322 + }; 323 + let corner = anchor_from(world, snap); 324 + let Some(corner_pos) = click_position(&sketch, corner) else { 325 + return (None, Some(Pending::First(center))); 326 + }; 327 + let (cx, cy) = center_pos.coords_mm(); 328 + let (ox, oy) = corner_pos.coords_mm(); 329 + if !axes_distinct((cx, cy), (ox, oy)) || anchors_collide(center, corner) { 330 + return (None, Some(Pending::First(center))); 331 + } 332 + let dx = ox - cx; 333 + let dy = oy - cy; 334 + let (sketch, center_id) = materialize(sketch, center); 335 + let (sketch, p1_id) = materialize(sketch, corner); 336 + let (sketch, p2_id) = add_point(sketch, Point2::from_mm(cx - dx, cy + dy)); 337 + let (sketch, p3_id) = add_point(sketch, Point2::from_mm(cx - dx, cy - dy)); 338 + let (sketch, p4_id) = add_point(sketch, Point2::from_mm(cx + dx, cy - dy)); 339 + let (sketch, top) = add_line(sketch, p1_id, p2_id, false); 340 + let (sketch, left) = add_line(sketch, p2_id, p3_id, false); 341 + let (sketch, bottom) = add_line(sketch, p3_id, p4_id, false); 342 + let (sketch, right) = add_line(sketch, p4_id, p1_id, false); 343 + let (sketch, diag1) = add_line(sketch, p1_id, p3_id, true); 344 + let (sketch, diag2) = add_line(sketch, p2_id, p4_id, true); 345 + let sketch = [ 346 + SketchRelation::Horizontal(top), 347 + SketchRelation::Horizontal(bottom), 348 + SketchRelation::Vertical(left), 349 + SketchRelation::Vertical(right), 350 + SketchRelation::Midpoint { 351 + point: center_id, 352 + line: diag1, 353 + }, 354 + SketchRelation::Midpoint { 355 + point: center_id, 356 + line: diag2, 357 + }, 358 + ] 359 + .into_iter() 360 + .fold(sketch, add_relation); 361 + (Some(sketch), None) 362 + }) 363 + } 364 + 365 + pub(super) fn place_three_point_corner_rectangle( 366 + sketch: Sketch, 367 + world: Point2, 368 + pending: Option<Pending>, 369 + snap: Option<SnapHit>, 370 + ) -> PlaceResult { 371 + dispatch_three(pending, world, snap, move |c1, c2| { 372 + let Some(p1) = click_position(&sketch, c1) else { 373 + return (None, None); 374 + }; 375 + let Some(p2) = click_position(&sketch, c2) else { 376 + return (None, None); 377 + }; 378 + let Some((p3_pos, p4_pos)) = three_point_corner_extents(p1, p2, snap_world(world, snap)) 379 + else { 380 + return (None, Some(Pending::Second(c1, c2))); 381 + }; 382 + let (sketch, p1_id) = materialize(sketch, c1); 383 + let (sketch, p2_id) = materialize(sketch, c2); 384 + let (sketch, p3_id) = add_point(sketch, p3_pos); 385 + let (sketch, p4_id) = add_point(sketch, p4_pos); 386 + let (sketch, base) = add_line(sketch, p1_id, p2_id, false); 387 + let (sketch, side1) = add_line(sketch, p2_id, p3_id, false); 388 + let (sketch, top) = add_line(sketch, p3_id, p4_id, false); 389 + let (sketch, side2) = add_line(sketch, p4_id, p1_id, false); 390 + let sketch = [ 391 + SketchRelation::Parallel(base, top), 392 + SketchRelation::Parallel(side1, side2), 393 + SketchRelation::Perpendicular(base, side1), 394 + ] 395 + .into_iter() 396 + .fold(sketch, add_relation); 397 + (Some(sketch), None) 398 + }) 399 + } 400 + 401 + pub(super) fn place_three_point_center_rectangle( 402 + sketch: Sketch, 403 + world: Point2, 404 + pending: Option<Pending>, 405 + snap: Option<SnapHit>, 406 + ) -> PlaceResult { 407 + dispatch_three(pending, world, snap, move |center, mid| { 408 + let Some(center_pos) = click_position(&sketch, center) else { 409 + return (None, None); 410 + }; 411 + let Some(mid_pos) = click_position(&sketch, mid) else { 412 + return (None, None); 413 + }; 414 + let Some((p1_pos, p2_pos, p3_pos, p4_pos)) = 415 + three_point_center_corners(center_pos, mid_pos, snap_world(world, snap)) 416 + else { 417 + return (None, Some(Pending::Second(center, mid))); 418 + }; 419 + let (sketch, center_id) = materialize(sketch, center); 420 + let (sketch, mid_id) = materialize(sketch, mid); 421 + let (sketch, p1_id) = add_point(sketch, p1_pos); 422 + let (sketch, p2_id) = add_point(sketch, p2_pos); 423 + let (sketch, p3_id) = add_point(sketch, p3_pos); 424 + let (sketch, p4_id) = add_point(sketch, p4_pos); 425 + let (sketch, edge1) = add_line(sketch, p1_id, p2_id, false); 426 + let (sketch, edge2) = add_line(sketch, p2_id, p3_id, false); 427 + let (sketch, _edge3) = add_line(sketch, p3_id, p4_id, false); 428 + let (sketch, edge4) = add_line(sketch, p4_id, p1_id, false); 429 + let (sketch, diag1) = add_line(sketch, p1_id, p3_id, true); 430 + let (sketch, diag2) = add_line(sketch, p2_id, p4_id, true); 431 + let sketch = [ 432 + SketchRelation::Perpendicular(edge1, edge2), 433 + SketchRelation::Midpoint { 434 + point: center_id, 435 + line: diag1, 436 + }, 437 + SketchRelation::Midpoint { 438 + point: center_id, 439 + line: diag2, 440 + }, 441 + SketchRelation::Midpoint { 442 + point: mid_id, 443 + line: edge4, 444 + }, 445 + ] 446 + .into_iter() 447 + .fold(sketch, add_relation); 448 + (Some(sketch), None) 449 + }) 450 + } 451 + 452 + pub(super) fn place_parallelogram( 453 + sketch: Sketch, 454 + world: Point2, 455 + pending: Option<Pending>, 456 + snap: Option<SnapHit>, 457 + ) -> PlaceResult { 458 + dispatch_three(pending, world, snap, move |c1, c2| { 459 + let Some(p1) = click_position(&sketch, c1) else { 460 + return (None, None); 461 + }; 462 + let Some(p2) = click_position(&sketch, c2) else { 463 + return (None, None); 464 + }; 465 + let c3 = anchor_from(world, snap); 466 + let Some(p3) = click_position(&sketch, c3) else { 467 + return (None, Some(Pending::Second(c1, c2))); 468 + }; 469 + if degenerate_triangle(p1, p2, p3) 470 + || anchors_collide(c1, c2) 471 + || anchors_collide(c1, c3) 472 + || anchors_collide(c2, c3) 473 + { 474 + return (None, Some(Pending::Second(c1, c2))); 475 + } 476 + let (p1x, p1y) = p1.coords_mm(); 477 + let (p2x, p2y) = p2.coords_mm(); 478 + let (p3x, p3y) = p3.coords_mm(); 479 + let p4_pos = Point2::from_mm(p1x + (p3x - p2x), p1y + (p3y - p2y)); 480 + let (sketch, p1_id) = materialize(sketch, c1); 481 + let (sketch, p2_id) = materialize(sketch, c2); 482 + let (sketch, p3_id) = materialize(sketch, c3); 483 + let (sketch, p4_id) = add_point(sketch, p4_pos); 484 + let (sketch, edge_a) = add_line(sketch, p1_id, p2_id, false); 485 + let (sketch, edge_b) = add_line(sketch, p2_id, p3_id, false); 486 + let (sketch, edge_c) = add_line(sketch, p3_id, p4_id, false); 487 + let (sketch, edge_d) = add_line(sketch, p4_id, p1_id, false); 488 + let sketch = [ 489 + SketchRelation::Parallel(edge_a, edge_c), 490 + SketchRelation::Parallel(edge_b, edge_d), 491 + ] 492 + .into_iter() 493 + .fold(sketch, add_relation); 494 + (Some(sketch), None) 495 + }) 496 + }