s = open('v20_src.html').read()
def R(a, b, cnt=1):
    global s
    n = s.count(a)
    assert n == cnt, (n, a[:90])
    s = s.replace(a, b)
def RS(start, end, new):
    global s
    i = s.index(start); j = s.index(end, i)
    s = s[:i] + new + s[j:]
def RL(start, new):
    global s
    i = s.index(start); j = s.index('\n', i)
    s = s[:i] + new + s[j:]

# ---------- 头部 ----------
R('<title>NobadyBox v20 口边插线拆解图</title>', '<title>NobadyBox v21 无线充电拆解图</title>')
R('NOBADY BOX · 硬件方案 v20 · 口边插线版 · 拆解效果图', 'NOBADY BOX · 硬件方案 v21 · 无线充电版 · 拆解效果图')
R('<h1>AI 手机仓 v20 口边插线版逐级拆解</h1>', '<h1>AI 手机仓 v21 无线充电版逐级拆解</h1>')
R('电源口、分线板、走线槽、线头座、前口尺寸', '电源口、电源板、无线充电线圈和后挡、前口尺寸')

RS('<section>\n  <h3>v20 改了什么', '<section>\n  <h3>4 个 3D 打印零件</h3>', '''<section>
  <h3>v21 改了什么：手机放进去就充电，没有线头</h3>
  <p class="lead">仓底手机正下方放一个无线充电线圈。手机屏幕朝上推到底，碰到后面的 TPU 后挡就停住，线圈正好对着手机背面，自动开始充电。不用插线，也不用拔线。盒子外面还是只有后面一根电源线。</p>
  <div class="tbl"><table class="cmp">
    <thead><tr><th></th><th>v20 口边插线版</th><th>v21 无线充电版</th></tr></thead>
    <tbody>
      <tr><td>放手机</td><td>推到底，再在前口边上插线</td><td>推到底就充电</td></tr>
      <tr><td>拿手机</td><td>先拔线插回线头座，再拉出手机</td><td>直接拉出来</td></tr>
      <tr><td>仓里的线</td><td>手机线走手机底下右侧到线头座</td><td>只有线圈到无线充电板的一小段，压在手机底下</td></tr>
      <tr><td>手机要求</td><td>USB-C 口在端面正中</td><td>手机要支持无线充电（Qi）</td></tr>
      <tr><td>充电器</td><td>5 V 3 A</td><td>PD 20 W（电源板取 9 V）</td></tr>
      <tr><td>多出来的零件</td><td>手机充电线、线头座</td><td>无线充电线圈 + 无线充电板，TPU 条多一个后挡</td></tr>
    </tbody>
  </table></div>
</section>

''')
RS('        <li>口子后面贴一块小分线板', '        <li>第二路给主板', '''        <li>口子后面贴一块电源板（约 28 × 22 mm），上面是 USB-C 母座、PD 取电芯片、保险丝和 5 V 降压。充电器要支持 PD 20 W，电源板向它要 9 V。</li>
        <li>第一路给无线充电：9 V 经后壁上一根短线到左边的无线充电板（约 28 × 22 mm，2 颗 M2 装在后壁），再从手机框后端的出线口沿仓底中间到线圈。这一路不过合页，压在手机底下看不见。</li>
''')
R('<li>第二路给主板：一根 3 芯细线（5 V、地、充电开关信号）', '<li>第二路给主板：电源板降出 5 V，一根 3 芯细线（5 V、地、充电开关信号）')
R('主板用开关信号控制手机充电：电量 40–80% 之间开关充电，超过 40 °C 先停。', '主板用开关信号开关无线充电：电量 40–80% 之间开关充电，超过 40 °C 先停，减少发热、保护手机电池。')
RS('      <h4>手机怎么放、怎么充电<small>先推进去，再插线</small></h4>', '    <div class="req">\n      <h4>合页、磁铁和校准', '''      <h4>手机怎么放、怎么充电<small>推到底就充电</small></h4>
      <ul>
        <li>前口 90 × 45 mm，在前壁最底部，只用来放手机和拿手机。</li>
        <li>放手机：屏幕朝上，平着推到底，碰到后面的 TPU 后挡就停。手机背面离线圈约 2 mm，正对线圈中心，自动开始充电。</li>
        <li>拿手机：手指伸进前口捏住手机端面往外拉，没有线要拔。</li>
        <li>线圈 Ø50 mm，嵌在仓底中间的线圈座里，两条中间支撑筋在线圈处断开让位。手机长短不同时，后挡按手机分档，让手机的充电线圈对准仓底线圈。</li>
      </ul>
    </div>
''')
R('<li>烫 10 颗 M2 热熔螺母（多 2 颗装分线板）。</li>', '<li>烫 12 颗 M2 热熔螺母（多 4 颗装电源板和无线充电板）。</li>')
R('<li>PCB（连摄像头）4 颗 M2 装到安装板上，风扇 2 颗自攻装到顶盖外壳上，插好风扇线和充电线。</li>', '<li>PCB（连摄像头）4 颗 M2 装到安装板上，风扇 2 颗自攻装到顶盖外壳上，插好风扇线。</li>')
R('分线板 2 颗 M2 装到后壁，手机线穿过后端出线口、压在右侧支撑筋外边，线头插进线头座，3 芯线压进后壁线槽；', '电源板和无线充电板各 2 颗 M2 装到后壁，线圈放进线圈座、线从后端出线口过去，3 芯线压进后壁线槽；')
R('    <div><b>充电口位置</b>按充电口在手机端面正中设计。手机推到底时端面离外壳前面约 10 mm；带壳的手机端面会往外多几毫米，更好插。</div>\n',
  '    <div><b>你的手机支不支持无线充电</b>这一版的前提。iPhone 8 以后和大部分中高端安卓都支持；很多低价安卓不支持，那就用 v20。</div>\n'
  '    <div><b>发热</b>无线充电比插线热，手机背面会到 35–40 °C。风扇从仓里往上抽风能带走一部分，主板在 40 °C 先停充、电量 80% 停充。长期开机建议先实测一周手机温度。</div>\n'
  '    <div><b>带壳</b>手机壳 3 mm 以内可以充，金属壳、带磁吸铁片的壳不行。</div>\n')

