diff --git a/backend/engine/cdsl_engine/runtime.py b/backend/engine/cdsl_engine/runtime.py index 060d4208..30a342d1 100644 --- a/backend/engine/cdsl_engine/runtime.py +++ b/backend/engine/cdsl_engine/runtime.py @@ -320,71 +320,105 @@ def _revolve_axis(node: FeaturePlanNode, session: ExecutionSession) -> AxisSpec: def _shape_from_primary(node: FeaturePlanNode, session: ExecutionSession, *, sketch: dict[str, Any] | None = None) -> FeatureResult: + # 主形状特征(拉伸 / 旋转)的统一入口:由草图生成实体并与当前主体做布尔合并或切除。 + + # 1. 取草图:优先使用外部传入的 sketch_override(阵列/镜像等重放场景), + # 否则按 sketch_id 从会话草图表中取原始草图。 selected_sketch = sketch or session.sketches.get(str(node.sketch_id)) if selected_sketch is None: raise ValueError("primary feature has no resolved sketch") + # 2. 从草图解析闭合轮廓区域(faces),没有闭合区域就无法生成实体。 faces = session.adapter.faces_for_sketch(selected_sketch) if not faces: raise ValueError("sketch does not create a closed profile region") + # 3. 按特征类型生成子实体: if node.atomic_id.startswith("extrude_"): + # 拉伸:先按终止条件(盲孔/贯穿/至面/双侧等)求出位移向量, + # 再对每个面沿每个向量做拉伸,得到实体列表。 vectors = _extent_vectors(node, faces, selected_sketch, session) solids = [session.adapter.extrude(face, vector) for face in faces for vector in vectors] else: + # 旋转:解析旋转轴并校验旋转角,然后绕轴旋转每个面得到实体列表。 axis = _revolve_axis(node, session) angle = float(node.params.get("angle_deg") or 0.0) if angle <= 0: raise ValueError("revolve requires angle_deg > 0") solids = [session.adapter.revolve(face, angle, axis) for face in faces] + # 4. 将所有子实体做布尔并(fuse)合并为一个工具体(tool)。 tool = None for solid in solids: tool = session.adapter.fuse(tool, solid) if tool is None: raise ValueError("primary feature produced no solid") + # 5. 与当前主体做布尔操作: if "cut" in node.atomic_id: + # 切除类特征:要求已有主体,从主体上减去工具体(cut)。 if session.body is None: raise ValueError("cut feature has no body") body = session.adapter.cut(session.body, tool) else: + # 添加类特征:将工具体并到当前主体上(fuse),首个特征时 body 为 None 也能直接成立。 body = session.adapter.fuse(session.body, tool) + # 6. 登记新主体(更新拓扑、记录重放定义),并返回该特征的结果对象。 session.register_body(node.feature_id, body, replay_node=node) return session.result(node) def _execute_reference_plane(node: FeaturePlanNode, session: ExecutionSession) -> FeatureResult: + # 基准面特征(reference_plane)执行入口:从参数解析平面并登记为拓扑上下文。 + + # 1. 从特征参数 plane 中解析出平面定义 PlaneSpec(原点到法向)。 plane = PlaneSpec.from_mapping(node.params.get("plane") or {}) + # 2. 将该平面注册到拓扑上下文,供后续特征(如草图基准、参考轴)引用。 session.topology.register_context(node.feature_id, plane) + # 3. 返回结果对象,并将该平面作为上下文一并携带。 return session.result(node, context=plane) def _execute_reference_axis(node: FeaturePlanNode, session: ExecutionSession) -> FeatureResult: + # 基准轴特征(reference_axis)执行入口:由参数直接定义轴,或由两个基准平面求交线得到轴。 + + # 1. 尝试直接取参数:若同时给出原点 origin_mm 与方向 direction,则直接构造轴。 params = node.params.get("axis") or {} if params.get("origin_mm") and params.get("direction"): axis = AxisSpec.from_mapping(params) else: + # 2. 否则从特征选择器中筛选出已解析的基准平面。 planes = [session.resolve(selector) for selector in node.selectors if selector.get("kind") == "plane"] resolved = [item.record.value for item in planes if item.status == "resolved" and isinstance(item.record.value, PlaneSpec)] + # 3. 校验:轴需要两个非平行的平面,不足两个则报错。 if len(resolved) < 2: raise ValueError("reference axis requires two uniquely resolved planes") + # 4. 用两平面法线叉积求交线方向;若方向长度接近 0 说明两平面平行,无法成轴。 first, second = resolved[0], resolved[1] n1, n2 = first.normal, second.normal direction = vector_cross(n1, n2) squared_length = vector_dot(direction, direction) if squared_length <= 1e-18: raise ValueError("reference planes are parallel and cannot define an axis") + # 5. 求交线上的一点:两平面到各自原点的垂距参与线性组合,得到交线上的最近点。 d1 = vector_dot(n1, first.origin_mm) d2 = vector_dot(n2, second.origin_mm) point = vector_scale(vector_add(vector_scale(vector_cross(n2, direction), d1), vector_scale(vector_cross(direction, n1), d2)), 1 / squared_length) + # 6. 由该点与归一化的交线方向组合成基准轴 AxisSpec。 axis = AxisSpec(origin_mm=point, direction=vector_unit(direction, field_name="reference axis")) + # 7. 注册为拓扑上下文,并返回结果对象(携带该轴)。 session.topology.register_context(node.feature_id, axis) return session.result(node, context=axis) def _execute_sphere(node: FeaturePlanNode, session: ExecutionSession) -> FeatureResult: + # 球体特征(sphere_add)执行入口:按球心与半径生成球体并并入当前主体。 + + # 1. 解析参数:半径 radius_mm 与球心 center_mm。 radius = float(node.params.get("radius_mm") or 0.0) center = node.params.get("center_mm") or [] + # 2. 校验:半径必须大于 0,球心必须是三维坐标。 if radius <= 0 or len(center) != 3: raise ValueError("sphere_add requires radius_mm and a three-dimensional center_mm") + # 3. 由适配器创建球体实体。 solid = session.adapter.sphere(radius, (float(center[0]), float(center[1]), float(center[2]))) + # 4. 球体与当前主体做布尔并(fuse)后登记为新主体,并返回该特征的结果对象。 session.register_body(node.feature_id, session.adapter.fuse(session.body, solid), replay_node=node) return session.result(node) @@ -423,13 +457,19 @@ def _hole_starts( def _execute_hole(node: FeaturePlanNode, session: ExecutionSession, *, wizard: bool = False) -> FeatureResult: + # 孔特征(hole)执行入口:在指定宿主面上按孔规格生成切除工具,并从主体上减去。 + + # 1. 校验:孔是切除操作,必须先有主体。 if session.body is None: raise ValueError("hole feature has no body") + # 2. 确定宿主面 host_face: host_selector = node.params.get("host_face") if isinstance(host_selector, dict) and isinstance(host_selector.get("frame"), dict): + # 若直接带 frame(平面定义),则以该平面为宿主,孔位按局部坐标解释。 host = PlaneSpec.from_mapping(host_selector["frame"]) positions_are_local = True else: + # 否则从特征选择器中取 face,解析出宿主平面,孔位按世界坐标解释。 selectors = list(node.selectors) if isinstance(host_selector, dict): selectors.append(host_selector) @@ -438,15 +478,19 @@ def _execute_hole(node: FeaturePlanNode, session: ExecutionSession, *, wizard: b raise ValueError("hole requires host_face selector or frame") host = _host_plane(session.resolve(selector)) positions_are_local = False + # 3. 解析孔规格 HoleSpec(直径、深度、类型等,wizard 模式提供额外默认值)。 spec = HoleSpec.from_feature(node.atomic_id, node.params, wizard=wizard) + # 4. 确定孔轴向:默认沿宿主面法向,但需保证指向主体内部(按主体中心与面原点的相对位置取反)。 normal = host.normal inward = normal if vector_dot(vector_subtract(session.adapter.body_center(session.body), host.origin_mm), normal) >= 0 else vector_scale(normal, -1) + # 5. 生成孔切除工具:按孔规格、起始位置、内方向及“贯穿到主体底面”的深度构造工具实体。 tool = session.adapter.hole_tool( spec, _hole_starts(spec, host_plane=host, positions_are_local=positions_are_local), inward, session.adapter.body_span(session.body, inward) + 2.0, ) + # 6. 从主体上减去工具实体,登记新主体并返回结果。 session.register_body(node.feature_id, session.adapter.cut(session.body, tool), replay_node=node) return session.result(node) @@ -468,28 +512,40 @@ def _selector_edges(node: FeaturePlanNode, session: ExecutionSession, *, tangent def _execute_fillet(node: FeaturePlanNode, session: ExecutionSession) -> FeatureResult: + # 圆角特征(fillet)执行入口:对选中边按半径做圆角,平滑尖角与棱边。 + + # 1. 校验:圆角作用于已有主体,必须先有主体。 if session.body is None: raise ValueError("fillet has no body") + # 2. 解析圆角半径并校验必须大于 0。 radius = float(node.params.get("radius_mm") or 0) if radius <= 0: raise ValueError("fillet radius_mm must be > 0") + # 3. 解析目标边(支持 tangent_propagation 相切传播),并执行圆角。 body = session.adapter.fillet( session.body, radius, _selector_edges(node, session, tangent_propagation=bool(node.params.get("tangent_propagation"))), ) + # 4. 登记新主体并返回结果。 session.register_body(node.feature_id, body, replay_node=node) return session.result(node) def _execute_chamfer(node: FeaturePlanNode, session: ExecutionSession) -> FeatureResult: + # 倒角特征(chamfer)执行入口:对选中边按距离做倒角(可带第二距离形成不对称倒角)。 + + # 1. 校验:倒角作用于已有主体,必须先有主体。 if session.body is None: raise ValueError("chamfer has no body") + # 2. 解析主距离并校验必须大于 0。 distance = float(node.params.get("distance_mm") or 0) if distance <= 0: raise ValueError("chamfer distance_mm must be > 0") + # 3. 解析目标边(支持相切传播),执行倒角;distance_2_mm 提供时产生非对称倒角。 body = session.adapter.chamfer( session.body, distance, node.params.get("distance_2_mm"), _selector_edges(node, session, tangent_propagation=bool(node.params.get("tangent_propagation"))), ) + # 4. 登记新主体并返回结果。 session.register_body(node.feature_id, body, replay_node=node) return session.result(node) @@ -539,26 +595,36 @@ def _translated_node(node: FeaturePlanNode, instance_id: str, offset: Vector3) - def _execute_linear_pattern(node: FeaturePlanNode, session: ExecutionSession, execute: Callable[[FeaturePlanNode, ExecutionSession, dict[str, Any] | None], FeatureResult]) -> FeatureResult: + # 线性阵列特征(pattern)执行入口:沿两个方向按数量与间距重放源特征形成阵列。 + + # 1. 取源特征的 replay 定义(源特征按 feature_id 在会话中登记,供本阵列重放)。 params = node.params sources = session.replay_sources(params.get("source_feature_ids") or []) if not sources: raise ValueError("pattern source features have no replay definitions") + # 2. 解析两个方向的实例数量。 count_1 = int(params.get("pattern_count_1") or 1) count_2 = int(params.get("pattern_count_2") or 1) + # 3. 解析两个方向的步长向量(方向单位向量 × 间距),作为阵列位移基准。 direction_1 = vector_scale(vector_unit(tuple(float(value) for value in (params.get("direction_1") or [1, 0, 0])), field_name="pattern direction_1"), float(params.get("spacing_1_mm") or 0)) direction_2 = vector_scale(vector_unit(tuple(float(value) for value in (params.get("direction_2") or [0, 1, 0])), field_name="pattern direction_2"), float(params.get("spacing_2_mm") or 0)) + # 4. 双重循环生成每个阵列实例(跳过原点 0,0 处,那里是源特征本身)。 for first in range(count_1): for second in range(count_2): if first == 0 and second == 0: continue + # 计算当前实例相对源特征的偏移向量。 offset = vector_add(vector_scale(direction_1, first), vector_scale(direction_2, second)) for source in sources: + # 逐个源特征克隆并按偏移平移后重放执行(草图也同步平移)。 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) sketch = session.sketches.get(str(source.sketch_id)) execute(cloned, session, _translated_sketch(sketch, offset) if sketch else None) + # 5. 记录本阵列的 replay 定义:后续阵列若选中本阵列,按定义递归重放, + # 而非复制当前主体做近似。 # A later pattern may select this pattern feature. The definition is # replayed recursively, never approximated by copying the current body. session.replay_definitions[node.feature_id] = node