diff --git a/designs/swivel_clamp_connector_joint/NOTES.md b/designs/swivel_clamp_connector_joint/NOTES.md new file mode 100644 index 0000000..d628a5b --- /dev/null +++ b/designs/swivel_clamp_connector_joint/NOTES.md @@ -0,0 +1,184 @@ +# GN 490 implementation and assembly notes + +This family implements the aluminum JW Winco GN 490 swivel clamp connector +joint proposed in Issue #57. It is a fixed-pose, seven-component `cq.Assembly`, +not a motion simulation. + +## Sources + +- Product page: +- Official metric dimension PDF: +- Official engineering drawing: +- Family proposal and reference images: + +The source publishes eight equal-diameter rows, Types A/B, aluminum finishes +MT/SW, and the V-groove and pressure-spring functions. It identifies the body, +DIN 912 socket screw, DIN 934 hex nut, adjustable lever, pressure spring, and +insert/distance bushing. It does not publish a swivel-angle range, V-groove +detail, body-bore clearance, spring construction, bushing profile, or the +secondary Type A/Type B fastener dimensions. + +## Datasheet symbols to parameters + +| Datasheet | Build parameter | Treatment | +|---|---|---| +| `d1`, `d2` | `clamp_d` | One catalog key; only `d1=d2` rows are allowed | +| `d3` | `thread_d` | Exact selected-row lookup | +| `d4` | `body_d4` | Exact selected-row lookup | +| `l1` | `body_l1` | Exact selected-row lookup | +| `l2` | `lever_l2` | Exact selected-row lookup; Type B axial envelope | +| `l3` | `lever_l3` | Exact selected-row lookup; Type B radial envelope | +| `l4` | `jaw_l4` | Exact selected-row lookup; thickness of each complete body | +| `l5` | `gap_l5` | Exact selected-row lookup; separation between the bodies | +| `m` | `catalog_m` | Exact selected-row lookup; clamping-axis spacing | +| Type A/B | `actuator_type` | `0` = A, `1` = B | + +`refine()` copies all eight dependent dimensions from one row selected by +`clamp_d`. It never interpolates, independently samples, or mixes rows. + +## Table transcription and recomputation + +| `clamp_d` | `d3` | `d4` | `l1` | `l2` | `l3` | `l4` | `l5` | `m` | `2*l4+l5` | `d1+l5` | +|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| 8 | 6 | 28 | 33 | 35 | 45 | 14 | 5 | 13 | 33 | 13 | +| 10 | 8 | 32 | 45 | 45 | 63 | 20 | 5 | 15 | 45 | 15 | +| 12 | 8 | 36 | 47 | 45 | 63 | 21 | 5 | 17 | 47 | 17 | +| 14 | 8 | 46 | 57 | 55 | 78 | 25.5 | 6 | 20 | 57 | 20 | +| 15 | 10 | 46 | 57 | 55 | 78 | 25.5 | 6 | 21 | 57 | 21 | +| 16 | 10 | 46 | 57 | 55 | 78 | 25.5 | 6 | 22 | 57 | 22 | +| 18 | 10 | 56 | 63 | 55 | 78 | 28.5 | 6 | 24 | 63 | 24 | +| 20 | 10 | 56 | 65 | 55 | 78 | 28.5 | 8 | 28 | 65 | 28 | + +The two identities hold for every published equal-diameter row: + +- `l1 = 2*l4 + l5` +- `m = d1 + l5` + +The second identity places the implied clamped-rod centers one radius inside +the two faces adjacent to the central gap. No independent center-distance or +mixed row can be sampled. + +## Assembly axes and component map + +The central screw/stud axis is `Z`. The lower body occupies +`[-l1/2, -l5/2]`, and the upper body occupies `[+l5/2, +l1/2]`. The lower +V-groove axis is `X`; the upper body uses the same construction rotated 90 +degrees, so its V-groove axis is `Y`. This is the reference pose shown by the +catalog application graphic, not a claimed angular limit. + +`build()` returns a named `cq.Assembly` whose seven child names exactly match +`family.json.components`: + +1. `lower_clamp_body` +2. `upper_clamp_body` +3. `actuator` +4. `hex_nut` +5. `distance_bushing` +6. `lower_compression_spring` +7. `upper_compression_spring` + +Each child is produced by an independent `build_()` function. The +two clamp bodies are complete physical cast bodies, not four artificial axial +jaw slices. `actuator` is either the Type A socket screw or the Type B +adjustable-lever/stud purchased component. The DIN 934-like nut is an external +part. Both clamp bodies have only a round central passage; their outer faces +have no counterbore or hexagonal recess. + +The nut and actuator head/hub touch the two outer body faces without positive +volume overlap. The round stud passes through round proportional-clearance +bores. The stud extends through nearly the full nut thickness. One circular +`distance_bushing` is centered in `l5` and has only its central round stud +bore—there is no eccentric hole. Two separate helical pressure springs are +coaxial with the actuator, one below and one above that bushing. + +## Published Type B envelope + +The drawing defines two orthogonal outer projections: + +- `l2`: axial reach from the actuator-side body face to the handle's furthest + axial edge; +- `l3`: radial reach from the central fastener axis to the handle's furthest + radial edge. + +The handle centerline is inclined in the axial/radial plane. Its rounded end is +backed off by the end radius so the finished solid, rather than only its +centerline, reaches the published `l2` and `l3` limits. The direction follows +the Type B drawing and product image. Local hub, shoulder, taper, thickness, +and rounding remain proportions. + +## Proportion formulas + +The following values are not published GN 490 dimensions: + +| Detail | Formula | Purpose | +|---|---|---| +| Body passage radius | `0.60*d3` | Clear the nominal round stud | +| V-groove radial center | `-(0.50*d1 + 0.60*d3 + 0.02*d1)` | Clear the central stud passage while keeping the rod inside `d4` | +| V-groove depth | `(0.50 + 1/sqrt(2))*d1 + 0.02*d1` | 90-degree V tangent geometry plus proportional clearance | +| V-groove mouth | `2*groove_depth` | Contain the full nominal rod proxy at the inner face | +| Body end fillet | `min(0.035*d4, 0.18*l4)` | Approximate the rounded casting edge | +| Type A shaft/head | radii `0.50*d3`, `0.80*d3`; head height `0.85*d3` | Simplified DIN 912 envelope | +| Type A socket | hex circumdiameter `0.55*d3`; depth `0.45*head_h` | Visible socket only | +| Nut | height `0.80*d3`; circumdiameter `1.80*d3`; bore radius `0.54*d3` | Simplified separate DIN 934 envelope | +| Distance bushing | radius `0.46*d4`; height `0.24*l5` | Single visible circular middle separator | +| Distance-plate stud bore | `0.50*d3 + max(0.10, 0.02*d3)` | Clear the round stud | +| Each spring height | `0.30*l5` | Fit one spring on each side of the bushing | +| Spring mean/wire radius | `0.72*d3`; `min(0.045*d3, 0.035*l5)` | Clear the actuator and remain non-degenerate | +| Spring pitch | `spring_h/2.5` | 2.5-turn helical sweep without self-intersection | +| Spring/bushing/body clearance | `0.04*l5` | Separate all three central components | +| Type B handle thickness | `max(0.70*d3, 0.14*d4)` | Flat die-cast lever section | +| Type B handle width | `1.40*handle_t` | Simplified die-cast section | +| Type B profile thickness | `1.18*handle_t` at hub, `0.92*handle_t` at end | Simplified taper | +| Type B hub | height `1.35*d3`; radius derived from stud/handle width | Connect stud and handle | + +Each pressure spring is a genuine CadQuery helix sweep. All 16 legal +catalog/actuator combinations produce two valid, non-degenerate spring solids +in the pinned CadQuery/OCP environment. + +## Geometry and interference acceptance + +Local acceptance must cover every one of the 16 allowed catalog/actuator +combinations (eight rows times Types A/B): + +- Assembly root and seven semantic child names are stable. +- Every component contains exactly one non-degenerate solid. +- `family.json.solids == len(family.json.components) == 7`. +- Every pair of physical components has zero positive intersection volume. +- Round stud, body passage, nut bore, and bushing bore retain their documented + proportional clearances. +- The central distance bushing and both coaxial springs are mutually separated + by their modeled axial clearances. +- Both clamp bodies retain positive material behind the V-groove and around + the central passage. +- A full-diameter nominal rod proxy at each published axis has zero positive + intersection volume with its clamp body, the stud passage, and the middle + distance plate. +- Type B actuator bounding boxes reproduce the published `l2`/`l3` envelopes. + +`bench2 validate` is still the required final machine gate, but it does not +validate Assembly child names or pairwise collision volume. Those checks are +therefore included in the local acceptance report. + +## Deliberate deviations and unresolved source detail + +- Casting draft, texture, local ribs, small blends, chamfers, and parting lines + are omitted. +- The V-groove angle, tip radius, depth, radial offset, and mouth are not + dimensioned; the model records them as `proportion`. +- Threads, thread runout, exact DIN fastener tolerances, lever ratchet internals, + and surface finishes are omitted. +- The single circular middle separator is a visual/proportional interpretation + of the catalog image and the listed insert/distance-bushing item; its local + profile is not dimensioned. It has no eccentric opening. +- The two pressure springs' exact wire, turn count, pitch, and end treatment are + not published. Their coaxial helical geometry is proportional. +- MT/SW finish and the excluded stainless-steel NI material do not change CAD. +- Context rods/tubes are omitted from the seven delivered components. +- No swivel motion, allowable angle, load rating, fit class, or tolerance is + invented. + +The two-body topology, plain outer body faces, one central distance bushing, +and two coaxial springs supersede the earlier local +four-jaw/one-offset-spring approximation. This is a user-authorized visual +correction made during local acceptance and must be called out in the eventual +PR. diff --git a/designs/swivel_clamp_connector_joint/family.json b/designs/swivel_clamp_connector_joint/family.json new file mode 100644 index 0000000..fc8ad54 --- /dev/null +++ b/designs/swivel_clamp_connector_joint/family.json @@ -0,0 +1,18 @@ +{ + "family": "swivel_clamp_connector_joint", + "standard": "JW Winco GN 490", + "base_plane": "XY", + "description": "GN 490 aluminum swivel clamp connector joint in a fixed reference pose, with two complete clamp bodies, one central distance bushing, two coaxial springs, actuator, and hex nut.", + "source": "JW Winco GN 490 product page, official metric dimension PDF, and documented proportion rules in NOTES.md", + "contributor": "sunzhenchuan007", + "solids": 7, + "components": [ + "lower_clamp_body", + "upper_clamp_body", + "actuator", + "hex_nut", + "distance_bushing", + "lower_compression_spring", + "upper_compression_spring" + ] +} diff --git a/designs/swivel_clamp_connector_joint/part.py b/designs/swivel_clamp_connector_joint/part.py new file mode 100644 index 0000000..e305d8a --- /dev/null +++ b/designs/swivel_clamp_connector_joint/part.py @@ -0,0 +1,395 @@ +"""JW Winco GN 490 swivel clamp connector joint (seven-component assembly). + +Published GN 490 table values set the two clamp-body envelopes and the +actuator envelopes. Unpublished V-groove, clearance, spring, bushing, and +secondary fastener details use the proportion rules recorded in NOTES.md. +""" + +import math + +import cadquery as cq + + +def _cylinder(radius, height, z0): + return ( + cq.Workplane("XY") + .circle(radius) + .extrude(height) + .translate((0.0, 0.0, z0)) + ) + + +def _clamp_body( + z0, + inner_face_z, + into_sign, + tube_axis, + body_d4, + jaw_l4, + clamp_d, + thread_d, +): + """One complete cast clamp body with an end-entry V-groove.""" + body_r = body_d4 / 2.0 + passage_r = 0.60 * thread_d + groove_clearance = 0.02 * clamp_d + groove_center = -( + 0.50 * clamp_d + passage_r + groove_clearance + ) + groove_depth = (0.50 + math.sqrt(0.50)) * clamp_d + groove_clearance + groove_mouth = 2.0 * groove_depth + + edge_r = min(0.035 * body_d4, 0.18 * jaw_l4) + body = _cylinder(body_r, jaw_l4, z0).edges("%Circle").fillet(edge_r) + passage = _cylinder(passage_r, jaw_l4 + 2.0, z0 - 1.0) + + # The rod axis is X in one body and Y in the other. In the normal plane, + # the triangular prism enters from the face adjacent to the central l5 gap. + # Its trough is radially offset, matching the catalog section and photo. + points = [ + (groove_center - groove_mouth / 2.0, inner_face_z), + (groove_center + groove_mouth / 2.0, inner_face_z), + (groove_center, inner_face_z + into_sign * groove_depth), + ] + plane = "YZ" if int(tube_axis) == 0 else "XZ" + groove = ( + cq.Workplane(plane) + .polyline(points) + .close() + .extrude(body_d4 * 0.56, both=True) + ) + + result = body.cut(passage).cut(groove) + return result + + +def build_lower_clamp_body( + body_l1, + body_d4, + jaw_l4, + gap_l5, + catalog_m, + clamp_d, + thread_d, +): + """Lower complete GN 490 clamp body; V-groove axis is X.""" + z0 = -body_l1 / 2.0 + inner_face_z = -catalog_m / 2.0 + clamp_d / 2.0 + result = _clamp_body( + z0, + inner_face_z, + -1.0, + 0, + body_d4, + jaw_l4, + clamp_d, + thread_d, + ) + return result + + +def build_upper_clamp_body( + body_l1, + body_d4, + jaw_l4, + gap_l5, + catalog_m, + clamp_d, + thread_d, +): + """Upper complete GN 490 clamp body, rotated 90 degrees about the stud.""" + z0 = gap_l5 / 2.0 + inner_face_z = catalog_m / 2.0 - clamp_d / 2.0 + result = _clamp_body( + z0, + inner_face_z, + 1.0, + 1, + body_d4, + jaw_l4, + clamp_d, + thread_d, + ) + return result + + +def _type_a_actuator(body_l1, thread_d): + """DIN 912-like socket-head screw, separate from the DIN 934 nut.""" + head_h = 0.85 * thread_d + head_r = 0.80 * thread_d + shaft_r = 0.50 * thread_d + nut_h = 0.80 * thread_d + shaft_bottom = -body_l1 / 2.0 - nut_h + 0.08 * thread_d + head_bottom = body_l1 / 2.0 + + shaft = _cylinder( + shaft_r, + head_bottom + 0.12 * head_h - shaft_bottom, + shaft_bottom, + ) + head = _cylinder(head_r, head_h, head_bottom) + socket = ( + cq.Workplane("XY") + .polygon(6, 0.55 * thread_d) + .extrude(0.45 * head_h) + .translate((0.0, 0.0, head_bottom + 0.60 * head_h)) + ) + result = shaft.union(head.cut(socket)) + return result + + +def _type_b_actuator(body_l1, body_d4, thread_d, lever_l2, lever_l3): + """Adjustable-lever/stud component with the published l2/l3 envelope.""" + hub_h = 1.35 * thread_d + handle_t = max(0.70 * thread_d, 0.14 * body_d4) + handle_w = 1.40 * handle_t + handle_t0 = 1.18 * handle_t + handle_t1 = 0.92 * handle_t + end_r = handle_t1 / 2.0 + hub_r = max(0.90 * thread_d, 0.62 * handle_w) + z_top = body_l1 / 2.0 + nut_h = 0.80 * thread_d + shaft_bottom = -body_l1 / 2.0 - nut_h + 0.08 * thread_d + + shaft = _cylinder( + 0.50 * thread_d, + z_top + 0.18 * hub_h - shaft_bottom, + shaft_bottom, + ) + hub = _cylinder(hub_r, hub_h, z_top) + + # Keep the inclination established from the two published orthogonal outer + # projections. The rounded finished end, not only its centerline, reaches + # x=l3 and z=body_l1/2+l2. + start_x = 0.35 * hub_r + start_z = z_top + 0.70 * hub_h + end_x = lever_l3 - end_r + end_z = body_l1 / 2.0 + lever_l2 - end_r + dx = end_x - start_x + dz = end_z - start_z + centerline_len = math.sqrt(dx * dx + dz * dz) + normal_x = -dz / centerline_len + normal_z = dx / centerline_len + handle = ( + cq.Workplane("XZ") + .polyline( + [ + ( + start_x + normal_x * handle_t0 / 2.0, + start_z + normal_z * handle_t0 / 2.0, + ), + ( + end_x + normal_x * handle_t1 / 2.0, + end_z + normal_z * handle_t1 / 2.0, + ), + ( + end_x - normal_x * handle_t1 / 2.0, + end_z - normal_z * handle_t1 / 2.0, + ), + ( + start_x - normal_x * handle_t0 / 2.0, + start_z - normal_z * handle_t0 / 2.0, + ), + ] + ) + .close() + .extrude(handle_w / 2.0, both=True) + ) + end = ( + cq.Workplane("XZ") + .center(end_x, end_z) + .circle(end_r) + .extrude(handle_w / 2.0, both=True) + ) + handle = handle.union(end).edges().fillet(0.14 * handle_t) + button_h = 0.15 * hub_h + button = _cylinder( + 0.34 * hub_r, + button_h, + z_top + hub_h - 0.05 * button_h, + ) + result = shaft.union(hub).union(handle).union(button) + return result + + +def build_actuator( + actuator_type, + body_l1, + body_d4, + thread_d, + lever_l2, + lever_l3, +): + """Published Type A screw or Type B adjustable-lever/stud component.""" + if int(actuator_type) == 0: + result = _type_a_actuator(body_l1, thread_d) + else: + result = _type_b_actuator( + body_l1, + body_d4, + thread_d, + lever_l2, + lever_l3, + ) + return result + + +def build_hex_nut(body_l1, thread_d): + """Separate DIN 934-like hex nut with a round thread-envelope bore.""" + nut_h = 0.80 * thread_d + nut_circum_d = 1.80 * thread_d + nut_top = -body_l1 / 2.0 + outer = ( + cq.Workplane("XY") + .polygon(6, nut_circum_d) + .extrude(nut_h) + .translate((0.0, 0.0, nut_top - nut_h)) + ) + bore = _cylinder( + 0.54 * thread_d, + nut_h + 0.4, + nut_top - nut_h - 0.2, + ) + result = outer.cut(bore) + return result + + +def build_distance_bushing(gap_l5, body_d4, thread_d): + """Single central circular distance bushing with no eccentric opening.""" + clearance = max(0.10, 0.02 * thread_d) + height = 0.24 * gap_l5 + outer_r = 0.46 * body_d4 + z0 = -height / 2.0 + outer = _cylinder(outer_r, height, z0) + bore = _cylinder( + 0.50 * thread_d + clearance, + height + 0.4, + z0 - 0.2, + ) + result = outer.cut(bore) + return result + + +def _compression_spring(z0, spring_h, gap_l5, thread_d): + """One coaxial helical compression spring around the actuator.""" + coil_r = 0.72 * thread_d + wire_r = min(0.045 * thread_d, 0.035 * gap_l5) + pitch = spring_h / 2.5 + path = cq.Wire.makeHelix(pitch, spring_h, coil_r) + result = ( + cq.Workplane("XZ") + .center(coil_r, 0.0) + .circle(wire_r) + .sweep(path, isFrenet=True) + .translate((0.0, 0.0, z0)) + ) + return result + + +def build_lower_compression_spring(gap_l5, thread_d): + """Lower spring between the lower body and central distance bushing.""" + bushing_h = 0.24 * gap_l5 + spring_h = 0.30 * gap_l5 + clearance = 0.04 * gap_l5 + z0 = -bushing_h / 2.0 - clearance - spring_h + result = _compression_spring(z0, spring_h, gap_l5, thread_d) + return result + + +def build_upper_compression_spring(gap_l5, thread_d): + """Upper spring between the central distance bushing and upper body.""" + bushing_h = 0.24 * gap_l5 + spring_h = 0.30 * gap_l5 + clearance = 0.04 * gap_l5 + z0 = bushing_h / 2.0 + clearance + result = _compression_spring(z0, spring_h, gap_l5, thread_d) + return result + + +def build( + clamp_d, + actuator_type, + thread_d, + body_d4, + body_l1, + lever_l2, + lever_l3, + jaw_l4, + gap_l5, + catalog_m, +): + """Build the GN 490 reference pose as seven named physical components.""" + lower_clamp_body = build_lower_clamp_body( + body_l1, + body_d4, + jaw_l4, + gap_l5, + catalog_m, + clamp_d, + thread_d, + ) + upper_clamp_body = build_upper_clamp_body( + body_l1, + body_d4, + jaw_l4, + gap_l5, + catalog_m, + clamp_d, + thread_d, + ) + actuator = build_actuator( + actuator_type, + body_l1, + body_d4, + thread_d, + lever_l2, + lever_l3, + ) + hex_nut = build_hex_nut(body_l1, thread_d) + distance_bushing = build_distance_bushing(gap_l5, body_d4, thread_d) + lower_compression_spring = build_lower_compression_spring( + gap_l5, + thread_d, + ) + upper_compression_spring = build_upper_compression_spring( + gap_l5, + thread_d, + ) + + result = cq.Assembly(name="swivel_clamp_connector_joint") + result.add( + lower_clamp_body, + name="lower_clamp_body", + color=cq.Color(0.64, 0.66, 0.68), + ) + result.add( + upper_clamp_body, + name="upper_clamp_body", + color=cq.Color(0.72, 0.74, 0.76), + ) + result.add( + actuator, + name="actuator", + color=cq.Color(0.72, 0.74, 0.76), + ) + result.add( + hex_nut, + name="hex_nut", + color=cq.Color(0.48, 0.50, 0.52), + ) + result.add( + distance_bushing, + name="distance_bushing", + color=cq.Color(0.88, 0.82, 0.36), + ) + result.add( + lower_compression_spring, + name="lower_compression_spring", + color=cq.Color(0.38, 0.42, 0.46), + ) + result.add( + upper_compression_spring, + name="upper_compression_spring", + color=cq.Color(0.48, 0.52, 0.56), + ) + return result diff --git a/designs/swivel_clamp_connector_joint/preview.png b/designs/swivel_clamp_connector_joint/preview.png new file mode 100644 index 0000000..b8055d0 Binary files /dev/null and b/designs/swivel_clamp_connector_joint/preview.png differ diff --git a/designs/swivel_clamp_connector_joint/preview_extremes.png b/designs/swivel_clamp_connector_joint/preview_extremes.png new file mode 100644 index 0000000..09a5325 Binary files /dev/null and b/designs/swivel_clamp_connector_joint/preview_extremes.png differ diff --git a/designs/swivel_clamp_connector_joint/preview_parts.png b/designs/swivel_clamp_connector_joint/preview_parts.png new file mode 100644 index 0000000..ae899e9 Binary files /dev/null and b/designs/swivel_clamp_connector_joint/preview_parts.png differ diff --git a/designs/swivel_clamp_connector_joint/preview_views.png b/designs/swivel_clamp_connector_joint/preview_views.png new file mode 100644 index 0000000..ac44873 Binary files /dev/null and b/designs/swivel_clamp_connector_joint/preview_views.png differ diff --git a/designs/swivel_clamp_connector_joint/spec.py b/designs/swivel_clamp_connector_joint/spec.py new file mode 100644 index 0000000..f9f1255 --- /dev/null +++ b/designs/swivel_clamp_connector_joint/spec.py @@ -0,0 +1,290 @@ +"""Sampling contract for the JW Winco GN 490 aluminum family. + +All published dimensions are selected as one indivisible catalog row. The +sampler may choose only clamp diameter and actuator type; refine() fills every +dependent dimension from that same row. +""" + + +DATASHEET = ( + "JW Winco GN 490 official metric dimension PDF " + "https://live-catalog.jwwinco.com/pdf/winco/us/490.pdf" +) + +MODEL_ROWS = [ + dict(clamp_d=8, thread_d=6, body_d4=28, body_l1=33, lever_l2=35, + lever_l3=45, jaw_l4=14, gap_l5=5, catalog_m=13), + dict(clamp_d=10, thread_d=8, body_d4=32, body_l1=45, lever_l2=45, + lever_l3=63, jaw_l4=20, gap_l5=5, catalog_m=15), + dict(clamp_d=12, thread_d=8, body_d4=36, body_l1=47, lever_l2=45, + lever_l3=63, jaw_l4=21, gap_l5=5, catalog_m=17), + dict(clamp_d=14, thread_d=8, body_d4=46, body_l1=57, lever_l2=55, + lever_l3=78, jaw_l4=25.5, gap_l5=6, catalog_m=20), + dict(clamp_d=15, thread_d=10, body_d4=46, body_l1=57, lever_l2=55, + lever_l3=78, jaw_l4=25.5, gap_l5=6, catalog_m=21), + dict(clamp_d=16, thread_d=10, body_d4=46, body_l1=57, lever_l2=55, + lever_l3=78, jaw_l4=25.5, gap_l5=6, catalog_m=22), + dict(clamp_d=18, thread_d=10, body_d4=56, body_l1=63, lever_l2=55, + lever_l3=78, jaw_l4=28.5, gap_l5=6, catalog_m=24), + dict(clamp_d=20, thread_d=10, body_d4=56, body_l1=65, lever_l2=55, + lever_l3=78, jaw_l4=28.5, gap_l5=8, catalog_m=28), +] + +DIFFICULTY_DIAMETERS = { + # Choice order is intentionally deterministic-seed friendly: it changes + # no allowed catalog row, but prevents the validator's fixed preview seeds + # from collapsing onto the same few rows across all three difficulties. + "easy": [16, 15, 14], + "medium": [10, 12, 14, 15, 16, 18], + "hard": [10, 12, 14, 20, 15, 16, 8, 18], +} + +DIFFICULTY_ACTUATORS = { + "easy": [0], + "medium": [0, 1], + "hard": [0, 1], +} + +ROW_KEYS = ( + "thread_d", + "body_d4", + "body_l1", + "lever_l2", + "lever_l3", + "jaw_l4", + "gap_l5", + "catalog_m", +) + + +def _rows_for(difficulty): + allowed = set(DIFFICULTY_DIAMETERS[difficulty]) + return [row for row in MODEL_ROWS if row["clamp_d"] in allowed] + + +def _row_range(name): + return { + difficulty: ( + min(row[name] for row in _rows_for(difficulty)), + max(row[name] for row in _rows_for(difficulty)), + ) + for difficulty in DIFFICULTY_DIAMETERS + } + + +def _selected_row(p): + diameter = int(p["clamp_d"]) + for row in MODEL_ROWS: + if row["clamp_d"] == diameter: + return row + return None + + +PARAM_SPEC = { + "clamp_d": dict( + desc="published equal clamping diameters d1=d2; complete-row selector", + unit="mm", + range={"easy": (14, 16), "medium": (10, 18), "hard": (8, 20)}, + choices=DIFFICULTY_DIAMETERS, + integer=True, + coverage=[8, 10, 12, 14, 15, 16, 18, 20], + source=f"{DATASHEET}; published d1=d2 rows only", + askable=True, + ), + "actuator_type": dict( + desc="published actuator variant: 0=Type A DIN 912 socket screw, 1=Type B adjustable lever", + unit="", + range={"easy": (0, 0), "medium": (0, 1), "hard": (0, 1)}, + choices=DIFFICULTY_ACTUATORS, + integer=True, + coverage=[0, 1], + feature=True, + source=f"{DATASHEET}; Type A and Type B order-code field", + ), + "thread_d": dict( + desc="nominal metric thread diameter d3", + unit="mm", + range=_row_range("thread_d"), + refine=True, + source=f"{DATASHEET}; d3 from the row selected by clamp_d", + askable=True, + ), + "body_d4": dict( + desc="outside cylindrical body diameter d4", + unit="mm", + range=_row_range("body_d4"), + refine=True, + source=f"{DATASHEET}; d4 from the row selected by clamp_d", + askable=True, + ), + "body_l1": dict( + desc="overall two-body assembly length l1 along the central fastener axis", + unit="mm", + range=_row_range("body_l1"), + refine=True, + source=f"{DATASHEET}; l1 from the row selected by clamp_d", + askable=True, + ), + "lever_l2": dict( + desc="published Type B axial envelope l2 beyond the actuator-side body face", + unit="mm", + range=_row_range("lever_l2"), + refine=True, + source=f"{DATASHEET}; l2 from the row selected by clamp_d", + askable=True, + ), + "lever_l3": dict( + desc="published Type B radial envelope l3 from the fastener axis to the handle end", + unit="mm", + range=_row_range("lever_l3"), + refine=True, + source=f"{DATASHEET}; l3 from the row selected by clamp_d", + askable=True, + ), + "jaw_l4": dict( + desc="axial thickness l4 of each complete clamp body", + unit="mm", + range=_row_range("jaw_l4"), + refine=True, + source=f"{DATASHEET}; l4 from the row selected by clamp_d", + askable=True, + ), + "gap_l5": dict( + desc="published central separation l5 between the clamp sections", + unit="mm", + range=_row_range("gap_l5"), + refine=True, + source=f"{DATASHEET}; l5 from the row selected by clamp_d", + askable=True, + ), + "catalog_m": dict( + desc="published spacing m between the two clamping axes", + unit="mm", + range=_row_range("catalog_m"), + refine=True, + source=f"{DATASHEET}; m from the row selected by clamp_d", + askable=True, + ), +} + + +def refine(p, difficulty, rng): + """Fill every coupled dimension from the selected official table row.""" + row = _selected_row(p) + if row is None: + return + for name in ROW_KEYS: + p[name] = row[name] + + +def _row_consistency_errors(p): + row = _selected_row(p) + if row is None: + return ["clamp_d does not select a published GN 490 metric row"] + bad = [] + for name in ROW_KEYS: + if name in p and abs(float(p[name]) - float(row[name])) > 1e-9: + bad.append(f"{name} must match the GN 490 row selected by clamp_d") + return bad + + +def check(p): + """Engineering and topology constraints for the documented reference pose.""" + bad = _row_consistency_errors(p) + + if int(p["actuator_type"]) not in (0, 1): + bad.append("actuator_type must be published GN 490 Type A or Type B") + + # The official table obeys l1 = 2*l4 + l5 in every row. Keeping this exact + # prevents a mixed or mistyped row from moving the body end faces. + if abs(p["body_l1"] - (2.0 * p["jaw_l4"] + p["gap_l5"])) > 1e-9: + bad.append("body_l1 must equal 2*jaw_l4 + gap_l5 (GN 490 table identity)") + if abs(p["catalog_m"] - (p["clamp_d"] + p["gap_l5"])) > 1e-9: + bad.append("catalog_m must equal clamp_d + gap_l5 (GN 490 table identity)") + + # The end-entry V-groove dimensions and radial offset are unpublished + # proportions. Each complete l4-thick body remains one connected solid. + groove_clearance = 0.02 * p["clamp_d"] + passage_r = 0.60 * p["thread_d"] + groove_center = ( + 0.50 * p["clamp_d"] + passage_r + groove_clearance + ) + groove_depth = ( + (0.50 + 2.0 ** -0.5) * p["clamp_d"] + groove_clearance + ) + groove_mouth = 2.0 * groove_depth + if groove_depth >= 0.90 * p["jaw_l4"]: + bad.append("V-groove depth must retain an axial back wall in each l4 body (proportion)") + if groove_mouth <= 2.40 * p["clamp_d"]: + bad.append("V-groove mouth must admit the selected clamping diameter (proportion)") + if groove_center + 0.50 * p["clamp_d"] >= p["body_d4"] / 2.0: + bad.append("nominal clamped rod must remain inside the d4 body envelope (geometry)") + + # The passage is proportioned from d3; GN 490 publishes the thread, not + # the body through-hole. It clears the stud while retaining a web to the + # radially offset V-groove. + shaft_r = 0.50 * p["thread_d"] + radial_wall = p["body_d4"] / 2.0 - passage_r + if passage_r <= shaft_r: + bad.append("central passage must clear the nominal actuator shaft (proportion)") + if radial_wall <= 0.25 * p["clamp_d"]: + bad.append("central passage must retain >0.25*clamp_d radial jaw wall (proportion)") + if ( + groove_center + - 0.50 * p["clamp_d"] + - passage_r + + 1e-9 + < groove_clearance + ): + bad.append("V-groove must retain a positive web to the central passage (proportion)") + + # One central distance bushing and two coaxial springs fit in l5. The + # bushing has only its central stud bore; there is no eccentric opening. + bushing_h = 0.24 * p["gap_l5"] + bushing_outer_r = 0.46 * p["body_d4"] + bushing_bore_r = 0.50 * p["thread_d"] + max(0.10, 0.02 * p["thread_d"]) + spring_h = 0.30 * p["gap_l5"] + spring_clearance = 0.04 * p["gap_l5"] + coil_r = 0.72 * p["thread_d"] + wire_r = min(0.045 * p["thread_d"], 0.035 * p["gap_l5"]) + pitch = spring_h / 2.5 + if bushing_h >= p["gap_l5"]: + bad.append("distance bushing must fit inside the central l5 gap (proportion)") + if bushing_outer_r <= bushing_bore_r: + bad.append("distance bushing must retain positive annular wall (proportion)") + if bushing_h + 2.0 * spring_h + 4.0 * spring_clearance > p["gap_l5"] + 1e-9: + bad.append("two springs, central bushing, and clearances must fit inside l5 (proportion)") + if coil_r - wire_r <= shaft_r + max(0.10, 0.02 * p["thread_d"]): + bad.append("both coaxial springs must clear the actuator shaft (proportion)") + if coil_r + wire_r >= p["body_d4"] / 2.0: + bad.append("both spring outer envelopes must remain inside d4 (proportion)") + if spring_clearance <= wire_r: + bad.append("spring end clearance must exceed wire radius (non-interference)") + if pitch <= 2.0 * wire_r: + bad.append("helical spring pitch must exceed wire diameter (non-self-intersection)") + + # The DIN 934-like nut is a separate component, not a fabricated hex + # capture pocket. Its round bore clears the round stud envelope. + if 0.54 * p["thread_d"] <= shaft_r: + bad.append("separate hex-nut bore must clear the round stud envelope (proportion)") + + # Type B l2 and l3 are orthogonal published outer-envelope projections. + # Local hub and handle section sizes are proportions; their final bounding + # box is checked against l2/l3 by the exhaustive local geometry audit. + if int(p["actuator_type"]) == 1: + hub_h = 1.35 * p["thread_d"] + handle_t = max(0.70 * p["thread_d"], 0.14 * p["body_d4"]) + end_r = 0.46 * handle_t + start_bottom = ( + p["body_l1"] / 2.0 + + 0.70 * hub_h + - 0.59 * handle_t + ) + if p["lever_l2"] <= hub_h + 2.0 * end_r: + bad.append("Type B l2 must exceed local hub and rounded-end sizes (published envelope + proportion)") + if p["lever_l3"] <= 2.0 * end_r: + bad.append("Type B l3 must leave positive radial handle reach (published envelope + proportion)") + if start_bottom <= p["body_l1"] / 2.0: + bad.append("Type B handle must clear the upper jaw in the fixed reference pose (geometry)") + + return bad