# ---------- INFO ----------
R("带线头座、右侧走线槽、支撑筋、手机框侧壁、后壁线槽", "带线圈座、支撑筋、手机框侧壁、后壁线槽")
R("wall:'侧条 4.5 mm，高 6 mm'", "wall:'侧条 4.5 mm，后挡 10 mm，高 6 mm'")
R("role:'两侧把手机卡在中间，屏幕对准镜头。'}", "role:'两侧把手机卡在中间，屏幕对准镜头；后挡让手机每次停在同一个位置，充电线圈对准。'}")
R("role:'屏幕朝上，充电口朝外，从前口推到底，再在前口边上插线。屏幕离盒底 16 mm。'}", "role:'屏幕朝上，从前口推到底就开始无线充电。屏幕离盒底 16 mm。'}")
RL("  cable:{name:'电源分线板 + 手机充电线'", "  cable:{name:'电源板 + 无线充电板', mat:'电源板约 28 × 22 mm（USB-C 母座、PD 取电 9 V、保险丝、5 V 降压、XH 3P）；无线充电板约 28 × 22 mm（Qi 15 W 发射）；3 芯线约 400 mm', role:'整机唯一电源口。9 V 给无线充电板，5 V 经 3 芯线沿后壁线槽、贴合页转轴上去给主板。'},\n"
   "  coil:{name:'无线充电线圈', mat:'Ø50 × 2.5 mm（线圈 + 铁氧体片），线约 120 mm', role:'嵌在仓底线圈座里，正对手机背面，间隙约 2 mm。'},")
