import re
s = open('v15_src.html').read()
def R(a, b, cnt=1):
    global s
    n = s.count(a)
    assert n == cnt, (n, a[:80])
    s = s.replace(a, b)
def RS(start, end, new):
    """replace from start marker (inclusive) to end marker (exclusive)"""
    global s
    i = s.index(start); j = s.index(end, i)
    s = s[:i] + new + s[j:]

# ---------- 头部 ----------
R('<title>NobadyBox v15 翻盖拆解图</title>', '<title>NobadyBox v16 抽屉拆解图</title>')
R('NOBADY BOX · 硬件方案 v15 · 顶部翻盖版 · 拆解效果图', 'NOBADY BOX · 硬件方案 v16 · 底部抽屉版 · 拆解效果图')
R('<h1>AI 手机仓 v15 翻盖版逐级拆解</h1>', '<h1>AI 手机仓 v16 底部抽屉版逐级拆解</h1>')
R('<span>打印件 <b>3 + 1</b></span><span>拆解 <b>12</b> 级</span>', '<span>打印件 <b>4 + 1</b></span><span>拆解 <b>14</b> 级</span>')
R('max="12"', 'max="14"')
R('<span>11</span><span>12</span></div>', '<span>11</span><span>12</span><span>13</span><span>14</span></div>')
R('<span class="readout" id="readout">0 / 12</span>', '<span class="readout" id="readout">0 / 14</span>')
R('壁厚、合页、风道、走线和卡托是我按这组参数定的估算值。', '壁厚、合页、风道、走线、抽屉托和余量线是我按这组参数定的估算值。')

# ---------- 对比表 ----------
RS('<section>\n  <h3>为什么改成顶部翻盖</h3>', '<section>\n  <h3>4 个 3D 打印零件</h3>', '''<section>
  <h3>v16 改了什么：手机从前面的抽屉进出</h3>
  <p class="lead">v15 的手机躺在约 10 cm 深的仓底，四周只有 3 到 5 mm 缝，手伸不下去。v16 在底仓前面开一个扁口，手机放在抽屉托里，拉出来放、推回去就插好电。顶盖平时不用再打开，只在维修时掀开。</p>
  <div class="tbl"><table class="cmp">
    <thead><tr><th></th><th>v15 顶部翻盖版</th><th>v16 底部抽屉版</th></tr></thead>
    <tbody>
      <tr><td>放手机</td><td>掀顶盖，从 10 cm 深的仓口放下去，手伸不进去</td><td>拉出抽屉托，手机露出 9 cm，放好往后一推</td></tr>
      <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>上方留约 19 cm 开盖</td><td>前方留约 12 cm 拉抽屉</td></tr>
      <tr><td>打印件</td><td>3 + 1</td><td>4 + 1（多一个抽屉托）</td></tr>
      <tr><td>要注意</td><td>线要过合页</td><td>充电线要换成扁平线，在抽屉下面留 U 形余量</td></tr>
    </tbody>
  </table></div>
</section>

''')
R('''<h3>4 个 3D 打印零件</h3>
  <p class="lead">3 个 PETG 打印件，加 1 件按手机大小选的 TPU 卡托。''', '''<h3>5 个 3D 打印零件</h3>
  <p class="lead">4 个 PETG 打印件，加 1 套按手机大小选的 TPU 垫。''')

