diff --git a/backend/engine/cdsl_engine/capabilities.py b/backend/engine/cdsl_engine/capabilities.py index 9c0ff2de..aeb3b237 100644 --- a/backend/engine/cdsl_engine/capabilities.py +++ b/backend/engine/cdsl_engine/capabilities.py @@ -102,6 +102,12 @@ def pattern_transform_blocker(source: FeaturePlanNode) -> str | None: # 孔宿主面:主体上的固定面,不随实例平移;实例位置由 positions # 平移决定(_translated_node),selector 原样保留即可正确 resolve。 continue + if selector.get("kind") == "plane" and source.atomic_id == "pattern_mirror" and selector.get("owner_feature_id"): + # #6 pattern 引用重解析:pattern_mirror 的镜像面是对 + # reference_plane 的 plane 引用(带 owner_feature_id),重放时 + # 由运行时从 owner 解析显式 frame 并随实例变换 + # (_translated_node/_mirrored_node 内联 frame),放行。 + continue return "feature selector" host = source.params.get("host_face") if host is not None and not _has_explicit_host_frame(source.params): diff --git a/backend/engine/cdsl_engine/runtime.py b/backend/engine/cdsl_engine/runtime.py index 889ef830..5aaae5c8 100644 --- a/backend/engine/cdsl_engine/runtime.py +++ b/backend/engine/cdsl_engine/runtime.py @@ -660,7 +660,23 @@ def _translated_sketch(sketch: dict[str, Any], offset: Vector3) -> dict[str, Any return output -def _translated_node(node: FeaturePlanNode, instance_id: str, offset: Vector3) -> FeaturePlanNode: +def _owner_plane_frame(session: ExecutionSession, selector: dict[str, Any]) -> dict[str, Any] | None: + """解析 selector 的 owner 特征(reference_plane)注册的显式平面 frame。 + + #6 pattern 引用重解析:pattern 重放 source(pattern_mirror)时,镜像面 + 是 selector,其 owner 是 reference_plane 特征;该特征执行时把显式 + PlaneSpec 登记为拓扑上下文,这里取出该 frame 供随实例变换使用。 + """ + owner = selector.get("owner_feature_id") + if not owner: + return None + for record in session.topology.records_for_feature(str(owner)): + if record.kind == "plane" and isinstance(record.value, PlaneSpec): + return record.value.as_dict() + return None + + +def _translated_node(node: FeaturePlanNode, instance_id: str, offset: Vector3, session: ExecutionSession) -> FeaturePlanNode: params = deepcopy(node.params) components = offset if isinstance(params.get("plane"), dict) and params["plane"].get("origin_mm"): @@ -679,6 +695,45 @@ def _translated_node(node: FeaturePlanNode, instance_id: str, offset: Vector3) - axis = params.get("axis") or {} if axis.get("origin_mm"): axis["origin_mm"] = [float(axis["origin_mm"][index]) + components[index] for index in range(3)] + mirror_plane = params.get("mirror_plane") + if isinstance(mirror_plane, dict) and node.atomic_id == "pattern_mirror": + # #6 pattern 引用重解析:镜像面是 reference_plane 引用,随实例平移 + # 到新位置后内联为显式 frame;否则重放时 resolve 到原始面,镜像 + # 副本会错误地重合在源特征附近。同时源特征也必须平移后重放:镜像 + # 副本 = reflect(源@t, 面@t),只平移面不平移源会落在 2P+t-x 处 + # 而非正确位置 2P-x+t。 + frame = _owner_plane_frame(session, mirror_plane) + if frame is None: + raise ValueError("mirror plane reference cannot be transformed for pattern replay") + cloned_selector = deepcopy(mirror_plane) + cloned_selector["frame"] = { + "origin_mm": [frame["origin_mm"][index] + components[index] for index in range(3)], + "x_dir": list(frame["x_dir"]), + "normal": list(frame["normal"]), + } + params["mirror_plane"] = cloned_selector + transformed_ids: list[str] = [] + for source_id in node.params.get("source_feature_ids") or []: + source_node = session.replay_definitions.get(str(source_id)) + if source_node is None: + raise ValueError(f"mirror pattern source feature {source_id} has no replay definition") + temp_id = f"{instance_id}.src.{source_id}" + shifted = _translated_node(source_node, temp_id, offset, session) + if shifted.sketch_id: + source_sketch = session.sketches.get(str(source_node.sketch_id)) + if source_sketch is not None: + temp_sketch_id = f"{temp_id}.sk" + session.sketches[temp_sketch_id] = _translated_sketch(source_sketch, offset) + shifted = FeaturePlanNode( + shifted.feature_id, shifted.atomic_id, shifted.name, shifted.depends_on, + shifted.params, shifted.selectors, temp_sketch_id, + shifted.declared_status, shifted.source_feature, + ) + # 临时 replay 定义同样进入 nodes 表(replay_sources 以此过滤)。 + session.nodes[temp_id] = shifted + session.replay_definitions[temp_id] = shifted + transformed_ids.append(temp_id) + params["source_feature_ids"] = transformed_ids return FeaturePlanNode(instance_id, node.atomic_id, node.name, (), params, node.selectors, node.sketch_id, node.declared_status, node.source_feature) @@ -708,7 +763,7 @@ def _execute_linear_pattern(node: FeaturePlanNode, session: ExecutionSession, ex dependency = pattern_transform_blocker(source) if dependency: raise ValueError(f"pattern source uses an unsupported {dependency}") - cloned = _translated_node(source, f"{node.feature_id}.p{first}_{second}.{source.feature_id}", offset) + cloned = _translated_node(source, f"{node.feature_id}.p{first}_{second}.{source.feature_id}", offset, session) sketch = session.sketches.get(str(source.sketch_id)) execute(cloned, session, _translated_sketch(sketch, offset) if sketch else None) # 5. 记录本阵列的 replay 定义:后续阵列若选中本阵列,按定义递归重放, @@ -775,7 +830,7 @@ def _mirrored_sketch(sketch: dict[str, Any], plane: PlaneSpec) -> dict[str, Any] return output -def _mirrored_node(node: FeaturePlanNode, instance_id: str, plane: PlaneSpec) -> FeaturePlanNode: +def _mirrored_node(node: FeaturePlanNode, instance_id: str, plane: PlaneSpec, session: ExecutionSession) -> FeaturePlanNode: params = deepcopy(node.params) if isinstance(params.get("plane"), dict): for key in ("origin_mm", "x_dir", "y_dir", "normal"): @@ -816,6 +871,44 @@ def _mirrored_node(node: FeaturePlanNode, instance_id: str, plane: PlaneSpec) -> axis["origin_mm"] = _reflect_point(axis["origin_mm"], plane) if axis.get("direction"): axis["direction"] = _reflect_point(axis["direction"], plane, vector=True) + mirror_plane = params.get("mirror_plane") + if isinstance(mirror_plane, dict) and node.atomic_id == "pattern_mirror": + # #6 pattern 引用重解析:镜像重放 mirror source 时,其镜像面引用 + # 随本实例的镜像面一起反射(内联为显式 frame),否则重放 resolve + # 到原始面,嵌套镜像会退化成与源镜像重合的错误几何。源特征同样 + # 反射后重放:镜像副本 = reflect(源@P_B, reflect(面,P_B))。 + frame = _owner_plane_frame(session, mirror_plane) + if frame is None: + raise ValueError("mirror plane reference cannot be transformed for pattern replay") + cloned_selector = deepcopy(mirror_plane) + cloned_selector["frame"] = { + "origin_mm": _reflect_point(frame["origin_mm"], plane), + "x_dir": _reflect_point(frame["x_dir"], plane, vector=True), + "normal": _reflect_point(frame["normal"], plane, vector=True), + } + params["mirror_plane"] = cloned_selector + transformed_ids: list[str] = [] + for source_id in node.params.get("source_feature_ids") or []: + source_node = session.replay_definitions.get(str(source_id)) + if source_node is None: + raise ValueError(f"mirror pattern source feature {source_id} has no replay definition") + temp_id = f"{instance_id}.src.{source_id}" + shifted = _mirrored_node(source_node, temp_id, plane, session) + if shifted.sketch_id: + source_sketch = session.sketches.get(str(source_node.sketch_id)) + if source_sketch is not None: + temp_sketch_id = f"{temp_id}.sk" + session.sketches[temp_sketch_id] = _mirrored_sketch(source_sketch, plane) + shifted = FeaturePlanNode( + shifted.feature_id, shifted.atomic_id, shifted.name, shifted.depends_on, + shifted.params, shifted.selectors, temp_sketch_id, + shifted.declared_status, shifted.source_feature, + ) + # 临时 replay 定义同样进入 nodes 表(replay_sources 以此过滤)。 + session.nodes[temp_id] = shifted + session.replay_definitions[temp_id] = shifted + transformed_ids.append(temp_id) + params["source_feature_ids"] = transformed_ids return FeaturePlanNode(instance_id, node.atomic_id, node.name, (), params, node.selectors, node.sketch_id, node.declared_status, node.source_feature) @@ -831,7 +924,7 @@ def _execute_mirror_pattern(node: FeaturePlanNode, session: ExecutionSession) -> dependency = pattern_transform_blocker(source) if dependency: raise ValueError(f"mirror pattern source uses an unsupported {dependency}") - cloned = _mirrored_node(source, f"{node.feature_id}.m.{source.feature_id}", resolution.record.value) + cloned = _mirrored_node(source, f"{node.feature_id}.m.{source.feature_id}", resolution.record.value, session) sketch = session.sketches.get(str(source.sketch_id)) _execute_node(cloned, session, _mirrored_sketch(sketch, resolution.record.value) if sketch else None) session.replay_definitions[node.feature_id] = node diff --git a/backend/engine/cdsl_engine/runtime_types.py b/backend/engine/cdsl_engine/runtime_types.py index 0c2a7a4c..698112e9 100644 --- a/backend/engine/cdsl_engine/runtime_types.py +++ b/backend/engine/cdsl_engine/runtime_types.py @@ -719,6 +719,39 @@ class TopologyRegistry: ] if owner: candidates = [record for record in candidates if owner in record.owners] + if kind == "plane" and selector.get("frame") is not None: + # #6 pattern 引用重解析:pattern 重放 source(pattern_mirror)时, + # mirror_plane 的 plane 引用由运行时随实例变换后内联为显式 frame + # (_translated_node / _mirrored_node),这里直接构造 PlaneSpec, + # 不再走 stable_id / 几何匹配,避免解析到未随实例变换的原始面。 + try: + plane = PlaneSpec.from_mapping(selector.get("frame") or {}) + except (TypeError, ValueError): + plane = None + if plane is not None: + record = TopologyRecord( + record_id=f"inline:{id(plane)}", + kind="plane", + feature_id=str(owner or "inline"), + geometry=plane.as_dict(), + value=plane, + ) + return SelectorResolution( + selector=selector, + status="resolved", + record=record, + candidates=({"score": 1.0, **record.public_dict()},), + ) + return SelectorResolution( + selector=selector, + status="not_found", + candidates=(), + diagnostic=RuntimeDiagnostic( + code="selector_frame_incomplete", + message="An inline plane frame requires origin_mm, x_dir and normal", + detail={"frame": selector.get("frame")}, + ), + ) geometry = normalize_selector_geometry(selector.get("geometry")) if selector.get("snapshot_id") and not owner: return SelectorResolution(