Skip to content

Commit a996afa

Browse files
Sync SPECIFICATION.md with implementation
Phase 3 of code review fix plan: - Fix ElementPrefix.ELLIPTICAL_ARC from "EA" to "e" (issue #23) - Add missing CONSTRAINT_RULES: COLLINEAR, DISTANCE_X/Y, SYMMETRIC, MIDPOINT (issue #24) - Add Ellipse/EllipticalArc to _next_index and add_primitive (issue #25) - Add Ellipse/EllipticalArc to primitive_to_dict, fix constraint_to_dict (issue #26) - Add MIDPOINT to EllipticalArc valid point types (issue #27) - Add sketch name header, [C] marker, Ellipse/EllipticalArc to _describe_primitive (issue #28) - Add reference validation to add_constraint (issue #29) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 34f46f2 commit a996afa

1 file changed

Lines changed: 113 additions & 31 deletions

File tree

SPECIFICATION.md

Lines changed: 113 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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("\nConstraints:")
771815
for c in self.constraints:
772816
lines.append(f" {c}")
773-
817+
774818
lines.append(f"\nStatus: {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

22452320
def 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

Comments
 (0)