jinglun-cloud
2026-04-24 6ecd168c3bb818397ca97650da9bfeb1b85bfd6e
添加环穿
1个文件已添加
6个文件已修改
828 ■■■■■ 已修改文件
src/main/java/com/zy/asrs/domain/BasMapEditorElement.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/BasMapEditorServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/components/MapCanvas.js 503 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/components/MapCanvasBak.js 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/basMap/mapTrackGeometry.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/basMap/地图编辑器使用说明.md 221 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/watch/console.html 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/domain/BasMapEditorElement.java
@@ -1,10 +1,14 @@
package com.zy.asrs.domain;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BasMapEditorElement implements Serializable {
    private static final long serialVersionUID = 1L;
@@ -22,4 +26,13 @@
    private Double height;
    private String value;
        /** çŽ¯ç©¿ç­‰è½¨é“çš„å‡ ä½•ï¼šä¸Žå‰ç«¯ editor ä¸€è‡´ */
    private String shape;
    private List<Map<String, Object>> pathList;
    private Map<String, Object> turningPoint;
    private Double annulusBandInset;
}
src/main/java/com/zy/asrs/service/impl/BasMapEditorServiceImpl.java
@@ -47,7 +47,7 @@
    private static final String FREE_EDITOR_MODE = "free-v1";
    private static final Set<String> RUNTIME_TYPES = new HashSet<>(Arrays.asList(
            "shelf", "crn", "dualCrn", "devp", "rgv"
            "shelf", "crn", "dualCrn", "devp", "rgv", "annulus"
    ));
    private static final int DEFAULT_ROW_HEIGHT = 200;
    private static final int DEFAULT_COL_WIDTH = 1000;
