← Commit history
README.md+5−3
@@ -4,7 +4,7 @@ <!-- rev2-status:begin --> ## Rev 2 build status -Rev 2 is being built right now by three AI threads in Adom Hydrogen; this section follows their work. Last updated Nov 8, 2:18 PM Central.+Rev 2 is being built right now by three AI threads in Adom Hydrogen; this section follows their work. Last updated Nov 8, 2:27 PM Central.  | Stage | | What it covers | |---|---|---|@@ -417,7 +417,9 @@ screen: the chat on the left, the AI Flow dashboard, the Studio and this page in  ## Case -The 3D view at the top of this page is the assembled rev 2 gadget: open its Layers panel to hide the case shell, the light pipes or the battery, and press Replay to watch it assemble.+The 3D view at the top of this page is the assembled rev 2 gadget next to an adult hand for scale: open its Layers panel to hide the case shell, the light pipes, the battery or the hand, and press Replay to watch it assemble.++The hand (180 mm wrist to middle fingertip) is cut from the MakeHuman base mesh, CC0; how it was made: [docs/3d/size-hand](docs/3d/size-hand/README.md).  ![The assembled rev 2 case](case/renders/assembly-iso-rev2.png) ![Exploded: foot, base, battery, board, gasket, cover, light pipes](case/renders/exploded-rev2.png)@@ -436,7 +438,7 @@ Design notes, print settings and assembly: [case/README.md](case/README.md); eng  ## Firmware -Plain C for the ATtiny1616, updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge) (avr-gcc, about 13.1 KB of the 16 KB flash). The echo path is pure hardware:+Plain C for the ATtiny1616, updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge), updated for rev 2 (the STATUS dot takes the logo's job at about half the drive, a 30 mA light cap for the small cell, the 77 mA charge) (avr-gcc, about 13.1 KB of the 16 KB flash). The echo path is pure hardware: AC0, the event system and TCB0/TCB1 single-shots. The CPU runs the state machine, the learner, the lights and the safety checks, and a 1 s watchdog guards the main loop. Full source is in [firmware/](firmware/README.md), with a host test that runs the learner against simulated faucets
docs/3d/ghost-hand-hero-47282fc0.glbadded
⋯ 1 unchanged line ⋯
docs/3d/size-hand/README.mdadded+7
@@ -0,0 +1,7 @@+# Size-reference hand++An average adult right hand (180 mm wrist to middle fingertip), palm down and flat on the counter, for scale in the Ghost Hand hero model.++Cut from the MakeHuman base mesh (`makehuman/data/3dobjs/base.obj`) using its default skeleton weights (`makehuman/data/rigs/default_weights.mhw`), both released as CC0 by Data Collection AB (https://github.com/makehumancommunity/makehuman). Steps: `extract.py` (hand plus distal forearm by skin weights), `pose.py` (palm down, fingers +Y, scaled to 180 mm), `level.py` (forearm trimmed 55 mm behind the wrist, levelled), `cap.py` (wrist cut capped, watertight).++Frame: metres, Z up, Z = 0 on the counter, wrist stub at Y = 0, fingers toward +Y, centred on X. 146 x 227 x 54 mm, 234 KB.
docs/3d/size-hand/cap.pyadded+9
@@ -0,0 +1,9 @@+import trimesh, numpy as np, networkx as nx+h=trimesh.load('hand_ref.glb',force='mesh'); mat=h.visual.material+e=h.edges_sorted; u,c=np.unique(e,axis=0,return_counts=True); G=nx.Graph(); G.add_edges_from(u[c==1].tolist())+V=h.vertices; F=list(h.faces)+for comp in nx.connected_components(G):+    if len(comp)<6: continue+    cyc=[x for x,_ in nx.find_cycle(G.subgraph(comp))]; ci=len(V); V=np.vstack([V,V[cyc].mean(0)])+    F+=[[cyc[i],cyc[(i+1)%len(cyc)],ci] for i in range(len(cyc))]+h2=trimesh.Trimesh(V,np.array(F),process=True); trimesh.repair.fix_normals(h2); h2.visual=trimesh.visual.TextureVisuals(material=mat); h2.export('hand_ref.glb')
docs/3d/size-hand/extract.pyadded+24
@@ -0,0 +1,24 @@+import json, numpy as np, trimesh+V=[];F=[];grp=None+for l in open('base.obj'):+    if l.startswith('v '): V.append([float(x) for x in l.split()[1:4]])+    elif l.startswith('g '): grp=l.split()[1]+    elif l.startswith('f ') and grp=='body':+        F.append([int(t.split('/')[0])-1 for t in l.split()[1:]])+V=np.array(V)+w=json.load(open('default_weights.mhw'))['weights']+hb=[k for k in w if k.endswith('.R') and k.startswith(('finger','metacarpal','wrist'))]+s=np.zeros(len(V)); s2=np.zeros(len(V))+for b in hb:+    for i,x in w[b]: s[i]+=x+for i,x in w['lowerarm02.R']: s2[i]+=x+keep=(s>0.3)|(s2>0.3)+tris=[]+for f in F:+    if all(keep[i] for i in f):+        tris += [[f[0],f[1],f[2]]] + ([[f[0],f[2],f[3]]] if len(f)==4 else [])+m=trimesh.Trimesh(V,np.array(tris),process=True); m.remove_unreferenced_vertices()+m=max(m.split(only_watertight=False),key=lambda c:len(c.faces))+print('verts',len(m.vertices),'faces',len(m.faces),'bounds',m.bounds.round(3).tolist())+m.export('hand_raw.ply')+hk=np.where(s>0.5)[0]; P=V[hk]; print('hand-only min',P.min(0).round(3),'max',P.max(0).round(3))
docs/3d/size-hand/hand_ref.glbadded
⋯ 1 unchanged line ⋯
docs/3d/size-hand/hand_views.pngadded
⋯ 1 unchanged line ⋯
docs/3d/size-hand/level.pyadded+20
@@ -0,0 +1,20 @@+import trimesh, numpy as np+h=trimesh.load('hand_ref.glb',force='mesh')+v=h.vertices.copy()+# trim the forearm 55 mm behind the wrist (wrist is at y=0)+keepf=np.all(v[h.faces][:,:,1]>-0.055,axis=1)+h=trimesh.Trimesh(v,h.faces[keepf],process=True); h.remove_unreferenced_vertices()+h=max(h.split(only_watertight=False),key=lambda c:len(c.faces))+trimesh.repair.fill_holes(h)+best=None+for a in np.radians(np.arange(-35,36,1)):+    R=trimesh.transformations.rotation_matrix(a,[1,0,0])+    z=(h.vertices@R[:3,:3].T)[:,2]; e=z.max()-z.min()+    if best is None or e<best[0]: best=(e,a)+R=trimesh.transformations.rotation_matrix(best[1],[1,0,0]); h.apply_transform(R)+h.vertices[:,2]-=h.vertices[:,2].min()+h.vertices[:,1]-=h.vertices[:,1].min()       # forearm stub at y=0, fingers toward +Y+h.vertices[:,0]-=(h.vertices[:,0].min()+h.vertices[:,0].max())/2+h.visual=trimesh.visual.TextureVisuals(material=trimesh.visual.material.PBRMaterial(name='skin_reference',baseColorFactor=[224,172,140,255],metallicFactor=0.0,roughnessFactor=0.65))+print('tilt deg',np.degrees(best[1]).round(1),'extents mm',(h.extents*1000).round(1),'watertight',h.is_watertight,'faces',len(h.faces))+h.export('hand_ref.glb'); import os; print(os.path.getsize('hand_ref.glb')//1024,'KB')
docs/3d/size-hand/pose.pyadded+37
@@ -0,0 +1,37 @@+import json, numpy as np, trimesh+m=trimesh.load('hand_raw.ply')+V=[];+for l in open('base.obj'):+    if l.startswith('v '): V.append([float(x) for x in l.split()[1:4]])+V=np.array(V)+w=json.load(open('default_weights.mhw'))['weights']+def pts(prefixes,th=0.5):+    s=np.zeros(len(V))+    for b in w:+        if b.endswith('.R') and b.startswith(prefixes):+            for i,x in w[b]: s[i]+=x+    return V[s>th]+wrist=pts(('wrist',)).mean(0)+tip=pts(('finger3-3',),0.8)            # middle fingertip segment+tipf=tip[np.argmax(np.linalg.norm(tip-wrist,axis=1))]+thumb=pts(('finger1-3',),0.8).mean(0)+pinky=pts(('finger5-3',),0.8).mean(0)+L=tipf-wrist; Lu=L/np.linalg.norm(L)+side=thumb-pinky; side-=side.dot(Lu)*Lu; Su=side/np.linalg.norm(side)+N=np.cross(Lu,Su)                      # normal; sign checked below+# palm faces the body (+x in MakeHuman A-pose); make Nd point to the palm side+if N[0]<0: N=-N+print('wrist',wrist.round(3),'len dm',np.linalg.norm(L).round(3))+# right hand palm down: fingers +Y, palm normal -Z, then thumb lands on -X (left side seen from above)+R=np.stack([np.cross(Lu,-N),Lu,-N])    # rows: new X,Y,Z axes expressed in old coords -> want Lu->Y, N->-Z+# new coords = R @ (p - wrist): X = (Lu x -N)·p, Y = Lu·p, Z = -N·p  => palm (N side) goes to -Z+P=(m.vertices-wrist)@R.T+scale=0.180/(np.linalg.norm(L))        # 180 mm wrist-to-middle-tip, in metres+P*=scale+P[:,2]-=P[:,2].min()                   # rest on the counter+h=trimesh.Trimesh(P,m.faces,process=False)+if h.volume<0: h.invert()+h=h.subdivide_loop(1) if hasattr(h,'subdivide_loop') else h+h.visual=trimesh.visual.TextureVisuals(material=trimesh.visual.material.PBRMaterial(name='skin',baseColorFactor=[224,172,140,255],metallicFactor=0.0,roughnessFactor=0.65))+print('extents mm',(h.extents*1000).round(1),'faces',len(h.faces),'thumb x',((thumb-wrist)@R.T*scale*1000).round(1))+h.export('hand_ref.glb'); import os; print(os.path.getsize('hand_ref.glb')//1024,'KB')
page.json+1−1
@@ -43,7 +43,7 @@     "subcategory": "",     "body_size": null,     "parts": {-      "model_3d": "docs/3d/ghost-hand-hero-abd28840.glb"+      "model_3d": "docs/3d/ghost-hand-hero-47282fc0.glb"     },     "distributor_links": {},     "viewer_3d": {