rushabhcodes/ESP32-E-Reader

A compact ESP32-C3 e-reader PCB integrating LiPo charging/protection, USB-C, 3.3 V regulation, e-paper display bias generation, microSD storage, and tactile controls.

Version
1.0.85
License
unset
Stars
3

scripts/render-printable-enclosure.py

"""Render the reader using the current native assembly and transparent source GLBs.

Run after `bun run export:assembly`. Rendering preserves source CAD alpha;
it illustrates a transparent CAD material, not the optical finish of FDM PETG.
"""
from pathlib import Path
from math import pi
import subprocess
import json
import sys
import bpy
from mathutils import Matrix, Vector
ROOT=Path(__file__).resolve().parents[1];OUT=ROOT/'assets/enclosure'
DISPLAY_SPEC=json.loads((ROOT/'assets/enclosure/display-connection.json').read_text())
DISPLAY_ANCHOR=tuple(DISPLAY_SPEC['connector'][key] for key in ('centerX','centerY','boardSurfaceZ'))
bpy.ops.object.select_all(action='SELECT');bpy.ops.object.delete(use_global=False)

def local(name,offset=(0,0,0),rotation_z=0):
    before=set(bpy.data.objects);bpy.ops.import_scene.gltf(filepath=str(OUT/name))
    objects=[o for o in set(bpy.data.objects)-before if o.type=='MESH']
    for o in objects:o.matrix_world=Matrix.Translation(Vector(offset))@Matrix.Rotation(rotation_z*pi/180,4,'Z')@Matrix.Rotation(-pi/2,4,'X')@o.matrix_world
    return objects

front=local('front-chassis.glb');rear=local('rear-cover.glb')
cell=local('battery-pack.glb')
leads=local('battery-leads.glb');plug=local('battery-plug.glb')
liner=local('battery-liner-DO-NOT-PRINT.glb');adhesive=local('battery-adhesive-DO-NOT-PRINT.glb')
antenna=local('antenna-envelope-DO-NOT-PRINT.glb')
keys=front # Key caps/leaves are part of the same connected chassis mesh.
display=local('display-panel.glb',DISPLAY_ANCHOR,DISPLAY_SPEC['connector']['screenCadRotationZ'])
pads=local('display-cushioning.glb');hardware=local('pcb-cover-fasteners.glb')
pcb_screws=[o for o in hardware if o.name.startswith('PCB-')]
cover_screws=[o for o in hardware if o not in pcb_screws]
before=set(bpy.data.objects);bpy.ops.import_scene.gltf(filepath=str(ROOT/'dist/index/assembly.glb'))
conversion=Matrix(((-1,0,0,0),(0,-1,0,0),(0,0,1,0),(0,0,0,1)))
assembly={'EPD1','battery_pack','battery_leads','battery_plug','wifi_antenna_envelope','front_chassis','rear_cover','battery_liner','battery_adhesive','display_cushioning','pcb_cover_fasteners'}
board=[]
for o in set(bpy.data.objects)-before:
    if o.type!='MESH':continue
    if o.name.split('.')[0] in assembly:bpy.data.objects.remove(o,do_unlink=True);continue
    o.matrix_world=conversion@o.matrix_world;board.append(o)
# Work in metre-sized Blender coordinates, keeping millimetre source files.
for o in bpy.data.objects:
    if o.type=='MESH':o.matrix_world=Matrix.Scale(.01,4)@o.matrix_world
objects=[o for o in bpy.data.objects if o.type=='MESH'];original={o:o.matrix_world.copy() for o in objects}
scene=bpy.context.scene;scene.render.engine='CYCLES';scene.cycles.samples=24
if '--battery-update' in sys.argv:scene.cycles.samples=12
scene.cycles.use_denoising=True;scene.render.resolution_x=1100;scene.render.resolution_y=1000
scene.render.resolution_percentage=100;scene.render.image_settings.file_format='PNG'
scene.world.use_nodes=True;scene.world.node_tree.nodes['Background'].inputs[0].default_value=(.82,.87,.91,1)
scene.world.node_tree.nodes['Background'].inputs[1].default_value=.6
scene.view_settings.view_transform='AgX';scene.view_settings.exposure=-1.2
for pos,power,size in [((1.3,-1.6,2.6),500,2),((-1.8,1,1.6),350,2),((1.2,1.8,-2),450,2)]:
    bpy.ops.object.light_add(type='AREA',location=pos);o=bpy.context.object;o.data.energy=power;o.data.shape='DISK';o.data.size=size
    o.rotation_euler=(-o.location).to_track_quat('-Z','Y').to_euler()

