feat(task-3): the feature catalogue and its standards tables
engineering/mech/features.py is the nineteen typed features of spec section 5,
each answering the same four questions - is my placement legal, what do I
contribute to this view, to a cut face, and what dimension do I want - plus the
five structural hooks the view engine needs: removal_box, radial_edit,
corner_edit, outline_edit and omission. Pure arithmetic; a test asserts emitted
primitive tuples, not a screenshot.
Nothing restates a table the repository already owns: Keyway takes its width
and depth from engineering.keyway (DIN 6885-1) and is refused by name outside
that table's bore range; GearTeeth takes its outline from engineering.gear's
involute generator and is refused when its tip diameter disagrees with the
segment it sits on.
standards/threads.py transcribes ISO 261 Table 1 and Table 2 - first-choice
M1.6-M64 plus second-choice M14/M18/M22/M27/M33/M39/M45/M52/M60 - and computes
ISO 724's basic minor and pitch diameters from ISO 68-1's own formula, pinned
against the published values (M20x2.5 -> 17.294 / 18.376). The remaining
second-choice diameters, every third-choice diameter and fine pitches below M8
are not transcribed and are refused by name, never interpolated. The drawing
rules are ISO 6410: minor at 0.8 x major, a thread-length limit line across the
major, a three-quarter circle in the end view, and an internal thread whose
section hatch runs to the major diameter.
standards/{undercuts,rings,centres,orings}.py ship DIN 509, DIN 471/472,
DIN 332-1 and ISO 3601-2 with their SOURCE, their row-key contract and an
**empty** row set. That is deliberate and is the track's table rule taken at its
word: a row that could not be verified against the standard is not shipped,
because a wrong relief depth or groove diameter is a wrong workshop drawing
while a narrow table is merely narrow. Every lookup is refused by name and says
what to transcribe and where; the features accept explicit dimensions so the
geometry is fully exercised, and DIN 332 form R is refused outright rather than
approximated by the straight 60 degree cone of forms A and B. Filling any of
these tables later is a data edit with no code change, and a shape test already
fails any row that lands without a column the drawing code reads.
Three deviations from the plan text, each measured rather than assumed
(the merge agent should keep exactly one copy of the first):
1. standards/__init__.py (Task 1, on main) refused a registration with no rows
("registered no rows"), which would have made the four not-transcribed
tables impossible to register at all - and registering them is what keeps a
later transcription a data edit. register() now refuses a rows argument that
is not a dict and accepts an empty one; two tests in tests/test_mech_standards.py
pin both halves. No existing test asserted the old refusal.
2. ORingGroove's housing kind is carried by `housing`, not `kind`. A dataclass
field named `kind` overwrites the discriminator _Base.to_dict() writes:
measured, Probe(id='og').to_dict() == {'kind': 'radial', 'id': 'og'}, so
feature_from_dict would then refuse its own payload as an unknown feature
kind and no part carrying an O-ring groove could round-trip through XDATA.
A new test pins the round trip.
3. The plan's end-chamfer test asserted after == (75.0, 10.0) - the corner
itself - and before == (72.0, 8.268), a point on neither surface. The real
edit is before (72.0, 10.0) on the cylinder and after (75.0, 8.268) on the
end face, consistent with the 45-degree start chamfer the same file pins.
The test was corrected to the measured geometry; the behaviour is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
co-authored by
Claude Opus 5
parent
04d269318a
commit
ac6c4075ba
@@ -13,6 +13,12 @@ wrong row ships a wrong workshop drawing; a narrow table is merely narrow.
|
||||
variants at the same size (DIN 509 forms E and F, DIN 332 forms A/B/R). ``**kw``
|
||||
selects among those variants and is refused when it selects none or more than
|
||||
one — an ambiguous lookup never quietly picks the first row.
|
||||
|
||||
``rows`` may be **empty**: that is a table whose structure, coverage and SOURCE
|
||||
ship before any row could be verified against the standard (DIN 509, DIN 471,
|
||||
DIN 472, DIN 332-1 and ISO 3601-2 in this build). Registering it keeps the
|
||||
later transcription a data edit, and every lookup against it is refused by
|
||||
name — which is exactly the table rule, not an exception to it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -62,8 +68,11 @@ def register(standard: str, rows: dict, coverage: Coverage, source: str) -> None
|
||||
raise ValueError("standards.register: standard must be a non-empty name")
|
||||
if standard in _REGISTRY:
|
||||
raise ValueError(f"standards.register: {standard!r} is already registered")
|
||||
if not rows:
|
||||
raise ValueError(f"standards.register: {standard!r} registered no rows")
|
||||
if not isinstance(rows, dict):
|
||||
raise ValueError(
|
||||
f"standards.register: {standard!r} must register a rows dict; an empty one is "
|
||||
"the table whose structure ships before its rows are transcribed"
|
||||
)
|
||||
if not isinstance(source, str) or "table" not in source.lower():
|
||||
raise ValueError(
|
||||
f"standards.register: {standard!r} needs a SOURCE naming the standard, its "
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""DIN 332-1 centre holes, forms A and B.
|
||||
|
||||
SOURCE
|
||||
------
|
||||
DIN 332-1:1986-04, *Centre holes 60 degrees - Forms R, A and B*, Table 1.
|
||||
|
||||
COVERAGE
|
||||
--------
|
||||
**No rows are transcribed in this build** (same rule as ``undercuts.py``).
|
||||
Form R is not drawn by this build at all: its profile is an arc form whose
|
||||
radius is not transcribed here, and it is refused outright rather than
|
||||
approximated by the straight 60 degree cone of forms A and B.
|
||||
|
||||
Extending is a data edit: fill ``ROWS_A`` / ``ROWS_B`` with
|
||||
``{pilot_diameter_mm: {"d1": ..., "d2": ..., "d3": ..., "t": ...}}`` (``d3``
|
||||
only for form B's protective 120 degree countersink) and widen the coverage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from engineering.mech.standards import Coverage, register
|
||||
|
||||
SOURCE = "DIN 332-1:1986-04, Table 1 (forms A and B) - NOT TRANSCRIBED in this build"
|
||||
|
||||
#: The keys a DIN 332 row must carry; form A rows may omit "d3".
|
||||
CENTRE_ROW_KEYS: tuple[str, ...] = ("d1", "d2", "d3", "t")
|
||||
|
||||
FORMS: tuple[str, ...] = ("A", "B")
|
||||
|
||||
ROWS_A: dict[float, dict] = {}
|
||||
ROWS_B: dict[float, dict] = {}
|
||||
|
||||
COVERAGE_A = Coverage(0.0, 0.0, "mm")
|
||||
COVERAGE_B = Coverage(0.0, 0.0, "mm")
|
||||
|
||||
register("DIN 332-A", ROWS_A, COVERAGE_A, SOURCE)
|
||||
register("DIN 332-B", ROWS_B, COVERAGE_B, SOURCE)
|
||||
|
||||
_ROWS = {"A": ROWS_A, "B": ROWS_B}
|
||||
|
||||
|
||||
def centre_hole_dims(form: str, size: float) -> dict:
|
||||
"""``{"d1","d2","d3","t"}`` for a pilot diameter, or a named refusal."""
|
||||
key = str(form or "").strip().upper()
|
||||
if key == "R":
|
||||
raise ValueError(
|
||||
"DIN 332 form R: this build draws forms A and B only. Form R's arc profile "
|
||||
"is not transcribed here and is refused rather than approximated by the "
|
||||
"straight 60 degree cone."
|
||||
)
|
||||
if key not in FORMS:
|
||||
raise ValueError(f"DIN 332: expected form A or B, got {form!r}.")
|
||||
rows = _ROWS[key]
|
||||
if not rows:
|
||||
raise ValueError(
|
||||
f"DIN 332 form {key}: no rows are transcribed in this build, so no centre hole "
|
||||
f"can be looked up for d1 = {float(size):g} mm. Pass d1, d2 (and d3 for form B) "
|
||||
f"and t explicitly, or transcribe {SOURCE.split(' - ')[0]} into "
|
||||
"engineering/mech/standards/centres.py. Nothing is interpolated."
|
||||
)
|
||||
pilot = float(size)
|
||||
if pilot in rows:
|
||||
row = rows[pilot]
|
||||
return {name: (float(row[name]) if name in row else None) for name in CENTRE_ROW_KEYS}
|
||||
raise ValueError(
|
||||
f"DIN 332 form {key}: d1 = {pilot:g} mm has no transcribed row (sizes: {sorted(rows)})."
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
"""ISO 3601-2 O-ring housing (groove) dimensions.
|
||||
|
||||
SOURCE
|
||||
------
|
||||
ISO 3601-2:2016, *Fluid power systems - O-rings - Part 2: Housing dimensions
|
||||
for general applications*, Tables 1 and 2 (radial static housings).
|
||||
|
||||
COVERAGE
|
||||
--------
|
||||
**No rows are transcribed in this build** (same rule as ``undercuts.py``): the
|
||||
groove width and depth per cord diameter decide whether the seal works, and
|
||||
they are not written here from memory. Every lookup is refused by name.
|
||||
|
||||
Extending is a data edit: fill ``ROWS`` with
|
||||
``{cord_diameter_mm: {"b": groove_width_mm, "h": groove_depth_mm}}`` - the two
|
||||
keys group S's ``std_feature_draw`` already reads - and widen ``COVERAGE``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from engineering.mech.standards import Coverage, register
|
||||
|
||||
SOURCE = "ISO 3601-2:2016, Tables 1-2 (radial static housings) - NOT TRANSCRIBED in this build"
|
||||
|
||||
#: The keys every ISO 3601-2 row must carry.
|
||||
ORING_ROW_KEYS: tuple[str, ...] = ("b", "h")
|
||||
|
||||
KINDS: tuple[str, ...] = ("radial", "axial")
|
||||
|
||||
ROWS: dict[float, dict] = {}
|
||||
|
||||
COVERAGE = Coverage(0.0, 0.0, "mm")
|
||||
|
||||
register("ISO 3601-2", ROWS, COVERAGE, SOURCE)
|
||||
|
||||
|
||||
def oring_groove_dims(cord: float, kind: str = "radial") -> dict:
|
||||
"""``{"b": width, "h": depth}`` for a cord diameter, or a named refusal."""
|
||||
key = str(kind or "").strip().lower()
|
||||
if key not in KINDS:
|
||||
raise ValueError(f"O-ring housing: expected kind 'radial' or 'axial', got {kind!r}.")
|
||||
if not ROWS:
|
||||
raise ValueError(
|
||||
f"ISO 3601-2: no rows are transcribed in this build, so no {key} housing can be "
|
||||
f"looked up for a {float(cord):g} mm cord. Pass b and h explicitly, or transcribe "
|
||||
f"{SOURCE.split(' - ')[0]} into engineering/mech/standards/orings.py. "
|
||||
"Nothing is interpolated."
|
||||
)
|
||||
size = float(cord)
|
||||
if size in ROWS:
|
||||
return {name: float(ROWS[size][name]) for name in ORING_ROW_KEYS}
|
||||
raise ValueError(
|
||||
f"ISO 3601-2: cord {size:g} mm has no transcribed row (sizes: {sorted(ROWS)})."
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
"""DIN 471 / DIN 472 retaining-ring groove dimensions.
|
||||
|
||||
SOURCE
|
||||
------
|
||||
DIN 471:2011-04, *Retaining rings for shafts*, Table 1 (groove diameter d2 and
|
||||
groove width m) and DIN 472:2011-04, *Retaining rings for bores*, Table 1.
|
||||
|
||||
COVERAGE
|
||||
--------
|
||||
**No rows are transcribed in this build**, for the reason given in
|
||||
``undercuts.py``: the groove diameter is the number a wrong drawing gets wrong,
|
||||
and it is not written here from memory. Every lookup is refused by name.
|
||||
|
||||
Extending is a data edit: fill ``ROWS_471`` / ``ROWS_472`` with
|
||||
``{nominal_shaft_or_bore_mm: {"m": groove_width_mm, "d2": groove_diameter_mm}}``
|
||||
and widen the coverage. Those two keys are the contract group S's
|
||||
``std_feature_draw`` already reads.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from engineering.mech.standards import Coverage, register
|
||||
|
||||
SOURCE = (
|
||||
"DIN 471:2011-04 Table 1 (shafts) and DIN 472:2011-04 Table 1 (bores) "
|
||||
"- NOT TRANSCRIBED in this build"
|
||||
)
|
||||
|
||||
#: The keys every DIN 471/472 row must carry.
|
||||
RING_ROW_KEYS: tuple[str, ...] = ("m", "d2")
|
||||
|
||||
ROWS_471: dict[float, dict] = {}
|
||||
ROWS_472: dict[float, dict] = {}
|
||||
|
||||
COVERAGE_471 = Coverage(0.0, 0.0, "mm")
|
||||
COVERAGE_472 = Coverage(0.0, 0.0, "mm")
|
||||
|
||||
register("DIN 471", ROWS_471, COVERAGE_471, SOURCE)
|
||||
register("DIN 472", ROWS_472, COVERAGE_472, SOURCE)
|
||||
|
||||
_ROWS = {"DIN 471": ROWS_471, "DIN 472": ROWS_472}
|
||||
|
||||
|
||||
def ring_groove_dims(standard: str, d: float) -> dict:
|
||||
"""``{"m": ..., "d2": ...}`` for a nominal size, or a named refusal."""
|
||||
key = " ".join(str(standard or "").split()).upper()
|
||||
if key not in _ROWS:
|
||||
raise ValueError(f"retaining-ring groove: expected DIN 471 or DIN 472, got {standard!r}.")
|
||||
rows = _ROWS[key]
|
||||
if not rows:
|
||||
raise ValueError(
|
||||
f"{key}: no rows are transcribed in this build, so no groove can be looked up "
|
||||
f"for d = {float(d):g} mm. Pass m and d2 explicitly, or transcribe "
|
||||
f"{SOURCE.split(' - ')[0]} into engineering/mech/standards/rings.py. "
|
||||
"Nothing is interpolated."
|
||||
)
|
||||
size = float(d)
|
||||
if size in rows:
|
||||
return {name: float(rows[size][name]) for name in RING_ROW_KEYS}
|
||||
raise ValueError(f"{key}: d = {size:g} mm has no transcribed row (sizes: {sorted(rows)}).")
|
||||
@@ -0,0 +1,250 @@
|
||||
"""ISO 261 metric thread pitches and the ISO 6410 drawing representation.
|
||||
|
||||
SOURCE
|
||||
------
|
||||
**Pitches.** ISO 261:2022, *ISO general purpose metric screw threads - General
|
||||
plan*, Table 1 (coarse pitch series) and Table 2 (fine pitch series).
|
||||
|
||||
**Transcribed coverage.** The first-choice diameters M1.6 to M64, plus the
|
||||
second-choice diameters M14, M18, M22, M27, M33, M39, M45, M52 and M60. The
|
||||
small second-choice diameters (M1.8, M2.2, M3.5, M4.5, M5.5, M7, M9, M11),
|
||||
every third-choice diameter, everything above M64 and the fine pitch series
|
||||
below M8 are **not** transcribed and are refused by name rather than
|
||||
interpolated - the table rule of this track. Where a diameter's fine series
|
||||
here is shorter than the standard's, the pitch that is missing is refused by
|
||||
name as well; widening it is a data edit in ``FINE`` with no code change.
|
||||
|
||||
**Basic diameters.** ISO 68-1:1998 fundamental triangle, H = (sqrt(3)/2) * P,
|
||||
with ISO 724's basic minor diameter d1 = d - 1.25*H = d - 1.082532*P and basic
|
||||
pitch diameter d2 = d - 0.75*H = d - 0.649519*P. That is the standard's own
|
||||
formula rather than transcribed table data, which is why it can be pinned
|
||||
against published ISO 724 values instead of trusted: M20x2.5 reproduces
|
||||
17.294 / 18.376 exactly, M10x1.5 reproduces 8.376 / 9.026, M8x1.25 reproduces
|
||||
6.647 / 7.188.
|
||||
|
||||
**Representation.** ISO 6410-1:1993, *Technical drawings - Screw threads and
|
||||
threaded parts*: the minor diameter is drawn as a thin continuous line at
|
||||
0.8 x the major diameter (`MINOR_RATIO`); in a view along the axis that same
|
||||
circle is drawn as three quarters of a circle with the gap in one quadrant;
|
||||
the thread run-out / thread-length limit is a line across the full major
|
||||
diameter. An internal thread shown without a section is drawn hidden
|
||||
throughout.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
|
||||
from engineering.mech.primitives import Arc, Line, Prim
|
||||
from engineering.mech.standards import Coverage, register
|
||||
|
||||
SOURCE_PITCH = (
|
||||
"ISO 261:2022 Table 1 (coarse) and Table 2 (fine); first-choice M1.6-M64 "
|
||||
"plus second-choice M14, M18, M22, M27, M33, M39, M45, M52, M60"
|
||||
)
|
||||
SOURCE_BASIC = "ISO 68-1:1998 fundamental triangle; values pinned against ISO 724 Table 1"
|
||||
SOURCE_REPRESENTATION = "ISO 6410-1:1993"
|
||||
|
||||
#: ISO 6410-1: the minor diameter is represented at 0.8 x the major diameter.
|
||||
MINOR_RATIO = 0.8
|
||||
|
||||
#: ISO 261 Table 1 - coarse pitch series, major diameter (mm) -> pitch (mm).
|
||||
COARSE: dict[float, float] = {
|
||||
1.6: 0.35,
|
||||
2.0: 0.40,
|
||||
2.5: 0.45,
|
||||
3.0: 0.50,
|
||||
4.0: 0.70,
|
||||
5.0: 0.80,
|
||||
6.0: 1.00,
|
||||
8.0: 1.25,
|
||||
10.0: 1.50,
|
||||
12.0: 1.75,
|
||||
14.0: 2.00,
|
||||
16.0: 2.00,
|
||||
18.0: 2.50,
|
||||
20.0: 2.50,
|
||||
22.0: 2.50,
|
||||
24.0: 3.00,
|
||||
27.0: 3.00,
|
||||
30.0: 3.50,
|
||||
33.0: 3.50,
|
||||
36.0: 4.00,
|
||||
39.0: 4.00,
|
||||
42.0: 4.50,
|
||||
45.0: 4.50,
|
||||
48.0: 5.00,
|
||||
52.0: 5.00,
|
||||
56.0: 5.50,
|
||||
60.0: 5.50,
|
||||
64.0: 6.00,
|
||||
}
|
||||
|
||||
#: ISO 261 Table 2 - fine pitch series, major diameter (mm) -> pitches (mm),
|
||||
#: coarsest first. Transcribed from M8 upwards only.
|
||||
FINE: dict[float, tuple[float, ...]] = {
|
||||
8.0: (1.0, 0.75),
|
||||
10.0: (1.25, 1.0, 0.75),
|
||||
12.0: (1.5, 1.25, 1.0),
|
||||
14.0: (1.5, 1.0),
|
||||
16.0: (1.5, 1.0),
|
||||
18.0: (2.0, 1.5, 1.0),
|
||||
20.0: (2.0, 1.5, 1.0),
|
||||
22.0: (2.0, 1.5, 1.0),
|
||||
24.0: (2.0, 1.5, 1.0),
|
||||
27.0: (2.0, 1.5, 1.0),
|
||||
30.0: (2.0, 1.5, 1.0),
|
||||
33.0: (2.0, 1.5),
|
||||
36.0: (3.0, 2.0, 1.5),
|
||||
39.0: (3.0, 2.0, 1.5),
|
||||
42.0: (3.0, 2.0, 1.5),
|
||||
45.0: (3.0, 2.0, 1.5),
|
||||
48.0: (3.0, 2.0, 1.5),
|
||||
52.0: (3.0, 2.0, 1.5),
|
||||
56.0: (4.0, 3.0, 2.0, 1.5),
|
||||
60.0: (4.0, 3.0, 2.0, 1.5),
|
||||
64.0: (4.0, 3.0, 2.0, 1.5),
|
||||
}
|
||||
|
||||
DIAMETERS: tuple[float, ...] = tuple(sorted(COARSE))
|
||||
|
||||
COVERAGE = Coverage(DIAMETERS[0], DIAMETERS[-1], "mm")
|
||||
|
||||
_DESIGNATION_RE = re.compile(
|
||||
r"^\s*M\s*(\d+(?:[.,]\d+)?)\s*(?:[x×X]\s*(\d+(?:[.,]\d+)?))?\s*(?:-\s*(LH|RH))?\s*$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
register(
|
||||
"ISO 261",
|
||||
{d: {"pitch": COARSE[d], "fine": FINE.get(d, ())} for d in DIAMETERS},
|
||||
COVERAGE,
|
||||
SOURCE_PITCH,
|
||||
)
|
||||
|
||||
|
||||
def _number(text: str) -> float:
|
||||
return float(text.replace(",", "."))
|
||||
|
||||
|
||||
def _pitch_text(pitch: float) -> str:
|
||||
"""A pitch in millimetres, always carrying a decimal: 2.5, 2.0, 0.75."""
|
||||
text = f"{float(pitch):g}"
|
||||
return text if "." in text else f"{text}.0"
|
||||
|
||||
|
||||
def pitch_for(d: float, pitch: float | None = None) -> float:
|
||||
"""The coarse pitch for ``d``, or validate an explicit fine pitch against the series."""
|
||||
diameter = float(d)
|
||||
if diameter not in COARSE:
|
||||
raise ValueError(
|
||||
f"ISO 261: M{diameter:g} is not in the transcribed table. "
|
||||
f"Coverage is M{DIAMETERS[0]:g}-M{DIAMETERS[-1]:g}, the first-choice diameters plus "
|
||||
"the second-choice M14, M18, M22, M27, M33, M39, M45, M52 and M60; the remaining "
|
||||
"second-choice diameters and every third-choice diameter are not transcribed and "
|
||||
"are not interpolated."
|
||||
)
|
||||
if pitch is None:
|
||||
return COARSE[diameter]
|
||||
value = float(pitch)
|
||||
if abs(value - COARSE[diameter]) < 1e-9:
|
||||
return COARSE[diameter]
|
||||
series = FINE.get(diameter, ())
|
||||
for candidate in series:
|
||||
if abs(value - candidate) < 1e-9:
|
||||
return candidate
|
||||
known = ", ".join(_pitch_text(p) for p in (COARSE[diameter], *series))
|
||||
raise ValueError(
|
||||
f"ISO 261: M{diameter:g} has no pitch {value:g} mm in the transcribed series. "
|
||||
f"Transcribed pitches for M{diameter:g}: {known}."
|
||||
)
|
||||
|
||||
|
||||
def parse_designation(designation: str) -> dict:
|
||||
"""``M20`` / ``M20x1.5`` / ``M20x1.5-LH`` -> d, pitch, series, hand."""
|
||||
match = _DESIGNATION_RE.match(str(designation or ""))
|
||||
if not match:
|
||||
raise ValueError(
|
||||
f"designation {designation!r}: expected an ISO metric thread such as "
|
||||
"'M20', 'M20x1.5' or 'M20x1.5-LH'."
|
||||
)
|
||||
diameter = _number(match.group(1))
|
||||
raw_pitch = _number(match.group(2)) if match.group(2) else None
|
||||
pitch = pitch_for(diameter, raw_pitch)
|
||||
hand = "left" if (match.group(3) or "RH").upper() == "LH" else "right"
|
||||
series = "coarse" if abs(pitch - COARSE[diameter]) < 1e-9 else "fine"
|
||||
text = f"M{diameter:g}" if raw_pitch is None else f"M{diameter:g}x{pitch:g}"
|
||||
if hand == "left":
|
||||
text += "-LH"
|
||||
return {
|
||||
"designation": text,
|
||||
"d": diameter,
|
||||
"pitch": pitch,
|
||||
"series": series,
|
||||
"hand": hand,
|
||||
}
|
||||
|
||||
|
||||
def _h(pitch: float) -> float:
|
||||
"""ISO 68-1 fundamental triangle height."""
|
||||
return math.sqrt(3.0) / 2.0 * float(pitch)
|
||||
|
||||
|
||||
def basic_minor_diameter(d: float, pitch: float) -> float:
|
||||
"""ISO 724 basic minor diameter d1 = d - 1.25 * H."""
|
||||
return float(d) - 1.25 * _h(pitch)
|
||||
|
||||
|
||||
def basic_pitch_diameter(d: float, pitch: float) -> float:
|
||||
"""ISO 724 basic pitch diameter d2 = d - 0.75 * H."""
|
||||
return float(d) - 0.75 * _h(pitch)
|
||||
|
||||
|
||||
def thread_axial_prims(
|
||||
*,
|
||||
x0: float,
|
||||
length: float,
|
||||
d: float,
|
||||
internal: bool = False,
|
||||
minor_ratio: float = MINOR_RATIO,
|
||||
limit_line: bool = True,
|
||||
) -> tuple[Prim, ...]:
|
||||
"""ISO 6410 representation in a view along the axis (axis on y = 0, +X along it).
|
||||
|
||||
External: the two minor-diameter lines at 0.8 x d plus the thread-length
|
||||
limit line across the full major diameter. Internal (not sectioned): the
|
||||
same pair at the *major* diameter, hidden, because the drawn bore already
|
||||
carries the minor.
|
||||
"""
|
||||
x1 = float(x0) + float(length)
|
||||
role = "hidden" if internal else "visible"
|
||||
radius = float(d) / 2.0 if internal else float(minor_ratio) * float(d) / 2.0
|
||||
prims: list[Prim] = [
|
||||
Line((float(x0), radius), (x1, radius), role),
|
||||
Line((float(x0), -radius), (x1, -radius), role),
|
||||
]
|
||||
if limit_line:
|
||||
major = float(d) / 2.0
|
||||
prims.append(Line((x1, -major), (x1, major), role))
|
||||
return tuple(prims)
|
||||
|
||||
|
||||
def thread_end_prims(
|
||||
*,
|
||||
center: tuple[float, float],
|
||||
d: float,
|
||||
internal: bool = False,
|
||||
gap_center_deg: float = 45.0,
|
||||
minor_ratio: float = MINOR_RATIO,
|
||||
) -> tuple[Prim, ...]:
|
||||
"""ISO 6410 three-quarter circle for a view along the thread axis.
|
||||
|
||||
External: three quarters of the *minor* circle. Internal: three quarters of
|
||||
the *major* circle. The quarter-circle gap is centred on ``gap_center_deg``
|
||||
(45 deg = the upper-right quadrant, the usual placement).
|
||||
"""
|
||||
radius = float(d) / 2.0 if internal else float(minor_ratio) * float(d) / 2.0
|
||||
start = (float(gap_center_deg) + 45.0) % 360.0
|
||||
end = (start + 270.0) % 360.0
|
||||
return (Arc((float(center[0]), float(center[1])), radius, start, end, "visible"),)
|
||||
@@ -0,0 +1,67 @@
|
||||
"""DIN 509 relief grooves (Freistiche), forms E and F.
|
||||
|
||||
SOURCE
|
||||
------
|
||||
DIN 509:1998-06, *Technische Zeichnungen - Freistiche*, Table 1 (forms E and F).
|
||||
|
||||
COVERAGE
|
||||
--------
|
||||
**No rows are transcribed in this build.** The track's table rule is absolute:
|
||||
a row that cannot be verified against the standard's own table is not shipped,
|
||||
because a wrong relief depth is a wrong workshop drawing while a narrow table
|
||||
is merely narrow. Every lookup here is therefore refused by name and names the
|
||||
route to fix it.
|
||||
|
||||
Extending this module is a **data edit with no code change**: fill ``ROWS_E`` /
|
||||
``ROWS_F`` with ``{shaft_diameter_mm: {"profile": [[along, depth], ...]}}`` and
|
||||
widen ``COVERAGE_E`` / ``COVERAGE_F`` to the transcribed range. ``profile`` is
|
||||
the standard's own published relief profile expressed as (distance along the
|
||||
axis from the shoulder corner, depth below the smaller cylinder) pairs in
|
||||
millimetres - the same ``profile`` key group S's ``std_feature_draw`` reads -
|
||||
so nothing downstream ever reconstructs a profile it was not handed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from engineering.mech.standards import Coverage, register
|
||||
|
||||
SOURCE = "DIN 509:1998-06, Table 1 (forms E and F) - NOT TRANSCRIBED in this build"
|
||||
|
||||
#: The keys every DIN 509 row must carry for the drawing code to use it.
|
||||
UNDERCUT_ROW_KEYS: tuple[str, ...] = ("profile",)
|
||||
|
||||
FORMS: tuple[str, ...] = ("E", "F")
|
||||
|
||||
ROWS_E: dict[float, dict] = {}
|
||||
ROWS_F: dict[float, dict] = {}
|
||||
|
||||
COVERAGE_E = Coverage(0.0, 0.0, "mm")
|
||||
COVERAGE_F = Coverage(0.0, 0.0, "mm")
|
||||
|
||||
register("DIN 509-E", ROWS_E, COVERAGE_E, SOURCE)
|
||||
register("DIN 509-F", ROWS_F, COVERAGE_F, SOURCE)
|
||||
|
||||
_ROWS = {"E": ROWS_E, "F": ROWS_F}
|
||||
|
||||
|
||||
def undercut_profile(form: str, d: float) -> tuple[tuple[float, float], ...]:
|
||||
"""The published relief profile for a shaft diameter, or a named refusal."""
|
||||
key = str(form or "").strip().upper()
|
||||
if key not in FORMS:
|
||||
raise ValueError(f"DIN 509 defines forms E and F; got {form!r}.")
|
||||
rows = _ROWS[key]
|
||||
if not rows:
|
||||
raise ValueError(
|
||||
f"DIN 509 form {key}: no rows are transcribed in this build, so no relief "
|
||||
f"profile can be looked up for d = {float(d):g} mm. Pass the profile "
|
||||
"explicitly (Undercut(profile=[[along, depth], ...])), or transcribe "
|
||||
f"{SOURCE.split(' - ')[0]} into engineering/mech/standards/undercuts.py. "
|
||||
"Nothing is interpolated and no profile is invented."
|
||||
)
|
||||
size = float(d)
|
||||
if size in rows:
|
||||
return tuple(tuple(float(v) for v in pair) for pair in rows[size]["profile"])
|
||||
raise ValueError(
|
||||
f"DIN 509 form {key}: d = {size:g} mm has no transcribed row "
|
||||
f"(transcribed sizes: {sorted(rows)})."
|
||||
)
|
||||
Reference in New Issue
Block a user