abse/smartwatch-flex-top-only
Three-panel flex smartwatch with two parallel R3.9 bends, all components flat-top, low-profile JST connector harnesses, split heartbeat optical windows and screw-retained PCB mounting. Modeled 39 × 50.5 × 14.5mm prototype; 100% routing, zero native DRC and shorts. Hardware qualification pending.
- Version
- 0.2.20
- License
- unset
- Stars
- 0
scripts/prepare-mount-hardware.py
"""Purchased M1.6 hardware, with nominal threads and a visual PH0 recess."""
from pathlib import Path
import base64,json,numpy as np,trimesh
from trimesh.visual.material import PBRMaterial
root=Path(__file__).resolve().parent.parent
h=json.loads((root/'mechanical/mount-hardware.json').read_text())
pitch=h['pitchMM']; radius=h['diameterMM']/2; length=h['overallLengthMM']
# ISO basic external-root diameter; this visual mesh is not a thread gauge.
root_radius=(h['diameterMM']-1.226869*pitch)/2
segments=48; levels=np.linspace(-length,-h['headHeightMM']+.005,round(length/pitch*8))
vertices=[]
for z in levels:
for i in range(segments):
theta=2*np.pi*i/segments
phase=(z/pitch-theta/(2*np.pi))%1
distance=abs(phase-.5)
fraction=np.clip((.375-distance)/.3125,0,1)
r=min(root_radius+(radius-root_radius)*fraction,radius-.175+max(0,z+length))
vertices.append([r*np.cos(theta),r*np.sin(theta),z])
faces=[]
for j in range(len(levels)-1):
for i in range(segments):
a=j*segments+i;b=j*segments+(i+1)%segments;c=a+segments;d=b+segments
faces.extend([[a,b,c],[b,d,c]])
bottom=len(vertices);vertices.append([0,0,levels[0]])
top=len(vertices);vertices.append([0,0,levels[-1]])
for i in range(segments):
faces.extend([[bottom,(i+1)%segments,i],[top,(len(levels)-1)*segments+i,(len(levels)-1)*segments+(i+1)%segments]])
shaft=trimesh.Trimesh(vertices=vertices,faces=faces,process=True)
assert shaft.is_volume
def cylinder(r,z0,z1):
m=trimesh.creation.cylinder(radius=r,height=z1-z0,sections=48);m.apply_translation([0,0,(z0+z1)/2]);return m
head=trimesh.creation.cone(radius=1.5,height=1.5,sections=48)
head.apply_transform(trimesh.transformations.rotation_matrix(np.pi,[1,0,0]));head.apply_translation([0,0,-.26])
clip=trimesh.creation.box(extents=[4,4,.7],transform=trimesh.transformations.translation_matrix([0,0,-.61]))
head=trimesh.boolean.intersection([head,clip],engine='manifold')
body=trimesh.boolean.union([shaft,head,cylinder(1.5,-.26,0)],engine='manifold')
recess=[]
for angle in [0,np.pi/2]:
# Tapered cross recess is illustrative; PH0 designation comes from supplier.
v=[]
for z,long,short in [(-.5,.55,.13),(.05,1.5,.30)]:
v.extend([[x,y,z] for x,y in [(-long/2,-short/2),(long/2,-short/2),(long/2,short/2),(-long/2,short/2)]])
f=[[0,2,1],[0,3,2],[4,5,6],[4,6,7]]
for i in range(4):j=(i+1)%4;f.extend([[i,j,j+4],[i,j+4,i+4]])
m=trimesh.Trimesh(vertices=v,faces=f,process=True);m.apply_transform(trimesh.transformations.rotation_matrix(angle,[0,0,1]));recess.append(m)
screw=trimesh.boolean.difference([body,*recess],engine='manifold');assert screw.is_volume and screw.body_count==1
metal=PBRMaterial(baseColorFactor=[180,185,190,255],roughnessFactor=.3,metallicFactor=.9)
screw.visual=trimesh.visual.TextureVisuals(material=metal)
single=trimesh.Scene(screw);binary=single.export(file_type='glb')
(root/'mechanical/m1-6x6-iso7046.glb').write_bytes(binary)
(root/'mechanical/m1-6x6-iso7046.ts').write_text('export default "data:model/gltf-binary;base64,'+base64.b64encode(binary).decode()+'"\n')
world=trimesh.Scene()
for mount in h['mounts']:
m=screw.copy();m.apply_translation([mount['x'],mount['y'],h['headTopZMM']]);world.add_geometry(m,geom_name=mount['name'],node_name=mount['name'])
(root/'mechanical/pcb-mount-screws.glb').write_bytes(world.export(file_type='glb'))
print('Generated two purchased ISO7046 M1.6 x 6 PH0 screw meshes; datum is head top, shaft points -Z.')