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(
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#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
365#[ts(export, export_to = "FrontendApi.ts", rename = "ApiArc")]
366pub struct Arc {
367    pub start: ObjectId,
368    pub end: ObjectId,
369    pub center: ObjectId,
370    // Invariant: Arc
371    pub ctor: SegmentCtor,
372    pub ctor_applicable: bool,
373    pub construction: bool,
374}
375
376impl Arc {
377    /// Compute the overall freedom of this arc by merging the freedom of its
378    /// start, end, and center points. Returns `None` if a point lookup failed.
379    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
380        let start = lookup(self.start)?;
381        let end = lookup(self.end)?;
382        let center = lookup(self.center)?;
383        Some(start.merge(end).merge(center))
384    }
385}
386
387#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
388#[ts(export, export_to = "FrontendApi.ts")]
389pub struct ArcCtor {
390    pub start: Point2d<Expr>,
391    pub end: Point2d<Expr>,
392    pub center: Point2d<Expr>,
393    #[serde(skip_serializing_if = "Option::is_none")]
394    #[ts(optional)]
395    pub construction: Option<bool>,
396}
397
398#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
399#[ts(export, export_to = "FrontendApi.ts", rename = "ApiCircle")]
400pub struct Circle {
401    pub start: ObjectId,
402    pub center: ObjectId,
403    // Invariant: Circle
404    pub ctor: SegmentCtor,
405    pub ctor_applicable: bool,
406    pub construction: bool,
407}
408
409impl Circle {
410    /// Compute the overall freedom of this circle by merging the freedom of its
411    /// start and center points. Returns `None` if a point lookup failed.
412    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
413        let start = lookup(self.start)?;
414        let center = lookup(self.center)?;
415        Some(start.merge(center))
416    }
417}
418
419#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
420#[ts(export, export_to = "FrontendApi.ts")]
421pub struct CircleCtor {
422    pub start: Point2d<Expr>,
423    pub center: Point2d<Expr>,
424    #[serde(skip_serializing_if = "Option::is_none")]
425    #[ts(optional)]
426    pub construction: Option<bool>,
427}
428
429#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
430#[ts(export, export_to = "FrontendApi.ts", rename = "ApiControlPointSpline")]
431pub struct ControlPointSpline {
432    pub controls: Vec<ObjectId>,
433    pub degree: u32,
434    pub ctor: SegmentCtor,
435    pub ctor_applicable: bool,
436    pub construction: bool,
437}
438
439impl ControlPointSpline {
440    /// Compute the overall freedom of this spline by merging the freedom of its
441    /// control points. Returns `None` if any required point lookup failed.
442    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
443        let mut controls = self.controls.iter();
444        let first = lookup(*controls.next()?)?;
445        let merged = controls.try_fold(first, |acc, id| lookup(*id).map(|freedom| acc.merge(freedom)))?;
446        Some(merged)
447    }
448}
449
450#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
451#[ts(export, export_to = "FrontendApi.ts")]
452pub struct ControlPointSplineCtor {
453    pub points: Vec<Point2d<Expr>>,
454    #[serde(skip_serializing_if = "Option::is_none")]
455    #[ts(optional)]
456    pub construction: Option<bool>,
457}
458
459#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
460#[ts(export, export_to = "FrontendApi.ts", rename = "ApiConstraint")]
461#[serde(tag = "type")]
462// When adding a new constraint type, check trim compatibility. New constraints
463// can break trim in unexpected ways, especially when endpoints are edited,
464// segments are split, or constraints are migrated. Try the trim tool on sketches
465// using the new constraint, and talk to Kurt, Max, or a mechanical engineer if
466// the intended trim behavior is unclear.
467pub enum Constraint {
468    Coincident(Coincident),
469    Distance(Distance),
470    Angle(Angle),
471    Diameter(Diameter),
472    EqualRadius(EqualRadius),
473    Fixed(Fixed),
474    HorizontalDistance(Distance),
475    VerticalDistance(Distance),
476    Horizontal(Horizontal),
477    LinesEqualLength(LinesEqualLength),
478    Midpoint(Midpoint),
479    Parallel(Parallel),
480    Perpendicular(Perpendicular),
481    Radius(Radius),
482    Symmetric(Symmetric),
483    Tangent(Tangent),
484    Vertical(Vertical),
485}
486
487#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
488#[ts(export, export_to = "FrontendApi.ts")]
489pub struct Coincident {
490    pub segments: Vec<ConstraintSegment>,
491}
492
493impl Coincident {
494    pub fn get_segments(&self) -> Vec<ObjectId> {
495        self.segments
496            .iter()
497            .filter_map(|segment| match segment {
498                ConstraintSegment::Segment(id) => Some(*id),
499                ConstraintSegment::Origin(_) => None,
500            })
501            .collect()
502    }
503
504    pub fn segment_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
505        self.segments.iter().filter_map(|segment| match segment {
506            ConstraintSegment::Segment(id) => Some(*id),
507            ConstraintSegment::Origin(_) => None,
508        })
509    }
510
511    pub fn contains_segment(&self, segment_id: ObjectId) -> bool {
512        self.segment_ids().any(|id| id == segment_id)
513    }
514}
515
516#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
517#[ts(export, export_to = "FrontendApi.ts")]
518#[serde(untagged)]
519pub enum ConstraintSegment {
520    Segment(ObjectId),
521    Origin(OriginLiteral),
522}
523
524impl ConstraintSegment {
525    pub const ORIGIN: Self = Self::Origin(OriginLiteral::Origin);
526}
527
528#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
529#[ts(export, export_to = "FrontendApi.ts")]
530#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
531pub enum OriginLiteral {
532    Origin,
533}
534
535impl From<ObjectId> for ConstraintSegment {
536    fn from(value: ObjectId) -> Self {
537        Self::Segment(value)
538    }
539}
540
541#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
542#[ts(export, export_to = "FrontendApi.ts")]
543pub struct Distance {
544    pub points: Vec<ConstraintSegment>,
545    pub distance: Number,
546    #[serde(rename = "labelPosition")]
547    #[serde(default, skip_serializing_if = "Option::is_none")]
548    #[ts(rename = "labelPosition")]
549    #[ts(optional)]
550    pub label_position: Option<Point2d<Number>>,
551    pub source: ConstraintSource,
552}
553
554impl Distance {
555    pub fn point_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
556        self.points.iter().filter_map(|point| match point {
557            ConstraintSegment::Segment(id) => Some(*id),
558            ConstraintSegment::Origin(_) => None,
559        })
560    }
561
562    pub fn contains_point(&self, point_id: ObjectId) -> bool {
563        self.point_ids().any(|id| id == point_id)
564    }
565}
566
567#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
568#[ts(export, export_to = "FrontendApi.ts")]
569pub struct Angle {
570    pub lines: Vec<ObjectId>,
571    pub angle: Number,
572    pub source: ConstraintSource,
573}
574
575#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize, ts_rs::TS)]
576#[ts(export, export_to = "FrontendApi.ts")]
577pub struct ConstraintSource {
578    pub expr: String,
579    pub is_literal: bool,
580}
581
582#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
583#[ts(export, export_to = "FrontendApi.ts")]
584pub struct Radius {
585    pub arc: ObjectId,
586    pub radius: Number,
587    #[serde(rename = "labelPosition")]
588    #[serde(default, skip_serializing_if = "Option::is_none")]
589    #[ts(rename = "labelPosition")]
590    #[ts(optional)]
591    pub label_position: Option<Point2d<Number>>,
592    #[serde(default)]
593    pub source: ConstraintSource,
594}
595
596#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
597#[ts(export, export_to = "FrontendApi.ts")]
598pub struct Diameter {
599    pub arc: ObjectId,
600    pub diameter: Number,
601    #[serde(rename = "labelPosition")]
602    #[serde(default, skip_serializing_if = "Option::is_none")]
603    #[ts(rename = "labelPosition")]
604    #[ts(optional)]
605    pub label_position: Option<Point2d<Number>>,
606    #[serde(default)]
607    pub source: ConstraintSource,
608}
609
610#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
611#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
612pub struct EqualRadius {
613    pub input: Vec<ObjectId>,
614}
615
616/// Multiple fixed constraints, allowing callers to add fixed constraints on
617/// multiple points at once.
618#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
619#[ts(export, export_to = "FrontendApi.ts")]
620pub struct Fixed {
621    pub points: Vec<FixedPoint>,
622}
623
624/// A fixed constraint on a single point.
625#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
626#[ts(export, export_to = "FrontendApi.ts")]
627pub struct FixedPoint {
628    pub point: ObjectId,
629    pub position: Point2d<Number>,
630}
631
632#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
633#[ts(export, export_to = "FrontendApi.ts")]
634#[serde(untagged)]
635pub enum Horizontal {
636    Line { line: ObjectId },
637    Points { points: Vec<ConstraintSegment> },
638}
639
640#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
641#[ts(export, export_to = "FrontendApi.ts")]
642pub struct LinesEqualLength {
643    pub lines: Vec<ObjectId>,
644}
645
646#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
647#[ts(export, export_to = "FrontendApi.ts")]
648pub struct Midpoint {
649    pub point: ConstraintSegment,
650    #[serde(alias = "line")]
651    pub segment: ObjectId,
652}
653
654#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
655#[ts(export, export_to = "FrontendApi.ts")]
656#[serde(untagged)]
657pub enum Vertical {
658    Line { line: ObjectId },
659    Points { points: Vec<ConstraintSegment> },
660}
661
662#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
663#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
664pub struct Parallel {
665    pub lines: Vec<ObjectId>,
666}
667
668#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
669#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
670pub struct Perpendicular {
671    pub lines: Vec<ObjectId>,
672}
673
674#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
675#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
676pub struct Symmetric {
677    pub input: Vec<ObjectId>,
678    pub axis: ObjectId,
679}
680
681#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
682#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
683pub struct Tangent {
684    pub input: Vec<ObjectId>,
685}