lsh
2026-04-21 7443e8040d9a7669a8117c8a6937dbd4bd792709
src/main/webapp/static/js/basMap/editor.js
@@ -1,4059 +1,4778 @@
(function () {
    var FREE_EDITOR_MODE = 'free-v1';
    var MAP_TRANSFER_FORMAT = 'bas-map-editor-transfer-v2';
    var HISTORY_LIMIT = 60;
    var DEFAULT_CANVAS_WIDTH = 6400;
    var DEFAULT_CANVAS_HEIGHT = 3600;
    var MIN_ELEMENT_SIZE = 24;
    var HANDLE_SCREEN_SIZE = 10;
    var DRAG_START_THRESHOLD = 5;
    var EDGE_SNAP_SCREEN_TOLERANCE = 8;
    var COORD_EPSILON = 0.01;
    var DEFERRED_STATIC_REBUILD_DELAY = 120;
    var PAN_LABEL_REFRESH_DELAY = 160;
    var ZOOM_REFRESH_DELAY = 220;
    var POINTER_STATUS_UPDATE_INTERVAL = 48;
    var SPATIAL_BUCKET_SIZE = 240;
    var STATIC_VIEW_PADDING = 120;
    var MIN_LABEL_SCALE = 0.17;
    var ABS_MIN_LABEL_SCREEN_WIDTH = 26;
    var ABS_MIN_LABEL_SCREEN_HEIGHT = 14;
    var STATIC_SPRITE_SCALE_THRESHOLD = 0.85;
    var STATIC_SIMPLIFY_SCALE_THRESHOLD = 0.22;
    var DENSE_SIMPLIFY_SCALE_THRESHOLD = 0.8;
    var DENSE_SIMPLIFY_ELEMENT_THRESHOLD = 1200;
    var DENSE_LABEL_HIDE_SCALE_THRESHOLD = 1.05;
    var DENSE_LABEL_HIDE_ELEMENT_THRESHOLD = 1200;
    var STATIC_SPRITE_POOL_SLACK = 96;
    var MIN_LABEL_COUNT = 180;
    var MAX_LABEL_COUNT = 360;
    var SHOW_CANVAS_ELEMENT_LABELS = true;
    var DRAW_TYPES = ['shelf', 'devp', 'crn', 'dualCrn', 'rgv'];
    var ARRAY_TEMPLATE_TYPES = ['shelf', 'crn', 'dualCrn', 'rgv'];
    var DEVICE_CONFIG_TYPES = ['crn', 'dualCrn', 'rgv'];
    var DEVP_DIRECTION_OPTIONS = [
        { key: 'top', label: '上', arrow: '↑' },
        { key: 'right', label: '右', arrow: '→' },
        { key: 'bottom', label: '下', arrow: '↓' },
        { key: 'left', label: '左', arrow: '←' }
    ];
    var idSeed = Date.now();
  var FREE_EDITOR_MODE = 'free-v1';
  var MAP_TRANSFER_FORMAT = 'bas-map-editor-transfer-v2';
  var HISTORY_LIMIT = 60;
  var DEFAULT_CANVAS_WIDTH = 6400;
  var DEFAULT_CANVAS_HEIGHT = 3600;
  var MIN_ELEMENT_SIZE = 24;
  var HANDLE_SCREEN_SIZE = 10;
  var DRAG_START_THRESHOLD = 5;
  var EDGE_SNAP_SCREEN_TOLERANCE = 8;
  var COORD_EPSILON = 0.01;
  var DEFERRED_STATIC_REBUILD_DELAY = 120;
  var PAN_LABEL_REFRESH_DELAY = 160;
  var ZOOM_REFRESH_DELAY = 220;
  var POINTER_STATUS_UPDATE_INTERVAL = 48;
  var SPATIAL_BUCKET_SIZE = 240;
  var STATIC_VIEW_PADDING = 120;
  var MIN_LABEL_SCALE = 0.17;
  var ABS_MIN_LABEL_SCREEN_WIDTH = 26;
  var ABS_MIN_LABEL_SCREEN_HEIGHT = 14;
  var STATIC_SPRITE_SCALE_THRESHOLD = 0.85;
  var STATIC_SIMPLIFY_SCALE_THRESHOLD = 0.22;
  var DENSE_SIMPLIFY_SCALE_THRESHOLD = 0.8;
  var DENSE_SIMPLIFY_ELEMENT_THRESHOLD = 1200;
  var DENSE_LABEL_HIDE_SCALE_THRESHOLD = 1.05;
  var DENSE_LABEL_HIDE_ELEMENT_THRESHOLD = 1200;
  var STATIC_SPRITE_POOL_SLACK = 96;
  var MIN_LABEL_COUNT = 180;
  var MAX_LABEL_COUNT = 360;
  var DRAW_TYPES = ['shelf', 'repairHub', 'devp', 'crn', 'dualCrn', 'rgv', 'annulus'];
  var ARRAY_TEMPLATE_TYPES = ['shelf', 'repairHub'];
  var DEVICE_CONFIG_TYPES = ['crn', 'dualCrn', 'rgv', 'annulus'];
  var DEVP_DIRECTION_OPTIONS = [
    { key: 'top', label: '上', arrow: '↑' },
    { key: 'right', label: '右', arrow: '→' },
    { key: 'bottom', label: '下', arrow: '↓' },
    { key: 'left', label: '左', arrow: '←' }
  ];
  var idSeed = Date.now();
    var TYPE_META = {
        shelf: { label: '货架', shortLabel: 'SHELF', fill: 0x7d96bf, border: 0x4f6486 },
        devp: { label: '输送线', shortLabel: 'DEVP', fill: 0xf0b06f, border: 0xa45f21 },
        crn: { label: '堆垛机轨道', shortLabel: 'CRN', fill: 0x68bfd0, border: 0x1d6e81 },
        dualCrn: { label: '双工位轨道', shortLabel: 'DCRN', fill: 0x54c1a4, border: 0x0f7b62 },
        rgv: { label: 'RGV轨道', shortLabel: 'RGV', fill: 0xc691e9, border: 0x744b98 }
  var G = window.BasMapTrackGeometry;
  if (!G) {
    throw new Error('mapTrackGeometry.js must be loaded before editor.js');
  }
  function nextId() {
    idSeed += 1;
    return 'el_' + idSeed;
  }
  function deepClone(obj) {
    return JSON.parse(JSON.stringify(obj == null ? null : obj));
  }
  function padNumber(value) {
    return value < 10 ? '0' + value : String(value);
  }
  function authHeaders() {
    return {
      token: localStorage.getItem('token')
    };
  }
  function getQueryParam(name) {
    var search = window.location.search || '';
    if (!search) {
      return '';
    }
    var target = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    var match = search.match(new RegExp('(?:[?&])' + target + '=([^&]*)'));
    return match ? decodeURIComponent(match[1]) : '';
  }
  function toNumber(value, defaultValue) {
    if (value === null || value === undefined || value === '') {
      return defaultValue;
    }
    var parsed = Number(value);
    return isFinite(parsed) ? parsed : defaultValue;
  }
  function toInt(value, defaultValue) {
    return Math.round(toNumber(value, defaultValue));
  }
  function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
  }
  function roundCoord(value) {
    return Math.round(value * 1000) / 1000;
  }
  function normalizeValue(value) {
    if (value === null || value === undefined) {
      return '';
    }
    return typeof value === 'string' ? value : JSON.stringify(value);
  }
  function parseShelfLocationValue(value) {
    var text = normalizeValue(value).trim();
    var matched = text.match(/^(-?\d+)\s*-\s*(-?\d+)$/);
    if (!matched) {
      return null;
    }
    return {
      row: toInt(matched[1], 0),
      col: toInt(matched[2], 0)
    };
  }
  function formatShelfLocationValue(row, col) {
    return String(toInt(row, 0)) + '-' + String(toInt(col, 0));
  }
  function isShelfLikeNodeType(type) {
    return type === 'shelf' || type === 'repairHub';
  }
  function safeParseJson(text) {
    if (!text || typeof text !== 'string') {
      return null;
    }
    try {
      return JSON.parse(text);
    } catch (e) {
      return null;
    }
  }
  function boolFlag(value) {
    return value === true || value === 1 || value === '1';
  }
  function normalizeDirectionList(direction) {
    var list = Array.isArray(direction) ? direction : String(direction || '').split(/[,\s|/]+/);
    var result = [];
    var seen = {};
    for (var i = 0; i < list.length; i++) {
      var item = String(list[i] || '')
        .trim()
        .toLowerCase();
      if (!item || seen[item]) {
        continue;
      }
      seen[item] = true;
      result.push(item);
    }
    return result;
  }
  function directionTokenToArrow(token) {
    if (token === 'top' || token === 'up' || token === 'north' || token === 'n') {
      return '↑';
    }
    if (token === 'right' || token === 'east' || token === 'e') {
      return '→';
    }
    if (token === 'bottom' || token === 'down' || token === 'south' || token === 's') {
      return '↓';
    }
    if (token === 'left' || token === 'west' || token === 'w') {
      return '←';
    }
    return '';
  }
  function formatDirectionArrows(direction) {
    var list = normalizeDirectionList(direction);
    var arrows = [];
    for (var i = 0; i < list.length; i++) {
      var arrow = directionTokenToArrow(list[i]);
      if (arrow) {
        arrows.push(arrow);
      }
    }
    return arrows.join('');
  }
  function isDeviceConfigType(type) {
    return DEVICE_CONFIG_TYPES.indexOf(type) >= 0;
  }
  function pickDeviceValueKey(type, json) {
    if (type === 'crn' || type === 'dualCrn') {
      return 'crnNo';
    }
    if (type === 'rgv') {
      return 'rgvNo';
    }
    return 'deviceNo';
  }
  function isInputLike(target) {
    if (!target || !target.tagName) {
      return false;
    }
    var tag = String(target.tagName || '').toLowerCase();
    return tag === 'input' || tag === 'textarea' || tag === 'select' || !!target.isContentEditable;
  }
  function rectsOverlap(a, b) {
    return (
      a.x < b.x + b.width - COORD_EPSILON &&
      a.x + a.width > b.x + COORD_EPSILON &&
      a.y < b.y + b.height - COORD_EPSILON &&
      a.y + a.height > b.y + COORD_EPSILON
    );
  }
  function rectIntersects(a, b) {
    return (
      a.x <= b.x + b.width && a.x + a.width >= b.x && a.y <= b.y + b.height && a.y + a.height >= b.y
    );
  }
  function isRectWithinCanvas(rect, canvasWidth, canvasHeight) {
    return (
      rect.x >= -COORD_EPSILON &&
      rect.y >= -COORD_EPSILON &&
      rect.x + rect.width <= canvasWidth + COORD_EPSILON &&
      rect.y + rect.height <= canvasHeight + COORD_EPSILON
    );
  }
  function findDocOverlapId(doc) {
    if (!doc || !doc.elements || !doc.elements.length) {
      return '';
    }
    var buckets = {};
    var elements = doc.elements;
    for (var i = 0; i < elements.length; i++) {
      var element = elements[i];
      var minX = Math.floor(element.x / SPATIAL_BUCKET_SIZE);
      var maxX = Math.floor((element.x + element.width) / SPATIAL_BUCKET_SIZE);
      var minY = Math.floor(element.y / SPATIAL_BUCKET_SIZE);
      var maxY = Math.floor((element.y + element.height) / SPATIAL_BUCKET_SIZE);
      for (var bx = minX; bx <= maxX; bx++) {
        for (var by = minY; by <= maxY; by++) {
          var key = bucketKey(bx, by);
          var bucket = buckets[key];
          if (!bucket || !bucket.length) {
            continue;
          }
          for (var j = 0; j < bucket.length; j++) {
            if (rectsOverlap(element, bucket[j])) {
              return element.id || 'el_' + i;
            }
          }
        }
      }
      for (bx = minX; bx <= maxX; bx++) {
        for (by = minY; by <= maxY; by++) {
          key = bucketKey(bx, by);
          if (!buckets[key]) {
            buckets[key] = [];
          }
          buckets[key].push(element);
        }
      }
    }
    return '';
  }
  function buildRectFromPoints(a, b) {
    var left = Math.min(a.x, b.x);
    var top = Math.min(a.y, b.y);
    var right = Math.max(a.x, b.x);
    var bottom = Math.max(a.y, b.y);
    return {
      x: roundCoord(left),
      y: roundCoord(top),
      width: roundCoord(right - left),
      height: roundCoord(bottom - top)
    };
  }
  function getTypeMeta(type) {
    return G.TYPE_META[type] || G.TYPE_META.shelf;
  }
  function rangesNearOrOverlap(a1, a2, b1, b2, tolerance) {
    return a1 <= b2 + tolerance && a2 >= b1 - tolerance;
  }
  function bucketKey(x, y) {
    return x + ':' + y;
  }
  function getPreferredResolution() {
    return Math.min(window.devicePixelRatio || 1, 1.25);
  }
  function drawElementByType(graphics, element, type, style) {
    const cameraScale = this.camera.scale;
    const rect = element;
    const drawDeviceList = () => {
      const deviceForm = G.safeParseJson(rect.value);
      if (!deviceForm || !deviceForm.deviceList || deviceForm.deviceList.length === 0) {
        return;
      }
      const fontSize = Math.max(10 / cameraScale, 6);
      const textStyle = new PIXI.TextStyle({
        fontFamily: 'Arial',
        fontSize: fontSize,
        fill: '#000000',
        stroke: '#ffffff',
        strokeThickness: Math.max(1 / cameraScale, 0.5),
        align: 'center'
      });
      const getDeviceNoText = (item) => {
        if (item == null) return '';
        if (item.deviceNo != null && String(item.deviceNo).trim() !== '') {
          return String(item.deviceNo).trim();
        }
        // 兼容历史字段
        if (item.crnNo != null && String(item.crnNo).trim() !== '') {
          return String(item.crnNo).trim();
        }
        if (item.rgvNo != null && String(item.rgvNo).trim() !== '') {
          return String(item.rgvNo).trim();
        }
        return '';
      };
      const newDeviceInfo = G.getDeviceInfo(rect);
      newDeviceInfo.deviceList.forEach((item) => {
        // annulus 轨道上放置的是 rgv 设备
        const deviceType = type === 'annulus' ? 'rgv' : type;
        const deviceContainer = new PIXI.Container();
        const isHorizontal = rect.width > rect.height;
        const centerX = type === 'annulus' ? item.x : item.x + item.width / 2;
        const centerY = type === 'annulus' ? item.y : item.y + item.height / 2;
        // 与 getDeviceInfo / getDevicePixelBoxForTrack 一致
        const drawW = Math.max(2, Math.round(item.width));
        const drawH = Math.max(2, Math.round(item.height));
        deviceContainer.pivot.set(drawW / 2, drawH / 2);
        deviceContainer.position.set(centerX, centerY);
        deviceContainer.rotation =
          type === 'annulus'
            ? G.getRotate({ x: item.x, y: item.y }, item.path)
            : isHorizontal
              ? 0
              : Math.PI / 2;
        const deviceGraphics = new PIXI.Graphics();
        if (deviceType === 'rgv') {
          G.drawRgvDeviceGraphics(deviceGraphics, drawW, drawH, 0x245a9a);
        } else if (deviceType === 'crn' || deviceType === 'dualCrn') {
          G.drawCrnDeviceGraphics(deviceGraphics, drawW, drawH, 0x245a9a);
        } else {
          // fallback: keep previous minimal shape for unknown types
          const radius = Math.max(6 / cameraScale, 2);
          deviceGraphics.beginFill(style.fill.color, 0.92);
          deviceGraphics.lineStyle(style.line.width, style.line.color, style.line.alpha);
          deviceGraphics.drawRoundedRect(0, 0, drawW, drawH, radius);
          deviceGraphics.endFill();
        }
        deviceContainer.addChild(deviceGraphics);
        const txt = getDeviceNoText(item);
        if (txt) {
          const text = new PIXI.Text(txt, textStyle);
          text.anchor.set(0.5);
          text.position.set(drawW / 2, drawH / 2);
          deviceContainer.addChild(text);
        }
        graphics.addChild(deviceContainer);
      });
    };
    function nextId() {
        idSeed += 1;
        return 'el_' + idSeed;
    }
    graphics.lineStyle(style.line.width, style.line.color, style.line.alpha);
    graphics.beginFill(style.fill.color, style.fill.alpha);
    if (type === 'annulus') {
      G.startDrawSmoothedPath(graphics, element, element.shape || this.annulusShape);
      drawDeviceList(rect);
    } else if (isDeviceConfigType(type)) {
      graphics.lineStyle(0);
      graphics.beginFill(style.fill.color, style.fill.alpha);
      graphics.drawRoundedRect(
        rect.x,
        rect.y,
        rect.width,
        rect.height,
        Math.max(6 / cameraScale, 2)
      );
      graphics.endFill();
      graphics.lineStyle(style.line.width, style.line.color, style.line.alpha);
      const isHorizontal = rect.width > rect.height;
    function deepClone(obj) {
        return JSON.parse(JSON.stringify(obj == null ? null : obj));
      if (isHorizontal) {
        const center = rect.height / 2;
        graphics.moveTo(rect.x, rect.y + center);
        graphics.lineTo(rect.x + rect.width, rect.y + center);
      } else {
        const center = rect.width / 2;
        graphics.moveTo(rect.x + center, rect.y);
        graphics.lineTo(rect.x + center, rect.y + rect.height);
      }
      drawDeviceList(rect);
    } else {
      graphics.drawRoundedRect(
        element.x,
        element.y,
        element.width,
        element.height,
        Math.max(6 / cameraScale, 2)
      );
    }
    graphics.endFill();
  }
    function padNumber(value) {
        return value < 10 ? ('0' + value) : String(value);
    }
    function authHeaders() {
        return {
            token: localStorage.getItem('token')
        };
    }
    function getQueryParam(name) {
        var search = window.location.search || '';
        if (!search) {
            return '';
  new Vue({
    el: '#app',
    data: function () {
      return {
        remoteLevOptions: [],
        levOptions: [],
        currentLev: null,
        floorPickerLev: null,
        draftDocs: {},
        doc: null,
        activeTool: 'select',
        toolPanelCollapsed: false,
        inspectorPanelCollapsed: false,
        interactionTools: [
          {
            key: 'select',
            label: '选择 / 移动',
            desc: '点击元素选择,拖拽移动,空白处拖动画布'
          },
          { key: 'marquee', label: '框选', desc: '在画布中框选一组元素' },
          {
            key: 'array',
            label: '阵列',
            desc: '选中货架或维修站台模板后拖线生成一排,按排-列续号'
          },
          { key: 'pan', label: '平移', desc: '专门用于拖动画布和观察全图' }
        ],
        drawTools: [
          { key: 'shelf', label: '货架', desc: '自由拉出货架矩形' },
          { key: 'repairHub', label: '维修站台', desc: '自由拉出维修站台矩形' },
          { key: 'devp', label: '输送线', desc: '拉出站点 / 输送线矩形' },
          { key: 'crn', label: '堆垛机', desc: '拉出堆垛机轨道矩形' },
          { key: 'dualCrn', label: '双工位堆垛机', desc: '拉出双工位轨道矩形' },
          { key: 'rgv', label: 'RGV', desc: '拉出 RGV 轨道矩形' },
          { key: 'annulus', label: '环穿', desc: '拉出环穿轨道矩形' }
        ],
        pixiApp: null,
        mapRoot: null,
        gridLayer: null,
        trackLayer: null,
        nodeLayer: null,
        patchObjectLayer: null,
        activeLayer: null,
        labelLayer: null,
        selectionLayer: null,
        guideLayer: null,
        guideText: null,
        hoverLayer: null,
        labelPool: [],
        renderQueued: false,
        gridSceneDirty: true,
        staticSceneDirty: true,
        spatialIndexDirty: true,
        spatialBuckets: null,
        gridRenderRect: null,
        gridRenderKey: '',
        staticRenderRect: null,
        staticRenderKey: '',
        staticExcludedKey: '',
        camera: {
          x: 80,
          y: 80,
          scale: 1
        },
        viewZoom: 1,
        selectedIds: [],
        clipboard: [],
        hoverElementId: '',
        pointerStatus: '--',
        lastPointerStatusUpdateTs: 0,
        pixiResolution: getPreferredResolution(),
        fpsValue: 0,
        fpsFrameCount: 0,
        fpsSampleStartTs: 0,
        fpsTickerHandler: null,
        interactionState: null,
        isZooming: false,
        isPanning: false,
        zoomRefreshTimer: null,
        panRefreshTimer: null,
        pendingViewportRefresh: false,
        pendingStaticCommit: null,
        deferredStaticRebuildTimer: null,
        currentPointerId: null,
        boundCanvasHandlers: null,
        boundWindowHandlers: null,
        resizeObserver: null,
        labelCapability: {
          maxWidth: 0,
          maxHeight: 0
        },
        labelCapabilityDirty: true,
        undoStack: [],
        redoStack: [],
        savedSnapshot: '',
        isDirty: false,
        saving: false,
        savingAll: false,
        loadingFloor: false,
        switchingFloorLev: null,
        floorRequestSeq: 0,
        activeFloorRequestSeq: 0,
        blankDialogVisible: false,
        blankForm: {
          lev: '',
          width: String(DEFAULT_CANVAS_WIDTH),
          height: String(DEFAULT_CANVAS_HEIGHT)
        },
        canvasForm: {
          width: String(DEFAULT_CANVAS_WIDTH),
          height: String(DEFAULT_CANVAS_HEIGHT)
        },
        geometryForm: {
          x: '',
          y: '',
          width: '',
          height: ''
        },
        devpForm: {
          stationId: '',
          deviceNo: '',
          direction: [],
          isBarcodeStation: false,
          barcodeIdx: '',
          backStation: '',
          backStationDeviceNo: '',
          isInStation: false,
          barcodeStation: '',
          barcodeStationDeviceNo: '',
          isOutStation: false,
          runBlockReassign: false,
          isOutOrder: false,
          isLiftTransfer: false
        },
        deviceForm: {
          trackId: '',
          barCodeStart: 0,
          barCodeEnd: 100000,
          deviceList: []
        },
        devpDirectionOptions: DEVP_DIRECTION_OPTIONS,
        shelfFillForm: {
          startValue: '',
          rowStep: 'desc',
          colStep: 'asc'
        },
        valueEditorText: '',
        spacePressed: false,
        lastCursor: 'default',
        annulusShape: 'rect'
      };
    },
    computed: {
      singleSelectedElement: function () {
        if (!this.doc || this.selectedIds.length !== 1) {
          return null;
        }
        var target = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        var match = search.match(new RegExp('(?:[?&])' + target + '=([^&]*)'));
        return match ? decodeURIComponent(match[1]) : '';
    }
    function toNumber(value, defaultValue) {
        if (value === null || value === undefined || value === '') {
            return defaultValue;
        return this.findElementById(this.selectedIds[0]);
      },
      singleSelectedDeviceElement: function () {
        if (!this.singleSelectedElement || !isDeviceConfigType(this.singleSelectedElement.type)) {
          return null;
        }
        var parsed = Number(value);
        return isFinite(parsed) ? parsed : defaultValue;
    }
    function toInt(value, defaultValue) {
        return Math.round(toNumber(value, defaultValue));
    }
    function clamp(value, min, max) {
        return Math.max(min, Math.min(max, value));
    }
    function roundCoord(value) {
        return Math.round(value * 1000) / 1000;
    }
    function normalizeValue(value) {
        if (value === null || value === undefined) {
            return '';
        return this.singleSelectedElement;
      },
      selectedShelfElements: function () {
        if (!this.doc || !this.selectedIds.length) {
          return [];
        }
        return typeof value === 'string' ? value : JSON.stringify(value);
    }
    function parseShelfLocationValue(value) {
        var text = normalizeValue(value).trim();
        var matched = text.match(/^(-?\d+)\s*-\s*(-?\d+)$/);
        if (!matched) {
            return null;
        return this.getSelectedElements().filter(function (item) {
          return item && isShelfLikeNodeType(item.type);
        });
      },
      devpRequiresBarcodeLink: function () {
        return !!(this.devpForm && this.devpForm.isInStation);
      },
      devpRequiresBarcodeIndex: function () {
        return !!(this.devpForm && this.devpForm.isBarcodeStation);
      },
      devpRequiresBackStation: function () {
        return !!(this.devpForm && this.devpForm.isBarcodeStation);
      },
      arrayPreviewCount: function () {
        if (!this.interactionState || this.interactionState.type !== 'array') {
          return 0;
        }
        return {
            row: toInt(matched[1], 0),
            col: toInt(matched[2], 0)
        };
    }
    function formatShelfLocationValue(row, col) {
        return String(toInt(row, 0)) + '-' + String(toInt(col, 0));
    }
    function safeParseJson(text) {
        if (!text || typeof text !== 'string') {
            return null;
        }
        try {
            return JSON.parse(text);
        } catch (e) {
            return null;
        }
    }
    function boolFlag(value) {
        return value === true || value === 1 || value === '1';
    }
    function normalizeDirectionList(direction) {
        var list = Array.isArray(direction) ? direction : String(direction || '').split(/[,\s|/]+/);
        return this.interactionState.previewItems ? this.interactionState.previewItems.length : 0;
      },
      viewPercent: function () {
        return Math.round(this.viewZoom * 100);
      },
      fpsText: function () {
        return this.fpsValue > 0 ? String(this.fpsValue) : '--';
      },
      dirtyDraftLevs: function () {
        var result = [];
        var seen = {};
        if (this.doc && this.doc.lev && this.isDirty) {
          var currentLev = toInt(this.doc.lev, 0);
          if (currentLev > 0) {
            seen[currentLev] = true;
            result.push(currentLev);
          }
        }
        var self = this;
        Object.keys(this.draftDocs || {}).forEach(function (key) {
          var lev = toInt(key, 0);
          if (lev <= 0 || seen[lev]) {
            return;
          }
          if (self.hasDirtyDraft(lev)) {
            seen[lev] = true;
            result.push(lev);
          }
        });
        result.sort(function (a, b) {
          return a - b;
        });
        return result;
      },
      dirtyDraftCount: function () {
        return this.dirtyDraftLevs.length;
      }
    },
    mounted: function () {
      this.initPixi();
      this.attachEvents();
      this.loadLevOptions();
      var lev = toInt(getQueryParam('lev'), 0);
      if (lev > 0) {
        this.floorPickerLev = lev;
        this.fetchFloor(lev);
      } else {
        this.createLocalBlankDoc(1, DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT, '');
      }
    },
    beforeDestroy: function () {
      this.detachEvents();
      if (this.zoomRefreshTimer) {
        window.clearTimeout(this.zoomRefreshTimer);
        this.zoomRefreshTimer = null;
      }
      if (this.panRefreshTimer) {
        window.clearTimeout(this.panRefreshTimer);
        this.panRefreshTimer = null;
      }
      this.clearDeferredStaticCommit();
      this.stopFpsTicker();
      if (this.pixiApp) {
        this.pixiApp.destroy(true, { children: true });
        this.pixiApp = null;
      }
    },
    methods: {
      showMessage: function (type, message) {
        if (this.$message) {
          this.$message({ type: type, message: message });
        }
      },
      formatNumber: function (value) {
        var num = toNumber(value, 0);
        if (Math.abs(num) >= 1000 || num === Math.round(num)) {
          return String(Math.round(num));
        }
        return String(Math.round(num * 100) / 100);
      },
      syncFloorQueryParam: function (lev) {
        lev = toInt(lev, 0);
        if (lev <= 0 || !window.history || !window.history.replaceState || !window.URL) {
          return;
        }
        try {
          var url = new URL(window.location.href);
          url.searchParams.set('lev', String(lev));
          window.history.replaceState(null, '', url.toString());
        } catch (e) {
          // Ignore URL sync failures and keep editor usable.
        }
      },
      toolLabel: function (tool) {
        var list = this.interactionTools.concat(this.drawTools);
        for (var i = 0; i < list.length; i++) {
            var item = String(list[i] || '').trim().toLowerCase();
            if (!item || seen[item]) {
                continue;
          if (list[i].key === tool) {
            return list[i].label;
          }
        }
        return tool || '--';
      },
      toggleToolPanel: function () {
        this.toolPanelCollapsed = !this.toolPanelCollapsed;
      },
      toggleInspectorPanel: function () {
        this.inspectorPanelCollapsed = !this.inspectorPanelCollapsed;
      },
      startFpsTicker: function () {
        if (!this.pixiApp || !this.pixiApp.ticker || this.fpsTickerHandler) {
          return;
        }
        var self = this;
        this.fpsValue = 0;
        this.fpsFrameCount = 0;
        this.fpsSampleStartTs =
          window.performance && performance.now ? performance.now() : Date.now();
        this.fpsTickerHandler = function () {
          var now = window.performance && performance.now ? performance.now() : Date.now();
          self.fpsFrameCount += 1;
          var elapsed = now - self.fpsSampleStartTs;
          if (elapsed < 400) {
            return;
          }
          self.fpsValue = Math.max(0, Math.round((self.fpsFrameCount * 1000) / elapsed));
          self.fpsFrameCount = 0;
          self.fpsSampleStartTs = now;
        };
        this.pixiApp.ticker.add(this.fpsTickerHandler);
      },
      stopFpsTicker: function () {
        if (this.pixiApp && this.pixiApp.ticker && this.fpsTickerHandler) {
          this.pixiApp.ticker.remove(this.fpsTickerHandler);
        }
        this.fpsTickerHandler = null;
        this.fpsFrameCount = 0;
        this.fpsSampleStartTs = 0;
      },
      initPixi: function () {
        var host = this.$refs.canvasHost;
        if (!host) {
          return;
        }
        var resolution = getPreferredResolution();
        this.pixiResolution = resolution;
        var app = new PIXI.Application({
          width: Math.max(host.clientWidth, 320),
          height: Math.max(host.clientHeight, 320),
          antialias: false,
          autoDensity: true,
          backgroundAlpha: 1,
          backgroundColor: 0xf6f9fc,
          resolution: resolution,
          powerPreference: 'high-performance'
        });
        host.innerHTML = '';
        host.appendChild(app.view);
        app.view.style.width = '100%';
        app.view.style.height = '100%';
        app.view.style.touchAction = 'none';
        app.view.style.background = '#f6f9fc';
        app.renderer.roundPixels = true;
        this.pixiApp = app;
        this.mapRoot = new PIXI.Container();
        app.stage.addChild(this.mapRoot);
        this.gridLayer = new PIXI.Graphics();
        this.staticLayer = new PIXI.Container();
        this.staticTrackSpriteLayer = null;
        this.staticNodeSpriteLayer = null;
        this.trackLayer = new PIXI.Graphics();
        this.nodeLayer = new PIXI.Graphics();
        this.eraseLayer = new PIXI.Graphics();
        this.patchObjectLayer = new PIXI.Graphics();
        this.activeLayer = new PIXI.Graphics();
        this.labelLayer = new PIXI.Container();
        this.selectionLayer = new PIXI.Graphics();
        this.guideLayer = new PIXI.Graphics();
        this.guideText = new PIXI.Text('', {
          fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
          fontSize: 14,
          fontWeight: '700',
          fill: 0x1f4f86,
          stroke: 0xffffff,
          strokeThickness: 4,
          lineJoin: 'round'
        });
        this.guideText.anchor.set(0.5, 1);
        this.guideText.visible = false;
        this.hoverLayer = new PIXI.Graphics();
        this.staticTrackSpritePool = [];
        this.staticNodeSpritePool = [];
        this.mapRoot.addChild(this.gridLayer);
        this.staticTrackSpriteLayer = new PIXI.ParticleContainer(
          12000,
          {
            position: true,
            scale: true,
            alpha: true,
            tint: true
          },
          16384,
          true
        );
        this.staticNodeSpriteLayer = new PIXI.ParticleContainer(
          12000,
          {
            position: true,
            scale: true,
            alpha: true,
            tint: true
          },
          16384,
          true
        );
        this.staticLayer.addChild(this.staticTrackSpriteLayer);
        this.staticLayer.addChild(this.trackLayer);
        this.staticLayer.addChild(this.staticNodeSpriteLayer);
        this.staticLayer.addChild(this.nodeLayer);
        this.mapRoot.addChild(this.staticLayer);
        this.mapRoot.addChild(this.eraseLayer);
        this.mapRoot.addChild(this.patchObjectLayer);
        this.mapRoot.addChild(this.activeLayer);
        this.mapRoot.addChild(this.labelLayer);
        this.mapRoot.addChild(this.hoverLayer);
        this.mapRoot.addChild(this.selectionLayer);
        this.mapRoot.addChild(this.guideLayer);
        this.mapRoot.addChild(this.guideText);
        this.boundCanvasHandlers = {
          pointerdown: this.onCanvasPointerDown.bind(this),
          wheel: this.onCanvasWheel.bind(this)
        };
        app.view.addEventListener('pointerdown', this.boundCanvasHandlers.pointerdown);
        app.view.addEventListener('wheel', this.boundCanvasHandlers.wheel, {
          passive: false
        });
        if (window.ResizeObserver) {
          this.resizeObserver = new ResizeObserver(this.handleResize.bind(this));
          this.resizeObserver.observe(host);
        }
        this.startFpsTicker();
        this.handleResize();
      },
      attachEvents: function () {
        this.boundWindowHandlers = {
          pointermove: this.onWindowPointerMove.bind(this),
          pointerup: this.onWindowPointerUp.bind(this),
          pointercancel: this.onWindowPointerUp.bind(this),
          keydown: this.onWindowKeyDown.bind(this),
          keyup: this.onWindowKeyUp.bind(this),
          beforeunload: this.onBeforeUnload.bind(this),
          resize: this.handleResize.bind(this)
        };
        window.addEventListener('pointermove', this.boundWindowHandlers.pointermove);
        window.addEventListener('pointerup', this.boundWindowHandlers.pointerup);
        window.addEventListener('pointercancel', this.boundWindowHandlers.pointercancel);
        window.addEventListener('keydown', this.boundWindowHandlers.keydown);
        window.addEventListener('keyup', this.boundWindowHandlers.keyup);
        window.addEventListener('beforeunload', this.boundWindowHandlers.beforeunload);
        window.addEventListener('resize', this.boundWindowHandlers.resize);
      },
      detachEvents: function () {
        if (this.pixiApp && this.boundCanvasHandlers) {
          this.pixiApp.view.removeEventListener(
            'pointerdown',
            this.boundCanvasHandlers.pointerdown
          );
          this.pixiApp.view.removeEventListener('wheel', this.boundCanvasHandlers.wheel);
        }
        if (this.resizeObserver) {
          this.resizeObserver.disconnect();
          this.resizeObserver = null;
        }
        if (!this.boundWindowHandlers) {
          return;
        }
        window.removeEventListener('pointermove', this.boundWindowHandlers.pointermove);
        window.removeEventListener('pointerup', this.boundWindowHandlers.pointerup);
        window.removeEventListener('pointercancel', this.boundWindowHandlers.pointercancel);
        window.removeEventListener('keydown', this.boundWindowHandlers.keydown);
        window.removeEventListener('keyup', this.boundWindowHandlers.keyup);
        window.removeEventListener('beforeunload', this.boundWindowHandlers.beforeunload);
        window.removeEventListener('resize', this.boundWindowHandlers.resize);
      },
      handleResize: function () {
        if (!this.pixiApp || !this.$refs.canvasHost) {
          return;
        }
        var host = this.$refs.canvasHost;
        var width = Math.max(host.clientWidth, 320);
        var height = Math.max(host.clientHeight, 320);
        this.pixiApp.renderer.resize(width, height);
        this.markGridSceneDirty();
        this.markStaticSceneDirty();
        this.scheduleRender();
      },
      loadLevOptions: function () {
        var self = this;
        $.ajax({
          url: baseUrl + '/basMap/getLevList',
          method: 'GET',
          headers: authHeaders(),
          success: function (res) {
            if (res && res.code === 200 && Array.isArray(res.data)) {
              self.remoteLevOptions = res.data
                .map(function (item) {
                  return toInt(item, 0);
                })
                .filter(function (item) {
                  return item > 0;
                });
              self.refreshLevOptions();
            }
            seen[item] = true;
            result.push(item);
          }
        });
      },
      refreshLevOptions: function () {
        var set = {};
        var result = [];
        var pushLev = function (lev) {
          lev = toInt(lev, 0);
          if (lev <= 0 || set[lev]) {
            return;
          }
          set[lev] = true;
          result.push(lev);
        };
        this.remoteLevOptions.forEach(pushLev);
        Object.keys(this.draftDocs || {}).forEach(pushLev);
        pushLev(this.currentLev);
        pushLev(this.floorPickerLev);
        result.sort(function (a, b) {
          return a - b;
        });
        this.levOptions = result;
      },
      exportDoc: function (doc) {
        var source = doc || this.doc || {};
        return {
          lev: toInt(source.lev, 0),
          editorMode: FREE_EDITOR_MODE,
          canvasWidth: roundCoord(toNumber(source.canvasWidth, DEFAULT_CANVAS_WIDTH)),
          canvasHeight: roundCoord(toNumber(source.canvasHeight, DEFAULT_CANVAS_HEIGHT)),
          elements: (source.elements || []).map(function (item, index) {
            const info = {
              id: item && item.id ? String(item.id) : 'el_' + (index + 1),
              type: DRAW_TYPES.indexOf(item && item.type) >= 0 ? item.type : 'shelf',
              x: roundCoord(Math.max(0, toNumber(item && item.x, 0))),
              y: roundCoord(Math.max(0, toNumber(item && item.y, 0))),
              width: roundCoord(
                Math.max(MIN_ELEMENT_SIZE, toNumber(item && item.width, MIN_ELEMENT_SIZE))
              ),
              height: roundCoord(
                Math.max(MIN_ELEMENT_SIZE, toNumber(item && item.height, MIN_ELEMENT_SIZE))
              ),
              value: normalizeValue(item && item.value),
              shape: item.shape,
              pathList: item.pathList,
              turningPoint: item.turningPoint
                ? { x: toNumber(item.turningPoint.x, 0), y: toNumber(item.turningPoint.y, 0) }
                : undefined,
              annulusBandInset:
                item.annulusBandInset != null ? toNumber(item.annulusBandInset, 0) : undefined
            };
            return info;
          })
        };
      },
      normalizeDoc: function (doc) {
        var normalized = this.exportDoc(doc || {});
        if (normalized.lev <= 0) {
          normalized.lev = toInt(this.currentLev, 1) || 1;
        }
        return normalized;
      },
      snapshotDoc: function (doc) {
        return JSON.stringify(this.exportDoc(doc));
      },
      syncDirty: function () {
        var currentSnapshot = this.snapshotDoc(this.doc);
        this.isDirty = currentSnapshot !== this.savedSnapshot;
      },
      setDraftDocEntry: function (lev, doc, savedSnapshot) {
        lev = toInt(lev, 0);
        if (lev <= 0 || !doc) {
          return;
        }
        var entry = {
          doc: this.exportDoc(doc),
          savedSnapshot: savedSnapshot != null ? savedSnapshot : ''
        };
        if (this.$set) {
          this.$set(this.draftDocs, lev, entry);
        } else {
          this.draftDocs[lev] = entry;
        }
      },
      removeDraftDocEntry: function (lev) {
        lev = toInt(lev, 0);
        if (lev <= 0) {
          return;
        }
        if (this.$delete) {
          this.$delete(this.draftDocs, lev);
        } else {
          delete this.draftDocs[lev];
        }
      },
      cacheCurrentDraft: function () {
        if (!this.doc || !this.doc.lev) {
          return;
        }
        this.setDraftDocEntry(this.doc.lev, this.doc, this.savedSnapshot);
        this.refreshLevOptions();
      },
      clearCurrentDraftIfSaved: function () {
        if (!this.doc || !this.doc.lev) {
          return;
        }
        this.setDraftDocEntry(this.doc.lev, this.doc, this.savedSnapshot);
      },
      clearFloorTransientState: function () {
        this.clearDeferredStaticCommit();
        this.interactionState = null;
        this.currentPointerId = null;
        this.hoverElementId = '';
        this.pointerStatus = '--';
        this.lastPointerStatusUpdateTs = 0;
        this.selectedIds = [];
        this.isPanning = false;
        this.isZooming = false;
        this.pendingViewportRefresh = false;
        if (this.zoomRefreshTimer) {
          window.clearTimeout(this.zoomRefreshTimer);
          this.zoomRefreshTimer = null;
        }
        if (this.panRefreshTimer) {
          window.clearTimeout(this.panRefreshTimer);
          this.panRefreshTimer = null;
        }
      },
      resetRenderLayers: function () {
        if (this.gridLayer) {
          this.gridLayer.clear();
        }
        if (this.trackLayer) {
          this.trackLayer.clear();
          this.trackLayer.removeChildren();
        }
        if (this.nodeLayer) {
          this.nodeLayer.clear();
          this.nodeLayer.removeChildren();
        }
        if (this.eraseLayer) {
          this.eraseLayer.clear();
        }
        if (this.patchObjectLayer) {
          this.patchObjectLayer.clear();
          this.patchObjectLayer.removeChildren();
        }
        if (this.activeLayer) {
          this.activeLayer.clear();
          this.activeLayer.removeChildren();
        }
        if (this.selectionLayer) {
          this.selectionLayer.clear();
          this.selectionLayer.removeChildren();
        }
        if (this.guideLayer) {
          this.guideLayer.clear();
          this.guideLayer.removeChildren();
        }
        if (this.hoverLayer) {
          this.hoverLayer.clear();
        }
        if (this.guideText) {
          this.guideText.visible = false;
          this.guideText.text = '';
        }
        if (this.labelLayer) {
          this.labelLayer.visible = false;
        }
        for (var i = 0; i < this.labelPool.length; i++) {
          this.labelPool[i].visible = false;
          this.labelPool[i].text = '';
        }
        this.hideUnusedStaticSprites(this.staticTrackSpritePool || [], 0);
        this.hideUnusedStaticSprites(this.staticNodeSpritePool || [], 0);
        if (this.staticTrackSpriteLayer) {
          this.staticTrackSpriteLayer.removeChildren();
          this.staticTrackSpritePool = [];
        }
        if (this.staticNodeSpriteLayer) {
          this.staticNodeSpriteLayer.removeChildren();
          this.staticNodeSpritePool = [];
        }
        if (this.staticTrackSpriteLayer) {
          this.staticTrackSpriteLayer.visible = false;
        }
        if (this.staticNodeSpriteLayer) {
          this.staticNodeSpriteLayer.visible = false;
        }
      },
      hasDirtyDraft: function (lev) {
        lev = toInt(lev, 0);
        if (lev <= 0) {
          return false;
        }
        var entry = this.draftDocs[lev];
        if (!entry || !entry.doc) {
          return false;
        }
        var snapshot = this.snapshotDoc(entry.doc);
        return snapshot !== (entry.savedSnapshot || '');
      },
      markStaticSceneDirty: function () {
        this.staticSceneDirty = true;
      },
      markGridSceneDirty: function () {
        this.gridSceneDirty = true;
      },
      clearRenderCaches: function () {
        this.gridRenderRect = null;
        this.gridRenderKey = '';
        this.staticRenderRect = null;
        this.staticRenderKey = '';
        this.staticExcludedKey = '';
      },
      scheduleZoomRefresh: function () {
        if (this.zoomRefreshTimer) {
          window.clearTimeout(this.zoomRefreshTimer);
        }
        this.isZooming = true;
        this.zoomRefreshTimer = window.setTimeout(
          function () {
            this.zoomRefreshTimer = null;
            this.isZooming = false;
            if (this.isPanning || (this.interactionState && this.interactionState.type === 'pan')) {
              this.pendingViewportRefresh = true;
              return;
            }
            this.markGridSceneDirty();
            this.markStaticSceneDirty();
            this.scheduleRender();
          }.bind(this),
          ZOOM_REFRESH_DELAY
        );
      },
      cancelPanRefresh: function () {
        if (this.panRefreshTimer) {
          window.clearTimeout(this.panRefreshTimer);
          this.panRefreshTimer = null;
        }
      },
      schedulePanRefresh: function () {
        this.cancelPanRefresh();
        this.isPanning = true;
        this.panRefreshTimer = window.setTimeout(
          function () {
            this.panRefreshTimer = null;
            this.isPanning = false;
            if (this.pendingViewportRefresh) {
              this.pendingViewportRefresh = false;
              this.markGridSceneDirty();
              this.markStaticSceneDirty();
            }
            this.scheduleRender();
          }.bind(this),
          PAN_LABEL_REFRESH_DELAY
        );
      },
      rebuildLabelCapability: function () {
        var maxWidth = 0;
        var maxHeight = 0;
        var elements = this.doc && this.doc.elements ? this.doc.elements : [];
        for (var i = 0; i < elements.length; i++) {
          var element = elements[i];
          if (element.width > maxWidth) {
            maxWidth = element.width;
          }
          if (element.height > maxHeight) {
            maxHeight = element.height;
          }
        }
        this.labelCapability = {
          maxWidth: maxWidth,
          maxHeight: maxHeight
        };
        this.labelCapabilityDirty = false;
      },
      ensureLabelCapability: function () {
        if (this.labelCapabilityDirty) {
          this.rebuildLabelCapability();
        }
        return this.labelCapability;
      },
      markSpatialIndexDirty: function () {
        this.spatialIndexDirty = true;
      },
      rebuildSpatialIndex: function () {
        var buckets = {};
        var elements = this.doc && this.doc.elements ? this.doc.elements : [];
        for (var i = 0; i < elements.length; i++) {
          var element = elements[i];
          var minX = Math.floor(element.x / SPATIAL_BUCKET_SIZE);
          var maxX = Math.floor((element.x + element.width) / SPATIAL_BUCKET_SIZE);
          var minY = Math.floor(element.y / SPATIAL_BUCKET_SIZE);
          var maxY = Math.floor((element.y + element.height) / SPATIAL_BUCKET_SIZE);
          for (var bx = minX; bx <= maxX; bx++) {
            for (var by = minY; by <= maxY; by++) {
              var key = bucketKey(bx, by);
              if (!buckets[key]) {
                buckets[key] = [];
              }
              buckets[key].push(element);
            }
          }
        }
        this.spatialBuckets = buckets;
        this.spatialIndexDirty = false;
      },
      ensureSpatialIndex: function () {
        if (this.spatialIndexDirty || !this.spatialBuckets) {
          this.rebuildSpatialIndex();
        }
      },
      querySpatialCandidates: function (rect, padding, excludeIds) {
        if (!this.doc || !rect) {
          return [];
        }
        this.ensureSpatialIndex();
        var excludeMap = {};
        excludeIds = excludeIds || [];
        for (var i = 0; i < excludeIds.length; i++) {
          excludeMap[excludeIds[i]] = true;
        }
        var seen = {};
        var result = [];
        var pad = Math.max(0, padding || 0);
        var minX = Math.floor((rect.x - pad) / SPATIAL_BUCKET_SIZE);
        var maxX = Math.floor((rect.x + rect.width + pad) / SPATIAL_BUCKET_SIZE);
        var minY = Math.floor((rect.y - pad) / SPATIAL_BUCKET_SIZE);
        var maxY = Math.floor((rect.y + rect.height + pad) / SPATIAL_BUCKET_SIZE);
        for (var bx = minX; bx <= maxX; bx++) {
          for (var by = minY; by <= maxY; by++) {
            var key = bucketKey(bx, by);
            var bucket = this.spatialBuckets[key];
            if (!bucket || !bucket.length) {
              continue;
            }
            for (var j = 0; j < bucket.length; j++) {
              var element = bucket[j];
              if (!element || seen[element.id] || excludeMap[element.id]) {
                continue;
              }
              seen[element.id] = true;
              result.push(element);
            }
          }
        }
        return result;
    }
    function directionTokenToArrow(token) {
        if (token === 'top' || token === 'up' || token === 'north' || token === 'n') {
            return '↑';
      },
      cancelDeferredStaticRebuild: function () {
        if (this.deferredStaticRebuildTimer) {
          window.clearTimeout(this.deferredStaticRebuildTimer);
          this.deferredStaticRebuildTimer = null;
        }
        if (token === 'right' || token === 'east' || token === 'e') {
            return '→';
        }
        if (token === 'bottom' || token === 'down' || token === 'south' || token === 's') {
            return '↓';
        }
        if (token === 'left' || token === 'west' || token === 'w') {
            return '←';
        }
        return '';
    }
    function formatDirectionArrows(direction) {
        var list = normalizeDirectionList(direction);
        var arrows = [];
        for (var i = 0; i < list.length; i++) {
            var arrow = directionTokenToArrow(list[i]);
            if (arrow) {
                arrows.push(arrow);
            }
        }
        return arrows.join('');
    }
    function isDeviceConfigType(type) {
        return DEVICE_CONFIG_TYPES.indexOf(type) >= 0;
    }
    function pickDeviceValueKey(type, json) {
        if (json && json.deviceNo != null) {
            return 'deviceNo';
        }
        if ((type === 'crn' || type === 'dualCrn') && json && json.crnNo != null) {
            return 'crnNo';
        }
        if (type === 'rgv' && json && json.rgvNo != null) {
            return 'rgvNo';
        }
        return 'deviceNo';
    }
    function isInputLike(target) {
        if (!target || !target.tagName) {
            return false;
        }
        var tag = String(target.tagName || '').toLowerCase();
        return tag === 'input' || tag === 'textarea' || tag === 'select' || !!target.isContentEditable;
    }
    function rectsOverlap(a, b) {
        return a.x < b.x + b.width - COORD_EPSILON && a.x + a.width > b.x + COORD_EPSILON
            && a.y < b.y + b.height - COORD_EPSILON && a.y + a.height > b.y + COORD_EPSILON;
    }
    function rectIntersects(a, b) {
        return a.x <= b.x + b.width && a.x + a.width >= b.x
            && a.y <= b.y + b.height && a.y + a.height >= b.y;
    }
    function isRectWithinCanvas(rect, canvasWidth, canvasHeight) {
        return rect.x >= -COORD_EPSILON && rect.y >= -COORD_EPSILON
            && rect.x + rect.width <= canvasWidth + COORD_EPSILON
            && rect.y + rect.height <= canvasHeight + COORD_EPSILON;
    }
    function findDocOverlapId(doc) {
        if (!doc || !doc.elements || !doc.elements.length) {
            return '';
        }
        var buckets = {};
        var elements = doc.elements;
        for (var i = 0; i < elements.length; i++) {
            var element = elements[i];
            var minX = Math.floor(element.x / SPATIAL_BUCKET_SIZE);
            var maxX = Math.floor((element.x + element.width) / SPATIAL_BUCKET_SIZE);
            var minY = Math.floor(element.y / SPATIAL_BUCKET_SIZE);
            var maxY = Math.floor((element.y + element.height) / SPATIAL_BUCKET_SIZE);
            for (var bx = minX; bx <= maxX; bx++) {
                for (var by = minY; by <= maxY; by++) {
                    var key = bucketKey(bx, by);
                    var bucket = buckets[key];
                    if (!bucket || !bucket.length) {
                        continue;
                    }
                    for (var j = 0; j < bucket.length; j++) {
                        if (rectsOverlap(element, bucket[j])) {
                            return element.id || ('el_' + i);
                        }
                    }
                }
            }
            for (bx = minX; bx <= maxX; bx++) {
                for (by = minY; by <= maxY; by++) {
                    key = bucketKey(bx, by);
                    if (!buckets[key]) {
                        buckets[key] = [];
                    }
                    buckets[key].push(element);
                }
            }
        }
        return '';
    }
    function buildRectFromPoints(a, b) {
        var left = Math.min(a.x, b.x);
        var top = Math.min(a.y, b.y);
        var right = Math.max(a.x, b.x);
        var bottom = Math.max(a.y, b.y);
        return {
            x: roundCoord(left),
            y: roundCoord(top),
            width: roundCoord(right - left),
            height: roundCoord(bottom - top)
        };
    }
    function getTypeMeta(type) {
        return TYPE_META[type] || TYPE_META.shelf;
    }
    function rangesNearOrOverlap(a1, a2, b1, b2, tolerance) {
        return a1 <= b2 + tolerance && a2 >= b1 - tolerance;
    }
    function bucketKey(x, y) {
        return x + ':' + y;
    }
    function getPreferredResolution() {
        return Math.min(window.devicePixelRatio || 1, 1.25);
    }
    new Vue({
        el: '#app',
        data: function () {
      },
      stageDeferredStaticCommit: function (ids, eraseRects) {
        this.pendingStaticCommit = {
          ids: (ids || []).slice(),
          eraseRects: (eraseRects || []).map(function (item) {
            return {
                remoteLevOptions: [],
                levOptions: [],
                currentLev: null,
                floorPickerLev: null,
                draftDocs: {},
                doc: null,
                activeTool: 'select',
                toolPanelCollapsed: false,
                inspectorPanelCollapsed: false,
                interactionTools: [
                    { key: 'select', label: '选择 / 移动', desc: '点击元素选择,拖拽移动,空白处拖动画布' },
                    { key: 'marquee', label: '框选', desc: '在画布中框选一组元素' },
                    { key: 'array', label: '阵列', desc: '选中一个货架 / 轨道后拖一条线自动生成一排' },
                    { key: 'pan', label: '平移', desc: '专门用于拖动画布和观察全图' }
                ],
                drawTools: [
                    { key: 'shelf', label: '货架', desc: '自由拉出货架矩形' },
                    { key: 'devp', label: '输送线', desc: '拉出站点 / 输送线矩形' },
                    { key: 'crn', label: 'CRN', desc: '拉出堆垛机轨道矩形' },
                    { key: 'dualCrn', label: '双工位', desc: '拉出双工位轨道矩形' },
                    { key: 'rgv', label: 'RGV', desc: '拉出 RGV 轨道矩形' }
                ],
                pixiApp: null,
                mapRoot: null,
                gridLayer: null,
                trackLayer: null,
                nodeLayer: null,
                patchObjectLayer: null,
                activeLayer: null,
                labelLayer: null,
                selectionLayer: null,
                guideLayer: null,
                guideText: null,
                hoverLayer: null,
                labelPool: [],
                renderQueued: false,
                gridSceneDirty: true,
                staticSceneDirty: true,
                spatialIndexDirty: true,
                spatialBuckets: null,
                gridRenderRect: null,
                gridRenderKey: '',
                staticRenderRect: null,
                staticRenderKey: '',
                staticExcludedKey: '',
                camera: {
                    x: 80,
                    y: 80,
                    scale: 1
                },
                viewZoom: 1,
                selectedIds: [],
                clipboard: [],
                hoverElementId: '',
                pointerStatus: '--',
                lastPointerStatusUpdateTs: 0,
                pixiResolution: getPreferredResolution(),
                fpsValue: 0,
                fpsFrameCount: 0,
                fpsSampleStartTs: 0,
                fpsTickerHandler: null,
                interactionState: null,
                isZooming: false,
                isPanning: false,
                zoomRefreshTimer: null,
                panRefreshTimer: null,
                pendingViewportRefresh: false,
                pendingStaticCommit: null,
                deferredStaticRebuildTimer: null,
                currentPointerId: null,
                boundCanvasHandlers: null,
                boundWindowHandlers: null,
                resizeObserver: null,
                labelCapability: {
                    maxWidth: 0,
                    maxHeight: 0
                },
                labelCapabilityDirty: true,
                undoStack: [],
                redoStack: [],
                savedSnapshot: '',
                isDirty: false,
                saving: false,
                savingAll: false,
                loadingFloor: false,
                switchingFloorLev: null,
                floorRequestSeq: 0,
                activeFloorRequestSeq: 0,
                blankDialogVisible: false,
                blankForm: {
                    lev: '',
                    width: String(DEFAULT_CANVAS_WIDTH),
                    height: String(DEFAULT_CANVAS_HEIGHT)
                },
                canvasForm: {
                    width: String(DEFAULT_CANVAS_WIDTH),
                    height: String(DEFAULT_CANVAS_HEIGHT)
                },
                geometryForm: {
                    x: '',
                    y: '',
                    width: '',
                    height: ''
                },
                devpForm: {
                    stationId: '',
                    deviceNo: '',
                    direction: [],
                    isBarcodeStation: false,
                    barcodeIdx: '',
                    backStation: '',
                    backStationDeviceNo: '',
                    isInStation: false,
                    barcodeStation: '',
                    barcodeStationDeviceNo: '',
                    isOutStation: false,
                    runBlockReassign: false,
                    isOutOrder: false,
                    isLiftTransfer: false
                },
                deviceForm: {
                    valueKey: '',
                    deviceNo: ''
                },
                devpDirectionOptions: DEVP_DIRECTION_OPTIONS,
                shelfFillForm: {
                    startValue: '',
                    rowStep: 'desc',
                    colStep: 'asc'
                },
                valueEditorText: '',
                spacePressed: false,
                lastCursor: 'default'
              x: item.x,
              y: item.y,
              width: item.width,
              height: item.height
            };
        },
        computed: {
            singleSelectedElement: function () {
                if (!this.doc || this.selectedIds.length !== 1) {
                    return null;
                }
                return this.findElementById(this.selectedIds[0]);
            },
            singleSelectedDeviceElement: function () {
                if (!this.singleSelectedElement || !isDeviceConfigType(this.singleSelectedElement.type)) {
                    return null;
                }
                return this.singleSelectedElement;
            },
            selectedShelfElements: function () {
                if (!this.doc || !this.selectedIds.length) {
                    return [];
                }
                return this.getSelectedElements().filter(function (item) {
                    return item && item.type === 'shelf';
                });
            },
            devpRequiresBarcodeLink: function () {
                return !!(this.devpForm && this.devpForm.isInStation);
            },
            devpRequiresBarcodeIndex: function () {
                return !!(this.devpForm && this.devpForm.isBarcodeStation);
            },
            devpRequiresBackStation: function () {
                return !!(this.devpForm && this.devpForm.isBarcodeStation);
            },
            arrayPreviewCount: function () {
                if (!this.interactionState || this.interactionState.type !== 'array') {
                    return 0;
                }
                return this.interactionState.previewItems ? this.interactionState.previewItems.length : 0;
            },
            viewPercent: function () {
                return Math.round(this.viewZoom * 100);
            },
            fpsText: function () {
                return this.fpsValue > 0 ? String(this.fpsValue) : '--';
            },
            dirtyDraftLevs: function () {
                var result = [];
                var seen = {};
                if (this.doc && this.doc.lev && this.isDirty) {
                    var currentLev = toInt(this.doc.lev, 0);
                    if (currentLev > 0) {
                        seen[currentLev] = true;
                        result.push(currentLev);
                    }
                }
                var self = this;
                Object.keys(this.draftDocs || {}).forEach(function (key) {
                    var lev = toInt(key, 0);
                    if (lev <= 0 || seen[lev]) {
                        return;
                    }
                    if (self.hasDirtyDraft(lev)) {
                        seen[lev] = true;
                        result.push(lev);
                    }
                });
                result.sort(function (a, b) { return a - b; });
                return result;
            },
            dirtyDraftCount: function () {
                return this.dirtyDraftLevs.length;
            }
        },
        mounted: function () {
            this.initPixi();
            this.attachEvents();
            this.loadLevOptions();
            var lev = toInt(getQueryParam('lev'), 0);
            if (lev > 0) {
                this.floorPickerLev = lev;
                this.fetchFloor(lev);
            } else {
                this.createLocalBlankDoc(1, DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT, '');
            }
        },
        beforeDestroy: function () {
            this.detachEvents();
            if (this.zoomRefreshTimer) {
                window.clearTimeout(this.zoomRefreshTimer);
                this.zoomRefreshTimer = null;
            }
            if (this.panRefreshTimer) {
                window.clearTimeout(this.panRefreshTimer);
                this.panRefreshTimer = null;
            }
            this.clearDeferredStaticCommit();
            this.stopFpsTicker();
            if (this.pixiApp) {
                this.pixiApp.destroy(true, { children: true });
                this.pixiApp = null;
            }
        },
        methods: {
            showMessage: function (type, message) {
                if (this.$message) {
                    this.$message({ type: type, message: message });
                }
            },
            formatNumber: function (value) {
                var num = toNumber(value, 0);
                if (Math.abs(num) >= 1000 || num === Math.round(num)) {
                    return String(Math.round(num));
                }
                return String(Math.round(num * 100) / 100);
            },
            syncFloorQueryParam: function (lev) {
                lev = toInt(lev, 0);
                if (lev <= 0 || !window.history || !window.history.replaceState || !window.URL) {
                    return;
                }
                try {
                    var url = new URL(window.location.href);
                    url.searchParams.set('lev', String(lev));
                    window.history.replaceState(null, '', url.toString());
                } catch (e) {
                    // Ignore URL sync failures and keep editor usable.
                }
            },
            toolLabel: function (tool) {
                var list = this.interactionTools.concat(this.drawTools);
                for (var i = 0; i < list.length; i++) {
                    if (list[i].key === tool) {
                        return list[i].label;
                    }
                }
                return tool || '--';
            },
            toggleToolPanel: function () {
                this.toolPanelCollapsed = !this.toolPanelCollapsed;
            },
            toggleInspectorPanel: function () {
                this.inspectorPanelCollapsed = !this.inspectorPanelCollapsed;
            },
            startFpsTicker: function () {
                if (!this.pixiApp || !this.pixiApp.ticker || this.fpsTickerHandler) {
                    return;
                }
                var self = this;
                this.fpsValue = 0;
                this.fpsFrameCount = 0;
                this.fpsSampleStartTs = (window.performance && performance.now) ? performance.now() : Date.now();
                this.fpsTickerHandler = function () {
                    var now = (window.performance && performance.now) ? performance.now() : Date.now();
                    self.fpsFrameCount += 1;
                    var elapsed = now - self.fpsSampleStartTs;
                    if (elapsed < 400) {
                        return;
                    }
                    self.fpsValue = Math.max(0, Math.round(self.fpsFrameCount * 1000 / elapsed));
                    self.fpsFrameCount = 0;
                    self.fpsSampleStartTs = now;
                };
                this.pixiApp.ticker.add(this.fpsTickerHandler);
            },
            stopFpsTicker: function () {
                if (this.pixiApp && this.pixiApp.ticker && this.fpsTickerHandler) {
                    this.pixiApp.ticker.remove(this.fpsTickerHandler);
                }
                this.fpsTickerHandler = null;
                this.fpsFrameCount = 0;
                this.fpsSampleStartTs = 0;
            },
            initPixi: function () {
                var host = this.$refs.canvasHost;
                if (!host) {
                    return;
                }
                var resolution = getPreferredResolution();
                this.pixiResolution = resolution;
                var app = new PIXI.Application({
                    width: Math.max(host.clientWidth, 320),
                    height: Math.max(host.clientHeight, 320),
                    antialias: false,
                    autoDensity: true,
                    backgroundAlpha: 1,
                    backgroundColor: 0xf6f9fc,
                    resolution: resolution,
                    powerPreference: 'high-performance'
                });
                host.innerHTML = '';
                host.appendChild(app.view);
                app.view.style.width = '100%';
                app.view.style.height = '100%';
                app.view.style.touchAction = 'none';
                app.view.style.background = '#f6f9fc';
                app.renderer.roundPixels = true;
                this.pixiApp = app;
                this.mapRoot = new PIXI.Container();
                app.stage.addChild(this.mapRoot);
                this.gridLayer = new PIXI.Graphics();
                this.staticLayer = new PIXI.Container();
                this.staticTrackSpriteLayer = null;
                this.staticNodeSpriteLayer = null;
                this.trackLayer = new PIXI.Graphics();
                this.nodeLayer = new PIXI.Graphics();
                this.eraseLayer = new PIXI.Graphics();
                this.patchObjectLayer = new PIXI.Graphics();
                this.activeLayer = new PIXI.Graphics();
                this.labelLayer = new PIXI.Container();
                this.selectionLayer = new PIXI.Graphics();
                this.guideLayer = new PIXI.Graphics();
                this.guideText = new PIXI.Text('', {
                    fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
                    fontSize: 14,
                    fontWeight: '700',
                    fill: 0x1f4f86,
                    stroke: 0xffffff,
                    strokeThickness: 4,
                    lineJoin: 'round'
                });
                this.guideText.anchor.set(0.5, 1);
                this.guideText.visible = false;
                this.hoverLayer = new PIXI.Graphics();
                this.staticTrackSpritePool = [];
                this.staticNodeSpritePool = [];
                this.mapRoot.addChild(this.gridLayer);
                this.staticTrackSpriteLayer = new PIXI.ParticleContainer(12000, {
                    position: true,
                    scale: true,
                    alpha: true,
                    tint: true
                }, 16384, true);
                this.staticNodeSpriteLayer = new PIXI.ParticleContainer(12000, {
                    position: true,
                    scale: true,
                    alpha: true,
                    tint: true
                }, 16384, true);
                this.staticLayer.addChild(this.staticTrackSpriteLayer);
                this.staticLayer.addChild(this.trackLayer);
                this.staticLayer.addChild(this.staticNodeSpriteLayer);
                this.staticLayer.addChild(this.nodeLayer);
                this.mapRoot.addChild(this.staticLayer);
                this.mapRoot.addChild(this.eraseLayer);
                this.mapRoot.addChild(this.patchObjectLayer);
                this.mapRoot.addChild(this.activeLayer);
                this.mapRoot.addChild(this.labelLayer);
                this.mapRoot.addChild(this.hoverLayer);
                this.mapRoot.addChild(this.selectionLayer);
                this.mapRoot.addChild(this.guideLayer);
                this.mapRoot.addChild(this.guideText);
                this.boundCanvasHandlers = {
                    pointerdown: this.onCanvasPointerDown.bind(this),
                    wheel: this.onCanvasWheel.bind(this)
                };
                app.view.addEventListener('pointerdown', this.boundCanvasHandlers.pointerdown);
                app.view.addEventListener('wheel', this.boundCanvasHandlers.wheel, { passive: false });
                if (window.ResizeObserver) {
                    this.resizeObserver = new ResizeObserver(this.handleResize.bind(this));
                    this.resizeObserver.observe(host);
                }
                this.startFpsTicker();
                this.handleResize();
            },
            attachEvents: function () {
                this.boundWindowHandlers = {
                    pointermove: this.onWindowPointerMove.bind(this),
                    pointerup: this.onWindowPointerUp.bind(this),
                    pointercancel: this.onWindowPointerUp.bind(this),
                    keydown: this.onWindowKeyDown.bind(this),
                    keyup: this.onWindowKeyUp.bind(this),
                    beforeunload: this.onBeforeUnload.bind(this),
                    resize: this.handleResize.bind(this)
                };
                window.addEventListener('pointermove', this.boundWindowHandlers.pointermove);
                window.addEventListener('pointerup', this.boundWindowHandlers.pointerup);
                window.addEventListener('pointercancel', this.boundWindowHandlers.pointercancel);
                window.addEventListener('keydown', this.boundWindowHandlers.keydown);
                window.addEventListener('keyup', this.boundWindowHandlers.keyup);
                window.addEventListener('beforeunload', this.boundWindowHandlers.beforeunload);
                window.addEventListener('resize', this.boundWindowHandlers.resize);
            },
            detachEvents: function () {
                if (this.pixiApp && this.boundCanvasHandlers) {
                    this.pixiApp.view.removeEventListener('pointerdown', this.boundCanvasHandlers.pointerdown);
                    this.pixiApp.view.removeEventListener('wheel', this.boundCanvasHandlers.wheel);
                }
                if (this.resizeObserver) {
                    this.resizeObserver.disconnect();
                    this.resizeObserver = null;
                }
                if (!this.boundWindowHandlers) {
                    return;
                }
                window.removeEventListener('pointermove', this.boundWindowHandlers.pointermove);
                window.removeEventListener('pointerup', this.boundWindowHandlers.pointerup);
                window.removeEventListener('pointercancel', this.boundWindowHandlers.pointercancel);
                window.removeEventListener('keydown', this.boundWindowHandlers.keydown);
                window.removeEventListener('keyup', this.boundWindowHandlers.keyup);
                window.removeEventListener('beforeunload', this.boundWindowHandlers.beforeunload);
                window.removeEventListener('resize', this.boundWindowHandlers.resize);
            },
            handleResize: function () {
                if (!this.pixiApp || !this.$refs.canvasHost) {
                    return;
                }
                var host = this.$refs.canvasHost;
                var width = Math.max(host.clientWidth, 320);
                var height = Math.max(host.clientHeight, 320);
                this.pixiApp.renderer.resize(width, height);
                this.markGridSceneDirty();
                this.markStaticSceneDirty();
                this.scheduleRender();
            },
            loadLevOptions: function () {
                var self = this;
                $.ajax({
                    url: baseUrl + '/basMap/getLevList',
                    method: 'GET',
                    headers: authHeaders(),
                    success: function (res) {
                        if (res && res.code === 200 && Array.isArray(res.data)) {
                            self.remoteLevOptions = res.data.map(function (item) {
                                return toInt(item, 0);
                            }).filter(function (item) {
                                return item > 0;
                            });
                            self.refreshLevOptions();
                        }
                    }
                });
            },
            refreshLevOptions: function () {
                var set = {};
                var result = [];
                var pushLev = function (lev) {
                    lev = toInt(lev, 0);
                    if (lev <= 0 || set[lev]) {
                        return;
                    }
                    set[lev] = true;
                    result.push(lev);
                };
                this.remoteLevOptions.forEach(pushLev);
                Object.keys(this.draftDocs || {}).forEach(pushLev);
                pushLev(this.currentLev);
                pushLev(this.floorPickerLev);
                result.sort(function (a, b) { return a - b; });
                this.levOptions = result;
            },
            exportDoc: function (doc) {
                var source = doc || this.doc || {};
                return {
                    lev: toInt(source.lev, 0),
                    editorMode: FREE_EDITOR_MODE,
                    canvasWidth: roundCoord(toNumber(source.canvasWidth, DEFAULT_CANVAS_WIDTH)),
                    canvasHeight: roundCoord(toNumber(source.canvasHeight, DEFAULT_CANVAS_HEIGHT)),
                    elements: (source.elements || []).map(function (item, index) {
                        return {
                            id: item && item.id ? String(item.id) : ('el_' + (index + 1)),
                            type: DRAW_TYPES.indexOf(item && item.type) >= 0 ? item.type : 'shelf',
                            x: roundCoord(Math.max(0, toNumber(item && item.x, 0))),
                            y: roundCoord(Math.max(0, toNumber(item && item.y, 0))),
                            width: roundCoord(Math.max(MIN_ELEMENT_SIZE, toNumber(item && item.width, MIN_ELEMENT_SIZE))),
                            height: roundCoord(Math.max(MIN_ELEMENT_SIZE, toNumber(item && item.height, MIN_ELEMENT_SIZE))),
                            value: normalizeValue(item && item.value)
                        };
                    })
                };
            },
            normalizeDoc: function (doc) {
                var normalized = this.exportDoc(doc || {});
                if (normalized.lev <= 0) {
                    normalized.lev = toInt(this.currentLev, 1) || 1;
                }
                return normalized;
            },
            snapshotDoc: function (doc) {
                return JSON.stringify(this.exportDoc(doc));
            },
            syncDirty: function () {
                var currentSnapshot = this.snapshotDoc(this.doc);
                this.isDirty = currentSnapshot !== this.savedSnapshot;
            },
            setDraftDocEntry: function (lev, doc, savedSnapshot) {
                lev = toInt(lev, 0);
                if (lev <= 0 || !doc) {
                    return;
                }
                var entry = {
                    doc: this.exportDoc(doc),
                    savedSnapshot: savedSnapshot != null ? savedSnapshot : ''
                };
                if (this.$set) {
                    this.$set(this.draftDocs, lev, entry);
                } else {
                    this.draftDocs[lev] = entry;
                }
            },
            removeDraftDocEntry: function (lev) {
                lev = toInt(lev, 0);
                if (lev <= 0) {
                    return;
                }
                if (this.$delete) {
                    this.$delete(this.draftDocs, lev);
                } else {
                    delete this.draftDocs[lev];
                }
            },
            cacheCurrentDraft: function () {
                if (!this.doc || !this.doc.lev) {
                    return;
                }
                this.setDraftDocEntry(this.doc.lev, this.doc, this.savedSnapshot);
                this.refreshLevOptions();
            },
            clearCurrentDraftIfSaved: function () {
                if (!this.doc || !this.doc.lev) {
                    return;
                }
                this.setDraftDocEntry(this.doc.lev, this.doc, this.savedSnapshot);
            },
            clearFloorTransientState: function () {
                this.clearDeferredStaticCommit();
                this.interactionState = null;
                this.currentPointerId = null;
                this.hoverElementId = '';
                this.pointerStatus = '--';
                this.lastPointerStatusUpdateTs = 0;
                this.selectedIds = [];
                this.isPanning = false;
                this.isZooming = false;
                this.pendingViewportRefresh = false;
                if (this.zoomRefreshTimer) {
                    window.clearTimeout(this.zoomRefreshTimer);
                    this.zoomRefreshTimer = null;
                }
                if (this.panRefreshTimer) {
                    window.clearTimeout(this.panRefreshTimer);
                    this.panRefreshTimer = null;
                }
            },
            resetRenderLayers: function () {
                if (this.gridLayer) {
                    this.gridLayer.clear();
                }
                if (this.trackLayer) {
                    this.trackLayer.clear();
                }
                if (this.nodeLayer) {
                    this.nodeLayer.clear();
                }
                if (this.eraseLayer) {
                    this.eraseLayer.clear();
                }
                if (this.patchObjectLayer) {
                    this.patchObjectLayer.clear();
                }
                if (this.activeLayer) {
                    this.activeLayer.clear();
                }
                if (this.selectionLayer) {
                    this.selectionLayer.clear();
                }
                if (this.guideLayer) {
                    this.guideLayer.clear();
                }
                if (this.hoverLayer) {
                    this.hoverLayer.clear();
                }
                if (this.guideText) {
                    this.guideText.visible = false;
                    this.guideText.text = '';
                }
                if (this.labelLayer) {
                    this.labelLayer.visible = false;
                }
                for (var i = 0; i < this.labelPool.length; i++) {
                    this.labelPool[i].visible = false;
                    this.labelPool[i].text = '';
                }
                this.hideUnusedStaticSprites(this.staticTrackSpritePool || [], 0);
                this.hideUnusedStaticSprites(this.staticNodeSpritePool || [], 0);
                if (this.staticTrackSpriteLayer) {
                    this.staticTrackSpriteLayer.removeChildren();
                    this.staticTrackSpritePool = [];
                }
                if (this.staticNodeSpriteLayer) {
                    this.staticNodeSpriteLayer.removeChildren();
                    this.staticNodeSpritePool = [];
                }
                if (this.staticTrackSpriteLayer) {
                    this.staticTrackSpriteLayer.visible = false;
                }
                if (this.staticNodeSpriteLayer) {
                    this.staticNodeSpriteLayer.visible = false;
                }
            },
            hasDirtyDraft: function (lev) {
                lev = toInt(lev, 0);
                if (lev <= 0) {
                    return false;
                }
                var entry = this.draftDocs[lev];
                if (!entry || !entry.doc) {
                    return false;
                }
                var snapshot = this.snapshotDoc(entry.doc);
                return snapshot !== (entry.savedSnapshot || '');
            },
            markStaticSceneDirty: function () {
                this.staticSceneDirty = true;
            },
            markGridSceneDirty: function () {
                this.gridSceneDirty = true;
            },
            clearRenderCaches: function () {
                this.gridRenderRect = null;
                this.gridRenderKey = '';
                this.staticRenderRect = null;
                this.staticRenderKey = '';
                this.staticExcludedKey = '';
            },
            scheduleZoomRefresh: function () {
                if (this.zoomRefreshTimer) {
                    window.clearTimeout(this.zoomRefreshTimer);
                }
                this.isZooming = true;
                this.zoomRefreshTimer = window.setTimeout(function () {
                    this.zoomRefreshTimer = null;
                    this.isZooming = false;
                    if (this.isPanning || (this.interactionState && this.interactionState.type === 'pan')) {
                        this.pendingViewportRefresh = true;
                        return;
                    }
                    this.markGridSceneDirty();
                    this.markStaticSceneDirty();
                    this.scheduleRender();
                }.bind(this), ZOOM_REFRESH_DELAY);
            },
            cancelPanRefresh: function () {
                if (this.panRefreshTimer) {
                    window.clearTimeout(this.panRefreshTimer);
                    this.panRefreshTimer = null;
                }
            },
            schedulePanRefresh: function () {
                this.cancelPanRefresh();
                this.isPanning = true;
                this.panRefreshTimer = window.setTimeout(function () {
                    this.panRefreshTimer = null;
                    this.isPanning = false;
                    if (this.pendingViewportRefresh) {
                        this.pendingViewportRefresh = false;
                        this.markGridSceneDirty();
                        this.markStaticSceneDirty();
                    }
                    this.scheduleRender();
                }.bind(this), PAN_LABEL_REFRESH_DELAY);
            },
            rebuildLabelCapability: function () {
                var maxWidth = 0;
                var maxHeight = 0;
                var elements = this.doc && this.doc.elements ? this.doc.elements : [];
                for (var i = 0; i < elements.length; i++) {
                    var element = elements[i];
                    if (element.width > maxWidth) {
                        maxWidth = element.width;
                    }
                    if (element.height > maxHeight) {
                        maxHeight = element.height;
                    }
                }
                this.labelCapability = {
                    maxWidth: maxWidth,
                    maxHeight: maxHeight
                };
                this.labelCapabilityDirty = false;
            },
            ensureLabelCapability: function () {
                if (this.labelCapabilityDirty) {
                    this.rebuildLabelCapability();
                }
                return this.labelCapability;
            },
            markSpatialIndexDirty: function () {
                this.spatialIndexDirty = true;
            },
            rebuildSpatialIndex: function () {
                var buckets = {};
                var elements = this.doc && this.doc.elements ? this.doc.elements : [];
                for (var i = 0; i < elements.length; i++) {
                    var element = elements[i];
                    var minX = Math.floor(element.x / SPATIAL_BUCKET_SIZE);
                    var maxX = Math.floor((element.x + element.width) / SPATIAL_BUCKET_SIZE);
                    var minY = Math.floor(element.y / SPATIAL_BUCKET_SIZE);
                    var maxY = Math.floor((element.y + element.height) / SPATIAL_BUCKET_SIZE);
                    for (var bx = minX; bx <= maxX; bx++) {
                        for (var by = minY; by <= maxY; by++) {
                            var key = bucketKey(bx, by);
                            if (!buckets[key]) {
                                buckets[key] = [];
                            }
                            buckets[key].push(element);
                        }
                    }
                }
                this.spatialBuckets = buckets;
                this.spatialIndexDirty = false;
            },
            ensureSpatialIndex: function () {
                if (this.spatialIndexDirty || !this.spatialBuckets) {
                    this.rebuildSpatialIndex();
                }
            },
            querySpatialCandidates: function (rect, padding, excludeIds) {
                if (!this.doc || !rect) {
                    return [];
                }
                this.ensureSpatialIndex();
                var excludeMap = {};
                excludeIds = excludeIds || [];
                for (var i = 0; i < excludeIds.length; i++) {
                    excludeMap[excludeIds[i]] = true;
                }
                var seen = {};
                var result = [];
                var pad = Math.max(0, padding || 0);
                var minX = Math.floor((rect.x - pad) / SPATIAL_BUCKET_SIZE);
                var maxX = Math.floor((rect.x + rect.width + pad) / SPATIAL_BUCKET_SIZE);
                var minY = Math.floor((rect.y - pad) / SPATIAL_BUCKET_SIZE);
                var maxY = Math.floor((rect.y + rect.height + pad) / SPATIAL_BUCKET_SIZE);
                for (var bx = minX; bx <= maxX; bx++) {
                    for (var by = minY; by <= maxY; by++) {
                        var key = bucketKey(bx, by);
                        var bucket = this.spatialBuckets[key];
                        if (!bucket || !bucket.length) {
                            continue;
                        }
                        for (var j = 0; j < bucket.length; j++) {
                            var element = bucket[j];
                            if (!element || seen[element.id] || excludeMap[element.id]) {
                                continue;
                            }
                            seen[element.id] = true;
                            result.push(element);
                        }
                    }
                }
                return result;
            },
            cancelDeferredStaticRebuild: function () {
                if (this.deferredStaticRebuildTimer) {
                    window.clearTimeout(this.deferredStaticRebuildTimer);
                    this.deferredStaticRebuildTimer = null;
                }
            },
            stageDeferredStaticCommit: function (ids, eraseRects) {
                this.pendingStaticCommit = {
                    ids: (ids || []).slice(),
                    eraseRects: (eraseRects || []).map(function (item) {
                        return {
                            x: item.x,
                            y: item.y,
                            width: item.width,
                            height: item.height
                        };
                    })
                };
            },
            clearDeferredStaticCommit: function () {
                this.cancelDeferredStaticRebuild();
                this.pendingStaticCommit = null;
            },
            scheduleDeferredStaticRebuild: function () {
                this.cancelDeferredStaticRebuild();
                this.deferredStaticRebuildTimer = window.setTimeout(function () {
                    this.deferredStaticRebuildTimer = null;
                    this.pendingStaticCommit = null;
                    this.markStaticSceneDirty();
                    this.scheduleRender();
                }.bind(this), DEFERRED_STATIC_REBUILD_DELAY);
            },
            selectionKey: function (ids) {
                return (ids || []).slice().sort().join('|');
            },
            setSelectedIds: function (ids, options) {
                options = options || {};
                var nextIds = (ids || []).filter(Boolean);
                this.selectedIds = nextIds.slice();
                if (options.refreshInspector !== false) {
                    this.refreshInspector();
                }
            },
            setCurrentDoc: function (doc, options) {
                options = options || {};
                var normalized = this.normalizeDoc(doc);
                this.clearFloorTransientState();
                this.resetRenderLayers();
                this.clearRenderCaches();
                this.doc = normalized;
                this.markSpatialIndexDirty();
                this.labelCapabilityDirty = true;
                this.pendingViewportRefresh = false;
                this.currentLev = normalized.lev;
                this.floorPickerLev = normalized.lev;
                this.switchingFloorLev = null;
                this.loadingFloor = false;
                this.syncFloorQueryParam(normalized.lev);
                this.markGridSceneDirty();
                this.markStaticSceneDirty();
                this.undoStack = [];
                this.redoStack = [];
                this.savedSnapshot = options.savedSnapshot != null ? options.savedSnapshot : this.snapshotDoc(normalized);
                this.syncDirty();
                this.refreshInspector();
                this.refreshLevOptions();
                this.$nextTick(function () {
                    this.fitContent();
                    this.scheduleRender();
                }.bind(this));
            },
            replaceDocFromSnapshot: function (snapshot) {
                if (!snapshot) {
                    return;
                }
                try {
                    this.clearFloorTransientState();
                    this.resetRenderLayers();
                    this.clearRenderCaches();
                    this.doc = this.normalizeDoc(JSON.parse(snapshot));
                    this.markSpatialIndexDirty();
                    this.labelCapabilityDirty = true;
                    this.pendingViewportRefresh = false;
                } catch (e) {
                    this.showMessage('error', '历史记录恢复失败');
                    return;
                }
                this.markGridSceneDirty();
                this.markStaticSceneDirty();
                this.floorPickerLev = this.doc.lev;
                this.currentLev = this.doc.lev;
                this.refreshInspector();
                this.syncDirty();
                this.cacheCurrentDraft();
                this.scheduleRender();
            },
            pushUndoSnapshot: function (snapshot) {
                if (!snapshot) {
                    return;
                }
                if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length - 1] === snapshot) {
                    return;
                }
                this.undoStack.push(snapshot);
                if (this.undoStack.length > HISTORY_LIMIT) {
                    this.undoStack.shift();
                }
            },
            commitMutation: function (beforeSnapshot, options) {
                options = options || {};
                var afterSnapshot = this.snapshotDoc(this.doc);
                if (beforeSnapshot === afterSnapshot) {
                    this.scheduleRender();
                    this.refreshInspector();
                    return false;
                }
                this.pushUndoSnapshot(beforeSnapshot);
                this.redoStack = [];
                this.markSpatialIndexDirty();
                this.labelCapabilityDirty = true;
                if (options.staticSceneDirty !== false) {
                    this.clearDeferredStaticCommit();
                    this.markStaticSceneDirty();
                }
                this.syncDirty();
                this.cacheCurrentDraft();
                this.refreshInspector();
                this.scheduleRender();
                return true;
            },
            runMutation: function (mutator) {
                if (!this.doc) {
                    return false;
                }
                var beforeSnapshot = this.snapshotDoc(this.doc);
                mutator();
                return this.commitMutation(beforeSnapshot);
            },
            undo: function () {
                if (this.undoStack.length === 0 || !this.doc) {
                    return;
                }
                var currentSnapshot = this.snapshotDoc(this.doc);
                var snapshot = this.undoStack.pop();
                this.redoStack.push(currentSnapshot);
                this.replaceDocFromSnapshot(snapshot);
            },
            redo: function () {
                if (this.redoStack.length === 0 || !this.doc) {
                    return;
                }
                var currentSnapshot = this.snapshotDoc(this.doc);
                var snapshot = this.redoStack.pop();
                this.pushUndoSnapshot(currentSnapshot);
                this.replaceDocFromSnapshot(snapshot);
            },
            createLocalBlankDoc: function (lev, width, height, savedSnapshot) {
                var doc = {
                    lev: toInt(lev, 1),
                    editorMode: FREE_EDITOR_MODE,
                    canvasWidth: Math.max(MIN_ELEMENT_SIZE * 4, toNumber(width, DEFAULT_CANVAS_WIDTH)),
                    canvasHeight: Math.max(MIN_ELEMENT_SIZE * 4, toNumber(height, DEFAULT_CANVAS_HEIGHT)),
                    elements: []
                };
                this.setCurrentDoc(doc, {
                    savedSnapshot: savedSnapshot != null ? savedSnapshot : ''
                });
                this.cacheCurrentDraft();
                this.syncDirty();
            },
            openBlankDialog: function () {
                var lev = this.currentLev || 1;
                this.blankForm = {
                    lev: String(lev),
                    width: String(Math.round(this.doc ? this.doc.canvasWidth : DEFAULT_CANVAS_WIDTH)),
                    height: String(Math.round(this.doc ? this.doc.canvasHeight : DEFAULT_CANVAS_HEIGHT))
                };
                this.blankDialogVisible = true;
            },
            createBlankMap: function () {
                var lev = toInt(this.blankForm.lev, 0);
                var width = toNumber(this.blankForm.width, DEFAULT_CANVAS_WIDTH);
                var height = toNumber(this.blankForm.height, DEFAULT_CANVAS_HEIGHT);
                if (lev <= 0) {
                    this.showMessage('warning', '楼层不能为空');
                    return;
                }
                if (width <= 0 || height <= 0) {
                    this.showMessage('warning', '画布尺寸必须大于 0');
                    return;
                }
                this.blankDialogVisible = false;
                this.createLocalBlankDoc(lev, width, height, '');
            },
            buildTransferPayload: function () {
                var doc = this.exportDoc(this.doc);
                return {
                    format: MAP_TRANSFER_FORMAT,
                    exportedAt: new Date().toISOString(),
                    source: {
                        lev: doc.lev,
                        editorMode: doc.editorMode
                    },
                    docs: [doc]
                };
            },
            buildTransferFilename: function (docs) {
                var levs = (docs || []).map(function (item) {
                    return toInt(item && item.lev, 0);
                }).filter(function (lev) {
                    return lev > 0;
                }).sort(function (a, b) {
                    return a - b;
                });
                var scope = levs.length <= 1
                    ? (String(levs[0] || (this.currentLev || 1)) + 'F')
                    : ('all-' + levs.length + '-floors');
                var now = new Date();
                return [
                    'bas-map',
                    scope,
                    now.getFullYear(),
                    padNumber(now.getMonth() + 1),
                    padNumber(now.getDate()),
                    padNumber(now.getHours()),
                    padNumber(now.getMinutes()),
                    padNumber(now.getSeconds())
                ].join('-') + '.json';
            },
            requestEditorDoc: function (lev) {
                return new Promise(function (resolve, reject) {
                    $.ajax({
                        url: baseUrl + '/basMap/editor/' + lev + '/auth',
                        method: 'GET',
                        headers: authHeaders(),
                        success: function (res) {
                            if (!res || res.code !== 200 || !res.data) {
                                reject(new Error((res && res.msg) ? res.msg : ('加载 ' + lev + 'F 地图失败')));
                                return;
                            }
                            resolve(res.data);
                        },
                        error: function () {
                            reject(new Error('加载 ' + lev + 'F 地图失败'));
                        }
                    });
                });
            },
            collectAllTransferDocs: function () {
                var self = this;
                var levMap = {};
                (this.remoteLevOptions || []).forEach(function (lev) {
                    lev = toInt(lev, 0);
                    if (lev > 0) {
                        levMap[lev] = true;
                    }
                });
                Object.keys(this.draftDocs || {}).forEach(function (key) {
                    var lev = toInt(key, 0);
                    if (lev > 0) {
                        levMap[lev] = true;
                    }
                });
                if (this.doc && this.doc.lev) {
                    levMap[toInt(this.doc.lev, 0)] = true;
                }
                var levs = Object.keys(levMap).map(function (key) {
                    return toInt(key, 0);
                }).filter(function (lev) {
                    return lev > 0;
                }).sort(function (a, b) {
                    return a - b;
                });
                if (!levs.length) {
                    return Promise.resolve([]);
                }
                return Promise.all(levs.map(function (lev) {
                    if (self.doc && self.doc.lev === lev) {
                        return Promise.resolve(self.exportDoc(self.doc));
                    }
                    if (self.draftDocs[lev] && self.draftDocs[lev].doc) {
                        return Promise.resolve(self.exportDoc(self.draftDocs[lev].doc));
                    }
                    return self.requestEditorDoc(lev).then(function (doc) {
                        return self.normalizeDoc(doc);
                    });
                }));
            },
            exportMapPackage: function () {
                var self = this;
                if (!this.doc && (!this.remoteLevOptions || !this.remoteLevOptions.length)) {
                    this.showMessage('warning', '当前没有可导出的地图');
                    return;
                }
                this.collectAllTransferDocs().then(function (docs) {
                    if (!docs || !docs.length) {
                        self.showMessage('warning', '当前没有可导出的地图');
                        return;
                    }
                    var payload = {
                        format: MAP_TRANSFER_FORMAT,
                        exportedAt: new Date().toISOString(),
                        source: {
                            lev: self.currentLev || (docs[0] && docs[0].lev) || 1,
                            editorMode: FREE_EDITOR_MODE
                        },
                        docs: docs.map(function (doc) {
                            return self.exportDoc(doc);
                        })
                    };
                    var blob = new Blob([JSON.stringify(payload, null, 2)], {
                        type: 'application/json;charset=utf-8'
                    });
                    var href = window.URL.createObjectURL(blob);
                    var link = document.createElement('a');
                    link.href = href;
                    link.download = self.buildTransferFilename(payload.docs);
                    document.body.appendChild(link);
                    link.click();
                    document.body.removeChild(link);
                    window.setTimeout(function () {
                        window.URL.revokeObjectURL(href);
                    }, 0);
                    self.showMessage('success', '已导出 ' + payload.docs.length + ' 个楼层的地图包');
                }).catch(function (error) {
                    self.showMessage('error', error && error.message ? error.message : '导出地图失败');
                });
            },
            triggerImportMap: function () {
                if (this.$refs.mapImportInput) {
                    this.$refs.mapImportInput.value = '';
                    this.$refs.mapImportInput.click();
                }
            },
            parseTransferPackage: function (raw) {
                if (!raw) {
                    return null;
                }
                if (raw.format === MAP_TRANSFER_FORMAT && Array.isArray(raw.docs) && raw.docs.length) {
                    return {
                        docs: raw.docs,
                        activeLev: toInt(raw.source && raw.source.lev, 0)
                    };
                }
                if ((raw.format === 'bas-map-editor-transfer-v1' || raw.format === MAP_TRANSFER_FORMAT) && raw.doc) {
                    return {
                        docs: [raw.doc],
                        activeLev: toInt(raw.source && raw.source.lev, 0)
                    };
                }
                if (raw.editorMode === FREE_EDITOR_MODE && Array.isArray(raw.elements)) {
                    return {
                        docs: [raw],
                        activeLev: toInt(raw.lev, 0)
                    };
                }
                return null;
            },
            importMapPackage: function (payload, options) {
                options = options || {};
                if (!payload || !Array.isArray(payload.docs) || !payload.docs.length) {
                    this.showMessage('error', '导入文件格式不正确');
                    return;
                }
                if (this.isDirty && options.skipConfirm !== true) {
                    if (!window.confirm('导入地图会替换当前编辑态未保存内容,是否继续?')) {
                        return;
                    }
                }
                if (this.doc) {
                    this.cacheCurrentDraft();
                }
                var self = this;
                var normalizedDocs = payload.docs.map(function (item) {
                    return self.normalizeDoc(item);
                }).sort(function (a, b) {
                    return toInt(a.lev, 0) - toInt(b.lev, 0);
                });
                normalizedDocs.forEach(function (doc) {
                    self.setDraftDocEntry(doc.lev, doc, '');
                });
                var activeLev = toInt(payload.activeLev, 0);
                var targetDoc = normalizedDocs[0];
                for (var i = 0; i < normalizedDocs.length; i++) {
                    if (normalizedDocs[i].lev === activeLev) {
                        targetDoc = normalizedDocs[i];
                        break;
                    }
                }
                this.refreshLevOptions();
                this.floorPickerLev = targetDoc.lev;
                this.setCurrentDoc(targetDoc, { savedSnapshot: '' });
                if (normalizedDocs.length > 1) {
                    this.showMessage('success', '地图包已导入 ' + normalizedDocs.length + ' 个楼层,可点击“保存全部楼层”落库');
                    return;
                }
                this.showMessage('success', '地图包已导入,保存后才会覆盖运行地图');
            },
            handleImportMap: function (event) {
                var file = event && event.target && event.target.files ? event.target.files[0] : null;
                if (!file) {
                    return;
                }
                var self = this;
                var reader = new FileReader();
                reader.onload = function (loadEvent) {
                    try {
                        var text = loadEvent && loadEvent.target ? loadEvent.target.result : '';
                        var raw = JSON.parse(text || '{}');
                        var payload = self.parseTransferPackage(raw);
                        self.importMapPackage(payload);
                    } catch (e) {
                        self.showMessage('error', '地图文件解析失败');
                    }
                };
                reader.onerror = function () {
                    self.showMessage('error', '地图文件读取失败');
                };
                reader.readAsText(file, 'utf-8');
            },
            triggerImportExcel: function () {
                if (this.$refs.importInput) {
                    this.$refs.importInput.value = '';
                    this.$refs.importInput.click();
                }
            },
            handleImportExcel: function (event) {
                var self = this;
                var file = event && event.target && event.target.files ? event.target.files[0] : null;
                if (!file) {
                    return;
                }
                var formData = new FormData();
                formData.append('file', file);
                $.ajax({
                    url: baseUrl + '/basMap/editor/importExcel/auth',
                    method: 'POST',
                    headers: authHeaders(),
                    data: formData,
                    processData: false,
                    contentType: false,
                    success: function (res) {
                        if (!res || res.code !== 200 || !Array.isArray(res.data) || res.data.length === 0) {
                            self.showMessage('error', (res && res.msg) ? res.msg : 'Excel 导入失败');
                            return;
                        }
                        res.data.forEach(function (item) {
                            var doc = self.normalizeDoc(item);
                            self.setDraftDocEntry(doc.lev, doc, '');
                        });
                        self.refreshLevOptions();
                        self.floorPickerLev = toInt(res.data[0].lev, 0);
                        self.setCurrentDoc(res.data[0], { savedSnapshot: '' });
                        self.showMessage('success', 'Excel 已导入到编辑器,保存后才会覆盖运行地图');
                    },
                    error: function () {
                        self.showMessage('error', 'Excel 导入失败');
                    }
                });
            },
            handleFloorChange: function (lev) {
                lev = toInt(lev, 0);
                if (lev <= 0) {
                    return;
                }
                this.floorPickerLev = lev;
                if (this.doc && this.doc.lev === lev && !this.loadingFloor) {
                    this.switchingFloorLev = null;
                    return;
                }
                if (this.doc) {
                    this.cacheCurrentDraft();
                }
                this.clearFloorTransientState();
                this.resetRenderLayers();
                this.switchingFloorLev = lev;
                this.markGridSceneDirty();
                this.markStaticSceneDirty();
                this.scheduleRender();
                this.fetchFloor(lev);
            },
            loadCurrentFloor: function () {
                if (!this.currentLev) {
                    this.showMessage('warning', '请先选择楼层');
                    return;
                }
                if (this.isDirty && !window.confirm('重新读取会丢弃当前楼层未保存的自由画布编辑,是否继续?')) {
                    return;
                }
                this.removeDraftDocEntry(this.currentLev);
                this.refreshLevOptions();
                this.fetchFloor(this.currentLev);
            },
            fetchFloor: function (lev) {
                var self = this;
                lev = toInt(lev, 0);
                if (lev <= 0) {
                    return;
                }
                var requestSeq = ++this.floorRequestSeq;
                this.activeFloorRequestSeq = requestSeq;
                this.loadingFloor = true;
                this.switchingFloorLev = lev;
                $.ajax({
                    url: baseUrl + '/basMap/editor/' + lev + '/auth',
                    method: 'GET',
                    headers: authHeaders(),
                    success: function (res) {
                        if (requestSeq !== self.activeFloorRequestSeq) {
                            return;
                        }
                        self.loadingFloor = false;
                        if (!res || res.code !== 200 || !res.data) {
                            self.switchingFloorLev = null;
                            self.floorPickerLev = self.currentLev;
                            self.markGridSceneDirty();
                            self.markStaticSceneDirty();
                            self.scheduleRender();
                            self.showMessage('error', (res && res.msg) ? res.msg : '加载地图失败');
                            return;
                        }
                        var normalized = self.normalizeDoc(res.data);
                        self.setDraftDocEntry(normalized.lev, normalized, self.snapshotDoc(normalized));
                        self.setCurrentDoc(normalized, {
                            savedSnapshot: self.snapshotDoc(normalized)
                        });
                    },
                    error: function () {
                        if (requestSeq !== self.activeFloorRequestSeq) {
                            return;
                        }
                        self.loadingFloor = false;
                        self.switchingFloorLev = null;
                        self.floorPickerLev = self.currentLev;
                        self.markGridSceneDirty();
                        self.markStaticSceneDirty();
                        self.scheduleRender();
                        self.showMessage('error', '加载地图失败');
                    }
                });
            },
            validateDocBeforeSave: function (doc) {
                var source = this.normalizeDoc(doc);
                if (!source || !source.lev) {
                    return '楼层不能为空';
                }
                if (toNumber(source.canvasWidth, 0) <= 0 || toNumber(source.canvasHeight, 0) <= 0) {
                    return '画布尺寸必须大于 0';
                }
                var elements = source.elements || [];
                for (var i = 0; i < elements.length; i++) {
                    var element = elements[i];
                    if (element.width <= 0 || element.height <= 0) {
                        return '存在尺寸无效的元素';
                    }
                    if (element.x < 0 || element.y < 0) {
                        return '元素坐标不能小于 0';
                    }
                    if (!isRectWithinCanvas(element, source.canvasWidth, source.canvasHeight)) {
                        return '存在超出画布边界的元素: ' + element.id;
                    }
                    if (element.type === 'devp') {
                        var value = safeParseJson(element.value);
                        if (!value || toInt(value.stationId, 0) <= 0 || toInt(value.deviceNo, 0) <= 0) {
                            return '输送线元素必须配置有效的 stationId 和 deviceNo';
                        }
                    }
                }
                var overlapId = findDocOverlapId(source);
                if (overlapId) {
                    return '存在重叠元素: ' + overlapId;
                }
                return '';
            },
            validateBeforeSave: function () {
                return this.validateDocBeforeSave(this.doc);
            },
            requestSaveDoc: function (doc) {
                return new Promise(function (resolve, reject) {
                    $.ajax({
                        url: baseUrl + '/basMap/editor/save/auth',
                        method: 'POST',
                        headers: $.extend({
                            'Content-Type': 'application/json;charset=UTF-8'
                        }, authHeaders()),
                        data: JSON.stringify(doc),
                        success: function (res) {
                            if (!res || res.code !== 200) {
                                reject(new Error((res && res.msg) ? res.msg : '保存失败'));
                                return;
                            }
                            resolve(res);
                        },
                        error: function () {
                            reject(new Error('保存失败'));
                        }
                    });
                });
            },
            collectDirtyDocsForSave: function () {
                var result = [];
                var seen = {};
                if (this.doc && this.doc.lev && this.isDirty) {
                    var currentDoc = this.exportDoc(this.doc);
                    result.push(currentDoc);
                    seen[currentDoc.lev] = true;
                }
                var self = this;
                Object.keys(this.draftDocs || {}).forEach(function (key) {
                    var lev = toInt(key, 0);
                    if (lev <= 0 || seen[lev]) {
                        return;
                    }
                    var entry = self.draftDocs[lev];
                    if (!entry || !entry.doc) {
                        return;
                    }
                    var snapshot = self.snapshotDoc(entry.doc);
                    if (snapshot === (entry.savedSnapshot || '')) {
                        return;
                    }
                    var doc = self.exportDoc(entry.doc);
                    result.push(doc);
                    seen[doc.lev] = true;
                });
                result.sort(function (a, b) {
                    return toInt(a.lev, 0) - toInt(b.lev, 0);
                });
                return result;
            },
            markDocSavedState: function (doc) {
                var normalized = this.normalizeDoc(doc);
                var savedSnapshot = this.snapshotDoc(normalized);
                this.setDraftDocEntry(normalized.lev, normalized, savedSnapshot);
                if (this.doc && this.doc.lev === normalized.lev) {
                    this.savedSnapshot = savedSnapshot;
                    this.syncDirty();
                }
            },
            saveDoc: function () {
                var self = this;
                if (!this.doc) {
                    return;
                }
                var error = this.validateBeforeSave();
                if (error) {
                    this.showMessage('warning', error);
                    return;
                }
                this.saving = true;
                var payload = this.exportDoc(this.doc);
                this.requestSaveDoc(payload).then(function () {
                    self.saving = false;
                        self.savedSnapshot = self.snapshotDoc(self.doc);
                        self.syncDirty();
                        self.clearCurrentDraftIfSaved();
                        self.refreshLevOptions();
                    self.showMessage('success', '当前楼层已保存并编译到运行地图');
                }).catch(function (error) {
                        self.saving = false;
                    self.showMessage('error', error && error.message ? error.message : '保存失败');
                });
            },
            saveAllDocs: function () {
                var self = this;
                if (this.saving || this.savingAll) {
                    return;
                }
                var docs = this.collectDirtyDocsForSave();
                if (!docs.length) {
                    this.showMessage('warning', '当前没有需要保存的楼层');
                    return;
                }
                if (docs.length > 1 && !window.confirm('将保存 ' + docs.length + ' 个楼层到运行地图,是否继续?')) {
                    return;
                }
                for (var i = 0; i < docs.length; i++) {
                    var error = this.validateDocBeforeSave(docs[i]);
                    if (error) {
                        this.showMessage('warning', docs[i].lev + 'F 保存前校验失败: ' + error);
                        return;
                    }
                }
                this.savingAll = true;
                var index = 0;
                var total = docs.length;
                var next = function () {
                    if (index >= total) {
                        self.savingAll = false;
                        self.refreshLevOptions();
                        self.showMessage('success', '已保存 ' + total + ' 个楼层到运行地图');
                        return;
                    }
                    var doc = docs[index++];
                    self.requestSaveDoc(doc).then(function () {
                        self.markDocSavedState(doc);
                        next();
                    }).catch(function (error) {
                        self.savingAll = false;
                        self.showMessage('error', doc.lev + 'F 保存失败: ' + (error && error.message ? error.message : '保存失败'));
                    });
                };
                next();
            },
            setTool: function (tool) {
                this.activeTool = tool;
                this.updateCursor();
            },
            findElementById: function (id) {
                if (!this.doc || !id) {
                    return null;
                }
                var elements = this.doc.elements || [];
                for (var i = 0; i < elements.length; i++) {
                    if (elements[i].id === id) {
                        return elements[i];
                    }
                }
                return null;
            },
            getSelectedElements: function () {
                var self = this;
                return this.selectedIds.map(function (id) {
                    return self.findElementById(id);
                }).filter(Boolean);
            },
            refreshInspector: function () {
                var element = this.singleSelectedElement;
                if (!this.doc) {
                    this.canvasForm = {
                        width: String(DEFAULT_CANVAS_WIDTH),
                        height: String(DEFAULT_CANVAS_HEIGHT)
                    };
                    this.valueEditorText = '';
                    this.resetDevpForm();
                    this.resetDeviceForm();
                    return;
                }
                this.canvasForm = {
                    width: String(Math.round(this.doc.canvasWidth)),
                    height: String(Math.round(this.doc.canvasHeight))
                };
                if (!element) {
                    this.geometryForm = { x: '', y: '', width: '', height: '' };
                    this.valueEditorText = '';
                    this.resetDevpForm();
                    this.resetDeviceForm();
                    return;
                }
                this.geometryForm = {
                    x: String(this.formatNumber(element.x)),
                    y: String(this.formatNumber(element.y)),
                    width: String(this.formatNumber(element.width)),
                    height: String(this.formatNumber(element.height))
                };
                this.valueEditorText = element.value || '';
                if (element.type === 'devp') {
                    this.loadDevpForm(element.value);
                } else {
                    this.resetDevpForm();
                }
                if (isDeviceConfigType(element.type)) {
                    this.loadDeviceForm(element.type, element.value);
                } else {
                    this.resetDeviceForm();
                }
                this.ensureShelfFillStartValue();
            },
            resetDevpForm: function () {
                this.devpForm = {
                    stationId: '',
                    deviceNo: '',
                    direction: [],
                    isBarcodeStation: false,
                    barcodeIdx: '',
                    backStation: '',
                    backStationDeviceNo: '',
                    isInStation: false,
                    barcodeStation: '',
                    barcodeStationDeviceNo: '',
                    isOutStation: false,
                    runBlockReassign: false,
                    isOutOrder: false,
                    isLiftTransfer: false
                };
            },
            resetDeviceForm: function () {
                this.deviceForm = {
                    valueKey: '',
                    deviceNo: ''
                };
            },
            ensureShelfFillStartValue: function () {
                var element = this.singleSelectedElement;
                if (!element || element.type !== 'shelf') {
                    return;
                }
                if (!this.shelfFillForm.startValue || !parseShelfLocationValue(this.shelfFillForm.startValue)) {
                    this.shelfFillForm.startValue = normalizeValue(element.value || '');
                }
            },
            loadDevpForm: function (value) {
                this.resetDevpForm();
                var json = safeParseJson(value);
                if (!json) {
                    return;
                }
                this.devpForm.stationId = json.stationId != null ? String(json.stationId) : '';
                this.devpForm.deviceNo = json.deviceNo != null ? String(json.deviceNo) : '';
                this.devpForm.direction = normalizeDirectionList(json.direction);
                this.devpForm.isBarcodeStation = boolFlag(json.isBarcodeStation);
                this.devpForm.barcodeIdx = json.barcodeIdx != null ? String(json.barcodeIdx) : '';
                this.devpForm.backStation = json.backStation != null ? String(json.backStation) : '';
                this.devpForm.backStationDeviceNo = json.backStationDeviceNo != null ? String(json.backStationDeviceNo) : '';
                this.devpForm.isInStation = boolFlag(json.isInStation);
                this.devpForm.barcodeStation = json.barcodeStation != null ? String(json.barcodeStation) : '';
                this.devpForm.barcodeStationDeviceNo = json.barcodeStationDeviceNo != null ? String(json.barcodeStationDeviceNo) : '';
                this.devpForm.isOutStation = boolFlag(json.isOutStation);
                this.devpForm.runBlockReassign = boolFlag(json.runBlockReassign);
                this.devpForm.isOutOrder = boolFlag(json.isOutOrder);
                this.devpForm.isLiftTransfer = boolFlag(json.isLiftTransfer);
            },
            getDeviceConfigLabel: function (type) {
                var meta = getTypeMeta(type);
                return meta.label + '参数';
            },
            getDeviceConfigKeyLabel: function (type, valueKey) {
                if (valueKey === 'crnNo') {
                    return 'crnNo';
                }
                if (valueKey === 'rgvNo') {
                    return 'rgvNo';
                }
                return type === 'rgv' ? 'deviceNo / rgvNo' : 'deviceNo / crnNo';
            },
            loadDeviceForm: function (type, value) {
                this.resetDeviceForm();
                if (!isDeviceConfigType(type)) {
                    return;
                }
                var json = safeParseJson(value);
                var valueKey = pickDeviceValueKey(type, json);
                var deviceNo = '';
                if (json && json[valueKey] != null) {
                    deviceNo = String(json[valueKey]);
                }
                this.deviceForm = {
                    valueKey: valueKey,
                    deviceNo: deviceNo
                };
            },
            isDevpDirectionActive: function (directionKey) {
                return this.devpForm.direction.indexOf(directionKey) >= 0;
            },
            toggleDevpDirection: function (directionKey) {
                if (!directionKey) {
                    return;
                }
                var next = this.devpForm.direction.slice();
                var index = next.indexOf(directionKey);
                if (index >= 0) {
                    next.splice(index, 1);
                } else {
                    next.push(directionKey);
                }
                this.devpForm.direction = DEVP_DIRECTION_OPTIONS.map(function (item) {
                    return item.key;
                }).filter(function (item) {
                    return next.indexOf(item) >= 0;
                });
            },
            applyCanvasSize: function () {
                var self = this;
                if (!this.doc) {
                    return;
                }
                var width = toNumber(this.canvasForm.width, 0);
                var height = toNumber(this.canvasForm.height, 0);
                if (width <= 0 || height <= 0) {
                    this.showMessage('warning', '画布尺寸必须大于 0');
                    return;
                }
                var bounds = this.getElementBounds((this.doc.elements || []).map(function (item) {
                    return item.id;
                }));
                if (bounds && (width < bounds.x + bounds.width || height < bounds.y + bounds.height)) {
                    this.showMessage('warning', '画布不能小于当前元素占用范围');
                    return;
                }
                this.runMutation(function () {
                    self.doc.canvasWidth = roundCoord(width);
                    self.doc.canvasHeight = roundCoord(height);
                });
            },
            applyGeometry: function () {
                var self = this;
                var element = this.singleSelectedElement;
                if (!element) {
                    return;
                }
                var next = {
                    x: roundCoord(Math.max(0, toNumber(this.geometryForm.x, element.x))),
                    y: roundCoord(Math.max(0, toNumber(this.geometryForm.y, element.y))),
                    width: roundCoord(Math.max(MIN_ELEMENT_SIZE, toNumber(this.geometryForm.width, element.width))),
                    height: roundCoord(Math.max(MIN_ELEMENT_SIZE, toNumber(this.geometryForm.height, element.height)))
                };
                if (!this.isWithinCanvas(next)) {
                    this.showMessage('warning', '几何属性超出当前画布范围');
                    return;
                }
                var preview = deepClone(element);
                preview.x = next.x;
                preview.y = next.y;
                preview.width = next.width;
                preview.height = next.height;
                if (this.hasOverlap(preview, [preview.id])) {
                    this.showMessage('warning', '调整后会与其他元素重叠');
                    return;
                }
                this.runMutation(function () {
                    element.x = next.x;
                    element.y = next.y;
                    element.width = next.width;
                    element.height = next.height;
                });
            },
            applyRawValue: function () {
                var self = this;
                var element = this.singleSelectedElement;
                if (!element || element.type === 'devp') {
                    return;
                }
                this.runMutation(function () {
                    element.value = normalizeValue(self.valueEditorText);
                });
            },
            applyDeviceForm: function () {
                var self = this;
                var element = this.singleSelectedDeviceElement;
                if (!element) {
                    return;
                }
                var deviceNo = toInt(this.deviceForm.deviceNo, 0);
                if (deviceNo <= 0) {
                    this.showMessage('warning', '设备编号必须大于 0');
                    return;
                }
                var valueKey = this.deviceForm.valueKey || pickDeviceValueKey(element.type, safeParseJson(element.value));
                this.runMutation(function () {
                    var payload = safeParseJson(element.value) || {};
                    delete payload.deviceNo;
                    delete payload.crnNo;
                    delete payload.rgvNo;
                    payload[valueKey] = deviceNo;
                    element.value = JSON.stringify(payload);
                    self.valueEditorText = element.value;
                });
            },
            applyDevpForm: function () {
                var self = this;
                var element = this.singleSelectedElement;
                if (!element || element.type !== 'devp') {
                    return;
                }
                var stationId = toInt(this.devpForm.stationId, 0);
                var deviceNo = toInt(this.devpForm.deviceNo, 0);
                if (stationId <= 0 || deviceNo <= 0) {
                    this.showMessage('warning', '站号和 PLC 编号必须大于 0');
                    return;
                }
                var payload = {
                    stationId: stationId,
                    deviceNo: deviceNo
                };
                var directionList = normalizeDirectionList(this.devpForm.direction);
                if (directionList.length > 0) {
                    payload.direction = directionList;
                }
                var barcodeIdx = this.devpForm.barcodeIdx === '' ? 0 : toInt(this.devpForm.barcodeIdx, 0);
                var backStation = this.devpForm.backStation === '' ? 0 : toInt(this.devpForm.backStation, 0);
                var backStationDeviceNo = this.devpForm.backStationDeviceNo === '' ? 0 : toInt(this.devpForm.backStationDeviceNo, 0);
                var barcodeStation = this.devpForm.barcodeStation === '' ? 0 : toInt(this.devpForm.barcodeStation, 0);
                var barcodeStationDeviceNo = this.devpForm.barcodeStationDeviceNo === '' ? 0 : toInt(this.devpForm.barcodeStationDeviceNo, 0);
                if (this.devpForm.isInStation && (barcodeStation <= 0 || barcodeStationDeviceNo <= 0)) {
                    this.showMessage('warning', '入站点必须填写条码站和条码站 PLC 编号');
                    return;
                }
                if (this.devpForm.isBarcodeStation && (backStation <= 0 || backStationDeviceNo <= 0 || barcodeIdx <= 0)) {
                    this.showMessage('warning', '条码站必须填写条码索引、退回站和退回站 PLC 编号');
                    return;
                }
                if (this.devpForm.isBarcodeStation) {
                    payload.isBarcodeStation = 1;
                }
                if (barcodeIdx > 0) {
                    payload.barcodeIdx = barcodeIdx;
                }
                if (backStation > 0) {
                    payload.backStation = backStation;
                }
                if (backStationDeviceNo > 0) {
                    payload.backStationDeviceNo = backStationDeviceNo;
                }
                if (this.devpForm.isInStation) {
                    payload.isInStation = 1;
                }
                if (barcodeStation > 0) {
                    payload.barcodeStation = barcodeStation;
                }
                if (barcodeStationDeviceNo > 0) {
                    payload.barcodeStationDeviceNo = barcodeStationDeviceNo;
                }
                if (this.devpForm.isOutStation) {
                    payload.isOutStation = 1;
                }
                if (this.devpForm.runBlockReassign) {
                    payload.runBlockReassign = 1;
                }
                if (this.devpForm.isOutOrder) {
                    payload.isOutOrder = 1;
                }
                if (this.devpForm.isLiftTransfer) {
                    payload.isLiftTransfer = 1;
                }
                this.runMutation(function () {
                    element.value = JSON.stringify(payload);
                    self.valueEditorText = element.value;
                });
            },
            deleteSelection: function () {
                var self = this;
                if (!this.doc || this.selectedIds.length === 0) {
                    return;
                }
                var ids = this.selectedIds.slice();
                this.runMutation(function () {
                    self.doc.elements = self.doc.elements.filter(function (item) {
                        return ids.indexOf(item.id) === -1;
                    });
                    self.selectedIds = [];
                });
            },
            copySelection: function () {
                var elements = this.getSelectedElements();
                if (!elements.length) {
                    return;
                }
                this.clipboard = deepClone(elements);
                this.showMessage('success', '已复制 ' + elements.length + ' 个元素');
            },
            getElementListBounds: function (elements) {
                if (!elements || !elements.length) {
                    return null;
                }
                var minX = elements[0].x;
                var minY = elements[0].y;
                var maxX = elements[0].x + elements[0].width;
                var maxY = elements[0].y + elements[0].height;
                for (var i = 1; i < elements.length; i++) {
                    var element = elements[i];
                    minX = Math.min(minX, element.x);
                    minY = Math.min(minY, element.y);
                    maxX = Math.max(maxX, element.x + element.width);
                    maxY = Math.max(maxY, element.y + element.height);
                }
                return {
                    x: minX,
                    y: minY,
                    width: maxX - minX,
                    height: maxY - minY
                };
            },
            getPasteTargetWorld: function () {
                if (!this.doc) {
                    return { x: 0, y: 0 };
                }
                var visible = this.getVisibleCanvasRect ? this.getVisibleCanvasRect() : this.getVisibleWorldRect();
                var fallback = {
                    x: visible.x + visible.width / 2,
                    y: visible.y + visible.height / 2
                };
                if (!this.lastPointerWorld) {
                    return fallback;
                }
                return {
                    x: clamp(this.lastPointerWorld.x, 0, this.doc.canvasWidth),
                    y: clamp(this.lastPointerWorld.y, 0, this.doc.canvasHeight),
                    screenX: this.lastPointerWorld.screenX,
                    screenY: this.lastPointerWorld.screenY
                };
            },
            pasteClipboard: function () {
                var self = this;
                if (!this.doc || !this.clipboard.length) {
                    return;
                }
                var sourceBounds = this.getElementListBounds(this.clipboard);
                if (!sourceBounds) {
                    return;
                }
                var target = this.getPasteTargetWorld();
                var offsetX = target.x - (sourceBounds.x + sourceBounds.width / 2);
                var offsetY = target.y - (sourceBounds.y + sourceBounds.height / 2);
                var minOffsetX = -sourceBounds.x;
                var maxOffsetX = this.doc.canvasWidth - (sourceBounds.x + sourceBounds.width);
                var minOffsetY = -sourceBounds.y;
                var maxOffsetY = this.doc.canvasHeight - (sourceBounds.y + sourceBounds.height);
                offsetX = clamp(offsetX, minOffsetX, maxOffsetX);
                offsetY = clamp(offsetY, minOffsetY, maxOffsetY);
                var copies = deepClone(this.clipboard).map(function (item) {
                    item.id = nextId();
                    item.x = roundCoord(item.x + offsetX);
                    item.y = roundCoord(item.y + offsetY);
                    return item;
                });
                if (!this.canPlaceElements(copies, [])) {
                    this.showMessage('warning', '粘贴后的元素与现有元素重叠或超出画布');
                    return;
                }
                this.runMutation(function () {
                    self.doc.elements = self.doc.elements.concat(copies);
                    self.selectedIds = copies.map(function (item) { return item.id; });
                });
            },
            canArrayFromElement: function (element) {
                return !!(element && ARRAY_TEMPLATE_TYPES.indexOf(element.type) >= 0);
            },
            getShelfFillSteps: function () {
                return {
                    row: this.shelfFillForm.rowStep === 'asc' ? 1 : -1,
                    col: this.shelfFillForm.colStep === 'desc' ? -1 : 1
                };
            },
            applyShelfSequenceToArrayCopies: function (template, copies) {
                if (!template || template.type !== 'shelf' || !copies || !copies.length) {
                    return copies;
                }
                var base = parseShelfLocationValue(template.value) || parseShelfLocationValue(this.shelfFillForm.startValue);
                if (!base) {
                    return copies;
                }
                var steps = this.getShelfFillSteps();
                var horizontal = Math.abs(copies[0].x - template.x) >= Math.abs(copies[0].y - template.y);
                var direction = 1;
                if (horizontal) {
                    direction = copies[0].x >= template.x ? 1 : -1;
                } else {
                    direction = copies[0].y >= template.y ? 1 : -1;
                }
                for (var i = 0; i < copies.length; i++) {
                    var offset = i + 1;
                    var row = base.row;
                    var col = base.col;
                    if (horizontal) {
                        col = base.col + steps.col * direction * offset;
                    } else {
                        row = base.row + steps.row * direction * offset;
                    }
                    copies[i].value = formatShelfLocationValue(row, col);
                }
                return copies;
            },
            buildShelfGridAssignments: function (elements) {
                if (!elements || !elements.length) {
                    return null;
                }
                var clusterAxis = function (list, axis, sizeKey) {
                    var sorted = list.map(function (item) {
                        return {
                            id: item.id,
                            center: item[axis] + item[sizeKey] / 2,
                            size: item[sizeKey]
                        };
                    }).sort(function (a, b) {
                        return a.center - b.center;
                    });
                    var avgSize = sorted.reduce(function (sum, item) {
                        return sum + item.size;
                    }, 0) / sorted.length;
                    var tolerance = Math.max(6, avgSize * 0.45);
                    var groups = [];
                    for (var i = 0; i < sorted.length; i++) {
                        var current = sorted[i];
                        var last = groups.length ? groups[groups.length - 1] : null;
                        if (!last || Math.abs(current.center - last.center) > tolerance) {
                            groups.push({
                                center: current.center,
                                items: [current]
                            });
                        } else {
                            last.items.push(current);
                            last.center = last.items.reduce(function (sum, item) {
                                return sum + item.center;
                            }, 0) / last.items.length;
                        }
                    }
                    var indexById = {};
                    for (var groupIndex = 0; groupIndex < groups.length; groupIndex++) {
                        for (var itemIndex = 0; itemIndex < groups[groupIndex].items.length; itemIndex++) {
                            indexById[groups[groupIndex].items[itemIndex].id] = groupIndex;
                        }
                    }
                    return indexById;
                };
                return {
                    rowById: clusterAxis(elements, 'y', 'height'),
                    colById: clusterAxis(elements, 'x', 'width')
                };
            },
            applyShelfAutoFill: function () {
                var self = this;
                var shelves = this.selectedShelfElements.slice();
                if (!shelves.length) {
                    this.showMessage('warning', '请先选中至少一个货架');
                    return;
                }
                var start = parseShelfLocationValue(this.shelfFillForm.startValue);
                if (!start) {
                    this.showMessage('warning', '起始值格式必须是 排-列,例如 12-1');
                    return;
                }
                var grid = this.buildShelfGridAssignments(shelves);
                if (!grid) {
                    return;
                }
                var steps = this.getShelfFillSteps();
                this.runMutation(function () {
                    shelves.forEach(function (item) {
                        var rowIndex = grid.rowById[item.id] || 0;
                        var colIndex = grid.colById[item.id] || 0;
                        item.value = formatShelfLocationValue(
                            start.row + rowIndex * steps.row,
                            start.col + colIndex * steps.col
                        );
                    });
                    if (self.singleSelectedElement && self.singleSelectedElement.type === 'shelf') {
                        self.valueEditorText = self.singleSelectedElement.value || '';
                    }
                });
            },
            buildArrayCopies: function (template, startWorld, currentWorld) {
                if (!this.doc || !template || !startWorld || !currentWorld || !this.canArrayFromElement(template)) {
                    return [];
                }
                var deltaX = currentWorld.x - startWorld.x;
                var deltaY = currentWorld.y - startWorld.y;
                if (Math.abs(deltaX) < COORD_EPSILON && Math.abs(deltaY) < COORD_EPSILON) {
                    return [];
                }
                var horizontal = Math.abs(deltaX) >= Math.abs(deltaY);
                var step = horizontal ? template.width : template.height;
                if (step <= COORD_EPSILON) {
                    return [];
                }
                var direction = (horizontal ? deltaX : deltaY) >= 0 ? 1 : -1;
                var distance;
                if (horizontal) {
                    distance = direction > 0
                        ? currentWorld.x - (template.x + template.width)
                        : template.x - currentWorld.x;
                } else {
                    distance = direction > 0
                        ? currentWorld.y - (template.y + template.height)
                        : template.y - currentWorld.y;
                }
                var count = Math.max(0, Math.floor((distance + step * 0.5) / step));
                if (count <= 0) {
                    return [];
                }
                var copies = [];
                for (var i = 1; i <= count; i++) {
                    copies.push({
                        type: template.type,
                        x: roundCoord(template.x + (horizontal ? direction * template.width * i : 0)),
                        y: roundCoord(template.y + (horizontal ? 0 : direction * template.height * i)),
                        width: template.width,
                        height: template.height,
                        value: template.value
                    });
                }
                return this.applyShelfSequenceToArrayCopies(template, copies);
            },
            duplicateSelection: function () {
                this.copySelection();
                this.pasteClipboard();
            },
            getElementBounds: function (ids) {
                if (!this.doc) {
                    return null;
                }
                var elements = ids && ids.length ? this.getSelectedElements() : (this.doc.elements || []);
                if (ids && ids.length) {
                    elements = ids.map(function (id) {
                        return this.findElementById(id);
                    }, this).filter(Boolean);
                }
                if (!elements.length) {
                    return null;
                }
                var minX = elements[0].x;
                var minY = elements[0].y;
                var maxX = elements[0].x + elements[0].width;
                var maxY = elements[0].y + elements[0].height;
                for (var i = 1; i < elements.length; i++) {
                    var element = elements[i];
                    minX = Math.min(minX, element.x);
                    minY = Math.min(minY, element.y);
                    maxX = Math.max(maxX, element.x + element.width);
                    maxY = Math.max(maxY, element.y + element.height);
                }
                return {
                    x: minX,
                    y: minY,
                    width: maxX - minX,
                    height: maxY - minY
                };
            },
            fitContent: function () {
                if (!this.doc || !this.pixiApp) {
                    return;
                }
                var contentBounds = this.getElementBounds();
                if (contentBounds && contentBounds.width > 0 && contentBounds.height > 0) {
                    this.fitRect(contentBounds, this.pixiApp.renderer.width, this.pixiApp.renderer.height);
                    return;
                }
                this.fitCanvas();
            },
            fitCanvas: function () {
                if (!this.doc || !this.pixiApp) {
                    return;
                }
                var renderer = this.pixiApp.renderer;
                var target = {
                    x: 0,
                    y: 0,
                    width: Math.max(1, this.doc.canvasWidth),
                    height: Math.max(1, this.doc.canvasHeight)
                };
                this.fitRect(target, renderer.width, renderer.height);
            },
            fitSelection: function () {
                if (!this.selectedIds.length || !this.pixiApp) {
                    return;
                }
                var bounds = this.getElementBounds(this.selectedIds);
                if (!bounds) {
                    return;
                }
                this.fitRect(bounds, this.pixiApp.renderer.width, this.pixiApp.renderer.height);
            },
            fitRect: function (rect, viewportWidth, viewportHeight) {
                var padding = 80;
                var scale = Math.min(
                    (viewportWidth - padding * 2) / Math.max(rect.width, 1),
                    (viewportHeight - padding * 2) / Math.max(rect.height, 1)
                );
                scale = clamp(scale, 0.06, 4);
                this.camera.scale = scale;
                this.camera.x = Math.round((viewportWidth - rect.width * scale) / 2 - rect.x * scale);
                this.camera.y = Math.round((viewportHeight - rect.height * scale) / 2 - rect.y * scale);
                this.viewZoom = scale;
                this.markGridSceneDirty();
                this.markStaticSceneDirty();
                this.scheduleRender();
            },
            resetView: function () {
                this.fitCanvas();
            },
            getVisibleWorldRect: function () {
                if (!this.pixiApp) {
                    return {
                        x: 0,
                        y: 0,
                        width: 0,
                        height: 0
                    };
                }
                return {
                    x: (-this.camera.x) / this.camera.scale,
                    y: (-this.camera.y) / this.camera.scale,
                    width: this.pixiApp.renderer.width / this.camera.scale,
                    height: this.pixiApp.renderer.height / this.camera.scale
                };
            },
            getVisibleCanvasRect: function () {
                if (!this.doc) {
                    return {
                        x: 0,
                        y: 0,
                        width: 0,
                        height: 0
                    };
                }
                var visible = this.getVisibleWorldRect();
                var left = clamp(visible.x, 0, this.doc.canvasWidth);
                var top = clamp(visible.y, 0, this.doc.canvasHeight);
                var right = clamp(visible.x + visible.width, 0, this.doc.canvasWidth);
                var bottom = clamp(visible.y + visible.height, 0, this.doc.canvasHeight);
                return {
                    x: left,
                    y: top,
                    width: Math.max(0, right - left),
                    height: Math.max(0, bottom - top)
                };
            },
            getWorldRectWithPadding: function (screenPadding) {
                if (!this.doc) {
                    return {
                        x: 0,
                        y: 0,
                        width: 0,
                        height: 0
                    };
                }
                var visible = this.getVisibleWorldRect();
                var padding = Math.max(screenPadding / this.camera.scale, 24);
                var left = Math.max(0, visible.x - padding);
                var top = Math.max(0, visible.y - padding);
                var right = Math.min(this.doc.canvasWidth, visible.x + visible.width + padding);
                var bottom = Math.min(this.doc.canvasHeight, visible.y + visible.height + padding);
                return {
                    x: left,
                    y: top,
                    width: Math.max(0, right - left),
                    height: Math.max(0, bottom - top)
                };
            },
            worldRectContains: function (outer, inner) {
                if (!outer || !inner) {
                    return false;
                }
                return inner.x >= outer.x - COORD_EPSILON
                    && inner.y >= outer.y - COORD_EPSILON
                    && inner.x + inner.width <= outer.x + outer.width + COORD_EPSILON
                    && inner.y + inner.height <= outer.y + outer.height + COORD_EPSILON;
            },
            getGridRenderKey: function () {
                var minorStep = this.camera.scale > 1.5 ? 50 : (this.camera.scale > 0.45 ? 100 : 200);
                return minorStep + '|' + (Math.round(this.camera.scale * 8) / 8);
            },
            getStaticRenderKey: function () {
                return (this.camera.scale >= 0.85 ? 'round' : 'flat') + '|' + (Math.round(this.camera.scale * 8) / 8);
            },
            scheduleRender: function () {
                if (this.renderQueued) {
                    return;
                }
                this.renderQueued = true;
                window.requestAnimationFrame(function () {
                    this.renderQueued = false;
                    this.renderScene();
                }.bind(this));
            },
            renderScene: function () {
                if (!this.pixiApp || !this.doc) {
                    return;
                }
                this.mapRoot.position.set(this.camera.x, this.camera.y);
                this.mapRoot.scale.set(this.camera.scale, this.camera.scale);
                this.viewZoom = this.camera.scale;
                var visible = this.getVisibleCanvasRect();
                var viewportSettled = !this.isZooming && !this.isPanning && !(this.interactionState && this.interactionState.type === 'pan');
                var gridKeyChanged = this.gridRenderKey !== this.getGridRenderKey();
                if (this.gridSceneDirty || !this.gridRenderRect || (viewportSettled && gridKeyChanged) || (viewportSettled && !this.worldRectContains(this.gridRenderRect, visible))) {
                    this.renderGrid(this.getWorldRectWithPadding(STATIC_VIEW_PADDING));
                    this.gridSceneDirty = false;
                }
                var excludedKey = this.selectionKey(this.getStaticExcludedIds());
                var staticKeyChanged = this.staticRenderKey !== this.getStaticRenderKey();
                if (this.staticSceneDirty || !this.staticRenderRect || (viewportSettled && staticKeyChanged)
                    || this.staticExcludedKey !== excludedKey || (viewportSettled && !this.worldRectContains(this.staticRenderRect, visible))) {
                    this.renderStaticElements(this.getWorldRectWithPadding(STATIC_VIEW_PADDING), excludedKey);
                    this.staticSceneDirty = false;
                }
                this.renderActiveElements();
                this.renderLabels();
                this.renderHover();
                this.renderSelection();
                this.renderGuide();
                this.updateCursor();
            },
            getStaticExcludedIds: function () {
                if (!this.interactionState) {
                    return [];
                }
                if (this.interactionState.type === 'move' && this.selectedIds.length) {
                    return this.selectedIds.slice();
                }
                if (this.interactionState.type === 'resize' && this.interactionState.elementId) {
                    return [this.interactionState.elementId];
                }
                return [];
            },
            getRenderableElements: function (excludeIds, renderRect) {
                if (!this.doc) {
                    return [];
                }
                var rect = renderRect || this.getWorldRectWithPadding(STATIC_VIEW_PADDING);
                var candidates = this.querySpatialCandidates(rect, 0, excludeIds);
                var result = [];
                for (var i = 0; i < candidates.length; i++) {
                    if (rectIntersects(rect, candidates[i])) {
                        result.push(candidates[i]);
                    }
                }
                return result;
            },
            renderGrid: function (renderRect) {
                if (!this.gridLayer || !this.doc) {
                    return;
                }
                var visible = renderRect || this.getVisibleWorldRect();
                var width = this.doc.canvasWidth;
                var height = this.doc.canvasHeight;
                var minorStep = this.camera.scale > 1.5 ? 50 : (this.camera.scale > 0.45 ? 100 : 200);
                var majorStep = minorStep * 5;
                var lineWidth = 1 / this.camera.scale;
                var xStart = Math.max(0, Math.floor(visible.x / minorStep) * minorStep);
                var yStart = Math.max(0, Math.floor(visible.y / minorStep) * minorStep);
                var xEnd = Math.min(width, visible.x + visible.width);
                var yEnd = Math.min(height, visible.y + visible.height);
                this.gridLayer.clear();
                this.gridLayer.beginFill(0xfafcff, 1);
                this.gridLayer.drawRect(0, 0, width, height);
                this.gridLayer.endFill();
                this.gridLayer.lineStyle(lineWidth, 0xdbe4ee, 1);
                this.gridLayer.drawRect(0, 0, width, height);
                for (var x = xStart; x <= xEnd; x += minorStep) {
                    var colorX = (x % majorStep === 0) ? 0xc9d7e6 : 0xe4ebf3;
                    this.gridLayer.lineStyle(lineWidth, colorX, x % majorStep === 0 ? 0.95 : 0.75);
                    this.gridLayer.moveTo(x, 0);
                    this.gridLayer.lineTo(x, height);
                }
                for (var y = yStart; y <= yEnd; y += minorStep) {
                    var colorY = (y % majorStep === 0) ? 0xc9d7e6 : 0xe4ebf3;
                    this.gridLayer.lineStyle(lineWidth, colorY, y % majorStep === 0 ? 0.95 : 0.75);
                    this.gridLayer.moveTo(0, y);
                    this.gridLayer.lineTo(width, y);
                }
                this.gridRenderRect = {
                    x: visible.x,
                    y: visible.y,
                    width: visible.width,
                    height: visible.height
                };
                this.gridRenderKey = this.getGridRenderKey();
            },
            drawGridPatch: function (rects, layer) {
                if (!this.doc || !layer || !rects || !rects.length) {
                    return;
                }
                var width = this.doc.canvasWidth;
                var height = this.doc.canvasHeight;
                var minorStep = this.camera.scale > 1.5 ? 50 : (this.camera.scale > 0.45 ? 100 : 200);
                var majorStep = minorStep * 5;
                var lineWidth = 1 / this.camera.scale;
                for (var i = 0; i < rects.length; i++) {
                    var rect = rects[i];
                    var left = clamp(rect.x - lineWidth, 0, width);
                    var top = clamp(rect.y - lineWidth, 0, height);
                    var right = clamp(rect.x + rect.width + lineWidth, 0, width);
                    var bottom = clamp(rect.y + rect.height + lineWidth, 0, height);
                    if (right <= left || bottom <= top) {
                        continue;
                    }
                    layer.lineStyle(0, 0, 0, 0);
                    layer.beginFill(0xfafcff, 1);
                    layer.drawRect(left, top, right - left, bottom - top);
                    layer.endFill();
                    if (right - left < minorStep || bottom - top < minorStep) {
                        continue;
                    }
                    var xStart = Math.floor(left / minorStep) * minorStep;
                    var yStart = Math.floor(top / minorStep) * minorStep;
                    for (var x = xStart; x <= right; x += minorStep) {
                        if (x < left || x > right) {
                            continue;
                        }
                        var colorX = (x % majorStep === 0) ? 0xc9d7e6 : 0xe4ebf3;
                        layer.lineStyle(lineWidth, colorX, x % majorStep === 0 ? 0.95 : 0.75);
                        layer.moveTo(x, top);
                        layer.lineTo(x, bottom);
                    }
                    for (var y = yStart; y <= bottom; y += minorStep) {
                        if (y < top || y > bottom) {
                            continue;
                        }
                        var colorY = (y % majorStep === 0) ? 0xc9d7e6 : 0xe4ebf3;
                        layer.lineStyle(lineWidth, colorY, y % majorStep === 0 ? 0.95 : 0.75);
                        layer.moveTo(left, y);
                        layer.lineTo(right, y);
                    }
                }
            },
            drawPatchObjects: function (rects, excludeIds) {
                if (!rects || !rects.length || !this.patchObjectLayer) {
                    return;
                }
                var seen = {};
                var elements = [];
                for (var i = 0; i < rects.length; i++) {
                    var candidates = this.querySpatialCandidates(rects[i], 0, excludeIds);
                    for (var j = 0; j < candidates.length; j++) {
                        var item = candidates[j];
                        if (!seen[item.id] && rectIntersects(rects[i], item)) {
                            seen[item.id] = true;
                            elements.push(item);
                        }
                    }
                }
                if (!elements.length) {
                    return;
                }
                this.drawElementsToLayers(elements, this.patchObjectLayer, this.patchObjectLayer);
            },
            drawElementsToLayers: function (elements, trackLayer, nodeLayer) {
                var lineWidth = 1 / this.camera.scale;
                var useRounded = this.camera.scale >= 0.85;
                var radius = Math.max(6 / this.camera.scale, 2);
                var buckets = {};
                for (var i = 0; i < elements.length; i++) {
                    var element = elements[i];
                    var bucketKey = (element.type === 'shelf' ? 'node' : 'track') + ':' + element.type;
                    if (!buckets[bucketKey]) {
                        buckets[bucketKey] = [];
                    }
                    buckets[bucketKey].push(element);
                }
                for (var bucketKey in buckets) {
                    if (!buckets.hasOwnProperty(bucketKey)) {
                        continue;
                    }
                    var parts = bucketKey.split(':');
                    var type = parts[1];
                    var meta = getTypeMeta(type);
                    var layer = parts[0] === 'node' ? nodeLayer : trackLayer;
                    layer.lineStyle(lineWidth, meta.border, 1);
                    layer.beginFill(meta.fill, 0.92);
                    var bucket = buckets[bucketKey];
                    for (var j = 0; j < bucket.length; j++) {
                        var item = bucket[j];
                        if (useRounded) {
                            layer.drawRoundedRect(item.x, item.y, item.width, item.height, radius);
                        } else {
                            layer.drawRect(item.x, item.y, item.width, item.height);
                        }
                    }
                    layer.endFill();
                }
            },
            ensureStaticSprite: function (poolName, index) {
                var pool = poolName === 'node' ? this.staticNodeSpritePool : this.staticTrackSpritePool;
                var layer = poolName === 'node' ? this.staticNodeSpriteLayer : this.staticTrackSpriteLayer;
                if (pool[index]) {
                    return pool[index];
                }
                var sprite = new PIXI.Sprite(PIXI.Texture.WHITE);
                sprite.position.set(0, 0);
                sprite.anchor.set(0, 0);
                sprite.visible = false;
                sprite.alpha = 0;
                layer.addChild(sprite);
                pool[index] = sprite;
                return sprite;
            },
            hideUnusedStaticSprites: function (pool, fromIndex) {
                for (var i = fromIndex; i < pool.length; i++) {
                    pool[i].visible = false;
                    pool[i].alpha = 0;
                    pool[i].width = 0;
                    pool[i].height = 0;
                    pool[i].position.set(-99999, -99999);
                }
            },
            pruneStaticSpritePool: function (poolName, keepCount, slack) {
                var pool = poolName === 'node' ? this.staticNodeSpritePool : this.staticTrackSpritePool;
                var layer = poolName === 'node' ? this.staticNodeSpriteLayer : this.staticTrackSpriteLayer;
                var target = Math.max(0, keepCount + Math.max(0, slack || 0));
                if (!pool || !layer || pool.length <= target) {
                    return;
                }
                for (var i = pool.length - 1; i >= target; i--) {
                    var sprite = pool[i];
                    layer.removeChild(sprite);
                    if (sprite && sprite.destroy) {
                        sprite.destroy();
                    }
                    pool.pop();
                }
            },
            drawElementsToSpriteLayers: function (elements) {
                var trackCount = 0;
                var nodeCount = 0;
                for (var i = 0; i < elements.length; i++) {
                    var item = elements[i];
                    var meta = getTypeMeta(item.type);
                    var poolName = item.type === 'shelf' ? 'node' : 'track';
                    var sprite = this.ensureStaticSprite(poolName, poolName === 'node' ? nodeCount : trackCount);
                    sprite.visible = true;
                    sprite.position.set(item.x, item.y);
                    sprite.width = item.width;
                    sprite.height = item.height;
                    sprite.tint = meta.fill;
                    sprite.alpha = 0.92;
                    if (poolName === 'node') {
                        nodeCount += 1;
                    } else {
                        trackCount += 1;
                    }
                }
                this.hideUnusedStaticSprites(this.staticTrackSpritePool, trackCount);
                this.hideUnusedStaticSprites(this.staticNodeSpritePool, nodeCount);
                if (this.camera.scale < DENSE_SIMPLIFY_SCALE_THRESHOLD) {
                    this.pruneStaticSpritePool('track', trackCount, STATIC_SPRITE_POOL_SLACK);
                    this.pruneStaticSpritePool('node', nodeCount, STATIC_SPRITE_POOL_SLACK);
                }
            },
            simplifyRenderableElements: function (elements) {
                if (!elements || elements.length < 2) {
                    return elements || [];
                }
                var sorted = elements.slice().sort(function (a, b) {
                    if (a.type !== b.type) {
                        return a.type < b.type ? -1 : 1;
                    }
                    if (Math.abs(a.y - b.y) > COORD_EPSILON) {
                        return a.y - b.y;
                    }
                    if (Math.abs(a.height - b.height) > COORD_EPSILON) {
                        return a.height - b.height;
                    }
                    return a.x - b.x;
                });
                var result = [];
                var current = null;
                for (var i = 0; i < sorted.length; i++) {
                    var item = sorted[i];
                    if (!current) {
                        current = {
                            type: item.type,
                            x: item.x,
                            y: item.y,
                            width: item.width,
                            height: item.height
                        };
                        continue;
                    }
                    var currentRight = current.x + current.width;
                    var itemRight = item.x + item.width;
                    var sameBand = current.type === item.type
                        && Math.abs(current.y - item.y) <= 0.5
                        && Math.abs(current.height - item.height) <= 0.5;
                    var joinable = item.x <= currentRight + 0.5;
                    if (sameBand && joinable) {
                        current.width = roundCoord(Math.max(currentRight, itemRight) - current.x);
                    } else {
                        result.push(current);
                        current = {
                            type: item.type,
                            x: item.x,
                            y: item.y,
                            width: item.width,
                            height: item.height
                        };
                    }
                }
                if (current) {
                    result.push(current);
                }
                return result;
            },
            renderStaticElements: function (renderRect, excludedKey) {
                if (!this.doc) {
                    return;
                }
                this.trackLayer.clear();
                this.nodeLayer.clear();
                this.eraseLayer.clear();
                this.patchObjectLayer.clear();
                var renderableElements = this.getRenderableElements(this.getStaticExcludedIds(), renderRect);
                var useSpriteMode = this.camera.scale < STATIC_SPRITE_SCALE_THRESHOLD;
                var shouldSimplify = this.camera.scale < STATIC_SIMPLIFY_SCALE_THRESHOLD
                    || (this.camera.scale < DENSE_SIMPLIFY_SCALE_THRESHOLD && renderableElements.length > DENSE_SIMPLIFY_ELEMENT_THRESHOLD);
                this.staticTrackSpriteLayer.visible = useSpriteMode;
                this.staticNodeSpriteLayer.visible = useSpriteMode;
                this.trackLayer.visible = !useSpriteMode;
                this.nodeLayer.visible = !useSpriteMode;
                if (useSpriteMode) {
                    if (shouldSimplify) {
                        renderableElements = this.simplifyRenderableElements(renderableElements);
                    }
                    this.drawElementsToSpriteLayers(renderableElements);
                } else {
                    this.hideUnusedStaticSprites(this.staticTrackSpritePool, 0);
                    this.hideUnusedStaticSprites(this.staticNodeSpritePool, 0);
                    this.drawElementsToLayers(renderableElements, this.trackLayer, this.nodeLayer);
                }
                var rect = renderRect || this.getWorldRectWithPadding(STATIC_VIEW_PADDING);
                this.staticRenderRect = {
                    x: rect.x,
                    y: rect.y,
                    width: rect.width,
                    height: rect.height
                };
                this.staticRenderKey = this.getStaticRenderKey();
                this.staticExcludedKey = excludedKey != null ? excludedKey : this.selectionKey(this.getStaticExcludedIds());
                this.pendingStaticCommit = null;
            },
            renderActiveElements: function () {
                this.activeLayer.clear();
                this.eraseLayer.clear();
                this.patchObjectLayer.clear();
                var activeIds = this.getStaticExcludedIds();
                if (!activeIds.length) {
                    return;
                }
                var activeElements = [];
                for (var idx = 0; idx < activeIds.length; idx++) {
                    var element = this.findElementById(activeIds[idx]);
                    if (element) {
                        activeElements.push(element);
                    }
                }
                if (!activeElements.length) {
                    return;
                }
                this.drawElementsToLayers(activeElements, this.activeLayer, this.activeLayer);
            },
            getLabelText: function (element) {
                var meta = getTypeMeta(element.type);
                var value = safeParseJson(element.value);
                if (element.type === 'devp' && value) {
                    var station = value.stationId != null ? String(value.stationId) : '';
                    var arrows = formatDirectionArrows(value.direction);
                    if (station && arrows) {
                        return element.height > element.width * 1.15 ? (station + '\n' + arrows) : (station + ' ' + arrows);
                    }
                    if (station) {
                        return station;
                    }
                    if (arrows) {
                        return arrows;
                    }
                    return meta.shortLabel;
                }
                if ((element.type === 'crn' || element.type === 'dualCrn' || element.type === 'rgv') && value) {
                    if (value.deviceNo != null) {
                        return meta.shortLabel + ' ' + value.deviceNo;
                    }
                    if (value.crnNo != null) {
                        return meta.shortLabel + ' ' + value.crnNo;
                    }
                    if (value.rgvNo != null) {
                        return meta.shortLabel + ' ' + value.rgvNo;
                    }
                }
                if (element.value && element.value.length <= 18 && element.value.indexOf('{') !== 0) {
                    return element.value;
                }
                return meta.shortLabel;
            },
            ensureLabelSprite: function (index) {
                if (this.labelPool[index]) {
                    return this.labelPool[index];
                }
                var label = new PIXI.Text('', {
                    fontFamily: 'Avenir Next, PingFang SC, Microsoft YaHei, sans-serif',
                    fontSize: 12,
                    fontWeight: '600',
                    fill: 0x223448,
                    align: 'center'
                });
                label.anchor.set(0.5);
                this.labelLayer.addChild(label);
                this.labelPool[index] = label;
                return label;
            },
            getLabelRenderBudget: function () {
                if (!this.pixiApp || !this.pixiApp.renderer) {
                    return MIN_LABEL_COUNT;
                }
                var renderer = this.pixiApp.renderer;
                var viewportArea = renderer.width * renderer.height;
                return clamp(Math.round(viewportArea / 12000), MIN_LABEL_COUNT, MAX_LABEL_COUNT);
            },
            getLabelMinScreenWidth: function (text) {
                var lines = String(text || '').split('\n');
                var length = 0;
                for (var i = 0; i < lines.length; i++) {
                    length = Math.max(length, String(lines[i] || '').trim().length);
                }
                if (length <= 4) {
                    return 26;
                }
                if (length <= 8) {
                    return 40;
                }
                if (length <= 12) {
                    return 52;
                }
                return 64;
            },
            getLabelMinScreenHeight: function (text) {
                var lines = String(text || '').split('\n');
                var length = 0;
                for (var i = 0; i < lines.length; i++) {
                    length = Math.max(length, String(lines[i] || '').trim().length);
                }
                var lineHeight = length <= 4 ? 14 : 18;
                return lineHeight * Math.max(lines.length, 1);
            },
            renderLabels: function () {
                if (!this.doc) {
                    return;
                }
                if (!SHOW_CANVAS_ELEMENT_LABELS) {
                    this.labelLayer.visible = false;
                    for (var hiddenIdx = 0; hiddenIdx < this.labelPool.length; hiddenIdx++) {
                        this.labelPool[hiddenIdx].visible = false;
                    }
                    return;
                }
                var capability = this.ensureLabelCapability();
                if (capability.maxWidth * this.camera.scale < ABS_MIN_LABEL_SCREEN_WIDTH
                    || capability.maxHeight * this.camera.scale < ABS_MIN_LABEL_SCREEN_HEIGHT) {
                    this.labelLayer.visible = false;
                    return;
                }
                if (this.isZooming || this.isPanning || this.camera.scale < MIN_LABEL_SCALE
                    || (this.interactionState && (this.interactionState.type === 'move' || this.interactionState.type === 'resize' || this.interactionState.type === 'pan'))) {
                    this.labelLayer.visible = false;
                    return;
                }
                this.labelLayer.visible = true;
                var visible = this.getVisibleWorldRect();
                var elements = this.querySpatialCandidates(visible, 0, []);
                if (elements.length > DENSE_LABEL_HIDE_ELEMENT_THRESHOLD && this.camera.scale < DENSE_LABEL_HIDE_SCALE_THRESHOLD) {
                    this.labelLayer.visible = false;
                    return;
                }
                var hasRoomForAnyLabel = false;
                for (var roomIdx = 0; roomIdx < elements.length; roomIdx++) {
                    var candidate = elements[roomIdx];
                    if (candidate.width * this.camera.scale >= ABS_MIN_LABEL_SCREEN_WIDTH
                        && candidate.height * this.camera.scale >= ABS_MIN_LABEL_SCREEN_HEIGHT) {
                        hasRoomForAnyLabel = true;
                        break;
                    }
                }
                if (!hasRoomForAnyLabel) {
                    this.labelLayer.visible = false;
                    return;
                }
                var visibleElements = [];
                for (var i = 0; i < elements.length; i++) {
                    var element = elements[i];
                    var text = this.getLabelText(element);
                    if (!text) {
                        continue;
                    }
                    if (!rectIntersects(visible, element)) {
                        continue;
                    }
                    if (element.width * this.camera.scale < this.getLabelMinScreenWidth(text) || element.height * this.camera.scale < this.getLabelMinScreenHeight(text)) {
                        continue;
                    }
                    visibleElements.push({
                        element: element,
                        text: text
                    });
                }
                visibleElements.sort(function (a, b) {
                    return (b.element.width * b.element.height) - (a.element.width * a.element.height);
                });
                var labelBudget = this.getLabelRenderBudget();
                if (visibleElements.length > labelBudget) {
                    visibleElements = visibleElements.slice(0, labelBudget);
                }
                for (var j = 0; j < visibleElements.length; j++) {
                    var item = visibleElements[j].element;
                    var label = this.ensureLabelSprite(j);
                    label.visible = true;
                    label.text = visibleElements[j].text;
                    label.position.set(item.x + item.width / 2, item.y + item.height / 2);
                    label.scale.set(1 / this.camera.scale, 1 / this.camera.scale);
                    label.alpha = this.selectedIds.indexOf(item.id) >= 0 ? 1 : 0.88;
                }
                for (var k = visibleElements.length; k < this.labelPool.length; k++) {
                    this.labelPool[k].visible = false;
                }
            },
            renderHover: function () {
                this.hoverLayer.clear();
                if (this.interactionState || !this.hoverElementId || this.selectedIds.indexOf(this.hoverElementId) >= 0) {
                    return;
                }
                var element = this.findElementById(this.hoverElementId);
                if (!element) {
                    return;
                }
                var lineWidth = 2 / this.camera.scale;
                this.hoverLayer.lineStyle(lineWidth, 0x2f79d6, 0.95);
                this.hoverLayer.drawRoundedRect(element.x, element.y, element.width, element.height, Math.max(6 / this.camera.scale, 2));
            },
            renderSelection: function () {
                this.selectionLayer.clear();
                if (!this.selectedIds.length || (this.interactionState && (this.interactionState.type === 'move' || this.interactionState.type === 'resize'))) {
                    return;
                }
                var elements = this.getSelectedElements();
                var lineWidth = 2 / this.camera.scale;
                for (var i = 0; i < elements.length; i++) {
                    var element = elements[i];
                    this.selectionLayer.lineStyle(lineWidth, 0x2568b8, 1);
                    this.selectionLayer.beginFill(0x2f79d6, 0.07);
                    this.selectionLayer.drawRoundedRect(element.x, element.y, element.width, element.height, Math.max(6 / this.camera.scale, 2));
                    this.selectionLayer.endFill();
                }
                if (elements.length !== 1) {
                    return;
                }
                var handleSize = HANDLE_SCREEN_SIZE / this.camera.scale;
                var handlePositions = this.getHandlePositions(elements[0]);
                this.selectionLayer.lineStyle(1 / this.camera.scale, 0x1d5ea9, 1);
                this.selectionLayer.beginFill(0xffffff, 1);
                for (var key in handlePositions) {
                    if (!handlePositions.hasOwnProperty(key)) {
                        continue;
                    }
                    var pos = handlePositions[key];
                    this.selectionLayer.drawRect(pos.x - handleSize / 2, pos.y - handleSize / 2, handleSize, handleSize);
                }
                this.selectionLayer.endFill();
            },
            renderGuide: function () {
                this.guideLayer.clear();
                if (this.guideText) {
                    this.guideText.visible = false;
                }
                if (!this.interactionState) {
                    return;
                }
                var state = this.interactionState;
                if (state.type === 'draw' && state.rect && state.rect.width > 0 && state.rect.height > 0) {
                    var drawMeta = getTypeMeta(state.elementType);
                    this.guideLayer.lineStyle(2 / this.camera.scale, drawMeta.border, 0.95);
                    this.guideLayer.beginFill(drawMeta.fill, 0.18);
                    this.guideLayer.drawRoundedRect(state.rect.x, state.rect.y, state.rect.width, state.rect.height, Math.max(6 / this.camera.scale, 2));
                    this.guideLayer.endFill();
                    return;
                }
                if (state.type === 'array' && state.template) {
                    var previewItems = state.previewItems || [];
                    var arrayMeta = getTypeMeta(state.template.type);
                    var lineWidth = 2 / this.camera.scale;
                    var templateCenterX = state.template.x + state.template.width / 2;
                    var templateCenterY = state.template.y + state.template.height / 2;
                    this.guideLayer.lineStyle(lineWidth, arrayMeta.border, 0.9);
                    this.guideLayer.moveTo(templateCenterX, templateCenterY);
                    this.guideLayer.lineTo(state.currentWorld.x, state.currentWorld.y);
                    if (!previewItems.length) {
                        return;
                    }
                    this.guideLayer.lineStyle(1 / this.camera.scale, arrayMeta.border, 0.8);
                    this.guideLayer.beginFill(arrayMeta.fill, 0.2);
                    for (var previewIndex = 0; previewIndex < previewItems.length; previewIndex++) {
                        var preview = previewItems[previewIndex];
                        this.guideLayer.drawRoundedRect(preview.x, preview.y, preview.width, preview.height, Math.max(6 / this.camera.scale, 2));
                    }
                    this.guideLayer.endFill();
                    if (this.guideText) {
                        this.guideText.text = '将生成 ' + previewItems.length + ' 个';
                        this.guideText.position.set(state.currentWorld.x, state.currentWorld.y - 10 / this.camera.scale);
                        this.guideText.scale.set(1 / this.camera.scale);
                        this.guideText.visible = true;
                    }
                    return;
                }
                if (state.type === 'marquee') {
                    var rect = buildRectFromPoints(state.startWorld, state.currentWorld);
                    if (rect.width <= 0 || rect.height <= 0) {
                        return;
                    }
                    this.guideLayer.lineStyle(2 / this.camera.scale, 0x2f79d6, 0.92);
                    this.guideLayer.beginFill(0x2f79d6, 0.06);
                    this.guideLayer.drawRect(rect.x, rect.y, rect.width, rect.height);
                    this.guideLayer.endFill();
                }
            },
            pointerToWorld: function (event) {
                var rect = this.pixiApp.view.getBoundingClientRect();
                var screenX = event.clientX - rect.left;
                var screenY = event.clientY - rect.top;
                return {
                    screenX: screenX,
                    screenY: screenY,
                    x: roundCoord((screenX - this.camera.x) / this.camera.scale),
                    y: roundCoord((screenY - this.camera.y) / this.camera.scale)
                };
            },
            isWithinCanvas: function (rect) {
                if (!this.doc) {
                    return false;
                }
                return rect.x >= -COORD_EPSILON && rect.y >= -COORD_EPSILON
                    && rect.x + rect.width <= this.doc.canvasWidth + COORD_EPSILON
                    && rect.y + rect.height <= this.doc.canvasHeight + COORD_EPSILON;
            },
            canPlaceElements: function (elements, excludeIds) {
                excludeIds = excludeIds || [];
                for (var i = 0; i < elements.length; i++) {
                    if (!this.isWithinCanvas(elements[i])) {
                        return false;
                    }
                    if (this.hasOverlap(elements[i], excludeIds.concat([elements[i].id]))) {
                        return false;
                    }
                }
                return true;
            },
            hasOverlap: function (candidate, excludeIds) {
                if (!this.doc) {
                    return false;
                }
                var elements = this.querySpatialCandidates(candidate, COORD_EPSILON, excludeIds);
                for (var i = 0; i < elements.length; i++) {
                    var item = elements[i];
                    if (rectsOverlap(candidate, item)) {
                        return true;
                    }
                }
                return false;
            },
            snapToleranceWorld: function () {
                return Math.max(1, EDGE_SNAP_SCREEN_TOLERANCE / this.camera.scale);
            },
            collectMoveSnap: function (baseItems, dx, dy, excludeIds) {
                if (!this.doc || !baseItems || !baseItems.length) {
                    return { dx: 0, dy: 0 };
                }
                var tolerance = this.snapToleranceWorld();
                var bestDx = null;
                var bestDy = null;
                for (var i = 0; i < baseItems.length; i++) {
                    var moving = baseItems[i];
                    var movedLeft = moving.x + dx;
                    var movedRight = movedLeft + moving.width;
                    var movedTop = moving.y + dy;
                    var movedBottom = movedTop + moving.height;
                    var candidates = this.querySpatialCandidates({
                        x: movedLeft,
                        y: movedTop,
                        width: moving.width,
                        height: moving.height
                    }, tolerance, excludeIds);
                    for (var j = 0; j < candidates.length; j++) {
                        var other = candidates[j];
                        var otherLeft = other.x;
                        var otherRight = other.x + other.width;
                        var otherTop = other.y;
                        var otherBottom = other.y + other.height;
                        if (rangesNearOrOverlap(movedTop, movedBottom, otherTop, otherBottom, tolerance)) {
                            var horizontalCandidates = [
                                otherLeft - movedRight,
                                otherRight - movedLeft,
                                otherLeft - movedLeft,
                                otherRight - movedRight
                            ];
                            for (var hx = 0; hx < horizontalCandidates.length; hx++) {
                                var deltaX = horizontalCandidates[hx];
                                if (Math.abs(deltaX) <= tolerance && (bestDx === null || Math.abs(deltaX) < Math.abs(bestDx))) {
                                    bestDx = deltaX;
                                }
                            }
                        }
                        if (rangesNearOrOverlap(movedLeft, movedRight, otherLeft, otherRight, tolerance)) {
                            var verticalCandidates = [
                                otherTop - movedBottom,
                                otherBottom - movedTop,
                                otherTop - movedTop,
                                otherBottom - movedBottom
                            ];
                            for (var vy = 0; vy < verticalCandidates.length; vy++) {
                                var deltaY = verticalCandidates[vy];
                                if (Math.abs(deltaY) <= tolerance && (bestDy === null || Math.abs(deltaY) < Math.abs(bestDy))) {
                                    bestDy = deltaY;
                                }
                            }
                        }
                    }
                }
                return {
                    dx: bestDx == null ? 0 : bestDx,
                    dy: bestDy == null ? 0 : bestDy
                };
            },
            collectResizeSnap: function (rect, handle, excludeIds) {
                if (!this.doc || !rect) {
                    return null;
                }
                var tolerance = this.snapToleranceWorld();
                var left = rect.x;
                var right = rect.x + rect.width;
                var top = rect.y;
                var bottom = rect.y + rect.height;
                var bestLeft = null;
                var bestRight = null;
                var bestTop = null;
                var bestBottom = null;
                function pickBest(current, candidate) {
                    if (candidate == null) {
                        return current;
                    }
                    if (current == null || Math.abs(candidate) < Math.abs(current)) {
                        return candidate;
                    }
                    return current;
                }
                if (handle.indexOf('w') >= 0) {
                    bestLeft = pickBest(bestLeft, -left);
                }
                if (handle.indexOf('e') >= 0) {
                    bestRight = pickBest(bestRight, this.doc.canvasWidth - right);
                }
                if (handle.indexOf('n') >= 0) {
                    bestTop = pickBest(bestTop, -top);
                }
                if (handle.indexOf('s') >= 0) {
                    bestBottom = pickBest(bestBottom, this.doc.canvasHeight - bottom);
                }
                var elements = this.querySpatialCandidates(rect, tolerance, excludeIds);
                for (var i = 0; i < elements.length; i++) {
                    var other = elements[i];
                    var otherLeft = other.x;
                    var otherRight = other.x + other.width;
                    var otherTop = other.y;
                    var otherBottom = other.y + other.height;
                    if (rangesNearOrOverlap(top, bottom, otherTop, otherBottom, tolerance)) {
                        if (handle.indexOf('w') >= 0) {
                            bestLeft = pickBest(bestLeft, otherLeft - left);
                            bestLeft = pickBest(bestLeft, otherRight - left);
                        }
                        if (handle.indexOf('e') >= 0) {
                            bestRight = pickBest(bestRight, otherLeft - right);
                            bestRight = pickBest(bestRight, otherRight - right);
                        }
                    }
                    if (rangesNearOrOverlap(left, right, otherLeft, otherRight, tolerance)) {
                        if (handle.indexOf('n') >= 0) {
                            bestTop = pickBest(bestTop, otherTop - top);
                            bestTop = pickBest(bestTop, otherBottom - top);
                        }
                        if (handle.indexOf('s') >= 0) {
                            bestBottom = pickBest(bestBottom, otherTop - bottom);
                            bestBottom = pickBest(bestBottom, otherBottom - bottom);
                        }
                    }
                }
                if (bestLeft != null && Math.abs(bestLeft) > tolerance) {
                    bestLeft = null;
                }
                if (bestRight != null && Math.abs(bestRight) > tolerance) {
                    bestRight = null;
                }
                if (bestTop != null && Math.abs(bestTop) > tolerance) {
                    bestTop = null;
                }
                if (bestBottom != null && Math.abs(bestBottom) > tolerance) {
                    bestBottom = null;
                }
                return {
                    left: bestLeft,
                    right: bestRight,
                    top: bestTop,
                    bottom: bestBottom
                };
            },
            hitTestElement: function (point) {
                if (!this.doc) {
                    return null;
                }
                var candidates = this.querySpatialCandidates({
                    x: point.x,
                    y: point.y,
                    width: 0,
                    height: 0
                }, 0, []);
                if (!candidates.length) {
                    return null;
                }
                var candidateMap = {};
                for (var c = 0; c < candidates.length; c++) {
                    candidateMap[candidates[c].id] = true;
                }
                var elements = this.doc.elements || [];
                for (var i = elements.length - 1; i >= 0; i--) {
                    var element = elements[i];
                    if (!candidateMap[element.id]) {
                        continue;
                    }
                    if (point.x >= element.x && point.x <= element.x + element.width
                        && point.y >= element.y && point.y <= element.y + element.height) {
                        return element;
                    }
                }
                return null;
            },
            getHandlePositions: function (element) {
                var x = element.x;
                var y = element.y;
                var w = element.width;
                var h = element.height;
                var cx = x + w / 2;
                var cy = y + h / 2;
                return {
                    nw: { x: x, y: y },
                    n: { x: cx, y: y },
                    ne: { x: x + w, y: y },
                    e: { x: x + w, y: cy },
                    se: { x: x + w, y: y + h },
                    s: { x: cx, y: y + h },
                    sw: { x: x, y: y + h },
                    w: { x: x, y: cy }
                };
            },
            getResizeHandleAt: function (point, element) {
                var handlePositions = this.getHandlePositions(element);
                var baseTolerance = HANDLE_SCREEN_SIZE / this.camera.scale;
                var sizeLimitedTolerance = Math.max(Math.min(element.width, element.height) / 4, 3 / this.camera.scale);
                var tolerance = Math.min(baseTolerance, sizeLimitedTolerance);
                var bestHandle = '';
                var bestDistance = Infinity;
                for (var key in handlePositions) {
                    if (!handlePositions.hasOwnProperty(key)) {
                        continue;
                    }
                    var pos = handlePositions[key];
                    var dx = Math.abs(point.x - pos.x);
                    var dy = Math.abs(point.y - pos.y);
                    if (dx <= tolerance && dy <= tolerance) {
                        var distance = dx + dy;
                        if (distance < bestDistance) {
                            bestDistance = distance;
                            bestHandle = key;
                        }
                    }
                }
                return bestHandle;
            },
            cursorForHandle: function (handle) {
                if (handle === 'nw' || handle === 'se') {
                    return 'nwse-resize';
                }
                if (handle === 'ne' || handle === 'sw') {
                    return 'nesw-resize';
                }
                if (handle === 'n' || handle === 's') {
                    return 'ns-resize';
                }
                if (handle === 'e' || handle === 'w') {
                    return 'ew-resize';
                }
                return 'default';
            },
            updateCursor: function () {
                if (!this.pixiApp) {
                    return;
                }
                var cursor = 'default';
                if (this.interactionState) {
                    if (this.interactionState.type === 'pan') {
                        cursor = 'grabbing';
                    } else if (this.interactionState.type === 'draw' || this.interactionState.type === 'marquee') {
                        cursor = 'crosshair';
                    } else if (this.interactionState.type === 'array') {
                        cursor = 'crosshair';
                    } else if (this.interactionState.type === 'move') {
                        cursor = 'move';
                    } else if (this.interactionState.type === 'movePending') {
                        cursor = 'grab';
                    } else if (this.interactionState.type === 'resize') {
                        cursor = this.cursorForHandle(this.interactionState.handle);
                    }
                } else if (this.spacePressed || this.activeTool === 'pan') {
                    cursor = 'grab';
                } else if (DRAW_TYPES.indexOf(this.activeTool) >= 0 || this.activeTool === 'marquee' || this.activeTool === 'array') {
                    cursor = 'crosshair';
                } else if (this.singleSelectedElement) {
                    var point = this.lastPointerWorld || null;
                    if (point) {
                        var handle = this.getResizeHandleAt(point, this.singleSelectedElement);
                        cursor = handle ? this.cursorForHandle(handle) : 'default';
                    }
                    if (cursor === 'default' && this.hoverElementId) {
                        cursor = 'move';
                    } else if (cursor === 'default') {
                        cursor = 'grab';
                    }
                } else {
                    cursor = this.hoverElementId ? 'move' : 'grab';
                }
                if (cursor !== this.lastCursor) {
                    this.lastCursor = cursor;
                    this.pixiApp.view.style.cursor = cursor;
                }
            },
            startPan: function (point) {
                this.cancelDeferredStaticRebuild();
                this.cancelPanRefresh();
                if (this.zoomRefreshTimer) {
                    window.clearTimeout(this.zoomRefreshTimer);
                    this.zoomRefreshTimer = null;
                    this.isZooming = false;
                    this.pendingViewportRefresh = true;
                }
                this.isPanning = true;
                this.interactionState = {
                    type: 'pan',
                    startScreen: {
                        x: point.screenX,
                        y: point.screenY
                    },
                    startCamera: {
                        x: this.camera.x,
                        y: this.camera.y
                    }
                };
                this.updateCursor();
            },
            startMarquee: function (point, additive) {
                this.cancelDeferredStaticRebuild();
                this.interactionState = {
                    type: 'marquee',
                    additive: !!additive,
                    startWorld: { x: point.x, y: point.y },
                    currentWorld: { x: point.x, y: point.y }
                };
                this.updateCursor();
            },
            startDraw: function (point) {
                this.cancelDeferredStaticRebuild();
                this.interactionState = {
                    type: 'draw',
                    beforeSnapshot: this.snapshotDoc(this.doc),
                    elementType: this.activeTool,
                    startWorld: { x: point.x, y: point.y },
                    rect: { x: point.x, y: point.y, width: 0, height: 0 }
                };
                this.updateCursor();
            },
            startArray: function (point, element) {
                if (!this.canArrayFromElement(element)) {
                    this.showMessage('warning', '阵列工具当前只支持货架、CRN、双工位和 RGV');
                    return;
                }
                this.cancelDeferredStaticRebuild();
                this.interactionState = {
                    type: 'array',
                    beforeSnapshot: this.snapshotDoc(this.doc),
                    template: {
                        id: element.id,
                        type: element.type,
                        x: element.x,
                        y: element.y,
                        width: element.width,
                        height: element.height,
                        value: element.value
                    },
                    startWorld: { x: point.x, y: point.y },
                    currentWorld: { x: point.x, y: point.y },
                    previewItems: []
                };
                this.updateCursor();
            },
            startMove: function (point) {
                var selected = this.getSelectedElements();
                if (!selected.length) {
                    return;
                }
                this.cancelDeferredStaticRebuild();
                var baseItems = selected.map(function (item) {
                    return {
                        id: item.id,
                        x: item.x,
                        y: item.y,
                        width: item.width,
                        height: item.height,
                        value: item.value,
                        type: item.type
                    };
                });
                this.interactionState = {
                    type: 'movePending',
                    beforeSnapshot: this.snapshotDoc(this.doc),
                    startScreen: { x: point.screenX, y: point.screenY },
                    startWorld: { x: point.x, y: point.y },
                    baseItems: baseItems
                };
                this.updateCursor();
            },
            startResize: function (point, element, handle) {
                this.cancelDeferredStaticRebuild();
                this.interactionState = {
                    type: 'resize',
                    handle: handle,
                    elementId: element.id,
                    beforeSnapshot: this.snapshotDoc(this.doc),
                    baseRect: {
                        x: element.x,
                        y: element.y,
                        width: element.width,
                        height: element.height
                    }
                };
                this.markStaticSceneDirty();
                this.scheduleRender();
                this.updateCursor();
            },
            onCanvasPointerDown: function (event) {
                if (!this.doc || !this.pixiApp) {
                    return;
                }
                if (event.button !== 0 && event.button !== 1) {
                    return;
                }
                if (this.pixiApp.view.setPointerCapture && event.pointerId != null) {
                    try {
                        this.pixiApp.view.setPointerCapture(event.pointerId);
                    } catch (ignore) {
                    }
                }
                this.currentPointerId = event.pointerId;
                var point = this.pointerToWorld(event);
                this.lastPointerWorld = point;
                this.pointerStatus = this.formatNumber(point.x) + ', ' + this.formatNumber(point.y);
                if (this.spacePressed || this.activeTool === 'pan' || event.button === 1) {
                    this.startPan(point);
                    return;
                }
                if (DRAW_TYPES.indexOf(this.activeTool) >= 0) {
                    this.startDraw(point);
                    return;
                }
                if (this.activeTool === 'marquee') {
                    this.startMarquee(point, event.shiftKey);
                    return;
                }
                if (this.activeTool === 'array') {
                    var arrayHit = this.hitTestElement(point);
                    var arrayTemplate = arrayHit || this.singleSelectedElement;
                    if (arrayHit && this.selectedIds.indexOf(arrayHit.id) < 0) {
                        this.setSelectedIds([arrayHit.id]);
                        arrayTemplate = arrayHit;
                    }
                    if (!arrayTemplate) {
                        this.showMessage('warning', '请先选中一个货架或轨道作为阵列模板');
                        return;
                    }
                    this.startArray(point, arrayTemplate);
                    return;
                }
                var selected = this.singleSelectedElement;
                var handle = selected ? this.getResizeHandleAt(point, selected) : '';
                if (handle) {
                    this.startResize(point, selected, handle);
                    return;
                }
                var hit = this.hitTestElement(point);
                if (hit) {
                    if (event.shiftKey) {
                        var index = this.selectedIds.indexOf(hit.id);
                        if (index >= 0) {
                            var nextIds = this.selectedIds.slice();
                            nextIds.splice(index, 1);
                            this.setSelectedIds(nextIds);
                        } else {
                            this.setSelectedIds(this.selectedIds.concat([hit.id]));
                        }
                        this.scheduleRender();
                        return;
                    }
                    if (this.selectedIds.indexOf(hit.id) < 0) {
                        this.setSelectedIds([hit.id]);
                        this.scheduleRender();
                    }
                    this.startMove(point);
                    return;
                }
                if (this.selectedIds.length) {
                    this.setSelectedIds([]);
                    this.scheduleRender();
                }
                this.startPan(point);
            },
            onCanvasWheel: function (event) {
                if (!this.pixiApp || !this.doc) {
                    return;
                }
                event.preventDefault();
                var point = this.pointerToWorld(event);
                var delta = event.deltaY < 0 ? 1.12 : 0.89;
                var nextScale = clamp(this.camera.scale * delta, 0.06, 4);
                this.camera.scale = nextScale;
                this.camera.x = Math.round(point.screenX - point.x * nextScale);
                this.camera.y = Math.round(point.screenY - point.y * nextScale);
                this.viewZoom = nextScale;
                this.scheduleZoomRefresh();
                this.scheduleRender();
            },
            onWindowPointerMove: function (event) {
                if (!this.pixiApp || !this.doc) {
                    return;
                }
                var point = this.pointerToWorld(event);
                this.lastPointerWorld = point;
                var pointerText = this.formatNumber(point.x) + ', ' + this.formatNumber(point.y);
                var now = (window.performance && performance.now) ? performance.now() : Date.now();
                if (pointerText !== this.pointerStatus && (now - this.lastPointerStatusUpdateTs >= POINTER_STATUS_UPDATE_INTERVAL || this.pointerStatus === '--')) {
                    this.pointerStatus = pointerText;
                    this.lastPointerStatusUpdateTs = now;
                }
                if (!this.interactionState) {
                    var hover = this.hitTestElement(point);
                    var hoverId = hover ? hover.id : '';
                    if (hoverId !== this.hoverElementId) {
                        this.hoverElementId = hoverId;
                        this.scheduleRender();
                    }
                    this.updateCursor();
                    return;
                }
                var state = this.interactionState;
                if (state.type === 'pan') {
                    this.camera.x = Math.round(state.startCamera.x + (point.screenX - state.startScreen.x));
                    this.camera.y = Math.round(state.startCamera.y + (point.screenY - state.startScreen.y));
                    this.scheduleRender();
                    return;
                }
                if (state.type === 'marquee') {
                    state.currentWorld = { x: point.x, y: point.y };
                    this.scheduleRender();
                    return;
                }
                if (state.type === 'draw') {
                    var rawRect = buildRectFromPoints(state.startWorld, point);
                    var clipped = {
                        x: clamp(rawRect.x, 0, this.doc.canvasWidth),
                        y: clamp(rawRect.y, 0, this.doc.canvasHeight),
                        width: clamp(rawRect.width, 0, this.doc.canvasWidth),
                        height: clamp(rawRect.height, 0, this.doc.canvasHeight)
                    };
                    if (clipped.x + clipped.width > this.doc.canvasWidth) {
                        clipped.width = roundCoord(this.doc.canvasWidth - clipped.x);
                    }
                    if (clipped.y + clipped.height > this.doc.canvasHeight) {
                        clipped.height = roundCoord(this.doc.canvasHeight - clipped.y);
                    }
                    state.rect = clipped;
                    this.scheduleRender();
                    return;
                }
                if (state.type === 'array') {
                    state.currentWorld = { x: point.x, y: point.y };
                    state.previewItems = this.buildArrayCopies(state.template, state.startWorld, state.currentWorld);
                    this.scheduleRender();
                    return;
                }
                if (state.type === 'movePending') {
                    var dragDistance = Math.max(Math.abs(point.screenX - state.startScreen.x), Math.abs(point.screenY - state.startScreen.y));
                    if (dragDistance < DRAG_START_THRESHOLD) {
                        return;
                    }
                    state.type = 'move';
                    this.markStaticSceneDirty();
                    this.scheduleRender();
                    this.updateCursor();
                }
                if (state.type === 'move') {
                    var dx = point.x - state.startWorld.x;
                    var dy = point.y - state.startWorld.y;
                    var minDx = -Infinity;
                    var maxDx = Infinity;
                    var minDy = -Infinity;
                    var maxDy = Infinity;
                    for (var i = 0; i < state.baseItems.length; i++) {
                        var base = state.baseItems[i];
                        minDx = Math.max(minDx, -base.x);
                        minDy = Math.max(minDy, -base.y);
                        maxDx = Math.min(maxDx, this.doc.canvasWidth - (base.x + base.width));
                        maxDy = Math.min(maxDy, this.doc.canvasHeight - (base.y + base.height));
                    }
                    dx = clamp(dx, minDx, maxDx);
                    dy = clamp(dy, minDy, maxDy);
                    var snapDelta = this.collectMoveSnap(state.baseItems, dx, dy, this.selectedIds.slice());
                    dx = clamp(dx + snapDelta.dx, minDx, maxDx);
                    dy = clamp(dy + snapDelta.dy, minDy, maxDy);
                    for (var j = 0; j < state.baseItems.length; j++) {
                        var baseItem = state.baseItems[j];
                        var element = this.findElementById(baseItem.id);
                        if (!element) {
                            continue;
                        }
                        element.x = roundCoord(baseItem.x + dx);
                        element.y = roundCoord(baseItem.y + dy);
                    }
                    this.scheduleRender();
                    return;
                }
                if (state.type === 'resize') {
                    var target = this.findElementById(state.elementId);
                    if (!target) {
                        return;
                    }
                    var baseRect = state.baseRect;
                    var left = baseRect.x;
                    var right = baseRect.x + baseRect.width;
                    var top = baseRect.y;
                    var bottom = baseRect.y + baseRect.height;
                    if (state.handle.indexOf('w') >= 0) {
                        left = clamp(point.x, 0, right - MIN_ELEMENT_SIZE);
                    }
                    if (state.handle.indexOf('e') >= 0) {
                        right = clamp(point.x, left + MIN_ELEMENT_SIZE, this.doc.canvasWidth);
                    }
                    if (state.handle.indexOf('n') >= 0) {
                        top = clamp(point.y, 0, bottom - MIN_ELEMENT_SIZE);
                    }
                    if (state.handle.indexOf('s') >= 0) {
                        bottom = clamp(point.y, top + MIN_ELEMENT_SIZE, this.doc.canvasHeight);
                    }
                    var snapped = this.collectResizeSnap({
                        x: left,
                        y: top,
                        width: right - left,
                        height: bottom - top
                    }, state.handle, [target.id]);
                    if (snapped) {
                        if (state.handle.indexOf('w') >= 0 && snapped.left != null) {
                            left = clamp(left + snapped.left, 0, right - MIN_ELEMENT_SIZE);
                        }
                        if (state.handle.indexOf('e') >= 0 && snapped.right != null) {
                            right = clamp(right + snapped.right, left + MIN_ELEMENT_SIZE, this.doc.canvasWidth);
                        }
                        if (state.handle.indexOf('n') >= 0 && snapped.top != null) {
                            top = clamp(top + snapped.top, 0, bottom - MIN_ELEMENT_SIZE);
                        }
                        if (state.handle.indexOf('s') >= 0 && snapped.bottom != null) {
                            bottom = clamp(bottom + snapped.bottom, top + MIN_ELEMENT_SIZE, this.doc.canvasHeight);
                        }
                    }
                    target.x = roundCoord(left);
                    target.y = roundCoord(top);
                    target.width = roundCoord(right - left);
                    target.height = roundCoord(bottom - top);
                    this.scheduleRender();
                }
            },
            onWindowPointerUp: function (event) {
                if (!this.interactionState) {
                    return;
                }
                if (this.currentPointerId != null && event.pointerId != null && this.currentPointerId !== event.pointerId) {
                    return;
                }
                if (this.pixiApp && this.pixiApp.view.releasePointerCapture && event.pointerId != null) {
                    try {
                        this.pixiApp.view.releasePointerCapture(event.pointerId);
                    } catch (ignore) {
                    }
                }
                this.currentPointerId = null;
                var state = this.interactionState;
                this.interactionState = null;
                if (state.type === 'pan') {
                    this.updateCursor();
                    this.schedulePanRefresh();
                    this.scheduleRender();
                    return;
                }
                if (state.type === 'marquee') {
                    var rect = buildRectFromPoints(state.startWorld, state.currentWorld);
                    if (rect.width > 2 && rect.height > 2) {
                        var matched = (this.doc.elements || []).filter(function (item) {
                            return rectIntersects(rect, item);
                        }).map(function (item) {
                            return item.id;
                        });
                        this.setSelectedIds(state.additive ? Array.from(new Set(this.selectedIds.concat(matched))) : matched);
                    }
                    this.scheduleRender();
                    return;
                }
                if (state.type === 'movePending') {
                    this.updateCursor();
                    return;
                }
                if (state.type === 'draw') {
                    var drawRect = state.rect;
                    if (drawRect && drawRect.width >= MIN_ELEMENT_SIZE && drawRect.height >= MIN_ELEMENT_SIZE) {
                        var newElement = {
                            id: nextId(),
                            type: state.elementType,
                            x: roundCoord(drawRect.x),
                            y: roundCoord(drawRect.y),
                            width: roundCoord(drawRect.width),
                            height: roundCoord(drawRect.height),
                            value: ''
                        };
                        if (this.hasOverlap(newElement, [])) {
                            this.showMessage('warning', '新元素不能与已有元素重叠');
                        } else if (!this.isWithinCanvas(newElement)) {
                            this.showMessage('warning', '新元素超出画布范围');
                        } else {
                            this.doc.elements.push(newElement);
                            this.selectedIds = [newElement.id];
                            this.commitMutation(state.beforeSnapshot);
                            this.refreshInspector();
                            return;
                        }
                    }
                    this.refreshInspector();
                    this.scheduleRender();
                    return;
                }
                if (state.type === 'array') {
                    var copies = state.previewItems && state.previewItems.length
                        ? state.previewItems
                        : this.buildArrayCopies(state.template, state.startWorld, state.currentWorld || state.startWorld);
                    if (!copies.length) {
                        this.scheduleRender();
                        return;
                    }
                    if (!this.canPlaceElements(copies, [])) {
                        this.showMessage('warning', '阵列生成后会重叠或超出画布,已取消');
                        this.scheduleRender();
                        return;
                    }
                    var finalizedCopies = copies.map(function (item) {
                        return $.extend({}, item, { id: nextId() });
                    });
                    var self = this;
                    this.runMutation(function () {
                        self.doc.elements = self.doc.elements.concat(finalizedCopies);
                        self.selectedIds = [finalizedCopies[finalizedCopies.length - 1].id];
                    });
                    return;
                }
                if (state.type === 'move') {
                    var movedElements = this.getSelectedElements();
                    if (!this.canPlaceElements(movedElements, this.selectedIds.slice())) {
                        for (var i = 0; i < state.baseItems.length; i++) {
                            var base = state.baseItems[i];
                            var element = this.findElementById(base.id);
                            if (!element) {
                                continue;
                            }
                            element.x = base.x;
                            element.y = base.y;
                        }
                        this.showMessage('warning', '移动后会重叠或超出画布,已恢复');
                        this.refreshInspector();
                        this.scheduleRender();
                        return;
                    }
                    if (!this.commitMutation(state.beforeSnapshot)) {
                        this.markStaticSceneDirty();
                        this.scheduleRender();
                    }
                    return;
                }
                if (state.type === 'resize') {
                    var resized = this.findElementById(state.elementId);
                    if (resized) {
                        if (!this.isWithinCanvas(resized) || this.hasOverlap(resized, [resized.id])) {
                            resized.x = state.baseRect.x;
                            resized.y = state.baseRect.y;
                            resized.width = state.baseRect.width;
                            resized.height = state.baseRect.height;
                            this.showMessage('warning', '缩放后会重叠或超出画布,已恢复');
                            this.refreshInspector();
                            this.scheduleRender();
                            return;
                        }
                    }
                    if (!this.commitMutation(state.beforeSnapshot)) {
                        this.markStaticSceneDirty();
                        this.scheduleRender();
                    }
                    return;
                }
                this.scheduleRender();
            },
            onWindowKeyDown: function (event) {
                if (event.key === ' ' && !isInputLike(event.target)) {
                    this.spacePressed = true;
                    this.updateCursor();
                    event.preventDefault();
                }
                if (!this.doc) {
                    return;
                }
                if (isInputLike(event.target)) {
                    return;
                }
                var ctrl = event.ctrlKey || event.metaKey;
                if (event.key === 'Delete' || event.key === 'Backspace') {
                    event.preventDefault();
                    this.deleteSelection();
                    return;
                }
                if (ctrl && (event.key === 'z' || event.key === 'Z')) {
                    event.preventDefault();
                    if (event.shiftKey) {
                        this.redo();
                    } else {
                        this.undo();
                    }
                    return;
                }
                if (ctrl && (event.key === 'y' || event.key === 'Y')) {
                    event.preventDefault();
                    this.redo();
                    return;
                }
                if (ctrl && (event.key === 'c' || event.key === 'C')) {
                    event.preventDefault();
                    this.copySelection();
                    return;
                }
                if (ctrl && (event.key === 'v' || event.key === 'V')) {
                    event.preventDefault();
                    this.pasteClipboard();
                    return;
                }
                if (event.key === 'Escape') {
                    this.interactionState = null;
                    this.setSelectedIds([]);
                    this.hoverElementId = '';
                    this.scheduleRender();
                }
            },
            onWindowKeyUp: function (event) {
                if (event.key === ' ') {
                    this.spacePressed = false;
                    this.updateCursor();
                }
            },
            onBeforeUnload: function (event) {
                if (!this.isDirty) {
                    return;
                }
                event.preventDefault();
                event.returnValue = '';
            }
          })
        };
      },
      clearDeferredStaticCommit: function () {
        this.cancelDeferredStaticRebuild();
        this.pendingStaticCommit = null;
      },
      scheduleDeferredStaticRebuild: function () {
        this.cancelDeferredStaticRebuild();
        this.deferredStaticRebuildTimer = window.setTimeout(
          function () {
            this.deferredStaticRebuildTimer = null;
            this.pendingStaticCommit = null;
            this.markStaticSceneDirty();
            this.scheduleRender();
          }.bind(this),
          DEFERRED_STATIC_REBUILD_DELAY
        );
      },
      selectionKey: function (ids) {
        return (ids || []).slice().sort().join('|');
      },
      setSelectedIds: function (ids, options) {
        options = options || {};
        var nextIds = (ids || []).filter(Boolean);
        this.selectedIds = nextIds.slice();
        if (options.refreshInspector !== false) {
          this.refreshInspector();
        }
    });
      },
      setCurrentDoc: function (doc, options) {
        options = options || {};
        var normalized = this.normalizeDoc(doc);
        this.clearFloorTransientState();
        this.resetRenderLayers();
        this.clearRenderCaches();
        this.doc = normalized;
        this.markSpatialIndexDirty();
        this.labelCapabilityDirty = true;
        this.pendingViewportRefresh = false;
        this.currentLev = normalized.lev;
        this.floorPickerLev = normalized.lev;
        this.switchingFloorLev = null;
        this.loadingFloor = false;
        this.syncFloorQueryParam(normalized.lev);
        this.markGridSceneDirty();
        this.markStaticSceneDirty();
        this.undoStack = [];
        this.redoStack = [];
        this.savedSnapshot =
          options.savedSnapshot != null ? options.savedSnapshot : this.snapshotDoc(normalized);
        this.syncDirty();
        this.refreshInspector();
        this.refreshLevOptions();
        this.$nextTick(
          function () {
            this.fitContent();
            this.scheduleRender();
          }.bind(this)
        );
      },
      replaceDocFromSnapshot: function (snapshot) {
        if (!snapshot) {
          return;
        }
        try {
          this.clearFloorTransientState();
          this.resetRenderLayers();
          this.clearRenderCaches();
          this.doc = this.normalizeDoc(JSON.parse(snapshot));
          this.markSpatialIndexDirty();
          this.labelCapabilityDirty = true;
          this.pendingViewportRefresh = false;
        } catch (e) {
          this.showMessage('error', '历史记录恢复失败');
          return;
        }
        this.markGridSceneDirty();
        this.markStaticSceneDirty();
        this.floorPickerLev = this.doc.lev;
        this.currentLev = this.doc.lev;
        this.refreshInspector();
        this.syncDirty();
        this.cacheCurrentDraft();
        this.scheduleRender();
      },
      pushUndoSnapshot: function (snapshot) {
        if (!snapshot) {
          return;
        }
        if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length - 1] === snapshot) {
          return;
        }
        this.undoStack.push(snapshot);
        if (this.undoStack.length > HISTORY_LIMIT) {
          this.undoStack.shift();
        }
      },
      commitMutation: function (beforeSnapshot, options) {
        options = options || {};
        var afterSnapshot = this.snapshotDoc(this.doc);
        if (beforeSnapshot === afterSnapshot) {
          this.scheduleRender();
          this.refreshInspector();
          return false;
        }
        this.pushUndoSnapshot(beforeSnapshot);
        this.redoStack = [];
        this.markSpatialIndexDirty();
        this.labelCapabilityDirty = true;
        if (options.staticSceneDirty !== false) {
          this.clearDeferredStaticCommit();
          this.markStaticSceneDirty();
        }
        this.syncDirty();
        this.cacheCurrentDraft();
        this.refreshInspector();
        this.scheduleRender();
        return true;
      },
      runMutation: function (mutator) {
        if (!this.doc) {
          return false;
        }
        var beforeSnapshot = this.snapshotDoc(this.doc);
        mutator();
        return this.commitMutation(beforeSnapshot);
      },
      undo: function () {
        if (this.undoStack.length === 0 || !this.doc) {
          return;
        }
        var currentSnapshot = this.snapshotDoc(this.doc);
        var snapshot = this.undoStack.pop();
        this.redoStack.push(currentSnapshot);
        this.replaceDocFromSnapshot(snapshot);
      },
      redo: function () {
        if (this.redoStack.length === 0 || !this.doc) {
          return;
        }
        var currentSnapshot = this.snapshotDoc(this.doc);
        var snapshot = this.redoStack.pop();
        this.pushUndoSnapshot(currentSnapshot);
        this.replaceDocFromSnapshot(snapshot);
      },
      createLocalBlankDoc: function (lev, width, height, savedSnapshot) {
        var doc = {
          lev: toInt(lev, 1),
          editorMode: FREE_EDITOR_MODE,
          canvasWidth: Math.max(MIN_ELEMENT_SIZE * 4, toNumber(width, DEFAULT_CANVAS_WIDTH)),
          canvasHeight: Math.max(MIN_ELEMENT_SIZE * 4, toNumber(height, DEFAULT_CANVAS_HEIGHT)),
          elements: []
        };
        this.setCurrentDoc(doc, {
          savedSnapshot: savedSnapshot != null ? savedSnapshot : ''
        });
        this.cacheCurrentDraft();
        this.syncDirty();
      },
      openBlankDialog: function () {
        var lev = this.currentLev || 1;
        this.blankForm = {
          lev: String(lev),
          width: String(Math.round(this.doc ? this.doc.canvasWidth : DEFAULT_CANVAS_WIDTH)),
          height: String(Math.round(this.doc ? this.doc.canvasHeight : DEFAULT_CANVAS_HEIGHT))
        };
        this.blankDialogVisible = true;
      },
      createBlankMap: function () {
        var lev = toInt(this.blankForm.lev, 0);
        var width = toNumber(this.blankForm.width, DEFAULT_CANVAS_WIDTH);
        var height = toNumber(this.blankForm.height, DEFAULT_CANVAS_HEIGHT);
        if (lev <= 0) {
          this.showMessage('warning', '楼层不能为空');
          return;
        }
        if (width <= 0 || height <= 0) {
          this.showMessage('warning', '画布尺寸必须大于 0');
          return;
        }
        this.blankDialogVisible = false;
        this.createLocalBlankDoc(lev, width, height, '');
      },
      buildTransferPayload: function () {
        var doc = this.exportDoc(this.doc);
        return {
          format: MAP_TRANSFER_FORMAT,
          exportedAt: new Date().toISOString(),
          source: {
            lev: doc.lev,
            editorMode: doc.editorMode
          },
          docs: [doc]
        };
      },
      buildTransferFilename: function (docs) {
        var levs = (docs || [])
          .map(function (item) {
            return toInt(item && item.lev, 0);
          })
          .filter(function (lev) {
            return lev > 0;
          })
          .sort(function (a, b) {
            return a - b;
          });
        var scope =
          levs.length <= 1
            ? String(levs[0] || this.currentLev || 1) + 'F'
            : 'all-' + levs.length + '-floors';
        var now = new Date();
        return (
          [
            'bas-map',
            scope,
            now.getFullYear(),
            padNumber(now.getMonth() + 1),
            padNumber(now.getDate()),
            padNumber(now.getHours()),
            padNumber(now.getMinutes()),
            padNumber(now.getSeconds())
          ].join('-') + '.json'
        );
      },
      requestEditorDoc: function (lev) {
        return new Promise(function (resolve, reject) {
          $.ajax({
            url: baseUrl + '/basMap/editor/' + lev + '/auth',
            method: 'GET',
            headers: authHeaders(),
            success: function (res) {
              if (!res || res.code !== 200 || !res.data) {
                reject(new Error(res && res.msg ? res.msg : '加载 ' + lev + 'F 地图失败'));
                return;
              }
              resolve(res.data);
            },
            error: function () {
              reject(new Error('加载 ' + lev + 'F 地图失败'));
            }
          });
        });
      },
      collectAllTransferDocs: function () {
        var self = this;
        var levMap = {};
        (this.remoteLevOptions || []).forEach(function (lev) {
          lev = toInt(lev, 0);
          if (lev > 0) {
            levMap[lev] = true;
          }
        });
        Object.keys(this.draftDocs || {}).forEach(function (key) {
          var lev = toInt(key, 0);
          if (lev > 0) {
            levMap[lev] = true;
          }
        });
        if (this.doc && this.doc.lev) {
          levMap[toInt(this.doc.lev, 0)] = true;
        }
        var levs = Object.keys(levMap)
          .map(function (key) {
            return toInt(key, 0);
          })
          .filter(function (lev) {
            return lev > 0;
          })
          .sort(function (a, b) {
            return a - b;
          });
        if (!levs.length) {
          return Promise.resolve([]);
        }
        return Promise.all(
          levs.map(function (lev) {
            if (self.doc && self.doc.lev === lev) {
              return Promise.resolve(self.exportDoc(self.doc));
            }
            if (self.draftDocs[lev] && self.draftDocs[lev].doc) {
              return Promise.resolve(self.exportDoc(self.draftDocs[lev].doc));
            }
            return self.requestEditorDoc(lev).then(function (doc) {
              return self.normalizeDoc(doc);
            });
          })
        );
      },
      exportMapPackage: function () {
        var self = this;
        if (!this.doc && (!this.remoteLevOptions || !this.remoteLevOptions.length)) {
          this.showMessage('warning', '当前没有可导出的地图');
          return;
        }
        this.collectAllTransferDocs()
          .then(function (docs) {
            if (!docs || !docs.length) {
              self.showMessage('warning', '当前没有可导出的地图');
              return;
            }
            var payload = {
              format: MAP_TRANSFER_FORMAT,
              exportedAt: new Date().toISOString(),
              source: {
                lev: self.currentLev || (docs[0] && docs[0].lev) || 1,
                editorMode: FREE_EDITOR_MODE
              },
              docs: docs.map(function (doc) {
                return self.exportDoc(doc);
              })
            };
            var blob = new Blob([JSON.stringify(payload, null, 2)], {
              type: 'application/json;charset=utf-8'
            });
            var href = window.URL.createObjectURL(blob);
            var link = document.createElement('a');
            link.href = href;
            link.download = self.buildTransferFilename(payload.docs);
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            window.setTimeout(function () {
              window.URL.revokeObjectURL(href);
            }, 0);
            self.showMessage('success', '已导出 ' + payload.docs.length + ' 个楼层的地图包');
          })
          .catch(function (error) {
            self.showMessage('error', error && error.message ? error.message : '导出地图失败');
          });
      },
      triggerImportMap: function () {
        if (this.$refs.mapImportInput) {
          this.$refs.mapImportInput.value = '';
          this.$refs.mapImportInput.click();
        }
      },
      parseTransferPackage: function (raw) {
        if (!raw) {
          return null;
        }
        if (raw.format === MAP_TRANSFER_FORMAT && Array.isArray(raw.docs) && raw.docs.length) {
          return {
            docs: raw.docs,
            activeLev: toInt(raw.source && raw.source.lev, 0)
          };
        }
        if (
          (raw.format === 'bas-map-editor-transfer-v1' || raw.format === MAP_TRANSFER_FORMAT) &&
          raw.doc
        ) {
          return {
            docs: [raw.doc],
            activeLev: toInt(raw.source && raw.source.lev, 0)
          };
        }
        if (raw.editorMode === FREE_EDITOR_MODE && Array.isArray(raw.elements)) {
          return {
            docs: [raw],
            activeLev: toInt(raw.lev, 0)
          };
        }
        return null;
      },
      importMapPackage: function (payload, options) {
        options = options || {};
        if (!payload || !Array.isArray(payload.docs) || !payload.docs.length) {
          this.showMessage('error', '导入文件格式不正确');
          return;
        }
        if (this.isDirty && options.skipConfirm !== true) {
          if (!window.confirm('导入地图会替换当前编辑态未保存内容,是否继续?')) {
            return;
          }
        }
        if (this.doc) {
          this.cacheCurrentDraft();
        }
        var self = this;
        var normalizedDocs = payload.docs
          .map(function (item) {
            return self.normalizeDoc(item);
          })
          .sort(function (a, b) {
            return toInt(a.lev, 0) - toInt(b.lev, 0);
          });
        normalizedDocs.forEach(function (doc) {
          self.setDraftDocEntry(doc.lev, doc, '');
        });
        var activeLev = toInt(payload.activeLev, 0);
        var targetDoc = normalizedDocs[0];
        for (var i = 0; i < normalizedDocs.length; i++) {
          if (normalizedDocs[i].lev === activeLev) {
            targetDoc = normalizedDocs[i];
            break;
          }
        }
        this.refreshLevOptions();
        this.floorPickerLev = targetDoc.lev;
        this.setCurrentDoc(targetDoc, { savedSnapshot: '' });
        if (normalizedDocs.length > 1) {
          this.showMessage(
            'success',
            '地图包已导入 ' + normalizedDocs.length + ' 个楼层,可点击“保存全部楼层”落库'
          );
          return;
        }
        this.showMessage('success', '地图包已导入,保存后才会覆盖运行地图');
      },
      handleImportMap: function (event) {
        var file = event && event.target && event.target.files ? event.target.files[0] : null;
        if (!file) {
          return;
        }
        var self = this;
        var reader = new FileReader();
        reader.onload = function (loadEvent) {
          try {
            var text = loadEvent && loadEvent.target ? loadEvent.target.result : '';
            var raw = JSON.parse(text || '{}');
            var payload = self.parseTransferPackage(raw);
            self.importMapPackage(payload);
          } catch (e) {
            self.showMessage('error', '地图文件解析失败');
          }
        };
        reader.onerror = function () {
          self.showMessage('error', '地图文件读取失败');
        };
        reader.readAsText(file, 'utf-8');
      },
      triggerImportExcel: function () {
        if (this.$refs.importInput) {
          this.$refs.importInput.value = '';
          this.$refs.importInput.click();
        }
      },
      handleImportExcel: function (event) {
        var self = this;
        var file = event && event.target && event.target.files ? event.target.files[0] : null;
        if (!file) {
          return;
        }
        var formData = new FormData();
        formData.append('file', file);
        $.ajax({
          url: baseUrl + '/basMap/editor/importExcel/auth',
          method: 'POST',
          headers: authHeaders(),
          data: formData,
          processData: false,
          contentType: false,
          success: function (res) {
            if (!res || res.code !== 200 || !Array.isArray(res.data) || res.data.length === 0) {
              self.showMessage('error', res && res.msg ? res.msg : 'Excel 导入失败');
              return;
            }
            res.data.forEach(function (item) {
              var doc = self.normalizeDoc(item);
              self.setDraftDocEntry(doc.lev, doc, '');
            });
            self.refreshLevOptions();
            self.floorPickerLev = toInt(res.data[0].lev, 0);
            self.setCurrentDoc(res.data[0], { savedSnapshot: '' });
            self.showMessage('success', 'Excel 已导入到编辑器,保存后才会覆盖运行地图');
          },
          error: function () {
            self.showMessage('error', 'Excel 导入失败');
          }
        });
      },
      handleFloorChange: function (lev) {
        lev = toInt(lev, 0);
        if (lev <= 0) {
          return;
        }
        this.floorPickerLev = lev;
        if (this.doc && this.doc.lev === lev && !this.loadingFloor) {
          this.switchingFloorLev = null;
          return;
        }
        if (this.doc) {
          this.cacheCurrentDraft();
        }
        this.clearFloorTransientState();
        this.resetRenderLayers();
        this.switchingFloorLev = lev;
        this.markGridSceneDirty();
        this.markStaticSceneDirty();
        this.scheduleRender();
        this.fetchFloor(lev);
      },
      loadCurrentFloor: function () {
        if (!this.currentLev) {
          this.showMessage('warning', '请先选择楼层');
          return;
        }
        if (
          this.isDirty &&
          !window.confirm('重新读取会丢弃当前楼层未保存的自由画布编辑,是否继续?')
        ) {
          return;
        }
        this.removeDraftDocEntry(this.currentLev);
        this.refreshLevOptions();
        this.fetchFloor(this.currentLev);
      },
      fetchFloor: function (lev) {
        var self = this;
        lev = toInt(lev, 0);
        if (lev <= 0) {
          return;
        }
        var requestSeq = ++this.floorRequestSeq;
        this.activeFloorRequestSeq = requestSeq;
        this.loadingFloor = true;
        this.switchingFloorLev = lev;
        $.ajax({
          url: baseUrl + '/basMap/editor/' + lev + '/auth',
          method: 'GET',
          headers: authHeaders(),
          success: function (res) {
            if (requestSeq !== self.activeFloorRequestSeq) {
              return;
            }
            self.loadingFloor = false;
            if (!res || res.code !== 200 || !res.data) {
              self.switchingFloorLev = null;
              self.floorPickerLev = self.currentLev;
              self.markGridSceneDirty();
              self.markStaticSceneDirty();
              self.scheduleRender();
              self.showMessage('error', res && res.msg ? res.msg : '加载地图失败');
              return;
            }
            var normalized = self.normalizeDoc(res.data);
            self.setDraftDocEntry(normalized.lev, normalized, self.snapshotDoc(normalized));
            self.setCurrentDoc(normalized, {
              savedSnapshot: self.snapshotDoc(normalized)
            });
          },
          error: function () {
            if (requestSeq !== self.activeFloorRequestSeq) {
              return;
            }
            self.loadingFloor = false;
            self.switchingFloorLev = null;
            self.floorPickerLev = self.currentLev;
            self.markGridSceneDirty();
            self.markStaticSceneDirty();
            self.scheduleRender();
            self.showMessage('error', '加载地图失败');
          }
        });
      },
      validateDocBeforeSave: function (doc) {
        var source = this.normalizeDoc(doc);
        if (!source || !source.lev) {
          return '楼层不能为空';
        }
        if (toNumber(source.canvasWidth, 0) <= 0 || toNumber(source.canvasHeight, 0) <= 0) {
          return '画布尺寸必须大于 0';
        }
        var elements = source.elements || [];
        for (var i = 0; i < elements.length; i++) {
          var element = elements[i];
          if (element.width <= 0 || element.height <= 0) {
            return '存在尺寸无效的元素';
          }
          if (element.x < 0 || element.y < 0) {
            return '元素坐标不能小于 0';
          }
          if (!isRectWithinCanvas(element, source.canvasWidth, source.canvasHeight)) {
            return '存在超出画布边界的元素: ' + element.id;
          }
          if (element.type === 'devp') {
            var value = safeParseJson(element.value);
            if (!value || toInt(value.stationId, 0) <= 0 || toInt(value.deviceNo, 0) <= 0) {
              return '输送线元素必须配置有效的 stationId 和 deviceNo';
            }
          }
        }
        var overlapId = findDocOverlapId(source);
        if (overlapId) {
          return '存在重叠元素: ' + overlapId;
        }
        return '';
      },
      validateBeforeSave: function () {
        return this.validateDocBeforeSave(this.doc);
      },
      requestSaveDoc: function (doc) {
        return new Promise(function (resolve, reject) {
          $.ajax({
            url: baseUrl + '/basMap/editor/save/auth',
            method: 'POST',
            headers: $.extend(
              {
                'Content-Type': 'application/json;charset=UTF-8'
              },
              authHeaders()
            ),
            data: JSON.stringify(doc),
            success: function (res) {
              if (!res || res.code !== 200) {
                reject(new Error(res && res.msg ? res.msg : '保存失败'));
                return;
              }
              resolve(res);
            },
            error: function () {
              reject(new Error('保存失败'));
            }
          });
        });
      },
      collectDirtyDocsForSave: function () {
        var result = [];
        var seen = {};
        if (this.doc && this.doc.lev && this.isDirty) {
          var currentDoc = this.exportDoc(this.doc);
          result.push(currentDoc);
          seen[currentDoc.lev] = true;
        }
        var self = this;
        Object.keys(this.draftDocs || {}).forEach(function (key) {
          var lev = toInt(key, 0);
          if (lev <= 0 || seen[lev]) {
            return;
          }
          var entry = self.draftDocs[lev];
          if (!entry || !entry.doc) {
            return;
          }
          var snapshot = self.snapshotDoc(entry.doc);
          if (snapshot === (entry.savedSnapshot || '')) {
            return;
          }
          var doc = self.exportDoc(entry.doc);
          result.push(doc);
          seen[doc.lev] = true;
        });
        result.sort(function (a, b) {
          return toInt(a.lev, 0) - toInt(b.lev, 0);
        });
        return result;
      },
      markDocSavedState: function (doc) {
        var normalized = this.normalizeDoc(doc);
        var savedSnapshot = this.snapshotDoc(normalized);
        this.setDraftDocEntry(normalized.lev, normalized, savedSnapshot);
        if (this.doc && this.doc.lev === normalized.lev) {
          this.savedSnapshot = savedSnapshot;
          this.syncDirty();
        }
      },
      saveDoc: function () {
        var self = this;
        if (!this.doc) {
          return;
        }
        var error = this.validateBeforeSave();
        if (error) {
          this.showMessage('warning', error);
          return;
        }
        this.saving = true;
        var payload = this.exportDoc(this.doc);
        this.requestSaveDoc(payload)
          .then(function () {
            self.saving = false;
            self.savedSnapshot = self.snapshotDoc(self.doc);
            self.syncDirty();
            self.clearCurrentDraftIfSaved();
            self.refreshLevOptions();
            self.showMessage('success', '当前楼层已保存并编译到运行地图');
          })
          .catch(function (error) {
            self.saving = false;
            self.showMessage('error', error && error.message ? error.message : '保存失败');
          });
      },
      saveAllDocs: function () {
        var self = this;
        if (this.saving || this.savingAll) {
          return;
        }
        var docs = this.collectDirtyDocsForSave();
        if (!docs.length) {
          this.showMessage('warning', '当前没有需要保存的楼层');
          return;
        }
        if (
          docs.length > 1 &&
          !window.confirm('将保存 ' + docs.length + ' 个楼层到运行地图,是否继续?')
        ) {
          return;
        }
        for (var i = 0; i < docs.length; i++) {
          var error = this.validateDocBeforeSave(docs[i]);
          if (error) {
            this.showMessage('warning', docs[i].lev + 'F 保存前校验失败: ' + error);
            return;
          }
        }
        this.savingAll = true;
        var index = 0;
        var total = docs.length;
        var next = function () {
          if (index >= total) {
            self.savingAll = false;
            self.refreshLevOptions();
            self.showMessage('success', '已保存 ' + total + ' 个楼层到运行地图');
            return;
          }
          var doc = docs[index++];
          self
            .requestSaveDoc(doc)
            .then(function () {
              self.markDocSavedState(doc);
              next();
            })
            .catch(function (error) {
              self.savingAll = false;
              self.showMessage(
                'error',
                doc.lev + 'F 保存失败: ' + (error && error.message ? error.message : '保存失败')
              );
            });
        };
        next();
      },
      setTool: function (tool) {
        this.activeTool = tool;
        this.updateCursor();
      },
      findElementById: function (id) {
        if (!this.doc || !id) {
          return null;
        }
        var elements = this.doc.elements || [];
        for (var i = 0; i < elements.length; i++) {
          if (elements[i].id === id) {
            return elements[i];
          }
        }
        return null;
      },
      getSelectedElements: function () {
        var self = this;
        return this.selectedIds
          .map(function (id) {
            return self.findElementById(id);
          })
          .filter(Boolean);
      },
      refreshInspector: function () {
        var element = this.singleSelectedElement;
        if (!this.doc) {
          this.canvasForm = {
            width: String(DEFAULT_CANVAS_WIDTH),
            height: String(DEFAULT_CANVAS_HEIGHT)
          };
          this.valueEditorText = '';
          this.resetDevpForm();
          this.resetDeviceForm();
          return;
        }
        this.canvasForm = {
          width: String(Math.round(this.doc.canvasWidth)),
          height: String(Math.round(this.doc.canvasHeight))
        };
        if (!element) {
          this.geometryForm = { x: '', y: '', width: '', height: '' };
          this.valueEditorText = '';
          this.resetDevpForm();
          this.resetDeviceForm();
          return;
        }
        this.geometryForm = {
          x: String(this.formatNumber(element.x)),
          y: String(this.formatNumber(element.y)),
          width: String(this.formatNumber(element.width)),
          height: String(this.formatNumber(element.height))
        };
        this.valueEditorText = element.value || '';
        if (element.type === 'devp') {
          this.loadDevpForm(element.value);
        } else {
          this.resetDevpForm();
        }
        if (isDeviceConfigType(element.type)) {
          this.loadDeviceForm(element.type, element.value);
        } else {
          this.resetDeviceForm();
        }
        this.ensureShelfFillStartValue();
      },
      resetDevpForm: function () {
        this.devpForm = {
          stationId: '',
          deviceNo: '',
          direction: [],
          isBarcodeStation: false,
          barcodeIdx: '',
          backStation: '',
          backStationDeviceNo: '',
          isInStation: false,
          barcodeStation: '',
          barcodeStationDeviceNo: '',
          isOutStation: false,
          runBlockReassign: false,
          isOutOrder: false,
          isLiftTransfer: false
        };
      },
      resetDeviceForm: function () {
        this.deviceForm = {
          trackId: '',
          barCodeStart: 0,
          barCodeEnd: 100000,
          deviceList: [
            {
              valueKey: '',
              deviceNo: '',
              progress: 0,
              deviceLength: '',
              deviceWidth: ''
            }
          ]
        };
      },
      ensureShelfFillStartValue: function () {
        var element = this.singleSelectedElement;
        if (!element || !isShelfLikeNodeType(element.type)) {
          return;
        }
        if (
          !this.shelfFillForm.startValue ||
          !parseShelfLocationValue(this.shelfFillForm.startValue)
        ) {
          this.shelfFillForm.startValue = normalizeValue(element.value || '');
        }
      },
      loadDevpForm: function (value) {
        this.resetDevpForm();
        var json = safeParseJson(value);
        if (!json) {
          return;
        }
        this.devpForm.stationId = json.stationId != null ? String(json.stationId) : '';
        this.devpForm.deviceNo = json.deviceNo != null ? String(json.deviceNo) : '';
        this.devpForm.direction = normalizeDirectionList(json.direction);
        this.devpForm.isBarcodeStation = boolFlag(json.isBarcodeStation);
        this.devpForm.barcodeIdx = json.barcodeIdx != null ? String(json.barcodeIdx) : '';
        this.devpForm.backStation = json.backStation != null ? String(json.backStation) : '';
        this.devpForm.backStationDeviceNo =
          json.backStationDeviceNo != null ? String(json.backStationDeviceNo) : '';
        this.devpForm.isInStation = boolFlag(json.isInStation);
        this.devpForm.barcodeStation =
          json.barcodeStation != null ? String(json.barcodeStation) : '';
        this.devpForm.barcodeStationDeviceNo =
          json.barcodeStationDeviceNo != null ? String(json.barcodeStationDeviceNo) : '';
        this.devpForm.isOutStation = boolFlag(json.isOutStation);
        this.devpForm.runBlockReassign = boolFlag(json.runBlockReassign);
        this.devpForm.isOutOrder = boolFlag(json.isOutOrder);
        this.devpForm.isLiftTransfer = boolFlag(json.isLiftTransfer);
      },
      getDeviceConfigLabel: function (type) {
        var meta = getTypeMeta(type);
        return meta.label + '参数';
      },
      getDeviceConfigKeyLabel: function (type, valueKey) {
        if (valueKey === 'crnNo') {
          return 'crnNo';
        }
        if (valueKey === 'rgvNo') {
          return 'rgvNo';
        }
        return type === 'rgv' ? 'deviceNo / rgvNo' : 'deviceNo / crnNo';
      },
      getAutoTrackDeviceBox: function () {
        var element = this.singleSelectedDeviceElement;
        if (!element || !G || !G.getAutoTrackDeviceBox) {
          return null;
        }
        return G.getAutoTrackDeviceBox(element);
      },
      getDeviceLengthPlaceholder: function () {
        var box = this.getAutoTrackDeviceBox();
        return box && box.along ? '默认: ' + box.along : '';
      },
      getDeviceWidthPlaceholder: function () {
        var box = this.getAutoTrackDeviceBox();
        return box && box.across ? '默认: ' + box.across : '';
      },
      loadDeviceForm: function (type, value) {
        this.resetDeviceForm();
        if (!isDeviceConfigType(type)) {
          return;
        }
        var json = safeParseJson(value) || {};
        this.deviceForm = {
          trackId: '',
          barCodeStart: 0,
          barCodeEnd: 100000,
          deviceList: [
            {
              valueKey: '',
              deviceNo: '',
              progress: 0,
              deviceLength: '',
              deviceWidth: ''
            }
          ],
          ...json
        };
        var trackId = toInt(this.deviceForm.trackId, 0);
        if (trackId <= 0) {
          this.deviceForm.trackId = String(this.getNextDeviceTrackId(this.singleSelectedElement));
        } else {
          this.deviceForm.trackId = String(trackId);
        }
        this.deviceForm.barCodeStart = toInt(this.deviceForm.barCodeStart, 0);
        this.deviceForm.barCodeEnd = toInt(this.deviceForm.barCodeEnd, 100000);
      },
      getNextDeviceTrackId: function (excludeElement) {
        if (!this.doc || !Array.isArray(this.doc.elements)) {
          return 1;
        }
        var excludeId = excludeElement && excludeElement.id ? String(excludeElement.id) : '';
        var maxValue = 0;
        for (var i = 0; i < this.doc.elements.length; i++) {
          var item = this.doc.elements[i];
          if (!item || (excludeId && item.id === excludeId) || !isDeviceConfigType(item.type)) {
            continue;
          }
          var json = safeParseJson(item.value);
          if (!json) {
            continue;
          }
          var trackId = toInt(json.trackId, 0);
          if (trackId > maxValue) {
            maxValue = trackId;
          }
        }
        return Math.max(1, maxValue + 1);
      },
      isDevpDirectionActive: function (directionKey) {
        return this.devpForm.direction.indexOf(directionKey) >= 0;
      },
      toggleDevpDirection: function (directionKey) {
        if (!directionKey) {
          return;
        }
        var next = this.devpForm.direction.slice();
        var index = next.indexOf(directionKey);
        if (index >= 0) {
          next.splice(index, 1);
        } else {
          next.push(directionKey);
        }
        this.devpForm.direction = DEVP_DIRECTION_OPTIONS.map(function (item) {
          return item.key;
        }).filter(function (item) {
          return next.indexOf(item) >= 0;
        });
      },
      applyCanvasSize: function () {
        var self = this;
        if (!this.doc) {
          return;
        }
        var width = toNumber(this.canvasForm.width, 0);
        var height = toNumber(this.canvasForm.height, 0);
        if (width <= 0 || height <= 0) {
          this.showMessage('warning', '画布尺寸必须大于 0');
          return;
        }
        var bounds = this.getElementBounds(
          (this.doc.elements || []).map(function (item) {
            return item.id;
          })
        );
        if (bounds && (width < bounds.x + bounds.width || height < bounds.y + bounds.height)) {
          this.showMessage('warning', '画布不能小于当前元素占用范围');
          return;
        }
        this.runMutation(function () {
          self.doc.canvasWidth = roundCoord(width);
          self.doc.canvasHeight = roundCoord(height);
        });
      },
      applyGeometry: function () {
        var self = this;
        var element = this.singleSelectedElement;
        if (!element) {
          return;
        }
        var next = {
          x: roundCoord(Math.max(0, toNumber(this.geometryForm.x, element.x))),
          y: roundCoord(Math.max(0, toNumber(this.geometryForm.y, element.y))),
          width: roundCoord(
            Math.max(MIN_ELEMENT_SIZE, toNumber(this.geometryForm.width, element.width))
          ),
          height: roundCoord(
            Math.max(MIN_ELEMENT_SIZE, toNumber(this.geometryForm.height, element.height))
          )
        };
        if (!this.isWithinCanvas(next)) {
          this.showMessage('warning', '几何属性超出当前画布范围');
          return;
        }
        var preview = deepClone(element);
        preview.x = next.x;
        preview.y = next.y;
        preview.width = next.width;
        preview.height = next.height;
        if (this.hasOverlap(preview, [preview.id])) {
          this.showMessage('warning', '调整后会与其他元素重叠');
          return;
        }
        this.runMutation(function () {
          element.x = next.x;
          element.y = next.y;
          element.width = next.width;
          element.height = next.height;
        });
      },
      applyRawValue: function () {
        var self = this;
        var element = this.singleSelectedElement;
        if (!element || element.type === 'devp') {
          return;
        }
        this.runMutation(function () {
          element.value = normalizeValue(self.valueEditorText);
        });
      },
      addDeviceForm: function () {
        this.deviceForm.deviceList.push({
          valueKey: '',
          deviceNo: '',
          progress: 0,
          deviceLength: '',
          deviceWidth: ''
        });
      },
      applyDeviceForm: function () {
        var self = this;
        var element = this.singleSelectedDeviceElement;
        if (!element) {
          return;
        }
        var trackId = toInt(this.deviceForm.trackId, 0);
        if (trackId <= 0) {
          this.showMessage('warning', '轨道ID必须大于 0');
          return;
        }
        if (
          !this.deviceForm.deviceList ||
          this.deviceForm.deviceList.length === 0 ||
          this.deviceForm.deviceList.some((item) => item.deviceNo === '')
        ) {
          this.showMessage('warning', '设备列表不能为空');
          return;
        }
        var valueKey = pickDeviceValueKey(element.type);
        this.runMutation(function () {
          var payload = safeParseJson(element.value) || {};
          delete payload.deviceNo;
          delete payload.crnNo;
          delete payload.rgvNo;
          self.deviceForm.deviceList.forEach((item) => {
            item.valueKey = valueKey;
            // 允许通过属性面板覆盖默认设备像素尺寸(沿轨道/垂直轨道)
            var deviceLength = toInt(item.deviceLength, 0);
            var deviceWidth = toInt(item.deviceWidth, 0);
            if (deviceLength > 0) {
              item.deviceLength = String(deviceLength);
            } else {
              delete item.deviceLength;
            }
            if (deviceWidth > 0) {
              item.deviceWidth = String(deviceWidth);
            } else {
              delete item.deviceWidth;
            }
          });
          self.deviceForm.trackId = String(trackId);
          self.deviceForm.barCodeStart = toInt(self.deviceForm.barCodeStart, 0);
          self.deviceForm.barCodeEnd = toInt(self.deviceForm.barCodeEnd, 100000);
          element.value = JSON.stringify(self.deviceForm);
          self.valueEditorText = element.value;
        });
      },
      applyDevpForm: function () {
        var self = this;
        var element = this.singleSelectedElement;
        if (!element || element.type !== 'devp') {
          return;
        }
        var stationId = toInt(this.devpForm.stationId, 0);
        var deviceNo = toInt(this.devpForm.deviceNo, 0);
        if (stationId <= 0 || deviceNo <= 0) {
          this.showMessage('warning', '站号和 PLC 编号必须大于 0');
          return;
        }
        var payload = {
          stationId: stationId,
          deviceNo: deviceNo
        };
        var directionList = normalizeDirectionList(this.devpForm.direction);
        if (directionList.length > 0) {
          payload.direction = directionList;
        }
        var barcodeIdx = this.devpForm.barcodeIdx === '' ? 0 : toInt(this.devpForm.barcodeIdx, 0);
        var backStation =
          this.devpForm.backStation === '' ? 0 : toInt(this.devpForm.backStation, 0);
        var backStationDeviceNo =
          this.devpForm.backStationDeviceNo === ''
            ? 0
            : toInt(this.devpForm.backStationDeviceNo, 0);
        var barcodeStation =
          this.devpForm.barcodeStation === '' ? 0 : toInt(this.devpForm.barcodeStation, 0);
        var barcodeStationDeviceNo =
          this.devpForm.barcodeStationDeviceNo === ''
            ? 0
            : toInt(this.devpForm.barcodeStationDeviceNo, 0);
        if (this.devpForm.isInStation && (barcodeStation <= 0 || barcodeStationDeviceNo <= 0)) {
          this.showMessage('warning', '入站点必须填写条码站和条码站 PLC 编号');
          return;
        }
        if (
          this.devpForm.isBarcodeStation &&
          (backStation <= 0 || backStationDeviceNo <= 0 || barcodeIdx <= 0)
        ) {
          this.showMessage('warning', '条码站必须填写条码索引、退回站和退回站 PLC 编号');
          return;
        }
        if (this.devpForm.isBarcodeStation) {
          payload.isBarcodeStation = 1;
        }
        if (barcodeIdx > 0) {
          payload.barcodeIdx = barcodeIdx;
        }
        if (backStation > 0) {
          payload.backStation = backStation;
        }
        if (backStationDeviceNo > 0) {
          payload.backStationDeviceNo = backStationDeviceNo;
        }
        if (this.devpForm.isInStation) {
          payload.isInStation = 1;
        }
        if (barcodeStation > 0) {
          payload.barcodeStation = barcodeStation;
        }
        if (barcodeStationDeviceNo > 0) {
          payload.barcodeStationDeviceNo = barcodeStationDeviceNo;
        }
        if (this.devpForm.isOutStation) {
          payload.isOutStation = 1;
        }
        if (this.devpForm.runBlockReassign) {
          payload.runBlockReassign = 1;
        }
        if (this.devpForm.isOutOrder) {
          payload.isOutOrder = 1;
        }
        if (this.devpForm.isLiftTransfer) {
          payload.isLiftTransfer = 1;
        }
        this.runMutation(function () {
          element.value = JSON.stringify(payload);
          self.valueEditorText = element.value;
        });
      },
      deleteSelection: function () {
        var self = this;
        if (!this.doc || this.selectedIds.length === 0) {
          return;
        }
        var ids = this.selectedIds.slice();
        this.runMutation(function () {
          self.doc.elements = self.doc.elements.filter(function (item) {
            return ids.indexOf(item.id) === -1;
          });
          self.selectedIds = [];
        });
      },
      copySelection: function () {
        var elements = this.getSelectedElements();
        if (!elements.length) {
          return;
        }
        this.clipboard = deepClone(elements);
        this.showMessage('success', '已复制 ' + elements.length + ' 个元素');
      },
      getElementListBounds: function (elements) {
        if (!elements || !elements.length) {
          return null;
        }
        var minX = elements[0].x;
        var minY = elements[0].y;
        var maxX = elements[0].x + elements[0].width;
        var maxY = elements[0].y + elements[0].height;
        for (var i = 1; i < elements.length; i++) {
          var element = elements[i];
          minX = Math.min(minX, element.x);
          minY = Math.min(minY, element.y);
          maxX = Math.max(maxX, element.x + element.width);
          maxY = Math.max(maxY, element.y + element.height);
        }
        return {
          x: minX,
          y: minY,
          width: maxX - minX,
          height: maxY - minY
        };
      },
      getPasteTargetWorld: function () {
        if (!this.doc) {
          return { x: 0, y: 0 };
        }
        var visible = this.getVisibleCanvasRect
          ? this.getVisibleCanvasRect()
          : this.getVisibleWorldRect();
        var fallback = {
          x: visible.x + visible.width / 2,
          y: visible.y + visible.height / 2
        };
        if (!this.lastPointerWorld) {
          return fallback;
        }
        return {
          x: clamp(this.lastPointerWorld.x, 0, this.doc.canvasWidth),
          y: clamp(this.lastPointerWorld.y, 0, this.doc.canvasHeight),
          screenX: this.lastPointerWorld.screenX,
          screenY: this.lastPointerWorld.screenY
        };
      },
      pasteClipboard: function () {
        var self = this;
        if (!this.doc || !this.clipboard.length) {
          return;
        }
        var sourceBounds = this.getElementListBounds(this.clipboard);
        if (!sourceBounds) {
          return;
        }
        var target = this.getPasteTargetWorld();
        var offsetX = target.x - (sourceBounds.x + sourceBounds.width / 2);
        var offsetY = target.y - (sourceBounds.y + sourceBounds.height / 2);
        var minOffsetX = -sourceBounds.x;
        var maxOffsetX = this.doc.canvasWidth - (sourceBounds.x + sourceBounds.width);
        var minOffsetY = -sourceBounds.y;
        var maxOffsetY = this.doc.canvasHeight - (sourceBounds.y + sourceBounds.height);
        offsetX = clamp(offsetX, minOffsetX, maxOffsetX);
        offsetY = clamp(offsetY, minOffsetY, maxOffsetY);
        var copies = deepClone(this.clipboard).map(function (item) {
          item.id = nextId();
          item.x = roundCoord(item.x + offsetX);
          item.y = roundCoord(item.y + offsetY);
          return item;
        });
        if (!this.canPlaceElements(copies, [])) {
          this.showMessage('warning', '粘贴后的元素与现有元素重叠或超出画布');
          return;
        }
        this.runMutation(function () {
          self.doc.elements = self.doc.elements.concat(copies);
          self.selectedIds = copies.map(function (item) {
            return item.id;
          });
        });
      },
      canArrayFromElement: function (element) {
        return !!(element && ARRAY_TEMPLATE_TYPES.indexOf(element.type) >= 0);
      },
      getShelfFillSteps: function () {
        return {
          row: this.shelfFillForm.rowStep === 'asc' ? 1 : -1,
          col: this.shelfFillForm.colStep === 'desc' ? -1 : 1
        };
      },
      applyShelfSequenceToArrayCopies: function (template, copies) {
        if (!template || !isShelfLikeNodeType(template.type) || !copies || !copies.length) {
          return copies;
        }
        var base =
          parseShelfLocationValue(template.value) ||
          parseShelfLocationValue(this.shelfFillForm.startValue);
        if (!base) {
          return copies;
        }
        var steps = this.getShelfFillSteps();
        var horizontal = Math.abs(copies[0].x - template.x) >= Math.abs(copies[0].y - template.y);
        var direction = 1;
        if (horizontal) {
          direction = copies[0].x >= template.x ? 1 : -1;
        } else {
          direction = copies[0].y >= template.y ? 1 : -1;
        }
        for (var i = 0; i < copies.length; i++) {
          var offset = i + 1;
          var row = base.row;
          var col = base.col;
          if (horizontal) {
            col = base.col + steps.col * direction * offset;
          } else {
            row = base.row + steps.row * direction * offset;
          }
          copies[i].value = formatShelfLocationValue(row, col);
        }
        return copies;
      },
      buildShelfGridAssignments: function (elements) {
        if (!elements || !elements.length) {
          return null;
        }
        var clusterAxis = function (list, axis, sizeKey) {
          var sorted = list
            .map(function (item) {
              return {
                id: item.id,
                center: item[axis] + item[sizeKey] / 2,
                size: item[sizeKey]
              };
            })
            .sort(function (a, b) {
              return a.center - b.center;
            });
          var avgSize =
            sorted.reduce(function (sum, item) {
              return sum + item.size;
            }, 0) / sorted.length;
          var tolerance = Math.max(6, avgSize * 0.45);
          var groups = [];
          for (var i = 0; i < sorted.length; i++) {
            var current = sorted[i];
            var last = groups.length ? groups[groups.length - 1] : null;
            if (!last || Math.abs(current.center - last.center) > tolerance) {
              groups.push({
                center: current.center,
                items: [current]
              });
            } else {
              last.items.push(current);
              last.center =
                last.items.reduce(function (sum, item) {
                  return sum + item.center;
                }, 0) / last.items.length;
            }
          }
          var indexById = {};
          for (var groupIndex = 0; groupIndex < groups.length; groupIndex++) {
            for (var itemIndex = 0; itemIndex < groups[groupIndex].items.length; itemIndex++) {
              indexById[groups[groupIndex].items[itemIndex].id] = groupIndex;
            }
          }
          return indexById;
        };
        return {
          rowById: clusterAxis(elements, 'y', 'height'),
          colById: clusterAxis(elements, 'x', 'width')
        };
      },
      applyShelfAutoFill: function () {
        var self = this;
        var shelves = this.selectedShelfElements.slice();
        if (!shelves.length) {
          this.showMessage('warning', '请先选中至少一个货架或维修站台');
          return;
        }
        var start = parseShelfLocationValue(this.shelfFillForm.startValue);
        if (!start) {
          this.showMessage('warning', '起始值格式必须是 排-列,例如 12-1');
          return;
        }
        var grid = this.buildShelfGridAssignments(shelves);
        if (!grid) {
          return;
        }
        var steps = this.getShelfFillSteps();
        this.runMutation(function () {
          shelves.forEach(function (item) {
            var rowIndex = grid.rowById[item.id] || 0;
            var colIndex = grid.colById[item.id] || 0;
            item.value = formatShelfLocationValue(
              start.row + rowIndex * steps.row,
              start.col + colIndex * steps.col
            );
          });
          if (self.singleSelectedElement && isShelfLikeNodeType(self.singleSelectedElement.type)) {
            self.valueEditorText = self.singleSelectedElement.value || '';
          }
        });
      },
      buildArrayCopies: function (template, startWorld, currentWorld) {
        if (
          !this.doc ||
          !template ||
          !startWorld ||
          !currentWorld ||
          !this.canArrayFromElement(template)
        ) {
          return [];
        }
        var deltaX = currentWorld.x - startWorld.x;
        var deltaY = currentWorld.y - startWorld.y;
        if (Math.abs(deltaX) < COORD_EPSILON && Math.abs(deltaY) < COORD_EPSILON) {
          return [];
        }
        var horizontal = Math.abs(deltaX) >= Math.abs(deltaY);
        var step = horizontal ? template.width : template.height;
        if (step <= COORD_EPSILON) {
          return [];
        }
        var direction = (horizontal ? deltaX : deltaY) >= 0 ? 1 : -1;
        var distance;
        if (horizontal) {
          distance =
            direction > 0
              ? currentWorld.x - (template.x + template.width)
              : template.x - currentWorld.x;
        } else {
          distance =
            direction > 0
              ? currentWorld.y - (template.y + template.height)
              : template.y - currentWorld.y;
        }
        var count = Math.max(0, Math.floor((distance + step * 0.5) / step));
        if (count <= 0) {
          return [];
        }
        var copies = [];
        for (var i = 1; i <= count; i++) {
          copies.push({
            type: template.type,
            x: roundCoord(template.x + (horizontal ? direction * template.width * i : 0)),
            y: roundCoord(template.y + (horizontal ? 0 : direction * template.height * i)),
            width: template.width,
            height: template.height,
            value: template.value
          });
        }
        return this.applyShelfSequenceToArrayCopies(template, copies);
      },
      duplicateSelection: function () {
        this.copySelection();
        this.pasteClipboard();
      },
      getElementBounds: function (ids) {
        if (!this.doc) {
          return null;
        }
        var elements = ids && ids.length ? this.getSelectedElements() : this.doc.elements || [];
        if (ids && ids.length) {
          elements = ids
            .map(function (id) {
              return this.findElementById(id);
            }, this)
            .filter(Boolean);
        }
        if (!elements.length) {
          return null;
        }
        var minX = elements[0].x;
        var minY = elements[0].y;
        var maxX = elements[0].x + elements[0].width;
        var maxY = elements[0].y + elements[0].height;
        for (var i = 1; i < elements.length; i++) {
          var element = elements[i];
          minX = Math.min(minX, element.x);
          minY = Math.min(minY, element.y);
          maxX = Math.max(maxX, element.x + element.width);
          maxY = Math.max(maxY, element.y + element.height);
        }
        return {
          x: minX,
          y: minY,
          width: maxX - minX,
          height: maxY - minY
        };
      },
      fitContent: function () {
        if (!this.doc || !this.pixiApp) {
          return;
        }
        var contentBounds = this.getElementBounds();
        if (contentBounds && contentBounds.width > 0 && contentBounds.height > 0) {
          this.fitRect(contentBounds, this.pixiApp.renderer.width, this.pixiApp.renderer.height);
          return;
        }
        this.fitCanvas();
      },
      fitCanvas: function () {
        if (!this.doc || !this.pixiApp) {
          return;
        }
        var renderer = this.pixiApp.renderer;
        var target = {
          x: 0,
          y: 0,
          width: Math.max(1, this.doc.canvasWidth),
          height: Math.max(1, this.doc.canvasHeight)
        };
        this.fitRect(target, renderer.width, renderer.height);
      },
      fitSelection: function () {
        if (!this.selectedIds.length || !this.pixiApp) {
          return;
        }
        var bounds = this.getElementBounds(this.selectedIds);
        if (!bounds) {
          return;
        }
        this.fitRect(bounds, this.pixiApp.renderer.width, this.pixiApp.renderer.height);
      },
      fitRect: function (rect, viewportWidth, viewportHeight) {
        var padding = 80;
        var scale = Math.min(
          (viewportWidth - padding * 2) / Math.max(rect.width, 1),
          (viewportHeight - padding * 2) / Math.max(rect.height, 1)
        );
        scale = clamp(scale, 0.06, 4);
        this.camera.scale = scale;
        this.camera.x = Math.round((viewportWidth - rect.width * scale) / 2 - rect.x * scale);
        this.camera.y = Math.round((viewportHeight - rect.height * scale) / 2 - rect.y * scale);
        this.viewZoom = scale;
        this.markGridSceneDirty();
        this.markStaticSceneDirty();
        this.scheduleRender();
      },
      resetView: function () {
        this.fitCanvas();
      },
      getVisibleWorldRect: function () {
        if (!this.pixiApp) {
          return {
            x: 0,
            y: 0,
            width: 0,
            height: 0
          };
        }
        return {
          x: -this.camera.x / this.camera.scale,
          y: -this.camera.y / this.camera.scale,
          width: this.pixiApp.renderer.width / this.camera.scale,
          height: this.pixiApp.renderer.height / this.camera.scale
        };
      },
      getVisibleCanvasRect: function () {
        if (!this.doc) {
          return {
            x: 0,
            y: 0,
            width: 0,
            height: 0
          };
        }
        var visible = this.getVisibleWorldRect();
        var left = clamp(visible.x, 0, this.doc.canvasWidth);
        var top = clamp(visible.y, 0, this.doc.canvasHeight);
        var right = clamp(visible.x + visible.width, 0, this.doc.canvasWidth);
        var bottom = clamp(visible.y + visible.height, 0, this.doc.canvasHeight);
        return {
          x: left,
          y: top,
          width: Math.max(0, right - left),
          height: Math.max(0, bottom - top)
        };
      },
      getWorldRectWithPadding: function (screenPadding) {
        if (!this.doc) {
          return {
            x: 0,
            y: 0,
            width: 0,
            height: 0
          };
        }
        var visible = this.getVisibleWorldRect();
        var padding = Math.max(screenPadding / this.camera.scale, 24);
        var left = Math.max(0, visible.x - padding);
        var top = Math.max(0, visible.y - padding);
        var right = Math.min(this.doc.canvasWidth, visible.x + visible.width + padding);
        var bottom = Math.min(this.doc.canvasHeight, visible.y + visible.height + padding);
        return {
          x: left,
          y: top,
          width: Math.max(0, right - left),
          height: Math.max(0, bottom - top)
        };
      },
      worldRectContains: function (outer, inner) {
        if (!outer || !inner) {
          return false;
        }
        return (
          inner.x >= outer.x - COORD_EPSILON &&
          inner.y >= outer.y - COORD_EPSILON &&
          inner.x + inner.width <= outer.x + outer.width + COORD_EPSILON &&
          inner.y + inner.height <= outer.y + outer.height + COORD_EPSILON
        );
      },
      getGridRenderKey: function () {
        var minorStep = this.camera.scale > 1.5 ? 50 : this.camera.scale > 0.45 ? 100 : 200;
        return minorStep + '|' + Math.round(this.camera.scale * 8) / 8;
      },
      getStaticRenderKey: function () {
        return (
          (this.camera.scale >= 0.85 ? 'round' : 'flat') +
          '|' +
          Math.round(this.camera.scale * 8) / 8
        );
      },
      scheduleRender: function () {
        if (this.renderQueued) {
          return;
        }
        this.renderQueued = true;
        window.requestAnimationFrame(
          function () {
            this.renderQueued = false;
            this.renderScene();
          }.bind(this)
        );
      },
      renderScene: function () {
        if (!this.pixiApp || !this.doc) {
          return;
        }
        this.mapRoot.position.set(this.camera.x, this.camera.y);
        this.mapRoot.scale.set(this.camera.scale, this.camera.scale);
        this.viewZoom = this.camera.scale;
        var visible = this.getVisibleCanvasRect();
        var viewportSettled =
          !this.isZooming &&
          !this.isPanning &&
          !(this.interactionState && this.interactionState.type === 'pan');
        var gridKeyChanged = this.gridRenderKey !== this.getGridRenderKey();
        if (
          this.gridSceneDirty ||
          !this.gridRenderRect ||
          (viewportSettled && gridKeyChanged) ||
          (viewportSettled && !this.worldRectContains(this.gridRenderRect, visible))
        ) {
          this.renderGrid(this.getWorldRectWithPadding(STATIC_VIEW_PADDING));
          this.gridSceneDirty = false;
        }
        var excludedKey = this.selectionKey(this.getStaticExcludedIds());
        var staticKeyChanged = this.staticRenderKey !== this.getStaticRenderKey();
        if (
          this.staticSceneDirty ||
          !this.staticRenderRect ||
          (viewportSettled && staticKeyChanged) ||
          this.staticExcludedKey !== excludedKey ||
          (viewportSettled && !this.worldRectContains(this.staticRenderRect, visible))
        ) {
          this.renderStaticElements(this.getWorldRectWithPadding(STATIC_VIEW_PADDING), excludedKey);
          this.staticSceneDirty = false;
        }
        this.renderActiveElements();
        this.renderLabels();
        this.renderHover();
        this.renderSelection();
        this.renderGuide();
        this.updateCursor();
      },
      getStaticExcludedIds: function () {
        if (!this.interactionState) {
          return [];
        }
        if (this.interactionState.type === 'move' && this.selectedIds.length) {
          return this.selectedIds.slice();
        }
        if (this.interactionState.type === 'resize' && this.interactionState.elementId) {
          return [this.interactionState.elementId];
        }
        return [];
      },
      getRenderableElements: function (excludeIds, renderRect) {
        if (!this.doc) {
          return [];
        }
        var rect = renderRect || this.getWorldRectWithPadding(STATIC_VIEW_PADDING);
        var candidates = this.querySpatialCandidates(rect, 0, excludeIds);
        var result = [];
        for (var i = 0; i < candidates.length; i++) {
          if (rectIntersects(rect, candidates[i])) {
            result.push(candidates[i]);
          }
        }
        return result;
      },
      renderGrid: function (renderRect) {
        if (!this.gridLayer || !this.doc) {
          return;
        }
        var visible = renderRect || this.getVisibleWorldRect();
        var width = this.doc.canvasWidth;
        var height = this.doc.canvasHeight;
        var minorStep = this.camera.scale > 1.5 ? 50 : this.camera.scale > 0.45 ? 100 : 200;
        var majorStep = minorStep * 5;
        var lineWidth = 1 / this.camera.scale;
        var xStart = Math.max(0, Math.floor(visible.x / minorStep) * minorStep);
        var yStart = Math.max(0, Math.floor(visible.y / minorStep) * minorStep);
        var xEnd = Math.min(width, visible.x + visible.width);
        var yEnd = Math.min(height, visible.y + visible.height);
        this.gridLayer.clear();
        this.gridLayer.beginFill(0xfafcff, 1);
        this.gridLayer.drawRect(0, 0, width, height);
        this.gridLayer.endFill();
        this.gridLayer.lineStyle(lineWidth, 0xdbe4ee, 1);
        this.gridLayer.drawRect(0, 0, width, height);
        for (var x = xStart; x <= xEnd; x += minorStep) {
          var colorX = x % majorStep === 0 ? 0xc9d7e6 : 0xe4ebf3;
          this.gridLayer.lineStyle(lineWidth, colorX, x % majorStep === 0 ? 0.95 : 0.75);
          this.gridLayer.moveTo(x, 0);
          this.gridLayer.lineTo(x, height);
        }
        for (var y = yStart; y <= yEnd; y += minorStep) {
          var colorY = y % majorStep === 0 ? 0xc9d7e6 : 0xe4ebf3;
          this.gridLayer.lineStyle(lineWidth, colorY, y % majorStep === 0 ? 0.95 : 0.75);
          this.gridLayer.moveTo(0, y);
          this.gridLayer.lineTo(width, y);
        }
        this.gridRenderRect = {
          x: visible.x,
          y: visible.y,
          width: visible.width,
          height: visible.height
        };
        this.gridRenderKey = this.getGridRenderKey();
      },
      drawGridPatch: function (rects, layer) {
        if (!this.doc || !layer || !rects || !rects.length) {
          return;
        }
        var width = this.doc.canvasWidth;
        var height = this.doc.canvasHeight;
        var minorStep = this.camera.scale > 1.5 ? 50 : this.camera.scale > 0.45 ? 100 : 200;
        var majorStep = minorStep * 5;
        var lineWidth = 1 / this.camera.scale;
        for (var i = 0; i < rects.length; i++) {
          var rect = rects[i];
          var left = clamp(rect.x - lineWidth, 0, width);
          var top = clamp(rect.y - lineWidth, 0, height);
          var right = clamp(rect.x + rect.width + lineWidth, 0, width);
          var bottom = clamp(rect.y + rect.height + lineWidth, 0, height);
          if (right <= left || bottom <= top) {
            continue;
          }
          layer.lineStyle(0, 0, 0, 0);
          layer.beginFill(0xfafcff, 1);
          layer.drawRect(left, top, right - left, bottom - top);
          layer.endFill();
          if (right - left < minorStep || bottom - top < minorStep) {
            continue;
          }
          var xStart = Math.floor(left / minorStep) * minorStep;
          var yStart = Math.floor(top / minorStep) * minorStep;
          for (var x = xStart; x <= right; x += minorStep) {
            if (x < left || x > right) {
              continue;
            }
            var colorX = x % majorStep === 0 ? 0xc9d7e6 : 0xe4ebf3;
            layer.lineStyle(lineWidth, colorX, x % majorStep === 0 ? 0.95 : 0.75);
            layer.moveTo(x, top);
            layer.lineTo(x, bottom);
          }
          for (var y = yStart; y <= bottom; y += minorStep) {
            if (y < top || y > bottom) {
              continue;
            }
            var colorY = y % majorStep === 0 ? 0xc9d7e6 : 0xe4ebf3;
            layer.lineStyle(lineWidth, colorY, y % majorStep === 0 ? 0.95 : 0.75);
            layer.moveTo(left, y);
            layer.lineTo(right, y);
          }
        }
      },
      drawPatchObjects: function (rects, excludeIds) {
        if (!rects || !rects.length || !this.patchObjectLayer) {
          return;
        }
        var seen = {};
        var elements = [];
        for (var i = 0; i < rects.length; i++) {
          var candidates = this.querySpatialCandidates(rects[i], 0, excludeIds);
          for (var j = 0; j < candidates.length; j++) {
            var item = candidates[j];
            if (!seen[item.id] && rectIntersects(rects[i], item)) {
              seen[item.id] = true;
              elements.push(item);
            }
          }
        }
        if (!elements.length) {
          return;
        }
        this.drawElementsToLayers(elements, this.patchObjectLayer, this.patchObjectLayer);
      },
      drawElementsToLayers: function (elements, trackLayer, nodeLayer) {
        console.log('drawElementsToLayers');
        var lineWidth = 2 / this.camera.scale;
        var buckets = {};
        for (var i = 0; i < elements.length; i++) {
          var element = elements[i];
          var bucketKey =
            (isShelfLikeNodeType(element.type) ? 'node' : 'track') + ':' + element.type;
          if (!buckets[bucketKey]) {
            buckets[bucketKey] = [];
          }
          buckets[bucketKey].push(element);
        }
        for (var bucketKey in buckets) {
          if (!buckets.hasOwnProperty(bucketKey)) {
            continue;
          }
          var parts = bucketKey.split(':');
          var type = parts[1];
          var meta = getTypeMeta(type);
          var layer = parts[0] === 'node' ? nodeLayer : trackLayer;
          var bucket = buckets[bucketKey];
          for (var j = 0; j < bucket.length; j++) {
            var item = bucket[j];
            drawElementByType.call(this, layer, item, type, {
              line: {
                width: lineWidth,
                color: meta.border,
                alpha: 0.95
              },
              fill: {
                color: meta.fill,
                alpha: meta.alpha ?? 0.92
              }
            });
          }
        }
      },
      ensureStaticSprite: function (poolName, index) {
        var pool = poolName === 'node' ? this.staticNodeSpritePool : this.staticTrackSpritePool;
        var layer = poolName === 'node' ? this.staticNodeSpriteLayer : this.staticTrackSpriteLayer;
        if (pool[index]) {
          return pool[index];
        }
        var sprite = new PIXI.Sprite(PIXI.Texture.WHITE);
        sprite.position.set(0, 0);
        sprite.anchor.set(0, 0);
        sprite.visible = false;
        sprite.alpha = 0;
        layer.addChild(sprite);
        pool[index] = sprite;
        return sprite;
      },
      hideUnusedStaticSprites: function (pool, fromIndex) {
        for (var i = fromIndex; i < pool.length; i++) {
          pool[i].visible = false;
          pool[i].alpha = 0;
          pool[i].width = 0;
          pool[i].height = 0;
          pool[i].position.set(-99999, -99999);
        }
      },
      pruneStaticSpritePool: function (poolName, keepCount, slack) {
        var pool = poolName === 'node' ? this.staticNodeSpritePool : this.staticTrackSpritePool;
        var layer = poolName === 'node' ? this.staticNodeSpriteLayer : this.staticTrackSpriteLayer;
        var target = Math.max(0, keepCount + Math.max(0, slack || 0));
        if (!pool || !layer || pool.length <= target) {
          return;
        }
        for (var i = pool.length - 1; i >= target; i--) {
          var sprite = pool[i];
          layer.removeChild(sprite);
          if (sprite && sprite.destroy) {
            sprite.destroy();
          }
          pool.pop();
        }
      },
      drawElementsToSpriteLayers: function (elements) {
        var trackCount = 0;
        var nodeCount = 0;
        for (var i = 0; i < elements.length; i++) {
          var item = elements[i];
          var meta = getTypeMeta(item.type);
          var poolName = isShelfLikeNodeType(item.type) ? 'node' : 'track';
          var sprite = this.ensureStaticSprite(
            poolName,
            poolName === 'node' ? nodeCount : trackCount
          );
          sprite.visible = true;
          sprite.position.set(item.x, item.y);
          sprite.width = item.width;
          sprite.height = item.height;
          sprite.tint = meta.fill;
          sprite.alpha = meta.alpha ?? 1;
          if (poolName === 'node') {
            nodeCount += 1;
          } else {
            trackCount += 1;
          }
        }
        this.hideUnusedStaticSprites(this.staticTrackSpritePool, trackCount);
        this.hideUnusedStaticSprites(this.staticNodeSpritePool, nodeCount);
        if (this.camera.scale < DENSE_SIMPLIFY_SCALE_THRESHOLD) {
          this.pruneStaticSpritePool('track', trackCount, STATIC_SPRITE_POOL_SLACK);
          this.pruneStaticSpritePool('node', nodeCount, STATIC_SPRITE_POOL_SLACK);
        }
      },
      simplifyRenderableElements: function (elements) {
        if (!elements || elements.length < 2) {
          return elements || [];
        }
        var sorted = elements.slice().sort(function (a, b) {
          if (a.type !== b.type) {
            return a.type < b.type ? -1 : 1;
          }
          if (Math.abs(a.y - b.y) > COORD_EPSILON) {
            return a.y - b.y;
          }
          if (Math.abs(a.height - b.height) > COORD_EPSILON) {
            return a.height - b.height;
          }
          return a.x - b.x;
        });
        var result = [];
        var current = null;
        for (var i = 0; i < sorted.length; i++) {
          var item = sorted[i];
          if (!current) {
            current = {
              type: item.type,
              x: item.x,
              y: item.y,
              width: item.width,
              height: item.height
            };
            continue;
          }
          var currentRight = current.x + current.width;
          var itemRight = item.x + item.width;
          var sameBand =
            current.type === item.type &&
            Math.abs(current.y - item.y) <= 0.5 &&
            Math.abs(current.height - item.height) <= 0.5;
          var joinable = item.x <= currentRight + 0.5;
          if (sameBand && joinable) {
            current.width = roundCoord(Math.max(currentRight, itemRight) - current.x);
          } else {
            result.push(current);
            current = {
              type: item.type,
              x: item.x,
              y: item.y,
              width: item.width,
              height: item.height
            };
          }
        }
        if (current) {
          result.push(current);
        }
        return result;
      },
      renderStaticElements: function (renderRect, excludedKey) {
        if (!this.doc) {
          return;
        }
        this.trackLayer.clear();
        this.trackLayer.removeChildren();
        this.nodeLayer.clear();
        this.nodeLayer.removeChildren();
        this.eraseLayer.clear();
        this.patchObjectLayer.clear();
        this.patchObjectLayer.removeChildren();
        var renderableElements = this.getRenderableElements(
          this.getStaticExcludedIds(),
          renderRect
        );
        // 1. 筛选出 annulus 和设备轨道元素,因为 Sprite 不支持绘制中心线
        var annulusElements = [];
        var deviceElements = [];
        var normalElements = [];
        for (var i = 0; i < renderableElements.length; i++) {
          var el = renderableElements[i];
          if (el.type === 'annulus') {
            annulusElements.push(el);
          } else if (isDeviceConfigType(el.type)) {
            deviceElements.push(el);
          } else {
            normalElements.push(el);
          }
        }
        var hasAnnulus = annulusElements.length > 0;
        var hasDeviceElements = deviceElements.length > 0;
        var useSpriteMode = this.camera.scale < STATIC_SPRITE_SCALE_THRESHOLD;
        var shouldSimplify =
          this.camera.scale < STATIC_SIMPLIFY_SCALE_THRESHOLD ||
          (this.camera.scale < DENSE_SIMPLIFY_SCALE_THRESHOLD &&
            renderableElements.length > DENSE_SIMPLIFY_ELEMENT_THRESHOLD);
        // 2. 重新设定层的可见性
        // Sprite层:仅在需要且有普通元素时显示
        this.staticTrackSpriteLayer.visible = useSpriteMode && normalElements.length > 0;
        this.staticNodeSpriteLayer.visible = useSpriteMode && normalElements.length > 0;
        // Graphics层:如果不是 Sprite 模式,或者存在 annulus 或设备轨道元素,则必须显示
        this.trackLayer.visible = !useSpriteMode || hasAnnulus || hasDeviceElements;
        this.nodeLayer.visible = !useSpriteMode;
        if (useSpriteMode) {
          // 3. 绘制普通元素到 Sprite 层
          if (shouldSimplify) {
            normalElements = this.simplifyRenderableElements(normalElements);
          }
          this.drawElementsToSpriteLayers(normalElements);
          // 4. 绘制 annulus 和设备轨道元素到 Graphics 层
          // 即使在缩略图模式下,这些元素也必须用 Graphics 绘制以保持中心线
          var graphicsElements = annulusElements.concat(deviceElements);
          if (graphicsElements.length > 0) {
            // 注意:drawElementsToLayers 会根据元素类型自动分配到 trackLayer 或 nodeLayer
            // annulus 和设备轨道属于 track 类别,会绘制到 trackLayer
            this.drawElementsToLayers(graphicsElements, this.trackLayer, this.trackLayer);
          }
        } else {
          this.hideUnusedStaticSprites(this.staticTrackSpritePool, 0);
          this.hideUnusedStaticSprites(this.staticNodeSpritePool, 0);
          this.drawElementsToLayers(renderableElements, this.trackLayer, this.nodeLayer);
        }
        var rect = renderRect || this.getWorldRectWithPadding(STATIC_VIEW_PADDING);
        this.staticRenderRect = {
          x: rect.x,
          y: rect.y,
          width: rect.width,
          height: rect.height
        };
        this.staticRenderKey = this.getStaticRenderKey();
        this.staticExcludedKey =
          excludedKey != null ? excludedKey : this.selectionKey(this.getStaticExcludedIds());
        this.pendingStaticCommit = null;
      },
      renderActiveElements: function () {
        this.activeLayer.clear();
        this.activeLayer.removeChildren();
        this.eraseLayer.clear();
        this.patchObjectLayer.clear();
        this.patchObjectLayer.removeChildren();
        var activeIds = this.getStaticExcludedIds();
        if (!activeIds.length) {
          return;
        }
        var activeElements = [];
        for (var idx = 0; idx < activeIds.length; idx++) {
          var element = this.findElementById(activeIds[idx]);
          if (element) {
            activeElements.push(element);
          }
        }
        if (!activeElements.length) {
          return;
        }
        this.drawElementsToLayers(activeElements, this.activeLayer, this.activeLayer);
      },
      getLabelText: function (element) {
        var meta = getTypeMeta(element.type);
        var value = safeParseJson(element.value);
        if (element.type === 'devp' && value) {
          var station = value.stationId != null ? String(value.stationId) : '';
          var arrows = formatDirectionArrows(value.direction);
          if (station && arrows) {
            return element.height > element.width * 1.15
              ? station + '\n' + arrows
              : station + ' ' + arrows;
          }
          if (station) {
            return station;
          }
          if (arrows) {
            return arrows;
          }
          return meta.shortLabel;
        }
        if (
          (element.type === 'crn' || element.type === 'dualCrn' || element.type === 'rgv') &&
          value
        ) {
          if (value.deviceNo != null) {
            return meta.shortLabel + ' ' + value.deviceNo;
          }
          if (value.crnNo != null) {
            return meta.shortLabel + ' ' + value.crnNo;
          }
          if (value.rgvNo != null) {
            return meta.shortLabel + ' ' + value.rgvNo;
          }
        }
        if (element.value && element.value.length <= 18 && element.value.indexOf('{') !== 0) {
          return element.value;
        }
        return meta.shortLabel;
      },
      ensureLabelSprite: function (index) {
        if (this.labelPool[index]) {
          return this.labelPool[index];
        }
        var label = new PIXI.Text('', {
          fontFamily: 'Avenir Next, PingFang SC, Microsoft YaHei, sans-serif',
          fontSize: 12,
          fontWeight: '600',
          fill: 0x223448,
          align: 'center'
        });
        label.anchor.set(0.5);
        this.labelLayer.addChild(label);
        this.labelPool[index] = label;
        return label;
      },
      getLabelRenderBudget: function () {
        if (!this.pixiApp || !this.pixiApp.renderer) {
          return MIN_LABEL_COUNT;
        }
        var renderer = this.pixiApp.renderer;
        var viewportArea = renderer.width * renderer.height;
        return clamp(Math.round(viewportArea / 12000), MIN_LABEL_COUNT, MAX_LABEL_COUNT);
      },
      getLabelMinScreenWidth: function (text) {
        var lines = String(text || '').split('\n');
        var length = 0;
        for (var i = 0; i < lines.length; i++) {
          length = Math.max(length, String(lines[i] || '').trim().length);
        }
        if (length <= 4) {
          return 26;
        }
        if (length <= 8) {
          return 40;
        }
        if (length <= 12) {
          return 52;
        }
        return 64;
      },
      getLabelMinScreenHeight: function (text) {
        var lines = String(text || '').split('\n');
        var length = 0;
        for (var i = 0; i < lines.length; i++) {
          length = Math.max(length, String(lines[i] || '').trim().length);
        }
        var lineHeight = length <= 4 ? 14 : 18;
        return lineHeight * Math.max(lines.length, 1);
      },
      renderLabels: function () {
        if (!this.doc) {
          return;
        }
        var capability = this.ensureLabelCapability();
        if (
          capability.maxWidth * this.camera.scale < ABS_MIN_LABEL_SCREEN_WIDTH ||
          capability.maxHeight * this.camera.scale < ABS_MIN_LABEL_SCREEN_HEIGHT
        ) {
          this.labelLayer.visible = false;
          return;
        }
        if (
          this.isZooming ||
          this.isPanning ||
          this.camera.scale < MIN_LABEL_SCALE ||
          (this.interactionState &&
            (this.interactionState.type === 'move' ||
              this.interactionState.type === 'resize' ||
              this.interactionState.type === 'pan'))
        ) {
          this.labelLayer.visible = false;
          return;
        }
        this.labelLayer.visible = true;
        var visible = this.getVisibleWorldRect();
        var elements = this.querySpatialCandidates(visible, 0, []);
        if (
          elements.length > DENSE_LABEL_HIDE_ELEMENT_THRESHOLD &&
          this.camera.scale < DENSE_LABEL_HIDE_SCALE_THRESHOLD
        ) {
          this.labelLayer.visible = false;
          return;
        }
        var hasRoomForAnyLabel = false;
        for (var roomIdx = 0; roomIdx < elements.length; roomIdx++) {
          var candidate = elements[roomIdx];
          if (
            candidate.width * this.camera.scale >= ABS_MIN_LABEL_SCREEN_WIDTH &&
            candidate.height * this.camera.scale >= ABS_MIN_LABEL_SCREEN_HEIGHT
          ) {
            hasRoomForAnyLabel = true;
            break;
          }
        }
        if (!hasRoomForAnyLabel) {
          this.labelLayer.visible = false;
          return;
        }
        var visibleElements = [];
        for (var i = 0; i < elements.length; i++) {
          var element = elements[i];
          var text = this.getLabelText(element);
          if (!text) {
            continue;
          }
          if (!rectIntersects(visible, element)) {
            continue;
          }
          if (
            element.width * this.camera.scale < this.getLabelMinScreenWidth(text) ||
            element.height * this.camera.scale < this.getLabelMinScreenHeight(text)
          ) {
            continue;
          }
          visibleElements.push({
            element: element,
            text: text
          });
        }
        visibleElements.sort(function (a, b) {
          return b.element.width * b.element.height - a.element.width * a.element.height;
        });
        var labelBudget = this.getLabelRenderBudget();
        if (visibleElements.length > labelBudget) {
          visibleElements = visibleElements.slice(0, labelBudget);
        }
        for (var j = 0; j < visibleElements.length; j++) {
          var item = visibleElements[j].element;
          var label = this.ensureLabelSprite(j);
          label.visible = true;
          label.text = visibleElements[j].text;
          label.position.set(item.x + item.width / 2, item.y + item.height / 2);
          label.scale.set(1 / this.camera.scale, 1 / this.camera.scale);
          label.alpha = this.selectedIds.indexOf(item.id) >= 0 ? 1 : 0.88;
        }
        for (var k = visibleElements.length; k < this.labelPool.length; k++) {
          this.labelPool[k].visible = false;
        }
      },
      renderHover: function () {
        this.hoverLayer.clear();
        if (
          this.interactionState ||
          !this.hoverElementId ||
          this.selectedIds.indexOf(this.hoverElementId) >= 0
        ) {
          return;
        }
        var element = this.findElementById(this.hoverElementId);
        if (!element) {
          return;
        }
        var lineWidth = 2 / this.camera.scale;
        this.hoverLayer.lineStyle(lineWidth, 0x2f79d6, 0.95);
        this.hoverLayer.drawRoundedRect(
          element.x,
          element.y,
          element.width,
          element.height,
          Math.max(6 / this.camera.scale, 2)
        );
      },
      renderSelection: function () {
        console.log('renderSelection');
        this.selectionLayer.clear();
        this.selectionLayer.removeChildren();
        if (
          !this.selectedIds.length ||
          (this.interactionState &&
            (this.interactionState.type === 'move' || this.interactionState.type === 'resize'))
        ) {
          return;
        }
        var elements = this.getSelectedElements();
        var lineWidth = 2 / this.camera.scale;
        for (var i = 0; i < elements.length; i++) {
          var element = elements[i];
          drawElementByType.call(this, this.selectionLayer, element, element.type, {
            line: {
              width: lineWidth,
              color: 0x2568b8,
              alpha: 1
            },
            fill: {
              color: 0x2f79d6,
              alpha: 0.07
            }
          });
          this.selectionLayer.endFill();
        }
        if (elements.length !== 1) {
          return;
        }
        var handleSize = HANDLE_SCREEN_SIZE / this.camera.scale;
        var handlePositions = this.getHandlePositions(elements[0]);
        this.selectionLayer.lineStyle(1 / this.camera.scale, 0x1d5ea9, 1);
        this.selectionLayer.beginFill(0xffffff, 1);
        for (var key in handlePositions) {
          if (!handlePositions.hasOwnProperty(key)) {
            continue;
          }
          var pos = handlePositions[key];
          this.selectionLayer.drawRect(
            pos.x - handleSize / 2,
            pos.y - handleSize / 2,
            handleSize,
            handleSize
          );
        }
        this.selectionLayer.endFill();
      },
      renderGuide: function () {
        console.log('renderGuide');
        this.guideLayer.clear();
        this.guideLayer.removeChildren();
        if (this.guideText) {
          this.guideText.visible = false;
        }
        if (!this.interactionState) {
          return;
        }
        var state = this.interactionState;
        if (state.type === 'draw' && state.rect && state.rect.width > 0 && state.rect.height > 0) {
          var drawMeta = getTypeMeta(state.elementType);
          drawElementByType.call(this, this.guideLayer, state.rect, state.elementType, {
            line: {
              width: 2 / this.camera.scale,
              color: drawMeta.border,
              alpha: 0.95
            },
            fill: {
              color: drawMeta.fill,
              alpha: drawMeta.alpha ?? 0.18
            }
          });
          return;
        }
        if (state.type === 'array' && state.template) {
          var previewItems = state.previewItems || [];
          var arrayMeta = getTypeMeta(state.template.type);
          var lineWidth = 2 / this.camera.scale;
          var templateCenterX = state.template.x + state.template.width / 2;
          var templateCenterY = state.template.y + state.template.height / 2;
          this.guideLayer.lineStyle(lineWidth, arrayMeta.border, 0.9);
          this.guideLayer.moveTo(templateCenterX, templateCenterY);
          this.guideLayer.lineTo(state.currentWorld.x, state.currentWorld.y);
          if (!previewItems.length) {
            return;
          }
          this.guideLayer.lineStyle(1 / this.camera.scale, arrayMeta.border, 0.8);
          this.guideLayer.beginFill(arrayMeta.fill, 0.2);
          for (var previewIndex = 0; previewIndex < previewItems.length; previewIndex++) {
            var preview = previewItems[previewIndex];
            this.guideLayer.drawRoundedRect(
              preview.x,
              preview.y,
              preview.width,
              preview.height,
              Math.max(6 / this.camera.scale, 2)
            );
          }
          this.guideLayer.endFill();
          if (this.guideText) {
            this.guideText.text = '将生成 ' + previewItems.length + ' 个';
            this.guideText.position.set(
              state.currentWorld.x,
              state.currentWorld.y - 10 / this.camera.scale
            );
            this.guideText.scale.set(1 / this.camera.scale);
            this.guideText.visible = true;
          }
          return;
        }
        if (state.type === 'marquee') {
          var rect = buildRectFromPoints(state.startWorld, state.currentWorld);
          if (rect.width <= 0 || rect.height <= 0) {
            return;
          }
          this.guideLayer.lineStyle(2 / this.camera.scale, 0x2f79d6, 0.92);
          this.guideLayer.beginFill(0x2f79d6, 0.06);
          this.guideLayer.drawRect(rect.x, rect.y, rect.width, rect.height);
          this.guideLayer.endFill();
        }
      },
      pointerToWorld: function (event) {
        var rect = this.pixiApp.view.getBoundingClientRect();
        var screenX = event.clientX - rect.left;
        var screenY = event.clientY - rect.top;
        return {
          screenX: screenX,
          screenY: screenY,
          x: roundCoord((screenX - this.camera.x) / this.camera.scale),
          y: roundCoord((screenY - this.camera.y) / this.camera.scale)
        };
      },
      isWithinCanvas: function (rect) {
        if (!this.doc) {
          return false;
        }
        return (
          rect.x >= -COORD_EPSILON &&
          rect.y >= -COORD_EPSILON &&
          rect.x + rect.width <= this.doc.canvasWidth + COORD_EPSILON &&
          rect.y + rect.height <= this.doc.canvasHeight + COORD_EPSILON
        );
      },
      canPlaceElements: function (elements, excludeIds) {
        excludeIds = excludeIds || [];
        for (var i = 0; i < elements.length; i++) {
          if (!this.isWithinCanvas(elements[i])) {
            return false;
          }
          if (this.hasOverlap(elements[i], excludeIds.concat([elements[i].id]))) {
            return false;
          }
        }
        return true;
      },
      hasOverlap: function (candidate, excludeIds) {
        if (!this.doc) {
          return false;
        }
        var elements = this.querySpatialCandidates(candidate, COORD_EPSILON, excludeIds);
        for (var i = 0; i < elements.length; i++) {
          var item = elements[i];
          if (rectsOverlap(candidate, item)) {
            return true;
          }
        }
        return false;
      },
      snapToleranceWorld: function () {
        return Math.max(1, EDGE_SNAP_SCREEN_TOLERANCE / this.camera.scale);
      },
      collectMoveSnap: function (baseItems, dx, dy, excludeIds) {
        if (!this.doc || !baseItems || !baseItems.length) {
          return { dx: 0, dy: 0 };
        }
        var tolerance = this.snapToleranceWorld();
        var bestDx = null;
        var bestDy = null;
        for (var i = 0; i < baseItems.length; i++) {
          var moving = baseItems[i];
          var movedLeft = moving.x + dx;
          var movedRight = movedLeft + moving.width;
          var movedTop = moving.y + dy;
          var movedBottom = movedTop + moving.height;
          var candidates = this.querySpatialCandidates(
            {
              x: movedLeft,
              y: movedTop,
              width: moving.width,
              height: moving.height
            },
            tolerance,
            excludeIds
          );
          for (var j = 0; j < candidates.length; j++) {
            var other = candidates[j];
            var otherLeft = other.x;
            var otherRight = other.x + other.width;
            var otherTop = other.y;
            var otherBottom = other.y + other.height;
            if (rangesNearOrOverlap(movedTop, movedBottom, otherTop, otherBottom, tolerance)) {
              var horizontalCandidates = [
                otherLeft - movedRight,
                otherRight - movedLeft,
                otherLeft - movedLeft,
                otherRight - movedRight
              ];
              for (var hx = 0; hx < horizontalCandidates.length; hx++) {
                var deltaX = horizontalCandidates[hx];
                if (
                  Math.abs(deltaX) <= tolerance &&
                  (bestDx === null || Math.abs(deltaX) < Math.abs(bestDx))
                ) {
                  bestDx = deltaX;
                }
              }
            }
            if (rangesNearOrOverlap(movedLeft, movedRight, otherLeft, otherRight, tolerance)) {
              var verticalCandidates = [
                otherTop - movedBottom,
                otherBottom - movedTop,
                otherTop - movedTop,
                otherBottom - movedBottom
              ];
              for (var vy = 0; vy < verticalCandidates.length; vy++) {
                var deltaY = verticalCandidates[vy];
                if (
                  Math.abs(deltaY) <= tolerance &&
                  (bestDy === null || Math.abs(deltaY) < Math.abs(bestDy))
                ) {
                  bestDy = deltaY;
                }
              }
            }
          }
        }
        return {
          dx: bestDx == null ? 0 : bestDx,
          dy: bestDy == null ? 0 : bestDy
        };
      },
      collectResizeSnap: function (rect, handle, excludeIds) {
        if (!this.doc || !rect) {
          return null;
        }
        var tolerance = this.snapToleranceWorld();
        var left = rect.x;
        var right = rect.x + rect.width;
        var top = rect.y;
        var bottom = rect.y + rect.height;
        var bestLeft = null;
        var bestRight = null;
        var bestTop = null;
        var bestBottom = null;
        function pickBest(current, candidate) {
          if (candidate == null) {
            return current;
          }
          if (current == null || Math.abs(candidate) < Math.abs(current)) {
            return candidate;
          }
          return current;
        }
        if (handle.indexOf('w') >= 0) {
          bestLeft = pickBest(bestLeft, -left);
        }
        if (handle.indexOf('e') >= 0) {
          bestRight = pickBest(bestRight, this.doc.canvasWidth - right);
        }
        if (handle.indexOf('n') >= 0) {
          bestTop = pickBest(bestTop, -top);
        }
        if (handle.indexOf('s') >= 0) {
          bestBottom = pickBest(bestBottom, this.doc.canvasHeight - bottom);
        }
        var elements = this.querySpatialCandidates(rect, tolerance, excludeIds);
        for (var i = 0; i < elements.length; i++) {
          var other = elements[i];
          var otherLeft = other.x;
          var otherRight = other.x + other.width;
          var otherTop = other.y;
          var otherBottom = other.y + other.height;
          if (rangesNearOrOverlap(top, bottom, otherTop, otherBottom, tolerance)) {
            if (handle.indexOf('w') >= 0) {
              bestLeft = pickBest(bestLeft, otherLeft - left);
              bestLeft = pickBest(bestLeft, otherRight - left);
            }
            if (handle.indexOf('e') >= 0) {
              bestRight = pickBest(bestRight, otherLeft - right);
              bestRight = pickBest(bestRight, otherRight - right);
            }
          }
          if (rangesNearOrOverlap(left, right, otherLeft, otherRight, tolerance)) {
            if (handle.indexOf('n') >= 0) {
              bestTop = pickBest(bestTop, otherTop - top);
              bestTop = pickBest(bestTop, otherBottom - top);
            }
            if (handle.indexOf('s') >= 0) {
              bestBottom = pickBest(bestBottom, otherTop - bottom);
              bestBottom = pickBest(bestBottom, otherBottom - bottom);
            }
          }
        }
        if (bestLeft != null && Math.abs(bestLeft) > tolerance) {
          bestLeft = null;
        }
        if (bestRight != null && Math.abs(bestRight) > tolerance) {
          bestRight = null;
        }
        if (bestTop != null && Math.abs(bestTop) > tolerance) {
          bestTop = null;
        }
        if (bestBottom != null && Math.abs(bestBottom) > tolerance) {
          bestBottom = null;
        }
        return {
          left: bestLeft,
          right: bestRight,
          top: bestTop,
          bottom: bestBottom
        };
      },
      hitTestElement: function (point) {
        if (!this.doc) {
          return null;
        }
        var candidates = this.querySpatialCandidates(
          {
            x: point.x,
            y: point.y,
            width: 0,
            height: 0
          },
          0,
          []
        );
        if (!candidates.length) {
          return null;
        }
        var candidateMap = {};
        for (var c = 0; c < candidates.length; c++) {
          candidateMap[candidates[c].id] = true;
        }
        var elements = this.doc.elements || [];
        for (var i = elements.length - 1; i >= 0; i--) {
          var element = elements[i];
          if (!candidateMap[element.id]) {
            continue;
          }
          if (
            point.x >= element.x &&
            point.x <= element.x + element.width &&
            point.y >= element.y &&
            point.y <= element.y + element.height
          ) {
            return element;
          }
        }
        return null;
      },
      getHandlePositions: function (element) {
        var x = element.x;
        var y = element.y;
        var w = element.width;
        var h = element.height;
        var cx = x + w / 2;
        var cy = y + h / 2;
        if (element.type === 'annulus' && element.shape !== 'rect') {
          return {
            nw: { x: x, y: y },
            ne: { x: x + w, y: y },
            se: { x: x + w, y: y + h },
            sw: { x: x, y: y + h },
            turningPoint: element.turningPoint,
            shape: element.shape
          };
        }
        return {
          nw: { x: x, y: y },
          n: { x: cx, y: y },
          ne: { x: x + w, y: y },
          e: { x: x + w, y: cy },
          se: { x: x + w, y: y + h },
          s: { x: cx, y: y + h },
          sw: { x: x, y: y + h },
          w: { x: x, y: cy }
        };
      },
      getResizeHandleAt: function (point, element) {
        var handlePositions = this.getHandlePositions(element);
        var baseTolerance = HANDLE_SCREEN_SIZE / this.camera.scale;
        var sizeLimitedTolerance = Math.max(
          Math.min(element.width, element.height) / 4,
          3 / this.camera.scale
        );
        var tolerance = Math.min(baseTolerance, sizeLimitedTolerance);
        var bestHandle = '';
        var bestDistance = Infinity;
        for (var key in handlePositions) {
          if (!handlePositions.hasOwnProperty(key)) {
            continue;
          }
          var pos = handlePositions[key];
          var dx = Math.abs(point.x - pos.x);
          var dy = Math.abs(point.y - pos.y);
          if (dx <= tolerance && dy <= tolerance) {
            var distance = dx + dy;
            if (distance < bestDistance) {
              bestDistance = distance;
              bestHandle = key;
            }
          }
        }
        return bestHandle;
      },
      cursorForHandle: function (handle) {
        if (handle === 'nw' || handle === 'se') {
          return 'nwse-resize';
        }
        if (handle === 'ne' || handle === 'sw') {
          return 'nesw-resize';
        }
        if (handle === 'n' || handle === 's') {
          return 'ns-resize';
        }
        if (handle === 'e' || handle === 'w') {
          return 'ew-resize';
        }
        if (handle === 'turningPoint') {
          const element = this.singleSelectedElement || {};
          return element.shape === 'L1' || element.shape === 'L3' ? 'nesw-resize' : 'nwse-resize';
        }
        return 'default';
      },
      updateCursor: function () {
        if (!this.pixiApp) {
          return;
        }
        var cursor = 'default';
        if (this.interactionState) {
          if (this.interactionState.type === 'pan') {
            cursor = 'grabbing';
          } else if (
            this.interactionState.type === 'draw' ||
            this.interactionState.type === 'marquee'
          ) {
            cursor = 'crosshair';
          } else if (this.interactionState.type === 'array') {
            cursor = 'crosshair';
          } else if (this.interactionState.type === 'move') {
            cursor = 'move';
          } else if (this.interactionState.type === 'movePending') {
            cursor = 'grab';
          } else if (this.interactionState.type === 'resize') {
            cursor = this.cursorForHandle(this.interactionState.handle);
          }
        } else if (this.spacePressed || this.activeTool === 'pan') {
          cursor = 'grab';
        } else if (
          DRAW_TYPES.indexOf(this.activeTool) >= 0 ||
          this.activeTool === 'marquee' ||
          this.activeTool === 'array'
        ) {
          cursor = 'crosshair';
        } else if (this.singleSelectedElement) {
          var point = this.lastPointerWorld || null;
          if (point) {
            var handle = this.getResizeHandleAt(point, this.singleSelectedElement);
            cursor = handle ? this.cursorForHandle(handle) : 'default';
          }
          if (cursor === 'default' && this.hoverElementId) {
            cursor = 'move';
          } else if (cursor === 'default') {
            cursor = 'grab';
          }
        } else {
          cursor = this.hoverElementId ? 'move' : 'grab';
        }
        if (cursor !== this.lastCursor) {
          this.lastCursor = cursor;
          this.pixiApp.view.style.cursor = cursor;
        }
      },
      startPan: function (point) {
        this.cancelDeferredStaticRebuild();
        this.cancelPanRefresh();
        if (this.zoomRefreshTimer) {
          window.clearTimeout(this.zoomRefreshTimer);
          this.zoomRefreshTimer = null;
          this.isZooming = false;
          this.pendingViewportRefresh = true;
        }
        this.isPanning = true;
        this.interactionState = {
          type: 'pan',
          startScreen: {
            x: point.screenX,
            y: point.screenY
          },
          startCamera: {
            x: this.camera.x,
            y: this.camera.y
          }
        };
        this.updateCursor();
      },
      startMarquee: function (point, additive) {
        this.cancelDeferredStaticRebuild();
        this.interactionState = {
          type: 'marquee',
          additive: !!additive,
          startWorld: { x: point.x, y: point.y },
          currentWorld: { x: point.x, y: point.y }
        };
        this.updateCursor();
      },
      startDraw: function (point) {
        this.cancelDeferredStaticRebuild();
        var initialRect = { x: point.x, y: point.y, width: 0, height: 0 };
        if (this.activeTool === 'annulus') {
          initialRect.type = 'annulus';
          initialRect.shape = this.annulusShape;
        }
        this.interactionState = {
          type: 'draw',
          beforeSnapshot: this.snapshotDoc(this.doc),
          elementType: this.activeTool,
          startWorld: { x: point.x, y: point.y },
          rect: initialRect
        };
        this.updateCursor();
      },
      startArray: function (point, element) {
        if (!this.canArrayFromElement(element)) {
          this.showMessage('warning', '阵列工具当前只支持货架与维修站台');
          return;
        }
        this.cancelDeferredStaticRebuild();
        this.interactionState = {
          type: 'array',
          beforeSnapshot: this.snapshotDoc(this.doc),
          template: {
            id: element.id,
            type: element.type,
            x: element.x,
            y: element.y,
            width: element.width,
            height: element.height,
            value: element.value
          },
          startWorld: { x: point.x, y: point.y },
          currentWorld: { x: point.x, y: point.y },
          previewItems: []
        };
        this.updateCursor();
      },
      startMove: function (point) {
        var selected = this.getSelectedElements();
        if (!selected.length) {
          return;
        }
        this.cancelDeferredStaticRebuild();
        var baseItems = selected.map(function (item) {
          return {
            id: item.id,
            x: item.x,
            y: item.y,
            width: item.width,
            height: item.height,
            value: item.value,
            type: item.type,
            turningPoint: item.turningPoint
              ? { x: item.turningPoint.x, y: item.turningPoint.y }
              : null
          };
        });
        this.interactionState = {
          type: 'movePending',
          beforeSnapshot: this.snapshotDoc(this.doc),
          startScreen: { x: point.screenX, y: point.screenY },
          startWorld: { x: point.x, y: point.y },
          baseItems: baseItems
        };
        this.updateCursor();
      },
      startResize: function (point, element, handle) {
        this.cancelDeferredStaticRebuild();
        this.interactionState = {
          type: 'resize',
          handle: handle,
          elementId: element.id,
          beforeSnapshot: this.snapshotDoc(this.doc),
          startWorld: { x: point.x, y: point.y },
          baseRect: {
            x: element.x,
            y: element.y,
            width: element.width,
            height: element.height,
            baseTurningPoint: element.turningPoint
              ? { x: element.turningPoint.x, y: element.turningPoint.y }
              : null,
            shape: element.shape
          }
        };
        this.markStaticSceneDirty();
        this.scheduleRender();
        this.updateCursor();
      },
      onCanvasPointerDown: function (event) {
        if (!this.doc || !this.pixiApp) {
          return;
        }
        if (event.button !== 0 && event.button !== 1) {
          return;
        }
        if (this.pixiApp.view.setPointerCapture && event.pointerId != null) {
          try {
            this.pixiApp.view.setPointerCapture(event.pointerId);
          } catch (ignore) {}
        }
        this.currentPointerId = event.pointerId;
        var point = this.pointerToWorld(event);
        this.lastPointerWorld = point;
        this.pointerStatus = this.formatNumber(point.x) + ', ' + this.formatNumber(point.y);
        if (this.spacePressed || this.activeTool === 'pan' || event.button === 1) {
          this.startPan(point);
          return;
        }
        if (DRAW_TYPES.indexOf(this.activeTool) >= 0) {
          this.startDraw(point);
          return;
        }
        if (this.activeTool === 'marquee') {
          this.startMarquee(point, event.shiftKey);
          return;
        }
        if (this.activeTool === 'array') {
          var arrayHit = this.hitTestElement(point);
          var arrayTemplate = arrayHit || this.singleSelectedElement;
          if (arrayHit && this.selectedIds.indexOf(arrayHit.id) < 0) {
            this.setSelectedIds([arrayHit.id]);
            arrayTemplate = arrayHit;
          }
          if (!arrayTemplate) {
            this.showMessage('warning', '请先选中一个货架或维修站台作为阵列模板');
            return;
          }
          this.startArray(point, arrayTemplate);
          return;
        }
        var selected = this.singleSelectedElement;
        var handle = selected ? this.getResizeHandleAt(point, selected) : '';
        if (handle) {
          this.startResize(point, selected, handle);
          return;
        }
        var hit = this.hitTestElement(point);
        if (hit) {
          if (event.shiftKey) {
            var index = this.selectedIds.indexOf(hit.id);
            if (index >= 0) {
              var nextIds = this.selectedIds.slice();
              nextIds.splice(index, 1);
              this.setSelectedIds(nextIds);
            } else {
              this.setSelectedIds(this.selectedIds.concat([hit.id]));
            }
            this.scheduleRender();
            return;
          }
          if (this.selectedIds.indexOf(hit.id) < 0) {
            this.setSelectedIds([hit.id]);
            this.scheduleRender();
          }
          this.startMove(point);
          return;
        }
        if (this.selectedIds.length) {
          this.setSelectedIds([]);
          this.scheduleRender();
        }
        this.startPan(point);
      },
      onCanvasWheel: function (event) {
        if (!this.pixiApp || !this.doc) {
          return;
        }
        event.preventDefault();
        var point = this.pointerToWorld(event);
        var delta = event.deltaY < 0 ? 1.12 : 0.89;
        var nextScale = clamp(this.camera.scale * delta, 0.06, 4);
        this.camera.scale = nextScale;
        this.camera.x = Math.round(point.screenX - point.x * nextScale);
        this.camera.y = Math.round(point.screenY - point.y * nextScale);
        this.viewZoom = nextScale;
        this.scheduleZoomRefresh();
        this.scheduleRender();
      },
      onWindowPointerMove: function (event) {
        if (!this.pixiApp || !this.doc) {
          return;
        }
        var point = this.pointerToWorld(event);
        this.lastPointerWorld = point;
        var pointerText = this.formatNumber(point.x) + ', ' + this.formatNumber(point.y);
        var now = window.performance && performance.now ? performance.now() : Date.now();
        if (
          pointerText !== this.pointerStatus &&
          (now - this.lastPointerStatusUpdateTs >= POINTER_STATUS_UPDATE_INTERVAL ||
            this.pointerStatus === '--')
        ) {
          this.pointerStatus = pointerText;
          this.lastPointerStatusUpdateTs = now;
        }
        if (!this.interactionState) {
          var hover = this.hitTestElement(point);
          var hoverId = hover ? hover.id : '';
          if (hoverId !== this.hoverElementId) {
            this.hoverElementId = hoverId;
            this.scheduleRender();
          }
          this.updateCursor();
          return;
        }
        var state = this.interactionState;
        if (state.type === 'pan') {
          this.camera.x = Math.round(state.startCamera.x + (point.screenX - state.startScreen.x));
          this.camera.y = Math.round(state.startCamera.y + (point.screenY - state.startScreen.y));
          this.scheduleRender();
          return;
        }
        if (state.type === 'marquee') {
          state.currentWorld = { x: point.x, y: point.y };
          this.scheduleRender();
          return;
        }
        if (state.type === 'draw') {
          var rawRect = buildRectFromPoints(state.startWorld, point);
          var clipped = {
            x: clamp(rawRect.x, 0, this.doc.canvasWidth),
            y: clamp(rawRect.y, 0, this.doc.canvasHeight),
            width: clamp(rawRect.width, 0, this.doc.canvasWidth),
            height: clamp(rawRect.height, 0, this.doc.canvasHeight)
          };
          if (clipped.x + clipped.width > this.doc.canvasWidth) {
            clipped.width = roundCoord(this.doc.canvasWidth - clipped.x);
          }
          if (clipped.y + clipped.height > this.doc.canvasHeight) {
            clipped.height = roundCoord(this.doc.canvasHeight - clipped.y);
          }
          if (state.elementType === 'annulus') {
            clipped.type = 'annulus';
            clipped.shape = this.annulusShape;
          }
          state.rect = clipped;
          this.scheduleRender();
          return;
        }
        if (state.type === 'array') {
          state.currentWorld = { x: point.x, y: point.y };
          state.previewItems = this.buildArrayCopies(
            state.template,
            state.startWorld,
            state.currentWorld
          );
          this.scheduleRender();
          return;
        }
        if (state.type === 'movePending') {
          var dragDistance = Math.max(
            Math.abs(point.screenX - state.startScreen.x),
            Math.abs(point.screenY - state.startScreen.y)
          );
          if (dragDistance < DRAG_START_THRESHOLD) {
            return;
          }
          state.type = 'move';
          this.markStaticSceneDirty();
          this.scheduleRender();
          this.updateCursor();
        }
        if (state.type === 'move') {
          var dx = point.x - state.startWorld.x;
          var dy = point.y - state.startWorld.y;
          var minDx = -Infinity;
          var maxDx = Infinity;
          var minDy = -Infinity;
          var maxDy = Infinity;
          for (var i = 0; i < state.baseItems.length; i++) {
            var base = state.baseItems[i];
            minDx = Math.max(minDx, -base.x);
            minDy = Math.max(minDy, -base.y);
            maxDx = Math.min(maxDx, this.doc.canvasWidth - (base.x + base.width));
            maxDy = Math.min(maxDy, this.doc.canvasHeight - (base.y + base.height));
          }
          dx = clamp(dx, minDx, maxDx);
          dy = clamp(dy, minDy, maxDy);
          var snapDelta = this.collectMoveSnap(state.baseItems, dx, dy, this.selectedIds.slice());
          dx = clamp(dx + snapDelta.dx, minDx, maxDx);
          dy = clamp(dy + snapDelta.dy, minDy, maxDy);
          for (var j = 0; j < state.baseItems.length; j++) {
            var baseItem = state.baseItems[j];
            var element = this.findElementById(baseItem.id);
            if (!element) {
              continue;
            }
            element.x = roundCoord(baseItem.x + dx);
            element.y = roundCoord(baseItem.y + dy);
            if (element.type === 'annulus' && baseItem.turningPoint) {
              if (!element.turningPoint) element.turningPoint = {};
              element.turningPoint.x = baseItem.turningPoint.x + dx;
              element.turningPoint.y = baseItem.turningPoint.y + dy;
            }
          }
          this.scheduleRender();
          return;
        }
        if (state.type === 'resize') {
          var target = this.findElementById(state.elementId);
          if (!target) {
            return;
          }
          if (state.handle === 'turningPoint') {
            var deltaX = point.x - state.startWorld.x;
            var deltaY = point.y - state.startWorld.y;
            var newTurningPoint = {
              x: roundCoord(state.baseRect.baseTurningPoint.x + deltaX),
              y: roundCoord(state.baseRect.baseTurningPoint.y + deltaY)
            };
            // 边界约束:拐点必须在矩形内部,且不能导致臂长小于 MIN_ELEMENT_SIZE
            var minX = target.x + MIN_ELEMENT_SIZE;
            var maxX = target.x + target.width - MIN_ELEMENT_SIZE;
            var minY = target.y + MIN_ELEMENT_SIZE;
            var maxY = target.y + target.height - MIN_ELEMENT_SIZE;
            newTurningPoint.x = clamp(newTurningPoint.x, minX, maxX);
            newTurningPoint.y = clamp(newTurningPoint.y, minY, maxY);
            // 额外保证两条臂至少为 MIN_ELEMENT_SIZE
            var leftArm = newTurningPoint.x - target.x;
            var rightArm = target.x + target.width - newTurningPoint.x;
            var topArm = newTurningPoint.y - target.y;
            var bottomArm = target.y + target.height - newTurningPoint.y;
            if (leftArm < MIN_ELEMENT_SIZE) {
              newTurningPoint.x = target.x + MIN_ELEMENT_SIZE;
            }
            if (rightArm < MIN_ELEMENT_SIZE) {
              newTurningPoint.x = target.x + target.width - MIN_ELEMENT_SIZE;
            }
            if (topArm < MIN_ELEMENT_SIZE) {
              newTurningPoint.y = target.y + MIN_ELEMENT_SIZE;
            }
            if (bottomArm < MIN_ELEMENT_SIZE) {
              newTurningPoint.y = target.y + target.height - MIN_ELEMENT_SIZE;
            }
            const isStillHalf = G.getIsStillHalf({
              ...target,
              turningPoint: {
                x: newTurningPoint.x,
                y: newTurningPoint.y
              }
            });
            if (!isStillHalf) {
              return;
            }
            target.turningPoint = {
              x: newTurningPoint.x,
              y: newTurningPoint.y
            };
            this.markStaticSceneDirty();
            this.scheduleRender();
            return;
          }
          var baseRect = state.baseRect;
          var left = baseRect.x;
          var right = baseRect.x + baseRect.width;
          var top = baseRect.y;
          var bottom = baseRect.y + baseRect.height;
          if (state.handle.indexOf('w') >= 0) {
            left = clamp(point.x, 0, right - MIN_ELEMENT_SIZE);
          }
          if (state.handle.indexOf('e') >= 0) {
            right = clamp(point.x, left + MIN_ELEMENT_SIZE, this.doc.canvasWidth);
          }
          if (state.handle.indexOf('n') >= 0) {
            top = clamp(point.y, 0, bottom - MIN_ELEMENT_SIZE);
          }
          if (state.handle.indexOf('s') >= 0) {
            bottom = clamp(point.y, top + MIN_ELEMENT_SIZE, this.doc.canvasHeight);
          }
          var snapped = this.collectResizeSnap(
            {
              x: left,
              y: top,
              width: right - left,
              height: bottom - top
            },
            state.handle,
            [target.id]
          );
          if (snapped) {
            if (state.handle.indexOf('w') >= 0 && snapped.left != null) {
              left = clamp(left + snapped.left, 0, right - MIN_ELEMENT_SIZE);
            }
            if (state.handle.indexOf('e') >= 0 && snapped.right != null) {
              right = clamp(right + snapped.right, left + MIN_ELEMENT_SIZE, this.doc.canvasWidth);
            }
            if (state.handle.indexOf('n') >= 0 && snapped.top != null) {
              top = clamp(top + snapped.top, 0, bottom - MIN_ELEMENT_SIZE);
            }
            if (state.handle.indexOf('s') >= 0 && snapped.bottom != null) {
              bottom = clamp(
                bottom + snapped.bottom,
                top + MIN_ELEMENT_SIZE,
                this.doc.canvasHeight
              );
            }
          }
          if (target.type === 'annulus' && target.shape !== 'rect') {
            const isStillHalf = G.getIsStillHalf({
              ...target,
              x: roundCoord(left),
              y: roundCoord(top),
              width: roundCoord(right - left),
              height: roundCoord(bottom - top)
            });
            if (!isStillHalf) {
              return;
            }
          }
          // 防止边越过 turningPoint 导致破坏 L 型结构,限制边距离 turningPoint 至少 MIN_ELEMENT_SIZE
          if (target.type === 'annulus' && target.shape !== 'rect' && target.turningPoint) {
            if (state.handle.indexOf('w') >= 0) {
              left = Math.min(left, target.turningPoint.x - MIN_ELEMENT_SIZE);
            }
            if (state.handle.indexOf('e') >= 0) {
              right = Math.max(right, target.turningPoint.x + MIN_ELEMENT_SIZE);
            }
            if (state.handle.indexOf('n') >= 0) {
              top = Math.min(top, target.turningPoint.y - MIN_ELEMENT_SIZE);
            }
            if (state.handle.indexOf('s') >= 0) {
              bottom = Math.max(bottom, target.turningPoint.y + MIN_ELEMENT_SIZE);
            }
          }
          target.x = roundCoord(left);
          target.y = roundCoord(top);
          target.width = roundCoord(right - left);
          target.height = roundCoord(bottom - top);
          this.scheduleRender();
        }
      },
      onWindowPointerUp: function (event) {
        if (!this.interactionState) {
          return;
        }
        if (
          this.currentPointerId != null &&
          event.pointerId != null &&
          this.currentPointerId !== event.pointerId
        ) {
          return;
        }
        if (this.pixiApp && this.pixiApp.view.releasePointerCapture && event.pointerId != null) {
          try {
            this.pixiApp.view.releasePointerCapture(event.pointerId);
          } catch (ignore) {}
        }
        this.currentPointerId = null;
        var state = this.interactionState;
        this.interactionState = null;
        if (state.type === 'pan') {
          this.updateCursor();
          this.schedulePanRefresh();
          this.scheduleRender();
          return;
        }
        if (state.type === 'marquee') {
          var rect = buildRectFromPoints(state.startWorld, state.currentWorld);
          if (rect.width > 2 && rect.height > 2) {
            var matched = (this.doc.elements || [])
              .filter(function (item) {
                return rectIntersects(rect, item);
              })
              .map(function (item) {
                return item.id;
              });
            this.setSelectedIds(
              state.additive ? Array.from(new Set(this.selectedIds.concat(matched))) : matched
            );
          }
          this.scheduleRender();
          return;
        }
        if (state.type === 'movePending') {
          this.updateCursor();
          return;
        }
        if (state.type === 'draw') {
          var drawRect = state.rect;
          if (
            drawRect &&
            drawRect.width >= MIN_ELEMENT_SIZE &&
            drawRect.height >= MIN_ELEMENT_SIZE
          ) {
            var newElement = {
              id: nextId(),
              type: state.elementType,
              x: roundCoord(drawRect.x),
              y: roundCoord(drawRect.y),
              width: roundCoord(drawRect.width),
              height: roundCoord(drawRect.height),
              value: '',
              shape: drawRect.shape,
              pathList: drawRect.pathList
            };
            if (isDeviceConfigType(newElement.type)) {
              newElement.value = JSON.stringify({
                trackId: this.getNextDeviceTrackId(null),
                deviceList: [
                  {
                    valueKey: '',
                    deviceNo: '',
                    progress: 0
                  }
                ]
              });
            }
            if (this.hasOverlap(newElement, [])) {
              this.showMessage('warning', '新元素不能与已有元素重叠');
            } else if (!this.isWithinCanvas(newElement)) {
              this.showMessage('warning', '新元素超出画布范围');
            } else {
              this.doc.elements.push(newElement);
              this.selectedIds = [newElement.id];
              this.commitMutation(state.beforeSnapshot);
              this.refreshInspector();
              return;
            }
          }
          this.refreshInspector();
          this.scheduleRender();
          return;
        }
        if (state.type === 'array') {
          var copies =
            state.previewItems && state.previewItems.length
              ? state.previewItems
              : this.buildArrayCopies(
                  state.template,
                  state.startWorld,
                  state.currentWorld || state.startWorld
                );
          if (!copies.length) {
            this.scheduleRender();
            return;
          }
          if (!this.canPlaceElements(copies, [])) {
            this.showMessage('warning', '阵列生成后会重叠或超出画布,已取消');
            this.scheduleRender();
            return;
          }
          var finalizedCopies = copies.map(function (item) {
            return $.extend({}, item, { id: nextId() });
          });
          var self = this;
          this.runMutation(function () {
            self.doc.elements = self.doc.elements.concat(finalizedCopies);
            self.selectedIds = [finalizedCopies[finalizedCopies.length - 1].id];
          });
          return;
        }
        if (state.type === 'move') {
          var movedElements = this.getSelectedElements();
          if (!this.canPlaceElements(movedElements, this.selectedIds.slice())) {
            for (var i = 0; i < state.baseItems.length; i++) {
              var base = state.baseItems[i];
              var element = this.findElementById(base.id);
              if (!element) {
                continue;
              }
              element.x = base.x;
              element.y = base.y;
              if (base.turningPoint) {
                element.turningPoint = {
                  x: base.turningPoint.x,
                  y: base.turningPoint.y
                };
              } else if (element.turningPoint) {
                delete element.turningPoint;
              }
            }
            this.showMessage('warning', '移动后会重叠或超出画布,已恢复');
            this.refreshInspector();
            this.scheduleRender();
            return;
          }
          if (!this.commitMutation(state.beforeSnapshot)) {
            this.markStaticSceneDirty();
            this.scheduleRender();
          }
          return;
        }
        if (state.type === 'resize') {
          var resized = this.findElementById(state.elementId);
          if (resized) {
            if (!this.isWithinCanvas(resized) || this.hasOverlap(resized, [resized.id])) {
              resized.x = state.baseRect.x;
              resized.y = state.baseRect.y;
              resized.width = state.baseRect.width;
              resized.height = state.baseRect.height;
              if (state.baseTurningPoint) {
                resized.turningPoint = {
                  x: state.baseTurningPoint.x,
                  y: state.baseTurningPoint.y
                };
              } else if (resized.turningPoint) {
                delete resized.turningPoint;
              }
              this.showMessage('warning', '缩放后会重叠或超出画布,已恢复');
              this.refreshInspector();
              this.scheduleRender();
              return;
            }
          }
          if (!this.commitMutation(state.beforeSnapshot)) {
            this.markStaticSceneDirty();
            this.scheduleRender();
          }
          return;
        }
        this.scheduleRender();
      },
      onWindowKeyDown: function (event) {
        if (event.key === ' ' && !isInputLike(event.target)) {
          this.spacePressed = true;
          this.updateCursor();
          event.preventDefault();
        }
        if (!this.doc) {
          return;
        }
        if (isInputLike(event.target)) {
          return;
        }
        var ctrl = event.ctrlKey || event.metaKey;
        if (event.key === 'Delete' || event.key === 'Backspace') {
          event.preventDefault();
          this.deleteSelection();
          return;
        }
        if (ctrl && (event.key === 'z' || event.key === 'Z')) {
          event.preventDefault();
          if (event.shiftKey) {
            this.redo();
          } else {
            this.undo();
          }
          return;
        }
        if (ctrl && (event.key === 'y' || event.key === 'Y')) {
          event.preventDefault();
          this.redo();
          return;
        }
        if (ctrl && (event.key === 'c' || event.key === 'C')) {
          event.preventDefault();
          this.copySelection();
          return;
        }
        if (ctrl && (event.key === 'v' || event.key === 'V')) {
          event.preventDefault();
          this.pasteClipboard();
          return;
        }
        if (event.key === 'Escape') {
          this.interactionState = null;
          this.setSelectedIds([]);
          this.hoverElementId = '';
          this.scheduleRender();
        }
      },
      onWindowKeyUp: function (event) {
        if (event.key === ' ') {
          this.spacePressed = false;
          this.updateCursor();
        }
      },
      onBeforeUnload: function (event) {
        if (!this.isDirty) {
          return;
        }
        event.preventDefault();
        event.returnValue = '';
      }
    }
  });
})();