Skip to main content

kcl_api/front/
sketch.rs

1use serde::Deserialize;
2use serde::Serialize;
3
4use crate::Expr;
5use crate::Number;
6use crate::ObjectId;
7use crate::front::Plane;
8
9#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
10#[ts(export, export_to = "FrontendApi.ts", rename = "ApiSketch")]
11pub struct Sketch {
12    pub args: SketchCtor,
13    pub plane: ObjectId,
14    pub segments: Vec<ObjectId>,
15    pub constraints: Vec<ObjectId>,
16}
17
18/// Arguments for creating a new sketch. This is similar to the constructor of
19/// other kinds of objects in that it is the inputs to the sketch, not the
20/// outputs.
21#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
22#[ts(export, export_to = "FrontendApi.ts")]
23pub struct SketchCtor {
24    /// The sketch surface.
25    pub on: Plane,
26}
27
28#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
29#[ts(export, export_to = "FrontendApi.ts", rename = "ApiSegment")]
30#[serde(tag = "type")]
31pub enum Segment {
32    Point(Point),
33    Line(Line),
34    Arc(Arc),
35    Circle(Circle),
36    ControlPointSpline(ControlPointSpline),
37}
38
39#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
40#[ts(export, export_to = "FrontendApi.ts", rename = "ApiPoint")]
41pub struct Point {
42    pub position: Point2d<Number>,
43    pub ctor: Option<PointCtor>,
44    pub owner: Option<ObjectId>,
45    pub freedom: Freedom,
46    pub constraints: Vec<ObjectId>,
47}
48
49#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
50#[ts(export, export_to = "FrontendApi.ts", rename = "ApiLine")]
51pub struct Line {
52    pub start: ObjectId,
53    pub end: ObjectId,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    #[ts(optional)]
56    pub owner: Option<ObjectId>,
57    // Invariant: Line or MidPointLine
58    pub ctor: SegmentCtor,
59    // The constructor is applicable if changing the values of the constructor will change the rendering
60    // of the segment (modulo multiple valid solutions). I.e., whether the object is constrained with
61    // respect to the constructor inputs.
62    // The frontend should only display handles for the constructor inputs if the ctor is applicable.
63    // (Or because they are the (locked) start/end of the segment).
64    pub ctor_applicable: bool,
65    pub construction: bool,
66}
67
68#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
69#[ts(export, export_to = "FrontendApi.ts", rename = "ApiArc")]
70pub struct Arc {
71    pub start: ObjectId,
72    pub end: ObjectId,
73    pub center: ObjectId,
74    // Invariant: Arc
75    pub ctor: SegmentCtor,
76    pub ctor_applicable: bool,
77    pub construction: bool,
78    /// The direction that the arc sweeps from start to end. Omitted when it's
79    /// the default, counterclockwise.
80    #[serde(default, skip_serializing_if = "ArcDirection::is_ccw")]
81    #[ts(as = "Option<ArcDirection>")]
82    #[ts(optional)]
83    pub direction: ArcDirection,
84}
85
86#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
87#[ts(export, export_to = "FrontendApi.ts", rename = "ApiCircle")]
88pub struct Circle {
89    pub start: ObjectId,
90    pub center: ObjectId,
91    // Invariant: Circle
92    pub ctor: SegmentCtor,
93    pub ctor_applicable: bool,
94    pub construction: bool,
95}
96
97#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
98#[ts(export, export_to = "FrontendApi.ts")]
99#[serde(tag = "type")]
100pub enum SegmentCtor {
101    Point(PointCtor),
102    Line(LineCtor),
103    Arc(ArcCtor),
104    Circle(CircleCtor),
105    ControlPointSpline(ControlPointSplineCtor),
106}
107
108impl SegmentCtor {
109    /// What kind of geometry is this (point, line, arc, etc)
110    /// Suitable for use in user-facing messages.
111    pub fn human_friendly_kind_with_article(&self) -> &'static str {
112        match self {
113            Self::Point(_) => "a Point constructor",
114            Self::Line(_) => "a Line constructor",
115            Self::Arc(_) => "an Arc constructor",
116            Self::Circle(_) => "a Circle constructor",
117            Self::ControlPointSpline(_) => "a Control Point Spline constructor",
118        }
119    }
120}
121
122/// The direction that an arc sweeps from its start point to its end point.
123#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ts_rs::TS)]
124#[ts(export, export_to = "FrontendApi.ts")]
125#[serde(rename_all = "lowercase")]
126pub enum ArcDirection {
127    #[default]
128    Ccw,
129    Cw,
130}
131
132impl ArcDirection {
133    pub fn is_ccw(&self) -> bool {
134        matches!(self, ArcDirection::Ccw)
135    }
136
137    pub fn is_clockwise(self) -> bool {
138        matches!(self, ArcDirection::Cw)
139    }
140
141    /// Reorder an arc's declared start and end so that sweeping
142    /// counterclockwise from the first to the second traverses the arc. Useful
143    /// for consumers like the solver that only understand counterclockwise
144    /// arcs.
145    pub fn ccw_order<T>(self, start: T, end: T) -> (T, T) {
146        match self {
147            ArcDirection::Ccw => (start, end),
148            ArcDirection::Cw => (end, start),
149        }
150    }
151}
152
153#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
154#[ts(export, export_to = "FrontendApi.ts", rename = "ApiControlPointSpline")]
155pub struct ControlPointSpline {
156    pub controls: Vec<ObjectId>,
157    pub degree: u32,
158    pub ctor: SegmentCtor,
159    pub ctor_applicable: bool,
160    pub construction: bool,
161}
162
163#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
164#[ts(export, export_to = "FrontendApi.ts", rename = "ApiPoint2d")]
165pub struct Point2d<U: std::fmt::Debug + Clone + ts_rs::TS> {
166    pub x: U,
167    pub y: U,
168}
169
170#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
171#[ts(export, export_to = "FrontendApi.ts")]
172pub struct PointCtor {
173    pub position: Point2d<Expr>,
174}
175
176#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
177#[ts(export, export_to = "FrontendApi.ts")]
178pub struct LineCtor {
179    pub start: Point2d<Expr>,
180    pub end: Point2d<Expr>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    #[ts(optional)]
183    pub construction: Option<bool>,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
187#[ts(export, export_to = "FrontendApi.ts")]
188pub enum Freedom {
189    Free,
190    Fixed,
191    Conflict,
192}
193
194impl Freedom {
195    /// Merges two Freedom values. For example, a point has a solver variable
196    /// for each dimension, x and y. If one dimension is `Free` and the other is
197    /// `Fixed`, the point overall is `Free` since it isn't fully constrained.
198    /// `Conflict` infects the most, followed by `Free`. An object must be fully
199    /// `Fixed` to be `Fixed` overall.
200    pub fn merge(self, other: Self) -> Self {
201        match (self, other) {
202            (Self::Conflict, _) | (_, Self::Conflict) => Self::Conflict,
203            (Self::Free, _) | (_, Self::Free) => Self::Free,
204            (Self::Fixed, Self::Fixed) => Self::Fixed,
205        }
206    }
207}
208
209#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
210#[ts(export, export_to = "FrontendApi.ts")]
211pub struct ArcCtor {
212    pub start: Point2d<Expr>,
213    pub end: Point2d<Expr>,
214    pub center: Point2d<Expr>,
215    /// The direction that the arc sweeps from start to end. `None` means it
216    /// wasn't written in the source, which defaults to counterclockwise.
217    #[serde(default, skip_serializing_if = "Option::is_none")]
218    #[ts(optional)]
219    pub direction: Option<ArcDirection>,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    #[ts(optional)]
222    pub construction: Option<bool>,
223}
224
225#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
226#[ts(export, export_to = "FrontendApi.ts")]
227pub struct CircleCtor {
228    pub start: Point2d<Expr>,
229    pub center: Point2d<Expr>,
230    #[serde(skip_serializing_if = "Option::is_none")]
231    #[ts(optional)]
232    pub construction: Option<bool>,
233}
234
235#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
236#[ts(export, export_to = "FrontendApi.ts")]
237pub struct ControlPointSplineCtor {
238    pub points: Vec<Point2d<Expr>>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    #[ts(optional)]
241    pub construction: Option<bool>,
242}
243
244#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
245#[ts(export, export_to = "FrontendApi.ts", rename = "ApiConstraint")]
246#[serde(tag = "type")]
247// When adding a new constraint type, check trim compatibility. New constraints
248// can break trim in unexpected ways, especially when endpoints are edited,
249// segments are split, or constraints are migrated. Try the trim tool on sketches
250// using the new constraint, and talk to Kurt, Max, or a mechanical engineer if
251// the intended trim behavior is unclear.
252pub enum Constraint {
253    Coincident(Coincident),
254    Distance(Distance),
255    Angle(Angle),
256    Diameter(Diameter),
257    EqualRadius(EqualRadius),
258    Fixed(Fixed),
259    HorizontalDistance(Distance),
260    VerticalDistance(Distance),
261    Horizontal(Horizontal),
262    LinesEqualLength(LinesEqualLength),
263    Midpoint(Midpoint),
264    Parallel(Parallel),
265    Perpendicular(Perpendicular),
266    Radius(Radius),
267    Symmetric(Symmetric),
268    Tangent(Tangent),
269    Vertical(Vertical),
270}
271
272#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
273#[ts(export, export_to = "FrontendApi.ts")]
274pub struct Diameter {
275    pub arc: ObjectId,
276    pub diameter: Number,
277    #[serde(rename = "labelPosition")]
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    #[ts(rename = "labelPosition")]
280    #[ts(optional)]
281    pub label_position: Option<Point2d<Number>>,
282    #[serde(default)]
283    pub source: ConstraintSource,
284}
285
286#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
287#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
288pub struct EqualRadius {
289    pub input: Vec<ObjectId>,
290}
291
292/// Multiple fixed constraints, allowing callers to add fixed constraints on
293/// multiple points at once.
294#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
295#[ts(export, export_to = "FrontendApi.ts")]
296pub struct Fixed {
297    pub points: Vec<FixedPoint>,
298}
299
300/// A fixed constraint on a single point.
301#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
302#[ts(export, export_to = "FrontendApi.ts")]
303pub struct FixedPoint {
304    pub point: ObjectId,
305    pub position: Point2d<Number>,
306}
307
308#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
309#[ts(export, export_to = "FrontendApi.ts")]
310#[serde(untagged)]
311pub enum Horizontal {
312    Line { line: ObjectId },
313    Points { points: Vec<ConstraintSegment> },
314}
315
316#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
317#[ts(export, export_to = "FrontendApi.ts")]
318pub struct LinesEqualLength {
319    pub lines: Vec<ObjectId>,
320}
321
322#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
323#[ts(export, export_to = "FrontendApi.ts")]
324pub struct Midpoint {
325    pub point: ConstraintSegment,
326    #[serde(alias = "line")]
327    pub segment: ObjectId,
328}
329
330#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
331#[ts(export, export_to = "FrontendApi.ts")]
332#[serde(untagged)]
333pub enum Vertical {
334    Line { line: ObjectId },
335    Points { points: Vec<ConstraintSegment> },
336}
337
338#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
339#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
340pub struct Parallel {
341    pub lines: Vec<ObjectId>,
342}
343
344#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
345#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
346pub struct Perpendicular {
347    pub lines: Vec<ObjectId>,
348}
349
350#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
351#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
352pub struct Symmetric {
353    pub input: Vec<ObjectId>,
354    pub axis: ObjectId,
355}
356
357#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
358#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
359pub struct Tangent {
360    pub input: Vec<ObjectId>,
361}
362
363#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
364#[ts(export, export_to = "FrontendApi.ts")]
365pub struct Coincident {
366    pub segments: Vec<ConstraintSegment>,
367}
368#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
369#[ts(export, export_to = "FrontendApi.ts")]
370#[serde(untagged)]
371pub enum ConstraintSegment {
372    Segment(ObjectId),
373    Origin(OriginLiteral),
374}
375
376impl ConstraintSegment {
377    pub const ORIGIN: Self = Self::Origin(OriginLiteral::Origin);
378}
379
380#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
381#[ts(export, export_to = "FrontendApi.ts")]
382#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
383pub enum OriginLiteral {
384    Origin,
385}
386
387#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
388#[ts(export, export_to = "FrontendApi.ts")]
389pub struct Distance {
390    pub segments: Vec<ConstraintSegment>,
391    pub distance: Number,
392    #[serde(rename = "labelPosition")]
393    #[serde(default, skip_serializing_if = "Option::is_none")]
394    #[ts(rename = "labelPosition")]
395    #[ts(optional)]
396    pub label_position: Option<Point2d<Number>>,
397    pub source: ConstraintSource,
398}
399
400#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
401#[ts(export, export_to = "FrontendApi.ts")]
402pub struct Angle {
403    pub lines: Vec<ObjectId>,
404    pub angle: Number,
405    #[serde(default, skip_serializing_if = "Option::is_none")]
406    #[ts(optional)]
407    pub sector: Option<u8>,
408    #[serde(default, skip_serializing_if = "Option::is_none")]
409    #[ts(optional)]
410    pub inverse: Option<bool>,
411    #[serde(rename = "labelPosition")]
412    #[serde(default, skip_serializing_if = "Option::is_none")]
413    #[ts(rename = "labelPosition")]
414    #[ts(optional)]
415    pub label_position: Option<Point2d<Number>>,
416    pub source: ConstraintSource,
417}
418
419#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
420#[ts(export, export_to = "FrontendApi.ts")]
421pub struct Radius {
422    pub arc: ObjectId,
423    pub radius: Number,
424    #[serde(rename = "labelPosition")]
425    #[serde(default, skip_serializing_if = "Option::is_none")]
426    #[ts(rename = "labelPosition")]
427    #[ts(optional)]
428    pub label_position: Option<Point2d<Number>>,
429    #[serde(default)]
430    pub source: ConstraintSource,
431}
432
433#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize, ts_rs::TS)]
434#[ts(export, export_to = "FrontendApi.ts")]
435pub struct ConstraintSource {
436    pub expr: String,
437    pub is_literal: bool,
438}
439
440impl From<ObjectId> for ConstraintSegment {
441    fn from(value: ObjectId) -> Self {
442        Self::Segment(value)
443    }
444}