




















(function () {
  'use strict';

  var MIN_SPEED = 0.1;
  var MAX_SPEED = 1.0;

  
  var MOTOR_RATIOS = [25, 20, 25, 10, 4, 1];
  var MOTOR_MAX_RPM = [200, 200, 200, 200, 200, 200];
  var VEL_MARGIN = 1.2;

  




  var ARM_ACK_MS = 4000;
  
  var GOTO_MARGIN_MS = 2500;

  function fmtS(v) { return (Math.round(v * 10) / 10) + 's'; }

  









  function expandSvg(open) {
    
    var d = open
      ? 'M3 9h6V3M21 9h-6V3M3 15h6v6M21 15h-6v6' +      
        'M9 9L3 3M15 9l6-6M9 15l-6 6M15 15l6 6'         
      : 'M9 3H3v6M15 3h6v6M9 21H3v-6M15 21h6v-6' +      
        'M3 3l6 6M21 3l-6 6M3 21l6-6M21 21l-6-6';       
    return '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" ' +
      'stroke="currentColor" stroke-width="2" stroke-linecap="round" ' +
      'stroke-linejoin="round" style="display:block;">' +
      '<path d="' + d + '"/></svg>';
  }

  


  function paintExpand(btn, open) {
    btn.innerHTML = expandSvg(open);
    btn.title = open ? VizI18n.tr('viz_teach_track_collapse', '收起时间轴')
                     : VizI18n.tr('viz_teach_track_open', '展开时间轴，加动作点');
    btn.style.cssText = 'flex:0 0 auto; padding:0 5px; line-height:1;' +
      (open ? 'background:#2d5f8b; border-color:#4da3ff; color:#fff;' : '');
  }

  




  var MAX_WAIT_S = 600;
  var GRIPPER_MAX_DEG = 110;
  var DEFAULT_WAIT_S = 1.0;

  function clampNum(v, lo, hi, dflt) {
    v = parseFloat(v);
    if (!isFinite(v)) return dflt;
    return Math.max(lo, Math.min(hi, v));
  }

  



  function actionDwellS(a) {
    if (!a) return 0;
    if (a.type === 'wait') return Number(a.seconds) || 0;
    return 0;
  }

  


  function dwellOf(actions) {
    var s = 0;
    (actions || []).forEach(function (a) { s += actionDwellS(a); });
    return s;
  }

  

  function stopsOf(t) {
    var byAt = {};
    (t.actions || []).forEach(function (a) {
      (byAt[a.at] = byAt[a.at] || []).push(a);
    });
    return Object.keys(byAt).map(Number).sort(function (x, y) { return x - y; })
      .map(function (at) {
        return { at: at, actions: byAt[at], dwell: dwellOf(byAt[at]) };
      });
  }

  function sortActions(t) {
    
    t.actions.sort(function (a, b) { return a.at - b.at; });
  }

  


  function Player() {
    this.playing = false;
    this.teach = null;
    this._t0 = 0;
    this._pausedUntil = 0;     
  }

  Player.prototype.start = function (teach) {
    this.teach = teach;
    this.playing = true;
    this._t0 = performance.now();
    
    this._stops = stopsOf(teach);
    this._acts = actionTimesOf(this._stops, this._rate());
    this._events = toolEventsOf(this._acts);
    this._fired = 0;
  };

  









  function actionTimesOf(stops, rate) {
    var out = [], acc = 0;
    stops.forEach(function (st) {
      var off = 0;
      st.actions.forEach(function (a) {
        out.push({ t: st.at / rate + acc + off, a: a });
        off += actionDwellS(a);
      });
      acc += st.dwell;
    });
    return out;      
  }

  





  function toolEventsOf(acts) {
    return acts.filter(function (e) { return e.a.type !== 'wait'; });
  }

  Player.prototype.stop = function () {
    this.playing = false;
    this.teach = null;
  };

  








  Player.prototype._rate = function () {   
    return this.teach.hz * this.teach.speed;
  };

  

  Player.prototype.elapsedS = function () {
    var e = (performance.now() - this._t0) / 1000;
    return e > 0 ? e : 0;                   
  };

  




  Player.prototype._at = function () {
    var e = this.elapsedS();
    var r = this._rate(), stops = this._stops || [], acc = 0;
    for (var k = 0; k < stops.length; k++) {
      var arrive = stops[k].at / r + acc;
      if (e < arrive) break;                
      if (e < arrive + stops[k].dwell) {
        return { idx: stops[k].at, stop: k, off: e - arrive };   
      }
      acc += stops[k].dwell;
    }
    return { idx: (e - acc) * r, stop: -1, off: 0 };
  };

  Player.prototype._idx = function () {
    return this._at().idx;
  };

  
  Player.prototype.sample = function (now) {
    var t = this.teach;
    if (!this.playing || !t || !t.points.length) return null;
    
    var idx = this._idx();
    if (idx >= t.points.length - 1) return t.points[t.points.length - 1].slice();
    var i = Math.floor(idx), k = idx - i;
    var a = t.points[i], b = t.points[i + 1], out = [];
    for (var j = 0; j < 6; j++) out.push(a[j] + (b[j] - a[j]) * k);
    return out;
  };

  Player.prototype.done = function (now) {
    var t = this.teach;
    if (!this.playing || !t) return false;
    
    var e = (performance.now() - this._t0) / 1000;
    return e >= this.durationS(t);
  };

  


  Player.prototype.seek = function (idx) {
    var t = this.teach;
    if (!t) return;
    var stops = this._stops || (this._stops = stopsOf(t)), acc = 0;
    for (var k = 0; k < stops.length; k++) {
      if (stops[k].at >= idx) break;
      acc += stops[k].dwell;
    }
    this._t0 = performance.now() - (idx / this._rate() + acc) * 1000;
    
    
    var e = this.elapsedS();
    var ev = this._events || [];
    this._fired = 0;
    while (this._fired < ev.length && ev[this._fired].t <= e + 1e-6) this._fired++;
    return e;
  };

  


  Player.prototype.toolStateAt = function (e) {
    var ev = this._events || [], last = {};
    for (var i = 0; i < ev.length && ev[i].t <= e + 1e-6; i++) last[ev[i].a.type] = ev[i].a;
    return Object.keys(last).map(function (k) { return last[k]; });
  };

  




  Player.prototype.actionAt = function (e) {
    var acts = this._acts || [], last = null;
    for (var i = 0; i < acts.length && acts[i].t <= e + 1e-6; i++) last = acts[i].a;
    return last;
  };

  
  Player.prototype.progress = function () {
    return this.teach ? this._idx() : 0;
  };

  
  Player.prototype.durationS = function (t) {
    return (t.points.length - 1) / t.hz / t.speed + dwellOf(t.actions);
  };

  

  function TeachesUI(rootEl, opts) {
    opts = opts || {};
    this.root = rootEl;
    this.teaches = [];
    this.getServerRunning = opts.getServerRunning || function () { return false; };
    this.getExecuting = opts.getExecuting || function () { return false; };
    


    this.getOtherBusy = opts.getOtherBusy || function () { return false; };
    this.onJoints = opts.onJoints || function () {};        
    this.onPlayingChanged = opts.onPlayingChanged || function () {};
    this.onPreview = opts.onPreview || function () {};      
    this.computePath = opts.computePath || function () { return null; };
    this.confirmDelete = opts.confirmDelete || function (t, ok) { ok(); };
    
    this.onToolEvent = opts.onToolEvent || function () {};
    
    this.onActionsChanged = opts.onActionsChanged || function () {};
    
    this.getToolKind = opts.getToolKind || function () { return null; };
    
    this.getToolAngle = opts.getToolAngle || function () { return 0; };
    
    this.showMenu = opts.showMenu || function () {};
    
    this.onOpenTrack = opts.onOpenTrack || function () {};
    this.onCloseTrack = opts.onCloseTrack || function () {};   
    
    this.isTrackOpen = opts.isTrackOpen || function () { return false; };
    this.onToggleFold = opts.onToggleFold || function () {};  
    
    this.getItemFolded = opts.getItemFolded || function () { return false; };
    this.onItemFold = opts.onItemFold || function () {};
    this.onAdd = opts.onAdd || function () {};              
    
    this.onGotoStart = opts.onGotoStart || function () {};
    this.player = new Player();
    this.running = null;        
    this.syncing = false;       
    this.selected = null;       
    this._seq = 0;
    this._armed = null;
    this._armTimer = 0;
    this._gotoEta = 0;          
    this._wrapping = false;     
    this._wrapTimer = 0;
    this.runCycles = 0;         
    this._runAction = null;     
    
    
    this._prep = { bar: new PrepBar({ variant: 'banner' }) };
    this._build();
  }

  TeachesUI.prototype._build = function () {
    var self = this;
    
    
    
    
    this.section = PanelSection.make({
      key: 'teaches', i18nKey: 'viz_teach_section', title: '示教轨迹',
      listId: 'te-list',
      buttons: [{
        id: 'te-add', text: '+',
        i18nTitleKey: 'viz_teach_add', title: '开始拖拽示教',
        onClick: function (e) { self.onAdd(e.clientX, e.clientY); }
      }],
      onToggle: this.onToggleFold
    });
    this.section.sec.style.marginTop = '8px';
    this.root.appendChild(this.section.sec);
    this.listEl = this.section.listEl;
    this._render();
  };

  
  TeachesUI.prototype.addRecorded = function (p) {
    if (!p || !p.ok || !p.points || p.points.length < 2) return null;
    this._seq++;
    var t = {
      name: 'teach' + this._seq,
      hz: p.hz || 50,
      speed: 1.0,
      syncReal: false,
      loop: false,
      points: p.points.map(decodeRow),
      raw: p.points,                     
      actions: []                        
    };
    
    
    t.peakRpm = p.peak_rpm || peakRpm(t.points, t.hz, 1.0);
    t.maxSafe = (typeof p.max_safe_speed === 'number') ? p.max_safe_speed
                                                       : maxSafeSpeed(t.peakRpm);
    this.teaches.push(t);
    this._render();
    return t;
  };

  function decodeRow(s) {
    if (Array.isArray(s)) return s.map(Number);
    return String(s).split(',').map(Number);
  }

  

  function peakRpm(points, hz, speed) {
    var out = [0, 0, 0, 0, 0, 0];
    var dt = 1 / hz / Math.max(speed, 1e-6);
    for (var i = 1; i < points.length; i++) {
      for (var j = 0; j < 6; j++) {
        var rpm = Math.abs(points[i][j] - points[i - 1][j]) / dt
                  / 360 * 60 * MOTOR_RATIOS[j] * VEL_MARGIN;
        if (rpm > out[j]) out[j] = rpm;
      }
    }
    return out;
  }

  
  function maxSafeSpeed(peakAt1x) {
    var s = 1.0;
    for (var j = 0; j < 6; j++) {
      if (peakAt1x[j] > 1e-9) s = Math.min(s, MOTOR_MAX_RPM[j] / peakAt1x[j]);
    }
    return Math.max(0.05, Math.min(1.0, s));
  }

  




  function overJoints(t) {
    var out = [];
    for (var j = 0; j < 6; j++) {
      if (t.peakRpm[j] * t.speed > MOTOR_MAX_RPM[j] * (1 + 1e-9)) out.push(j + 1);
    }
    return out;
  }

  








  function realSpeedCap(t) {
    return Math.max(0.05, Math.min(MAX_SPEED, Math.floor(t.maxSafe * 20) / 20));
  }

  TeachesUI.prototype.remove = function (t) {
    
    
    if (this.editLocked()) return;
    var i = this.teaches.indexOf(t);
    if (i === -1) return;
    if (this.running === t) this.stop();
    if (this.selected === t) { this.selected = null; this.onPreview(null); }
    this.teaches.splice(i, 1);
    this._render();
  };

  

  TeachesUI.prototype.serialize = function () {
    return this.teaches.map(function (t) {
      return {
        name: t.name, hz: t.hz, speed: t.speed, sync_real: !!t.syncReal,
        loop: !!t.loop,
        points: t.raw || t.points.map(function (p) {
          return p.map(function (v) { return v.toFixed(2); }).join(',');
        }),
        
        actions: (t.actions || []).map(function (a) { return copyAction(a); })
      };
    });
  };

  function copyAction(a) {
    var o = { at: a.at, type: a.type };
    if (a.type === 'wait') o.seconds = a.seconds;
    else if (a.type === 'gripper') o.angle_deg = a.angle_deg;
    else if (a.type === 'vacuum') o.state = a.state;
    return o;
  }

  


  function restoreActions(raw, nPoints) {
    var out = [];
    (raw || []).forEach(function (a) {
      if (!a || typeof a !== 'object') return;
      var at = Math.round(Number(a.at));
      if (!isFinite(at) || at < 0 || at >= nPoints) {
        VizChannel.log(VizI18n.tr('viz_log_import_tag', '导入：') +
          VizI18n.tr('viz_log_action_oob',
            '示教动作点 at={at} 越界，已丢弃').replace('{at}', a.at));
        return;
      }
      if (a.type === 'wait') {
        out.push({ at: at, type: 'wait',
                   seconds: clampNum(a.seconds, 0, MAX_WAIT_S, DEFAULT_WAIT_S) });
      } else if (a.type === 'gripper') {
        out.push({ at: at, type: 'gripper',
                   angle_deg: clampNum(a.angle_deg, 0, GRIPPER_MAX_DEG, 0) });
      } else if (a.type === 'vacuum') {
        out.push({ at: at, type: 'vacuum', state: a.state === 'off' ? 'off' : 'on' });
      }
    });
    out.sort(function (x, y) { return x.at - y.at; });
    return out;
  }

  TeachesUI.prototype.restore = function (list) {
    this.stop();
    this.teaches = [];
    this._seq = 0;
    var self = this;
    (list || []).forEach(function (raw) {
      if (!raw || !raw.points || raw.points.length < 2) return;
      var t = {
        name: raw.name || ('teach' + (self.teaches.length + 1)),
        hz: raw.hz || 50,
        speed: Math.min(MAX_SPEED, Math.max(MIN_SPEED, raw.speed || 1.0)),
        syncReal: !!raw.sync_real,
        loop: !!raw.loop,
        points: raw.points.map(decodeRow),
        raw: raw.points,
        actions: restoreActions(raw.actions, raw.points.length)
      };
      t.peakRpm = peakRpm(t.points, t.hz, 1.0);
      t.maxSafe = maxSafeSpeed(t.peakRpm);
      self.teaches.push(t);
      var n = parseInt(String(t.name).replace(/^\D+/, ''), 10);
      if (n > self._seq) self._seq = n;     
    });
    this._render();
  };

  TeachesUI.prototype.clear = function () { this.restore([]); };

  

  


  TeachesUI.prototype.play = function (t) {
    if (!t || this.player.playing) return;
    
    
    if (this.getOtherBusy()) return;
    if (t.syncReal) {
      var cap = realSpeedCap(t);
      if (t.speed > cap) { t.speed = cap; this._render(); }
    }
    this.running = t;
    this.syncing = !!t.syncReal && !!VizChannel.bridge;
    this.runCycles = 0;
    if (!this.syncing) {
      this.player.start(t);
      this._afterStart();
      return;
    }
    
    
    
    
    VizChannel.bridge.play_teach(JSON.stringify({
      points: this.serializeOne(t), hz: t.hz, speed: t.speed,
      actions: (t.actions || []).map(copyAction), loop: !!t.loop
    }));
    this._armPending(t);
    this._render();
  };

  TeachesUI.prototype.serializeOne = function (t) {
    return t.raw || t.points.map(function (p) {
      return p.map(function (v) { return v.toFixed(2); }).join(',');
    });
  };

  TeachesUI.prototype._armPending = function (t) {
    this._clearArm();
    this._armed = t;
    this._gotoEta = 0;
    
    
    this._setArmTimer(ARM_ACK_MS,
      VizI18n.tr('viz_arm_why_ack', '{ms}ms 内没等到真机回话').replace('{ms}', ARM_ACK_MS));
  };

  TeachesUI.prototype._setArmTimer = function (ms, why) {
    var self = this;
    if (this._armTimer) clearTimeout(this._armTimer);
    this._armTimer = setTimeout(function () {
      if (!self._armed) return;
      VizChannel.log(VizI18n.tr('viz_log_arm_timeout',
        '未等到真机起跑事件（{why}），按原方式起播').replace('{why}', why));
      self.armPlayback();
    }, ms);
  };

  




  TeachesUI.prototype.notePreparing = function (etaS, cycle) {
    
    
    if (!this._armed && this.running && (this.player.playing || this._wrapping)) {
      this._wrapping = false;
      this.player.stop();
      this._armPending(this.running);
    }
    if (!this._armed) return;
    var eta = Math.max(0, Number(etaS) || 0);
    this._gotoEta = eta;
    this._cycle = Math.max(0, Number(cycle) || 0);
    
    
    if (this._cycle >= 1) this.runCycles = Math.max(this.runCycles, this._cycle - 1);
    
    
    
    this._prep.bar.clear();
    this._setArmTimer(eta * 1000 + GOTO_MARGIN_MS,
      VizI18n.tr('viz_arm_why_eta', '对位预计 {s}s 已过').replace('{s}', eta.toFixed(1)));
    if (this._armed.points.length) this.onGotoStart(this._armed.points[0].slice(), eta);
    this._render();
  };

  TeachesUI.prototype._clearArm = function () {
    if (this._armTimer) { clearTimeout(this._armTimer); this._armTimer = 0; }
    this._armed = null;
    this._gotoEta = 0;
    this._cycle = 0;
  };

  

  TeachesUI.prototype.armPlayback = function () {
    var t = this._armed;
    if (!t) return;
    this._clearArm();
    this.player.start(t);
    this._afterStart();
  };

  TeachesUI.prototype._afterStart = function () {
    this.onPlayingChanged(true);
    this._render();
  };

  TeachesUI.prototype.stop = function () {
    this._clearArm();
    
    this._wrapping = false;
    if (this._wrapTimer) { clearTimeout(this._wrapTimer); this._wrapTimer = 0; }
    var wasSync = this.syncing;
    this.player.stop();
    this.running = null;
    this.syncing = false;
    this.runCycles = 0;
    
    
    if (wasSync && VizChannel.bridge) VizChannel.bridge.stop_teach_play();
    this.onPlayingChanged(false);
    this._paintActionRun();      
    this._render();
  };

  




  TeachesUI.prototype.seek = function (idx) {
    var e = this.player.seek(idx);
    
    
    
    this.onToolEvent({ type: 'reset' });
    this._emitToolEvents(this.player.toolStateAt(e));
    this._paintActionRun();      
  };

  
  TeachesUI.prototype.update = function (now) {
    if (!this.player.playing) return false;
    var q = this.player.sample(now);
    if (q) this.onJoints(q);
    var t = this.running;
    
    
    
    var p = this.player, e = p.elapsedS(), ev = p._events || [];
    while (p._fired < ev.length && ev[p._fired].t <= e + 1e-6) {
      this._emitToolEvents([ev[p._fired++].a]);
    }
    
    this._paintActionRun();
    
    
    if (t && t._seek) {
      t._seek.paint(Math.min(t.points.length - 1, Math.round(this.player.progress())));
    }
    if (this.player.done(now)) {
      if (t && t.loop) { this._wrap(t); return !!q; }
      
      
      this.player.stop();
      this.running = null;
      this.syncing = false;
      this.runCycles = 0;
      this.onPlayingChanged(false);
      this._render();
    }
    return !!q;
  };

  






  TeachesUI.prototype._wrap = function (t) {
    var self = this;
    this.player.stop();
    this._wrapping = true;
    


    this.runCycles += 1;
    this._paintCycles(t);
    if (this.syncing) return;                 
    var wrap = wrapDurationS(t);
    this._wrapEta = wrap;
    this._prep.bar.clear();                 
    this.onGotoStart(t.points[0].slice(), wrap);
    if (this._wrapTimer) clearTimeout(this._wrapTimer);
    this._wrapTimer = setTimeout(function () {
      self._wrapTimer = 0;
      if (!self._wrapping || self.running !== t) return;   
      self._wrapping = false;
      self.player.start(t);
      self._render();
    }, wrap * 1000);
    this._render();
  };

  
  function wrapDurationS(t) {
    var a = t.points[t.points.length - 1], b = t.points[0], mx = 0;
    for (var j = 0; j < 6; j++) mx = Math.max(mx, Math.abs(a[j] - b[j]));
    return Math.max(0.5, Math.min(5, mx / 60));
  }

  

  TeachesUI.prototype._emitToolEvents = function (actions) {
    var self = this;
    (actions || []).forEach(function (a) {
      if (a.type === 'gripper') {
        self.onToolEvent({ type: 'servo', angle: a.angle_deg });
      } else if (a.type === 'vacuum') {
        self.onToolEvent({ type: 'vacuum', on: a.state === 'on' });
      }
    });
  };

  
  TeachesUI.prototype.playing = function () {
    return this.player.playing || !!this._armed || !!this._wrapping;
  };

  









  TeachesUI.prototype.editLocked = function () {
    return !!(this.syncing && this.playing());
  };

  


  TeachesUI.prototype._paintCycles = function (t) {
    if (!t || !t.cycleEl) return;
    var on = (this.running === t && this.playing() && !!t.loop);
    t.cycleEl.style.display = on ? '' : 'none';
    if (on) {
      t.cycleEl.textContent = VizI18n.tr('viz_motion_cycles', '↻ 已循环 {n} 次')
        .replace('{n}', this.runCycles);
    }
  };

  

  






  TeachesUI.prototype.addAction = function (t, at, kind, order, init) {
    if (!t || this.editLocked()) return null;
    at = Math.max(0, Math.min(t.points.length - 1, Math.round(at) || 0));
    var a = { at: at, type: kind };
    if (kind === 'wait') a.seconds = DEFAULT_WAIT_S;
    


    else if (kind === 'gripper') a.angle_deg = clampNum(this.getToolAngle(),
                                                        0, GRIPPER_MAX_DEG, 0);
    else if (kind === 'vacuum') a.state = 'on';
    else return null;
    if (init) {
      Object.keys(init).forEach(function (k) {
        if (k !== 'at' && k !== 'type') a[k] = init[k];   
      });
    }
    if (typeof order === 'number' && order >= 0 && order <= t.actions.length) {
      t.actions.splice(order, 0, a);
    } else {
      t.actions.push(a);
    }
    sortActions(t);
    this._render();
    this.onActionsChanged(t, a);
    return a;
  };

  

  TeachesUI.prototype.removeAction = function (t, a) {
    if (this.editLocked()) return;
    var i = t.actions.indexOf(a);
    if (i === -1) return;
    t.actions.splice(i, 1);
    this._render();
    this.onActionsChanged(t, null);
  };

  


  function mergeWaitsAt(t, at) {
    var all = t.actions || [];
    var run = all.filter(function (x) { return x.at === at; });
    if (run.length < 2) return;
    var i0 = all.indexOf(run[0]), out = [];
    run.forEach(function (x) {
      var last = out[out.length - 1];
      if (x.type === 'wait' && last && last.type === 'wait') {
        last.seconds = round1(last.seconds + x.seconds);
      } else {
        out.push(x);
      }
    });
    if (out.length === run.length) return;
    all.splice(i0, run.length);
    out.forEach(function (x, k) { all.splice(i0 + k, 0, x); });
  }

  
  TeachesUI.prototype.moveAction = function (t, a, at) {
    if (this.editLocked()) return false;
    at = Math.max(0, Math.min(t.points.length - 1, Math.round(at) || 0));
    if (a.at === at) return false;
    var from = a.at;
    a.at = at;
    sortActions(t);
    if (a.type !== 'wait') mergeWaitsAt(t, from);   
    this._render();
    this.onActionsChanged(t, a);
    return true;
  };

  function round1(v) { return Math.round((Number(v) || 0) * 10) / 10; }

  









  TeachesUI.prototype.setActionOffset = function (t, a, offS) {
    if (!t || !a || a.type === 'wait' || this.editLocked()) return false;
    var all = t.actions || [];
    var run = all.filter(function (x) { return x.at === a.at; });
    if (run.indexOf(a) < 0) return false;
    var i0 = all.indexOf(run[0]);

    
    var merged = [];
    run.forEach(function (x) {
      if (x === a) return;
      var last = merged[merged.length - 1];
      if (x.type === 'wait' && last && last.type === 'wait') {
        last.seconds = round1(last.seconds + x.seconds);
      } else {
        merged.push(x);
      }
    });

    
    var total = merged.reduce(function (s, x) { return s + actionDwellS(x); }, 0);
    offS = Math.max(0, Math.min(total, round1(offS)));
    var out = [], acc = 0, placed = false;
    merged.forEach(function (x) {
      var d = actionDwellS(x);
      if (!placed && offS <= acc + 1e-9) { out.push(a); placed = true; }
      if (!placed && x.type === 'wait' && offS < acc + d - 1e-9) {
        var head = round1(offS - acc);
        if (head > 0) out.push({ at: a.at, type: 'wait', seconds: head });
        out.push(a);
        placed = true;
        var tail = round1(d - head);
        if (tail > 0) { x.seconds = tail; out.push(x); }
        acc += d;
        return;
      }
      out.push(x);
      acc += d;
    });
    if (!placed) out.push(a);
    
    out = out.filter(function (x) {
      return !(x.type === 'wait' && !(Number(x.seconds) > 0));
    });

    
    all.splice(i0, run.length);
    out.forEach(function (x, k) { all.splice(i0 + k, 0, x); });
    sortActions(t);
    this._render();
    this.onActionsChanged(t, a);
    return true;
  };

  


  TeachesUI.prototype.setActionSeconds = function (t, a, sec) {
    if (!t || !a || a.type !== 'wait' || this.editLocked()) return false;
    var v = clampNum(sec, 0, MAX_WAIT_S, DEFAULT_WAIT_S);
    v = round1(v);
    if (Math.abs(v - (Number(a.seconds) || 0)) < 1e-9) return false;
    a.seconds = v;
    this._render();
    this.onActionsChanged(t, a);
    return true;
  };

  
  TeachesUI.prototype.actionOffsetS = function (t, a) {
    var off = 0, hit = false;
    (t.actions || []).forEach(function (x) {
      if (x.at !== a.at || hit) return;
      if (x === a) { hit = true; return; }
      off += actionDwellS(x);
    });
    return off;
  };

  TeachesUI.prototype.stops = function (t) { return stopsOf(t); };
  
  TeachesUI.prototype.actionDwellS = function (a) { return actionDwellS(a); };

  

  TeachesUI.prototype._render = function () {
    var self = this;
    
    
    this._runAction = null;
    this.section.setCount(this.teaches.length);   
    this.listEl.innerHTML = '';
    
    this._paintPrep();
    if (this.teaches.length === 0) {
      var empty = document.createElement('div');
      empty.style.cssText = 'color:#8f979e; font-size:11px; padding:4px 0;';
      empty.textContent = VizI18n.tr('viz_teach_empty',
        '（暂无示教轨迹。点右上角的「+」，拖着真机走一段）');
      this.listEl.appendChild(empty);
      return;
    }
    this.teaches.forEach(function (t) { self.listEl.appendChild(self._renderRow(t)); });
  };

  function syncTag() {
    var tag = document.createElement('span');
    tag.textContent = ' ⚙' + VizI18n.tr('viz_tag_robot', '真机');
    tag.style.cssText = 'color:#ffcc44; font-weight:400; font-size:10px;';
    return tag;
  }

  

  TeachesUI.prototype._canPlay = function (t) {
    if (this.playing()) return false;
    
    
    
    if (this.getOtherBusy()) return false;
    if (!t.syncReal) return true;
    return this.getServerRunning() && !this.getExecuting();
  };

  


  TeachesUI.prototype._foldOf = function (t) {
    if (t.folded === undefined) t.folded = !!this.getItemFolded(t.name);
    if (this.running === t || this._armed === t) return false;
    return t.folded;
  };

  TeachesUI.prototype._toggleFold = function (t) {
    t.folded = !this._foldOf(t);
    this.onItemFold(t.name, t.folded);
    this._render();
  };

  TeachesUI.prototype._renderRow = function (t) {
    var self = this;
    var folded = this._foldOf(t);
    
    
    var lk = this.editLocked();
    var lkTip = VizI18n.tr('viz_edit_locked',
      '正在运行：这一段只能看，不能改。真机跑的是按下播放那一刻的副本。要改先停下来。');
    var box = document.createElement('div');
    box.style.cssText = 'border:1px solid #3a4148; border-radius:4px; padding:4px 6px; ' +
      'margin:3px 0; font-size:11px;';
    if (this.running === t) box.style.borderColor = '#4da3ff';
    else if (this.selected === t) box.style.borderColor = '#6b7883';

    
    var head = document.createElement('div');
    head.style.cssText = 'display:flex; align-items:center; gap:6px;';

    
    var caret = document.createElement('span');
    caret.setAttribute('data-role', 'fold-teach');
    caret.textContent = folded ? '▸' : '▾';
    caret.title = VizI18n.tr('viz_item_fold', '折叠/展开这一条');
    caret.style.cssText = 'flex:0 0 auto; width:10px; color:#8ab4f8; cursor:pointer;';
    caret.addEventListener('click', function (e) {
      e.stopPropagation();
      self._toggleFold(t);
    });
    head.appendChild(caret);

    var title = document.createElement('b');
    title.appendChild(document.createTextNode(t.name));
    
    if (t.syncReal) title.appendChild(syncTag());
    
    
    if (lk && this.running === t) {
      title.appendChild(document.createTextNode(' 🔒'));
      title.title = lkTip;
    }
    
    
    
    
    title.style.cssText = 'flex:0 1 auto; overflow:hidden; text-overflow:ellipsis; ' +
      'white-space:nowrap;';
    head.appendChild(title);

    var meta = document.createElement('span');
    meta.style.cssText = 'flex:1 1 auto; color:#8f979e; text-align:right; white-space:nowrap;';
    meta.textContent = fmtS((t.points.length - 1) / t.hz) + ' · ' +
      t.points.length + VizI18n.tr('viz_teach_pts', '点');
    
    
    if (folded && (t.actions || []).length) {
      meta.textContent += ' · ' + VizI18n.tr('viz_teach_folded_acts', '{n} 动作')
        .replace('{n}', t.actions.length);
    }
    head.appendChild(meta);

    




    t.cycleEl = document.createElement('span');
    t.cycleEl.style.cssText = 'flex:0 0 auto; color:#8fd08f; font-size:10px; ' +
      'white-space:nowrap;';
    t.cycleEl.title = VizI18n.tr('viz_motion_cycles_tip', '已完成的循环圈数');
    this._paintCycles(t);
    head.appendChild(t.cycleEl);

    var del = document.createElement('button');
    del.textContent = '×';
    del.disabled = lk;
    del.title = lk ? lkTip : VizI18n.tr('viz_wp_del', '删除');
    del.style.cssText = 'flex:0 0 auto; padding:0 6px; line-height:1.4;';
    del.addEventListener('click', function (e) {
      e.stopPropagation();
      self.confirmDelete(t, function () { self.remove(t); });
    });
    head.appendChild(del);
    box.appendChild(head);

    
    var srow = document.createElement('div');
    srow.style.cssText = 'display:flex; align-items:center; gap:6px; margin-top:3px;';
    var slab = document.createElement('span');
    slab.style.cssText = 'flex:0 0 auto; color:#8f979e;';
    slab.textContent = VizI18n.tr('viz_teach_speed', '速率');
    srow.appendChild(slab);
    var slider = document.createElement('input');
    slider.type = 'range';
    slider.min = MIN_SPEED; slider.max = MAX_SPEED; slider.step = 0.05;
    slider.value = t.speed;
    
    slider.disabled = lk;
    if (lk) { slider.title = lkTip; srow.style.opacity = '.45'; }
    slider.style.cssText = 'flex:1 1 auto; min-width:0;';
    srow.appendChild(slider);
    var sval = document.createElement('span');
    sval.style.cssText = 'flex:0 0 auto; width:38px; text-align:right; ' +
      'font-variant-numeric:tabular-nums;';
    srow.appendChild(sval);
    



    if (!folded) box.appendChild(srow);

    
    var warn = document.createElement('div');
    warn.style.cssText = 'color:#ffcc44; font-size:10px; line-height:1.5; margin-top:2px;';
    if (!folded) box.appendChild(warn);

    
    
    
    
    var krow = document.createElement('div');
    krow.style.cssText = 'display:flex; align-items:center; gap:6px; margin-top:3px;';
    var seek = document.createElement('input');
    seek.type = 'range';
    seek.min = 0; seek.max = Math.max(1, t.points.length - 1); seek.step = 1;
    





    t.seekAt = Math.max(0, Math.min(t.points.length - 1, Math.round(t.seekAt) || 0));
    seek.value = t.seekAt;
    seek.style.cssText = 'flex:1 1 auto; min-width:0;';
    krow.appendChild(seek);
    var kval = document.createElement('span');
    kval.style.cssText = 'flex:0 0 auto; width:62px; text-align:right; color:#8f979e; ' +
      'font-variant-numeric:tabular-nums;';
    krow.appendChild(kval);
    
    
    
    
    var expand = document.createElement('button');
    
    expand.setAttribute('data-role', 'expand-track');
    paintExpand(expand, this.isTrackOpen(t));
    expand.addEventListener('click', function (e) {
      e.stopPropagation();
      if (self.isTrackOpen(t)) self.onCloseTrack(t);
      else self.onOpenTrack(t);
      
      
      
      
      
      paintExpand(expand, self.isTrackOpen(t));
    });
    krow.appendChild(expand);
    if (!folded) box.appendChild(krow);

    



    function paintSeek(i) {
      t.seekAt = i;                    
      if (document.activeElement !== seek) seek.value = i;
      kval.textContent = fmtS(i / t.hz / t.speed) + '/' +
        fmtS((t.points.length - 1) / t.hz / t.speed);
    }
    


    function paintSeekEnabled() {
      var driving = (self.running === t) && self.syncing;
      seek.disabled = driving;
      krow.style.opacity = driving ? '.45' : '';
      seek.title = driving
        ? VizI18n.tr('viz_teach_seek_no_real', '同步真机回放时不能拖时间轴（真机没法跳帧）')
        : VizI18n.tr('viz_teach_seek', '拖动预览任意一帧');
    }
    seek.addEventListener('input', function () {
      var i = parseInt(seek.value, 10) || 0;
      i = Math.max(0, Math.min(t.points.length - 1, i));
      paintSeek(i);
      
      if (self.running === t && self.player.playing) self.seek(i);
      else self.onJoints(t.points[i].slice());
    });
    
    t._seek = {el: seek, paint: paintSeek};

    

    function applyCap() {
      var cap = t.syncReal ? realSpeedCap(t) : MAX_SPEED;
      
      slider.min = Math.min(MIN_SPEED, cap);
      slider.max = cap;
      if (t.speed > cap) t.speed = cap;
      slider.value = t.speed;
      return cap;
    }

    function paintSpeed() {
      var cap = applyCap();
      sval.textContent = t.speed.toFixed(2) + '×';
      var msg = '';
      if (t.syncReal && cap < MAX_SPEED - 1e-9) {
        



        msg = VizI18n.tr('viz_teach_cap',
          '⚠ 按本条轨迹算出的上限 {s}×（每条不同）：再快会抖动，可能损伤电机')
          .replace('{s}', cap.toFixed(2));
      } else if (t.syncReal) {
        




        var over = overJoints(t);
        if (over.length) {
          msg = VizI18n.tr('viz_teach_over',
            '⚠ J{j} 超出电机速率：可能抖动、位置不准确，建议用推荐速率 {s}×')
            .replace('{j}', over.join('/')).replace('{s}', t.maxSafe.toFixed(2));
        }
      }
      warn.style.display = msg ? '' : 'none';
      warn.textContent = msg;       
      paintSeek(parseInt(seek.value, 10) || 0);   
    }
    slider.addEventListener('input', function () {
      t.speed = parseFloat(slider.value) || 1.0;
      paintSpeed();
    });
    paintSpeed();
    paintSeekEnabled();

    
    var brow = document.createElement('div');
    brow.style.cssText = 'display:flex; align-items:center; gap:6px; margin-top:3px;';
    var sync = document.createElement('label');
    sync.className = 'chk';
    sync.style.cssText = 'flex:1 1 auto; padding:2px 6px; display:flex; ' +
      'align-items:center; gap:4px;';
    var cb = document.createElement('input');
    cb.type = 'checkbox';
    cb.checked = !!t.syncReal;
    
    
    
    cb.disabled = this.playing();
    
    
    cb.addEventListener('change', function () {
      t.syncReal = cb.checked;
      
      
      paintSpeed();
      paintSeekEnabled();
      play.disabled = !self._canPlay(t);
      title.innerHTML = '';
      title.appendChild(document.createTextNode(t.name));
      if (t.syncReal) title.appendChild(syncTag());
    });
    sync.appendChild(cb);
    sync.appendChild(document.createTextNode(VizI18n.tr('viz_teach_sync', '同步真机')));
    brow.appendChild(sync);

    
    
    var loopBtn = document.createElement('button');
    loopBtn.textContent = '↻';
    loopBtn.title = VizI18n.tr('viz_teach_loop', '循环执行（跑完回起点再来一遍）');
    loopBtn.disabled = this.playing();       
    loopBtn.style.cssText = 'flex:0 0 auto; padding:0 8px; line-height:1.4;' +
      (t.loop ? 'background:#2d5f8b; border-color:#4da3ff; color:#fff;' : '');
    loopBtn.addEventListener('click', function (e) {
      e.stopPropagation();
      t.loop = !t.loop;
      self._render();
      self.onActionsChanged(t, null);       
    });
    brow.appendChild(loopBtn);

    var playing = (this.running === t);
    var play = document.createElement('button');
    play.textContent = '▶';
    play.style.cssText = 'flex:0 0 auto; padding:0 8px; line-height:1.4;';
    play.disabled = !this._canPlay(t);
    
    
    
    
    play.title = !play.disabled ? VizI18n.tr('viz_teach_play', '播放')
      : this.getOtherBusy()
        ? VizI18n.tr('viz_play_busy_motion', '运动正在播放，先停下来')
        : (t.syncReal && !this.getServerRunning())
          ? VizI18n.tr('viz_teach_sync_need_server', '真机未连接')
          : VizI18n.tr('viz_teach_play', '播放');
    play.addEventListener('click', function (e) { e.stopPropagation(); self.play(t); });
    brow.appendChild(play);

    var stop = document.createElement('button');
    stop.textContent = '■';
    stop.title = VizI18n.tr('viz_teach_stop_play', '停止');
    stop.style.cssText = 'flex:0 0 auto; padding:0 8px; line-height:1.4;';
    stop.disabled = !playing && !(this._armed === t);
    stop.addEventListener('click', function (e) { e.stopPropagation(); self.stop(); });
    brow.appendChild(stop);
    box.appendChild(brow);

    
    
    
    (t.actions || []).forEach(function (a) {
      var row = self._renderAction(t, a);
      if (!folded) box.appendChild(row);
    });

    


    
    
    
    
    
    
    
    
    box.addEventListener('click', function (e) {
      if (e.target !== box && e.target.closest('input, button, label, select')) return;
      self.selected = (self.selected === t) ? null : t;
      self.onPreview(self.selected ? self.computePath(self.selected.points) : null);
      self._paintSelection();
    });
    t.rowEl = box;
    return box;
  };

  

  TeachesUI.prototype._renderAction = function (t, a) {
    var self = this;
    var lk = this.editLocked();       
    var lkTip = VizI18n.tr('viz_edit_locked',
      '正在运行：这一段只能看，不能改。真机跑的是按下播放那一刻的副本。要改先停下来。');
    var row = document.createElement('div');
    
    
    row.style.cssText = 'display:flex; align-items:center; gap:5px; margin:3px 0 0 8px; ' +
      'padding:2px 3px; border:1px solid transparent; border-radius:3px; ' +
      'font-size:11px; color:#c8cdd2;';
    a._rowEl = row;         
    


    var lab = document.createElement('span');
    lab.textContent = ActIcon.of(a.type, a.state !== 'off');
    lab.style.cssText = 'flex:0 0 auto; width:18px;';
    row.appendChild(lab);
    var name = document.createElement('span');
    name.textContent = { wait: VizI18n.tr('viz_act_wait', '延时'),
                         gripper: VizI18n.tr('viz_act_gripper', '夹爪开合'),
                         vacuum: VizI18n.tr('viz_act_vacuum', '吸盘吸/放') }[a.type] || '';
    
    
    name.style.cssText = 'flex:1 1 auto; min-width:0; color:#c5c8c6; overflow:hidden; ' +
      'text-overflow:ellipsis; white-space:nowrap;';
    
    var tool = this.getToolKind();
    if ((a.type === 'gripper' && tool !== 'gripper') ||
        (a.type === 'vacuum' && tool !== 'suction')) {
      name.style.color = '#ffcc44';
      row.title = VizI18n.tr('viz_act_tool_off',
        '当前没装这个工具，该动作在虚拟播放里看不到效果');
    }
    row.appendChild(name);

    if (a.type === 'wait') {
      var sec = document.createElement('input');
      sec.type = 'number';
      sec.min = '0'; sec.max = String(MAX_WAIT_S); sec.step = '0.1';
      sec.value = a.seconds;
      sec.disabled = lk;
      if (lk) sec.title = lkTip;
      sec.style.cssText = 'flex:0 0 auto; width:56px;';
      sec.addEventListener('change', function () {
        a.seconds = clampNum(sec.value, 0, MAX_WAIT_S, DEFAULT_WAIT_S);
        sec.value = a.seconds;
        self.onActionsChanged(t, a);
      });
      row.appendChild(sec);
      row.appendChild(document.createTextNode(VizI18n.tr('viz_act_sec', '秒')));
    } else if (a.type === 'gripper') {
      var ang = document.createElement('input');
      ang.type = 'number';
      ang.min = '0'; ang.max = String(GRIPPER_MAX_DEG); ang.step = '1';
      ang.value = a.angle_deg;
      ang.disabled = lk;
      if (lk) ang.title = lkTip;
      ang.style.cssText = 'flex:0 0 auto; width:56px;';
      ang.addEventListener('change', function () {
        a.angle_deg = clampNum(ang.value, 0, GRIPPER_MAX_DEG, 0);
        ang.value = a.angle_deg;
        self.onActionsChanged(t, a);
      });
      row.appendChild(ang);
      row.appendChild(document.createTextNode('°'));
    } else {
      var sel = document.createElement('select');
      sel.style.cssText = 'flex:0 0 auto;';
      sel.innerHTML =
        '<option value="on">' + VizI18n.tr('viz_suction_vacuum', '吸附') + '</option>' +
        '<option value="off">' + VizI18n.tr('viz_act_release', '放开') + '</option>';
      sel.value = a.state;
      sel.disabled = lk;
      if (lk) sel.title = lkTip;
      sel.addEventListener('change', function () {
        a.state = sel.value === 'off' ? 'off' : 'on';
        
        lab.textContent = ActIcon.of(a.type, a.state !== 'off');
        self.onActionsChanged(t, a);
      });
      row.appendChild(sel);
    }

    var at = document.createElement('span');
    at.style.cssText = 'flex:0 0 auto; text-align:right; color:#8f979e; ' +
      'font-variant-numeric:tabular-nums; cursor:pointer;';
    at.textContent = '@' + fmtS(a.at / t.hz / t.speed);
    


    if (lk) {
      at.title = lkTip;
      at.style.cursor = 'not-allowed';
      at.style.opacity = '.45';
    } else {
      at.title = VizI18n.tr('viz_teach_act_goto', '点这里在时间轴上定位');
      at.addEventListener('click', function (e) {
        e.stopPropagation();
        self.onOpenTrack(t, a);
      });
    }
    row.appendChild(at);

    var del = document.createElement('button');
    del.textContent = '×';
    del.disabled = lk;
    del.title = lk ? lkTip : VizI18n.tr('viz_wp_del', '删除');
    del.style.cssText = 'flex:0 0 auto; padding:0 5px; line-height:1.3;';
    del.addEventListener('click', function (e) {
      e.stopPropagation();
      self.removeAction(t, a);
    });
    row.appendChild(del);
    return row;
  };

  
  TeachesUI.prototype.prepEl = function () { return this._prep.bar.el(); };

  
  TeachesUI.prototype.tickHints = function () {
    if (!this._prep.bar.active()) return;
    this._paintPrep();
    this._prep.bar.tick();
  };

  






  TeachesUI.prototype._paintPrep = function () {
    var bar = this._prep.bar;
    if (this._wrapping && this.running) {
      bar.setEta(VizI18n.tr('viz_teach_wrap', '一圈跑完，正在绕回起点…'), this._wrapEta);
      return;
    }
    if (this._armed) {
      var eta = this._gotoEta;
      
      var what = eta ? VizI18n.tr('viz_teach_goto_start', '正在把真机移到轨迹起点…')
                     : VizI18n.tr('viz_teach_arming', '已发给真机，等它开始…');
      if (eta && this._cycle > 1) {
        what = VizI18n.tr('viz_teach_goto_cycle', '第 {n} 圈：').replace('{n}', this._cycle) +
               what;
      }
      bar.setEta(what, eta);
      return;
    }
    bar.clear();                 
  };

  




  var RUN_BORDER = '#4da3ff';
  var RUN_BG = 'rgba(77,163,255,.12)';

  function paintRun(a, on) {
    if (!a || !a._rowEl) return;
    a._rowEl.style.borderColor = on ? RUN_BORDER : 'transparent';
    a._rowEl.style.background = on ? RUN_BG : '';
  }

  TeachesUI.prototype._paintActionRun = function () {
    var t = this.running;
    var cur = (t && this.player.playing) ? this.player.actionAt(this.player.elapsedS()) : null;
    if (cur === this._runAction) return;      
    
    
    paintRun(this._runAction, false);
    this._runAction = cur;
    paintRun(cur, true);
  };

  
  TeachesUI.prototype._paintSelection = function () {
    var self = this;
    this.teaches.forEach(function (t) {
      if (!t.rowEl) return;
      t.rowEl.style.borderColor = (self.running === t) ? '#4da3ff'
        : (self.selected === t) ? '#6b7883' : '#3a4148';
    });
  };

  




  TeachesUI.prototype.refresh = function () {
    var a = document.activeElement;
    if (a && this.listEl.contains(a)) return;
    this._render();
  };

  window.TeachesUI = TeachesUI;
})();
