Skip to main content

geop_lib_stdlib/
drawing.rs

1//! [`Drawing`]: a sketch whose every point is placed by formulas of the
2//! program's parameters — what makes one file draw every size of a part.
3
4use geop_core_program::{
5    numeric::{NumericInput, evaluate},
6    program::Program,
7};
8use geop_ops_model::parameters::parameter_numbers;
9use std::collections::BTreeMap;
10
11use geop_core_geometry::nurb_curve::bezier_knots;
12use geop_core_math::{
13    geop_error::{GeopError, GeopResult},
14    scalars::Scalar,
15};
16use geop_core_sketch::{ConstraintId, CurveId, CurveKind, PointId, SplineShape};
17use geop_ops_model::sketch::{Constraint, Sketch, SketchPlane};
18use geop_ops_sketch::AddSketchArgs;
19
20/// A sketch drawn point by point, each point placed by two formulas — its
21/// `x` and `y` from a fixed origin — so that every point follows the
22/// parameters on its own, and solving the sketch is solving a linear
23/// system with one answer. Curves only join points; arcs take a radius.
24///
25/// It is drawn where the parameters as defined put it, so a program built
26/// as it is needs no solving.
27pub struct Drawing {
28    sketch: Sketch,
29    origin: PointId,
30    formulas: BTreeMap<ConstraintId, NumericInput>,
31    /// The numbers the parameters come to as defined: where to draw.
32    numbers: BTreeMap<String, f64>,
33}
34
35impl Drawing {
36    /// A drawing for a step of `program`, drawn with its parameters as it
37    /// defines them.
38    pub fn new(program: &Program) -> GeopResult<Self> {
39        let numbers = parameter_numbers(program)?;
40        let mut sketch = Sketch::new();
41        let origin = sketch.add_fixed_point(Scalar::ZERO, Scalar::ZERO);
42        Ok(Self {
43            sketch,
44            origin,
45            formulas: BTreeMap::new(),
46            numbers,
47        })
48    }
49
50    /// The value of `formula` with the parameters as defined.
51    fn value(&self, formula: &str) -> GeopResult<f64> {
52        evaluate(formula, |name| self.numbers.get(name).copied())
53    }
54
55    /// Adds the dimension `constraint`, given by `formula` — a plain
56    /// number is a value, not a formula.
57    fn dimension(&mut self, constraint: Constraint, formula: &str) {
58        let id = self.sketch.constrain(constraint);
59        let input = NumericInput::from(formula);
60        if input.plain().is_none() {
61            self.formulas.insert(id, input);
62        }
63    }
64
65    /// The point at `(x, y)`, each a formula.
66    pub fn point(&mut self, x: impl AsRef<str>, y: impl AsRef<str>) -> GeopResult<PointId> {
67        let (x, y) = (x.as_ref(), y.as_ref());
68        let at = [self.value(x)?, self.value(y)?].map(Scalar::from_f64);
69        let p = self.sketch.add_point(at[0], at[1]);
70        let origin = self.origin;
71        self.dimension(
72            Constraint::DistanceX {
73                a: origin,
74                b: p,
75                value: at[0],
76            },
77            x,
78        );
79        self.dimension(
80            Constraint::DistanceY {
81                a: origin,
82                b: p,
83                value: at[1],
84            },
85            y,
86        );
87        Ok(p)
88    }
89
90    pub fn line(&mut self, a: PointId, b: PointId) -> CurveId {
91        self.sketch.add_line(a, b)
92    }
93
94    /// The Bézier curve of the control points `control`, of degree one
95    /// less than there are: a spline on one span, through its first and
96    /// last point.
97    pub fn bezier(&mut self, control: Vec<PointId>) -> CurveId {
98        let n = control.len();
99        let knots = bezier_knots(n - 1);
100        self.sketch.add_curve(CurveKind::Spline {
101            control_points: control,
102            shape: Some(SplineShape {
103                degree: n - 1,
104                knots,
105                weights: vec![Scalar::ONE; n],
106            }),
107        })
108    }
109
110    /// A closed polygon through `corners`, each `[x, y]` formulas: its
111    /// lines, the first from the first corner to the second.
112    pub fn polygon(&mut self, corners: &[[String; 2]]) -> GeopResult<Vec<CurveId>> {
113        let points = corners
114            .iter()
115            .map(|[x, y]| self.point(x, y))
116            .collect::<GeopResult<Vec<_>>>()?;
117        Ok((0..points.len())
118            .map(|i| self.line(points[i], points[(i + 1) % points.len()]))
119            .collect())
120    }
121
122    /// The shorter arc of `radius` from `start` to `end`, turning
123    /// counter-clockwise if `ccw`.
124    pub fn arc(
125        &mut self,
126        start: PointId,
127        end: PointId,
128        radius: &str,
129        ccw: bool,
130    ) -> GeopResult<CurveId> {
131        let r = self.value(radius)?;
132        let at = |p: PointId| {
133            let point = &self.sketch.points[&p];
134            [point.x.to_f64(), point.y.to_f64()]
135        };
136        let ([x0, y0], [x1, y1]) = (at(start), at(end));
137        let chord = (x1 - x0).hypot(y1 - y0);
138        if chord > 2.0 * r {
139            return Err(GeopError::new(format!(
140                "no arc of radius {radius:?} = {r} joins points {chord} apart"
141            )));
142        }
143        let sweep = 2.0 * (chord / (2.0 * r)).asin();
144        let arc = self.sketch.add_arc(
145            start,
146            end,
147            Scalar::from_f64(if ccw { sweep } else { -sweep }),
148        );
149        self.dimension(
150            Constraint::Radius {
151                curve: arc,
152                value: Scalar::from_f64(r),
153            },
154            radius,
155        );
156        Ok(arc)
157    }
158
159    /// A circle around `center` of `diameter`.
160    pub fn circle(&mut self, center: PointId, diameter: &str) -> GeopResult<CurveId> {
161        let d = self.value(diameter)?;
162        let circle = self.sketch.add_circle(center, Scalar::from_f64(d / 2.0));
163        self.dimension(
164            Constraint::Diameter {
165                curve: circle,
166                value: Scalar::from_f64(d),
167            },
168            diameter,
169        );
170        Ok(circle)
171    }
172
173    /// The fixed point every other is placed from, at `(0, 0)`.
174    pub fn origin(&self) -> PointId {
175        self.origin
176    }
177
178    /// The sketch, on `plane`: drawn where its dimensions put it, so it
179    /// needs no solving — and a build with the parameters as defined then
180    /// solves none (see `AddSketch`), which is what keeps a large profile, a
181    /// rack's teeth, cheap to place.
182    pub fn on(self, plane: SketchPlane) -> AddSketchArgs {
183        AddSketchArgs {
184            formulas: self.formulas,
185            ..AddSketchArgs::on(plane, self.sketch)
186        }
187    }
188}