"""Inc 3 + 4 gate: file I/O and the text tail. Round-trips rather than digests: STEP and IGES payloads carry timestamps and a generator banner, so byte comparison is meaningless — what has to survive is the geometry, which these check by measuring the shape that comes back. This file also settles open question **S5** from docs/design.md: whether the wheel has to ship OCCT's share/ resources for the XSTEP layer to resolve units when no CSF_* variable is set. These tests run in exactly that situation — a container with the wheel and nothing else — so a pass means the answer is no. """ from __future__ import annotations import os import tempfile import pytest from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox from OCP.IFSelect import IFSelect_RetDone from OCP.IGESControl import IGESControl_Controller, IGESControl_Reader, IGESControl_Writer from OCP.Interface import Interface_Static from OCP.STEPControl import ( STEPControl_AsIs, STEPControl_Controller, STEPControl_Reader, STEPControl_Writer, ) from OCP.TColStd import TColStd_SequenceOfAsciiString from .test_inc1_modeling import assert_close, measure BOX = (10.0, 20.0, 30.0) @pytest.fixture(scope="module") def initialized(): """The app's own import bootstrap, and the S5 check in one. If the controllers could not initialise without OCCT's resource files, this is where it would fail — there is no CSF_* in this environment. """ assert not any(k.startswith("CSF_") for k in os.environ), ( "a CSF_* variable is set, so this run cannot answer S5" ) assert STEPControl_Controller.Init_s() assert IGESControl_Controller.Init_s() assert Interface_Static.SetCVal_s("xstep.cascade.unit", "MM") return True def _box(): return BRepPrimAPI_MakeBox(*BOX).Shape() def _same_geometry(got, expected_volume: float, what: str) -> None: m = measure(got) assert m["n_solids"] == 1, f"{what}: solid count" assert m["n_faces"] == 6, f"{what}: face count" assert_close(m["volume"], expected_volume, f"{what}: volume") def test_step_round_trip(initialized, tmp_path): """Write a box to STEP and read it back with the geometry intact.""" path = tmp_path / "roundtrip.step" writer = STEPControl_Writer() assert writer.Transfer(_box(), STEPControl_AsIs) == IFSelect_RetDone assert writer.Write(str(path)) == IFSelect_RetDone assert path.stat().st_size > 0 reader = STEPControl_Reader() assert reader.ReadFile(str(path)) == IFSelect_RetDone assert reader.NbRootsForTransfer() >= 1 assert reader.TransferRoots() >= 1 _same_geometry(reader.OneShape(), BOX[0] * BOX[1] * BOX[2], "STEP round trip") def test_step_read_stream_takes_a_python_file_like(initialized, tmp_path): """ReadStream is how the app imports an uploaded payload without touching the filesystem. It goes through the same slurp bridge BinTools uses.""" import io path = tmp_path / "stream.step" writer = STEPControl_Writer() writer.Transfer(_box(), STEPControl_AsIs) writer.Write(str(path)) reader = STEPControl_Reader() assert ( reader.ReadStream("stream.step", io.BytesIO(path.read_bytes())) == IFSelect_RetDone ) assert reader.TransferRoots() >= 1 _same_geometry(reader.OneShape(), BOX[0] * BOX[1] * BOX[2], "STEP stream") def test_step_reports_its_file_units(initialized, tmp_path): """FileUnits fills three sequences by reference — the out-parameter shape the app relies on to decide whether to rescale the transfer.""" path = tmp_path / "units.step" writer = STEPControl_Writer() writer.Transfer(_box(), STEPControl_AsIs) writer.Write(str(path)) reader = STEPControl_Reader() reader.ReadFile(str(path)) length, angle, solid_angle = ( TColStd_SequenceOfAsciiString(), TColStd_SequenceOfAsciiString(), TColStd_SequenceOfAsciiString(), ) reader.FileUnits(length, angle, solid_angle) assert length.Length() >= 1, "no length unit reported — S5 would be live" names = [length.Value(i).ToCString() for i in range(1, length.Length() + 1)] assert any("MM" in n.upper() or "MILLI" in n.upper() for n in names), names def test_interface_static_round_trips_a_setting(initialized): """The unit override the IGES import brackets its read with.""" previous = Interface_Static.CVal_s("xstep.cascade.unit") try: assert Interface_Static.SetCVal_s("xstep.cascade.unit", "INCH") assert Interface_Static.CVal_s("xstep.cascade.unit") == "INCH" finally: Interface_Static.SetCVal_s("xstep.cascade.unit", previous or "MM") assert Interface_Static.CVal_s("xstep.cascade.unit") == (previous or "MM") def test_iges_round_trip_and_unit_probe(initialized, tmp_path): """The IGES half, including the model→global-section→unit-name chain that forces IGESData to be registered.""" path = tmp_path / "roundtrip.igs" writer = IGESControl_Writer("MM", 0) assert writer.AddShape(_box()) writer.ComputeModel() assert writer.Write(str(path)) reader = IGESControl_Reader() assert reader.ReadFile(str(path)) == IFSelect_RetDone assert reader.NbRootsForTransfer() >= 1 section = reader.IGESModel().GlobalSection() unit_name = section.UnitName() assert unit_name is not None assert "MM" in unit_name.ToCString().upper() assert section.UnitValue() > 0 assert reader.TransferRoots() >= 1 shape = reader.OneShape() assert not shape.IsNull() # IGES carries surfaces, not solids, so only the face count is meaningful. assert measure(shape)["n_faces"] == 6 def test_stl_read_returns_a_triangulation(tmp_path): """RWStl hands back a bare Poly_Triangulation; the app wraps it into a shell itself, which is why nothing here builds a shape.""" from OCP.RWStl import RWStl path = tmp_path / "cube.stl" # A minimal ASCII STL: one triangle is enough to prove the reader. path.write_text( "solid t\n" "facet normal 0 0 1\n outer loop\n" " vertex 0 0 0\n vertex 1 0 0\n vertex 0 1 0\n" " endloop\nendfacet\n" "endsolid t\n" ) triangulation = RWStl.ReadFile_s(str(path)) assert triangulation is not None assert triangulation.NbTriangles() == 1 assert triangulation.NbNodes() == 3 # -------------------------------------------------------------------------- # Inc 4 — text # -------------------------------------------------------------------------- def _a_font_path() -> str | None: """Any TrueType face on the box, since no font is committed here.""" roots = ("/usr/share/fonts", "/usr/local/share/fonts") for root in roots: for dirpath, _dirs, files in os.walk(root): for name in sorted(files): if name.lower().endswith((".ttf", ".otf")): return os.path.join(dirpath, name) return None def test_text_builder_renders_glyphs_as_faces(): """Text emboss builds its glyph outlines this way. The whole chain crosses four modules: NCollection carries the strings, Graphic3d the alignment, StdPrs does the work, gp positions the pen.""" from OCP.Graphic3d import Graphic3d_HTA_CENTER, Graphic3d_VTA_CENTER from OCP.NCollection import NCollection_Utf8String from OCP.StdPrs import StdPrs_BRepFont, StdPrs_BRepTextBuilder from OCP.gp import gp_Ax3 from OCP.TopAbs import TopAbs_FACE from .test_inc1_modeling import sub_shapes path = _a_font_path() if path is None: pytest.skip("no TrueType font in the image to render with") font = StdPrs_BRepFont() assert font.Init(NCollection_Utf8String(path), 10.0, 0), path builder = StdPrs_BRepTextBuilder() shape = builder.Perform( font, NCollection_Utf8String("Ab"), gp_Ax3(), Graphic3d_HTA_CENTER, Graphic3d_VTA_CENTER, ) assert not shape.IsNull() # Two glyphs, each at least one face; 'A' contributes a counter too. assert len(sub_shapes(shape, TopAbs_FACE)) >= 2 def test_font_loading_reports_failure_rather_than_raising(): """The app treats a bad font as a user error, so Init must return False.""" from OCP.NCollection import NCollection_Utf8String from OCP.StdPrs import StdPrs_BRepFont with tempfile.NamedTemporaryFile(suffix=".ttf") as bogus: bogus.write(b"not a font") bogus.flush() font = StdPrs_BRepFont() assert font.Init(NCollection_Utf8String(bogus.name), 10.0, 0) is False