# ---------- 充电线说明 ----------
RS('      <h4>充电线怎么走<small>先插后放</small></h4>', '    <div class="req">\n      <h4>合页、磁铁和校准', '''      <h4>充电线和抽屉<small>往后一推就插上</small></h4>
      <ul>
        <li>主板 XH 插座 → 贴右侧合页转轴穿过 → 后壁线槽 → 仓底 → 抽屉托底下的 U 形余量线 → 抽屉托后端的插头。</li>
        <li>插头是下出线的弯头 C 公头，卡在抽屉托后端的插头座里，口朝前，高度对准手机充电口。手机放进抽屉托，往后一推就插上。</li>
        <li>抽屉拉出 100 mm，线在抽屉托底下像卷尺一样滚动展开，推回去自己收好，不会堆在仓里。线要用约 1.2 × 3 mm 的扁平 2 芯硅胶线，抽屉下面只有约 2.8 mm 高。</li>
        <li>抽屉托前面板正好堵住前面的扁口，推到底不漏光。抽屉托后端有 2 个弹性限位舌，拉到 100 mm 会卡住，按下舌头才能整个抽出来。</li>
      </ul>
    </div>
''')
R('<li>合页带约 100° 限位，掀开后顶盖自己立住，不会往后倒。</li>', '<li>合页带约 100° 限位，掀开后顶盖自己立住，不会往后倒。平时放手机走抽屉，顶盖只在维修时打开。</li>')
R('<li>合页总有一点间隙，每次合盖后摄像头先拍仓底 2 个定位标记，自动修正位置，用户感觉不到。</li>', '<li>2 个定位标记做在抽屉托上。每次推回抽屉，摄像头先拍一下标记，自动修正抽屉和顶盖的位置误差，用户感觉不到。</li>')
R('<li>底仓开口朝上竖着打；顶盖外壳顶面朝下打；安装板平放打；卡托平放打。</li>', '<li>底仓开口朝上竖着打；顶盖外壳顶面朝下打；安装板平放打；抽屉托底面贴板平放打；TPU 垫平放打。</li>')
R('<li>进风百叶是 45° 斜片；合页耳和磁铁座下面垫 45° 斜撑；合页孔做泪滴形。</li>', '<li>进风百叶是 45° 斜片；合页耳和磁铁座下面垫 45° 斜撑；合页孔做泪滴形；前面扁口顶边 88 mm 宽，靠桥接打出来。</li>')
R('<li>底仓两侧下部 4 扇进风百叶。手机底下垫 4 mm 高的支撑筋，风能从手机背面下方流过，手机背面也能散热。</li>', '<li>底仓两侧下部 4 扇进风百叶。手机放在抽屉托里，抽屉托两侧是开着的，风从手机上面流过去。</li>')
R('<li>2 颗 M3×30 穿合页把顶盖装到底仓上，压 4 颗磁铁，放卡托，做一次标定。</li>', '<li>2 颗 M3×30 穿合页把顶盖装到底仓上，压 4 颗磁铁。把插头卡进抽屉托后端，放 TPU 垫，推进底仓，做一次标定。</li>')
R('<div><b>手机框要给插头留位置</b>弯头插头要占约 10 mm。手机框内长 175 mm 时，能放的手机最长约 165 mm。如果要放更长的手机，手机框要加长。</div>',
  '<div><b>手机最长约 165 mm</b>抽屉托内长约 180 mm，后端插头占约 10 mm，前面 TPU 垫至少 3 mm。超过 165 mm 的手机要把底仓加长。</div>\n    <div><b>抽屉下面的扁平线</b>抽屉托底下只有约 2.8 mm 高，普通圆线放不下。要找约 1.2 × 3 mm 的扁平 2 芯硅胶线，或者让我把底仓加高 2 mm。</div>')

# ---------- 尺寸 ----------
R("const PIV = new T.Vector3(0, SPLIT, -97); // 合页轴", "const PIV = new T.Vector3(0, SPLIT, -97); // 合页轴\nconst TRAVEL = 100, SLOT_W = 44, SLOT_Y = 25, RIB = 5.8;   // 抽屉行程 / 扁口半宽 / 扁口顶 / 抽屉托底面")

# ---------- INFO ----------
R("time:'约 11 h · 280 g', role:'放手机的仓。带手机框、支撑筋、后壁线槽、进风百叶、合页耳和定位标记。'},",
  "time:'约 11 h · 280 g', role:'放手机的仓。前面开 88 × 22 mm 抽屉口，带导轨筋、手机框侧壁、后壁线槽、进风百叶、合页耳和磁铁座。'},")
