添加cadfs_to_cdsl代码,优化cad生成
This commit is contained in:
+112
-26
@@ -24,6 +24,11 @@ class LoweringResult:
|
||||
history: list[dict[str, Any]]
|
||||
|
||||
|
||||
class UnsupportedCapability(ValueError):
|
||||
def __init__(self, capability: str, message: str):
|
||||
super().__init__(message); self.capability = capability
|
||||
|
||||
|
||||
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]
|
||||
@@ -38,7 +43,7 @@ def _bool(value: Any) -> bool:
|
||||
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}
|
||||
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__":
|
||||
@@ -71,19 +76,40 @@ 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 _plane_from_query(value: Any, feature_frames: dict[str, dict[str, Any]]) -> dict[str, Any]:
|
||||
def _oriented_plane(plane: dict[str, Any], normal_sign: float, distance: float = 0.0) -> dict[str, Any]:
|
||||
shifted = _shift_plane(plane, distance)
|
||||
return {**shifted, "normal": [normal_sign * value for value in plane["normal"]]}
|
||||
|
||||
|
||||
def _plane_from_query(value: Any, feature_frames: dict[str, dict[str, Any]], sketch_by_source: 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)
|
||||
query = parse_query(value)
|
||||
if query.topology_type == "IMPRINT" and sketch_by_source and query.source_sketch in sketch_by_source:
|
||||
return dict(sketch_by_source[query.source_sketch]["workplane"])
|
||||
frame = feature_frames.get(query.owner_feature or "")
|
||||
if frame:
|
||||
if frame and query.topology_type == "CAP_FACE":
|
||||
return dict(frame["start" if query.is_start is not False else "end"])
|
||||
if frame and frame.get("start") == frame.get("end") and "qCreatedBy" in query.calls:
|
||||
return dict(frame["start"])
|
||||
raise ValueError("unsupported or unresolved sketch workplane")
|
||||
|
||||
|
||||
def _bound_name(value: Any) -> str:
|
||||
return str(value or "BLIND").split(".")[-1].upper()
|
||||
|
||||
|
||||
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}
|
||||
raise UnsupportedCapability(f"extrude_extent:{name.lower()}", f"current CDSL atomic set has no exact extrusion operation for {name}")
|
||||
|
||||
|
||||
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))
|
||||
@@ -134,7 +160,10 @@ def _lower_sketch(sketch: SketchIR, plane: dict[str, Any]) -> tuple[dict[str, An
|
||||
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"]}
|
||||
else:
|
||||
contours, construction = _contours(segments); construction.extend(explicit_construction)
|
||||
contours, open_segments = _contours(segments)
|
||||
if open_segments:
|
||||
raise ValueError(f"sketch has {len(open_segments)} open non-construction segment(s)")
|
||||
construction = list(explicit_construction)
|
||||
if not contours:
|
||||
profile = {"type": "analytic_contours", "contours": [], "construction": construction}
|
||||
return {"id": f"sketch_{sketch.feature_id}", "name": sketch.feature_id, "workplane": plane, "profile": profile, "role": "reference"}, entities
|
||||
@@ -164,6 +193,13 @@ def _source_sketch(params: dict[str, Any]) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
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
|
||||
@@ -188,7 +224,7 @@ def lower_model(model: ModelIR, provenance: dict[str, Any]) -> LoweringResult:
|
||||
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:
|
||||
plane = _plane_from_query(step.workplane, feature_frames)
|
||||
plane = _plane_from_query(step.workplane, feature_frames, sketch_by_source)
|
||||
lowered, entities = _lower_sketch(step, plane); sketches.append(lowered); sketch_by_source[step.feature_id] = lowered; entity_by_sketch[step.feature_id] = entities
|
||||
except Exception as exc:
|
||||
diagnostics.append({"code": "sketch_deferred", "feature_id": step.feature_id, "message": str(exc)}); complete = False
|
||||
@@ -200,20 +236,52 @@ def lower_model(model: ModelIR, provenance: dict[str, Any]) -> LoweringResult:
|
||||
try:
|
||||
fid = f"f_{item.feature_id}"; depends = list(previous[-1:]); p = item.params; feature: dict[str, Any]
|
||||
if item.operation == "cPlane":
|
||||
base = _plane_from_query(p.get("entities"), feature_frames); offset = _number(p.get("offset", 0), True); plane = _shift_plane(base, offset)
|
||||
plane_type = str(p.get("cplaneType") or "OFFSET").split(".")[-1].upper()
|
||||
if plane_type != "OFFSET":
|
||||
raise UnsupportedCapability(f"reference_plane:{plane_type.lower()}", f"current converter only supports exact OFFSET reference planes, not {plane_type}")
|
||||
base = _plane_from_query(p.get("entities"), feature_frames, sketch_by_source); offset = _number(p.get("offset", 0), True); plane = _shift_plane(base, offset)
|
||||
feature = {"id": fid, "name": item.feature_id, "atomic_id": "reference_plane", "depends_on": depends, "params": {"plane": plane, "offset_mm": offset}, "execution_status": "supported"}
|
||||
feature_frames[item.feature_id] = {"start": plane, "end": plane}
|
||||
elif item.operation == "extrude":
|
||||
if p.get("surfaceOperationType") is not None:
|
||||
raise UnsupportedCapability("extrude_surface_or_mixed", "current CDSL engine has no exact surface or mixed solid/surface extrusion operation")
|
||||
profile_kind = _profile_query_kind(p)
|
||||
if profile_kind and profile_kind not in {"IMPRINT"}:
|
||||
raise UnsupportedCapability(f"extrude_profile_topology:{profile_kind.lower()}", f"current CDSL engine cannot exactly replay an extrude profile selected from {profile_kind}")
|
||||
source = _source_sketch(p)
|
||||
if not source or source not in sketch_by_source: raise ValueError("extrude sketch query is unresolved")
|
||||
if not _profile_executable(sketch_by_source[source]): raise ValueError("extrude sketch has no closed profile")
|
||||
depth = _number(p.get("depth"), True); operation = str(p.get("operationType") or "NEW").upper(); reverse = _bool(p.get("oppositeDirection"))
|
||||
atomic = "extrude_cut_blind" if any(x in operation for x in ("REMOVE", "CUT")) else "extrude_add_two_sided" if _bool(p.get("hasSecondDirection")) else "extrude_add_blind"
|
||||
params = {"distance_mm": depth, "reverse": reverse}
|
||||
if atomic == "extrude_add_two_sided": params["reverse_distance_mm"] = _number(p.get("secondDirectionDepth", depth), True)
|
||||
end = _end_condition("SYMMETRIC" if _bool(p.get("symmetric")) else p.get("endBound"))
|
||||
depth_value = p.get("depth"); depth = _number(depth_value, True) if depth_value is not None else 1.0
|
||||
operation = str(p.get("operationType") or "NEW").upper(); reverse = _bool(p.get("oppositeDirection")); cutting = any(x in operation for x in ("REMOVE", "CUT"))
|
||||
second = _bool(p.get("hasSecondDirection"))
|
||||
if cutting and (second or end["type"] != "blind"):
|
||||
capability = "extrude_cut_two_sided" if second or end["type"] == "mid_plane" else 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"}:
|
||||
capability = f"extrude_add_{end['type']}"
|
||||
raise UnsupportedCapability(capability, f"current CDSL atomic set has no exact {capability} operation")
|
||||
if second:
|
||||
atomic = "extrude_add_two_sided"; params = {"distance_mm": depth, "reverse": reverse, "end_condition": end}
|
||||
reverse_depth = _number(p.get("secondDirectionDepth", depth), True)
|
||||
params.update({"reverse_distance_mm": reverse_depth, "reverse_end_condition": _end_condition(p.get("secondDirectionBound"))})
|
||||
elif end["type"] == "mid_plane" and not cutting:
|
||||
blind = _end_condition("BLIND")
|
||||
atomic = "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}
|
||||
feature = {"id": fid, "name": item.feature_id, "atomic_id": atomic, "depends_on": depends, "sketch_id": sketch_by_source[source]["id"], "params": params, "execution_status": "supported"}
|
||||
plane = sketch_by_source[source]["workplane"]; feature_frames[item.feature_id] = {"start": plane, "end": _shift_plane(plane, -depth if reverse else depth)}
|
||||
plane = sketch_by_source[source]["workplane"]
|
||||
if end["type"] == "blind" and not second:
|
||||
direction = -1 if reverse else 1
|
||||
feature_frames[item.feature_id] = {"start": dict(plane), "end": _shift_plane(plane, direction * depth)}
|
||||
elif end["type"] == "mid_plane":
|
||||
direction = -1 if reverse else 1
|
||||
feature_frames[item.feature_id] = {"start": _shift_plane(plane, -direction * depth / 2), "end": _shift_plane(plane, direction * depth / 2)}
|
||||
elif item.operation == "revolve":
|
||||
if p.get("surfaceOperationType") is not None and p.get("operationType") is None:
|
||||
raise UnsupportedCapability("revolve_surface", "current CDSL engine has no exact surface-revolve operation")
|
||||
source = _source_sketch(p)
|
||||
if not source or source not in sketch_by_source: raise ValueError("revolve sketch query is unresolved")
|
||||
if not _profile_executable(sketch_by_source[source]): raise ValueError("revolve sketch has no closed profile")
|
||||
@@ -223,17 +291,24 @@ def lower_model(model: ModelIR, provenance: dict[str, Any]) -> LoweringResult:
|
||||
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]
|
||||
operation = str(p.get("operationType") or p.get("surfaceOperationType") or "NEW").upper(); atomic = "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))
|
||||
feature = {"id": fid, "name": item.feature_id, "atomic_id": atomic, "depends_on": depends, "sketch_id": sketch_by_source[source]["id"], "params": {"angle_deg": angle, "axis": {"origin_mm": start, "direction": direction}}, "execution_status": "supported"}
|
||||
feature = {"id": fid, "name": item.feature_id, "atomic_id": atomic, "depends_on": depends, "sketch_id": sketch_by_source[source]["id"], "params": {"angle_deg": angle, "reverse": _bool(p.get("oppositeDirection")), "axis": {"origin_mm": start, "direction": direction}}, "execution_status": "supported"}
|
||||
elif item.operation in {"fillet", "chamfer"}:
|
||||
key = "radius" if item.operation == "fillet" else "width"; amount = _number(p.get(key), True); selectors = []
|
||||
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 = []
|
||||
for index, query_value in enumerate(_queries(p.get("entities"))):
|
||||
query = parse_query(query_value); owner = query.owner_feature
|
||||
if not owner: raise ValueError("selector owner is unresolved")
|
||||
selector_kind = "face" if query.kind in {"face", "entitytype.face"} or query.topology_type in {"CAP_FACE", "SWEPT_FACE"} else "edge"
|
||||
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 and frame["start" if query.is_start else "end"]
|
||||
if source_entity and cap:
|
||||
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))}
|
||||
if selector_kind == "edge" and source_entity and cap:
|
||||
if query.topology_type == "SWEPT_EDGE" and frame:
|
||||
local_point = source_entity.get("point") if source_entity["type"] == "point" else None
|
||||
if len(refs) >= 2:
|
||||
@@ -244,20 +319,28 @@ def lower_model(model: ModelIR, provenance: dict[str, Any]) -> LoweringResult:
|
||||
start, end = _global(frame["start"], local_point), _global(frame["end"], local_point)
|
||||
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)]}
|
||||
elif source_entity["type"] == "circle":
|
||||
# OCC/build123d commonly splits a closed circular edge into four
|
||||
# quarter-circle records. Bind all four deterministic arc centres.
|
||||
radius = source_entity["radius_mm"]; center = source_entity["center"]
|
||||
for quadrant, (sx, sy) in enumerate(((1, 1), (-1, 1), (-1, -1), (1, -1))):
|
||||
local = [center[0] + sx*radius/math.sqrt(2), center[1] + sy*radius/math.sqrt(2)]
|
||||
selectors.append({"kind": "edge", "owner_feature_id": f"f_{owner}", "stable_id": f"cadfs_{fid}_{index}_{quadrant}", "source": "runtime_snapshot", "confidence": 1.0, "geometry": {"curve_type": "circle", "center_mm": _global(cap, local)}})
|
||||
# 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"]}})
|
||||
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")
|
||||
selectors.append({"kind": "edge", "owner_feature_id": f"f_{owner}", "stable_id": f"cadfs_{fid}_{index}", "source": "runtime_snapshot", "confidence": 1.0, "geometry": geometry})
|
||||
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})
|
||||
params = {"radius_mm" if item.operation == "fillet" else "distance_mm": amount}
|
||||
if item.operation == "fillet": params["tangent_propagation"] = _bool(p.get("tangentPropagation"))
|
||||
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 == "hole":
|
||||
locations = _queries(p.get("locations")); positions = []; host_plane = None
|
||||
@@ -274,11 +357,12 @@ def lower_model(model: ModelIR, provenance: dict[str, Any]) -> LoweringResult:
|
||||
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
|
||||
hole_params: dict[str, Any] = {"hole_type": style, "diameter_mm": _number(p.get("holeDiameter"), True), "depth_mm": depth, "end_condition": {"type": condition, "solidworks_code": 1}, "positions": positions, "host_face": {"frame": frame}}
|
||||
if "COUNTERSINK" in style.upper():
|
||||
hole_params["countersink"] = {"diameter_mm": _number(p.get("countersinkDiameter") or p.get("majorDiameter"), True), "angle_rad": math.radians(_number(p.get("countersinkAngle") or 90.0))}
|
||||
if "COUNTERBORE" in style.upper():
|
||||
hole_params["counterbore"] = {"diameter_mm": _number(p.get("counterboreDiameter") or p.get("majorDiameter"), True), "depth_mm": _number(p.get("counterboreDepth"), True)}
|
||||
condition_code = {"blind": 0, "through_all": 1, "through_all_both": 2}[condition]
|
||||
hole_params: dict[str, Any] = {"hole_type": style, "diameter_mm": _number(p.get("holeDiameter"), True), "depth_mm": depth, "end_condition": {"type": condition, "solidworks_code": condition_code}, "positions": positions, "host_face": {"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 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}
|
||||
feature = {"id": fid, "name": item.feature_id, "atomic_id": "hole_wizard", "depends_on": depends, "params": hole_params, "execution_status": "supported"}
|
||||
elif item.operation == "mirror":
|
||||
@@ -304,6 +388,8 @@ def lower_model(model: ModelIR, provenance: dict[str, Any]) -> LoweringResult:
|
||||
else:
|
||||
raise ValueError(f"operation mapping not implemented: {item.operation}")
|
||||
features.append(feature); 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)
|
||||
|
||||
Reference in New Issue
Block a user