""" The renderer and the composer, tested the way a viewer can actually fail. A picture that looks plausible while being wrong is worse than no picture, and this is the first part of the system CIVICVS will judge by eye rather than by test output. So the things asserted here are the ones an eye cannot check: that the flip is applied exactly once, that holes are not filled in, that a cavity really is larger than the stock inside it, and that a rejection reaches the screen as a message rather than a stack trace. """ from __future__ import annotations import re import pytest from mechcomp import svg SQUARE = [[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)]] WITH_HOLE = [ [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)], [(4.0, 4.0), (6.0, 4.0), (6.0, 6.0), (4.0, 6.0)], ] # --------------------------------------------------------------------------- # The renderer # --------------------------------------------------------------------------- def test_every_ring_reaches_the_path(): """A hole silently dropped would render as solid material.""" d = svg.path_d(WITH_HOLE) assert d.count("M ") == 2, d assert d.count("Z") == 2 assert "4 4" in d.replace(".0", "") def test_holes_use_evenodd_so_winding_does_not_matter(): """ Ring winding is not something this module should have to know. evenodd renders a hole as a hole either way, and its absence is the difference between a bore and a solid plug. """ out = svg.render(WITH_HOLE) assert out.count('fill-rule="evenodd"') >= 1 def test_degenerate_rings_are_skipped_not_emitted(): assert svg.path_d([[(0.0, 0.0), (1.0, 1.0)]]) == "" assert svg.path_d([[]]) == "" def test_the_vertical_flip_is_applied_exactly_once(): """ Geometry has +Y up, SVG has +Y down. Flipping twice, or not at all, gives a part that is upside down -- which for an asymmetric profile like T or A Frame is wrong in a way that is easy to miss and impossible to print around. """ out = svg.render(SQUARE) assert out.count("scale(1,-1)") == 1 assert len(re.findall(r"transform=", out)) == 1 def test_coordinates_are_left_in_geometry_space(): """ The flip is in the transform, so the numbers in the file are the geometry's own and can be read against the report. Negating them instead would make the file unreadable against anything. """ out = svg.render(SQUARE) assert " 10 " in out or " 10 10" in out assert "-10" not in svg.path_d(SQUARE) def test_viewbox_contains_the_geometry_plus_margin(): out = svg.render(SQUARE, margin=3.0) vb = re.search(r'viewBox="([-\d. ]+)"', out).group(1).split() x0, y0, w, h = (float(v) for v in vb) assert x0 == pytest.approx(-3.0) assert y0 == pytest.approx(-3.0) assert w == pytest.approx(16.0) assert h == pytest.approx(16.0) def test_layers_are_separately_classed(): """ ROADMAP section 4's stated reason for the port. A flattened silhouette would not have justified leaving OpenSCAD. """ out = svg.render(SQUARE, cavities=WITH_HOLE, stock=[SQUARE[0]]) for cls in ("mc-layer-material", "mc-layer-cavity", "mc-layer-stock"): assert cls in out, cls def test_optional_layers_are_omitted_when_absent(): out = svg.render(SQUARE) assert "mc-layer-material" in out assert "mc-layer-cavity" not in out assert "mc-layer-stock" not in out def test_bounds_of_nothing_does_not_divide_by_zero(): assert svg.bounds([]) == (-1.0, -1.0, 1.0, 1.0) assert svg.render([]).startswith("") for cls in ("mc-layer-material", "mc-layer-cavity", "mc-layer-stock"): assert cls in out, "%s missing from a real render" % cls assert out.count("scale(1,-1)") == 1 def test_the_cavity_is_drawn_larger_than_the_stock_in_it(): """ The fit gap is the reason to look at the picture at all. If cavity and stock render identically, the clearance is invisible and the viewer is useless for the one judgement it exists to support. """ build = pytest.importorskip("mechcomp.profiles").build from mechcomp.geom.join import cavity_region from mechcomp.geom.records import strap_path from mechcomp.geom.region import area as region_area result = build(family="3x", profile="Y", params={"fit_clearance_mm": 0.4}) cav = cavity_region(result.members, result.geo) stock = [strap_path(m, result.geo) for m in result.members] assert region_area(cav) > region_area(stock), ( "cavity is not larger than the stock it holds") def test_every_profile_in_both_families_renders(): """A viewer that works for Y and breaks on Cross is not a catalogue.""" profiles = pytest.importorskip("mechcomp.profiles") from mechcomp.web.app import families for name, family in families().items(): for profile in sorted(family.catalogue): try: result = profiles.build(family=name, profile=profile, params={}) except profiles.ProfileRejected: continue out = svg.render_result(result) assert out.startswith("