RS("const STEPS = [", "const NSTEP", """const STEPS = [
  {p:[], t:'放好手机的样子', d:'100 × 190 × 138.2 mm。前面底部是 90 × 45 mm 的前口，后面底部是唯一的电源口。手机推到底就在无线充电。往右拖滑块，每一级拆下一个零件，当前这一级会标成橙色。'},
  {p:['phone'], t:'拉出手机', d:'手指伸进前口捏住手机端面往外拉，没有线要拔。放手机反过来：屏幕朝上推到底，碰到后挡就停，自动开始充电。'},
  {p:['tpu'], t:'取出 TPU 条', d:'两侧条把手机卡在中间，屏幕对准镜头；后挡让手机每次停在同一个位置。按手机大小选 1 套。'},
  {p:['shell'], t:'掀开顶盖（维修时）', d:'平时不用开。维修时顶盖绕后边的合页往后翻，约 100° 停住自己立着。前面 2 对磁铁平时把它吸住。'},
  {p:['hinge'], t:'拔出合页销', d:'2 颗 M3×30 螺丝当合页销，拧下防松螺母就能抽出来。拆之前先拔掉主板上的 XH 3P。'},
  {p:['shell','plate'], t:'取下整个顶盖模块', d:'主板、摄像头、风扇都在顶盖里，整块拿下来。维修或者换代时直接换整个顶盖。'},
  {p:['shell'], t:'拆开顶盖外壳', d:'拧下 4 颗 M2 螺丝，顶盖外壳和安装板分开。外壳顶上嵌着风扇。'},
  {p:['fan'], t:'拆风扇', d:'3007 风扇用 2 颗自攻螺丝装在外壳顶板下面，正对镜头中心，离 PCB 顶面约 2.5 mm，往上排风。'},
  {p:['pcb'], t:'拆 PCB 和摄像头', d:'摄像头焊在 PCB 中心，4 颗 M2 螺丝装在安装板的凸台上。镜头从安装板中间的 Ø15 孔穿下去，只露出镜头。'},
  {p:['coil'], t:'取出无线充电线圈', d:'Ø50 线圈嵌在仓底中间的线圈座里，不用螺丝，背面铁氧体片挡住往下的磁场。线从手机框后端的出线口过去。'},
  {p:['cable'], t:'取出电源板和无线充电板', d:'两块小板各 2 颗 M2 装在后壁。右边电源板接电源口，左边无线充电板接线圈；3 芯线沿后壁线槽上去，贴合页转轴到主板 XH 3P。'},
  {p:['mag'], t:'取出磁铁', d:'4 颗 Ø6 × 3 mm 钕磁铁压入，不用胶。压之前对一下极性，保证上下相吸。'},
  {p:['ins'], t:'热熔螺母', d:'12 颗 M2：安装板 4 颗（PCB），顶盖外壳 4 颗（安装板），后壁 4 颗（两块小板）。烙铁约 220 °C 烫进去。'},
  {p:['body'], t:'只剩底仓', d:'一体打印的底仓。虚线标注是它身上做出来的功能：前口、电源口、线圈座、支撑筋、后壁线槽、进风百叶、合页耳、磁铁座和定位标记。'}
];
""")

# ---------- 底仓：去掉线头座，加线圈座；支撑筋在线圈处断开 ----------
R("  g.add(boxR(39.6,XI,FL,14,73,79.5,m), boxR(46.5,XI,FL,14,79.5,92.4,m));   // 线头座（右前角，线头尖插进去）\n", "")
R("  for(const x of [-30,-12,12,30]) g.add(boxR(x-.8,x+.8,FL,RIB,-78,84.5,m));   // 4 mm 支撑筋（后端让出走线位）",
  "  for(const x of [-30,30]) g.add(boxR(x-.8,x+.8,FL,RIB,-78,84.5,m));   // 4 mm 支撑筋\n"
  "  for(const x of [-12,12]) g.add(boxR(x-.8,x+.8,FL,RIB,-78,PZ-27,m), boxR(x-.8,x+.8,FL,RIB,PZ+27,84.5,m));   // 中间两条在线圈处断开\n"
  "  { const ring=new T.Mesh(new T.TorusGeometry(26.2,.8,8,48), m); ring.rotation.x=Math.PI/2; ring.position.set(0,FL+.8,PZ); g.add(ring); }   // 线圈座（定位圈）")
R("    g.add(boxR(sd*FX,sd*(FX+2),FL,15,-FZ,-25,m), boxR(sd*FX,sd*(FX+2),FL,15,25,sd>0?72:FZ,m));",
  "    g.add(boxR(sd*FX,sd*(FX+2),FL,15,-FZ,-25,m), boxR(sd*FX,sd*(FX+2),FL,15,25,FZ,m));")
R("  g.add(boxR(-FX,-38,7,13,-77.5,84.5,m), boxR(38,FX,7,13,-77.5,72,m));   // 右条前端让给线头座",
  "  g.add(boxR(-FX,-38,7,13,-77.5,84.5,m), boxR(38,FX,7,13,-77.5,84.5,m), boxR(-20,20,7,13,-87.5,-77.5,m));   // 侧条 + 后挡")

