from __future__ import annotations import math from copy import deepcopy from dataclasses import dataclass from typing import Any from .featurescript_parser import symbolic_string from .ir import Call, FeatureIR, ModelIR, SketchIR from .query_parser import parse_query, query_expr, walk_calls UNSUPPORTED = {"draft", "thicken", "split", "moveFace", "replaceFace", "deleteFace", "import", "derive"} # The parser preserves every direct ``operation(context, id + "F...", ...)`` # call. Keep lowering's executable surface explicit so newly observed source # APIs get a stable unsupported-operation diagnostic rather than disappearing # from history or falling through to an incidental implementation error. LOWERABLE_OPERATIONS = { "assignVariable", "transform", "deleteBodies", "cPoint", "cPlane", "extrude", "loft", "sweep", "booleanBodies", "revolve", "fillet", "chamfer", "shell", "hole", "circularPattern", "mirror", } PLANES = { "Top": {"origin_mm": [0., 0., 0.], "x_dir": [1., 0., 0.], "normal": [0., 0., 1.]}, "Front": {"origin_mm": [0., 0., 0.], "x_dir": [1., 0., 0.], "normal": [0., -1., 0.]}, "Right": {"origin_mm": [0., 0., 0.], "x_dir": [0., 1., 0.], "normal": [1., 0., 0.]}, } # CADFS 未携带标准孔表的实体定义。下列条目由 source STEP 验证:该组合在 # CADFS 的 B-rep 中不是普通 blind counterbore,而是简化为贯穿的攻丝孔。 # 不能为未知目录项推测尺寸;未列入的孔仍按显式 FeatureScript 尺寸 lower。 _STANDARD_TAPPED_THROUGH_BORE_DIAMETERS = { ("ISO", "M10", "Clearance & tapped"): 15.0, } @dataclass class LoweringResult: cdsl: dict[str, Any] | None status: str diagnostics: list[dict[str, Any]] history: list[dict[str, Any]] class UnsupportedCapability(ValueError): def __init__(self, capability: str, message: str): super().__init__(message); self.capability = capability class OpenSketchProfileError(ValueError): pass def plain(value: Any) -> Any: if isinstance(value, Call): return {"call": value.name, "args": [plain(arg) for arg in value.args], "line": value.line} if isinstance(value, list): return [plain(item) for item in value] if isinstance(value, dict): return {key: plain(item) for key, item in value.items()} return value def _selector_intent( value: Any, *, query_family: str, kind: str, evidence: str, allowed: tuple[str, ...] = ("continuation",), multiplicity: str = "one", output_role: str | None = None, disambiguation: dict[str, Any] | None = None, ) -> dict[str, Any]: """Keep the source query as semantics, separate from runtime topology IDs.""" query = parse_query(value) intent: dict[str, Any] = { "version": "1.0", "kind": kind, "query_family": query_family, # Version metadata is populated from the enclosing ModelIR only when # the exported source actually contains it. ``"0"`` used to look # like a real FeatureScript version and caused downstream capability # checks to make an unjustified compatibility decision. "source_query": {"ast": query.ast}, # Keep an executable-independent typed expression as well as the raw # source AST. This records set boundaries and filters without claiming # that an unsupported family can be resolved from geometry. "query_expr": query_expr(value), "derivation_policy": {"allowed": list(allowed), "multiplicity": multiplicity}, "evidence": evidence, } if query.source_sketch and query.source_entity: intent["source_entity"] = {"sketch_id": query.source_sketch, "entity_id": query.source_entity} if output_role is not None: intent["output_role"] = output_role if disambiguation is not None: intent["disambiguation"] = disambiguation return intent def _deferred_featurescript_selector_intent(value: Any, *, kind: str) -> dict[str, Any]: """Preserve an unsupported CADFS topology query without authorizing hints. Geometry remains useful diagnostic context, but it cannot be mistaken for an implementation of the source query. A non-empty policy keeps the intent structurally valid while ``none`` makes the resolver reject it before stable-ID or geometry matching. """ query = parse_query(value) try: _call, _owner, family, _query_kind, _definition = _direct_make_query(value) except ValueError: family = str(query.topology_type or "FEATURESCRIPT_QUERY").rsplit(".", 1)[-1].upper() if not family or not family.replace("_", "").isalnum() or not family[0].isalpha(): family = "FEATURESCRIPT_QUERY" return _selector_intent( value, query_family=family, kind=kind, evidence="feature_script_query", allowed=("continuation",), multiplicity="none", disambiguation={ "type": "deferred_source_query", "query_combinators": list(query.query_combinators), "filters": list(query.filters), "body_scope": list(query.body_scope), "source_refs": [ {"sketch_id": sketch_id, "entity_id": entity_id} for sketch_id, entity_id in _source_refs(value) ], }, ) def _explicit_datum_selector_intent(value: Any, *, kind: str) -> dict[str, Any]: """Mark a source datum as an explicit geometric, not lineage, selector.""" return _selector_intent( value, query_family="GEOMETRIC", kind=kind, evidence="explicit_datum", ) def _finalize_selector_intents(cdsl: dict[str, Any], model: ModelIR) -> None: """Bind source-version metadata for top-level and nested selectors.""" source_version = model.featurescript_version stack: list[Any] = [cdsl] while stack: value = stack.pop() if isinstance(value, list): stack.extend(value) continue if not isinstance(value, dict): continue intent = value.get("selector_intent") if isinstance(intent, dict): source_query = intent.get("source_query") if isinstance(source_query, dict): if source_version: source_query["featurescript_version"] = source_version if model.standard_library: source_query["standard_library"] = model.standard_library if model.standard_library_version: source_query["standard_library_version"] = model.standard_library_version if model.standard_library_imports: source_query["standard_library_imports"] = deepcopy(model.standard_library_imports) stack.extend(value.values()) def _bool(value: Any) -> bool: return value is True or (isinstance(value, str) and value.lower() == "true") def _number(value: Any, units: bool = False) -> float: if isinstance(value, (float, int)): return float(value) if isinstance(value, str): constants = {"mm": 1., "millimeter": 1., "cm": 10., "m": 1000., "inch": 25.4, "in": 25.4, "ft": 304.8, "degree": 1.} if value in constants: return constants[value] return float(value) if isinstance(value, Call) and value.name == "__binary__": left, op, right = value.args; a, b = _number(left, units), _number(right, units) return {"+": a + b, "-": a - b, "*": a * b, "/": a / b}[str(op)] if isinstance(value, Call) and value.name == "round" and len(value.args) == 1: # CADFS sometimes serializes a known pattern count as ``round(8)``. # An already integral constant is provably unchanged, so it needs no # FeatureScript rounding-mode assumption. Non-integral calls remain # unsupported until that language-level semantic is represented. rounded_input = _number(value.args[0], units) if math.isfinite(rounded_input) and rounded_input.is_integer(): return rounded_input raise ValueError(f"not a constant number: {plain(value)!r}") def _resolve_source_variables(value: Any, variables: dict[str, Any]) -> Any: """Resolve exact earlier ``getVariable(context, name)`` source calls. CADFS variables are ordered context state, not topology. Lowering captures the declared expression before it can be consumed by geometry parameters; a missing or malformed lookup remains a deterministic source error rather than becoming a guessed numeric default. """ if isinstance(value, Call): if value.name == "getVariable": if len(value.args) != 2 or value.args[0] != "context" or not isinstance(value.args[1], str): raise ValueError("getVariable must name one source context variable") name = value.args[1] if name not in variables: raise ValueError(f"source variable is unavailable: {name}") return deepcopy(variables[name]) return Call(value.name, [_resolve_source_variables(argument, variables) for argument in value.args], value.line, value.raw) if isinstance(value, list): return [_resolve_source_variables(item, variables) for item in value] if isinstance(value, dict): return {key: _resolve_source_variables(item, variables) for key, item in value.items()} return value def _assign_variable_params(params: dict[str, Any]) -> tuple[str, Any, dict[str, Any]]: """Lower the two exported scalar assignVariable forms without coercion.""" name = params.get("name") if not isinstance(name, str) or not name: raise ValueError("assignVariable requires one non-empty source name") variants = [(key, params[key]) for key in ("anyValue", "lengthValue") if key in params] if len(variants) != 1: raise UnsupportedCapability( "assign_variable_value", "assignVariable requires exactly one anyValue or lengthValue source expression", ) key, value = variants[0] numeric_value = _number(value, units=key == "lengthValue") if not math.isfinite(numeric_value): raise UnsupportedCapability("assign_variable_value", "assignVariable value must be finite") return name, value, { "name": name, "value": numeric_value, "value_kind": "length" if key == "lengthValue" else "any", } def _lookup_table_definition(value: Any) -> dict[str, str]: for call in walk_calls(value): if call.name != "lookupTablePath" or not call.args or not isinstance(call.args[0], dict): continue return {str(key): str(item) for key, item in call.args[0].items() if isinstance(item, (str, int, float))} return {} def _standard_tapped_through_bore_diameter(params: dict[str, Any], style: str, end_style: str) -> float | None: if style.upper() not in {"COUNTERBORE", "C_BORE"} or "BLIND" not in end_style or not _bool(params.get("isTappedThrough")): return None definition = _lookup_table_definition(params.get("standardBlindInLast")) return _STANDARD_TAPPED_THROUGH_BORE_DIAMETERS.get((definition.get("standard", ""), definition.get("size", ""), definition.get("type", ""))) def _point(value: Any) -> list[float]: if isinstance(value, Call) and value.name == "__binary__" and value.args[1] == "*": scale = _number(value.args[2], True); point = _point(value.args[0]); return [v * scale for v in point] if isinstance(value, Call) and value.name in {"v", "vector"} and len(value.args) >= 2: return [_number(value.args[0]), _number(value.args[1])] if isinstance(value, list) and len(value) >= 2: return [_number(value[0]), _number(value[1])] raise ValueError(f"not a 2D point: {plain(value)!r}") def _cross(a: list[float], b: list[float]) -> list[float]: return [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]] def _dot(a: list[float], b: list[float]) -> float: return sum(left * right for left, right in zip(a, b)) def _unit(value: list[float], message: str) -> list[float]: length = math.sqrt(_dot(value, value)) if length <= 1e-9: raise ValueError(message) return [component / length for component in value] def _sub(a: list[float], b: list[float]) -> list[float]: return [a[index] - b[index] for index in range(3)] def _rotate(value: list[float], axis: list[float], angle_rad: float) -> list[float]: axis = _unit(axis, "rotation axis is degenerate") cosine, sine = math.cos(angle_rad), math.sin(angle_rad) cross = _cross(axis, value); projection = _dot(axis, value) * (1.0 - cosine) return [value[index] * cosine + cross[index] * sine + axis[index] * projection for index in range(3)] def _translate_frame(frame: dict[str, Any], offset: list[float]) -> dict[str, Any]: return {**frame, "origin_mm": [frame["origin_mm"][index] + offset[index] for index in range(3)]} def _rotate_point(point: list[float], axis: dict[str, list[float]], angle_rad: float) -> list[float]: relative = _rotate(_sub(point, axis["origin_mm"]), axis["direction"], angle_rad) return [axis["origin_mm"][index] + relative[index] for index in range(3)] def _rotate_frame(frame: dict[str, Any], axis: dict[str, list[float]], angle_rad: float) -> dict[str, Any]: return { **frame, "origin_mm": _rotate_point(frame["origin_mm"], axis, angle_rad), "x_dir": _rotate(frame["x_dir"], axis["direction"], angle_rad), "normal": _rotate(frame["normal"], axis["direction"], angle_rad), } def _reflect_vector(value: list[float], plane: dict[str, Any]) -> list[float]: normal = _unit(list(plane["normal"]), "mirror plane normal is degenerate") amount = 2.0 * _dot(value, normal) return [value[index] - amount * normal[index] for index in range(3)] def _reflect_point(point: list[float], plane: dict[str, Any]) -> list[float]: normal = _unit(list(plane["normal"]), "mirror plane normal is degenerate") amount = 2.0 * _dot(_sub(point, plane["origin_mm"]), normal) return [point[index] - amount * normal[index] for index in range(3)] def _reflect_frame(frame: dict[str, Any], plane: dict[str, Any]) -> dict[str, Any]: return { **frame, "origin_mm": _reflect_point(frame["origin_mm"], plane), "x_dir": _reflect_vector(frame["x_dir"], plane), "normal": _reflect_vector(frame["normal"], plane), } def _y_dir(plane: dict[str, Any]) -> list[float]: return _cross(plane["normal"], plane["x_dir"]) def _global(plane: dict[str, Any], point: list[float]) -> list[float]: y = _y_dir(plane); return [plane["origin_mm"][i] + plane["x_dir"][i]*point[0] + y[i]*point[1] for i in range(3)] def _shift_plane(plane: dict[str, Any], distance: float) -> dict[str, Any]: return {**plane, "origin_mm": [plane["origin_mm"][i] + plane["normal"][i]*distance for i in range(3)]} def _oriented_plane(plane: dict[str, Any], normal_sign: float, distance: float = 0.0) -> dict[str, Any]: shifted = _shift_plane(plane, distance) # 翻转端盖法向时同步翻转 x 轴,保持 normal × x_dir 的草图局部 y 轴 # 不变。否则同一 CADFS 草图会在附着端盖后被镜像。 return { **shifted, "x_dir": [normal_sign * value for value in plane["x_dir"]], "normal": [normal_sign * value for value in plane["normal"]], } def _attachment_plane(plane: dict[str, Any]) -> dict[str, Any]: """Choose the global-origin projection as the later sketch attachment origin. CADFS keeps a plane's physical location and the plane coordinates used by a later sketch separate. The latter is the global origin projected onto the physical plane, not necessarily the selected point or profile centre. Keep this only as an attachment frame; selectors and body topology continue to use the physical frame. """ normal = plane["normal"] origin = plane["origin_mm"] distance = _dot(origin, normal) return {**plane, "origin_mm": [distance * normal[index] for index in range(3)]} def _loft_cap_frames(start: dict[str, Any], end: dict[str, Any]) -> dict[str, dict[str, Any]] | None: """Record the physical outer normals for the first and last loft sections.""" direction = _sub(end["origin_mm"], start["origin_mm"]) if math.sqrt(_dot(direction, direction)) <= 1e-9: return None # Loft start/end caps face away from the section sequence. Sketch normals do # not necessarily have that orientation, so retain the CDSL frame handedness # while choosing the actual B-rep exterior normal for CAP references. start_sign = 1.0 if _dot(start["normal"], direction) <= 0 else -1.0 end_sign = 1.0 if _dot(end["normal"], direction) >= 0 else -1.0 return {"start": _oriented_plane(start, start_sign), "end": _oriented_plane(end, end_sign)} def _frame(origin: list[float], x_dir: list[float], normal: list[float]) -> dict[str, list[float]]: normal = _unit(normal, "reference plane normal is degenerate") x_dir = _sub(x_dir, [normal[index] * _dot(x_dir, normal) for index in range(3)]) return {"origin_mm": origin, "x_dir": _unit(x_dir, "reference plane x direction is degenerate"), "normal": normal} def _pattern_copy_cap_plane(value: Any, feature_frames: dict[str, dict[str, Any]]) -> dict[str, Any] | None: """Resolve one mirrored COPY(CAP_FACE) frame without flattening its source query.""" try: _call, pattern_owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None if topology != "COPY" or kind != "face": return None derived = definition.get("derivedFrom") if derived is None: return None try: _source, source_owner, source_topology, source_kind, source_definition = _direct_make_query(derived) except ValueError: return None if source_topology != "CAP_FACE" or source_kind != "face": return None pattern_frame = feature_frames.get(pattern_owner) or {} transform = pattern_frame.get("copy_transform") or {} if transform.get("type") != "mirror" or source_owner not in pattern_frame.get("copy_source_features", []): return None source_frame = feature_frames.get(source_owner) if source_frame is None or "start" not in source_frame or "end" not in source_frame: return None cap = source_frame["start" if _bool(source_definition.get("isStart")) else "end"] plane = transform.get("plane") if not isinstance(plane, dict): return None reflected = _reflect_frame(cap, plane) # 镜像会把 source frame 变成左手系;CDSL workplane 必须始终由 # normal × x_dir 推导局部 y 轴。翻转 x_dir 后,镜像 CAP 上新草图的 # source-local y 方向保持物理不变,FeatureScript 导出的镜像侧 x 坐标 # 也能落回正确的世界位置。 reflected["x_dir"] = [-value for value in reflected["x_dir"]] return reflected def _plane_from_query( value: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]] | None = None, entity_by_sketch: dict[str, dict[str, dict[str, Any]]] | None = None, ) -> dict[str, Any]: for call in walk_calls(value): if call.name in {"makeId", "qCreatedBy"}: text = " ".join(symbolic_string(arg) for arg in call.args) for name, plane in PLANES.items(): if f"{name}.planeOp" in text: return dict(plane) copied_cap = _pattern_copy_cap_plane(value, feature_frames) if copied_cap is not None: return copied_cap # Workplane materialization must follow the outer query expression. A # A derived topology query may contain a nested CAP/SWEPT face, but that # producer frame is not proof that the outer query has one active face # successor. ``parse_query`` visits nested calls for diagnostic context, # so it cannot define this boundary. The explicit mirrored COPY(CAP_FACE) # contract was handled above; COPY and MERGE workplanes both require a # dedicated runtime face relation before a sketch can attach to them. try: _call, owner, topology, outer_kind, _definition = _direct_make_query(value) except ValueError: owner = topology = outer_kind = None query = parse_query(value) direct_created_by = value if isinstance(direct_created_by, Call) and direct_created_by.name == "qUnion" and len(direct_created_by.args) == 1 and isinstance(direct_created_by.args[0], list) and len(direct_created_by.args[0]) == 1: direct_created_by = direct_created_by.args[0][0] if isinstance(direct_created_by, Call) and direct_created_by.name == "qCreatedBy": owner = query.owner_feature if topology == "IMPRINT" and sketch_by_source and query.source_sketch in sketch_by_source: return dict(sketch_by_source[query.source_sketch]["workplane"]) if topology == "OFFSET_FACE" and sketch_by_source is not None and entity_by_sketch is not None: return _offset_face_plane(value, feature_frames, sketch_by_source, entity_by_sketch) if topology == "COPY" and outer_kind == "face": raise ValueError( "COPY(FACE) workplane requires a dedicated complete/proven runtime face relation" ) if topology == "MERGE" and outer_kind == "face": raise ValueError( "MERGE(FACE) workplane requires a dedicated complete/proven runtime face relation" ) frame = feature_frames.get(owner or "") if frame and topology == "CAP_FACE": cap = "start" if query.is_start is not False else "end" return dict(frame.get(f"{cap}_attachment") or frame[cap]) if frame and frame.get("start") == frame.get("end") and isinstance(direct_created_by, Call) and direct_created_by.name == "qCreatedBy": return dict(frame.get("start_attachment") or frame["start"]) raise ValueError("unsupported or unresolved sketch workplane") def _bound_name(value: Any) -> str: return str(value or "BLIND").split(".")[-1].upper() def _is_new_body_operation(value: str) -> bool: # FeatureScript ``NewBodyOperationType.NEW`` creates an independent # context body. ``ADD`` is a boolean union into its merge scope, so it # must keep CDSL's default fusing result mode rather than being rewritten # as an independent member. normalized = str(value or "").upper() return normalized.rsplit(".", 1)[-1] == "NEW" def _has_active_body(features: list[dict[str, Any]]) -> bool: """Whether the lowered history already has a result body to extend.""" return any(feature.get("atomic_id") not in {"reference_plane", "reference_axis"} for feature in features) def _end_condition(value: Any) -> dict[str, Any]: name = _bound_name(value) if name == "BLIND": return {"type": "blind", "solidworks_code": 0} if name == "SYMMETRIC": return {"type": "mid_plane", "solidworks_code": 8} if name == "THROUGH_ALL": return {"type": "through_all", "solidworks_code": 1} if name == "UP_TO_NEXT": return {"type": "through_next", "solidworks_code": 4} if name == "UP_TO_SURFACE": return {"type": "up_to_surface", "solidworks_code": 2} if name == "UP_TO_BODY": return {"type": "up_to_body", "solidworks_code": 6} if name == "UP_TO_VERTEX": return {"type": "up_to_vertex", "solidworks_code": 5} raise UnsupportedCapability(f"extrude_extent:{name.lower()}", f"current CDSL atomic set has no exact extrusion operation for {name}") def _extent_reference( value: Any, expected_kind: str, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], *, feature_by_id: dict[str, dict[str, Any]] | None = None, sketches_by_id: dict[str, dict[str, Any]] | None = None, previous: list[str] | None = None, allow_cap_output_role: bool = False, allow_primary_add_up_to_surface: bool = False, allow_direct_prism_swept_lineage: bool = False, featurescript_version: str | None = None, ) -> dict[str, Any]: # qOwnerBody projects a selected topology member to its containing active # body. Its nested makeQuery retains the member kind, so recognize the # narrow executable bridge before checking the outer extent kind. if expected_kind == "body": owner_body = _direct_owner_body_extent_selector( value, feature_by_id=feature_by_id or {}, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id or {}, entity_by_sketch=entity_by_sketch, previous=previous or [], featurescript_version=featurescript_version, ) if owner_body is not None: return owner_body query = parse_query(value) if query.kind not in {expected_kind, f"entitytype.{expected_kind}"}: raise ValueError(f"extrude extent target is not a {expected_kind}") if expected_kind == "face": # An up-to-surface reference may consume a direct cap only while its # producing prism is the active preceding result. A later body # mutation, composed query, or unsupported producer keeps the source # query as deferred evidence instead of choosing a similar face. cap_output_role = ( _cap_face_output_role_selector(value, feature_by_id or {}, sketches_by_id or {}) if feature_by_id is not None and sketches_by_id is not None else None ) if ( allow_cap_output_role and cap_output_role is not None and previous[-1:] == [cap_output_role["owner_feature_id"]] ): return cap_output_role # A primary ADD prism is transient: its cap only becomes selectable # after the exact union topology delta proves one active successor. # This is intentionally a separate consumer opt-in from the direct # new-body cap bridge, and remains limited to the immediately # following one-sided up-to-surface extent. primary_add_cap_output_role = ( _cap_face_output_role_selector( value, feature_by_id or {}, sketches_by_id or {}, allow_primary_add_up_to_surface=True, ) if feature_by_id is not None and sketches_by_id is not None else None ) if ( allow_primary_add_up_to_surface and primary_add_cap_output_role is not None and previous[-1:] == [primary_add_cap_output_role["owner_feature_id"]] ): return primary_add_cap_output_role # A direct blind prism side wall is not an output-role shortcut. Its # source edge and every later continuation must be proven by the # topology registry. This bounded consumer only emits the executable # intent for a one-sided extent; all other face queries stay deferred. if ( allow_direct_prism_swept_lineage and query.topology_type == "SWEPT_FACE" and query.owner_feature and feature_by_id is not None and sketches_by_id is not None ): swept_lineage = _direct_prism_swept_selector( value, owner=query.owner_feature, selector_kind="face", feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous or [], featurescript_version=featurescript_version, allow_continuation=True, allow_immediate_retained_source_edge=True, ) if swept_lineage is not None: return swept_lineage if expected_kind == "body": swept_body = _direct_swept_body_extent_selector( value, feature_by_id=feature_by_id or {}, previous=previous or [], featurescript_version=featurescript_version, ) if swept_body is not None: return swept_body # A direct source owner alone is not evidence for an active result # topology member. In particular, a deferred CAP/SWEPT query cannot be # converted into an extent reference via a stable ID or a geometric # fallback: that would authorize an arbitrary later/current body and can # also leave an owner that was never lowered. Keep the feature deferred # until a bounded provenance contract above proves the exact reference. raise UnsupportedCapability( f"extrude_extent_{expected_kind}_selector", f"current CDSL {expected_kind} extent requires a complete/proven active selector reference", ) def _direct_make_query(value: Any) -> tuple[Call, str, str, str, dict[str, Any]]: """Return one direct CADFS makeQuery without flattening its provenance.""" current = value if isinstance(current, Call) and current.name == "qUnion" and len(current.args) == 1 and isinstance(current.args[0], list): if len(current.args[0]) != 1: raise ValueError("query union does not identify one topology item") current = current.args[0][0] if not isinstance(current, Call) or current.name != "makeQuery" or len(current.args) < 3: raise ValueError("topology query is unresolved") owner_text = symbolic_string(current.args[0]) if "F" not in owner_text: raise ValueError("topology query owner is unresolved") owner = owner_text[owner_text.find("F"):].split(".", 1)[0] topology = str(current.args[1]).split(".")[-1].upper() kind = str(current.args[2]).split(".")[-1].lower() definition = next((arg for arg in current.args if isinstance(arg, dict)), {}) return current, owner, topology, kind, definition def _direct_swept_body_extent_selector( value: Any, *, feature_by_id: dict[str, dict[str, Any]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Lower one direct active body query for a one-sided ``up_to_body`` extent. A single-item ``qUnion`` is the FeatureScript set identity, but it does not grant general query composition. The producer has to be the immediately active independent prism, so runtime can prove the selected body from the body graph without using a stable ID, aggregate body, or geometry score. """ if featurescript_version != "1511": return None try: _call, owner, topology, kind, _definition = _direct_make_query(value) except ValueError: return None producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) or {} params = producer.get("params") or {} if ( topology != "SWEPT_BODY" or kind not in {"body", "entitytype.body"} or previous[-1:] != [producer_id] or producer.get("atomic_id") != "extrude_add_blind" or params.get("result_mode") != "new_body" or (params.get("end_condition") or {}).get("type") not in {"blind", "through_all"} or params.get("draft") is not None ): return None intent = _selector_intent( value, query_family="SWEPT_BODY", kind="body", evidence="active_body_member", allowed=("boundary",), ) intent["body_member_contract"] = "direct_new_body" return { "kind": "body", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } def _direct_prism_cap_vertex_extent_selector( value: Any, *, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Lower one immediate direct-prism CAP_VERTEX target to kernel history. A CAP vertex is not identified by its cap-plane coordinates. The two source profile edges must name exactly one original shared endpoint, and OCC must carry that source vertex to the requested prism cap. This first contract deliberately excludes continuation, draft, multi-profile, and additive/cutting body lifecycles. """ if featurescript_version != "1511": return None try: _call, owner, topology, kind, _definition = _direct_make_query(value) except ValueError: return None query = parse_query(value) producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) or {} params = producer.get("params") or {} frame = feature_frames.get(owner) or {} profile_source = frame.get("profile_source") profile_sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) source_sketch = sketch_by_source.get(profile_source) if isinstance(profile_source, str) else None if producer_id not in previous: return None producer_index = previous.index(producer_id) intervening = previous[producer_index + 1:] # An independent member can remain selectable across datum features and # later independent new-body creation. Any operation that may mutate, # fuse, delete, copy, or transform the producer is deliberately excluded; # runtime still has to prove the exact member-preservation relation. preserves_member = all( (feature_by_id.get(feature_id) or {}).get("atomic_id") == "reference_plane" or ( (feature_by_id.get(feature_id) or {}).get("atomic_id") == "extrude_add_blind" and ((feature_by_id.get(feature_id) or {}).get("params") or {}).get("result_mode") == "new_body" and (((feature_by_id.get(feature_id) or {}).get("params") or {}).get("end_condition") or {}).get("type") == "blind" and ((feature_by_id.get(feature_id) or {}).get("params") or {}).get("draft") is None ) for feature_id in intervening ) if ( topology != "CAP_VERTEX" or kind not in {"vertex", "entitytype.vertex"} or query.is_start is None or not preserves_member or producer.get("atomic_id") != "extrude_add_blind" or params.get("result_mode") != "new_body" or (params.get("end_condition") or {}).get("type") not in {"blind", "through_all"} or params.get("draft") is not None or not isinstance(profile_source, str) or source_sketch is None or profile_sketch is None or profile_sketch.get("source_sketch_id") != profile_source or not _profile_matches_direct_source(profile_sketch, source_sketch) ): return None refs = _source_refs(value) if len(refs) != 2 or {source for source, _token in refs} != {profile_source}: return None source_ids = _direct_profile_source_entity_ids(profile_sketch) resolved_ids: list[str] = [] for source, token in refs: resolved = _source_ref_entity(source, token, entity_by_sketch) if resolved is None: return None entity_id, entity = resolved if entity.get("construction") or entity_id not in source_ids or entity_id in resolved_ids: return None resolved_ids.append(entity_id) if _shared_source_endpoint(refs, profile_source, entity_by_sketch) is None: return None intent = _selector_intent( value, query_family="CAP_VERTEX", kind="vertex", evidence="kernel_history", allowed=("boundary", "continuation") if intervening else ("boundary",), ) intent.pop("source_entity", None) intent["source_entities"] = [ {"sketch_id": profile_source, "entity_id": entity_id} for entity_id in sorted(resolved_ids) ] intent["lineage_role"] = f"extrude.{'start' if query.is_start else 'end'}" return { "kind": "vertex", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } def _direct_source_vertex_extent_reference( value: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], *, featurescript_version: str | None, ) -> dict[str, Any] | None: """Capture one direct source-sketch vertex as an immutable extent datum. This is deliberately not a topology selector. A direct ``sQuery`` vertex is defined by the source sketch's explicit workplane and entity endpoint, so it can supply an extrusion distance without asking the runtime to find a similarly placed B-rep vertex. Composed, derived, runtime-topology, and multi-item query forms stay outside this datum contract. """ if featurescript_version != "1511": return None queries = _queries(value) if len(queries) != 1 or not _is_direct_hole_location_query(queries[0]): return None source = _direct_hole_location(queries[0], sketch_by_source, entity_by_sketch) if source is None: return None local_point, plane = source info = parse_query(queries[0]) if not isinstance(info.source_sketch, str) or not isinstance(info.source_entity, str): return None return { "kind": "source_vertex", "source_sketch_id": info.source_sketch, "source_entity_id": info.source_entity, "point_mm": _global(plane, local_point[:2]), } def _direct_prism_cap_vertex_datum_point( value: Any, *, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> list[float] | None: """Calculate one source-defined direct-prism cap vertex for a datum. This is deliberately separate from runtime selector resolution. A datum frame needs a physical source point, while an extent needs a live active B-rep vertex. Both nevertheless require the same direct prism and OSD proof; no stale frame, transformed body, geometry search, or STEP result may stand in for a CAP_VERTEX query. """ if featurescript_version != "1511": return None try: _call, owner, topology, kind, _definition = _direct_make_query(value) except ValueError: return None query = parse_query(value) producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) or {} params = producer.get("params") or {} frame = feature_frames.get(owner) or {} profile_source = frame.get("profile_source") profile_sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) source_sketch = sketch_by_source.get(profile_source) if isinstance(profile_source, str) else None if producer_id not in previous: return None producer_index = previous.index(producer_id) intervening = previous[producer_index + 1:] def preserves_datum_source(feature_id: str) -> bool: feature = feature_by_id.get(feature_id) or {} atomic_id = feature.get("atomic_id") params = feature.get("params") or {} if atomic_id == "reference_plane": return True if ( atomic_id == "extrude_add_blind" and params.get("result_mode") == "new_body" and (params.get("end_condition") or {}).get("type") == "blind" and params.get("draft") is None ): return True # A direct one-cap shell can intervene for a datum-only CAP_VERTEX: # makeQuery(owner.opExtrude, CAP_VERTEX) names the source-defined # producer cap position, not a current shell-result vertex. Restrict # it to one exact producer cap role; side/set shells lack that proof. selectors = feature.get("selectors") or () return ( atomic_id == "shell" and len(selectors) == 1 and isinstance(selectors[0], dict) and selectors[0].get("kind") == "face" and selectors[0].get("owner_feature_id") == producer_id and selectors[0].get("output_role") in {"extrude.start", "extrude.end"} and (selectors[0].get("selector_intent") or {}).get("query_family") == "CAP_FACE" and (selectors[0].get("selector_intent") or {}).get("evidence") == "operation_role" ) preserves_source_datum = all( preserves_datum_source(feature_id) for feature_id in intervening ) if ( topology != "CAP_VERTEX" or kind not in {"vertex", "entitytype.vertex"} or query.is_start is None or not preserves_source_datum or producer.get("atomic_id") != "extrude_add_blind" or params.get("result_mode") != "new_body" or (params.get("end_condition") or {}).get("type") != "blind" or params.get("draft") is not None or not isinstance(profile_source, str) or source_sketch is None or profile_sketch is None or profile_sketch.get("source_sketch_id") != profile_source or not _profile_matches_direct_source(profile_sketch, source_sketch) ): return None refs = _source_refs(value) if len(refs) != 2 or {source for source, _token in refs} != {profile_source}: return None source_ids = _direct_profile_source_entity_ids(profile_sketch) resolved_ids: set[str] = set() for source, token in refs: resolved = _source_ref_entity(source, token, entity_by_sketch) if resolved is None: return None entity_id, entity = resolved if entity.get("construction") or entity_id not in source_ids or entity_id in resolved_ids: return None resolved_ids.add(entity_id) local_point = _shared_source_endpoint(refs, profile_source, entity_by_sketch) cap = frame.get("start" if query.is_start else "end") profile = frame.get("profile") if ( local_point is None or not isinstance(cap, dict) or not isinstance(profile, dict) or not isinstance(cap.get("origin_mm"), list) ): return None return _global({**profile, "origin_mm": list(cap["origin_mm"])}, local_point) def _q_owner_body_input(value: Any) -> Any | None: """Return the sole source input of a direct ``qOwnerBody`` expression.""" if not isinstance(value, Call) or value.name != "qOwnerBody" or len(value.args) != 1: return None return value.args[0] def _direct_owner_body_extent_selector( value: Any, *, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Lower one exact topology-to-active-body ``qOwnerBody`` extent. This is deliberately an ownership projection over a direct, already executable topology selector. It does not infer an owner from the current aggregate, an OCC proximity search, or a body-producing feature. """ if featurescript_version != "1511": return None query_input = _q_owner_body_input(value) if query_input is None: return None try: _call, owner, topology, kind, _definition = _direct_make_query(query_input) except ValueError: return None if kind != "face" or topology not in {"CAP_FACE", "SWEPT_FACE"}: return None producer_id = f"f_{owner}" if topology == "SWEPT_FACE": input_selector = _direct_prism_swept_selector( query_input, owner=owner, selector_kind="face", feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=featurescript_version, ) else: input_selector = _cap_face_output_role_selector( query_input, feature_by_id, sketches_by_id, ) if previous[-1:] == [producer_id] else None if input_selector is None: return None intent = _selector_intent( value, query_family="OWNER_BODY", kind="body", evidence="kernel_history", allowed=("boundary",), ) intent.update({ "owner_body_contract": "exact_input_owner", "body_scope": "active_member", "empty_policy": "reject", "multiple_policy": "one", }) return { "kind": "body", "source": "runtime_snapshot", "confidence": 1.0, "query_input": input_selector, "selector_intent": intent, } def _face_reference( value: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], *, owner_feature_id: str | None = None, binding_feature_id: str | None = None, match_mode: str | None = None, rotation: tuple[dict[str, list[float]], float] | None = None, ) -> dict[str, Any]: """Lower one CAP/SWEPT face query into a bindable CDSL selector.""" _call, owner, topology, kind, _definition = _direct_make_query(value) if kind != "face" or topology not in {"CAP_FACE", "SWEPT_FACE"}: raise ValueError("intersection source is not a supported face query") source = parse_query(value) selector_owner = owner_feature_id or f"f_{owner}" reference: dict[str, Any] = { "kind": "face", "owner_feature_id": selector_owner, "stable_id": f"cadfs_{selector_owner}_{topology.lower()}", "source": "solidworks", "confidence": 1.0, } frame = feature_frames.get(owner) geometry: dict[str, Any] point: list[float] if topology == "CAP_FACE": if frame is None or "start" not in frame or "end" not in frame: raise ValueError("cap face source frame is unresolved") cap = frame["start" if source.is_start else "end"] point, normal = list(cap["origin_mm"]), list(cap["normal"]) if rotation is not None: axis, angle = rotation; point, normal = _rotate_point(point, axis, angle), _rotate(normal, axis["direction"], angle) geometry = {"normal": normal, "plane_offset_mm": _dot(normal, point)} # 同一 feature 的多个圆形端盖可以共面。仅用平面方程会把它们误判为 # 同一个 selector;保留由 source circle 给出的物理圆心和最小面积, # 使 pattern COPY(CAP_FACE) 在 prefix B-rep 中仍可唯一绑定。 for sketch_id, entity_id in _source_refs(value): entity = (entity_by_sketch.get(sketch_id) or {}).get(entity_id) sketch = sketch_by_source.get(sketch_id) if entity is None or sketch is None or entity.get("type") != "circle": continue center = _global(cap, entity["center"]) if rotation is not None: axis, angle = rotation; center = _rotate_point(center, axis, angle) geometry["center_mm"] = center geometry["minimum_area_mm2"] = math.pi * float(entity["radius_mm"]) ** 2 * 0.5 break else: sketch = sketch_by_source.get(source.source_sketch or "") entity = (entity_by_sketch.get(source.source_sketch or "") or {}).get(source.source_entity or "") if sketch is None or entity is None: raise ValueError("swept face source geometry is unresolved") # 直接 source 草图可能已被后续 transform 烘焙到 feature profile。 # SWEPT_FACE 必须在该实际 profile workplane 上还原,不能回退到变换前 # 的草图坐标系,否则 pattern copy 的平面 selector 会落在错误位置。 plane = (frame or {}).get("profile") or sketch["workplane"] if entity["type"] == "circle": point, direction = _global(plane, entity["center"]), list(plane["normal"]) if rotation is not None: axis, angle = rotation; point, direction = _rotate_point(point, axis, angle), _rotate(direction, axis["direction"], angle) geometry = {"axis_origin_mm": point, "axis_direction": direction, "radius_mm": entity["radius_mm"]} elif entity["type"] == "line": point = _global(plane, entity["start"]); end = _global(plane, entity["end"]) normal = _unit(_cross(_sub(end, point), plane["normal"]), "swept face source line is degenerate") if rotation is not None: axis, angle = rotation; point, normal = _rotate_point(point, axis, angle), _rotate(normal, axis["direction"], angle) geometry = {"normal": normal, "plane_offset_mm": _dot(normal, point)} if rotation is None and frame and frame.get("start") and frame.get("end"): span = math.dist(frame["start"]["origin_mm"], frame["end"]["origin_mm"]) length = math.dist(point, end) if span > 1e-6 and length > 1e-6: # 后续圆角和布尔可能将一个侧壁分裂成同平面的多个 face。 # 原始 SWEPT_FACE 的母线长度与拉伸跨度给出确定的面积下界, # 可排除与其共面的微小端盖,而不要求内核保留原始面积。 geometry["minimum_area_mm2"] = length * span * 0.5 else: raise ValueError("swept face source entity is unsupported") reference["geometry"] = geometry reference["selector_intent"] = _deferred_featurescript_selector_intent(value, kind="face") if binding_feature_id is not None: reference["binding_feature_id"] = binding_feature_id if match_mode is not None: reference["match_mode"] = match_mode return reference def _direct_linear_extrude_swept_face_shell_reference( value: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_by_id: dict[str, dict[str, Any]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any]: """Lower one source-proven linear-extrude side wall for a shell removal. This is deliberately narrower than generic ``SWEPT_FACE`` replay. The original sketch line defines an exact planar side wall only while its direct blind/two-sided extrusion is the immediately preceding producer; boolean, dress-up, copy, and transformed continuations have different ownership and are left to future topology-history contracts. """ _call, owner, topology, kind, _definition = _direct_make_query(value) producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) or {} frame = feature_frames.get(owner) or {} params = producer.get("params") or {} atomic_id = str(producer.get("atomic_id") or "") supported_atomics = { "extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided", } profile_source = frame.get("profile_source") source_sketch = sketch_by_source.get(profile_source) if isinstance(profile_source, str) else None profile_sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) immediate_producer = previous[-1:] == [producer_id] immediate_primary_cut = False if len(previous) >= 2 and previous[-2] == producer_id: cut = feature_by_id.get(previous[-1]) or {} cut_params = cut.get("params") or {} immediate_primary_cut = ( cut.get("atomic_id") == "extrude_cut_blind" and cut.get("depends_on") == [producer_id] and (cut_params.get("end_condition") or {}).get("type") == "blind" and cut_params.get("draft") is None ) if ( topology != "SWEPT_FACE" or kind not in {"face", "entitytype.face"} or atomic_id not in supported_atomics or not (immediate_producer or immediate_primary_cut) or not isinstance(frame.get("profile"), dict) or not isinstance(frame.get("start"), dict) or not isinstance(frame.get("end"), dict) or source_sketch is None or profile_sketch is None or not _profile_matches_direct_source(profile_sketch, source_sketch) or (params.get("end_condition") or {}).get("type") != "blind" or ( atomic_id.endswith("two_sided") and (params.get("reverse_end_condition") or {}).get("type") != "blind" ) ): raise UnsupportedCapability( "shell_face_selector", "current CDSL shell SWEPT_FACE requires the immediately preceding direct blind/two-sided linear extrusion", ) query = parse_query(value) refs = _source_refs(value) if ( len(refs) != 1 or refs[0][0] != profile_source or query.source_sketch != profile_source or query.source_entity != refs[0][1] ): raise UnsupportedCapability( "shell_face_selector", "current CDSL shell SWEPT_FACE requires one direct source-profile line", ) entity = (entity_by_sketch.get(profile_source) or {}).get(refs[0][1]) if not entity or entity.get("type") != "line" or entity.get("construction"): raise UnsupportedCapability( "shell_face_selector", "current CDSL shell SWEPT_FACE requires one original non-construction source line", ) lineage_selector = _direct_prism_swept_selector( value, owner=owner, selector_kind="face", feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=featurescript_version, # The only non-immediate shell tuple keeps one exact target-side # BRepAlgoAPI_Cut continuation between the prism and shell. allow_continuation=immediate_primary_cut, ) if lineage_selector is not None: return lineage_selector return _face_reference(value, feature_frames, sketch_by_source, entity_by_sketch) def _shell_offset_face_output_role_selector( value: Any, feature_by_id: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], previous: list[str], ) -> dict[str, Any]: """Lower a shell OFFSET_FACE only through its explicit CAP true dependency. An OFFSET_FACE is a generated B-rep face, so a bare geometric signature is not enough: a shell may generate several offset faces. CADFS can retain the exact semantic source as a nested true-dependency CAP query. Preserve that relation for the runtime topology registry instead of choosing a nearby shell face. """ _call, owner, topology, kind, _definition = _direct_make_query(value) owner_feature_id = f"f_{owner}" producer = feature_by_id.get(owner_feature_id) if ( topology != "OFFSET_FACE" or kind not in {"face", "entitytype.face"} or producer is None or producer.get("atomic_id") != "shell" or previous[-1:] != [owner_feature_id] ): raise UnsupportedCapability( "shell_offset_face_selector", "current CDSL shell OFFSET_FACE requires the immediately preceding direct shell owner", ) # Only a true-dependency disambiguation may establish a source relation. # ``walk_calls`` is deliberately not used here: arbitrary nested CAP_FACE # queries can describe ordering or source-profile evidence, but do not # prove which shell offset face they produced. source_roles = [] disambiguation = _definition.get("disambiguationData") for item in disambiguation if isinstance(disambiguation, list) else (): if ( not isinstance(item, Call) or item.name not in {"TDD", "trueDependencyDisambiguation"} or len(item.args) != 1 or not isinstance(item.args[0], list) ): continue for candidate in item.args[0]: try: cap = _cap_face_output_role_selector(candidate, feature_by_id, sketches_by_id) except ValueError: continue if cap is not None: source_roles.append(cap) unique_roles = { (item["owner_feature_id"], item["output_role"]): item for item in source_roles } if len(unique_roles) != 1: raise UnsupportedCapability( "shell_offset_face_selector", "current CDSL shell OFFSET_FACE requires one direct builder CAP_FACE true dependency", ) source = next(iter(unique_roles.values())) return { "kind": "face", "owner_feature_id": owner_feature_id, "output_role": "shell.offset_face", "output_role_source": { "owner_feature_id": source["owner_feature_id"], "output_role": source["output_role"], }, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": _selector_intent( value, query_family="OFFSET_FACE", kind="face", evidence="operation_role", allowed=("boundary", "replacement"), output_role="shell.offset_face", disambiguation={"type": "true_dependency", "sources": [ {"owner_feature_id": source["owner_feature_id"], "output_role": source["output_role"]}, ]}, ), } def _shell_retained_direct_prism_cap_offset_face_profile_selector( value: Any, feature_by_id: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], previous: list[str], *, featurescript_version: str | None, ) -> dict[str, Any] | None: """Use a retained shell cap as an extrusion profile only with full source proof. This is deliberately distinct from the TDD OFFSET_FACE selector bridge. The exported query names the entire original direct-prism profile through one OSD set, while the immediately preceding shell removes the opposite cap. The runtime still has to prove the retained cap -> offset-face kernel relation; source-profile membership alone never selects a wall. """ if featurescript_version != "1511": return None try: _call, shell_name, topology, kind, definition = _direct_make_query(value) except ValueError: return None shell_id = f"f_{shell_name}" shell = feature_by_id.get(shell_id) or {} if ( topology != "OFFSET_FACE" or kind not in {"face", "entitytype.face"} or previous[-1:] != [shell_id] or shell.get("atomic_id") != "shell" ): return None # This contract describes one retained cap only. More than one removal # target can alter the cap's topology and must remain a diagnostic. shell_selectors = shell.get("selectors") or [] if len(shell_selectors) != 1 or not isinstance(shell_selectors[0], dict): return None removed = shell_selectors[0] removed_intent = removed.get("selector_intent") or {} source_id = removed.get("owner_feature_id") removed_role = removed.get("output_role") if ( not isinstance(source_id, str) or source_id not in feature_by_id or removed_role not in {"extrude.start", "extrude.end"} or removed_intent.get("query_family") != "CAP_FACE" or shell.get("depends_on") != [source_id] ): return None source_feature = feature_by_id[source_id] source_params = source_feature.get("params") or {} source_sketch = sketches_by_id.get(str(source_feature.get("sketch_id") or "")) if ( source_feature.get("atomic_id") != "extrude_add_blind" or source_params.get("result_mode") != "new_body" or (source_params.get("end_condition") or {}).get("type") != "blind" or source_params.get("draft") is not None or not isinstance(source_sketch, dict) ): return None source_ids = _direct_profile_source_entity_ids(source_sketch) profile_source = source_sketch.get("source_sketch_id") query = parse_query(value) refs = _source_refs(value) if ( not source_ids or not isinstance(profile_source, str) or query.source_sketch != profile_source or len(refs) != len(source_ids) or {sketch_id for sketch_id, _entity_id in refs} != {profile_source} or {entity_id for _sketch_id, entity_id in refs} != source_ids ): return None disambiguation = definition.get("disambiguationData") if ( not isinstance(disambiguation, list) or len(disambiguation) != 1 or not isinstance(disambiguation[0], Call) or disambiguation[0].name not in {"OSD", "originalSetDisambiguation"} or len(disambiguation[0].args) != 1 or not isinstance(disambiguation[0].args[0], list) ): return None retained_role = "extrude.end" if removed_role == "extrude.start" else "extrude.start" intent = _selector_intent( value, query_family="OFFSET_FACE", kind="face", evidence="operation_role", allowed=("boundary", "replacement"), output_role="shell.offset_face", disambiguation={ "source_profile_entity_ids": sorted(source_ids), "removed_cap_role": removed_role, }, ) intent["consumer_contract"] = "shell_retained_direct_prism_cap_offset_face_profile" return { "kind": "face", "owner_feature_id": shell_id, "output_role": "shell.offset_face", "output_role_source": { "owner_feature_id": source_id, "output_role": retained_role, }, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } def _pattern_copy_face_reference( value: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_by_id: dict[str, dict[str, Any]], ) -> dict[str, Any]: """Preserve a circular-pattern COPY face as one specific replay instance.""" _call, pattern_owner, topology, kind, definition = _direct_make_query(value) if topology != "COPY" or kind != "face": raise ValueError("intersection copy source is unresolved") derived = definition.get("derivedFrom") if derived is None: raise ValueError("pattern copy has no derived face") _source_call, source_owner, _source_topology, source_kind, _source_definition = _direct_make_query(derived) if source_kind != "face": raise ValueError("pattern copy source is not a face") pattern_id = f"f_{pattern_owner}"; source_id = f"f_{source_owner}" pattern = feature_by_id.get(pattern_id) if pattern is None or pattern.get("atomic_id") != "pattern_circular": raise ValueError("pattern copy owner is not a circular pattern") if source_id not in (pattern.get("params") or {}).get("source_feature_ids", []): raise ValueError("pattern copy source feature is not replayed by its owner") try: instance = int(str(definition.get("instanceName"))) except (TypeError, ValueError) as error: raise ValueError("pattern copy instance is unresolved") from error axis = (pattern.get("params") or {}).get("axis") count = int((pattern.get("params") or {}).get("pattern_count") or 0) if not isinstance(axis, dict) or count < 1 or instance < 0 or instance >= count: raise ValueError("pattern copy instance transform is unresolved") angle = math.radians(float((pattern.get("params") or {}).get("sweep_angle_deg") or 360.0) * instance / count) reference = _face_reference( derived, feature_frames, sketch_by_source, entity_by_sketch, owner_feature_id=f"{pattern_id}.c{instance}.{source_id}", binding_feature_id=pattern_id, rotation=(axis, angle), ) reference["owner_match_required"] = True # The outer COPY query, not its CAP/SWEPT source, defines the semantic # request for the pattern instance. reference["selector_intent"] = _deferred_featurescript_selector_intent(value, kind="face") return reference def _intersection_vertex_reference( value: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_by_id: dict[str, dict[str, Any]], feature_id: str, ) -> dict[str, Any]: """Lower a CADFS INTERSECT vertex without replacing it with a coordinate.""" _call, owner, topology, kind, definition = _direct_make_query(value) if topology != "INTERSECT" or kind != "vertex": raise ValueError("up-to-vertex target is not an INTERSECT vertex") derived = definition.get("derivedFrom") if not isinstance(derived, list) or len(derived) < 2: raise ValueError("intersection vertex has no complete face provenance") components = [] for item in derived: _item_call, item_owner, item_topology, item_kind, _item_definition = _direct_make_query(item) if item_kind != "face": raise ValueError("intersection vertex source is not a face") if item_topology == "COPY": components.append(_pattern_copy_face_reference(item, feature_frames, sketch_by_source, entity_by_sketch, feature_by_id)) elif item_topology == "SWEPT_FACE": components.append(_face_reference( item, feature_frames, sketch_by_source, entity_by_sketch, owner_feature_id=f"f_{item_owner}", binding_feature_id=f"f_{item_owner}", match_mode="all", )) else: raise ValueError(f"intersection vertex source topology {item_topology} is unsupported") return { "kind": "vertex", "owner_feature_id": f"f_{owner}", "stable_id": f"cadfs_f_{feature_id}_intersect_vertex", "source": "solidworks", "confidence": 1.0, "intersection_of": components, "selector_intent": _deferred_featurescript_selector_intent(value, kind="vertex"), } def _arc(start: list[float], mid: list[float], end: list[float]) -> dict[str, Any]: ax, ay = start; bx, by = mid; cx, cy = end d = 2 * (ax*(by-cy) + bx*(cy-ay) + cx*(ay-by)) if abs(d) < 1e-9: raise ValueError("collinear arc points") ux = ((ax*ax+ay*ay)*(by-cy)+(bx*bx+by*by)*(cy-ay)+(cx*cx+cy*cy)*(ay-by))/d uy = ((ax*ax+ay*ay)*(cx-bx)+(bx*bx+by*by)*(ax-cx)+(cx*cx+cy*cy)*(bx-ax))/d cross = (mid[0]-start[0])*(end[1]-mid[1])-(mid[1]-start[1])*(end[0]-mid[0]) clockwise = cross < 0 # CADFS 的倒圆角会把中点按显示精度写回 FeatureScript。端点已经完整 # 表达等半径直角圆弧时,优先从端点恢复原始圆心,避免把显示精度误作 # 几何精度;任意三点圆弧仍保持原来的外接圆求解。 for center in ([ax, cy], [cx, ay]): first = [ax - center[0], ay - center[1]]; second = [cx - center[0], cy - center[1]] radius = math.hypot(*first) if radius <= 1e-9 or abs(math.hypot(*second) - radius) > 1e-9 or abs(_dot(first, second)) > 1e-9: continue start_angle = math.atan2(first[1], first[0]); end_angle = math.atan2(second[1], second[0]) sweep = end_angle - start_angle if clockwise and sweep >= 0: sweep -= math.tau elif not clockwise and sweep <= 0: sweep += math.tau expected = [center[0] + radius * math.cos(start_angle + sweep / 2), center[1] + radius * math.sin(start_angle + sweep / 2)] if math.dist(mid, expected) <= max(1e-6, radius * 5e-4): return {"type": "arc", "start": start, "end": end, "center": center, "radius_mm": radius, "clockwise": clockwise} return {"type": "arc", "start": start, "end": end, "center": [ux, uy], "radius_mm": math.hypot(ax-ux, ay-uy), "clockwise": clockwise} def _endpoint(segment: dict[str, Any], end: bool = False) -> tuple[int, int]: point = segment["end" if end else "start"]; return round(point[0]*1e5), round(point[1]*1e5) def _reverse_segment(segment: dict[str, Any]) -> dict[str, Any]: output = dict(segment) output["start"], output["end"] = segment["end"], segment["start"] if output["type"] == "arc": output["clockwise"] = not bool(segment["clockwise"]) elif output["type"] == "bspline": # OCC 的 periodic interpolator 对 reversed point list 会求出另一条 # 曲线。半边图只需要反转其拓扑走向,保留原插值点并以私有标记供 # 切线和面积计算使用;该标记不会进入 CDSL profile。 if output.get("periodic"): output["_reversed"] = not bool(segment.get("_reversed")) else: output["points"] = list(reversed(segment["points"])) parameters = output.get("parameters") if parameters is not None: final_parameter = float(parameters[-1]) output["parameters"] = [final_parameter - float(value) for value in reversed(parameters)] start_tangent = output.pop("start_tangent", None) end_tangent = output.pop("end_tangent", None) if end_tangent is not None: output["start_tangent"] = [-float(value) for value in end_tangent] if start_tangent is not None: output["end_tangent"] = [-float(value) for value in start_tangent] return output def _segment_tangent(segment: dict[str, Any]) -> tuple[float, float]: if segment["type"] == "line": return segment["end"][0] - segment["start"][0], segment["end"][1] - segment["start"][1] if segment["type"] == "arc": radius = [segment["start"][0] - segment["center"][0], segment["start"][1] - segment["center"][1]] return (radius[1], -radius[0]) if segment["clockwise"] else (-radius[1], radius[0]) points = segment["points"] if segment.get("periodic") and segment.get("_reversed"): return points[0][0] - points[-2][0], points[0][1] - points[-2][1] return points[1][0] - points[0][0], points[1][1] - points[0][1] def _contour_area(segments: list[dict[str, Any]]) -> float: points: list[list[float]] = [] for segment in segments: points.append(segment["start"]) if segment["type"] == "arc": start = segment["start"]; end = segment["end"]; center = segment["center"] start_angle = math.atan2(start[1] - center[1], start[0] - center[0]) end_angle = math.atan2(end[1] - center[1], end[0] - center[0]) sweep = end_angle - start_angle if segment["clockwise"] and sweep >= 0: sweep -= 2 * math.pi if not segment["clockwise"] and sweep <= 0: sweep += 2 * math.pi angle = start_angle + sweep / 2 points.append([center[0] + segment["radius_mm"] * math.cos(angle), center[1] + segment["radius_mm"] * math.sin(angle)]) elif segment["type"] == "bspline": interior = segment["points"][1:-1] points.extend(reversed(interior) if segment.get("_reversed") else interior) return sum(points[index][0] * points[(index + 1) % len(points)][1] - points[(index + 1) % len(points)][0] * points[index][1] for index in range(len(points))) / 2 def _split_line_vertices(edges: list[dict[str, Any]]) -> list[dict[str, Any]]: # FeatureScript 允许小轮廓端点落在一条未显式分段的长线上。面环遍历前将 # 长线按这些顶点切开,避免 T 形连接被误判为开链。 points = [point for edge in edges for point in (edge["start"], edge["end"])] output: list[dict[str, Any]] = [] for edge in edges: if edge["type"] != "line": output.append(edge); continue start, end = edge["start"], edge["end"] direction = [end[0] - start[0], end[1] - start[1]] length_squared = direction[0] * direction[0] + direction[1] * direction[1] if length_squared <= 1e-16: output.append(edge); continue cuts = {0.0, 1.0} for point in points: delta = [point[0] - start[0], point[1] - start[1]] parameter = (delta[0] * direction[0] + delta[1] * direction[1]) / length_squared cross = abs(delta[0] * direction[1] - delta[1] * direction[0]) if 1e-8 < parameter < 1.0 - 1e-8 and cross <= 1e-7 * max(1.0, math.sqrt(length_squared)): cuts.add(parameter) ordered = sorted(cuts) # A line cut by an intervening profile vertex no longer maps one-to-one # to its FeatureScript source entity. Keep the semantic source anchor # only when this operation retained the original B-rep edge intact. preserve_source = len(ordered) == 2 and isinstance(edge.get("source_entity_id"), str) output.extend({ "type": "line", "start": [start[0] + direction[0] * left, start[1] + direction[1] * left], "end": [start[0] + direction[0] * right, start[1] + direction[1] * right], **({"source_entity_id": edge["source_entity_id"]} if preserve_source else {}), } for left, right in zip(ordered, ordered[1:])) return output def _contours(segments: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: closed_curves = [{"role": "unknown", "closed": True, "segments": [item]} for item in segments if item["type"] in {"circle", "ellipse"}] edges = _split_line_vertices([item for item in segments if item["type"] not in {"circle", "ellipse"}]) # CADFS 草图可包含共享边和多个 qSketchRegion。逐条贪心接线会在第一个 # 分叉处吃掉共用边,使其余合法区域退化为“open”。这里按局部切线建立平面 # 半边图;每条有向边沿入射顶点的左侧继续,稳定枚举所有有界面环。 half_edges: list[dict[str, Any]] = [] outgoing: dict[tuple[int, int], list[int]] = {} for edge_index, edge in enumerate(edges): for forward, segment in ((True, edge), (False, _reverse_segment(edge))): index = len(half_edges) half_edges.append({"edge_index": edge_index, "forward": forward, "segment": segment}) outgoing.setdefault(_endpoint(segment), []).append(index) for vertex, indexes in outgoing.items(): del vertex indexes.sort(key=lambda index: (math.atan2(*reversed(_segment_tangent(half_edges[index]["segment"]))), index)) next_edge: dict[int, int] = {} for index, item in enumerate(half_edges): arrivals = outgoing[_endpoint(item["segment"], True)] twin = next(candidate for candidate in arrivals if half_edges[candidate]["edge_index"] == item["edge_index"] and half_edges[candidate]["forward"] != item["forward"]) next_edge[index] = arrivals[(arrivals.index(twin) - 1) % len(arrivals)] visited: set[int] = set(); used_edges: set[int] = set(); contours = [] for start in range(len(half_edges)): if start in visited: continue chain: list[int] = []; current = start while current not in visited: visited.add(current); chain.append(current); current = next_edge[current] if current != start: continue contour = [half_edges[index]["segment"] for index in chain] if contour and _contour_area(contour) > 1e-8: # `_reversed` belongs only to the planar half-edge traversal. # A periodic B-spline remains the same physical closed profile # regardless of the retained OCC edge orientation. public_segments = [{key: value for key, value in segment.items() if key != "_reversed"} for segment in contour] contours.append({"role": "unknown", "closed": True, "segments": public_segments}) used_edges.update(half_edges[index]["edge_index"] for index in chain) construction = [edge for index, edge in enumerate(edges) if index not in used_edges] return contours + closed_curves, construction def _recover_imperial_grid(items: list[dict[str, Any]], point_records: list[dict[str, Any]]) -> None: # 部分 CADFS 导出会将英制草图的个别坐标截断为两位小数,但其余坐标仍 # 保留 0.001 in 网格。只在同一草图已能证明该网格时恢复相邻显示值; # 半径和 feature 参数不属于这一坐标恢复规则,绝不能在这里猜测或改写。 grid = 0.0254; tolerance = grid / 5 coordinates: list[float] = [] def collect(point: list[float] | None) -> None: if isinstance(point, list): coordinates.extend(float(value) for value in point[:2]) for item in items: if item["type"] in {"line", "arc"}: collect(item.get("start")); collect(item.get("end")) if item["type"] in {"circle", "ellipse"}: collect(item.get("center")) for point_record in point_records: collect(point_record.get("point")) nonzero = [value for value in coordinates if abs(value) > 1e-9] if not nonzero: return error = lambda value: abs(value - round(value / grid) * grid) aligned = [value for value in nonzero if error(value) <= tolerance] exact = [value for value in nonzero if error(value) <= 1e-8] if len(aligned) / len(nonzero) < 0.9 or len(exact) * 2 < len(nonzero): return def recover(point: list[float] | None) -> None: if not isinstance(point, list): return for index, value in enumerate(point[:2]): if error(float(value)) <= tolerance: point[index] = round(float(value) / grid) * grid for item in items: if item["type"] in {"line", "arc"}: recover(item.get("start")); recover(item.get("end")) if item["type"] in {"circle", "ellipse"}: recover(item.get("center")) for point_record in point_records: recover(point_record.get("point")) def _centripetal_parameters(points: list[list[float]]) -> list[float]: parameters = [0.0] for start, end in zip(points, points[1:]): distance = math.dist(start, end) if distance <= 1e-9: raise ValueError("fit spline has coincident interpolation points") parameters.append(parameters[-1] + math.sqrt(distance)) total = parameters[-1] return [value / total for value in parameters] def _lower_sketch(sketch: SketchIR, plane: dict[str, Any], allow_open: bool = False) -> tuple[dict[str, Any], dict[str, dict[str, Any]]]: segments: list[dict[str, Any]] = []; explicit_construction: list[dict[str, Any]] = []; entities: dict[str, dict[str, Any]] = {}; points: list[dict[str, Any]] = []; unsupported = [] def sketch_result(profile: dict[str, Any], *, role: str | None = None) -> dict[str, Any]: result = { "id": f"sketch_{sketch.feature_id}", "name": sketch.feature_id, "source_sketch_id": sketch.feature_id, "workplane": plane, "profile": profile, } if role is not None: result["role"] = role return result for entity in sketch.entities: p = entity.params if entity.operation == "skPoint": item = {"type": "point", "point": _point(p["position"])}; entities[entity.feature_id] = item; points.append(item); continue if entity.operation == "skLineSegment": item = {"type": "line", "start": _point(p["start"]), "end": _point(p["end"])} elif entity.operation == "skCircle": item = {"type": "circle", "center": _point(p["center"]), "radius_mm": _number(p["radius"], True)} elif entity.operation == "skEllipse": major_axis = _unit(_point(p["majorAxis"]), "ellipse major axis is degenerate") item = {"type": "ellipse", "center": _point(p["center"]), "major_radius_mm": _number(p["majorRadius"], True), "minor_radius_mm": _number(p["minorRadius"], True), "major_axis": major_axis} elif entity.operation == "skArc": item = _arc(_point(p["start"]), _point(p["mid"]), _point(p["end"])) elif entity.operation == "skFitSpline": spline_points = [_point(point) for point in p.get("points") or []] if len(spline_points) < 2: raise ValueError("fit spline needs at least 2 points") start_derivative = p.get("startDerivative") end_derivative = p.get("endDerivative") if len(spline_points) == 2: if _same_point(spline_points[0], spline_points[1]): raise ValueError("two-point fit spline endpoints must be distinct") if start_derivative is None or end_derivative is None: raise ValueError("two-point fit spline requires both endpoint derivatives") # FeatureScript 的 skFitSpline 以根号弦长参数化。参数域既决定 # 插值曲线,也决定端点导数的长度语义;半边遍历反转轮廓时会将 # 它按反向参数域同步变换,不能交给 OCC 默认重新计算。 item = { "type": "bspline", "start": spline_points[0], "end": spline_points[-1], "points": spline_points, "parameterization": "centripetal", "parameters": _centripetal_parameters(spline_points), } # skFitSpline uses a periodic curve when the author closes it by # repeating the first interpolation point. Passing that repeated # point into a non-periodic interpolator changes the profile area # and produces a seam that does not exist in CADFS. if _same_point(spline_points[0], spline_points[-1]): item["periodic"] = True if start_derivative is not None: item["start_tangent"] = _point(start_derivative) if end_derivative is not None: item["end_tangent"] = _point(end_derivative) else: unsupported.append(entity.operation); continue if _bool(p.get("construction")): # Keep construction provenance available to topology-query lowering, # but never let it participate in a planar IMPRINT arrangement. # ``construction`` is runtime-only mapping metadata and must not # leak into the public analytic-segment schema. entities[entity.feature_id] = {**item, "construction": True} explicit_construction.append(item) else: # This is source construction provenance, not a geometric # selector hint. Derived or split segments deliberately do not # inherit this identity later in lowering. item["source_entity_id"] = entity.feature_id entities[entity.feature_id] = item segments.append(item) if unsupported: raise ValueError("unsupported sketch entities: " + ",".join(sorted(set(unsupported)))) _recover_imperial_grid(segments + explicit_construction, points) if not segments: profile: dict[str, Any] = {"type": "analytic_contours", "contours": []} if explicit_construction: profile["construction"] = explicit_construction return sketch_result(profile, role="reference"), entities if len(segments) == 1 and segments[0]["type"] == "circle" and not explicit_construction: profile = { "type": "circle", "center": segments[0]["center"], "radius_mm": segments[0]["radius_mm"], "source_entity_id": segments[0]["source_entity_id"], } else: contours, open_segments = _contours(segments) if open_segments: # qSketchRegion 只会选取闭合区域。与之无关的开链必须保留为 # reference geometry,不能因为它们存在就丢弃同一草图中的合法 # 区域;若草图没有任何闭合区域,则仍按原有规则拒绝实体 profile。 if not contours: if not allow_open: raise OpenSketchProfileError(f"sketch has {len(open_segments)} open non-construction segment(s)") profile = {"type": "analytic_contours", "contours": [], "construction": explicit_construction + open_segments} return sketch_result(profile, role="reference"), entities explicit_construction = [*explicit_construction, *open_segments] construction = list(explicit_construction) if not contours: profile = {"type": "analytic_contours", "contours": [], "construction": construction} return sketch_result(profile, role="reference"), entities profile = {"type": "analytic_contours", "contours": contours} if construction: profile["construction"] = construction return sketch_result(profile), entities def _queries(value: Any) -> list[Any]: if isinstance(value, Call) and value.name == "qUnion" and value.args and isinstance(value.args[0], list): return value.args[0] return [value] def _direct_query_set_operands(value: Any) -> tuple[str, str, list[Any]] | None: """Return a direct source set only when its operand boundary is explicit.""" if not isinstance(value, Call): return None if value.name == "qUnion": if len(value.args) != 1 or not isinstance(value.args[0], list): return None operands = value.args[0] contract = "proven_operand_union" operator = "union" elif value.name == "qIntersection": operands = value.args[0] if len(value.args) == 1 and isinstance(value.args[0], list) else value.args contract = "proven_operand_intersection" operator = "intersection" elif value.name == "qSubtraction": operands = value.args contract = "proven_operand_subtraction" operator = "subtraction" else: return None if ( not isinstance(operands, list) or len(operands) < 2 or contract == "proven_operand_subtraction" and len(operands) != 2 ): return None return contract, operator, operands def _query_set_leaf_values(value: Any) -> list[Any]: """Flatten only the leaves of an explicit recursive source set tree. The flattened list is an implementation detail of lowering: it lets the existing direct-provenance constructors handle every ``makeQuery`` leaf. ``_proven_query_set_selector`` below reconstructs the exact source tree, so flattening here never turns nested FeatureScript set semantics into an unstructured selector list. """ source_set = _direct_query_set_operands(value) if source_set is None: return [value] return [leaf for operand in source_set[2] for leaf in _query_set_leaf_values(operand)] def _proven_query_set_selector(value: Any, selectors: list[dict[str, Any]]) -> dict[str, Any] | None: """Preserve a recursive FeatureScript query set with proven operands. A selector set is not a list of equivalent single-selector decisions. Every set node retains its own source expression and runtime composes only already-proven direct leaf results. Filters and non-``makeQuery`` leaves stay deferred, but unions/intersections/subtractions can be freely nested when every branch has the same exact source provenance contract. """ if _direct_query_set_operands(value) is None: return None def direct_leaf(selector_value: Any, selector: dict[str, Any]) -> set[str] | None: """Return the leaf derivations only for an executable provenance leaf.""" intent = selector.get("selector_intent") policy = intent.get("derivation_policy") if isinstance(intent, dict) else None expression = intent.get("query_expr") if isinstance(intent, dict) else None if ( not isinstance(selector_value, Call) or selector_value.name != "makeQuery" or selector.get("source") != "runtime_snapshot" or any(selector.get(key) is not None for key in ( "stable_id", "snapshot_id", "geometry", "binding_feature_id", )) or not isinstance(intent, dict) or intent.get("query_family") in {None, "GEOMETRIC", "QUERY_SET"} or not isinstance(policy, dict) or policy.get("multiplicity") == "none" or not isinstance(expression, dict) or expression.get("root") != query_expr(selector_value)["root"] ): return None return {item for item in policy.get("allowed") or () if isinstance(item, str)} cursor = 0 def build(node: Any) -> tuple[dict[str, Any], set[str]] | None: nonlocal cursor source_set = _direct_query_set_operands(node) if source_set is None: if cursor >= len(selectors): return None selector = selectors[cursor] cursor += 1 allowed = direct_leaf(node, selector) return (selector, allowed) if allowed else None contract, _operator, operands = source_set children: list[dict[str, Any]] = [] allowed: set[str] = set() for operand in operands: child = build(operand) if child is None: return None child_selector, child_allowed = child children.append(child_selector) allowed.update(child_allowed) kinds = {child.get("kind") for child in children} if len(kinds) != 1 or next(iter(kinds), None) not in {"face", "edge"} or not allowed: return None kind = next(iter(kinds)) intent = _selector_intent( node, query_family="QUERY_SET", kind=kind, evidence="kernel_history", allowed=tuple(item for item in ( "continuation", "fragment", "merge", "intersection", "boundary", "replacement", ) if item in allowed), multiplicity="source_qualified", ) # A parent set has no singular source anchor. The exact anchors remain # in its recursively matched child selectors. intent.pop("source_entity", None) intent.update({ "query_set_contract": contract, "set_kind": kind, "body_scope": "active_member", "empty_policy": "reject", "multiple_policy": "all", }) return { "kind": kind, "source": "runtime_snapshot", "confidence": 1.0, "query_operands": children, "selector_intent": intent, }, allowed result = build(value) if result is None or cursor != len(selectors): return None return result[0] def _source_refs(value: Any) -> list[tuple[str, str]]: refs = [] for call in walk_calls(value): if call.name in {"sQuery", "sketchEntityQuery"} and len(call.args) >= 3: refs.append((symbolic_string(call.args[0]).split(".", 1)[0], str(call.args[2]))) return refs def _source_ref_entity( sketch_id: str, token: str, entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> tuple[str, dict[str, Any]] | None: """Resolve an original sketch entity without accepting derived-name aliases.""" entities = entity_by_sketch.get(sketch_id) or {} if token in entities: return token, entities[token] entity_id = max((key for key in entities if token.startswith(key + ".")), key=len, default="") if not entity_id: return None suffix = token[len(entity_id) + 1:] # A named source endpoint is still direct provenance. Other suffixes # (trim offspring, mirrored construction, generated fillet arcs, ...) do # not identify one source-local endpoint and must not be guessed. if suffix not in {"start", "end"}: return None return entity_id, entities[entity_id] def _source_ref_endpoint_points(entity_id: str, token: str, entity: dict[str, Any]) -> list[list[float]]: """Return only endpoint coordinates that FeatureScript explicitly exposes.""" if entity.get("type") == "point" and token == entity_id: point = entity.get("point") return [point] if isinstance(point, list) and len(point) == 2 else [] start, end = entity.get("start"), entity.get("end") if not all(isinstance(point, list) and len(point) == 2 for point in (start, end)): return [] if token == entity_id: return [start, end] suffix = token[len(entity_id) + 1:] return [start] if suffix == "start" else [end] if suffix == "end" else [] def _shared_source_endpoint( refs: list[tuple[str, str]], sketch_id: str, entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> list[float] | None: """Find the one explicit endpoint shared by a set of source curves. ``SWEPT_EDGE`` provenance from CADFS uses an original-set disambiguation containing the source curves incident at a profile vertex. A single curve has two possible ends, so it cannot identify a swept edge by itself. This routine deliberately accepts only one shared endpoint from distinct direct source entities, with the direct ``skPoint`` case retained for point-based provenance. """ selected: dict[str, tuple[str, dict[str, Any]]] = {} for source, token in refs: if source != sketch_id: continue resolved = _source_ref_entity(source, token, entity_by_sketch) if resolved is None: return None entity_id, entity = resolved selected.setdefault(entity_id, (token, entity)) if not selected: return None if len(selected) == 1: entity_id, (token, entity) = next(iter(selected.items())) if entity.get("type") == "point": points = _source_ref_endpoint_points(entity_id, token, entity) return points[0] if len(points) == 1 else None return None endpoints: list[tuple[str, list[float]]] = [] for entity_id, (token, entity) in selected.items(): points = _source_ref_endpoint_points(entity_id, token, entity) if not points: return None endpoints.extend((entity_id, point) for point in points) candidates: list[list[float]] = [] for _entity_id, point in endpoints: incident = { candidate_id for candidate_id, candidate_point in endpoints if math.dist(point, candidate_point) <= 1e-5 } if len(incident) < 2 or any(math.dist(point, candidate) <= 1e-5 for candidate in candidates): continue candidates.append(point) return candidates[0] if len(candidates) == 1 else None def _endpoint_bbox(start: list[float], end: list[float], *, known_line: bool) -> dict[str, Any] | None: if math.dist(start, end) <= 1e-8: return None geometry = {"bbox_mm": [min(start[i], end[i]) for i in range(3)] + [max(start[i], end[i]) for i in range(3)]} if known_line: geometry["curve_type"] = "line" return geometry def _swept_edge_line_selector_geometry( owner: str, refs: list[tuple[str, str]], frame: dict[str, Any], feature_by_id: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> dict[str, Any] | None: """Lower a directly proven ``SWEPT_EDGE`` into an endpoint-bbox selector. This is intentionally not a general topology replay. It covers an edge created by a translational extrude from one profile vertex, or by a direct two-section loft whose source query identifies one profile vertex on each section. Both constructions provide exact source-local endpoints. A sweep, revolve, multi-section loft, generated/trimmed source, or any ambiguous original-set remains unsupported rather than selecting a nearby B-rep edge. """ producer = feature_by_id.get(f"f_{owner}") or {} atomic_id = str(producer.get("atomic_id") or "") source_ids = {source for source, _token in refs} if atomic_id in {"extrude_add_blind", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided"}: profile_source = frame.get("profile_source") profile = frame.get("profile") if ( not isinstance(profile_source, str) or source_ids != {profile_source} or not isinstance(profile, dict) or not isinstance(frame.get("start"), dict) or not isinstance(frame.get("end"), dict) ): return None local = _shared_source_endpoint(refs, profile_source, entity_by_sketch) if local is None: return None base = _global(profile, local) start = [base[index] + frame["start"]["origin_mm"][index] - profile["origin_mm"][index] for index in range(3)] end = [base[index] + frame["end"]["origin_mm"][index] - profile["origin_mm"][index] for index in range(3)] return _endpoint_bbox(start, end, known_line=True) if atomic_id != "loft_add": return None profile_sources = frame.get("loft_profile_sources") if not isinstance(profile_sources, list) or len(profile_sources) != 2 or len(set(profile_sources)) != 2: return None if source_ids != set(profile_sources): return None points = [ _shared_source_endpoint(refs, source, entity_by_sketch) for source in profile_sources ] if any(point is None or source not in sketch_by_source for point, source in zip(points, profile_sources)): return None start = _global(sketch_by_source[profile_sources[0]]["workplane"], points[0]) end = _global(sketch_by_source[profile_sources[1]]["workplane"], points[1]) # A ThruSections loft may represent this side edge as a B-spline even # though its endpoint correspondence is exact. Do not claim a line type. return _endpoint_bbox(start, end, known_line=False) def _swept_edge_revolve_circle_selector_geometry( owner: str, refs: list[tuple[str, str]], frame: dict[str, Any], feature_by_id: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> dict[str, Any] | None: """Lower one direct full-revolve profile vertex to a circular edge signature. This deliberately accepts only an independent full solid revolve from its original sketch and original in-sketch axis. Its source query must prove exactly one profile endpoint, and that endpoint must have positive radial distance to the explicit axis. Partial/surface/additive revolutions and generated or transformed profile provenance have different topology and remain unsupported instead of being geometrically guessed. """ producer = feature_by_id.get(f"f_{owner}") or {} if ( producer.get("atomic_id") != "revolve_add" or (producer.get("params") or {}).get("result_mode") != "new_body" or not frame.get("revolve_full") ): return None profile_source = frame.get("profile_source") axis = frame.get("revolve_axis") if ( not isinstance(profile_source, str) or {source for source, _token in refs} != {profile_source} or profile_source not in sketch_by_source or not isinstance(axis, dict) ): return None local = _shared_source_endpoint(refs, profile_source, entity_by_sketch) if local is None: return None try: origin = [float(value) for value in axis["origin_mm"]] direction = [float(value) for value in axis["direction"]] except (KeyError, TypeError, ValueError): return None if len(origin) != 3 or len(direction) != 3 or not all(math.isfinite(value) for value in origin + direction): return None direction_length = math.sqrt(sum(value * value for value in direction)) if direction_length <= 1e-9: return None direction = [value / direction_length for value in direction] point = _global(sketch_by_source[profile_source]["workplane"], local) projection = sum((point[index] - origin[index]) * direction[index] for index in range(3)) center = [origin[index] + projection * direction[index] for index in range(3)] radius = math.dist(point, center) if not math.isfinite(radius) or radius <= 1e-8: return None return {"curve_type": "circle", "circle_center_mm": center, "radius_mm": radius} def _profile_matches_direct_source(selected: dict[str, Any], source: dict[str, Any]) -> bool: """Prove that a materialized profile selection changed no source geometry. ``IMPRINT`` is sometimes only the FeatureScript representation of selecting the one existing sketch region. It may also select a proper subset. The latter must not inherit a source-profile SWEPT_EDGE contract, so accept the former only when its physical workplane and complete profile are exactly the original lowered mappings. """ return ( selected.get("workplane") == source.get("workplane") and selected.get("profile") == source.get("profile") ) def _direct_profile_source_entity_ids(profile_sketch: dict[str, Any]) -> set[str]: """Return only source entities still represented by one direct profile edge. This follows the explicit source labels carried by the CDSL profile. A source entity dropped by region selection or split during contour preparation is intentionally absent instead of being recovered by shape comparison. """ profile = profile_sketch.get("profile") or {} source_ids: set[str] = set() direct_circle = profile.get("source_entity_id") if profile.get("type") == "circle" else None if isinstance(direct_circle, str) and direct_circle: source_ids.add(direct_circle) for contour in profile.get("contours") or (): if not isinstance(contour, dict): continue for segment in contour.get("segments") or (): source_entity_id = segment.get("source_entity_id") if isinstance(segment, dict) else None if isinstance(source_entity_id, str) and source_entity_id: source_ids.add(source_entity_id) return source_ids def _has_one_exact_retained_source_edge( selected: dict[str, Any], source: dict[str, Any], source_entity_id: str, ) -> bool: """Prove one source curve remains one unchanged selected profile edge. A bounded IMPRINT region may deliberately omit other source loops while retaining an original outer boundary. That is not a complete-direct- profile contract, but it can still supply one source-qualified prism wall when the exact curve survives unchanged and the adapter preserves its OCC handle. Do not use labels alone: transformed, split, duplicate, or cross-frame curves are not equivalent source edges. """ if selected.get("workplane") != source.get("workplane"): return False def segments(sketch: dict[str, Any]) -> list[dict[str, Any]]: profile = sketch.get("profile") or {} if profile.get("type") != "analytic_contours": return [] return [ segment for contour in profile.get("contours") or () if isinstance(contour, dict) for segment in contour.get("segments") or () if isinstance(segment, dict) and segment.get("source_entity_id") == source_entity_id ] source_segments = segments(source) selected_segments = segments(selected) return len(source_segments) == len(selected_segments) == 1 and selected_segments[0] == source_segments[0] def _direct_prism_swept_selector( value: Any, *, owner: str, selector_kind: str, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, allow_continuation: bool = False, allow_immediate_retained_source_edge: bool = False, ) -> dict[str, Any] | None: """Lower one direct source-profile prism query to kernel lineage intent. This is deliberately narrower than generic SWEPT support. By default the producer must be the immediately preceding independent blind prism. A specifically enabled consumer may opt into proven continuation: it still requires the complete direct source sketch, and the runtime must prove every boundary/continuation edge to the active body. Any IMPRINT-derived subset, split source edge, draft, fused result, or other generator keeps the existing bounded diagnostic path. """ query = parse_query(value) topology = query.topology_type if topology not in {"CAP_EDGE", "SWEPT_FACE", "SWEPT_EDGE"}: return None producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) or {} params = producer.get("params") or {} frame = feature_frames.get(owner) or {} profile_source = frame.get("profile_source") profile_sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) source_sketch = sketch_by_source.get(profile_source) if isinstance(profile_source, str) else None two_sided_cap_contract = ( topology == "CAP_EDGE" and featurescript_version == "2491" and producer.get("atomic_id") == "extrude_add_two_sided" and (params.get("reverse_end_condition") or {}).get("type") == "blind" ) if featurescript_version != "1511" and not two_sided_cap_contract: return None immediate_producer = previous[-1:] == [producer_id] result_mode = params.get("result_mode") # A primary ADD is represented by the default/fuse result mode. Its # direct prism result may continue into the active body only when the # runtime's singular OCC union records a complete relation. Lowering # keeps the source contract identical to an independent prism; it merely # permits the resolver to demand that extra continuation proof. is_primary_add = ( topology == "CAP_EDGE" and producer.get("atomic_id") == "extrude_add_blind" and result_mode != "new_body" ) direct_profile = _profile_matches_direct_source(profile_sketch or {}, source_sketch or {}) if ( producer.get("atomic_id") not in ( {"extrude_add_two_sided"} if two_sided_cap_contract else {"extrude_add_blind", "extrude_cut_blind"} ) or (not immediate_producer and (not allow_continuation or producer_id not in previous)) or (result_mode != "new_body" and not is_primary_add) or (params.get("end_condition") or {}).get("type") != "blind" or params.get("draft") is not None or not isinstance(profile_source, str) or source_sketch is None or profile_sketch is None or profile_sketch.get("source_sketch_id") != profile_source or not direct_profile and not ( allow_immediate_retained_source_edge and immediate_producer and topology == "SWEPT_FACE" ) ): return None source_ids = _direct_profile_source_entity_ids(profile_sketch) refs = _source_refs(value) if topology == "CAP_EDGE": if ( selector_kind != "edge" or query.is_start is None or len(refs) != 1 or refs[0][0] != profile_source or query.source_sketch != profile_source or query.source_entity != refs[0][1] or refs[0][1] not in source_ids ): return None entity = (entity_by_sketch.get(profile_source) or {}).get(refs[0][1]) if not entity or entity.get("construction"): return None intent = _selector_intent( value, query_family="CAP_EDGE", kind="edge", evidence="kernel_history", allowed=("boundary",) if two_sided_cap_contract else ( ("boundary", "continuation") if (allow_continuation or is_primary_add) else ("boundary",) ), ) intent["lineage_role"] = f"extrude.{'start' if query.is_start else 'end'}" return { "kind": "edge", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } if topology == "SWEPT_FACE": if ( selector_kind != "face" or len(refs) != 1 or refs[0][0] != profile_source or query.source_sketch != profile_source or query.source_entity != refs[0][1] or refs[0][1] not in source_ids ): return None entity = (entity_by_sketch.get(profile_source) or {}).get(refs[0][1]) if not entity or entity.get("construction"): return None retained_source_edge = not direct_profile if retained_source_edge and not _has_one_exact_retained_source_edge( profile_sketch, source_sketch, refs[0][1], ): return None intent = _selector_intent( value, query_family="SWEPT_FACE", kind="face", evidence="kernel_history", allowed=("boundary",) if retained_source_edge else ( ("boundary", "continuation") if allow_continuation else ("boundary",) ), ) if retained_source_edge: intent["consumer_contract"] = "immediate_retained_source_prism_swept_face_up_to_surface" return { "kind": "face", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } if selector_kind != "edge" or len(refs) < 2 or {source for source, _token in refs} != {profile_source}: return None resolved_ids: list[str] = [] for source, token in refs: resolved = _source_ref_entity(source, token, entity_by_sketch) if resolved is None: return None entity_id, entity = resolved if entity.get("construction") or entity_id not in source_ids: return None if entity_id not in resolved_ids: resolved_ids.append(entity_id) if len(resolved_ids) < 2 or _shared_source_endpoint(refs, profile_source, entity_by_sketch) is None: return None intent = _selector_intent( value, query_family="SWEPT_EDGE", kind="edge", evidence="kernel_history", allowed=("boundary", "continuation") if allow_continuation else ("boundary",), ) intent.pop("source_entity", None) intent["source_entities"] = [ {"sketch_id": profile_source, "entity_id": entity_id} for entity_id in sorted(resolved_ids) ] return { "kind": "edge", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } def _direct_prism_shell_offset_edge_tdd_selector( value: Any, *, owner: str, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Lower one retained direct-prism cap edge from an outer shell OFFSET_EDGE. The TDD form is not a request for the shell's inner wall. It names a CAP_EDGE on the cap retained by an immediately following shell, so the executable witness is the direct-prism cap boundary followed by one exact shell continuation. OSD-only pairs, generated inner-wall edges, and delayed shell consumers deliberately remain deferred. """ if featurescript_version != "1511": return None try: _call, shell_owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None if shell_owner != owner or topology != "OFFSET_EDGE" or kind not in {"edge", "entitytype.edge"}: return None disambiguation = definition.get("disambiguationData") if not isinstance(disambiguation, list) or len(disambiguation) != 2: return None original_set, tdd = disambiguation if ( not isinstance(original_set, Call) or original_set.name != "OSD" or len(original_set.args) != 1 or not isinstance(original_set.args[0], list) or len(original_set.args[0]) != 1 or not isinstance(tdd, Call) or tdd.name != "TDD" or len(tdd.args) != 1 or not isinstance(tdd.args[0], list) or len(tdd.args[0]) != 1 ): return None outer_refs = _source_refs(original_set) nested = tdd.args[0][0] try: _nested_call, producer_owner, nested_topology, nested_kind, nested_definition = _direct_make_query(nested) except ValueError: return None if nested_topology != "CAP_EDGE" or nested_kind not in {"edge", "entitytype.edge"}: return None nested_disambiguation = nested_definition.get("disambiguationData") if ( len(outer_refs) != 1 or not isinstance(nested_disambiguation, list) or len(nested_disambiguation) != 1 or not isinstance(nested_disambiguation[0], Call) or nested_disambiguation[0].name != "OSD" or len(nested_disambiguation[0].args) != 1 or not isinstance(nested_disambiguation[0].args[0], list) or len(nested_disambiguation[0].args[0]) != 1 or _source_refs(nested_disambiguation[0]) != outer_refs ): return None cap_selector = _direct_prism_swept_selector( nested, owner=producer_owner, selector_kind="edge", feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=featurescript_version, allow_continuation=True, ) if cap_selector is None: return None producer_id = f"f_{producer_owner}" shell_id = f"f_{shell_owner}" shell = feature_by_id.get(shell_id) or {} cap_intent = cap_selector.get("selector_intent") or {} cap_role = cap_intent.get("lineage_role") shell_selectors = shell.get("selectors") or [] if ( previous[-1:] != [shell_id] or shell.get("atomic_id") != "shell" or shell.get("depends_on") != [producer_id] or (shell.get("params") or {}).get("inward") is not True or len(shell_selectors) != 1 or not isinstance(shell_selectors[0], dict) or shell_selectors[0].get("owner_feature_id") != producer_id or shell_selectors[0].get("output_role") != ("extrude.end" if cap_role == "extrude.start" else "extrude.start") ): return None source_entity = cap_intent.get("source_entity") if not isinstance(source_entity, dict): return None intent = _selector_intent( value, query_family="OFFSET_EDGE", kind="edge", evidence="kernel_history", allowed=("boundary", "continuation"), disambiguation={ "type": "offset_edge_tdd_cap_continuation", "shell_feature_id": shell_id, "outer_owner_feature_id": shell_id, "tdd_cap_owner_feature_id": producer_id, "tdd_cap_role": cap_role, "source_entity": dict(source_entity), }, ) intent["source_entity"] = dict(source_entity) intent["lineage_role"] = cap_role intent["consumer_contract"] = "direct_prism_shell_offset_edge_tdd" return { "kind": "edge", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } def _direct_prism_shell_offset_edge_vertex_selector( value: Any, *, owner: str, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Lower an OSD-only shell ``OFFSET_EDGE`` anchored at one profile vertex. This is deliberately distinct from the retained-cap TDD form. Two incident source profile edges name one source vertex, whose direct-prism swept edge must survive the immediately following shell by a complete kernel continuation. A shell-wall generated from a removed cap edge is neither this source vertex nor a substitute for it. """ if featurescript_version != "1511": return None try: _call, shell_owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None if shell_owner != owner or topology != "OFFSET_EDGE" or kind not in {"edge", "entitytype.edge"}: return None disambiguation = definition.get("disambiguationData") if ( not isinstance(disambiguation, list) or len(disambiguation) != 1 or not isinstance(disambiguation[0], Call) or disambiguation[0].name != "OSD" or len(disambiguation[0].args) != 1 or not isinstance(disambiguation[0].args[0], list) ): return None refs = _source_refs(disambiguation[0]) shell_id = f"f_{shell_owner}" shell = feature_by_id.get(shell_id) or {} depends_on = shell.get("depends_on") or [] if previous[-1:] != [shell_id] or len(depends_on) != 1: return None producer_id = depends_on[0] producer = feature_by_id.get(producer_id) or {} producer_owner = producer_id.removeprefix("f_") frame = feature_frames.get(producer_owner) or {} profile_source = frame.get("profile_source") profile_sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) source_sketch = sketch_by_source.get(profile_source) if isinstance(profile_source, str) else None params = producer.get("params") or {} source_ids = _direct_profile_source_entity_ids(profile_sketch or {}) if ( producer.get("atomic_id") != "extrude_add_blind" or params.get("result_mode") != "new_body" or (params.get("end_condition") or {}).get("type") != "blind" or params.get("draft") is not None or not isinstance(profile_source, str) or source_sketch is None or profile_sketch is None or profile_sketch.get("source_sketch_id") != profile_source or not _profile_matches_direct_source(profile_sketch, source_sketch) or len(refs) != 2 or {source for source, _token in refs} != {profile_source} or _shared_source_endpoint(refs, profile_source, entity_by_sketch) is None ): return None resolved_ids: list[str] = [] for source, token in refs: resolved = _source_ref_entity(source, token, entity_by_sketch) if resolved is None: return None entity_id, entity = resolved if entity.get("construction") or entity_id not in source_ids or entity_id in resolved_ids: return None resolved_ids.append(entity_id) shell_selectors = shell.get("selectors") or [] if ( shell.get("atomic_id") != "shell" or (shell.get("params") or {}).get("inward") is not True or len(shell_selectors) != 1 or not isinstance(shell_selectors[0], dict) or shell_selectors[0].get("kind") != "face" or shell_selectors[0].get("owner_feature_id") != producer_id or shell_selectors[0].get("output_role") not in {"extrude.start", "extrude.end"} ): return None source_entities = [ {"sketch_id": profile_source, "entity_id": entity_id} for entity_id in sorted(resolved_ids) ] intent = _selector_intent( value, query_family="OFFSET_EDGE", kind="edge", evidence="kernel_history", allowed=("boundary", "continuation"), disambiguation={ "type": "offset_edge_vertex_continuation", "shell_feature_id": shell_id, "outer_owner_feature_id": shell_id, "prism_owner_feature_id": producer_id, "removed_cap_role": shell_selectors[0]["output_role"], "source_entities": source_entities, }, ) intent.pop("source_entity", None) intent["source_entities"] = source_entities intent["consumer_contract"] = "direct_prism_shell_offset_edge_vertex" return { "kind": "edge", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } def _direct_blend_edge_selector( value: Any, *, owner: str, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Lower one exact direct-prism dress-up ``BLEND_EDGE`` source tuple. The FeatureScript query names both the original edge and the original face that the first dress-up blends into. Retain those source semantics rather than collapsing the result to a nearby patch boundary edge. This accepts only one source edge, one complete direct-prism CAP_FACE or same-anchor SWEPT_FACE, and one immediate native fillet/chamfer producer; split, merge/COPY and mixed query sets stay deferred for a later component contract. """ try: _call, outer_owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None if topology != "BLEND_EDGE" or kind not in {"edge", "entitytype.edge"} or outer_owner != owner: return None blended_from = definition.get("blendedFrom") blended_into = definition.get("blendedInto") if not isinstance(blended_from, list) or len(blended_from) != 2 or not isinstance(blended_into, list) or len(blended_into) != 1: return None edge_value, source_face_value = blended_from target_face_value = blended_into[0] try: _edge_call, edge_owner, edge_topology, edge_kind, _edge_definition = _direct_make_query(edge_value) _face_call, face_owner, face_topology, face_kind, _face_definition = _direct_make_query(source_face_value) _target_call, target_owner, target_topology, target_kind, _target_definition = _direct_make_query(target_face_value) except ValueError: return None face_family = face_topology if face_topology == target_topology else None if ( edge_topology != "CAP_EDGE" or edge_kind not in {"edge", "entitytype.edge"} or face_family not in {"CAP_FACE", "SWEPT_FACE"} or face_kind not in {"face", "entitytype.face"} or target_kind not in {"face", "entitytype.face"} or len({edge_owner, face_owner, target_owner}) != 1 ): return None source_feature_id = f"f_{edge_owner}" dressup_feature_id = f"f_{owner}" dressup = feature_by_id.get(dressup_feature_id) or {} if ( previous[-1:] != [dressup_feature_id] or dressup.get("atomic_id") not in {"fillet", "chamfer"} or source_feature_id not in previous ): return None edge_selector = _direct_prism_swept_selector( edge_value, owner=edge_owner, selector_kind="edge", feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=featurescript_version, allow_continuation=True, ) if face_family == "CAP_FACE": source_face_selector = _cap_face_output_role_selector( source_face_value, feature_by_id, sketches_by_id, ) target_face_selector = _cap_face_output_role_selector( target_face_value, feature_by_id, sketches_by_id, ) else: source_face_selector = _direct_prism_swept_selector( source_face_value, owner=face_owner, selector_kind="face", feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=featurescript_version, allow_continuation=True, ) target_face_selector = _direct_prism_swept_selector( target_face_value, owner=target_owner, selector_kind="face", feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=featurescript_version, allow_continuation=True, ) if edge_selector is None or source_face_selector is None or target_face_selector is None: return None edge_intent = edge_selector.get("selector_intent") or {} source_face_intent = source_face_selector.get("selector_intent") or {} target_face_intent = target_face_selector.get("selector_intent") or {} if ( edge_intent.get("query_family") != "CAP_EDGE" or source_face_intent.get("query_family") != face_family or target_face_intent.get("query_family") != face_family or source_face_selector.get("owner_feature_id") != source_feature_id or target_face_selector.get("owner_feature_id") != source_feature_id ): return None if face_family == "CAP_FACE" and source_face_selector.get("output_role") != target_face_selector.get("output_role"): return None source_profile = sketches_by_id.get(str((feature_by_id.get(source_feature_id) or {}).get("sketch_id") or "")) or {} source_profile_ids = _direct_profile_source_entity_ids(source_profile) source_sketch_id = (feature_frames.get(edge_owner) or {}).get("profile_source") if face_family == "CAP_FACE": source_refs = _source_refs(source_face_value) target_refs = _source_refs(target_face_value) required_refs = {(source_sketch_id, entity_id) for entity_id in source_profile_ids} if ( not isinstance(source_sketch_id, str) or not source_profile_ids or set(source_refs) != required_refs or set(target_refs) != required_refs or len(source_refs) != len(source_profile_ids) or len(target_refs) != len(source_profile_ids) ): return None edge_source = edge_intent.get("source_entity") edge_role = edge_intent.get("lineage_role") face_source_entity = source_face_intent.get("source_entity") target_face_entity = target_face_intent.get("source_entity") face_role = source_face_selector.get("output_role") if not isinstance(edge_source, dict) or edge_role not in {"extrude.start", "extrude.end"}: return None if face_family == "CAP_FACE": if face_role not in {"extrude.start", "extrude.end"}: return None elif not isinstance(face_source_entity, dict) or face_source_entity != target_face_entity: return None intent = _selector_intent( value, query_family="BLEND_EDGE", kind="edge", evidence="kernel_history", allowed=("boundary",), ) intent["blend_sources"] = { "edge": { "query_family": "CAP_EDGE", "owner_feature_id": source_feature_id, "source_entity": dict(edge_source), "lineage_role": edge_role, }, "face": ( { "query_family": "CAP_FACE", "owner_feature_id": source_feature_id, "output_role": face_role, } if face_family == "CAP_FACE" else { "query_family": "SWEPT_FACE", "owner_feature_id": source_feature_id, "source_entity": dict(face_source_entity), } ), } return { "kind": "edge", "owner_feature_id": dressup_feature_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } def _direct_prism_blend_face_attachment( value: Any, *, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Attach one immediate direct-prism ``BLEND_FACE`` sketch at runtime. ``BLEND_FACE`` is the patch generated by a dress-up input edge, rather than an arbitrary face adjacent to that edge. Accept it only when the preceding native dress-up already selected one role-qualified direct-prism cap edge. The runtime then requires that exact ``Generated(edge -> patch_face)`` relation in the active snapshot before it materializes the sketch frame. """ if featurescript_version != "1511": return None try: _call, dressup_owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None refs = _source_refs(definition.get("disambiguationData")) dressup_id = f"f_{dressup_owner}" dressup = feature_by_id.get(dressup_id) or {} if ( topology != "BLEND_FACE" or kind not in {"face", "entitytype.face"} or previous[-1:] != [dressup_id] or dressup.get("atomic_id") not in {"fillet", "chamfer"} or len(dressup.get("depends_on") or ()) != 1 or len(refs) != 1 ): return None source_sketch_id, source_token = refs[0] resolved_source = _source_ref_entity(source_sketch_id, source_token, entity_by_sketch) if resolved_source is None or resolved_source[0] != source_token: return None source_entity_id, source_entity = resolved_source producer_id = (dressup.get("depends_on") or [None])[0] producer = feature_by_id.get(str(producer_id)) or {} params = producer.get("params") or {} profile = sketches_by_id.get(str(producer.get("sketch_id") or "")) source_sketch = sketch_by_source.get(source_sketch_id) if ( not isinstance(producer_id, str) or producer.get("atomic_id") != "extrude_add_blind" or params.get("result_mode") != "new_body" or (params.get("end_condition") or {}).get("type") != "blind" or params.get("draft") is not None or profile is None or source_sketch is None or profile.get("source_sketch_id") != source_sketch_id or not _profile_matches_direct_source(profile, source_sketch) or source_entity.get("construction") or source_entity_id not in _direct_profile_source_entity_ids(profile) ): return None def leaves(items: list[dict[str, Any]]) -> list[dict[str, Any]]: output: list[dict[str, Any]] = [] for selector in items: nested = selector.get("query_operands") if isinstance(nested, list): output.extend(leaves([item for item in nested if isinstance(item, dict)])) else: output.append(selector) return output matches = [] for selector in leaves([item for item in dressup.get("selectors") or () if isinstance(item, dict)]): intent = selector.get("selector_intent") or {} if ( selector.get("kind") == "edge" and selector.get("owner_feature_id") == producer_id and selector.get("source") == "runtime_snapshot" and intent.get("query_family") == "CAP_EDGE" and intent.get("source_entity") == { "sketch_id": source_sketch_id, "entity_id": source_entity_id, } and intent.get("lineage_role") in {"extrude.start", "extrude.end"} ): matches.append(selector) if len(matches) != 1: return None cap_intent = matches[0]["selector_intent"] intent = _selector_intent( value, query_family="BLEND_FACE", kind="face", evidence="kernel_history", allowed=("boundary",), ) intent["blend_face_source"] = { "query_family": "CAP_EDGE", "owner_feature_id": producer_id, "source_entity": dict(cap_intent["source_entity"]), "lineage_role": cap_intent["lineage_role"], } return { "kind": "face", "owner_feature_id": dressup_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } def _direct_full_revolve_swept_edge_selector( value: Any, *, owner: str, selector_kind: str, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Lower one full-revolve source vertex to exact ``MakeRevol`` history. This is intentionally separate from the direct-prism contract. A full independent solid revolve has one usable native witness only: ``BRepPrimAPI_MakeRevol.Generated(source_vertex)``. The profile vertex is identified by the complete incident set in the FeatureScript OSD query; circle centre/radius remains diagnostic geometry and never binds the edge. """ query = parse_query(value) if query.topology_type != "SWEPT_EDGE" or selector_kind != "edge": return None # The full-revolve contract is separately registered for each source # tuple. Do not infer other revisions from a merely similar makeQuery AST. if featurescript_version not in {"1511", "2491"}: return None producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) or {} frame = feature_frames.get(owner) or {} profile_source = frame.get("profile_source") profile_sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) source_sketch = sketch_by_source.get(profile_source) if isinstance(profile_source, str) else None if ( producer.get("atomic_id") != "revolve_add" or (producer.get("params") or {}).get("result_mode") != "new_body" or not frame.get("revolve_full") or not isinstance(frame.get("revolve_axis"), dict) or producer_id not in previous or not isinstance(profile_source, str) or source_sketch is None or profile_sketch is None or profile_sketch.get("source_sketch_id") != profile_source or not _profile_matches_direct_source(profile_sketch, source_sketch) or frame.get("revolve_profile_contract") not in { "original_source", "verified_complete_materialization", } # The 1511 contract does not let an IMPRINT profile borrow the source # sketch's vertex identities merely because lowering reuses equal # profile data. 2491 separately admits its verified complete form. or ( featurescript_version == "1511" and frame.get("revolve_profile_contract") != "original_source" ) ): return None refs = _source_refs(value) if len(refs) < 2 or {source for source, _token in refs} != {profile_source}: return None source_ids = _direct_profile_source_entity_ids(profile_sketch) resolved_ids: list[str] = [] for source, token in refs: resolved = _source_ref_entity(source, token, entity_by_sketch) if resolved is None: return None entity_id, entity = resolved if entity.get("construction") or entity_id not in source_ids: return None if entity_id not in resolved_ids: resolved_ids.append(entity_id) if len(resolved_ids) < 2 or _shared_source_endpoint(refs, profile_source, entity_by_sketch) is None: return None intent = _selector_intent( value, query_family="SWEPT_EDGE", kind="edge", evidence="kernel_history", # Earlier dress-ups may preserve this edge through exact one-to-one # OCC history. The resolver rejects any split, merge or missing branch. allowed=("boundary", "continuation"), ) intent.pop("source_entity", None) intent["source_entities"] = [ {"sketch_id": profile_source, "entity_id": entity_id} for entity_id in sorted(resolved_ids) ] return { "kind": "edge", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } def _direct_primary_cut_copy_cap_edge_selector( value: Any, *, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Lower one primary-cut ``COPY(CAP_EDGE)`` to its proven input lineage. FeatureScript's boolean COPY query is not permission to select a similar final edge. This narrow form keeps its outer COPY AST and delegates only its explicit CAP_EDGE source to the transient-prism and cut-builder chain already registered in the same replay. The tool is never active topology. """ if featurescript_version != "1511": return None try: _outer_call, owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None derived = definition.get("derivedFrom") if isinstance(definition, dict) else None if topology != "COPY" or kind not in {"edge", "entitytype.edge"} or derived is None: return None try: _inner_call, inner_owner, inner_topology, inner_kind, _inner_definition = _direct_make_query(derived) except ValueError: return None producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) or {} params = producer.get("params") or {} frame = feature_frames.get(owner) or {} profile_source = frame.get("profile_source") profile_sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) source_sketch = sketch_by_source.get(profile_source) if isinstance(profile_source, str) else None inner_query = parse_query(derived) refs = _source_refs(derived) if ( inner_owner != owner or inner_topology != "CAP_EDGE" or inner_kind not in {"edge", "entitytype.edge"} or inner_query.is_start is None or previous[-1:] != [producer_id] or producer.get("atomic_id") != "extrude_cut_blind" or params.get("result_mode") is not None or (params.get("end_condition") or {}).get("type") != "blind" or params.get("draft") is not None or not isinstance(profile_source, str) or source_sketch is None or profile_sketch is None or profile_sketch.get("source_sketch_id") != profile_source or not _profile_matches_direct_source(profile_sketch, source_sketch) or len(refs) != 1 or refs[0][0] != profile_source or inner_query.source_sketch != profile_source or inner_query.source_entity != refs[0][1] or refs[0][1] not in _direct_profile_source_entity_ids(profile_sketch) ): return None entity = (entity_by_sketch.get(profile_source) or {}).get(refs[0][1]) if not entity or entity.get("construction"): return None input_intent = _selector_intent( derived, query_family="CAP_EDGE", kind="edge", evidence="kernel_history", allowed=("boundary", "continuation"), ) input_intent["lineage_role"] = f"extrude.{'start' if inner_query.is_start else 'end'}" intent = _selector_intent( value, query_family="COPY", kind="edge", evidence="kernel_history", allowed=("boundary", "continuation"), ) # The anchor belongs to the nested CAP_EDGE, not the outer boolean COPY. intent.pop("source_entity", None) intent["copy_contract"] = "primary_cut_cap_edge" return { "kind": "edge", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "query_input": { "kind": "edge", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": input_intent, }, "selector_intent": intent, } def _direct_primary_cut_copy_cap_face_attachment( value: Any, *, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Lower an immediate primary-cut ``COPY(CAP_FACE)`` sketch attachment. The emitted selector names no face by geometry or static cap frame. It instead requires the runtime to prove the direct tool cap and its exact same-owner subtract successor before materializing the sketch workplane. """ if featurescript_version != "1511": return None try: _outer, owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None derived = definition.get("derivedFrom") if isinstance(definition, dict) else None if topology != "COPY" or kind not in {"face", "entitytype.face"} or derived is None: return None try: _inner, inner_owner, inner_topology, inner_kind, _inner_definition = _direct_make_query(derived) except ValueError: return None producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) or {} params = producer.get("params") or {} frame = feature_frames.get(owner) or {} profile_source = frame.get("profile_source") profile_sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) source_sketch = sketch_by_source.get(profile_source) if isinstance(profile_source, str) else None inner_query = parse_query(derived) refs = _source_refs(derived) source_ids = _direct_profile_source_entity_ids(profile_sketch or {}) if ( inner_owner != owner or inner_topology != "CAP_FACE" or inner_kind not in {"face", "entitytype.face"} or inner_query.is_start is None or previous[-1:] != [producer_id] or producer.get("atomic_id") != "extrude_cut_blind" or params.get("result_mode") is not None or (params.get("end_condition") or {}).get("type") != "blind" or params.get("draft") is not None or not isinstance(profile_source, str) or source_sketch is None or profile_sketch is None or profile_sketch.get("source_sketch_id") != profile_source or not _profile_matches_direct_source(profile_sketch, source_sketch) or not source_ids or len(refs) != len(source_ids) or {source for source, _token in refs} != {profile_source} or {token for _source, token in refs} != source_ids ): return None for entity_id in source_ids: entity = (entity_by_sketch.get(profile_source) or {}).get(entity_id) if not entity or entity.get("construction"): return None role = f"extrude.{'start' if inner_query.is_start else 'end'}" input_intent = _selector_intent( derived, query_family="CAP_FACE", kind="face", evidence="kernel_history", allowed=("boundary", "continuation"), ) input_intent.pop("source_entity", None) input_intent["source_entities"] = [ {"sketch_id": profile_source, "entity_id": entity_id} for entity_id in sorted(source_ids) ] input_intent["lineage_role"] = role intent = _selector_intent( value, query_family="COPY", kind="face", evidence="kernel_history", allowed=("boundary", "continuation"), ) intent.pop("source_entity", None) intent["copy_contract"] = "primary_cut_cap_face_workplane" return { "kind": "face", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "query_input": { "kind": "face", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": input_intent, }, "selector_intent": intent, } def _direct_prism_cap_face_attachment( value: Any, *, feature_by_id: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Attach a following sketch to one exact direct-prism cap at runtime. The direct CAP role is a builder result, not a copied static workplane. This bridge intentionally applies only while the independent blind prism remains the immediately preceding active body. It does not interpret CAP_EDGE, boolean successors, or an attached profile's future splitter boundary; those need their own source-qualified contracts. """ if featurescript_version != "1511": return None try: _call, _owner, topology, kind, _definition = _direct_make_query(value) except ValueError: return None if topology != "CAP_FACE" or kind not in {"face", "entitytype.face"}: return None selector = _cap_face_output_role_selector(value, feature_by_id, sketches_by_id) if selector is None or previous[-1:] != [selector.get("owner_feature_id")]: return None intent = selector.get("selector_intent") or {} if ( selector.get("kind") != "face" or selector.get("output_role") not in {"extrude.start", "extrude.end"} or intent.get("query_family") != "CAP_FACE" or intent.get("evidence") != "operation_role" or intent.get("consumer_contract") is not None ): return None intent["consumer_contract"] = "direct_prism_cap_face_workplane" return selector def _direct_primary_cut_copy_swept_face_attachment( value: Any, *, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Lower one direct primary-cut ``COPY(SWEPT_FACE)`` sketch attachment. The source edge is only an anchor for the cut tool's native prism history. The eventual workplane is materialized exclusively from the exact active face reached through that tool face's same-owner subtract continuation. """ if featurescript_version != "1511": return None try: _outer, owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None derived = definition.get("derivedFrom") if isinstance(definition, dict) else None if topology != "COPY" or kind not in {"face", "entitytype.face"} or derived is None: return None try: _inner, inner_owner, inner_topology, inner_kind, _inner_definition = _direct_make_query(derived) except ValueError: return None producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) or {} params = producer.get("params") or {} frame = feature_frames.get(owner) or {} profile_source = frame.get("profile_source") profile_sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) source_sketch = sketch_by_source.get(profile_source) if isinstance(profile_source, str) else None refs = _source_refs(derived) if ( inner_owner != owner or inner_topology != "SWEPT_FACE" or inner_kind not in {"face", "entitytype.face"} or previous[-1:] != [producer_id] or producer.get("atomic_id") != "extrude_cut_blind" or params.get("result_mode") is not None or (params.get("end_condition") or {}).get("type") != "blind" or params.get("draft") is not None or not isinstance(profile_source, str) or source_sketch is None or profile_sketch is None or profile_sketch.get("source_sketch_id") != profile_source or not _profile_matches_direct_source(profile_sketch, source_sketch) or len(refs) != 1 or refs[0][0] != profile_source or refs[0][1] not in _direct_profile_source_entity_ids(profile_sketch) ): return None entity = (entity_by_sketch.get(profile_source) or {}).get(refs[0][1]) if not entity or entity.get("construction"): return None input_intent = _selector_intent( derived, query_family="SWEPT_FACE", kind="face", evidence="kernel_history", allowed=("boundary", "continuation"), ) input_intent["source_entity"] = {"sketch_id": refs[0][0], "entity_id": refs[0][1]} intent = _selector_intent( value, query_family="COPY", kind="face", evidence="kernel_history", allowed=("boundary", "continuation"), ) intent.pop("source_entity", None) intent["copy_contract"] = "primary_cut_swept_face_workplane" return { "kind": "face", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "query_input": { "kind": "face", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": input_intent, }, "selector_intent": intent, } def _planar_imprint_prism_selector( value: Any, *, owner: str, selector_kind: str, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Lower a bounded IMPRINT prism query only when its source set is exact. An IMPRINT profile may split one FeatureScript curve into several selected B-rep fragments. The adapter records each exact fragment and the resolver must return the complete set. This helper therefore does not reuse the direct-profile one-to-one contract or turn a set-valued query into an arbitrary geometric candidate. """ query = parse_query(value) topology = query.topology_type if topology not in {"CAP_FACE", "SWEPT_FACE", "SWEPT_EDGE"} or featurescript_version != "1511": return None producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) or {} params = producer.get("params") or {} frame = feature_frames.get(owner) or {} profile_source = frame.get("profile_source") profile_sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) profile = (profile_sketch or {}).get("profile") or {} if ( producer.get("atomic_id") not in {"extrude_add_blind", "extrude_cut_blind"} or previous[-1:] != [producer_id] or params.get("result_mode") != "new_body" or (params.get("end_condition") or {}).get("type") != "blind" or params.get("draft") is not None or not isinstance(profile_source, str) or profile_sketch is None or profile_sketch.get("source_sketch_id") != profile_source or profile.get("type") != "planar_imprint" ): return None source_ids = { str(entry.get("id")) for entry in profile.get("source_entities") or () if isinstance(entry, dict) and isinstance(entry.get("id"), str) and entry.get("id") } refs = list(dict.fromkeys(_source_refs(value))) if not source_ids or not refs or {sketch_id for sketch_id, _entity_id in refs} != {profile_source}: return None ref_ids = {entity_id for _sketch_id, entity_id in refs} if not ref_ids.issubset(source_ids): return None if topology == "CAP_FACE": if selector_kind != "face" or query.is_start is None or ref_ids != source_ids: return None intent = _selector_intent( value, query_family="CAP_FACE", kind="face", evidence="kernel_history", allowed=("boundary", "fragment"), multiplicity="all_fragments", ) # The complete OSD source set proves the output role. Its last parsed # source token is diagnostic context, not a single-edge CAP anchor. intent.pop("source_entity", None) output_role = f"extrude.{'start' if query.is_start else 'end'}" intent["output_role"] = output_role intent["disambiguation"] = { "type": "complete_imprint_profile_source_set", "source_entity_ids": sorted(source_ids), } return { "kind": "face", "owner_feature_id": producer_id, "output_role": output_role, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } if topology == "SWEPT_FACE": if selector_kind != "face" or len(refs) != 1: return None _source_sketch, source_entity = refs[0] intent = _selector_intent( value, query_family="SWEPT_FACE", kind="face", evidence="kernel_history", allowed=("boundary", "fragment"), multiplicity="all_fragments", ) intent["source_entity"] = {"sketch_id": profile_source, "entity_id": source_entity} return { "kind": "face", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } if selector_kind != "edge" or len(refs) != 2 or len(ref_ids) != 2: return None intent = _selector_intent( value, query_family="SWEPT_EDGE", kind="edge", evidence="kernel_history", allowed=("boundary",), ) intent.pop("source_entity", None) intent["source_entities"] = [ {"sketch_id": profile_source, "entity_id": entity_id} for entity_id in sorted(ref_ids) ] return { "kind": "edge", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } def _intersection_source_face_descriptor( value: Any, *, allowed_input_features: set[str], primary_tool_owner: str | None = None, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], featurescript_version: str | None, ) -> dict[str, Any] | None: """Describe one exact boolean-input face without lowering a generic query. This is intentionally narrower than the standalone CAP/SWEPT selector paths. The source must be an independently retained direct prism, or the one transient direct prism built by the enclosing primary REMOVE. It records the semantic anchor used to locate the *input* face; section-edge selection is deferred to runtime builder history. """ if featurescript_version != "1511": return None try: _call, owner, topology, kind, _definition = _direct_make_query(value) except ValueError: return None owner_feature_id = f"f_{owner}" if kind != "face" or owner_feature_id not in allowed_input_features: return None producer = feature_by_id.get(owner_feature_id) or {} params = producer.get("params") or {} is_primary_tool = owner_feature_id == primary_tool_owner if ( producer.get("atomic_id") != ("extrude_cut_blind" if is_primary_tool else "extrude_add_blind") or (params.get("result_mode") is not None if is_primary_tool else params.get("result_mode") != "new_body") or (params.get("end_condition") or {}).get("type") != "blind" or params.get("draft") is not None ): return None if topology == "CAP_FACE": query = parse_query(value) if query.is_start is None: return None return { "query_family": "CAP_FACE", "owner_feature_id": owner_feature_id, "output_role": f"extrude.{'start' if query.is_start else 'end'}", } if topology != "SWEPT_FACE": return None frame = feature_frames.get(owner) or {} profile_source = frame.get("profile_source") profile_sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) source_sketch = sketch_by_source.get(profile_source) if isinstance(profile_source, str) else None refs = _source_refs(value) if ( not isinstance(profile_source, str) or source_sketch is None or profile_sketch is None or profile_sketch.get("source_sketch_id") != profile_source or not _profile_matches_direct_source(profile_sketch, source_sketch) or len(refs) != 1 or refs[0][0] != profile_source or parse_query(value).source_sketch != profile_source or parse_query(value).source_entity != refs[0][1] or refs[0][1] not in _direct_profile_source_entity_ids(profile_sketch) ): return None entity = (entity_by_sketch.get(profile_source) or {}).get(refs[0][1]) if not entity or entity.get("construction"): return None return { "query_family": "SWEPT_FACE", "owner_feature_id": owner_feature_id, "source_entity": {"sketch_id": profile_source, "entity_id": refs[0][1]}, } def _direct_boolean_intersection_selector( value: Any, *, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], featurescript_version: str | None, ) -> dict[str, Any] | None: """Lower one source-qualified `INTERSECT EDGE` boolean query. The operation accepts exactly two direct prism source faces from the explicitly named target/tool members of a single `boolean_bodies` node. COPY, transformed, patterned, TDD/TD/IMPRINT-derived and primary-tool booleans intentionally retain deferred source intent. """ if featurescript_version != "1511": return None try: _call, owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None if topology != "INTERSECT" or kind != "edge": return None boolean_id = f"f_{owner}" boolean = feature_by_id.get(boolean_id) or {} params = boolean.get("params") or {} target_ids = params.get("target_feature_ids") tool_ids = params.get("tool_feature_ids") if ( boolean.get("atomic_id") != "boolean_bodies" or params.get("operation") not in {"subtract", "intersect"} or bool(params.get("keep_tools")) or not isinstance(target_ids, list) or not isinstance(tool_ids, list) or len(target_ids) != 1 or len(tool_ids) != 1 ): return None derived = definition.get("derivedFrom") if not isinstance(derived, list) or len(derived) != 2: return None allowed_inputs = {str(target_ids[0]), str(tool_ids[0])} sources = [ _intersection_source_face_descriptor( item, allowed_input_features=allowed_inputs, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, featurescript_version=featurescript_version, ) for item in derived ] if any(source is None for source in sources): return None owners = {str(source["owner_feature_id"]) for source in sources if source is not None} if owners != allowed_inputs: return None return { "kind": "edge", "owner_feature_id": boolean_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": _selector_intent( value, query_family="INTERSECT", kind="edge", evidence="kernel_history", allowed=("intersection",), ) | { "intersection_sources": sources, "disambiguation": {"type": "source_qualified_boolean_section"}, }, } def _direct_primary_cut_intersection_selector( value: Any, *, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Lower one unique source-qualified section from a primary REMOVE. The primary cut's tool is not a CDSL body member. Runtime creates a transient direct-prism snapshot only for this exact source pair, then binds the final section edge through the cut builder. Any FeatureScript disambiguation encoding remains deferred until its API semantics are independently verified; OCC section-list position is not a substitute. """ if featurescript_version != "1511": return None try: _call, owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None if ( topology != "INTERSECT" or kind != "edge" or definition.get("disambiguationData") not in (None, []) ): return None primary_id = f"f_{owner}" primary = feature_by_id.get(primary_id) or {} primary_params = primary.get("params") or {} if ( primary.get("atomic_id") != "extrude_cut_blind" or primary_params.get("result_mode") is not None or (primary_params.get("end_condition") or {}).get("type") != "blind" or primary_params.get("draft") is not None ): return None derived = definition.get("derivedFrom") if not isinstance(derived, list) or len(derived) != 2: return None try: input_owners = [f"f_{_direct_make_query(item)[1]}" for item in derived] except ValueError: return None if input_owners.count(primary_id) != 1 or len(set(input_owners)) != 2: return None target_id = next(item for item in input_owners if item != primary_id) # At the primary cut, one immediately preceding body is both the runtime # target and the only allowed source-qualified target producer. if previous[-2:] != [target_id, primary_id]: return None sources = [ _intersection_source_face_descriptor( item, allowed_input_features={target_id, primary_id}, primary_tool_owner=primary_id, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, featurescript_version=featurescript_version, ) for item in derived ] if any(source is None for source in sources): return None return { "kind": "edge", "owner_feature_id": primary_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": _selector_intent( value, query_family="INTERSECT", kind="edge", evidence="kernel_history", allowed=("intersection",), ) | { "intersection_sources": sources, "disambiguation": {"type": "source_qualified_primary_section"}, }, } def _uses_planar_imprint_profile( owner: str, feature_by_id: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], ) -> bool: """Return whether a producer's profile was selected by B-rep IMPRINT.""" feature = feature_by_id.get(f"f_{owner}") or {} sketch = sketches_by_id.get(str(feature.get("sketch_id") or "")) or {} return (sketch.get("profile") or {}).get("type") == "planar_imprint" def _source_sketch(params: dict[str, Any]) -> str | None: for key in ("entities", "sheetProfilesArray", "surfaceEntities"): if key in params: query = parse_query(params[key]) if query.source_sketch: return query.source_sketch return None def _same_profile_frame(left: dict[str, Any], right: dict[str, Any]) -> bool: """Require identical source frames, without inventing a coordinate map.""" for key in ("origin_mm", "x_dir", "normal"): first, second = left.get(key), right.get(key) if ( not isinstance(first, list) or not isinstance(second, list) or len(first) != 3 or len(second) != 3 or any(not isinstance(value, (int, float)) or not math.isfinite(float(value)) for value in [*first, *second]) or any(float(a) != float(b) for a, b in zip(first, second)) ): return False return True def _direct_sketch_region_union_leaves(value: Any) -> list[Call] | None: """Flatten only associative qUnion wrappers around direct region leaves.""" if not isinstance(value, Call) or value.name != "qUnion" or len(value.args) != 1 or not isinstance(value.args[0], list): return None leaves: list[Call] = [] for operand in value.args[0]: if isinstance(operand, Call) and operand.name == "qUnion": nested = _direct_sketch_region_union_leaves(operand) if nested is None: return None leaves.extend(nested) elif isinstance(operand, Call) and operand.name == "qSketchRegion" and len(operand.args) == 2: leaves.append(operand) else: return None return leaves def _multi_source_sketch_region_profile( value: Any, sketch_by_source: dict[str, dict[str, Any]], feature_id: str, ) -> dict[str, Any] | None: """Materialize a direct same-frame ``qUnion(qSketchRegion(...))`` profile. This is profile geometry, not a topology selector: each source region is resolved independently by the runtime so a contour nested in a different sketch cannot accidentally become a hole in another sketch's region. """ leaves = _direct_sketch_region_union_leaves(value) if leaves is None or len(leaves) < 2: return None sources: list[str] = [] for leaf in leaves: if str(leaf.args[1]).lower() != "true": return None source = symbolic_string(leaf.args[0]).split(".", 1)[0] if not source: return None sources.append(source) # qUnion is a set union. Repeating the whole source region cannot create # another profile region, so preserve first-occurrence source ordering. sources = list(dict.fromkeys(sources)) if len(sources) < 2 or any(source not in sketch_by_source for source in sources): return None sketches = [sketch_by_source[source] for source in sources] if any(sketch.get("attachment") is not None or not _profile_executable(sketch) for sketch in sketches): return None frame = sketches[0].get("workplane") if not isinstance(frame, dict) or any(not _same_profile_frame(frame, sketch.get("workplane") or {}) for sketch in sketches[1:]): return None profiles = [deepcopy(sketch["profile"]) for sketch in sketches] if any(profile.get("type") not in {"circle", "polygon", "analytic_contours"} for profile in profiles): return None return { "id": f"sketch_regions_{feature_id}", "name": f"regions_{feature_id}", "workplane": deepcopy(frame), "profile": { "type": "multi_source_regions", "source_sketch_ids": sources, "profiles": profiles, }, } def _has_composed_sketch_region_union(value: Any) -> bool: """Identify composed region input that must not fall back to one source.""" leaves = _direct_sketch_region_union_leaves(value) if leaves is None: return False sources = { symbolic_string(leaf.args[0]).split(".", 1)[0] for leaf in leaves } return len(leaves) >= 2 and len(sources - {""}) >= 2 def _sketch_region_query(value: Any) -> Any | None: """Return one explicit qSketchRegion when a profile query also carries context faces.""" matches = [ item for item in _queries(value) if any(call.name == "qSketchRegion" for call in walk_calls(item)) ] return matches[0] if len(matches) == 1 else None def _imprint_sketch(value: Any) -> str | None: for call in walk_calls(value): if call.name != "makeQuery" or not call.args: continue owner = symbolic_string(call.args[0]) if owner.endswith(".imprint"): return owner.split(".", 1)[0] return None def _profile_selection_side(value: Any) -> float | None: def numbers(item: Any): if isinstance(item, (int, float)): yield float(item) elif isinstance(item, list): for child in item: yield from numbers(child) elif isinstance(item, dict): for child in item.values(): yield from numbers(child) for call in walk_calls(value): if call.name in {"TD", "topologyDisambiguation"}: return next(numbers(call.args), None) return None def _profile_selection_sides(value: Any) -> list[float]: """Return every explicit topology side from one nested IMPRINT query.""" def numbers(item: Any): if isinstance(item, (int, float)): yield float(item) elif isinstance(item, list): for child in item: yield from numbers(child) elif isinstance(item, dict): for child in item.values(): yield from numbers(child) sides = [] for call in walk_calls(value): if call.name in {"TD", "topologyDisambiguation"}: side = next(numbers(call.args), None) if side is not None: sides.append(side) return sides def _partitioned_imprint_sketch( value: Any, entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> str | None: """Resolve a two-sided split IMPRINT union back to its bounded sketch region. A closed sketch curve that intersects a parent-face boundary is split into two IMPRINT regions. CADFS records the two halves as a qUnion, with the same outer face side and opposite sides around the intersection vertex. The union is exactly the curve's bounded sketch region. Do not accept a partial union, a mixed source curve, or an open curve here: each case has different material semantics and must remain an explicit capability gap. """ roots = _queries(value) if len(roots) != 2: return None source_sketch: str | None = None source_entity: str | None = None nested_sides: set[float] = set() for root in roots: try: _call, owner, topology, kind, _definition = _direct_make_query(root) except ValueError: return None info = parse_query(root) references = { (sketch, entity) for sketch, entity in _source_refs(root) if sketch == owner } sides = _profile_selection_sides(root) if topology != "IMPRINT" or kind != "face" or owner != info.source_sketch: return None if len(references) != 1 or len(sides) != 2 or sides[0] >= 0: return None sketch, entity = next(iter(references)) if source_sketch is None: source_sketch, source_entity = sketch, entity elif (source_sketch, source_entity) != (sketch, entity): return None nested_sides.add(sides[1]) if source_sketch is None or source_entity is None or nested_sides != {-1.0, 1.0}: return None entities = entity_by_sketch.get(source_sketch) or {} source = entities.get(source_entity) if source is None: source_id = max((key for key in entities if source_entity.startswith(key + ".")), key=len, default="") source = entities.get(source_id) if source is None or source.get("type") != "bspline" or not _same_point(source["start"], source["end"]): return None return source_sketch def _intersect_partition_profile_sketch( value: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_id: str, ) -> dict[str, Any] | None: """Materialize one full circular region selected through a split diameter. A sketch line that crosses nested concentric circles is split at the selected circle intersection. CADFS can select the two face sides around that split edge instead of naming the circular region directly. Both sides together are exactly the bounded region inside the selected circle: either its disk or the annulus ending at the immediately inner circle. Keep this deliberately narrow. A non-diameter line, any non-circular source, a partial side selection, or an ambiguous nested query remains an INTERSECT capability diagnostic rather than an invented profile. """ roots = _queries(value) if len(roots) != 2: return None source_sketch: str | None = None source_line: str | None = None source_circle: str | None = None outer_sides: set[float] = set() for root in roots: try: _call, owner, topology, kind, _definition = _direct_make_query(root) except ValueError: return None sides = _profile_selection_sides(root) if topology != "IMPRINT" or kind != "face" or len(sides) != 2 or sides[1] != -1.0: return None references = list(dict.fromkeys(_source_refs(root))) if len(references) != 2 or any(sketch != owner for sketch, _entity in references): return None entities = entity_by_sketch.get(owner) or {} line_ids = [entity_id for _sketch, entity_id in references if (entities.get(entity_id) or {}).get("type") == "line"] circle_ids = [entity_id for _sketch, entity_id in references if (entities.get(entity_id) or {}).get("type") == "circle"] if len(line_ids) != 1 or len(circle_ids) != 1: return None if source_sketch is None: source_sketch, source_line, source_circle = owner, line_ids[0], circle_ids[0] elif (source_sketch, source_line, source_circle) != (owner, line_ids[0], circle_ids[0]): return None outer_sides.add(sides[0]) if source_sketch is None or source_line is None or source_circle is None or outer_sides != {-1.0, 1.0}: return None sketch = sketch_by_source.get(source_sketch) entities = entity_by_sketch.get(source_sketch) or {} line, circle = entities.get(source_line), entities.get(source_circle) if sketch is None or line is None or circle is None: return None start, end, center = line.get("start"), line.get("end"), circle.get("center") if not all(isinstance(point, list) and len(point) == 2 for point in (start, end, center)): return None direction = [end[index] - start[index] for index in range(2)] length = math.hypot(*direction) if length <= 1e-9: return None projection = sum((center[index] - start[index]) * direction[index] for index in range(2)) / (length * length) distance = abs((center[0] - start[0]) * direction[1] - (center[1] - start[1]) * direction[0]) / length if not 1e-6 < projection < 1.0 - 1e-6 or distance > 1e-6: return None profile = _circle_imprint_region((sketch.get("profile") or {}).get("contours") or [], circle, 1.0) if profile is None: return None output = deepcopy(sketch) output["id"] = f"{sketch['id']}__{feature_id}" output["name"] = f"{sketch['name']}__{feature_id}" output["profile"] = profile return output def _definition_topology_side(definition: dict[str, Any]) -> float | None: """Read the directly attached ``TD`` sign from one query definition.""" def numbers(item: Any): if isinstance(item, (int, float)): yield float(item) elif isinstance(item, list): for child in item: yield from numbers(child) elif isinstance(item, dict): for child in item.values(): yield from numbers(child) for call in walk_calls(definition.get("disambiguationData")): if call.name in {"TD", "topologyDisambiguation"}: side = next(numbers(call.args), None) if side in {-1.0, 1.0}: return side return None def _definition_order(definition: dict[str, Any]) -> int | None: """Read a finite non-negative ``OD`` index without guessing an intersection.""" def numbers(item: Any): if isinstance(item, (int, float)): yield float(item) elif isinstance(item, list): for child in item: yield from numbers(child) elif isinstance(item, dict): for child in item.values(): yield from numbers(child) for call in walk_calls(definition.get("disambiguationData")): if call.name in {"OD", "orderDisambiguation"}: value = next(numbers(call.args), None) if value is None or not math.isfinite(value) or value < 0 or value != round(value): return None return int(value) return None def _planar_imprint_selection(value: Any) -> tuple[str, dict[str, Any]] | None: """Lower one IMPRINT face query to source-edge and side evidence. The result deliberately retains the nested edge-fragment proof instead of flattening it to an arbitrary original profile. A fragment-side sign is only meaningful together with an exact ``INTERSECT`` vertex and optional FeatureScript order disambiguation; any other nested form stays outside this contract. """ try: _root, owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None face_side = _definition_topology_side(definition) if topology != "IMPRINT" or kind != "face" or face_side is None: return None edge_queries: list[tuple[Call, dict[str, Any]]] = [] for call in walk_calls(definition.get("disambiguationData")): if call.name != "makeQuery": continue try: _edge, edge_owner, edge_topology, edge_kind, edge_definition = _direct_make_query(call) except ValueError: continue if edge_owner == owner and edge_topology == "IMPRINT" and edge_kind == "edge": edge_queries.append((call, edge_definition)) if len(edge_queries) != 1: return None _edge, edge_definition = edge_queries[0] sources = list(dict.fromkeys(_source_refs(edge_definition.get("derivedFrom")))) if len(sources) != 1 or sources[0][0] != owner: return None source_entity = sources[0][1] selection: dict[str, Any] = {"source_entity_id": source_entity, "face_side": face_side} intersections: list[dict[str, Any]] = [] for call in walk_calls(edge_definition.get("disambiguationData")): if call.name != "makeQuery": continue try: _intersection, intersection_owner, intersection_topology, intersection_kind, intersection_definition = _direct_make_query(call) except ValueError: continue if intersection_owner == owner and intersection_topology == "INTERSECT" and intersection_kind == "vertex": intersections.append(intersection_definition) if not intersections: return owner, selection if len(intersections) != 1: return None fragment_side = _definition_topology_side(edge_definition) intersection_derived = intersections[0].get("derivedFrom") intersection_sources = list(dict.fromkeys(_source_refs(intersection_derived))) if fragment_side is None or len(intersection_sources) != 2: return None same_sketch_anchor = [ entity for sketch, entity in intersection_sources if sketch == owner and entity != source_entity ] fragment: dict[str, Any] if len(same_sketch_anchor) == 1 and all(sketch == owner for sketch, _entity in intersection_sources): fragment = {"anchor_entity_id": same_sketch_anchor[0], "side": fragment_side} else: external_queries: list[Call] = [] for call in walk_calls(intersection_derived): if call.name != "makeQuery": continue try: _edge, _edge_owner, edge_topology, edge_kind, _edge_definition = _direct_make_query(call) except ValueError: continue if edge_topology == "CAP_EDGE" and edge_kind in {"edge", "entitytype.edge"}: external_queries.append(call) local_sources = [(sketch, entity) for sketch, entity in intersection_sources if sketch == owner] if len(local_sources) != 1 or local_sources[0][1] != source_entity or len(external_queries) != 1: return None # Kept only while lowering this profile. The CDSL profile receives a # typed selector through the external-anchor contract below. fragment = {"_external_anchor_query": external_queries[0], "side": fragment_side} order = _definition_order(intersections[0]) if order is not None: fragment["intersection_index"] = order selection["fragment"] = fragment return owner, selection def _planar_imprint_profile_sketch( value: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_id: str, *, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], previous: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Create an exact planar-arrangement profile from IMPRINT face queries. The adapter later splits a bounded support face with these original analytic curves and consumes only the selected B-rep regions. This is a typed derived-profile contract, not a polygonization or a reconstruction of a potentially unrelated source contour. """ selections: list[dict[str, Any]] = [] external_anchors: list[dict[str, Any]] = [] source_sketch: str | None = None for root in _queries(value): parsed = _planar_imprint_selection(root) if parsed is None: return None owner, selection = parsed if source_sketch is None: source_sketch = owner elif source_sketch != owner: return None fragment = selection.get("fragment") or {} external_query = fragment.pop("_external_anchor_query", None) if external_query is not None: try: _call, external_owner, external_topology, external_kind, _definition = _direct_make_query(external_query) except ValueError: return None if external_topology != "CAP_EDGE" or external_kind not in {"edge", "entitytype.edge"}: return None external_selector = _direct_prism_swept_selector( external_query, owner=external_owner, selector_kind="edge", feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=featurescript_version, allow_continuation=False, ) attachment = (sketch_by_source.get(owner) or {}).get("attachment") attachment_intent = attachment.get("selector_intent") if isinstance(attachment, dict) else None external_intent = external_selector.get("selector_intent") if isinstance(external_selector, dict) else None if ( external_selector is None or not isinstance(attachment, dict) or attachment.get("owner_feature_id") != external_selector.get("owner_feature_id") or attachment.get("output_role") != (external_intent or {}).get("lineage_role") or not isinstance(attachment_intent, dict) or attachment_intent.get("consumer_contract") != "direct_prism_cap_face_workplane" ): return None anchor_id = f"cap_boundary_{len(external_anchors)}" fragment["external_anchor_id"] = anchor_id external_anchors.append({"id": anchor_id, "selector": external_selector}) selections.append(selection) if source_sketch is None or not selections or source_sketch not in sketch_by_source: return None entities = entity_by_sketch.get(source_sketch) or {} source_entities: list[dict[str, Any]] = [] for entity_id, curve in entities.items(): if curve.get("construction") or curve.get("type") not in {"line", "arc", "circle", "ellipse", "bspline"}: continue source_entities.append({"id": entity_id, "curve": {key: deepcopy(value) for key, value in curve.items() if key != "construction"}}) known = {entry["id"] for entry in source_entities} for selection in selections: fragment = selection.get("fragment") or {} if selection["source_entity_id"] not in known or (fragment.get("anchor_entity_id") and fragment.get("anchor_entity_id") not in known): return None # A same-sketch fragment needs a second source curve as its splitter # anchor. The current source admission for an attached boundary is only # the one-circle / one-edge form: a periodic B-spline has seam ambiguity, # and multi-curve or multi-anchor arrangements need a set-cardinality # contract rather than reusing this singleton policy. single_external_circle_fragment = ( len(source_entities) == 1 and source_entities[0].get("curve", {}).get("type") == "circle" and bool(external_anchors) and len(external_anchors) == 1 and len(external_anchors) == len(selections) and all( isinstance(selection.get("fragment"), dict) and isinstance(selection["fragment"].get("external_anchor_id"), str) and selection["fragment"]["external_anchor_id"] for selection in selections ) ) if external_anchors and not single_external_circle_fragment: return None if len(source_entities) < 2 and not single_external_circle_fragment: return None output = deepcopy(sketch_by_source[source_sketch]) output["id"] = f"{output['id']}__{feature_id}" output["name"] = f"{output['name']}__{feature_id}" output["profile"] = { "type": "planar_imprint", "source_entities": source_entities, "selections": selections, } if external_anchors: output["profile"]["external_anchors"] = external_anchors return output def _same_point(left: list[float], right: list[float]) -> bool: return math.dist(left, right) <= 1e-5 def _matching_profile_segment(segment: dict[str, Any], source: dict[str, Any]) -> int: if segment.get("type") != source.get("type"): return 0 if segment["type"] == "circle": return 1 if _same_point(segment["center"], source["center"]) and abs(segment["radius_mm"] - source["radius_mm"]) <= 1e-5 else 0 if segment["type"] == "ellipse": if not ( _same_point(segment["center"], source["center"]) and abs(segment["major_radius_mm"] - source["major_radius_mm"]) <= 1e-5 and abs(segment["minor_radius_mm"] - source["minor_radius_mm"]) <= 1e-5 ): return 0 direction = _dot(segment["major_axis"], source["major_axis"]) return 1 if direction >= 0 else -1 if _same_point(segment["start"], source["start"]) and _same_point(segment["end"], source["end"]): return 1 if _same_point(segment["start"], source["end"]) and _same_point(segment["end"], source["start"]): return -1 return 0 def _circle_imprint_region(contours: list[dict[str, Any]], source: dict[str, Any], side: float | None) -> dict[str, Any] | None: """Materialize one bounded side of a nested circular IMPRINT edge.""" if source.get("type") != "circle" or side is None: return None center = source["center"]; radius = float(source["radius_mm"]) circles = sorted({ float(segment["radius_mm"]) for contour in contours for segment in contour.get("segments") or [] if segment.get("type") == "circle" and _same_point(segment.get("center") or [], center) }) index = next((item for item, value in enumerate(circles) if abs(value - radius) <= 1e-5), None) if index is None: return None # 标准 skCircle 的正侧位于圆内。若同心圆存在,选区应止于其直接 # 内层边界;负侧则选择与直接外层边界之间的环域。没有相邻边界时, # 只有圆内区域是可由 CDSL 闭合 profile 表达的。 inner = circles[index - 1] if side > 0 and index > 0 else radius outer = radius if side > 0 else (circles[index + 1] if index + 1 < len(circles) else None) if outer is None: return None if inner == 0 or (side > 0 and index == 0): return {"type": "circle", "center": list(center), "radius_mm": outer} return { "type": "analytic_contours", "contours": [ {"role": "outer", "closed": True, "segments": [{"type": "circle", "center": list(center), "radius_mm": outer}]}, {"role": "inner", "closed": True, "segments": [{"type": "circle", "center": list(center), "radius_mm": inner}]}, ], } def _circle_imprint_union_profile( contours: list[dict[str, Any]], query_values: list[Any], entities: dict[str, dict[str, Any]], ) -> dict[str, Any] | None: """Materialize one bounded union of concentric circular IMPRINT regions.""" resolved: list[tuple[dict[str, Any], float]] = [] for query_value in query_values: selection = parse_query(query_value) source_id = max((key for key in entities if selection.source_entity and selection.source_entity.startswith(key)), key=len, default="") source = entities.get(source_id) side = _profile_selection_side(query_value) if source is None or source.get("type") != "circle" or side is None: return None resolved.append((source, side)) if not resolved: return None center = resolved[0][0]["center"] if any(not _same_point(source["center"], center) for source, _side in resolved[1:]): return None radii = sorted({ float(segment["radius_mm"]) for contour in contours for segment in contour.get("segments") or [] if segment.get("type") == "circle" and _same_point(segment.get("center") or [], center) }) intervals: list[tuple[float, float]] = [] for source, side in resolved: radius = float(source["radius_mm"]) index = next((item for item, value in enumerate(radii) if abs(value - radius) <= 1e-5), None) if index is None: return None if side > 0: intervals.append((radii[index - 1] if index else 0.0, radius)) elif index + 1 < len(radii): intervals.append((radius, radii[index + 1])) else: # 最外圆的负侧是无界区域,不能变成任意默认实体。 return None intervals.sort() merged: list[list[float]] = [] for lower, upper in intervals: if merged and lower <= merged[-1][1] + 1e-5: merged[-1][1] = max(merged[-1][1], upper) else: merged.append([lower, upper]) if len(merged) != 1: return None inner, outer = merged[0] outer_sources = { segment.get("source_entity_id") for contour in contours for segment in contour.get("segments") or [] if ( segment.get("type") == "circle" and _same_point(segment.get("center") or [], center) and abs(float(segment.get("radius_mm") or 0.0) - outer) <= 1e-5 and isinstance(segment.get("source_entity_id"), str) and segment["source_entity_id"] ) } # The union's outer boundary is still one original source circle when the # source sketch names it uniquely. Preserve that exact source identity so # a later direct-prism builder can expose its native swept-face history. # An annulus or a duplicate boundary intentionally carries no inferred # label: a source query then remains unresolved rather than borrowing a # geometrically equal circle. outer_source = next(iter(outer_sources)) if len(outer_sources) == 1 else None if inner <= 1e-5: return { "type": "circle", "center": list(center), "radius_mm": outer, **({"source_entity_id": outer_source} if outer_source is not None else {}), } return { "type": "analytic_contours", "contours": [ { "role": "outer", "closed": True, "segments": [{ "type": "circle", "center": list(center), "radius_mm": outer, **({"source_entity_id": outer_source} if outer_source is not None else {}), }], }, {"role": "inner", "closed": True, "segments": [{"type": "circle", "center": list(center), "radius_mm": inner}]}, ], } def _surface_circle_radii(profile: dict[str, Any]) -> list[tuple[list[float], float]]: """Return the explicit circular wires retained by a surface extrusion.""" contours = profile.get("contours") if profile.get("type") == "analytic_contours" else None if not isinstance(contours, list): return [] circles = [] for contour in contours: segments = contour.get("segments") or [] if len(segments) != 1 or segments[0].get("type") != "circle": continue circles.append((list(segments[0].get("center") or []), float(segments[0]["radius_mm"]))) return circles def _surface_trimmed_imprint_profile( profile_sketch: dict[str, Any], surface_profile_sketch: dict[str, Any] | None, previous_surfaces: list[dict[str, Any]], ) -> dict[str, Any]: """Materialize the bounded solid region created by a coaxial surface split. CADFS can retain an earlier, bidirectional surface extrusion while a later mixed extrusion selects an IMPRINT face. The selected circular face is then bounded by that surface, rather than by the sketch origin. CDSL has no general surface/solid trim operation yet, so preserve this proven circular case as its actual annular profile. Other surface combinations remain on the ordinary IMPRINT lowering path instead of guessing a trim. """ profile = profile_sketch.get("profile") or {} if profile.get("type") != "circle" or surface_profile_sketch is None: return profile_sketch center = list(profile.get("center") or []) outer = float(profile.get("radius_mm") or 0.0) if len(center) != 2 or outer <= 0: return profile_sketch selected_radii = _surface_circle_radii(surface_profile_sketch.get("profile") or {}) if not any(_same_point(item_center, center) and abs(radius - outer) <= 1e-5 for item_center, radius in selected_radii): return profile_sketch plane = profile_sketch.get("workplane") or {} origin = plane.get("origin_mm") or [] x_dir = plane.get("x_dir") or [] y_dir = _y_dir(plane) if len(origin) != 3 or len(x_dir) != 3: return profile_sketch world_center = [origin[index] + center[0] * x_dir[index] + center[1] * y_dir[index] for index in range(3)] candidates: list[float] = [] for surface in previous_surfaces: surface_plane = surface["workplane"] normal = surface_plane["normal"] surface_origin = surface_plane["origin_mm"] span_start = -float(surface.get("reverse_distance_mm") or 0.0) span_end = float(surface.get("distance_mm") or 0.0) projection = _dot(_sub(world_center, surface_origin), normal) if projection < span_start - 1e-5 or projection > span_end + 1e-5: continue surface_x_dir = surface_plane["x_dir"] surface_y_dir = _y_dir(surface_plane) local_center = [_dot(_sub(world_center, surface_origin), surface_x_dir), _dot(_sub(world_center, surface_origin), surface_y_dir)] for surface_center, radius in _surface_circle_radii(surface["profile"]): if _same_point(surface_center, local_center) and 1e-5 < radius < outer - 1e-5: candidates.append(radius) if not candidates: return profile_sketch inner = max(candidates) output = deepcopy(profile_sketch) output["profile"] = { "type": "analytic_contours", "contours": [ {"role": "outer", "closed": True, "segments": [{"type": "circle", "center": center, "radius_mm": outer}]}, {"role": "inner", "closed": True, "segments": [{"type": "circle", "center": center, "radius_mm": inner}]}, ], } return output def _open_imprint_profile_sketch( sketch: dict[str, Any], value: Any, feature_id: str, ) -> dict[str, Any] | None: """Materialize an open IMPRINT contour closed by its attached host face.""" query = parse_query(value) profile = sketch.get("profile") or {} segments = profile.get("construction") or [] if query.topology_type != "IMPRINT" or profile.get("contours") or len(segments) < 2: return None if any(segment.get("type") not in {"line", "arc", "bspline"} for segment in segments): return None return { "id": f"{sketch['id']}__{feature_id}", "name": f"{sketch['name']}__{feature_id}", "workplane": dict(sketch["workplane"]), "profile": { "type": "analytic_contours", "contours": [{"role": "open", "closed": False, "segments": deepcopy(segments)}], }, } def _cap_face_selector(value: Any, feature_frames: dict[str, dict[str, Any]], feature_id: str, suffix: str) -> dict[str, Any] | None: """Capture one uniquely framed CAP_FACE/CAP_EDGE as a runtime face selector.""" query = parse_query(value) if query.topology_type not in {"CAP_FACE", "CAP_EDGE"} or not query.owner_feature or query.is_start is None: return None frame = feature_frames.get(query.owner_feature) if frame is None: return None cap = frame.get("start" if query.is_start else "end") if cap is None: return None normal = list(cap["normal"]) return { "kind": "face", "owner_feature_id": f"f_{query.owner_feature}", "stable_id": f"cadfs_{feature_id}_{suffix}", "source": "runtime_snapshot", "confidence": 1.0, "binding_feature_id": f"f_{query.owner_feature}", "geometry": {"normal": normal, "plane_offset_mm": _dot(normal, cap["origin_mm"])}, "selector_intent": _deferred_featurescript_selector_intent(value, kind="face"), } def _cap_face_output_role_selector( value: Any, feature_by_id: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], *, allow_initial_loft: bool = False, allow_two_sided_circle_shell: bool = False, allow_primary_add_shell: bool = False, allow_primary_add_up_to_surface: bool = False, allow_primary_add_dressup: bool = False, allow_two_sided_up_to_surface_pair: bool = False, source_sketches: dict[str, dict[str, Any]] | None = None, ) -> dict[str, Any] | None: """Reference one direct-builder cap face without reconstructing its sketch. A CAP_FACE is a B-rep output, not an alias for the profile that originally produced it. The selector is therefore legal only when its producer has a runtime builder role capable of proving the exact active face. The runtime rejects it if a later mutation makes that role non-unique or unavailable. """ try: _call, owner, topology, kind, _definition = _direct_make_query(value) except ValueError: return None if topology != "CAP_FACE" or kind != "face": return None query = parse_query(value) if query.is_start is None: return None owner_feature_id = f"f_{owner}" producer = feature_by_id.get(owner_feature_id) params = (producer or {}).get("params") or {} producer_sketch = sketches_by_id.get(str((producer or {}).get("sketch_id") or "")) # Both far caps of a symmetric direct prism are exact builder outputs, but # neither is independently valid as a one-sided extent target. The caller # admits them only as the complete opposite-role pair below. if allow_two_sided_up_to_surface_pair: profile_source = (producer_sketch or {}).get("source_sketch_id") source_sketch = (source_sketches or {}).get(str(profile_source or "")) source_ids = _direct_profile_source_entity_ids(producer_sketch or {}) refs = list(dict.fromkeys(_source_refs(value))) ref_ids = {entity_id for _sketch_id, entity_id in refs} if ( producer is not None and producer.get("atomic_id") == "extrude_add_two_sided" and params.get("result_mode") == "new_body" and (params.get("end_condition") or {}).get("type") == "blind" and (params.get("reverse_end_condition") or {}).get("type") == "blind" and params.get("draft") is None and isinstance(profile_source, str) and source_sketch is not None and _profile_matches_direct_source(producer_sketch or {}, source_sketch) and source_ids and refs and {sketch_id for sketch_id, _entity_id in refs} == {profile_source} and ref_ids == source_ids and query.source_sketch == profile_source and query.source_entity in source_ids ): role = f"extrude.{'start' if query.is_start else 'end'}" intent = _selector_intent( value, query_family="CAP_FACE", kind="face", evidence="operation_role", allowed=("boundary",), output_role=role, disambiguation={"source_profile_entity_ids": sorted(source_ids)}, ) intent["consumer_contract"] = "symmetric_direct_prism_two_sided_up_to_surface_cap_pair" return { "kind": "face", "owner_feature_id": owner_feature_id, "output_role": role, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } # The two builders of a symmetric prism share the source plane, so only # their far First/LastShape handles identify the physical CAP faces. The # executor records those final-snapshot roles exactly. Keep this bridge # confined to a shell removing either cap of one direct circle: unlike a # general CAP_FACE it has an explicit one-edge source anchor and no # cross-feature continuation or extent semantics. if allow_two_sided_circle_shell: profile = (producer_sketch or {}).get("profile") or {} source_sketch_id = (producer_sketch or {}).get("source_sketch_id") source_entity_id = profile.get("source_entity_id") refs = _source_refs(value) if ( producer is not None and producer.get("atomic_id") == "extrude_add_two_sided" and params.get("result_mode") == "new_body" and (params.get("end_condition") or {}).get("type") == "blind" and (params.get("reverse_end_condition") or {}).get("type") == "blind" and params.get("draft") is None and profile.get("type") == "circle" and isinstance(source_sketch_id, str) and isinstance(source_entity_id, str) and refs == [(source_sketch_id, source_entity_id)] and query.source_sketch == source_sketch_id and query.source_entity == source_entity_id ): role = f"extrude.{'start' if query.is_start else 'end'}" return { "kind": "face", "owner_feature_id": owner_feature_id, "output_role": role, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": _selector_intent( value, query_family="CAP_FACE", kind="face", evidence="operation_role", allowed=("boundary",), output_role=role, ), } # A loft CAP is identified by its OSD profile provenance, not by # ``isStart``. FeatureScript's CAP naming direction is independent of # the ThruSections wire order. if producer and producer.get("atomic_id") == "loft_add": if not allow_initial_loft: return None profile_sources = params.get("cap_output_profile_sources") if ( params.get("initial_output_roles") is not True or not isinstance(profile_sources, list) or len(profile_sources) != 2 or len(set(profile_sources)) != 2 ): return None imprint_owners = set() for call in walk_calls(_definition.get("disambiguationData")): if call.name != "makeQuery": continue try: _direct, direct_owner, direct_topology, direct_kind, _direct_definition = _direct_make_query(call) except ValueError: continue if direct_topology == "IMPRINT" and direct_kind == "face": imprint_owners.add(direct_owner) if len(imprint_owners) != 1: return None source = next(iter(imprint_owners)) if source not in profile_sources: return None role = ("loft.start", "loft.end")[profile_sources.index(source)] return { "kind": "face", "owner_feature_id": owner_feature_id, "output_role": role, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": _selector_intent( value, query_family="CAP_FACE", kind="face", evidence="operation_role", allowed=("boundary",), output_role=role, disambiguation={"type": "loft_profile_source", "source_sketch_id": source}, ), } def has_one_closed_outer_region(sketch: dict[str, Any] | None) -> bool: """Prove the narrowed draft builder can receive exactly one face.""" profile = (sketch or {}).get("profile") or {} if profile.get("type") == "circle": return True contours = profile.get("contours") return ( profile.get("type") == "analytic_contours" and isinstance(contours, list) and len(contours) == 1 and bool((contours[0] or {}).get("closed")) ) # This derived-profile contract exposes only caps that a direct, one-sided, # independently retained builder result can prove. The adapter supports # both a regular prism and the restricted one-face LocOpe drafted-prism # builder path. Fused/multi-extent/profile results still have no # unambiguous builder output in the active snapshot. # A primary ADD has no independently active tool body. It may expose this # CAP role only to its immediate shell, one-sided up-to-surface, or # dress-up consumer: # execution registers the prism as transient and the resolver must prove # the exact union successor in the active final member. Other CAP_FACE # consumers keep the existing independent-new-body contract. primary_add_consumer = ( ( allow_primary_add_shell or allow_primary_add_up_to_surface or allow_primary_add_dressup ) and producer is not None and producer.get("atomic_id") == "extrude_add_blind" and params.get("result_mode") != "new_body" ) if ( producer is None or producer.get("atomic_id") != "extrude_add_blind" or (params.get("result_mode") != "new_body" and not primary_add_consumer) or (params.get("end_condition") or {}).get("type") != "blind" # LocOpe_DPrism exposes one builder cap only for the one-face path. # A profile with holes is valid geometry but follows the fallback # tapered-extrude path, which intentionally carries no cap role. or params.get("draft") is not None and not has_one_closed_outer_region(producer_sketch) ): return None role_prefix = { "extrude_add_blind": "extrude", }[str(producer["atomic_id"])] selector_intent = _selector_intent( value, query_family="CAP_FACE", kind="face", evidence="operation_role", allowed=("boundary", "continuation"), output_role=f"{role_prefix}.{'start' if query.is_start else 'end'}", ) if primary_add_consumer: selector_intent["consumer_contract"] = { "shell": "primary_add_shell_union_continuation", "up_to_surface": "primary_add_up_to_surface_union_continuation", "dressup": "primary_add_dressup_union_continuation", }[ "up_to_surface" if allow_primary_add_up_to_surface else "dressup" if allow_primary_add_dressup else "shell" ] return { "kind": "face", "owner_feature_id": owner_feature_id, "output_role": f"{role_prefix}.{'start' if query.is_start else 'end'}", "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": selector_intent, } def _two_sided_up_to_surface_cap_pair( forward_value: Any, reverse_value: Any, *, feature_by_id: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], source_sketches: dict[str, dict[str, Any]], previous: list[str], featurescript_version: str | None, ) -> tuple[dict[str, Any], dict[str, Any]] | None: """Lower opposite direct CAP faces only as one symmetric-extent pair.""" if featurescript_version != "1511": return None forward = _cap_face_output_role_selector( forward_value, feature_by_id, sketches_by_id, allow_two_sided_up_to_surface_pair=True, source_sketches=source_sketches, ) reverse = _cap_face_output_role_selector( reverse_value, feature_by_id, sketches_by_id, allow_two_sided_up_to_surface_pair=True, source_sketches=source_sketches, ) if ( forward is None or reverse is None or forward["owner_feature_id"] != reverse["owner_feature_id"] or previous[-1:] != [forward["owner_feature_id"]] or {forward["output_role"], reverse["output_role"]} != {"extrude.start", "extrude.end"} ): return None forward_ids = (forward.get("selector_intent") or {}).get("disambiguation", {}).get("source_profile_entity_ids") reverse_ids = (reverse.get("selector_intent") or {}).get("disambiguation", {}).get("source_profile_entity_ids") if not isinstance(forward_ids, list) or forward_ids != reverse_ids: return None return forward, reverse def _two_sided_up_to_surface_shell_swept_face_pair( forward_value: Any, reverse_value: Any, *, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> tuple[dict[str, Any], dict[str, Any]] | None: """Lower two retained prism side walls through one immediate shell. The source queries remain source-edge anchored ``SWEPT_FACE`` lineage; this function only admits their paired extent consumer when the shell is the exact current lifecycle successor. The topology registry must still prove each source edge -> prism wall -> shell offset-face continuation at runtime, so this does not select a face by geometry or body position. """ if featurescript_version != "1511" or len(previous) < 2: return None try: _forward_call, forward_owner, forward_topology, forward_kind, _forward_definition = _direct_make_query(forward_value) _reverse_call, reverse_owner, reverse_topology, reverse_kind, _reverse_definition = _direct_make_query(reverse_value) except ValueError: return None if ( forward_topology != "SWEPT_FACE" or reverse_topology != "SWEPT_FACE" or forward_kind not in {"face", "entitytype.face"} or reverse_kind not in {"face", "entitytype.face"} or forward_owner != reverse_owner ): return None producer_id = f"f_{forward_owner}" shell_id = previous[-1] shell = feature_by_id.get(shell_id) or {} if ( previous[-2] != producer_id or shell.get("atomic_id") != "shell" or shell.get("depends_on") != [producer_id] ): return None forward = _direct_prism_swept_selector( forward_value, owner=forward_owner, selector_kind="face", feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=featurescript_version, allow_continuation=True, ) reverse = _direct_prism_swept_selector( reverse_value, owner=reverse_owner, selector_kind="face", feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=featurescript_version, allow_continuation=True, ) if forward is None or reverse is None: return None forward_source = (forward.get("selector_intent") or {}).get("source_entity") or {} reverse_source = (reverse.get("selector_intent") or {}).get("source_entity") or {} if ( forward_source == reverse_source or not isinstance(forward_source.get("sketch_id"), str) or not isinstance(forward_source.get("entity_id"), str) or not isinstance(reverse_source.get("entity_id"), str) ): return None shell_source_entities = { (intent.get("source_entity") or {}).get("entity_id") for selector in shell.get("selectors") or () if isinstance(selector, dict) for intent in [selector.get("selector_intent")] if isinstance(intent, dict) and intent.get("query_family") == "SWEPT_FACE" and isinstance((intent.get("source_entity") or {}).get("entity_id"), str) } if { forward_source["entity_id"], reverse_source["entity_id"], } & shell_source_entities: # A shell may retain a planar closing descendant of a removed side. # It is not the physical offset face requested by this bridge. return None source_sketch_id = forward_source["sketch_id"] forward_entity = (entity_by_sketch.get(source_sketch_id) or {}).get(forward_source["entity_id"]) reverse_entity = (entity_by_sketch.get(source_sketch_id) or {}).get(reverse_source["entity_id"]) if ( forward_entity is None or reverse_entity is None or forward_entity.get("type") != "line" or reverse_entity.get("type") != "line" or forward_entity.get("construction") or reverse_entity.get("construction") ): return None for selector in (forward, reverse): selector["selector_intent"]["consumer_contract"] = "symmetric_direct_prism_shell_swept_face_up_to_surface_pair" return forward, reverse def _initial_direct_sweep_cap_output_role_selector( value: Any, feature_by_id: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], ) -> dict[str, Any] | None: """Bind a direct PipeShell cap by profile and path-end source anchors. The builder's ``FirstShape``/``LastShape`` facts identify the result. A CADFS CAP query additionally carries the profile edge and path endpoint; require that exact pair so ``isStart`` cannot select a cap by position alone. Only the narrow source contract emitted below for an initial, direct, independent sweep is accepted. """ try: _call, owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None if topology != "CAP_FACE" or kind != "face": return None query = parse_query(value) if query.is_start is None: return None producer = feature_by_id.get(f"f_{owner}") params = (producer or {}).get("params") or {} contract = params.get("cap_output_contract") if ( producer is None or producer.get("atomic_id") != "sweep_add" or params.get("result_mode") != "new_body" or params.get("initial_output_roles") is not True or not isinstance(contract, dict) ): return None required = ("profile_source", "profile_entity", "path_source", "path_entity", "path_reversed") if ( any(not isinstance(contract.get(name), str) or not contract[name] for name in required[:-1]) or not isinstance(contract.get("path_reversed"), bool) ): return None sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) or {} if sketch.get("source_sketch_id") != contract["profile_source"]: return None refs = _source_refs(definition.get("disambiguationData")) profile_ref = (contract["profile_source"], contract["profile_entity"]) path_prefix = f"{contract['path_entity']}." path_refs = [ref for ref in refs if ref[0] == contract["path_source"] and ref[1].startswith(path_prefix)] if len(refs) != 2 or profile_ref not in refs or len(path_refs) != 1: return None suffix = path_refs[0][1][len(path_prefix):] if suffix not in {"start", "end"}: return None expected_endpoint = "start" if query.is_start else "end" # ``isStart`` is a source statement about the path endpoint. If lowering # reversed the path to attach the profile, invert it before choosing the # physical PipeShell output role. if suffix != expected_endpoint: return None role_endpoint = suffix if not contract["path_reversed"] else ("end" if suffix == "start" else "start") role = f"sweep.{role_endpoint}" return { "kind": "face", "owner_feature_id": f"f_{owner}", "output_role": role, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": _selector_intent( value, query_family="CAP_FACE", kind="face", evidence="operation_role", allowed=("boundary",), output_role=role, disambiguation={ "type": "sweep_profile_path_endpoint", **{name: contract[name] for name in required}, "path_endpoint": suffix, }, ), } def _initial_direct_sweep_cap_edge_selector( value: Any, feature_by_id: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], previous: list[str], ) -> dict[str, Any] | None: """Bind the one-edge boundary of an immediate direct PipeShell cap. PipeShell has no per-edge history callback. This contract consequently admits only the same direct source pair as the cap-face bridge and only a profile with one retained direct source edge. The adapter independently proves that the role face has one exact final boundary edge. """ try: _call, owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None if topology != "CAP_EDGE" or kind not in {"edge", "entitytype.edge"}: return None query = parse_query(value) if query.is_start is None: return None producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) params = (producer or {}).get("params") or {} contract = params.get("cap_output_contract") if ( producer is None or previous[-1:] != [producer_id] or producer.get("atomic_id") != "sweep_add" or params.get("result_mode") != "new_body" or params.get("initial_output_roles") is not True or not isinstance(contract, dict) ): return None required = ("profile_source", "profile_entity", "path_source", "path_entity", "path_reversed") if ( any(not isinstance(contract.get(name), str) or not contract[name] for name in required[:-1]) or not isinstance(contract.get("path_reversed"), bool) ): return None sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) or {} profile = sketch.get("profile") or {} if ( sketch.get("source_sketch_id") != contract["profile_source"] or profile.get("type") != "circle" or profile.get("source_entity_id") != contract["profile_entity"] ): return None refs = _source_refs(definition.get("disambiguationData")) profile_ref = (contract["profile_source"], contract["profile_entity"]) path_prefix = f"{contract['path_entity']}." path_refs = [ref for ref in refs if ref[0] == contract["path_source"] and ref[1].startswith(path_prefix)] if len(refs) != 2 or profile_ref not in refs or len(path_refs) != 1: return None suffix = path_refs[0][1][len(path_prefix):] expected_endpoint = "start" if query.is_start else "end" if suffix != expected_endpoint: return None role_endpoint = suffix if not contract["path_reversed"] else ("end" if suffix == "start" else "start") role = f"sweep.{role_endpoint}" intent = _selector_intent( value, query_family="CAP_EDGE", kind="edge", evidence="kernel_history", allowed=("boundary",), disambiguation={ "type": "sweep_profile_path_endpoint", **{name: contract[name] for name in required}, "path_endpoint": suffix, }, ) # ``parse_query`` retains the endpoint as its convenience primary source # for this two-anchor OSD. CAP_EDGE lineage is anchored on the profile # edge; the path endpoint remains explicit disambiguation evidence. intent["source_entity"] = { "sketch_id": contract["profile_source"], "entity_id": contract["profile_entity"], } intent["lineage_role"] = role return { "kind": "edge", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } def _initial_direct_sweep_swept_face_selector( value: Any, feature_by_id: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], previous: list[str], ) -> dict[str, Any] | None: """Bind one direct PipeShell side face through `Generated(profile_edge)`. The complete direct profile/path source contract remains mandatory. The consumer receives only its selected profile edge as its runtime anchor: the path edge is retained as source disambiguation, not turned into geometry. Listing every source edge in the contract prevents a partial or solver-rebuilt profile from looking like a direct builder history. """ try: _call, owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None if topology != "SWEPT_FACE" or kind not in {"face", "entitytype.face"}: return None producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) params = (producer or {}).get("params") or {} contract = params.get("swept_face_contract") if ( producer is None or previous[-1:] != [producer_id] or producer.get("atomic_id") != "sweep_add" or params.get("result_mode") != "new_body" or params.get("initial_output_roles") is not True or not isinstance(contract, dict) ): return None required = ("profile_source", "profile_entities", "path_source", "path_entity", "path_reversed") if ( any(not isinstance(contract.get(name), str) or not contract[name] for name in ("profile_source", "path_source", "path_entity")) or not isinstance(contract.get("profile_entities"), list) or not contract["profile_entities"] or any(not isinstance(entity, str) or not entity for entity in contract["profile_entities"]) or len(set(contract["profile_entities"])) != len(contract["profile_entities"]) or not isinstance(contract.get("path_reversed"), bool) ): return None sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) or {} profile_entities = _direct_sweep_profile_entities(sketch) if sketch.get("source_sketch_id") != contract["profile_source"] or profile_entities != contract["profile_entities"]: return None refs = _source_refs(definition.get("disambiguationData")) path_ref = (contract["path_source"], contract["path_entity"]) profile_refs = [ref for ref in refs if ref[0] == contract["profile_source"] and ref[1] in profile_entities] if len(refs) != 2 or len(profile_refs) != 1 or path_ref not in refs: return None selected_profile_entity = profile_refs[0][1] intent = _selector_intent( value, query_family="SWEPT_FACE", kind="face", evidence="kernel_history", allowed=("boundary",), disambiguation={ "type": "sweep_profile_path", **{name: contract[name] for name in required}, }, ) intent["source_entity"] = { "sketch_id": contract["profile_source"], "entity_id": selected_profile_entity, } return { "kind": "face", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } def _direct_sweep_profile_entities(sketch: dict[str, Any]) -> list[str] | None: """Return one unchanged direct closed profile's complete source edge set. PipeShell can report `Generated(edge)` for every profile edge, but only if CDSL retains the full original contour. This deliberately rejects holes, regions and any unlabeled/split edge rather than guessing a correspondence. """ profile = sketch.get("profile") or {} if profile.get("type") == "circle": entity = profile.get("source_entity_id") return [entity] if isinstance(entity, str) and entity else None contours = profile.get("contours") if profile.get("type") == "analytic_contours" else None if not isinstance(contours, list) or len(contours) != 1: return None contour = contours[0] or {} segments = contour.get("segments") if isinstance(contour, dict) else None if not contour.get("closed") or not isinstance(segments, list) or not segments: return None entities = [segment.get("source_entity_id") for segment in segments if isinstance(segment, dict)] if len(entities) != len(segments) or any(not isinstance(entity, str) or not entity for entity in entities): return None return entities if len(set(entities)) == len(entities) else None def _direct_sweep_profile_vertex_entity_pairs(sketch: dict[str, Any]) -> set[tuple[str, str]]: """Return exact adjacent source-edge pairs for a direct closed contour.""" entities = _direct_sweep_profile_entities(sketch) if entities is None or len(entities) < 2: return set() return { tuple(sorted((entities[index], entities[(index + 1) % len(entities)]))) for index in range(len(entities)) } def _initial_direct_sweep_swept_edge_selector( value: Any, feature_by_id: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], previous: list[str], ) -> dict[str, Any] | None: """Bind a PipeShell swept edge through `Generated(profile_vertex)`. CADFS identifies the source vertex by exactly two incident profile edges and separately supplies the direct path edge. The path remains semantic disambiguation: the runtime anchor is the exact profile vertex only. """ try: _call, owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None if topology != "SWEPT_EDGE" or kind not in {"edge", "entitytype.edge"}: return None producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) params = (producer or {}).get("params") or {} contract = params.get("swept_edge_contract") if ( producer is None or previous[-1:] != [producer_id] or producer.get("atomic_id") != "sweep_add" or params.get("result_mode") != "new_body" or params.get("initial_output_roles") is not True or not isinstance(contract, dict) ): return None required = ("profile_source", "profile_entities", "path_source", "path_entity", "path_reversed") if ( any(not isinstance(contract.get(name), str) or not contract[name] for name in ("profile_source", "path_source", "path_entity")) or contract["profile_source"] == contract["path_source"] or not isinstance(contract.get("profile_entities"), list) or len(contract["profile_entities"]) < 2 or any(not isinstance(entity, str) or not entity for entity in contract["profile_entities"]) or len(set(contract["profile_entities"])) != len(contract["profile_entities"]) or not isinstance(contract.get("path_reversed"), bool) ): return None sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) or {} profile_entities = _direct_sweep_profile_entities(sketch) if sketch.get("source_sketch_id") != contract["profile_source"] or profile_entities != contract["profile_entities"]: return None refs = _source_refs(definition.get("disambiguationData")) path_ref = (contract["path_source"], contract["path_entity"]) profile_refs = [ref for ref in refs if ref[0] == contract["profile_source"] and ref[1] in profile_entities] vertex_entities = tuple(sorted(ref[1] for ref in profile_refs)) if ( len(refs) != 3 or len(set(refs)) != 3 or len(profile_refs) != 2 or len(set(vertex_entities)) != 2 or path_ref not in refs or vertex_entities not in _direct_sweep_profile_vertex_entity_pairs(sketch) ): return None intent = _selector_intent( value, query_family="SWEPT_EDGE", kind="edge", evidence="kernel_history", allowed=("boundary",), disambiguation={ "type": "sweep_profile_vertex_path", **{name: contract[name] for name in required}, "profile_vertex_entities": list(vertex_entities), }, ) intent.pop("source_entity", None) intent["source_entities"] = [ {"sketch_id": contract["profile_source"], "entity_id": entity_id} for entity_id in vertex_entities ] return { "kind": "edge", "owner_feature_id": producer_id, "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": intent, } def _profile_query_union_leaves(value: Any) -> list[Any]: """Flatten only associative query unions used to select one profile. FeatureScript histories can wrap a qUnion in a second qUnion when a local alias is later assigned to ``entities``. The wrapper changes neither the selected topology nor the source provenance. Keeping this normalization local to CAP_EDGE profile recognition avoids changing generic selector parsing, where query grouping may still be diagnostically meaningful. """ if isinstance(value, Call) and value.name == "qUnion" and value.args and isinstance(value.args[0], list): return [leaf for item in value.args[0] for leaf in _profile_query_union_leaves(item)] return [value] def _cap_edge_hole_profile_sketch( value: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_frames: dict[str, dict[str, Any]], feature_id: str, ) -> tuple[dict[str, Any], dict[str, Any]] | None: """Materialize an outer IMPRINT profile with one CAP_EDGE-derived inner wire. The inner curve may already be a draft/offset B-rep edge, so it must be taken from its producer's selected cap face at runtime rather than guessed from the original sketch entity. This intentionally accepts one outer closed region and one uniquely framed cap edge only. """ roots = _profile_query_union_leaves(value) if len(roots) != 2: return None outer_value = next((item for item in roots if parse_query(item).topology_type == "IMPRINT"), None) inner_value = next((item for item in roots if parse_query(item).topology_type == "CAP_EDGE"), None) if outer_value is None or inner_value is None: return None # 同向的 TD region query 选择的是外轮廓两侧相邻的两个 IMPRINT 区域。 # 将二者并集解释成 "外轮廓减 CAP_EDGE" 会错误挖去中心,留下一个仅沿 # 边接触前序实体的薄环。只有两条边的 region side 相反时,才有明确的 # 外环 + 内孔语义可以交给受限的 CAP_EDGE profile atomic。 outer_side = _profile_selection_side(outer_value) inner_side = _profile_selection_side(inner_value) if outer_side is not None and inner_side is not None and outer_side == inner_side: return None outer = parse_query(outer_value) if not outer.source_sketch or outer.source_sketch not in sketch_by_source: return None outer_sketch = _profile_selection_sketch( sketch_by_source[outer.source_sketch], outer_value, entity_by_sketch[outer.source_sketch], feature_id, ) profile = outer_sketch.get("profile") or {} contours = profile.get("contours") if profile.get("type") == "analytic_contours" else None if not ( profile.get("type") == "circle" or isinstance(contours, list) and len(contours) == 1 and bool(contours[0].get("closed")) ): return None selector = _cap_face_selector(inner_value, feature_frames, feature_id, "profile_hole") return (outer_sketch, selector) if selector is not None else None def _cap_edge_union_profile_sketch( value: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_id: str, ) -> dict[str, Any] | None: """Materialize the complete outer region selected around one CAP_EDGE.""" roots = _profile_query_union_leaves(value) if len(roots) != 2: return None outer_value = next((item for item in roots if parse_query(item).topology_type == "IMPRINT"), None) inner_value = next((item for item in roots if parse_query(item).topology_type == "CAP_EDGE"), None) if outer_value is None or inner_value is None: return None outer_side = _profile_selection_side(outer_value) inner_side = _profile_selection_side(inner_value) if outer_side is None or inner_side is None or outer_side != inner_side: return None outer = parse_query(outer_value) if not outer.source_sketch or outer.source_sketch not in sketch_by_source: return None return _profile_selection_sketch( sketch_by_source[outer.source_sketch], outer_value, entity_by_sketch[outer.source_sketch], feature_id, ) def _loft_cap_face_profile( params: dict[str, Any], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_frames: dict[str, dict[str, Any]], feature_id: str, ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]] | None: """Resolve one CAP_FACE outer wire followed by one sketch-imprint loft section.""" profiles = params.get("sheetProfilesArray") if not isinstance(profiles, list) or len(profiles) != 2: return None values = [item.get("sheetProfileEntities") if isinstance(item, dict) else item for item in profiles] cap, sketch_query = (parse_query(value) for value in values) if cap.topology_type != "CAP_FACE" or sketch_query.topology_type != "IMPRINT": return None if not sketch_query.source_sketch or sketch_query.source_sketch not in sketch_by_source: return None selector = _cap_face_selector(values[0], feature_frames, feature_id, "loft_cap") frame = feature_frames.get(cap.owner_feature or "") cap_frame = frame and frame.get("start" if cap.is_start else "end") if selector is None or cap_frame is None: return None sketch = _profile_selection_sketch( sketch_by_source[sketch_query.source_sketch], values[1], entity_by_sketch[sketch_query.source_sketch], feature_id, ) return sketch, selector, dict(cap_frame) def _profile_selection_sketch( sketch: dict[str, Any], query_value: Any, entities: dict[str, dict[str, Any]], feature_id: str, ) -> dict[str, Any]: """Materialize a uniquely selected sketch region for an IMPRINT query.""" query = parse_query(query_value) if query.topology_type != "IMPRINT" or not query.source_entity: return sketch contours = (sketch.get("profile") or {}).get("contours") or [] query_values = _queries(query_value) selections = [parse_query(item) for item in query_values] union_profile = _circle_imprint_union_profile(contours, query_values, entities) if union_profile is not None: output = deepcopy(sketch) output["id"] = f"{sketch['id']}__{feature_id}" output["name"] = f"{sketch['name']}__{feature_id}" output["profile"] = union_profile return output def line_loop(contour: dict[str, Any]) -> list[list[float]] | None: segments = contour.get("segments") or [] if not isinstance(segments, list) or len(segments) < 3 or not all( isinstance(segment, dict) and segment.get("type") == "line" and isinstance(segment.get("start"), list) and isinstance(segment.get("end"), list) for segment in segments ): return None points = [list(segment["start"]) for segment in segments] if any(math.dist(segment["end"], segments[(index + 1) % len(segments)]["start"]) > 1e-6 for index, segment in enumerate(segments)): return None return points def strictly_contains(outer: list[list[float]], inner: list[list[float]]) -> bool: """Return whether a closed line loop contains every inner vertex. This is source-profile construction, not a selector fallback: both loops originate from the exact FeatureScript sketch entities. Boundary contact remains rejected because it has split/region semantics that cannot be represented by a single direct analytic profile. """ def inside(point: list[float]) -> bool: crossings = 0 for index, start in enumerate(outer): end = outer[(index + 1) % len(outer)] cross = (end[0] - start[0]) * (point[1] - start[1]) - (end[1] - start[1]) * (point[0] - start[0]) if abs(cross) <= 1e-8 and min(start[0], end[0]) - 1e-8 <= point[0] <= max(start[0], end[0]) + 1e-8 and min(start[1], end[1]) - 1e-8 <= point[1] <= max(start[1], end[1]) + 1e-8: return False if (start[1] > point[1]) != (end[1] > point[1]): x = start[0] + (point[1] - start[1]) * (end[0] - start[0]) / (end[1] - start[1]) if abs(x - point[0]) <= 1e-8: return False if x > point[0]: crossings += 1 return bool(crossings % 2) return bool(inner) and all(inside(point) for point in inner) # A single IMPRINT query over one outer direct line loop denotes the # source-side region, not merely that loop's curve. If every other source # line loop lies strictly inside it, preserve those nested boundaries so # the derived profile retains its holes and their CAP-edge provenance. # Disjoint, touching, curved, split and multiple-outer arrangements keep # the existing explicit-region paths below. if len(selections) == 1: source_id = max((key for key in entities if selections[0].source_entity and selections[0].source_entity.startswith(key)), key=len, default="") side = _profile_selection_side(query_values[0]) if query_values else None if source_id and side is not None: matches = [ (contour, next((_matching_profile_segment(segment, entities[source_id]) for segment in contour.get("segments") or [] if _matching_profile_segment(segment, entities[source_id])), 0)) for contour in contours if any(_matching_profile_segment(segment, entities[source_id]) for segment in contour.get("segments") or []) ] outer_loop = line_loop(matches[0][0]) if len(matches) == 1 and matches[0][1] == (1 if side > 0 else -1) else None other_loops = [line_loop(contour) for contour in contours if not matches or contour is not matches[0][0]] if outer_loop is not None and other_loops and all(loop is not None and strictly_contains(outer_loop, loop) for loop in other_loops): output = deepcopy(sketch) output["id"] = f"{sketch['id']}__{feature_id}" output["name"] = f"{sketch['name']}__{feature_id}" # Keep the source profile roles/segments intact. The solver # performs the established containment-parity classification. return output # qUnion 可以从同一草图选择多个互不相同的 IMPRINT region。此前只取 # parse_query(qUnion(...)) 最后看到的一个 source entity,且在没有正反 # orientation 对时退回整张草图,导致未选圆也被错误拉伸。仅当每个 source # entity 唯一对应一条 contour 时,直接物化该明确选择集;同源的双侧 region # 继续交给下方的 orientation 逻辑,避免把环形区域猜成圆盘。 source_ids = [ max((key for key in entities if selection.source_entity and selection.source_entity.startswith(key)), key=len, default="") for selection in selections ] # 单一同心圆边的 IMPRINT 查询并不总是选择圆盘。它表达的是该边 # 一侧的 bounded region;例如 `00111611` F7 选 E6 的正侧时,真实 # profile 是 E7/E6 环域而不是 E6 圆盘。 if len(selections) == 1 and source_ids and source_ids[0]: source = entities[source_ids[0]] profile = _circle_imprint_region(contours, source, _profile_selection_side(_queries(query_value)[0])) if profile is not None: output = deepcopy(sketch) output["id"] = f"{sketch['id']}__{feature_id}" output["name"] = f"{sketch['name']}__{feature_id}" output["profile"] = profile return output if ( len(source_ids) == len(set(source_ids)) and all(source_ids) and all(selection.topology_type == "IMPRINT" and selection.source_entity for selection in selections) ): selected: list[dict[str, Any]] = [] for selected_id in source_ids: source = entities[selected_id] matches = [ contour for contour in contours if any(_matching_profile_segment(segment, source) for segment in contour.get("segments") or []) ] if len(matches) != 1: break selected.append(matches[0]) else: output = deepcopy(sketch) output["id"] = f"{sketch['id']}__{feature_id}" output["name"] = f"{sketch['name']}__{feature_id}" output["profile"] = {**output["profile"], "contours": selected} return output source_id = max((key for key in entities if query.source_entity.startswith(key)), key=len, default="") source = entities.get(source_id) if source is None or len(contours) < 2: return sketch selection_sides = [_profile_selection_side(item) for item in _queries(query_value)] if source.get("type") == "circle" and all(side is not None for side in selection_sides): # 两个相反 orientation 的同一圆形 IMPRINT region 表示其两侧区域的 # 并集。对于同心圆草图,CADFS 用这套写法选择内圆盘和相邻圆环;将它 # 们规整为最小包围圆盘,不能按普通 nested-circle profile 误作圆环。 circles = [ segment for contour in contours for segment in contour.get("segments") or [] if segment.get("type") == "circle" and _same_point(segment.get("center") or [], source.get("center") or []) ] radii = sorted({float(segment["radius_mm"]) for segment in circles}) if len(radii) >= 2 and any(side < 0 for side in selection_sides) and any(side > 0 for side in selection_sides): outer = next((radius for radius in radii if radius > float(source["radius_mm"]) + 1e-6), None) if outer is not None: output = deepcopy(sketch) output["id"] = f"{sketch['id']}__{feature_id}" output["name"] = f"{sketch['name']}__{feature_id}" output["profile"] = {"type": "circle", "center": list(source["center"]), "radius_mm": outer} return output matches: list[tuple[dict[str, Any], int]] = [] for contour in contours: direction = next((_matching_profile_segment(segment, source) for segment in contour.get("segments") or [] if _matching_profile_segment(segment, source)), 0) if direction: matches.append((contour, direction)) side = _profile_selection_side(query_value) if side is None or len(matches) < 2: return sketch selected = [contour for contour, direction in matches if direction == (1 if side > 0 else -1)] if len(selected) != 1: raise UnsupportedCapability("sketch_region_selection", "IMPRINT profile selection does not resolve one sketch region") output = deepcopy(sketch) output["id"] = f"{sketch['id']}__{feature_id}" output["name"] = f"{sketch['name']}__{feature_id}" output["profile"] = {**output["profile"], "contours": [selected[0]]} return output def _surface_profile_selection_sketch( sketch: dict[str, Any], query_value: Any, entities: dict[str, dict[str, Any]], feature_id: str, *, allow_open_wire: bool = False, ) -> dict[str, Any]: """Materialize explicitly selected source wires from ``surfaceEntities``. Mixed solid/surface extrusion continues to accept one or more circular wires. A pure ``ToolBodyType.SURFACE`` extrusion may additionally carry one or more original lines. Each connected, non-branching open chain becomes one shell wire; it never becomes a solid profile or active-body member. """ references = _source_refs(query_value) selected: list[tuple[str, dict[str, Any]]] = [] seen = set() duplicate_source = False for source, entity_id in references: if source != sketch["name"]: raise UnsupportedCapability( "extrude_surface_profile", "surface extrude source wires must come from one source sketch", ) if entity_id in seen: duplicate_source = True continue entity = entities.get(entity_id) if entity is None or entity.get("construction"): raise UnsupportedCapability( "extrude_surface_profile", "current CDSL surface extrude requires one explicit original source wire", ) selected.append((entity_id, entity)); seen.add(entity_id) if not selected: raise ValueError("surface extrude profile query is unresolved") output = deepcopy(sketch) output["id"] = f"{sketch['id']}__{feature_id}_surface" output["name"] = f"{sketch['name']}__{feature_id}_surface" if allow_open_wire and all(entity.get("type") == "line" for _entity_id, entity in selected): if duplicate_source: raise UnsupportedCapability( "extrude_surface_profile", "pure ToolBodyType.SURFACE open-wire extrusion requires distinct source lines", ) records: list[tuple[str, dict[str, Any], list[float], list[float]]] = [] for entity_id, entity in selected: start, end = entity.get("start"), entity.get("end") if ( not isinstance(start, list) or not isinstance(end, list) or len(start) != 2 or len(end) != 2 or not all(isinstance(value, (int, float)) and math.isfinite(float(value)) for value in [*start, *end]) or _same_point(start, end) ): raise UnsupportedCapability( "extrude_surface_profile", "pure ToolBodyType.SURFACE open-wire extrusion requires finite non-degenerate source lines", ) records.append((entity_id, entity, list(start), list(end))) # A query set may deliberately contain disconnected wires, but each # component must itself have one unambiguous pair of terminal points. # Query encounter order is only a tie-breaker for a proven chain's # direction; it is never used to join or choose nearby geometry. remaining_components = set(range(len(records))) contours: list[dict[str, Any]] = [] while remaining_components: seed = min(remaining_components) component = {seed} frontier = [seed] while frontier: index = frontier.pop() _entity_id, _entity, start, end = records[index] for candidate in remaining_components - component: _candidate_id, _candidate, candidate_start, candidate_end = records[candidate] if any( _same_point(point, candidate_point) for point in (start, end) for candidate_point in (candidate_start, candidate_end) ): component.add(candidate) frontier.append(candidate) remaining_components -= component def incident_count(point: list[float]) -> int: return sum( int(_same_point(point, start)) + int(_same_point(point, end)) for index in component for _entity_id, _entity, start, end in [records[index]] ) terminals = [ endpoint for index in sorted(component) for endpoint in records[index][2:] if incident_count(endpoint) == 1 ] if len(terminals) != 2: raise UnsupportedCapability( "extrude_surface_profile", "pure ToolBodyType.SURFACE source lines must form non-branching open chains", ) current = terminals[0] component_remaining = set(component) segments: list[dict[str, Any]] = [] while component_remaining: matches = [ index for index in component_remaining if _same_point(current, records[index][2]) or _same_point(current, records[index][3]) ] if len(matches) != 1: raise UnsupportedCapability( "extrude_surface_profile", "pure ToolBodyType.SURFACE source lines must form non-branching open chains", ) index = matches[0] entity_id, entity, start, end = records[index] if _same_point(current, start): segment = deepcopy(entity) current = end else: segment = _reversed_sweep_path_segment(entity) current = start segment.setdefault("source_entity_id", entity_id) segments.append(segment) component_remaining.remove(index) if incident_count(current) != 1: raise UnsupportedCapability( "extrude_surface_profile", "pure ToolBodyType.SURFACE source lines must form non-branching open chains", ) contours.append({ "role": "open", "closed": False, "surface_wire": True, "segments": segments, }) output["profile"] = { "type": "analytic_contours", "contours": contours, } return output if any(entity.get("type") != "circle" for _entity_id, entity in selected): raise UnsupportedCapability( "extrude_surface_profile", "current CDSL mixed surface extrude requires explicitly selected circular wires", ) output["profile"] = { "type": "analytic_contours", "contours": [ {"role": "unknown", "closed": True, "segments": [deepcopy(entity)]} for _entity_id, entity in selected ], } return output def _pattern_source_features(value: Any, previous: list[str]) -> list[str]: sources = [] for call in walk_calls(value): if call.name != "makeQuery" or not call.args: continue owner = symbolic_string(call.args[0]) if "F" not in owner: continue source = "f_" + owner[owner.find("F"):].split(".", 1)[0] if source in previous and source not in sources: sources.append(source) if not sources: raise ValueError("pattern source features are unresolved") return sources def _pattern_body_history_sources( value: Any, sources: list[str], previous: list[str], feature_by_id: dict[str, dict[str, Any]], body_aliases: dict[str, str] | None = None, ) -> list[str]: """Resolve a selected SWEPT_BODY to its exact current member when known. CADFS body queries name the feature that originally created the body. A following ADD sweep can already have become part of that same body before `circularPattern` copies it. When the restricted one-body successor state proves that current member, use it directly: the runtime can transform its actual B-rep and retain a concrete COPY body member. Replaying a creator plus its additive history would make each replay fragment look like a separate source, which loses the instance ownership needed by a later COPY(BODY) boolean/transform. Without that one-to-one proof, retain the older bounded replay expansion. It can preserve geometry for patterns with fused history, but deliberately does not claim individual COPY body ownership. """ body_aliases = body_aliases or {} swept_body_sources = { f"f_{query.owner_feature}" for item in _queries(value) for query in [parse_query(item)] if query.topology_type == "SWEPT_BODY" and query.kind in {"body", "entitytype.body"} and query.owner_feature } has_swept_body = bool(swept_body_sources) if not has_swept_body: return sources resolved_sources = { source: _resolved_body_alias(source, body_aliases) for source in swept_body_sources } if any(resolved != source for source, resolved in resolved_sources.items()): return list(dict.fromkeys( resolved_sources.get(source, source) for source in sources )) replayable_adds = { "extrude_add_blind", "extrude_add_two_sided", "loft_add", "loft_add_with_cap_face", "sweep_add", "revolve_add", "sphere_add", "box_add", "cylinder_add", } output = list(sources) source_indexes = [previous.index(source) for source in sources if source in previous] if not source_indexes: return output for feature_id in previous[max(source_indexes) + 1:]: feature = feature_by_id.get(feature_id) or {} if feature.get("atomic_id") not in replayable_adds: continue if (feature.get("params") or {}).get("result_mode") == "new_body": continue output.append(feature_id) return list(dict.fromkeys(output)) def _sweep_cap_frames(profile: dict[str, Any], path: dict[str, Any]) -> dict[str, dict[str, Any]] | None: """Record physical CAP_FACE frames for one direct open sweep path.""" segment = path.get("segment") or {} path_plane = path.get("workplane") or {} profile_plane = profile.get("workplane") or {} # A cross-sketch spatial path deliberately has no single planar frame. # Its source capture can execute a sweep, but it has not established the # planar cap-frame contract used by downstream CAP_FACE lowering. if not isinstance(path_plane, dict) or not path_plane: return None points = segment.get("points") if segment.get("type") == "bspline" else [segment.get("start"), segment.get("end")] if not isinstance(points, list) or len(points) < 2 or any(not isinstance(point, list) or len(point) != 2 for point in points): return None y_dir = _y_dir(path_plane) def point(value: list[float]) -> list[float]: return [ path_plane["origin_mm"][index] + path_plane["x_dir"][index] * value[0] + y_dir[index] * value[1] for index in range(3) ] def direction(value: Any, fallback: list[float]) -> list[float]: if isinstance(value, list) and len(value) == 2: return _unit([ path_plane["x_dir"][index] * value[0] + y_dir[index] * value[1] for index in range(3) ], "sweep cap tangent is degenerate") return _unit(fallback, "sweep path is degenerate") start, end = point(points[0]), point(points[-1]) if segment.get("type") == "arc": center = segment.get("center") if not isinstance(center, list) or len(center) != 2: return None start_radial = [float(points[0][0]) - float(center[0]), float(points[0][1]) - float(center[1])] end_radial = [float(points[-1][0]) - float(center[0]), float(points[-1][1]) - float(center[1])] if bool(segment.get("clockwise", False)): start_tangent, end_tangent = [start_radial[1], -start_radial[0]], [end_radial[1], -end_radial[0]] else: start_tangent, end_tangent = [-start_radial[1], start_radial[0]], [-end_radial[1], end_radial[0]] start_direction = direction(start_tangent, _sub(end, start)) end_direction = direction(end_tangent, _sub(end, start)) else: start_direction = direction(segment.get("start_tangent"), _sub(point(points[1]), start)) end_direction = direction(segment.get("end_tangent"), _sub(end, point(points[-2]))) x_dir = list(profile_plane["x_dir"]) return { # OCC keeps the source profile as the start cap of a solid sweep. Its # plane follows the profile workplane, not the tangent of a curved # spine. The profile centre may lie at the path point while the plane # origin is elsewhere, so retain the actual profile plane frame. "start": _frame(list(profile_plane["origin_mm"]), x_dir, list(profile_plane["normal"])), "end": _frame(end, x_dir, end_direction), "profile": dict(profile_plane), } def _reversed_sweep_path_segment(segment: dict[str, Any]) -> dict[str, Any]: """Reverse one CADFS line/arc/B-spline sweep path without changing its curve.""" output = deepcopy(segment) if output.get("type") in {"line", "arc"}: output["start"], output["end"] = segment["end"], segment["start"] if output.get("type") == "arc": output["clockwise"] = not bool(segment.get("clockwise", False)) if output.get("type") == "bspline": output["points"] = list(reversed(segment.get("points") or [])) if "start" in segment and "end" in segment: output["start"], output["end"] = segment["end"], segment["start"] parameters = segment.get("parameters") if isinstance(parameters, list) and parameters: start_parameter, end_parameter = float(parameters[0]), float(parameters[-1]) output["parameters"] = [start_parameter + end_parameter - float(value) for value in reversed(parameters)] start_tangent, end_tangent = segment.get("start_tangent"), segment.get("end_tangent") if isinstance(start_tangent, list) and isinstance(end_tangent, list): output["start_tangent"] = [-float(value) for value in end_tangent] output["end_tangent"] = [-float(value) for value in start_tangent] return output def _reversed_spatial_sweep_path_segment(segment: dict[str, Any]) -> dict[str, Any]: """Reverse one globally captured source curve without changing its locus.""" output = deepcopy(segment) if output.get("type") in {"line", "arc"}: output["start_mm"], output["end_mm"] = segment["end_mm"], segment["start_mm"] if output.get("type") == "arc": output["clockwise"] = not bool(segment.get("clockwise", False)) if output.get("type") == "bspline": output["points_mm"] = list(reversed(segment.get("points_mm") or [])) parameters = segment.get("parameters") if isinstance(parameters, list) and parameters: start_parameter, end_parameter = float(parameters[0]), float(parameters[-1]) output["parameters"] = [start_parameter + end_parameter - float(value) for value in reversed(parameters)] start_tangent, end_tangent = segment.get("start_tangent_mm"), segment.get("end_tangent_mm") if isinstance(start_tangent, list) and isinstance(end_tangent, list): output["start_tangent_mm"] = [-float(value) for value in end_tangent] output["end_tangent_mm"] = [-float(value) for value in start_tangent] return output def _sweep_path_endpoint(segment: dict[str, Any], endpoint: str) -> list[float] | None: if endpoint not in {"start", "end"}: return None if segment.get("type") == "bspline": points = segment.get("points") value = points[0 if endpoint == "start" else -1] if isinstance(points, list) and points else None else: value = segment.get(endpoint) if ( not isinstance(value, list) or len(value) != 2 or not all(isinstance(component, (int, float)) and math.isfinite(float(component)) for component in value) ): return None return [float(value[0]), float(value[1])] def _same_sweep_path_point(left: list[float], right: list[float]) -> bool: return math.dist(left, right) <= 1e-8 def _reversed_sweep_path(path: dict[str, Any]) -> dict[str, Any]: output = deepcopy(path) segments = output.get("segments") if isinstance(segments, list): reverse_segment = ( _reversed_spatial_sweep_path_segment if "workplane" not in output else _reversed_sweep_path_segment ) output["segments"] = [reverse_segment(segment) for segment in reversed(segments)] else: segment = output.get("segment") if not isinstance(segment, dict): raise ValueError("sweep path has no reversible segment") output["segment"] = _reversed_sweep_path_segment(segment) return output def _sweep_profile_attaches_at_path_end(profile: dict[str, Any], path: dict[str, Any]) -> bool: """Detect a circle profile placed at the terminal point of a CADFS path.""" profile_plane = profile.get("workplane") or {} profile_shape = profile.get("profile") or {} segments = path.get("segments") if isinstance(segments, list): sequence = segments else: segment = path.get("segment") sequence = [segment] if isinstance(segment, dict) else [] if profile_shape.get("type") != "circle" or not sequence: return False center = profile_shape.get("center") or [0.0, 0.0] if not isinstance(center, list) or len(center) != 2: return False try: profile_center = _global(profile_plane, center) except (KeyError, TypeError, ValueError): return False if "workplane" not in path: def spatial_endpoint(segment: dict[str, Any], endpoint: str) -> list[float] | None: if segment.get("type") == "bspline": points = segment.get("points_mm") value = points[0 if endpoint == "start" else -1] if isinstance(points, list) and points else None else: value = segment.get(f"{endpoint}_mm") if ( not isinstance(value, list) or len(value) != 3 or not all(isinstance(component, (int, float)) and math.isfinite(float(component)) for component in value) ): return None return [float(component) for component in value] start_point = spatial_endpoint(sequence[0], "start") end_point = spatial_endpoint(sequence[-1], "end") if start_point is None or end_point is None: return False else: workplane = path.get("workplane") or {} start, end = _sweep_path_endpoint(sequence[0], "start"), _sweep_path_endpoint(sequence[-1], "end") if not all(isinstance(value, list) and len(value) == 2 for value in (start, end)): return False try: start_point, end_point = _global(workplane, start), _global(workplane, end) except (KeyError, TypeError, ValueError): return False return math.dist(profile_center, end_point) <= 1e-5 and math.dist(profile_center, start_point) > 1e-5 def _pattern_copy_body(value: Any) -> tuple[str, str, int]: """Resolve one CADFS circular-pattern body copy without flattening it to a feature.""" _call, pattern_owner, topology, kind, definition = _direct_make_query(value) if topology != "COPY" or kind not in {"body", "entitytype.body"}: raise UnsupportedCapability("delete_bodies", "current CDSL deleteBodies only supports circular pattern body copies") derived = definition.get("derivedFrom") if derived is None: raise ValueError("pattern copy deletion has no derived body") _source, source_owner, source_topology, source_kind, _source_definition = _direct_make_query(derived) if source_topology != "SWEPT_BODY" or source_kind not in {"body", "entitytype.body"}: raise UnsupportedCapability("delete_bodies", "pattern copy deletion source is not a direct swept body") try: instance = int(str(definition.get("instanceName"))) except (TypeError, ValueError) as error: raise ValueError("pattern copy deletion instance is unresolved") from error return f"f_{pattern_owner}", f"f_{source_owner}", instance def _boolean_body_sources(value: Any) -> list[str]: """Resolve CADFS SWEPT_BODY query members to their producing features.""" sources: list[str] = [] for query_value in _queries(value): query = parse_query(query_value) if query.topology_type != "SWEPT_BODY" or query.kind not in {"body", "entitytype.body"}: raise ValueError("booleanBodies requires explicit SWEPT_BODY queries") if not query.owner_feature: raise ValueError("booleanBodies source feature is unresolved") source = f"f_{query.owner_feature}" if source not in sources: sources.append(source) if not sources: raise ValueError("booleanBodies has no selected bodies") return sources def _boolean_body_references( value: Any, previous: list[str], feature_by_id: dict[str, dict[str, Any]], body_aliases: dict[str, str] | None = None, ) -> tuple[list[str], list[dict[str, Any]], list[dict[str, str]]]: """Keep direct pattern COPY bodies explicit for boolean selection. A body selected from a mirror, circular pattern, or multi-source transform COPY is not its producer's aggregate result. Reuse the established transform-query provenance parser so each selected body stays qualified by its source member at runtime. """ sources, instance_refs, transform_copy_refs = _transform_body_references( value, previous, feature_by_id, body_aliases, ) return sources, instance_refs, transform_copy_refs def _ordered_boolean_body_references( value: Any, previous: list[str], feature_by_id: dict[str, dict[str, Any]], body_aliases: dict[str, str] | None = None, ) -> list[tuple[str, Any]]: """Resolve targetless body operands without losing FeatureScript order. The ordinary helper groups feature, pattern and transform-COPY references for the CDSL schema. That grouping is correct for an explicit target/tool pair but cannot select one qualified member from a targetless source set. Each source query must therefore resolve to exactly one qualified member here before the caller applies its explicit target-selection policy. """ ordered: list[tuple[str, Any]] = [] seen: set[tuple[Any, ...]] = set() for query_value in _queries(value): sources, instance_refs, transform_copy_refs = _boolean_body_references( query_value, previous, feature_by_id, body_aliases, ) candidates = ( [("feature", source) for source in sources] + [("pattern", reference) for reference in instance_refs] + [("transform_copy", reference) for reference in transform_copy_refs] ) if len(candidates) != 1: raise UnsupportedCapability( "boolean_bodies_targets", "targetless booleanBodies requires each source operand to resolve one explicit body member", ) kind, candidate = candidates[0] if kind == "feature": identity = (kind, candidate) elif kind == "pattern": identity = ( kind, candidate["pattern_feature_id"], candidate["source_feature_id"], candidate["instance_index"], ) else: identity = ( kind, candidate["transform_feature_id"], candidate["source_feature_id"], ) if identity not in seen: seen.add(identity) ordered.append((kind, candidate)) return ordered def _shell_target_body_source( value: Any, previous: list[str], body_aliases: dict[str, str], body_members: set[str], ) -> str: """Lower one CADFS shell ``parts`` query to its live body member. ``parts`` is not a hint to shell whichever aggregate currently contains the selected faces. It names the CADFS body that owns the shell operation. A direct SWEPT_BODY can follow only a lowering-time successor that already proves a one-to-one active member; patterns and fused aggregates deliberately never enter that alias map. """ queries = _queries(value) if len(queries) != 1: raise UnsupportedCapability( "shell_parts_body_source", "current CDSL shell.parts requires exactly one direct SWEPT_BODY", ) _call, owner, topology, kind, _definition = _direct_make_query(queries[0]) if topology != "SWEPT_BODY" or kind not in {"body", "entitytype.body"}: raise UnsupportedCapability( "shell_parts_body_source", "current CDSL shell.parts requires one direct SWEPT_BODY", ) source = f"f_{owner}" if source not in previous: raise ValueError("shell parts body source is unresolved") source = _resolved_body_alias(source, body_aliases) if source not in body_members: raise UnsupportedCapability( "shell_parts_body_source", "shell parts body no longer has one independently selectable member", ) return source def _hole_scope_body_source( value: Any, previous: list[str], body_aliases: dict[str, str], body_members: set[str], ) -> str: """Lower one CADFS ``hole.scope`` query to its sole live body member. Hole scope is an ownership constraint, not permission to cut the current aggregate. This first contract accepts one direct CADFS ``SWEPT_BODY`` only while the lowering-side body graph proves that it remains the one independently selectable active member. """ queries = _queries(value) if len(queries) != 1: raise UnsupportedCapability( "hole_scope_body_source", "current CDSL hole.scope requires exactly one direct SWEPT_BODY", ) _call, owner, topology, kind, _definition = _direct_make_query(queries[0]) if topology != "SWEPT_BODY" or kind not in {"body", "entitytype.body"}: raise UnsupportedCapability( "hole_scope_body_source", "current CDSL hole.scope requires one direct SWEPT_BODY", ) source = f"f_{owner}" if source not in previous: raise ValueError("hole scope body source is unresolved") source = _resolved_body_alias(source, body_aliases) if source not in body_members: raise UnsupportedCapability( "hole_scope_body_source", "hole scope body is no longer an independently selectable member", ) return source def _is_direct_hole_location_query(value: Any) -> bool: """Return whether ``value`` has Hole's direct original-vertex form. This is intentionally narrower than ``parse_query``'s recursive source extraction. A wrapper can contain an ``sQuery`` leaf, but that does not make it a direct Hole location or turn a missing sketch frame into its primary diagnostic. """ if not isinstance(value, Call) or value.name not in {"sQuery", "sketchEntityQuery"} or len(value.args) < 3: return False info = parse_query(value) return ( info.topology_type is None and info.kind in {"vertex", "entitytype.vertex"} and isinstance(info.source_sketch, str) and isinstance(info.source_entity, str) ) def _direct_hole_location( value: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> tuple[list[float], dict[str, Any]] | None: """Resolve one original sketch vertex accepted by FeatureScript hole. The Hole API exposes locations as sketch vertices. A circle centre, an explicitly computed original arc centre, and direct line/arc endpoints are source vertices too, but derived suffixes, topology output, query combinators, and geometry proximity do not prove a location. Return local plane coordinates because the hole executor uses its host frame to construct the final 3D point. """ if not _is_direct_hole_location_query(value): return None info = parse_query(value) sketch = sketch_by_source.get(info.source_sketch) entities = entity_by_sketch.get(info.source_sketch) or {} if sketch is None: return None token = info.source_entity entity_id = max((key for key in entities if token == key or token.startswith(key + ".")), key=len, default="") entity = entities.get(entity_id) if entity is None: return None suffix = "" if token == entity_id else token[len(entity_id) + 1:] point: list[float] | None = None if entity.get("type") == "point" and not suffix: point = entity.get("point") elif entity.get("type") == "circle" and suffix == "center": point = entity.get("center") elif entity.get("type") == "arc" and suffix == "center": # ``_arc`` derives this exact source datum from skArc's required # start/mid/end inputs. It remains source-sketch provenance, unlike # a center inferred from a runtime curve or a trimmed arc offspring. point = entity.get("center") elif entity.get("type") in {"line", "arc"} and suffix in {"start", "end"}: point = entity.get(suffix) if not isinstance(point, list) or len(point) != 2 or not all(isinstance(component, (int, float)) for component in point): return None return [float(point[0]), float(point[1]), 0.0], sketch["workplane"] def _direct_sketch_wire_selection( value: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], *, featurescript_version: str | None, standard_library: str | None, standard_library_version: str | None, permit_construction: bool = False, allowed_curve_types: frozenset[str] = frozenset({"line", "arc", "bspline"}), ) -> tuple[str, list[tuple[str, dict[str, Any]]], dict[str, Any]] | None: """Resolve the exact source entities named by one direct sketch-wire query. ``qBodyType(qCreatedBy(sketch, EDGE), WIRE)`` filters the source sketch's reference-wire entities; it does not select runtime body topology. The optional ``qConstructionFilter(..., NO)`` is evaluated against source construction metadata. Without it, any construction curve in the source result makes a sweep-path query ambiguous and remains deferred. The source-only datum-axis caller may explicitly retain construction curves, because FeatureScript permits a construction wire as a plane axis. A single-item qUnion is only the set identity here, never permission to flatten or pick among several source sketches. """ if ( featurescript_version != "1511" or standard_library != "onshape/std/geometry.fs" or standard_library_version != "1511.0" ): return None if ( not isinstance(value, Call) or value.name != "qUnion" or len(value.args) != 1 or not isinstance(value.args[0], list) or not value.args[0] ): return None operands = value.args[0] # A direct union of sketch edges is already an explicit source set. It # has different semantics from qCreatedBy(..., WIRE): do not flatten # nested queries or evaluate filters here. Each leaf must identify one # original source curve from exactly one sketch. if all( isinstance(operand, Call) and operand.name in {"sQuery", "sketchEntityQuery"} and len(operand.args) >= 3 for operand in operands ): infos = [parse_query(operand) for operand in operands] if any( info.topology_type is not None or info.kind not in {"edge", "entitytype.edge"} or not isinstance(info.source_sketch, str) or not isinstance(info.source_entity, str) for info in infos ): return None sources = {info.source_sketch for info in infos} if len(sources) != 1: return None source = next(iter(sources)) sketch = sketch_by_source.get(source) entities = entity_by_sketch.get(source) or {} entity_ids = [str(info.source_entity) for info in infos] if ( sketch is None or len(set(entity_ids)) != len(entity_ids) or any(entity_id not in entities for entity_id in entity_ids) ): return None candidates = [(entity_id, entities[entity_id]) for entity_id in entity_ids] if ( any(entity.get("construction") for _entity_id, entity in candidates) or any(entity.get("type") not in allowed_curve_types for _entity_id, entity in candidates) ): return None return source, candidates, sketch if len(operands) != 1: return None current = operands[0] has_construction_filter = isinstance(current, Call) and current.name == "qConstructionFilter" if has_construction_filter: if len(current.args) != 2 or symbolic_string(current.args[1]).rsplit(".", 1)[-1].upper() != "NO": return None current = current.args[0] if not isinstance(current, Call) or current.name != "qBodyType" or len(current.args) != 2: return None if symbolic_string(current.args[1]).rsplit(".", 1)[-1].upper() != "WIRE": return None current = current.args[0] if not isinstance(current, Call) or current.name != "qCreatedBy" or len(current.args) != 2: return None if symbolic_string(current.args[1]).rsplit(".", 1)[-1].upper() != "EDGE": return None source = parse_query(current).owner_feature sketch = sketch_by_source.get(source or "") entities = entity_by_sketch.get(source or "") or {} if sketch is None: return None # ``qCreatedBy(..., EDGE)`` cannot select direct sketch points. They remain # in the source-entity map for vertex consumers, but do not participate in # this source wire or its construction-filter semantics. all_candidates = [ (entity_id, entity) for entity_id, entity in entities.items() if entity.get("type") != "point" ] if not all_candidates: return None if ( not permit_construction and not has_construction_filter and any(entity.get("construction") for _entity_id, entity in all_candidates) ): return None candidates = [ (entity_id, entity) for entity_id, entity in all_candidates if not entity.get("construction") or (permit_construction and not has_construction_filter) ] if not candidates or any(entity.get("type") not in allowed_curve_types for _entity_id, entity in candidates): return None return str(source), candidates, sketch def _direct_closed_sketch_wire_profile( value: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], *, featurescript_version: str | None, standard_library: str | None, standard_library_version: str | None, feature_id: str, ) -> dict[str, Any] | None: """Materialize one source-only closed wire for an independent surface loft. This is deliberately narrower than general ``qBodyType`` evaluation: the exact source query must enumerate one sketch's non-construction wire curves, and those curves must form exactly one closed, non-branching contour. No runtime topology or geometric matching participates. """ if ( not isinstance(value, Call) or value.name != "qUnion" or len(value.args) != 1 or not isinstance(value.args[0], list) or len(value.args[0]) != 1 or not isinstance(value.args[0][0], Call) or value.args[0][0].name != "qConstructionFilter" ): return None selection = _direct_sketch_wire_selection( value, sketch_by_source, entity_by_sketch, featurescript_version=featurescript_version, standard_library=standard_library, standard_library_version=standard_library_version, allowed_curve_types=frozenset({"line", "arc", "bspline", "circle"}), ) if selection is None: return None source, candidates, sketch = selection if len(candidates) == 1 and candidates[0][1].get("type") == "circle": entity_id, entity = candidates[0] segment = deepcopy(entity) segment["source_entity_id"] = entity_id contours = [{"role": "unknown", "closed": True, "segments": [segment]}] else: if any(entity.get("type") == "circle" for _entity_id, entity in candidates): return None records: list[tuple[str, dict[str, Any], list[float], list[float]]] = [] for entity_id, entity in candidates: start = _sweep_path_endpoint(entity, "start") end = _sweep_path_endpoint(entity, "end") if start is None or end is None or _same_sweep_path_point(start, end): return None records.append((entity_id, entity, start, end)) if len(records) < 2: return None def incident_count(point: list[float]) -> int: return sum( int(_same_sweep_path_point(point, start)) + int(_same_sweep_path_point(point, end)) for _entity_id, _entity, start, end in records ) if any(incident_count(point) != 2 for _entity_id, _entity, start, end in records for point in (start, end)): return None remaining = set(range(len(records))) first_id, first_entity, first_start, first_end = records[0] ordered = [{**deepcopy(first_entity), "source_entity_id": first_id}] remaining.remove(0) current = first_end while remaining: matches = [ index for index in remaining if _same_sweep_path_point(current, records[index][2]) or _same_sweep_path_point(current, records[index][3]) ] if len(matches) != 1: return None index = matches[0] entity_id, entity, start, end = records[index] segment = deepcopy(entity) if _same_sweep_path_point(current, start) else _reversed_sweep_path_segment(entity) segment["source_entity_id"] = entity_id current = end if _same_sweep_path_point(current, start) else start ordered.append(segment) remaining.remove(index) if not _same_sweep_path_point(current, first_start): return None contours = [{"role": "unknown", "closed": True, "segments": ordered}] output = deepcopy(sketch) output["id"] = f"{sketch['id']}__{feature_id}" output["name"] = f"{sketch['name']}__{feature_id}" output["source_sketch_id"] = source output["profile"] = {"type": "analytic_contours", "contours": contours} return output def _direct_sketch_wire_path( value: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], *, featurescript_version: str | None, standard_library: str | None, standard_library_version: str | None, ) -> tuple[str, str, dict[str, Any], dict[str, Any]] | None: """Resolve one direct open line/arc/B-spline source-wire path contract.""" selection = _direct_sketch_wire_selection( value, sketch_by_source, entity_by_sketch, featurescript_version=featurescript_version, standard_library=standard_library, standard_library_version=standard_library_version, ) if selection is None: return None source, candidates, sketch = selection if len(candidates) != 1: return None entity_id, entity = candidates[0] # A sketch circle has coincident endpoints and is never an unambiguous # open sweep spine. A source ``skArc`` carries its exact directed arc # data, so it is safe to retain under the same singleton query gate. if entity.get("type") not in {"line", "arc", "bspline"}: return None return source, entity_id, entity, sketch def _direct_sketch_circle_wire_path( value: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], *, featurescript_version: str | None, standard_library: str | None, standard_library_version: str | None, ) -> tuple[str, str, dict[str, Any], dict[str, Any]] | None: """Resolve one closed source-circle wire without using a parsed leaf. A direct ``sQuery`` circle follows the legacy singleton path. This helper is deliberately for the distinct ``qBodyType(qCreatedBy(..., EDGE), WIRE)`` source-wire form, whose complete query result has already been checked by ``_direct_sketch_wire_selection``. A circle cannot enter the open line/arc/B-spline helper: it has no endpoint roles. """ selection = _direct_sketch_wire_selection( value, sketch_by_source, entity_by_sketch, featurescript_version=featurescript_version, standard_library=standard_library, standard_library_version=standard_library_version, allowed_curve_types=frozenset({"circle"}), ) if selection is None: return None source, candidates, sketch = selection if len(candidates) != 1: return None entity_id, entity = candidates[0] if entity.get("type") != "circle": return None return source, entity_id, entity, sketch def _direct_segmented_sketch_wire_path( value: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], *, featurescript_version: str | None, standard_library: str | None, standard_library_version: str | None, ) -> tuple[str, list[dict[str, Any]], dict[str, Any]] | None: """Resolve one source-only connected, non-branching, open path wire. Ordering uses only exact direct source endpoints. It rejects closed, disconnected, branching, degenerate, and construction-ambiguous source sets rather than letting OCC pick a wire or infer an orientation. """ selection = _direct_sketch_wire_selection( value, sketch_by_source, entity_by_sketch, featurescript_version=featurescript_version, standard_library=standard_library, standard_library_version=standard_library_version, ) if selection is None: return None source, candidates, sketch = selection if len(candidates) < 2: return None records: list[tuple[str, dict[str, Any], list[float], list[float]]] = [] for entity_id, entity in candidates: start, end = _sweep_path_endpoint(entity, "start"), _sweep_path_endpoint(entity, "end") if start is None or end is None or _same_sweep_path_point(start, end): return None records.append((entity_id, entity, start, end)) def incident_count(point: list[float]) -> int: return sum( int(_same_sweep_path_point(point, start)) + int(_same_sweep_path_point(point, end)) for _entity_id, _entity, start, end in records ) terminals = [ (index, endpoint) for index, (_entity_id, _entity, start, end) in enumerate(records) for endpoint in (start, end) if incident_count(endpoint) == 1 ] if len(terminals) != 2: return None _start_index, current = terminals[0] ordered: list[dict[str, Any]] = [] remaining = set(range(len(records))) while remaining: matching = [ index for index in remaining if _same_sweep_path_point(current, records[index][2]) or _same_sweep_path_point(current, records[index][3]) ] if len(matching) != 1: return None index = matching[0] entity_id, entity, start, end = records[index] if _same_sweep_path_point(current, start): segment = deepcopy(entity) current = end elif _same_sweep_path_point(current, end): segment = _reversed_sweep_path_segment(entity) current = start else: # pragma: no cover - guarded by matching above return None segment["source_entity_id"] = entity_id ordered.append(segment) remaining.remove(index) if incident_count(current) != 1: return None return source, ordered, sketch def _spatial_sweep_path_segment( entity_id: str, entity: dict[str, Any], sketch: dict[str, Any], source: str, ) -> tuple[dict[str, Any], list[float], list[float]] | None: """Capture one direct source sketch curve in its explicit global frame.""" plane = sketch.get("workplane") if not isinstance(plane, dict): return None kind = entity.get("type") if kind not in {"line", "arc", "bspline"}: return None def point(value: Any) -> list[float] | None: if not isinstance(value, list) or len(value) != 2 or not all(isinstance(component, (int, float)) and math.isfinite(float(component)) for component in value): return None try: return _global(plane, [float(value[0]), float(value[1])]) except (KeyError, TypeError, ValueError): return None def vector(value: Any) -> list[float] | None: if not isinstance(value, list) or len(value) != 2 or not all(isinstance(component, (int, float)) and math.isfinite(float(component)) for component in value): return None try: y_dir = _y_dir(plane) return [plane["x_dir"][index] * float(value[0]) + y_dir[index] * float(value[1]) for index in range(3)] except (KeyError, TypeError, ValueError): return None output: dict[str, Any] = { "type": kind, "source_sketch_id": source, "source_entity_id": entity_id, } if kind == "bspline": values = entity.get("points") if not isinstance(values, list) or len(values) < 2: return None points = [point(value) for value in values] if any(value is None for value in points) or entity.get("periodic"): return None output["points_mm"] = points parameters = entity.get("parameters") if parameters is not None: if not isinstance(parameters, list) or len(parameters) != len(points): return None try: output["parameters"] = [float(value) for value in parameters] except (TypeError, ValueError): return None start_tangent = entity.get("start_tangent") end_tangent = entity.get("end_tangent") if (start_tangent is None) != (end_tangent is None): return None if start_tangent is not None: start_vector, end_vector = vector(start_tangent), vector(end_tangent) if start_vector is None or end_vector is None: return None output["start_tangent_mm"] = start_vector output["end_tangent_mm"] = end_vector if len(points) == 2 and start_tangent is None: return None return output, points[0], points[-1] start, end = point(entity.get("start")), point(entity.get("end")) if start is None or end is None: return None output["start_mm"] = start output["end_mm"] = end if kind == "arc": center = point(entity.get("center")) radius = entity.get("radius_mm") normal = plane.get("normal") if ( center is None or not isinstance(radius, (int, float)) or not math.isfinite(float(radius)) or float(radius) <= 0 or not isinstance(normal, list) or len(normal) != 3 or not all(isinstance(value, (int, float)) and math.isfinite(float(value)) for value in normal) ): return None output["center_mm"] = center output["normal"] = [float(value) for value in normal] output["radius_mm"] = float(radius) output["clockwise"] = bool(entity.get("clockwise", False)) return output, start, end def _direct_spatial_segmented_sketch_wire_path( value: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], *, featurescript_version: str | None, standard_library: str | None, standard_library_version: str | None, ) -> list[dict[str, Any]] | None: """Resolve a direct union of source sketch wires into one global open wire. This remains source-only. Each outer operand must be the exact versioned wire query accepted by ``_direct_sketch_wire_selection``. Curves are carried through their explicit sketch frames and may join only at exact global source endpoints; no datum/topology/result geometry is consulted. """ if ( not isinstance(value, Call) or value.name != "qUnion" or len(value.args) != 1 or not isinstance(value.args[0], list) or len(value.args[0]) < 2 ): return None records: list[tuple[dict[str, Any], list[float], list[float]]] = [] source_ids: set[tuple[str, str]] = set() for operand in value.args[0]: selection = _direct_sketch_wire_selection( Call("qUnion", [[operand]]), sketch_by_source, entity_by_sketch, featurescript_version=featurescript_version, standard_library=standard_library, standard_library_version=standard_library_version, ) if selection is None: return None source, candidates, sketch = selection for entity_id, entity in candidates: key = (source, entity_id) if key in source_ids: return None source_ids.add(key) captured = _spatial_sweep_path_segment(entity_id, entity, sketch, source) if captured is None: return None segment, start, end = captured if _same_point(start, end): return None records.append((segment, start, end)) if len(records) < 2: return None def incident_count(point: list[float]) -> int: return sum( int(_same_point(point, start)) + int(_same_point(point, end)) for _segment, start, end in records ) terminals = [ endpoint for _segment, start, end in records for endpoint in (start, end) if incident_count(endpoint) == 1 ] if len(terminals) != 2: return None current = terminals[0] remaining = set(range(len(records))) ordered: list[dict[str, Any]] = [] while remaining: matching = [ index for index in remaining if _same_point(current, records[index][1]) or _same_point(current, records[index][2]) ] if len(matching) != 1: return None index = matching[0] segment, start, end = records[index] if _same_point(current, start): current = end elif _same_point(current, end): segment = _reversed_spatial_sweep_path_segment(segment) current = start else: # pragma: no cover - guarded by matching above return None ordered.append(segment) remaining.remove(index) return ordered if incident_count(current) == 1 else None def _pattern_remove_source(feature: dict[str, Any]) -> None: # circularPattern 的 REMOVE 会把源实体及其实例作为切削工具。仅直接的 # 加料拉伸/回转可无歧义改写为同一 profile 的切除;其他 source 需要 # body 生命周期与工具保留策略,不能猜测成任意 boolean。 cut_atomic = { "extrude_add_blind": "extrude_cut_blind", "extrude_add_two_sided": "extrude_cut_two_sided", "revolve_add": "revolve_cut", }.get(str(feature.get("atomic_id") or "")) if cut_atomic is None: raise UnsupportedCapability( "circular_pattern_remove_source", "current CDSL engine can only replay a REMOVE pattern from a direct additive extrusion or revolve", ) feature["atomic_id"] = cut_atomic feature["params"].pop("result_mode", None) def _resolved_body_alias(source: str, aliases: dict[str, str]) -> str: """Follow one proven direct body's current successor without guessing. CADFS ``SWEPT_BODY`` queries keep the original operation owner after a non-copy transform or a sole-body mutation. The source still names the same physical body, whose executable CDSL member is now the successor. This only follows lowering-time transitions that preserve a one-to-one body member. Boolean, multi-body, delete and replayed pattern output never enter the alias map. """ resolved = source visited = {source} while resolved in aliases: successor = aliases[resolved] if successor in visited: raise ValueError("body transform successor aliases contain a cycle") visited.add(successor) resolved = successor return resolved def _direct_transform_copy_member( value: Any, previous: list[str], feature_by_id: dict[str, dict[str, Any]], body_aliases: dict[str, str], visited: set[str] | None = None, ) -> tuple[str, dict[str, str] | None]: """Resolve an exact ``COPY`` chain emitted by explicit transform copies. FeatureScript represents a ``makeCopy`` transform result as ``owner.opPattern/COPY`` even when the owner is a plain transform rather than a CADFS pattern feature. The runtime already gives that transform a distinct body member under its feature ID. A multi-source transform COPY exposes one member for each selected source, so this resolver returns a structured reference for that case and validates its complete derived-from chain. It intentionally does not infer ownership for generic pattern, fused, or dress-up output. """ _call, owner, topology, kind, definition = _direct_make_query(value) if kind not in {"body", "entitytype.body"}: raise UnsupportedCapability("transform_pattern_copy", "CADFS transform COPY source is not a body") if topology == "SWEPT_BODY": source = f"f_{owner}" if source not in previous: raise ValueError("transform COPY source body is unresolved") return _resolved_body_alias(source, body_aliases), None if topology != "COPY": raise UnsupportedCapability( "transform_pattern_copy", "CADFS transform COPY source must descend from a direct swept body", ) try: instance = int(str(definition.get("instanceName"))) except (TypeError, ValueError) as error: raise ValueError("transform COPY instance is unresolved") from error if instance != 1: raise UnsupportedCapability( "transform_pattern_copy", "direct transform COPY provenance only has generated instance 1", ) member_id = f"f_{owner}" if member_id in (visited or set()): raise ValueError("transform COPY provenance contains a cycle") transform = feature_by_id.get(member_id) params = (transform or {}).get("params") or {} source_ids = params.get("source_feature_ids") or [] if ( member_id not in previous or transform is None or transform.get("atomic_id") != "transform_bodies" or not bool(params.get("make_copy")) or params.get("pattern_instance_refs") or params.get("transform_copy_refs") or not isinstance(source_ids, list) or not source_ids ): raise UnsupportedCapability( "transform_pattern_copy", "CADFS transform COPY must name an exact preceding explicit transform copy", ) derived = definition.get("derivedFrom") if derived is None: raise ValueError("transform COPY has no derived body") upstream, _upstream_copy_ref = _direct_transform_copy_member( derived, previous, feature_by_id, body_aliases, (visited or set()) | {member_id}, ) source_member_aliases = { str(alias.get("source_feature_id")): str(alias.get("active_member_feature_id")) for alias in params.get("source_member_aliases") or () if isinstance(alias, dict) and isinstance(alias.get("source_feature_id"), str) and isinstance(alias.get("active_member_feature_id"), str) } # A transform COPY preserves the FeatureScript owner of its source body # even when an earlier proven single-body successor supplies the current # runtime member. The explicit mapping is source provenance, not an # instruction to select a different body or a current-aggregate fallback. if len(source_ids) == 1 and ( source_ids == [upstream] or source_member_aliases == {upstream: source_ids[0]} ): return member_id, None if len(source_ids) > 1 and upstream in source_ids: return member_id, { "transform_feature_id": member_id, "source_feature_id": upstream, } raise UnsupportedCapability( "transform_pattern_copy", "CADFS transform COPY derived body does not match its CDSL transform source", ) def _transform_copy_terminal_source(value: Any) -> str | None: """Return the original direct body owner at one COPY chain's root.""" try: _call, owner, topology, kind, definition = _direct_make_query(value) except ValueError: return None if kind not in {"body", "entitytype.body"}: return None if topology == "SWEPT_BODY": return f"f_{owner}" if topology != "COPY" or definition.get("derivedFrom") is None: return None return _transform_copy_terminal_source(definition["derivedFrom"]) def _transform_source_member_aliases( value: Any, sources: list[str], ) -> list[dict[str, str]]: """Bind original transform query owners to their proved runtime members. This is only emitted for one-to-one direct body references. Pattern and multi-source COPY references already have their own instance-qualified contracts, so they must not be collapsed into this lifecycle mapping. """ query_values = _queries(value) if len(sources) != 1 or len(query_values) != 1: return [] aliases: list[dict[str, str]] = [] for query_value, active_member in zip(query_values, sources): semantic_source = _transform_copy_terminal_source(query_value) if semantic_source is None or semantic_source == active_member: continue alias = { "source_feature_id": semantic_source, "active_member_feature_id": active_member, } if alias not in aliases: aliases.append(alias) return aliases def _transform_copy_query_provenance( value: Any, previous: list[str], feature_by_id: dict[str, dict[str, Any]], visited: set[str] | None = None, ) -> tuple[Any, list[dict[str, Any]], str]: """Return a direct source query and exact transforms for a COPY descendant. The copied edge/vertex must be produced by the same restricted transform copy chain as its selected body. Applying the recorded CDSL transforms to the direct source reference preserves the physical location without a topology-nearest fallback. This is deliberately separate from runtime selector binding: the values are only used to lower a FeatureScript translation vector whose source points are explicit and unique. """ _call, owner, topology, _kind, definition = _direct_make_query(value) member_id = f"f_{owner}" if topology != "COPY": if member_id not in previous: raise ValueError("transform COPY reference owner is unresolved") return value, [], member_id try: instance = int(str(definition.get("instanceName"))) except (TypeError, ValueError) as error: raise ValueError("transform COPY reference instance is unresolved") from error if instance != 1: raise UnsupportedCapability( "transform_translation_entity", "COPY reference only has exact transform provenance for instance 1", ) if member_id in (visited or set()): raise ValueError("transform COPY reference provenance contains a cycle") transform = feature_by_id.get(member_id) params = (transform or {}).get("params") or {} source_ids = params.get("source_feature_ids") or [] transform_spec = params.get("transform") if ( member_id not in previous or transform is None or transform.get("atomic_id") != "transform_bodies" or not bool(params.get("make_copy")) or params.get("pattern_instance_refs") or not isinstance(source_ids, list) or len(source_ids) != 1 or not isinstance(transform_spec, dict) ): raise UnsupportedCapability( "transform_translation_entity", "COPY reference must name an exact preceding single-source transform copy", ) derived = definition.get("derivedFrom") if derived is None: raise ValueError("transform COPY reference has no derived geometry") source_query, transforms, upstream_member = _transform_copy_query_provenance( derived, previous, feature_by_id, (visited or set()) | {member_id}, ) if source_ids != [upstream_member]: raise UnsupportedCapability( "transform_translation_entity", "COPY reference derived geometry does not match its transform source", ) return source_query, [*transforms, transform_spec], member_id def _apply_body_transform_to_point(point: list[float], transform: dict[str, Any]) -> list[float]: """Apply one validated CDSL body transform to an explicit point.""" kind = str(transform.get("type") or "") if kind == "translation": offset = transform.get("translation_mm") if not isinstance(offset, list) or len(offset) != 3: raise ValueError("transform COPY translation is incomplete") return [point[index] + float(offset[index]) for index in range(3)] if kind == "rotation": axis = transform.get("axis") angle = transform.get("angle_deg") if not isinstance(axis, dict) or not isinstance(angle, (int, float)): raise ValueError("transform COPY rotation is incomplete") return _rotate_point(point, axis, math.radians(float(angle))) if kind == "uniform_scale": center = transform.get("center_mm") factor = transform.get("scale_factor") if not isinstance(center, list) or len(center) != 3 or not isinstance(factor, (int, float)): raise ValueError("transform COPY uniform scale is incomplete") return [float(center[index]) + (point[index] - float(center[index])) * float(factor) for index in range(3)] raise ValueError(f"transform COPY has unsupported transform type {kind!r}") def _transform_copy_point( query: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_by_id: dict[str, dict[str, Any]], previous: list[str], ) -> list[float]: try: _call, _owner, topology, _kind, _definition = _direct_make_query(query) except ValueError: return _query_point(query, feature_frames, sketch_by_source, entity_by_sketch) if topology != "COPY": return _query_point(query, feature_frames, sketch_by_source, entity_by_sketch) source, transforms, _member = _transform_copy_query_provenance(query, previous, feature_by_id) point = _query_point(source, feature_frames, sketch_by_source, entity_by_sketch) for transform in transforms: point = _apply_body_transform_to_point(point, transform) return point def _transform_copy_line( query: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_by_id: dict[str, dict[str, Any]], previous: list[str], ) -> tuple[list[float], list[float]]: try: _call, _owner, topology, _kind, _definition = _direct_make_query(query) except ValueError: return _query_line(query, feature_frames, sketch_by_source, entity_by_sketch) if topology != "COPY": return _query_line(query, feature_frames, sketch_by_source, entity_by_sketch) source, transforms, _member = _transform_copy_query_provenance(query, previous, feature_by_id) start, end = _query_line(source, feature_frames, sketch_by_source, entity_by_sketch) for transform in transforms: start = _apply_body_transform_to_point(start, transform) end = _apply_body_transform_to_point(end, transform) return start, end def _transform_source_features(value: Any, previous: list[str], body_aliases: dict[str, str] | None = None) -> list[str]: sources = [] body_aliases = body_aliases or {} for call in walk_calls(value): if call.name not in {"makeQuery", "qCreatedBy"} or not call.args: continue owner = symbolic_string(call.args[0]) if "F" not in owner: continue source = "f_" + owner[owner.find("F"):].split(".", 1)[0] resolved = _resolved_body_alias(source, body_aliases) if source in previous and resolved not in sources: sources.append(resolved) if not sources: raise ValueError("transform source features are unresolved") return sources def _transform_body_references( value: Any, previous: list[str], feature_by_id: dict[str, dict[str, Any]], body_aliases: dict[str, str] | None = None, ) -> tuple[list[str], list[dict[str, Any]], list[dict[str, str]]]: """Lower direct CADFS body queries without flattening COPY provenance. A pattern COPY is not its pattern's aggregate result. Keep the producer, source body and instance index as a structured CDSL reference so runtime can select a proven body member without receiving an internal state ID. """ sources: list[str] = [] instance_refs: list[dict[str, Any]] = [] transform_copy_refs: list[dict[str, str]] = [] body_aliases = body_aliases or {} query_values = _queries(value) # Older CADFS exports represent a directly created body as # qCreatedBy(id + "F1", BODY), without a makeQuery topology wrapper. # It has no COPY provenance, so the established source-feature contract # remains exact and does not need a body-member instance reference. if query_values and all(isinstance(item, Call) and item.name == "qCreatedBy" for item in query_values): return _transform_source_features(value, previous, body_aliases), instance_refs, transform_copy_refs for query_value in query_values: _call, owner, topology, kind, definition = _direct_make_query(query_value) if kind not in {"body", "entitytype.body"}: raise UnsupportedCapability("transform_body_query", "CADFS transform requires direct body queries") if topology == "SWEPT_BODY": source = f"f_{owner}" if source not in previous: raise ValueError("transform source body is unresolved") source = _resolved_body_alias(source, body_aliases) if source not in sources: sources.append(source) continue if topology != "COPY": raise UnsupportedCapability( "transform_body_query", "CADFS transform requires SWEPT_BODY or circular-pattern COPY body queries", ) derived = definition.get("derivedFrom") if derived is None: raise ValueError("pattern copy transform source is unresolved") pattern_id = f"f_{owner}" owner_feature = feature_by_id.get(pattern_id) if owner_feature is not None and owner_feature.get("atomic_id") == "transform_bodies": source, transform_copy_ref = _direct_transform_copy_member( query_value, previous, feature_by_id, body_aliases, ) if transform_copy_ref is not None: if transform_copy_ref not in transform_copy_refs: transform_copy_refs.append(transform_copy_ref) elif source not in sources: sources.append(source) continue _source_call, source_owner, source_topology, source_kind, _source_definition = _direct_make_query(derived) source_id = _resolved_body_alias(f"f_{source_owner}", body_aliases) pattern = feature_by_id.get(pattern_id) if ( source_topology == "SWEPT_BODY" and source_kind in {"body", "entitytype.body"} and pattern is not None and pattern.get("atomic_id") == "pattern_mirror" and pattern_id in previous and source_id in (pattern.get("params") or {}).get("source_feature_ids", []) and (feature_by_id.get(source_id) or {}).get("params", {}).get("result_mode") == "new_body" ): try: instance = int(str(definition.get("instanceName"))) except (TypeError, ValueError) as error: raise ValueError("mirror copy transform instance is unresolved") from error if instance != 1: raise UnsupportedCapability( "transform_pattern_copy", "direct mirror COPY provenance only has generated instance 1", ) reference = { "pattern_feature_id": pattern_id, "source_feature_id": source_id, "instance_index": instance, } if reference not in instance_refs: instance_refs.append(reference) continue if ( source_topology != "SWEPT_BODY" or source_kind not in {"body", "entitytype.body"} or pattern is None or pattern.get("atomic_id") != "pattern_circular" or pattern_id not in previous or source_id not in (pattern.get("params") or {}).get("source_feature_ids", []) ): raise UnsupportedCapability( "transform_pattern_copy", "CADFS transform COPY body must name a direct source of a preceding circular pattern", ) try: instance = int(str(definition.get("instanceName"))) except (TypeError, ValueError) as error: raise ValueError("pattern copy transform instance is unresolved") from error count = int((pattern.get("params") or {}).get("pattern_count") or 0) excluded = {int(value) for value in (pattern.get("params") or {}).get("excluded_instance_indices") or []} if instance < 1 or instance >= count or instance in excluded: raise ValueError("pattern copy transform instance is outside the generated range") reference = { "pattern_feature_id": pattern_id, "source_feature_id": source_id, "instance_index": instance, } if reference not in instance_refs: instance_refs.append(reference) if not sources and not instance_refs and not transform_copy_refs: raise ValueError("transform source bodies are unresolved") return sources, instance_refs, transform_copy_refs def _body_transform( params: dict[str, Any], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_by_id: dict[str, dict[str, Any]] | None = None, previous: list[str] | None = None, ) -> dict[str, Any]: """Lower one CADFS body transform without changing the selected body.""" transform_type = str(params.get("transformType") or "").split(".")[-1].upper() # FeatureScript COPY has no geometric displacement, but it does create a # separate body member. Represent its identity geometry explicitly and let # the enclosing transform_bodies operation retain the source via # ``make_copy``. This stays on the OCC transform/history path rather than # aliasing the source's runtime body. if transform_type == "COPY": return {"type": "translation", "translation_mm": [0.0, 0.0, 0.0]} if transform_type == "TRANSLATION_3D": return { "type": "translation", "translation_mm": [_number(params.get(key, 0.0), True) for key in ("dx", "dy", "dz")], } if transform_type == "TRANSLATION_DISTANCE": return { "type": "translation", "translation_mm": _translation_distance_vector( params, feature_frames, sketch_by_source, entity_by_sketch, feature_by_id, previous, ), } if transform_type == "TRANSLATION_ENTITY": return { "type": "translation", "translation_mm": _translation_entity_vector( params, feature_frames, sketch_by_source, entity_by_sketch, feature_by_id, previous, ), } if transform_type == "ROTATION": axis = _transform_axis(params.get("transformAxis"), feature_frames, sketch_by_source, entity_by_sketch) return {"type": "rotation", "axis": axis, "angle_deg": _number(params.get("angle"), True)} if transform_type == "SCALE_UNIFORMLY": scale_factor = _number(params.get("scale")) if not math.isfinite(scale_factor) or scale_factor <= 0: raise UnsupportedCapability( "transform_uniform_scale", "SCALE_UNIFORMLY requires a finite positive scale factor", ) return { "type": "uniform_scale", "center_mm": _scale_center(params.get("scalePoint"), feature_frames, sketch_by_source, entity_by_sketch), "scale_factor": scale_factor, } raise UnsupportedCapability("transform", f"current CDSL engine cannot exactly execute {transform_type or 'unknown'} transform") def _delete_body_source(value: Any) -> str: """Resolve a directly owned body output without broad query expansion.""" _call, owner, topology, kind, _definition = _direct_make_query(value) if kind not in {"body", "entitytype.body"} or topology not in {"SWEPT_BODY", "COPY"}: raise UnsupportedCapability( "delete_bodies", "current CDSL deleteBodies requires a direct SWEPT_BODY or COPY body query", ) return f"f_{owner}" def _circular_pattern_axis( value: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> dict[str, list[float]]: query = parse_query(value) entity = (entity_by_sketch.get(query.source_sketch or "") or {}).get(query.source_entity or "") if entity is None or query.source_sketch not in sketch_by_source: raise ValueError("circular pattern axis is unresolved") plane = sketch_by_source[query.source_sketch]["workplane"] if entity["type"] == "line": start = _global(plane, entity["start"]); end = _global(plane, entity["end"]) direction = [end[index] - start[index] for index in range(3)] norm = math.sqrt(sum(component * component for component in direction)) if norm <= 1e-9: raise ValueError("circular pattern axis line is degenerate") return {"origin_mm": start, "direction": [component / norm for component in direction]} if entity["type"] == "circle": frame = feature_frames.get(query.owner_feature or "") if frame and query.is_start is not None: plane = frame["start" if query.is_start else "end"] return {"origin_mm": _global(plane, entity["center"]), "direction": list(plane["normal"])} raise ValueError("circular pattern axis must be a sketch line or circular edge") def _loft_profile_sketches(params: dict[str, Any]) -> list[str]: # CADFS loft 的 profile 是草图 IMPRINT 面;几何仍来自原始闭合草图, # 保留草图 source,不能把前序实体的选中面近似为新的放样轮廓。 profiles = params.get("sheetProfilesArray") if not isinstance(profiles, list): raise ValueError("loft sheetProfilesArray is unresolved") sources: list[str] = [] for profile in profiles: query_value = profile.get("sheetProfileEntities") if isinstance(profile, dict) else profile query = parse_query(query_value) if query.topology_type and query.topology_type != "IMPRINT": raise UnsupportedCapability( f"loft_profile_topology:{query.topology_type.lower()}", f"current CDSL loft only supports sketch-imprint profiles, not {query.topology_type}", ) if not query.source_sketch: raise ValueError("loft profile sketch query is unresolved") sources.append(query.source_sketch) if len(sources) < 2: raise ValueError("loft requires at least two profile sketches") if len(set(sources)) != len(sources): raise ValueError("loft profile sketches must be distinct") return sources def _initial_direct_loft_cap_output_roles( params: dict[str, Any], sources: list[str], features: list[dict[str, Any]], featurescript_version: str | None, ) -> bool: """Whether an initial direct loft exposes exact endpoint CAP roles.""" if featurescript_version != "1511" or _has_active_body(features) or len(sources) != 2 or len(set(sources)) != 2: return False operation = str(params.get("operationType") or "NEW").rsplit(".", 1)[-1].upper() if operation != "NEW": return False if any(params.get(key) not in (None, [], {}, False) for key in ( "wireProfilesArray", "connections", "matchConnections", "startCondition", "endCondition", "startMagnitude", "endMagnitude", )): return False profiles = params.get("sheetProfilesArray") if not isinstance(profiles, list) or len(profiles) != 2: return False for source, profile in zip(sources, profiles): value = profile.get("sheetProfileEntities") if isinstance(profile, dict) else None try: _call, owner, topology, kind, _definition = _direct_make_query(value) except ValueError: return False if owner != source or topology != "IMPRINT" or kind != "face": return False return True def _profile_query_kind(params: dict[str, Any]) -> str | None: for key in ("entities", "sheetProfilesArray"): if key in params: return parse_query(params[key]).topology_type return None def _profile_executable(sketch: dict[str, Any]) -> bool: profile = sketch.get("profile") or {} if profile.get("type") == "circle": return True if profile.get("type") == "polygon": return len(profile.get("vertices") or []) >= 3 if profile.get("type") == "multi_source_regions": return bool(profile.get("source_sketch_ids")) and all( _profile_executable({"profile": child}) for child in profile.get("profiles") or [] ) if profile.get("type") == "planar_imprint": return bool(profile.get("source_entities") and profile.get("selections")) return bool(profile.get("contours")) def _default_plane(value: Any) -> dict[str, Any] | None: for call in walk_calls(value): text = " ".join(symbolic_string(arg) for arg in call.args) for name, plane in PLANES.items(): if f"{name}.planeOp" in text: return dict(plane) return None def _entity_from_query( query: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> tuple[dict[str, Any], dict[str, Any], str]: info = parse_query(query); source = info.source_sketch or ""; token = info.source_entity or "" available = entity_by_sketch.get(source) or {} entity = available.get(token) if entity is None: entity_id = max((key for key in available if token.startswith(key + ".")), key=len, default="") entity = available.get(entity_id) sketch = sketch_by_source.get(source) if entity is None or sketch is None: raise ValueError("reference geometry source is unresolved") return entity, sketch["workplane"], token def _entity_point(entity: dict[str, Any], plane: dict[str, Any], token: str) -> list[float]: if entity["type"] == "point": return _global(plane, entity["point"]) if entity["type"] == "circle" and ".center" in token: return _global(plane, entity["center"]) if entity["type"] == "line": local = entity["end"] if ".end" in token else entity["start"] return _global(plane, local) if entity["type"] == "arc": if token.endswith(".start"): return _global(plane, entity["start"]) if token.endswith(".end"): return _global(plane, entity["end"]) raise ValueError("arc reference point is unresolved") if entity["type"] == "bspline": points = entity.get("points") or [] if not points: raise ValueError("B-spline reference point is unresolved") # ``.start`` and ``.end`` are distinct direct source endpoints. # FeatureScript's ``.N.internal`` suffixes are zero-based output # vertex indexes; neither form may silently fall back to the first # interpolation point. if token.endswith(".start"): index = 0 elif token.endswith(".end"): index = len(points) - 1 else: index = next((int(part) for part in token.split(".") if part.isdigit()), -1) if index < 0: raise ValueError("B-spline reference point is unresolved") return _global(plane, points[max(0, min(index, len(points) - 1))]) raise ValueError("reference entity does not define a point") def _entity_line(entity: dict[str, Any], plane: dict[str, Any]) -> tuple[list[float], list[float]]: if entity["type"] != "line": raise ValueError("reference entity is not a line") return _global(plane, entity["start"]), _global(plane, entity["end"]) def _curve_endpoint_tangent(entity: dict[str, Any], endpoint: str) -> list[float]: """Return the exact directed source tangent at one direct curve endpoint. ``CURVE_POINT`` is defined by the curve tangent at its selected point, not by a chord through adjacent fit points. The CADFS source carries exact derivatives for the bounded B-spline case below, while an analytic arc has an exact radial construction. Interior spline vertices and splines without an exported endpoint derivative deliberately stay unsupported. """ if endpoint not in {"start", "end"}: raise UnsupportedCapability( "reference_plane:curve_point", "CURVE_POINT requires a direct source-curve endpoint", ) curve_type = entity.get("type") if curve_type == "arc": point = entity.get(endpoint) center = entity.get("center") if not ( isinstance(point, list) and len(point) == 2 and isinstance(center, list) and len(center) == 2 ): raise ValueError("arc endpoint tangent is unresolved") radial = [float(point[index]) - float(center[index]) for index in range(2)] return [radial[1], -radial[0]] if entity.get("clockwise") else [-radial[1], radial[0]] if curve_type == "bspline": tangent = entity.get(f"{endpoint}_tangent") if not ( isinstance(tangent, list) and len(tangent) == 2 and all(isinstance(value, (int, float)) and math.isfinite(value) for value in tangent) ): raise UnsupportedCapability( "reference_plane:curve_point", "CURVE_POINT B-spline endpoint has no exported tangent", ) return [float(value) for value in tangent] raise UnsupportedCapability( "reference_plane:curve_point", "CURVE_POINT supports only direct lines, arcs, or B-spline endpoints with exported tangents", ) def _curve_interpolation_tangent(entity: dict[str, Any], index: int) -> list[float]: """Return the kernel-proven tangent at one direct B-spline fit point. An ``E..internal`` source token denotes an interpolation vertex emitted by the source sketch solver. It is neither a derived topology suffix nor permission to estimate a tangent from adjacent fit points. We reproduce the CDSL runtime's explicit-parameter, non-scaling OCC interpolator and verify it still passes through that exact source point. """ if entity.get("type") != "bspline": raise UnsupportedCapability( "reference_plane:curve_point", "CURVE_POINT internal point requires a direct B-spline source curve", ) if entity.get("periodic"): raise UnsupportedCapability( "reference_plane:curve_point", "CURVE_POINT does not support periodic B-spline interpolation points", ) points = entity.get("points") parameters = entity.get("parameters") start_tangent = entity.get("start_tangent") end_tangent = entity.get("end_tangent") valid_vector = lambda value: ( isinstance(value, list) and len(value) == 2 and all(isinstance(component, (int, float)) and math.isfinite(component) for component in value) ) if ( not isinstance(points, list) or not all(valid_vector(point) for point in points) or not isinstance(parameters, list) or not valid_vector(start_tangent) or not valid_vector(end_tangent) ): raise UnsupportedCapability( "reference_plane:curve_point", "CURVE_POINT B-spline interpolation point has no complete source interpolation data", ) if index < 0 or index >= len(points): raise UnsupportedCapability( "reference_plane:curve_point", "CURVE_POINT B-spline interpolation point is out of range", ) try: # Import the runtime helper lazily: conversion remains usable in a # parser-only environment until this kernel-backed capability is used. from engine.cdsl_engine.build123d_adapter import interpolated_bspline_point_and_tangent point, tangent = interpolated_bspline_point_and_tangent( [(*map(float, value), 0.0) for value in points], start_tangent=(*map(float, start_tangent), 0.0), end_tangent=(*map(float, end_tangent), 0.0), parameters=[float(value) for value in parameters], interpolation_index=index, ) except (ImportError, TypeError, ValueError) as exc: raise UnsupportedCapability( "reference_plane:curve_point", f"CURVE_POINT B-spline interpolation tangent is unavailable: {exc}", ) from exc source_point = points[index] if math.dist(point[:2], source_point) > 1e-7: raise UnsupportedCapability( "reference_plane:curve_point", "CURVE_POINT B-spline interpolation does not reproduce its source vertex", ) if math.hypot(tangent[0], tangent[1]) <= 1e-12: raise UnsupportedCapability( "reference_plane:curve_point", "CURVE_POINT B-spline interpolation tangent is degenerate", ) return [tangent[0], tangent[1]] def _curve_point_tangent( curve_query: Any, point_query: Any, point: list[float], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> tuple[dict[str, Any], list[float]]: """Resolve one direct source curve's tangent at its explicitly named end.""" curve, plane, _ = _entity_from_query(curve_query, sketch_by_source, entity_by_sketch) curve_info = parse_query(curve_query) point_info = parse_query(point_query) _point_entity, _point_plane, point_token = _entity_from_query( point_query, sketch_by_source, entity_by_sketch, ) source_entity_id = curve.get("source_entity_id") if ( not isinstance(source_entity_id, str) or curve_info.source_sketch != point_info.source_sketch or not point_token.startswith(source_entity_id + ".") ): raise UnsupportedCapability( "reference_plane:curve_point", "CURVE_POINT point must be an endpoint of the same direct source curve", ) suffix = point_token[len(source_entity_id) + 1:] interpolation_index: int | None = None if suffix in {"start", "end"}: local_point = curve.get(suffix) else: parts = suffix.split(".") if ( curve.get("type") == "bspline" and len(parts) == 2 and parts[0].isdigit() and parts[1] == "internal" ): interpolation_index = int(parts[0]) points = curve.get("points") local_point = points[interpolation_index] if isinstance(points, list) and 0 <= interpolation_index < len(points) else None else: local_point = None if not isinstance(local_point, list) or len(local_point) != 2: raise UnsupportedCapability( "reference_plane:curve_point", "CURVE_POINT point must name a direct source-curve endpoint or B-spline interpolation vertex", ) expected_point = _global(plane, local_point) if math.dist(point, expected_point) > 1e-7: raise UnsupportedCapability( "reference_plane:curve_point", "CURVE_POINT point does not match its source-curve endpoint", ) local_tangent = ( _curve_interpolation_tangent(curve, interpolation_index) if interpolation_index is not None else _curve_endpoint_tangent(curve, suffix) ) y_dir = _y_dir(plane) tangent = [ local_tangent[0] * plane["x_dir"][index] + local_tangent[1] * y_dir[index] for index in range(3) ] return plane, tangent def _local(plane: dict[str, Any], point: list[float]) -> list[float]: relative = _sub(point, plane["origin_mm"]) return [_dot(relative, plane["x_dir"]), _dot(relative, _y_dir(plane))] def _revolve_swept_face_profile( query: Any, plane: dict[str, Any], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> dict[str, Any] | None: info = parse_query(query); frame = feature_frames.get(info.owner_feature or "") if info.topology_type != "SWEPT_FACE" or frame is None or not frame.get("revolve_full"): return None axis = frame.get("revolve_axis") if not isinstance(axis, dict): return None start, end = _query_line(query, feature_frames, sketch_by_source, entity_by_sketch) direction = _sub(end, start); axis_direction = axis["direction"] if abs(_dot(direction, axis_direction)) > 1e-6 * max(1.0, math.sqrt(_dot(direction, direction))): raise UnsupportedCapability("extrude_profile_topology:swept_face", "revolved swept face source line is not perpendicular to the revolve axis") start_offset = _dot(_sub(start, axis["origin_mm"]), axis_direction) end_offset = _dot(_sub(end, axis["origin_mm"]), axis_direction) if abs(start_offset - end_offset) > 1e-6: raise UnsupportedCapability("extrude_profile_topology:swept_face", "revolved swept face source line is not coplanar with the revolve axis") center = [axis["origin_mm"][index] + axis_direction[index] * start_offset for index in range(3)] radii = sorted([math.dist(start, center), math.dist(end, center)]) if radii[1] <= 1e-9: raise ValueError("revolved swept face source line is degenerate") local_center = _local(plane, center) contours = [{"role": "outer", "closed": True, "segments": [{"type": "circle", "center": local_center, "radius_mm": radii[1]}]}] if radii[0] > 1e-9: contours.append({"role": "inner", "closed": True, "segments": [{"type": "circle", "center": local_center, "radius_mm": radii[0]}]}) return {"type": "analytic_contours", "contours": contours} def _contour_line_interior_normal( contour: dict[str, Any], entity: dict[str, Any], source_plane: dict[str, Any], ) -> list[float] | None: """Return the source-proven interior side of one closed contour line. A vertex average is not an interior witness: on a valid asymmetric polygon it can lie on the selected boundary line. The ordered contour's signed area instead gives the exact left/right interior side for the matched source line, without inspecting any resulting B-rep geometry. """ segments = contour.get("segments") or [] if not contour.get("closed") or entity.get("type") != "line" or not segments: return None matches = [segment for segment in segments if _matching_profile_segment(segment, entity)] if len(matches) != 1: return None try: area = _contour_area(segments) if not math.isfinite(area) or abs(area) <= 1e-9: return None segment = matches[0] start = _global(source_plane, segment["start"]) end = _global(source_plane, segment["end"]) traversal = _unit(_sub(end, start), "swept face source line is degenerate") left = _cross(source_plane["normal"], traversal) orientation = 1.0 if area > 0 else -1.0 return _unit([orientation * value for value in left], "swept face source contour is degenerate") except (KeyError, TypeError, ValueError): return None def _query_plane( query: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> dict[str, Any]: try: return _plane_from_query(query, feature_frames, sketch_by_source, entity_by_sketch) except ValueError: info = parse_query(query) if info.topology_type != "SWEPT_FACE" or not info.owner_feature: raise entity, source_plane, _ = _entity_from_query(query, sketch_by_source, entity_by_sketch) start, end = _entity_line(entity, source_plane); frame = feature_frames.get(info.owner_feature) if frame is None: raise ValueError("swept face owner frame is unresolved") if frame.get("revolve_full"): axis = frame.get("revolve_axis") if not isinstance(axis, dict): raise ValueError("revolve swept face axis is unresolved") direction = _sub(end, start); axis_direction = axis["direction"] if abs(_dot(direction, axis_direction)) > 1e-6 * max(1.0, math.sqrt(_dot(direction, direction))): raise UnsupportedCapability("reference_plane:swept_face", "revolved swept face source line is not perpendicular to the revolve axis") return _frame(start, direction, axis_direction) direction = _unit(_sub(end, start), "swept face source line is degenerate") sketch = sketch_by_source.get(info.source_sketch or "") or {} contours = (sketch.get("profile") or {}).get("contours") or [] contour = next(( item for item in contours if any(_matching_profile_segment(segment, entity) for segment in item.get("segments") or []) ), None) interior_normal = _contour_line_interior_normal(contour, entity, source_plane) if isinstance(contour, dict) else None if interior_normal is not None: normal = [-value for value in interior_normal] else: # Keep the existing fallback for source contours without one # complete ordered line-loop witness. points = [ segment["start"] for segment in (contour or {}).get("segments") or [] if isinstance(segment.get("start"), list) ] if points: center = _global(source_plane, [ sum(point[index] for point in points) / len(points) for index in range(2) ]) midpoint = [(start[index] + end[index]) / 2.0 for index in range(3)] inward = _sub(center, midpoint) inward = _sub(inward, [direction[index] * _dot(inward, direction) for index in range(3)]) normal = [-value for value in _unit(inward, "swept face interior is degenerate")] else: normal = _cross(direction, frame["end"]["normal"]) x_dir = direction if _dot(_cross(normal, x_dir), source_plane["normal"]) < 0: x_dir = [-value for value in x_dir] # 附着草图的局部原点是全局原点在实体侧面上的投影,不是 source # 草图边的任一端点。后者会把以原全局平面坐标表达的 F7/F9 等草图 # 平移一个完整边长,令后续 cut 落在主体之外。 return _attachment_plane(_frame(start, x_dir, normal)) def _offset_face_plane( value: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> dict[str, Any]: """Recover one planar inner shell wall from its source profile edge.""" query = parse_query(value) frame = feature_frames.get(query.owner_feature or "") or {} if query.topology_type != "OFFSET_FACE" or not frame.get("shell_source"): raise ValueError("offset face owner is unresolved") thickness = float(frame.get("shell_thickness_mm") or 0.0) if thickness <= 0: raise ValueError("offset face shell thickness is unresolved") entity, source_plane, _ = _entity_from_query(value, sketch_by_source, entity_by_sketch) start, end = _entity_line(entity, source_plane) direction = _unit(_sub(end, start), "offset face source line is degenerate") source_sketch = sketch_by_source.get(query.source_sketch or "") or {} contours = (source_sketch.get("profile") or {}).get("contours") or [] contour = next(( item for item in contours if any(_matching_profile_segment(segment, entity) for segment in item.get("segments") or []) ), None) points = [ segment["start"] for segment in (contour or {}).get("segments") or [] if isinstance(segment.get("start"), list) ] if not points: raise ValueError("offset face source contour is unresolved") center = _global(source_plane, [ sum(point[index] for point in points) / len(points) for index in range(2) ]) midpoint = [(start[index] + end[index]) / 2.0 for index in range(3)] inward = _sub(center, midpoint) inward = _sub(inward, [direction[index] * _dot(inward, direction) for index in range(3)]) normal = _unit(inward, "offset face source interior is degenerate") x_dir = direction if _dot(_cross(normal, x_dir), source_plane["normal"]) < 0: x_dir = [-value for value in x_dir] offset = [start[index] + thickness * normal[index] for index in range(3)] return _attachment_plane(_frame(offset, x_dir, normal)) def _convex_linear_offset_wall_endpoints( profile: dict[str, Any], entity: dict[str, Any], source_plane: dict[str, Any], offset_plane: dict[str, Any], thickness: float, ) -> tuple[list[float], list[float]] | None: """Offset one convex, closed, linear profile edge with its true neighbors. A shell's offset wall ends at the intersections with the offset adjacent walls. This is not equivalent to translating the selected outer edge: a rectangular wall, for example, shortens at both corners. Restrict this construction to one ordered convex line loop so every miter and its interior side are uniquely defined. """ contours = (profile.get("profile") or {}).get("contours") or [] if len(contours) != 1: return None contour = contours[0] segments = contour.get("segments") or [] if ( not contour.get("closed") or len(segments) < 3 or any(segment.get("type") != "line" for segment in segments) or any(not _same_point(segment["end"], segments[(index + 1) % len(segments)]["start"]) for index, segment in enumerate(segments)) ): return None matches = [index for index, segment in enumerate(segments) if _matching_profile_segment(segment, entity)] if len(matches) != 1: return None area_twice = sum( segment["start"][0] * segment["end"][1] - segment["end"][0] * segment["start"][1] for segment in segments ) if abs(area_twice) <= 1e-9: return None orientation = 1.0 if area_twice > 0 else -1.0 def interior_normal(segment: dict[str, Any]) -> list[float] | None: dx = segment["end"][0] - segment["start"][0] dy = segment["end"][1] - segment["start"][1] length = math.hypot(dx, dy) if length <= 1e-9: return None return [-orientation * dy / length, orientation * dx / length] normals = [interior_normal(segment) for segment in segments] if any(normal is None for normal in normals): return None # A convex loop has one consistent signed turn direction. Concave offset # boundaries can self-intersect and require the shell kernel's exact trim # history, so they deliberately remain deferred. turn_signs = [] for index, segment in enumerate(segments): next_segment = segments[(index + 1) % len(segments)] dx = segment["end"][0] - segment["start"][0] dy = segment["end"][1] - segment["start"][1] next_dx = next_segment["end"][0] - next_segment["start"][0] next_dy = next_segment["end"][1] - next_segment["start"][1] turn = dx * next_dy - dy * next_dx if abs(turn) <= 1e-9: return None turn_signs.append(1.0 if turn > 0 else -1.0) if any(sign != turn_signs[0] for sign in turn_signs): return None selected_index = matches[0] selected_normal = normals[selected_index] global_normal = [ source_plane["x_dir"][index] * selected_normal[0] + _y_dir(source_plane)[index] * selected_normal[1] for index in range(3) ] if abs(_dot(_unit(global_normal, "offset wall normal is degenerate"), offset_plane["normal"]) - 1.0) > 1e-6: return None def shifted_line(index: int) -> tuple[list[float], list[float]]: segment = segments[index] normal = normals[index] return ( [segment["start"][axis] + thickness * normal[axis] for axis in range(2)], [segment["end"][axis] + thickness * normal[axis] for axis in range(2)], ) def intersection( first_start: list[float], first_end: list[float], second_start: list[float], second_end: list[float], ) -> list[float] | None: first_direction = [first_end[0] - first_start[0], first_end[1] - first_start[1]] second_direction = [second_end[0] - second_start[0], second_end[1] - second_start[1]] denominator = first_direction[0] * second_direction[1] - first_direction[1] * second_direction[0] if abs(denominator) <= 1e-9: return None difference = [second_start[0] - first_start[0], second_start[1] - first_start[1]] scale = (difference[0] * second_direction[1] - difference[1] * second_direction[0]) / denominator return [first_start[axis] + scale * first_direction[axis] for axis in range(2)] previous = (selected_index - 1) % len(segments) following = (selected_index + 1) % len(segments) selected_start, selected_end = shifted_line(selected_index) previous_start, previous_end = shifted_line(previous) following_start, following_end = shifted_line(following) first = intersection(previous_start, previous_end, selected_start, selected_end) second = intersection(selected_start, selected_end, following_start, following_end) if first is None or second is None or _same_point(first, second): return None return _global(source_plane, first), _global(source_plane, second) def _offset_face_profile_sketch( value: Any, feature_frames: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_by_id: dict[str, dict[str, Any]], feature_id: str, ) -> dict[str, Any] | None: """Materialize one planar, linear-extrude OFFSET_FACE wall when proven. An offset face is a generated side wall, not the source extrusion's cap region. Reusing the full source profile therefore changes both the face extent and its topology. The wall is reconstructible only when direct source provenance proves one non-construction edge of a convex linear profile, one finite direct extrusion span, and one removed extrusion cap. All other OFFSET_FACE queries remain an explicit deferred capability rather than receiving a guessed profile. """ query = parse_query(value) frame = feature_frames.get(query.owner_feature or "") or {} profile_id = frame.get("shell_profile_sketch_id") profile = sketches_by_id.get(str(profile_id)) if profile_id else None shell_source = frame.get("shell_source") direct_frame = feature_frames.get(str(shell_source)) if shell_source else None direct_feature = feature_by_id.get(f"f_{shell_source}") if shell_source else None profile_source = (direct_frame or {}).get("profile_source") source_sketch = sketch_by_source.get(str(profile_source)) if profile_source else None refs = _source_refs(value) if ( query.topology_type != "OFFSET_FACE" or profile is None or direct_frame is None or direct_feature is None or direct_feature.get("atomic_id") != "extrude_add_blind" or (direct_feature.get("params") or {}).get("result_mode") != "new_body" or (direct_feature.get("params") or {}).get("draft") is not None or frame.get("shell_inward") is not True or frame.get("shell_removed_cap") not in {"start", "end"} or not isinstance(profile_source, str) or source_sketch is None or not _profile_matches_direct_source(profile, source_sketch) or len(refs) != 1 or refs[0][0] != profile_source or query.source_sketch != profile_source or query.source_entity != refs[0][1] ): return None entity = (entity_by_sketch.get(profile_source) or {}).get(refs[0][1]) if entity is None or entity.get("type") != "line" or entity.get("construction"): return None profile_plane = direct_frame.get("profile") start_plane = direct_frame.get("start") end_plane = direct_frame.get("end") if not all(isinstance(plane, dict) for plane in (profile_plane, start_plane, end_plane)): return None try: source_start, source_end = _convex_linear_offset_wall_endpoints( profile, entity, source_sketch["workplane"], _offset_face_plane(value, feature_frames, sketch_by_source, entity_by_sketch), float(frame["shell_thickness_mm"]), ) or (None, None) if source_start is None or source_end is None: return None line_direction = _unit(_sub(source_end, source_start), "offset face source line is degenerate") span = _sub(end_plane["origin_mm"], start_plane["origin_mm"]) span_direction = _unit(span, "offset face extrusion span is degenerate") profile_normal = _unit(profile_plane["normal"], "offset face extrusion profile normal is degenerate") if ( abs(_dot(line_direction, span_direction)) > 1e-6 or abs(abs(_dot(span_direction, profile_normal)) - 1.0) > 1e-6 ): return None cap_shift = _sub(start_plane["origin_mm"], profile_plane["origin_mm"]) if not all(math.isfinite(component) for point in (source_start, source_end, span, cap_shift) for component in point): return None offset_plane = _offset_face_plane(value, feature_frames, sketch_by_source, entity_by_sketch) except (KeyError, TypeError, ValueError): return None cap_shrink = [float(frame["shell_thickness_mm"]) * component for component in span_direction] start_shift = list(cap_shift) end_shift = [cap_shift[index] + span[index] for index in range(3)] if frame["shell_removed_cap"] == "end": start_shift = [start_shift[index] + cap_shrink[index] for index in range(3)] else: end_shift = [end_shift[index] - cap_shrink[index] for index in range(3)] corners = [ [source_start[index] + start_shift[index] for index in range(3)], [source_end[index] + start_shift[index] for index in range(3)], [source_end[index] + end_shift[index] for index in range(3)], [source_start[index] + end_shift[index] for index in range(3)], ] local_corners = [_local(offset_plane, corner) for corner in corners] if not all(math.isfinite(component) for point in local_corners for component in point): return None segments = [ {"type": "line", "start": local_corners[index], "end": local_corners[(index + 1) % len(local_corners)]} for index in range(len(local_corners)) ] return { "id": f"sketch_{query.owner_feature}__{feature_id}", "name": f"{query.owner_feature}__{feature_id}", "workplane": offset_plane, "profile": {"type": "analytic_contours", "contours": [{"role": "unknown", "closed": True, "segments": segments}]}, } def _query_line( query: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> tuple[list[float], list[float]]: info = parse_query(query) entity, plane, _ = _entity_from_query(query, sketch_by_source, entity_by_sketch) if info.topology_type == "CAP_EDGE" and info.owner_feature in feature_frames: frame_data = feature_frames[info.owner_feature] frame = frame_data["start" if info.is_start else "end"] # CAP outer normals may flip the x direction to preserve the later # sketch attachment handedness. A source sketch edge, however, keeps # its original physical in-plane coordinates at either cap. Preserve # that profile frame and move only its origin to the selected cap. profile = frame_data.get("profile") plane = {**profile, "origin_mm": list(frame["origin_mm"])} if isinstance(profile, dict) else frame if entity["type"] == "circle": center = _global(plane, entity["center"]) return center, [center[index] + frame["normal"][index] for index in range(3)] if info.topology_type == "SWEPT_FACE" and entity["type"] == "circle" and info.owner_feature in feature_frames: frame = feature_frames[info.owner_feature] start, end = frame.get("start"), frame.get("end") if start is None or end is None: raise ValueError("cylindrical swept face frame is unresolved") center = _global(plane, entity["center"]) return [center[index] + start["origin_mm"][index] - plane["origin_mm"][index] for index in range(3)], [center[index] + end["origin_mm"][index] - plane["origin_mm"][index] for index in range(3)] return _entity_line(entity, plane) def _transform_axis( query: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> dict[str, list[float]]: start, end = _query_line(query, feature_frames, sketch_by_source, entity_by_sketch) return {"origin_mm": start, "direction": _unit(_sub(end, start), "transform axis is degenerate")} def _scale_center( value: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> list[float]: """Resolve the explicit center of a CADFS uniform scale. Origin point is a system datum with a known coordinate. Other centers must name exactly one direct sketch or CAP_VERTEX source whose physical point is available in the lowering state. COPY/SWEPT/OFFSET vertices need runtime topology provenance, so treating their nearest visible point as the scale center would change the operation's semantics. """ centers = _queries(value) if len(centers) != 1: raise UnsupportedCapability( "transform_uniform_scale_center", "SCALE_UNIFORMLY requires exactly one explicit scale point", ) center = centers[0] for call in walk_calls(center): if call.name == "qCreatedBy" and call.args and "Origin.pointOp" in symbolic_string(call.args[0]): return [0.0, 0.0, 0.0] info = parse_query(center) if info.kind not in {"vertex", "entitytype.vertex"} or info.topology_type not in {None, "CAP_VERTEX"}: raise UnsupportedCapability( "transform_uniform_scale_center", "SCALE_UNIFORMLY scale point must be Origin point or a direct sketch/CAP_VERTEX", ) try: return _query_point(center, feature_frames, sketch_by_source, entity_by_sketch) except ValueError as error: raise UnsupportedCapability( "transform_uniform_scale_center", "SCALE_UNIFORMLY scale point must resolve to one physical vertex", ) from error def _translation_distance_vector( params: dict[str, Any], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_by_id: dict[str, dict[str, Any]] | None = None, previous: list[str] | None = None, ) -> list[float]: """Resolve the restricted CADFS TRANSLATION_DISTANCE direction contract. An edge direction is only unambiguous when it comes from one source sketch line or its CAP_EDGE descendant. A direct CAP_FACE is also exact when its producer has recorded a physical start/end frame: its selected cap normal is the FeatureScript direction. A generic face normal, swept edge, offset edge, and curve tangent each need different source semantics, so they remain explicit capability gaps instead of borrowing a nearby direction from the active body. """ directions = _queries(params.get("transformDirection")) if len(directions) != 1: raise UnsupportedCapability( "transform_translation_direction", "TRANSLATION_DISTANCE requires exactly one direct linear sketch or CAP_EDGE direction", ) direction_query = directions[0] info = parse_query(direction_query) # System datum planes and previously lowered reference planes have an # explicit physical normal. ``_query_plane`` accepts only those two # qCreatedBy forms here, so this never treats an arbitrary produced face # as a translation direction. if "qCreatedBy" in info.calls and info.topology_type is None: try: direction = _unit( list(_query_plane(direction_query, feature_frames, sketch_by_source, entity_by_sketch)["normal"]), "transform reference-plane normal is degenerate", ) except ValueError as error: raise UnsupportedCapability( "transform_translation_direction", "TRANSLATION_DISTANCE reference-plane direction must have an explicit plane frame", ) from error elif info.kind in {"face", "entitytype.face"} and info.topology_type == "SWEPT_FACE": try: direction = _unit( list(_query_plane(direction_query, feature_frames, sketch_by_source, entity_by_sketch)["normal"]), "transform swept-face normal is degenerate", ) except (UnsupportedCapability, ValueError) as error: raise UnsupportedCapability( "transform_translation_direction", "TRANSLATION_DISTANCE SWEPT_FACE direction requires one direct planar source face", ) from error elif info.kind in {"face", "entitytype.face"} and info.topology_type == "CAP_FACE" and info.is_start is not None: frame = feature_frames.get(info.owner_feature or "") or {} cap = frame.get("start" if info.is_start else "end") try: direction = _unit(list((cap or {}).get("normal") or []), "transform cap-face normal is degenerate") except ValueError as error: raise UnsupportedCapability( "transform_translation_direction", "TRANSLATION_DISTANCE CAP_FACE direction requires a producer with a physical cap frame", ) from error else: if info.kind not in {"edge", "entitytype.edge"} or info.topology_type not in {None, "CAP_EDGE"}: raise UnsupportedCapability( "transform_translation_direction", "TRANSLATION_DISTANCE only supports a linear sketch/CAP_EDGE, exact transform copy, explicit reference plane, direct planar SWEPT_FACE, or framed CAP_FACE direction", ) try: entity, _plane, _token = _entity_from_query(direction_query, sketch_by_source, entity_by_sketch) if entity.get("type") != "line": raise ValueError("transform direction source is not linear") if feature_by_id is not None and previous is not None: start, end = _transform_copy_line( direction_query, feature_frames, sketch_by_source, entity_by_sketch, feature_by_id, previous, ) else: start, end = _query_line(direction_query, feature_frames, sketch_by_source, entity_by_sketch) direction = _unit(_sub(end, start), "transform translation direction is degenerate") except ValueError as error: raise UnsupportedCapability( "transform_translation_direction", "TRANSLATION_DISTANCE direction must resolve to a non-degenerate line", ) from error distance = _number(params.get("distance"), True) if distance < 0: raise UnsupportedCapability( "transform_translation_distance", "TRANSLATION_DISTANCE requires a non-negative distance", ) if _bool(params.get("oppositeDirection")): distance = -distance return [component * distance for component in direction] def _translation_entity_vector( params: dict[str, Any], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_by_id: dict[str, dict[str, Any]] | None = None, previous: list[str] | None = None, ) -> list[float]: """Resolve a direct CADFS TRANSLATION_ENTITY vector without topology guesses. FeatureScript accepts either a line entity, whose endpoint delta is the translation vector, or two vertices interpreted in selection order. This restricted lowering accepts raw sketch/CAP descendants and exact copies made by explicit single-source transforms. Other COPY/SWEPT/OFFSET geometry requires a kernel-proven successor relation. """ entities = _queries(params.get("transformLine")) if len(entities) == 1: line = entities[0] info = parse_query(line) if info.kind not in {"edge", "entitytype.edge"} or info.topology_type not in {None, "CAP_EDGE"}: raise UnsupportedCapability( "transform_translation_entity", "TRANSLATION_ENTITY requires a linear sketch/CAP_EDGE or its exact transform copy", ) try: entity, _plane, _token = _entity_from_query(line, sketch_by_source, entity_by_sketch) if entity.get("type") != "line": raise ValueError("transform line source is not linear") if feature_by_id is not None and previous is not None: start, end = _transform_copy_line( line, feature_frames, sketch_by_source, entity_by_sketch, feature_by_id, previous, ) else: start, end = _query_line(line, feature_frames, sketch_by_source, entity_by_sketch) except ValueError as error: raise UnsupportedCapability( "transform_translation_entity", "TRANSLATION_ENTITY line must resolve to a non-degenerate line", ) from error elif len(entities) == 2: first, second = entities first_info, second_info = parse_query(first), parse_query(second) if ( first_info.kind not in {"vertex", "entitytype.vertex"} or second_info.kind not in {"vertex", "entitytype.vertex"} or first_info.topology_type not in {None, "CAP_VERTEX"} or second_info.topology_type not in {None, "CAP_VERTEX"} ): raise UnsupportedCapability( "transform_translation_entity", "TRANSLATION_ENTITY requires exactly two sketch/CAP_VERTEX points or their exact transform copies", ) try: if feature_by_id is not None and previous is not None: start = _transform_copy_point( first, feature_frames, sketch_by_source, entity_by_sketch, feature_by_id, previous, ) end = _transform_copy_point( second, feature_frames, sketch_by_source, entity_by_sketch, feature_by_id, previous, ) else: start = _query_point(first, feature_frames, sketch_by_source, entity_by_sketch) end = _query_point(second, feature_frames, sketch_by_source, entity_by_sketch) except ValueError as error: raise UnsupportedCapability( "transform_translation_entity", "TRANSLATION_ENTITY vertices must resolve to unique points", ) from error else: raise UnsupportedCapability( "transform_translation_entity", "TRANSLATION_ENTITY requires one direct line or exactly two direct vertices", ) vector = _sub(end, start) if math.sqrt(sum(component * component for component in vector)) <= 1e-9: raise UnsupportedCapability("transform_translation_entity", "TRANSLATION_ENTITY vector is degenerate") if _bool(params.get("oppositeDirectionEntity")): vector = [-component for component in vector] return vector def _bake_transform( params: dict[str, Any], previous: list[str], feature_by_id: dict[str, dict[str, Any]], feature_source_by_id: dict[str, str], sketches_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> None: if _bool(params.get("makeCopy")): raise UnsupportedCapability("transform", "current CDSL engine cannot exactly copy transformed CADFS source bodies") sources = _transform_source_features(params.get("entities"), previous) if len(sources) != 1: raise UnsupportedCapability("transform", "current CDSL engine cannot exactly transform multiple selected CADFS source bodies") source_id = sources[0]; source = feature_by_id.get(source_id) if source is None or source.get("atomic_id") not in {"extrude_add_blind", "extrude_add_two_sided", "revolve_add"}: raise UnsupportedCapability("transform", "current CDSL engine can only bake a direct additive extrusion or revolve transform") sketch = sketches_by_id.get(str(source.get("sketch_id") or "")) source_feature_id = feature_source_by_id.get(source_id) if sketch is None or source_feature_id is None: raise UnsupportedCapability("transform", "current CDSL engine cannot resolve the transformed source feature geometry") transform_type = str(params.get("transformType") or "").split(".")[-1].upper() if transform_type == "TRANSLATION_3D": offset = [_number(params.get(key, 0.0), True) for key in ("dx", "dy", "dz")] transform_frame = lambda frame: _translate_frame(frame, offset) transform_axis = lambda axis: {**axis, "origin_mm": [axis["origin_mm"][index] + offset[index] for index in range(3)]} elif transform_type == "TRANSLATION_DISTANCE": offset = _translation_distance_vector(params, feature_frames, sketch_by_source, entity_by_sketch) transform_frame = lambda frame: _translate_frame(frame, offset) transform_axis = lambda axis: {**axis, "origin_mm": [axis["origin_mm"][index] + offset[index] for index in range(3)]} elif transform_type == "TRANSLATION_ENTITY": # Baking is only semantics-preserving while the selected NEW result # has not been absorbed or changed by another body-mutating feature. if source.get("params", {}).get("result_mode") != "new_body" or previous[-1:] != [source_id]: raise UnsupportedCapability( "transform_body_lifecycle", "TRANSLATION_ENTITY bake requires an immediately preceding independent NEW body", ) offset = _translation_entity_vector(params, feature_frames, sketch_by_source, entity_by_sketch) transform_frame = lambda frame: _translate_frame(frame, offset) transform_axis = lambda axis: {**axis, "origin_mm": [axis["origin_mm"][index] + offset[index] for index in range(3)]} elif transform_type == "ROTATION": axis = _transform_axis(params.get("transformAxis"), feature_frames, sketch_by_source, entity_by_sketch) angle_rad = math.radians(_number(params.get("angle"), True)) transform_frame = lambda frame: _rotate_frame(frame, axis, angle_rad) transform_axis = lambda value: { **value, "origin_mm": _rotate_point(value["origin_mm"], axis, angle_rad), "direction": _rotate(value["direction"], axis["direction"], angle_rad), } else: raise UnsupportedCapability("transform", f"current CDSL engine cannot exactly bake {transform_type or 'unknown'} transform") sketch["workplane"] = transform_frame(sketch["workplane"]) frame = feature_frames.get(source_feature_id) if frame is not None: transformed = {key: transform_frame(value) for key, value in frame.items() if key in {"start", "end", "profile"}} for cap in ("start", "end"): if cap in transformed: transformed[f"{cap}_attachment"] = _attachment_plane(transformed[cap]) if isinstance(frame.get("revolve_axis"), dict): transformed["revolve_axis"] = transform_axis(frame["revolve_axis"]) if "revolve_full" in frame: transformed["revolve_full"] = frame["revolve_full"] feature_frames[source_feature_id] = transformed source_axis = source.get("params", {}).get("axis") if isinstance(source_axis, dict) and source_axis.get("origin_mm") and source_axis.get("direction"): source["params"]["axis"] = transform_axis(source_axis) def _record_non_copy_body_successors( aliases: dict[str, str], source_ids: list[str], successor_id: str, ) -> None: """Bind direct transform sources to their latest physical body member. A non-copy transform replaces exactly the selected independent members in the runtime. Preserve that one-to-one lifecycle fact for later CADFS queries that retain the original producer ID. This deliberately has no fallback for fused/dress-up/pattern members because those are not entered into ``aliases`` by lowering. """ for source in source_ids: for owner, current in list(aliases.items()): if current == source: aliases[owner] = successor_id aliases[source] = successor_id _SINGLE_BODY_FUSING_ATOMICS = frozenset({ "extrude_add_blind", "extrude_add_blind_with_hole", "extrude_add_two_sided", "extrude_from_face", "loft_add", "loft_add_with_cap_face", "sweep_add", "revolve_add", }) _SINGLE_BODY_DRESSUP_ATOMICS = frozenset({"fillet", "chamfer", "shell"}) _SINGLE_BODY_CUT_ATOMICS = frozenset({ "extrude_cut_blind", "extrude_cut_two_sided", "revolve_cut", "hole_blind", "hole_countersink", "hole_counterbore", "hole_wizard", "thread_cut", }) _SINGLE_BODY_NON_MUTATING_ATOMICS = frozenset({"reference_plane", "reference_axis", "extrude_surface", "revolve_surface"}) _LOWERING_BODY_MUTATING_ATOMICS = frozenset({ "extrude_add_blind", "extrude_add_blind_with_hole", "extrude_add_two_sided", "extrude_cut_blind", "extrude_cut_two_sided", "extrude_cut_through", "extrude_from_face", "loft_add", "loft_add_with_cap_face", "sweep_add", "revolve_add", "revolve_cut", "sphere_add", "box_add", "cylinder_add", "thread_add", "thread_cut", "bend_add", "hole_blind", "hole_countersink", "hole_counterbore", "hole_wizard", "fillet", "chamfer", "shell", "boolean_bodies", }) _LOWERING_CUT_ATOMICS = frozenset({ "extrude_cut_blind", "extrude_cut_two_sided", "extrude_cut_through", "revolve_cut", "thread_cut", "hole_blind", "hole_countersink", "hole_counterbore", "hole_wizard", }) _LOWERING_PRIMARY_ATOMICS = frozenset({ "extrude_add_blind", "extrude_add_blind_with_hole", "extrude_add_two_sided", "extrude_from_face", "loft_add", "loft_add_with_cap_face", "sweep_add", "revolve_add", "sphere_add", "box_add", "cylinder_add", "thread_add", "bend_add", }) def _record_lowered_body_members(members: set[str], feature: dict[str, Any]) -> None: """Mirror the runtime's independently selectable body-member contract. This projection is intentionally narrower than geometric body ownership. It only decides whether a later direct CADFS SWEPT_BODY can be emitted as a CDSL shell target. No aggregate/current-body fallback is permitted here. """ feature_id = str(feature["id"]) atomic_id = str(feature.get("atomic_id") or "") params = feature.get("params") or {} if atomic_id == "boolean_bodies": targets = {str(value) for value in params.get("target_feature_ids") or ()} tools = {str(value) for value in params.get("tool_feature_ids") or ()} members.difference_update(targets | tools) members.add(feature_id) if bool(params.get("keep_tools")): members.update(tools) return if atomic_id == "transform_bodies": sources = {str(value) for value in params.get("source_feature_ids") or ()} if bool(params.get("make_copy")): if len(sources) == 1: members.add(feature_id) return members.difference_update(sources) members.add(feature_id) return if atomic_id == "delete_bodies": members.difference_update(str(value) for value in params.get("target_feature_ids") or ()) return if atomic_id in {"pattern_linear", "pattern_mirror", "pattern_circular"}: sources = {str(value) for value in params.get("source_feature_ids") or ()} if not ( atomic_id == "pattern_circular" and str(params.get("operation_mode") or "add") == "add" and sources and sources <= members ): members.clear() return if atomic_id not in _LOWERING_BODY_MUTATING_ATOMICS: return if atomic_id in _LOWERING_CUT_ATOMICS or ( atomic_id == "extrude_from_face" and params.get("operation") == "cut" ): return if atomic_id in _LOWERING_PRIMARY_ATOMICS and params.get("result_mode") == "new_body": members.add(feature_id) return members.clear() members.add(feature_id) def _clear_single_body_successor_state( aliases: dict[str, str], state: dict[str, Any], ) -> None: """Discard only aliases derived from the restricted aggregate lineage.""" for source in state["sources"]: aliases.pop(source, None) state["owner"] = None state["sources"] = set() def _record_single_body_successor( aliases: dict[str, str], state: dict[str, Any], feature: dict[str, Any], ) -> None: """Track one CADFS body through exact single-aggregate successors. A ``SWEPT_BODY`` query names a CADFS body object, not a frozen feature result. When an ordinary additive feature or dress-up mutates the only active body, the original body query still denotes that same physical body. Runtime collapses those operations to one explicit body member, so lowering may follow the successor only while this state machine mirrors that one-member lifecycle exactly. Multi-body, boolean, pattern, delete, and other body-changing paths intentionally clear the proof rather than substituting ``session.body``. """ feature_id = str(feature["id"]) atomic_id = str(feature.get("atomic_id") or "") params = feature.get("params") or {} owner = state["owner"] sources: set[str] = state["sources"] def advance() -> None: for source in sources: if source != feature_id: aliases[source] = feature_id aliases.pop(feature_id, None) sources.add(feature_id) state["owner"] = feature_id if atomic_id in _SINGLE_BODY_NON_MUTATING_ATOMICS: return if atomic_id in _SINGLE_BODY_CUT_ATOMICS: # Runtime preserves the selected member keys for a cut. The cut # feature itself is not a new independently selectable body member. return if atomic_id in _SINGLE_BODY_FUSING_ATOMICS: if atomic_id == "extrude_from_face" and params.get("operation") == "cut": return if params.get("result_mode") == "new_body": if owner is None: state["owner"] = feature_id sources.add(feature_id) else: # A new body does not mutate the preceding member. Existing # aliases still identify that member exactly (for example a # later SWEPT_BODY query of a shell's original direct-prism # owner), even though there is no longer one aggregate body # through which a future ordinary ADD may advance them. # Detach the single-body state without erasing those proven # member aliases. state["owner"] = None state["sources"] = set() return if owner is None: state["owner"] = feature_id sources.add(feature_id) else: advance() return if atomic_id in _SINGLE_BODY_DRESSUP_ATOMICS: if owner is not None: advance() return if atomic_id == "transform_bodies": source_ids = [str(value) for value in params.get("source_feature_ids") or ()] if bool(params.get("make_copy")): # The original member remains addressable, but the aggregate is no # longer a one-body lifecycle. Do not let a later ordinary ADD or # dress-up advance an alias across that unproven split. _clear_single_body_successor_state(aliases, state) return if owner is not None and source_ids == [owner] and not params.get("pattern_instance_refs"): advance() return _clear_single_body_successor_state(aliases, state) return if atomic_id == "pattern_circular": # A direct circular ADD over the sole current member takes the runtime # body-member path: it preserves that member and exposes each rotated # copy under an exact instance key. Keep the already-proven aliases so # a following CADFS COPY(SWEPT_BODY) resolves to the same member. source_ids = [str(value) for value in params.get("source_feature_ids") or ()] if ( owner is not None and str(params.get("operation_mode") or "add") == "add" and source_ids == [owner] ): return # The remaining body atomics either split ownership, select explicit # members, or replay feature geometry. Their CADFS body continuation is # not represented by this restricted one-member contract. _clear_single_body_successor_state(aliases, state) def _query_point( query: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> list[float]: info = parse_query(query) cpoint_frame = feature_frames.get(info.owner_feature or "") if ( info.topology_type is None and info.kind in {"vertex", "entitytype.vertex"} and "qCreatedBy" in info.calls and isinstance(cpoint_frame, dict) and isinstance(cpoint_frame.get("point_mm"), list) ): return list(cpoint_frame["point_mm"]) if info.topology_type == "CAP_VERTEX" and info.owner_feature in feature_frames: frame_data = feature_frames[info.owner_feature] cap = frame_data["start" if info.is_start else "end"] profile = frame_data.get("profile") plane = {**profile, "origin_mm": list(cap["origin_mm"])} if isinstance(profile, dict) else cap references = _source_refs(query) if len(references) >= 2: lines = [] for source, token in references: available = entity_by_sketch.get(source) or {} entity = available.get(token) if entity is None: entity_id = max((key for key in available if token.startswith(key + ".")), key=len, default="") entity = available.get(entity_id) if entity and entity.get("type") == "line": lines.append(_entity_line(entity, plane)) if len(lines) >= 2: pairs = [(math.dist(left, right), left) for left in lines[0] for right in lines[1]] distance, point = min(pairs, key=lambda item: item[0]) if distance <= 1e-5: return point entity, plane, token = _entity_from_query(query, sketch_by_source, entity_by_sketch) if info.topology_type == "CAP_VERTEX" and info.owner_feature in feature_frames: frame_data = feature_frames[info.owner_feature] cap = frame_data["start" if info.is_start else "end"] profile = frame_data.get("profile") plane = {**profile, "origin_mm": list(cap["origin_mm"])} if isinstance(profile, dict) else cap return _entity_point(entity, plane, token) def _featurescript_perpendicular_vector(value: list[float]) -> list[float]: """Match FeatureScript 1511's deterministic ``perpendicularVector``. ``LINE_ANGLE`` with one selected axis has no second reference that can determine its zero-angle direction. The standard library deliberately supplies one with ``perpendicularVector``; using an arbitrary local frame here changes subsequent sketch coordinates. Keep the same branch thresholds as the 1511 implementation instead of deriving a direction from a current body or a topology hint. """ direction = _unit(value, "line-angle reference axis is degenerate") if abs(direction[0]) > 1.036663652861932668633 * abs(direction[1]): different = [0.0, 0.0, 1.0] if abs(direction[0]) > .951702989392233451722 * abs(direction[2]) else [0.0, 1.0, 0.0] else: different = [1.0, 0.0, 0.0] if abs(direction[1]) > .920419947455385938102 * abs(direction[2]) else [0.0, 1.0, 0.0] return _unit(_cross(different, direction), "line-angle reference axis is degenerate") def _direct_line_angle_source_entity( query: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> tuple[dict[str, Any], dict[str, Any], str] | None: """Resolve one exact sketch entity without widening it to topology. ``LINE_ANGLE`` accepts direct sketch construction geometry as well as profile geometry. The source entity key must therefore match exactly; suffix matching would incorrectly turn a trim, CAP, SWEPT, COPY, or other derived query into an original sketch reference. """ info = parse_query(query) direct_calls = {"sQuery", "sketchEntityQuery", "__binary__"} if ( info.topology_type is not None or not info.calls or any(call not in direct_calls for call in info.calls) or not isinstance(info.source_sketch, str) or not isinstance(info.source_entity, str) ): return None sketch = sketch_by_source.get(info.source_sketch) entity = (entity_by_sketch.get(info.source_sketch) or {}).get(info.source_entity) if sketch is None or entity is None: return None return entity, sketch["workplane"], info.source_entity def _derived_line_angle_cylinder_axis( query: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> tuple[dict[str, Any], dict[str, Any], str] | None: """Accept the legacy derived-cylinder contract without widening it. The older LINE_ANGLE compatibility path is not a general fallback for a query which happens to mention a source line. It is limited to a direct source circle selected as a CAP_EDGE or SWEPT_FACE of an operation whose recorded frame can prove its cylinder axis. """ try: _call, owner, topology, kind, _definition = _direct_make_query(query) except ValueError: return None if ( (topology == "CAP_EDGE" and kind != "edge") or (topology == "SWEPT_FACE" and kind != "face") or topology not in {"CAP_EDGE", "SWEPT_FACE"} ): return None info = parse_query(query) if info.owner_feature != owner or not isinstance(info.source_sketch, str) or not isinstance(info.source_entity, str): return None references = _source_refs(query) if references != [(info.source_sketch, info.source_entity)]: return None resolved = _source_ref_entity(info.source_sketch, info.source_entity, entity_by_sketch) sketch = sketch_by_source.get(info.source_sketch) frame = feature_frames.get(owner) if ( resolved is None or sketch is None or frame is None or resolved[0] != info.source_entity or resolved[1].get("type") != "circle" or not isinstance(frame.get("start"), dict) or not isinstance(frame.get("end"), dict) ): return None return resolved[1], sketch["workplane"], info.source_entity def _direct_line_angle_axis( query: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> tuple[list[float], list[float]] | None: """Return the exact ``evAxis`` result for a direct sketch line/circle. FeatureScript's ``lineAnglePlane`` passes selected axes to ``evAxis``. A direct ``skLineSegment`` and a direct ``skCircle`` have unique source axes. This does not infer an axis from a resulting edge: CAP/SWEPT/COPY and suffix-derived references stay out of this source-only path. """ info = parse_query(query) if info.kind not in {"edge", "entitytype.edge"}: return None direct = _direct_line_angle_source_entity(query, sketch_by_source, entity_by_sketch) if direct is None: return None entity, plane, _token = direct if entity.get("type") == "line": start, end = _entity_line(entity, plane) return start, _unit(_sub(end, start), "line-angle reference axis is degenerate") if entity.get("type") == "circle": center = entity.get("center") if not isinstance(center, list) or len(center) != 2: return None return _global(plane, center), _unit(list(plane["normal"]), "line-angle reference axis is degenerate") return None def _direct_prism_line_angle_swept_edge_axis( query: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], *, previous: list[str], featurescript_version: str | None, ) -> tuple[list[float], list[float]] | None: """Return a source-determined datum axis for one direct prism SWEPT_EDGE. This is deliberately a datum calculation, not a topology resolver. A direct blind prism turns one explicitly identified source-profile vertex into the line connecting its two cap copies. The producer frame records that its profile was unchanged, so the two source curves and the prism span determine that line without asking the current body for an edge. """ if featurescript_version != "1511": return None try: _call, owner, topology, kind, _definition = _direct_make_query(query) except ValueError: return None producer_id = f"f_{owner}" frame = feature_frames.get(owner) or {} profile_source = frame.get("profile_source") if ( topology != "SWEPT_EDGE" or kind not in {"edge", "entitytype.edge"} or previous[-1:] != [producer_id] or frame.get("direct_prism_line_angle_axis") is not True or not isinstance(profile_source, str) or profile_source not in sketch_by_source or not isinstance(frame.get("profile"), dict) or not isinstance(frame.get("start"), dict) or not isinstance(frame.get("end"), dict) ): return None refs = _source_refs(query) if len(refs) != 2 or {source for source, _token in refs} != {profile_source}: return None source_ids = set(frame.get("direct_prism_source_entity_ids") or ()) resolved_ids: set[str] = set() for source, token in refs: resolved = _source_ref_entity(source, token, entity_by_sketch) if resolved is None: return None entity_id, entity = resolved if entity.get("construction") or entity_id not in source_ids: return None resolved_ids.add(entity_id) if len(resolved_ids) != 2: return None local = _shared_source_endpoint(refs, profile_source, entity_by_sketch) if local is None: return None try: source_point = _global(sketch_by_source[profile_source]["workplane"], local) profile_origin = frame["profile"]["origin_mm"] start_origin = frame["start"]["origin_mm"] end_origin = frame["end"]["origin_mm"] start = [source_point[index] + start_origin[index] - profile_origin[index] for index in range(3)] direction = _unit(_sub(end_origin, start_origin), "line-angle swept-edge axis is degenerate") except (KeyError, TypeError, ValueError): return None if not all(math.isfinite(component) for component in start + direction): return None return start, direction def _direct_line_angle_wire_axis( query: Any, sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], *, featurescript_version: str | None, standard_library: str | None, standard_library_version: str | None, ) -> tuple[list[float], list[float]] | None: """Return one exact source-wire line axis for ``LINE_ANGLE``. This interprets only one direct ``qBodyType(qCreatedBy(sketch, EDGE), WIRE)`` source query. It does not expose a runtime ``qBodyType`` selector: one line must be named from one source sketch, with no derived topology or current-body information. Construction lines are valid datum axes, unlike sweep paths. """ selection = _direct_sketch_wire_selection( Call("qUnion", [[query]]), sketch_by_source, entity_by_sketch, featurescript_version=featurescript_version, standard_library=standard_library, standard_library_version=standard_library_version, permit_construction=True, ) if selection is None: return None _source, candidates, sketch = selection if len(candidates) != 1 or candidates[0][1].get("type") != "line": return None _entity_id, entity = candidates[0] start, end = _entity_line(entity, sketch["workplane"]) return start, _unit(_sub(end, start), "line-angle reference axis is degenerate") def _direct_line_angle_reference_plane( query: Any, feature_frames: dict[str, dict[str, Any]], ) -> dict[str, Any] | None: """Resolve only a default plane or an already-lowered reference plane.""" default = _default_plane(query) if default is not None: return default info = parse_query(query) if ( info.topology_type is not None or "qCreatedBy" not in info.calls or not info.owner_feature ): return None frame = feature_frames.get(info.owner_feature) if not isinstance(frame, dict) or frame.get("start") != frame.get("end"): return None start = frame.get("start") return dict(start) if isinstance(start, dict) else None def _direct_line_angle_reference_point( query: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> list[float] | None: """Resolve one explicit datum point or direct sketch point/endpoint.""" info = parse_query(query) cpoint_frame = feature_frames.get(info.owner_feature or "") if ( info.topology_type is None and info.kind in {"vertex", "entitytype.vertex"} and "qCreatedBy" in info.calls and isinstance(cpoint_frame, dict) and isinstance(cpoint_frame.get("point_mm"), list) ): return list(cpoint_frame["point_mm"]) if ( info.topology_type is not None or info.kind not in {"vertex", "entitytype.vertex"} or not isinstance(info.source_sketch, str) or not isinstance(info.source_entity, str) ): return None sketch = sketch_by_source.get(info.source_sketch) resolved = _source_ref_entity(info.source_sketch, info.source_entity, entity_by_sketch) if sketch is None or resolved is None: return None entity_id, entity = resolved token = info.source_entity if entity.get("type") == "point" and token == entity_id: return _entity_point(entity, sketch["workplane"], token) if entity.get("type") == "line" and token in {f"{entity_id}.start", f"{entity_id}.end"}: return _entity_point(entity, sketch["workplane"], token) return None def _direct_line_angle_two_entity_plane( entities: list[Any], params: dict[str, Any], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], *, previous: list[str], featurescript_version: str | None, standard_library: str | None, standard_library_version: str | None, ) -> dict[str, Any] | None: """Implement FeatureScript's exact two-entity ``lineAnglePlane`` rule. The first entity normally supplies the axis. If it has no direct axis and the second entity does, FeatureScript swaps the axis and treats the original first entity as the plane/point reference. Parallel axes use their origin-to-origin vector; nonparallel axes use the second direction. """ if len(entities) != 2: return None first_axis = ( _direct_line_angle_axis(entities[0], sketch_by_source, entity_by_sketch) or _direct_prism_line_angle_swept_edge_axis( entities[0], feature_frames, sketch_by_source, entity_by_sketch, previous=previous, featurescript_version=featurescript_version, ) or _direct_line_angle_wire_axis( entities[0], sketch_by_source, entity_by_sketch, featurescript_version=featurescript_version, standard_library=standard_library, standard_library_version=standard_library_version, ) ) second_axis = ( _direct_line_angle_axis(entities[1], sketch_by_source, entity_by_sketch) or _direct_prism_line_angle_swept_edge_axis( entities[1], feature_frames, sketch_by_source, entity_by_sketch, previous=previous, featurescript_version=featurescript_version, ) or _direct_line_angle_wire_axis( entities[1], sketch_by_source, entity_by_sketch, featurescript_version=featurescript_version, standard_library=standard_library, standard_library_version=standard_library_version, ) ) if first_axis is None: if second_axis is None: return None axis_origin, axis_direction = second_axis reference = entities[0] reference_axis = None else: axis_origin, axis_direction = first_axis reference = entities[1] reference_axis = second_axis if reference_axis is not None: reference_origin, reference_direction = reference_axis if math.sqrt(_dot(_cross(axis_direction, reference_direction), _cross(axis_direction, reference_direction))) <= 1e-9: second_in_plane_direction = _sub(reference_origin, axis_origin) else: second_in_plane_direction = reference_direction else: reference_plane = _direct_line_angle_reference_plane(reference, feature_frames) if reference_plane is not None: second_in_plane_direction = _cross(axis_direction, reference_plane["normal"]) else: reference_point = _direct_line_angle_reference_point( reference, feature_frames, sketch_by_source, entity_by_sketch, ) if reference_point is None: return None second_in_plane_direction = _sub(reference_point, axis_origin) normal = _cross(axis_direction, second_in_plane_direction) _unit(normal, "line-angle reference selection is degenerate") signed_angle = _number(params.get("angle", 0.0)) if _bool(params.get("oppositeDirection")): signed_angle = -signed_angle return _frame( axis_origin, axis_direction, _rotate(normal, axis_direction, math.radians(signed_angle)), ) def _cpoint_parameter(params: dict[str, Any]) -> float: """Read the finite source-line parameter shared by every datum form.""" parameter = _number(params.get("parameter")) if not math.isfinite(parameter) or not 0.0 <= parameter <= 1.0: raise UnsupportedCapability("reference_point", "cPoint parameter must be a finite value in [0, 1]") return parameter def _direct_source_line_cpoint( params: dict[str, Any], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], ) -> list[float]: """Evaluate one `cPoint` on a direct source-sketch line. This is source datum geometry, not a runtime edge selector. FeatureScript parameterizes a line segment affinely, so the source endpoint coordinates and a finite unit-interval parameter completely determine the point. Derived CAP/SWEPT edges and curved source entities need their own contracts. """ entities = _queries(params.get("entities")) if len(entities) != 1: raise UnsupportedCapability("reference_point", "cPoint requires exactly one direct source line") query = parse_query(entities[0]) if ( query.topology_type is not None or query.kind not in {"edge", "entitytype.edge"} or not isinstance(query.source_sketch, str) or not isinstance(query.source_entity, str) ): raise UnsupportedCapability("reference_point", "cPoint source must be one direct sketch line") resolved = _source_ref_entity(query.source_sketch, query.source_entity, entity_by_sketch) sketch = sketch_by_source.get(query.source_sketch) if resolved is None or sketch is None: raise ValueError("cPoint source line is unresolved") entity_id, entity = resolved if entity_id != query.source_entity or entity.get("type") != "line": raise UnsupportedCapability("reference_point", "cPoint only supports an original unsuffixed source line") parameter = _cpoint_parameter(params) start, end = _entity_line(entity, sketch["workplane"]) return [start[index] + parameter * (end[index] - start[index]) for index in range(3)] def _direct_prism_swept_edge_cpoint( value: Any, params: dict[str, Any], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], *, previous: list[str], featurescript_version: str | None, ) -> list[float] | None: """Evaluate a source-defined direct-prism SWEPT_EDGE datum point. The LINE_ANGLE datum bridge proves the source vertex and prism span. cPoint interpolates that source-defined span; it never resolves a runtime edge or inspects the resulting body. """ try: _call, owner, topology, _kind, _definition = _direct_make_query(value) except ValueError: return None if topology != "SWEPT_EDGE": return None axis = _direct_prism_line_angle_swept_edge_axis( value, feature_frames, sketch_by_source, entity_by_sketch, previous=previous, featurescript_version=featurescript_version, ) frame = feature_frames.get(owner) or {} start_frame, end_frame = frame.get("start"), frame.get("end") if ( axis is None or not isinstance(start_frame, dict) or not isinstance(end_frame, dict) or not isinstance(start_frame.get("origin_mm"), list) or not isinstance(end_frame.get("origin_mm"), list) ): return None parameter = _cpoint_parameter(params) start, _direction = axis span = _sub(end_frame["origin_mm"], start_frame["origin_mm"]) return [start[index] + parameter * span[index] for index in range(3)] def _direct_prism_cap_edge_cpoint( value: Any, params: dict[str, Any], *, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> list[float] | None: """Evaluate one direct-prism CAP_EDGE datum from its source line and role. Source datum points remain valid through earlier cPoint/reference-plane features, but no body-mutating feature may intervene. The cap is not looked up from the current B-rep. """ if featurescript_version != "1511": return None try: _call, owner, topology, kind, _definition = _direct_make_query(value) except ValueError: return None query = parse_query(value) producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) or {} producer_params = producer.get("params") or {} frame = feature_frames.get(owner) or {} profile_source = frame.get("profile_source") profile_sketch = sketches_by_id.get(str(producer.get("sketch_id") or "")) source_sketch = sketch_by_source.get(profile_source) if isinstance(profile_source, str) else None if producer_id not in previous: return None producer_index = previous.index(producer_id) intervening = previous[producer_index + 1:] if ( topology != "CAP_EDGE" or kind not in {"edge", "entitytype.edge"} or query.is_start is None or any((feature_by_id.get(feature_id) or {}).get("atomic_id") not in {"reference_plane", "reference_point"} for feature_id in intervening) or producer.get("atomic_id") != "extrude_add_blind" or producer_params.get("result_mode") != "new_body" or (producer_params.get("end_condition") or {}).get("type") != "blind" or producer_params.get("draft") is not None or not isinstance(profile_source, str) or source_sketch is None or profile_sketch is None or profile_sketch.get("source_sketch_id") != profile_source or not _profile_matches_direct_source(profile_sketch, source_sketch) ): return None refs = _source_refs(value) if len(refs) != 1 or refs[0][0] != profile_source or query.source_entity != refs[0][1]: return None resolved = _source_ref_entity(profile_source, refs[0][1], entity_by_sketch) source_ids = _direct_profile_source_entity_ids(profile_sketch) if resolved is None: return None entity_id, entity = resolved if ( entity_id != refs[0][1] or entity_id not in source_ids or entity.get("construction") or entity.get("type") != "line" ): return None cap = frame.get("start" if query.is_start else "end") profile = frame.get("profile") if ( not isinstance(cap, dict) or not isinstance(profile, dict) or not isinstance(cap.get("origin_mm"), list) or not isinstance(profile.get("origin_mm"), list) ): return None parameter = _cpoint_parameter(params) start, end = _entity_line(entity, source_sketch["workplane"]) source_point = [start[index] + parameter * (end[index] - start[index]) for index in range(3)] return [source_point[index] + cap["origin_mm"][index] - profile["origin_mm"][index] for index in range(3)] def _cpoint_datum( params: dict[str, Any], *, feature_by_id: dict[str, dict[str, Any]], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], previous: list[str], featurescript_version: str | None, ) -> list[float]: """Select the only source-datum cPoint forms presently proven by source.""" entities = _queries(params.get("entities")) if len(entities) != 1: raise UnsupportedCapability("reference_point", "cPoint requires exactly one direct source line") query = entities[0] topology = parse_query(query).topology_type if topology is None: return _direct_source_line_cpoint(params, sketch_by_source, entity_by_sketch) point = ( _direct_prism_swept_edge_cpoint( query, params, feature_frames, sketch_by_source, entity_by_sketch, previous=previous, featurescript_version=featurescript_version, ) if topology == "SWEPT_EDGE" else _direct_prism_cap_edge_cpoint( query, params, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=featurescript_version, ) if topology == "CAP_EDGE" else None ) if point is None: raise UnsupportedCapability("reference_point", "cPoint datum source is unsupported") return point def _cplane( params: dict[str, Any], feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], *, feature_by_id: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], previous: list[str], featurescript_version: str | None = None, standard_library: str | None = None, standard_library_version: str | None = None, ) -> dict[str, Any]: plane_type = str(params.get("cplaneType") or "OFFSET").split(".")[-1].upper() entities = _queries(params.get("entities")) def datum_point(query: Any) -> list[float]: # The origin point is an explicit system datum. A CAP_VERTEX is not: # it must satisfy the direct source-prism contract above rather than # falling through to the legacy frame-based point reconstruction. if any( call.name == "qCreatedBy" and call.args and "Origin.pointOp" in symbolic_string(call.args[0]) for call in walk_calls(query) ): return [0.0, 0.0, 0.0] if parse_query(query).topology_type == "CAP_VERTEX": point = _direct_prism_cap_vertex_datum_point( query, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=featurescript_version, ) if point is None: raise ValueError("CAP_VERTEX datum source is unsupported") return point return _query_point(query, feature_frames, sketch_by_source, entity_by_sketch) if plane_type == "OFFSET": offset = _number(params.get("offset", 0), True) # CPlane OFFSET preserves the source plane's local frame. CADFS uses # oppositeDirection only to select the other signed offset side. if _bool(params.get("oppositeDirection")): offset = -offset return _shift_plane(_query_plane(entities[0], feature_frames, sketch_by_source, entity_by_sketch), offset) if plane_type == "LINE_ANGLE": # ``lineAnglePlane`` resolves the direct source contract before any # topology-derived workplane compatibility path. In particular, a # two-entity plane is defined by the axis plus the second entity's # direction, not by rotating an arbitrary source workplane frame. if len(entities) == 1: direct_axis = _direct_line_angle_axis( entities[0], sketch_by_source, entity_by_sketch, ) or _direct_prism_line_angle_swept_edge_axis( entities[0], feature_frames, sketch_by_source, entity_by_sketch, previous=previous, featurescript_version=featurescript_version, ) or _direct_line_angle_wire_axis( entities[0], sketch_by_source, entity_by_sketch, featurescript_version=featurescript_version, standard_library=standard_library, standard_library_version=standard_library_version, ) if direct_axis is not None: origin, axis = direct_axis signed_angle = _number(params.get("angle", 0.0)) if _bool(params.get("oppositeDirection")): signed_angle = -signed_angle base_normal = _featurescript_perpendicular_vector(axis) normal = _rotate(base_normal, axis, math.radians(signed_angle)) return _frame(origin, axis, normal) if len(entities) == 2: direct_plane = _direct_line_angle_two_entity_plane( entities, params, feature_frames, sketch_by_source, entity_by_sketch, previous=previous, featurescript_version=featurescript_version, standard_library=standard_library, standard_library_version=standard_library_version, ) if direct_plane is not None: return direct_plane # The retained compatibility path is specifically a cylinder # generatrix contract. Do not use it as a generic derived-topology # fallback: a CAP/SWEPT line or a query combinator has different # FeatureScript provenance and needs its own source proof. candidates = [ item for item in entities if _derived_line_angle_cylinder_axis( item, feature_frames, sketch_by_source, entity_by_sketch, ) is not None ] if len(entities) != 2 or len(candidates) != 1: raise ValueError("line-angle reference selection is unsupported") line_query = candidates[0] base_query = next(item for item in entities if item is not line_query) base = _direct_line_angle_reference_plane(base_query, feature_frames) if base is None: raise ValueError("line-angle cylinder reference plane is unsupported") start, end = _query_line(line_query, feature_frames, sketch_by_source, entity_by_sketch) axis = _sub(end, start); angle = -_number(params.get("angle", 0.0)) opposite = _bool(params.get("oppositeDirection")) normal = _rotate(base["normal"], axis, math.radians(angle)) x_dir = _rotate(base["x_dir"], axis, math.radians(angle)) entity, _plane, _token = _derived_line_angle_cylinder_axis( line_query, feature_frames, sketch_by_source, entity_by_sketch, ) or (None, None, None) if entity is None: # Kept for type narrowing; the candidate was checked above. raise ValueError("line-angle reference selection is unsupported") # 圆柱面/端盖圆边用于 LINE_ANGLE 时,FeatureScript 取圆柱面上的 # 母线作为旋转轴,而不是圆心处的中心轴。将该轴移到旋转后的面 # 法向所指的圆柱半径位置,才能保持后续草图和端面引用的位置。 axis_direction = _unit(axis, "line-angle reference line is degenerate") radial = _sub(normal, [axis_direction[index] * _dot(normal, axis_direction) for index in range(3)]) start = [start[index] + _unit(radial, "line-angle cylinder radial direction is degenerate")[index] * entity["radius_mm"] for index in range(3)] return _frame(start, x_dir, normal) if plane_type == "PLANE_POINT": # A PLANE_POINT definition is typed source data: one FACE plane and # one VERTEX point. Do not infer their roles from qCreatedBy text; # a direct CAP_FACE is equally a valid plane query and must retain # that source topology rather than falling back to a runtime face. face_queries = [ item for item in entities if (parse_query(item).kind or "").lower() in {"face", "entitytype.face"} ] point_queries = [ item for item in entities if (parse_query(item).kind or "").lower() in {"vertex", "entitytype.vertex"} ] if len(entities) != 2 or len(face_queries) != 1 or len(point_queries) != 1: raise ValueError("plane-point requires exactly one face and one vertex") base_query, point_query = face_queries[0], point_queries[0] base = _query_plane(base_query, feature_frames, sketch_by_source, entity_by_sketch) return _frame(datum_point(point_query), base["x_dir"], base["normal"]) if plane_type == "CURVE_POINT": point_query = next((item for item in entities if parse_query(item).kind and "vertex" in parse_query(item).kind), None) curve_query = next((item for item in entities if item is not point_query), None) if point_query is None or curve_query is None: raise ValueError("curve-point references are unresolved") point = _query_point(point_query, feature_frames, sketch_by_source, entity_by_sketch) curve, source_plane, _ = _entity_from_query(curve_query, sketch_by_source, entity_by_sketch) if curve.get("type") == "line": start, end = _query_line(curve_query, feature_frames, sketch_by_source, entity_by_sketch) tangent = _sub(end, start) else: source_plane, tangent = _curve_point_tangent( curve_query, point_query, point, sketch_by_source, entity_by_sketch, ) return _frame(point, source_plane["normal"], tangent) if plane_type == "THREE_POINT": if len(entities) != 3: raise ValueError("three-point plane requires exactly three points") first, second, third = [datum_point(item) for item in entities] normal = _cross(_sub(second, first), _sub(third, first)) if _bool(params.get("oppositeDirection")): normal = [-value for value in normal] return _frame(first, _sub(second, first), normal) if plane_type == "LINE_POINT": line_query = next((item for item in entities if "edge" in (parse_query(item).kind or "")), None) point_query = next((item for item in entities if item is not line_query), None) if line_query is None or point_query is None: raise ValueError("line-point references are unresolved") # FeatureScript 的 LINE_POINT 平面经过指定点,法向与参考线平行。 # 点可以是线端点;它不是用来和直线共同定义平面的第三个方向。 _, source_plane, _ = _entity_from_query(line_query, sketch_by_source, entity_by_sketch) start, end = _query_line(line_query, feature_frames, sketch_by_source, entity_by_sketch) point = _query_point(point_query, feature_frames, sketch_by_source, entity_by_sketch) normal = _sub(end, start) if _bool(params.get("oppositeDirection")): normal = [-value for value in normal] return _frame(point, _cross(source_plane["normal"], normal), normal) if plane_type == "MID_PLANE": if len(entities) != 2: raise ValueError("mid-plane requires exactly two reference planes") first, second = [_query_plane(item, feature_frames, sketch_by_source, entity_by_sketch) for item in entities] first_normal = _unit(first["normal"], "first mid-plane normal is degenerate") second_normal = _unit(second["normal"], "second mid-plane normal is degenerate") intersection = _cross(first_normal, second_normal) intersection_length_squared = _dot(intersection, intersection) if intersection_length_squared <= 1e-12: alignment = 1.0 if _dot(first_normal, second_normal) >= 0 else -1.0 offset = _dot(_sub(second["origin_mm"], first["origin_mm"]), first_normal) * alignment return _shift_plane(first, offset / 2.0) # 两个相交面没有“中点偏移面”。CADFS 的 MID_PLANE 是两面形成的 # 二面角平分面:先令两个法向同向,再取其和作为平分面的法向;平面 # 经过两原平面的交线。此处的 local x 轴由交线推导,后续草图的 # (u, v) 坐标不依赖任意选择的输入 face frame。 if _dot(first_normal, second_normal) < 0: second_normal = [-value for value in second_normal] # 法向翻转也会反转两平面的交线方向。交点公式中的交线必须与已 # 对齐的法向保持同一方向,否则会将原点映射到交线的对称位置。 intersection = _cross(first_normal, second_normal) normal = _unit([first_normal[index] + second_normal[index] for index in range(3)], "mid-plane angle bisector is degenerate") first_offset = _dot(first_normal, first["origin_mm"]) second_offset = _dot(second_normal, second["origin_mm"]) first_term = _cross(second_normal, intersection) second_term = _cross(intersection, first_normal) origin = [ (first_offset * first_term[index] + second_offset * second_term[index]) / intersection_length_squared for index in range(3) ] return _frame(origin, _cross(normal, intersection), normal) raise UnsupportedCapability(f"reference_plane:{plane_type.lower()}", f"current converter has no exact {plane_type} reference plane") def _mirror_plane_from_query( value: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: dict[str, dict[str, Any]], entity_by_sketch: dict[str, dict[str, dict[str, Any]]], feature_by_id: dict[str, dict[str, Any]], sketches_by_id: dict[str, dict[str, Any]], previous: list[str], mirror_source_ids: list[str], featurescript_version: str | None, ) -> dict[str, Any] | None: """Materialize the bounded planar mirror-face source into a CDSL frame. A full solid revolve turns a source line perpendicular to its axis into a planar annular face. A direct independent blind prism similarly has a physical cap or one source-line side plane while it is still the immediate mirror source. Those faces can materialize a CDSL datum plane without resolving a runtime topology selector. Curved, partial, derived, mutated, or unrelated faces remain unresolved instead of becoming guessed planes. """ plane = _default_plane(value) if plane is not None: return plane try: _call, owner, topology, kind, _definition = _direct_make_query(value) except ValueError: return None if kind not in {"face", "entitytype.face"}: return None producer_id = f"f_{owner}" producer = feature_by_id.get(producer_id) or {} frame = feature_frames.get(owner) or {} if topology == "SWEPT_FACE" and ( producer.get("atomic_id") == "revolve_add" and frame.get("revolve_full") and isinstance(frame.get("revolve_axis"), dict) ): try: return _query_plane(value, feature_frames, sketch_by_source, entity_by_sketch) except (UnsupportedCapability, ValueError): return None # A CAP/SWEPT source can only be a static mirror datum while its physical # direct-prism boundary is current and the mirror is operating on that # same independently selectable body. Do not reuse a frame after a # dress-up, Boolean, copy, or any other lifecycle transition. params = producer.get("params") or {} if ( featurescript_version != "1511" or topology not in {"CAP_FACE", "SWEPT_FACE"} or producer_id not in mirror_source_ids or previous[-1:] != [producer_id] or producer.get("atomic_id") != "extrude_add_blind" or params.get("result_mode") != "new_body" or (params.get("end_condition") or {}).get("type") != "blind" or params.get("draft") is not None ): return None try: if topology == "CAP_FACE": if _cap_face_output_role_selector(value, feature_by_id, sketches_by_id) is None: return None elif _direct_prism_swept_selector( value, owner=owner, selector_kind="face", feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=featurescript_version, ) is None: return None return _query_plane(value, feature_frames, sketch_by_source, entity_by_sketch) except (UnsupportedCapability, ValueError): return None def lower_model(model: ModelIR, provenance: dict[str, Any]) -> LoweringResult: diagnostics: list[dict[str, Any]] = []; history = [] sketches: list[dict[str, Any]] = []; sketches_by_id: dict[str, dict[str, Any]] = {}; sketch_by_source: dict[str, dict[str, Any]] = {}; entity_by_sketch: dict[str, dict[str, dict[str, Any]]] = {} feature_frames: dict[str, dict[str, Any]] = {}; source_variables: dict[str, Any] = {}; surface_profiles: list[dict[str, Any]] = []; swept_face_sketches: set[str] = set() features: list[dict[str, Any]] = []; complete = True; previous: list[str] = [] feature_by_id: dict[str, dict[str, Any]] = {}; feature_source_by_id: dict[str, str] = {} sketch_workplanes = {step.feature_id: step.workplane for step in model.sketches} # Original CADFS operation owners can remain the query anchor after an # explicit non-copy body transform. Map only those proven transform # successors; all other body lifecycle transitions remain unaliased. body_transform_aliases: dict[str, str] = {} # This is the lowering-side mirror of runtime ``body_members``. It makes # a source-qualified shell.parts target possible only while that source is # still one explicit selectable body member. lowered_body_members: set[str] = set() # A direct SWEPT_BODY query can continue to name the sole CADFS body after # ordinary additive and dress-up successors. This state is deliberately # cleared before any aggregate, multi-member, or otherwise ambiguous body # transition can make that continuation non-unique. single_body_successor_state: dict[str, Any] = {"owner": None, "sources": set()} for step in model.steps: if isinstance(step, SketchIR): history.append({"feature_id": step.feature_id, "operation": "newSketch", "parameters": {"sketchPlane": plain(step.workplane)}, "entities": [{"entity_id": e.feature_id, "operation": e.operation, "parameters": plain(e.params)} for e in step.entities]}) try: attachment = _direct_prism_cap_face_attachment( step.workplane, feature_by_id=feature_by_id, sketches_by_id=sketches_by_id, previous=previous, featurescript_version=model.featurescript_version, ) if attachment is not None: # CAP faces are native builder results. The exact plane is # resolved only when the consuming feature executes. lowered, entities = _lower_sketch(step, PLANES["Top"]) lowered["attachment"] = attachment sketches.append(lowered); sketches_by_id[lowered["id"]] = lowered; sketch_by_source[step.feature_id] = lowered; entity_by_sketch[step.feature_id] = entities continue attachment = _direct_primary_cut_copy_cap_face_attachment( step.workplane, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=model.featurescript_version, ) if attachment is not None: # This placeholder must never be consumed: attached # sketches remain local through preflight and are # materialized from the exact active face at execution. lowered, entities = _lower_sketch(step, PLANES["Top"]) lowered["attachment"] = attachment sketches.append(lowered); sketches_by_id[lowered["id"]] = lowered; sketch_by_source[step.feature_id] = lowered; entity_by_sketch[step.feature_id] = entities continue attachment = _direct_primary_cut_copy_swept_face_attachment( step.workplane, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=model.featurescript_version, ) if attachment is not None: # The swept tool face has no static source frame. It is # resolved through the native prism and subtract history # only when the immediate consumer executes. lowered, entities = _lower_sketch(step, PLANES["Top"]) lowered["attachment"] = attachment sketches.append(lowered); sketches_by_id[lowered["id"]] = lowered; sketch_by_source[step.feature_id] = lowered; entity_by_sketch[step.feature_id] = entities continue attachment = _direct_prism_blend_face_attachment( step.workplane, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=model.featurescript_version, ) if attachment is not None: # BLEND_FACE is a dress-up patch. Its physical plane and # native orientation are available only from the exact # runtime ``Generated(edge -> face)`` relation. lowered, entities = _lower_sketch(step, PLANES["Top"]) lowered["attachment"] = attachment sketches.append(lowered); sketches_by_id[lowered["id"]] = lowered; sketch_by_source[step.feature_id] = lowered; entity_by_sketch[step.feature_id] = entities continue # The outer query, rather than recursively parsed diagnostics, # determines whether a static SWEPT_FACE frame is admissible. # In particular MERGE(FACE) often contains SWEPT_FACE inputs # but is a distinct result that must be resolved at runtime. try: _outer, _owner, outer_topology, _outer_kind, _definition = _direct_make_query(step.workplane) except ValueError: outer_topology = None swept_face = outer_topology == "SWEPT_FACE" plane = _query_plane(step.workplane, feature_frames, sketch_by_source, entity_by_sketch) if swept_face else _plane_from_query(step.workplane, feature_frames, sketch_by_source, entity_by_sketch) profile = _revolve_swept_face_profile(step.workplane, plane, feature_frames, sketch_by_source, entity_by_sketch) if swept_face and not step.entities else None if profile is None: lowered, entities = _lower_sketch(step, plane) else: lowered, entities = {"id": f"sketch_{step.feature_id}", "name": step.feature_id, "workplane": plane, "profile": profile}, {} swept_face_sketches.add(step.feature_id) sketches.append(lowered); sketches_by_id[lowered["id"]] = lowered; sketch_by_source[step.feature_id] = lowered; entity_by_sketch[step.feature_id] = entities except Exception as exc: # 开放草图不能作为实体 profile,但其几何仍可能是后续基准面、 # 阵列轴或旋转轴的精确引用。保留为 reference 草图,后续实体 # 特征仍由 _profile_executable 明确拒绝,不能静默把开放轮廓实体化。 try: plane = _query_plane(step.workplane, feature_frames, sketch_by_source, entity_by_sketch) if swept_face else _plane_from_query(step.workplane, feature_frames, sketch_by_source, entity_by_sketch) lowered, entities = _lower_sketch(step, plane, allow_open=True) sketches.append(lowered); sketches_by_id[lowered["id"]] = lowered; sketch_by_source[step.feature_id] = lowered; entity_by_sketch[step.feature_id] = entities if isinstance(exc, OpenSketchProfileError): continue except Exception: pass diagnostics.append({"code": "sketch_deferred", "feature_id": step.feature_id, "message": str(exc)}); complete = False continue item = step history.append({"feature_id": item.feature_id, "operation": item.operation, "source_span": {"line_start": item.line_start, "line_end": item.line_end or item.line_start}, "parameters": plain(item.params), "raw_source": item.raw_source}) if item.operation in UNSUPPORTED or item.operation not in LOWERABLE_OPERATIONS: diagnostics.append({"code": "unsupported_operation", "feature_id": item.feature_id, "operation": item.operation}); complete = False; continue try: fid = f"f_{item.feature_id}"; depends = list(previous[-1:]); p = _resolve_source_variables(item.params, source_variables); feature: dict[str, Any] if item.operation == "assignVariable": variable_name, source_value, variable_params = _assign_variable_params(p) if variable_name in source_variables: raise UnsupportedCapability( "assign_variable_redeclaration", f"assignVariable redeclares source variable {variable_name}", ) source_variables[variable_name] = deepcopy(source_value) feature = { "id": fid, "name": item.feature_id, "atomic_id": "assign_variable", "depends_on": depends, "params": variable_params, "execution_status": "supported", } elif item.operation == "transform": # 单一直接 source 可以烘焙回原始几何。多 body 与 COPY instance # 必须保留为显式 body graph transform,不能移动聚合主体。 sources, pattern_instance_refs, transform_copy_refs = _transform_body_references( p.get("entities"), previous, feature_by_id, body_transform_aliases, ) transform_type = str(p.get("transformType") or "").split(".")[-1].upper() identity_copy = transform_type == "COPY" # Uniform scaling changes the B-rep's dimensions. It cannot be # baked into a source sketch without also transforming every # dependent parameter and topology frame, so retain it as an # explicit body-graph operation even for one direct source. source_for_bake = sources[0] if len(sources) == 1 else None source_feature_for_bake = feature_by_id.get(source_for_bake or "") if ( transform_type not in {"SCALE_UNIFORMLY", "COPY"} and not _bool(p.get("makeCopy")) and len(sources) == 1 and not pattern_instance_refs # Baking mutates an already emitted source sketch and its # lowering-only frames. It is therefore equivalent to a # transform only for the immediately preceding, # independent NEW member. A boolean, dress-up, later add, # or even a reference feature can retain geometry from the # pre-transform source; changing that source retroactively # would invert CADFS history order. and ( # _bake_transform has a stronger lifecycle diagnostic # for TRANSLATION_ENTITY. Let it run even when the # source was absorbed so that this unrepresentable # body move is rejected rather than silently lowered # as an explicit transform of a successor member. transform_type == "TRANSLATION_ENTITY" or ( # A later transform may retain the original CADFS # owner while the physical member is an earlier # transform's successor. Baking it into the # original sketch would discard the first move. sources == _transform_source_features(p.get("entities"), previous) and previous[-1:] == sources and (source_feature_for_bake or {}).get("params", {}).get("result_mode") == "new_body" ) ) ): _bake_transform(p, previous, feature_by_id, feature_source_by_id, sketches_by_id, feature_frames, sketch_by_source, entity_by_sketch) continue missing = [source for source in sources if source not in feature_by_id] if missing: raise ValueError("transform source bodies are unresolved: " + ", ".join(missing)) pattern_dependencies = [reference["pattern_feature_id"] for reference in pattern_instance_refs] transform_copy_dependencies = [reference["transform_feature_id"] for reference in transform_copy_refs] transform_params: dict[str, Any] = { "transform": _body_transform( p, feature_frames, sketch_by_source, entity_by_sketch, feature_by_id, previous, ), "make_copy": identity_copy or _bool(p.get("makeCopy")), } if sources: transform_params["source_feature_ids"] = sources if pattern_instance_refs: transform_params["pattern_instance_refs"] = pattern_instance_refs if transform_copy_refs: transform_params["transform_copy_refs"] = transform_copy_refs source_member_aliases = _transform_source_member_aliases(p.get("entities"), sources) if source_member_aliases: transform_params["source_member_aliases"] = source_member_aliases feature = { "id": fid, "name": item.feature_id, "atomic_id": "transform_bodies", "depends_on": list(dict.fromkeys(sources + pattern_dependencies + transform_copy_dependencies + depends)), "params": transform_params, "execution_status": "supported", } elif item.operation == "deleteBodies": queries = _queries(p.get("entities")) if not queries: raise ValueError("deleteBodies selection is empty") targets = [] for query in queries: _call, owner, topology, kind, _definition = _direct_make_query(query) pattern_id = f"f_{owner}" pattern = feature_by_id.get(pattern_id) if topology != "COPY" or kind not in {"body", "entitytype.body"} or pattern is None or pattern.get("atomic_id") != "pattern_circular": target = _delete_body_source(query) if target not in targets: targets.append(target) continue pattern_id, source_id, instance = _pattern_copy_body(query) params = pattern["params"] if source_id not in params.get("source_feature_ids", []): raise ValueError("pattern copy deletion source is not replayed by its owner") count = int(params.get("pattern_count") or 0) if instance < 1 or instance >= count: raise ValueError("pattern copy deletion instance is outside the generated range") excluded = params.setdefault("excluded_instance_indices", []) if instance not in excluded: excluded.append(instance) if not targets: continue missing = [target for target in targets if target not in feature_by_id] if missing: raise ValueError("deleteBodies source bodies are unresolved: " + ", ".join(missing)) feature = { "id": fid, "name": item.feature_id, "atomic_id": "delete_bodies", "depends_on": list(dict.fromkeys(targets + depends)), "params": {"target_feature_ids": targets}, "execution_status": "supported", } elif item.operation == "cPoint": point = _cpoint_datum( p, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=model.featurescript_version, ) feature_frames[item.feature_id] = {"point_mm": point} feature = { "id": fid, "name": item.feature_id, "atomic_id": "reference_point", "depends_on": depends, "params": {"point_mm": point}, "execution_status": "supported", } elif item.operation == "cPlane": plane = _cplane( p, feature_frames, sketch_by_source, entity_by_sketch, feature_by_id=feature_by_id, sketches_by_id=sketches_by_id, previous=previous, featurescript_version=model.featurescript_version, standard_library=model.standard_library, standard_library_version=model.standard_library_version, ) feature = {"id": fid, "name": item.feature_id, "atomic_id": "reference_plane", "depends_on": depends, "params": {"plane": plane}, "execution_status": "supported"} attachment = _attachment_plane(plane) feature_frames[item.feature_id] = { "start": plane, "end": plane, "start_attachment": attachment, "end_attachment": attachment, } elif item.operation == "extrude": surface_profile_sketch = None pure_surface_operation = ( str(p.get("bodyType") or "").rsplit(".", 1)[-1].upper() == "SURFACE" ) if pure_surface_operation: if p.get("surfaceOperationType") is not None: raise UnsupportedCapability( "extrude_surface_operation", "pure ToolBodyType.SURFACE extrusion cannot also declare a surface operation", ) surface_body_operation = str(p.get("operationType") or "").rsplit(".", 1)[-1].upper() if surface_body_operation not in {"", "ADD", "NEW"}: raise UnsupportedCapability( "extrude_surface_operation", "pure ToolBodyType.SURFACE extrusion supports only an independent ADD surface operation", ) surface_source = _source_sketch({"surfaceEntities": p.get("surfaceEntities")}) if not surface_source or surface_source not in sketch_by_source: raise ValueError("surface extrude sketch query is unresolved") surface_profile_sketch = _surface_profile_selection_sketch( sketch_by_source[surface_source], p.get("surfaceEntities"), entity_by_sketch[surface_source], fid, allow_open_wire=True, ) sketches.append(surface_profile_sketch); sketches_by_id[surface_profile_sketch["id"]] = surface_profile_sketch if p.get("surfaceOperationType") is not None: surface_operation = str(p.get("surfaceOperationType") or "").rsplit(".", 1)[-1].upper() if surface_operation != "ADD": raise UnsupportedCapability("extrude_surface_operation", "current CDSL surface extrude supports only NewSurfaceOperationType.ADD") surface_source = _source_sketch({"surfaceEntities": p.get("surfaceEntities")}) if not surface_source or surface_source not in sketch_by_source: raise ValueError("surface extrude sketch query is unresolved") surface_profile_sketch = _surface_profile_selection_sketch( sketch_by_source[surface_source], p.get("surfaceEntities"), entity_by_sketch[surface_source], fid, ) sketches.append(surface_profile_sketch); sketches_by_id[surface_profile_sketch["id"]] = surface_profile_sketch profile_value = ( p.get("surfaceEntities") if pure_surface_operation else _sketch_region_query(p.get("entities")) or p.get("entities") ) profile_operation = str(p.get("operationType") or "NEW").upper() profile_kind = parse_query(profile_value).topology_type multi_source_region_profile = ( _multi_source_sketch_region_profile(p.get("entities"), sketch_by_source, fid) if not pure_surface_operation else None ) if ( not pure_surface_operation and multi_source_region_profile is None and _has_composed_sketch_region_union(p.get("entities")) ): raise UnsupportedCapability( "extrude_multi_source_sketch_region", "extrude qSketchRegion union requires direct executable source profiles on one identical unattached frame", ) source = ( None if multi_source_region_profile is not None else parse_query(profile_value).source_sketch or _source_sketch(p) ) imprint = _imprint_sketch(profile_value) cap_face_output_selector = _cap_face_output_role_selector( profile_value, feature_by_id, sketches_by_id, ) retained_offset_cap_selector = _shell_retained_direct_prism_cap_offset_face_profile_selector( profile_value, feature_by_id, sketches_by_id, previous, featurescript_version=model.featurescript_version, ) if profile_kind == "OFFSET_FACE" else None cap_edge_hole = _cap_edge_hole_profile_sketch( profile_value, sketch_by_source, entity_by_sketch, feature_frames, fid, ) cap_edge_union_profile = _cap_edge_union_profile_sketch( profile_value, sketch_by_source, entity_by_sketch, fid, ) # First let the established profile materializer handle # single-region, circle and open-IMPRINT contracts. A # multi-face IMPRINT query reaches the arrangement path only # when that materializer cannot reduce the source sketch. materialized_imprint = ( _profile_selection_sketch( sketch_by_source[source], profile_value, entity_by_sketch[source], fid, ) if profile_kind == "IMPRINT" and source in sketch_by_source else None ) # ``parse_query`` retains nested-query information and its # last nested makeQuery can be a CAP_EDGE/INTERSECT leaf. # Only the external-boundary form needs direct qUnion-root # inspection here; ordinary single IMPRINT profiles keep # their established materialization path. planar_imprint_selections = [ selection for root in _queries(profile_value) if (selection := _planar_imprint_selection(root)) is not None ] has_external_planar_imprint_root = any( isinstance((selection[1].get("fragment") or {}).get("_external_anchor_query"), Call) for selection in planar_imprint_selections ) planar_imprint_profile = _planar_imprint_profile_sketch( profile_value, sketch_by_source, entity_by_sketch, fid, feature_by_id=feature_by_id, feature_frames=feature_frames, sketches_by_id=sketches_by_id, previous=previous, featurescript_version=model.featurescript_version, ) if ( profile_kind == "INTERSECT" or ( has_external_planar_imprint_root and _is_new_body_operation(profile_operation) ) or ( profile_kind == "IMPRINT" and len(_queries(profile_value)) > 1 and isinstance(source, str) and materialized_imprint is sketch_by_source.get(source) ) ) else None intersect_profile_sketch = _intersect_partition_profile_sketch( profile_value, sketch_by_source, entity_by_sketch, fid, ) if profile_kind == "INTERSECT" and planar_imprint_profile is None else None offset_face_profile = _offset_face_profile_sketch( profile_value, feature_frames, sketches_by_id, sketch_by_source, entity_by_sketch, feature_by_id, fid, ) if profile_kind == "OFFSET_FACE" else None profile_sketch: dict[str, Any] | None = None if cap_face_output_selector is not None or retained_offset_cap_selector is not None: # Keep the B-rep face as a runtime-derived profile. It # must not be reconstructed from the original sketch. pass elif multi_source_region_profile is not None: profile_sketch = multi_source_region_profile sketches.append(profile_sketch); sketches_by_id[profile_sketch["id"]] = profile_sketch elif cap_edge_hole is not None: profile_sketch, hole_selector = cap_edge_hole if profile_sketch["id"] not in sketches_by_id: sketches.append(profile_sketch); sketches_by_id[profile_sketch["id"]] = profile_sketch elif cap_edge_union_profile is not None: profile_sketch = cap_edge_union_profile if profile_sketch["id"] not in sketches_by_id: sketches.append(profile_sketch); sketches_by_id[profile_sketch["id"]] = profile_sketch elif profile_kind == "SWEPT_EDGE" and imprint in swept_face_sketches: source = imprint elif planar_imprint_profile is not None: profile_sketch = planar_imprint_profile sketches.append(profile_sketch); sketches_by_id[profile_sketch["id"]] = profile_sketch elif intersect_profile_sketch is not None: profile_sketch = intersect_profile_sketch sketches.append(profile_sketch); sketches_by_id[profile_sketch["id"]] = profile_sketch elif offset_face_profile is not None: profile_sketch = offset_face_profile sketches.append(profile_sketch); sketches_by_id[profile_sketch["id"]] = profile_sketch elif pure_surface_operation: profile_sketch = surface_profile_sketch elif profile_kind not in {None, "IMPRINT"}: partitioned = _partitioned_imprint_sketch(p.get("entities"), entity_by_sketch) if partitioned is not None: source = partitioned; profile_kind = "IMPRINT" else: raise UnsupportedCapability(f"extrude_profile_topology:{profile_kind.lower()}", f"current CDSL engine cannot exactly replay an extrude profile selected from {profile_kind}") if ( not pure_surface_operation and cap_face_output_selector is None and retained_offset_cap_selector is None and multi_source_region_profile is None and cap_edge_hole is None and cap_edge_union_profile is None and planar_imprint_profile is None and intersect_profile_sketch is None and offset_face_profile is None ): if not source or source not in sketch_by_source: raise ValueError("extrude sketch query is unresolved") open_profile_sketch = _open_imprint_profile_sketch(sketch_by_source[source], profile_value, fid) profile_sketch = open_profile_sketch or _profile_selection_sketch(sketch_by_source[source], profile_value, entity_by_sketch[source], fid) if profile_sketch is not sketch_by_source[source]: sketches.append(profile_sketch); sketches_by_id[profile_sketch["id"]] = profile_sketch operation = profile_operation cutting = any(x in operation for x in ("REMOVE", "CUT")) if cutting and profile_kind == "IMPRINT" and profile_sketch is not None: trimmed_profile_sketch = _surface_trimmed_imprint_profile(profile_sketch, surface_profile_sketch, surface_profiles) if trimmed_profile_sketch is not profile_sketch: for index, sketch in enumerate(sketches): if sketch is profile_sketch: sketches[index] = trimmed_profile_sketch; break sketches_by_id[trimmed_profile_sketch["id"]] = trimmed_profile_sketch profile_sketch = trimmed_profile_sketch if ( profile_sketch is not None and not pure_surface_operation and not _profile_executable(profile_sketch) ): raise ValueError("extrude sketch has no closed profile") second = _bool(p.get("hasSecondDirection")) end = _end_condition("SYMMETRIC" if _bool(p.get("symmetric")) else p.get("endBound")) if end["type"] == "up_to_body": end["reference"] = _extent_reference( p.get("endBoundEntityBody"), "body", feature_frames, sketch_by_source, entity_by_sketch, feature_by_id=feature_by_id, sketches_by_id=sketches_by_id, previous=previous, featurescript_version=model.featurescript_version, ) elif end["type"] == "up_to_surface": # A two-sided face extent is one paired provenance # request. Defer resolving either side until the reverse # query is available below, where the exact CAP/SWEPT # pair contracts can prove both members together. if not second: end["reference"] = _extent_reference( p.get("endBoundEntityFace"), "face", feature_frames, sketch_by_source, entity_by_sketch, feature_by_id=feature_by_id, sketches_by_id=sketches_by_id, previous=previous, allow_cap_output_role=True, allow_primary_add_up_to_surface=True, allow_direct_prism_swept_lineage=True, featurescript_version=model.featurescript_version, ) elif end["type"] == "up_to_vertex": end["reference"] = ( _direct_source_vertex_extent_reference( p.get("endBoundEntityVertex"), sketch_by_source, entity_by_sketch, featurescript_version=model.featurescript_version, ) or _direct_prism_cap_vertex_extent_selector( p.get("endBoundEntityVertex"), feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=model.featurescript_version, ) or _intersection_vertex_reference( p.get("endBoundEntityVertex"), feature_frames, sketch_by_source, entity_by_sketch, feature_by_id, item.feature_id, ) ) if _bool(p.get("hasOffset")): offset = _number(p.get("offsetDistance"), True) if offset < 0: raise ValueError("extrude extent offset must be non-negative") end["offset_mm"] = offset depth_value = p.get("depth"); depth = _number(depth_value, True) if depth_value is not None else 1.0 # FeatureScript 的 operationType 缺省为 NEW。source 若要把结果 # 并入既有主体会显式给出 ADD;不能根据当前 CDSL history 猜成 ADD, # 否则会把应保留的独立 body 在转换时丢失。 reverse = _bool(p.get("oppositeDirection")); cutting = any(x in operation for x in ("REMOVE", "CUT")) if cutting and not second and end["type"] not in {"blind", "mid_plane", "through_all", "through_next", "up_to_surface", "up_to_vertex", "up_to_body"}: capability = f"extrude_cut_{end['type']}" raise UnsupportedCapability(capability, f"current CDSL atomic set has no exact {capability} operation") if not cutting and not second and end["type"] not in {"blind", "mid_plane", "through_all", "through_next", "up_to_surface", "up_to_vertex", "up_to_body"}: capability = f"extrude_add_{end['type']}" raise UnsupportedCapability(capability, f"current CDSL atomic set has no exact {capability} operation") if second: atomic = "extrude_cut_two_sided" if cutting else "extrude_add_two_sided" params = {"distance_mm": depth, "reverse": reverse, "end_condition": end} reverse_depth = _number(p.get("secondDirectionDepth", depth), True) reverse_end = _end_condition(p.get("secondDirectionBound")) if reverse_end["type"] == "up_to_body": reverse_end["reference"] = _extent_reference(p.get("secondDirectionBoundEntityBody"), "body", feature_frames, sketch_by_source, entity_by_sketch) elif reverse_end["type"] == "up_to_vertex": reverse_end["reference"] = ( _direct_source_vertex_extent_reference( p.get("secondDirectionBoundEntityVertex"), sketch_by_source, entity_by_sketch, featurescript_version=model.featurescript_version, ) or _direct_prism_cap_vertex_extent_selector( p.get("secondDirectionBoundEntityVertex"), feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=model.featurescript_version, ) or _intersection_vertex_reference( p.get("secondDirectionBoundEntityVertex"), feature_frames, sketch_by_source, entity_by_sketch, feature_by_id, item.feature_id, ) ) params.update({"reverse_distance_mm": reverse_depth, "reverse_end_condition": reverse_end}) if end["type"] == "up_to_surface" and reverse_end["type"] == "up_to_surface": cap_pair = _two_sided_up_to_surface_cap_pair( p.get("endBoundEntityFace"), p.get("secondDirectionBoundEntityFace"), feature_by_id=feature_by_id, sketches_by_id=sketches_by_id, source_sketches=sketch_by_source, previous=previous, featurescript_version=model.featurescript_version, ) if cap_pair is not None: end["reference"], reverse_end["reference"] = cap_pair else: swept_pair = _two_sided_up_to_surface_shell_swept_face_pair( p.get("endBoundEntityFace"), p.get("secondDirectionBoundEntityFace"), feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=model.featurescript_version, ) if swept_pair is not None: end["reference"], reverse_end["reference"] = swept_pair else: end["reference"] = _extent_reference( p.get("endBoundEntityFace"), "face", feature_frames, sketch_by_source, entity_by_sketch, feature_by_id=feature_by_id, sketches_by_id=sketches_by_id, previous=previous, featurescript_version=model.featurescript_version, ) reverse_end["reference"] = _extent_reference( p.get("secondDirectionBoundEntityFace"), "face", feature_frames, sketch_by_source, entity_by_sketch, feature_by_id=feature_by_id, sketches_by_id=sketches_by_id, previous=previous, featurescript_version=model.featurescript_version, ) elif reverse_end["type"] == "up_to_surface": end["reference"] = _extent_reference( p.get("endBoundEntityFace"), "face", feature_frames, sketch_by_source, entity_by_sketch, feature_by_id=feature_by_id, sketches_by_id=sketches_by_id, previous=previous, featurescript_version=model.featurescript_version, ) reverse_end["reference"] = _extent_reference( p.get("secondDirectionBoundEntityFace"), "face", feature_frames, sketch_by_source, entity_by_sketch, feature_by_id=feature_by_id, sketches_by_id=sketches_by_id, previous=previous, featurescript_version=model.featurescript_version, ) elif end["type"] == "mid_plane": blind = _end_condition("BLIND") atomic = "extrude_cut_two_sided" if cutting else "extrude_add_two_sided" params = {"distance_mm": depth / 2, "reverse_distance_mm": depth / 2, "reverse": reverse, "end_condition": blind, "reverse_end_condition": dict(blind)} else: atomic = "extrude_cut_blind" if cutting else "extrude_add_blind" params = {"distance_mm": depth, "reverse": reverse, "end_condition": end} if pure_surface_operation: if second or end["type"] not in {"blind", "mid_plane"}: raise UnsupportedCapability( "extrude_surface_extent", "pure ToolBodyType.SURFACE extrusion supports blind and symmetric extents only", ) if _bool(p.get("hasDraft")): if second or end["type"] != "blind": raise UnsupportedCapability("extrude_draft_extent", "current CDSL draft supports only one-sided blind extrusions") params["draft"] = { "angle_deg": _number(p.get("draftAngle")), "pull_direction": _bool(p.get("draftPullDirection")), } if cap_edge_hole is not None: if cutting or second or end["type"] != "blind" or "draft" in params: raise UnsupportedCapability("extrude_cap_edge_profile", "current CAP_EDGE profile extrusion supports one-sided blind additive results") atomic = "extrude_add_blind_with_hole" if not cutting and _is_new_body_operation(operation): params["result_mode"] = "new_body" derived_face_selector = cap_face_output_selector or retained_offset_cap_selector if derived_face_selector is not None: params["operation"] = "cut" if cutting else "add" if second or end["type"] == "mid_plane": params["two_sided"] = True feature = { "id": fid, "name": item.feature_id, "atomic_id": "extrude_from_face", "depends_on": depends, "params": params, "selectors": [derived_face_selector], "execution_status": "supported", } else: if profile_sketch is None: raise ValueError("extrude profile is unresolved") if pure_surface_operation: surface_params = {"distance_mm": params["distance_mm"], "reverse": bool(params.get("reverse"))} if "reverse_distance_mm" in params: surface_params["reverse_distance_mm"] = params["reverse_distance_mm"] feature = {"id": fid, "name": item.feature_id, "atomic_id": "extrude_surface", "depends_on": depends, "sketch_id": profile_sketch["id"], "params": surface_params, "execution_status": "supported"} else: feature = {"id": fid, "name": item.feature_id, "atomic_id": atomic, "depends_on": depends, "sketch_id": profile_sketch["id"], "params": params, "execution_status": "supported"} if cap_edge_hole is not None: feature["selectors"] = [hole_selector] if surface_profile_sketch is not None and not pure_surface_operation: if end["type"] not in {"blind", "mid_plane"}: raise UnsupportedCapability("extrude_surface_extent", "current CDSL surface extrude supports blind and symmetric extents only") surface_params = {"distance_mm": params["distance_mm"], "reverse": bool(params.get("reverse"))} if "reverse_distance_mm" in params: surface_params["reverse_distance_mm"] = params["reverse_distance_mm"] surface_feature = { "id": f"{fid}_surface", "name": f"{item.feature_id} surface", "atomic_id": "extrude_surface", "depends_on": [fid], "sketch_id": surface_profile_sketch["id"], "params": surface_params, "execution_status": "supported", } plane = _query_plane(profile_value, feature_frames, sketch_by_source, entity_by_sketch) if derived_face_selector is not None else profile_sketch["workplane"] if end["type"] == "blind" and not second: direction = -1 if reverse else 1 # FeatureScript 的 CAP_FACE 是实体端盖,而不是原草图平面。 # 记录实际外法向后,后续附着在 start/end cap 的草图才能沿 # 正确一侧拉伸;不能复用 profile 的初始法向。 feature_frames[item.feature_id] = { "start": _oriented_plane(plane, -direction), "end": _oriented_plane(plane, direction, direction * depth), "profile": dict(plane), "profile_source": source, } elif ( second and end["type"] == "blind" and (params.get("reverse_end_condition") or {}).get("type") == "blind" ): # A two-sided blind extrusion has no cap at the source # plane. Keep the one-sided CAP convention: ``isStart`` # is the cap on the side opposite the primary extent and # ``isStart:false`` is the primary-extent cap. This makes # the zero-second-distance limit agree with the # one-sided frame above, regardless of oppositeDirection. direction = -1 if reverse else 1 reverse_depth = float(params["reverse_distance_mm"]) feature_frames[item.feature_id] = { "start": _oriented_plane(plane, -direction, -direction * reverse_depth), "end": _oriented_plane(plane, direction, direction * depth), "profile": dict(plane), "profile_source": source, } elif end["type"] == "mid_plane": direction = -1 if reverse else 1 feature_frames[item.feature_id] = { "start": _oriented_plane(plane, -direction, -direction * depth / 2), "end": _oriented_plane(plane, direction, direction * depth / 2), "profile": dict(plane), "profile_source": source, } # This source-only datum contract is intentionally narrower # than the runtime SWEPT_EDGE selector: a LINE_ANGLE axis can # be computed from the original profile vertex and the prism # span only while the whole profile was retained unchanged. frame = feature_frames.get(item.feature_id) if ( atomic == "extrude_add_blind" and params.get("result_mode") == "new_body" and end["type"] == "blind" and not second and params.get("draft") is None and isinstance(source, str) and profile_sketch is not None and source in sketch_by_source and profile_sketch.get("source_sketch_id") == source and _profile_matches_direct_source(profile_sketch, sketch_by_source[source]) and isinstance(frame, dict) ): source_entity_ids = _direct_profile_source_entity_ids(profile_sketch) if source_entity_ids: frame["direct_prism_line_angle_axis"] = True frame["direct_prism_source_entity_ids"] = sorted(source_entity_ids) elif item.operation == "loft": cap_face_loft = _loft_cap_face_profile(p, sketch_by_source, entity_by_sketch, feature_frames, fid) if cap_face_loft is not None: profile_sketch, cap_selector, cap_plane = cap_face_loft if profile_sketch["id"] not in sketches_by_id: sketches.append(profile_sketch); sketches_by_id[profile_sketch["id"]] = profile_sketch if not _profile_executable(profile_sketch): raise ValueError("loft profile sketch has no closed profile") feature = { "id": fid, "name": item.feature_id, "atomic_id": "loft_add_with_cap_face", "depends_on": depends, "params": {"profile_sketch_ids": [profile_sketch["id"]]}, "selectors": [cap_selector], "execution_status": "supported", } frames = _loft_cap_frames(cap_plane, profile_sketch["workplane"]) else: wire_profiles = p.get("wireProfilesArray") body_type = str(p.get("bodyType") or "").rsplit(".", 1)[-1].upper() is_surface_wire_loft = body_type == "SURFACE" and isinstance(wire_profiles, list) surface_loft = ( is_surface_wire_loft and len(wire_profiles) == 2 and not p.get("sheetProfilesArray") and not p.get("guidesArray") and p.get("spine") is None and str(p.get("operationType") or "NEW").rsplit(".", 1)[-1].upper() == "NEW" and all(p.get(key) in (None, False, [], {}) for key in ( "connections", "matchConnections", "startCondition", "endCondition", "startMagnitude", "endMagnitude", )) ) if surface_loft: profiles = [ _direct_closed_sketch_wire_profile( profile.get("wireProfileEntities") if isinstance(profile, dict) else None, sketch_by_source, entity_by_sketch, featurescript_version=model.featurescript_version, standard_library=model.standard_library, standard_library_version=model.standard_library_version, feature_id=f"{fid}_{index}", ) for index, profile in enumerate(wire_profiles) ] if any(profile is None for profile in profiles): raise UnsupportedCapability( "loft_surface_wire_profiles", "current CDSL surface loft requires two direct closed source-wire profiles", ) profile_sketches = [profile for profile in profiles if profile is not None] if len({profile["source_sketch_id"] for profile in profile_sketches}) != len(profile_sketches): raise UnsupportedCapability( "loft_surface_wire_profiles", "current CDSL surface loft requires profiles from distinct source sketches", ) for profile in profile_sketches: sketches.append(profile); sketches_by_id[profile["id"]] = profile feature = { "id": fid, "name": item.feature_id, "atomic_id": "loft_surface", "depends_on": depends, "params": {"profile_sketch_ids": [profile["id"] for profile in profile_sketches]}, "execution_status": "supported", } frames = None elif is_surface_wire_loft: raise UnsupportedCapability( "loft_surface_wire_profiles", "current CDSL surface loft requires two direct closed source-wire profiles", ) else: sources = _loft_profile_sketches(p) missing = [source for source in sources if source not in sketch_by_source] if missing: raise ValueError("loft profile sketches are unresolved: " + ", ".join(missing)) non_closed = [source for source in sources if not _profile_executable(sketch_by_source[source])] if non_closed: raise ValueError("loft profile sketches have no closed profile: " + ", ".join(non_closed)) feature = { "id": fid, "name": item.feature_id, "atomic_id": "loft_add", "depends_on": depends, "params": {"profile_sketch_ids": [sketch_by_source[source]["id"] for source in sources]}, "execution_status": "supported", } if _initial_direct_loft_cap_output_roles(p, sources, features, model.featurescript_version): feature["params"].update({ "initial_output_roles": True, "cap_output_profile_sources": list(sources), }) frames = _loft_cap_frames( sketch_by_source[sources[0]]["workplane"], sketch_by_source[sources[-1]]["workplane"], ) if frames is not None: # This lowering-only provenance permits an exact source # endpoint pair for a two-section direct loft. It is not # a replacement for runtime topology history. if cap_face_loft is None: frames["loft_profile_sources"] = sources feature_frames[item.feature_id] = frames elif item.operation == "sweep": profile_source = _source_sketch({"entities": p.get("profiles")}) if not profile_source or profile_source not in sketch_by_source: raise ValueError("sweep profile sketch is unresolved") profile_sketch = _profile_selection_sketch( sketch_by_source[profile_source], p.get("profiles"), entity_by_sketch[profile_source], fid, ) if profile_sketch is not sketch_by_source[profile_source]: sketches.append(profile_sketch); sketches_by_id[profile_sketch["id"]] = profile_sketch if not _profile_executable(profile_sketch): raise ValueError("sweep profile sketch has no closed profile") path_query = parse_query(p.get("path")) path_source = path_query.source_sketch path_entity = (entity_by_sketch.get(path_source or "") or {}).get(path_query.source_entity or "") path_sketch = sketch_by_source.get(path_source or "") # A raw arc/circle leaf must be named by exactly one source # query: the query parser retains a representative qUnion # leaf, and accepting it would discard the other curves. A # single original curve selected by the versioned source-wire # contract is different: its complete source set is already # proven to contain precisely that one line/arc/B-spline. direct_single_path_source = path_query.calls.count("sQuery") == 1 path: dict[str, Any] | None = None # A multi-leaf query is a source wire request, never an # authorization to use the parser's representative leaf. # Resolve the complete same-sketch or spatial wire before # considering the singleton segment path. In particular, # parse_query retains the last leaf of qUnion for context; # treating that arc or line as the path would discard every # preceding curve in the FeatureScript query. if not direct_single_path_source: segmented_wire_path = _direct_segmented_sketch_wire_path( p.get("path"), sketch_by_source, entity_by_sketch, featurescript_version=model.featurescript_version, standard_library=model.standard_library, standard_library_version=model.standard_library_version, ) if segmented_wire_path is not None: path_source, path_segments, path_sketch = segmented_wire_path path = {"workplane": path_sketch["workplane"], "segments": path_segments} else: spatial_segments = _direct_spatial_segmented_sketch_wire_path( p.get("path"), sketch_by_source, entity_by_sketch, featurescript_version=model.featurescript_version, standard_library=model.standard_library, standard_library_version=model.standard_library_version, ) if spatial_segments is not None: path = {"segments": spatial_segments} # Do not fall back to the representative direct leaf. path_entity = path_sketch = None if path is None and (path_entity is None or path_sketch is None): direct_wire_path = _direct_sketch_wire_path( p.get("path"), sketch_by_source, entity_by_sketch, featurescript_version=model.featurescript_version, standard_library=model.standard_library, standard_library_version=model.standard_library_version, ) if direct_wire_path is not None: path_source, _path_entity_id, path_entity, path_sketch = direct_wire_path elif not direct_single_path_source: # Unlike a raw direct circle leaf, a qBodyType source # wire has no parser entity that may stand in for the # query result. Admit it only after this helper proves # its complete selected source set is exactly one # original circle. circle_wire_path = _direct_sketch_circle_wire_path( p.get("path"), sketch_by_source, entity_by_sketch, featurescript_version=model.featurescript_version, standard_library=model.standard_library, standard_library_version=model.standard_library_version, ) if circle_wire_path is not None: path_source, _path_entity_id, path_entity, path_sketch = circle_wire_path elif direct_single_path_source: segmented_wire_path = _direct_segmented_sketch_wire_path( p.get("path"), sketch_by_source, entity_by_sketch, featurescript_version=model.featurescript_version, standard_library=model.standard_library, standard_library_version=model.standard_library_version, ) if segmented_wire_path is not None: path_source, path_segments, path_sketch = segmented_wire_path path = {"workplane": path_sketch["workplane"], "segments": path_segments} else: spatial_segments = _direct_spatial_segmented_sketch_wire_path( p.get("path"), sketch_by_source, entity_by_sketch, featurescript_version=model.featurescript_version, standard_library=model.standard_library, standard_library_version=model.standard_library_version, ) if spatial_segments is not None: path = {"segments": spatial_segments} if path_entity is None or path_sketch is None: if path is None: raise UnsupportedCapability( "sweep_path_query", "current CDSL sweep requires one direct source sketch line/arc/B-spline, a connected source-sketch wire, or a connected union of source wires", ) if path is None and path_entity.get("type") not in {"line", "arc", "circle", "bspline"}: raise UnsupportedCapability("sweep_path", "current CDSL sweep requires one line, arc, circle, or B-spline path") operation = str(p.get("operationType") or "NEW").upper() is_cut_operation = any(value in operation for value in ("REMOVE", "CUT")) if "INTERSECT" in operation: raise UnsupportedCapability("sweep_intersect", "current CDSL sweep does not yet support intersection body semantics") if path is None: path = {"workplane": path_sketch["workplane"], "segment": deepcopy(path_entity)} path_reversed = _sweep_profile_attaches_at_path_end(profile_sketch, path) if path_reversed: path = _reversed_sweep_path(path) feature = { "id": fid, "name": item.feature_id, "atomic_id": "sweep_cut" if is_cut_operation else "sweep_add", "depends_on": depends, "sketch_id": profile_sketch["id"], "params": {"path": path}, "execution_status": "supported", } if _is_new_body_operation(operation): feature["params"]["result_mode"] = "new_body" frames = _sweep_cap_frames(profile_sketch, feature["params"]["path"]) if frames is not None: feature_frames[item.feature_id] = frames # PipeShell FirstShape/LastShape can prove two physical cap # faces only for one direct profile and one direct source # path. Preserve that singleton pair for CAP_FACE. Its # Generated(profile_edge) history is distinct and can retain # a complete direct analytic contour for SWEPT_FACE. path_segment = feature["params"]["path"].get("segment") or {} closed_circle_path = path_segment.get("type") == "circle" profile_shape = profile_sketch.get("profile") or {} if ( feature["params"].get("result_mode") == "new_body" and profile_sketch.get("source_sketch_id") == profile_source and profile_shape.get("source_entity_id") and isinstance(path_segment.get("source_entity_id"), str) and path_segment["source_entity_id"] and direct_single_path_source and isinstance(feature["params"]["path"].get("workplane"), dict) and not feature["params"]["path"].get("segments") and not closed_circle_path and frames is not None ): feature["params"]["initial_output_roles"] = True feature["params"]["cap_output_contract"] = { "profile_source": profile_source, "profile_entity": str(profile_shape["source_entity_id"]), "path_source": path_source, "path_entity": path_segment["source_entity_id"], "path_reversed": path_reversed, } direct_profile_entities = _direct_sweep_profile_entities(profile_sketch) if ( feature["params"].get("result_mode") == "new_body" and profile_sketch.get("source_sketch_id") == profile_source and direct_profile_entities is not None and isinstance(path_segment.get("source_entity_id"), str) and path_segment["source_entity_id"] and direct_single_path_source and isinstance(feature["params"]["path"].get("workplane"), dict) and not feature["params"]["path"].get("segments") and not closed_circle_path ): feature["params"]["initial_output_roles"] = True feature["params"]["swept_face_contract"] = { "profile_source": profile_source, "profile_entities": direct_profile_entities, "path_source": path_source, "path_entity": path_segment["source_entity_id"], "path_reversed": path_reversed, } if ( feature["params"].get("result_mode") == "new_body" and profile_source != path_source and profile_sketch.get("source_sketch_id") == profile_source and direct_profile_entities is not None and len(direct_profile_entities) >= 2 and isinstance(path_segment.get("source_entity_id"), str) and path_segment["source_entity_id"] and direct_single_path_source and isinstance(feature["params"]["path"].get("workplane"), dict) and not feature["params"]["path"].get("segments") and not closed_circle_path ): feature["params"]["initial_output_roles"] = True feature["params"]["swept_edge_contract"] = { "profile_source": profile_source, "profile_entities": direct_profile_entities, "path_source": path_source, "path_entity": path_segment["source_entity_id"], "path_reversed": path_reversed, } elif item.operation == "booleanBodies": operation = str(p.get("operationType") or "").split(".")[-1].upper() operation_map = { "UNION": "union", "SUBTRACTION": "subtract", "INTERSECTION": "intersect", } if operation not in operation_map: raise UnsupportedCapability("boolean_bodies_operation", "current CDSL booleanBodies supports union, subtraction and intersection") # An omitted FeatureScript ``targets`` field is the exact # targetless-UNION form. ``_queries(None)`` intentionally # returns one placeholder for callers that need a diagnostic, # so it cannot be used to decide whether this field exists. has_targets = p.get("targets") is not None targets, target_instance_refs, target_transform_copy_refs = ( _boolean_body_references( p.get("targets"), previous, feature_by_id, body_transform_aliases, ) if has_targets else ([], [], []) ) tools, tool_instance_refs, tool_transform_copy_refs = _boolean_body_references( p.get("tools"), previous, feature_by_id, body_transform_aliases, ) if not has_targets: # A targetless FeatureScript boolean denotes an ordered # set of body operands. For UNION and INTERSECTION, the # first explicit source member can be made CDSL's left # operand and every following member its right set. This # preserves the operation while keeping the source body # set explicit; SUBTRACTION has no such source-proven # targetless ordering contract. if operation not in {"UNION", "INTERSECTION"}: raise UnsupportedCapability( "boolean_bodies_targets", "targetless booleanBodies currently supports only UNION or INTERSECTION", ) selections = _ordered_boolean_body_references( p.get("tools"), previous, feature_by_id, body_transform_aliases, ) if len(selections) < 2: raise ValueError("targetless booleanBodies requires at least two explicit bodies") # FeatureScript supplies a set here, not a directional # target/tool pair. Preserve the existing deterministic # lifecycle policy: select an explicit direct member when # the set has one; otherwise choose its first qualified # COPY member. UNION/INTERSECTION are commutative, so the # choice does not alter their shape, while it avoids # rewriting a direct source as a pattern aggregate. target_index = next( (index for index, (kind, _value) in enumerate(selections) if kind == "feature"), 0, ) target_kind, target = selections.pop(target_index) right_operands = selections targets = [target] if target_kind == "feature" else [] target_instance_refs = [target] if target_kind == "pattern" else [] target_transform_copy_refs = [target] if target_kind == "transform_copy" else [] tools = [value for kind, value in right_operands if kind == "feature"] tool_instance_refs = [value for kind, value in right_operands if kind == "pattern"] tool_transform_copy_refs = [value for kind, value in right_operands if kind == "transform_copy"] targetless_body_set = True else: targetless_body_set = False target_keys = {("feature", source) for source in targets} | { ("pattern", reference["pattern_feature_id"], reference["source_feature_id"], reference["instance_index"]) for reference in target_instance_refs } | { ("transform_copy", reference["transform_feature_id"], reference["source_feature_id"]) for reference in target_transform_copy_refs } tool_keys = {("feature", source) for source in tools} | { ("pattern", reference["pattern_feature_id"], reference["source_feature_id"], reference["instance_index"]) for reference in tool_instance_refs } | { ("transform_copy", reference["transform_feature_id"], reference["source_feature_id"]) for reference in tool_transform_copy_refs } if target_keys & tool_keys: raise ValueError("booleanBodies targets and tools must be disjoint") missing = [source for source in targets + tools if source not in feature_by_id] if missing: raise ValueError("booleanBodies source features are unresolved: " + ", ".join(missing)) pattern_dependencies = list(dict.fromkeys([ *(reference["pattern_feature_id"] for reference in target_instance_refs), *(reference["pattern_feature_id"] for reference in tool_instance_refs), ])) transform_copy_dependencies = list(dict.fromkeys([ *(reference["transform_feature_id"] for reference in target_transform_copy_refs), *(reference["transform_feature_id"] for reference in tool_transform_copy_refs), ])) feature = { "id": fid, "name": item.feature_id, "atomic_id": "boolean_bodies", "depends_on": list(dict.fromkeys(targets + tools + pattern_dependencies + transform_copy_dependencies + depends)), "params": { "operation": operation_map[operation], "keep_tools": _bool(p.get("keepTools")), }, "execution_status": "supported", } if targetless_body_set: feature["params"]["targetless_body_set"] = True if targets: feature["params"]["target_feature_ids"] = targets if tools: feature["params"]["tool_feature_ids"] = tools if target_instance_refs: feature["params"]["target_pattern_instance_refs"] = target_instance_refs if tool_instance_refs: feature["params"]["tool_pattern_instance_refs"] = tool_instance_refs if target_transform_copy_refs: feature["params"]["target_transform_copy_refs"] = target_transform_copy_refs if tool_transform_copy_refs: feature["params"]["tool_transform_copy_refs"] = tool_transform_copy_refs elif item.operation == "revolve": # surfaceOperationType alone does not make a body operation a # surface operation. CADFS emits it for closed sketch regions # that produce ordinary solids too. Only ToolBodyType.SURFACE # selects a sheet result; its profile is in surfaceEntities. surface_operation = str(p.get("bodyType") or "").rsplit(".", 1)[-1].upper() == "SURFACE" profile_entities = p.get("surfaceEntities") if surface_operation else p.get("entities") profile_kind = parse_query(profile_entities).topology_type source = _source_sketch(p) if not source or source not in sketch_by_source: raise ValueError("revolve sketch query is unresolved") profile_sketch = _profile_selection_sketch(sketch_by_source[source], profile_entities, entity_by_sketch[source], fid) if profile_sketch is not sketch_by_source[source]: sketches.append(profile_sketch); sketches_by_id[profile_sketch["id"]] = profile_sketch if not _profile_executable(profile_sketch): raise ValueError("revolve sketch has no closed profile") axis_q = parse_query(p.get("axis")); axis_entity = (entity_by_sketch.get(axis_q.source_sketch or "") or {}).get(axis_q.source_entity or "") if not axis_entity or axis_entity.get("type") != "line": raise ValueError("revolve axis is unresolved") plane = sketch_by_source[axis_q.source_sketch]["workplane"]; start, end = _global(plane, axis_entity["start"]), _global(plane, axis_entity["end"]) direction = [end[i]-start[i] for i in range(3)]; norm = math.sqrt(sum(x*x for x in direction)); direction = [x/norm for x in direction] # 闭合实体回转携带 surfaceOperationType、但没有 bodyType 时, # 该字段只是曲面处理参数,不能被误读为 NewBodyOperationType.NEW。 # 它保持默认 ADD,F8 这类重叠回转因而会参与当前实体的融合。 # 完全没有该字段的普通实体回转仍保持 FeatureScript 的默认 NEW # body 语义;显式 ToolBodyType.SURFACE 则以独立 shell 执行。 default_operation = "NEW" if surface_operation or p.get("surfaceOperationType") is None else "ADD" operation = str(p.get("operationType") or default_operation).upper() surface_kind = str(p.get("surfaceOperationType") or "NEW").upper() if surface_operation and "NEW" not in surface_kind: raise UnsupportedCapability("revolve_surface_operation", "current CDSL surface revolve supports only NewSurfaceOperationType.NEW") atomic = "revolve_surface" if surface_operation else "revolve_cut" if "REMOVE" in operation else "revolve_add" full = "FULL" in str(p.get("revolveType") or "FULL").upper(); angle = 360.0 if full else _number(p.get("angle", 360.0)) params = {"angle_deg": angle, "reverse": _bool(p.get("oppositeDirection")), "axis": {"origin_mm": start, "direction": direction}} if atomic == "revolve_add" and _is_new_body_operation(operation): params["result_mode"] = "new_body" feature = {"id": fid, "name": item.feature_id, "atomic_id": atomic, "depends_on": depends, "sketch_id": profile_sketch["id"], "params": params, "execution_status": "supported"} if full: frame = {"revolve_axis": {"origin_mm": start, "direction": direction}, "revolve_full": True} # A SWEPT_EDGE circle is only lowerable when both profile # and axis are still direct source-sketch entities. Keep # this lowering-only provenance out of the public CDSL. axis_direct = ( axis_q.source_sketch == source and axis_q.source_entity is not None and _source_ref_entity(source, axis_q.source_entity, entity_by_sketch) is not None ) if ( atomic == "revolve_add" and params.get("result_mode") == "new_body" and _profile_matches_direct_source(profile_sketch, sketch_by_source[source]) and axis_direct ): frame["profile_source"] = source # Keep the FeatureScript profile provenance separate # from geometry equality. Some 1511 IMPRINT queries # lower to identical contours but are not the original # sketch topology; 2491 has explicit evidence for one # complete unchanged materialization. frame["revolve_profile_contract"] = ( "verified_complete_materialization" if profile_kind == "IMPRINT" else "original_source" ) feature_frames[item.feature_id] = frame elif item.operation in {"fillet", "chamfer"}: key = "radius" if item.operation == "fillet" else "width" chamfer_type = str(p.get("chamferType") or "EQUAL_OFFSETS").split(".")[-1].upper() amount_value = p.get(key) if item.operation == "chamfer" and chamfer_type == "TWO_OFFSETS": amount_value = p.get("width1") amount = _number(amount_value, True); selectors = [] source_set = _direct_query_set_operands(p.get("entities")) query_values = _query_set_leaf_values(p.get("entities")) if source_set is not None else _queries(p.get("entities")) offset_edge_forms: set[str] = set() for query_value in query_values: try: _call, _owner, topology, _kind, definition = _direct_make_query(query_value) except ValueError: continue if topology != "OFFSET_EDGE": continue disambiguation = definition.get("disambiguationData") if isinstance(disambiguation, list): offset_edge_forms.add( "tdd" if any(isinstance(item, Call) and item.name == "TDD" for item in disambiguation) else "osd" ) mixed_offset_edge_forms = len(offset_edge_forms) > 1 for index, query_value in enumerate(query_values): query = parse_query(query_value) # ``parse_query`` retains nested source metadata for # diagnostics, but its recursive walk ends on an inner # CAP/SWEPT query. A fillet/chamfer consumes the outer # makeQuery result, so use the direct outer expression # for its topology kind and owner. try: _outer_call, outer_owner, outer_topology, outer_kind, _outer_definition = _direct_make_query(query_value) except ValueError: outer_owner = query.owner_feature outer_topology = query.topology_type outer_kind = query.kind owner = outer_owner if not owner: raise ValueError("selector owner is unresolved") # A deferred source query still describes a CADFS result, # but it cannot bind to a producer omitted from the CDSL # prefix. Keeping that missing owner in a supported # dress-up would create semantically invalid CDSL and # falsely move this lowering failure into validation. if f"f_{owner}" not in feature_by_id: raise UnsupportedCapability( "selector_owner_unavailable", f"selector owner {owner} has no executable CDSL producer", ) selector_kind = "face" if outer_kind in {"face", "entitytype.face"} else "edge" if outer_topology == "BLEND_EDGE": blend_selector = _direct_blend_edge_selector( query_value, owner=owner, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=model.featurescript_version, ) if blend_selector is not None: selectors.append(blend_selector) continue selectors.append({ "kind": selector_kind, "owner_feature_id": f"f_{owner}", "stable_id": f"cadfs_{fid}_{index}", "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": _deferred_featurescript_selector_intent( query_value, kind=selector_kind, ), }) continue if outer_topology == "OFFSET_EDGE": offset_edge_selector = _direct_prism_shell_offset_edge_tdd_selector( query_value, owner=owner, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=model.featurescript_version, ) if offset_edge_selector is None and not mixed_offset_edge_forms: offset_edge_selector = _direct_prism_shell_offset_edge_vertex_selector( query_value, owner=owner, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=model.featurescript_version, ) if offset_edge_selector is not None: selectors.append(offset_edge_selector) continue if outer_topology == "INTERSECT": intersection_selector = _direct_boolean_intersection_selector( query_value, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, featurescript_version=model.featurescript_version, ) if intersection_selector is None: intersection_selector = _direct_primary_cut_intersection_selector( query_value, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=model.featurescript_version, ) if intersection_selector is not None: selectors.append(intersection_selector) continue selectors.append({ "kind": selector_kind, "owner_feature_id": f"f_{owner}", "stable_id": f"cadfs_{fid}_{index}", "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": _deferred_featurescript_selector_intent( query_value, kind=selector_kind, ), }) continue if outer_topology == "COPY": copy_selector = _direct_primary_cut_copy_cap_edge_selector( query_value, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=model.featurescript_version, ) if copy_selector is not None: selectors.append(copy_selector) continue if outer_topology == "CAP_FACE": # A primary ADD has only a transient prism tool. Its # cap may drive the immediately following dress-up # only when runtime proves the exact one-to-one # extrude-to-union successor in the active member. cap_selector = _cap_face_output_role_selector( query_value, feature_by_id, sketches_by_id, allow_primary_add_dressup=True, ) if ( cap_selector is not None and previous[-1:] == [cap_selector["owner_feature_id"]] ): selectors.append(cap_selector) continue if outer_topology in {"CAP_FACE", "CAP_EDGE", "SWEPT_FACE", "SWEPT_EDGE"}: imprint_selector = _planar_imprint_prism_selector( query_value, owner=owner, selector_kind=selector_kind, feature_by_id=feature_by_id, feature_frames=feature_frames, sketches_by_id=sketches_by_id, previous=previous, featurescript_version=model.featurescript_version, ) if imprint_selector is not None: selectors.append(imprint_selector) continue if outer_topology in {"CAP_EDGE", "SWEPT_FACE", "SWEPT_EDGE"}: sweep_cap_edge_selector = _initial_direct_sweep_cap_edge_selector( query_value, feature_by_id, sketches_by_id, previous, ) if outer_topology == "CAP_EDGE" else None if sweep_cap_edge_selector is not None: selectors.append(sweep_cap_edge_selector) continue sweep_swept_face_selector = _initial_direct_sweep_swept_face_selector( query_value, feature_by_id, sketches_by_id, previous, ) if outer_topology == "SWEPT_FACE" else None if sweep_swept_face_selector is not None: selectors.append(sweep_swept_face_selector) continue sweep_swept_edge_selector = _initial_direct_sweep_swept_edge_selector( query_value, feature_by_id, sketches_by_id, previous, ) if outer_topology == "SWEPT_EDGE" else None if sweep_swept_edge_selector is not None: selectors.append(sweep_swept_edge_selector) continue # A direct PipeShell producer with an explicit source # contract must not fall through to the legacy # cylinder geometry hint when its profile/path pair is # incomplete or contradictory. Preserve the source # query for diagnostics and let runtime reject it. sweep_producer = feature_by_id.get(f"f_{owner}") or {} sweep_params = sweep_producer.get("params") or {} if ( outer_topology in {"SWEPT_FACE", "SWEPT_EDGE"} and sweep_producer.get("atomic_id") == "sweep_add" and sweep_params.get("initial_output_roles") is True and isinstance( sweep_params.get( "swept_face_contract" if outer_topology == "SWEPT_FACE" else "swept_edge_contract" ), dict, ) ): selectors.append({ "kind": selector_kind, "owner_feature_id": f"f_{owner}", "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": _deferred_featurescript_selector_intent( query_value, kind=selector_kind, ), }) continue lineage_selector = _direct_prism_swept_selector( query_value, owner=owner, selector_kind=selector_kind, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=model.featurescript_version, allow_continuation=outer_topology in {"CAP_EDGE", "SWEPT_EDGE"}, ) if lineage_selector is not None: selectors.append(lineage_selector) continue if outer_topology == "SWEPT_EDGE": revolve_selector = _direct_full_revolve_swept_edge_selector( query_value, owner=owner, selector_kind=selector_kind, feature_by_id=feature_by_id, feature_frames=feature_frames, sketch_by_source=sketch_by_source, sketches_by_id=sketches_by_id, entity_by_sketch=entity_by_sketch, previous=previous, featurescript_version=model.featurescript_version, ) if revolve_selector is not None: selectors.append(revolve_selector) continue if _uses_planar_imprint_profile(owner, feature_by_id, sketches_by_id): selectors.append({ "kind": selector_kind, "owner_feature_id": f"f_{owner}", "stable_id": f"cadfs_{fid}_{index}", "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": _deferred_featurescript_selector_intent( query_value, kind=selector_kind, ), }) continue refs = _source_refs(query_value) source_entity = (entity_by_sketch.get(query.source_sketch or "") or {}).get(query.source_entity or "") geometry: dict[str, Any] = {} frame = feature_frames.get(owner) cap = frame.get("start" if query.is_start else "end") if frame and {"start", "end"}.issubset(frame) else None if selector_kind == "face" and query.topology_type == "CAP_FACE" and cap: geometry = {"normal": cap["normal"], "plane_offset_mm": sum(cap["normal"][i] * cap["origin_mm"][i] for i in range(3))} elif selector_kind == "face" and query.topology_type == "SWEPT_FACE" and source_entity and source_entity["type"] == "circle": # 圆形 profile 拉伸得到的侧面以轴线、半径为标识。同一 feature 可以 # 生成多个半径相同的圆柱面,因此保留草图圆心作为轴原点来解消歧义。 source_plane = sketch_by_source.get(query.source_sketch or "", {}).get("workplane") if source_plane is not None: geometry = { "axis_origin_mm": _global(source_plane, source_entity["center"]), "axis_direction": source_plane["normal"], "radius_mm": source_entity["radius_mm"], } elif selector_kind == "edge" and query.topology_type == "OFFSET_EDGE" and source_entity and source_entity["type"] == "circle": offset_planes = (frame or {}).get("shell_offset_edge_planes") or {} source_key = f"{query.source_sketch}:{query.source_entity}" offset_plane = offset_planes.get(source_key) if offset_plane is not None: geometry = { "source_circle_center_mm": _global(offset_plane, source_entity["center"]), "source_circle_radius_mm": source_entity["radius_mm"], "source_plane_normal": offset_plane["normal"], } if selector_kind == "edge" and query.topology_type == "SWEPT_EDGE": geometry = _swept_edge_line_selector_geometry( owner, refs, frame or {}, feature_by_id, sketch_by_source, entity_by_sketch, ) if geometry is None: geometry = _swept_edge_revolve_circle_selector_geometry( owner, refs, frame or {}, feature_by_id, sketch_by_source, entity_by_sketch, ) if geometry is None: raise ValueError("swept edge source endpoint provenance is unsupported") elif selector_kind == "edge" and source_entity and cap: if source_entity["type"] == "circle": # Keep the source circle signature until the prefix has been # rebuilt. OCC may expose it as one edge or several arcs. selectors.append({"kind": selector_kind, "owner_feature_id": f"f_{owner}", "stable_id": f"cadfs_{fid}_{index}", "source": "runtime_snapshot", "confidence": 1.0, "geometry": geometry or {"curve_type": "circle", "source_circle_center_mm": _global(cap, source_entity["center"]), "source_circle_radius_mm": source_entity["radius_mm"], "source_plane_normal": cap["normal"]}, "selector_intent": _deferred_featurescript_selector_intent(query_value, kind=selector_kind)}) continue elif source_entity["type"] == "line": start, end = _global(cap, source_entity["start"]), _global(cap, source_entity["end"]) geometry = {"curve_type": "line", "bbox_mm": [min(start[i], end[i]) for i in range(3)] + [max(start[i], end[i]) for i in range(3)]} if not geometry: raise ValueError("selector geometry is unresolved") if selector_kind == "face" and not geometry: raise ValueError(f"{query.topology_type or 'face'} selector geometry is unresolved") selectors.append({"kind": selector_kind, "owner_feature_id": f"f_{owner}", "stable_id": f"cadfs_{fid}_{index}", "source": "runtime_snapshot", "confidence": 1.0, "geometry": geometry, "selector_intent": _deferred_featurescript_selector_intent(query_value, kind=selector_kind)}) # Dress-up consumers must retain the source set expression, # including a direct qUnion, because its ordered child # provenance is what lets the runtime apply exact set # algebra. (The shell consumer below intentionally keeps # its legacy flat direct-union contract.) query_set = _proven_query_set_selector(p.get("entities"), selectors) if source_set is not None else None if query_set is not None: selectors = [query_set] params = {"radius_mm" if item.operation == "fillet" else "distance_mm": amount} if item.operation == "fillet": params["tangent_propagation"] = _bool(p.get("tangentPropagation")) elif _bool(p.get("tangentPropagation")): params["tangent_propagation"] = True elif chamfer_type == "TWO_OFFSETS": second = _number(p.get("width2"), True) if _bool(p.get("oppositeDirection")): params["distance_mm"], second = second, params["distance_mm"] params["distance_2_mm"] = second elif chamfer_type == "OFFSET_ANGLE": angle = math.radians(_number(p.get("angle"))) if _bool(p.get("oppositeDirection")): second = amount * math.tan(angle); params["distance_mm"] = second; params["distance_2_mm"] = amount else: params["angle_rad"] = angle feature = {"id": fid, "name": item.feature_id, "atomic_id": item.operation, "depends_on": depends, "params": params, "selectors": selectors, "execution_status": "supported"} elif item.operation == "shell": thickness = _number(p.get("thickness"), True) selectors = []; offset_edge_planes: dict[str, dict[str, Any]] = {} cap_removals: list[tuple[str, str]] = [] source_set = _direct_query_set_operands(p.get("entities")) query_values = ( _query_set_leaf_values(p.get("entities")) if source_set is not None else _queries(p.get("entities")) ) for index, query_value in enumerate(query_values): query = parse_query(query_value) _call, _owner, topology, kind, _definition = _direct_make_query(query_value) if kind not in {"face", "entitytype.face"}: raise UnsupportedCapability("shell_face_selector", "current CDSL shell requires face removal selectors") if topology == "CAP_FACE": # A direct CAP_FACE names the extrusion's physical # builder output. It may serve a shell only while # that independent prism is still the active, # immediately preceding body. Other CAP histories # retain their deferred source query and cannot pick a # geometrically similar face here. selector = _cap_face_output_role_selector( query_value, feature_by_id, sketches_by_id, allow_initial_loft=True, allow_two_sided_circle_shell=True, allow_primary_add_shell=True, ) if selector is None: selector = _initial_direct_sweep_cap_output_role_selector( query_value, feature_by_id, sketches_by_id, ) if selector is None or previous[-1:] != [selector["owner_feature_id"]]: selector = _face_reference(query_value, feature_frames, sketch_by_source, entity_by_sketch) if query.owner_feature and query.is_start is not None: cap_removals.append((query.owner_feature, "start" if query.is_start else "end")) source_frame = feature_frames.get(query.owner_feature or "") or {} cap = source_frame.get("start" if query.is_start else "end") if cap is not None: for source, entity_id in _source_refs(query_value): offset_edge_planes[f"{source}:{entity_id}"] = dict(cap) elif topology == "SWEPT_FACE": selector = _direct_linear_extrude_swept_face_shell_reference( query_value, feature_frames, sketch_by_source, sketches_by_id, entity_by_sketch, feature_by_id, previous, model.featurescript_version, ) elif topology == "OFFSET_FACE": selector = _shell_offset_face_output_role_selector( query_value, feature_by_id, sketches_by_id, previous, ) elif topology == "COPY": selector = _pattern_copy_face_reference( query_value, feature_frames, sketch_by_source, entity_by_sketch, feature_by_id, ) else: raise UnsupportedCapability("shell_face_selector", "current CDSL shell requires CAP_FACE, direct linear-extrude SWEPT_FACE, or COPY(CAP_FACE) removal selectors") # Feature-output roles resolve only through the active # kernel snapshot. A stable id would turn that semantic # evidence into a stale geometric selector. if selector.get("output_role") is None and not isinstance(selector.get("selector_intent"), dict): selector["stable_id"] = f"cadfs_{fid}_{index}" selectors.append(selector) # A direct qUnion is already represented by the ordered flat # removal-face list expected by the established shell # contract. Preserve a QUERY_SET parent only for set # operators whose intersection/subtraction or nested union # semantics cannot be represented by that list. needs_query_set_parent = bool( source_set is not None and ( source_set[1] != "union" or any(_direct_query_set_operands(item) is not None for item in source_set[2]) ) ) query_set = _proven_query_set_selector(p.get("entities"), selectors) if needs_query_set_parent else None if query_set is not None: selectors = [query_set] if not selectors: raise ValueError("shell has no face removal selector") # CADFS's oppositeDirection selects the exterior material # side. The runtime contract carries this directly to OCC's # signed offset; it is not a request to reverse removal-face # ownership or a candidate for a current-body fallback. shell_params = {"thickness_mm": thickness, "inward": not _bool(p.get("oppositeDirection"))} if p.get("parts") is not None: try: shell_params["target_feature_id"] = _shell_target_body_source( p["parts"], previous, body_transform_aliases, lowered_body_members, ) except (UnsupportedCapability, ValueError) as error: # Keep the established face-scoped execution path for # legacy histories, but make the omitted parts-owner # proof visible instead of silently treating the active # aggregate as an explicitly selected body. diagnostics.append({ "code": "unresolved_body_source", "capability": "shell_parts_body_source", "feature_id": item.feature_id, "operation": item.operation, "message": str(error), }) feature = {"id": fid, "name": item.feature_id, "atomic_id": "shell", "depends_on": depends, "params": shell_params, "selectors": selectors, "execution_status": "supported"} def selector_owners(selector: dict[str, Any]) -> set[str]: owners: set[str] = set() owner = selector.get("owner_feature_id") if isinstance(owner, str) and owner: owners.add(owner.removeprefix("f_")) for child in selector.get("query_operands") or (): if isinstance(child, dict): owners.update(selector_owners(child)) return owners owners = set().union(*(selector_owners(selector) for selector in selectors)) if len(owners) == 1: source = next(iter(owners)) source_feature = feature_by_id.get(f"f_{source}") or {} source_frame = feature_frames.get(source) if source_frame is not None and source_feature.get("sketch_id"): shell_frame = { **source_frame, "shell_source": source, "shell_thickness_mm": thickness, "shell_profile_sketch_id": source_feature["sketch_id"], } # A derived inner wall has a bounded span only when # this direct shell removes exactly one known cap of # the same extrusion. Additional removal faces can # change its trim topology, so do not infer a wall. if ( shell_params["inward"] and len(cap_removals) == 1 and cap_removals[0][0] == source ): shell_frame["shell_inward"] = True shell_frame["shell_removed_cap"] = cap_removals[0][1] feature_frames[item.feature_id] = shell_frame if offset_edge_planes: frame = feature_frames.setdefault(item.feature_id, {}) frame["shell_offset_edge_planes"] = offset_edge_planes elif item.operation == "hole": locations = _queries(p.get("locations")); positions = []; host_plane = None; host_attachment = None for location in locations: location_query = parse_query(location) # A direct source vertex only has a usable local position # once its source sketch has an executable, explicit frame. # Do not label an unresolved host workplane as a malformed # vertex selector: it is an upstream sketch dependency. if ( _is_direct_hole_location_query(location) and isinstance(location_query.source_sketch, str) and location_query.source_sketch not in sketch_by_source ): raise UnsupportedCapability( "hole_location_sketch_unavailable", "hole location source sketch is not executable", ) resolved_location = _direct_hole_location(location, sketch_by_source, entity_by_sketch) if resolved_location is None: raise UnsupportedCapability( "hole_location_vertex", "hole location requires one direct original sketch vertex", ) position, location_plane = resolved_location if host_plane is not None and location_plane != host_plane: raise UnsupportedCapability( "hole_location_plane", "hole locations must share one explicit sketch plane", ) positions.append({"mm": position}) host_plane = location_plane attachment = ( sketch_by_source.get(location_query.source_sketch or "", {}).get("attachment") if isinstance(location_query.source_sketch, str) else None ) if attachment is not None and not isinstance(attachment, dict): raise UnsupportedCapability("hole_location_attachment", "hole location sketch attachment is invalid") if host_attachment is not None and attachment != host_attachment: raise UnsupportedCapability("hole_location_attachment", "hole locations must share one runtime attachment") host_attachment = attachment if not positions or host_plane is None: raise ValueError("hole has no resolved locations") frame = {**host_plane, "y_dir": _y_dir(host_plane)} if _bool(p.get("oppositeDirection")): if host_attachment is not None: raise UnsupportedCapability("hole_location_attachment", "attached hole workplane does not yet support oppositeDirection") frame = {**frame, "normal": [-v for v in frame["normal"]]} style = str(p.get("style") or "SIMPLE").split(".")[-1].lower(); end = str(p.get("endStyle") or "BLIND").upper() standard_through_diameter = _standard_tapped_through_bore_diameter(p, style, end) condition = "through_all_both" if "BOTH" in end else "through_all" if "THROUGH" in end or standard_through_diameter is not None else "blind" depth_value = p.get("holeDepth") or p.get("tappedDepth") if condition == "blind" and depth_value is None: raise ValueError("blind hole depth is unresolved") depth = _number(depth_value, True) if depth_value is not None else 1.0 condition_code = {"blind": 0, "through_all": 1, "through_all_both": 2}[condition] hole_params: dict[str, Any] = {"hole_type": style, "diameter_mm": standard_through_diameter or _number(p.get("holeDiameter"), True), "depth_mm": depth, "end_condition": {"type": condition, "solidworks_code": condition_code}, "positions": positions, "host_face": host_attachment if host_attachment is not None else {"frame": frame}} if style.upper() in {"COUNTERSINK", "C_SINK"}: hole_params["countersink"] = {"diameter_mm": _number(p.get("countersinkDiameter") or p.get("cSinkDiameter") or p.get("majorDiameter"), True), "angle_rad": math.radians(_number(p.get("countersinkAngle") or p.get("cSinkAngle") or 90.0))} if standard_through_diameter is None and style.upper() in {"COUNTERBORE", "C_BORE"}: hole_params["counterbore"] = {"diameter_mm": _number(p.get("counterboreDiameter") or p.get("cBoreDiameter") or p.get("majorDiameter"), True), "depth_mm": _number(p.get("counterboreDepth") or p.get("cBoreDepth"), True)} if _bool(p.get("isTappedThrough")) or p.get("tapSize") is not None: hole_params["thread"] = {"source": "CADFS", "decorative": True} if p.get("scope") is None: raise UnsupportedCapability( "hole_scope_body_source", "current CDSL CADFS hole lowering requires an explicit scope", ) hole_params["scope_feature_id"] = _hole_scope_body_source( p["scope"], previous, body_transform_aliases, lowered_body_members, ) feature = {"id": fid, "name": item.feature_id, "atomic_id": "hole_wizard", "depends_on": depends, "params": hole_params, "execution_status": "supported"} elif item.operation == "circularPattern": sources = _pattern_source_features(p.get("entities"), previous) sources = _pattern_body_history_sources( p.get("entities"), sources, previous, feature_by_id, body_transform_aliases, ) axis = _circular_pattern_axis(p.get("axis"), feature_frames, sketch_by_source, entity_by_sketch) count = int(_number(p.get("instanceCount"))) if count < 1: raise ValueError("circular pattern instanceCount must be positive") operation = str(p.get("operationType") or "NEW").split(".")[-1].upper() operation_mode = "remove" if any(value in operation for value in ("REMOVE", "CUT")) else "add" if operation_mode == "remove": for source in sources: source_feature = feature_by_id.get(source) if source_feature is None: raise ValueError("circular remove pattern source is unresolved") _pattern_remove_source(source_feature) feature = { "id": fid, "name": item.feature_id, "atomic_id": "pattern_circular", "depends_on": list(dict.fromkeys(sources + depends)), "params": { "source_feature_ids": sources, "axis": axis, "pattern_count": count, "sweep_angle_deg": _number(p.get("angle", 360.0)), "operation_mode": operation_mode, }, "execution_status": "supported", } elif item.operation == "mirror": owners = [] mirror_current_body = False for call in walk_calls(p.get("entities")): if call.name == "makeQuery" and call.args: owner = symbolic_string(call.args[0]); if "F" in owner: source_id = "f_" + owner[owner.find("F"):].split(".", 1)[0] if source_id in previous and source_id not in owners: owners.append(source_id) query = parse_query(call) if query.topology_type == "SWEPT_BODY" and query.kind in {"body", "entitytype.body"}: mirror_current_body = True if not owners: raise ValueError("mirror source features are unresolved") plane_query = p.get("mirrorPlane"); plane_info = parse_query(plane_query); plane_owner = f"f_{plane_info.owner_feature}" if plane_info.owner_feature else None if plane_owner and any(existing["id"] == plane_owner and existing["atomic_id"] == "reference_plane" for existing in features): mirror_plane = {"kind": "plane", "owner_feature_id": plane_owner, "stable_id": f"cadfs_{fid}_plane", "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": _explicit_datum_selector_intent(plane_query, kind="plane")} source_plane = feature_frames.get(plane_info.owner_feature or "", {}).get("start") if source_plane is None: raise ValueError("mirror plane frame is unresolved") else: plane = _mirror_plane_from_query( plane_query, feature_frames, sketch_by_source, entity_by_sketch, feature_by_id, sketches_by_id, previous, owners, model.featurescript_version, ) if plane is None: raise ValueError("mirror plane is not a default or reference plane") plane_owner = f"{fid}_plane" features.append({"id": plane_owner, "name": f"{item.feature_id} plane", "atomic_id": "reference_plane", "depends_on": depends, "params": {"plane": plane}, "execution_status": "supported"}) previous.append(plane_owner) mirror_plane = {"kind": "plane", "owner_feature_id": plane_owner, "stable_id": f"cadfs_{fid}_plane", "source": "runtime_snapshot", "confidence": 1.0, "selector_intent": _explicit_datum_selector_intent(plane_query, kind="plane")} source_plane = plane feature = {"id": fid, "name": item.feature_id, "atomic_id": "pattern_mirror", "depends_on": list(dict.fromkeys(owners + [plane_owner])), "params": {"source_feature_ids": owners, "mirror_plane": mirror_plane}, "selectors": [mirror_plane], "execution_status": "supported"} if mirror_current_body: feature["params"]["mirror_current_body"] = True # COPY(CAP_FACE) query 属于 pattern 的特定 instance,不能直接回退到 # source loft 的端盖。保存镜像变换和 source feature,供后续草图 # 在 lowering 期从 CADFS provenance 复算物理端盖 frame。 feature_frames[item.feature_id] = { "copy_transform": {"type": "mirror", "plane": source_plane}, "copy_source_features": [source[2:] for source in owners], } else: raise ValueError(f"operation mapping not implemented: {item.operation}") features.append(feature); feature_by_id[fid] = feature; feature_source_by_id[fid] = item.feature_id if ( item.operation == "transform" and feature.get("atomic_id") == "transform_bodies" and not bool(feature["params"].get("make_copy")) and not feature["params"].get("pattern_instance_refs") ): _record_non_copy_body_successors( body_transform_aliases, list(feature["params"].get("source_feature_ids") or ()), fid, ) _record_single_body_successor( body_transform_aliases, single_body_successor_state, feature, ) _record_lowered_body_members(lowered_body_members, feature) if ( item.operation == "extrude" and surface_profile_sketch is not None and not pure_surface_operation ): features.append(surface_feature) surface_profiles.append({ "profile": deepcopy(surface_profile_sketch["profile"]), "workplane": dict(surface_profile_sketch["workplane"]), **surface_feature["params"], }) if item.operation == "extrude" and pure_surface_operation: # A surface shell has no CAP/SWEPT body lifecycle. Do not let # subsequent source queries inherit a static prism frame. feature_frames.pop(item.feature_id, None) if item.operation != "assignVariable": previous.append(fid) except UnsupportedCapability as exc: diagnostics.append({"code": "unsupported_engine_capability", "capability": exc.capability, "feature_id": item.feature_id, "operation": item.operation, "message": str(exc)}); complete = False except Exception as exc: diagnostics.append({"code": "feature_deferred", "feature_id": item.feature_id, "operation": item.operation, "message": str(exc)}); complete = False if not features: return LoweringResult(None, "deferred_no_executable_feature", diagnostics, history) source_featurescript = { **({"version": model.featurescript_version} if model.featurescript_version else {}), **({"standard_library": model.standard_library} if model.standard_library else {}), **({"standard_library_version": model.standard_library_version} if model.standard_library_version else {}), **({"standard_library_imports": deepcopy(model.standard_library_imports)} if model.standard_library_imports else {}), } cdsl = {"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": model.sample_id, "meta": {"unit": "mm", "source": "CADFS", "provenance": provenance, "capability_gaps": sorted({d.get("operation") for d in diagnostics if d.get("operation")}), "source_featurescript": source_featurescript}, "geometry": {"sketches": sketches}, "features": features} _finalize_selector_intents(cdsl, model) return LoweringResult(cdsl, "converted_complete" if complete else "converted_partial", diagnostics, history)