优化skill,添加特征树、编辑窗口
This commit is contained in:
@@ -15,6 +15,14 @@ MODULE = importlib.util.module_from_spec(SPEC)
|
||||
assert SPEC.loader
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
FEATURE_TREE_SCRIPT = ROOT / "scripts" / "feature_tree.py"
|
||||
FEATURE_TREE_SPEC = importlib.util.spec_from_file_location(
|
||||
"feature_tree", FEATURE_TREE_SCRIPT
|
||||
)
|
||||
FEATURE_TREE = importlib.util.module_from_spec(FEATURE_TREE_SPEC)
|
||||
assert FEATURE_TREE_SPEC.loader
|
||||
FEATURE_TREE_SPEC.loader.exec_module(FEATURE_TREE)
|
||||
|
||||
|
||||
class DesignIRPipelineTests(unittest.TestCase):
|
||||
def payload(self) -> dict:
|
||||
@@ -66,6 +74,257 @@ class DesignIRPipelineTests(unittest.TestCase):
|
||||
MODULE.compile_designir(hybrid, rebuilt)
|
||||
self.assertTrue(rebuilt.is_file())
|
||||
|
||||
def test_feature_tree_is_derived_from_semantic_designir(self) -> None:
|
||||
migrated = MODULE.migrate_designir_2_to_3(self.payload())
|
||||
tree = FEATURE_TREE.generate_feature_tree(
|
||||
migrated,
|
||||
designir_path="flange.designir.json",
|
||||
step_path="flange.step",
|
||||
backend="build123d",
|
||||
compiled=True,
|
||||
validated=True,
|
||||
)
|
||||
self.assertEqual("solidworks_feature_manager", tree["tree_kind"])
|
||||
self.assertEqual("1.0", tree["schema_version"])
|
||||
serialized = json.dumps(tree)
|
||||
self.assertNotIn('"designir_3"', serialized)
|
||||
self.assertNotIn('"source_designir_feature"', serialized)
|
||||
self.assertNotIn('"manufacturing"', serialized)
|
||||
self.assertEqual(
|
||||
"authored_semantic",
|
||||
tree["source"]["history_kind"],
|
||||
)
|
||||
self.assertEqual(
|
||||
"valid",
|
||||
tree["validation"]["structure"]["status"],
|
||||
)
|
||||
nodes = {
|
||||
node["id"]: node
|
||||
for node in tree["feature_manager"]["nodes"]
|
||||
}
|
||||
self.assertIn("sw:front_plane", nodes)
|
||||
self.assertIn("sketch:base_flange", nodes)
|
||||
self.assertIn("feature:base_flange", nodes)
|
||||
self.assertIn("feature:central_bore", nodes)
|
||||
self.assertEqual(
|
||||
"BossExtrude",
|
||||
nodes["feature:base_flange"]["solidworks_type"],
|
||||
)
|
||||
self.assertEqual(
|
||||
"BossExtrude",
|
||||
nodes["feature:base_flange"]["definition"]["feature_type"],
|
||||
)
|
||||
self.assertTrue(nodes["feature:base_flange"]["dimensions"])
|
||||
self.assertIn(
|
||||
"feature:central_bore",
|
||||
nodes["feature:base_flange"]["references"]["child_features"],
|
||||
)
|
||||
self.assertEqual(
|
||||
"CutExtrude",
|
||||
nodes["feature:central_bore"]["solidworks_type"],
|
||||
)
|
||||
self.assertEqual(
|
||||
"ThroughAll",
|
||||
nodes["feature:central_bore"]["definition"]["end_condition"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[],
|
||||
nodes["feature:central_bore"]["selection_sets"]["selected_edges"],
|
||||
)
|
||||
self.assertFalse(nodes["feature:central_bore"]["rebuild"]["suppressed"])
|
||||
self.assertIn(
|
||||
"凸台-拉伸",
|
||||
nodes["feature:base_flange"]["display_name"],
|
||||
)
|
||||
self.assertEqual(
|
||||
["bore_diameter"],
|
||||
[
|
||||
parameter["binding_id"]
|
||||
for parameter in nodes["feature:central_bore"]["parameters"]
|
||||
],
|
||||
)
|
||||
replay_designir = FEATURE_TREE.designir_from_feature_tree(tree)
|
||||
self.assertEqual("3.0", replay_designir["schema_version"])
|
||||
self.assertEqual(
|
||||
"feature_manager_tree_replay",
|
||||
replay_designir["authoring_mode"],
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
original_step = Path(temporary) / "original.step"
|
||||
replay_step = Path(temporary) / "replayed.step"
|
||||
original = MODULE.compile_designir(migrated, original_step)
|
||||
result = FEATURE_TREE.compile_feature_tree(tree, replay_step)
|
||||
self.assertTrue(replay_step.is_file())
|
||||
self.assertEqual("feature_manager_native", result["replay_kind"])
|
||||
for key in ("solid_count", "face_count", "edge_count"):
|
||||
self.assertEqual(original["facts"][key], result["facts"][key])
|
||||
self.assertEqual(original["facts"]["size_mm"], result["facts"]["size_mm"])
|
||||
self.assertAlmostEqual(
|
||||
original["facts"]["volume_mm3"],
|
||||
result["facts"]["volume_mm3"],
|
||||
places=7,
|
||||
)
|
||||
|
||||
def test_llm_flange_variant_normalizes_and_compiles(self) -> None:
|
||||
payload = {
|
||||
"designir_kind": "independent_parametric_cad",
|
||||
"reconstruction_mode": "fully_semantic_parametric",
|
||||
"authoring_mode": "semantic_feature_program",
|
||||
"semantic_layer": {
|
||||
"reconstruction_status": "ready",
|
||||
"coordinate_system": {
|
||||
"origin": [0, 0, 0],
|
||||
"x_axis": [1, 0, 0],
|
||||
"y_axis": [0, 1, 0],
|
||||
"z_axis": [0, 0, 1],
|
||||
},
|
||||
"datums": [
|
||||
{
|
||||
"id": "datum_xy",
|
||||
"plane": {"normal": [0, 0, 1], "offset": 0},
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "flange_outer_diameter",
|
||||
"value": 120,
|
||||
"unit": "mm",
|
||||
"editable": True,
|
||||
},
|
||||
{
|
||||
"name": "flange_thickness",
|
||||
"value": 16,
|
||||
"unit": "mm",
|
||||
"editable": True,
|
||||
},
|
||||
{
|
||||
"name": "bore_diameter",
|
||||
"value": 40,
|
||||
"unit": "mm",
|
||||
"editable": True,
|
||||
},
|
||||
{
|
||||
"name": "bolt_circle_diameter",
|
||||
"value": 90,
|
||||
"unit": "mm",
|
||||
"editable": True,
|
||||
},
|
||||
{
|
||||
"name": "bolt_hole_diameter",
|
||||
"value": 11,
|
||||
"unit": "mm",
|
||||
"editable": True,
|
||||
},
|
||||
{
|
||||
"name": "bolt_count",
|
||||
"value": 4,
|
||||
"unit": "count",
|
||||
"editable": True,
|
||||
},
|
||||
],
|
||||
"expressions": [
|
||||
{
|
||||
"name": "flange_outer_radius",
|
||||
"expression": "flange_outer_diameter / 2",
|
||||
}
|
||||
],
|
||||
"sketches": [],
|
||||
"constraints": [],
|
||||
"features": [
|
||||
{
|
||||
"id": "feat_flange_body",
|
||||
"operation": "extrude_circle",
|
||||
"radius": {
|
||||
"expression": "flange_outer_diameter / 2"
|
||||
},
|
||||
"height": {"parameter": "flange_thickness"},
|
||||
},
|
||||
{
|
||||
"id": "feat_central_bore",
|
||||
"operation": "through_hole",
|
||||
"diameter": {"parameter": "bore_diameter"},
|
||||
"reference": {"feature": "feat_flange_body"},
|
||||
},
|
||||
{
|
||||
"id": "feat_bolt_holes",
|
||||
"operation": "polar_hole_pattern",
|
||||
"count": {"parameter": "bolt_count"},
|
||||
"pitch_diameter": {
|
||||
"parameter": "bolt_circle_diameter"
|
||||
},
|
||||
"hole_diameter": {
|
||||
"parameter": "bolt_hole_diameter"
|
||||
},
|
||||
"reference": {"feature": "feat_flange_body"},
|
||||
},
|
||||
],
|
||||
"patterns": [],
|
||||
"attachments": [],
|
||||
"construction_stages": [],
|
||||
},
|
||||
"edit_interface": {
|
||||
"semantic_parameters": [
|
||||
{"name": "flange_outer_diameter", "label": "法兰外径"},
|
||||
{"name": "flange_thickness", "label": "法兰厚度"},
|
||||
{"name": "bore_diameter", "label": "中心孔径"},
|
||||
{"name": "bolt_circle_diameter", "label": "分布圆直径"},
|
||||
{"name": "bolt_hole_diameter", "label": "螺栓孔径"},
|
||||
{"name": "bolt_count", "label": "螺栓孔数量"},
|
||||
],
|
||||
"surface_parameter_groups": [],
|
||||
"modification_levels": ["semantic_feature"],
|
||||
"preserved_interfaces": [],
|
||||
},
|
||||
"validation_contract": {
|
||||
"source_independence": True,
|
||||
"geometry_checks": [],
|
||||
"edit_checks": [],
|
||||
"invariants": [],
|
||||
"perturbations": [
|
||||
{"parameter": "flange_outer_diameter", "delta": 10}
|
||||
],
|
||||
"thresholds": {},
|
||||
},
|
||||
"backend_hint": "build123d",
|
||||
}
|
||||
normalized = MODULE._normalize_tool_authored_designir(payload)
|
||||
self.assertEqual("3.0", normalized["schema_version"])
|
||||
self.assertEqual("mm", normalized["units"])
|
||||
self.assertIsInstance(
|
||||
normalized["semantic_layer"]["parameters"], dict
|
||||
)
|
||||
self.assertEqual(
|
||||
["flange_outer_diameter", "flange_thickness", "bore_diameter",
|
||||
"bolt_circle_diameter", "bolt_hole_diameter", "bolt_count"],
|
||||
normalized["edit_interface"]["semantic_parameters"],
|
||||
)
|
||||
compiled = MODULE.validate_designir(payload)
|
||||
self.assertEqual(
|
||||
{"parameter": "bolt_hole_diameter"},
|
||||
compiled["features"][2]["diameter"],
|
||||
)
|
||||
self.assertEqual("feat_flange_body", compiled["features"][1]["host"])
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
output = Path(temporary) / "flange.step"
|
||||
result = MODULE.compile_designir(payload, output)
|
||||
self.assertEqual(1, result["facts"]["solid_count"])
|
||||
self.assertEqual([120.0, 120.0, 16.0], result["facts"]["size_mm"])
|
||||
tree = FEATURE_TREE.generate_feature_tree(
|
||||
normalized,
|
||||
designir_path="flange.designir.json",
|
||||
step_path="flange.step",
|
||||
backend="build123d",
|
||||
compiled=True,
|
||||
validated=True,
|
||||
)
|
||||
serialized = json.dumps(tree)
|
||||
self.assertNotIn('"designir_3"', serialized)
|
||||
self.assertNotIn('"source_designir_feature"', serialized)
|
||||
replay_step = Path(temporary) / "flange-replayed.step"
|
||||
replay = FEATURE_TREE.compile_feature_tree(tree, replay_step)
|
||||
self.assertEqual("feature_manager_native", replay["replay_kind"])
|
||||
self.assertEqual(result["facts"]["size_mm"], replay["facts"]["size_mm"])
|
||||
|
||||
def test_teacher_dependency_is_rejected(self) -> None:
|
||||
payload = self.payload()
|
||||
payload["source_step"] = "teacher.step"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts" / "normalize_backend_result.py"
|
||||
SPEC = importlib.util.spec_from_file_location("normalize_backend_result", SCRIPT)
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = MODULE
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
class NormalizeBackendResultTests(unittest.TestCase):
|
||||
def test_unsupported_surfaceir_snapshot_does_not_block_native_publish(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
step_path = directory / "model.step"
|
||||
metadata_path = directory / "backend-metadata.json"
|
||||
output_path = directory / "model.designir.json"
|
||||
step_path.write_bytes(b"ISO-10303-21;\nEND-ISO-10303-21;\n")
|
||||
metadata_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"parameters": [
|
||||
{
|
||||
"name": "module",
|
||||
"value": 2.5,
|
||||
"unit": "mm",
|
||||
"editable": True,
|
||||
"binding_kind": "native_python",
|
||||
"parameter_path": "PARAMETERS.module",
|
||||
"regenerate_adapter": "simplecadapi",
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with patch.object(
|
||||
MODULE.surfaceir_pipeline,
|
||||
"extract_surfaceir",
|
||||
side_effect=ValueError("Unsupported surface type: 8"),
|
||||
):
|
||||
result = MODULE.normalize_backend_result(
|
||||
request="standard spur gear",
|
||||
backend="simplecadapi",
|
||||
source_of_truth="model_graph",
|
||||
step_path=step_path,
|
||||
output_path=output_path,
|
||||
model_id="spur_gear",
|
||||
family="simplecadapi_native_model",
|
||||
native_source_path="model.simplecadapi.py",
|
||||
backend_graph_path="model.simplecad.model.json",
|
||||
metadata_path=metadata_path,
|
||||
validation_paths=["backend-validation.json"],
|
||||
)
|
||||
|
||||
payload = json.loads(output_path.read_text(encoding="utf-8"))
|
||||
self.assertTrue(result["valid"])
|
||||
self.assertEqual("fully_semantic_parametric", result["reconstruction_mode"])
|
||||
self.assertEqual("unavailable", result["surface_snapshot"]["status"])
|
||||
self.assertNotIn("surface_layer", payload)
|
||||
self.assertEqual("partial", payload["semantic_layer"]["reconstruction_status"])
|
||||
snapshot_stage = payload["semantic_layer"]["construction_stages"][1]
|
||||
self.assertEqual("unavailable", snapshot_stage["status"])
|
||||
self.assertEqual("Unsupported surface type: 8", snapshot_stage["error"])
|
||||
|
||||
@@ -26,6 +26,9 @@ SURFACEIR = load_module(
|
||||
DESIGNIR = load_module(
|
||||
"designir_pipeline_surface_test", ROOT / "scripts" / "designir_pipeline.py"
|
||||
)
|
||||
FEATURE_TREE = load_module(
|
||||
"feature_tree_surface_test", ROOT / "scripts" / "feature_tree.py"
|
||||
)
|
||||
|
||||
|
||||
class SurfaceIRPipelineTests(unittest.TestCase):
|
||||
@@ -128,6 +131,59 @@ class SurfaceIRPipelineTests(unittest.TestCase):
|
||||
DESIGNIR._symmetric_difference_volume(expected, actual), 1e-8
|
||||
)
|
||||
|
||||
def test_feature_tree_for_uploaded_step_is_inferred_summary(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
teacher = directory / "teacher.step"
|
||||
DESIGNIR.export_step(DESIGNIR.Box(10, 20, 30), teacher)
|
||||
payload = SURFACEIR.extract_surfaceir(teacher)
|
||||
tree = FEATURE_TREE.generate_feature_tree(
|
||||
payload,
|
||||
designir_path="box.designir.json",
|
||||
step_path="box.reconstructed.step",
|
||||
parameters_path="parameters.json",
|
||||
backend="surfaceir_occt",
|
||||
compiled=True,
|
||||
validated=True,
|
||||
)
|
||||
self.assertEqual(
|
||||
"inferred_from_step",
|
||||
tree["source"]["history_kind"],
|
||||
)
|
||||
self.assertEqual("solidworks_feature_manager", tree["tree_kind"])
|
||||
self.assertFalse(
|
||||
tree["source"]["claims_original_solidworks_history"]
|
||||
)
|
||||
imported = next(
|
||||
node
|
||||
for node in tree["feature_manager"]["nodes"]
|
||||
if node["solidworks_type"] == "ImportedFeature"
|
||||
)
|
||||
self.assertEqual("导入1", imported["display_name"])
|
||||
self.assertEqual(
|
||||
"ImportedFeature",
|
||||
imported["definition"]["feature_type"],
|
||||
)
|
||||
self.assertFalse(imported["definition"]["native_history_recovered"])
|
||||
self.assertNotIn("manufacturing", imported)
|
||||
self.assertEqual(1, imported["result"]["solid_count"])
|
||||
self.assertEqual(
|
||||
"gzip+base64+json",
|
||||
imported["operation_spec"]["payload_encoding"],
|
||||
)
|
||||
self.assertEqual(
|
||||
"fallback_imported_feature",
|
||||
tree["validation"]["feature_recognition"]["status"],
|
||||
)
|
||||
replay_designir = FEATURE_TREE.designir_from_feature_tree(tree)
|
||||
self.assertEqual(payload["surface_layer"], replay_designir["surface_layer"])
|
||||
replayed = directory / "replayed_from_tree.step"
|
||||
result = FEATURE_TREE.compile_feature_tree(tree, replayed)
|
||||
self.assertTrue(replayed.is_file())
|
||||
self.assertEqual("surfaceir_imported_feature", result["replay_kind"])
|
||||
serialized = json.dumps(tree)
|
||||
self.assertNotIn('"surface_layer": {"vertices"', serialized)
|
||||
|
||||
def test_empty_step_document_is_represented_honestly(self) -> None:
|
||||
payload = {
|
||||
"schema_version": "3.0",
|
||||
|
||||
Reference in New Issue
Block a user