@@ -100,7 +100,7 @@ class ElementPrefix:
100100 POINT = " P"
101101 SPLINE = " S"
102102 ELLIPSE = " E"
103- ELLIPTICAL_ARC = " EA "
103+ ELLIPTICAL_ARC = " e "
104104```
105105
106106---
@@ -418,7 +418,7 @@ class EllipticalArc(SketchPrimitive):
418418 """
419419 Arc of an ellipse defined by center, radii, rotation, and angular range.
420420
421- Valid point types: START, END, CENTER
421+ Valid point types: START, END, CENTER, MIDPOINT
422422 """
423423 center: Point2D
424424 major_radius: float
@@ -433,10 +433,11 @@ class EllipticalArc(SketchPrimitive):
433433 case PointType.START : return self .point_at_parameter(self .start_param)
434434 case PointType.END : return self .point_at_parameter(self .end_param)
435435 case PointType.CENTER : return self .center
436+ case PointType.MIDPOINT : return self .midpoint
436437 case _: raise ValueError (f " Invalid point type { point_type} for EllipticalArc " )
437438
438439 def get_valid_point_types (self ) -> list[PointType]:
439- return [PointType.START , PointType.END , PointType.CENTER ]
440+ return [PointType.START , PointType.END , PointType.CENTER , PointType. MIDPOINT ]
440441
441442 def point_at_parameter (self , t : float ) -> Point2D:
442443 """ Get point on ellipse at parameter t (radians)."""
@@ -578,6 +579,40 @@ CONSTRAINT_RULES = {
578579 " value_required" : True ,
579580 " notes" : " Value in degrees"
580581 },
582+ ConstraintType.COLLINEAR : {
583+ " min_refs" : 2 ,
584+ " max_refs" : None , # Can chain multiple
585+ " ref_types" : [" line" ],
586+ " value_required" : False ,
587+ },
588+ ConstraintType.DISTANCE_X : {
589+ " min_refs" : 1 ,
590+ " max_refs" : 2 ,
591+ " ref_types" : [" point" ],
592+ " value_required" : True ,
593+ " notes" : " One point = distance from origin; two points = horizontal distance between them"
594+ },
595+ ConstraintType.DISTANCE_Y : {
596+ " min_refs" : 1 ,
597+ " max_refs" : 2 ,
598+ " ref_types" : [" point" ],
599+ " value_required" : True ,
600+ " notes" : " One point = distance from origin; two points = vertical distance between them"
601+ },
602+ ConstraintType.SYMMETRIC : {
603+ " min_refs" : 3 ,
604+ " max_refs" : 3 ,
605+ " ref_types" : [" any" , " any" , " line" ], # Two elements + symmetry axis
606+ " value_required" : False ,
607+ " notes" : " Third reference is the symmetry axis (line)"
608+ },
609+ ConstraintType.MIDPOINT : {
610+ " min_refs" : 2 ,
611+ " max_refs" : 2 ,
612+ " ref_types" : [" point" , " line" ],
613+ " value_required" : False ,
614+ " notes" : " Point constrained to midpoint of line"
615+ },
581616}
582617```
583618
@@ -701,21 +736,25 @@ class SketchDocument:
701736
702737 # ID counters for stable ID generation
703738 _next_index: dict[str , int ] = field(default_factory = lambda : {
704- ElementPrefix.LINE : 0 ,
705- ElementPrefix.ARC : 0 ,
706- ElementPrefix.CIRCLE : 0 ,
707- ElementPrefix.POINT : 0 ,
708- ElementPrefix.SPLINE : 0
739+ ElementPrefix.LINE : 0 ,
740+ ElementPrefix.ARC : 0 ,
741+ ElementPrefix.CIRCLE : 0 ,
742+ ElementPrefix.POINT : 0 ,
743+ ElementPrefix.SPLINE : 0 ,
744+ ElementPrefix.ELLIPSE : 0 ,
745+ ElementPrefix.ELLIPTICAL_ARC : 0 ,
709746 })
710747
711748 def add_primitive (self , primitive : SketchPrimitive) -> str :
712749 """ Add primitive and assign stable ID. Returns the assigned ID."""
713750 prefix = {
714- Line: ElementPrefix.LINE ,
715- Arc: ElementPrefix.ARC ,
751+ Line: ElementPrefix.LINE ,
752+ Arc: ElementPrefix.ARC ,
716753 Circle: ElementPrefix.CIRCLE ,
717- Point: ElementPrefix.POINT ,
718- Spline: ElementPrefix.SPLINE
754+ Point: ElementPrefix.POINT ,
755+ Spline: ElementPrefix.SPLINE ,
756+ Ellipse: ElementPrefix.ELLIPSE ,
757+ EllipticalArc: ElementPrefix.ELLIPTICAL_ARC ,
719758 }[type (primitive)]
720759
721760 idx = self ._next_index[prefix]
@@ -736,6 +775,11 @@ class SketchDocument:
736775
737776 def add_constraint (self , constraint : SketchConstraint) -> None :
738777 """ Add a constraint to the sketch."""
778+ # Validate that all referenced elements exist
779+ for elem_id in constraint.get_element_ids():
780+ if elem_id not in self .primitives:
781+ raise KeyError (f " Constraint references non-existent element ' { elem_id} ' " )
782+
739783 self .constraints.append(constraint)
740784 self .solver_status = SolverStatus.DIRTY # Mark as needing re-solve
741785
@@ -755,41 +799,54 @@ class SketchDocument:
755799 def to_text_description (self , include_point_coords : bool = False ) -> str :
756800 """
757801 Generate human/AI-readable description of the sketch.
758-
802+
759803 Args:
760804 include_point_coords: If True, list all referenceable points with coordinates
761805 """
762- lines = [" Elements:" ]
806+ lines = [f " Sketch: { self .name } " , " " , " Elements:" ]
763807 for id , prim in sorted (self .primitives.items()):
764808 lines.append(f " { self ._describe_primitive(prim)} " )
765809 if include_point_coords:
766810 for pt_type in prim.get_valid_point_types():
767811 pt = prim.get_point(pt_type)
768812 lines.append(f " { id } . { pt_type.value} : ( { pt.x:.2f } , { pt.y:.2f } ) " )
769-
813+
770814 lines.append(" \n Constraints:" )
771815 for c in self .constraints:
772816 lines.append(f " { c} " )
773-
817+
774818 lines.append(f " \n Status: { self .solver_status.value} " )
775819 if self .degrees_of_freedom >= 0 :
776820 lines.append(f " Degrees of Freedom: { self .degrees_of_freedom} " )
777-
821+
778822 return " \n " .join(lines)
779-
823+
780824 def _describe_primitive (self , p : SketchPrimitive) -> str :
825+ const_marker = " [C]" if p.construction else " "
826+
781827 if isinstance (p, Line):
782- return f " { p.id} : Line ( { p.start.x:.2f } , { p.start.y:.2f } ) → ( { p.end.x:.2f } , { p.end.y:.2f } ) "
828+ return f " { p.id} : Line ( { p.start.x:.2f } , { p.start.y:.2f } ) → ( { p.end.x:.2f } , { p.end.y:.2f } ) { const_marker } "
783829 elif isinstance (p, Arc):
784- return f " { p.id} : Arc center=( { p.center.x:.2f } , { p.center.y:.2f } ) r= { p.radius:.2f } { ' CCW' if p.ccw else ' CW' } "
830+ direction = " CCW" if p.ccw else " CW"
831+ return f " { p.id} : Arc center=( { p.center.x:.2f } , { p.center.y:.2f } ) r= { p.radius:.2f } { direction}{ const_marker} "
785832 elif isinstance (p, Circle):
786- return f " { p.id} : Circle center=( { p.center.x:.2f } , { p.center.y:.2f } ) r= { p.radius:.2f } "
833+ return f " { p.id} : Circle center=( { p.center.x:.2f } , { p.center.y:.2f } ) r= { p.radius:.2f }{ const_marker } "
787834 elif isinstance (p, Point):
788- return f " { p.id} : Point ( { p.position.x:.2f } , { p.position.y:.2f } ) "
835+ return f " { p.id} : Point ( { p.position.x:.2f } , { p.position.y:.2f } ) { const_marker } "
789836 elif isinstance (p, Spline):
790- return f " { p.id} : Spline degree= { p.degree} points= { len (p.control_points)} { ' periodic' if p.periodic else ' open' } "
837+ periodic = " periodic" if p.periodic else " open"
838+ return f " { p.id} : Spline degree= { p.degree} points= { len (p.control_points)} { periodic}{ const_marker} "
839+ elif isinstance (p, Ellipse):
840+ import math
841+ rot_deg = math.degrees(p.rotation)
842+ return f " { p.id} : Ellipse center=( { p.center.x:.2f } , { p.center.y:.2f } ) a= { p.major_radius:.2f } b= { p.minor_radius:.2f } rot= { rot_deg:.1f } ° { const_marker} "
843+ elif isinstance (p, EllipticalArc):
844+ import math
845+ direction = " CCW" if p.ccw else " CW"
846+ rot_deg = math.degrees(p.rotation)
847+ return f " { p.id} : EllipticalArc center=( { p.center.x:.2f } , { p.center.y:.2f } ) a= { p.major_radius:.2f } b= { p.minor_radius:.2f } rot= { rot_deg:.1f } ° { direction}{ const_marker} "
791848 else :
792- return f " { p.id} : { type (p).__name__ } "
849+ return f " { p.id} : { type (p).__name__ }{ const_marker } "
793850```
794851
795852---
@@ -2235,11 +2292,29 @@ def primitive_to_dict(p: SketchPrimitive) -> dict:
22352292 " degree" : p.degree,
22362293 " control_points" : [[pt.x, pt.y] for pt in p.control_points],
22372294 " knots" : p.knots,
2238- " weights" : p.weights,
22392295 " periodic" : p.periodic,
22402296 " is_fit_spline" : p.is_fit_spline,
22412297 })
2242-
2298+ if p.weights is not None :
2299+ base[" weights" ] = p.weights
2300+ elif isinstance (p, Ellipse):
2301+ base.update({
2302+ " center" : [p.center.x, p.center.y],
2303+ " major_radius" : p.major_radius,
2304+ " minor_radius" : p.minor_radius,
2305+ " rotation" : p.rotation,
2306+ })
2307+ elif isinstance (p, EllipticalArc):
2308+ base.update({
2309+ " center" : [p.center.x, p.center.y],
2310+ " major_radius" : p.major_radius,
2311+ " minor_radius" : p.minor_radius,
2312+ " rotation" : p.rotation,
2313+ " start_param" : p.start_param,
2314+ " end_param" : p.end_param,
2315+ " ccw" : p.ccw,
2316+ })
2317+
22432318 return base
22442319
22452320def constraint_to_dict (c : SketchConstraint) -> dict :
@@ -2249,15 +2324,22 @@ def constraint_to_dict(c: SketchConstraint) -> dict:
22492324 refs.append({" element" : r.element_id, " point" : r.point_type.value})
22502325 else :
22512326 refs.append(r)
2252-
2253- return {
2327+
2328+ result = {
22542329 " id" : c.id,
22552330 " type" : c.constraint_type.value,
22562331 " references" : refs,
2257- " value" : c.value,
2258- " inferred" : c.inferred,
2259- " confidence" : c.confidence,
22602332 }
2333+
2334+ # Add optional fields only if they have non-default values
2335+ if c.value is not None :
2336+ result[" value" ] = c.value
2337+ if c.inferred:
2338+ result[" inferred" ] = c.inferred
2339+ if c.confidence != 1.0 :
2340+ result[" confidence" ] = c.confidence
2341+
2342+ return result
22612343```
22622344
22632345### 13.2 Example JSON Output
0 commit comments