diff --git a/designs/window_cam_lock/NOTES.md b/designs/window_cam_lock/NOTES.md new file mode 100644 index 0000000..678345c --- /dev/null +++ b/designs/window_cam_lock/NOTES.md @@ -0,0 +1,163 @@ +# window_cam_lock engineering notes + +This family contains two discrete AmesburyTruth catalog rows. Official row +dimensions are copied together by `refine()` and are never sampled +independently. + +## Official references + +- 17.23.XX.200 EntryGard drawing: + +- 17.32.XX.200 Trimline drawing: + +- 17.23 product page: + +- 17.32 product page: + + +## Vertical-stack recalculation + +### 17.23.XX.200 + +- lower tier / end pad: 12.7 mm +- middle-deck top: 13.5 mm +- overall boss top: 23.1 mm +- modeled deck rise: `13.5 - 12.7 = 0.8 mm` +- modeled boss rise: `23.1 - 13.5 = 9.6 mm` +- stack check: `12.7 + 0.8 + 9.6 = 23.1 mm` + +### 17.32.XX.200 + +- two end mounting feet: `2X 6.4 mm` +- lower central body: 11.3 mm +- middle-deck top: 12.5 mm +- overall boss top: 20.1 mm +- modeled deck rise: `12.5 - 11.3 = 1.2 mm` +- modeled boss rise: `20.1 - 12.5 = 7.6 mm` +- central stack check: `11.3 + 1.2 + 7.6 = 20.1 mm` + +The enlarged 17.32 drawing shows that the `2X .25 in [6.4 mm]` leader points +to the two end mounting feet. It does **not** dimension the alignment lugs. +No published lug height was found, so the lug geometry is explicitly +proportion-based. + +## Mounting-hole and countersink recalculation + +Both rows specify 65.9 mm overall length, 52.4 mm hole spacing, 4.3 mm through +bores, and an 11.1 mm hole-center offset from the nearer long edge. + +The manufacturer drawing does not dimension a countersink major diameter or +angle. This model deliberately uses an 8.0 mm major diameter and a single 82 +degree included angle, based on the legacy Truth recommendation for a #7 +Phillips flat-head inch screw and the visible product proportions. + +- end center land: `(65.9 - 52.4) / 2 = 6.75 mm` +- end countersink edge margin: `6.75 - 8.0 / 2 = 2.75 mm` +- near long-edge margin: `11.1 - 8.0 / 2 = 7.10 mm` +- 17.23 far long-edge margin: `28.6 - 11.1 - 8.0 / 2 = 13.50 mm` +- 17.32 far long-edge margin: `26.2 - 11.1 - 8.0 / 2 = 11.10 mm` +- 82 degree countersink depth: + `(8.0 - 4.3) / (2 * tan(82 deg / 2)) = 2.13 mm` +- 17.23 residual end-pad thickness: `12.7 - 2.13 = 10.57 mm` +- 17.32 residual end-foot thickness: `6.4 - 2.13 = 4.27 mm` + +The mounting-hole Y coordinate is +`-body_width / 2 + 11.1`, rather than being placed on the body centerline. + +## Proportion formulas + +Every formula below is labeled `proportion` in `spec.py`; none is presented as +an AmesburyTruth production dimension. + +- clipped base-corner setback: `0.10 * body_width` +- central housing outside width: `0.82 * body_width` +- shallow cavity/opening height: sampled `cavity_h`; easy 4.4 mm, + medium 4.0-4.8 mm, hard 3.8-5.2 mm +- central cavity length: `housing_length - 2 * closed_wall_t` +- asymmetric cavity width: + `0.82 * body_width + 1.0 mm - closed_wall_t` +- asymmetric cavity Y center: `-(1.0 mm + closed_wall_t) / 2` +- the negative-Y long side is open; the positive-Y long side and both short + ends retain `closed_wall_t` +- retained material above the cavity: `deck_h - cavity_h` +- pivot X offset from body center: `0.10 * body_length` +- pivot boss diameter: `0.48 * body_width` +- simplified spindle diameter: `0.16 * body_width` +- spindle radial clearance: 0.20 mm +- rotating-cap axial clearance above the deck: 0.20 mm +- lever middle thickness: `0.48 * lever_t` +- lever root-plan transition: `-0.18 * lever_length` +- lever free-end step starts at: `-0.76 * lever_length` +- lever shoulder half-width at that transition: + `0.42 * lever_tip_width` +- lever root thick-step length: `0.22 * lever_length` +- lever free-end thick step uses the exact upper-plan boundary from + `-0.76 * lever_length` to `-lever_length`; it is the same tapered/widened + outline extruded downward by `lever_t - 0.48 * lever_t` +- 17.32 lug length: `0.11 * body_length` +- 17.32 lug width: `0.055 * body_width` +- 17.32 lug height above the mounting plane: `0.28 * end_pad_h` +- 17.32 lug longitudinal centers: `+/- 0.28 * body_length`; both lugs are + located on the same long edge, matching the official top view + +## Components and visual assembly + +`family.json` declares two physical components, and their names exactly match +the stable child names returned by `cq.Assembly`: + +| Component builder | Assembly child | Quantity | +|---|---|---:| +| `build_fixed_body()` | `fixed_body` | 1 | +| `build_rotating_body()` | `rotating_body` | 1 | + +The image-first assembly sequence is committed as `preview_parts.png`. +Successive panels highlight each component in the assembled context, then show +the rotating body exploded in `+Z` and inserted back along `-Z`, followed by +the final two-color assembly. The spindle and fixed-body pivot bore share the +axis at `X = 0.10 * body_length`, `Y = 0`. Final seating preserves 0.20 mm +radial clearance around the spindle and 0.20 mm axial clearance between the +fixed deck and rotating cap. + +## Engineering constraints + +- Every official dimension and the lug state must equal the complete selected + product row. +- The 8.0 mm countersink must retain at least 2.5 mm of material to all outside + edges and at least 0.75 mm to the central housing envelope. +- Countersink depth must leave at least 1.5 mm of mounting-pad floor. +- `cavity_h` is independent of `closed_wall_t`; changing a wall thickness cannot + deepen the opening or pierce the roof. +- The shallow cavity must stop at least 1.0 mm below the selected row's end-foot + top and retain at least 6.5 mm to the official deck top. +- Both short-end walls and the opposite long-side wall must remain closed. +- `deck_h > body_h` and `overall_h > deck_h`. +- The lever free end must be wider than the neck. +- The downward lever tip step must clear the middle deck by at least 0.75 mm. +- The handle is allowed to overhang the body during its modeled 0-90 degree + travel; vertical clearance prevents contact with the fixed deck and feet. +- The fixed housing and rotating handle/spindle are exactly two non-intersecting + solids separated by 0.20 mm axial/radial bearing clearances. + +## Deliberate deviations and omissions + +- EntryGard 17.23 and Trimline 17.32 share one benchmark family because their + external architecture and official hole pattern are the same; `product_row` + preserves their distinct dimension stacks and lug state. +- The model captures the external die-cast envelope, a shallow one-side-open + mechanism bay, the opposite closed long wall, closed short ends, three height + tiers, and stepped lever. +- It is a two-body assembly: one fixed housing and one rotating + lever/cap/spindle body. +- The alignment-lug location and dimensions are proportional because the + official 17.32 drawing/product resource confirms the feature but does not + publish its dimensions. Lugs start at `Z=0`; they do not project below the + flat mounting plane. +- The 8.0 mm countersink major diameter is a deliberate approximation. The + official 4.3 mm through bore remains exact. +- The production lever's fillets, draft, texture, and local curvature are + simplified into a continuous planform with abrupt lower root/tip steps. +- A simplified cylindrical spindle is included only to establish the second + rotating body. Internal cam profile, spring, snap-back, fasteners, stops, and + production wall ribs are omitted because no manufacturing dimensions were + published. +- Casting draft, small rounds, embossed marks, and finish variants are omitted. diff --git a/designs/window_cam_lock/family.json b/designs/window_cam_lock/family.json new file mode 100644 index 0000000..e0e52a6 --- /dev/null +++ b/designs/window_cam_lock/family.json @@ -0,0 +1,21 @@ +{ + "family": "window_cam_lock", + "standard": null, + "base_plane": "XY", + "solids": 2, + "components": [ + { + "name": "fixed_body", + "quantity": 1, + "role": "Stationary mounting body and cam housing" + }, + { + "name": "rotating_body", + "quantity": 1, + "role": "Rotating cap, spindle, and operating lever" + } + ], + "description": "Two-body surface-mounted cam-lock assembly with discrete AmesburyTruth 17.23 EntryGard and 17.32 Trimline product rows.", + "source": "AmesburyTruth drawings 17.23.XX.200 and 17.32.XX.200, official product pages and images, legacy Truth SW07 table, and documented proportion rules for undimensioned details", + "contributor": "sunzhenchuan007" +} diff --git a/designs/window_cam_lock/part.py b/designs/window_cam_lock/part.py new file mode 100644 index 0000000..3d144bd --- /dev/null +++ b/designs/window_cam_lock/part.py @@ -0,0 +1,260 @@ +"""Two-component simplified AmesburyTruth 17 Series cam-lock assembly.""" + +import cadquery as cq + + +_RADIAL_CLEARANCE = 0.20 +_AXIAL_CLEARANCE = 0.20 + + +def build_fixed_body( + body_length, + body_width, + end_pad_h, + body_h, + deck_h, + hole_spacing, + hole_d, + hole_edge_offset, + has_alignment_lugs, + hole_csk_d, + hole_csk_angle, + housing_length, + cavity_h, + closed_wall_t, +): + """Build the stationary mounting body and hollow cam housing.""" + half_l = body_length / 2.0 + half_w = body_width / 2.0 + corner_cut = 0.10 * body_width + base_profile = [ + (-half_l + corner_cut, -half_w), + (half_l - corner_cut, -half_w), + (half_l, -half_w + corner_cut), + (half_l, half_w - corner_cut), + (half_l - corner_cut, half_w), + (-half_l + corner_cut, half_w), + (-half_l, half_w - corner_cut), + (-half_l, -half_w + corner_cut), + ] + + base = cq.Workplane("XY").polyline(base_profile).close().extrude(end_pad_h) + hole_y = -half_w + hole_edge_offset + hole_points = [(-hole_spacing / 2.0, hole_y), (hole_spacing / 2.0, hole_y)] + base = ( + base.faces(">Z") + .workplane() + .pushPoints(hole_points) + .cskHole(hole_d, hole_csk_d, hole_csk_angle) + ) + + housing_width = 0.82 * body_width + lower_housing = ( + cq.Workplane("XY") + .ellipse(housing_length / 2.0, housing_width / 2.0) + .extrude(body_h) + ) + deck = ( + cq.Workplane("XY") + .workplane(offset=body_h) + .ellipse(housing_length / 2.0, housing_width / 2.0) + .extrude(deck_h - body_h) + ) + fixed_body = base.union(lower_housing).union(deck) + + # A shallow underside cavity is asymmetric: it opens through the near + # (negative-Y) long side while the far long side and both short ends remain + # closed. Cavity height is independent of every retained wall thickness. + cavity_width = housing_width + 1.0 - closed_wall_t + cavity_center_y = -(1.0 + closed_wall_t) / 2.0 + cavity = ( + cq.Workplane("XY") + .center(0.0, cavity_center_y) + .box( + housing_length - 2.0 * closed_wall_t, + cavity_width, + cavity_h, + centered=(True, True, False), + ) + ) + fixed_body = fixed_body.cut(cavity) + + # 17.32 has two undimensioned alignment lugs on one long side. Both start + # at Z=0 and therefore do not project below the flat mounting plane. + if has_alignment_lugs: + lug_length = 0.11 * body_length + lug_width = 0.055 * body_width + lug_h = 0.28 * end_pad_h + lug_x = 0.28 * body_length + lugs = ( + cq.Workplane("XY") + .pushPoints( + [ + (-lug_x, -half_w - lug_width / 2.0), + (lug_x, -half_w - lug_width / 2.0), + ] + ) + .box(lug_length, lug_width, lug_h, centered=(True, True, False)) + ) + fixed_body = fixed_body.union(lugs) + + # The rotating spindle is built separately. This bore retains the declared + # radial gap all around it and opens into the shallow mechanism cavity. + pivot_x = 0.10 * body_length + spindle_d = 0.16 * body_width + pivot_bore = ( + cq.Workplane("XY") + .workplane(offset=cavity_h) + .center(pivot_x, 0.0) + .circle(spindle_d / 2.0 + _RADIAL_CLEARANCE) + .extrude(deck_h - cavity_h + _AXIAL_CLEARANCE) + ) + return fixed_body.cut(pivot_bore) + + +def build_rotating_body( + body_length, + body_width, + cavity_h, + deck_h, + overall_h, + lever_length, + lever_width, + lever_tip_width, + lever_t, + lever_angle, +): + """Build the rotating cap, spindle, and operating lever as one solid.""" + pivot_x = 0.10 * body_length + boss_d = 0.48 * body_width + spindle_d = 0.16 * body_width + + cap_bottom = deck_h + _AXIAL_CLEARANCE + cap = ( + cq.Workplane("XY") + .workplane(offset=cap_bottom) + .center(pivot_x, 0.0) + .circle(boss_d / 2.0) + .extrude(overall_h - cap_bottom) + ) + spindle = ( + cq.Workplane("XY") + .workplane(offset=cavity_h + _AXIAL_CLEARANCE) + .center(pivot_x, 0.0) + .circle(spindle_d / 2.0) + .extrude(overall_h - cavity_h - _AXIAL_CLEARANCE) + ) + + # The lever is one continuous solid with a thin middle and abrupt downward + # steps at the pivot and free end. It is fused only to the rotating cap. + root_x = -0.18 * lever_length + tip_step_x = -0.76 * lever_length + lever_plan = [ + (0.0, -lever_width / 2.0), + (root_x, -lever_width / 2.0), + (tip_step_x, -0.42 * lever_tip_width), + (-lever_length, -lever_tip_width / 2.0), + (-lever_length, lever_tip_width / 2.0), + (tip_step_x, 0.42 * lever_tip_width), + (root_x, lever_width / 2.0), + (0.0, lever_width / 2.0), + ] + lever_mid_t = 0.48 * lever_t + lever_top_z = overall_h + lever = ( + cq.Workplane("XY") + .workplane(offset=lever_top_z - lever_mid_t) + .polyline(lever_plan) + .close() + .extrude(lever_mid_t) + ) + root_step = ( + cq.Workplane("XY") + .workplane(offset=lever_top_z - lever_t) + .rect(0.22 * lever_length, lever_width) + .extrude(lever_t) + .translate((-0.09 * lever_length, 0.0, 0.0)) + ) + tip_step_plan = [ + (tip_step_x, -0.42 * lever_tip_width), + (-lever_length, -lever_tip_width / 2.0), + (-lever_length, lever_tip_width / 2.0), + (tip_step_x, 0.42 * lever_tip_width), + ] + tip_step = ( + cq.Workplane("XY") + .workplane(offset=lever_top_z - lever_t) + .polyline(tip_step_plan) + .close() + .extrude(lever_t - lever_mid_t) + ) + lever = lever.union(root_step).union(tip_step) + lever = ( + lever.translate((pivot_x, 0.0, 0.0)) + .rotate( + (pivot_x, 0.0, lever_top_z), + (pivot_x, 0.0, lever_top_z + 1.0), + lever_angle, + ) + ) + return cap.union(spindle).union(lever) + + +def build( + product_row, + body_length, + body_width, + end_pad_h, + body_h, + deck_h, + overall_h, + hole_spacing, + hole_d, + hole_edge_offset, + has_alignment_lugs, + hole_csk_d, + hole_csk_angle, + housing_length, + cavity_h, + closed_wall_t, + lever_length, + lever_width, + lever_tip_width, + lever_t, + lever_angle, +): + """Build the named two-component cam-lock assembly.""" + fixed_body = build_fixed_body( + body_length, + body_width, + end_pad_h, + body_h, + deck_h, + hole_spacing, + hole_d, + hole_edge_offset, + has_alignment_lugs, + hole_csk_d, + hole_csk_angle, + housing_length, + cavity_h, + closed_wall_t, + ) + rotating_body = build_rotating_body( + body_length, + body_width, + cavity_h, + deck_h, + overall_h, + lever_length, + lever_width, + lever_tip_width, + lever_t, + lever_angle, + ) + + # These stable names exactly match family.json components[].name. + result = cq.Assembly(name="window_cam_lock") + result.add(fixed_body, name="fixed_body") + result.add(rotating_body, name="rotating_body") + return result diff --git a/designs/window_cam_lock/preview.png b/designs/window_cam_lock/preview.png new file mode 100644 index 0000000..42b4afa Binary files /dev/null and b/designs/window_cam_lock/preview.png differ diff --git a/designs/window_cam_lock/preview_extremes.png b/designs/window_cam_lock/preview_extremes.png new file mode 100644 index 0000000..978ec6b Binary files /dev/null and b/designs/window_cam_lock/preview_extremes.png differ diff --git a/designs/window_cam_lock/preview_parts.png b/designs/window_cam_lock/preview_parts.png new file mode 100644 index 0000000..3be3611 Binary files /dev/null and b/designs/window_cam_lock/preview_parts.png differ diff --git a/designs/window_cam_lock/preview_views.png b/designs/window_cam_lock/preview_views.png new file mode 100644 index 0000000..a70f77a Binary files /dev/null and b/designs/window_cam_lock/preview_views.png differ diff --git a/designs/window_cam_lock/spec.py b/designs/window_cam_lock/spec.py new file mode 100644 index 0000000..906865e --- /dev/null +++ b/designs/window_cam_lock/spec.py @@ -0,0 +1,304 @@ +"""Sampling contract for two AmesburyTruth 17 Series window cam-lock rows.""" + +import math + + +PRODUCT_ROWS = { + 23: { + "body_length": 65.9, + "body_width": 28.6, + "end_pad_h": 12.7, + "body_h": 12.7, + "deck_h": 13.5, + "overall_h": 23.1, + "hole_spacing": 52.4, + "hole_d": 4.3, + "hole_edge_offset": 11.1, + "has_alignment_lugs": 0, + }, + 32: { + "body_length": 65.9, + "body_width": 26.2, + "end_pad_h": 6.4, + "body_h": 11.3, + "deck_h": 12.5, + "overall_h": 20.1, + "hole_spacing": 52.4, + "hole_d": 4.3, + "hole_edge_offset": 11.1, + "has_alignment_lugs": 1, + }, +} + +ROW_KEYS = ( + "body_length", + "body_width", + "end_pad_h", + "body_h", + "deck_h", + "overall_h", + "hole_spacing", + "hole_d", + "hole_edge_offset", + "has_alignment_lugs", +) + +DIFFICULTY_ROWS = { + "easy": [23], + "medium": [23, 32], + "hard": [32], +} + + +def _row_range(name): + return { + difficulty: ( + min(PRODUCT_ROWS[row][name] for row in rows), + max(PRODUCT_ROWS[row][name] for row in rows), + ) + for difficulty, rows in DIFFICULTY_ROWS.items() + } + + +PARAM_SPEC = { + "product_row": dict( + desc="AmesburyTruth product row: 23=17.23.XX.200 EntryGard, 32=17.32.XX.200 Trimline", + unit="", + range={"easy": (23, 23), "medium": (23, 32), "hard": (32, 32)}, + choices=DIFFICULTY_ROWS, + coverage=[23, 32], + integer=True, + feature=True, + source="AmesburyTruth drawings 17.23.XX.200 and 17.32.XX.200; complete selected row", + askable=True, + ), + "body_length": dict( + desc="Official overall body length", + unit="mm", + range=_row_range("body_length"), + refine=True, + source="AmesburyTruth selected product-row drawing", + askable=True, + ), + "body_width": dict( + desc="Official overall body width", + unit="mm", + range=_row_range("body_width"), + refine=True, + source="AmesburyTruth selected product-row drawing", + askable=True, + ), + "end_pad_h": dict( + desc="Official height of the end mounting pad or foot", + unit="mm", + range=_row_range("end_pad_h"), + refine=True, + source="AmesburyTruth selected product-row drawing; 17.32 callout is 2X 6.4 mm", + askable=True, + ), + "body_h": dict( + desc="Official lower central-body height", + unit="mm", + range=_row_range("body_h"), + refine=True, + source="AmesburyTruth selected product-row drawing", + askable=True, + ), + "deck_h": dict( + desc="Official middle-deck top height", + unit="mm", + range=_row_range("deck_h"), + refine=True, + source="AmesburyTruth selected product-row drawing", + askable=True, + ), + "overall_h": dict( + desc="Official overall height through the pivot boss", + unit="mm", + range=_row_range("overall_h"), + refine=True, + source="AmesburyTruth selected product-row drawing", + askable=True, + ), + "hole_spacing": dict( + desc="Official center-to-center spacing of the two mounting holes", + unit="mm", + range=_row_range("hole_spacing"), + refine=True, + source="AmesburyTruth selected product-row drawing", + askable=True, + ), + "hole_d": dict( + desc="Official diameter of each mounting-hole through bore", + unit="mm", + range=_row_range("hole_d"), + refine=True, + source="AmesburyTruth selected product-row drawing", + askable=True, + ), + "hole_edge_offset": dict( + desc="Official mounting-hole center offset from the nearer long edge", + unit="mm", + range=_row_range("hole_edge_offset"), + refine=True, + source="AmesburyTruth selected product-row drawing", + askable=True, + ), + "has_alignment_lugs": dict( + desc="Selected row includes alignment lugs: 17.23=without, 17.32=with", + unit="", + range=_row_range("has_alignment_lugs"), + refine=True, + integer=True, + feature=True, + source="Truth SW07 table and AmesburyTruth 17.32 product resource name 'With LG'", + ), + "hole_csk_d": dict( + desc="Modeled major diameter of each mounting-hole countersink", + unit="mm", + range={"easy": (8.0, 8.0), "medium": (8.0, 8.0), "hard": (8.0, 8.0)}, + choices=[8.0], + source="proportion; deliberate approximation around the recommended #7 flat-head screw", + askable=True, + ), + "hole_csk_angle": dict( + desc="Included angle of the modeled inch-series flat-head countersink", + unit="deg", + range={"easy": (82.0, 82.0), "medium": (82.0, 82.0), "hard": (82.0, 82.0)}, + choices=[82.0], + source="ASME inch flat-head convention used with the Truth #7 screw recommendation; not drawing-dimensioned", + askable=True, + ), + "housing_length": dict( + desc="Modeled length of the hollow central cam housing", + unit="mm", + range={"easy": (37.0, 37.0), "medium": (36.0, 38.0), "hard": (35.0, 39.0)}, + source="proportion from AmesburyTruth product image", + askable=True, + ), + "cavity_h": dict( + desc="Modeled height of the shallow underside cavity and its single open long side", + unit="mm", + range={"easy": (4.4, 4.4), "medium": (4.0, 4.8), "hard": (3.8, 5.2)}, + source="proportion from AmesburyTruth 17.32 side view; deliberately shallower than the 6.4 mm end feet", + askable=True, + ), + "closed_wall_t": dict( + desc="Modeled wall thickness at both short ends and the one closed long side", + unit="mm", + range={"easy": (2.4, 2.4), "medium": (2.2, 2.8), "hard": (2.0, 3.0)}, + source="proportion from AmesburyTruth isometric view: one long side open, opposite long side closed", + askable=True, + ), + "lever_length": dict( + desc="Modeled lever length from pivot to free end", + unit="mm", + range={"easy": (31.5, 31.5), "medium": (30.0, 33.0), "hard": (29.0, 34.0)}, + source="proportion from AmesburyTruth product image", + askable=True, + ), + "lever_width": dict( + desc="Modeled operating-lever neck width", + unit="mm", + range={"easy": (8.0, 8.0), "medium": (7.5, 8.8), "hard": (7.0, 9.2)}, + source="proportion from AmesburyTruth product image", + askable=True, + ), + "lever_tip_width": dict( + desc="Modeled widened operating-lever width at the free end", + unit="mm", + range={"easy": (11.5, 11.5), "medium": (10.8, 12.2), "hard": (10.4, 12.8)}, + source="proportion from AmesburyTruth product image", + askable=True, + ), + "lever_t": dict( + desc="Modeled maximum lever thickness at its two end steps", + unit="mm", + range={"easy": (3.2, 3.2), "medium": (3.0, 3.5), "hard": (2.8, 3.6)}, + source="proportion from AmesburyTruth product image", + askable=True, + ), + "lever_angle": dict( + desc="Displayed lever rotation about the cam pivot", + unit="deg", + range={"easy": (0, 0), "medium": (0, 90), "hard": (0, 90)}, + choices={"easy": [0], "medium": [0, 45, 90], "hard": [0, 30, 60, 90]}, + source="operational rotation accepted for this benchmark; modeled display range is 0 to 90 degrees", + ), +} + + +def refine(p, difficulty, rng): + row = PRODUCT_ROWS.get(int(p["product_row"])) + if row is None: + return + for key in ROW_KEYS: + p[key] = row[key] + + +def _row_consistency_errors(p): + row = PRODUCT_ROWS.get(int(p["product_row"])) + if row is None: + return ["product_row must select 17.23 or 17.32"] + bad = [] + for key in ROW_KEYS: + if key in p and abs(float(p[key]) - float(row[key])) > 1e-9: + bad.append(f"{key} must match complete AmesburyTruth row 17.{int(p['product_row']):02d}") + return bad + + +def check(p: dict) -> list[str]: + bad = _row_consistency_errors(p) + end_center_margin = (p["body_length"] - p["hole_spacing"]) / 2.0 + csk_r = p["hole_csk_d"] / 2.0 + near_long_margin = p["hole_edge_offset"] - csk_r + far_long_margin = p["body_width"] - p["hole_edge_offset"] - csk_r + csk_depth = (p["hole_csk_d"] - p["hole_d"]) / ( + 2.0 * math.tan(math.radians(p["hole_csk_angle"] / 2.0)) + ) + housing_to_csk = p["hole_spacing"] / 2.0 - csk_r - p["housing_length"] / 2.0 + boss_r = 0.24 * p["body_width"] + cavity_length = p["housing_length"] - 2.0 * p["closed_wall_t"] + roof_above_cavity = p["deck_h"] - p["cavity_h"] + lever_mid_t = 0.48 * p["lever_t"] + lever_tip_bottom = p["overall_h"] - p["lever_t"] + + if p["hole_csk_d"] <= p["hole_d"]: + bad.append("countersink major diameter must exceed the official 4.3 mm through bore") + if abs(p["hole_csk_angle"] - 82.0) > 1e-9: + bad.append("modeled countersink must use one consistent 82 degree included angle") + if end_center_margin - csk_r < 2.5: + bad.append("countersink must retain at least 2.5 mm material to each body end") + if near_long_margin < 2.5 or far_long_margin < 2.5: + bad.append("countersink must retain at least 2.5 mm material to both long edges") + if csk_depth >= p["end_pad_h"] - 1.5: + bad.append("countersink depth must leave at least 1.5 mm mounting-pad floor") + if housing_to_csk < 0.75: + bad.append("central housing must clear each countersink envelope by at least 0.75 mm") + if cavity_length <= 0.45 * p["housing_length"]: + bad.append("closed short-end walls must leave a non-degenerate central opening") + if p["closed_wall_t"] < 1.8 or p["closed_wall_t"] > 0.12 * p["housing_length"]: + bad.append("closed short/long-side wall thickness must remain a plausible die-cast proportion") + if p["cavity_h"] >= p["end_pad_h"] - 1.0: + bad.append("underside cavity must stop at least 1.0 mm below the end-foot top") + if roof_above_cavity < 6.5: + bad.append("shallow cavity must retain at least 6.5 mm to the official deck top") + if p["deck_h"] <= p["body_h"]: + bad.append("middle deck must stand above the lower central body per selected drawing") + if p["overall_h"] <= p["deck_h"]: + bad.append("pivot boss top must stand above the middle deck per selected drawing") + if p["lever_tip_width"] <= p["lever_width"]: + bad.append("lever free end must be wider than its neck per product image") + if p["lever_tip_width"] >= 0.55 * p["body_width"]: + bad.append("lever free end must remain within the modeled housing-width envelope") + if p["lever_length"] >= 0.56 * p["body_length"]: + bad.append("lever length must remain below the documented body-length proportion") + if lever_mid_t >= p["lever_t"]: + bad.append("lever middle must be thinner than its two abrupt end steps") + if lever_tip_bottom <= p["deck_h"] + 0.75: + bad.append("downward free-end step must clear the middle deck by at least 0.75 mm") + if boss_r <= p["lever_width"] / 2.0 + 1.0: + bad.append("pivot boss must retain material around the lever root") + if p["lever_angle"] < 0.0 or p["lever_angle"] > 90.0: + bad.append("displayed lever rotation must remain within the modeled 0 to 90 degree travel") + return bad