315 lines
13 KiB
Python
315 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from build123d import Align, Box, Plane, Rectangle, Sphere, export_step
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def load_module(name: str, path: Path):
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
assert spec.loader
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
SURFACEIR = load_module(
|
|
"surfaceir_pipeline", ROOT / "scripts" / "surfaceir_pipeline.py"
|
|
)
|
|
DESIGNIR = load_module(
|
|
"designir_pipeline_surface_test", ROOT / "scripts" / "designir_pipeline.py"
|
|
)
|
|
|
|
|
|
class SurfaceIRPipelineTests(unittest.TestCase):
|
|
def test_closed_single_face_sphere_roundtrip(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
directory = Path(temporary)
|
|
teacher = directory / "sphere.step"
|
|
rebuilt_path = directory / "sphere-rebuilt.step"
|
|
expected = Sphere(1)
|
|
export_step(expected, teacher)
|
|
|
|
payload = SURFACEIR.extract_surfaceir(teacher)
|
|
shell = payload["surface_layer"]["solids"][0]["shells"][0]
|
|
self.assertEqual(1, len(shell["faces"]))
|
|
|
|
rebuilt = SURFACEIR.build_surfaceir(
|
|
payload, "exact_3d_pcurve"
|
|
)
|
|
SURFACEIR.export_step_shape(rebuilt, rebuilt_path)
|
|
actual = DESIGNIR.import_step(rebuilt_path)
|
|
self.assertTrue(actual.is_valid)
|
|
self.assertEqual(1, len(actual.solids()))
|
|
self.assertAlmostEqual(expected.volume, actual.volume, places=9)
|
|
|
|
def test_multi_shell_solid_preserves_periodic_inner_shell(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
directory = Path(temporary)
|
|
teacher = directory / "cavity.step"
|
|
rebuilt_path = directory / "cavity-rebuilt.step"
|
|
expected = Box(
|
|
10,
|
|
10,
|
|
10,
|
|
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
|
) - Sphere(1)
|
|
export_step(expected, teacher)
|
|
|
|
payload = SURFACEIR.extract_surfaceir(teacher)
|
|
shells = payload["surface_layer"]["solids"][0]["shells"]
|
|
self.assertEqual(2, len(shells))
|
|
self.assertTrue(any(len(shell["faces"]) == 1 for shell in shells))
|
|
|
|
rebuilt = SURFACEIR.build_surfaceir(
|
|
payload, "exact_3d_pcurve"
|
|
)
|
|
SURFACEIR.export_step_shape(rebuilt, rebuilt_path)
|
|
actual = DESIGNIR.import_step(rebuilt_path)
|
|
self.assertTrue(actual.is_valid)
|
|
self.assertEqual(1, len(actual.solids()))
|
|
self.assertEqual(2, len(actual.solids()[0].shells()))
|
|
self.assertLess(
|
|
abs(expected.volume - actual.volume) / expected.volume, 1e-10
|
|
)
|
|
|
|
def test_independent_json_roundtrip_preserves_exact_geometry(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
directory = Path(temporary)
|
|
teacher = directory / "teacher.step"
|
|
portable_json = directory / "model.designir.json"
|
|
rebuilt = directory / "rebuilt.step"
|
|
expected = DESIGNIR.Box(10, 20, 30)
|
|
DESIGNIR.export_step(expected, teacher)
|
|
surfaceir = SURFACEIR.extract_surfaceir(teacher)
|
|
SURFACEIR.write_json(portable_json, surfaceir)
|
|
teacher.unlink()
|
|
|
|
serialized = portable_json.read_text(encoding="utf-8").lower()
|
|
stored = json.loads(serialized)
|
|
self.assertEqual(
|
|
"gzip+base64+json",
|
|
stored["surface_layer"]["encoding"],
|
|
)
|
|
self.assertEqual(
|
|
"surfaceir-3.0",
|
|
stored["surface_layer"]["format"],
|
|
)
|
|
self.assertNotIn("teacher.step", serialized)
|
|
self.assertNotIn("source_path", serialized)
|
|
self.assertNotIn("import_step", serialized)
|
|
self.assertNotIn('"mesh"', serialized)
|
|
|
|
decoded = SURFACEIR.read_designir(portable_json)
|
|
self.assertEqual(surfaceir["surface_layer"], decoded["surface_layer"])
|
|
self.assertIn(
|
|
"overall_scale",
|
|
decoded["edit_interface"]["semantic_parameters"],
|
|
)
|
|
rebuilt_shape = SURFACEIR.build_surfaceir(decoded)
|
|
SURFACEIR.export_step_shape(rebuilt_shape, rebuilt)
|
|
self.assertTrue(rebuilt.is_file())
|
|
|
|
actual = DESIGNIR.import_step(rebuilt)
|
|
self.assertEqual(len(expected.solids()), len(actual.solids()))
|
|
self.assertEqual(len(expected.faces()), len(actual.faces()))
|
|
self.assertEqual(len(expected.edges()), len(actual.edges()))
|
|
self.assertLess(
|
|
abs(expected.volume - actual.volume) / expected.volume, 1e-10
|
|
)
|
|
self.assertLess(
|
|
DESIGNIR._symmetric_difference_volume(expected, actual), 1e-8
|
|
)
|
|
|
|
def test_empty_step_document_is_represented_honestly(self) -> None:
|
|
payload = {
|
|
"schema_version": "3.0",
|
|
"designir_kind": "independent_parametric_cad",
|
|
"document_status": "empty_geometry",
|
|
"surface_layer": {
|
|
"vertices": [],
|
|
"solids": [],
|
|
"free_shells": [],
|
|
},
|
|
}
|
|
rebuilt = SURFACEIR.build_surfaceir(payload)
|
|
self.assertEqual(SURFACEIR._facts_from_shape(rebuilt)["face_count"], 0)
|
|
|
|
def test_open_shell_roundtrip_preserves_surface_geometry(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
directory = Path(temporary)
|
|
teacher = directory / "surface.step"
|
|
rebuilt_path = directory / "surface-rebuilt.step"
|
|
face = (Plane.XY * Rectangle(10, 20)).face()
|
|
builder = SURFACEIR.BRep_Builder()
|
|
shell = SURFACEIR.TopoDS_Shell()
|
|
builder.MakeShell(shell)
|
|
builder.Add(shell, face.wrapped)
|
|
SURFACEIR.export_step_shape(shell, teacher)
|
|
|
|
payload = SURFACEIR.extract_surfaceir(teacher)
|
|
self.assertEqual(payload["document_status"], "geometry_present")
|
|
self.assertEqual(len(payload["surface_layer"]["solids"]), 0)
|
|
self.assertEqual(len(payload["surface_layer"]["free_shells"]), 1)
|
|
rebuilt = SURFACEIR.build_surfaceir(
|
|
payload, "exact_3d_pcurve"
|
|
)
|
|
SURFACEIR.export_step_shape(rebuilt, rebuilt_path)
|
|
_, teacher_facts = SURFACEIR._shape_facts(teacher)
|
|
_, rebuilt_facts = SURFACEIR._shape_facts(rebuilt_path)
|
|
self.assertEqual(teacher_facts["face_count"], 1)
|
|
self.assertEqual(
|
|
teacher_facts["face_count"], rebuilt_facts["face_count"]
|
|
)
|
|
self.assertAlmostEqual(
|
|
teacher_facts["surface_area"],
|
|
rebuilt_facts["surface_area"],
|
|
places=8,
|
|
)
|
|
|
|
def test_overall_scale_is_executable_and_independently_validated(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
directory = Path(temporary)
|
|
teacher = directory / "teacher.step"
|
|
designir_path = directory / "box.designir.json"
|
|
expected = DESIGNIR.Box(10, 20, 30)
|
|
DESIGNIR.export_step(expected, teacher)
|
|
payload = SURFACEIR.extract_surfaceir(teacher)
|
|
self.assertIn(
|
|
"overall_scale",
|
|
payload["edit_interface"]["semantic_parameters"],
|
|
)
|
|
|
|
edited = SURFACEIR.apply_semantic_parameter(
|
|
payload, "overall_scale", 1.1
|
|
)
|
|
self.assertEqual(
|
|
edited["edit_operations"],
|
|
[
|
|
{
|
|
"operation": "uniform_scale",
|
|
"factor": 1.1,
|
|
"pivot": [0.0, 0.0, 0.0],
|
|
}
|
|
],
|
|
)
|
|
self.assertIsNotNone(SURFACEIR.build_surfaceir(edited))
|
|
|
|
SURFACEIR.write_json(designir_path, payload)
|
|
acceptance = SURFACEIR.validate_semantic_edit(
|
|
teacher,
|
|
designir_path,
|
|
"overall_scale",
|
|
1.1,
|
|
directory / "acceptance",
|
|
)
|
|
self.assertTrue(acceptance["accepted"])
|
|
self.assertTrue(
|
|
acceptance["checks"]["target_parameter"]["pass"]
|
|
)
|
|
|
|
def test_primary_axis_dimensions_are_executable_and_validated(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
directory = Path(temporary)
|
|
teacher = directory / "shaft.step"
|
|
designir_path = directory / "shaft.designir.json"
|
|
DESIGNIR.export_step(DESIGNIR.Cylinder(10, 40), teacher)
|
|
payload = SURFACEIR.extract_surfaceir(teacher)
|
|
SURFACEIR.write_json(designir_path, payload)
|
|
|
|
for parameter, value in (
|
|
("outer_diameter", 22.0),
|
|
("body_length", 44.0),
|
|
):
|
|
acceptance = SURFACEIR.validate_semantic_edit(
|
|
teacher,
|
|
designir_path,
|
|
parameter,
|
|
value,
|
|
directory / parameter,
|
|
)
|
|
self.assertTrue(acceptance["accepted"])
|
|
self.assertTrue(
|
|
acceptance["checks"]["target_parameter"]["pass"]
|
|
)
|
|
|
|
def test_canonical_semantics_name_bore_and_polar_hole_pattern(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
teacher = Path(temporary) / "flange.step"
|
|
expected = DESIGNIR.Cylinder(20, 5) - DESIGNIR.Cylinder(5, 5)
|
|
for x, y in ((10, 0), (0, 10), (-10, 0), (0, -10)):
|
|
expected -= DESIGNIR.Pos(x, y, 0) * DESIGNIR.Cylinder(1, 5)
|
|
DESIGNIR.export_step(expected, teacher)
|
|
|
|
payload = SURFACEIR.extract_surfaceir(teacher)
|
|
semantic = payload["semantic_layer"]
|
|
parameters = semantic["parameters"]
|
|
|
|
self.assertEqual(parameters["hole_count"]["value"], 4)
|
|
self.assertAlmostEqual(parameters["hole_diameter"]["value"], 2)
|
|
self.assertAlmostEqual(
|
|
parameters["bolt_circle_diameter"]["value"], 20
|
|
)
|
|
self.assertAlmostEqual(parameters["bore_diameter"]["value"], 10)
|
|
self.assertAlmostEqual(parameters["outer_diameter"]["value"], 40)
|
|
self.assertAlmostEqual(parameters["plate_thickness"]["value"], 5)
|
|
self.assertTrue(
|
|
any(
|
|
feature["operation"] == "polar_hole_pattern"
|
|
for feature in semantic["features"]
|
|
)
|
|
)
|
|
self.assertFalse(
|
|
semantic["inference_policy"]["claims_original_feature_history"]
|
|
)
|
|
|
|
edited = SURFACEIR.apply_semantic_parameter(
|
|
payload, "hole_diameter", 3
|
|
)
|
|
self.assertEqual(len(edited["edit_operations"]), 4)
|
|
self.assertEqual(
|
|
edited["active_edit"]["acceptance_status"], "pending"
|
|
)
|
|
with self.assertRaisesRegex(ValueError, "increases only"):
|
|
SURFACEIR.apply_semantic_parameter(
|
|
payload, "hole_diameter", 1.5
|
|
)
|
|
edited_step = Path(temporary) / "flange-edited.step"
|
|
edited_shape = SURFACEIR.build_surfaceir(edited)
|
|
SURFACEIR.export_step_shape(edited_shape, edited_step)
|
|
edited_payload = SURFACEIR.extract_surfaceir(edited_step)
|
|
edited_parameters = edited_payload["semantic_layer"]["parameters"]
|
|
self.assertAlmostEqual(
|
|
edited_parameters["hole_diameter"]["value"], 3
|
|
)
|
|
self.assertAlmostEqual(
|
|
edited_parameters["bore_diameter"]["value"], 10
|
|
)
|
|
self.assertEqual(edited_parameters["hole_count"]["value"], 4)
|
|
actual = DESIGNIR.import_step(edited_step)
|
|
self.assertTrue(actual.is_valid)
|
|
self.assertLess(actual.volume, expected.volume)
|
|
|
|
original_ir = Path(temporary) / "flange.designir.json"
|
|
SURFACEIR.write_json(original_ir, payload)
|
|
acceptance = SURFACEIR.validate_semantic_edit(
|
|
teacher,
|
|
original_ir,
|
|
"hole_diameter",
|
|
2.5,
|
|
Path(temporary) / "acceptance",
|
|
)
|
|
self.assertTrue(acceptance["accepted"])
|
|
self.assertEqual(acceptance["state"], "accepted")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|