open_slot 的第二次迭代

This commit is contained in:
2026-09-07 17:44:05 +08:00
parent 9a3e09b9f5
commit a208ea5c8b
2 changed files with 55 additions and 9 deletions
@@ -18,6 +18,9 @@ from .operation_contracts import materialized_feature_contracts
_SELECTOR_REQUIRED = frozenset({"fillet", "chamfer"}) _SELECTOR_REQUIRED = frozenset({"fillet", "chamfer"})
_SKETCH_ATOM_PREFIXES = ("extrude_", "revolve_") _SKETCH_ATOM_PREFIXES = ("extrude_", "revolve_")
# 开放轮廓(closed=false / role=open)只有"刀具截面补槽口边闭合后作切除"的
# 物理意义:仅 extrude 直切类原子支持;add/回转对开放轮廓会造出无意义的封块。
_OPEN_PROFILE_ATOMICS = frozenset({"extrude_cut_blind", "extrude_cut_through"})
_PRIMARY_ATOMICS = frozenset({ _PRIMARY_ATOMICS = frozenset({
"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind",
"extrude_cut_through", "extrude_cut_through",
@@ -281,6 +284,17 @@ class CapabilityAnalyzer:
"The resolved sketch contains no closed profile region", "The resolved sketch contains no closed profile region",
sketch_id=node.sketch_id, sketch_id=node.sketch_id,
)) ))
elif (
not resolution_error
and sketches[node.sketch_id].get("_open_contour")
and node.atomic_id not in _OPEN_PROFILE_ATOMICS
):
blockers.append(self._blocker(
node.feature_id, "unsupported_open_profile",
"Open profiles are only supported for straight extruded cut features",
sketch_id=node.sketch_id,
atomic_id=node.atomic_id,
))
if node.atomic_id.startswith(_SKETCH_ATOM_PREFIXES): if node.atomic_id.startswith(_SKETCH_ATOM_PREFIXES):
# #2 draftextrudeParams.draft 在 cdsl_schema.json 中被允许, # #2 draftextrudeParams.draft 在 cdsl_schema.json 中被允许,
# 但 runtime 的拉伸执行器(build123d Solid.extrude)没有锥形 # 但 runtime 的拉伸执行器(build123d Solid.extrude)没有锥形
+41 -9
View File
@@ -121,7 +121,9 @@ def _reverse(edge: _Ctx) -> _Ctx:
return output return output
def _join(edges: list[_Ctx]) -> list[_Ctx]: def _join(edges: list[_Ctx], *, allow_open: bool = False) -> list[_Ctx]:
# 将边排序成一条连通链。allow_open=False(默认)要求首尾相接成闭合环;
# allow_open=True 时允许链首尾不接(供开放轮廓先拼链、后补闭合边)。
if not edges: if not edges:
return [] return []
remaining = [deepcopy(edge) for edge in edges] remaining = [deepcopy(edge) for edge in edges]
@@ -137,11 +139,22 @@ def _join(edges: list[_Ctx]) -> list[_Ctx]:
break break
else: else:
raise ValueError("analytic_contours: segments do not form a connected contour") raise ValueError("analytic_contours: segments do not form a connected contour")
if _distance(ordered[0]["start_mm"], ordered[-1]["end_mm"]) > _TOLERANCE_MM: if not allow_open and _distance(ordered[0]["start_mm"], ordered[-1]["end_mm"]) > _TOLERANCE_MM:
raise ValueError("analytic_contours: closed contour endpoints do not meet") raise ValueError("analytic_contours: closed contour endpoints do not meet")
return ordered return ordered
def _close_open_contour(edges: list[_Ctx]) -> tuple[list[_Ctx], bool]:
# 开放链补闭合边:若首尾未相接,则沿两点连线补一条直线边形成闭合环。
# 返回 (edges, opened)opened=False 表示首尾已天然相接(无需补边)。
if len(edges) < 2:
raise ValueError("analytic_contours: open contour needs at least 2 connected segments")
if _distance(edges[0]["start_mm"], edges[-1]["end_mm"]) <= _TOLERANCE_MM:
return edges, False
closing = _contour_line(edges[-1]["end_mm"][:2], edges[0]["start_mm"][:2])
return [*edges, closing], True
def _circle_edges(segment: _Ctx) -> list[_Ctx]: def _circle_edges(segment: _Ctx) -> list[_Ctx]:
center = segment.get("center") or [0.0, 0.0] center = segment.get("center") or [0.0, 0.0]
radius = float(segment.get("radius_mm") or 0.0) radius = float(segment.get("radius_mm") or 0.0)
@@ -239,22 +252,35 @@ def _gen_analytic_contours(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[
loops: list[_Ctx] = [] loops: list[_Ctx] = []
entities: list[_Ctx] = [] entities: list[_Ctx] = []
for index, contour in enumerate(profile.get("contours") or []): for index, contour in enumerate(profile.get("contours") or []):
if not contour.get("closed"): # 开放轮廓(closed=false / role=open):先按开放链拼装,再在首尾间补一条
raise ValueError(f"analytic_contours: contour {index} is open") # 闭合边成闭合 region —— V 槽/开放型腔切除的刀具截面本就靠这条"槽口边"
# 闭合,故 B-rep 层可完全复用闭合链路。
contour_open = not bool(contour.get("closed", False))
if contour_open and contour.get("role") == "inner":
raise ValueError(f"analytic_contours: inner contour {index} cannot be open")
raw_edges: list[_Ctx] = [] raw_edges: list[_Ctx] = []
for segment in contour.get("segments") or []: for segment in contour.get("segments") or []:
if segment.get("type") == "line": if segment.get("type") == "line":
entities.append(_line(segment["start"], segment["end"])) entities.append(_line(segment["start"], segment["end"]))
elif segment.get("type") == "circle": elif segment.get("type") == "circle":
if contour_open:
raise ValueError(f"analytic_contours: open contour {index} cannot contain a full circle segment")
entities.append(_circle(segment.get("center") or [0.0, 0.0], float(segment.get("radius_mm") or 0.0))) entities.append(_circle(segment.get("center") or [0.0, 0.0], float(segment.get("radius_mm") or 0.0)))
raw_edges.extend(_segment_edges(segment)) raw_edges.extend(_segment_edges(segment))
if raw_edges: if raw_edges:
edges = _join(raw_edges) edges = _join(raw_edges, allow_open=contour_open)
contour_opened = False
if contour_open:
if _distance(edges[0]["start_mm"], edges[-1]["end_mm"]) <= _TOLERANCE_MM:
raise ValueError(f"analytic_contours: contour {index} is geometrically closed; use closed=true")
edges, contour_opened = _close_open_contour(edges)
meta["_has_open_contour"] = True
_normalize_quarter_rounding_direction(edges) _normalize_quarter_rounding_direction(edges)
sample = _sample_loop(edges) sample = _sample_loop(edges)
if _area(sample) <= _TOLERANCE_MM * _TOLERANCE_MM: if _area(sample) <= _TOLERANCE_MM * _TOLERANCE_MM:
raise ValueError(f"analytic_contours: contour {index} is degenerate") raise ValueError(f"analytic_contours: contour {index} is degenerate")
loops.append({"edges": edges, "points": sample, "area": _area(sample)}) loops.append({"edges": edges, "points": sample, "area": _area(sample),
"open": contour_open and contour_opened})
for segment in profile.get("construction") or []: for segment in profile.get("construction") or []:
if segment.get("type") == "line": if segment.get("type") == "line":
entities.append(_line(segment["start"], segment["end"], construction=True)) entities.append(_line(segment["start"], segment["end"], construction=True))
@@ -265,11 +291,13 @@ def _gen_analytic_contours(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[
for loop in loops: for loop in loops:
loop["role"] = "inner" if sum(_contains(loop["points"][0], other["points"]) for other in loops if other is not loop) % 2 else "outer" loop["role"] = "inner" if sum(_contains(loop["points"][0], other["points"]) for other in loops if other is not loop) % 2 else "outer"
outers = [loop for loop in loops if loop["role"] == "outer"] outers = [loop for loop in loops if loop["role"] == "outer"]
regions = [{"outer": outer["edges"], "holes": []} for outer in outers] regions = [{"outer": outer["edges"], "holes": [], "open": bool(outer.get("open"))} for outer in outers]
for inner in (loop for loop in loops if loop["role"] == "inner"): for inner in (loop for loop in loops if loop["role"] == "inner"):
containing = [outer for outer in outers if _contains(inner["points"][0], outer["points"])] containing = [outer for outer in outers if _contains(inner["points"][0], outer["points"])]
if not containing: if not containing:
raise ValueError("analytic_contours: inner contour has no containing outer contour") raise ValueError("analytic_contours: inner contour has no containing outer contour")
if any(outer.get("open") for outer in containing):
raise ValueError("analytic_contours: an open outer contour cannot contain nested holes")
selected = min(containing, key=lambda outer: outer["area"]) selected = min(containing, key=lambda outer: outer["area"])
regions[outers.index(selected)]["holes"].append(inner["edges"]) regions[outers.index(selected)]["holes"].append(inner["edges"])
meta["_regions"] = regions meta["_regions"] = regions
@@ -300,7 +328,7 @@ def resolve_profile(sketch: _Ctx) -> _Ctx:
generator = SHAPE_GENERATORS.get(profile.get("type")) generator = SHAPE_GENERATORS.get(profile.get("type"))
if generator is None: if generator is None:
raise ValueError(f"sketch {sketch.get('id')}: unsupported profile type {profile.get('type')!r}") raise ValueError(f"sketch {sketch.get('id')}: unsupported profile type {profile.get('type')!r}")
meta: _Ctx = {"id": sketch.get("id"), "_entities": sketch.get("entities"), "_regions": []} meta: _Ctx = {"id": sketch.get("id"), "_entities": sketch.get("entities"), "_regions": [], "_has_open_contour": False}
entities, contour = generator(profile, meta) entities, contour = generator(profile, meta)
output = deepcopy(sketch) output = deepcopy(sketch)
original_circles = [entity for entity in sketch.get("entities") or [] if entity.get("type") == "circle" and not entity.get("construction")] original_circles = [entity for entity in sketch.get("entities") or [] if entity.get("type") == "circle" and not entity.get("construction")]
@@ -310,9 +338,13 @@ def resolve_profile(sketch: _Ctx) -> _Ctx:
output["contour_edges_mm"] = _transform_contours(contour, workplane) if workplane else contour output["contour_edges_mm"] = _transform_contours(contour, workplane) if workplane else contour
if meta["_regions"]: if meta["_regions"]:
output["contour_regions_mm"] = [ output["contour_regions_mm"] = [
{"outer": _transform_contours(region["outer"], workplane) if workplane else region["outer"], "holes": [_transform_contours(hole, workplane) if workplane else hole for hole in region.get("holes") or []]} {"outer": _transform_contours(region["outer"], workplane) if workplane else region["outer"],
"holes": [_transform_contours(hole, workplane) if workplane else hole for hole in region.get("holes") or []],
"open": bool(region.get("open"))}
for region in meta["_regions"] for region in meta["_regions"]
] ]
if meta["_has_open_contour"]:
output["_open_contour"] = True
return output return output