R("wall:'侧壁 / 后壁 2.4 mm，底 3.0 mm，手机框 2.0 mm，支撑筋 4 mm 高'", "wall:'侧壁 / 后壁 / 前壁 2.4 mm，底 3.0 mm，手机框 2.0 mm，导轨筋 2.8 mm 高'")
R("support:'不需要（百叶 45°，合页耳和磁铁座下 45° 斜撑）'", "support:'不需要（百叶 45°，合页耳和磁铁座下 45° 斜撑，抽屉口顶边桥接 88 mm）'")
R("""  tpu:{name:'TPU 卡托', code:'P4', printed:true, tpu:true, mat:'TPU 95A', size:'按手机分档（图中 76 × 162 手机）',
    wall:'侧条 4.5 mm，前块 3 mm，高 6 mm', orient:'平放', support:'不需要',
    time:'约 0.4 h / 件', role:'小一点的手机用它卡在框中间，让屏幕中心对准镜头。软料不刮手机。'},""",
  """  tray:{name:'抽屉托', code:'P4', printed:true, mat:'PETG HF 黑', size:'87.6 × 182 × 21.4 mm',
    wall:'底 1.2 mm，侧边 1.5 mm，前面板 2.4 mm，插头座 2.3 mm', orient:'底面贴板平放，前面板竖着', support:'不需要',
    time:'约 1.5 h · 40 g', role:'手机放在里面拉进拉出。后端卡着充电插头，前面板堵住抽屉口，上面做 2 个定位标记。'},
  tpu:{name:'TPU 垫', code:'P5', printed:true, tpu:true, mat:'TPU 95A', size:'按手机分档（图中 76 × 162 手机）',
    wall:'侧条 2.8 mm，前垫 8 mm（波浪形可压缩），高 6 mm', orient:'平放', support:'不需要',
    time:'约 0.4 h / 套', role:'两侧把手机卡在中间对准镜头；前垫把手机往后顶，插头不会松。'},""")
R("cable:{name:'手机充电线', mat:'约 450 mm，90° 弯头 C 公头，JST-XH 2P', role:'主板 → 贴合页转轴 → 后壁线槽 → 仓底后端。'},",
  "cable:{name:'手机充电线', mat:'约 500 mm 扁平 2 芯硅胶线，下出线弯头 C 公头（内置 56 kΩ），JST-XH 2P', role:'主板 → 贴合页转轴 → 后壁线槽 → 仓底 → 抽屉托下 U 形余量 → 抽屉托后端插头座。'},")
R("phone:{name:'手机（示意）', mat:'76 × 162 × 9 mm', role:'屏幕朝上，充电口朝后。屏幕离盒底 16 mm。'},",
  "phone:{name:'手机（示意）', mat:'76 × 162 × 9 mm', role:'屏幕朝上，充电口朝后，往后一推插上充电线。屏幕离盒底 16 mm。'},")

# ---------- STEPS ----------
RS("const STEPS = [", "const NSTEP = STEPS.length - 1;", """const STEPS = [
  {p:[], t:'推进去的样子', d:'100 × 190 × 138.2 mm。前面底部那条是抽屉口。往右拖滑块，每一级拆下一个零件，当前这一级会标成橙色。3D 打印件的标注一直显示。'},
  {p:['tray','phone'], t:'拉出抽屉托', d:'手指勾住前面板的拉手往外拉，抽屉托带着手机滑出 100 mm 后卡住。充电线在底下跟着展开。平时放手机、拿手机就是这一步。'},
  {p:['phone'], t:'取出手机', d:'捏住手机两边往前一拔，充电口就离开插头，然后拿起来。放手机反过来：放进抽屉托，往后一推插上，再推回去。'},
  {p:['tpu'], t:'取出 TPU 垫', d:'两侧条把手机卡在中间对准镜头，前垫把手机往后顶住插头。按手机大小选 1 套。'},
  {p:['tray'], t:'整个抽出抽屉托', d:'按下后端 2 个限位舌，抽屉托就能整个抽出来。插头从插头座里按出来，留在线上。'},
  {p:['shell'], t:'掀开顶盖（维修时）', d:'平时不用开。维修时顶盖绕后边的合页往后翻，约 100° 停住自己立着。前面 2 对磁铁平时把它吸住。'},
  {p:['hinge'], t:'拔出合页销', d:'2 颗 M3×30 螺丝当合页销，拧下防松螺母就能抽出来。拆之前先拔掉主板上手机充电线的 XH 插头。'},
  {p:['shell','plate'], t:'取下整个顶盖模块', d:'主板、摄像头、风扇都在顶盖里，整块拿下来。维修或者换代时直接换整个顶盖。'},
  {p:['shell'], t:'拆开顶盖外壳', d:'拧下 4 颗 M2 螺丝，顶盖外壳和安装板分开。外壳顶上嵌着风扇，+X 侧是 USB-C 开口。'},
  {p:['fan'], t:'拆风扇', d:'3007 风扇用 2 颗自攻螺丝装在外壳顶板下面，正对镜头中心，离 PCB 顶面约 2.5 mm，往上排风。'},
  {p:['pcb'], t:'拆 PCB 和摄像头', d:'摄像头焊在 PCB 中心，4 颗 M2 螺丝装在安装板的凸台上。镜头从安装板中间的 Ø15 孔穿下去，只露出镜头。'},
  {p:['cable'], t:'取出手机充电线', d:'从主板 XH 插座出来，贴合页转轴进底仓，沿后壁线槽下到仓底，从手机框后端缺口钻到抽屉托底下，留一段 U 形余量，最后接到插头。'},
  {p:['mag'], t:'取出磁铁', d:'4 颗 Ø6 × 3 mm 钕磁铁压入，不用胶。压之前对一下极性，保证上下相吸。'},
  {p:['ins'], t:'热熔螺母', d:'8 颗 M2：安装板 4 颗（PCB），顶盖外壳 4 颗（安装板）。烙铁约 220 °C 烫进去。'},
  {p:['body'], t:'只剩底仓', d:'一体打印的底仓。虚线标注是它身上做出来的功能：抽屉口、导轨筋、手机框侧壁、进风百叶、后壁线槽、合页耳和磁铁座。'}
];
""")

