"""Validate photo-estimated lower steering sections, never fitted dimensions.

Run from repository root. Image spans are rough projection observations with
unresolved perspective; they do not calibrate a shaft or establish clearance.
"""
from pathlib import Path
import json,math,hashlib,subprocess,tempfile
import cadquery as cq
import numpy as np
from PIL import Image,ImageOps
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
CAD=Path('cars/1975-mg-midget/cad');BASE='b49b556c';S=2032/2.4
body=json.loads((CAD.parent/'body-data.json').read_text())
data=json.loads((CAD/'steering-rack-reference.json').read_text())
old=json.loads(subprocess.check_output(['git','show',BASE+':'+str(CAD/'steering-rack-reference.json')]))
def obj(a,n):
 s=a.objects[n].obj
 return s.val()if isinstance(s,cq.Workplane)else s
assembly=cq.Assembly.importStep(str(CAD/'steering-rack-reference.step'))
whole=cq.importers.importStep(str(CAD/'steering-rack-reference.step')).val()
assert whole.isValid()and len(whole.Solids())==45 and all(s.Volume()>0 for s in whole.Solids())
assert data['column_axis_mm']==old['column_axis_mm']and data['nodes']==old['nodes']
assert data['column_layout']==old['column_layout']
oldparts={p['name']:p for p in old['parts']}
same=[p['name']for p in data['parts']if p==oldparts[p['name']]]
changed=[p['name']for p in data['parts']if p!=oldparts[p['name']]]
assert len(same)==20 and len(changed)==4,(same,changed)
shaft=obj(assembly,'steering_column_retained_axis_smooth_shaft')
clamp=obj(assembly,'steering_split_pinion_coupling_envelope')
assert len(shaft.Solids())==len(clamp.Solids())==1
axis_points=data['column_axis_mm'];top,bottom=map(lambda p:cq.Vector(*p),axis_points);axis=(top-bottom).normalized()
cross=cq.Vector(*data['column_layout']['estimated_body_crossing_units'])*S
with tempfile.TemporaryDirectory(prefix='midget-shaft-section-')as tmp:
 p=Path(tmp)/'baseline.step';p.write_bytes(subprocess.check_output(['git','show',BASE+':'+str(CAD/'steering-rack-reference.step')]))
 former=obj(cq.Assembly.importStep(str(p)),'steering_column_retained_axis_smooth_shaft')
upper_guard=cq.Solid.makeCylinder(15,(top-cross).Length,cross,axis)
a,b=shaft.intersect(upper_guard),former.intersect(upper_guard)
assert a.cut(b).Volume()+b.cut(a).Volume()<1e-4
# Circular witness cuts well inside the exposed section and hidden upper shaft.
# Area establishes the intended reference section, not its fitted diameter.
mid=(bottom+axis*24+cross)*.5
slab=cq.Solid.makeCylinder(20,.5,mid,axis)
assert math.isclose(shaft.intersect(slab).Volume(),math.pi*8**2*.5,rel_tol=1e-6)
assert shaft.intersect(clamp).Volume()<1e-4
housing=obj(assembly,'steering_lhd_pinion_housing_and_input_shaft_comparison')
assert shaft.intersect(housing).Volume()<1e-4
# Keep mounting/engine frames. Record residual contacts with the actual source
# support geometry without shrinking/repositioning either assembly to erase them.
support=cq.Assembly.importStep(str(CAD/'front-power-support-reference.step'))
contacts={}
for name in ['front_left_rubber_and_bonded_face_comparison','front_left_frame_bracket_comparison','front_engine_plate_with_integral_bent_mounting_tabs_comparison']:
 obstacle=obj(support,name)
 for label,part in [('old_shaft',former),('current_shaft',shaft)]:
  contacts[label+'__'+name]=round(part.intersect(obstacle).Volume(),5)
