Files
cdsl-cad/cadfs_to_cdsl/lowering.py
T

11322 lines
544 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<n>.<index>.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)