# ---------- 底仓 ----------
R("g.add(boxR(-XI,39,0,SPLIT,-L/2,-ZI,m), boxR(45,XI,0,SPLIT,-L/2,-ZI,m), boxR(39,45,0,114,-L/2,-ZI,m), boxR(-XI,XI,0,SPLIT,ZI,L/2,m));   // 后壁（顶上 6 mm 过线缺口）/ 前壁",
  "g.add(boxR(-XI,39,0,SPLIT,-L/2,-ZI,m), boxR(45,XI,0,SPLIT,-L/2,-ZI,m), boxR(39,45,0,114,-L/2,-ZI,m));   // 后壁（顶上 6 mm 过线缺口）\n"
  "  g.add(boxR(-XI,-SLOT_W,0,SPLIT,ZI,L/2,m), boxR(SLOT_W,XI,0,SPLIT,ZI,L/2,m), boxR(-SLOT_W,SLOT_W,0,FL,ZI,L/2,m), boxR(-SLOT_W,SLOT_W,SLOT_Y,SPLIT,ZI,L/2,m));   // 前壁，底部开 88 × 22 抽屉口")
R("for(const x of [-30,-12,12,30]) g.add(boxR(x-.8,x+.8,FL,7,-FZ+2,FZ-2,m));   // 4 mm 支撑筋",
  "for(const x of [-30,-12,12,30]) g.add(boxR(x-.8,x+.8,FL,RIB,-FZ+2,ZI,m));   // 导轨筋，中间 22 mm 走余量线")
R("  g.add(boxR(-FX-2,FX+2,FL,15,FZ,FZ+2,m));\n", "")
R("""  const mk = M(0xd9dde2);
  for(const [x,z] of [[-36,-84],[36,84]]){ const a=boxR(x-4,x+4,7,7.25,z-.5,z+.5,mk), b=boxR(x-.5,x+.5,7,7.25,z-4,z+4,mk); a.userData.noEdge=b.userData.noEdge=true; g.add(a,b); }
  return g;
}""", """  return g;
}""")

# ---------- 抽屉托 + TPU 垫 ----------
R("""function buildTPU(m){
  const g = new T.Group();
  g.add(boxR(-FX,-38,7,13,-77.5,84.5,m), boxR(38,FX,7,13,-77.5,84.5,m), boxR(-FX,FX,7,13,84.5,FZ,m));
  return g;
}""", """function buildTray(m){
  const g = new T.Group();
  g.add(boxR(-42.3,42.3,RIB,7,-87.3,ZI,m));                                    // 底 1.2 mm
  for(const sd of [-1,1]){
    g.add(boxR(sd*40.8,sd*42.3,7,13,-87.3,ZI,m));                              // 侧边
    g.add(boxR(sd*39,sd*40.8,13,15,-84,-76,m));                                // 弹性限位舌
    g.add(boxR(sd*5.7,sd*8,7,15,-87.3,-79.5,m));                               // 插头座
  }
  g.add(boxR(-8,8,7,8.3,-87.3,-79.5,m));
  g.add(boxR(-43.6,43.6,FL+.4,SLOT_Y-.4,ZI,L/2,m));                            // 前面板，堵住抽屉口
  g.add(boxR(-16,16,SLOT_Y-4.4,SLOT_Y-.4,L/2,L/2+4,m));                        // 拉手
  const mk = M(0xd9dde2);
  for(const [x,z] of [[-36,-84],[36,88]]){ const a=boxR(x-3.5,x+3.5,7,7.25,z-.5,z+.5,mk), b=boxR(x-.5,x+.5,7,7.25,z-3.5,z+3.5,mk); a.userData.noEdge=b.userData.noEdge=true; g.add(a,b); }   // 定位标记
  return g;
}
function buildTPU(m){
  const g = new T.Group();
  g.add(boxR(-40.8,-38,7,13,-77.5,84.5,m), boxR(38,40.8,7,13,-77.5,84.5,m));
  for(let i=0;i<4;i++) g.add(boxR(-30,30,7,13,84.5+i*2,85.7+i*2,m).rotateY(0));
  g.add(boxR(-30,30,7,13,84.5,ZI,m));
  return g;
}""")
R("const hTPU = holder(asm,0,0,0); hTPU.add(addEdges(buildTPU(mTPU))); reg('tpu',hTPU,[mTPU]);",
  "const mTray=M(0x565e68,{roughness:.5}); shellMats.push(mTray);\n"
  "const hTray = holder(asm,0,0,0); hTray.add(addEdges(buildTray(mTray))); reg('tray',hTray,[mTray]);\n"
  "const hTPU = holder(asm,0,0,0); hTPU.add(addEdges(buildTPU(mTPU))); reg('tpu',hTPU,[mTPU]);")

