s = open('v18_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 v18 内插拆解图</title>', '<title>NobadyBox v19 外插拆解图</title>')
R('NOBADY BOX · 硬件方案 v18 · 前口内插版 · 拆解效果图', 'NOBADY BOX · 硬件方案 v19 · 外插再推版 · 拆解效果图')
R('<h1>AI 手机仓 v18 前口内插版逐级拆解</h1>', '<h1>AI 手机仓 v19 外插再推版逐级拆解</h1>')
R('<span>拆解 <b>12</b> 级</span>', '<span>拆解 <b>13</b> 级</span>')
R('max="12"', 'max="13"')
R('<span>11</span><span>12</span></div>', '<span>11</span><span>12</span><span>13</span></div>')
R('0 / 12</span>', '0 / 13</span>')
R('电源口、分线板、插头座、前口尺寸', '电源口、分线板、走线槽、线夹、前口尺寸')

RS('<section>\n  <h3>v18 改了什么', '<section>\n  <h3>4 个 3D 打印零件</h3>', '''<section>
  <h3>v19 改了什么：先在外面插线，再把手机推进去</h3>
  <p class="lead">v18 让手机在仓底里自己对插头，实际很难对准，这一版去掉了。充电线还是从里面的分线板出来，沿仓底中间的槽走到前口，线头留出来约 15 cm。你在外面把线插上手机，再把手机推进去，多出来的线在前口里绕成一个 S 放平。整机还是只有后面一个电源口。</p>
  <div class="tbl"><table class="cmp">
    <thead><tr><th></th><th>v18 前口内插版</th><th>v19 外插再推版</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>插头固定在仓底后端</td><td>从分线板沿仓底中间的槽到前口线夹，留 15 cm 线头</td></tr>
      <tr><td>外面的线</td><td>1 根电源线</td><td>1 根电源线（不变）</td></tr>
      <tr><td>打印件</td><td>3 + 1</td><td>3 + 1</td></tr>
    </tbody>
  </table></div>
</section>

''')
R('<li>第一路给手机：分线板旁边就是手机插头座，线只有约 5 cm，大电流不用绕远路，也不过合页。</li>',
  '<li>第一路给手机：从分线板下到仓底，沿手机底下中间的走线槽一直到前口的线夹，再留约 15 cm 线头，末端是 90° 弯头 C 公头。这一路不过合页。</li>')
RS('      <h4>手机怎么放、怎么充电<small>推到底就插上</small></h4>', '    <div class="req">\n      <h4>合页、磁铁和校准', '''      <h4>手机怎么放、怎么充电<small>先插线，再推进去</small></h4>
      <ul>
        <li>前口 90 × 45 mm，在前壁最底部。线头平时搭在前口里。</li>
        <li>放手机：把线头拉出来，在外面插上手机（充电口朝外），屏幕朝上，平着推进去。手机停住时端面离外壳前面约 10 mm。</li>
        <li>推进去时线跟着退回前口，在手机端面前面绕成一个 S 平放，不会压到手机底下。</li>
        <li>拿手机：手指伸进前口捏住手机往外拉，拉出来以后再拔线。线夹把线固定住，拉的时候里面的线不会被扯动。</li>
      </ul>
    </div>
''')
R('分线板 2 颗 M2 装到后壁，插头卡进插头座，3 芯线压进后壁线槽；', '分线板 2 颗 M2 装到后壁，手机线压进仓底走线槽、卡进前口线夹，3 芯线压进后壁线槽；')
R('    <div><b>手机要能对准插头</b>插头座按手机充电口在端面正中、手机厚 7–9 mm 设计。带厚壳的手机插不到底，需要摘壳；不同厚度用 TPU 垫片调高度。</div>\n',
  '    <div><b>线头长度</b>前口外留 15 cm，手机拉出约 12 cm 就能拔线。线头在前口里收成 S 形，如果嫌露在外面，可以在前口下沿加一道小挡边。</div>\n')

