Skip to main content

kcl_api/front/
sketch.rs

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