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(tag = "type")]
274pub enum SegmentCtor {
275    Point(PointCtor),
276    Line(LineCtor),
277    Arc(ArcCtor),
278    Circle(CircleCtor),
279    ControlPointSpline(ControlPointSplineCtor),
280}
281
282impl SegmentCtor {
283    /// What kind of geometry is this (point, line, arc, etc)
284    /// Suitable for use in user-facing messages.
285    pub fn human_friendly_kind_with_article(&self) -> &'static str {
286        match self {
287            Self::Point(_) => "a Point constructor",
288            Self::Line(_) => "a Line constructor",
289            Self::Arc(_) => "an Arc constructor",
290            Self::Circle(_) => "a Circle constructor",
291            Self::ControlPointSpline(_) => "a Control Point Spline constructor",
292        }
293    }
294}
295
296#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
297#[ts(export, export_to = "FrontendApi.ts")]
298pub struct PointCtor {
299    pub position: Point2d<Expr>,
300}
301
302#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
303#[ts(export, export_to = "FrontendApi.ts", rename = "ApiPoint2d")]
304pub struct Point2d<U: std::fmt::Debug + Clone + ts_rs::TS> {
305    pub x: U,
306    pub y: U,
307}
308
309#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
310#[ts(export, export_to = "FrontendApi.ts", rename = "ApiLine")]
311pub struct Line {
312    pub start: ObjectId,
313    pub end: ObjectId,
314    #[serde(skip_serializing_if = "Option::is_none")]
315    #[ts(optional)]
316    pub owner: Option<ObjectId>,
317    // Invariant: Line or MidPointLine
318    pub ctor: SegmentCtor,
319    // The constructor is applicable if changing the values of the constructor will change the rendering
320    // of the segment (modulo multiple valid solutions). I.e., whether the object is constrained with
321    // respect to the constructor inputs.
322    // The frontend should only display handles for the constructor inputs if the ctor is applicable.
323    // (Or because they are the (locked) start/end of the segment).
324    pub ctor_applicable: bool,
325    pub construction: bool,
326}
327
328impl Line {
329    /// Compute the overall freedom of this line by merging the freedom of its
330    /// start and end points. Returns `None` if a point lookup failed.
331    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
332        let start = lookup(self.start)?;
333        let end = lookup(self.end)?;
334        Some(start.merge(end))
335    }
336}
337
338#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
339#[ts(export, export_to = "FrontendApi.ts")]
340pub struct LineCtor {
341    pub start: Point2d<Expr>,
342    pub end: Point2d<Expr>,
343    #[serde(skip_serializing_if = "Option::is_none")]
344    #[ts(optional)]
345    pub construction: Option<bool>,
346}
347
348#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
349#[ts(export, export_to = "FrontendApi.ts", rename = "ApiStartOrEnd")]
350#[serde(tag = "type")]
351pub enum StartOrEnd<T> {
352    Start(T),
353    End(T),
354}
355
356#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
357#[ts(export, export_to = "FrontendApi.ts", rename = "ApiArc")]
358pub struct Arc {
359    pub start: ObjectId,
360    pub end: ObjectId,
361    pub center: ObjectId,
362    // Invariant: Arc
363    pub ctor: SegmentCtor,
364    pub ctor_applicable: bool,
365    pub construction: bool,
366}
367
368impl Arc {
369    /// Compute the overall freedom of this arc by merging the freedom of its
370    /// start, end, and center points. Returns `None` if a point lookup failed.
371    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
372        let start = lookup(self.start)?;
373        let end = lookup(self.end)?;
374        let center = lookup(self.center)?;
375        Some(start.merge(end).merge(center))
376    }
377}
378
379#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
380#[ts(export, export_to = "FrontendApi.ts")]
381pub struct ArcCtor {
382    pub start: Point2d<Expr>,
383    pub end: Point2d<Expr>,
384    pub center: Point2d<Expr>,
385    #[serde(skip_serializing_if = "Option::is_none")]
386    #[ts(optional)]
387    pub construction: Option<bool>,
388}
389
390#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
391#[ts(export, export_to = "FrontendApi.ts", rename = "ApiCircle")]
392pub struct Circle {
393    pub start: ObjectId,
394    pub center: ObjectId,
395    // Invariant: Circle
396    pub ctor: SegmentCtor,
397    pub ctor_applicable: bool,
398    pub construction: bool,
399}
400
401impl Circle {
402    /// Compute the overall freedom of this circle by merging the freedom of its
403    /// start and center points. Returns `None` if a point lookup failed.
404    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
405        let start = lookup(self.start)?;
406        let center = lookup(self.center)?;
407        Some(start.merge(center))
408    }
409}
410
411#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
412#[ts(export, export_to = "FrontendApi.ts")]
413pub struct CircleCtor {
414    pub start: Point2d<Expr>,
415    pub center: Point2d<Expr>,
416    #[serde(skip_serializing_if = "Option::is_none")]
417    #[ts(optional)]
418    pub construction: Option<bool>,
419}
420
421#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
422#[ts(export, export_to = "FrontendApi.ts", rename = "ApiControlPointSpline")]
423pub struct ControlPointSpline {
424    pub controls: Vec<ObjectId>,
425    pub degree: u32,
426    pub ctor: SegmentCtor,
427    pub ctor_applicable: bool,
428    pub construction: bool,
429}
430
431impl ControlPointSpline {
432    /// Compute the overall freedom of this spline by merging the freedom of its
433    /// control points. Returns `None` if any required point lookup failed.
434    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
435        let mut controls = self.controls.iter();
436        let first = lookup(*controls.next()?)?;
437        let merged = controls.try_fold(first, |acc, id| lookup(*id).map(|freedom| acc.merge(freedom)))?;
438        Some(merged)
439    }
440}
441
442#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
443#[ts(export, export_to = "FrontendApi.ts")]
444pub struct ControlPointSplineCtor {
445    pub points: Vec<Point2d<Expr>>,
446    #[serde(skip_serializing_if = "Option::is_none")]
447    #[ts(optional)]
448    pub construction: Option<bool>,
449}
450
451#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
452#[ts(export, export_to = "FrontendApi.ts", rename = "ApiConstraint")]
453#[serde(tag = "type")]
454// When adding a new constraint type, check trim compatibility. New constraints
455// can break trim in unexpected ways, especially when endpoints are edited,
456// segments are split, or constraints are migrated. Try the trim tool on sketches
457// using the new constraint, and talk to Kurt, Max, or a mechanical engineer if
458// the intended trim behavior is unclear.
459pub enum Constraint {
460    Coincident(Coincident),
461    Distance(Distance),
462    Angle(Angle),
463    Diameter(Diameter),
464    EqualRadius(EqualRadius),
465    Fixed(Fixed),
466    HorizontalDistance(Distance),
467    VerticalDistance(Distance),
468    Horizontal(Horizontal),
469    LinesEqualLength(LinesEqualLength),
470    Midpoint(Midpoint),
471    Parallel(Parallel),
472    Perpendicular(Perpendicular),
473    Radius(Radius),
474    Symmetric(Symmetric),
475    Tangent(Tangent),
476    Vertical(Vertical),
477}
478
479#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
480#[ts(export, export_to = "FrontendApi.ts")]
481pub struct Coincident {
482    pub segments: Vec<ConstraintSegment>,
483}
484
485impl Coincident {
486    pub fn get_segments(&self) -> Vec<ObjectId> {
487        self.segments
488            .iter()
489            .filter_map(|segment| match segment {
490                ConstraintSegment::Segment(id) => Some(*id),
491                ConstraintSegment::Origin(_) => None,
492            })
493            .collect()
494    }
495
496    pub fn segment_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
497        self.segments.iter().filter_map(|segment| match segment {
498            ConstraintSegment::Segment(id) => Some(*id),
499            ConstraintSegment::Origin(_) => None,
500        })
501    }
502
503    pub fn contains_segment(&self, segment_id: ObjectId) -> bool {
504        self.segment_ids().any(|id| id == segment_id)
505    }
506}
507
508#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
509#[ts(export, export_to = "FrontendApi.ts")]
510#[serde(untagged)]
511pub enum ConstraintSegment {
512    Segment(ObjectId),
513    Origin(OriginLiteral),
514}
515
516impl ConstraintSegment {
517    pub const ORIGIN: Self = Self::Origin(OriginLiteral::Origin);
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
521#[ts(export, export_to = "FrontendApi.ts")]
522#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
523pub enum OriginLiteral {
524    Origin,
525}
526
527impl From<ObjectId> for ConstraintSegment {
528    fn from(value: ObjectId) -> Self {
529        Self::Segment(value)
530    }
531}
532
533#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
534#[ts(export, export_to = "FrontendApi.ts")]
535pub struct Distance {
536    pub points: Vec<ConstraintSegment>,
537    pub distance: Number,
538    #[serde(rename = "labelPosition")]
539    #[serde(default, skip_serializing_if = "Option::is_none")]
540    #[ts(rename = "labelPosition")]
541    #[ts(optional)]
542    pub label_position: Option<Point2d<Number>>,
543    pub source: ConstraintSource,
544}
545
546impl Distance {
547    pub fn point_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
548        self.points.iter().filter_map(|point| match point {
549            ConstraintSegment::Segment(id) => Some(*id),
550            ConstraintSegment::Origin(_) => None,
551        })
552    }
553
554    pub fn contains_point(&self, point_id: ObjectId) -> bool {
555        self.point_ids().any(|id| id == point_id)
556    }
557}
558
559#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
560#[ts(export, export_to = "FrontendApi.ts")]
561pub struct Angle {
562    pub lines: Vec<ObjectId>,
563    pub angle: Number,
564    pub source: ConstraintSource,
565}
566
567#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize, ts_rs::TS)]
568#[ts(export, export_to = "FrontendApi.ts")]
569pub struct ConstraintSource {
570    pub expr: String,
571    pub is_literal: bool,
572}
573
574#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
575#[ts(export, export_to = "FrontendApi.ts")]
576pub struct Radius {
577    pub arc: ObjectId,
578    pub radius: Number,
579    #[serde(rename = "labelPosition")]
580    #[serde(default, skip_serializing_if = "Option::is_none")]
581    #[ts(rename = "labelPosition")]
582    #[ts(optional)]
583    pub label_position: Option<Point2d<Number>>,
584    #[serde(default)]
585    pub source: ConstraintSource,
586}
587
588#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
589#[ts(export, export_to = "FrontendApi.ts")]
590pub struct Diameter {
591    pub arc: ObjectId,
592    pub diameter: Number,
593    #[serde(rename = "labelPosition")]
594    #[serde(default, skip_serializing_if = "Option::is_none")]
595    #[ts(rename = "labelPosition")]
596    #[ts(optional)]
597    pub label_position: Option<Point2d<Number>>,
598    #[serde(default)]
599    pub source: ConstraintSource,
600}
601
602#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
603#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
604pub struct EqualRadius {
605    pub input: Vec<ObjectId>,
606}
607
608/// Multiple fixed constraints, allowing callers to add fixed constraints on
609/// multiple points at once.
610#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
611#[ts(export, export_to = "FrontendApi.ts")]
612pub struct Fixed {
613    pub points: Vec<FixedPoint>,
614}
615
616/// A fixed constraint on a single point.
617#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
618#[ts(export, export_to = "FrontendApi.ts")]
619pub struct FixedPoint {
620    pub point: ObjectId,
621    pub position: Point2d<Number>,
622}
623
624#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
625#[ts(export, export_to = "FrontendApi.ts")]
626#[serde(untagged)]
627pub enum Horizontal {
628    Line { line: ObjectId },
629    Points { points: Vec<ConstraintSegment> },
630}
631
632#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
633#[ts(export, export_to = "FrontendApi.ts")]
634pub struct LinesEqualLength {
635    pub lines: Vec<ObjectId>,
636}
637
638#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
639#[ts(export, export_to = "FrontendApi.ts")]
640pub struct Midpoint {
641    pub point: ObjectId,
642    #[serde(alias = "line")]
643    pub segment: ObjectId,
644}
645
646#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
647#[ts(export, export_to = "FrontendApi.ts")]
648#[serde(untagged)]
649pub enum Vertical {
650    Line { line: ObjectId },
651    Points { points: Vec<ConstraintSegment> },
652}
653
654#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
655#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
656pub struct Parallel {
657    pub lines: Vec<ObjectId>,
658}
659
660#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
661#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
662pub struct Perpendicular {
663    pub lines: Vec<ObjectId>,
664}
665
666#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
667#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
668pub struct Symmetric {
669    pub input: Vec<ObjectId>,
670    pub axis: ObjectId,
671}
672
673#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
674#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
675pub struct Tangent {
676    pub input: Vec<ObjectId>,
677}