# ---------- INFO ----------
R("带插头座、支撑筋、手机框侧壁、后壁线槽", "带仓底走线槽、前口线夹、支撑筋、手机框侧壁、后壁线槽")
R("wall:'侧条 4.5 mm，高 6 mm；另配 0.5 / 1 mm 垫片调插头高度'", "wall:'侧条 4.5 mm，高 6 mm'")
R("role:'两侧把手机卡在中间，屏幕对准镜头，充电口对准后端插头。'}", "role:'两侧把手机卡在中间，屏幕对准镜头。'}")
R("role:'屏幕朝上，充电口朝里，从前口推到底就插上。屏幕离盒底 16 mm。'}", "role:'屏幕朝上，充电口朝外。先在外面插上线，再从前口推进去。屏幕离盒底 16 mm。'}")
RL("  cable:{name:'电源分线板 + 充电插头'", "  cable:{name:'电源分线板 + 手机充电线', mat:'分线板约 28 × 22 mm（USB-C 母座、保险丝、电子开关、XH 3P）；手机线约 350 mm，90° 弯头 C 公头（内置 56 kΩ）；3 芯线约 400 mm', role:'整机唯一电源口。一路沿仓底走线槽到前口线夹，留 15 cm 线头给手机；一路沿后壁线槽、贴合页转轴上去给主板。'},")

RS("const STEPS = [", "const NSTEP", """const STEPS = [
  {p:[], t:'放好手机的样子', d:'100 × 190 × 138.2 mm。前面底部是 90 × 45 mm 的前口，后面底部是唯一的电源口。往右拖滑块，每一级拆下一个零件，当前这一级会标成橙色。'},
  {p:['phone'], t:'拉出手机', d:'手指伸进前口捏住手机往外拉，线跟着一起出来。放手机反过来：在外面插好线，再推进去。'},
  {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:['cable'], t:'取出分线板和充电线', d:'分线板 2 颗 M2 装在后壁电源口后面。手机线沿仓底走线槽到前口线夹；3 芯线沿后壁线槽上去，贴合页转轴到主板 XH 3P。'},
  {p:['mag'], t:'取出磁铁', d:'4 颗 Ø6 × 3 mm 钕磁铁压入，不用胶。压之前对一下极性，保证上下相吸。'},
  {p:['ins'], t:'热熔螺母', d:'10 颗 M2：安装板 4 颗（PCB），顶盖外壳 4 颗（安装板），后壁 2 颗（分线板）。烙铁约 220 °C 烫进去。'},
  {p:['body'], t:'只剩底仓', d:'一体打印的底仓。虚线标注是它身上做出来的功能：前口、电源口、仓底走线槽、前口线夹、支撑筋、后壁线槽、进风百叶、合页耳、磁铁座和定位标记。'}
];
""")

# ---------- 底仓：去掉插头座，加线夹 ----------
R("  g.add(boxR(-9,-5.7,7,16.4,-89.5,-79.5,m), boxR(5.7,9,7,16.4,-89.5,-79.5,m), boxR(-9,9,14.9,16.4,-89.5,-80,m), boxR(-5.7,5.7,7,8.2,-89.5,-79.5,m));   // 插头座",
  "  g.add(boxR(31,39,FL,9.2,88.2,89.4,m), boxR(31,39,8,9.2,89.4,92.4,m));   // 前口线夹")
R("const hPhone = holder(asm,0,0,PZ);", "const hPhone = holder(asm,0,0,PZ); hPhone.rotation.y = Math.PI;")