# ---------- 插头和线 ----------
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));",
  "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), boxR(-2,2,7,8.3,-84.5,-81,mPlug));")
RS("const CAB_FIXED = [", "// 热熔螺母", """const CAB_FIXED = [[12,3.9,-91],[36,3.9,-91.2],[42,8,-91.2],[42,40,-91.2],[42,110,-91.2],[42,117,-93.5],[42,PIV.y-1,PIV.z]].map(a=>new T.Vector3(...a));
const CAB_LID = [[42,123.5,-93],[42,124,-88],[38,124.5,-60],[-20,124.5,-40]].map(a=>new T.Vector3(...a));   // 安装板上方（lidG 坐标）
const CAB_PCB = [[-25,4,-22],[-25,4.6,-12]].map(a=>new T.Vector3(...a));                      // XH（PCB 坐标）
// 抽屉托下面的 U 形余量线：插头一端跟着抽屉走 d，U 形弯走 d/2
function slackPts(d){ const zL=-25+d/2, zs=-80+d, p=[];
  p.push([0,7.5,-82.8+d],[0,5.2,zs]);
  for(let z=zs+8; z<zL-3; z+=12) p.push([0,5.2,z]);
  p.push([0,5.0,zL-1.5],[0,4.5,zL],[0,4.0,zL-1.5]);
  for(let z=zL-10; z>-84; z-=12) p.push([0,3.9,z]);
  p.push([0,3.9,-86],[2,3.9,-89.5]);
  return p.map(a=>new T.Vector3(...a)); }
""")

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

