jinglun-cloud
2026-04-24 6ecd168c3bb818397ca97650da9bfeb1b85bfd6e
src/main/webapp/components/MapCanvas.js
@@ -1,4 +1,4 @@
const EPSILON = 1; // 容差,用于处理浮点数精度问题
const EPSILON = 1e-9; // 容差,用于处理浮点数精度问题
const nowMs = () =>
  typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
@@ -8,6 +8,9 @@
  throw new Error('mapTrackGeometry.js must be loaded before MapCanvas.js');
}
// 仅本地测试用
const FAKE_MAX_LAYER = 1730000;
const FAKE_MAX_CRN_LAYER = 4800
/**
 * 通用轮询器类
 * 封装带请求取消、平滑延迟计算、错误退避的轮询逻辑
@@ -193,9 +196,6 @@
      shelfChunkSize: 2048,
      shelfCullPadding: 160,
      shelfCullRaf: null,
      crnList: [],
      dualCrnList: [],
      rgvList: [],
      locListMap: new Map(),
      locListLoaded: false,
      locListLoading: false,
@@ -215,8 +215,8 @@
      tracksGraphics: null,
      shelvesContainer: null,
      graphicsCrn: null,
      graphicsCrnTrack: null,
      graphicsRgvTrack: null,
      // graphicsCrnTrack: null,
      // graphicsRgvTrack: null,
      graphicsRgv: null,
      shelfTooltip: {
        visible: false,
@@ -253,12 +253,54 @@
        'machine-pakin': 0x30bffc,
        'machine-pakout': 0x97b400,
        'site-run-block': 0xe69138,
        'site-error': 0xDB2828
        'site-error': 0xdb2828
      },
      fakeOperationVisible: false
    }
    };
  },
    mounted() {
  mounted() {
    this.DEVICE_MAP = {
      crn: {
        createTexture: this.createCrnTexture,
        graphics: this.graphicsCrn,
        emitName: 'crn-click',
        pixiMap: this.pixiCrnMap,
        type: 'crn',
        idName: 'crnId',
        statusInfo: {
          name: 'crnStatus',
          getStatus: this.getCrnStatusColor,
          updateTextureColor: this.updateCrnTextureColor
        }
      },
      dualcrn: {
        createTexture: this.createCrnTexture,
        graphics: this.graphicsCrn,
        emitName: 'dual-crn-click',
        pixiMap: this.pixiDualCrnMap,
        type: 'dualCrn',
        idName: 'crnId',
        statusInfo: {
          name: 'crnStatus',
          getStatus: this.getCrnStatusColor,
          updateTextureColor: this.updateCrnTextureColor
        }
      },
      rgv: {
        createTexture: this.createRgvTexture,
        graphics: this.graphicsRgv,
        emitName: 'rgv-click',
        pixiMap: this.pixiRgvMap,
        type: 'rgv',
        idName: 'rgvNo',
        statusInfo: {
          name: 'rgvStatus',
          getStatus: this.getRgvStatusColor,
          updateTextureColor: this.updateRgvTextureColor
        }
      }
    };
    this.DEVICE_MAP.dualCrn = this.DEVICE_MAP.dualcrn;
    this.currentLev = this.lev || 1;
    this.createMap();
    this.startContainerResizeObserve();
@@ -278,7 +320,9 @@
      this.getRgvInfo();
    }, 1000);
    this.startAnnulusDevicePoll();
    setTimeout(()=>{
      this.startAnnulusDevicePoll();
    },1000)
    // todo:测试代码
    setTimeout(() => {
@@ -389,11 +433,11 @@
     * 切换楼层或重载地图前的共用清场(可选:对设备精灵 kill GSAP)。
     * @param {{ killDeviceGsap?: boolean, skipClearLoopHighlight?: boolean }} options
     */
    prepareMapSceneReload(options) {
      const opts = options || {};
      if (!opts.skipClearLoopHighlight) {
        this.clearLoopStationHighlight();
      }
    clearMap() {
      // const opts = options || {};
      // if (!opts.skipClearLoopHighlight) {
      //   this.clearLoopStationHighlight();
      // }
      this.hideShelfTooltip();
      this.hoveredShelfCell = null;
      this.mapRowOffsets = [];
@@ -404,8 +448,8 @@
        clearTimeout(this.adjustLabelTimer);
        this.adjustLabelTimer = null;
      }
      if (opts.killDeviceGsap && window.gsap) {
        [this.pixiStaMap, this.pixiCrnMap, this.pixiDualCrnMap, this.pixiRgvMap].forEach((m) => {
      if (window.gsap) {
        [this.pixiStaMap].forEach((m) => {
          m &&
            m.forEach((s) => {
              try {
@@ -415,6 +459,21 @@
        });
      }
      this.objectsContainer.removeChildren();
      this.clearShelfChunks();
      this.pixiStaMap = new Map();
      this.pixiStageList = [];
    },
    clearMap2() {
      if (window.gsap) {
        [this.pixiCrnMap, this.pixiDualCrnMap, this.pixiRgvMap].forEach((m) => {
          m &&
            m.forEach((s) => {
              try {
                window.gsap.killTweensOf(s);
              } catch (e) {}
            });
        });
      }
      this.objectsContainer2.removeChildren();
      if (this.tracksContainer) {
        this.tracksContainer.removeChildren();
@@ -422,15 +481,9 @@
      if (this.tracksGraphics) {
        this.tracksGraphics.clear();
      }
      this.clearShelfChunks();
      this.crnList = [];
      this.dualCrnList = [];
      this.rgvList = [];
      this.pixiCrnMap.clear();
      this.pixiDualCrnMap.clear();
      this.pixiRgvMap.clear();
      this.pixiStaMap = new Map();
      this.pixiStageList = [];
    },
    cycleCapacityPanelStyle() {
      const hud = this.hudPadding || {};
@@ -582,8 +635,8 @@
      this.pixiApp.view.style.display = 'block';
      this.resizeToContainer();
      window.addEventListener('resize', this.scheduleResizeToContainer);
      this.graphicsCrnTrack = this.createTrackTexture(25, 25, 10);
      this.graphicsRgvTrack = this.createTrackTexture(25, 25, 10);
      // this.graphicsCrnTrack = this.createTrackTexture(25, 25, 10);
      // this.graphicsRgvTrack = this.createTrackTexture(25, 25, 10);
      this.objectsContainer = new PIXI.Container();
      this.objectsContainer2 = new PIXI.Container();
      this.tracksContainer = new PIXI.ParticleContainer(10000, {
@@ -834,15 +887,16 @@
          data: {}
        })
      );
      this.setMap2();
    },
    changeFloor(lev) {
      this.currentLev = lev;
      this.clearLoopStationHighlight();
      this.isSwitchingFloor = true;
      this.prepareMapSceneReload({ killDeviceGsap: false, skipClearLoopHighlight: true });
      this.getMap();
    },
    setMap(res) {
      this.clearMap();
      this.createMapData(JSON.parse(res.data));
    },
    // 轨道在Map2中
@@ -853,6 +907,7 @@
        method: 'get',
        success: function (res) {
          if (res && res.code === 200 && res.data && res.data.elements) {
            this.clearMap2();
            this.createMap2Data(
              res.data.elements.filter((item) =>
                ['crn', 'dualCrn', 'rgv', 'annulus'].includes(item.type)
@@ -863,10 +918,7 @@
      });
    },
    createMapData(map) {
      this.prepareMapSceneReload({ killDeviceGsap: true });
      this.pixiStageList = [map.length];
      this.setMap2();
      const bayHeightList = this.initHeight(map);
      const bayWidthList = this.initWidth(map);
@@ -995,17 +1047,12 @@
          if (val.type == undefined || val.type === 'none') {
            continue;
          }
          // if (this.isTrackType(val)) {
          //   this.collectTrackItem(val);
          //   continue;
          // }
          if (val.type === 'shelf') {
            continue;
          }
          if (['crn', 'dualCrn', 'rgv', 'annulus'].includes(val.type)) {
            continue;
          }
          // 收集crnList等
          let sprite = this.getSprite(val, (e) => {});
          if (sprite == null) {
            continue;
@@ -1050,10 +1097,12 @@
        // 每个设备独立创建纹理,不要缓存复用,否则所有设备尺寸会相同
        const graphics = deviceTypeInfo.createTexture(along, across);
        let sprite = new PIXI.Sprite(graphics);
        // anchor 居中后子坐标原点在纹理中心,编号须用 (0,0),见 positionSpriteLabelToTextureCenter
        sprite.anchor.set(0.5);
        const deviceNo = device.deviceNo || device[deviceTypeInfo.idName];
        const style = new PIXI.TextStyle({
          fontFamily: 'Arial',
          fontSize: 10,
          fontSize: 12,
          fill: '#000000',
          stroke: '#ffffff',
          strokeThickness: 1,
@@ -1064,8 +1113,8 @@
        const text = new PIXI.Text(txt, style);
        text.anchor.set(0.5);
        sprite.addChild(text);
        text.position.set(sprite.width / 2, sprite.height / 2);
        sprite.textObj = text;
        this.positionSpriteLabelToTextureCenter(sprite);
        // 这里item已经是中心点了,直接用就行
        device.width = sprite.width;
@@ -1092,7 +1141,6 @@
        sprite.currentAngle = mappingInfo.angle;
        // sprite.vector = mappingInfo.vector
        // sprite.time = Date.now()
        sprite.anchor.set(0.5);
        sprite.rotation = G.getRotate(mappingInfo, mappingInfo.path) || sprite.rotation;
        sprite.x = mappingInfo.x;
        sprite.y = mappingInfo.y;
@@ -1198,7 +1246,7 @@
          return;
        }
        if (workNo != null && workNo > 0) {
          sta.textObj.text = id + '(' + workNo + ')';
          sta.textObj.text = String(id) + '\n(' + workNo + ')';
        } else {
          sta.textObj.text = String(id);
        }
@@ -1276,30 +1324,9 @@
      }
      return (value * 100).toFixed(1) + '%';
    },
    /******************setDeviceInfo用的函数:******************/
    //设备编号:画布坐标系固定字号,随地图缩放与设备图标同比例变化(避免缩小视图时相对设备变大)
    applyEditorLikeTrackDeviceTextStyle(textObj) {
      if (!textObj || !textObj.style) {
        return;
      }
      textObj.style.fontSize = 10;
      textObj.style.strokeThickness = 1;
    },
    parseHexColor(color) {
      if (typeof color !== 'string' || color.charAt(0) !== '#') {
        return null;
      }
      let hex = color.slice(1);
      if (hex.length === 3) {
        hex = hex.replace(/(.)/g, '$1$1');
      }
      if (!/^[0-9a-fA-F]{6}$/.test(hex)) {
        return null;
      }
      return parseInt(hex, 16);
    },
    /******************setDeviceInfo使用的函数:******************/
    getAnnulusAwarePoint(trackInfo, x, y, path) {
      return trackInfo.type === 'annulus' && G.snapToAnnulusOuterPath
      return trackInfo.type === 'annulus'
        ? G.snapToAnnulusOuterPath(x, y, path)
        : { x, y };
    },
@@ -1341,7 +1368,7 @@
      const vx = path0.x - x0;
      const vy = path0.y - y0;
      const segLen = Math.sqrt(vx * vx + vy * vy);
      if (!(segLen > 1e-6)) {
      if (!(segLen > EPSILON)) {
        return null;
      }
      const clampT = (px, py) => {
@@ -1385,8 +1412,7 @@
      } else {
        sprite.textObj.text = String(id);
      }
      this.applyEditorLikeTrackDeviceTextStyle(sprite.textObj);
      const statusColor = this.parseHexColor(device.statusColor);
      const statusColor = device.statusColor;
      if (statusColor != null) {
        deviceTypeInfo.statusInfo.updateTextureColor(sprite, statusColor);
      }
@@ -1460,7 +1486,7 @@
        mappingInfo.path
      );
      sprite._barcodeAnchor = this.createBarcodeAnchorState(
        sprite.id,
        sprite.trackInfo.id,
        minBarcode,
        maxBarcode,
        totalSegmentCount,
@@ -1520,13 +1546,13 @@
        );
        if (remainAlong != null) {
          let stepMag = Math.min(stepCap, Math.abs(remainAlong), restDistance);
          if (stepMag < 1e-6 && restDistance > EPSILON) {
          if (stepMag < EPSILON && restDistance > EPSILON) {
            const x0 = path.startX;
            const y0 = path.startY;
            const vx = path.x - x0;
            const vy = path.y - y0;
            const sl = Math.sqrt(vx * vx + vy * vy);
            if (sl > 1e-6) {
            if (sl > EPSILON) {
              const ux = vx / sl;
              const uy = vy / sl;
              const wdx = sprite.mappingInfo.x - sprite.x;
@@ -1593,7 +1619,7 @@
      let anchor = sprite._barcodeAnchor;
      const needResetAnchor =
        !anchor ||
        anchor.trackId !== sprite.id ||
        anchor.trackId !== sprite.trackInfo.id ||
        anchor.minBarcode !== minBarcode ||
        anchor.maxBarcode !== maxBarcode ||
        anchor.totalSegmentCount !== totalSegmentCount;
@@ -1607,7 +1633,7 @@
        );
        const anchorBarcode = oldSprite.barcode != null ? oldSprite.barcode : device.rgvPos;
        anchor = this.createBarcodeAnchorState(
          sprite.id,
          sprite.trackInfo.id,
          minBarcode,
          maxBarcode,
          totalSegmentCount,
@@ -1635,7 +1661,7 @@
        deltaDistance,
        anchor.angle
      );
      let curveDistance = G.calcDistance(oldSprite.mappingInfo || oldSprite, finalPosition);
      let curveDistance = G.calcDistance(sprite, finalPosition);
      if (!isFinite(curveDistance)) {
        curveDistance = deltaDistance;
      }
@@ -1653,6 +1679,9 @@
            ? sprite.maV * (1 - alpha) + v * alpha
            : v;
      }
      if(device.index === 18) {
        console.log("device",device,curveDistance,oldSprite.barcode, device.rgvPos,sprite.id)
      }
      if (curveDistance < EPSILON) {
        this.finishDeviceMotion(sprite);
        return;
@@ -1668,6 +1697,7 @@
    deviceAdapter(type, res, trackType) {
      const devices = this.parseBarcodeDevicesResponse(res);
      const deviceTypeInfo = this.DEVICE_MAP[type];
      // 环穿接口
      if (trackType === 'annulus') {
        return devices.map((device) => {
          const index = +device.index;
@@ -1676,9 +1706,11 @@
            return {};
          }
          const trackInfoParse = G.safeParseJson(sprite.trackInfo.value);
          const statusColorStr = device.statusColor.split('#')[1];
          const statusColor = parseInt(statusColorStr, 16);
          return {
            index,
            statusColor: device.statusColor,
            statusColor,
            sprite,
            minBarcode: trackInfoParse.barCodeStart,
            maxBarcode: trackInfoParse.barCodeEnd,
@@ -1687,22 +1719,25 @@
          };
        });
      }
      // 原版接口
      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 statusName = device[statusInfo.statusName];
        const statusColor = statusInfo[statusInfo.getStatus](statusName);
        const statusColor = statusInfo.getStatus(device[statusInfo.name]);
        return {
          index,
          statusColor,
          sprite,
          minBarcode,
          maxBarcode,
          rgvPos: device.rgvPos,
          rgvPos: device.laserValue * FAKE_MAX_LAYER / FAKE_MAX_CRN_LAYER,
          taskNo: device.taskNo
        };
      });
@@ -1751,7 +1786,7 @@
      }
      this.scheduleAdjustLabels();
    },
    /******************setDeviceInfo用的函数结束******************/
    /******************setDeviceInfo相关函数结束******************/
    webSocketOnOpen(e) {
      if (this.wsReconnectTimer) {
        clearTimeout(this.wsReconnectTimer);
@@ -1770,13 +1805,14 @@
        result.url === '/console/latest/data/station' ||
        result.url === '/console/latest/data/site'
      ) {
        this.setSiteInfo(JSON.parse(result.data));
        // todo
        // this.setSiteInfo(JSON.parse(result.data));
      } else if (result.url === '/console/latest/data/crn') {
        // this.setDeviceInfo('crn', JSON.parse(result.data));
        this.setDeviceInfoByBarcode('crn', JSON.parse(result.data), 'crn');
      } else if (result.url === '/console/latest/data/dualcrn') {
        // this.setDeviceInfo('dualcrn', JSON.parse(result.data));
        this.setDeviceInfoByBarcode('dualCrn', JSON.parse(result.data), 'dualCrn');
      } else if (result.url === '/console/latest/data/rgv') {
        // this.setDeviceInfo('rgv', JSON.parse(result.data));
        // this.setDeviceInfoByBarcode('rgv', JSON.parse(result.data), 'rgv');
      } else if (result.url === '/console/latest/data/station/cycle/capacity') {
        this.setCycleCapacityInfo(JSON.parse(result.data));
      } else if (typeof result.url === 'string' && result.url.indexOf('/basMap/lev/') === 0) {
@@ -1834,16 +1870,6 @@
      }
      return new PIXI.Sprite(texture);
    },
    createTrackSprite(width, height, mask) {
      const trackMask = mask != null ? mask : 10;
      let idx = width + '-' + height + '-' + trackMask;
      let texture = this.pixiTrackMap.get(idx);
      if (texture == undefined) {
        texture = this.createTrackTexture(width, height, trackMask);
        this.pixiTrackMap.set(idx, texture);
      }
      return new PIXI.Sprite(texture);
    },
    getContainer(type, width, height) {
      let graphics = new PIXI.Graphics();
      let drawBorder = true;
@@ -1861,75 +1887,6 @@
      }
      graphics.endFill();
      return graphics;
    },
    createTrackTexture(width, height, mask) {
      const TRACK_N = 1;
      const TRACK_E = 2;
      const TRACK_S = 4;
      const TRACK_W = 8;
      const trackMask = mask != null ? mask : TRACK_E | TRACK_W;
      const g = new PIXI.Graphics();
      const size = Math.max(1, Math.min(width, height));
      const rail = Math.max(2, Math.round(size * 0.12));
      const gap = Math.max(4, Math.round(size * 0.38));
      const midX = Math.round(width / 2);
      const midY = Math.round(height / 2);
      const y1 = midY - Math.round(gap / 2);
      const y2 = midY + Math.round(gap / 2);
      const x1 = midX - Math.round(gap / 2);
      const x2 = midX + Math.round(gap / 2);
      const hasN = (trackMask & TRACK_N) !== 0;
      const hasE = (trackMask & TRACK_E) !== 0;
      const hasS = (trackMask & TRACK_S) !== 0;
      const hasW = (trackMask & TRACK_W) !== 0;
      const hStart = hasW ? 0 : midX;
      const hEnd = hasE ? width : midX;
      const vStart = hasN ? 0 : midY;
      const vEnd = hasS ? height : midY;
      const railColor = 0x555555;
      const drawLine = (x1p, y1p, x2p, y2p, w, color) => {
        g.lineStyle(w, color, 1);
        g.moveTo(x1p, y1p);
        g.lineTo(x2p, y2p);
      };
      const hasH = hasW || hasE;
      const hasV = hasN || hasS;
      const isCorner = hasH && hasV && !(hasW && hasE) && !(hasN && hasS);
      if (hasH && !isCorner) {
        const w = Math.max(1, hEnd - hStart);
        g.beginFill(railColor);
        g.drawRect(hStart, midY - Math.round(rail / 2), w, rail);
        g.endFill();
      }
      if (hasV && !isCorner) {
        const h = Math.max(1, vEnd - vStart);
        g.beginFill(railColor);
        g.drawRect(midX - Math.round(rail / 2), vStart, rail, h);
        g.endFill();
      }
      if (isCorner) {
        const cw = hasE;
        const ch = hasS;
        const cx = cw ? width - 1 : 0;
        const cy = ch ? height - 1 : 0;
        const angStart = cw && ch ? Math.PI : cw ? Math.PI / 2 : ch ? -Math.PI / 2 : 0;
        const angEnd = cw && ch ? Math.PI * 1.5 : cw ? Math.PI : ch ? 0 : Math.PI / 2;
        const rX = Math.abs(cx - midX);
        const rY = Math.abs(cy - midY);
        const rMid = Math.min(rX, rY);
        g.lineStyle(rail, railColor, 1);
        g.arc(cx, cy, rMid, angStart, angEnd);
        g.lineStyle(0, 0, 0);
      }
      // no sleepers; keep a single continuous line
      const rt = PIXI.RenderTexture.create({ width: width, height: height });
      this.pixiApp.renderer.render(g, rt);
      return rt;
    },
    createCrnTexture(width, height) {
      const g = new PIXI.Graphics();
@@ -1992,9 +1949,9 @@
      const fill = this.getContrastColor(color);
      textObj.style.fill = fill;
      textObj.style.stroke = fill === '#000000' ? '#ffffff' : '#000000';
      this.applyEditorLikeTrackDeviceTextStyle(textObj);
      textObj.style.strokeThickness = 1;
      if (repositionText) {
        textObj.position.set(sprite.width / 2, sprite.height / 2);
        this.positionSpriteLabelToTextureCenter(sprite);
      }
    },
    updateRgvTextureColor(sprite, color) {
@@ -2030,7 +1987,9 @@
      return colorMap['site-unauto'] != null ? colorMap['site-unauto'] : 0xb8b8b8;
    },
    resolveStationStatus(item) {
      if (item && item.error > 0) { return 'site-error'; }
      if (item && item.error > 0) {
        return 'site-error';
      }
      const status = item && (item.siteStatus != null ? item.siteStatus : item.stationStatus);
      const taskNo = this.parseStationTaskNo(
        item && (item.workNo != null ? item.workNo : item.taskNo)
@@ -2174,7 +2133,8 @@
          fontSize: 10,
          fill: '#000000',
          stroke: '#ffffff',
          strokeThickness: 1
          strokeThickness: 1,
          align: 'center'
        });
        const text = new PIXI.Text(String(siteId), style);
        text.anchor.set(0.5);
@@ -2200,39 +2160,11 @@
            this.$emit('station-click', id);
          }
        });
      } else if (item.type == 'crn') {
        sprite = this.createTrackSprite(item.width, item.height, item.trackMask);
        sprite._kind = 'crn-track';
        this.crnList.push(item);
      } else if (item.type == 'dualCrn') {
        sprite = this.createTrackSprite(item.width, item.height, item.trackMask);
        sprite._kind = 'crn-track';
        this.dualCrnList.push(item);
      } else if (item.type == 'rgv') {
        sprite = this.createTrackSprite(item.width, item.height, item.trackMask);
        sprite._kind = 'rgv-track';
        this.rgvList.push(item);
      } else {
        return null;
      }
      sprite.position.set(item.posX, item.posY);
      return sprite;
    },
    collectTrackItem(item) {
      const value = item.value;
      if (item.type === 'crn') {
        if (this.getDeviceNo(value) > 0) {
          this.crnList.push(item);
        }
      } else if (item.type === 'dualCrn') {
        if (this.getDeviceNo(value) > 0) {
          this.dualCrnList.push(item);
        }
      } else if (item.type === 'rgv') {
        if (this.getDeviceNo(value) > 0) {
          this.rgvList.push(item);
        }
      }
    },
    isTrackType(cell) {
      return (
@@ -2286,43 +2218,7 @@
      }
      return null;
    },
    // getTrackMask(map, rowIndex, colIndex) {
    //   const TRACK_N = 1;
    //   const TRACK_E = 2;
    //   const TRACK_S = 4;
    //   const TRACK_W = 8;
    //   const baseRow = map[rowIndex];
    //   if (!baseRow) {
    //     return 0;
    //   }
    //   const base = baseRow[colIndex];
    //   if (!this.isTrackType(base)) {
    //     return 0;
    //   }
    //   const rowSpan = base.rowSpan || 1;
    //   const colSpan = base.colSpan || 1;
    //   let mask = 0;
    //   const n = this.resolveMergedCell(map, rowIndex - 1, colIndex);
    //   const s = this.resolveMergedCell(map, rowIndex + rowSpan, colIndex);
    //   const w = this.resolveMergedCell(map, rowIndex, colIndex - 1);
    //   const e = this.resolveMergedCell(map, rowIndex, colIndex + colSpan);
    //   if (n && n !== base && this.isTrackType(n)) {
    //     mask |= TRACK_N;
    //   }
    //   if (e && e !== base && this.isTrackType(e)) {
    //     mask |= TRACK_E;
    //   }
    //   if (s && s !== base && this.isTrackType(s)) {
    //     mask |= TRACK_S;
    //   }
    //   if (w && w !== base && this.isTrackType(w)) {
    //     mask |= TRACK_W;
    //   }
    //   if (mask === 0) {
    //     mask = TRACK_E | TRACK_W;
    //   }
    //   return mask;
    // },
    drawTracks(map) {
      if (!this.tracksGraphics) {
        return;
@@ -3161,7 +3057,11 @@
      }
    },
    getShelfArrangeInfo(item) {
      return item.value;
      if (!item.value) {
        return '';
      }
      const list = item.value.split('-');
      return `${list[1]}-${list[0]}`;
      // const parts = [];
      // const matchKey = this.getShelfMatchKey(item);
      // if (matchKey != null) {
@@ -3229,51 +3129,42 @@
        zIndex: 10
      };
    },
    /** Pixi v5:Sprite.anchor 为 (0.5,0.5) 时局部原点在纹理中心,文字用 (0,0);站点等默认 anchor (0,0) 仍用宽高一半。 */
    positionSpriteLabelToTextureCenter(sprite) {
      const textObj = sprite && sprite.textObj;
      if (!textObj) {
        return;
      }
      const ax = sprite.anchor != null ? sprite.anchor.x : 0;
      const ay = sprite.anchor != null ? sprite.anchor.y : 0;
      const originAtTextureCenter =
        Math.abs(ax - 0.5) < 0.001 && Math.abs(ay - 0.5) < 0.001;
      if (originAtTextureCenter) {
        textObj.position.set(0, 0);
      } else {
        textObj.position.set(sprite.width / 2, sprite.height / 2);
      }
    },
    adjustLabelScale() {
      const s = this.pixiApp && this.pixiApp.stage ? Math.abs(this.pixiApp.stage.scale.x || 1) : 1;
      const minPx = 14;
      const viewport = this.getViewportSize();
      const vw = viewport.width;
      const vh = viewport.height;
      const margin = 50;
      const mirrorSign = this.mapMirrorX ? -1 : 1;
      const inverseRotation = -(((this.mapRotation % 360) * Math.PI) / 180);
      const inverseRotation = -((this.mapRotation % 360) * Math.PI) / 180;
      const tmpPoint = new PIXI.Point();
      this.pixiStaMap &&
        this.pixiStaMap.forEach((sprite) => {
          const textObj = sprite && sprite.textObj;
          if (!textObj) {
            return;
          }
          const base = textObj.style && textObj.style.fontSize ? textObj.style.fontSize : 10;
          let scale = minPx / (base * s);
          if (!isFinite(scale)) {
            scale = 1;
          }
          scale = Math.max(0.8, Math.min(scale, 3));
          textObj.scale.set(mirrorSign, 1);
          textObj.rotation = inverseRotation;
          textObj.position.set(sprite.width / 2, sprite.height / 2);
          sprite.getGlobalPosition(tmpPoint);
          const on =
            tmpPoint.x >= -margin &&
            tmpPoint.y >= -margin &&
            tmpPoint.x <= vw + margin &&
            tmpPoint.y <= vh + margin;
          textObj.visible = s >= 0.25 && on;
        });
      [this.pixiCrnMap, this.pixiDualCrnMap, this.pixiRgvMap].forEach((deviceMap) => {
        deviceMap &&
          deviceMap.forEach((sprite) => {
      // 标签随地图缩放(与设备/站点几何一致);仅校正水平镜像与地图旋转,不再按 stage 缩放做反向补偿。
      const apply = (map) => {
        map &&
          map.forEach((sprite) => {
            const textObj = sprite && sprite.textObj;
            if (!textObj) {
              return;
            }
            this.applyEditorLikeTrackDeviceTextStyle(textObj);
            textObj.anchor.set(0.5);
            textObj.rotation = 0;
            textObj.scale.set(mirrorSign, 1);
            textObj.position.set(sprite.width / 2, sprite.height / 2);
            textObj.rotation = inverseRotation;
            this.positionSpriteLabelToTextureCenter(sprite);
            sprite.getGlobalPosition(tmpPoint);
            const on =
              tmpPoint.x >= -margin &&
@@ -3282,7 +3173,11 @@
              tmpPoint.y <= vh + margin;
            textObj.visible = s >= 0.25 && on;
          });
      });
      };
      apply(this.pixiStaMap);
      apply(this.pixiCrnMap);
      apply(this.pixiDualCrnMap);
      apply(this.pixiRgvMap);
    },
    rotateMap() {
      this.mapRotation = (this.mapRotation + 90) % 360;
@@ -3324,7 +3219,11 @@
      window.open(url, '_blank');
    },
    loadFakeProcessStatus() {
      if (typeof window === 'undefined' || typeof $ === 'undefined' || typeof baseUrl === 'undefined') {
      if (
        typeof window === 'undefined' ||
        typeof $ === 'undefined' ||
        typeof baseUrl === 'undefined'
      ) {
        this.fakeOperationVisible = false;
        return;
      }
@@ -3341,8 +3240,11 @@
      });
    },
    openFakeOperationConfigPage() {
      if (typeof window === 'undefined' || !this.fakeOperationVisible) { return; }
      const url = (typeof baseUrl !== 'undefined' ? baseUrl : '') + '/views/watch/fakeOperationConfig.html';
      if (typeof window === 'undefined' || !this.fakeOperationVisible) {
        return;
      }
      const url =
        (typeof baseUrl !== 'undefined' ? baseUrl : '') + '/views/watch/fakeOperationConfig.html';
      const layerInstance = (window.top && window.top.layer) || window.layer;
      if (layerInstance && typeof layerInstance.open === 'function') {
        layerInstance.open({
@@ -3386,7 +3288,7 @@
        'machine-pakin': 0x30bffc,
        'machine-pakout': 0x97b400,
        'site-run-block': 0xe69138,
        'site-error': 0xDB2828
        'site-error': 0xdb2828
      };
    },
    parseColorConfigValue(value, fallback) {
@@ -3787,7 +3689,7 @@
      return { x, y, angle, path: minPath };
    },
    distanceBasedEasingSigmoid(remaining, threshold = 1, steepness = 10, maxSpeedChange = 0.3) {
      // 此乃缓动函数,使用Sigmoid函数作为核心:f(x) = 1 / (1 + e^(-k*(x - threshold)))
      // 缓动函数,使用Sigmoid函数作为核心:f(x) = 1 / (1 + e^(-k*(x - threshold)))
      // remaining: 输入值
      // threshold: 函数中心点,输出为1
      // steepness: 曲线陡峭度,控制过渡区的宽度
@@ -3834,6 +3736,24 @@
      this.setDeviceInfoByBarcode('rgv', json);
    },
    //todo: 测试代码
    fakeSetSiteInfo(staSiteList) {
      const staItem = {
        autoing:true,
        barcode:"",
        emptyMk:false,
        enableIn:false,
        error:0,
        fullPlt:false,
        inBarcodeError:false,
        inEnable:true,ioMode:2,loading:false,outEnable:true,palletHeight:0,runBlock:false,stationId:1102,stationStatus:"site-auto",taskNo:0}
      const list = staSiteList.map(item => {
        return {
          ...staItem,
          ...item
        }
      })
      this.setSiteInfo(list);
    },
    async fakeMove(newList = [0, 10, 20, 30, 40, 50], newRaw = {}) {
      const finalList = newList;
      const rawByBarCode = {
@@ -3841,7 +3761,7 @@
        modeColor: '#4169E1',
        statusColor: '#27AE60',
        rgvPos: 0,
        rgvPosMax: ['el_1775520471475', 0, 100]
        rgvPosMax: ['el_1775520471475', 0, FAKE_MAX_LAYER]
      };
      const createTimeout = () => {
        const p1 = new Promise((res, rej) => {
@@ -3854,7 +3774,7 @@
      for await (const p of finalList) {
        await createTimeout();
        this.setDeviceInfoByBarcode(newRaw.type || 'rgv', [
          { ...rawByBarCode, ...newRaw, rgvPos: p * 17370 }
          { ...rawByBarCode, ...newRaw, rgvPos: (p * FAKE_MAX_LAYER) / 100 }
        ]);
      }
    },
@@ -3881,7 +3801,7 @@
        fill: '#ffffff',
        align: 'center'
      });
      const label = new PIXI.Text('模拟运动', textStyle);
      const label = new PIXI.Text('模拟运动(仅测试用)', textStyle);
      label.anchor.set(0.5);
      label.position.set(bw / 2, bh / 2);
      const btn = new PIXI.Container();
@@ -3917,19 +3837,46 @@
      }, 1000);
      // 设备18
      for (let i = 0; i < 7; i++) {
        list2.push(i * 7);
      for (let i = 0; i <= 8; i++) {
        list2.push(i * 2);
      }
      list2.push(47.8);
      const p2 = this.fakeMove(list2, { index: 18 });
      p2.then(async () => {
      const firstPhasePos =  17
      list2.push(firstPhasePos);
      Promise.resolve().then( async ()=>{
        await this.fakeMove(list2, { index: 18 });
        await sleep(1000);
        await this.fakeMove([47.8], { index: 18, statusColor: '#a5d6f7' });
        await this.fakeSetSiteInfo([{stationId:1211,stationStatus:"site-auto-run-id"}]);
        await sleep(1000);
        await this.fakeMove([52.8, 57.8, 62.8, 63.5], { index: 18, statusColor: '#a5d6f7' });
        await this.fakeSetSiteInfo([{stationId:1209,stationStatus:"site-auto-run-id"},{stationId:1211,stationStatus:"site-auto"}]);
        await sleep(1000);
        await this.fakeMove([63.5], { index: 18, statusColor: '#245a9a' });
      });
        await this.fakeSetSiteInfo([{stationId:1208,stationStatus:"site-auto-run-id"},{stationId:1209,stationStatus:"site-auto"}]);
        await sleep(1000);
        await this.fakeSetSiteInfo([{stationId:1208,stationStatus:"site-auto"}]);
        await this.fakeMove([firstPhasePos], { index: 18 , statusColor: '#a5d6f7' });
        const list2_2 = []
        for (let i = 1; i <= 8; i++) {
          list2_2.push(firstPhasePos + i * 5.5);
        }
        list2_2.push(62.7)
        await this.fakeMove(list2_2, { index: 18 , statusColor: '#a5d6f7' });
        await sleep(1000);
        await this.fakeMove([list2_2.at(-1)], { index: 18 , statusColor: '#245a9a' });
        await this.fakeSetSiteInfo([{stationId:1107,stationStatus:"site-auto-run-id"}]);
        await sleep(1000);
        await this.fakeSetSiteInfo([{stationId:1107,stationStatus:"site-auto"}]);
        await sleep(1000);
        const list_crn = []
        for (let i = 0; i <= 7; i++) {
          list_crn.push(i * 10);
        }
        await this.fakeMove(list_crn, { index: 15, statusColor: '#a5d6f7', type: 'crn' });
        await this.fakeMove([list_crn.at(-1)], { index: 15, statusColor: '#245a9a', type: 'crn' });
      })
        // await this.fakeMove([47.8], { index: 18, statusColor: '#a5d6f7' });
        // await sleep(1000);
        // await this.fakeMove([52.8, 57.8, 62.8, 63.5], { index: 18, statusColor: '#a5d6f7' });
        // await sleep(1000);
        // await this.fakeMove([63.5], { index: 18, statusColor: '#245a9a' });
      // 设备16
      let list3 = [];