jinglun-cloud
18 小时以前 1ef1063281497f32fcfa4f14b07d99399c0bb765
refactor(设备运动): 重构条码设备运动逻辑,提取运动常量并优化代码结构

- 将运动相关常量提取到 MOTION 对象中集中管理
- 重构 calcSegmentDelta 函数,明确区分环形与非环形轨道计算
- 优化 finishDeviceMotion 函数,简化逻辑并直接解构 mappingInfo
- 拆分运动速度更新逻辑到独立函数 updateMotionSpeed
- 提取单线轨道步进计算到 computeSingleLineSignedStep 函数
- 简化设备同步逻辑,减少不必要的对象拷贝
- 统一设备适配器接口,提取公共逻辑到独立函数
1个文件已修改
464 ■■■■■ 已修改文件
src/main/webapp/components/MapCanvas.js 464 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/components/MapCanvas.js
@@ -10,7 +10,25 @@
// 仅本地测试用
const FAKE_MAX_LAYER = 1730000;
const FAKE_MAX_CRN_LAYER = 4800
const FAKE_MAX_CRN_LAYER = 4800;
// 条码设备运动学常量(setDeviceInfoByBarcode 链路使用)
// 这里集中放是为了让 tickBarcodeTrackSpriteMotion / updateMotionSpeed
// 等内部方法不再散布大量 magic number
const MOTION = {
  DEFAULT_TICKER_DELTA_MS: 16.667, // dt 缺失时按约 60fps 估算
  FALLBACK_BASE_V_FACTOR: 2, // baseV 兜底 = sprite.width * factor
  FALLBACK_BASE_V_MIN: 20,
  STALE_INTERVAL_MULT: 2.65, // 多少个典型推送周期后判定为长时间无更新
  SLOW_RADIUS_FACTOR: 6.5, // 进入该半径开始做停止圆缓动
  SLOW_RADIUS_MIN: 72,
  EASE_EXPONENT: 0.45, // 停止圆内速率与距离的幂次关系
  INITIAL_SPEED_RATIO: 0.35, // 起步速度 = 目标速度 * ratio
  MAX_ACCEL_FACTOR: 1.75, // 最大加速度 = baseV * factor
  MAX_ACCEL_MIN: 22,
  VELOCITY_EWMA_ALPHA: 0.2, // 服务端均速估计的 EWMA 平滑系数
  VELOCITY_SAMPLE_MAX_DT: 10 // 超过该秒数的样本不参与均速估计
};
/**
 * 通用轮询器类
 * 封装带请求取消、平滑延迟计算、错误退避的轮询逻辑
@@ -1325,29 +1343,28 @@
      return (value * 100).toFixed(1) + '%';
    },
    /******************setDeviceInfo使用的函数:******************/
    // 环穿条码在总长上的有符号段差
    calcSignedSegmentDelta(fromBarcode, toBarcode, trackInfo, totalSegmentCount) {
    /**
     * 计算两条码之间的"段差"。
     * - 环形轨道按总段数取模,永远向前推进([0, totalSegmentCount))。
     * - 非环形轨道返回有符号差,方向由调用方决定。
     */
    calcSegmentDelta(fromBarcode, toBarcode, isAnnulus, totalSegmentCount) {
      const from = Number(fromBarcode);
      const to = Number(toBarcode);
      if (!isFinite(from) || !isFinite(to)) return 0;
      const raw = to - from;
      if (trackInfo.type !== 'annulus' || totalSegmentCount <= 0) return raw;
      // 非负取模
      if (!isAnnulus || totalSegmentCount <= 0) return raw;
      return ((raw % totalSegmentCount) + totalSegmentCount) % totalSegmentCount;
    },
    /** 把设备直接对齐到 mappingInfo 上并清理 ticker */
    finishDeviceMotion(sprite) {
      if (!sprite || !sprite.mappingInfo) {
        return false;
      }
      const mx = sprite.mappingInfo.x;
      const my = sprite.mappingInfo.y;
      if (!isFinite(mx) || !isFinite(my)) {
        return false;
      }
      if (!sprite || !sprite.mappingInfo) return false;
      const { x: mx, y: my, path: mPath } = sprite.mappingInfo;
      if (!isFinite(mx) || !isFinite(my)) return false;
      sprite.isFinish = true;
      sprite.x = mx;
      sprite.y = my;
      sprite.path = sprite.mappingInfo.path || sprite.path;
      sprite.path = mPath || sprite.path;
      sprite.rotation = G.getRotate(sprite.mappingInfo, sprite.mappingInfo.path) || sprite.rotation;
      if (sprite.ticker) {
        this.pixiApp.ticker.remove(sprite.ticker);
@@ -1357,35 +1374,17 @@
      sprite._motionSpeed = undefined;
      return true;
    },
    /** Web 接口返回统一成设备数组 */
    parseBarcodeDevicesResponse(res) {
      return Array.isArray(res) ? res : res && res.code === 200 ? res.data : null;
    },
    /** 条码设备:编号文字 + 状态色 */
    applyBarcodeSpriteAppearance(sprite, device, deviceTypeInfo) {
      const id = +device.index;
      const taskNo = device.taskNo;
      if (taskNo != null && taskNo > 0) {
        sprite.textObj.text = id + '(' + taskNo + ')';
      } else {
        sprite.textObj.text = String(id);
      }
      const statusColor = device.statusColor;
      if (statusColor != null) {
        deviceTypeInfo.statusInfo.updateTextureColor(sprite, statusColor);
      sprite.textObj.text = taskNo != null && taskNo > 0 ? id + '(' + taskNo + ')' : String(id);
      if (device.statusColor != null) {
        deviceTypeInfo.statusInfo.updateTextureColor(sprite, device.statusColor);
      }
    },
    /** 条码锚点对象(两处构造合并为同一形状) */
    createBarcodeAnchorState(
      trackId,
      minBarcode,
      maxBarcode,
      totalSegmentCount,
      barcode,
      point,
      path,
      angle
    ) {
    /** 条码锚点对象(init 与 sync 共用) */
    createBarcodeAnchorState(trackId, minBarcode, maxBarcode, totalSegmentCount, barcode, point, path, angle) {
      return {
        barcode,
        x: point.x,
@@ -1398,33 +1397,26 @@
        totalSegmentCount
      };
    },
    /** 锚点是否需要重置:轨道、条码区间、段数任一变更都要重新建立 */
    isBarcodeAnchorStale(anchor, sprite, minBarcode, maxBarcode, totalSegmentCount) {
      return (
        !anchor ||
        anchor.trackId !== sprite.trackInfo.id ||
        anchor.minBarcode !== minBarcode ||
        anchor.maxBarcode !== maxBarcode ||
        anchor.totalSegmentCount !== totalSegmentCount
      );
    },
    /** 首次有条码数据:按 rgvPos 落到轨道上并建立锚点 */
    initializeBarcodeTrackSprite(
      sprite,
      device,
      pathList,
      allDistance,
      minBarcode,
      maxBarcode,
      totalSegmentCount
    ) {
    initializeBarcodeTrackSprite(sprite, device, pathList, allDistance, minBarcode, maxBarcode, totalSegmentCount) {
      sprite.barcode = device.rgvPos;
      sprite.time = nowMs();
      const passedSegmentCount = this.calcSignedSegmentDelta(
        minBarcode,
        device.rgvPos,
        sprite.trackInfo,
        totalSegmentCount
      );
      const isAnnulus = sprite.trackInfo.type === 'annulus';
      const passedSegmentCount = this.calcSegmentDelta(minBarcode, device.rgvPos, isAnnulus, totalSegmentCount);
      const deltaDistance = (allDistance * passedSegmentCount) / totalSegmentCount;
      const initPath = sprite.path;
      const initMovePoint = G.snapToAnnulusPath(
        sprite.trackInfo,
        sprite.x,
        sprite.y,
        initPath
      );
      let mappingInfo = G.getPositionAfterMove({
      const initMovePoint = G.snapToAnnulusPath(sprite.trackInfo, sprite.x, sprite.y, initPath);
      const mappingInfo = G.getPositionAfterMove({
        point: initMovePoint,
        pathList,
        path: initPath,
@@ -1437,12 +1429,7 @@
      sprite.rotation = G.getRotate(mappingInfo, mappingInfo.path) || sprite.rotation;
      sprite.currentAngle = mappingInfo.angle;
      sprite.mappingInfo = mappingInfo;
      const anchorPoint = G.snapToAnnulusPath(
        sprite.trackInfo,
        mappingInfo.x,
        mappingInfo.y,
        mappingInfo.path
      );
      const anchorPoint = G.snapToAnnulusPath(sprite.trackInfo, mappingInfo.x, mappingInfo.y, mappingInfo.path);
      sprite._barcodeAnchor = this.createBarcodeAnchorState(
        sprite.trackInfo.id,
        minBarcode,
@@ -1454,138 +1441,118 @@
        mappingInfo.angle
      );
    },
    /** 当前帧 dt(秒);ticker 不可用时按约 60fps 估计 */
    getMotionFrameDt() {
      const ticker = this.pixiApp && this.pixiApp.ticker;
      const dtMs = ticker && typeof ticker.deltaMS === 'number' ? ticker.deltaMS : MOTION.DEFAULT_TICKER_DELTA_MS;
      return Math.max(0, dtMs) / 1000;
    },
    /**
     * 推进 sprite._motionSpeed(带加速度上限和"停止圆"缓动),返回本帧最大可走距离。
     * 服务端长时间未推送新条码时,靠近目标点要降速避免越过/抖动。
     */
    updateMotionSpeed(sprite, restDistance, dt) {
      const baseV =
        typeof sprite.maV === 'number' && isFinite(sprite.maV) && sprite.maV > 0
          ? sprite.maV
          : Math.max(sprite.width * MOTION.FALLBACK_BASE_V_FACTOR, MOTION.FALLBACK_BASE_V_MIN);
      const msSinceUpdate = sprite.time ? nowMs() - sprite.time : 0;
      const typicalIntervalMs = (sprite.lastDeltaTime || 1) * 1000;
      const isStale = msSinceUpdate > typicalIntervalMs * MOTION.STALE_INTERVAL_MULT;
      const slowRadius = Math.max(sprite.width * MOTION.SLOW_RADIUS_FACTOR, MOTION.SLOW_RADIUS_MIN);
      const easing = isStale ? Math.min(1.0, Math.pow(restDistance / slowRadius, MOTION.EASE_EXPONENT)) : 1.0;
      const targetSpeed = baseV * easing;
      if (typeof sprite._motionSpeed !== 'number' || !isFinite(sprite._motionSpeed)) {
        sprite._motionSpeed = targetSpeed * MOTION.INITIAL_SPEED_RATIO;
      }
      const maxAccel = Math.max(baseV * MOTION.MAX_ACCEL_FACTOR, MOTION.MAX_ACCEL_MIN);
      const dv = targetSpeed - sprite._motionSpeed;
      const cap = maxAccel * dt;
      sprite._motionSpeed += Math.max(-cap, Math.min(cap, dv));
      return Math.min(restDistance, Math.max(0, sprite._motionSpeed) * dt);
    },
    /**
     * 单线非环形轨道:方向取决于条码进退,需要返回有符号 step。
     * 返回 null 表示这帧应直接停在 mappingInfo(投影退化或直线退化)。
     */
    computeSingleLineSignedStep(sprite, path, stepCap, restDistance) {
      const remainAlong = G.lineSignedRemainAlong(
        path,
        sprite.x,
        sprite.y,
        sprite.mappingInfo.x,
        sprite.mappingInfo.y
      );
      if (remainAlong == null) return stepCap;
      const stepMag = Math.min(stepCap, Math.abs(remainAlong), restDistance);
      if (stepMag >= EPSILON || restDistance <= EPSILON) {
        return Math.sign(remainAlong) * stepMag;
      }
      // 投影 stepMag 退化为 0 但欧氏距离仍存在:用世界向量与直线方向的内积兜底判方向
      const lineStart = { x: path.startX, y: path.startY };
      const lineEnd = { x: path.x, y: path.y };
      if (G.calcDistance(lineStart, lineEnd) <= EPSILON) return null;
      const { x: ux, y: uy } = G.normalizeVector(lineStart, lineEnd);
      const hint = (sprite.mappingInfo.x - sprite.x) * ux + (sprite.mappingInfo.y - sprite.y) * uy;
      if (hint === 0) return null;
      return Math.sign(hint) * Math.min(stepCap, restDistance);
    },
    isSingleLineTrack(sprite, pathList) {
      return pathList.length === 1 && pathList[0].type === 'line' && sprite.trackInfo.type !== 'annulus';
    },
    /** 条码设备每帧插值移动(Pixi ticker 回调) */
    tickBarcodeTrackSpriteMotion(sprite, pathList) {
      if (sprite.isFinish) {
        return;
      }
      if (sprite.isFinish) return;
      const restDistance = G.calcDistance(sprite, sprite.mappingInfo);
      if (restDistance <= EPSILON) {
        this.finishDeviceMotion(sprite);
        return;
      }
      const dtMs =
        this.pixiApp && this.pixiApp.ticker && typeof this.pixiApp.ticker.deltaMS === 'number'
          ? this.pixiApp.ticker.deltaMS
          : 16.667;
      const dt = Math.max(0, dtMs) / 1000;
      if (dt <= 0) {
        return;
      }
      const baseV =
        typeof sprite.maV === 'number' && isFinite(sprite.maV) && sprite.maV > 0
          ? sprite.maV
          : Math.max(sprite.width * 2, 20);
      const msSinceUpdate = sprite.time ? nowMs() - sprite.time : 0;
      const typicalIntervalMs = (sprite.lastDeltaTime || 1) * 1000;
      const isStale = msSinceUpdate > typicalIntervalMs * 2.65;
      const slowRadius = Math.max(sprite.width * 6.5, 72);
      const easing = isStale ? Math.min(1.0, Math.pow(restDistance / slowRadius, 0.45)) : 1.0;
      const targetSpeed = baseV * easing;
      if (typeof sprite._motionSpeed !== 'number' || !isFinite(sprite._motionSpeed)) {
        sprite._motionSpeed = targetSpeed * 0.35;
      }
      const maxAccel = Math.max(baseV * 1.75, 22);
      const dv = targetSpeed - sprite._motionSpeed;
      const cap = maxAccel * dt;
      sprite._motionSpeed += Math.max(-cap, Math.min(cap, dv));
      const dt = this.getMotionFrameDt();
      if (dt <= 0) return;
      const stepCap = this.updateMotionSpeed(sprite, restDistance, dt);
      const path = sprite.path;
      const stepCap = Math.min(restDistance, Math.max(0, sprite._motionSpeed) * dt);
      const singleLineTrack =
        pathList.length === 1 && pathList[0].type === 'line' && sprite.trackInfo.type !== 'annulus';
      let smoothDistance;
      if (singleLineTrack) {
        const remainAlong = G.lineSignedRemainAlong(
          path,
          sprite.x,
          sprite.y,
          sprite.mappingInfo.x,
          sprite.mappingInfo.y
        );
        if (remainAlong != null) {
          let stepMag = Math.min(stepCap, Math.abs(remainAlong), restDistance);
          if (stepMag < EPSILON && restDistance > EPSILON) {
            const lineStart = { x: path.startX, y: path.startY };
            const lineEnd = { x: path.x, y: path.y };
            const sl = G.calcDistance(lineStart, lineEnd);
            if (sl > EPSILON) {
              const { x: ux, y: uy } = G.normalizeVector(lineStart, lineEnd);
              const wdx = sprite.mappingInfo.x - sprite.x;
              const wdy = sprite.mappingInfo.y - sprite.y;
              const hint = wdx * ux + wdy * uy;
              stepMag = Math.min(stepCap, restDistance);
              if (hint === 0) {
                this.finishDeviceMotion(sprite);
                return;
              }
              smoothDistance = Math.sign(hint) * stepMag;
            } else {
              this.finishDeviceMotion(sprite);
              return;
            }
          } else {
            smoothDistance = Math.sign(remainAlong) * stepMag;
          }
        } else {
          smoothDistance = stepCap;
      let signedStep;
      if (this.isSingleLineTrack(sprite, pathList)) {
        signedStep = this.computeSingleLineSignedStep(sprite, path, stepCap, restDistance);
        if (signedStep == null) {
          this.finishDeviceMotion(sprite);
          return;
        }
      } else {
        smoothDistance = stepCap;
        signedStep = stepCap;
      }
      const movePointBarcode = G.snapToAnnulusPath(
        sprite.trackInfo,
        sprite.x,
        sprite.y,
        path
      );
      const angle = Math.atan2(sprite.y - path.y, sprite.x - path.x);
      const p = G.getPositionAfterMove({
        point: movePointBarcode,
      const movePoint = G.snapToAnnulusPath(sprite.trackInfo, sprite.x, sprite.y, path);
      const radialAngle = Math.atan2(sprite.y - path.y, sprite.x - path.x);
      const next = G.getPositionAfterMove({
        point: movePoint,
        pathList,
        path,
        deltaDistance: smoothDistance,
        angle
        deltaDistance: signedStep,
        angle: radialAngle
      });
      sprite.path = p.path;
      sprite.x = p.x;
      sprite.y = p.y;
      sprite.rotation = G.getRotate(p, p.path) || sprite.rotation;
      const restDistanceAfter = G.calcDistance({ x: sprite.x, y: sprite.y }, sprite.mappingInfo);
      if (restDistanceAfter <= EPSILON || Math.abs(smoothDistance) >= restDistance - EPSILON) {
      sprite.path = next.path;
      sprite.x = next.x;
      sprite.y = next.y;
      sprite.rotation = G.getRotate(next, next.path) || sprite.rotation;
      const restAfter = G.calcDistance({ x: sprite.x, y: sprite.y }, sprite.mappingInfo);
      if (restAfter <= EPSILON || Math.abs(signedStep) >= restDistance - EPSILON) {
        this.finishDeviceMotion(sprite);
      }
    },
    /**
     * 根据服务端条码刷新 mappingInfo、速度与 ticker。
     * rgvPosMax 通常是闭区间 [min, max],且 max 对应 100% 与 min 同一点(走一圈回到原点)。
     * 因此按 “段数 = max - min” 换算距离。
     * 根据服务端条码刷新 mappingInfo、速度估计与 ticker。
     * rgvPosMax 通常是闭区间 [min, max],max 与 min 在轨道上同点(走一圈回到原点),
     * 故按 段数 = max - min 换算距离。
     */
    syncBarcodeTrackSprite(
      sprite,
      device,
      oldSprite,
      pathList,
      allDistance,
      minBarcode,
      maxBarcode,
      totalSegmentCount
    ) {
    syncBarcodeTrackSprite(sprite, device, prevBarcode, prevTime, pathList, allDistance, minBarcode, maxBarcode, totalSegmentCount) {
      let anchor = sprite._barcodeAnchor;
      const needResetAnchor =
        !anchor ||
        anchor.trackId !== sprite.trackInfo.id ||
        anchor.minBarcode !== minBarcode ||
        anchor.maxBarcode !== maxBarcode ||
        anchor.totalSegmentCount !== totalSegmentCount;
      if (needResetAnchor) {
      if (this.isBarcodeAnchorStale(anchor, sprite, minBarcode, maxBarcode, totalSegmentCount)) {
        const anchorPath = sprite.path;
        const anchorPoint = G.snapToAnnulusPath(
          sprite.trackInfo,
          sprite.x,
          sprite.y,
          anchorPath
        );
        const anchorBarcode = oldSprite.barcode != null ? oldSprite.barcode : device.rgvPos;
        const anchorPoint = G.snapToAnnulusPath(sprite.trackInfo, sprite.x, sprite.y, anchorPath);
        const anchorBarcode = prevBarcode != null ? prevBarcode : device.rgvPos;
        anchor = this.createBarcodeAnchorState(
          sprite.trackInfo.id,
          minBarcode,
@@ -1598,120 +1565,101 @@
        );
        sprite._barcodeAnchor = anchor;
      }
      const passedSegmentCount = this.calcSignedSegmentDelta(
        anchor.barcode,
        device.rgvPos,
        sprite.trackInfo,
        totalSegmentCount
      );
      const isAnnulus = sprite.trackInfo.type === 'annulus';
      const passedSegmentCount = this.calcSegmentDelta(anchor.barcode, device.rgvPos, isAnnulus, totalSegmentCount);
      const deltaDistance = (allDistance * passedSegmentCount) / totalSegmentCount;
      const path = anchor.path || sprite.path;
      const barcodeMovePoint = { x: anchor.x, y: anchor.y };
      let finalPosition = G.getPositionAfterMove({
        point: barcodeMovePoint,
      const finalPosition = G.getPositionAfterMove({
        point: { x: anchor.x, y: anchor.y },
        pathList,
        path,
        deltaDistance,
        angle: anchor.angle
      });
      let curveDistance = G.calcDistance(sprite, finalPosition);
      if (!isFinite(curveDistance)) {
        curveDistance = deltaDistance;
      }
      if (!isFinite(curveDistance)) curveDistance = deltaDistance;
      sprite.mappingInfo = finalPosition;
      const now = nowMs();
      const deltaTime = (now - oldSprite.time) / 1000 || 0;
      const deltaTime = (now - prevTime) / 1000 || 0;
      sprite.time = now;
      sprite.barcode = device.rgvPos;
      if (deltaTime > 0 && deltaTime <= 10 && curveDistance > 0) {
      if (deltaTime > 0 && deltaTime <= MOTION.VELOCITY_SAMPLE_MAX_DT && curveDistance > 0) {
        sprite.lastDeltaTime = deltaTime;
        const v = curveDistance / deltaTime;
        const alpha = 0.2;
        sprite.maV =
          typeof sprite.maV === 'number' && isFinite(sprite.maV)
            ? sprite.maV * (1 - alpha) + v * alpha
            : v;
        const a = MOTION.VELOCITY_EWMA_ALPHA;
        sprite.maV = typeof sprite.maV === 'number' && isFinite(sprite.maV) ? sprite.maV * (1 - a) + v * a : v;
      }
      if (curveDistance < EPSILON) {
        this.finishDeviceMotion(sprite);
        return;
      }
      sprite.isFinish = false;
      if (sprite.ticker) {
        return;
      }
      if (sprite.ticker) return;
      sprite.ticker = () => this.tickBarcodeTrackSpriteMotion(sprite, pathList);
      this.pixiApp.ticker.add(sprite.ticker);
    },
    // 适配不同接口返回的数据结构
    /** 环穿条码接口的设备条目 -> 内部统一字段 */
    adaptAnnulusBarcodeDevice(device, deviceTypeInfo) {
      const index = +device.index;
      const sprite = deviceTypeInfo.pixiMap.get(index);
      if (!sprite) return null;
      const trackInfoParse = G.safeParseJson(sprite.trackInfo.value);
      // 后端返回形如 "#27AE60" 的颜色字符串
      const hex = device.statusColor ? device.statusColor.split('#')[1] : null;
      return {
        index,
        sprite,
        statusColor: hex != null ? parseInt(hex, 16) : null,
        minBarcode: trackInfoParse.barCodeStart,
        maxBarcode: trackInfoParse.barCodeEnd,
        rgvPos: device.rgvPos,
        taskNo: device.taskNo
      };
    },
    /** 原版条码接口(按 idName + statusInfo + laserValue)-> 内部统一字段 */
    adaptOriginalBarcodeDevice(device, deviceTypeInfo) {
      const index = +device[deviceTypeInfo.idName];
      const sprite = deviceTypeInfo.pixiMap.get(index);
      if (!sprite) return null;
      const trackInfoParse = G.safeParseJson(sprite.trackInfo.value);
      const statusInfo = deviceTypeInfo.statusInfo;
      return {
        index,
        sprite,
        statusColor: statusInfo.getStatus(device[statusInfo.name]),
        minBarcode: trackInfoParse.barCodeStart,
        maxBarcode: trackInfoParse.barCodeEnd,
        rgvPos: (device.laserValue * FAKE_MAX_LAYER) / FAKE_MAX_CRN_LAYER,
        taskNo: device.taskNo
      };
    },
    deviceAdapter(type, res, trackType) {
      const devices = this.parseBarcodeDevicesResponse(res);
      const devices = Array.isArray(res) ? res : res && res.code === 200 ? res.data : null;
      if (!devices) return [];
      const deviceTypeInfo = this.DEVICE_MAP[type];
      // 环穿接口
      if (trackType === 'annulus') {
        return devices.map((device) => {
          const index = +device.index;
          const sprite = deviceTypeInfo.pixiMap.get(index);
          if (!sprite) {
            return {};
          }
          const trackInfoParse = G.safeParseJson(sprite.trackInfo.value);
          const statusColorStr = device.statusColor.split('#')[1];
          const statusColor = parseInt(statusColorStr, 16);
          return {
            index,
            statusColor,
            sprite,
            minBarcode: trackInfoParse.barCodeStart,
            maxBarcode: trackInfoParse.barCodeEnd,
            rgvPos: device.rgvPos,
            taskNo: device.taskNo
          };
        });
      const adapt =
        trackType === 'annulus'
          ? (d) => this.adaptAnnulusBarcodeDevice(d, deviceTypeInfo)
          : (d) => this.adaptOriginalBarcodeDevice(d, deviceTypeInfo);
      const out = [];
      for (const device of devices) {
        const item = adapt(device);
        if (item) out.push(item);
      }
      // 原版接口
      return devices.map((device) => {
        const index = +device[deviceTypeInfo.idName];
        const sprite = deviceTypeInfo.pixiMap.get(index);
        if (!sprite) {
          return {};
        }
        const trackInfoParse = G.safeParseJson(sprite.trackInfo.value);
        const minBarcode = trackInfoParse.barCodeStart;
        const maxBarcode = trackInfoParse.barCodeEnd;
        const statusInfo = deviceTypeInfo.statusInfo;
        const statusColor = statusInfo.getStatus(device[statusInfo.name]);
        return {
          index,
          statusColor,
          sprite,
          minBarcode,
          maxBarcode,
          rgvPos: device.laserValue * FAKE_MAX_LAYER / FAKE_MAX_CRN_LAYER,
          taskNo: device.taskNo
        };
      });
      return out;
    },
    setDeviceInfoByBarcode(type, res, trackType = 'annulus') {
      const devices = this.deviceAdapter(type, res, trackType);
      const deviceTypeInfo = this.DEVICE_MAP[type];
      for (let i = 0; i < devices.length; i++) {
        const device = devices[i] || {};
      for (const device of devices) {
        const { sprite, minBarcode, maxBarcode } = device;
        if (!sprite) {
          continue;
        }
        this.applyBarcodeSpriteAppearance(sprite, device, deviceTypeInfo);
        const pathList = sprite.trackInfo.pathList;
        const allDistance = G.getAllDistance(pathList);
        const totalSegmentCount = Math.max(1, maxBarcode - minBarcode);
        const oldSprite = {
          ...sprite,
          x: sprite.x,
          y: sprite.y,
          mappingInfo: { ...sprite.mappingInfo }
        };
        if (!sprite.barcode && sprite.barcode !== 0) {
        if (sprite.barcode == null) {
          this.initializeBarcodeTrackSprite(
            sprite,
            device,
@@ -1723,10 +1671,14 @@
          );
          continue;
        }
        // sync 需要"刷新前"的条码与时间戳来计算段差/瞬时速度,单独取两个标量比 spread 整个 sprite 便宜得多
        const prevBarcode = sprite.barcode;
        const prevTime = sprite.time;
        this.syncBarcodeTrackSprite(
          sprite,
          device,
          oldSprite,
          prevBarcode,
          prevTime,
          pathList,
          allDistance,
          minBarcode,