# ---------- 分线板、手机线、插头 ----------
R("hCable.add(boxR(-5.5,5.5,8.3,14.7,-86,-79.5,mPlug), boxR(-4.15,4.15,10.3,12.7,-79.5,-74,mSteel));          // C 公头，口朝前\n", "")
R("hCable.add(new T.Mesh(new T.TubeGeometry(new T.CatmullRomCurve3([[0,11.5,-86],[0,11.5,-88.5],[4,10.5,-90.2],[10,12,-90.3],[14,13.5,-90.5]].map(a=>new T.Vector3(...a))),40,1.6,8,false),mCab));   // 第一路：给手机",
  "hCable.add(new T.Mesh(new T.TubeGeometry(new T.CatmullRomCurve3([[14,13.5,-90.5],[9,9,-90],[3,4.8,-88.5],[0,4.8,-80],[0,4.8,0],[0,4.8,80],[8,4.8,88.5],[22,4.8,89.6],[35,4.8,90.6]].map(a=>new T.Vector3(...a)),false,'centripetal'),120,1.6,8,false),mCab));   // 第一路：沿仓底走线槽到前口线夹\n"
  "// 手机插头（90° 弯头，跟着手机走）\n"
  "const hPlug = holder(asm,0,0,0);\n"
  "hPlug.add(boxR(-4.15,4.15,10.3,12.7,PZ+78,PZ+84.5,mSteel), boxR(-5.5,5.5,8.3,14.7,PZ+84.5,PZ+90.5,mPlug), boxR(-2,2,6.6,8.3,PZ+86,PZ+89.5,mPlug));\n"
  "reg('cable',hPlug,[]);\n"
  "let pigMesh=null;\n"
  "const PIG_CLIP=new T.Vector3(35,4.8,90.6);\n"
  "const PIG_S=[[35,4.8,90.6],[20,4.8,93.4],[-28,4.8,93.4],[-38,4.8,90.6],[-30,4.8,88.2],[-8,5,88.6]].map(a=>new T.Vector3(...a));\n"
  "function pigPts(off){ const E=new T.Vector3(0,6.6,PZ+87.7).add(off), w=Math.min(1,Math.max(0,Math.max(off.z,0)/45+Math.abs(off.y)/20));\n"
  "  const pts=PIG_S.map((p,i)=>p.clone().lerp(PIG_CLIP.clone().lerp(new T.Vector3(E.x,4.8+(E.y-4.8)*i/6,E.z-3),i/6),w));\n"
  "  pts.push(new T.Vector3(E.x,E.y-1.2,E.z-1.4), E.clone()); return pts; }")

# ---------- 每一级怎么动 ----------
RS("mv(hPhone,1,0,0,200);", "/* ---------- 打印摆放", """mv(hPhone,1,0,0,120); mv(hPlug,1,0,0,120);
mv(hPhone,2,0,0,90);
mv(hTPU,3,0,110,0);
hHinge.forEach((h,i)=> mv(h,5,(i?1:-1)*45,0,0));
mv(lidPivot,6,0,190,40);
mv(hShell,7,0,85,0);
mv(hFan,8,0,45,0);
mv(hPCB,9,0,22,0);
mv(hCable,10,85,0,0); mv(hPlug,10,85,0,0);
hMag.forEach(o=> mv(o.h,11,0,o.up?18:22,0));
hIns.forEach(k=> mv(k,12,...k.userData.dir));

""")

# ---------- 标注 ----------
R("PID.phone=lab(hPhone,[38,16,40],'手机（示意）','',()=>true);", "PID.phone=lab(hPhone,[38,16,-40],'手机（示意）','',()=>true);")
R("'合页销 M3×30','',after(4)", "'合页销 M3×30','',after(5)")
R("'3007 风扇','',after(7)", "'3007 风扇','',after(8)")
R("'PCB + 摄像头','',after(8)", "'PCB + 摄像头','',after(9)")
R("PID.cable=lab(hCable,[39,30,-90.8],'电源分线板 + 充电插头','',after(9));", "PID.cable=lab(hCable,[39,30,-90.8],'电源分线板 + 手机充电线','',after(10));\nlab(hPlug,[5.5,14.7,PZ+90.5],'手机充电线头（先插上再推进去）','',k=>k>=1);")
R("'钕磁铁 Ø6×3','',after(10)", "'钕磁铁 Ø6×3','',after(11)")
R("'热熔螺母 M2','',after(11)", "'热熔螺母 M2','',after(12)")
R("lab(hCable,[11,8,-90.8],'分线板：一路给手机，一路给主板','wire',WL);", "lab(hCable,[11,8,-90.8],'分线板：一路给手机，一路给主板','wire',WL);\nlab(hCable,[0,4.8,-40],'手机线走仓底中间的槽','wire',WL);\nlab(hBody,[39,9.2,90],'前口线夹，线头在前口里绕成 S','wire',WL);")
R("lab(hCable,[5.5,14.7,-83],'插头固定在插头座里，手机推到底就插上','wire',WL);", "lab(hPlug,[-5.5,14.7,PZ+90.5],'先在外面插上，再推进去','wire',WL);")
R("[[0,16.4,-84],'插头座'],", "[[35,9.2,90],'前口线夹'],[[0,FL,-40],'仓底走线槽'],")
R("].forEach(([p,t])=>lab(hBody,p,t,'feat',after(12)));", "].forEach(([p,t])=>lab(hBody,p,t,'feat',after(13)));")