held={}
for stem in ['column-assembly-reference','front-power-support-reference','midget-1500-measured-core','front-structure-reference','front-footwell-reference','wiper-drive-reference']:
 for ext in ['json','step']:
  p=CAD/(stem+'.'+ext);raw=p.read_bytes();assert raw==subprocess.check_output(['git','show',BASE+':'+str(p)]),p
  held[p.name]=hashlib.sha256(raw).hexdigest()
photo_path=Path('cars/assets/photos/IMG_7041.jpeg');raw=Image.open(photo_path);orientation=raw.getexif().get(274)
photo=ImageOps.exif_transpose(raw).resize((1800,1350))
# Coarse apparent spans on the EXIF-normalized working image. The PF53 marking
# is owner evidence;77mm is the existing catalog comparison, not an owner reading.
shaft_spans=[[[780,654],[774,703]],[[1010,679],[1003,731]]]
filter_span=[[940,510],[1210,510]]
length=lambda p:float(np.linalg.norm(np.array(p[1])-p[0]))
proxies=[77*length(p)/length(filter_span)for p in shaft_spans]
fig,ax=plt.subplots(figsize=(10,7),dpi=135);ax.imshow(photo)
for i,p in enumerate(shaft_spans):
 q=np.array(p);ax.plot(*q.T,color='#ffdc3b',lw=2);ax.text(q[0,0]+9,q[0,1]-8,'shaft apparent span '+str(i+1),fontsize=8,color='#ffdc3b')
q=np.array(filter_span);ax.plot(*q.T,color='#00aaff',lw=2);ax.text(q[0,0],q[0,1]-15,'PF53 apparent span /77mm catalog comparison',fontsize=8,color='#00aaff')
ax.set(xlim=(700,1260),ylim=(800,340),title='IMG_7041 · EXIF-normalized projection observations; no calibrated scale or fitted diameter')
fig.tight_layout();fig.savefig(CAD.parent/'steering-study/lower-shaft-photo.png');plt.close(fig)
report=dict(schema='lower-steering-section-validation-v1',baseline_commit=BASE,scope='Estimated exposed lower shaft and compact rack-entry geometry, not fitted diameter, alignment, load or clearance.',source_groups=24,valid_positive_solids=45,changed_source_groups=changed,unchanged_source_groups=same,axis_endpoints_nodes_and_layout_held=True,hidden_upper_shaft_geometry_held=True,exposed_reference_section_diameter_mm=16,shaft_and_coupling_connected_solids=True,source_shaft_vs_stub_and_coupling_contact_mm3=0,source_mount_contacts_mm3=contacts,held_source_sha256=held,section_reference=data['lower_shaft_section_reference'],photo_observations=dict(photo='IMG_7041',source_sha256=hashlib.sha256(photo_path.read_bytes()).hexdigest(),source_size_pixels=list(raw.size),exif_orientation=orientation,exif_orientation_applied_before_sampling=True,working_size_pixels=[1800,1350],approximate_shaft_span_endpoints=shaft_spans,approximate_filter_span_endpoints=filter_span,pf53_catalog_diameter_comparison_mm=77,catalog_source='https://www.jegs.com/i/acdelco-065-pf53-engine-oil-filter-spin-on-canister',uncalibrated_diameter_proxies_mm=proxies,selected_reference_diameter_mm=16,limits='Hand-picked blurred/occluded edges. Different object depths and orientations prevent direct pixel-to-mm calibration. These rough proxies motivate a slimmer source section; they are not owner measurements, statistical uncertainty bounds or proof of16mm fitted diameter. Upper body-plane step and matching entry envelopes remain construction hypotheses.'),source_sha256={name:hashlib.sha256((CAD/name).read_bytes()).hexdigest()for name in ['steering-rack-reference.json','steering-rack-reference.step']},limits=data['lower_shaft_section_reference']['limits'])
(CAD/'lower-steering-section-validation.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(dict(valid_solids=45,held_groups=len(same),revised_groups=changed,rough_photo_proxies_mm=proxies,mount_contacts=contacts),indent=2))
