Three defects in the orthographic view engine, all one missing idea: the engine
had no visibility rule and no single owner for the silhouette.
1. `_end_circles` inverted the hidden-line rule. Walking the bands from the
viewing end it drew a circle only when its radius EXCEEDED everything nearer,
and roled every such circle `hidden` unless it was the first band - so the one
edge that CANNOT be occluded (a step face larger than the material in front of
it) shipped dashed on HIDDEN, and the circle that really is covered was not
drawn at all. `_front_max` is now the rule: an edge of radius r at station x is
continuous when r is larger than every outer radius between x and the observer,
dashed when it is not, and it is always drawn. Bores follow the mirror image -
you see down a bore only while it stays at least as wide. Each distinct radius
is emitted once, because two coincident circles are a duplicate entity.
The same rule now roles a feature's axis-centred end-view circle, whose axial
station comes from `removal_box`; a feature that reports none keeps its own
verdict rather than having one guessed.
Ground truth by ray cast along the axis of the same solid of revolution
(segments 20x25, 40x30, 15x20; observer at +X, probe just inside each edge):
r=10 at x=75 -> first surface x=75 -> VISIBLE (was visible, kept)
r=15 at x=60 -> first surface x=60 -> VISIBLE (was HIDDEN, fixed)
r=12.5 at x=20 -> first surface x=60 -> HIDDEN (was not drawn, fixed)
2. Every axisymmetric groove was drawn twice - once as the notch
`radial_profile` cuts into the silhouette, once by the feature's own
`prims()`, which `_feature_layer` appended unconditionally. Six exactly
coincident LINEs per grooved shaft, which is precisely what
`critique.py::_check_duplicate_entities` matches, so premium rule 7 could
never be met on a part carrying a retaining or O-ring groove. `_profile_owned`
now drops a feature primitive that the body already emitted, and drops a
revolved front view's `visible` silhouette line from any feature that edits
the radial profile: the spec's rule is that grooves cut a notch in the
silhouette, and the notch belongs to the one calculation. Suppression is
"the profile already drew this", not "this feature edits the profile", so a
blind bore keeps the bottom line the profile cannot express, and an internal
thread keeps its ISO 6410 limit line.
A groove straddling a step was worse than duplicated: the feature reads one
surface radius for a span that has two, so it drew a groove floor through
solid material and left the +Y path with four odd-degree nodes. It now has
two - its two ends on the axis.
3. `build_view` validated `side` and put it in ctx, but the prismatic branch
ignored it: `side="left"` returned the right-hand view, with every feature on
the wrong side of the sheet and a symmetric outline rectangle to hide it. An
observer at -X sees screen-right = -Y, so the feature layer is now mirrored
about the outline's mid-Y. The top view is always seen from above and does
not move with `side`; that is now said in the docstring and pinned by a test.
Tests: the two that pinned the inverted rule are rewritten around the ray cast,
and one of them contradicted its own name. Seven tests added, and the prismatic
side-view test's hole moved off the y=25 mirror axis, where it could not tell a
left view from a right one. Nine of them fail against the previous `views.py`
and pass against this one.
Live AutoCAD was unavailable for a SOLPROF cross-check: the running AutoCAD 2026
(pid 40068) reports Responding=False and `GetActiveObject("AutoCAD.Application")`
never returned inside 100 s, so no scratch document was created and none is left
open. No new ActiveX member is called by this change - views.py is pure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
462 lines
18 KiB
Python
462 lines
18 KiB
Python
"""The orthographic view engine: silhouette, hidden lines, centre lines, layout.
|
|
|
|
Everything is asserted as emitted primitive tuples - counts, roles, coordinates
|
|
to 1e-9. The radial profile is the single calculation both the silhouette and
|
|
(from Task 5) the section cut face come out of, so it is pinned first.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
|
|
import pytest
|
|
|
|
from engineering.mech.features import (
|
|
AxialBore,
|
|
Chamfer,
|
|
CornerFillet,
|
|
CrossHole,
|
|
Fillet,
|
|
Hole,
|
|
Keyway,
|
|
ORingGroove,
|
|
RetainingGroove,
|
|
)
|
|
from engineering.mech.part import PrismaticPart, RevolvedPart, Segment
|
|
from engineering.mech.primitives import Arc, Circle, Line
|
|
from engineering.mech.views import (
|
|
PROJECTIONS,
|
|
VIEW_KINDS,
|
|
Band,
|
|
View,
|
|
build_view,
|
|
layout_origin,
|
|
radial_profile,
|
|
)
|
|
|
|
EPS = 1e-9
|
|
|
|
|
|
def shaft(*features) -> RevolvedPart:
|
|
return RevolvedPart(
|
|
name="shaft",
|
|
segments=(Segment(20.0, 25.0), Segment(40.0, 30.0), Segment(15.0, 20.0)),
|
|
features=tuple(features),
|
|
)
|
|
|
|
|
|
def plate(*features) -> PrismaticPart:
|
|
return PrismaticPart(
|
|
name="plate",
|
|
outline=((0.0, 0.0), (80.0, 0.0), (80.0, 50.0), (0.0, 50.0)),
|
|
thickness=8.0,
|
|
features=tuple(features),
|
|
)
|
|
|
|
|
|
def roles(view: View, role: str):
|
|
return [p for p in view.prims if getattr(p, "role", None) == role]
|
|
|
|
|
|
def coincident(view: View) -> dict:
|
|
"""Geometries drawn more than once - exactly what ``_check_duplicate_entities`` flags."""
|
|
counts: Counter = Counter()
|
|
for prim in view.prims:
|
|
if isinstance(prim, Line):
|
|
counts[tuple(sorted((prim.p1, prim.p2)))] += 1
|
|
elif isinstance(prim, Circle):
|
|
counts[(prim.center, round(prim.radius, 9))] += 1
|
|
return {key: n for key, n in counts.items() if n > 1}
|
|
|
|
|
|
def test_the_engine_declares_three_orthographic_kinds_and_two_projections():
|
|
assert VIEW_KINDS == ("front", "side", "top")
|
|
assert PROJECTIONS == ("first", "third")
|
|
|
|
|
|
# -- the radial profile ------------------------------------------------------
|
|
|
|
|
|
def test_a_plain_shaft_has_one_band_per_segment():
|
|
bands = radial_profile(shaft())
|
|
assert bands == (
|
|
Band(0.0, 20.0, 0.0, 12.5, 12.5),
|
|
Band(20.0, 60.0, 0.0, 15.0, 15.0),
|
|
Band(60.0, 75.0, 0.0, 10.0, 10.0),
|
|
)
|
|
|
|
|
|
def test_a_taper_is_carried_as_two_end_radii():
|
|
part = RevolvedPart(name="cone", segments=(Segment(20.0, 30.0, 0.0, 10.0),))
|
|
(band,) = radial_profile(part)
|
|
assert band.r_out0 == pytest.approx(15.0, abs=1e-6)
|
|
assert band.r_out1 == pytest.approx(5.0, abs=1e-6)
|
|
|
|
|
|
def test_a_ring_groove_cuts_a_band_out_of_the_outer_profile():
|
|
groove = RetainingGroove(id="rg", x=30.0, d=30.0, m=1.6, d2=28.6)
|
|
bands = radial_profile(shaft(groove))
|
|
# the cut band is the one INSIDE the 30 mm segment whose outside is pulled
|
|
# back; segments 0 (d25) and 2 (d20) are simply thinner and are not cuts.
|
|
cut = [b for b in bands if 20.0 <= b.x0 and b.x1 <= 60.0 and b.r_out0 < 15.0 - EPS]
|
|
assert len(cut) == 1
|
|
assert cut[0].x0 == pytest.approx(29.2)
|
|
assert cut[0].x1 == pytest.approx(30.8)
|
|
assert cut[0].r_out0 == pytest.approx(14.3)
|
|
|
|
|
|
def test_an_axial_bore_raises_the_inner_profile_over_its_depth():
|
|
bore = AxialBore(id="ab", at="start", diameter=8.0, depth=30.0)
|
|
bands = radial_profile(shaft(bore))
|
|
bored = [b for b in bands if b.r_in > 0.0]
|
|
assert [(b.x0, b.x1) for b in bored] == [(0.0, 20.0), (20.0, 30.0)]
|
|
assert {b.r_in for b in bored} == {4.0}
|
|
|
|
|
|
# -- the revolved front view -------------------------------------------------
|
|
|
|
|
|
def test_a_plain_shaft_front_view_is_a_mirrored_silhouette_on_an_axis():
|
|
view = build_view(shaft(), "front")
|
|
assert view.kind == "front"
|
|
axis = roles(view, "center")
|
|
assert len(axis) == 1
|
|
assert axis[0].p1[1] == 0.0 and axis[0].p2[1] == 0.0
|
|
assert axis[0].p1[0] < 0.0 and axis[0].p2[0] > 75.0
|
|
visible = roles(view, "visible")
|
|
# the +Y half-outline is 7 lines (left face, 3 cylinders, 2 steps, right face)
|
|
# and it is mirrored about the axis
|
|
assert len(visible) == 14
|
|
ys = sorted({round(line.p1[1], 6) for line in visible})
|
|
assert ys == [-15.0, -12.5, -10.0, 0.0, 10.0, 12.5, 15.0]
|
|
assert view.bbox == (pytest.approx(-7.5), -15.0, pytest.approx(82.5), 15.0)
|
|
|
|
|
|
def test_a_bore_reaches_the_front_view_as_hidden_lines():
|
|
part = RevolvedPart(name="sleeve", segments=(Segment(30.0, 40.0, 20.0),))
|
|
view = build_view(part, "front")
|
|
hidden = roles(view, "hidden")
|
|
assert len(hidden) == 2
|
|
assert {round(line.p1[1], 6) for line in hidden} == {10.0, -10.0}
|
|
assert hidden[0].p1[0] == 0.0 and hidden[0].p2[0] == 30.0
|
|
|
|
|
|
def test_a_chamfer_replaces_the_silhouette_corner_instead_of_being_drawn_over_it():
|
|
chamfer = Chamfer(id="c1", at="start", size=2.0)
|
|
view = build_view(shaft(chamfer), "front")
|
|
upper = [line for line in roles(view, "visible") if line.p1[1] > 0 or line.p2[1] > 0]
|
|
starts = {(round(line.p1[0], 6), round(line.p1[1], 6)) for line in upper}
|
|
assert (0.0, 10.5) in starts
|
|
assert (0.0, 12.5) not in starts
|
|
assert any(line.p1 == (0.0, 10.5) and line.p2 == (2.0, 12.5) for line in upper)
|
|
|
|
|
|
def test_a_fillet_puts_a_tangent_arc_into_the_silhouette():
|
|
fillet = Fillet(id="f1", at="step:0", radius=2.0)
|
|
view = build_view(shaft(fillet), "front")
|
|
arcs = [p for p in view.prims if isinstance(p, Arc)]
|
|
assert len(arcs) == 2 # the fillet and its mirror
|
|
upper = next(a for a in arcs if a.center[1] > 0)
|
|
assert upper.center == (18.0, 14.5)
|
|
assert (upper.radius, upper.start_deg, upper.end_deg) == (2.0, 270.0, 360.0)
|
|
|
|
|
|
def test_an_axisymmetric_feature_is_mirrored_and_a_keyway_is_not():
|
|
groove = RetainingGroove(id="rg", x=30.0, d=30.0, m=1.6, d2=28.6)
|
|
keyway = Keyway(id="kw", segment=1, length=20.0, offset=5.0)
|
|
view = build_view(shaft(groove, keyway), "front")
|
|
groove_lines = [
|
|
line for line in view.prims if isinstance(line, Line) and abs(abs(line.p1[1]) - 14.3) < 1e-6
|
|
]
|
|
assert {line.p1[1] > 0 for line in groove_lines} == {True, False}
|
|
floor = 15.0 - 4.0 # DIN 6885 depth_shaft for a 30 mm shaft
|
|
keyway_lines = [
|
|
line for line in view.prims if isinstance(line, Line) and abs(line.p1[1] - floor) < 1e-6
|
|
]
|
|
assert keyway_lines and all(line.p1[1] > 0 for line in keyway_lines)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"groove",
|
|
[
|
|
RetainingGroove(id="rg", x=30.0, d=30.0, m=1.6, d2=28.6),
|
|
ORingGroove(id="or", x=40.0, cord=3.0, b=4.1, h=2.3),
|
|
],
|
|
ids=["retaining", "oring"],
|
|
)
|
|
def test_a_groove_is_the_notch_in_the_silhouette_and_is_not_drawn_a_second_time(groove):
|
|
"""The spec's rule is `grooves cut a notch in the silhouette`. The notch is
|
|
the radial profile's; the feature does not redraw it. Two coincident LINEs
|
|
are what ``drawing_critique(focus=None)`` reports as a duplicate, and
|
|
premium rule 7 needs that report empty before ``drawing_finalize``."""
|
|
view = build_view(shaft(groove), "front")
|
|
assert coincident(view) == {}
|
|
# the notch is still there, mirrored, once per side
|
|
floor = {round(abs(line.p1[1]), 6) for line in view.prims if isinstance(line, Line)}
|
|
assert (14.3 in floor) or (12.7 in floor)
|
|
assert view.omitted == ()
|
|
|
|
|
|
def test_an_axial_bore_end_circle_is_drawn_once():
|
|
"""``_end_circles`` reads the bore off the same radial profile the feature
|
|
edited, so the feature's own circle would be the second copy of it."""
|
|
view = build_view(shaft(AxialBore(id="ab", at="start", diameter=8.0, depth=30.0)), "side")
|
|
assert coincident(view) == {}
|
|
assert len([c for c in view.prims if isinstance(c, Circle) and abs(c.radius - 4.0) < EPS]) == 1
|
|
|
|
|
|
def test_a_blind_bore_keeps_the_bottom_line_the_profile_cannot_draw():
|
|
"""Suppression is `the profile already drew this`, not `this feature edits
|
|
the profile` - the blind bottom is not on the radial profile at all."""
|
|
view = build_view(shaft(AxialBore(id="ab", at="start", diameter=8.0, depth=30.0)), "front")
|
|
bottoms = [
|
|
line
|
|
for line in roles(view, "hidden")
|
|
if isinstance(line, Line) and abs(line.p1[0] - 30.0) < EPS and abs(line.p2[0] - 30.0) < EPS
|
|
]
|
|
assert len(bottoms) == 1
|
|
assert {round(bottoms[0].p1[1], 6), round(bottoms[0].p2[1], 6)} == {-4.0, 4.0}
|
|
|
|
|
|
def test_a_groove_straddling_a_step_leaves_one_closed_silhouette():
|
|
"""The feature's own prims read a single surface radius for a span that has
|
|
two, so drawing them as well as the profile's notch put a groove floor
|
|
through solid material and left the +Y path with four odd-degree nodes.
|
|
An open path has exactly two: its two ends on the axis."""
|
|
part = shaft(RetainingGroove(id="rg", x=20.0, d=30.0, m=1.6, d2=28.6))
|
|
upper = [
|
|
line
|
|
for line in roles(build_view(part, "front"), "visible")
|
|
if isinstance(line, Line) and line.p1[1] >= 0.0 and line.p2[1] >= 0.0
|
|
]
|
|
degree: Counter = Counter()
|
|
for line in upper:
|
|
degree[(round(line.p1[0], 6), round(line.p1[1], 6))] += 1
|
|
degree[(round(line.p2[0], 6), round(line.p2[1], 6))] += 1
|
|
assert sorted(node for node, n in degree.items() if n % 2) == [(0.0, 0.0), (75.0, 0.0)]
|
|
# nothing is drawn at the groove floor radius outside the band the profile cut
|
|
assert not [
|
|
line
|
|
for line in upper
|
|
if abs(line.p1[1] - 14.3) < EPS and abs(line.p2[1] - 14.3) < EPS and line.p1[0] < 20.0
|
|
]
|
|
|
|
|
|
# -- the revolved end view ---------------------------------------------------
|
|
|
|
|
|
def test_the_end_view_hides_the_smaller_diameter_and_draws_the_step_face_continuous():
|
|
"""Ground truth by ray cast along the axis of the same solid of revolution
|
|
(viewer at +X, probe just inside each edge):
|
|
|
|
edge r=10 at x=75 -> first surface at x=75 -> VISIBLE
|
|
edge r=15 at x=60 -> first surface at x=60 -> VISIBLE
|
|
edge r=12.5 at x=20 -> first surface at x=60 -> HIDDEN
|
|
|
|
The annular step face at x=60 faces the observer and nothing at projected
|
|
radius 15-eps stands in front of it, so the 30 mm circle is continuous. The
|
|
25 mm circle is the one that is genuinely covered, and it is still *drawn* -
|
|
dashed, not dropped.
|
|
"""
|
|
view = build_view(shaft(), "side", side="right")
|
|
circles = [p for p in view.prims if isinstance(p, Circle)]
|
|
radii = {(round(c.radius, 6), c.role) for c in circles}
|
|
assert radii == {(10.0, "visible"), (15.0, "visible"), (12.5, "hidden")}
|
|
assert len([p for p in view.prims if getattr(p, "role", None) == "center"]) == 2
|
|
|
|
|
|
def test_looking_from_the_left_reverses_which_circle_is_visible():
|
|
"""From -X the near end is the 25 mm face; the 20 mm end is behind the 30 mm body."""
|
|
view = build_view(shaft(), "side", side="left")
|
|
circles = [p for p in view.prims if isinstance(p, Circle)]
|
|
radii = {(round(c.radius, 6), c.role) for c in circles}
|
|
assert radii == {(12.5, "visible"), (15.0, "visible"), (10.0, "hidden")}
|
|
|
|
|
|
def test_a_feature_circle_at_the_far_end_is_hidden_behind_the_larger_body():
|
|
"""A chamfer at x=0 seen from +X sits behind the 30 mm band, so its circle
|
|
is dashed; seen from -X nothing is in front of it and it is continuous."""
|
|
part = shaft(Chamfer(id="c1", at="start", size=2.0))
|
|
right = {
|
|
(round(c.radius, 6), c.role)
|
|
for c in build_view(part, "side", side="right").prims
|
|
if isinstance(c, Circle)
|
|
}
|
|
left = {
|
|
(round(c.radius, 6), c.role)
|
|
for c in build_view(part, "side", side="left").prims
|
|
if isinstance(c, Circle)
|
|
}
|
|
assert (10.5, "hidden") in right
|
|
assert (10.5, "visible") in left
|
|
|
|
|
|
def test_a_bore_that_widens_behind_a_narrower_one_is_hidden():
|
|
"""Looking into the narrow end you see the small bore; the wider bore behind
|
|
it is covered by the wall of the narrow one. From the other end, both are
|
|
seen down the widening hole."""
|
|
part = RevolvedPart(
|
|
name="stepped-bore",
|
|
segments=(Segment(20.0, 40.0, 10.0), Segment(20.0, 40.0, 20.0)),
|
|
)
|
|
right = {
|
|
(round(c.radius, 6), c.role)
|
|
for c in build_view(part, "side", side="right").prims
|
|
if isinstance(c, Circle)
|
|
}
|
|
left = {
|
|
(round(c.radius, 6), c.role)
|
|
for c in build_view(part, "side", side="left").prims
|
|
if isinstance(c, Circle)
|
|
}
|
|
assert (10.0, "visible") in right and (5.0, "visible") in right
|
|
assert (5.0, "visible") in left and (10.0, "hidden") in left
|
|
|
|
|
|
def test_a_revolved_top_view_is_refused_because_it_repeats_the_front():
|
|
with pytest.raises(ValueError, match="identical to its front view"):
|
|
build_view(shaft(), "top")
|
|
|
|
|
|
# -- the prismatic views -----------------------------------------------------
|
|
|
|
|
|
def test_a_prismatic_front_view_is_the_closed_outline():
|
|
view = build_view(plate(), "front")
|
|
lines = roles(view, "visible")
|
|
assert len(lines) == 4
|
|
assert view.bbox == (0.0, 0.0, 80.0, 50.0)
|
|
|
|
|
|
def test_a_corner_fillet_replaces_an_outline_vertex_with_an_arc():
|
|
view = build_view(plate(CornerFillet(id="cf", vertex=1, radius=10.0)), "front")
|
|
arcs = [p for p in view.prims if isinstance(p, Arc)]
|
|
# the substituted arc carries role "visible" too, so the straight edges are
|
|
# counted by type: 4 outline edges remain, two of them shortened.
|
|
lines = [p for p in roles(view, "visible") if isinstance(p, Line)]
|
|
assert len(arcs) == 1
|
|
assert arcs[0].center == (pytest.approx(70.0), pytest.approx(10.0))
|
|
assert len(lines) == 4
|
|
assert not any(abs(line.p1[0] - 80.0) < EPS and abs(line.p1[1]) < EPS for line in lines)
|
|
|
|
|
|
def test_a_prismatic_side_view_is_the_thickness_rectangle_with_a_hidden_line_per_hole():
|
|
# y=10, deliberately OFF the y=25 mirror axis of the 0..50 plate: a hole on
|
|
# the axis cannot tell a right-hand view from a left-hand one.
|
|
hole = Hole(id="h1", x=20.0, y=10.0, diameter=10.0)
|
|
view = build_view(plate(hole), "side")
|
|
visible = roles(view, "visible")
|
|
hidden = roles(view, "hidden")
|
|
assert len(visible) == 4
|
|
assert view.bbox == (0.0, 0.0, 50.0, 8.0)
|
|
assert len(hidden) == 2
|
|
assert {round(line.p1[0], 6) for line in hidden} == {5.0, 15.0}
|
|
|
|
|
|
def test_a_prismatic_left_side_view_is_the_right_view_mirrored():
|
|
"""An observer at -X sees screen-right = -Y, so the hole at y=10 in a plate
|
|
spanning y 0..50 lands at u = 50-15 .. 50-5. Accepting ``side='left'`` and
|
|
returning the right-hand view puts every feature on the wrong side of the
|
|
sheet with nothing in the drawing to reveal it."""
|
|
hole = Hole(id="h1", x=20.0, y=10.0, diameter=10.0)
|
|
part = plate(hole)
|
|
right = {
|
|
round(line.p1[0], 6) for line in roles(build_view(part, "side", side="right"), "hidden")
|
|
}
|
|
left = {round(line.p1[0], 6) for line in roles(build_view(part, "side", side="left"), "hidden")}
|
|
assert right == {5.0, 15.0}
|
|
assert left == {35.0, 45.0}
|
|
# the thickness rectangle is its own mirror, so only the features move
|
|
assert build_view(part, "side", side="left").bbox == (0.0, 0.0, 50.0, 8.0)
|
|
|
|
|
|
def test_a_prismatic_top_view_is_always_seen_from_above():
|
|
"""``side`` names the end an *end* view is taken from; the top view is not
|
|
one, so it must not move with it."""
|
|
part = plate(Hole(id="h1", x=20.0, y=10.0, diameter=10.0))
|
|
from_right = {
|
|
round(line.p1[0], 6) for line in roles(build_view(part, "top", side="right"), "hidden")
|
|
}
|
|
from_left = {
|
|
round(line.p1[0], 6) for line in roles(build_view(part, "top", side="left"), "hidden")
|
|
}
|
|
assert from_right == from_left == {15.0, 25.0}
|
|
|
|
|
|
def test_a_prismatic_top_view_projects_the_other_axis():
|
|
hole = Hole(id="h1", x=20.0, y=25.0, diameter=10.0)
|
|
view = build_view(plate(hole), "top")
|
|
assert view.bbox == (0.0, 0.0, 80.0, 8.0)
|
|
hidden = roles(view, "hidden")
|
|
assert {round(line.p1[0], 6) for line in hidden} == {15.0, 25.0}
|
|
|
|
|
|
# -- the honesty rule --------------------------------------------------------
|
|
|
|
|
|
def test_a_feature_that_projects_into_the_view_is_not_reported_as_omitted():
|
|
side = build_view(shaft(Chamfer(id="c1", at="start", size=2.0)), "side")
|
|
assert side.omitted == ()
|
|
front = build_view(shaft(CrossHole(id="xh", x=30.0, diameter=6.0)), "front")
|
|
assert front.omitted == ()
|
|
bore = build_view(shaft(AxialBore(id="ab", at="start", diameter=8.0, depth=12.0)), "front")
|
|
assert bore.omitted == ()
|
|
|
|
|
|
def test_a_bend_line_is_omitted_from_a_side_view_with_a_reason():
|
|
from engineering.mech.features import BendLine
|
|
|
|
bend = BendLine(id="b1", p1=(0.0, 25.0), p2=(80.0, 25.0), angle=90.0, inside_radius=2.0)
|
|
view = build_view(plate(bend), "side")
|
|
assert len(view.omitted) == 1
|
|
assert view.omitted[0]["feature"] == "b1"
|
|
assert "side" in view.omitted[0]["reason"]
|
|
|
|
|
|
def test_the_view_collects_the_dimension_intents_of_its_own_features():
|
|
keyway = Keyway(id="kw", segment=1, length=20.0)
|
|
view = build_view(shaft(keyway), "front")
|
|
assert [intent.feature for intent in view.dims] == ["kw"]
|
|
|
|
|
|
def test_an_unknown_view_kind_is_refused_by_name():
|
|
with pytest.raises(ValueError, match="front"):
|
|
build_view(shaft(), "isometric")
|
|
|
|
|
|
# -- projection layout -------------------------------------------------------
|
|
|
|
|
|
def test_first_angle_puts_the_right_hand_view_on_the_left_and_the_top_below():
|
|
front = build_view(shaft(), "front")
|
|
end = build_view(shaft(), "side")
|
|
left = layout_origin(front, "side", side="right", projection="first", child=end)
|
|
width = end.bbox[2] - end.bbox[0]
|
|
height = end.bbox[3] - end.bbox[1]
|
|
assert left[0] == pytest.approx(front.bbox[0] - 20.0 - width)
|
|
assert left[1] == pytest.approx((front.bbox[1] + front.bbox[3]) / 2.0 - height / 2.0)
|
|
below = layout_origin(front, "top", projection="first", child=end)
|
|
assert below[1] == pytest.approx(front.bbox[1] - 20.0 - height)
|
|
|
|
|
|
def test_third_angle_flips_both_placements():
|
|
front = build_view(shaft(), "front")
|
|
end = build_view(shaft(), "side")
|
|
right = layout_origin(front, "side", side="right", projection="third", child=end)
|
|
assert right[0] == pytest.approx(front.bbox[2] + 20.0)
|
|
above = layout_origin(front, "top", projection="third", child=end)
|
|
assert above[1] == pytest.approx(front.bbox[3] + 20.0)
|
|
|
|
|
|
def test_without_a_child_the_layout_assumes_the_parent_size():
|
|
front = build_view(shaft(), "front")
|
|
width = front.bbox[2] - front.bbox[0]
|
|
left = layout_origin(front, "side", side="right", projection="first")
|
|
assert left[0] == pytest.approx(front.bbox[0] - 20.0 - width)
|
|
|
|
|
|
def test_an_unknown_projection_is_refused():
|
|
front = build_view(shaft(), "front")
|
|
with pytest.raises(ValueError, match="first"):
|
|
layout_origin(front, "side", projection="second")
|