def hide(group):
    for o in group:o.hide_render=True

def reset():
    for o,m in original.items():o.hide_render=False;o.matrix_world=m

def shift(group,z):
    for o in group:o.matrix_world=Matrix.Translation(Vector((0,0,z*.01)))@o.matrix_world

def render(name,pos,scale=145,target=(0,5,3)):
    requested=next((arg.split('=',1)[1] for arg in sys.argv if arg.startswith('--view=')),None)
    if requested and name!=requested:return
    if '--battery-only' in sys.argv and name!='battery-wired-connection.png':return
    if '--battery-update' in sys.argv and name not in {'printable-front.png','printable-side.png','printable-exploded.png'}:return
    bpy.ops.object.camera_add(location=Vector(pos)*.01);cam=bpy.context.object
    cam.rotation_euler=(Vector(target)*.01-cam.location).to_track_quat('-Z','Y').to_euler()
    cam.data.type='ORTHO';cam.data.ortho_scale=scale*.01;cam.data.clip_start=.001
    scene.camera=cam;scene.render.filepath=str(OUT/name);bpy.ops.render.render(write_still=True)
    bpy.data.objects.remove(cam,do_unlink=True);print('Rendered',name,flush=True)

if '--battery-only' not in sys.argv:
    render('printable-front.png',(80,110,-125))
    render('printable-side.png',(170,-55,65),142)
hide(rear+adhesive)
render('battery-wired-connection.png',(55,-105,175),126,(-1,2,5))
hide(cell+leads+plug+liner+adhesive+antenna+cover_screws)
render('printable-open-back.png',(-85,-110,135))
hide(display+pads)
render('printable-pcb-mounts.png',(0,5,220),133)
reset();hide(rear+cell+leads+plug+liner+adhesive+antenna+hardware+board)
render('display-retainer-installed.png',(70,-110,150),132,(0,9,-3))
reset();hide(front+rear+cell+leads+plug+liner+adhesive+antenna+hardware+pads)
render('display-ribbon-connection.png',(45,-82,35),62,(8,-39,0))
reset();shift(display,8);shift(pads,8);shift(board+pcb_screws,25)
# Disconnect and lift the external harness with the pack in the exploded view.
shift(cell+leads+plug+liner+adhesive,44);shift(rear+antenna+cover_screws,60)
render('printable-exploded.png',(130,-135,130),185,(0,5,33))
# Print plate: show the actual two oriented exports with opaque material so
# the support features remain legible. Assembly previews above retain alpha.
hide(objects)
for name,x,y in [('front-chassis',65,110),('rear-cover',150,110)]:
    bpy.ops.wm.stl_import(filepath=str(OUT/'print'/(name+'.stl')));o=bpy.context.object
    o.matrix_world=Matrix.Scale(.01,4)@Matrix.Translation(Vector((x,y,0)))@o.matrix_world
    mat=bpy.data.materials.new(name+'-print-preview');mat.diffuse_color=(.16,.37,.43,1);mat.use_nodes=True
    mat.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value=mat.diffuse_color
    o.data.materials.clear();o.data.materials.append(mat)
    bpy.ops.object.text_add(location=((x-34)*.01,48*.01,.001));label=bpy.context.object
    label.data.body=name.replace('-',' ');label.data.size=.032
    ink=bpy.data.materials.new('label-ink');ink.diffuse_color=(.025,.04,.05,1);ink.use_nodes=True
    ink.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value=ink.diffuse_color
    label.data.materials.append(ink)
bpy.ops.mesh.primitive_cube_add(size=1,location=(1.1,1.1,-.015));bed=bpy.context.object;bed.dimensions=(2.2,2.2,.02)
render('reader-print-plate.png',(210,-135,360),295,(110,110,0))
subprocess.run(['python3',str(ROOT/'scripts/package-preview-images.py')],cwd=ROOT,check=True)