merge: tracks B+G group A

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-23 12:25:44 +03:00
co-authored by Claude Opus 5.5
10 changed files with 2798 additions and 10 deletions
+543
View File
@@ -0,0 +1,543 @@
"""ISO 21920-1 / ISO 1302 surface texture and ISO 2553 weld symbols.
Pure geometry is asserted as emitted primitive tuples (counts, roles,
coordinates to 1e-9); the async layer is asserted against a real ezdxf
document through the `backend` fixture. No screenshots anywhere.
"""
from __future__ import annotations
import math
import types
import pytest
from engineering.mech.annotate import (
ISO1302_SYMBOL_PROPORTIONS,
LAY_SYMBOLS,
MACHINING_KINDS,
SURFACE_HEIGHTS,
SURFACE_STANDARDS,
surface_texture_prims,
symbol_proportions,
)
from engineering.mech.primitives import Circle, Line, Text
pytestmark = pytest.mark.asyncio
EPS = 1e-9
K = 1.0 / math.sqrt(3.0) # tan(30 deg): the 60 deg legs' horizontal run per unit rise
def _flat(points):
"""Flatten a point sequence: pytest.approx refuses nested structures."""
return tuple(float(value) for point in points for value in point)
async def test_the_seven_iso1302_proportion_rows_are_the_transcribed_ones():
assert SURFACE_HEIGHTS == (2.5, 3.5, 5.0, 7.0, 10.0, 14.0, 20.0)
assert ISO1302_SYMBOL_PROPORTIONS[2.5] == (0.25, 3.5, 7.5)
assert ISO1302_SYMBOL_PROPORTIONS[3.5] == (0.35, 5.0, 10.5)
assert ISO1302_SYMBOL_PROPORTIONS[5.0] == (0.5, 7.0, 15.0)
assert ISO1302_SYMBOL_PROPORTIONS[7.0] == (0.7, 10.0, 21.0)
assert ISO1302_SYMBOL_PROPORTIONS[10.0] == (1.0, 14.0, 30.0)
assert ISO1302_SYMBOL_PROPORTIONS[14.0] == (1.4, 20.0, 42.0)
assert ISO1302_SYMBOL_PROPORTIONS[20.0] == (2.0, 28.0, 60.0)
# Every row: line width is h/10 and the minimum overall height is 3h.
for height, (width, _h1, h2) in ISO1302_SYMBOL_PROPORTIONS.items():
assert width == pytest.approx(height / 10.0, abs=EPS)
assert h2 == pytest.approx(3.0 * height, abs=EPS)
async def test_a_text_height_outside_the_table_is_refused_by_name():
with pytest.raises(ValueError) as excinfo:
symbol_proportions(4.0)
message = str(excinfo.value)
assert "ISO 1302" in message and "4 mm" in message
assert "2.5, 3.5, 5, 7, 10, 14, 20" in message
async def test_the_basic_symbol_is_a_60_degree_vee_on_the_dim_role():
prims = surface_texture_prims(height=3.5)
short, long_ = prims[0], prims[1]
assert isinstance(short, Line) and isinstance(long_, Line)
assert short.p1 == (0.0, 0.0) and long_.p1 == (0.0, 0.0)
assert short.p2 == pytest.approx((-5.0 * K, 5.0), abs=EPS)
assert long_.p2 == pytest.approx((10.5 * K, 10.5), abs=EPS)
assert short.role == "dim" and long_.role == "dim"
# The angle between the two legs is 60 degrees.
a = math.atan2(short.p2[1], short.p2[0])
b = math.atan2(long_.p2[1], long_.p2[0])
assert math.degrees(a - b) == pytest.approx(60.0, abs=1e-9)
# No bar, no circle: the basic symbol plus the extension line only.
assert [type(p).__name__ for p in prims] == ["Line", "Line", "Line"]
async def test_machining_required_closes_the_vee_with_the_bar():
prims = surface_texture_prims(height=3.5, machining="required")
bar = prims[2]
assert isinstance(bar, Line)
assert bar.p1 == pytest.approx((-5.0 * K, 5.0), abs=EPS)
assert bar.p2 == pytest.approx((5.0 * K, 5.0), abs=EPS)
assert bar.role == "dim"
async def test_machining_prohibited_inscribes_the_circle_in_the_vee():
prims = surface_texture_prims(height=3.5, machining="prohibited")
circle = prims[2]
assert isinstance(circle, Circle)
assert circle.center == pytest.approx((0.0, 10.0 / 3.0), abs=EPS)
assert circle.radius == pytest.approx(5.0 / 3.0, abs=EPS)
# Tangent to the bar height H1 and to both legs.
assert circle.center[1] + circle.radius == pytest.approx(5.0, abs=EPS)
async def test_ra_rz_process_lay_and_allowance_land_in_their_declared_positions():
prims = surface_texture_prims(
height=3.5, ra=3.2, rz=12.5, process="milled", lay="perpendicular", allowance=0.5
)
texts = [p for p in prims if isinstance(p, Text)]
assert [t.text for t in texts] == ["Ra 3.2", "Rz 12.5", "milled", "⊥ 0.5"]
x_leg = 10.5 * K
for t in texts:
assert t.at[0] == pytest.approx(x_leg + 0.5 * 3.5, abs=EPS)
assert t.height == 3.5 and t.role == "text"
# Above the extension line the three lines stack upward at 1.5h pitch.
assert texts[0].at[1] == pytest.approx(10.5 + 0.35 * 3.5, abs=EPS)
assert texts[1].at[1] == pytest.approx(10.5 + 0.35 * 3.5 + 1.5 * 3.5, abs=EPS)
assert texts[2].at[1] == pytest.approx(10.5 + 0.35 * 3.5 + 3.0 * 3.5, abs=EPS)
# Lay and allowance sit below it, on one line.
assert texts[3].at[1] == pytest.approx(10.5 - 0.35 * 3.5 - 3.5, abs=EPS)
async def test_the_extension_line_grows_with_the_widest_text():
prims = surface_texture_prims(height=3.5, ra=3.2)
extension = [p for p in prims if isinstance(p, Line)][-1]
x_leg = 10.5 * K
expected = 0.62 * 3.5 * len("Ra 3.2") + 3.5
assert extension.p1 == pytest.approx((x_leg, 10.5), abs=EPS)
assert extension.p2 == pytest.approx((x_leg + expected, 10.5), abs=EPS)
# With no text at all it falls back to the declared minimum of 2h.
bare = [p for p in surface_texture_prims(height=3.5) if isinstance(p, Line)][-1]
assert bare.p2[0] - bare.p1[0] == pytest.approx(2.0 * 3.5, abs=EPS)
async def test_all_around_puts_the_circle_on_the_kink():
prims = surface_texture_prims(height=3.5, all_around=True)
circle = [p for p in prims if isinstance(p, Circle)][0]
assert circle.center == pytest.approx((10.5 * K, 10.5), abs=EPS)
assert circle.radius == pytest.approx(0.35 * 3.5, abs=EPS)
async def test_iso1302_is_accepted_as_an_alias_designation_and_draws_the_same_symbol():
assert SURFACE_STANDARDS == ("ISO 21920-1", "ISO 1302")
new = surface_texture_prims(height=3.5, ra=3.2, standard="ISO 21920-1")
old = surface_texture_prims(height=3.5, ra=3.2, standard="ISO 1302")
assert new == old
with pytest.raises(ValueError) as excinfo:
surface_texture_prims(height=3.5, standard="ASME B46.1")
assert "ISO 21920-1, ISO 1302" in str(excinfo.value)
async def test_an_undefined_lay_and_an_undefined_machining_are_refused_with_the_list():
assert set(LAY_SYMBOLS) == {
"parallel",
"perpendicular",
"crossed",
"multidirectional",
"circular",
"radial",
"particulate",
}
assert MACHINING_KINDS == ("any", "required", "prohibited")
with pytest.raises(ValueError) as excinfo:
surface_texture_prims(lay="diagonal")
assert "circular, crossed, multidirectional" in str(excinfo.value)
with pytest.raises(ValueError) as excinfo:
surface_texture_prims(machining="maybe")
assert "any, required, prohibited" in str(excinfo.value)
# ── ISO 2553 weld symbols ───────────────────────────────────────────────────
from engineering.mech.annotate import ( # noqa: E402 - grouped with its own tests
WELD_KINDS,
WELD_SIDES,
weld_symbol_prims,
)
from engineering.mech.primitives import Arc, Poly # noqa: E402
async def test_only_the_six_transcribed_elementary_symbols_are_offered():
assert WELD_KINDS == ("square", "v", "bevel", "u", "j", "fillet")
assert WELD_SIDES == ("arrow", "other", "both")
with pytest.raises(ValueError) as excinfo:
weld_symbol_prims(kind="spot")
message = str(excinfo.value)
assert "square, v, bevel, u, j, fillet" in message
assert "ISO 2553" in message
with pytest.raises(ValueError) as excinfo:
weld_symbol_prims(kind="fillet", side="both_sides")
assert "arrow, other, both" in str(excinfo.value)
async def test_the_reference_line_comes_first_and_the_identification_line_is_dashed():
prims = weld_symbol_prims(kind="fillet", height=3.5)
reference = prims[0]
assert isinstance(reference, Line)
assert reference.p1 == (0.0, 0.0)
assert reference.p2[1] == 0.0 and reference.p2[0] > 0.0
assert reference.role == "dim"
length = reference.p2[0]
dashes = [
p for p in prims if isinstance(p, Line) and p.p1[1] == pytest.approx(-0.4 * 3.5, abs=EPS)
]
assert len(dashes) >= 2
assert all(d.p2[1] == pytest.approx(-0.4 * 3.5, abs=EPS) for d in dashes)
assert dashes[0].p1[0] == pytest.approx(0.0, abs=EPS)
assert dashes[0].p2[0] - dashes[0].p1[0] == pytest.approx(0.8 * 3.5, abs=EPS)
assert dashes[1].p1[0] - dashes[0].p2[0] == pytest.approx(0.4 * 3.5, abs=EPS)
assert dashes[-1].p2[0] <= length + EPS
async def test_a_symmetrical_weld_meets_back_to_back_on_the_reference_line():
"""ISO 2553: no identification line, and the two elementary symbols share it.
The earlier version of this test asserted the lower V apex at -0.4h, i.e.
on the (omitted) identification line, which drew a double-V butt weld as
two detached halves that were not even symmetric about the reference line.
"""
prims = weld_symbol_prims(kind="v", side="both", height=3.5)
assert not [
p for p in prims if isinstance(p, Line) and p.p1[1] == pytest.approx(-0.4 * 3.5, abs=EPS)
]
# One V above the reference line, one below it, both standing on y = 0.
vees = [p for p in prims if isinstance(p, Poly) and len(p.points) == 3 and not p.closed]
assert len(vees) == 2
upper, lower = vees
assert upper.points[1][1] == pytest.approx(0.0, abs=EPS)
assert lower.points[1][1] == pytest.approx(0.0, abs=EPS)
# Mirror images of each other about y = 0, same x.
assert _flat(lower.points) == pytest.approx(
tuple(v if i % 2 == 0 else -v for i, v in enumerate(_flat(upper.points))), abs=EPS
)
ys = [y for _x, y in upper.points + lower.points]
assert (min(ys), max(ys)) == pytest.approx((-3.5, 3.5), abs=EPS)
async def test_a_symmetrical_fillet_weld_is_the_double_fillet_not_two_halves():
prims = weld_symbol_prims(kind="fillet", side="both", height=3.5)
triangles = [p for p in prims if isinstance(p, Poly) and p.closed]
assert len(triangles) == 2
x0 = triangles[0].points[0][0]
assert _flat(triangles[0].points) == pytest.approx((x0, 0.0, x0, 3.5, x0 + 3.5, 0.0), abs=EPS)
assert _flat(triangles[1].points) == pytest.approx((x0, 0.0, x0, -3.5, x0 + 3.5, 0.0), abs=EPS)
async def test_the_fillet_triangle_stands_on_the_line_with_its_leg_on_the_left():
prims = weld_symbol_prims(kind="fillet", height=3.5)
triangle = [p for p in prims if isinstance(p, Poly) and p.closed][0]
x0 = triangle.points[0][0]
assert _flat(triangle.points) == pytest.approx((x0, 0.0, x0, 3.5, x0 + 3.5, 0.0), abs=EPS)
assert triangle.role == "dim"
async def test_the_other_side_symbol_hangs_below_the_identification_line():
prims = weld_symbol_prims(kind="fillet", side="other", height=3.5)
triangle = [p for p in prims if isinstance(p, Poly) and p.closed][0]
x0 = triangle.points[0][0]
assert _flat(triangle.points) == pytest.approx(
(x0, -1.4, x0, -1.4 - 3.5, x0 + 3.5, -1.4), abs=EPS
)
async def test_other_side_dimensions_are_written_beside_the_other_side_symbol():
"""ISO 2553 writes the size left of the symbol and the length right of it.
For an other-side weld the symbol hangs from the identification line, so
both texts belong there too. Written above the reference line instead,
`a6` sits exactly where an arrow-side throat dimension sits - on a joint
that has no arrow-side weld.
"""
prims = weld_symbol_prims(kind="fillet", side="other", size=6, length=50, height=3.5)
triangle = [p for p in prims if isinstance(p, Poly) and p.closed][0]
x_symbol = triangle.points[0][0]
texts = [p for p in prims if isinstance(p, Text)]
assert [t.text for t in texts] == ["a6", "50"]
# Beside the symbol: size to its left, length to its right.
assert texts[0].at[0] < x_symbol
assert texts[1].at[0] > x_symbol + 3.5
# Below the reference line, 0.25h clear of the identification line, so the
# whole text body lies between the identification line and the symbol's
# far edge - never above y = 0.
ident_y = -0.4 * 3.5
for t in texts:
assert t.at[1] == pytest.approx(ident_y - 1.25 * 3.5, abs=EPS)
assert t.at[1] + t.height < 0.0
# The arrow side is unchanged: its dimensions stay above the reference line.
arrow = [
p
for p in weld_symbol_prims(kind="fillet", side="arrow", size=6, length=50, height=3.5)
if isinstance(p, Text)
]
assert all(t.at[1] == pytest.approx(0.25 * 3.5, abs=EPS) for t in arrow)
# A symmetrical weld carries one set, beside its arrow-side symbol.
both = [
p
for p in weld_symbol_prims(kind="fillet", side="both", size=6, length=50, height=3.5)
if isinstance(p, Text)
]
assert all(t.at[1] == pytest.approx(0.25 * 3.5, abs=EPS) for t in both)
async def test_the_u_symbol_is_a_half_circle_between_two_uprights():
prims = weld_symbol_prims(kind="u", height=3.5)
arc = [p for p in prims if isinstance(p, Arc)][0]
assert arc.radius == pytest.approx(1.75, abs=EPS)
assert (arc.start_deg, arc.end_deg) == (180.0, 360.0)
assert arc.center[1] == pytest.approx(1.75, abs=EPS)
mirrored = [
p for p in weld_symbol_prims(kind="u", side="other", height=3.5) if isinstance(p, Arc)
][0]
assert (mirrored.start_deg, mirrored.end_deg) == (0.0, 180.0)
assert mirrored.center[1] == pytest.approx(-1.4 - 1.75, abs=EPS)
async def test_size_is_written_before_the_symbol_and_length_and_pitch_after_it():
prims = weld_symbol_prims(kind="fillet", size=5, length=50, pitch=100, height=3.5)
texts = [p for p in prims if isinstance(p, Text)]
assert [t.text for t in texts] == ["a5", "50 (100)"]
triangle = [p for p in prims if isinstance(p, Poly) and p.closed][0]
x_symbol = triangle.points[0][0]
assert texts[0].at[0] < x_symbol
assert texts[1].at[0] > x_symbol + 3.5
# A string size is written verbatim, so z-notation stays available.
verbatim = weld_symbol_prims(kind="fillet", size="z7", height=3.5)
assert [p.text for p in verbatim if isinstance(p, Text)] == ["z7"]
with pytest.raises(ValueError) as excinfo:
weld_symbol_prims(kind="fillet", pitch=100, height=3.5)
assert "pitch needs length" in str(excinfo.value)
async def test_field_weld_flag_and_all_around_circle_sit_on_the_kink():
prims = weld_symbol_prims(kind="fillet", field_weld=True, all_around=True, height=3.5)
pole = [p for p in prims if isinstance(p, Line) and p.p1 == (0.0, 0.0) and p.p2[0] == 0.0][0]
assert pole.p2 == pytest.approx((0.0, 1.4 * 3.5), abs=EPS)
flag = [p for p in prims if isinstance(p, Poly) and p.closed and p.points[0][0] == 0.0][0]
assert _flat(flag.points) == pytest.approx(
(
0.0,
1.4 * 3.5,
0.7 * 3.5,
1.4 * 3.5 - 0.25 * 3.5,
0.0,
1.4 * 3.5 - 0.5 * 3.5,
),
abs=EPS,
)
circle = [p for p in prims if isinstance(p, Circle)][0]
assert circle.center == (0.0, 0.0)
assert circle.radius == pytest.approx(0.4 * 3.5, abs=EPS)
async def test_a_process_reference_gets_the_tail_fork_at_the_far_end():
prims = weld_symbol_prims(kind="fillet", process="ISO 4063 - 135", height=3.5)
reference = prims[0]
length = reference.p2[0]
fork = [
p for p in prims if isinstance(p, Line) and p.p1 == pytest.approx((length, 0.0), abs=EPS)
]
assert len(fork) == 2
assert fork[0].p2 == pytest.approx((length + 0.8 * 3.5, 0.5 * 3.5), abs=EPS)
assert fork[1].p2 == pytest.approx((length + 0.8 * 3.5, -0.5 * 3.5), abs=EPS)
tail_text = [p for p in prims if isinstance(p, Text)][-1]
assert tail_text.text == "ISO 4063 - 135"
assert tail_text.at[0] == pytest.approx(length + 3.5, abs=EPS)
# ── the async layer: primitives -> a real ezdxf document ────────────────────
from engineering.mech.annotate import ( # noqa: E402 - grouped with its own tests
draw_annotation_prims,
draw_surface_texture,
draw_weld_symbol,
leader_prims,
)
from engineering.mech.primitives import ROLE_LAYER # noqa: E402
class _StrictLayerBackend:
"""A backend that refuses an entity on a layer the drawing does not have.
MEASURED on AutoCAD 2026: `Documents.Add()` gives a layer table of `['0']`,
and `entity.Layer = "DIM"` on it raises
`AutoCAD COM error (-0x7ffdfff7) ('Key not found')` - live ActiveX never
auto-creates the layer, while `EzdxfBackend._apply_attrs` does. This fake
models that measured refusal (and only it: it has no member the real
`AutoCADBackend` lacks), so the headless suite fails the way the live
engine fails. Before the fix it recorded one LINE and then raised; the
drawing kept that orphan, which is the partial write the fix removes.
"""
def __init__(self, layers=("0",)):
self.layers = {name.lower(): name for name in layers}
self.calls: list[tuple] = []
self._next = 0
async def layer_list(self):
self.calls.append(("layer_list",))
return [types.SimpleNamespace(name=name) for name in self.layers.values()]
async def layer_create(self, name, color=7, linetype="Continuous", lineweight=-3):
self.calls.append(("layer_create", name, color, linetype, lineweight))
self.layers[name.lower()] = name
return types.SimpleNamespace(name=name)
def _entity(self, kind, layer):
if layer is not None and layer.lower() not in self.layers:
raise RuntimeError(f"AutoCAD COM error (-0x7ffdfff7) ('Key not found'): {layer}")
self.calls.append((kind, layer))
self._next += 1
return types.SimpleNamespace(handle=f"{self._next:X}")
async def entity_create_line(self, x1, y1, x2, y2, z1=0.0, z2=0.0, layer=None, **kw):
return self._entity("LINE", layer)
async def entity_create_circle(self, cx, cy, radius, layer=None, **kw):
return self._entity("CIRCLE", layer)
async def entity_create_arc(self, cx, cy, radius, start_angle, end_angle, layer=None, **kw):
return self._entity("ARC", layer)
async def entity_create_polyline(self, points, closed=False, layer=None, **kw):
return self._entity("LWPOLYLINE", layer)
async def entity_create_text(self, text, x, y, height=2.5, rotation=0.0, layer=None, **kw):
return self._entity("TEXT", layer)
async def test_every_target_layer_is_created_before_the_first_entity():
backend = _StrictLayerBackend()
result = await draw_surface_texture(
backend, at=(0.0, 0.0), ra=3.2, machining="required", lay="perpendicular", allowance=0.5
)
assert result["ok"] is True and result["count"] == 6
assert result["layers_created"] == [ROLE_LAYER["dim"], ROLE_LAYER["text"]]
# Nothing is drawn until every layer the symbol needs exists.
first_entity = next(
i
for i, call in enumerate(backend.calls)
if call[0] != "layer_list" and call[0] != "layer_create"
)
assert {call[1] for call in backend.calls[:first_entity] if call[0] == "layer_create"} == {
ROLE_LAYER["dim"],
ROLE_LAYER["text"],
}
# Created with their ENGINEERING_LAYERS definition, not a bare default.
dim_create = next(c for c in backend.calls if c[0] == "layer_create" and c[1] == "DIM")
assert dim_create[2:] == (2, "Continuous", 0.18)
async def test_a_drawing_that_already_has_the_layers_is_not_rewritten():
backend = _StrictLayerBackend(layers=("0", "dim", "TEXT"))
result = await draw_weld_symbol(
backend, at=(0.0, 0.0), kind="fillet", size=5, length=50, leader_to=(-10.0, -10.0)
)
assert result["layers_created"] == []
assert not [c for c in backend.calls if c[0] == "layer_create"]
async def _types_and_layers(backend, handles):
rows = []
for handle in handles:
info = await backend.entity_get(handle)
rows.append((info.type, info.layer))
return rows
async def test_draw_annotation_prims_puts_every_role_on_its_own_layer(backend):
prims = (
Line((0.0, 0.0), (10.0, 0.0), "dim"),
Circle((5.0, 5.0), 2.0, "dim"),
Text((0.0, 12.0), "Ra 3.2", 3.5),
)
result = await draw_annotation_prims(backend, prims, at=(100.0, 50.0))
assert result["ok"] is True and result["count"] == 3
assert await _types_and_layers(backend, result["handles"]) == [
("LINE", ROLE_LAYER["dim"]),
("CIRCLE", ROLE_LAYER["dim"]),
("TEXT", ROLE_LAYER["text"]),
]
line = await backend.entity_get(result["handles"][0])
assert line.properties["start"] == pytest.approx([100.0, 50.0], abs=EPS)
assert line.properties["end"] == pytest.approx([110.0, 50.0], abs=EPS)
circle = await backend.entity_get(result["handles"][1])
assert circle.properties["center"] == pytest.approx([105.0, 55.0], abs=EPS)
async def test_an_explicit_layer_overrides_every_role(backend):
result = await draw_annotation_prims(
backend,
(Line((0.0, 0.0), (1.0, 0.0), "dim"), Text((0.0, 2.0), "x", 3.5)),
layer="ANNOT",
)
assert {layer for _type, layer in await _types_and_layers(backend, result["handles"])} == {
"ANNOT"
}
async def test_leader_prims_reuse_the_repository_arrowhead_proportions():
line, head = leader_prims((0.0, 0.0), (10.0, 0.0), arrow_size=2.5)
assert isinstance(line, Line) and isinstance(head, Poly) and head.closed
assert _flat(head.points) == pytest.approx(
(0.0, 0.0, 2.5, 2.5 * 0.35, 2.5, -2.5 * 0.35), abs=EPS
)
with pytest.raises(ValueError) as excinfo:
leader_prims((1.0, 1.0), (1.0, 1.0))
assert "distinct" in str(excinfo.value)
async def test_draw_surface_texture_draws_the_symbol_and_its_leader(backend):
result = await draw_surface_texture(
backend, at=(40.0, 60.0), leader_to=(20.0, 50.0), ra=3.2, machining="required"
)
assert result["ok"] is True and result["standard"] == "ISO 21920-1"
rows = await _types_and_layers(backend, result["handles"])
# 3 vee/bar lines + extension line + the Ra text + leader line + arrowhead.
assert [t for t, _ in rows] == [
"LINE",
"LINE",
"LINE",
"LINE",
"TEXT",
"LINE",
"LWPOLYLINE",
]
apex = await backend.entity_get(result["handles"][0])
assert apex.properties["start"] == pytest.approx([40.0, 60.0], abs=EPS)
leader = await backend.entity_get(result["handles"][5])
assert leader.properties["start"] == pytest.approx([20.0, 50.0], abs=EPS)
assert leader.properties["end"] == pytest.approx([40.0, 60.0], abs=EPS)
async def test_draw_weld_symbol_refuses_an_unknown_kind_before_touching_the_drawing(backend):
before = (await backend.analysis_stats())["total_entities"]
with pytest.raises(ValueError):
await draw_weld_symbol(backend, at=(0.0, 0.0), kind="seam")
assert (await backend.analysis_stats())["total_entities"] == before
async def test_draw_weld_symbol_places_the_kink_at_the_given_point(backend):
result = await draw_weld_symbol(
backend, at=(10.0, 20.0), kind="fillet", size=5, length=50, leader_to=(0.0, 10.0)
)
assert result["ok"] is True
reference = await backend.entity_get(result["handles"][0])
assert reference.properties["start"] == pytest.approx([10.0, 20.0], abs=EPS)
assert reference.properties["end"][1] == pytest.approx(20.0, abs=EPS)
texts = []
for handle in result["handles"]:
info = await backend.entity_get(handle)
if info.type == "TEXT":
texts.append(info.properties["text"])
assert texts == ["a5", "50"]
+495
View File
@@ -0,0 +1,495 @@
"""Centre marks (ISO 128-23), the cutting-plane line (ISO 128-40) and material hatch.
Pure geometry asserted as emitted primitive tuples; the async layer against a
real ezdxf document through the `backend` fixture.
"""
from __future__ import annotations
import math
import pytest
from engineering.mech.marks import (
CENTRE_STYLES,
centre_mark_prims,
)
from engineering.mech.primitives import Line
pytestmark = pytest.mark.asyncio
EPS = 1e-9
def _flat(points):
"""Flatten a point sequence: pytest.approx refuses nested structures."""
return tuple(float(value) for point in points for value in point)
async def test_a_centre_mark_is_a_small_cross_on_the_centre_role():
prims = centre_mark_prims((10.0, 20.0), 5.0, style="mark", extension=3.0)
assert len(prims) == 2
horizontal, vertical = prims
assert isinstance(horizontal, Line) and isinstance(vertical, Line)
assert horizontal.p1 == pytest.approx((7.0, 20.0), abs=EPS)
assert horizontal.p2 == pytest.approx((13.0, 20.0), abs=EPS)
assert vertical.p1 == pytest.approx((10.0, 17.0), abs=EPS)
assert vertical.p2 == pytest.approx((10.0, 23.0), abs=EPS)
assert horizontal.role == "center" and vertical.role == "center"
async def test_centre_lines_cross_the_circle_and_overrun_it_by_the_extension():
prims = centre_mark_prims((10.0, 20.0), 5.0, style="lines", extension=3.0)
horizontal, vertical = prims
assert horizontal.p1 == pytest.approx((2.0, 20.0), abs=EPS)
assert horizontal.p2 == pytest.approx((18.0, 20.0), abs=EPS)
assert vertical.p1 == pytest.approx((10.0, 12.0), abs=EPS)
assert vertical.p2 == pytest.approx((10.0, 28.0), abs=EPS)
assert horizontal.role == "center" and vertical.role == "center"
async def test_the_style_enum_is_closed_and_a_bad_radius_is_refused():
assert CENTRE_STYLES == ("mark", "lines")
with pytest.raises(ValueError) as excinfo:
centre_mark_prims((0.0, 0.0), 5.0, style="cross")
assert "mark, lines" in str(excinfo.value)
with pytest.raises(ValueError) as excinfo:
centre_mark_prims((0.0, 0.0), 0.0, style="lines")
assert "radius" in str(excinfo.value)
with pytest.raises(ValueError) as excinfo:
centre_mark_prims((0.0, 0.0), 5.0, extension=0.0)
assert "extension" in str(excinfo.value)
# ── ISO 128-40 cutting-plane line ───────────────────────────────────────────
from engineering.mech.marks import ( # noqa: E402 - grouped with its own tests
SECTION_STYLES,
section_line_prims,
section_plane,
)
from engineering.mech.primitives import Poly, Text # noqa: E402
async def test_the_cutting_plane_line_is_thick_at_the_ends_and_thin_between():
prims = section_line_prims(
(0.0, 0.0), (100.0, 0.0), label="A", direction=(0.0, -1.0), height=5.0
)
head_end, tail_end, middle = prims[0], prims[1], prims[2]
assert head_end.p1 == pytest.approx((0.0, 0.0), abs=EPS)
assert head_end.p2 == pytest.approx((10.0, 0.0), abs=EPS)
assert head_end.role == "visible"
assert tail_end.p1 == pytest.approx((90.0, 0.0), abs=EPS)
assert tail_end.p2 == pytest.approx((100.0, 0.0), abs=EPS)
assert tail_end.role == "visible"
assert middle.p1 == pytest.approx((10.0, 0.0), abs=EPS)
assert middle.p2 == pytest.approx((90.0, 0.0), abs=EPS)
assert middle.role == "center"
async def test_a_short_cutting_plane_is_one_wide_segment_with_no_thin_middle():
prims = section_line_prims((0.0, 0.0), (15.0, 0.0), height=5.0)
assert prims[0].p1 == pytest.approx((0.0, 0.0), abs=EPS)
assert prims[0].p2 == pytest.approx((15.0, 0.0), abs=EPS)
assert prims[0].role == "visible"
assert not [p for p in prims if isinstance(p, Line) and p.role == "center"]
async def test_each_end_gets_an_arrow_pointing_the_viewing_direction_and_the_same_letter():
prims = section_line_prims(
(0.0, 0.0), (100.0, 0.0), label="B", direction=(0.0, -1.0), height=5.0
)
shafts = [p for p in prims if isinstance(p, Line) and p.p1[0] == p.p2[0]]
assert len(shafts) == 2
# The shaft comes from outside and the head's tip sits on the line end.
assert shafts[0].p1 == pytest.approx((0.0, 10.0), abs=EPS)
assert shafts[0].p2 == pytest.approx((0.0, 0.0), abs=EPS)
assert shafts[1].p1 == pytest.approx((100.0, 10.0), abs=EPS)
assert shafts[1].p2 == pytest.approx((100.0, 0.0), abs=EPS)
heads = [p for p in prims if isinstance(p, Poly)]
assert len(heads) == 2 and all(h.closed for h in heads)
assert _flat(heads[0].points) == pytest.approx((0.0, 0.0, 1.4, 4.0, -1.4, 4.0), abs=EPS)
labels = [p for p in prims if isinstance(p, Text)]
assert [t.text for t in labels] == ["B", "B"]
assert labels[0].at == pytest.approx((0.0, 13.5), abs=EPS)
assert labels[1].at == pytest.approx((100.0, 13.5), abs=EPS)
assert all(t.height == 5.0 and t.role == "text" for t in labels)
async def test_a_viewing_direction_parallel_to_the_plane_is_refused():
with pytest.raises(ValueError) as excinfo:
section_line_prims((0.0, 0.0), (100.0, 0.0), direction=(1.0, 0.0))
assert "perpendicular" in str(excinfo.value)
with pytest.raises(ValueError) as excinfo:
section_line_prims((0.0, 0.0), (0.0, 0.0), direction=(0.0, -1.0))
assert "distinct" in str(excinfo.value)
async def test_section_plane_is_exactly_the_dict_the_view_engine_consumes():
plane = section_plane((0.0, 0.0), (100.0, 0.0), label="A", direction=(0.0, -2.0))
assert plane == {
"p1": (0.0, 0.0),
"p2": (100.0, 0.0),
"label": "A",
"direction": (0.0, -1.0),
"style": "full",
}
assert list(plane) == ["p1", "p2", "label", "direction", "style"]
assert SECTION_STYLES == ("full", "half", "offset", "revolved")
with pytest.raises(ValueError) as excinfo:
section_plane((0.0, 0.0), (10.0, 0.0), style="broken")
assert "full, half, offset, revolved" in str(excinfo.value)
with pytest.raises(ValueError) as excinfo:
section_plane((0.0, 0.0), (10.0, 0.0), label="")
assert "label" in str(excinfo.value)
# ── the async layer ─────────────────────────────────────────────────────────
from engineering.mech.marks import ( # noqa: E402 - grouped with its own tests
draw_centre_marks,
draw_material_hatch,
draw_section_line,
)
from engineering.mech.primitives import ROLE_LAYER # noqa: E402
from engineering.mech.standards.materials import MATERIALS, hatch_for # noqa: E402
async def test_centre_marks_read_the_circles_real_radius_from_the_drawing(backend):
small = await backend.entity_create_circle(0.0, 0.0, 4.0, "GEOMETRY")
large = await backend.entity_create_circle(50.0, 0.0, 12.0, "GEOMETRY")
result = await draw_centre_marks(
backend, handles=[small.handle, large.handle], style="lines", extension=2.0
)
assert result["ok"] is True and result["count"] == 4
assert result["sources"] == [
{"handle": small.handle, "center": [0.0, 0.0], "radius": 4.0},
{"handle": large.handle, "center": [50.0, 0.0], "radius": 12.0},
]
first = await backend.entity_get(result["handles"][0])
assert first.layer == ROLE_LAYER["center"]
assert first.properties["start"] == pytest.approx([-6.0, 0.0], abs=EPS)
assert first.properties["end"] == pytest.approx([6.0, 0.0], abs=EPS)
third = await backend.entity_get(result["handles"][2])
assert third.properties["start"] == pytest.approx([36.0, 0.0], abs=EPS)
assert third.properties["end"] == pytest.approx([64.0, 0.0], abs=EPS)
async def test_centre_marks_refuse_a_handle_that_is_not_a_circular_feature(backend):
line = await backend.entity_create_line(0.0, 0.0, 10.0, 0.0, 0.0, 0.0, "GEOMETRY")
before = (await backend.analysis_stats())["total_entities"]
with pytest.raises(ValueError) as excinfo:
await draw_centre_marks(backend, handles=[line.handle])
assert line.handle in str(excinfo.value) and "LINE" in str(excinfo.value)
assert (await backend.analysis_stats())["total_entities"] == before
async def test_explicit_centres_are_accepted_and_lines_need_a_radius(backend):
result = await draw_centre_marks(backend, centers=[[5.0, 5.0]], style="mark", extension=3.0)
assert result["count"] == 2 and result["sources"] == [
{"handle": None, "center": [5.0, 5.0], "radius": None}
]
with pytest.raises(ValueError) as excinfo:
await draw_centre_marks(backend, centers=[[5.0, 5.0]], style="lines")
assert "centers[0]" in str(excinfo.value)
with pytest.raises(ValueError) as excinfo:
await draw_centre_marks(backend)
assert "handles" in str(excinfo.value) and "centers" in str(excinfo.value)
async def test_draw_section_line_returns_the_plane_dict_the_view_engine_consumes(backend):
result = await draw_section_line(
backend, (0.0, 0.0), (100.0, 0.0), label="A", direction=(0.0, -1.0), height=5.0
)
assert result["ok"] is True
assert result["plane"] == {
"p1": (0.0, 0.0),
"p2": (100.0, 0.0),
"label": "A",
"direction": (0.0, -1.0),
"style": "full",
}
assert list(result["plane"]) == ["p1", "p2", "label", "direction", "style"]
layers = set()
for handle in result["handles"]:
layers.add((await backend.entity_get(handle)).layer)
assert layers == {ROLE_LAYER["visible"], ROLE_LAYER["center"], ROLE_LAYER["text"]}
async def test_material_hatch_uses_the_iso_128_50_map_and_scales_on_top_of_it(backend):
steel = hatch_for("steel")
result = await draw_material_hatch(
backend,
boundary=[[0.0, 0.0], [40.0, 0.0], [40.0, 20.0], [0.0, 20.0]],
material="steel",
scale=2.0,
)
assert result["ok"] is True and result["material"] == "steel"
assert result["pattern"] == steel["pattern"]
assert result["scale"] == pytest.approx(steel["scale"] * 2.0, abs=EPS)
assert result["angle"] == pytest.approx(steel["angle"], abs=EPS)
hatch = backend._doc.entitydb.get(result["handle"])
assert hatch.dxftype() == "HATCH"
assert hatch.dxf.pattern_name == steel["pattern"]
assert hatch.dxf.pattern_scale == pytest.approx(steel["scale"] * 2.0, abs=EPS)
assert hatch.dxf.pattern_angle == pytest.approx(steel["angle"], abs=EPS)
assert (await backend.entity_get(result["handle"])).layer == ROLE_LAYER["hatch"]
# An explicit angle overrides the material's own.
turned = await draw_material_hatch(
backend,
boundary=[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]],
material="steel",
angle=90.0,
)
assert turned["angle"] == pytest.approx(90.0, abs=EPS)
async def test_an_unknown_material_is_refused_with_the_list_before_any_write(backend):
assert "steel" in MATERIALS
before = (await backend.analysis_stats())["total_entities"]
with pytest.raises(ValueError):
await draw_material_hatch(
backend,
boundary=[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]],
material="unobtainium",
)
assert (await backend.analysis_stats())["total_entities"] == before
async def test_a_boundary_that_is_not_a_closed_polygon_is_refused(backend):
with pytest.raises(ValueError) as excinfo:
await draw_material_hatch(backend, boundary=[[0.0, 0.0], [10.0, 0.0]], material="steel")
assert "at least three" in str(excinfo.value)
with pytest.raises(ValueError) as excinfo:
await draw_material_hatch(backend, material="steel")
assert "boundary" in str(excinfo.value) and "handles" in str(excinfo.value)
# ── the HATCH layer must exist before the hatch is created (live) ────────────
import types # noqa: E402
class _StrictLayerHatchBackend:
"""A backend that refuses a hatch on a layer the drawing does not have.
MEASURED on AutoCAD 2026 (probe: GetActiveObject, `Documents.Add()`, a
scratch document whose layer table is `['0']`, `doc.Close(False)` in a
`finally:`): `msp.AddHatch(0, "ANSI31", False)` + `AppendOuterLoop` +
`Evaluate()` leaves the AcDbHatch in model space, and the very next
statement `hatch.Layer = "HATCH"` raises
`com_error (-2147352567, ..., 'Key not found', ..., -2145386476)`. Model
space then still reports `[('AcDbHatch', '0')]` and the layer table is
still `['0']` - the refusal leaves an orphan hatch behind. After
`doc.Layers.Add("HATCH")` the same assignment succeeds. `ComBackend`
reaches that assignment through `_apply_entity_attrs`, which is the one
line `entity.Layer = layer`, so this is the hatch spelling of the failure
`ensure_annotation_layers` already fixed for the annotation symbols.
The fake models that measured refusal and nothing else: every member below
exists on the real `AutoCADBackend` with this signature.
"""
def __init__(self, layers=("0",)):
self.layers = {name.lower(): name for name in layers}
self.calls: list[tuple] = []
async def layer_list(self):
self.calls.append(("layer_list",))
return [types.SimpleNamespace(name=name) for name in self.layers.values()]
async def layer_create(self, name, color=7, linetype="Continuous", lineweight=-3):
self.calls.append(("layer_create", name, color, linetype, lineweight))
self.layers[name.lower()] = name
return types.SimpleNamespace(name=name)
async def entity_create_hatch(
self, pattern, boundary_points, scale=1.0, angle=0.0, layer=None, color=None
):
if layer is not None and layer.lower() not in self.layers:
raise RuntimeError(f"AutoCAD COM error (-0x7ffdfff7) ('Key not found'): {layer}")
self.calls.append(("HATCH", layer, pattern))
return types.SimpleNamespace(handle="2F")
async def test_the_hatch_layer_is_created_before_the_hatch_is():
backend = _StrictLayerHatchBackend()
result = await draw_material_hatch(
backend,
boundary=[[0.0, 0.0], [40.0, 0.0], [40.0, 20.0], [0.0, 20.0]],
material="steel",
)
assert result["ok"] is True and result["handle"] == "2F"
assert result["layers_created"] == [ROLE_LAYER["hatch"]]
# The layer is created strictly before the hatch entity.
kinds = [call[0] for call in backend.calls]
assert kinds.index("layer_create") < kinds.index("HATCH")
# ENGINEERING_LAYERS' own definition, not a bare default.
create = next(c for c in backend.calls if c[0] == "layer_create")
assert create[1] == ROLE_LAYER["hatch"]
async def test_a_drawing_that_already_has_the_hatch_layer_is_not_rewritten():
backend = _StrictLayerHatchBackend(layers=("0", "hatch"))
result = await draw_material_hatch(
backend, boundary=[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]], material="steel"
)
assert result["layers_created"] == []
assert not [c for c in backend.calls if c[0] == "layer_create"]
# ── the cut face is the face, not its chords (regression) ───────────────────
from engineering.layers import ENGINEERING_LAYERS # noqa: E402
from engineering.mech import marks as _marks # noqa: E402
from engineering.mech.marks import ( # noqa: E402
HATCH_FLATTEN_SAGITTA,
_flatten_bulge,
_polygon_area,
)
async def test_a_bulged_segment_is_expanded_onto_its_real_arc():
"""A bulge of 1 is a 180 degree arc; its chords must sit on that circle."""
interior = _flatten_bulge((40.0, 10.0), (0.0, 10.0), 1.0)
assert len(interior) >= 40 # a 0.01 sagitta on r=20 needs ~50 chords
for x, y in interior:
assert math.hypot(x - 20.0, y - 10.0) == pytest.approx(20.0, abs=1e-9)
assert y >= 10.0 # a positive bulge sweeps counter-clockwise, i.e. above
# Every chord's sagitta stays inside the declared tolerance.
walk = [(40.0, 10.0), *interior, (0.0, 10.0)]
for (x1, y1), (x2, y2) in zip(walk, walk[1:], strict=False):
chord = math.hypot(x2 - x1, y2 - y1)
sagitta = 20.0 - math.sqrt(max(0.0, 400.0 - (chord / 2.0) ** 2))
assert sagitta <= HATCH_FLATTEN_SAGITTA + 1e-12
assert _flatten_bulge((0.0, 0.0), (10.0, 0.0), 0.0) == []
async def _arc_topped_face(backend):
"""A 40x10 box closed by a 180 degree arc: 400 straight + 628.3 under the arc."""
lower = await backend.entity_create_line(0.0, 0.0, 40.0, 0.0, 0.0, 0.0, "GEOMETRY")
right = await backend.entity_create_line(40.0, 0.0, 40.0, 10.0, 0.0, 0.0, "GEOMETRY")
arc = await backend.entity_create_arc(20.0, 10.0, 20.0, 0.0, 180.0, "GEOMETRY")
left = await backend.entity_create_line(0.0, 10.0, 0.0, 0.0, 0.0, 0.0, "GEOMETRY")
return [lower.handle, right.handle, arc.handle, left.handle]
async def _rectangle(backend):
handles = []
for start, end in (
((0.0, 0.0), (40.0, 0.0)),
((40.0, 0.0), (40.0, 10.0)),
((40.0, 10.0), (0.0, 10.0)),
((0.0, 10.0), (0.0, 0.0)),
):
line = await backend.entity_create_line(start[0], start[1], end[0], end[1], 0.0, 0.0)
handles.append(line.handle)
return handles
async def test_an_arc_bounded_cut_face_is_hatched_on_the_arc_not_its_chord(backend):
"""The failure README.md names: reading `points` drops the parallel `bulges`.
Reading only `traced.properties["points"]` hatched the chord polygon -
400.0 of a 1028.3 cut face, 61% of it silently unhatched. The hatch must
now measure the face, and the payload must state its own accuracy.
"""
handles = await _arc_topped_face(backend)
exact = (await backend.boundary_from_entities(handles))["area"]
assert exact == pytest.approx(400.0 + 200.0 * math.pi, abs=1e-6)
result = await draw_material_hatch(backend, handles=handles, material="steel")
assert result["ok"] is True
assert result["curved_edges"] == 1
assert result["points"] > 4 # not the four-vertex chord polygon
assert result["boundary_area"] == pytest.approx(exact, abs=1e-6)
# Inside the declared flattening tolerance of the real face, not 61% short.
assert result["area"] == pytest.approx(exact, rel=1e-3)
assert result["accuracy"] == "flatten_tolerance"
assert result["flatten_tolerance"] == HATCH_FLATTEN_SAGITTA
# What the drawing holds agrees with what the payload claims.
measured = await backend.entity_measure(result["handle"])
assert measured["area"] == pytest.approx(result["area"], abs=1e-6)
async def test_a_straight_sided_face_is_still_reported_exact(backend):
handles = await _rectangle(backend)
result = await draw_material_hatch(backend, handles=handles, material="steel")
assert result["accuracy"] == "exact"
assert result["flatten_tolerance"] is None
assert result["curved_edges"] == 0
assert result["area"] == pytest.approx(400.0, abs=EPS)
assert result["boundary_area"] == pytest.approx(400.0, abs=EPS)
assert result["points"] == 4
async def test_the_traced_outline_is_removed_and_reported_not_left_on_layer_0(backend):
"""`boundary_from_entities` *draws* its loop; hatch_material asked for a hatch."""
handles = await _rectangle(backend)
before = await backend.analysis_stats()
result = await draw_material_hatch(backend, handles=handles, material="steel")
after = await backend.analysis_stats()
# Exactly one new entity: the hatch. No LWPOLYLINE scaffolding survives.
assert after["total_entities"] == before["total_entities"] + 1
assert after["by_type"].get("LWPOLYLINE", 0) == 0
assert result["traced"]["removed"] is True
assert result["traced"]["handle"]
live = {entity.dxf.handle for entity in backend._doc.modelspace()}
assert result["traced"]["handle"] not in live
async def test_a_loop_that_encloses_no_area_is_refused_and_leaves_nothing_behind(backend):
"""Three collinear segments chain into a closed loop of area 0.0.
The handles branch had no such guard - the 'at least three points' refusal
only ever covered `boundary` - so it wrote a HATCH of area 0.0 and returned
ok. The traced outline must still be cleaned up on the refusal path.
"""
handles = []
for start, end in (
((0.0, 0.0), (10.0, 0.0)),
((10.0, 0.0), (20.0, 0.0)),
((20.0, 0.0), (0.0, 0.0)),
):
line = await backend.entity_create_line(start[0], start[1], end[0], end[1], 0.0, 0.0)
handles.append(line.handle)
before = await backend.analysis_stats()
with pytest.raises(ValueError) as excinfo:
await draw_material_hatch(backend, handles=handles, material="steel")
assert "no area" in str(excinfo.value)
after = await backend.analysis_stats()
assert after["total_entities"] == before["total_entities"]
assert after["by_type"].get("LWPOLYLINE", 0) == 0
assert after["by_type"].get("HATCH", 0) == 0
async def test_the_polygon_area_helper_is_orientation_blind():
square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)]
assert _polygon_area(square) == pytest.approx(100.0, abs=EPS)
assert _polygon_area(list(reversed(square))) == pytest.approx(100.0, abs=EPS)
# ── the SOURCE block may not quote a lineweight the layer set does not have ──
async def test_the_section_line_source_block_quotes_the_layer_set_it_draws_on():
"""A SOURCE block asserting 0.25 mm while CENTER is 0.18 mm is a fabricated
number in the one place this repository refuses to fabricate one."""
widths = {name: weight for name, _c, _lt, weight, _d in ENGINEERING_LAYERS}
wide, narrow = widths["GEOMETRY"], widths["CENTER"]
doc = _marks.__doc__
assert f"GEOMETRY is {wide:.2f} mm" in doc
assert f"CENTER is {narrow:.2f} mm" in doc
assert f"{wide / narrow:.2f}:1" in doc
async def test_the_cutting_plane_line_really_lands_on_those_two_lineweights(backend):
result = await draw_section_line(
backend, (0.0, 0.0), (100.0, 0.0), label="A", direction=(0.0, -1.0), height=5.0
)
widths = {layer.name: layer.lineweight for layer in await backend.layer_list()}
drawn = {(await backend.entity_get(handle)).layer for handle in result["handles"]}
assert {ROLE_LAYER["visible"], ROLE_LAYER["center"]} <= drawn
# lineweight is reported in 1/100 mm; the SOURCE block quotes millimetres.
assert widths[ROLE_LAYER["visible"]] == pytest.approx(50.0, abs=EPS)
assert widths[ROLE_LAYER["center"]] == pytest.approx(18.0, abs=EPS)
+8 -2
View File
@@ -7,7 +7,8 @@ essentials and stay in `core`, so `TOOL_PACKS=core` hides the environment
surface and nothing else.
v1.6 tracks B+G add the `mech` pack: SECTION 21's six part-model tools and
SECTION 22's three standard-parts tools.
SECTION 22's three standard-parts tools and SECTION 23's five annotation
symbols - 14 in all.
"""
from __future__ import annotations
@@ -91,6 +92,11 @@ MECH_TOOLS = {
"std_part_list",
"std_part_insert",
"std_feature_draw",
"surface_texture",
"weld_symbol",
"centre_marks",
"section_line",
"hatch_material",
}
LEAN_SETTINGS_ESSENTIALS = {
@@ -120,7 +126,7 @@ def test_pack_registry_names_real_tools_and_only_them():
assert server.PACK_TOOL_NAMES["mech"] == frozenset(MECH_TOOLS)
assert server.PACK_TOOL_NAMES["settings"] == frozenset(SETTINGS_TOOLS)
assert len(SETTINGS_TOOLS) == 22
assert len(MECH_TOOLS) == 9
assert len(MECH_TOOLS) == 14
assert not (server.PACK_TOOL_NAMES["settings"] & server.PACK_TOOL_NAMES["pid"])
assert not (server.PACK_TOOL_NAMES["mech"] & server.PACK_TOOL_NAMES["pid"])
assert not (server.PACK_TOOL_NAMES["mech"] & server.PACK_TOOL_NAMES["settings"])
+9 -3
View File
@@ -495,7 +495,7 @@ async def test_tool_groups_is_byte_identical_to_the_source_declared_grouping():
async def test_tool_group_sizes_are_unchanged():
"""Frozen snapshot of the surface (204 tools, 23 groups).
"""Frozen snapshot of the surface (209 tools, 23 groups).
Taken before @cad_tool landed at 131 tools; `batch` moved 2 -> 3 when
v1.5.0's `cad_batch` joined `entity_batch_create`/`entity_batch_modify`, and
@@ -540,6 +540,12 @@ async def test_tool_group_sizes_are_unchanged():
SECTION 22 added the standard-parts catalogue (`std_part_list` /
`std_part_insert` / `std_feature_draw`) to the same group, filed under
`mech` rather than under their secondary `query` / `create` tags.
SECTION 23 added the mechanical annotation symbols (`surface_texture` /
`weld_symbol` / `centre_marks` / `section_line` / `hatch_material`). They
are tagged `engineering` and `mech`; with `mech` ranked ahead of
`engineering` in `_GROUP_TAG_PRIORITY` they file under `mech` (14 in all,
exactly `PACK_TOOL_NAMES["mech"]`), and `engineering` stays at 10.
"""
sizes = {label: len(names) for label, names in (await server._tool_groups()).items()}
assert sizes == {
@@ -556,7 +562,7 @@ async def test_tool_group_sizes_are_unchanged():
"environment": 22,
"layers": 14,
"layouts": 12,
"mech": 9,
"mech": 14,
"page_setup": 6,
"pid": 9,
"premium": 12,
@@ -568,4 +574,4 @@ async def test_tool_group_sizes_are_unchanged():
"validation": 1,
"view": 4,
}
assert sum(sizes.values()) == 213
assert sum(sizes.values()) == 218