优化skill,添加特征树、编辑窗口

This commit is contained in:
2026-08-07 17:44:12 +08:00
parent 35e18ca956
commit 5fbcf2c0b8
70 changed files with 13076 additions and 465 deletions
+541 -8
View File
@@ -231,7 +231,13 @@ def _validate_compiler_payload(payload: dict[str, Any]) -> dict[str, Any]:
name
for name in editable
if not parameters[name].get("editable")
or _parameter_reference_count(features, name) == 0
or (
_parameter_reference_count(features, name) == 0
and _parameter_expression_reference_count(
features, payload.get("expressions", {}), name
)
== 0
)
)
if disconnected:
raise DesignIRError(
@@ -307,12 +313,15 @@ def migrate_designir_2_to_3(payload: dict[str, Any]) -> dict[str, Any]:
def compiler_payload(payload: dict[str, Any]) -> dict[str, Any]:
"""Return the stable flat representation consumed by both CAD backends."""
version = payload.get("schema_version")
if version is None and payload.get("designir_kind") == DESIGNIR_KIND:
version = SCHEMA_VERSION
if version == LEGACY_SCHEMA_VERSION:
return _validate_compiler_payload(copy.deepcopy(payload))
if version != SCHEMA_VERSION or payload.get("designir_kind") != DESIGNIR_KIND:
raise DesignIRError(
"DesignIR must be legacy 2.0 or independent_parametric_cad 3.0"
)
payload = _normalize_tool_authored_designir(payload)
mode = payload.get("reconstruction_mode")
if mode == "surface_parametric":
raise DesignIRError(
@@ -331,8 +340,16 @@ def compiler_payload(payload: dict[str, Any]) -> dict[str, Any]:
semantic = payload.get("semantic_layer")
if not isinstance(semantic, dict):
raise DesignIRError("semantic_layer must be an object")
semantic = _normalize_tool_authored_semantic_layer(payload, semantic)
edit_interface = payload.get("edit_interface", {})
validation = payload.get("validation_contract", {})
validation = copy.deepcopy(payload.get("validation_contract", {}))
if isinstance(validation, dict):
validation["perturbations"] = _normalize_validation_list(
validation.get("perturbations", [])
)
validation["invariants"] = _normalize_validation_list(
validation.get("invariants", []), parameter_key="id"
)
flat = {
"schema_version": LEGACY_SCHEMA_VERSION,
"designir_kind": LEGACY_DESIGNIR_KIND,
@@ -366,6 +383,462 @@ def compiler_payload(payload: dict[str, Any]) -> dict[str, Any]:
return _validate_compiler_payload(flat)
def _map_object_to_list(value: Any) -> Any:
if isinstance(value, dict):
return [
{"id": str(key), **item}
if isinstance(item, dict) and "id" not in item
else item
for key, item in value.items()
]
return value
def _map_named_list_to_object(value: Any) -> Any:
if not isinstance(value, list):
return value
mapped: dict[str, Any] = {}
for index, item in enumerate(value):
if not isinstance(item, dict):
continue
name = item.get("name") or item.get("id") or f"item_{index + 1}"
mapped[str(name)] = {
key: child
for key, child in item.items()
if key not in {"name", "id"}
}
return mapped if mapped else value
def _normalize_expression_object(value: Any) -> Any:
if not isinstance(value, list):
return value
mapped: dict[str, Any] = {}
for item in value:
if not isinstance(item, dict):
continue
name = item.get("name") or item.get("id")
expression = item.get("expression") or item.get("value")
if name and expression is not None:
mapped[str(name)] = expression
return mapped if mapped else value
def _normalize_semantic_parameter_names(value: Any) -> list[str]:
if not isinstance(value, list):
return []
names = []
for item in value:
if isinstance(item, str):
names.append(item)
elif isinstance(item, dict):
name = item.get("name") or item.get("id")
if isinstance(name, str):
names.append(name)
return names
def _semantic_parameter_metadata(value: Any) -> dict[str, dict[str, Any]]:
if not isinstance(value, list):
return {}
metadata: dict[str, dict[str, Any]] = {}
for item in value:
if not isinstance(item, dict):
continue
name = item.get("name") or item.get("id")
if isinstance(name, str):
metadata[name] = item
return metadata
def _normalize_coordinate_system(value: Any) -> Any:
if not isinstance(value, dict):
return value
axes = value.get("axes")
if isinstance(axes, dict):
normalized = dict(value)
normalized.setdefault("x_axis", axes.get("x"))
normalized.setdefault("y_axis", axes.get("y"))
normalized.setdefault("z_axis", axes.get("z"))
normalized.pop("axes", None)
return normalized
return value
def _parameter_ref(name: str) -> dict[str, str]:
return {"parameter": name}
def _value_or_parameter_ref(parameters: dict[str, Any], name: str, fallback: Any) -> Any:
return _parameter_ref(name) if name in parameters else fallback
def _expression_or_value_ref(expressions: dict[str, Any], name: str, fallback: Any) -> Any:
return {"expression": name} if name in expressions else fallback
def _circle_profile(sketches: dict[str, Any], sketch_id: Any) -> dict[str, Any] | None:
sketch = sketches.get(str(sketch_id))
if not isinstance(sketch, dict):
return None
profile = sketch.get("profile")
if isinstance(profile, dict) and profile.get("type") == "circle":
return profile
return None
def _center3(profile: dict[str, Any], z: Any = 0) -> list[Any]:
center = profile.get("center", [0, 0])
if not isinstance(center, list):
center = [0, 0]
return [
center[0] if len(center) > 0 else 0,
center[1] if len(center) > 1 else 0,
z,
]
def _diameter_from_profile(
parameters: dict[str, Any],
expressions: dict[str, Any],
profile: dict[str, Any],
*,
diameter_parameter: str,
radius_expression: str,
) -> Any:
if diameter_parameter in parameters:
return _parameter_ref(diameter_parameter)
if radius_expression in expressions:
return {"expression": radius_expression}
radius = profile.get("radius")
if isinstance(radius, (int, float)):
return float(radius) * 2.0
return radius
def _radius_from_profile(
parameters: dict[str, Any],
expressions: dict[str, Any],
profile: dict[str, Any],
*,
radius_parameter: str,
radius_expression: str,
) -> Any:
if radius_parameter in parameters:
return _parameter_ref(radius_parameter)
if radius_expression in expressions:
return {"expression": radius_expression}
return profile.get("radius")
def _distance_xy(center: list[Any]) -> float | None:
if len(center) < 2:
return None
try:
return math.sqrt(float(center[0]) ** 2 + float(center[1]) ** 2)
except (TypeError, ValueError):
return None
def _normalize_common_llm_feature_program(
semantic: dict[str, Any],
) -> list[dict[str, Any]] | None:
raw_features = semantic.get("features")
if not isinstance(raw_features, dict):
return None
sketches = semantic.get("sketches", {})
if not isinstance(sketches, dict):
return None
patterns = semantic.get("patterns", {})
if not isinstance(patterns, dict):
patterns = {}
parameters = semantic.get("parameters", {})
if not isinstance(parameters, dict):
parameters = {}
expressions = semantic.get("expressions", {})
if not isinstance(expressions, dict):
expressions = {}
generated_bodies: list[dict[str, Any]] = []
generated_cuts: list[dict[str, Any]] = []
generated_patterns: list[dict[str, Any]] = []
converted_pattern_sources: set[str] = set()
host_feature_id: str | None = None
for pattern_id, pattern in patterns.items():
if not isinstance(pattern, dict) or pattern.get("type") not in {
"circular",
"polar",
}:
continue
source_feature_id = str(pattern.get("feature") or "")
source_feature = raw_features.get(source_feature_id)
if not isinstance(source_feature, dict):
continue
profile = _circle_profile(sketches, source_feature.get("sketch"))
if profile is None:
continue
center = profile.get("center", [0, 0])
pitch = _value_or_parameter_ref(
parameters,
"bolt_circle_diameter",
None,
)
if pitch is None:
radius = _distance_xy(center if isinstance(center, list) else [])
pitch = radius * 2.0 if radius is not None else pattern.get("pitch_diameter")
generated_patterns.append(
{
"id": str(pattern_id),
"operation": "polar_hole_pattern",
"count": _value_or_parameter_ref(
parameters,
"bolt_count",
pattern.get("count", len(pattern.get("instances", [])) or 1),
),
"diameter": _diameter_from_profile(
parameters,
expressions,
profile,
diameter_parameter="bolt_hole_diameter",
radius_expression="bolt_hole_radius",
),
"pitch_diameter": pitch,
"axis": "primary_axis",
"host": host_feature_id or "flange_body",
}
)
converted_pattern_sources.add(source_feature_id)
for feature_id, feature in raw_features.items():
if not isinstance(feature, dict) or feature_id in converted_pattern_sources:
continue
profile = _circle_profile(sketches, feature.get("sketch"))
if profile is None:
continue
feature_type = str(feature.get("type") or "").lower()
operation = str(feature.get("operation") or "").lower()
if feature_type == "extrude" or operation == "new_body":
generated_feature = {
"id": str(feature_id),
"operation": "extrude_circle",
"radius": _radius_from_profile(
parameters,
expressions,
profile,
radius_parameter="flange_outer_radius",
radius_expression="flange_outer_radius",
),
"height": _value_or_parameter_ref(
parameters,
"flange_thickness",
feature.get("depth", feature.get("height")),
),
"axis": "primary_axis",
"center": _center3(profile),
}
generated_bodies.append(generated_feature)
host_feature_id = str(feature_id)
elif feature_type == "cut_extrude" or operation == "cut":
generated_cuts.append(
{
"id": str(feature_id),
"operation": "through_hole",
"diameter": _diameter_from_profile(
parameters,
expressions,
profile,
diameter_parameter="bore_diameter",
radius_expression="bore_radius",
),
"axis": "primary_axis",
"center": _center3(profile),
"host": host_feature_id or "flange_body",
}
)
generated = generated_bodies + generated_cuts
for pattern_feature in generated_patterns:
if (
pattern_feature.get("host") == "flange_body"
and host_feature_id is not None
):
pattern_feature["host"] = host_feature_id
generated.append(pattern_feature)
if not generated:
return None
return generated
def _normalize_validation_list(value: Any, *, parameter_key: str = "parameter") -> Any:
if isinstance(value, dict):
rows = [
(
{
parameter_key: str(key),
**item,
}
if isinstance(item, dict)
else {parameter_key: str(key), "description": item}
)
for key, item in value.items()
]
elif isinstance(value, list):
rows = [
copy.deepcopy(item)
if isinstance(item, dict)
else {"description": item}
for item in value
]
else:
return value
if parameter_key == "parameter":
for row in rows:
row.setdefault(
"expected_change",
row.get("acceptance") or row.get("description") or "geometry_changes",
)
return rows
def _normalize_feature_aliases(feature: dict[str, Any]) -> dict[str, Any]:
normalized = copy.deepcopy(feature)
operation = normalized.get("operation")
if operation == "polar_hole_pattern":
if "diameter" not in normalized and "hole_diameter" in normalized:
normalized["diameter"] = normalized.pop("hole_diameter")
reference = normalized.get("reference")
if (
"host" not in normalized
and isinstance(reference, dict)
and isinstance(reference.get("feature"), str)
):
normalized["host"] = reference["feature"]
elif operation == "through_hole":
reference = normalized.get("reference")
if (
"host" not in normalized
and isinstance(reference, dict)
and isinstance(reference.get("feature"), str)
):
normalized["host"] = reference["feature"]
if operation in SUPPORTED_OPERATIONS:
normalized.setdefault("axis", "primary_axis")
return normalized
def _normalize_feature_list_aliases(value: Any) -> Any:
features = _map_object_to_list(value)
if not isinstance(features, list):
return features
return [
_normalize_feature_aliases(feature)
if isinstance(feature, dict)
else feature
for feature in features
]
def _normalize_tool_authored_designir(payload: dict[str, Any]) -> dict[str, Any]:
normalized = copy.deepcopy(payload)
normalized.setdefault("schema_version", SCHEMA_VERSION)
if normalized.get("units") is None:
normalized["units"] = "mm"
semantic = normalized.get("semantic_layer")
if isinstance(semantic, dict):
normalized["semantic_layer"] = _normalize_tool_authored_semantic_layer(
normalized, semantic
)
edit_interface = normalized.get("edit_interface")
if isinstance(edit_interface, dict):
edit_interface["semantic_parameters"] = _normalize_semantic_parameter_names(
edit_interface.get("semantic_parameters", [])
)
validation = normalized.get("validation_contract")
if isinstance(validation, dict):
validation["perturbations"] = _normalize_validation_list(
validation.get("perturbations", [])
)
validation["invariants"] = _normalize_validation_list(
validation.get("invariants", []), parameter_key="id"
)
return normalized
def _normalize_tool_authored_semantic_layer(
payload: dict[str, Any], semantic: dict[str, Any]
) -> dict[str, Any]:
normalized = copy.deepcopy(semantic)
if normalized.get("reconstruction_status") in {
"independent",
"fully_parametric",
"fully_semantic_parametric",
}:
normalized["reconstruction_status"] = "ready"
normalized["coordinate_system"] = _normalize_coordinate_system(
normalized.get("coordinate_system")
)
coordinate = normalized.get("coordinate_system")
if isinstance(coordinate, dict):
coordinate.setdefault("origin", [0, 0, 0])
coordinate.setdefault("x_axis", [1, 0, 0])
coordinate.setdefault("y_axis", [0, 1, 0])
coordinate.setdefault("z_axis", [0, 0, 1])
normalized["parameters"] = _map_named_list_to_object(
normalized.get("parameters", {})
)
normalized["expressions"] = _normalize_expression_object(
normalized.get("expressions", {})
)
normalized["datums"] = _map_named_list_to_object(
normalized.get("datums", {})
)
datums = normalized.setdefault("datums", {})
if isinstance(datums, dict):
datums.setdefault("primary_axis", {"kind": "axis", "axis": "z"})
edit_interface = payload.get("edit_interface", {})
editable_names = set(
_normalize_semantic_parameter_names(
edit_interface.get("semantic_parameters", [])
)
if isinstance(edit_interface, dict)
else []
)
edit_metadata = (
_semantic_parameter_metadata(edit_interface.get("semantic_parameters", []))
if isinstance(edit_interface, dict)
else {}
)
parameters = normalized.get("parameters")
if isinstance(parameters, dict):
for name, parameter in parameters.items():
if isinstance(parameter, dict):
parameter.setdefault("editable", name in editable_names)
metadata = edit_metadata.get(name, {})
if metadata.get("label") and not parameter.get("display_name"):
parameter["display_name"] = metadata["label"]
if metadata.get("range") and not parameter.get("range"):
parameter["range"] = metadata["range"]
converted_features = _normalize_common_llm_feature_program(normalized)
if converted_features is not None:
normalized["features"] = converted_features
else:
normalized["features"] = _normalize_feature_list_aliases(
normalized.get("features", [])
)
normalized["sketches"] = _map_object_to_list(normalized.get("sketches", []))
normalized["patterns"] = _map_object_to_list(normalized.get("patterns", []))
normalized["attachments"] = _map_object_to_list(normalized.get("attachments", []))
normalized["constraints"] = _normalize_validation_list(
normalized.get("constraints", []), parameter_key="id"
)
normalized["construction_stages"] = _map_object_to_list(
normalized.get("construction_stages", [])
)
return normalized
def validate_designir(payload: dict[str, Any]) -> dict[str, Any]:
"""Validate either supported version and return its compiler representation."""
return compiler_payload(payload)
@@ -423,7 +896,7 @@ def resolve_value(value: Any, values: dict[str, float], label: str) -> float:
if isinstance(value, dict) and set(value) == {"expression"}:
name = str(value["expression"])
if name not in values:
raise DesignIRError(f"{label} references unknown expression {name}")
return _safe_expression(name, values)
return values[name]
raise DesignIRError(f"{label} must be a number, parameter, or expression")
@@ -578,10 +1051,9 @@ def build_shape(payload: dict[str, Any]) -> Part:
if shape is None:
raise DesignIRError("DesignIR produced no geometry")
result = Part(shape.wrapped)
if not list(result.solids()) or result.volume <= 0:
if not list(shape.solids()) or shape.volume <= 0:
raise DesignIRError("DesignIR did not produce a valid solid")
return result
return shape
def build_simplecad_shape(payload: dict[str, Any]) -> tuple[Any, str]:
@@ -920,9 +1392,26 @@ def _distance(first: list[float], second: list[float]) -> float:
return math.sqrt(sum((a - b) ** 2 for a, b in zip(first, second)))
def _boolean_result_volume(value: Any) -> float:
volume = getattr(value, "volume", None)
if isinstance(volume, (int, float)):
return float(volume)
try:
return sum(
float(getattr(item, "volume", 0.0))
for item in value
)
except TypeError as exc:
raise DesignIRError(
f"Boolean result has no measurable volume: {type(value).__name__}"
) from exc
def _symmetric_difference_volume(first: Part, second: Part) -> float:
try:
return float((first - second).volume + (second - first).volume)
return _boolean_result_volume(first - second) + _boolean_result_volume(
second - first
)
except Exception as exc: # OpenCascade failures become an explicit metric state.
raise DesignIRError(f"Symmetric-difference boolean failed: {exc}") from exc
@@ -939,6 +1428,50 @@ def _parameter_reference_count(value: Any, parameter: str) -> int:
return 0
def _expression_reference_names(value: Any) -> set[str]:
if isinstance(value, dict):
direct = {str(value["expression"])} if isinstance(value.get("expression"), str) else set()
return direct.union(
*(_expression_reference_names(child) for child in value.values())
)
if isinstance(value, list):
names: set[str] = set()
for child in value:
names.update(_expression_reference_names(child))
return names
return set()
def _expression_reference_values(value: Any) -> set[str]:
if isinstance(value, dict):
direct = {str(value["expression"])} if isinstance(value.get("expression"), str) else set()
values = set(direct)
for child in value.values():
values.update(_expression_reference_values(child))
return values
if isinstance(value, list):
values: set[str] = set()
for child in value:
values.update(_expression_reference_values(child))
return values
return set()
def _parameter_expression_reference_count(
value: Any, expressions: Any, parameter: str
) -> int:
if not isinstance(expressions, dict):
expressions = {}
count = 0
for expression_value in _expression_reference_values(value):
expression = expressions.get(expression_value, expression_value)
if isinstance(expression, str):
count += len(
re.findall(rf"\b{re.escape(parameter)}\b", expression)
)
return count
def acceptance_report(
teacher_path: Path,
rebuilt_path: Path,
@@ -1186,7 +1719,7 @@ def main(argv: list[str] | None = None) -> int:
migrated = (
migrate_designir_2_to_3(original)
if original.get("schema_version") == LEGACY_SCHEMA_VERSION
else original
else _normalize_tool_authored_designir(original)
)
compiler_payload(migrated)
write_json(args.output, migrated)