""")

# ---------- 打印摆放 ----------
R("""  [[-300,'盘 1 · PETG HF · 约 11 h'],[0,'盘 2 · PETG HF · 约 4.5 h'],[300,'盘 3 · TPU 95A · 约 0.4 h']].forEach""",
  """  [[-420,'盘 1 · PETG HF · 约 11 h'],[-140,'盘 2 · PETG HF · 约 4.5 h'],[140,'盘 3 · PETG HF · 约 1.5 h'],[420,'盘 4 · TPU 95A · 约 0.4 h']].forEach""")
R("const b=addEdges(buildBody(mBody)); b.position.set(-300,0,0); pv.add(b); PV_LABELS.push({pos:new T.Vector3(-300,120,-40)",
  "const b=addEdges(buildBody(mBody)); b.position.set(-420,0,0); pv.add(b); PV_LABELS.push({pos:new T.Vector3(-420,120,-40)")
R("s.position.set(-55,H,0); pv.add(s); PV_LABELS.push({pos:new T.Vector3(-55,20,-40)", "s.position.set(-195,H,0); pv.add(s); PV_LABELS.push({pos:new T.Vector3(-195,20,-40)")
R("p.position.set(55,-SPLIT,0); pv.add(p); PV_LABELS.push({pos:new T.Vector3(55,4,40)", "p.position.set(-85,-SPLIT,0); pv.add(p); PV_LABELS.push({pos:new T.Vector3(-85,4,40)")
R("const tp=addEdges(buildTPU(mTPU)); tp.position.set(300,-7,-3.5); pv.add(tp); PV_LABELS.push({pos:new T.Vector3(300-40,6,0),text:'<em>P4</em>卡托 · 平放',cls:'printed tpu'});",
  "const tr=addEdges(buildTray(mTray)); tr.position.set(140,-RIB,-4); pv.add(tr); PV_LABELS.push({pos:new T.Vector3(140-42,4,0),text:'<em>P4</em>抽屉托 · 底面贴板',cls:'printed'});\n"
  "  const tp=addEdges(buildTPU(mTPU)); tp.position.set(420,-7,-3.5); pv.add(tp); PV_LABELS.push({pos:new T.Vector3(420-40,6,0),text:'<em>P5</em>TPU 垫 · 平放',cls:'printed tpu'});")
R("camera.position.set(0,820,1000).multiplyScalar(stage.clientWidth<600?1.7:1); controls.target.set(0,20,0);",
  "camera.position.set(0,1000,1250).multiplyScalar(stage.clientWidth<600?1.8:1); controls.target.set(0,20,0);")

# ---------- 标注 ----------
R("PID.tpu=lab(hTPU,[-FX,13,-40],'<em>P4</em>TPU 卡托','printed tpu',()=>true);",
  "PID.tray=lab(hTray,[42.3,13,40],'<em>P4</em>抽屉托','printed',()=>true);\nPID.tpu=lab(hTPU,[-40.8,13,-40],'<em>P5</em>TPU 垫','printed tpu',()=>true);")
R("PID.phone=lab(hPhone,[38,16,40],'手机（示意）','',after(2));", "PID.phone=lab(hPhone,[38,16,40],'手机（示意）','',after(1));")
R("'合页销 M3×30','',after(4)", "'合页销 M3×30','',after(6)")
R("'3007 风扇','',after(7)", "'3007 风扇','',after(9)")
R("'PCB + 摄像头','',after(8)", "'PCB + 摄像头','',after(10)")
R("PID.cable=lab(hCable,[42,70,-91.5],'手机充电线','',after(9));", "PID.cable=lab(hCable,[5.5,14.7,-83],'手机充电线','',after(11));")
R("'钕磁铁 Ø6×3','',after(10)", "'钕磁铁 Ø6×3','',after(12)")
R("'热熔螺母 M2','',after(11)", "'热熔螺母 M2','',after(13)")
R("lab(hBody,[5,14,-84],'90° 弯头 C 公头，先插后放','wire',WL);",
  "lab(hCable,[5.5,14,-84],'插头卡在抽屉托后端，手机往后推就插上','wire',WL);\nlab(hBody,[0,4,-45],'抽屉托下 U 形余量线（扁平 2 芯）','wire',WL);")
R("[[30,7,0],'4 mm 支撑筋'],[[FX+2,15,60],'手机框 85 × 175'],", "[[0,SLOT_Y,L/2],'抽屉口 88 × 22'],[[30,RIB,0],'导轨筋'],[[FX+2,15,60],'手机框侧壁'],")
R(",[[-36,7.3,-84],'定位标记'] ].forEach(([p,t])=>lab(hBody,p,t,'feat',after(12)));", " ].forEach(([p,t])=>lab(hBody,p,t,'feat',after(14)));\nlab(hTray,[-36,7.3,-84],'定位标记','feat',k=>k===4);")

# ---------- 姿态 ----------
R("lidPivot.rotation.x = -Math.PI*100/180 * tOf(1,v) * (1-tOf(5,v));", "lidPivot.rotation.x = -Math.PI*100/180 * tOf(5,v) * (1-tOf(7,v));")
R("""  const tc=tOf(9,v), off=new T.Vector3(85*tc,0,0);
  const pts=CAB_FIXED.map(p=>p.clone().add(off));""", """  const tc=tOf(11,v), off=new T.Vector3(85*tc,0,0);
  const pts=slackPts(TRAVEL*tOf(1,v)).concat(CAB_FIXED).map(p=>p.clone().add(off));""")
R("TubeGeometry(new T.CatmullRomCurve3(pts,false,'centripetal'),300,1.6,8,false),mCab); asm.add(cableMesh);",
  "TubeGeometry(new T.CatmullRomCurve3(pts,false,'centripetal'),400,1.0,8,false),mCab); asm.add(cableMesh);")
R("if(k===0) h+='<p class=\"note\">打印件 4 个：P1 底仓、P2 顶盖外壳、P3 摄像头安装板、P4 TPU 卡托。点「走线」「风道」看线和风怎么走。</p>';",
  "if(k===0) h+='<p class=\"note\">打印件 5 个：P1 底仓、P2 顶盖外壳、P3 摄像头安装板、P4 抽屉托、P5 TPU 垫。点「走线」「风道」看线和风怎么走。</p>';")
R("function frameFor(x){ const s=Math.min(1,Math.max(0,x)), s2=Math.min(1,Math.max(0,(x-4)/3)); return {ty:69+45*s+65*s2, r:1+0.3*s+0.3*s2}; }",
  "function frameFor(x){ const c=a=>Math.min(1,Math.max(0,a)); const s0=c(x), s=c(x-4), s2=c((x-6)/3); return {ty:69+45*s+65*s2, r:1+0.15*s0+0.3*s+0.3*s2}; }")

# ---------- 零件卡与清单 ----------
R("""  tpu:'<svg viewBox="0 0 220 90" aria-label="卡托打印方向示意">""", """  tray:'<svg viewBox="0 0 220 90" aria-label="抽屉托打印方向示意"><rect x="10" y="78" width="200" height="6" fill="var(--ink)"/><path d="M20 76 H196 V52" stroke="var(--muted)" stroke-width="3" fill="none"/><path d="M28 76 V66 M60 76 V66" stroke="var(--muted)" stroke-width="3"/><text x="110" y="50" text-anchor="middle" font-size="11" fill="var(--muted)">底面贴板，前面板竖着</text></svg>',
  tpu:'<svg viewBox="0 0 220 90" aria-label="TPU 垫打印方向示意">""")
