Skip to main content

kcl_lib/frontend/
sketch.rs

1#![allow(async_fn_in_trait)]
2
3use serde::Deserialize;
4use serde::Serialize;
5
6use crate::ExecutorContext;
7use crate::KclErrorWithOutputs;
8use crate::front::Plane;
9use crate::frontend::api::Expr;
10use crate::frontend::api::FileId;
11use crate::frontend::api::Number;
12use crate::frontend::api::ObjectId;
13use crate::frontend::api::ProjectId;
14use crate::frontend::api::SceneGraph;
15use crate::frontend::api::SceneGraphDelta;
16use crate::frontend::api::SourceDelta;
17use crate::frontend::api::Version;
18
19pub type ExecResult<T> = std::result::Result<T, KclErrorWithOutputs>;
20
21/// Information about a newly created segment for batch operations
22#[derive(Debug, Clone)]
23pub struct NewSegmentInfo {
24    pub segment_id: ObjectId,
25    pub start_point_id: ObjectId,
26    pub end_point_id: ObjectId,
27    pub center_point_id: Option<ObjectId>,
28}
29
30pub trait SketchApi {
31    /// Execute the sketch in mock mode, without changing anything. This is
32    /// useful after editing segments, and the user releases the mouse button.
33    async fn execute_mock(
34        &mut self,
35        ctx: &ExecutorContext,
36        version: Version,
37        sketch: ObjectId,
38    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
39
40    async fn new_sketch(
41        &mut self,
42        ctx: &ExecutorContext,
43        project: ProjectId,
44        file: FileId,
45        version: Version,
46        args: SketchCtor,
47    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)>;
48
49    // Enters sketch mode
50    async fn edit_sketch(
51        &mut self,
52        ctx: &ExecutorContext,
53        project: ProjectId,
54        file: FileId,
55        version: Version,
56        sketch: ObjectId,
57    ) -> ExecResult<SceneGraphDelta>;
58
59    async fn exit_sketch(
60        &mut self,
61        ctx: &ExecutorContext,
62        version: Version,
63        sketch: ObjectId,
64    ) -> ExecResult<SceneGraph>;
65
66    async fn delete_sketch(
67        &mut self,
68        ctx: &ExecutorContext,
69        version: Version,
70        sketch: ObjectId,
71    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
72
73    async fn add_segment(
74        &mut self,
75        ctx: &ExecutorContext,
76        version: Version,
77        sketch: ObjectId,
78        segment: SegmentCtor,
79        label: Option<String>,
80    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
81
82    async fn edit_segments(
83        &mut self,
84        ctx: &ExecutorContext,
85        version: Version,
86        sketch: ObjectId,
87        segments: Vec<ExistingSegmentCtor>,
88    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
89
90    async fn delete_objects(
91        &mut self,
92        ctx: &ExecutorContext,
93        version: Version,
94        sketch: ObjectId,
95        constraint_ids: Vec<ObjectId>,
96        segment_ids: Vec<ObjectId>,
97    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
98
99    async fn add_constraint(
100        &mut self,
101        ctx: &ExecutorContext,
102        version: Version,
103        sketch: ObjectId,
104        constraint: Constraint,
105    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
106
107    async fn chain_segment(
108        &mut self,
109        ctx: &ExecutorContext,
110        version: Version,
111        sketch: ObjectId,
112        previous_segment_end_point_id: ObjectId,
113        segment: SegmentCtor,
114        label: Option<String>,
115    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
116
117    async fn edit_constraint_value(
118        &mut self,
119        ctx: &ExecutorContext,
120        version: Version,
121        sketch: ObjectId,
122        constraint_id: ObjectId,
123        value_expression: String,
124    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
125
126    async fn edit_distance_constraint_label_position(
127        &mut self,
128        ctx: &ExecutorContext,
129        version: Version,
130        sketch: ObjectId,
131        constraint_id: ObjectId,
132        label_position: Point2d<Number>,
133        anchor_segment_ids: Vec<ObjectId>,
134    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
135
136    /// Batch operations for split segment: edit segments, add constraints, delete objects.
137    /// All operations are applied to a single AST and execute_after_edit is called once at the end.
138    /// new_segment_info contains the IDs from the segment(s) added in a previous step.
139    #[allow(clippy::too_many_arguments)]
140    async fn batch_split_segment_operations(
141        &mut self,
142        ctx: &ExecutorContext,
143        version: Version,
144        sketch: ObjectId,
145        edit_segments: Vec<ExistingSegmentCtor>,
146        add_constraints: Vec<Constraint>,
147        delete_constraint_ids: Vec<ObjectId>,
148        new_segment_info: NewSegmentInfo,
149    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
150
151    /// Batch operations for tail-cut trim: edit a segment, add coincident constraints,
152    /// delete constraints, and execute once.
153    #[allow(clippy::too_many_arguments)]
154    async fn batch_tail_cut_operations(
155        &mut self,
156        ctx: &ExecutorContext,
157        version: Version,
158        sketch: ObjectId,
159        edit_segments: Vec<ExistingSegmentCtor>,
160        add_constraints: Vec<Constraint>,
161        delete_constraint_ids: Vec<ObjectId>,
162        additional_edited_segment_ids: Vec<ObjectId>,
163    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
164}
165
166#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
167#[ts(export, export_to = "FrontendApi.ts", rename = "ApiSketch")]
168pub struct Sketch {
169    pub args: SketchCtor,
170    pub plane: ObjectId,
171    pub segments: Vec<ObjectId>,
172    pub constraints: Vec<ObjectId>,
173}
174
175/// Arguments for creating a new sketch. This is similar to the constructor of
176/// other kinds of objects in that it is the inputs to the sketch, not the
177/// outputs.
178#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
179#[ts(export, export_to = "FrontendApi.ts")]
180pub struct SketchCtor {
181    /// The sketch surface.
182    pub on: Plane,
183}
184
185#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
186#[ts(export, export_to = "FrontendApi.ts", rename = "ApiPoint")]
187pub struct Point {
188    pub position: Point2d<Number>,
189    pub ctor: Option<PointCtor>,
190    pub owner: Option<ObjectId>,
191    pub freedom: Freedom,
192    pub constraints: Vec<ObjectId>,
193}
194
195impl Point {
196    /// The freedom of this point.
197    pub fn freedom(&self) -> Freedom {
198        self.freedom
199    }
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
203#[ts(export, export_to = "FrontendApi.ts")]
204pub enum Freedom {
205    Free,
206    Fixed,
207    Conflict,
208}
209
210impl Freedom {
211    /// Merges two Freedom values. For example, a point has a solver variable
212    /// for each dimension, x and y. If one dimension is `Free` and the other is
213    /// `Fixed`, the point overall is `Free` since it isn't fully constrained.
214    /// `Conflict` infects the most, followed by `Free`. An object must be fully
215    /// `Fixed` to be `Fixed` overall.
216    pub fn merge(self, other: Self) -> Self {
217        match (self, other) {
218            (Self::Conflict, _) | (_, Self::Conflict) => Self::Conflict,
219            (Self::Free, _) | (_, Self::Free) => Self::Free,
220            (Self::Fixed, Self::Fixed) => Self::Fixed,
221        }
222    }
223}
224
225#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
226#[ts(export, export_to = "FrontendApi.ts", rename = "ApiSegment")]
227#[serde(tag = "type")]
228pub enum Segment {
229    Point(Point),
230    Line(Line),
231    Arc(Arc),
232    Circle(Circle),
233    ControlPointSpline(ControlPointSpline),
234}
235
236impl Segment {
237    /// What kind of geometry is this (point, line, arc, etc)
238    /// Suitable for use in user-facing messages.
239    pub fn human_friendly_kind_with_article(&self) -> &'static str {
240        match self {
241            Self::Point(_) => "a Point",
242            Self::Line(_) => "a Line",
243            Self::Arc(_) => "an Arc",
244            Self::Circle(_) => "a Circle",
245            Self::ControlPointSpline(_) => "a Control Point Spline",
246        }
247    }
248
249    /// Compute the overall freedom of this segment. For geometry types (Line,
250    /// Arc, Circle) this looks up and merges the freedom of their constituent
251    /// points. For points, returns the point's own freedom directly.
252    /// Returns `None` if a required point lookup failed.
253    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
254        match self {
255            Self::Point(p) => Some(p.freedom()),
256            Self::Line(l) => l.freedom(&lookup),
257            Self::Arc(a) => a.freedom(&lookup),
258            Self::Circle(c) => c.freedom(&lookup),
259            Self::ControlPointSpline(s) => s.freedom(&lookup),
260        }
261    }
262}
263
264#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
265#[ts(export, export_to = "FrontendApi.ts")]
266pub struct ExistingSegmentCtor {
267    pub id: ObjectId,
268    pub ctor: SegmentCtor,
269}
270
271#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
272#[ts(export, export_to = "FrontendApi.ts")]
273#[serde(rename_all = "camelCase")]
274pub struct ConstraintLabelPositionEdit {
275    pub constraint_id: ObjectId,
276    pub label_position: Point2d<Number>,
277}
278
279#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
280#[ts(export, export_to = "FrontendApi.ts")]
281#[serde(tag = "type")]
282pub enum SegmentCtor {
283    Point(PointCtor),
284    Line(LineCtor),
285    Arc(ArcCtor),
286    Circle(CircleCtor),
287    ControlPointSpline(ControlPointSplineCtor),
288}
289
290impl SegmentCtor {
291    /// What kind of geometry is this (point, line, arc, etc)
292    /// Suitable for use in user-facing messages.
293    pub fn human_friendly_kind_with_article(&self) -> &'static str {
294        match self {
295            Self::Point(_) => "a Point constructor",
296            Self::Line(_) => "a Line constructor",
297            Self::Arc(_) => "an Arc constructor",
298            Self::Circle(_) => "a Circle constructor",
299            Self::ControlPointSpline(_) => "a Control Point Spline constructor",
300        }
301    }
302}
303
304#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
305#[ts(export, export_to = "FrontendApi.ts")]
306pub struct PointCtor {
307    pub position: Point2d<Expr>,
308}
309
310#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
311#[ts(export, export_to = "FrontendApi.ts", rename = "ApiPoint2d")]
312pub struct Point2d<U: std::fmt::Debug + Clone + ts_rs::TS> {
313    pub x: U,
314    pub y: U,
315}
316
317#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
318#[ts(export, export_to = "FrontendApi.ts", rename = "ApiLine")]
319pub struct Line {
320    pub start: ObjectId,
321    pub end: ObjectId,
322    #[serde(skip_serializing_if = "Option::is_none")]
323    #[ts(optional)]
324    pub owner: Option<ObjectId>,
325    // Invariant: Line or MidPointLine
326    pub ctor: SegmentCtor,
327    // The constructor is applicable if changing the values of the constructor will change the rendering
328    // of the segment (modulo multiple valid solutions). I.e., whether the object is constrained with
329    // respect to the constructor inputs.
330    // The frontend should only display handles for the constructor inputs if the ctor is applicable.
331    // (Or because they are the (locked) start/end of the segment).
332    pub ctor_applicable: bool,
333    pub construction: bool,
334}
335
336impl Line {
337    /// Compute the overall freedom of this line by merging the freedom of its
338    /// start and end points. Returns `None` if a point lookup failed.
339    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
340        let start = lookup(self.start)?;
341        let end = lookup(self.end)?;
342        Some(start.merge(end))
343    }
344}
345
346#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
347#[ts(export, export_to = "FrontendApi.ts")]
348pub struct LineCtor {
349    pub start: Point2d<Expr>,
350    pub end: Point2d<Expr>,
351    #[serde(skip_serializing_if = "Option::is_none")]
352    #[ts(optional)]
353    pub construction: Option<bool>,
354}
355
356#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
357#[ts(export, export_to = "FrontendApi.ts", rename = "ApiStartOrEnd")]
358#[serde(tag = "type")]
359pub enum StartOrEnd<T> {
360    Start(T),
361    End(T),
362}
363
364/// The direction that an arc sweeps from its start point to its end point.
365#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ts_rs::TS)]
366#[ts(export, export_to = "FrontendApi.ts")]
367#[serde(rename_all = "lowercase")]
368pub enum ArcDirection {
369    #[default]
370    Ccw,
371    Cw,
372}
373
374impl ArcDirection {
375    pub fn is_ccw(&self) -> bool {
376        matches!(self, ArcDirection::Ccw)
377    }
378
379    pub fn is_clockwise(self) -> bool {
380        matches!(self, ArcDirection::Cw)
381    }
382
383    /// Reorder an arc's declared start and end so that sweeping
384    /// counterclockwise from the first to the second traverses the arc. Useful
385    /// for consumers like the solver that only understand counterclockwise
386    /// arcs.
387    pub fn ccw_order<T>(self, start: T, end: T) -> (T, T) {
388        match self {
389            ArcDirection::Ccw => (start, end),
390            ArcDirection::Cw => (end, start),
391        }
392    }
393}
394
395#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
396#[ts(export, export_to = "FrontendApi.ts", rename = "ApiArc")]
397pub struct Arc {
398    pub start: ObjectId,
399    pub end: ObjectId,
400    pub center: ObjectId,
401    // Invariant: Arc
402    pub ctor: SegmentCtor,
403    pub ctor_applicable: bool,
404    pub construction: bool,
405    /// The direction that the arc sweeps from start to end. Omitted when it's
406    /// the default, counterclockwise.
407    #[serde(default, skip_serializing_if = "ArcDirection::is_ccw")]
408    #[ts(as = "Option<ArcDirection>")]
409    #[ts(optional)]
410    pub direction: ArcDirection,
411}
412
413impl Arc {
414    /// Compute the overall freedom of this arc by merging the freedom of its
415    /// start, end, and center points. Returns `None` if a point lookup failed.
416    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
417        let start = lookup(self.start)?;
418        let end = lookup(self.end)?;
419        let center = lookup(self.center)?;
420        Some(start.merge(end).merge(center))
421    }
422}
423
424#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
425#[ts(export, export_to = "FrontendApi.ts")]
426pub struct ArcCtor {
427    pub start: Point2d<Expr>,
428    pub end: Point2d<Expr>,
429    pub center: Point2d<Expr>,
430    /// The direction that the arc sweeps from start to end. `None` means it
431    /// wasn't written in the source, which defaults to counterclockwise.
432    #[serde(default, skip_serializing_if = "Option::is_none")]
433    #[ts(optional)]
434    pub direction: Option<ArcDirection>,
435    #[serde(skip_serializing_if = "Option::is_none")]
436    #[ts(optional)]
437    pub construction: Option<bool>,
438}
439
440#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
441#[ts(export, export_to = "FrontendApi.ts", rename = "ApiCircle")]
442pub struct Circle {
443    pub start: ObjectId,
444    pub center: ObjectId,
445    // Invariant: Circle
446    pub ctor: SegmentCtor,
447    pub ctor_applicable: bool,
448    pub construction: bool,
449}
450
451impl Circle {
452    /// Compute the overall freedom of this circle by merging the freedom of its
453    /// start and center points. Returns `None` if a point lookup failed.
454    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
455        let start = lookup(self.start)?;
456        let center = lookup(self.center)?;
457        Some(start.merge(center))
458    }
459}
460
461#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
462#[ts(export, export_to = "FrontendApi.ts")]
463pub struct CircleCtor {
464    pub start: Point2d<Expr>,
465    pub center: Point2d<Expr>,
466    #[serde(skip_serializing_if = "Option::is_none")]
467    #[ts(optional)]
468    pub construction: Option<bool>,
469}
470
471#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
472#[ts(export, export_to = "FrontendApi.ts", rename = "ApiControlPointSpline")]
473pub struct ControlPointSpline {
474    pub controls: Vec<ObjectId>,
475    pub degree: u32,
476    pub ctor: SegmentCtor,
477    pub ctor_applicable: bool,
478    pub construction: bool,
479}
480
481impl ControlPointSpline {
482    /// Compute the overall freedom of this spline by merging the freedom of its
483    /// control points. Returns `None` if any required point lookup failed.
484    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
485        let mut controls = self.controls.iter();
486        let first = lookup(*controls.next()?)?;
487        let merged = controls.try_fold(first, |acc, id| lookup(*id).map(|freedom| acc.merge(freedom)))?;
488        Some(merged)
489    }
490}
491
492#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
493#[ts(export, export_to = "FrontendApi.ts")]
494pub struct ControlPointSplineCtor {
495    pub points: Vec<Point2d<Expr>>,
496    #[serde(skip_serializing_if = "Option::is_none")]
497    #[ts(optional)]
498    pub construction: Option<bool>,
499}
500
501#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
502#[ts(export, export_to = "FrontendApi.ts", rename = "ApiConstraint")]
503#[serde(tag = "type")]
504// When adding a new constraint type, check trim compatibility. New constraints
505// can break trim in unexpected ways, especially when endpoints are edited,
506// segments are split, or constraints are migrated. Try the trim tool on sketches
507// using the new constraint, and talk to Kurt, Max, or a mechanical engineer if
508// the intended trim behavior is unclear.
509pub enum Constraint {
510    Coincident(Coincident),
511    Distance(Distance),
512    Angle(Angle),
513    Diameter(Diameter),
514    EqualRadius(EqualRadius),
515    Fixed(Fixed),
516    HorizontalDistance(Distance),
517    VerticalDistance(Distance),
518    Horizontal(Horizontal),
519    LinesEqualLength(LinesEqualLength),
520    Midpoint(Midpoint),
521    Parallel(Parallel),
522    Perpendicular(Perpendicular),
523    Radius(Radius),
524    Symmetric(Symmetric),
525    Tangent(Tangent),
526    Vertical(Vertical),
527}
528
529#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
530#[ts(export, export_to = "FrontendApi.ts")]
531pub struct Coincident {
532    pub segments: Vec<ConstraintSegment>,
533}
534
535impl Coincident {
536    pub fn get_segments(&self) -> Vec<ObjectId> {
537        self.segments
538            .iter()
539            .filter_map(|segment| match segment {
540                ConstraintSegment::Segment(id) => Some(*id),
541                ConstraintSegment::Origin(_) => None,
542            })
543            .collect()
544    }
545
546    pub fn segment_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
547        self.segments.iter().filter_map(|segment| match segment {
548            ConstraintSegment::Segment(id) => Some(*id),
549            ConstraintSegment::Origin(_) => None,
550        })
551    }
552
553    pub fn contains_segment(&self, segment_id: ObjectId) -> bool {
554        self.segment_ids().any(|id| id == segment_id)
555    }
556}
557
558#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
559#[ts(export, export_to = "FrontendApi.ts")]
560#[serde(untagged)]
561pub enum ConstraintSegment {
562    Segment(ObjectId),
563    Origin(OriginLiteral),
564}
565
566impl ConstraintSegment {
567    pub const ORIGIN: Self = Self::Origin(OriginLiteral::Origin);
568}
569
570#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
571#[ts(export, export_to = "FrontendApi.ts")]
572#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
573pub enum OriginLiteral {
574    Origin,
575}
576
577impl From<ObjectId> for ConstraintSegment {
578    fn from(value: ObjectId) -> Self {
579        Self::Segment(value)
580    }
581}
582
583#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
584#[ts(export, export_to = "FrontendApi.ts")]
585pub struct Distance {
586    pub segments: Vec<ConstraintSegment>,
587    pub distance: Number,
588    #[serde(rename = "labelPosition")]
589    #[serde(default, skip_serializing_if = "Option::is_none")]
590    #[ts(rename = "labelPosition")]
591    #[ts(optional)]
592    pub label_position: Option<Point2d<Number>>,
593    pub source: ConstraintSource,
594}
595
596impl Distance {
597    pub fn segment_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
598        self.segments.iter().filter_map(|segment| match segment {
599            ConstraintSegment::Segment(id) => Some(*id),
600            ConstraintSegment::Origin(_) => None,
601        })
602    }
603
604    pub fn contains_segment(&self, segment_id: ObjectId) -> bool {
605        self.segment_ids().any(|id| id == segment_id)
606    }
607}
608
609#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
610#[ts(export, export_to = "FrontendApi.ts")]
611pub struct Angle {
612    pub lines: Vec<ObjectId>,
613    pub angle: Number,
614    #[serde(default, skip_serializing_if = "Option::is_none")]
615    #[ts(optional)]
616    pub sector: Option<u8>,
617    #[serde(default, skip_serializing_if = "Option::is_none")]
618    #[ts(optional)]
619    pub inverse: Option<bool>,
620    #[serde(rename = "labelPosition")]
621    #[serde(default, skip_serializing_if = "Option::is_none")]
622    #[ts(rename = "labelPosition")]
623    #[ts(optional)]
624    pub label_position: Option<Point2d<Number>>,
625    pub source: ConstraintSource,
626}
627
628#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize, ts_rs::TS)]
629#[ts(export, export_to = "FrontendApi.ts")]
630pub struct ConstraintSource {
631    pub expr: String,
632    pub is_literal: bool,
633}
634
635#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
636#[ts(export, export_to = "FrontendApi.ts")]
637pub struct Radius {
638    pub arc: ObjectId,
639    pub radius: Number,
640    #[serde(rename = "labelPosition")]
641    #[serde(default, skip_serializing_if = "Option::is_none")]
642    #[ts(rename = "labelPosition")]
643    #[ts(optional)]
644    pub label_position: Option<Point2d<Number>>,
645    #[serde(default)]
646    pub source: ConstraintSource,
647}
648
649#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
650#[ts(export, export_to = "FrontendApi.ts")]
651pub struct Diameter {
652    pub arc: ObjectId,
653    pub diameter: Number,
654    #[serde(rename = "labelPosition")]
655    #[serde(default, skip_serializing_if = "Option::is_none")]
656    #[ts(rename = "labelPosition")]
657    #[ts(optional)]
658    pub label_position: Option<Point2d<Number>>,
659    #[serde(default)]
660    pub source: ConstraintSource,
661}
662
663#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
664#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
665pub struct EqualRadius {
666    pub input: Vec<ObjectId>,
667}
668
669/// Multiple fixed constraints, allowing callers to add fixed constraints on
670/// multiple points at once.
671#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
672#[ts(export, export_to = "FrontendApi.ts")]
673pub struct Fixed {
674    pub points: Vec<FixedPoint>,
675}
676
677/// A fixed constraint on a single point.
678#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
679#[ts(export, export_to = "FrontendApi.ts")]
680pub struct FixedPoint {
681    pub point: ObjectId,
682    pub position: Point2d<Number>,
683}
684
685#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
686#[ts(export, export_to = "FrontendApi.ts")]
687#[serde(untagged)]
688pub enum Horizontal {
689    Line { line: ObjectId },
690    Points { points: Vec<ConstraintSegment> },
691}
692
693#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
694#[ts(export, export_to = "FrontendApi.ts")]
695pub struct LinesEqualLength {
696    pub lines: Vec<ObjectId>,
697}
698
699#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
700#[ts(export, export_to = "FrontendApi.ts")]
701pub struct Midpoint {
702    pub point: ConstraintSegment,
703    #[serde(alias = "line")]
704    pub segment: ObjectId,
705}
706
707#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
708#[ts(export, export_to = "FrontendApi.ts")]
709#[serde(untagged)]
710pub enum Vertical {
711    Line { line: ObjectId },
712    Points { points: Vec<ConstraintSegment> },
713}
714
715#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
716#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
717pub struct Parallel {
718    pub lines: Vec<ObjectId>,
719}
720
721#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
722#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
723pub struct Perpendicular {
724    pub lines: Vec<ObjectId>,
725}
726
727#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
728#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
729pub struct Symmetric {
730    pub input: Vec<ObjectId>,
731    pub axis: ObjectId,
732}
733
734#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
735#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
736pub struct Tangent {
737    pub input: Vec<ObjectId>,
738}