"""Reconstrucción CAD independiente para presentación del plano público.
CadQuery 2.8.0. No es una ejecución del conversor I Love STEP.
Coordenadas: X ancho, Y alto, Z espesor; cara de cajeados en Z=20.
"""
from pathlib import Path
import json, math
import cadquery as cq
ROOT=Path(__file__).resolve().parent
P=json.loads((ROOT/'parametros.json').read_text())
W,H,T=P['width'],P['height'],P['thickness']
side=[(x,y-H/2) for x in (-W/2+P['side_hole_edge_offset'],W/2-P['side_hole_edge_offset']) for y in P['side_hole_y_from_bottom']]
small=[(x,y-H/2) for x in (-P['small_hole_x_offset_from_center'],P['small_hole_x_offset_from_center']) for y in P['center_y_from_bottom']]
center=[(0,y-H/2) for y in P['center_y_from_bottom']]
# Keep explicit world-space cutters: face workplane origins cannot drift after cuts.
body=cq.Workplane('XY').box(W,H,T,centered=(True,True,False))
# The 94 mm central land projects 1 mm beyond both rear side strips.
# Keep the maximum thickness 20 mm: remove material from the sides, not the centre.
strip_width=(W-P['rear_boss_width'])/2
for x in (-(W+P['rear_boss_width'])/4,(W+P['rear_boss_width'])/4):
 relief=cq.Workplane('XY',origin=(x,0,0)).box(strip_width,H+2,P['rear_boss_projection'],centered=(True,True,False))
 body=body.cut(relief)
def bore(solid,points,diameter,z0,depth):
 cutters=cq.Workplane('XY',origin=(0,0,z0)).pushPoints(points).circle(diameter/2).extrude(depth)
 return solid.cut(cutters)
body=bore(body,side,P['side_hole_diameter'],-1,T+2)
body=bore(body,side,P['side_counterbore_diameter'],T-P['side_counterbore_depth'],P['side_counterbore_depth']+1)
body=bore(body,small,P['small_hole_diameter'],-1,T+2)
body=bore(body,center,P['thread_nominal_diameter'],-1,T+2)
body=bore(body,center,P['center_counterbore_diameter'],T-P['center_counterbore_depth'],P['center_counterbore_depth']+1)
solid=body.val()
assert solid.isValid() and len(solid.Solids())==1
# Topology and dimension checks on the actual exported B-rep, not the illustration.
cq.exporters.export(body,str(ROOT/'placa-ilovestep.step'))
cq.exporters.export(body,str(ROOT/'placa-ilovestep.stl'),tolerance=0.015,angularTolerance=0.075)
assembly=cq.Assembly(body,name='Placa_I_Love_STEP_reconstruida',color=cq.Color(0.66,0.74,0.78))
assembly.export(str(ROOT/'placa-ilovestep.glb'),tolerance=0.015,angularTolerance=0.075)
read=cq.importers.importStep(str(ROOT/'placa-ilovestep.step')).val()
bb=read.BoundingBox()
assert read.isValid() and len(read.Solids())==1
assert max(abs(bb.xlen-W),abs(bb.ylen-H),abs(bb.zlen-T))<1e-6
# Check that all 24 intended holes are open through the stock.
for x,y in side+small+center:
 for z in (2,9,18): assert not read.isInside((x,y,z)),(x,y,z)
# Regression: central rear land must project, not recess. Sample both shoulders
# away from holes, including points immediately either side of x=+-47.
for x in (-46.5,-30,30,46.5): assert read.isInside((x,0,0.5)),x
for x in (-60,-47.5,47.5,60):
 assert not read.isInside((x,0,0.5)),x
 assert read.isInside((x,0,1.5)),x
def z_span(x):
 probe=cq.Workplane('XY',origin=(x,0,-1)).box(.1,.1,T+2,centered=(True,True,False)).val()
 span=read.intersect(probe).BoundingBox()
 return [span.zmin,span.zmax]
center_span=z_span(30);side_spans=[z_span(-55),z_span(55)]
assert max(abs(center_span[0]),abs(center_span[1]-T))<1e-6
for span in side_spans:
 assert max(abs(span[0]-P['rear_boss_projection']),abs(span[1]-T))<1e-6
sx,sy=side[0]
assert read.isInside((sx+4.5,sy,12.5)) and not read.isInside((sx+4.5,sy,13.5))
assert read.isInside((3.5,center[0][1],4.5)) and not read.isInside((3.5,center[0][1],5.5))
# Expected volume is independent of the modeling operations.
expected=W*H*T-(W-P['rear_boss_width'])*H*P['rear_boss_projection']
expected-=len(side)*math.pi*(P['side_hole_diameter']/2)**2*(T-P['rear_boss_projection'])
expected-=len(side)*math.pi*((P['side_counterbore_diameter']/2)**2-(P['side_hole_diameter']/2)**2)*P['side_counterbore_depth']
expected-=len(small)*math.pi*(P['small_hole_diameter']/2)**2*T
expected-=len(center)*math.pi*(P['thread_nominal_diameter']/2)**2*T
expected-=len(center)*math.pi*((P['center_counterbore_diameter']/2)**2-(P['thread_nominal_diameter']/2)**2)*P['center_counterbore_depth']
assert abs(read.Volume()-expected)<1e-5
# Viewer mesh derives from the re-imported STEP. Z recentered only for display.
vertices,triangles=read.tessellate(0.015,0.075)
positions=[round(c,6) for v in vertices for c in (v.x,v.y,v.z-T/2)]
indices=[i for tri in triangles for i in tri]
edges=[]
for edge in read.Edges():
 n=2 if edge.geomType()=='LINE' else max(36,min(256,math.ceil(edge.Length()/0.4)))
 points,_=edge.sample(n)
 for a,b in zip(points[:-1],points[1:]):edges.extend([round(c,6) for v in (a,b) for c in (v.x,v.y,v.z-T/2)])
mesh={'positions':positions,'indices':indices,'edges':edges,'dimensions':[W,H,T],'hole_count':len(side+small+center),'parameters':P}
(ROOT/'modelo.js').write_text('window.CAD_MODEL='+json.dumps(mesh,separators=(',',':'),ensure_ascii=False)+';\n')
report={'revision':2,'tool':'CadQuery 2.8.0','valid':read.isValid(),'solids':len(read.Solids()),'bounding_box_mm':[bb.xlen,bb.ylen,bb.zlen],'volume_mm3':read.Volume(),'expected_volume_mm3':expected,'hole_count':len(side+small+center),'through_holes_checked':True,'counterbores_depth_checked':True,'rear_boss_projection_checked':True,'section_z_extents_mm':{'center':center_span,'left':side_spans[0],'right':side_spans[1]},'step_roundtrip':True,'triangles':len(triangles),'assumptions':P['assumptions']}
(ROOT/'validacion.json').write_text(json.dumps(report,indent=2,ensure_ascii=False)+'\n')
print(json.dumps({k:v for k,v in report.items() if k!='assumptions'},ensure_ascii=False,indent=2))