# ---------- 电源板、无线充电板、线圈 ----------
R("hCable.add(new T.Mesh(new T.TubeGeometry(new T.CatmullRomCurve3([[14,13.5,-90.5],[9,9,-90],[6,4.8,-88.5],[9,4.8,-84.5],[30,4.8,-82.5],[35,4.8,-78],[35,4.8,0],[35,4.8,60]].map(a=>new T.Vector3(...a)),false,'centripetal'),120,1.6,8,false),mCab));   // 第一路：穿出线口，走手机底下右侧",
  "hCable.add(boxR(-39,-11,8,30,-92.4,-90.8,mBrd), boxR(-34,-26,14,22,-90.8,-87.8,M(0x30343a)), boxR(-22,-15,12,17,-90.8,-88.6,mPlug));   // 无线充电板（电感、芯片）\n"
  "hCable.add(new T.Mesh(new T.TubeGeometry(new T.CatmullRomCurve3([[12,26,-90.4],[4,28.5,-90.2],[-4,28.5,-90.2],[-12,26,-90.4]].map(a=>new T.Vector3(...a)),false,'centripetal'),40,1.1,8,false),mCab));   // 9 V 短线：电源板 → 无线充电板")
RS("// 手机插头（90° 弯头，线从下面出）", "reg('cable',hCable,[mCab,mPlug,mBrd]);", """// 无线充电线圈（嵌在仓底线圈座里）
const mCoil=M(0xb87333,{metalness:.6,roughness:.45}), mFerr=M(0x1b1d20,{roughness:.8});
const hCoil = holder(asm,0,0,0);
hCoil.add(cylY(25,.9,mFerr,0,FL+.45,PZ,48), cylY(24,1.5,mCoil,0,FL+1.65,PZ,48), cylY(7,1.6,mFerr,0,FL+1.7,PZ,32));
hCoil.add(new T.Mesh(new T.TubeGeometry(new T.CatmullRomCurve3([[0,4.6,PZ-24],[0,4.4,-40],[0,4.4,-84],[-3,5.5,-88.6],[-10,10,-90.4],[-16,13,-90.6]].map(a=>new T.Vector3(...a)),false,'centripetal'),60,1,8,false),mCab));
reg('coil',hCoil,[mCoil,mFerr]);
""")
R("for(const x of [14,36]) ins(hBody,x,19,-91.4,[0,0,25]);", "for(const x of [14,36,-14,-36]) ins(hBody,x,19,-91.4,[0,0,25]);")
R("lab(asm,[20,25.5,-114],'唯一电源口 USB-C 5 V 3 A','wire',WL);", "lab(asm,[20,25.5,-114],'唯一电源口 USB-C（PD 20 W）','wire',WL);")

# ---------- 每一级怎么动 ----------
RS("mv(hPhone,2,0,0,210);", "hMag.forEach", """mv(hPhone,1,0,0,210);
mv(hTPU,2,0,110,0);
hHinge.forEach((h,i)=> mv(h,4,(i?1:-1)*45,0,0));
mv(lidPivot,5,0,190,40);
mv(hShell,6,0,85,0);
mv(hFan,7,0,45,0);
mv(hPCB,8,0,22,0);
mv(hCoil,9,0,135,0);
mv(hCable,10,85,0,0);
""")

# ---------- 标注 ----------
R("'合页销 M3×30','',after(5)", "'合页销 M3×30','',after(4)")
R("'3007 风扇','',after(8)", "'3007 风扇','',after(7)")
R("'PCB + 摄像头','',after(9)", "'PCB + 摄像头','',after(8)")
R("PID.cable=lab(hCable,[39,30,-90.8],'电源分线板 + 手机充电线','',after(10));", "PID.cable=lab(hCable,[39,30,-90.8],'电源板 + 无线充电板','',after(10));\nPID.coil=lab(hCoil,[25,FL+2.4,PZ],'无线充电线圈 Ø50','',after(9));")
R("lab(hPlug,[3.2,-3.2,6],'充电线头：不用时插在线头座','',k=>k>=1&&k<10);\n", "")
R("lab(hCable,[11,8,-90.8],'分线板：一路给手机，一路给主板','wire',WL);", "lab(hCable,[11,8,-90.8],'电源板：9 V 给无线充电，5 V 给主板','wire',WL);\nlab(hCable,[-39,30,-90.8],'无线充电板','wire',WL);")
R("lab(hCable,[35,4.8,-40],'手机线压在手机底下右侧','wire',WL);\n", "")
R("lab(hBody,[46,14,76],'线头座：不用时线头插在这里','wire',WL);", "lab(hCoil,[25,FL+2.4,PZ],'线圈正对手机背面，间隙约 2 mm','wire',WL);\nlab(hCoil,[0,4.4,-50],'线圈线走仓底中间','wire',WL);")
R("lab(hPlug,[-5.5,3.2,6],'手机推到底，在前口边上插','wire',WL);\n", "")
R("[[46,14,76],'线头座'],[[35,FL,-40],'右侧走线位'],", "[[26,FL+.8,PZ],'线圈座'],")

