"""Small Jimny V2 fit coupons; run with existing V2 venv, python -B. Writes ONLY beside this file. No parent module imports or parent export adapter. All exports are individual, bed-oriented, XYZ millimetres. See README.md. """ from pathlib import Path import ast import hashlib import json import math import numpy as np import trimesh from build123d import (Align, Axis, Box, Cylinder, Plane, Polygon, Pos, extrude, export_step, import_step) OUT = Path(__file__).resolve().parent PROFILE_SOURCE = OUT.parent.parent / 'build_models.py' C = (Align.CENTER, Align.CENTER, Align.MIN) def box(x, y, z): return Box(x, y, z, align=C) def cyl(r, h): return Cylinder(r, h, align=C) def volume(shape): return sum(s.volume for s in shape.solids()) if shape else 0.0 def overlap(a, b): return volume(a.intersect(b)) def digit(shape, n, x, y, top): """Engraved, disconnected seven-segment strokes; never separate solids.""" segments = {'a': (0, 2, 2.8, .6), 'g': (0, 0, 2.8, .6), 'd': (0, -2, 2.8, .6), 'b': (1.2, 1, .6, 1.5), 'c': (1.2, -1, .6, 1.5), 'e': (-1.2, -1, .6, 1.5), 'f': (-1.2, 1, .6, 1.5)} for key in {1: 'bc', 2: 'abged', 3: 'abgcd'}[n]: dx, dy, w, h = segments[key] shape -= Pos(x + dx, y + dy, top - .6) * box(w, h, 1) # n countable dimples back up the numeral and act as tactile identification. for i in range(n): shape -= Pos(x - 3 + i * 3, y - 4, top - .6) * cyl(.65, 1) return shape def export_sample(shape, id, family, role, orientation, probes, **metadata): shape = shape.clean() assert shape.is_valid and len(shape.solids()) == 1, (id, 'source solid') assert abs(shape.bounding_box().min.Z) < 1e-7, (id, 'source bed datum') step = OUT / f'{id}.step' stl = OUT / f'{id}.stl' export_step(shape, step) imported = import_step(step) assert imported.is_valid and len(imported.solids()) == 1 cad = imported.solids()[0] # Avoid located-Compound boolean semantics. assert cad.is_valid and cad.volume > 0 assert abs(cad.volume - shape.volume) / shape.volume < 1e-6 bb = cad.bounding_box() assert abs(bb.min.Z) < 1e-6 and max(bb.size.X, bb.size.Y, bb.size.Z) <= 100 # Verify a real planar bed face, not merely a minimum vertex. bed_area = sum(f.area for f in cad.faces() if abs(f.bounding_box().min.Z) < 1e-6 and abs(f.bounding_box().max.Z) < 1e-6) assert bed_area > 10, (id, 'bed contact') probe_results = [] for name, point, expected in probes: got = bool(cad.is_inside(point)) assert got == expected, (id, name, point, got, expected) probe_results.append(dict(name=name, point_mm=point, material=got, passed=True)) vertices, faces = cad.tessellate(.035, .1) mesh = trimesh.Trimesh(vertices=np.round([[p.X, p.Y, p.Z] for p in vertices], 4), faces=faces, process=False) mesh.merge_vertices(digits_vertex=4) mesh.update_faces(mesh.nondegenerate_faces()) mesh.update_faces(mesh.unique_faces()) mesh.remove_unreferenced_vertices() assert mesh.is_watertight and mesh.volume > 0 and len(mesh.split()) == 1 mesh.export(stl) rt = trimesh.load_mesh(stl) assert rt.is_watertight and rt.is_winding_consistent and rt.volume > 0 assert len(rt.split()) == 1 and abs(rt.bounds[0, 2]) < .0001 assert max(rt.extents) <= 100 assert np.allclose(rt.bounds, [[bb.min.X, bb.min.Y, bb.min.Z], [bb.max.X, bb.max.Y, bb.max.Z]], atol=.04) # curved tessellation chord tolerance result = dict(id=id, family=family, role=role, files=dict(step=step.name, stl=stl.name), print_orientation=orientation, dimensions_mm=rt.extents.tolist(), bounds_mm=rt.bounds.tolist(), step_volume_mm3=cad.volume, stl_volume_mm3=float(rt.volume), step_valid=True, step_solids=1, stl_watertight=True, stl_components=1, stl_positive_volume=True, bed_contact_area_mm2=bed_area, probes=probe_results, sha256={p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in [step, stl]}, **metadata) print('VERIFIED', id, result['dimensions_mm'], flush=True) return cad, result def build(): source = PROFILE_SOURCE.read_text() tree = ast.parse(source) functions = {n.name: n for n in tree.body if isinstance(n, ast.FunctionDef)} excerpts = {name: ast.get_source_segment(source, functions[name]) for name in ['rail_profile', 'shoe_cut', 'capsule', 'swivel']} # Fail closed if parent changes the profiles used by these coupons. expected_fragments = [ 'box(12,6,2)', 'box(12,2,5)', 'Pos(x,sign*17,0)', 'Pos(x,sign*13.4,1.6)', 'Pos(0,0,6.4)', '31.4)*box(125,25,2.4)', '[0,0,29]', 'cyl(4,15.5)', 'cyl(1.7,34)', 'cyl(4.4,18)'] compact = source.replace(' ', '') for fragment in expected_fragments: assert fragment.replace(' ', '') in compact, ('profile changed', fragment) ns = dict(Plane=Plane, Polygon=Polygon, Pos=Pos, extrude=extrude) for name in ['rail_profile', 'shoe_cut']: exec(compile(ast.Module(body=[functions[name]], type_ignores=[]), str(PROFILE_SOURCE), 'exec'), ns) male_profile = ns['rail_profile'](32) exact_female = ns['shoe_cut'](26) samples, cads, checks = [], {}, [] for n, clearance in enumerate([.3, .4, .5], 1): # Identical actual 8-root / 14-crown / 3-rise male in all three pairs. male = box(32, 22, 3) + male_profile male += Pos(-21, 0, 0) * box(12, 22, 3) male = digit(male, n, -21, 2, 3) # Actual female template at 0.5. Change Y coordinates only for sweep. c = clearance cut = Pos(-13, 0, 0) * extrude(Plane.YZ * Polygon( (-4-c, -1), (4+c, -1), (4+c, -.4), (7+c, 2.6), (7+c, 3.2), (-7-c, 3.2), (-7-c, 2.6), (-4-c, -.4), align=None), amount=26) if c == .5: assert volume(cut - exact_female) < 1e-6 assert volume(exact_female - cut) < 1e-6 female_local = box(24, 22, 6.2) - cut # Handle stays above the rail base after assembly. female_local += Pos(-17, 0, 0) * box(12, 22, 6.2) # Do NOT block the channel with handle: continue the same cut through it. longcut = Pos(-31, 0, 0) * extrude(Plane.YZ * Polygon( (-4-c, -1), (4+c, -1), (4+c, -.4), (7+c, 2.6), (7+c, 3.2), (-7-c, 3.2), (-7-c, 2.6), (-4-c, -.4), align=None), amount=62) female_local -= longcut female = Pos(0, 0, 6.2) * female_local.rotate(Axis.X, 180) female += Pos(-17, 15, 0) * box(12, 10, 6.2) female = digit(female, n, -17, 15, 6.2) for shape, sex, probes in [ (male, 'male', [('crown material', (0, 6, 5.5), True), ('outside flank', (0, 7, 4), False), ('base material', (0, 9, 1), True), ('number engraving', (-19.8, 3, 2.7), False)]), (female, 'female', [('channel void', (0, 0, 4.5), False), ('45 degree flank void', (0, 6, 4.2), False), ('side material', (0, 9, 4.2), True), ('roof material', (0, 0, 1), True)])]: id = f'pod-{n}-{sex}-c{int(round(c*10)):02d}' cad, row = export_sample(shape, id, 'pod_dovetail', sex, 'Base on bed, rail up.' if sex == 'male' else 'Solid roof on bed, channel faces up; turn over to mate.', probes, pair_key=n, coordinate_flank_clearance_mm=c, normal_flank_clearance_mm=c/math.sqrt(2), crown_vertical_clearance_mm=.6, print_to_test=dict(rotation_x_deg=0 if sex == 'male' else 180, translation_mm=[0, 0, 0 if sex == 'male' else 9.6])) samples.append(row) cads[id] = cad m, f = [cads[f'pod-{n}-{sex}-c{int(round(c*10)):02d}'] for sex in ['male', 'female']] assembled = Pos(0, 0, 9.6) * f.rotate(Axis.X, 180) path = [] for dx in [-55, -35, -20, 0, 20, 35, 55]: hit = overlap(m, Pos(dx, 0, 0) * assembled) assert hit < 1e-6, ('pod slide', n, dx, hit) path.append(dict(x_mm=dx, overlap_mm3=hit)) lift_hit = overlap(m, Pos(0, 0, 2) * assembled) assert lift_hit > 1, ('pod capture', n) lateral_hit = overlap(m, Pos(0, c+.15, 0) * assembled) assert lateral_hit > .1 measured_distance = m.distance_to(assembled) assert abs(measured_distance - c/math.sqrt(2)) < 1e-5 checks.append(dict(family='pod_dovetail', pair_key=n, sliding_intersections=path, minimum_distance_mm=measured_distance, lift_2mm_intersection_mm3=lift_hit, lateral_overclearance_intersection_mm3=lateral_hit, passed=True)) # Current capsule uses 12x6x2 tabs, NOT the older brief's 12x8x2. tab_local = Pos(0, 17, 0)*box(12, 6, 2) tab_local += Pos(0, 13.4, 1.6)*box(12, 2, 5) tab_local += Pos(0, 16, 6.4)*box(12, 10, 4) # Constant-X section stands on its end: no unsupported lip overhang. tab = Pos(0, 0, 6)*tab_local.rotate(Axis.Y, 90) ledge_local = Pos(0, 24, 0)*box(32, 4, 7.4) ledge_local += Pos(0, 20, 5)*box(32, 10, 2.4) ledge = Pos(0, 0, 7.4)*ledge_local.rotate(Axis.X, 180) for shape, id, role, probes, transform in [ (tab, 'capsule-tab-2mm', 'tab', [ ('tab material', (1, 18, 6), True), ('stem material', (4, 13.4, 6), True), ('captured gap void', (4, 18, 6), False), ('lid material', (8, 16, 6), True)], dict(rotation_y_deg=-90, translation_mm=[6, 0, 2.6])), (ledge, 'capsule-ledge-04', 'ledge', [ ('ledge material', (0, -18, 1), True), ('underledge void', (0, -18, 4), False), ('backwall material', (0, -24, 5), True)], dict(rotation_x_deg=180, translation_mm=[0, 0, 7.4]))]: cad, row = export_sample(shape, id, 'capsule_tab_ledge', role, ('Constant section end on bed; 12 mm original X becomes print Z.' if role == 'tab' else 'Inverted: broad ledge face on bed. Turn over to test.'), probes, tab_dimensions_mm=[12, 6, 2], ledge_thickness_mm=2.4, underside_running_gap_mm=.4, stem_side_gap_mm=.6, print_to_test=transform) samples.append(row) cads[id] = cad t = Pos(6, 0, 2.6)*cads['capsule-tab-2mm'].rotate(Axis.Y, -90) l = Pos(0, 0, 7.4)*cads['capsule-ledge-04'].rotate(Axis.X, 180) path = [] for dx in [-32, -16, 0, 16, 32]: hit = overlap(l, Pos(dx, 0, 0)*t) assert hit < 1e-6 path.append(dict(x_mm=dx, overlap_mm3=hit)) lift_hit = overlap(l, Pos(0, 0, 1)*t) assert lift_hit > 1 assert abs(l.distance_to(t)-.4) < 1e-6 checks.append(dict(family='capsule_tab_ledge', sliding_intersections=path, running_distance_mm=l.distance_to(t), lift_1mm_intersection_mm3=lift_hit, passed=True)) sleeve = cyl(10, 3) + Pos(0, 0, 3)*cyl(4, 15.5) sleeve -= Pos(0, 0, -1)*cyl(1.7, 22) bore = cyl(10, 14.7) - Pos(0, 0, -1)*cyl(4.4, 18) for shape, id, role, probes, z in [ (sleeve, 'swivel-sleeve-8mm', 'sleeve', [ ('M3 bore void', (0, 0, 10), False), ('sleeve wall material', (3, 0, 10), True), ('outside sleeve void', (4.2, 0, 10), False), ('foot material', (8, 0, 1), True)], 0), (bore, 'swivel-bore-88mm', 'bore', [ ('8.8 bore void', (4.2, 0, 6), False), ('bore wall material', (5, 0, 6), True), ('bore center void', (0, 0, 6), False)], 3.4)]: cad, row = export_sample(shape, id, 'swivel_pivot', role, 'Axis vertical Z; flat annular foot on bed. Do not lay cylinder sideways.', probes, sleeve_od_mm=8, sleeve_id_mm=3.4, leaf_bore_diameter_mm=8.8, radial_clearance_mm=.4, diametral_clearance_mm=.8, print_to_test=dict(rotation_x_deg=0, translation_mm=[0, 0, z])) samples.append(row) cads[id] = cad s = cads['swivel-sleeve-8mm'] b = Pos(0, 0, 3.4)*cads['swivel-bore-88mm'] path = [] for dz in [0, 4, 10, 18]: hit = overlap(s, Pos(0, 0, dz)*b) assert hit < 1e-6 path.append(dict(z_mm=dz, overlap_mm3=hit)) eccentric = overlap(s, Pos(.6, 0, 0)*b) assert eccentric > .1 checks.append(dict(family='swivel_pivot', insertion_intersections=path, minimum_distance_mm=s.distance_to(b), eccentric_06mm_intersection_mm3=eccentric, passed=True)) assert len(samples) == 10 and len({p['id'] for p in samples}) == 10 # Guard concurrent upstream modification during build. assert PROFILE_SOURCE.read_text() == source, 'Parent profiles changed during build; rerun.' manifest = dict(schema_version=1, title='Jimny V2 local interface fit coupons', units='mm', coordinate_system='right-handed XYZ; Z up; each print artifact Zmin=0', status='digitally checked fit-test coupons; NOT physically validated', sample_count=len(samples), source=dict(path=str(PROFILE_SOURCE), sha256=hashlib.sha256(source.encode()).hexdigest(), excerpts=excerpts, capsule_tab_note='Current source is 12x6x2, not brief 12x8x2.'), samples=samples, interface_checks=checks, limitations=['Identification keys are numerals and counted dimples, not incompatible mating profiles.', 'Pod clearances are Y-coordinate offsets; normal flank gap is c/sqrt(2), not c.', 'Swivel 0.4 mm RADIAL means 0.8 mm DIAMETRAL clearance.', 'Capsule is one straight ledge/tab segment, not full release-window mechanism.', 'Local short-slot/rail coupons do not prove assembly stops, retention, heat resistance, wear or strength.', 'No physical printing, slicing or vehicle fit validation has been performed.']) (OUT/'manifest.json').write_text(json.dumps(manifest, indent=2)+'\n') print(json.dumps(dict(samples=len(samples), step_files=10, stl_files=10, interface_checks=len(checks), all_passed=True))) if __name__ == '__main__': build()