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(default)]
535    pub source: ConstraintSource,
536}
537
538#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
539#[ts(export, export_to = "FrontendApi.ts")]
540pub struct Diameter {
541    pub arc: ObjectId,
542    pub diameter: Number,
543    #[serde(default)]
544    pub source: ConstraintSource,
545}
546
547#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
548#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
549pub struct EqualRadius {
550    pub input: Vec<ObjectId>,
551}
552
553/// Multiple fixed constraints, allowing callers to add fixed constraints on
554/// multiple points at once.
555#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
556#[ts(export, export_to = "FrontendApi.ts")]
557pub struct Fixed {
558    pub points: Vec<FixedPoint>,
559}
560
561/// A fixed constraint on a single point.
562#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
563#[ts(export, export_to = "FrontendApi.ts")]
564pub struct FixedPoint {
565    pub point: ObjectId,
566    pub position: Point2d<Number>,
567}
568
569#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
570#[ts(export, export_to = "FrontendApi.ts")]
571#[serde(untagged)]
572pub enum Horizontal {
573    Line { line: ObjectId },
574    Points { points: Vec<ConstraintSegment> },
575}
576
577#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
578#[ts(export, export_to = "FrontendApi.ts")]
579pub struct LinesEqualLength {
580    pub lines: Vec<ObjectId>,
581}
582
583#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
584#[ts(export, export_to = "FrontendApi.ts")]
585pub struct Midpoint {
586    pub point: ObjectId,
587    #[serde(alias = "line")]
588    pub segment: ObjectId,
589}
590
591#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
592#[ts(export, export_to = "FrontendApi.ts")]
593#[serde(untagged)]
594pub enum Vertical {
595    Line { line: ObjectId },
596    Points { points: Vec<ConstraintSegment> },
597}
598
599#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
600#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
601pub struct Parallel {
602    pub lines: Vec<ObjectId>,
603}
604
605#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
606#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
607pub struct Perpendicular {
608    pub lines: Vec<ObjectId>,
609}
610
611#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
612#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
613pub struct Symmetric {
614    pub input: Vec<ObjectId>,
615    pub axis: ObjectId,
616}
617
618#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
619#[ts(export, export_to = "FrontendApi.ts", optional_fields)]
620pub struct Tangent {
621    pub input: Vec<ObjectId>,
622}