@@ -289,6 +289,26 @@
        element.setWidth(width);
        element.setHeight(height);
        element.setValue(stringifyValue(source == null ? null : source.getValue()));
        // æ·»åŠ çŽ¯ç©¿è½¨é“ç­‰ä¿¡æ¯
        if (source != null && !Cools.isEmpty(source.getShape())) {
            element.setShape(String.valueOf(source.getShape()).trim());
        }
        if (source != null && source.getPathList() != null && !source.getPathList().isEmpty()) {
            List<Map<String, Object>> copy = new ArrayList<>();
            for (Map<String, Object> seg : source.getPathList()) {
                if (seg == null || seg.isEmpty()) {
                    continue;
                }
                copy.add(new LinkedHashMap<>(seg));
            }
            element.setPathList(copy);
        }
        if (source != null && source.getTurningPoint() != null && !source.getTurningPoint().isEmpty()) {
            element.setTurningPoint(new LinkedHashMap<>(source.getTurningPoint()));
        }
        if (source != null && source.getAnnulusBandInset() != null) {
            element.setAnnulusBandInset(source.getAnnulusBandInset());
        }
        if ("devp".equals(element.getType())) {
            validateDevpValue(element.getValue(), index);
        }
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 = [];
src/main/webapp/components/MapCanvasBak.js
@@ -256,3 +256,41 @@
//   crnId: 15,
//   pathId: 'el_1775197504724'
// };
// 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;
// },
src/main/webapp/static/js/basMap/mapTrackGeometry.js
@@ -1086,12 +1086,14 @@
  }
  function getRotate(point, path) {
    var vector = normalizeVector(point, path);
    if (path.type === 'arc') {
      var angleToCenter = Math.atan2(vector.y, vector.x);
      return angleToCenter + (Math.PI / 2) * path.crossProduct;
    if (path.type === 'line') {
      var tdx = path.x - path.startX;
      var tdy = path.y - path.startY;
      return Math.atan2(tdy, tdx);
    }
    return Math.atan2(vector.y, vector.x);
    var vector = normalizeVector(point, path);
    var angleToCenter = Math.atan2(vector.y, vector.x);
    return angleToCenter + (Math.PI / 2) * path.crossProduct;
  }
  /**
src/main/webapp/static/js/basMap/µØÍ¼±à¼­Æ÷ʹÓÃ˵Ã÷.md
New file
@@ -0,0 +1,221 @@
# è‡ªç”±ç”»å¸ƒåœ°å›¾ç¼–辑器使用说明
## æ¦‚è¿°
本地图编辑器是一个基于 PixiJS çš„可视化编辑工具,用于绘制和编辑自动化立体仓库(AS/RS)的楼层地图。编辑器支持多种设备类型的绘制、编辑、复制、删除等操作,并支持多楼层管理。
## æ ¸å¿ƒåŠŸèƒ½
### 1. æ¥¼å±‚管理
- **楼层选择**:通过顶部下拉菜单切换不同楼层
- **新建楼层**:点击「新建自由画布」按钮创建新楼层
- **重新读取**:重新加载当前楼层的地图数据
- **保存**:支持单独保存当前楼层或保存所有楼层
### 2. è§†å›¾æ“ä½œ
- **缩放**:通过鼠标滚轮缩放视图
- **平移**:按住空格键拖拽画布,或使用「平移」工具
- **适配全图**:点击「适配全图」按钮自动缩放以显示所有元素
- **回到画布**:点击「回到画布」按钮重置视图到默认位置
## å·¥å…·ä½¿ç”¨
### äº¤äº’工具
| å·¥å…· | è¯´æ˜Ž | ä½¿ç”¨æ–¹æ³• |
|------|------|----------|
| **选择/移动** | é€‰æ‹©å’Œç§»åŠ¨å…ƒç´  | ç‚¹å‡»å…ƒç´ é€‰æ‹©ï¼Œæ‹–拽移动,空白处拖动画布 |
| **框选** | é€‰æ‹©å¤šä¸ªå…ƒç´  | åœ¨ç”»å¸ƒä¸Šæ‹–拽框选区域 |
| **阵列** | æ‰¹é‡ç”Ÿæˆè´§æž¶æˆ–维修站台 | å…ˆé€‰ä¸­ä¸€ä¸ªè´§æž¶/维修站台,再拖拽绘制阵列区域 |
| **平移** | ä¸“门用于拖动画布 | æ‹–拽画布平移视图 |
### ç»˜åˆ¶å…ƒç´ 
| å…ƒç´ ç±»åž‹ | è¯´æ˜Ž |
|----------|------|
| **货架** | æ™®é€šå­˜å‚¨è´§æž¶ |
| **维修站台** | è®¾å¤‡ç»´ä¿®ç«™ç‚¹ |
| **输送线** | è¾“送机站点,支持多方向配置 |
| **堆垛机** | å•工位堆垛机轨道 |
| **双工位堆垛机** | åŒå·¥ä½å †åž›æœºè½¨é“ |
| **RGV** | RGV(穿梭车)轨道 |
| **环穿** | çŽ¯å½¢ç©¿æ¢­è½¦è½¨é“ï¼Œæ”¯æŒå¤šç§å½¢çŠ¶ï¼ˆçŸ©å½¢ã€L型等) |
## å…ƒç´ ç¼–辑
### é€‰æ‹©å…ƒç´ 
- **单选**:点击元素进行选择
- **多选**:使用「框选」工具框选多个元素,或按住 Shift ç‚¹å‡»æ·»åŠ /移除选中
- **取消选择**:点击画布空白处
### åŸºæœ¬æ“ä½œ
| æ“ä½œ | å¿«æ·é”® | è¯´æ˜Ž |
|------|--------|------|
| **删除** | Delete | åˆ é™¤é€‰ä¸­çš„元素 |
| **复制** | Ctrl+C | å¤åˆ¶é€‰ä¸­çš„元素到剪贴板 |
| **粘贴** | Ctrl+V | ç²˜è´´å‰ªè´´æ¿ä¸­çš„元素 |
| **复制偏移** | - | å¤åˆ¶å¹¶åç§»é€‰ä¸­çš„元素 |
| **聚焦选中** | - | è‡ªåŠ¨ç¼©æ”¾ä»¥æ˜¾ç¤ºé€‰ä¸­çš„å…ƒç´  |
| **撤销** | Ctrl+Z | æ’¤é”€ä¸Šä¸€æ­¥æ“ä½œ |
| **重做** | Ctrl+Shift+Z æˆ– Ctrl+Y | é‡åšå·²æ’¤é”€çš„æ“ä½œ |
### å‡ ä½•属性编辑
在右侧属性面板中,当选中单个元素时,可以编辑:
- **类型**(只读)
- **ID**(只读)
- **X坐标**
- **Y坐标**
- **宽度**
- **高度**
点击「应用几何」按钮使更改生效。
### è¾“送线(devp)配置
选中输送线元素后,在属性面板中可以配置:
#### åŸºæœ¬ä¿¡æ¯
- **站号**:输送线站点编号
- **PLC编号**:关联的PLC设备编号
- **方向**:选择输送线的运行方向(可多选)
#### ç«™ç‚¹ç±»åž‹
- **条码站**:配置条码扫描功能
- **入站点**:配置入库功能,需关联条码站
- **出站点**:配置出库功能
- **堵塞重分配**:启用堵塞时的重新分配功能
- **出库排序**:启用出库排序功能
- **顶升移栽**:配置顶升移栽功能
#### æ¡ç ç›¸å…³ï¼ˆæ¡ç ç«™å¿…填)
- **条码索引**
- **退回站站号**
- **退回站PLC编号**
#### å…¥ç«™ç‚¹å¿…å¡«
- **条码站站号**
- **条码站PLC编号**
### è½¨é“设备配置(crn/dualCrn/rgv/annulus)
选中轨道元素后,可以配置:
- **轨道ID**
- **条码起始值**
- **条码结束值**
- **设备列表**:
  - **设备编号**
  - **位置(进度条)**:在轨道上的位置(0-100%)
  - **设备长(沿轨道)**
  - **设备宽(垂直轨道)**
点击「添加设备」按钮添加新设备,点击「应用设备参数」使更改生效。
### JSON编辑
对于非输送线元素,可以直接在属性面板中编辑原始JSON数据,点击「应用值」按钮使更改生效。
### è´§æž¶è‡ªåЍ填充
选中多个货架后,可以在属性面板中配置:
- **起始值**:货架编号起始值(如 12-1)
- **排方向**:排编号递增/递减方向
- **列方向**:列编号递增/递减方向
## çŽ¯ç©¿è½¨é“ç‰¹æ®ŠåŠŸèƒ½
环穿轨道支持多种形状:
- **矩形**(rect):标准矩形环
- **L型**(L1/L2/L3/L4):四个方向的L形环
环穿轨道的绘制会自动生成平滑路径,设备会沿路径摆放。
## ç”»å¸ƒè®¾ç½®
在右侧属性面板中,可以设置:
- **画布宽度**
- **画布高度**
点击「应用画布尺寸」按钮使更改生效。
## å¯¼å…¥å¯¼å‡º
- **导入地图**:导入已有的地图数据
- **导出地图**:导出当前地图数据
- **导入Excel**:(当前注释中,可能未完全实现)
## çŠ¶æ€æ˜¾ç¤º
顶部工具栏显示:
- å½“前楼层
- ç¼©æ”¾æ¯”例
- å½“前工具模式
- é€‰ä¸­å…ƒç´ æ•°é‡
- å…ƒç´ æ€»æ•°
- ç”»å¸ƒå°ºå¯¸
- æ¸²æŸ“倍率
- FPS(帧率)
左侧工具面板底部显示:
- å½“前楼层
- æŒ‡é’ˆçŠ¶æ€
- é˜µåˆ—预览数量
- æœªä¿å­˜çŠ¶æ€
## æ“ä½œæµç¨‹ç¤ºä¾‹
### ç»˜åˆ¶è´§æž¶é˜µåˆ—
1. é€‰æ‹©ã€Œè´§æž¶ã€å·¥å…·
2. åœ¨ç”»å¸ƒä¸Šæ‹–拽绘制一个货架
3. é€‰ä¸­æ–‡æ¡£å·¥å…·ï¼ˆç‚¹å‡»ç”»å¸ƒç©ºç™½å¤„)
4. åˆ‡æ¢åˆ°ã€Œé˜µåˆ—」工具
5. ç‚¹å‡»åˆšç»˜åˆ¶çš„货架选中它
6. æ‹–拽绘制阵列区域
7. ç¡®è®¤é˜µåˆ—预览数量符合预期
8. é‡Šæ”¾é¼ æ ‡å®Œæˆé˜µåˆ—创建
### é…ç½®è¾“送线
1. é€‰æ‹©ã€Œè¾“送线」工具
2. åœ¨ç”»å¸ƒä¸Šæ‹–拽绘制
3. é€‰æ‹©ã€Œé€‰æ‹©/移动」工具
4. ç‚¹å‡»åˆšç»˜åˆ¶çš„输送线
5. åœ¨å³ä¾§å±žæ€§é¢æ¿ä¸­é…ç½®å„项参数
6. ç‚¹å‡»ã€Œåº”用输送线配置」按钮
### é…ç½®è½¨é“设备
1. é€‰æ‹©ã€Œå †åž›æœºã€/「RGV」/「环穿」工具
2. åœ¨ç”»å¸ƒä¸Šæ‹–拽绘制
3. é€‰æ‹©ã€Œé€‰æ‹©/移动」工具
4. ç‚¹å‡»åˆšç»˜åˆ¶çš„轨道
5. åœ¨å³ä¾§å±žæ€§é¢æ¿ä¸­é…ç½®è½¨é“ID和条码范围
6. ç‚¹å‡»ã€Œæ·»åŠ è®¾å¤‡ã€æ·»åŠ è®¾å¤‡
7. é…ç½®è®¾å¤‡ç¼–号、位置和尺寸
8. ç‚¹å‡»ã€Œåº”用设备参数」按钮
## æ³¨æ„äº‹é¡¹
1. **保存操作**:修改后会标记为「未保存」,请及时保存
2. **撤销/重做**:最多保存60步操作记录
3. **元素重叠**:绘制时注意元素不要重叠
4. **坐标系统**:画布左上角为原点(0,0),X轴向右,Y轴向下
5. **快捷键冲突**:确保在画布区域内使用快捷键
## æŠ€æœ¯è¯´æ˜Ž
- **渲染引擎**:PixiJS
- **前端框架**:Vue 2
- **地图数据格式**:JSON
- **支持的浏览器**:现代浏览器(Chrome、Firefox、Edge等)
---
如有问题,请参考代码注释或联系开发团队。
src/main/webapp/views/watch/console.html
@@ -659,9 +659,10 @@
                            } else if (result.url == "/console/latest/data/station") {
                                 const res = JSON.parse(result.data);
                                 this.stationStateList = res && res.code === 200 ? (res.data || []) : [];
                            } else if (result.url == "/rgv/table/rgv/state") {
                                 const res = JSON.parse(result.data);
                                 this.rgvStateList = res && res.code === 200 ? (res.data || []) : [];
                                                    // åŽŸç‰ˆä»£ç 
                                                    // else if (result.url == "/rgv/table/rgv/state") {
                            //      const res = JSON.parse(result.data);
                            //      this.rgvStateList = res && res.code === 200 ? (res.data || []) : [];
                            } else if (result.url == "/basMap/lev/" + this.currentLev + "/auth") {
                                // åœ°å›¾æ•°æ®
                                 let res = JSON.parse(result.data);
@@ -750,7 +751,7 @@
                        this.activateCard = type;
                        this.refreshWorkbench(type);
                    },
                    refreshWorkbench(type) {
                    async refreshWorkbench(type) {
                        if (!type) { return; }
                        if (type === 'crn') {
                            this.sendWs(JSON.stringify({ url: "/crn/table/crn/state", data: {} }));
@@ -759,7 +760,15 @@
                        } else if (type === 'devp') {
                            this.sendWs(JSON.stringify({ url: "/console/latest/data/station", data: {} }));
                        } else if (type === 'rgv') {
                            this.sendWs(JSON.stringify({ url: "/rgv/table/rgv/state", data: {} }));
                            // this.sendWs(JSON.stringify({ url: "/rgv/table/rgv/state", data: {} }));
                            const res = await fetch('http://127.0.0.1:9091/rs-car/rgv/task/rgv/circular/shuttle/mast/position/data', {
                                method: 'POST',
                            });
                            if (!res.ok) {
                                return
                            }
                            const json = await res.json();
                            this.rgvStateList = json && json.code === 200 ? (json.data || []) : [];
                        }
                    },
                    updateMapViewportPadding() {