From 7443e8040d9a7669a8117c8a6937dbd4bd792709 Mon Sep 17 00:00:00 2001
From: lsh <lsh@163.com>
Date: 星期二, 21 四月 2026 15:49:05 +0800
Subject: [PATCH] 添加环穿轨道

---
 src/main/webapp/static/js/basMap/editor.js | 8793 ++++++++++++++++++++++++++++++++---------------------------
 1 files changed, 4,756 insertions(+), 4,037 deletions(-)

diff --git a/src/main/webapp/static/js/basMap/editor.js b/src/main/webapp/static/js/basMap/editor.js
index 40205cf..f164257 100644
--- a/src/main/webapp/static/js/basMap/editor.js
+++ b/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', '鍦板浘鏂囦欢瑙f瀽澶辫触');
-                    }
-                };
-                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', '闃靛垪宸ュ叿褰撳墠鍙敮鎸佽揣鏋躲�丆RN銆佸弻宸ヤ綅鍜� 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', '鍦板浘鏂囦欢瑙f瀽澶辫触');
+          }
+        };
+        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) {
+            // 娉ㄦ剰锛歞rawElementsToLayers 浼氭牴鎹厓绱犵被鍨嬭嚜鍔ㄥ垎閰嶅埌 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 = '';
+      }
+    }
+  });
 })();

--
Gitblit v1.9.1