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    async fn batch_tail_cut_operations(
154        &mut self,
155        ctx: &ExecutorContext,
156        version: Version,
157        sketch: ObjectId,
158        edit_segments: Vec<ExistingSegmentCtor>,
159        add_constraints: Vec<Constraint>,
160        delete_constraint_ids: Vec<ObjectId>,
161    ) -> ExecResult<(SourceDelta, SceneGraphDelta)>;
162}
163
164#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
165#[ts(export, export_to = "FrontendApi.ts", rename = "ApiSketch")]
166pub struct Sketch {
167    pub args: SketchCtor,
168    pub plane: ObjectId,
169    pub segments: Vec<ObjectId>,
170    pub constraints: Vec<ObjectId>,
171}
172
173/// Arguments for creating a new sketch. This is similar to the constructor of
174/// other kinds of objects in that it is the inputs to the sketch, not the
175/// outputs.
176#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
177#[ts(export, export_to = "FrontendApi.ts")]
178pub struct SketchCtor {
179    /// The sketch surface.
180    pub on: Plane,
181}
182
183#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
184#[ts(export, export_to = "FrontendApi.ts", rename = "ApiPoint")]
185pub struct Point {
186    pub position: Point2d<Number>,
187    pub ctor: Option<PointCtor>,
188    pub owner: Option<ObjectId>,
189    pub freedom: Freedom,
190    pub constraints: Vec<ObjectId>,
191}
192
193impl Point {
194    /// The freedom of this point.
195    pub fn freedom(&self) -> Freedom {
196        self.freedom
197    }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
201#[ts(export, export_to = "FrontendApi.ts")]
202pub enum Freedom {
203    Free,
204    Fixed,
205    Conflict,
206}
207
208impl Freedom {
209    /// Merges two Freedom values. For example, a point has a solver variable
210    /// for each dimension, x and y. If one dimension is `Free` and the other is
211    /// `Fixed`, the point overall is `Free` since it isn't fully constrained.
212    /// `Conflict` infects the most, followed by `Free`. An object must be fully
213    /// `Fixed` to be `Fixed` overall.
214    pub fn merge(self, other: Self) -> Self {
215        match (self, other) {
216            (Self::Conflict, _) | (_, Self::Conflict) => Self::Conflict,
217            (Self::Free, _) | (_, Self::Free) => Self::Free,
218            (Self::Fixed, Self::Fixed) => Self::Fixed,
219        }
220    }
221}
222
223#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
224#[ts(export, export_to = "FrontendApi.ts", rename = "ApiSegment")]
225#[serde(tag = "type")]
226pub enum Segment {
227    Point(Point),
228    Line(Line),
229    Arc(Arc),
230    Circle(Circle),
231}
232
233impl Segment {
234    /// What kind of geometry is this (point, line, arc, etc)
235    /// Suitable for use in user-facing messages.
236    pub fn human_friendly_kind_with_article(&self) -> &'static str {
237        match self {
238            Self::Point(_) => "a Point",
239            Self::Line(_) => "a Line",
240            Self::Arc(_) => "an Arc",
241            Self::Circle(_) => "a Circle",
242        }
243    }
244
245    /// Compute the overall freedom of this segment. For geometry types (Line,
246    /// Arc, Circle) this looks up and merges the freedom of their constituent
247    /// points. For points, returns the point's own freedom directly.
248    /// Returns `None` if a required point lookup failed.
249    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
250        match self {
251            Self::Point(p) => Some(p.freedom()),
252            Self::Line(l) => l.freedom(&lookup),
253            Self::Arc(a) => a.freedom(&lookup),
254            Self::Circle(c) => c.freedom(&lookup),
255        }
256    }
257}
258
259#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
260#[ts(export, export_to = "FrontendApi.ts")]
261pub struct ExistingSegmentCtor {
262    pub id: ObjectId,
263    pub ctor: SegmentCtor,
264}
265
266#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
267#[ts(export, export_to = "FrontendApi.ts")]
268#[serde(tag = "type")]
269pub enum SegmentCtor {
270    Point(PointCtor),
271    Line(LineCtor),
272    Arc(ArcCtor),
273    Circle(CircleCtor),
274}
275
276impl SegmentCtor {
277    /// What kind of geometry is this (point, line, arc, etc)
278    /// Suitable for use in user-facing messages.
279    pub fn human_friendly_kind_with_article(&self) -> &'static str {
280        match self {
281            Self::Point(_) => "a Point constructor",
282            Self::Line(_) => "a Line constructor",
283            Self::Arc(_) => "an Arc constructor",
284            Self::Circle(_) => "a Circle constructor",
285        }
286    }
287}
288
289#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
290#[ts(export, export_to = "FrontendApi.ts")]
291pub struct PointCtor {
292    pub position: Point2d<Expr>,
293}
294
295#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
296#[ts(export, export_to = "FrontendApi.ts", rename = "ApiPoint2d")]
297pub struct Point2d<U: std::fmt::Debug + Clone + ts_rs::TS> {
298    pub x: U,
299    pub y: U,
300}
301
302#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
303#[ts(export, export_to = "FrontendApi.ts", rename = "ApiLine")]
304pub struct Line {
305    pub start: ObjectId,
306    pub end: ObjectId,
307    // Invariant: Line or MidPointLine
308    pub ctor: SegmentCtor,
309    // The constructor is applicable if changing the values of the constructor will change the rendering
310    // of the segment (modulo multiple valid solutions). I.e., whether the object is constrained with
311    // respect to the constructor inputs.
312    // The frontend should only display handles for the constructor inputs if the ctor is applicable.
313    // (Or because they are the (locked) start/end of the segment).
314    pub ctor_applicable: bool,
315    pub construction: bool,
316}
317
318impl Line {
319    /// Compute the overall freedom of this line by merging the freedom of its
320    /// start and end points. Returns `None` if a point lookup failed.
321    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
322        let start = lookup(self.start)?;
323        let end = lookup(self.end)?;
324        Some(start.merge(end))
325    }
326}
327
328#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
329#[ts(export, export_to = "FrontendApi.ts")]
330pub struct LineCtor {
331    pub start: Point2d<Expr>,
332    pub end: Point2d<Expr>,
333    #[serde(skip_serializing_if = "Option::is_none")]
334    #[ts(optional)]
335    pub construction: Option<bool>,
336}
337
338#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
339#[ts(export, export_to = "FrontendApi.ts", rename = "ApiStartOrEnd")]
340#[serde(tag = "type")]
341pub enum StartOrEnd<T> {
342    Start(T),
343    End(T),
344}
345
346#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
347#[ts(export, export_to = "FrontendApi.ts", rename = "ApiArc")]
348pub struct Arc {
349    pub start: ObjectId,
350    pub end: ObjectId,
351    pub center: ObjectId,
352    // Invariant: Arc
353    pub ctor: SegmentCtor,
354    pub ctor_applicable: bool,
355    pub construction: bool,
356}
357
358impl Arc {
359    /// Compute the overall freedom of this arc by merging the freedom of its
360    /// start, end, and center points. Returns `None` if a point lookup failed.
361    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
362        let start = lookup(self.start)?;
363        let end = lookup(self.end)?;
364        let center = lookup(self.center)?;
365        Some(start.merge(end).merge(center))
366    }
367}
368
369#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
370#[ts(export, export_to = "FrontendApi.ts")]
371pub struct ArcCtor {
372    pub start: Point2d<Expr>,
373    pub end: Point2d<Expr>,
374    pub center: Point2d<Expr>,
375    #[serde(skip_serializing_if = "Option::is_none")]
376    #[ts(optional)]
377    pub construction: Option<bool>,
378}
379
380#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
381#[ts(export, export_to = "FrontendApi.ts", rename = "ApiCircle")]
382pub struct Circle {
383    pub start: ObjectId,
384    pub center: ObjectId,
385    // Invariant: Circle
386    pub ctor: SegmentCtor,
387    pub ctor_applicable: bool,
388    pub construction: bool,
389}
390
391impl Circle {
392    /// Compute the overall freedom of this circle by merging the freedom of its
393    /// start and center points. Returns `None` if a point lookup failed.
394    pub fn freedom(&self, lookup: impl Fn(ObjectId) -> Option<Freedom>) -> Option<Freedom> {
395        let start = lookup(self.start)?;
396        let center = lookup(self.center)?;
397        Some(start.merge(center))
398    }
399}
400
401#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
402#[ts(export, export_to = "FrontendApi.ts")]
403pub struct CircleCtor {
404    pub start: Point2d<Expr>,
405    pub center: Point2d<Expr>,
406    #[serde(skip_serializing_if = "Option::is_none")]
407    #[ts(optional)]
408    pub construction: Option<bool>,
409}
410
411#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
412#[ts(export, export_to = "FrontendApi.ts", rename = "ApiConstraint")]
413#[serde(tag = "type")]
414pub enum Constraint {
415    Coincident(Coincident),
416    Distance(Distance),
417    Angle(Angle),
418    Diameter(Diameter),
419    EqualRadius(EqualRadius),
420    Fixed(Fixed),
421    HorizontalDistance(Distance),
422    VerticalDistance(Distance),
423    Horizontal(Horizontal),
424    LinesEqualLength(LinesEqualLength),
425    Midpoint(Midpoint),
426    Parallel(Parallel),
427    Perpendicular(Perpendicular),
428    Radius(Radius),
429    Symmetric(Symmetric),
430    Tangent(Tangent),
431    Vertical(Vertical),
432}
433
434#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
435#[ts(export, export_to = "FrontendApi.ts")]
436pub struct Coincident {
437    pub segments: Vec<ConstraintSegment>,
438}
439
440impl Coincident {
441    pub fn get_segments(&self) -> Vec<ObjectId> {
442        self.segments
443            .iter()
444            .filter_map(|segment| match segment {
445                ConstraintSegment::Segment(id) => Some(*id),
446                ConstraintSegment::Origin(_) => None,
447            })
448            .collect()
449    }
450
451    pub fn segment_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
452        self.segments.iter().filter_map(|segment| match segment {
453            ConstraintSegment::Segment(id) => Some(*id),
454            ConstraintSegment::Origin(_) => None,
455        })
456    }
457
458    pub fn contains_segment(&self, segment_id: ObjectId) -> bool {
459        self.segment_ids().any(|id| id == segment_id)
460    }
461}
462
463#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
464#[ts(export, export_to = "FrontendApi.ts")]
465#[serde(untagged)]
466pub enum ConstraintSegment {
467    Segment(ObjectId),
468    Origin(OriginLiteral),
469}
470
471impl ConstraintSegment {
472    pub const ORIGIN: Self = Self::Origin(OriginLiteral::Origin);
473}
474
475#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, ts_rs::TS)]
476#[ts(export, export_to = "FrontendApi.ts")]
477#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
478pub enum OriginLiteral {
479    Origin,
480}
481
482impl From<ObjectId> for ConstraintSegment {
483    fn from(value: ObjectId) -> Self {
484        Self::Segment(value)
485    }
486}
487
488#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
489#[ts(export, export_to = "FrontendApi.ts")]
490pub struct Distance {
491    pub points: Vec<ConstraintSegment>,
492    pub distance: Number,
493    #[serde(rename = "labelPosition")]
494    #[serde(default, skip_serializing_if = "Option::is_none")]
495    #[ts(rename = "labelPosition")]
496    #[ts(optional)]
497    pub label_position: Option<Point2d<Number>>,
498    pub source: ConstraintSource,
499}
500
501impl Distance {
502    pub fn point_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
503        self.points.iter().filter_map(|point| match point {
504            ConstraintSegment::Segment(id) => Some(*id),
505            ConstraintSegment::Origin(_) => None,
506        })
507    }
508
509    pub fn contains_point(&self, point_id: ObjectId) -> bool {
510        self.point_ids().any(|id| id == point_id)
511    }
512}
513
514#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
515#[ts(export, export_to = "FrontendApi.ts")]
516pub struct Angle {
517    pub lines: Vec<ObjectId>,
518    pub angle: Number,
519    pub source: ConstraintSource,
520}
521
522#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize, ts_rs::TS)]
523#[ts(export, export_to = "FrontendApi.ts")]
524pub struct ConstraintSource {
525    pub expr: String,
526    pub is_literal: bool,
527}
528
529#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
530#[ts(export, export_to = "FrontendApi.ts")]
531pub struct Radius {
532    pub arc: ObjectId,
533    pub radius: Number,
534    #[serde(rename = "labelPosition")]
535    #[serde(default, skip_serializing_if = "Option::is_none")]
536    #[ts(rename = "labelPosition")]
537    #[ts(optional)]
538    pub label_position: Option<Point2d<Number>>,
539    #[serde(default)]
540    pub source: ConstraintSource,
541}
542
543#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
544#[ts(export, export_to = "FrontendApi.ts")]
545pub struct Diameter {
546    pub arc: ObjectId,
547    pub diameter: Number,
548    #[serde(rename = "labelPosition")]
549    #[serde(default, skip_serializing_if = "Option::is_none")]
550    #[ts(rename = "labelPosition")]
551    #[ts(optional)]
552    pub label_position: Option<Point2d<Number>>,
553    #[serde(default)]
554    pub source: ConstraintSource,
555}
556
557#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
558#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
559pub struct EqualRadius {
560    pub input: Vec<ObjectId>,
561}
562
563/// Multiple fixed constraints, allowing callers to add fixed constraints on
564/// multiple points at once.
565#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
566#[ts(export, export_to = "FrontendApi.ts")]
567pub struct Fixed {
568    pub points: Vec<FixedPoint>,
569}
570
571/// A fixed constraint on a single point.
572#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
573#[ts(export, export_to = "FrontendApi.ts")]
574pub struct FixedPoint {
575    pub point: ObjectId,
576    pub position: Point2d<Number>,
577}
578
579#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
580#[ts(export, export_to = "FrontendApi.ts")]
581#[serde(untagged)]
582pub enum Horizontal {
583    Line { line: ObjectId },
584    Points { points: Vec<ConstraintSegment> },
585}
586
587#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
588#[ts(export, export_to = "FrontendApi.ts")]
589pub struct LinesEqualLength {
590    pub lines: Vec<ObjectId>,
591}
592
593#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
594#[ts(export, export_to = "FrontendApi.ts")]
595pub struct Midpoint {
596    pub point: ObjectId,
597    #[serde(alias = "line")]
598    pub segment: ObjectId,
599}
600
601#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
602#[ts(export, export_to = "FrontendApi.ts")]
603#[serde(untagged)]
604pub enum Vertical {
605    Line { line: ObjectId },
606    Points { points: Vec<ConstraintSegment> },
607}
608
609#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
610#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
611pub struct Parallel {
612    pub lines: Vec<ObjectId>,
613}
614
615#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
616#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
617pub struct Perpendicular {
618    pub lines: Vec<ObjectId>,
619}
620
621#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
622#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
623pub struct Symmetric {
624    pub input: Vec<ObjectId>,
625    pub axis: ObjectId,
626}
627
628#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
629#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
630pub struct Tangent {
631    pub input: Vec<ObjectId>,
632}