更新功能

This commit is contained in:
2026-08-26 14:13:11 +08:00
parent beaa5dd9fe
commit a508e6f07e
47 changed files with 5616 additions and 520 deletions
+72 -5
View File
@@ -8,9 +8,10 @@ from typing import Any
QUALITY_RULE_TYPES = frozenset({
"bbox", "solid_count", "feature_count", "hole_count", "hole_diameter",
"hole_center", "overall_length", "overall_diameter", "through_condition",
"hole_center", "overall_length", "overall_width", "overall_height",
"overall_diameter", "through_condition",
})
_FEATURE_RULE_TYPES = {"hole_count", "hole_diameter", "hole_center", "through_condition"}
FEATURE_RULE_TYPES = frozenset({"hole_count", "hole_diameter", "hole_center", "through_condition"})
_SEVERITIES = {"blocking", "warning", "informational"}
@@ -22,7 +23,12 @@ def _bbox_bounds(engine_result: dict[str, Any], feature_id: str | None = None) -
if not isinstance(record, dict):
continue
owners = record.get("owner_feature_ids") or []
if record.get("feature_id") == feature_id or feature_id in owners:
# Runtime records may carry the current operation in feature_id
# while owner_feature_ids identify the actual geometry owner. Use
# ownership when present so inherited faces do not pollute a
# feature-local measurement.
matches = feature_id in owners if owners else record.get("feature_id") == feature_id
if matches:
geometry = record.get("geometry")
if isinstance(geometry, dict):
candidates.append(geometry.get("bbox_mm"))
@@ -88,12 +94,64 @@ def _circles(profile: dict[str, Any]) -> list[dict[str, Any]]:
]
def _circle_feature_bbox(feature: dict[str, Any] | None, sketch: dict[str, Any] | None) -> dict[str, Any] | None:
"""Derive a stable world-space bbox for a circular extrude feature."""
if not isinstance(feature, dict) or not isinstance(sketch, dict):
return None
if str(feature.get("atomic_id") or "") != "extrude_add_blind":
return None
profile = sketch.get("profile")
workplane = sketch.get("workplane")
params = feature.get("params")
if not isinstance(profile, dict) or profile.get("type") != "circle" or not isinstance(workplane, dict):
return None
center_local = profile.get("center")
radius = profile.get("radius_mm")
origin = workplane.get("origin_mm")
x_dir = workplane.get("x_dir")
y_dir = workplane.get("y_dir")
normal = workplane.get("normal")
distance = (params or {}).get("distance_mm") if isinstance(params, dict) else None
if not isinstance(center_local, list) or len(center_local) < 2 or not _finite_number(radius):
return None
if not all(isinstance(value, list) and len(value) >= 3 for value in (origin, x_dir, y_dir, normal)) or not _finite_number(distance):
return None
center = [
float(origin[index]) + float(center_local[0]) * float(x_dir[index]) + float(center_local[1]) * float(y_dir[index])
for index in range(3)
]
points = []
for axis_x in (-1.0, 1.0):
for axis_y in (-1.0, 1.0):
cross_section = [
center[index]
+ axis_x * float(radius) * float(x_dir[index])
+ axis_y * float(radius) * float(y_dir[index])
for index in range(3)
]
points.append(cross_section)
points.append([
cross_section[index] + float(distance) * float(normal[index])
for index in range(3)
])
minimum = [min(point[index] for point in points) for index in range(3)]
maximum = [max(point[index] for point in points) for index in range(3)]
return {
"min": minimum,
"max": maximum,
"dimensions": [maximum[index] - minimum[index] for index in range(3)],
}
def _finite_number(value: Any) -> bool:
return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(float(value))
def _validate_expected(kind: str, expected: Any, index: int) -> None:
scalar_types = {"solid_count", "feature_count", "hole_count", "hole_diameter", "overall_length", "overall_diameter"}
scalar_types = {
"solid_count", "feature_count", "hole_count", "hole_diameter",
"overall_length", "overall_width", "overall_height", "overall_diameter",
}
if kind in scalar_types and not _finite_number(expected):
raise ValueError(f"verification.rules[{index}].expected must be a finite number for {kind}")
if kind == "bbox":
@@ -157,7 +215,7 @@ def validate_verification(verification: Any, cdsl: dict[str, Any]) -> list[dict[
if not math.isfinite(tolerance) or tolerance < 0:
raise ValueError(f"verification.rules[{index}].tolerance must be finite and non-negative")
feature = str(raw.get("feature") or "").strip()
if kind in _FEATURE_RULE_TYPES and not feature:
if kind in FEATURE_RULE_TYPES and not feature:
raise ValueError(f"verification.rules[{index}].feature is required for {kind}")
if feature and feature not in feature_ids:
raise ValueError(f"verification.rules[{index}].feature must reference a CDSL feature ID")
@@ -175,6 +233,9 @@ def _actual(rule: dict[str, Any], cdsl: dict[str, Any], engine_result: dict[str,
circles = _circles(profile) if isinstance(profile, dict) else []
dimensions = _dimensions(engine_result)
if kind == "bbox":
derived = _circle_feature_bbox(feature, sketch)
if target and derived is not None:
return derived, f"cdsl.features.{target}.sketch + params"
value = _bbox_value(engine_result, target)
source = "runtime.bbox_mm" if not target else f"runtime.topology_records[{target}].bbox_mm"
return value, source
@@ -194,7 +255,13 @@ def _actual(rule: dict[str, Any], cdsl: dict[str, Any], engine_result: dict[str,
return (centers[0] if len(centers) == 1 else centers), f"cdsl.features.{target}.sketch"
if kind == "overall_length":
return (max(dimensions) if dimensions else None), "runtime.bbox_mm"
if kind == "overall_width":
return (dimensions[1] if dimensions and len(dimensions) > 1 else None), "runtime.bbox_mm[y]"
if kind == "overall_height":
return (dimensions[2] if dimensions and len(dimensions) > 2 else None), "runtime.bbox_mm[z]"
if kind == "overall_diameter":
if target and circles:
return max(circle["radius_mm"] for circle in circles) * 2.0, f"cdsl.features.{target}.sketch"
return (min(dimensions) if dimensions else None), "runtime.bbox_mm"
if kind == "through_condition":
end = params.get("end_condition") if isinstance(params.get("end_condition"), dict) else {}