Files
cadSet/SimpleCADAPI/examples/08_constrained_sketch.py

726 lines
22 KiB
Python

"""Constrained sketch-first modeling with isomorphic SimpleCADAPI calls.
Run from the repository root with:
uv run python examples/08_constrained_sketch.py
Generated files:
examples/out/constrained_sketch.model.json
examples/out/constrained_sketch.step
examples/out/constrained_sketch.fcstd
When the intent is a sketch/profile, use the sketch APIs. Concrete geometry
APIs remain for paths, pure geometry, and lowering targets.
"""
from __future__ import annotations
import json
from pathlib import Path
import simplecadapi as scad
OUT = Path("examples/out")
MODEL_JSON_PATH = OUT / "constrained_sketch.model.json"
STEP_PATH = OUT / "constrained_sketch.step"
FCSTD_PATH = OUT / "constrained_sketch.fcstd"
FREECAD_CMD = Path("/Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd")
def _solve_and_report(name: str, sketch: scad.Sketch) -> None:
result = scad.inspect_sketch_rsketchresult(
sketch=sketch,
require_fully_constrained=True,
)
points = sorted(
(point_id, round(point[0], 3), round(point[1], 3))
for point_id, point in result.solved_points.items()
)
scalars = sorted(
(key, round(value, 3)) for key, value in result.solved_scalars.items()
)
print(
f"{name}_sketch",
result.status,
"dof",
result.dof,
"residual",
f"{result.residual_norm:.2e}",
"points",
points[:4],
"scalars",
scalars[:2],
)
@scad.requires_session
def _promote_face(name: str, sketch: scad.Sketch):
_solve_and_report(name=name, sketch=sketch)
return scad.make_face_from_sketch_rface(
sketch=sketch,
require_fully_constrained=True,
)
@scad.requires_session
def make_rect_profile(name, x0, y0, width, height):
sketch = scad.make_sketch_rsketch(name=name, plane="XY")
sketch = scad.add_point_rsketch(sketch=sketch, point_id="p0", x=x0, y=y0)
sketch = scad.add_point_rsketch(
sketch=sketch,
point_id="p1",
x=x0 + width,
y=y0,
)
sketch = scad.add_point_rsketch(
sketch=sketch,
point_id="p2",
x=x0 + width,
y=y0 + height,
)
sketch = scad.add_point_rsketch(
sketch=sketch,
point_id="p3",
x=x0,
y=y0 + height,
)
sketch = scad.add_line_rsketch(
sketch=sketch,
entity_id="bottom",
start="p0",
end="p1",
)
sketch = scad.add_line_rsketch(
sketch=sketch,
entity_id="right",
start="p1",
end="p2",
)
sketch = scad.add_line_rsketch(
sketch=sketch,
entity_id="top",
start="p2",
end="p3",
)
sketch = scad.add_line_rsketch(
sketch=sketch,
entity_id="left",
start="p3",
end="p0",
)
sketch = scad.constrain_horizontal_rsketch(sketch=sketch, line="bottom")
sketch = scad.constrain_vertical_rsketch(sketch=sketch, line="right")
sketch = scad.constrain_parallel_rsketch(
sketch=sketch,
a="bottom",
b="top",
)
sketch = scad.constrain_parallel_rsketch(
sketch=sketch,
a="left",
b="right",
)
sketch = scad.constrain_perpendicular_rsketch(
sketch=sketch,
a="bottom",
b="right",
)
sketch = scad.constrain_equal_length_rsketch(
sketch=sketch,
a="bottom",
b="top",
)
sketch = scad.constrain_equal_length_rsketch(
sketch=sketch,
a="left",
b="right",
)
sketch = scad.constrain_distance_rsketch(
sketch=sketch,
a="p0",
b="p1",
value=width,
)
sketch = scad.constrain_distance_rsketch(
sketch=sketch,
a="p0",
b="p3",
value=height,
)
sketch = scad.constrain_fix_rsketch(sketch=sketch, target="p0")
return _promote_face(name=name, sketch=sketch)
@scad.requires_session
def make_circle_profile(name, center_x, center_y, radius, circle_id):
sketch = scad.make_sketch_rsketch(name=name, plane="XY")
sketch = scad.add_point_rsketch(
sketch=sketch,
point_id="center",
x=center_x,
y=center_y,
)
sketch = scad.add_circle_rsketch(
sketch=sketch,
entity_id=circle_id,
center="center",
radius=radius,
)
sketch = scad.constrain_fix_rsketch(sketch=sketch, target="center")
sketch = scad.constrain_radius_rsketch(
sketch=sketch,
circle=circle_id,
value=radius,
)
return _promote_face(name=name, sketch=sketch)
@scad.requires_session
def make_guided_diamond_profile(name, center_x, center_y, width, height, guide_gap):
half_w = width / 2.0
half_h = height / 2.0
sketch = scad.make_sketch_rsketch(name=name, plane="XY")
for point_id, x, y in (
("center", center_x, center_y),
("left", center_x - half_w, center_y),
("top", center_x, center_y + half_h),
("right", center_x + half_w, center_y),
("bottom", center_x, center_y - half_h),
("guide_upper_start", center_x - half_w, center_y + guide_gap),
("guide_upper_end", center_x, center_y + half_h + guide_gap),
("guide_lower_start", center_x + half_w, center_y - guide_gap),
("guide_lower_end", center_x, center_y - half_h - guide_gap),
):
sketch = scad.add_point_rsketch(
sketch=sketch,
point_id=point_id,
x=x,
y=y,
)
for entity_id, start, end, construction in (
("bottom_left", "left", "bottom", False),
("right_bottom", "bottom", "right", False),
("top_right", "right", "top", False),
("left_top", "top", "left", False),
("guide_upper", "guide_upper_start", "guide_upper_end", True),
("guide_lower", "guide_lower_start", "guide_lower_end", True),
):
sketch = scad.add_line_rsketch(
sketch=sketch,
entity_id=entity_id,
start=start,
end=end,
construction=construction,
)
sketch = scad.constrain_fix_rsketch(sketch=sketch, target="center")
for function, a, b, value in (
(scad.constrain_distance_x_rsketch, "left", "center", half_w),
(scad.constrain_distance_y_rsketch, "left", "center", 0.0),
(scad.constrain_distance_x_rsketch, "center", "right", half_w),
(scad.constrain_distance_y_rsketch, "center", "right", 0.0),
(scad.constrain_distance_x_rsketch, "center", "top", 0.0),
(scad.constrain_distance_y_rsketch, "center", "top", half_h),
(scad.constrain_distance_x_rsketch, "bottom", "center", 0.0),
(scad.constrain_distance_y_rsketch, "bottom", "center", half_h),
):
sketch = function(sketch=sketch, a=a, b=b, value=value)
for function, a, b in (
(scad.constrain_parallel_rsketch, "left_top", "right_bottom"),
(scad.constrain_parallel_rsketch, "top_right", "bottom_left"),
(scad.constrain_equal_length_rsketch, "left_top", "top_right"),
(scad.constrain_equal_length_rsketch, "top_right", "right_bottom"),
(scad.constrain_equal_length_rsketch, "right_bottom", "bottom_left"),
):
sketch = function(sketch=sketch, a=a, b=b)
for function, a, b, value in (
(scad.constrain_distance_x_rsketch, "left", "guide_upper_start", 0.0),
(scad.constrain_distance_y_rsketch, "left", "guide_upper_start", guide_gap),
(scad.constrain_distance_x_rsketch, "top", "guide_upper_end", 0.0),
(scad.constrain_distance_y_rsketch, "top", "guide_upper_end", guide_gap),
(scad.constrain_distance_x_rsketch, "guide_lower_start", "right", 0.0),
(scad.constrain_distance_y_rsketch, "guide_lower_start", "right", guide_gap),
(scad.constrain_distance_x_rsketch, "guide_lower_end", "bottom", 0.0),
(scad.constrain_distance_y_rsketch, "guide_lower_end", "bottom", guide_gap),
):
sketch = function(sketch=sketch, a=a, b=b, value=value)
for function, a, b in (
(scad.constrain_parallel_rsketch, "guide_upper", "guide_lower"),
(scad.constrain_parallel_rsketch, "guide_upper", "right_bottom"),
(scad.constrain_parallel_rsketch, "guide_lower", "left_top"),
(scad.constrain_equal_length_rsketch, "guide_upper", "right_bottom"),
(scad.constrain_equal_length_rsketch, "guide_lower", "left_top"),
):
sketch = function(sketch=sketch, a=a, b=b)
return _promote_face(name=name, sketch=sketch)
@scad.requires_session
def make_curve_guided_relief_profile(name, center_x, center_y, radius, guide_span):
sketch = scad.make_sketch_rsketch(name=name, plane="XY")
for point_id, x, y in (
("center", center_x, center_y),
("rim", center_x + radius, center_y),
("clearance_center", center_x, center_y),
("upper_left", center_x - guide_span, center_y + radius),
("upper_right", center_x + guide_span, center_y + radius),
("lower_left", center_x - guide_span, center_y - radius),
("lower_right", center_x + guide_span, center_y - radius),
):
sketch = scad.add_point_rsketch(
sketch=sketch,
point_id=point_id,
x=x,
y=y,
)
sketch = scad.add_circle_rsketch(
sketch=sketch,
entity_id="relief",
center="center",
radius=radius,
)
sketch = scad.add_circle_rsketch(
sketch=sketch,
entity_id="clearance",
center="clearance_center",
radius=radius,
construction=True,
)
for entity_id, start, end in (
("radius_probe", "center", "rim"),
("upper_rail", "upper_left", "upper_right"),
("lower_rail", "lower_left", "lower_right"),
):
sketch = scad.add_line_rsketch(
sketch=sketch,
entity_id=entity_id,
start=start,
end=end,
construction=True,
)
sketch = scad.constrain_fix_rsketch(sketch=sketch, target="center")
sketch = scad.constrain_radius_rsketch(
sketch=sketch,
circle="relief",
value=radius,
)
sketch = scad.constrain_point_on_rsketch(
sketch=sketch,
point="rim",
entity="relief",
)
sketch = scad.constrain_horizontal_rsketch(
sketch=sketch,
line="radius_probe",
)
sketch = scad.constrain_length_rsketch(
sketch=sketch,
line="radius_probe",
value=radius,
)
sketch = scad.constrain_concentric_rsketch(
sketch=sketch,
a="relief",
b="clearance",
)
sketch = scad.constrain_equal_radius_rsketch(
sketch=sketch,
a="relief",
b="clearance",
)
sketch = scad.constrain_horizontal_rsketch(
sketch=sketch,
line="upper_rail",
)
sketch = scad.constrain_horizontal_rsketch(
sketch=sketch,
line="lower_rail",
)
sketch = scad.constrain_tangent_rsketch(
sketch=sketch,
a="upper_rail",
b="relief",
)
sketch = scad.constrain_tangent_rsketch(
sketch=sketch,
a="lower_rail",
b="relief",
)
for a, b, value in (
("center", "upper_left", -guide_span),
("center", "upper_right", guide_span),
("center", "lower_left", -guide_span),
("center", "lower_right", guide_span),
):
sketch = scad.constrain_distance_x_rsketch(
sketch=sketch,
a=a,
b=b,
value=value,
)
return _promote_face(name=name, sketch=sketch)
@scad.model(graph_id="constrained_sketch")
def build_model():
plate_w = scad.var(name="plate_w", default=96.0, comment="plate width")
plate_h = scad.var(name="plate_h", default=54.0, comment="plate height")
plate_t = scad.var(name="plate_t", default=6.0, comment="plate thickness")
boss_r = scad.var(
name="boss_r",
default=14.0,
comment="raised center boss radius",
)
boss_h = scad.var(
name="boss_h",
default=5.0,
comment="raised center boss height",
)
bore_r = scad.var(name="bore_r", default=5.0, comment="through bore radius")
mount_r = scad.var(
name="mount_r",
default=3.0,
comment="mounting hole radius",
)
margin_x = scad.var(
name="mount_margin_x",
default=12.0,
comment="mounting hole x margin",
)
margin_y = scad.var(
name="mount_margin_y",
default=9.0,
comment="mounting hole y margin",
)
slot_w = scad.var(name="slot_w", default=34.0, comment="service slot width")
slot_h = scad.var(name="slot_h", default=8.0, comment="service slot height")
slot_y = scad.var(
name="slot_center_y",
default=16.0,
comment="service slot center y",
)
diamond_w = scad.var(
name="guided_diamond_w",
default=14.0,
comment="guided diamond pocket width",
)
diamond_h = scad.var(
name="guided_diamond_h",
default=8.0,
comment="guided diamond pocket height",
)
diamond_guide_gap = scad.var(
name="guided_diamond_guide_gap",
default=5.0,
comment="parallel guide rail offset",
)
relief_r = scad.var(
name="curve_relief_r",
default=4.0,
comment="curve-guided relief radius",
)
relief_guide_span = scad.var(
name="curve_relief_guide_span",
default=9.0,
comment="curve relief construction rail half span",
)
center_x = plate_w / 2.0
center_y = plate_h / 2.0
plate_profile = make_rect_profile(
name="plate_outline",
x0=0.0,
y0=0.0,
width=plate_w,
height=plate_h,
)
plate_profile = scad.apply_tag(
shape=plate_profile,
tag="demo.profile.plate",
)
plate = scad.extrude_rsolid(
profile=plate_profile,
direction=(0.0, 0.0, 1.0),
distance=plate_t,
tag_prefix="constrained_sketch.plate",
result_tag="part.constrained_sketch.base_plate",
)
plate = scad.apply_tag(shape=plate, tag="demo.body.base_plate")
boss_profile = make_circle_profile(
name="center_boss",
center_x=center_x,
center_y=center_y,
radius=boss_r,
circle_id="boss_outer",
)
boss_overlap = 1.0
boss = scad.extrude_rsolid(
profile=boss_profile,
direction=(0.0, 0.0, 1.0),
distance=boss_h + boss_overlap,
tag_prefix="constrained_sketch.boss",
result_tag="part.constrained_sketch.raised_boss",
)
boss = scad.translate_shape(
shape=boss,
vector=(0.0, 0.0, plate_t - boss_overlap),
)
boss = scad.apply_tag(shape=boss, tag="demo.body.raised_boss")
body = scad.union_rsolid(plate, boss, glue=False)
bore_profile = make_circle_profile(
name="center_bore",
center_x=center_x,
center_y=center_y,
radius=bore_r,
circle_id="bore",
)
bore_cutter = scad.extrude_rsolid(
profile=bore_profile,
direction=(0.0, 0.0, 1.0),
distance=plate_t + boss_h + 2.0,
tag_prefix="constrained_sketch.bore.cutter",
result_tag="tool.constrained_sketch.center_bore",
)
bore_cutter = scad.translate_shape(
shape=bore_cutter,
vector=(0.0, 0.0, -1.0),
)
slot_profile = make_rect_profile(
name="service_slot",
x0=center_x - slot_w / 2.0,
y0=slot_y - slot_h / 2.0,
width=slot_w,
height=slot_h,
)
slot_cutter = scad.extrude_rsolid(
profile=slot_profile,
direction=(0.0, 0.0, 1.0),
distance=plate_t + 2.0,
tag_prefix="constrained_sketch.slot.cutter",
result_tag="tool.constrained_sketch.service_slot",
)
slot_cutter = scad.translate_shape(
shape=slot_cutter,
vector=(0.0, 0.0, -1.0),
)
diamond_profile = make_guided_diamond_profile(
name="guided_diamond_pocket",
center_x=plate_w - 24.0,
center_y=plate_h - 18.0,
width=diamond_w,
height=diamond_h,
guide_gap=diamond_guide_gap,
)
diamond_cutter = scad.extrude_rsolid(
profile=diamond_profile,
direction=(0.0, 0.0, 1.0),
distance=plate_t + 2.0,
tag_prefix="constrained_sketch.diamond.cutter",
result_tag="tool.constrained_sketch.diamond_pocket",
)
diamond_cutter = scad.translate_shape(
shape=diamond_cutter,
vector=(0.0, 0.0, -1.0),
)
curve_relief_profile = make_curve_guided_relief_profile(
name="curve_guided_relief",
center_x=plate_w / 3.0,
center_y=plate_h - 12.0,
radius=relief_r,
guide_span=relief_guide_span,
)
curve_relief_cutter = scad.extrude_rsolid(
profile=curve_relief_profile,
direction=(0.0, 0.0, 1.0),
distance=plate_t + 2.0,
tag_prefix="constrained_sketch.curve_relief.cutter",
result_tag="tool.constrained_sketch.curve_relief",
)
curve_relief_cutter = scad.translate_shape(
shape=curve_relief_cutter,
vector=(0.0, 0.0, -1.0),
)
mount_centers = [
("mount_sw", margin_x, margin_y),
("mount_se", plate_w - margin_x, margin_y),
("mount_ne", plate_w - margin_x, plate_h - margin_y),
("mount_nw", margin_x, plate_h - margin_y),
]
mount_cutters = []
for name, x_pos, y_pos in mount_centers:
mount_tag = name.replace("_", ".")
mount_profile = make_circle_profile(
name=name,
center_x=x_pos,
center_y=y_pos,
radius=mount_r,
circle_id="mount_hole",
)
mount_cutter = scad.extrude_rsolid(
profile=mount_profile,
direction=(0.0, 0.0, 1.0),
distance=plate_t + 2.0,
tag_prefix=f"constrained_sketch.{mount_tag}.cutter",
result_tag=f"tool.constrained_sketch.{mount_tag}",
)
mount_cutters.append(
scad.translate_shape(
shape=mount_cutter,
vector=(0.0, 0.0, -1.0),
)
)
part = scad.cut_rsolid(
body,
bore_cutter,
slot_cutter,
diamond_cutter,
curve_relief_cutter,
mount_cutters,
skip_non_intersecting=False,
)
part = scad.apply_tag(
shape=part,
tag="demo.constrained_sketch_bracket",
)
scad.capture_result(value=part)
return {
"part": part,
"plate_profile": plate_profile,
"diamond_profile": diamond_profile,
"curve_relief_profile": curve_relief_profile,
}
def main() -> None:
OUT.mkdir(parents=True, exist_ok=True)
result = build_model()
data = result.value
MODEL_JSON_PATH.write_text(result.model_json, encoding="utf-8")
rebuilt = result.replay()
scad.export_step(shapes=rebuilt, filename=str(STEP_PATH))
freecad_cmd = str(FREECAD_CMD) if FREECAD_CMD.exists() else None
scad.translator.freecad_translator.translate_model_json_to_fcstd(
json_str=result.model_json,
output_path=str(FCSTD_PATH),
document_name="SimpleCADConstrainedSketchDemo",
freecad_cmd=freecad_cmd,
)
payload = json.loads(result.model_json)
ops = [node["op"] for node in payload["graph"]["nodes"]]
promotion_nodes = [
node
for node in payload["graph"]["nodes"]
if node["op"]
in {"make_face_from_sketch_rface", "make_wire_from_sketch_rwire"}
]
diamond_promotion = next(
node
for node in promotion_nodes
if node["params"]["sketch"].get("name") == "guided_diamond_pocket"
)
diamond_constraints = diamond_promotion["params"]["sketch"].get(
"constraints",
[],
)
curve_promotion = next(
node
for node in promotion_nodes
if node["params"]["sketch"].get("name") == "curve_guided_relief"
)
curve_constraints = curve_promotion["params"]["sketch"].get(
"constraints",
[],
)
sketch_entity_tags = sorted(
tag
for edge in scad.ql.select(items=data["plate_profile"].get_edges())
.where(scad.ql.tag(pattern="sketch_entity.*"))
.all()
for tag in scad.list_tags(shape=edge)
if tag.startswith("sketch_entity.")
)
diamond_entity_tags = sorted(
tag
for edge in scad.ql.select(items=data["diamond_profile"].get_edges())
.where(scad.ql.tag(pattern="sketch_entity.*"))
.all()
for tag in scad.list_tags(shape=edge)
if tag.startswith("sketch_entity.")
)
curve_entity_tags = sorted(
tag
for edge in scad.ql.select(items=data["curve_relief_profile"].get_edges())
.where(scad.ql.tag(pattern="sketch_entity.*"))
.all()
for tag in scad.list_tags(shape=edge)
if tag.startswith("sketch_entity.")
)
print("graph_nodes", len(ops))
print("sketch_ops", sum(1 for op in ops if "sketch" in op))
print("promotion_nodes", len(promotion_nodes))
print(
"promotion_solve_snapshots",
sum(
1
for node in promotion_nodes
if "solve_snapshot" in node.get("params", {})
),
)
print("contains_public_solve_node", "make_solve_sketch_rsketchresult" in ops)
print("plate_sketch_entity_tags", sketch_entity_tags)
print("diamond_sketch_entity_tags", diamond_entity_tags)
print("diamond_constraint_count", len(diamond_constraints))
print(
"diamond_parallel_equal_constraints",
sum(
1
for constraint in diamond_constraints
if constraint.get("kind") in {"parallel", "equal_length"}
),
)
print("curve_sketch_entity_tags", curve_entity_tags)
print("curve_constraint_count", len(curve_constraints))
print(
"curve_tangent_equal_radius_constraints",
sum(
1
for constraint in curve_constraints
if constraint.get("kind")
in {"tangent", "equal_radius", "concentric", "point_on"}
),
)
print("volume", round(data["part"].get_volume(), 3))
print("wrote", MODEL_JSON_PATH)
print("wrote", STEP_PATH)
print("wrote", FCSTD_PATH)
if __name__ == "__main__":
main()