1use geop_core_editor::{
12 edit_form::{Edit, EditForm, FormState},
13 editable::{Context, OperationGroup, OperationInfo, OperationTier},
14 editing::{CanvasEvent, Unit},
15 part_operation,
16 part_operation::PartOperation,
17};
18use geop_core_geometry::shape::Plane;
19use geop_core_math::{
20 geop_error::{GeopError, GeopResult, WithContext},
21 primitives::{CoordinateSystem, Datum, DatumKind},
22 scalars::Scalar,
23 vector::Vector3,
24 with_context,
25};
26use geop_core_program::{numeric::NumericInput, part::StepContext, reader::Reader};
27use serde::{Deserialize, Serialize};
28
29use geop_ops_model::datums::{Datums, datums};
30use geop_ops_model::frame_along;
31
32use crate::editor;
33use geop_ops_model::selection::{Role, Selected, Shape};
34
35#[derive(Clone, Copy, Debug, PartialEq)]
37pub enum ParamKind {
38 Number {
42 default: f64,
43 min: f64,
44 max: f64,
45 unit: Unit,
46 },
47 Bool {
48 default: bool,
49 },
50}
51
52#[derive(Clone, Copy, Debug, PartialEq)]
54pub struct Param {
55 pub name: &'static str,
56 pub doc: &'static str,
57 pub kind: ParamKind,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq)]
64pub struct ConstructionSchema {
65 pub method: &'static str,
67 pub label: &'static str,
68 pub doc: &'static str,
69 pub result: DatumKind,
70 pub inputs: &'static [Role],
71 pub params: &'static [Param],
72}
73
74macro_rules! constructions {
79 ($(
80 $variant:ident $method:literal $label:literal [$($role:ident),*] -> $result:ident,
81 $doc:literal {
82 $($param:ident: $pty:ty = $pkind:ident { $($kfield:ident: $kval:expr),* }, $pdoc:literal;)*
83 }
84 )*) => {
85 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
89 #[serde(tag = "method")]
90 pub enum Construction {
91 $(
92 #[doc = $doc]
93 #[serde(rename = $method)]
94 $variant { $(#[doc = $pdoc] $param: $pty),* },
95 )*
96 }
97
98 pub const CONSTRUCTIONS: &[ConstructionSchema] = &[$(
100 ConstructionSchema {
101 method: $method,
102 label: $label,
103 doc: $doc,
104 result: DatumKind::$result,
105 inputs: &[$(Role::$role),*],
106 params: &[$(Param {
107 name: stringify!($param),
108 doc: $pdoc,
109 kind: ParamKind::$pkind { $($kfield: $kval),* },
110 }),*],
111 },
112 )*];
113
114 impl Construction {
115 pub fn schema(&self) -> &'static ConstructionSchema {
117 let method = match self {
118 $(Construction::$variant { .. } => $method,)*
119 };
120 CONSTRUCTIONS
121 .iter()
122 .find(|c| c.method == method)
123 .expect("every construction is described")
124 }
125 }
126 };
127}
128
129constructions! {
130 Point "point" "Point" [Point] -> Point,
132 "A point offset from the selected one: along its own axes if it is a datum or the origin, along the world's otherwise. Its frame is that point's, moved." {
133 x: NumericInput = Number { default: 0.0, min: -10.0, max: 10.0, unit: Unit::Length }, "How far along x.";
134 y: NumericInput = Number { default: 0.0, min: -10.0, max: 10.0, unit: Unit::Length }, "How far along y.";
135 z: NumericInput = Number { default: 0.0, min: -10.0, max: 10.0, unit: Unit::Length }, "How far along z.";
136 }
137 Midpoint "midpoint" "Midpoint" [Point, Point] -> Point,
138 "The point halfway between two points." {}
139 EdgePoint "edge_point" "Point on edge" [Curve] -> Point,
140 "A point along an edge or a sketch's curve, its z axis along it." {
141 position: NumericInput = Number { default: 0.5, min: 0.0, max: 1.0, unit: Unit::Fraction }, "Where along the edge, from its start (0) to its end (1): by length on a straight or circular edge, by parameter on any other.";
142 }
143 Center "center" "Center" [Circle] -> Point,
144 "The center of a circular edge or a sketch's arc or circle, its z axis the one it turns around." {}
145 ProjectOnPlane "project_on_plane" "Projection onto plane" [Point, Plane] -> Point,
146 "The foot of the perpendicular dropped from a point onto a plane." {}
147 ProjectOnLine "project_on_line" "Projection onto line" [Point, Line] -> Point,
148 "The foot of the perpendicular dropped from a point onto a line." {}
149 LinePlane "line_plane" "Line meets plane" [Line, Plane] -> Point,
150 "Where a line pierces a plane." {}
151 LineLine "line_line" "Lines meet" [Line, Line] -> Point,
152 "Where two lines cross — or, if they miss each other, halfway between where they come closest. Its z axis is normal to both." {}
153 ThreePlanes "three_planes" "Three planes meet" [Plane, Plane, Plane] -> Point,
154 "The one point three planes share." {}
155
156 TwoPoints "two_points" "Line through points" [Point, Point] -> Axis,
158 "The line from one point through another." {}
159 AlongLine "along_line" "Along line" [Line] -> Axis,
160 "The line a straight edge or an axis runs along." {}
161 AxisOf "axis_of" "Axis of arc or cylinder" [Round] -> Axis,
162 "The axis a circular edge, or a cylindrical, conical or spherical face, turns around." {}
163 PlanePlane "plane_plane" "Two planes meet" [Plane, Plane] -> Axis,
164 "The line two planes meet in." {}
165 Perpendicular "perpendicular" "Perpendicular to plane" [Point, Plane] -> Axis,
166 "The perpendicular dropped from a point onto a plane: the line through the point along the plane's normal." {}
167 Parallel "parallel" "Parallel through point" [Point, Line] -> Axis,
168 "The line through a point parallel to a line." {}
169 PerpendicularToLine "perpendicular_to_line" "Perpendicular to line" [Point, Line] -> Axis,
170 "The perpendicular dropped from a point onto a line: from the point to its foot on the line." {}
171 Bisector "bisector" "Angle bisector" [Line, Line] -> Axis,
172 "The line halving the angle between two crossing lines, through where they cross — or, between parallel lines, the line halfway between them." {
173 other: bool = Bool { default: false }, "Halve the other angle: the one between the first line and the second one reversed.";
174 }
175 Tangent "tangent" "Tangent to edge" [Curve] -> Axis,
176 "The tangent to an edge or a sketch's curve at a point along it." {
177 position: NumericInput = Number { default: 0.5, min: 0.0, max: 1.0, unit: Unit::Fraction }, "Where along the edge, from its start (0) to its end (1): by length on a straight or circular edge, by parameter on any other.";
178 }
179
180 Offset "offset" "Offset plane" [Plane] -> Plane,
182 "A plane parallel to the selected one, a distance along its normal." {
183 distance: NumericInput = Number { default: 1.0, min: -10.0, max: 10.0, unit: Unit::Length }, "How far along the plane's normal; backwards if negative.";
184 }
185 Midplane "midplane" "Midplane" [Plane, Plane] -> Plane,
186 "The plane halfway between two parallel planes, or halving the angle between two that meet." {
187 other: bool = Bool { default: false }, "For planes that meet: halve the other angle between them.";
188 }
189 ThreePoints "three_points" "Plane through points" [Point, Point, Point] -> Plane,
190 "The plane through three points." {}
191 Angle "angle" "Plane at angle" [Plane, Line] -> Plane,
192 "The plane through a line at an angle to a plane: turned around the line from the plane through it most nearly parallel to the selected one — which, for a line parallel to that plane, is parallel to it." {
193 angle: NumericInput = Number { default: 45.0, min: -180.0, max: 180.0, unit: Unit::Angle }, "How far to turn, in degrees, right-handed about the line's direction.";
194 }
195 LinePoint "line_point" "Plane through line and point" [Line, Point] -> Plane,
196 "The plane through a line and a point off it." {}
197 TwoLines "two_lines" "Plane through lines" [Line, Line] -> Plane,
198 "The plane two crossing or parallel lines lie in — for lines that miss each other, the plane through the first parallel to the second." {}
199 ParallelPlane "parallel_plane" "Parallel plane through point" [Plane, Point] -> Plane,
200 "The plane through a point parallel to a plane." {}
201 NormalToLine "normal_to_line" "Plane normal to line" [Line, Point] -> Plane,
202 "The plane through a point perpendicular to a line." {}
203 NormalToEdge "normal_to_edge" "Plane normal to edge" [Curve] -> Plane,
204 "The plane perpendicular to an edge or a sketch's curve at a point along it." {
205 position: NumericInput = Number { default: 0.5, min: 0.0, max: 1.0, unit: Unit::Fraction }, "Where along the edge, from its start (0) to its end (1): by length on a straight or circular edge, by parameter on any other.";
206 }
207
208 FrameThreePoints "frame_three_points" "Coordinate system through points" [Point, Point, Point] -> Frame,
210 "The coordinate system at the first point, its x axis towards the second and its xy plane through the third." {}
211}
212
213#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
220pub struct AddDatum;
221
222#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
223pub struct AddDatumArgs {
224 pub selection: Vec<Selected>,
227 pub construction: Construction,
230}
231
232fn assign(inputs: &[Role], selection: &[Shape]) -> Option<Vec<usize>> {
236 fn extend(inputs: &[Role], selection: &[Shape], chosen: &mut Vec<usize>) -> bool {
237 let Some(role) = inputs.get(chosen.len()) else {
238 return true;
239 };
240 for (i, geometry) in selection.iter().enumerate() {
241 if !chosen.contains(&i) && geometry.fits(*role) {
242 chosen.push(i);
243 if extend(inputs, selection, chosen) {
244 return true;
245 }
246 chosen.pop();
247 }
248 }
249 false
250 }
251 let mut chosen = Vec::new();
252 (inputs.len() == selection.len() && extend(inputs, selection, &mut chosen)).then_some(chosen)
253}
254
255pub fn fitting_constructions(reader: &Reader, selection: &[Selected]) -> Vec<&'static str> {
259 let Ok(resolved) = selection
260 .iter()
261 .map(|e| e.shape(reader))
262 .collect::<GeopResult<Vec<_>>>()
263 else {
264 return Vec::new();
265 };
266 CONSTRUCTIONS
267 .iter()
268 .filter(|c| assign(c.inputs, &resolved).is_some())
269 .map(|c| c.method)
270 .collect()
271}
272
273impl AddDatumArgs {
274 pub(crate) fn inputs(&self, reader: &Reader) -> GeopResult<Vec<Shape>> {
277 let schema = self.construction.schema();
278 let resolved = self
279 .selection
280 .iter()
281 .map(|e| e.shape(reader))
282 .collect::<GeopResult<Vec<_>>>()?;
283 let Some(order) = assign(schema.inputs, &resolved) else {
284 let needs: Vec<&str> = schema.inputs.iter().map(|&r| r.describe()).collect();
285 return Err(GeopError::new(format!(
286 "{} needs {} selected, one each, and nothing else",
287 schema.label,
288 needs.join(" and ")
289 )));
290 };
291 Ok(order.into_iter().map(|i| resolved[i].clone()).collect())
292 }
293}
294
295fn plane_of(frame: &CoordinateSystem) -> Plane {
297 Plane {
298 point: *frame.origin(),
299 normal: *frame.w(),
300 }
301}
302
303fn moved(frame: &CoordinateSystem, origin: Vector3) -> GeopResult<CoordinateSystem> {
305 CoordinateSystem::try_new(origin, *frame.u(), *frame.v(), *frame.w())
306}
307
308fn along_edge(edge: &Shape, position: f64) -> GeopResult<(Vector3, Vector3)> {
311 if !(0.0..=1.0).contains(&position) {
312 return Err(GeopError::new(format!(
313 "position {position} is not along the edge: it must be from 0 to 1"
314 )));
315 }
316 let curve = edge.curve.as_ref().expect("an edge has a curve");
317 let (t0, t1) = curve.domain();
318 let fraction = Scalar::from_f64(position);
319 if let Some(line) = &edge.line {
320 let (a, b) = (curve.evaluate(t0)?, curve.evaluate(t1)?);
321 return Ok((Vector3::interpolate(&a, &b, fraction), line.direction));
322 }
323 if let Some(arc) = &edge.arc {
324 let p = arc.point_at(position)?;
325 return Ok((p, arc.tangent_at(&p)?));
326 }
327 let t = Scalar::interpolate(t0, t1, fraction);
328 Ok((curve.evaluate(t)?, curve.tangent(t)?.normalize()?))
329}
330
331fn radians(degrees: f64) -> Scalar {
333 Scalar::from_f64(degrees.to_radians())
334}
335
336impl Construction {
337 pub fn default_of(schema: &ConstructionSchema) -> Self {
339 let mut json = serde_json::json!({ "method": schema.method });
340 for param in schema.params {
341 json[param.name] = match param.kind {
342 ParamKind::Number { default, .. } => default.into(),
343 ParamKind::Bool { default } => default.into(),
344 };
345 }
346 serde_json::from_value(json).expect("every construction reads from its schema")
347 }
348
349 pub fn param(&self, name: &str) -> Option<serde_json::Value> {
351 serde_json::to_value(self).ok()?.get(name).cloned()
352 }
353
354 pub fn with_param(&self, name: &str, value: serde_json::Value) -> Option<Self> {
357 let mut json = serde_json::to_value(self).ok()?;
358 json.get(name)?;
359 json[name] = value;
360 serde_json::from_value(json).ok()
361 }
362
363 pub(crate) fn build(
367 &self,
368 inputs: &[Shape],
369 mut value: impl FnMut(&NumericInput) -> GeopResult<f64>,
370 ) -> GeopResult<CoordinateSystem> {
371 let point = |i: usize| inputs[i].point.expect("assigned a point");
372 let line = |i: usize| inputs[i].line.clone().expect("assigned a line");
373 let frame = |i: usize| inputs[i].plane.clone().expect("assigned a plane");
374 let plane = |i: usize| plane_of(&frame(i));
375 let half = Scalar::ONE.div(Scalar::TWO)?;
376 match self {
377 Construction::Point { x, y, z } => {
378 let p = point(0);
379 let base = match &inputs[0].frame {
380 Some(frame) => frame.clone(),
381 None => CoordinateSystem::world_at(p),
382 };
383 let xyz = [value(x)?, value(y)?, value(z)?];
384 let offset = base.to_xyz(&Vector3::from_array(xyz.map(Scalar::from_f64)));
385 moved(&base, offset)
386 }
387 Construction::Midpoint {} => Ok(CoordinateSystem::world_at(Vector3::interpolate(
388 &point(0),
389 &point(1),
390 half,
391 ))),
392 Construction::EdgePoint { position } => {
393 let (p, tangent) = along_edge(&inputs[0], value(position)?)?;
394 frame_along(p, &tangent)
395 }
396 Construction::Center {} => {
397 let arc = inputs[0].arc.clone().expect("assigned an arc");
398 let c = arc.circle;
399 let u = arc.start.sub(&c.center).normalize()?;
400 CoordinateSystem::try_from_xz(c.center, u, c.normal)
401 }
402 Construction::ProjectOnPlane {} => {
403 let foot = plane(1).project(&point(0));
404 moved(&frame(1), foot)
405 }
406 Construction::ProjectOnLine {} => {
407 let l = line(1);
408 frame_along(l.project(&point(0)), &l.direction)
409 }
410 Construction::LinePlane {} => {
411 let at = plane(1).intersect_axis(&line(0))?;
412 moved(&frame(1), at)
413 }
414 Construction::LineLine {} => {
415 let (a, b) = (line(0), line(1));
416 let at = a.nearest(&b)?;
417 let w = a.direction.prod_cross(&b.direction).normalize()?;
418 CoordinateSystem::try_from_xz(at, a.direction, w)
419 }
420 Construction::ThreePlanes {} => {
421 let meet = plane(0).intersect_plane(&plane(1))?;
422 Ok(CoordinateSystem::world_at(plane(2).intersect_axis(&meet)?))
423 }
424 Construction::TwoPoints {} => {
425 let (a, b) = (point(0), point(1));
426 let d = b.sub(&a);
427 if d.norm_sq().could_be_equal(Scalar::ZERO) {
428 return Err(GeopError::new(
429 "the points coincide: no line runs through both",
430 ));
431 }
432 frame_along(a, &d)
433 }
434 Construction::AlongLine {} => {
435 let l = line(0);
436 frame_along(l.point, &l.direction)
437 }
438 Construction::AxisOf {} => {
439 let axis = inputs[0].round.clone().expect("assigned something round");
440 frame_along(axis.point, &axis.direction)
441 }
442 Construction::PlanePlane {} => {
443 let meet = plane(0).intersect_plane(&plane(1))?;
444 frame_along(meet.point, &meet.direction)
445 }
446 Construction::Perpendicular {} => frame_along(point(0), &plane(1).normal),
447 Construction::Parallel {} => frame_along(point(0), &line(1).direction),
448 Construction::PerpendicularToLine {} => {
449 let p = point(0);
450 let d = line(1).project(&p).sub(&p);
451 if d.norm_sq().could_be_equal(Scalar::ZERO) {
452 return Err(GeopError::new(
453 "the point lies on the line: there is no perpendicular to drop",
454 ));
455 }
456 frame_along(p, &d)
457 }
458 Construction::Bisector { other } => {
459 let (a, b) = (line(0), line(1));
460 if a.could_be_parallel(&b) {
461 let mid = Vector3::interpolate(&a.point, &b.project(&a.point), half);
462 return frame_along(mid, &a.direction);
463 }
464 let at = a.nearest(&b)?;
465 let second = if *other {
466 b.direction.neg()
467 } else {
468 b.direction
469 };
470 frame_along(at, &a.direction.add(&second))
471 }
472 Construction::Tangent { position } => {
473 let (p, tangent) = along_edge(&inputs[0], value(position)?)?;
474 frame_along(p, &tangent)
475 }
476 Construction::Offset { distance } => {
477 let f = frame(0);
478 moved(
479 &f,
480 f.origin()
481 .add(&f.w().prod_scalar(Scalar::from_f64(value(distance)?))),
482 )
483 }
484 Construction::Midplane { other } => {
485 let (p, q) = (plane(0), plane(1));
493 let (d1, d2) = (p.point.prod_dot(&p.normal), q.point.prod_dot(&q.normal));
494 let inner = (p.normal.sub(&q.normal), d1.sub(d2));
495 let outer = (p.normal.add(&q.normal), d1.add(d2));
496 let (first, second) = if *other {
497 (outer, inner)
498 } else {
499 (inner, outer)
500 };
501 let (normal, offset) = if first.0.norm_sq().could_be_equal(Scalar::ZERO) {
504 second
505 } else {
506 first
507 };
508 let n2 = normal.norm_sq();
509 let midplane = Plane::try_new(normal.prod_scalar(offset.div(n2)?), normal)?;
510 let near = Vector3::interpolate(frame(0).origin(), frame(1).origin(), half);
511 frame_along(midplane.project(&near), &midplane.normal)
512 }
513 Construction::ThreePoints {} => {
514 let (a, b, c) = (point(0), point(1), point(2));
515 let n = b.sub(&a).prod_cross(&c.sub(&a));
516 if n.norm_sq().could_be_equal(Scalar::ZERO) {
517 return Err(GeopError::new(
518 "the points lie on one line: every plane through that line runs through them",
519 ));
520 }
521 frame_along(a, &n)
522 }
523 Construction::Angle { angle } => {
524 let (n, l) = (plane(0).normal, line(1));
525 let square = n.sub(&l.direction.prod_scalar(l.direction.prod_dot(&n)));
529 if square.norm_sq().could_be_equal(Scalar::ZERO) {
530 return Err(GeopError::new(
531 "the line is perpendicular to the plane: every plane through it is at right angles to it",
532 ));
533 }
534 let a: Scalar = radians(value(angle)?);
535 let normal = square
536 .prod_scalar(a.cos())
537 .add(&l.direction.prod_cross(&square).prod_scalar(a.sin()));
538 frame_along(l.point, &normal)
539 }
540 Construction::LinePoint {} => {
541 let l = line(0);
542 let n = l.direction.prod_cross(&point(1).sub(&l.point));
543 if n.norm_sq().could_be_equal(Scalar::ZERO) {
544 return Err(GeopError::new(
545 "the point lies on the line: every plane through the line runs through it",
546 ));
547 }
548 frame_along(l.point, &n)
549 }
550 Construction::TwoLines {} => {
551 let (a, b) = (line(0), line(1));
552 let n = if a.could_be_parallel(&b) {
553 let n = a.direction.prod_cross(&b.point.sub(&a.point));
554 if n.norm_sq().could_be_equal(Scalar::ZERO) {
555 return Err(GeopError::new(
556 "the lines coincide: every plane through one runs through the other",
557 ));
558 }
559 n
560 } else {
561 a.direction.prod_cross(&b.direction)
562 };
563 frame_along(a.point, &n)
564 }
565 Construction::ParallelPlane {} => {
566 let f = frame(0);
567 let d = plane(0).signed_distance(&point(1));
568 moved(&f, f.origin().add(&f.w().prod_scalar(d)))
569 }
570 Construction::NormalToLine {} => frame_along(point(1), &line(0).direction),
571 Construction::NormalToEdge { position } => {
572 let (p, tangent) = along_edge(&inputs[0], value(position)?)?;
573 frame_along(p, &tangent)
574 }
575 Construction::FrameThreePoints {} => {
576 let (a, b, c) = (point(0), point(1), point(2));
577 let x = b.sub(&a);
578 let z = x.prod_cross(&c.sub(&a));
579 if z.norm_sq().could_be_equal(Scalar::ZERO) {
580 return Err(GeopError::new(
581 "the points lie on one line: they pick out no plane for x and y",
582 ));
583 }
584 let (u, w) = (x.normalize()?, z.normalize()?);
585 CoordinateSystem::try_from_xz(a, u, w)
586 }
587 }
588 }
589}
590
591fn add(reader: &Reader, (step, args): &(String, AddDatumArgs)) -> GeopResult<Datums> {
594 let ctx = with_context!("add_datum({step}, {args:?})");
595 let inputs = args.inputs(reader).with_context(ctx)?;
596 let frame = (args.construction)
597 .build(&inputs, |f| f.resolve(reader))
598 .with_context(ctx)?;
599 let mut datums: Datums = reader.previous()?;
600 let datum = Datum {
601 kind: args.construction.schema().result,
602 frame,
603 };
604 datums.insert(step, datum).with_context(ctx)?;
605 Ok(datums)
606}
607
608part_operation!(AddDatum(AddDatumArgs));
609
610impl PartOperation for AddDatum {
611 const KIND: &'static str = "add_datum";
612 const INFO: OperationInfo = OperationInfo::new(
613 "Reference",
614 "Add a point, an axis, a plane or a coordinate system to build with, made from what is picked.",
615 OperationGroup::Sketch,
616 OperationTier::Small,
617 )
618 .shortcut("D")
619 .icon(crate::icons::ADD_DATUM);
620 type Args = AddDatumArgs;
621 type Session = ();
622
623 fn new_args(_: &Context) -> AddDatumArgs {
625 AddDatumArgs {
626 selection: Vec::new(),
627 construction: Construction::default_of(&CONSTRUCTIONS[0]),
628 }
629 }
630
631 fn run(ctx: &mut StepContext, args: &AddDatumArgs) -> GeopResult<()> {
633 let step = ctx.step();
634 ctx.produce(&datums(), &(step.to_string(), args.clone()), add)
635 }
636
637 fn form<'a>(
640 context: &'a Context<'a>,
641 args: &AddDatumArgs,
642 _: &FormState<()>,
643 ) -> EditForm<'a, AddDatumArgs> {
644 editor::form(&context.before, args)
645 }
646
647 fn canvas(context: &Context, edit: Edit<'_, AddDatumArgs, ()>, event: &CanvasEvent) {
648 editor::event(&context.before, edit.args, event);
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use geop_core_math::{
655 primitives::{DatumComponent, FrameAxis},
656 scalars::Scalar,
657 };
658 use geop_core_program::{element::ElementReference, operation::Operation};
659 use geop_ops_model::datums::{AnyDatum, ORIGIN, component_name, read_datums};
660 use geop_ops_model::model;
661
662 use super::*;
663
664 fn datum(name: impl Into<String>) -> Selected {
665 Selected::Datum(ElementReference::<Datums, AnyDatum>::new(datums(), name))
666 }
667 fn origin() -> Selected {
668 datum(ORIGIN)
669 }
670 fn base(normal: FrameAxis) -> Selected {
671 datum(component_name(ORIGIN, DatumComponent::Plane(normal)))
672 }
673 fn axis(along: FrameAxis) -> Selected {
674 datum(component_name(ORIGIN, DatumComponent::Axis(along)))
675 }
676 fn v(p: [f64; 3]) -> Vector3 {
677 Vector3::from_array(p.map(Scalar::from_f64))
678 }
679
680 fn added(args: &AddDatumArgs) -> GeopResult<Datum> {
683 let mut part = geop_core_program::part::Part::default();
684 let runner = geop_core_program::runner::Runner::new(Default::default());
685 runner.run_step(&mut part, "d", |ctx| {
686 <AddDatum as Operation>::run(ctx, args)
687 })?;
688 let datums = read_datums(&part.current())?.into_owned();
689 datums.get("d")
690 }
691
692 #[test]
695 fn constructions_serialize_as_described() {
696 for schema in CONSTRUCTIONS {
697 let mut json = serde_json::json!({ "method": schema.method });
698 for param in schema.params {
699 json[param.name] = match param.kind {
700 ParamKind::Number { default, .. } => default.into(),
701 ParamKind::Bool { default } => default.into(),
702 };
703 }
704 let construction: Construction = serde_json::from_value(json.clone())
705 .unwrap_or_else(|e| panic!("{}: {e}", schema.method));
706 assert_eq!(construction.schema(), schema);
707 assert_eq!(serde_json::to_value(&construction).unwrap(), json);
708 assert_eq!(Construction::default_of(schema), construction);
709 }
710 }
711
712 #[test]
715 fn an_offset_plane_is_added() {
716 let args = AddDatumArgs {
717 selection: vec![base(FrameAxis::Z)],
718 construction: Construction::Offset {
719 distance: 2.0.into(),
720 },
721 };
722 let datum = added(&args).unwrap();
723 assert_eq!(datum.kind, DatumKind::Plane);
724 assert!(datum.frame.origin().could_be_equal(&v([0.0, 0.0, 2.0])));
725 assert!(datum.frame.w().could_be_equal(&v([0.0, 0.0, 1.0])));
726 assert_eq!(
727 serde_json::to_value(&args.selection).unwrap(),
728 serde_json::json!([{"datum": "origin:xy"}])
729 );
730 }
731
732 #[test]
733 fn a_selection_that_does_not_fit_says_what_it_needs() {
734 let args = AddDatumArgs {
735 selection: vec![origin()],
736 construction: Construction::Offset {
737 distance: 1.0.into(),
738 },
739 };
740 let Err(e) = added(&args) else {
741 panic!("an offset plane from a point");
742 };
743 assert!(e.to_string().contains("needs a plane selected"), "{e}");
744 }
745
746 #[test]
748 fn degenerate_selections_fail() {
749 let err = |selection: Vec<Selected>, construction: Construction| {
750 let args = AddDatumArgs {
751 selection,
752 construction,
753 };
754 match added(&args) {
755 Ok(_) => panic!("{args:?} built a datum"),
756 Err(e) => format!("{e:?}"),
757 }
758 };
759 assert!(err(vec![origin(), origin()], Construction::TwoPoints {}).contains("coincide"));
760 assert!(
761 err(
762 vec![base(FrameAxis::Z), base(FrameAxis::Z)],
763 Construction::PlanePlane {}
764 )
765 .contains("parallel")
766 );
767 assert!(
768 err(
769 vec![origin(), axis(FrameAxis::X)],
770 Construction::PerpendicularToLine {}
771 )
772 .contains("lies on the line")
773 );
774 assert!(
775 err(
776 vec![base(FrameAxis::Z), axis(FrameAxis::Z)],
777 Construction::Angle { angle: 10.0.into() }
778 )
779 .contains("perpendicular")
780 );
781 let nowhere = Selected::Edge(ElementReference::new(model(), "nowhere"));
782 assert!(err(vec![nowhere], Construction::AlongLine {}).contains("nowhere"));
783 }
784}