# ---------- 姿态 ----------
R("  lidPivot.rotation.x = -Math.PI*100/180 * tOf(4,v) * (1-tOf(6,v));", "  lidPivot.rotation.x = -Math.PI*100/180 * tOf(3,v) * (1-tOf(5,v));")
R("  placePlug(tOf(1,v), off); setFront(tOf(1,v), off);\n", "")
R("  const tu=tOf(5,v);", "  const tu=tOf(4,v);")
R("const s0=c(x), s=c(x-3), s2=c((x-5)/3);", "const s0=c(x), s=c(x-2), s2=c((x-4)/3);")

# ---------- 零件卡与清单 ----------
R("'手机线压在右侧支撑筋外边，手机推进推出不磨线','右前角做出线头座，不用时线头插在里面，不伸出外壳',",
  "'仓底中间做出 Ø52 线圈座，线圈放进去就定位，不用螺丝',")
R("tpu:['软料吸震，不刮手机','把手机夹在正中，屏幕对准镜头','坏了单独重打，0.4 h','按手机宽度分档']",
  "tpu:['软料吸震，不刮手机','把手机夹在正中，屏幕对准镜头','后挡让手机每次停在同一位置，线圈对准','坏了单独重打，0.4 h','按手机大小分档']")
R("['P2 顶盖外壳','打印','1','PETG HF 黑','7'],['P3 摄像头安装板','打印','1','PETG HF 黑','6'],['P4 TPU 条','打印','按手机选 1 套','TPU 95A','3']",
  "['P2 顶盖外壳','打印','1','PETG HF 黑','6'],['P3 摄像头安装板','打印','1','PETG HF 黑','5'],['P4 TPU 条 + 后挡','打印','按手机选 1 套','TPU 95A','2']")
R("电源从 XH 3P 进','9'],['3007 风扇','采购','1','30 × 30 × 7 mm，5 V 滚珠','8']", "电源从 XH 3P 进','8'],['3007 风扇','采购','1','30 × 30 × 7 mm，5 V 滚珠','7']")
R("['电源分线板','新做','1','约 28 × 22 mm：USB-C 母座、保险丝、电子开关、XH 3P','10'],['手机充电线','采购','1','约 300 mm，90° 弯头 C 公头，线从插头下面出（内置 56 kΩ），另一头接分线板','10'],",
  "['电源板','新做','1','约 28 × 22 mm：USB-C 母座、PD 取电 9 V、保险丝、5 V 降压、XH 3P','10'],['无线充电板 + 线圈','采购','1','Qi 15 W 发射模块，线圈 Ø50 × 2.5 mm（带铁氧体片）','9 / 10'],")
R("['合页销','采购','2','M3×30 + 防松螺母','5']", "['合页销','采购','2','M3×30 + 防松螺母','4']")
R("['热熔螺母','采购','10','M2','12'],['其他螺丝','采购','12','M2×6 ×10（PCB、安装板、分线板）", "['热熔螺母','采购','12','M2','12'],['其他螺丝','采购','14','M2×6 ×12（PCB、安装板、两块小板）")
R("['PD 充电器 + 1 m C-C 线','随机附','1','必须支持 5 V 3 A','—']", "['PD 充电器 + 1 m C-C 线','随机附','1','PD 20 W（要有 9 V 档）','—']")
R("ins:{name:'热熔螺母 M2 × 10', mat:'黄铜', role:'安装板 4 颗（PCB）、顶盖外壳 4 颗（安装板）、后壁 2 颗（分线板）。'}", "ins:{name:'热熔螺母 M2 × 12', mat:'黄铜', role:'安装板 4 颗（PCB）、顶盖外壳 4 颗（安装板）、后壁 4 颗（电源板、无线充电板）。'}")
for bad in ['hPlug','setFront','placePlug']:
    assert bad not in s, bad
open('v21_src.html', 'w').write(s)
print('ok')
