180 lines
12 KiB
Python
180 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
from copy import deepcopy
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "json_to_cdsl"))
|
|
|
|
from evidence_v2_to_cdsl import StepInspector, batch_convert, convert_evidence
|
|
|
|
|
|
def _identity(stable_id: str, *, kind: str = "feature", geometry: dict | None = None) -> dict:
|
|
result = {"kind": kind, "stable_id": stable_id}
|
|
if geometry is not None:
|
|
result["geometry"] = geometry
|
|
return result
|
|
|
|
|
|
def _sketch(stable_id: str, segments: list[dict]) -> dict:
|
|
return {
|
|
"sequence": 10, "name": stable_id, "effective_type": "ProfileFeature", "stable_id": stable_id,
|
|
"sketch": {"model_to_sketch_transform": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], "segments": segments},
|
|
}
|
|
|
|
|
|
def _line(start: list[float], end: list[float], construction: bool = False) -> dict:
|
|
return {"geometry": {"segment_type": "swSketchLINE", "construction": construction, "start": start, "end": end, "curve": {"type": "line"}}}
|
|
|
|
|
|
def _arc(start: list[float], end: list[float], center: list[float], radius: float) -> dict:
|
|
return {"geometry": {"segment_type": "swSketchARC", "construction": False, "start": start, "end": end, "center": center, "direction": 1, "curve": {"type": "circle", "parameters": [*center, 0, 0, 1, radius]}}}
|
|
|
|
|
|
def _history(props: dict, methods: dict, parents: list[dict] | None = None) -> dict:
|
|
return {"definition_properties": {"values": props}, "definition_methods": {"values": methods}, "parents": parents or [], "selections": []}
|
|
|
|
|
|
FIXTURE = {
|
|
"schema": "solidworks.cad_evidence.v2", "status": "complete_with_blockers",
|
|
"self_validation": {"feature_contracts": [{"feature": "Hole", "blockers": ["Hole: hole has no captured semantic selections"]}]},
|
|
"features": [
|
|
{"sequence": 1, "name": "Front", "effective_type": "RefPlane", "stable_id": "plane", "definition_properties": {"values": {}}, "definition_methods": {"values": {}}, "parents": [], "selections": []},
|
|
{"sequence": 2, "name": "Axis", "effective_type": "RefAxis", "stable_id": "axis", "definition_properties": {"values": {"Axis": _identity("axis-line", kind="axis", geometry={"start": [0, 0, 0], "end": [0, 0, 0.01]})}}, "definition_methods": {"values": {}}, "parents": [], "selections": []},
|
|
_sketch("sketch-base", [_line([0, 0, 0], [0.01, 0, 0]), _line([0.01, 0, 0], [0.01, 0.01, 0]), _line([0.01, 0.01, 0], [0, 0.01, 0]), _line([0, 0.01, 0], [0, 0, 0])]),
|
|
{"sequence": 12, "name": "Boss", "effective_type": "Boss", "stable_id": "boss", "history_definition": _history({"BothDirections": False, "ReverseDirection": False}, {"GetDepth(true)": 0.01, "GetDepth(false)": 0, "GetEndCondition(true)": 0}, [_identity("sketch-base")])},
|
|
_sketch("sketch-curves", [_arc([0.01, 0, 0], [0, 0.01, 0], [0, 0, 0], 0.01), _line([0, 0.01, 0], [0.01, 0, 0]), {"geometry": {"segment_type": "swSketchSPLINE", "construction": True, "curve": {"type": "other"}}, "spline": {"dimension": 3, "degree": 2, "control_points": [0, 0, 0, 0.005, 0.002, 0, 0.01, 0, 0], "knots": [0, 0, 0, 1, 1, 1], "periodic": 0}}]),
|
|
{"sequence": 14, "name": "Cut", "effective_type": "Cut", "stable_id": "cut", "history_definition": _history({"BothDirections": False, "ReverseDirection": False}, {"GetDepth(true)": 0.002, "GetDepth(false)": 0, "GetEndCondition(true)": 0}, [_identity("sketch-curves"), _identity("boss")])},
|
|
{"sequence": 15, "name": "Revolve", "effective_type": "Revolution", "stable_id": "revolve", "history_definition": _history({"Axis": _identity("revolve-line", kind="sketch_segment", geometry={"start": [0, 0, 0], "end": [0, 0.01, 0]}), "ReverseDirection": False}, {"GetRevolutionAngle(true)": 6.283185307, "GetEndCondition(true)": 0}, [_identity("sketch-curves"), _identity("cut")])},
|
|
{"sequence": 16, "name": "Hole", "effective_type": "HoleWzd", "stable_id": "hole", "history_definition": _history({"ThreadDiameter": 0.004, "ThreadDepth": 0.005, "EndCondition": 0, "FastenerType": "threaded"}, {"GetSketchPoints": [{"geometry": {"point": [0.005, 0.005, 0]}}]}, [_identity("revolve")])},
|
|
{"sequence": 17, "name": "Fillet", "effective_type": "Fillet", "stable_id": "fillet", "history_definition": _history({"Radius": 0.001}, {}, [_identity("hole")])},
|
|
{"sequence": 18, "name": "Chamfer", "effective_type": "Chamfer", "stable_id": "chamfer", "history_definition": _history({}, {}, [_identity("fillet")]), "dimensions": [{"system_value": 0.001}, {"system_value": 0.785398}]},
|
|
{"sequence": 19, "name": "Pattern", "effective_type": "LPattern", "stable_id": "pattern", "history_definition": _history({"D1Spacing": 0.01, "D1TotalInstances": 2, "D1Axis": {"vector": [1, 0, 0]}}, {}, [_identity("chamfer")])},
|
|
{"sequence": 20, "name": "Mirror", "effective_type": "MirrorPattern", "stable_id": "mirror", "history_definition": _history({}, {}, [_identity("pattern")])},
|
|
],
|
|
}
|
|
|
|
|
|
class EvidenceV2ToCdslTests(unittest.TestCase):
|
|
def test_batch_conversion_recurses_and_records_relative_source_path(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory)
|
|
evidence_dir = root / "evidence"
|
|
source = evidence_dir / "category" / "part.solidworks_evidence_v2.json"
|
|
source.parent.mkdir(parents=True)
|
|
source.write_text(json.dumps(FIXTURE), encoding="utf-8")
|
|
|
|
output_dir = root / "output"
|
|
manifest = batch_convert(evidence_dir, output_dir)
|
|
|
|
self.assertEqual(manifest["input_count"], 1)
|
|
self.assertEqual(manifest["converted_count"], 1)
|
|
self.assertEqual(manifest["results"][0]["source"], "category/part.solidworks_evidence_v2.json")
|
|
diagnostic = json.loads((output_dir / "part.diagnostic.json").read_text(encoding="utf-8"))
|
|
self.assertEqual(diagnostic["source"], "category/part.solidworks_evidence_v2.json")
|
|
|
|
def test_batch_conversion_uses_full_filename_and_disambiguates_normalized_ids(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory)
|
|
evidence_dir = root / "evidence"
|
|
sources = [
|
|
evidence_dir / "first" / "14.Rod Nut.solidworks_evidence_v2.json",
|
|
evidence_dir / "second" / "14 Rod Nut.solidworks_evidence_v2.json",
|
|
]
|
|
for source in sources:
|
|
source.parent.mkdir(parents=True, exist_ok=True)
|
|
source.write_text(json.dumps(FIXTURE), encoding="utf-8")
|
|
|
|
manifest = batch_convert(evidence_dir, root / "output")
|
|
|
|
self.assertEqual(manifest["converted_count"], 2)
|
|
part_ids = {item["part_id"] for item in manifest["results"]}
|
|
self.assertEqual(len(part_ids), 2)
|
|
self.assertTrue(all(part_id.startswith("14-Rod-Nut-") for part_id in part_ids))
|
|
|
|
def test_converts_fixed_evidence_v2_fixture_to_semantic_cdsl(self) -> None:
|
|
cdsl, diagnostic = convert_evidence(FIXTURE, source_name="fixture.solidworks_evidence_v2.json")
|
|
atoms = {feature["atomic_id"] for feature in cdsl["features"]}
|
|
self.assertTrue({"reference_plane", "reference_axis", "extrude_add_blind", "extrude_cut_blind", "revolve_add", "hole_wizard", "fillet", "chamfer", "pattern_linear", "pattern_mirror"}.issubset(atoms))
|
|
profiles = [sketch["profile"] for sketch in cdsl["geometry"]["sketches"]]
|
|
self.assertTrue(any(profile["type"] == "analytic_contours" for profile in profiles))
|
|
self.assertTrue(any(segment["type"] == "bspline" for profile in profiles if profile["type"] == "analytic_contours" for segment in profile.get("construction", [])))
|
|
self.assertEqual(cdsl["schema_version"], "1.1.0")
|
|
self.assertTrue(diagnostic["semantic_validation"]["unresolved"])
|
|
|
|
def test_name_only_sketch_parent_and_through_all_are_not_unresolved(self) -> None:
|
|
fixture = deepcopy(FIXTURE)
|
|
boss = next(item for item in fixture["features"] if item.get("stable_id") == "boss")
|
|
boss["history_definition"]["parents"] = [{"kind": "feature_reference", "name": "sketch-base"}]
|
|
cut = next(item for item in fixture["features"] if item.get("stable_id") == "cut")
|
|
cut["history_definition"]["definition_methods"]["values"].update({
|
|
"GetDepth(true)": 0.0,
|
|
"GetEndCondition(true)": 1,
|
|
})
|
|
|
|
cdsl, _ = convert_evidence(fixture, source_name="name-parent.solidworks_evidence_v2.json")
|
|
boss_feature = next(item for item in cdsl["features"] if item["name"] == "Boss")
|
|
cut_feature = next(item for item in cdsl["features"] if item["name"] == "Cut")
|
|
self.assertEqual(boss_feature["sketch_id"], "sk_001")
|
|
self.assertEqual(cut_feature["params"]["end_condition"]["type"], "through_all")
|
|
self.assertNotIn("unresolved", cut_feature)
|
|
|
|
def test_reference_axis_is_derived_from_two_named_planes(self) -> None:
|
|
fixture = deepcopy(FIXTURE)
|
|
fixture["features"].extend([
|
|
{"sequence": 21, "name": "ip_1 XY", "effective_type": "RefPlane", "stable_id": "xy", "definition_properties": {"values": {}}, "definition_methods": {"values": {}}, "parents": [], "selections": []},
|
|
{"sequence": 22, "name": "ip_1 XZ", "effective_type": "RefPlane", "stable_id": "xz", "definition_properties": {"values": {}}, "definition_methods": {"values": {}}, "parents": [], "selections": []},
|
|
{"sequence": 23, "name": "ip_1 X", "effective_type": "RefAxis", "stable_id": "derived-axis", "history_definition": _history({"Type": 1}, {}, [{"kind": "feature", "stable_id": "xy", "name": "ip_1 XY"}, {"kind": "feature", "stable_id": "xz", "name": "ip_1 XZ"}])},
|
|
])
|
|
|
|
cdsl, _ = convert_evidence(fixture, source_name="derived-axis.solidworks_evidence_v2.json")
|
|
axis = next(item for item in cdsl["features"] if item["name"] == "ip_1 X")
|
|
self.assertEqual(axis["params"]["axis"]["direction"], [-1.0, 0.0, 0.0])
|
|
self.assertNotIn("unresolved", axis)
|
|
|
|
def test_mirror_pattern_uses_definition_property_references(self) -> None:
|
|
fixture = deepcopy(FIXTURE)
|
|
mirror = next(item for item in fixture["features"] if item.get("stable_id") == "mirror")
|
|
mirror["history_definition"]["definition_properties"]["values"] = {
|
|
"PatternFeatureArray": [_identity("pattern")],
|
|
"Plane": _identity("plane"),
|
|
}
|
|
|
|
cdsl, _ = convert_evidence(fixture, source_name="mirror-props.solidworks_evidence_v2.json")
|
|
pattern = next(item for item in cdsl["features"] if item["atomic_id"] == "pattern_mirror")
|
|
source = next(item for item in cdsl["features"] if item["atomic_id"] == "pattern_linear")
|
|
self.assertEqual(pattern["params"]["source_feature_ids"], [source["id"]])
|
|
self.assertEqual(pattern["params"]["mirror_plane"]["kind"], "plane")
|
|
self.assertNotIn("mirror pattern source features or plane were not captured", pattern.get("unresolved", []))
|
|
|
|
def test_step_truth_comparison_reports_numeric_and_topology_metrics(self) -> None:
|
|
inspector = StepInspector(None)
|
|
inspector.metrics = {
|
|
"bounding_box_mm": [0, 0, 0, 10, 10, 10],
|
|
"volume_mm3": 1000,
|
|
"surface_area_mm2": 600,
|
|
"solid_count": 1,
|
|
"face_count": 6,
|
|
"edge_count": 12,
|
|
"vertex_count": 8,
|
|
}
|
|
comparison = inspector.compare_truth({
|
|
"mass_properties": {"volume": 1e-6, "surface_area": 6e-4},
|
|
"geometry": {
|
|
"bounding_box": [0, 0, 0, 0.01, 0.01, 0.01],
|
|
"solid_body_count": 1,
|
|
"bodies": [{"geometry": {"face_count": 6, "edge_count": 12, "vertex_count": 8}}],
|
|
},
|
|
})
|
|
self.assertTrue(comparison["numeric_geometry_match"])
|
|
self.assertTrue(comparison["topology_counts_match"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|