# ---------- 姿态 ----------
R("lidPivot.rotation.x = -Math.PI*100/180 * tOf(3,v) * (1-tOf(5,v));", "lidPivot.rotation.x = -Math.PI*100/180 * tOf(4,v) * (1-tOf(6,v));")
R("  const tc=tOf(9,v), off=new T.Vector3(85*tc,0,0);", "  const tc=tOf(10,v), off=new T.Vector3(85*tc,0,0);\n"
  "  if(pigMesh){ asm.remove(pigMesh); pigMesh.geometry.dispose(); }\n"
  "  pigMesh=new T.Mesh(new T.TubeGeometry(new T.CatmullRomCurve3(pigPts(hPlug.position.clone().sub(hPlug.userData.base).sub(off)).map(p=>p.add(off)),false,'centripetal'),160,1.6,8,false),mCab); asm.add(pigMesh);")
R("const s0=c(x), s=c(x-2), s2=c((x-4)/3); return {ty:69+45*s+65*s2, tz:60*s0*(1-s),", "const s0=c(x), s=c(x-3), s2=c((x-5)/3); return {ty:69+45*s+65*s2, tz:60*s0*(1-s),")
R("camera.position.set(380,240,-400)", "camera.position.set(400,240,380)")

# ---------- 零件卡与清单 ----------
R("'插头座和底板一体打印，插拔上千次不松',", "'仓底中间留 22 mm 宽走线槽，手机线压在里面，手机推进推出不磨线','前口线夹把线固定住，拉线头不会扯到里面',")
R("tpu:['软料吸震，不刮手机','把手机夹在正中，充电口对准插头','坏了单独重打，0.4 h','按手机宽度分档']", "tpu:['软料吸震，不刮手机','把手机夹在正中，屏幕对准镜头','坏了单独重打，0.4 h','按手机宽度分档']")
R("['手机充电插头','采购','1','C 公头带短线，内置 56 kΩ','9']", "['手机充电线','采购','1','约 350 mm，90° 弯头 C 公头（内置 56 kΩ），另一头接分线板','10']")
R("['电源分线板','新做','1','约 28 × 22 mm：USB-C 母座、保险丝、电子开关、XH 3P','9']", "['电源分线板','新做','1','约 28 × 22 mm：USB-C 母座、保险丝、电子开关、XH 3P','10']")
R("['3 芯线','采购','1','约 400 mm，两端 XH 3P','9']", "['3 芯线','采购','1','约 400 mm，两端 XH 3P','10']")
R("['PCB + 摄像头','你的设计','1','57 × 28 × 6.5 mm，摄像头 Ø13.8 × 15 mm；电源从 XH 3P 进','8']", "['PCB + 摄像头','你的设计','1','57 × 28 × 6.5 mm，摄像头 Ø13.8 × 15 mm；电源从 XH 3P 进','9']")
R("['3007 风扇','采购','1','30 × 30 × 7 mm，5 V 滚珠','7']", "['3007 风扇','采购','1','30 × 30 × 7 mm，5 V 滚珠','8']")
R("['P2 顶盖外壳','打印','1','PETG HF 黑','6'],['P3 摄像头安装板','打印','1','PETG HF 黑','5'],['P4 TPU 条','打印','按手机选 1 套','TPU 95A','2']", "['P2 顶盖外壳','打印','1','PETG HF 黑','7'],['P3 摄像头安装板','打印','1','PETG HF 黑','6'],['P4 TPU 条','打印','按手机选 1 套','TPU 95A','3']")
R("['合页销','采购','2','M3×30 + 防松螺母','4'],['钕磁铁','采购','4','Ø6 × 3 mm','10']", "['合页销','采购','2','M3×30 + 防松螺母','5'],['钕磁铁','采购','4','Ø6 × 3 mm','11']")
R("['热熔螺母','采购','10','M2','11']", "['热熔螺母','采购','10','M2','12']")
R("['P1 底仓','打印','1','PETG HF 黑','12']", "['P1 底仓','打印','1','PETG HF 黑','13']")
open('v19_src.html', 'w').write(s)
print('ok')