R("""  tpu:['软料吸震，不刮手机','坏了单独重打，0.4 h','按手机分档，屏幕中心对准镜头']""",
  """  tray:['底 1.2 mm 但有两条侧边撑着，拉出来不会塌','插头座把插头卡死，插拔手机时插头不会跟着动','前面板 2.4 mm，堵住抽屉口不漏光','2 个弹性限位舌，拉到 100 mm 停住，不会一下拉掉'],
  tpu:['软料吸震，不刮手机','前垫是波浪形，压缩 5 mm，顶住手机不松','坏了单独重打，0.4 h','按手机分档，屏幕中心对准镜头']""")
R("body:['合页耳下面有 45° 斜撑，受力不会把耳朵掰断','手机框只有 2 mm 厚、12 mm 高，留中间缺口方便手指取手机','底板 3 mm，支撑筋把手机垫高 4 mm 通风']",
  "body:['合页耳下面有 45° 斜撑，受力不会把耳朵掰断','抽屉口上面还有 95 mm 高的前壁，和两侧壁连成一圈，开口不会变形','4 条导轨筋托住抽屉托，中间 22 mm 宽的槽给余量线滚动']")
R("['body','shell','plate','tpu'].map(id=>", "['body','shell','plate','tray','tpu'].map(id=>")
RS("const BOM=[", "document.getElementById('bom')", """const BOM=[
  ['P1 底仓','打印','1','PETG HF 黑','14'],['P2 顶盖外壳','打印','1','PETG HF 黑','8'],['P3 摄像头安装板','打印','1','PETG HF 黑','7'],['P4 抽屉托','打印','1','PETG HF 黑','4'],['P5 TPU 垫','打印','按手机选 1 套','TPU 95A','3'],
  ['PCB + 摄像头','你的设计','1','57 × 28 × 6.5 mm，摄像头 Ø13.8 × 15 mm','10'],['3007 风扇','采购','1','30 × 30 × 7 mm，5 V 滚珠','9'],
  ['手机充电线','采购','1 + 1 备用','约 500 mm 扁平 2 芯硅胶线，下出线弯头 C 公头（内置 56 kΩ），JST-XH 2P','11'],['合页销','采购','2','M3×30 + 防松螺母','6'],['钕磁铁','采购','4','Ø6 × 3 mm','12'],
  ['热熔螺母','采购','8','M2','13'],['其他螺丝','采购','10','M2×6 ×8（PCB、安装板）、M2 自攻 ×2（风扇）（图中省略）','—'],
  ['USB-C 短延长线 + 面板座','待确认','1','PCB 的 USB 口够不到侧壁时才需要','—'],['PD 充电器 + 1 m C-C 线','随机附','1','必须支持 5 V 3 A','—']
];
""")
open('v16_src.html', 'w').write(s)
print('ok')
