From a208ea5c8b4f7f5636802400971a143a637c774f Mon Sep 17 00:00:00 2001 From: ganjihong Date: Mon, 7 Sep 2026 17:44:05 +0800 Subject: [PATCH] =?UTF-8?q?open=5Fslot=20=E7=9A=84=E7=AC=AC=E4=BA=8C?= =?UTF-8?q?=E6=AC=A1=E8=BF=AD=E4=BB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/engine/cdsl_engine/capabilities.py | 14 ++++++ backend/engine/cdsl_engine/sketch_solver.py | 50 +++++++++++++++++---- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/backend/engine/cdsl_engine/capabilities.py b/backend/engine/cdsl_engine/capabilities.py index ef22863d..dd023cc7 100644 --- a/backend/engine/cdsl_engine/capabilities.py +++ b/backend/engine/cdsl_engine/capabilities.py @@ -18,6 +18,9 @@ from .operation_contracts import materialized_feature_contracts _SELECTOR_REQUIRED = frozenset({"fillet", "chamfer"}) _SKETCH_ATOM_PREFIXES = ("extrude_", "revolve_") +# 开放轮廓(closed=false / role=open)只有"刀具截面补槽口边闭合后作切除"的 +# 物理意义:仅 extrude 直切类原子支持;add/回转对开放轮廓会造出无意义的封块。 +_OPEN_PROFILE_ATOMICS = frozenset({"extrude_cut_blind", "extrude_cut_through"}) _PRIMARY_ATOMICS = frozenset({ "extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_through", @@ -281,6 +284,17 @@ class CapabilityAnalyzer: "The resolved sketch contains no closed profile region", 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): # #2 draft:extrudeParams.draft 在 cdsl_schema.json 中被允许, # 但 runtime 的拉伸执行器(build123d Solid.extrude)没有锥形 diff --git a/backend/engine/cdsl_engine/sketch_solver.py b/backend/engine/cdsl_engine/sketch_solver.py index 29bff622..8f1d3093 100644 --- a/backend/engine/cdsl_engine/sketch_solver.py +++ b/backend/engine/cdsl_engine/sketch_solver.py @@ -121,7 +121,9 @@ def _reverse(edge: _Ctx) -> _Ctx: 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: return [] remaining = [deepcopy(edge) for edge in edges] @@ -137,11 +139,22 @@ def _join(edges: list[_Ctx]) -> list[_Ctx]: break else: 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") 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]: center = segment.get("center") or [0.0, 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] = [] entities: list[_Ctx] = [] for index, contour in enumerate(profile.get("contours") or []): - if not contour.get("closed"): - raise ValueError(f"analytic_contours: contour {index} is open") + # 开放轮廓(closed=false / role=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] = [] for segment in contour.get("segments") or []: if segment.get("type") == "line": entities.append(_line(segment["start"], segment["end"])) 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))) raw_edges.extend(_segment_edges(segment)) 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) sample = _sample_loop(edges) if _area(sample) <= _TOLERANCE_MM * _TOLERANCE_MM: 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 []: if segment.get("type") == "line": 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: 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"] - 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"): containing = [outer for outer in outers if _contains(inner["points"][0], outer["points"])] if not containing: 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"]) regions[outers.index(selected)]["holes"].append(inner["edges"]) meta["_regions"] = regions @@ -300,7 +328,7 @@ def resolve_profile(sketch: _Ctx) -> _Ctx: generator = SHAPE_GENERATORS.get(profile.get("type")) if generator is None: 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) output = deepcopy(sketch) 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 if meta["_regions"]: 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"] ] + if meta["_has_open_contour"]: + output["_open_contour"] = True return output -- 2.52.0