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/components/MapCanvas.js              | 3356 ++++++++----
 src/main/webapp/static/js/basMap/mapTrackGeometry.js | 1195 ++++
 src/main/webapp/views/watch/console.html             |    1 
 src/main/webapp/static/js/basMap/editor.js           | 8793 ++++++++++++++++++---------------
 .gitignore                                           |    4 
 src/main/webapp/views/watch/stationTrace.html        |    1 
 src/main/webapp/views/watch/fakeTrace.html           |    1 
 src/main/webapp/components/MapCanvasBak.js           |  258 +
 .oxfmtrc.json                                        |    4 
 src/main/webapp/views/basMap/editor.html             | 1847 ++++--
 10 files changed, 9,570 insertions(+), 5,890 deletions(-)

diff --git a/.gitignore b/.gitignore
index 6a97fd6..d5248b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,7 +34,9 @@
 
 ### VS Code ###
 .vscode/
+.history/
+.cursor/
 
 ### LOG ###
 stock
-LOG_PATH_IS_UNDEFINED
\ No newline at end of file
+LOG_PATH_IS_UNDEFINED
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
new file mode 100644
index 0000000..32ebab4
--- /dev/null
+++ b/.oxfmtrc.json
@@ -0,0 +1,4 @@
+{
+  "singleQuote": true,
+  "trailingComma": "none"
+}
diff --git a/src/main/webapp/components/MapCanvas.js b/src/main/webapp/components/MapCanvas.js
index a4c0a7d..f4fc487 100644
--- a/src/main/webapp/components/MapCanvas.js
+++ b/src/main/webapp/components/MapCanvas.js
@@ -1,3 +1,101 @@
+const EPSILON = 1; // 瀹瑰樊锛岀敤浜庡鐞嗘诞鐐规暟绮惧害闂
+
+const nowMs = () =>
+  typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
+
+const G = window.BasMapTrackGeometry;
+if (!G) {
+  throw new Error('mapTrackGeometry.js must be loaded before MapCanvas.js');
+}
+
+/**
+ * 閫氱敤杞鍣ㄧ被
+ * 灏佽甯﹁姹傚彇娑堛�佸钩婊戝欢杩熻绠椼�侀敊璇��閬跨殑杞閫昏緫
+ */
+class Poller {
+  /**
+   * @param {Object} options
+   * @param {Function} options.fetchFn - 鏁版嵁鑾峰彇鍑芥暟銆傚彲杩斿洖 Promise<number> 浣滀负鏈鑰楁椂(ms)瑕嗙洊榛樿璁℃椂锛涘惁鍒欑敱 Poller 鑷姩璁℃椂
+   * @param {number} [options.periodMs=1000] - 鐩爣杞鍛ㄦ湡锛堟绉掞級
+   * @param {number} [options.alpha=0.2] - EWMA骞虫粦绯绘暟
+   */
+  constructor(options) {
+    this.fetchFn = options.fetchFn;
+    this.periodMs = options.periodMs || 1000;
+    this.alpha = options.alpha || 0.2;
+
+    this.timer = null;
+    this.abortController = null;
+    this.inFlight = false;
+    this.ewmaMs = 0;
+    this.errorBackoffMs = 0;
+  }
+
+  start() {
+    if (this.timer) return;
+    this.errorBackoffMs = 0;
+    this.scheduleNext(0);
+  }
+
+  stop() {
+    if (this.timer) {
+      clearTimeout(this.timer);
+      this.timer = null;
+    }
+    if (this.abortController) {
+      try {
+        this.abortController.abort();
+      } catch (e) {}
+      this.abortController = null;
+    }
+    this.inFlight = false;
+  }
+
+  scheduleNext(delayMs) {
+    if (this.timer) clearTimeout(this.timer);
+    this.timer = setTimeout(() => this.pollOnce(), Math.max(0, delayMs));
+  }
+
+  computeNextDelay(lastDurationMs, hadError) {
+    const minMs = 0;
+    const maxMs = this.periodMs;
+
+    if (hadError) {
+      this.errorBackoffMs = this.periodMs;
+      return this.periodMs;
+    }
+
+    this.errorBackoffMs = 0;
+
+    const duration = Number.isFinite(lastDurationMs) ? lastDurationMs : 0;
+    this.ewmaMs = this.ewmaMs ? (1 - this.alpha) * this.ewmaMs + this.alpha * duration : duration;
+
+    const targetDelay = this.periodMs - this.ewmaMs;
+    const jitter = Math.floor(60 * (Math.random() - 0.5)); // +/- 30ms
+    return Math.min(maxMs, Math.max(minMs, Math.floor(targetDelay + jitter)));
+  }
+
+  async pollOnce() {
+    if (this.inFlight) return;
+    this.inFlight = true;
+    let hadError = false;
+    let durationMs = 0;
+
+    try {
+      const t0 = nowMs();
+      const ret = await this.fetchFn(this);
+      const t1 = nowMs();
+      durationMs = Number.isFinite(ret) ? ret : Math.max(0, Math.floor(t1 - t0));
+    } catch (e) {
+      hadError = true;
+    } finally {
+      this.inFlight = false;
+      const nextDelay = this.computeNextDelay(durationMs, hadError);
+      this.scheduleNext(nextDelay);
+    }
+  }
+}
+
 Vue.component('map-canvas', {
   template: `
     <div style="width: 100%; height: 100%; position: relative;">
@@ -12,7 +110,6 @@
             鍦坽{ item.loopNo }} |
             绔欑偣: {{ item.stationCount || 0 }} |
             浠诲姟: {{ item.taskCount || 0 }} |
-            鎵嬪姩: {{ item.manualStationCount || 0 }} |
             鎵胯浇: {{ formatLoadPercent(item.currentLoad) }}
           </div>
         </div>
@@ -22,7 +119,7 @@
         {{ shelfTooltip.text }}
       </div>
       <div style="position: absolute; top: 18px; right: 34px; z-index: 30; display: flex; flex-direction: column; align-items: flex-end; gap: 8px;">
-        <div :style="mapToolFpsStyle()">FPS {{ mapFps }}</div>
+        <div :style="mapToolFpsStyle()" id="map-fps">FPS {{ mapFps }}</div>
         <button type="button" @click="toggleMapToolPanel" :style="mapToolToggleStyle(showMapToolPanel)">{{ showMapToolPanel ? '鏀惰捣鎿嶄綔' : '鍦板浘鎿嶄綔' }}</button>
         <div v-show="showMapToolPanel" :style="mapToolBarStyle()">
           <div :style="mapToolRowStyle()">
@@ -33,6 +130,7 @@
           </div>
           <div :style="mapToolRowStyle()">
             <button type="button" @click="openStationColorConfigPage" :style="mapToolButtonStyle(false)">绔欑偣棰滆壊</button>
+            <button v-if="fakeOperationVisible" type="button" @click="openFakeOperationConfigPage" :style="mapToolButtonStyle(false)">浠跨湡鎿嶄綔</button>
           </div>
           <div v-if="levList && levList.length > 1" :style="mapToolFloorSectionStyle()">
             <div :style="mapToolSectionLabelStyle()">妤煎眰</div>
@@ -50,7 +148,17 @@
       </div>
     </div>
   `,
-  props: ['lev', 'levList', 'crnParam', 'rgvParam', 'devpParam', 'stationTaskRange', 'highlightOnParamChange', 'viewportPadding', 'hudPadding', 'traceOverlay'],
+  props: [
+    'lev',
+    'levList',
+    'crnParam',
+    'rgvParam',
+    'devpParam',
+    'stationTaskRange',
+    'highlightOnParamChange',
+    'viewportPadding',
+    'hudPadding'
+  ],
   data() {
     return {
       map: [],
@@ -61,6 +169,7 @@
       wsReconnectAttempts: 0,
       wsReconnectBaseDelay: 1000,
       wsReconnectMaxDelay: 15000,
+      annulusPoller: null,
       pixiApp: null,
       pixiStageList: [],
       pixiStaMap: new Map(),
@@ -77,10 +186,6 @@
       },
       pixiShelfMap: new Map(),
       pixiTrackMap: new Map(),
-      pixiCrnTextureMap: new Map(),
-      pixiRgvTextureMap: new Map(),
-      pixiDevpTextureMap: new Map(),
-      pixiCrnColorTextureMap: new Map(),
       pixiDevpTextureMap: new Map(),
       pixiCrnColorTextureMap: new Map(),
       pixiRgvColorTextureMap: new Map(),
@@ -106,8 +211,6 @@
       hoverRaf: null,
       objectsContainer: null,
       objectsContainer2: null,
-      traceOverlayContainer: null,
-      tracePulseTween: null,
       tracksContainer: null,
       tracksGraphics: null,
       shelvesContainer: null,
@@ -132,8 +235,6 @@
         loopList: [],
         totalStationCount: 0,
         taskStationCount: 0,
-        manualStationCount: 0,
-        occupiedStationCount: 0,
         currentLoad: 0
       },
       showMapToolPanel: false,
@@ -147,26 +248,70 @@
         'site-auto-run': 0xfa51f6,
         'site-auto-id': 0xc4c400,
         'site-auto-run-id': 0x30bffc,
-        'site-enable-in': 0xA81DEE,
+        'site-enable-in': 0xa81dee,
         'site-unauto': 0xb8b8b8,
         'machine-pakin': 0x30bffc,
         'machine-pakout': 0x97b400,
-        'site-run-block': 0xe69138
-      }
-    }
+        'site-run-block': 0xe69138,
+        'site-error': 0xDB2828
+      },
+      fakeOperationVisible: false,
+      tickerMap: new Map()
+    };
   },
-    mounted() {
+  mounted() {
+    this.DEVICE_MAP = {
+      crn: {
+        createTexture: this.createCrnTexture,
+        graphics: this.graphicsCrn,
+        emitName: 'crn-click',
+        pixiMap: this.pixiCrnMap,
+        type: 'crn',
+        idName: 'crnId',
+        statusInfo: {
+          name: 'crnStatus',
+          getStatus: this.getCrnStatusColor,
+          updateTextureColor: this.updateCrnTextureColor
+        }
+      },
+      dualcrn: {
+        createTexture: this.createCrnTexture,
+        graphics: this.graphicsCrn,
+        emitName: 'dual-crn-click',
+        pixiMap: this.pixiDualCrnMap,
+        type: 'dualCrn',
+        idName: 'crnId',
+        statusInfo: {
+          name: 'crnStatus',
+          getStatus: this.getCrnStatusColor,
+          updateTextureColor: this.updateCrnTextureColor
+        }
+      },
+      rgv: {
+        createTexture: this.createRgvTexture,
+        graphics: this.graphicsRgv,
+        emitName: 'rgv-click',
+        pixiMap: this.pixiRgvMap,
+        type: 'rgv',
+        idName: 'rgvNo',
+        statusInfo: {
+          name: 'rgvStatus',
+          getStatus: this.getRgvStatusColor,
+          updateTextureColor: this.updateRgvTextureColor
+        }
+      }
+    };
+    this.DEVICE_MAP.dualCrn = this.DEVICE_MAP.dualcrn;
     this.currentLev = this.lev || 1;
     this.createMap();
     this.startContainerResizeObserve();
     this.loadMapTransformConfig();
     this.loadStationColorConfig();
+    this.loadFakeProcessStatus();
     this.loadLocList();
     this.connectWs();
-
-    setTimeout(() => {
-      this.getMap(this.currentLev);
-    }, 1000);
+    // 杞ㄩ亾渚濊禆 /basMap/editor 鐨� map2锛涢』鍦ㄤ富鍦板浘 createMapData 瀹屾垚鍚庡啀鎷夊彇骞� drawTracks锛�
+    // 鍚﹀垯 WebSocket 鍏堝埌浼� clear tracksGraphics锛屾妸鍏堣繑鍥炵殑缂栬緫鍣ㄨ建閬撴摝鎺夈��
 
     this.timer = setInterval(() => {
       this.getCrnInfo();
@@ -175,29 +320,70 @@
       this.getCycleCapacityInfo();
       this.getRgvInfo();
     }, 1000);
+
+    this.startAnnulusDevicePoll();
+
+    // todo:娴嬭瘯浠g爜
+    setTimeout(() => {
+      this.createFakeButton();
+    }, 1000);
   },
   beforeDestroy() {
-    if (this.timer) { clearInterval(this.timer); }
+    if (this.timer) {
+      clearInterval(this.timer);
+    }
+    this.stopAnnulusDevicePoll();
 
-    if (this.hoverRaf) { cancelAnimationFrame(this.hoverRaf); this.hoverRaf = null; }
-    if (this.shelfCullRaf) { cancelAnimationFrame(this.shelfCullRaf); this.shelfCullRaf = null; }
-    if (this.resizeDebounceTimer) { clearTimeout(this.resizeDebounceTimer); this.resizeDebounceTimer = null; }
-    if (window.gsap && this.pixiApp && this.pixiApp.stage) { window.gsap.killTweensOf(this.pixiApp.stage.position); }
-    this.clearTraceOverlay();
-    if (this.pixiApp) { this.pixiApp.destroy(true, { children: true }); }
-    if (this.containerResizeObserver) { this.containerResizeObserver.disconnect(); this.containerResizeObserver = null; }
+    if (this.hoverRaf) {
+      cancelAnimationFrame(this.hoverRaf);
+      this.hoverRaf = null;
+    }
+    if (this.shelfCullRaf) {
+      cancelAnimationFrame(this.shelfCullRaf);
+      this.shelfCullRaf = null;
+    }
+    if (this.resizeDebounceTimer) {
+      clearTimeout(this.resizeDebounceTimer);
+      this.resizeDebounceTimer = null;
+    }
+    if (window.gsap && this.pixiApp && this.pixiApp.stage) {
+      window.gsap.killTweensOf(this.pixiApp.stage.position);
+    }
+    if (this.pixiApp) {
+      this.pixiApp.destroy(true, { children: true });
+    }
+    if (this.containerResizeObserver) {
+      this.containerResizeObserver.disconnect();
+      this.containerResizeObserver = null;
+    }
     window.removeEventListener('resize', this.scheduleResizeToContainer);
-    if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; }
-    if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { try { this.ws.close(); } catch (e) {} }
+    if (this.wsReconnectTimer) {
+      clearTimeout(this.wsReconnectTimer);
+      this.wsReconnectTimer = null;
+    }
+    if (
+      this.ws &&
+      (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)
+    ) {
+      try {
+        this.ws.close();
+      } catch (e) {}
+    }
   },
   watch: {
     lev(newLev) {
-      if (newLev != null) { this.changeFloor(newLev); }
+      if (newLev != null) {
+        this.changeFloor(newLev);
+      }
     },
     viewportPadding: {
       deep: true,
       handler(newVal, oldVal) {
-        if (this.mapContentSize && this.mapContentSize.width > 0 && this.mapContentSize.height > 0) {
+        if (
+          this.mapContentSize &&
+          this.mapContentSize.width > 0 &&
+          this.mapContentSize.height > 0
+        ) {
           this.adjustStageForViewportPadding(oldVal, newVal);
         }
       }
@@ -205,53 +391,90 @@
     crnParam: {
       deep: true,
       handler(v) {
-        if (!this.highlightOnParamChange) { return; }
-        if (v && v.crnNo && this.pixiCrnMap) {
-          const id = parseInt(v.crnNo, 10);
-          const sprite = this.pixiCrnMap.get(id);
-          if (sprite && window.gsap) {
-            window.gsap.killTweensOf(sprite);
-            window.gsap.fromTo(sprite, { alpha: 1 }, { alpha: 0.2, yoyo: true, repeat: 6, duration: 0.15 });
-          }
+        if (!this.highlightOnParamChange || !v || !v.crnNo || !this.pixiCrnMap) {
+          return;
         }
+        this.highlightParamSpriteWithGsap(this.pixiCrnMap.get(parseInt(v.crnNo, 10)));
       }
     },
     rgvParam: {
       deep: true,
       handler(v) {
-        if (!this.highlightOnParamChange) { return; }
-        if (v && v.rgvNo && this.pixiRgvMap) {
-          const id = parseInt(v.rgvNo, 10);
-          const sprite = this.pixiRgvMap.get(id);
-          if (sprite && window.gsap) {
-            window.gsap.killTweensOf(sprite);
-            window.gsap.fromTo(sprite, { alpha: 1 }, { alpha: 0.2, yoyo: true, repeat: 6, duration: 0.15 });
-          }
+        if (!this.highlightOnParamChange || !v || !v.rgvNo || !this.pixiRgvMap) {
+          return;
         }
+        this.highlightParamSpriteWithGsap(this.pixiRgvMap.get(parseInt(v.rgvNo, 10)));
       }
     },
     devpParam: {
       deep: true,
       handler(v) {
-        if (!this.highlightOnParamChange) { return; }
-        if (v && v.stationId && this.pixiStaMap) {
-          const id = parseInt(v.stationId, 10);
-          const sprite = this.pixiStaMap.get(id);
-          if (sprite && window.gsap) {
-            window.gsap.killTweensOf(sprite);
-            window.gsap.fromTo(sprite, { alpha: 1 }, { alpha: 0.2, yoyo: true, repeat: 6, duration: 0.15 });
-          }
+        if (!this.highlightOnParamChange || !v || !v.stationId || !this.pixiStaMap) {
+          return;
         }
-      }
-    },
-    traceOverlay: {
-      deep: true,
-      handler() {
-        this.renderTraceOverlay();
+        this.highlightParamSpriteWithGsap(this.pixiStaMap.get(parseInt(v.stationId, 10)));
       }
     }
   },
   methods: {
+    highlightParamSpriteWithGsap(sprite) {
+      if (!sprite || !window.gsap) {
+        return;
+      }
+      window.gsap.killTweensOf(sprite);
+      window.gsap.fromTo(
+        sprite,
+        { alpha: 1 },
+        { alpha: 0.2, yoyo: true, repeat: 6, duration: 0.15 }
+      );
+    },
+    /**
+     * 鍒囨崲妤煎眰鎴栭噸杞藉湴鍥惧墠鐨勫叡鐢ㄦ竻鍦猴紙鍙�夛細瀵硅澶囩簿鐏� kill GSAP锛夈��
+     * @param {{ killDeviceGsap?: boolean, skipClearLoopHighlight?: boolean }} options
+     */
+    prepareMapSceneReload(options) {
+      const opts = options || {};
+      if (!opts.skipClearLoopHighlight) {
+        this.clearLoopStationHighlight();
+      }
+      this.hideShelfTooltip();
+      this.hoveredShelfCell = null;
+      this.mapRowOffsets = [];
+      this.mapRowHeights = [];
+      this.mapColOffsets = [];
+      this.mapColWidths = [];
+      if (this.adjustLabelTimer) {
+        clearTimeout(this.adjustLabelTimer);
+        this.adjustLabelTimer = null;
+      }
+      if (opts.killDeviceGsap && window.gsap) {
+        [this.pixiStaMap, this.pixiCrnMap, this.pixiDualCrnMap, this.pixiRgvMap].forEach((m) => {
+          m &&
+            m.forEach((s) => {
+              try {
+                window.gsap.killTweensOf(s);
+              } catch (e) {}
+            });
+        });
+      }
+      this.objectsContainer.removeChildren();
+      this.objectsContainer2.removeChildren();
+      if (this.tracksContainer) {
+        this.tracksContainer.removeChildren();
+      }
+      if (this.tracksGraphics) {
+        this.tracksGraphics.clear();
+      }
+      this.clearShelfChunks();
+      this.crnList = [];
+      this.dualCrnList = [];
+      this.rgvList = [];
+      this.pixiCrnMap.clear();
+      this.pixiDualCrnMap.clear();
+      this.pixiRgvMap.clear();
+      this.pixiStaMap = new Map();
+      this.pixiStageList = [];
+    },
     cycleCapacityPanelStyle() {
       const hud = this.hudPadding || {};
       const left = Math.max(14, Number(hud.left) || 0);
@@ -382,11 +605,19 @@
       this.showMapToolPanel = !this.showMapToolPanel;
     },
     selectFloorFromTool(lev) {
-      if (lev == null || lev === this.currentLev) { return; }
+      if (lev == null || lev === this.currentLev) {
+        return;
+      }
       this.$emit('switch-lev', lev);
     },
     createMap() {
-      this.pixiApp = new PIXI.Application({ backgroundColor: 0xF5F7F9, antialias: false, powerPreference: 'high-performance', autoDensity: true, resolution: Math.min(window.devicePixelRatio || 1, 2) });
+      this.pixiApp = new PIXI.Application({
+        backgroundColor: 0xf5f7f9,
+        antialias: false,
+        powerPreference: 'high-performance',
+        autoDensity: true,
+        resolution: Math.min(window.devicePixelRatio || 1, 2)
+      });
       PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.LINEAR;
       this.$refs.pixiView.appendChild(this.pixiApp.view);
       this.pixiApp.view.style.width = '100%';
@@ -398,8 +629,13 @@
       this.graphicsRgvTrack = this.createTrackTexture(25, 25, 10);
       this.objectsContainer = new PIXI.Container();
       this.objectsContainer2 = new PIXI.Container();
-      this.traceOverlayContainer = new PIXI.Container();
-      this.tracksContainer = new PIXI.ParticleContainer(10000, { scale: true, position: true, rotation: false, uvs: false, alpha: false });
+      this.tracksContainer = new PIXI.ParticleContainer(10000, {
+        scale: true,
+        position: true,
+        rotation: false,
+        uvs: false,
+        alpha: false
+      });
       this.tracksGraphics = new PIXI.Graphics();
       this.shelvesContainer = new PIXI.Container();
       this.tracksContainer.autoResize = true;
@@ -410,22 +646,26 @@
       this.mapRoot.addChild(this.shelvesContainer);
       this.mapRoot.addChild(this.objectsContainer);
       this.mapRoot.addChild(this.objectsContainer2);
-      this.mapRoot.addChild(this.traceOverlayContainer);
       this.pixiApp.renderer.roundPixels = true;
       this.hoveredShelfCell = null;
       this.hoverPointer = { x: 0, y: 0 };
       this.hoverRaf = null;
-      
-
 
       //*******************shelf hover*******************
       this.pixiApp.renderer.plugins.interaction.on('pointermove', (event) => {
-        if (!this.isShelfTooltipAllowed()) { this.hideShelfTooltip(); return; }
-        if (!this.map || !this.mapRoot) { return; }
+        if (!this.isShelfTooltipAllowed()) {
+          this.hideShelfTooltip();
+          return;
+        }
+        if (!this.map || !this.mapRoot) {
+          return;
+        }
         const pos = event.data.global;
         this.hoverPointer.x = pos.x;
         this.hoverPointer.y = pos.y;
-        if (this.hoverRaf) { return; }
+        if (this.hoverRaf) {
+          return;
+        }
         this.hoverRaf = requestAnimationFrame(() => {
           this.hoverRaf = null;
           this.updateShelfHoverFromPointer(this.hoverPointer);
@@ -443,7 +683,9 @@
         const globalPos = event.data.global;
         stageOriginalPos = [this.pixiApp.stage.position.x, this.pixiApp.stage.position.y];
         mouseDownPoint = [globalPos.x, globalPos.y];
-        if (!event.target || (event.target && event.target._kind === 'shelf')) { touchBlank = true; }
+        if (!event.target || (event.target && event.target._kind === 'shelf')) {
+          touchBlank = true;
+        }
       });
       this.pixiApp.renderer.plugins.interaction.on('pointermove', (event) => {
         const globalPos = event.data.global;
@@ -451,11 +693,13 @@
           const dx = globalPos.x - mouseDownPoint[0];
           const dy = globalPos.y - mouseDownPoint[1];
           this.pixiApp.stage.position.set(stageOriginalPos[0] + dx, stageOriginalPos[1] + dy);
+          this.scheduleAdjustLabels();
           this.scheduleShelfChunkCulling();
         }
       });
-      this.pixiApp.renderer.plugins.interaction.on('pointerup', () => { touchBlank = false; });
-
+      this.pixiApp.renderer.plugins.interaction.on('pointerup', () => {
+        touchBlank = false;
+      });
 
       //*******************缂╂斁鐢诲竷*******************
       this.pixiApp.view.addEventListener('wheel', (event) => {
@@ -482,22 +726,24 @@
       });
       //*******************缂╂斁鐢诲竷*******************
 
-
       //*******************FPS*******************
       let g_Time = 0;
       let fpsLastUpdateTs = 0;
       let fpsDeltaSumMs = 0;
       let fpsFrameCount = 0;
       const fpsUpdateInterval = 200;
+      const fpsElement = document.getElementById('map-fps');
       this.pixiApp.ticker.add((delta) => {
-        const timeNow = (new Date()).getTime();
+        const timeNow = nowMs();
         const timeDiff = timeNow - g_Time;
         g_Time = timeNow;
         fpsDeltaSumMs += timeDiff;
         fpsFrameCount += 1;
         if (timeNow - fpsLastUpdateTs >= fpsUpdateInterval) {
-          const avgFps = fpsDeltaSumMs > 0 ? (fpsFrameCount * 1000 / fpsDeltaSumMs) : 0;
-          this.mapFps = Math.round(avgFps);
+          const avgFps = fpsDeltaSumMs > 0 ? (fpsFrameCount * 1000) / fpsDeltaSumMs : 0;
+          // this.mapFps = Math.round(avgFps);
+          // 涓嶈蛋vue锛岀洿鎺ユ搷浣渄om闄嶄綆鎬ц兘娑堣��
+          fpsElement.innerText = 'FPS ' + Math.round(avgFps);
           fpsDeltaSumMs = 0;
           fpsFrameCount = 0;
           fpsLastUpdateTs = timeNow;
@@ -506,7 +752,9 @@
       //*******************FPS*******************
     },
     startContainerResizeObserve() {
-      if (typeof ResizeObserver === 'undefined' || !this.$el) { return; }
+      if (typeof ResizeObserver === 'undefined' || !this.$el) {
+        return;
+      }
       this.containerResizeObserver = new ResizeObserver(() => {
         this.scheduleResizeToContainer();
       });
@@ -522,7 +770,9 @@
       }, 80);
     },
     getViewportSize() {
-      if (!this.pixiApp || !this.pixiApp.renderer) { return { width: 0, height: 0 }; }
+      if (!this.pixiApp || !this.pixiApp.renderer) {
+        return { width: 0, height: 0 };
+      }
       const screen = this.pixiApp.renderer.screen;
       if (screen && screen.width > 0 && screen.height > 0) {
         return { width: screen.width, height: screen.height };
@@ -556,9 +806,13 @@
       };
     },
     adjustStageForViewportPadding(oldPadding, newPadding) {
-      if (!this.pixiApp || !this.pixiApp.stage) { return; }
+      if (!this.pixiApp || !this.pixiApp.stage) {
+        return;
+      }
       const viewport = this.getViewportSize();
-      if (viewport.width <= 0 || viewport.height <= 0) { return; }
+      if (viewport.width <= 0 || viewport.height <= 0) {
+        return;
+      }
       const prevCenter = this.getViewportCenter(viewport, oldPadding);
       const nextCenter = this.getViewportCenter(viewport, newPadding);
       const deltaX = nextCenter.x - prevCenter.x;
@@ -595,81 +849,79 @@
       const w = this.$el.clientWidth || 0;
       const h = this.$el.clientHeight || 0;
       if (w > 0 && h > 0 && this.pixiApp) {
-        const vw = this.pixiApp.renderer && this.pixiApp.renderer.screen ? this.pixiApp.renderer.screen.width : 0;
-        const vh = this.pixiApp.renderer && this.pixiApp.renderer.screen ? this.pixiApp.renderer.screen.height : 0;
-        if (vw === w && vh === h) { return; }
+        const vw =
+          this.pixiApp.renderer && this.pixiApp.renderer.screen
+            ? this.pixiApp.renderer.screen.width
+            : 0;
+        const vh =
+          this.pixiApp.renderer && this.pixiApp.renderer.screen
+            ? this.pixiApp.renderer.screen.height
+            : 0;
+        if (vw === w && vh === h) {
+          return;
+        }
         this.pixiApp.renderer.resize(w, h);
-        if (this.mapContentSize && this.mapContentSize.width > 0 && this.mapContentSize.height > 0) {
+        if (
+          this.mapContentSize &&
+          this.mapContentSize.width > 0 &&
+          this.mapContentSize.height > 0
+        ) {
           this.applyMapTransform(true);
         }
       }
     },
     getMap() {
-      this.sendWs(JSON.stringify({ url: "/basMap/lev/" + this.currentLev + "/auth", data: {} }));
+      this.sendWs(
+        JSON.stringify({
+          url: '/basMap/lev/' + this.currentLev + '/auth',
+          data: {}
+        })
+      );
     },
     changeFloor(lev) {
       this.currentLev = lev;
       this.clearLoopStationHighlight();
-      this.clearTraceOverlay();
       this.isSwitchingFloor = true;
-      this.hideShelfTooltip();
-      this.hoveredShelfCell = null;
-      this.mapRowOffsets = [];
-      this.mapRowHeights = [];
-      this.mapColOffsets = [];
-      this.mapColWidths = [];
-      if (this.adjustLabelTimer) { clearTimeout(this.adjustLabelTimer); this.adjustLabelTimer = null; }
-      this.objectsContainer.removeChildren();
-      this.objectsContainer2.removeChildren();
-      if (this.tracksContainer) { this.tracksContainer.removeChildren(); }
-      if (this.tracksGraphics) { this.tracksGraphics.clear(); }
-      this.clearShelfChunks();
-      this.crnList = [];
-      this.dualCrnList = [];
-      this.rgvList = [];
-      this.pixiCrnMap = new Map();
-      this.pixiDualCrnMap = new Map();
-      this.pixiRgvMap = new Map();
-      this.pixiStaMap = new Map();
-      this.pixiStageList = [];
+      this.prepareMapSceneReload({ killDeviceGsap: false, skipClearLoopHighlight: true });
       this.getMap();
     },
+    setMap(res) {
+      this.createMapData(JSON.parse(res.data));
+    },
+    // 杞ㄩ亾鍦∕ap2涓�
+    setMap2() {
+      $.ajax({
+        url: baseUrl + `/basMap/editor/${this.currentLev}/auth`,
+        headers: { token: localStorage.getItem('token') },
+        method: 'get',
+        success: function (res) {
+          if (res && res.code === 200 && res.data && res.data.elements) {
+            this.createMap2Data(
+              res.data.elements.filter((item) =>
+                ['crn', 'dualCrn', 'rgv', 'annulus'].includes(item.type)
+              )
+            );
+          }
+        }.bind(this)
+      });
+    },
     createMapData(map) {
-      this.clearLoopStationHighlight();
-      this.clearTraceOverlay();
-      this.hideShelfTooltip();
-      this.hoveredShelfCell = null;
-      this.mapRowOffsets = [];
-      this.mapRowHeights = [];
-      this.mapColOffsets = [];
-      this.mapColWidths = [];
-      if (window.gsap) {
-        this.pixiStaMap && this.pixiStaMap.forEach((s) => { try { window.gsap.killTweensOf(s); } catch (e) {} });
-        this.pixiCrnMap && this.pixiCrnMap.forEach((s) => { try { window.gsap.killTweensOf(s); } catch (e) {} });
-        this.pixiDualCrnMap && this.pixiDualCrnMap.forEach((s) => { try { window.gsap.killTweensOf(s); } catch (e) {} });
-        this.pixiRgvMap && this.pixiRgvMap.forEach((s) => { try { window.gsap.killTweensOf(s); } catch (e) {} });
-      }
-      this.objectsContainer.removeChildren();
-      this.objectsContainer2.removeChildren();
-      if (this.tracksContainer) { this.tracksContainer.removeChildren(); }
-      if (this.tracksGraphics) { this.tracksGraphics.clear(); }
-      this.clearShelfChunks();
-      this.crnList = [];
-      this.dualCrnList = [];
-      this.rgvList = [];
-      this.pixiCrnMap = new Map();
-      this.pixiDualCrnMap = new Map();
-      this.pixiRgvMap = new Map();
-      this.pixiStaMap = new Map();
-      this.pixiStageList = [];
+      this.prepareMapSceneReload({ killDeviceGsap: true });
       this.pixiStageList = [map.length];
+
+      this.setMap2();
+
       const bayHeightList = this.initHeight(map);
       const bayWidthList = this.initWidth(map);
       map.forEach((item, index) => {
         for (let idx = 0; idx < item.length; idx++) {
           let val = item[idx];
-          if (val.cellHeight == undefined || val.cellHeight === '') { val.cellHeight = bayHeightList[index]; }
-          if (val.cellWidth == undefined || val.cellWidth === '') { val.cellWidth = bayWidthList[idx]; }
+          if (val.cellHeight == undefined || val.cellHeight === '') {
+            val.cellHeight = bayHeightList[index];
+          }
+          if (val.cellWidth == undefined || val.cellWidth === '') {
+            val.cellWidth = bayWidthList[idx];
+          }
         }
       });
 
@@ -684,7 +936,9 @@
           if (val.rowSpan > 1) {
             for (let i = 1; i < val.rowSpan; i++) {
               let nextMerge = map[index + i][idx];
-              if (nextMerge.type != 'merge') { continue; }
+              if (nextMerge.type != 'merge') {
+                continue;
+              }
               let mergeCellHeight = nextMerge.cellHeight / 8;
               mergeHeight += mergeCellHeight;
             }
@@ -694,7 +948,9 @@
           if (val.colSpan > 1) {
             for (let i = 1; i < val.colSpan; i++) {
               let nextMerge = map[index][idx + i];
-              if (!nextMerge) { continue; }
+              if (!nextMerge) {
+                continue;
+              }
               let mergeCellWidth = nextMerge.cellWidth / 40;
               mergeWidth += mergeCellWidth;
               nextMerge.isMergedPart = true;
@@ -704,6 +960,7 @@
         }
       });
 
+      //
       const rowHeightScaled = [];
       for (let r = 0; r < map.length; r++) {
         const h = bayHeightList[r];
@@ -713,7 +970,10 @@
           let fallback = 0;
           for (let c = 0; c < map[r].length; c++) {
             const v = map[r][c];
-            if (v && v.type !== 'merge' && v.height != null && v.height > 0) { fallback = v.height; break; }
+            if (v && v.type !== 'merge' && v.height != null && v.height > 0) {
+              fallback = v.height;
+              break;
+            }
           }
           rowHeightScaled[r] = fallback > 0 ? fallback : 25;
         }
@@ -722,7 +982,7 @@
       let yCursor = 0;
       for (let r = 0; r < map.length; r++) {
         yOffsets[r] = yCursor;
-        yCursor += (rowHeightScaled[r] || 0);
+        yCursor += rowHeightScaled[r] || 0;
       }
 
       map.forEach((row, rowIndex) => {
@@ -745,7 +1005,9 @@
           if (val.colSpan > 1) {
             for (let i = 1; i < val.colSpan; i++) {
               const next = row[colIndex + i];
-              if (!next) { break; }
+              if (!next) {
+                break;
+              }
               next.posX = anchorX;
               next.posY = yOffsets[rowIndex];
             }
@@ -757,14 +1019,12 @@
       map.forEach((row, rowIndex) => {
         for (let colIndex = 0; colIndex < row.length; colIndex++) {
           const val = row[colIndex];
-          if (!val || val.type !== 'devp' || val.type === 'merge') { continue; }
+          if (!val || val.type !== 'devp' || val.type === 'merge') {
+            continue;
+          }
           val.stationDirectionList = this.resolveStationDirectionList(map, rowIndex, colIndex, val);
         }
       });
-
-      this.buildShelfHitGrid(map, rowHeightScaled, yOffsets);
-
-      this.drawTracks(map);
 
       map.forEach((item, index) => {
         this.pixiStageList[index] = [item.length];
@@ -772,116 +1032,30 @@
           let val = item[idx];
           val.rowIndex = index;
           val.colIndex = idx;
-          if (val.type === 'merge') { continue; }
-          if (val.type == undefined || val.type === 'none') { continue; }
-          if (this.isTrackType(val)) {
-            this.collectTrackItem(val);
+          if (val.type === 'merge') {
             continue;
           }
-          if (val.type === 'shelf') { continue; }
-          let sprite = this.getSprite(val, (e) => {
-            //鍥炶皟
-          });
-          if (sprite == null) { continue; }
+          if (val.type == undefined || val.type === 'none') {
+            continue;
+          }
+          // if (this.isTrackType(val)) {
+          //   this.collectTrackItem(val);
+          //   continue;
+          // }
+          if (val.type === 'shelf') {
+            continue;
+          }
+          if (['crn', 'dualCrn', 'rgv', 'annulus'].includes(val.type)) {
+            continue;
+          }
+          // 鏀堕泦crnList绛�
+          let sprite = this.getSprite(val, (e) => {});
+          if (sprite == null) {
+            continue;
+          }
           this.objectsContainer.addChild(sprite);
           this.pixiStageList[index][idx] = sprite;
         }
-      });
-
-      this.crnList.forEach((item) => {
-        let sprite = this.createCrnSprite(item.width * 0.9, item.height * 0.9);
-        const deviceNo = this.getDeviceNo(item.value);
-        const taskNo = this.getTaskNo(item.value);
-        const style = new PIXI.TextStyle({ fontFamily: 'Arial', fontSize: 12, fill: '#000000', stroke: '#ffffff', strokeThickness: 1 });
-        const txt = taskNo > 0 ? (deviceNo + "(" + taskNo + ")") : String(deviceNo);
-        const text = new PIXI.Text(txt, style);
-        text.anchor.set(0.5);
-        text.position.set(sprite.width / 2, sprite.height / 2);
-        sprite.addChild(text);
-        sprite.textObj = text;
-        sprite.position.set(item.posX, item.posY);
-        sprite.interactive = true; // 蹇呴』瑕佽缃墠鑳芥帴鏀朵簨浠�
-        sprite.buttonMode = true; // 璁╁厜鏍囧湪hover鏃跺彉涓烘墜鍨嬫寚浜嬩欢
-        sprite.on('pointerdown', () => {
-          if (window.gsap) { window.gsap.killTweensOf(sprite); }
-          sprite.alpha = 1;
-          const id = parseInt(deviceNo, 10);
-          this.$emit('crn-click', id);
-        });
-        let rowIndexForCrn = 0;
-        for (let r = 0; r < map.length; r++) {
-          if (map[r].length > 0) {
-            const rowY = map[r][0].posY;
-            if (Math.abs(rowY - item.posY) < 0.5) { rowIndexForCrn = r; break; }
-          }
-        }
-        sprite.rowIndex = rowIndexForCrn;
-        this.pixiCrnMap.set(parseInt(deviceNo), sprite);
-        this.objectsContainer2.addChild(sprite);
-      });
-      
-      this.dualCrnList.forEach((item) => {
-        let sprite = this.createCrnSprite(item.width * 0.9, item.height * 0.9);
-        const deviceNo = this.getDeviceNo(item.value);
-        const taskNo = this.getTaskNo(item.value);
-        const style = new PIXI.TextStyle({ fontFamily: 'Arial', fontSize: 12, fill: '#000000', stroke: '#ffffff', strokeThickness: 1 });
-        const txt = taskNo > 0 ? (deviceNo + "(" + taskNo + ")") : String(deviceNo);
-        const text = new PIXI.Text(txt, style);
-        text.anchor.set(0.5);
-        text.position.set(sprite.width / 2, sprite.height / 2);
-        sprite.addChild(text);
-        sprite.textObj = text;
-        sprite.position.set(item.posX, item.posY);
-        sprite.interactive = true;
-        sprite.buttonMode = true;
-        sprite.on('pointerdown', () => {
-          if (window.gsap) { window.gsap.killTweensOf(sprite); }
-          sprite.alpha = 1;
-          const id = parseInt(deviceNo, 10);
-          this.$emit('dual-crn-click', id);
-        });
-        let rowIndexForCrn = 0;
-        for (let r = 0; r < map.length; r++) {
-          if (map[r].length > 0) {
-            const rowY = map[r][0].posY;
-            if (Math.abs(rowY - item.posY) < 0.5) { rowIndexForCrn = r; break; }
-          }
-        }
-        sprite.rowIndex = rowIndexForCrn;
-        this.pixiDualCrnMap.set(parseInt(deviceNo), sprite);
-        this.objectsContainer2.addChild(sprite);
-      });
-      
-      this.rgvList.forEach((item) => {
-        let sprite = this.createRgvSprite(item.width * 0.9, item.height * 0.9);
-        const deviceNo = this.getDeviceNo(item.value);
-        const taskNo = this.getTaskNo(item.value);
-        const style = new PIXI.TextStyle({ fontFamily: 'Arial', fontSize: 12, fill: '#000000', stroke: '#ffffff', strokeThickness: 1 });
-        const txt = taskNo > 0 ? (deviceNo + "(" + taskNo + ")") : String(deviceNo);
-        const text = new PIXI.Text(txt, style);
-        text.anchor.set(0.5);
-        text.position.set(sprite.width / 2, sprite.height / 2);
-        sprite.addChild(text);
-        sprite.textObj = text;
-        sprite.position.set(item.posX, item.posY);
-        sprite.interactive = true; // 蹇呴』瑕佽缃墠鑳芥帴鏀朵簨浠�
-        sprite.buttonMode = true; // 璁╁厜鏍囧湪hover鏃跺彉涓烘墜鍨嬫寚浜嬩欢
-        sprite.on('pointerdown', () => {
-          if (window.gsap) { window.gsap.killTweensOf(sprite); }
-          sprite.alpha = 1;
-          const id = parseInt(deviceNo, 10);
-          this.$emit('rgv-click', id);
-        });
-        let rowIndexForRgv = 0;
-        for (let r = 0; r < map.length; r++) {
-          if (map[r].length > 0) {
-            const rowY = map[r][0].posY;
-            if (Math.abs(rowY - item.posY) < 0.5) { rowIndexForRgv = r; break; }
-          }
-        }
-        sprite.rowIndex = rowIndexForRgv;
-        this.pixiRgvMap.set(parseInt(deviceNo), sprite);
-        this.objectsContainer2.addChild(sprite);
       });
 
       let contentW = 0;
@@ -889,19 +1063,122 @@
       for (let r = 0; r < map.length; r++) {
         for (let c = 0; c < map[r].length; c++) {
           const cell = map[r][c];
-          if (!cell || cell.type === 'merge') { continue; }
+          if (!cell || cell.type === 'merge') {
+            continue;
+          }
           const right = cell.posX + cell.width;
           const bottom = cell.posY + cell.height;
-          if (right > contentW) { contentW = right; }
-          if (bottom > contentH) { contentH = bottom; }
+          if (right > contentW) {
+            contentW = right;
+          }
+          if (bottom > contentH) {
+            contentH = bottom;
+          }
         }
       }
       this.mapContentSize = { width: contentW, height: contentH };
       this.buildShelfChunks(map, contentW, contentH);
+      this.buildShelfHitGrid(map, rowHeightScaled, yOffsets);
       this.applyMapTransform(true);
       this.map = map;
       this.isSwitchingFloor = false;
-      this.renderTraceOverlay();
+    },
+    createMap2Data(map2) {
+      this.map2 = map2;
+
+      const handleDevice = (deviceTypeInfo, device, trackInfo) => {
+        // 浣跨敤 G.getDeviceInfo 宸茬粡璁$畻濂界殑灏哄锛堝凡閫氳繃 normalizeDeviceSizeOverride 澶勭悊 deviceLength/deviceWidth锛�
+        const along = device.width;
+        const across = device.height;
+        // 姣忎釜璁惧鐙珛鍒涘缓绾圭悊锛屼笉瑕佺紦瀛樺鐢紝鍚﹀垯鎵�鏈夎澶囧昂瀵镐細鐩稿悓
+        const graphics = deviceTypeInfo.createTexture(along, across);
+        let sprite = new PIXI.Sprite(graphics);
+        const deviceNo = device.deviceNo || device[deviceTypeInfo.idName];
+        const style = new PIXI.TextStyle({
+          fontFamily: 'Arial',
+          fontSize: 10,
+          fill: '#000000',
+          stroke: '#ffffff',
+          strokeThickness: 1,
+          align: 'center'
+        });
+        const txt = deviceNo != null ? String(deviceNo) : '';
+
+        const text = new PIXI.Text(txt, style);
+        text.anchor.set(0.5);
+        sprite.addChild(text);
+        text.position.set(sprite.width / 2, sprite.height / 2);
+        sprite.textObj = text;
+
+        // 杩欓噷item宸茬粡鏄腑蹇冪偣浜嗭紝鐩存帴鐢ㄥ氨琛�
+        device.width = sprite.width;
+        device.height = sprite.height;
+        if (['crn', 'dualCrn', 'rgv'].includes(trackInfo.type)) {
+          const isHorizontal = trackInfo.width > trackInfo.height;
+          const startX = isHorizontal ? trackInfo.x : trackInfo.width / 2 + trackInfo.x;
+          const startY = isHorizontal ? trackInfo.height / 2 + trackInfo.y : trackInfo.y;
+          // 杩欎簺璁惧璺緞鍥哄畾鏄竴鏉$洿绾匡紝杩欓噷鎵嬪姩璧嬪�艰矾寰�
+          trackInfo.pathList = [
+            {
+              x: isHorizontal ? startX + trackInfo.width : startX,
+              y: isHorizontal ? startY : startY + trackInfo.height,
+              type: 'line',
+              startX,
+              startY
+            }
+          ];
+        }
+        sprite.trackInfo = trackInfo;
+        const mappingInfo = this.getMappingInfo({ ...sprite, ...device });
+        sprite.mappingInfo = mappingInfo;
+        sprite.path = mappingInfo.path;
+        sprite.currentAngle = mappingInfo.angle;
+        // sprite.vector = mappingInfo.vector
+        // sprite.time = Date.now()
+        sprite.anchor.set(0.5);
+        sprite.rotation = G.getRotate(mappingInfo, mappingInfo.path) || sprite.rotation;
+        sprite.x = mappingInfo.x;
+        sprite.y = mappingInfo.y;
+        sprite.interactive = true; // 蹇呴』瑕佽缃墠鑳芥帴鏀朵簨浠�
+        sprite.buttonMode = true; // 璁╁厜鏍囧湪hover鏃跺彉涓烘墜鍨嬫寚浜嬩欢
+        sprite.on('pointerdown', () => {
+          if (window.gsap) {
+            window.gsap.killTweensOf(sprite);
+          }
+          sprite.alpha = 1;
+          const id = parseInt(deviceNo, 10);
+          this.$emit(deviceTypeInfo.emitName, id);
+        });
+
+        deviceTypeInfo.pixiMap.set(parseInt(deviceNo), sprite);
+        this.objectsContainer2.addChild(sprite);
+      };
+
+      map2.forEach((item) => {
+        if (['crn', 'dualCrn', 'rgv', 'annulus'].includes(item.type)) {
+          const deviceForm = G.safeParseJson(item.value);
+          if (deviceForm && deviceForm.deviceList && deviceForm.deviceList.length > 0) {
+            const newDeviceInfo = G.getDeviceInfo(item);
+            newDeviceInfo.deviceList.forEach((device) => {
+              //娉ㄦ剰锛歛nnulus閲岀殑璁惧鏄痳gv!
+              device.type = item.type === 'annulus' ? 'rgv' : item.type;
+
+              // 淇锛氬皢闈� annulus 杞ㄩ亾璁惧鐨勫乏涓婅浣嶇疆杞崲涓轰腑蹇冪偣浣嶇疆
+              // annulus 绫诲瀷鐨勮澶囦綅缃凡缁忔槸涓績鐐癸紝涓嶉渶瑕佽浆鎹�
+              if (item.type !== 'annulus') {
+                device.x = device.x + device.width / 2;
+                device.y = device.y + device.height / 2;
+              }
+
+              handleDevice(this.DEVICE_MAP[device.type], device, item);
+              // this.objectsContainer.addChild(sprite);
+            });
+          }
+        }
+      });
+
+      this.drawTracks(map2);
+      this.scheduleAdjustLabels();
     },
     initWidth(map) {
       let maxRow = map.length;
@@ -911,7 +1188,9 @@
         let bayWidth = -1;
         for (let row = 0; row < maxRow; row++) {
           let val = map[row][bay];
-          if (val.cellWidth == undefined || val.cellWidth === '') { continue; }
+          if (val.cellWidth == undefined || val.cellWidth === '') {
+            continue;
+          }
           bayWidth = Math.max(bayWidth, val.cellWidth);
           break;
         }
@@ -927,7 +1206,9 @@
         let bayHeight = -1;
         for (let bay = 0; bay < maxBay; bay++) {
           let val = map[row][bay];
-          if (val.cellHeight == undefined || val.cellHeight === '') { continue; }
+          if (val.cellHeight == undefined || val.cellHeight === '') {
+            continue;
+          }
           bayHeight = Math.max(bayHeight, val.cellHeight);
           break;
         }
@@ -936,62 +1217,87 @@
       return bayHeightList;
     },
     setSiteInfo(res) {
-      let sites = Array.isArray(res) ? res : (res && res.code === 200 ? res.data : null);
+      let sites = Array.isArray(res) ? res : res && res.code === 200 ? res.data : null;
       if (res && !Array.isArray(res)) {
-        if (res.code === 403) { parent.location.href = baseUrl + "/login"; return; }
-        if (res.code !== 200) { return; }
+        if (res.code === 403) {
+          parent.location.href = baseUrl + '/login';
+          return;
+        }
+        if (res.code !== 200) {
+          return;
+        }
       }
-      if (!sites) { return; }
+      if (!sites) {
+        return;
+      }
       sites.forEach((item) => {
         let id = item.siteId != null ? item.siteId : item.stationId;
         let workNo = item.workNo != null ? item.workNo : item.taskNo;
-        if (id == null) { return; }
+        if (id == null) {
+          return;
+        }
         let sta = this.pixiStaMap.get(parseInt(id));
-        if (sta == undefined) { return; }
-        if (workNo != null && workNo > 0) { sta.textObj.text = id + "(" + workNo + ")"; } else { sta.textObj.text = String(id); }
+        if (sta == undefined) {
+          return;
+        }
+        if (workNo != null && workNo > 0) {
+          sta.textObj.text = id + '(' + workNo + ')';
+        } else {
+          sta.textObj.text = String(id);
+        }
         if (sta.statusObj) {
           this.objectsContainer.removeChild(sta.statusObj);
           sta.statusObj = null;
-          if (sta.textObj.parent !== sta) { sta.addChild(sta.textObj); sta.textObj.position.set(sta.width / 2, sta.height / 2); }
+          if (sta.textObj.parent !== sta) {
+            sta.addChild(sta.textObj);
+            sta.textObj.position.set(sta.width / 2, sta.height / 2);
+          }
         }
         this.setStationBaseColor(sta, this.getStationStatusColor(this.resolveStationStatus(item)));
       });
     },
+    sendConsoleLatestPoll(url) {
+      if (this.isSwitchingFloor) {
+        return;
+      }
+      this.sendWs(JSON.stringify({ url, data: {} }));
+    },
     getCrnInfo() {
-      if (this.isSwitchingFloor) { return; }
-      this.sendWs(JSON.stringify({ url: "/console/latest/data/crn", data: {} }));
+      this.sendConsoleLatestPoll('/console/latest/data/crn');
     },
     getDualCrnInfo() {
-      if (this.isSwitchingFloor) { return; }
-      this.sendWs(JSON.stringify({ url: "/console/latest/data/dualcrn", data: {} }));
+      this.sendConsoleLatestPoll('/console/latest/data/dualcrn');
     },
     getSiteInfo() {
-      if (this.isSwitchingFloor) { return; }
-      this.sendWs(JSON.stringify({ url: "/console/latest/data/station", data: {} }));
+      this.sendConsoleLatestPoll('/console/latest/data/station');
     },
     getRgvInfo() {
-      if (this.isSwitchingFloor) { return; }
-      this.sendWs(JSON.stringify({ url: "/console/latest/data/rgv", data: {} }));
+      this.sendConsoleLatestPoll('/console/latest/data/rgv');
     },
     getCycleCapacityInfo() {
-      if (this.isSwitchingFloor) { return; }
-      this.sendWs(JSON.stringify({ url: "/console/latest/data/station/cycle/capacity", data: {} }));
+      this.sendConsoleLatestPoll('/console/latest/data/station/cycle/capacity');
     },
     setCycleCapacityInfo(res) {
       const payload = res && res.code === 200 ? res.data : null;
-      if (res && res.code === 403) { parent.location.href = baseUrl + "/login"; return; }
-      if (!payload) { return; }
+      if (res && res.code === 403) {
+        parent.location.href = baseUrl + '/login';
+        return;
+      }
+      if (!payload) {
+        return;
+      }
       const loopList = Array.isArray(payload.loopList) ? payload.loopList : [];
       this.cycleCapacity = {
         loopList: loopList,
         totalStationCount: payload.totalStationCount || 0,
         taskStationCount: payload.taskStationCount || 0,
-        manualStationCount: payload.manualStationCount || 0,
-        occupiedStationCount: payload.occupiedStationCount || 0,
-        currentLoad: typeof payload.currentLoad === 'number' ? payload.currentLoad : parseFloat(payload.currentLoad || 0)
+        currentLoad:
+          typeof payload.currentLoad === 'number'
+            ? payload.currentLoad
+            : parseFloat(payload.currentLoad || 0)
       };
       if (this.hoverLoopNo != null) {
-        const targetLoop = loopList.find(v => v && v.loopNo === this.hoverLoopNo);
+        const targetLoop = loopList.find((v) => v && v.loopNo === this.hoverLoopNo);
         if (targetLoop) {
           this.hoverLoopStationIdSet = this.buildStationIdSet(targetLoop.stationIdList);
           this.applyLoopStationHighlight();
@@ -1002,163 +1308,498 @@
     },
     formatLoadPercent(load) {
       let value = typeof load === 'number' ? load : parseFloat(load || 0);
-      if (!isFinite(value)) { value = 0; }
-      if (value < 0) { value = 0; }
-      if (value > 1) { value = 1; }
-      return (value * 100).toFixed(1) + "%";
+      if (!isFinite(value)) {
+        value = 0;
+      }
+      if (value < 0) {
+        value = 0;
+      }
+      if (value > 1) {
+        value = 1;
+      }
+      return (value * 100).toFixed(1) + '%';
     },
-    setCrnInfo(res) {
-      let crns = Array.isArray(res) ? res : (res && res.code === 200 ? res.data : null);
-      if (!crns) { return; }
-      for (var i = 0; i < crns.length; i++) {
-        const id = parseInt(crns[i].crnId);
-        const sprite = this.pixiCrnMap.get(id);
-        if (!sprite) { continue; }
-        const taskNo = crns[i].taskNo;
-        if (taskNo != null && taskNo > 0) { sprite.textObj.text = id + "(" + taskNo + ")"; } else { sprite.textObj.text = String(id); }
-        const status = crns[i].crnStatus;
-        const statusColor = this.getCrnStatusColor(status);
-        this.updateCrnTextureColor(sprite, statusColor);
-        let bay = parseInt(crns[i].bay, 10);
-        if (isNaN(bay) || bay < 1 || bay === -2) { bay = 1; }
-        let rowIndex = (sprite.rowIndex != null) ? sprite.rowIndex : -1;
-        if (rowIndex === -1) {
-          for (let r = 0; r < this.map.length; r++) {
-            if (this.map[r].length > 0) {
-              const rowY = this.map[r][0].posY;
-              if (Math.abs(rowY - sprite.y) < 0.5) { rowIndex = r; break; }
+    /******************setDeviceInfo鐢ㄧ殑鍑芥暟:******************/
+    //璁惧缂栧彿锛氱敾甯冨潗鏍囩郴鍥哄畾瀛楀彿锛岄殢鍦板浘缂╂斁涓庤澶囧浘鏍囧悓姣斾緥鍙樺寲锛堥伩鍏嶇缉灏忚鍥炬椂鐩稿璁惧鍙樺ぇ锛�
+    applyEditorLikeTrackDeviceTextStyle(textObj) {
+      if (!textObj || !textObj.style) {
+        return;
+      }
+      textObj.style.fontSize = 10;
+      textObj.style.strokeThickness = 1;
+    },
+    parseHexColor(color) {
+      if (typeof color !== 'string' || color.charAt(0) !== '#') {
+        return null;
+      }
+      let hex = color.slice(1);
+      if (hex.length === 3) {
+        hex = hex.replace(/(.)/g, '$1$1');
+      }
+      if (!/^[0-9a-fA-F]{6}$/.test(hex)) {
+        return null;
+      }
+      return parseInt(hex, 16);
+    },
+    getAnnulusAwarePoint(trackInfo, x, y, path) {
+      return trackInfo.type === 'annulus' && G.snapToAnnulusOuterPath
+        ? G.snapToAnnulusOuterPath(x, y, path)
+        : { x, y };
+    },
+    /** 鐜┛锛氭妸 getPositionAfterMove 鐨勭粨鏋滃帇鍒拌建甯︿腑绾匡紱闈炵幆绌垮師鏍疯繑鍥� */
+    applyAnnulusBandCenterToPosition(trackInfo, position) {
+      if (!position || trackInfo.type !== 'annulus') {
+        return position;
+      }
+      const c = G.centerAnnulusBandPoint(trackInfo, position.x, position.y, position.path);
+      return { ...position, x: c.x, y: c.y };
+    },
+    computeFinalPosition(trackInfo, point, pathList, path, deltaDistance, angle) {
+      const position = G.getPositionAfterMove({ point, pathList, path, deltaDistance, angle });
+      return this.applyAnnulusBandCenterToPosition(trackInfo, position);
+    },
+    calcSignedSegmentDelta(fromBarcode, toBarcode, trackInfo, totalSegmentCount) {
+      const from = Number(fromBarcode);
+      const to = Number(toBarcode);
+      if (!isFinite(from) || !isFinite(to)) {
+        return 0;
+      }
+      const raw = to - from;
+      if (trackInfo.type !== 'annulus') {
+        return raw;
+      }
+      const span = totalSegmentCount;
+      if (!(span > 0)) {
+        return raw;
+      }
+      const mod = (n, m) => ((n % m) + m) % m;
+      return mod(raw, span);
+    },
+    lineSignedRemainAlong(path0, sx, sy, mx, my) {
+      if (!path0 || path0.type !== 'line') {
+        return null;
+      }
+      const x0 = path0.startX;
+      const y0 = path0.startY;
+      const vx = path0.x - x0;
+      const vy = path0.y - y0;
+      const segLen = Math.sqrt(vx * vx + vy * vy);
+      if (!(segLen > 1e-6)) {
+        return null;
+      }
+      const clampT = (px, py) => {
+        const t = ((px - x0) * vx + (py - y0) * vy) / segLen;
+        return Math.max(0, Math.min(segLen, t));
+      };
+      return clampT(mx, my) - clampT(sx, sy);
+    },
+    finishDeviceMotion(sprite) {
+      if (!sprite || !sprite.mappingInfo) {
+        return false;
+      }
+      const mx = sprite.mappingInfo.x;
+      const my = sprite.mappingInfo.y;
+      if (!isFinite(mx) || !isFinite(my)) {
+        return false;
+      }
+      sprite.isFinish = true;
+      sprite.x = mx;
+      sprite.y = my;
+      sprite.path = sprite.mappingInfo.path || sprite.path;
+      sprite.rotation = G.getRotate(sprite.mappingInfo, sprite.mappingInfo.path) || sprite.rotation;
+      if (sprite.ticker) {
+        this.pixiApp.ticker.remove(sprite.ticker);
+      }
+      sprite.ticker = null;
+      sprite.tickerTime = 0;
+      sprite._motionSpeed = undefined;
+      return true;
+    },
+    /** Web 鎺ュ彛杩斿洖缁熶竴鎴愯澶囨暟缁� */
+    parseBarcodeDevicesResponse(res) {
+      return Array.isArray(res) ? res : res && res.code === 200 ? res.data : null;
+    },
+    /** 鏉$爜璁惧锛氱紪鍙锋枃瀛� + 鐘舵�佽壊 */
+    applyBarcodeSpriteAppearance(sprite, device, deviceTypeInfo) {
+      const id = +device.index;
+      const taskNo = device.taskNo;
+      if (taskNo != null && taskNo > 0) {
+        sprite.textObj.text = id + '(' + taskNo + ')';
+      } else {
+        sprite.textObj.text = String(id);
+      }
+      this.applyEditorLikeTrackDeviceTextStyle(sprite.textObj);
+      const statusColor = this.parseHexColor(device.statusColor);
+      if (statusColor != null) {
+        deviceTypeInfo.statusInfo.updateTextureColor(sprite, statusColor);
+      }
+    },
+    /** 鏉$爜閿氱偣瀵硅薄锛堜袱澶勬瀯閫犲悎骞朵负鍚屼竴褰㈢姸锛� */
+    createBarcodeAnchorState(
+      trackId,
+      minBarcode,
+      maxBarcode,
+      totalSegmentCount,
+      barcode,
+      point,
+      path,
+      angle
+    ) {
+      return {
+        barcode,
+        x: point.x,
+        y: point.y,
+        path,
+        angle,
+        trackId,
+        minBarcode,
+        maxBarcode,
+        totalSegmentCount
+      };
+    },
+    /** 棣栨鏈夋潯鐮佹暟鎹細鎸� rgvPos 钀藉埌杞ㄩ亾涓婂苟寤虹珛閿氱偣 */
+    initializeBarcodeTrackSprite(
+      sprite,
+      device,
+      pathList,
+      allDistance,
+      minBarcode,
+      maxBarcode,
+      totalSegmentCount
+    ) {
+      sprite.barcode = device.rgvPos;
+      sprite.time = nowMs();
+      let tmpPassedSegmentCount = device.rgvPos - minBarcode;
+      const passedSegmentCount =
+        tmpPassedSegmentCount < 0
+          ? tmpPassedSegmentCount + totalSegmentCount
+          : tmpPassedSegmentCount;
+      const deltaDistance = (allDistance * passedSegmentCount) / totalSegmentCount;
+      const initPath = sprite.path;
+      const initMovePoint = this.getAnnulusAwarePoint(
+        sprite.trackInfo,
+        sprite.x,
+        sprite.y,
+        initPath
+      );
+      let mappingInfo = this.computeFinalPosition(
+        sprite.trackInfo,
+        initMovePoint,
+        pathList,
+        initPath,
+        deltaDistance,
+        sprite.currentAngle
+      );
+      sprite.x = mappingInfo.x;
+      sprite.y = mappingInfo.y;
+      sprite.path = mappingInfo.path;
+      sprite.rotation = G.getRotate(mappingInfo, mappingInfo.path) || sprite.rotation;
+      sprite.currentAngle = mappingInfo.angle;
+      sprite.mappingInfo = mappingInfo;
+      const anchorPoint = this.getAnnulusAwarePoint(
+        sprite.trackInfo,
+        mappingInfo.x,
+        mappingInfo.y,
+        mappingInfo.path
+      );
+      sprite._barcodeAnchor = this.createBarcodeAnchorState(
+        sprite.id,
+        minBarcode,
+        maxBarcode,
+        totalSegmentCount,
+        device.rgvPos,
+        anchorPoint,
+        mappingInfo.path,
+        mappingInfo.angle
+      );
+    },
+    /** 鏉$爜璁惧姣忓抚鎻掑�肩Щ鍔紙Pixi ticker 鍥炶皟锛� */
+    tickBarcodeTrackSpriteMotion(sprite, pathList) {
+      if (sprite.isFinish) {
+        return;
+      }
+      const restDistance = G.calcDistance(sprite, sprite.mappingInfo);
+      if (restDistance <= EPSILON) {
+        this.finishDeviceMotion(sprite);
+        return;
+      }
+      const dtMs =
+        this.pixiApp && this.pixiApp.ticker && typeof this.pixiApp.ticker.deltaMS === 'number'
+          ? this.pixiApp.ticker.deltaMS
+          : 16.667;
+      const dt = Math.max(0, dtMs) / 1000;
+      if (dt <= 0) {
+        return;
+      }
+      const baseV =
+        typeof sprite.maV === 'number' && isFinite(sprite.maV) && sprite.maV > 0
+          ? sprite.maV
+          : Math.max(sprite.width * 2, 20);
+      const msSinceUpdate = sprite.time ? nowMs() - sprite.time : 0;
+      const typicalIntervalMs = (sprite.lastDeltaTime || 1) * 1000;
+      const isStale = msSinceUpdate > typicalIntervalMs * 2.65;
+      const slowRadius = Math.max(sprite.width * 6.5, 72);
+      const easing = isStale ? Math.min(1.0, Math.pow(restDistance / slowRadius, 0.45)) : 1.0;
+      const targetSpeed = baseV * easing;
+      if (typeof sprite._motionSpeed !== 'number' || !isFinite(sprite._motionSpeed)) {
+        sprite._motionSpeed = targetSpeed * 0.35;
+      }
+      const maxAccel = Math.max(baseV * 1.75, 22);
+      const dv = targetSpeed - sprite._motionSpeed;
+      const cap = maxAccel * dt;
+      sprite._motionSpeed += Math.max(-cap, Math.min(cap, dv));
+      const path = sprite.path;
+      const stepCap = Math.min(restDistance, Math.max(0, sprite._motionSpeed) * dt);
+      const singleLineTrack =
+        pathList.length === 1 && pathList[0].type === 'line' && sprite.trackInfo.type !== 'annulus';
+      let smoothDistance;
+      if (singleLineTrack) {
+        const remainAlong = this.lineSignedRemainAlong(
+          path,
+          sprite.x,
+          sprite.y,
+          sprite.mappingInfo.x,
+          sprite.mappingInfo.y
+        );
+        if (remainAlong != null) {
+          let stepMag = Math.min(stepCap, Math.abs(remainAlong), restDistance);
+          if (stepMag < 1e-6 && restDistance > EPSILON) {
+            const x0 = path.startX;
+            const y0 = path.startY;
+            const vx = path.x - x0;
+            const vy = path.y - y0;
+            const sl = Math.sqrt(vx * vx + vy * vy);
+            if (sl > 1e-6) {
+              const ux = vx / sl;
+              const uy = vy / sl;
+              const wdx = sprite.mappingInfo.x - sprite.x;
+              const wdy = sprite.mappingInfo.y - sprite.y;
+              const hint = wdx * ux + wdy * uy;
+              stepMag = Math.min(stepCap, restDistance);
+              if (hint === 0) {
+                this.finishDeviceMotion(sprite);
+                return;
+              }
+              smoothDistance = Math.sign(hint) * stepMag;
+            } else {
+              this.finishDeviceMotion(sprite);
+              return;
             }
+          } else {
+            smoothDistance = Math.sign(remainAlong) * stepMag;
           }
-          if (rowIndex === -1) { rowIndex = 0; }
-        }
-        let targetCell = null;
-        let crnCount = 0;
-        for (let c = 0; c < this.map[rowIndex].length; c++) {
-          const cell = this.map[rowIndex][c];
-          if (cell && cell.type === 'crn') { crnCount++; if (crnCount === bay) { targetCell = cell; break; } }
-        }
-        if (!targetCell) {
-          for (let c = this.map[rowIndex].length - 1; c >= 0; c--) {
-            const cell = this.map[rowIndex][c];
-            if (cell && cell.type === 'crn') { targetCell = cell; break; }
-          }
-        }
-        if (!targetCell) { continue; }
-        const targetX = targetCell.posX + (targetCell.width - sprite.width) / 2;
-        const dx = Math.abs(targetX - sprite.x);
-        if (dx < 1) {
-        } else if (dx < 5) {
-          sprite.x = targetX;
-        } else if (window.gsap) {
-          window.gsap.killTweensOf(sprite);
-          window.gsap.to(sprite, { x: targetX, duration: 0.3, ease: "power1.inOut" });
         } else {
-          sprite.x = targetX;
+          smoothDistance = stepCap;
         }
+      } else {
+        smoothDistance = stepCap;
+      }
+      const movePointBarcode = this.getAnnulusAwarePoint(
+        sprite.trackInfo,
+        sprite.x,
+        sprite.y,
+        path
+      );
+      const angle = Math.atan2(sprite.y - path.y, sprite.x - path.x);
+      const raw = G.getPositionAfterMove({
+        point: movePointBarcode,
+        pathList,
+        path,
+        deltaDistance: smoothDistance,
+        angle
+      });
+      const p = this.applyAnnulusBandCenterToPosition(sprite.trackInfo, raw);
+      sprite.path = p.path;
+      sprite.x = p.x;
+      sprite.y = p.y;
+      sprite.rotation = G.getRotate(raw, raw.path) || sprite.rotation;
+      const restDistanceAfter = G.calcDistance({ x: sprite.x, y: sprite.y }, sprite.mappingInfo);
+      if (restDistanceAfter <= EPSILON || Math.abs(smoothDistance) >= restDistance - EPSILON) {
+        this.finishDeviceMotion(sprite);
+      }
+    },
+    /**
+     * 鏍规嵁鏈嶅姟绔潯鐮佸埛鏂� mappingInfo銆侀�熷害涓� ticker銆�
+     * rgvPosMax 閫氬父鏄棴鍖洪棿 [min, max]锛屼笖 max 瀵瑰簲 100% 涓� min 鍚屼竴鐐癸紙璧颁竴鍦堝洖鍒板師鐐癸級銆�
+     * 鍥犳鎸� 鈥滄鏁� = max - min鈥� 鎹㈢畻璺濈銆�
+     */
+    syncBarcodeTrackSprite(
+      sprite,
+      device,
+      oldSprite,
+      pathList,
+      allDistance,
+      minBarcode,
+      maxBarcode,
+      totalSegmentCount
+    ) {
+      let anchor = sprite._barcodeAnchor;
+      const needResetAnchor =
+        !anchor ||
+        anchor.trackId !== sprite.id ||
+        anchor.minBarcode !== minBarcode ||
+        anchor.maxBarcode !== maxBarcode ||
+        anchor.totalSegmentCount !== totalSegmentCount;
+      if (needResetAnchor) {
+        const anchorPath = sprite.path;
+        const anchorPoint = this.getAnnulusAwarePoint(
+          sprite.trackInfo,
+          sprite.x,
+          sprite.y,
+          anchorPath
+        );
+        const anchorBarcode = oldSprite.barcode != null ? oldSprite.barcode : device.rgvPos;
+        anchor = this.createBarcodeAnchorState(
+          sprite.id,
+          minBarcode,
+          maxBarcode,
+          totalSegmentCount,
+          anchorBarcode,
+          anchorPoint,
+          anchorPath,
+          sprite.currentAngle
+        );
+        sprite._barcodeAnchor = anchor;
+      }
+      const passedSegmentCount = this.calcSignedSegmentDelta(
+        anchor.barcode,
+        device.rgvPos,
+        sprite.trackInfo,
+        totalSegmentCount
+      );
+      const deltaDistance = (allDistance * passedSegmentCount) / totalSegmentCount;
+      const path = anchor.path || sprite.path;
+      const barcodeMovePoint = { x: anchor.x, y: anchor.y };
+      let finalPosition = this.computeFinalPosition(
+        sprite.trackInfo,
+        barcodeMovePoint,
+        pathList,
+        path,
+        deltaDistance,
+        anchor.angle
+      );
+      let curveDistance = G.calcDistance(oldSprite.mappingInfo || oldSprite, finalPosition);
+      if (!isFinite(curveDistance)) {
+        curveDistance = deltaDistance;
+      }
+      sprite.mappingInfo = finalPosition;
+      const now = nowMs();
+      const deltaTime = (now - oldSprite.time) / 1000 || 0;
+      sprite.time = now;
+      sprite.barcode = device.rgvPos;
+      if (deltaTime > 0 && deltaTime <= 10 && curveDistance > 0) {
+        sprite.lastDeltaTime = deltaTime;
+        const v = curveDistance / deltaTime;
+        const alpha = 0.2;
+        sprite.maV =
+          typeof sprite.maV === 'number' && isFinite(sprite.maV)
+            ? sprite.maV * (1 - alpha) + v * alpha
+            : v;
+      }
+      if (curveDistance < EPSILON) {
+        this.finishDeviceMotion(sprite);
+        return;
+      }
+      sprite.isFinish = false;
+      if (sprite.ticker) {
+        return;
+      }
+      sprite.ticker = () => this.tickBarcodeTrackSpriteMotion(sprite, pathList);
+      this.pixiApp.ticker.add(sprite.ticker);
+    },
+    // 閫傞厤涓嶅悓鎺ュ彛杩斿洖鐨勬暟鎹粨鏋�
+    deviceAdapter(type, res, trackType) {
+      const devices = this.parseBarcodeDevicesResponse(res);
+      const deviceTypeInfo = this.DEVICE_MAP[type];
+      if (trackType === 'annulus') {
+        return devices.map((device) => {
+          const index = +device.index;
+          const sprite = deviceTypeInfo.pixiMap.get(index);
+          if (!sprite) {
+            return {};
+          }
+          const trackInfoParse = G.safeParseJson(sprite.trackInfo.value);
+          return {
+            index,
+            statusColor: device.statusColor,
+            sprite,
+            minBarcode: trackInfoParse.barCodeStart,
+            maxBarcode: trackInfoParse.barCodeEnd,
+            rgvPos: device.rgvPos,
+            taskNo: device.taskNo
+          };
+        });
+      }
+      return devices.map((device) => {
+        const index = +device[deviceTypeInfo.idName];
+        const sprite = deviceTypeInfo.pixiMap.get(index);
+        const trackInfoParse = G.safeParseJson(sprite.trackInfo.value);
+        const minBarcode = trackInfoParse.barCodeStart;
+        const maxBarcode = trackInfoParse.barCodeEnd;
+        const statusInfo = deviceTypeInfo.statusInfo;
+        const statusName = device[statusInfo.statusName];
+        const statusColor = statusInfo[statusInfo.getStatus](statusName);
+        return {
+          index,
+          statusColor,
+          sprite,
+          minBarcode,
+          maxBarcode,
+          rgvPos: device.rgvPos,
+          taskNo: device.taskNo
+        };
+      });
+    },
+    setDeviceInfoByBarcode(type, res, trackType = 'annulus') {
+      const devices = this.deviceAdapter(type, res, trackType);
+      const deviceTypeInfo = this.DEVICE_MAP[type];
+      for (let i = 0; i < devices.length; i++) {
+        const device = devices[i] || {};
+        const { sprite, minBarcode, maxBarcode } = device;
+        if (!sprite) {
+          continue;
+        }
+        this.applyBarcodeSpriteAppearance(sprite, device, deviceTypeInfo);
+        const pathList = sprite.trackInfo.pathList;
+        const allDistance = G.getAllDistance(pathList);
+        const totalSegmentCount = Math.max(1, maxBarcode - minBarcode);
+        const oldSprite = {
+          ...sprite,
+          x: sprite.x,
+          y: sprite.y,
+          mappingInfo: { ...sprite.mappingInfo }
+        };
+        if (!sprite.barcode && sprite.barcode !== 0) {
+          this.initializeBarcodeTrackSprite(
+            sprite,
+            device,
+            pathList,
+            allDistance,
+            minBarcode,
+            maxBarcode,
+            totalSegmentCount
+          );
+          continue;
+        }
+        this.syncBarcodeTrackSprite(
+          sprite,
+          device,
+          oldSprite,
+          pathList,
+          allDistance,
+          minBarcode,
+          maxBarcode,
+          totalSegmentCount
+        );
       }
       this.scheduleAdjustLabels();
     },
-    setDualCrnInfo(res) {
-      let crns = Array.isArray(res) ? res : (res && res.code === 200 ? res.data : null);
-      if (!crns) { return; }
-      for (var i = 0; i < crns.length; i++) {
-        const id = parseInt(crns[i].crnId);
-        const sprite = this.pixiDualCrnMap.get(id);
-        if (!sprite) { continue; }
-        const taskNo = crns[i].taskNo;
-        if (taskNo != null && taskNo > 0) { sprite.textObj.text = id + "(" + taskNo + ")"; } else { sprite.textObj.text = String(id); }
-        const status = crns[i].crnStatus;
-        const statusColor = this.getCrnStatusColor(status);
-        this.updateCrnTextureColor(sprite, statusColor);
-        let bay = parseInt(crns[i].bay, 10);
-        if (isNaN(bay) || bay < 1 || bay === -2) { bay = 1; }
-        let rowIndex = (sprite.rowIndex != null) ? sprite.rowIndex : -1;
-        if (rowIndex === -1) {
-          for (let r = 0; r < this.map.length; r++) {
-            if (this.map[r].length > 0) {
-              const rowY = this.map[r][0].posY;
-              if (Math.abs(rowY - sprite.y) < 0.5) { rowIndex = r; break; }
-            }
-          }
-          if (rowIndex === -1) { rowIndex = 0; }
-        }
-        let targetCell = null;
-        let crnCount = 0;
-        for (let c = 0; c < this.map[rowIndex].length; c++) {
-          const cell = this.map[rowIndex][c];
-          if (cell && (cell.type === 'dualCrn' || cell.type === 'dualcrn')) {
-            crnCount++;
-            if (crnCount === bay) { targetCell = cell; break; }
-          }
-        }
-        if (!targetCell) {
-          for (let c = this.map[rowIndex].length - 1; c >= 0; c--) {
-            const cell = this.map[rowIndex][c];
-            if (cell && (cell.type === 'dualCrn' || cell.type === 'dualcrn')) { targetCell = cell; break; }
-          }
-        }
-        if (!targetCell) { continue; }
-        const targetX = targetCell.posX + (targetCell.width - sprite.width) / 2;
-        const dx = Math.abs(targetX - sprite.x);
-        if (dx < 1) {
-        } else if (dx < 5) {
-          sprite.x = targetX;
-        } else if (window.gsap) {
-          window.gsap.killTweensOf(sprite);
-          window.gsap.to(sprite, { x: targetX, duration: 0.3, ease: "power1.inOut" });
-        } else {
-          sprite.x = targetX;
-        }
-      }
-      this.scheduleAdjustLabels();
-    },
-    setRgvInfo(res) {
-      let rgvs = Array.isArray(res) ? res : (res && res.code === 200 ? res.data : null);
-      if (!rgvs) { return; }
-      for (let i = 0; i < rgvs.length; i++) {
-        const id = parseInt(rgvs[i].rgvNo, 10);
-        const sprite = this.pixiRgvMap.get(id);
-        if (!sprite) { continue; }
-        const taskNo = rgvs[i].taskNo;
-        if (sprite.textObj) { if (taskNo != null && taskNo > 0) { sprite.textObj.text = id + "(" + taskNo + ")"; } else { sprite.textObj.text = String(id); } }
-        const statusColor = this.getRgvStatusColor(rgvs[i].rgvStatus);
-        this.updateRgvTextureColor(sprite, statusColor);
-        let trackSiteNo = parseInt(rgvs[i].trackSiteNo, 10);
-        if (!trackSiteNo || trackSiteNo <= 0) { continue; }
-        let rowIndex = (sprite.rowIndex != null) ? sprite.rowIndex : 0;
-        let targetCell = null;
-        for (let c = 0; c < this.map[rowIndex].length; c++) {
-          const cell = this.map[rowIndex][c];
-          if (!cell || cell.type !== 'rgv') { continue; }
-          const ts = this.getTrackSiteNo(cell.value);
-          if (ts === trackSiteNo) { targetCell = cell; break; }
-        }
-        if (!targetCell) {
-          for (let c = this.map[rowIndex].length - 1; c >= 0; c--) {
-            const cell = this.map[rowIndex][c];
-            if (cell && cell.type === 'rgv') { targetCell = cell; break; }
-          }
-        }
-        if (!targetCell) { continue; }
-        const targetX = targetCell.posX + (targetCell.width - sprite.width) / 2;
-        const dx = Math.abs(targetX - sprite.x);
-        if (dx < 1) {
-        } else if (dx < 5) {
-          sprite.x = targetX;
-        } else if (window.gsap) {
-          window.gsap.killTweensOf(sprite);
-          window.gsap.to(sprite, { x: targetX, duration: 0.3, ease: "power1.inOut" });
-        } else {
-          sprite.x = targetX;
-        }
-      }
-      this.scheduleAdjustLabels();
-    },
-    setMap(res) {
-      this.createMapData(JSON.parse(res.data));
-    },
+    /******************setDeviceInfo鐢ㄧ殑鍑芥暟缁撴潫******************/
     webSocketOnOpen(e) {
-      if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; }
+      if (this.wsReconnectTimer) {
+        clearTimeout(this.wsReconnectTimer);
+        this.wsReconnectTimer = null;
+      }
       this.wsReconnectAttempts = 0;
       this.getMap(this.currentLev);
       this.getCycleCapacityInfo();
@@ -1168,19 +1809,25 @@
     },
     webSocketOnMessage(e) {
       const result = JSON.parse(e.data);
-      if (result.url === "/console/latest/data/station" || result.url === "/console/latest/data/site") {
+      if (
+        result.url === '/console/latest/data/station' ||
+        result.url === '/console/latest/data/site'
+      ) {
         this.setSiteInfo(JSON.parse(result.data));
-      } else if (result.url === "/console/latest/data/crn") {
-        this.setCrnInfo(JSON.parse(result.data));
-      } else if (result.url === "/console/latest/data/dualcrn") {
-        this.setDualCrnInfo(JSON.parse(result.data));
-      } else if (result.url === "/console/latest/data/rgv") {
-        this.setRgvInfo(JSON.parse(result.data));
-      } else if (result.url === "/console/latest/data/station/cycle/capacity") {
+      } else if (result.url === '/console/latest/data/crn') {
+        // this.setDeviceInfo('crn', JSON.parse(result.data));
+      } else if (result.url === '/console/latest/data/dualcrn') {
+        // this.setDeviceInfo('dualcrn', JSON.parse(result.data));
+      } else if (result.url === '/console/latest/data/rgv') {
+        // this.setDeviceInfo('rgv', JSON.parse(result.data));
+      } else if (result.url === '/console/latest/data/station/cycle/capacity') {
         this.setCycleCapacityInfo(JSON.parse(result.data));
-      } else if (typeof result.url === "string" && result.url.indexOf("/basMap/lev/") === 0) {
+      } else if (typeof result.url === 'string' && result.url.indexOf('/basMap/lev/') === 0) {
         this.setMap(JSON.parse(result.data));
       }
+      // else if (result.url === 'rgv/ring/through/rgv/position/data') {
+      //   this.setDeviceInfoByBarcode('rgv', JSON.parse(result.data));
+      // }
     },
     webSocketClose(e) {
       this.scheduleReconnect();
@@ -1191,18 +1838,29 @@
       }
     },
     connectWs() {
-      if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { return; }
-      this.ws = new WebSocket("ws://" + window.location.host + baseUrl + "/console/websocket");
+      if (
+        this.ws &&
+        (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)
+      ) {
+        return;
+      }
+      this.ws = new WebSocket('ws://' + window.location.host + baseUrl + '/console/websocket');
       this.ws.onopen = this.webSocketOnOpen;
       this.ws.onerror = this.webSocketOnError;
       this.ws.onmessage = this.webSocketOnMessage;
       this.ws.onclose = this.webSocketClose;
     },
     scheduleReconnect() {
-      if (this.wsReconnectTimer) { return; }
+      if (this.wsReconnectTimer) {
+        return;
+      }
       const attempt = this.wsReconnectAttempts + 1;
       const jitter = Math.floor(Math.random() * 300);
-      const delay = Math.min(this.wsReconnectMaxDelay, this.wsReconnectBaseDelay * Math.pow(2, this.wsReconnectAttempts)) + jitter;
+      const delay =
+        Math.min(
+          this.wsReconnectMaxDelay,
+          this.wsReconnectBaseDelay * Math.pow(2, this.wsReconnectAttempts)
+        ) + jitter;
       this.wsReconnectTimer = setTimeout(() => {
         this.wsReconnectTimer = null;
         this.wsReconnectAttempts = attempt;
@@ -1210,7 +1868,7 @@
       }, delay);
     },
     createShelfSprite(width, height) {
-      let idx = width + "-" + height;
+      let idx = width + '-' + height;
       let texture = this.pixiShelfMap.get(idx);
       if (texture == undefined) {
         let graphics = this.getContainer('shelf', width, height);
@@ -1221,7 +1879,7 @@
     },
     createTrackSprite(width, height, mask) {
       const trackMask = mask != null ? mask : 10;
-      let idx = width + "-" + height + "-" + trackMask;
+      let idx = width + '-' + height + '-' + trackMask;
       let texture = this.pixiTrackMap.get(idx);
       if (texture == undefined) {
         texture = this.createTrackTexture(width, height, trackMask);
@@ -1229,35 +1887,21 @@
       }
       return new PIXI.Sprite(texture);
     },
-    createCrnSprite(width, height) {
-      const w = Math.max(1, Math.round(width));
-      const h = Math.max(1, Math.round(height));
-      const key = w + "-" + h;
-      let texture = this.pixiCrnTextureMap.get(key);
-      if (texture == undefined) {
-        texture = this.createCrnTexture(w, h);
-        this.pixiCrnTextureMap.set(key, texture);
-      }
-      return new PIXI.Sprite(texture);
-    },
-    createRgvSprite(width, height) {
-      const w = Math.max(1, Math.round(width));
-      const h = Math.max(1, Math.round(height));
-      const key = w + "-" + h;
-      let texture = this.pixiRgvTextureMap.get(key);
-      if (texture == undefined) {
-        texture = this.createRgvTexture(w, h);
-        this.pixiRgvTextureMap.set(key, texture);
-      }
-      return new PIXI.Sprite(texture);
-    },
     getContainer(type, width, height) {
       let graphics = new PIXI.Graphics();
       let drawBorder = true;
-      if (type == 'shelf') { graphics.beginFill(0xb6e2e2); }
-      else if (type == 'devp') { graphics.beginFill(0x00ff7f); graphics.visible = true; }
-      else if (type == 'crn') { graphics.beginFill(0xaaffff); }
-      if (drawBorder) { graphics.lineStyle(1, 0xffffff, 1); graphics.drawRect(0, 0, width, height); }
+      if (type == 'shelf') {
+        graphics.beginFill(0xb6e2e2);
+      } else if (type == 'devp') {
+        graphics.beginFill(0x00ff7f);
+        graphics.visible = true;
+      } else if (type == 'crn') {
+        graphics.beginFill(0xaaffff);
+      }
+      if (drawBorder) {
+        graphics.lineStyle(1, 0xffffff, 1);
+        graphics.drawRect(0, 0, width, height);
+      }
       graphics.endFill();
       return graphics;
     },
@@ -1266,7 +1910,7 @@
       const TRACK_E = 2;
       const TRACK_S = 4;
       const TRACK_W = 8;
-      const trackMask = mask != null ? mask : (TRACK_E | TRACK_W);
+      const trackMask = mask != null ? mask : TRACK_E | TRACK_W;
       const g = new PIXI.Graphics();
       const size = Math.max(1, Math.min(width, height));
       const rail = Math.max(2, Math.round(size * 0.12));
@@ -1313,10 +1957,10 @@
       if (isCorner) {
         const cw = hasE;
         const ch = hasS;
-        const cx = cw ? (width - 1) : 0;
-        const cy = ch ? (height - 1) : 0;
-        const angStart = (cw && ch) ? Math.PI : (cw ? Math.PI / 2 : (ch ? -Math.PI / 2 : 0));
-        const angEnd = (cw && ch) ? Math.PI * 1.5 : (cw ? Math.PI : (ch ? 0 : Math.PI / 2));
+        const cx = cw ? width - 1 : 0;
+        const cy = ch ? height - 1 : 0;
+        const angStart = cw && ch ? Math.PI : cw ? Math.PI / 2 : ch ? -Math.PI / 2 : 0;
+        const angEnd = cw && ch ? Math.PI * 1.5 : cw ? Math.PI : ch ? 0 : Math.PI / 2;
         const rX = Math.abs(cx - midX);
         const rY = Math.abs(cy - midY);
         const rMid = Math.min(rX, rY);
@@ -1332,72 +1976,21 @@
     },
     createCrnTexture(width, height) {
       const g = new PIXI.Graphics();
-      const yTop = Math.round(height * 0.1);
-      let deviceWidth = width * 2;
-      g.beginFill(0x999999);
-      g.drawRect(2, yTop, 3, height - yTop - 2);
-      g.drawRect(deviceWidth - 5, yTop, 3, height - yTop - 2);
-      g.endFill();
-      g.beginFill(0x999999);
-      g.drawRect(0, yTop, deviceWidth, 3);
-      g.endFill();
-      const cabW = Math.round(deviceWidth * 0.68);
-      const cabH = Math.round(height * 0.38);
-      const cabX = Math.round((deviceWidth - cabW) / 2);
-      const cabY = Math.round(height * 0.52 - cabH / 2);
-      g.beginFill(0x245a9a);
-      g.drawRect(cabX, cabY, cabW, cabH);
-      g.endFill();
-      const winW = Math.round(cabW * 0.6);
-      const winH = Math.round(cabH * 0.45);
-      const winX = cabX + Math.round((cabW - winW) / 2);
-      const winY = cabY + Math.round((cabH - winH) / 2);
-      g.beginFill(0xd0e8ff);
-      g.drawRect(winX, winY, winW, winH);
-      g.endFill();
-      const forkW = Math.round(deviceWidth * 0.8);
-      const forkH = Math.max(2, Math.round(height * 0.08));
-      const forkX = Math.round((deviceWidth - forkW) / 2);
-      const forkY = cabY + cabH;
-      g.beginFill(0x666666);
-      g.drawRect(forkX, forkY, forkW, forkH);
-      g.endFill();
-      const rt = PIXI.RenderTexture.create({ width: deviceWidth, height: height });
+      G.drawCrnDeviceGraphics(g, width, height, 0x245a9a);
+      const rt = PIXI.RenderTexture.create({
+        width: width,
+        height: height
+      });
       this.pixiApp.renderer.render(g, rt);
       return rt;
     },
     createCrnTextureColoredDevice(deviceWidth, height, color) {
       const g = new PIXI.Graphics();
-      const yTop = Math.round(height * 0.1);
-      g.beginFill(0x999999);
-      g.drawRect(2, yTop, 3, height - yTop - 2);
-      g.drawRect(deviceWidth - 5, yTop, 3, height - yTop - 2);
-      g.endFill();
-      g.beginFill(0x999999);
-      g.drawRect(0, yTop, deviceWidth, 3);
-      g.endFill();
-      const cabW = Math.round(deviceWidth * 0.68);
-      const cabH = Math.round(height * 0.38);
-      const cabX = Math.round((deviceWidth - cabW) / 2);
-      const cabY = Math.round(height * 0.52 - cabH / 2);
-      g.beginFill(color);
-      g.drawRect(cabX, cabY, cabW, cabH);
-      g.endFill();
-      const winW = Math.round(cabW * 0.6);
-      const winH = Math.round(cabH * 0.45);
-      const winX = cabX + Math.round((cabW - winW) / 2);
-      const winY = cabY + Math.round((cabH - winH) / 2);
-      g.beginFill(0xd0e8ff);
-      g.drawRect(winX, winY, winW, winH);
-      g.endFill();
-      const forkW = Math.round(deviceWidth * 0.8);
-      const forkH = Math.max(2, Math.round(height * 0.08));
-      const forkX = Math.round((deviceWidth - forkW) / 2);
-      const forkY = cabY + cabH;
-      g.beginFill(0x666666);
-      g.drawRect(forkX, forkY, forkW, forkH);
-      g.endFill();
-      const rt = PIXI.RenderTexture.create({ width: deviceWidth, height: height });
+      G.drawCrnDeviceGraphics(g, deviceWidth, height, color);
+      const rt = PIXI.RenderTexture.create({
+        width: deviceWidth,
+        height: height
+      });
       this.pixiApp.renderer.render(g, rt);
       return rt;
     },
@@ -1413,124 +2006,109 @@
     },
     createRgvTexture(width, height) {
       const g = new PIXI.Graphics();
-      const bodyW = Math.round(width * 0.8);
-      const bodyH = Math.round(height * 0.55);
-      const bodyX = Math.round((width - bodyW) / 2);
-      const bodyY = Math.round((height - bodyH) / 2);
-      g.beginFill(0x245a9a);
-      g.drawRect(bodyX, bodyY, bodyW, bodyH);
-      g.endFill();
-      const winW = Math.round(bodyW * 0.55);
-      const winH = Math.round(bodyH * 0.45);
-      const winX = bodyX + Math.round((bodyW - winW) / 2);
-      const winY = bodyY + Math.round((bodyH - winH) / 2);
-      g.beginFill(0xd0e8ff);
-      g.drawRect(winX, winY, winW, winH);
-      g.endFill();
-      const wheelW = Math.max(2, Math.round(width * 0.12));
-      const wheelH = Math.max(2, Math.round(height * 0.1));
-      const wheelY = bodyY + bodyH;
-      const wheelGap = Math.round((width - wheelW * 2) / 3);
-      const wheelX1 = wheelGap;
-      const wheelX2 = width - wheelGap - wheelW;
-      g.beginFill(0x333333);
-      g.drawRect(wheelX1, wheelY - Math.round(wheelH / 2), wheelW, wheelH);
-      g.drawRect(wheelX2, wheelY - Math.round(wheelH / 2), wheelW, wheelH);
-      g.endFill();
+      G.drawRgvDeviceGraphics(g, width, height, 0x245a9a);
       const rt = PIXI.RenderTexture.create({ width: width, height: height });
       this.pixiApp.renderer.render(g, rt);
       return rt;
     },
     createRgvTextureColoredDevice(width, height, color) {
       const g = new PIXI.Graphics();
-      const bodyW = Math.round(width * 0.8);
-      const bodyH = Math.round(height * 0.55);
-      const bodyX = Math.round((width - bodyW) / 2);
-      const bodyY = Math.round((height - bodyH) / 2);
-      g.beginFill(color);
-      g.drawRect(bodyX, bodyY, bodyW, bodyH);
-      g.endFill();
-      const winW = Math.round(bodyW * 0.55);
-      const winH = Math.round(bodyH * 0.45);
-      const winX = bodyX + Math.round((bodyW - winW) / 2);
-      const winY = bodyY + Math.round((bodyH - winH) / 2);
-      g.beginFill(0xd0e8ff);
-      g.drawRect(winX, winY, winW, winH);
-      g.endFill();
-      const wheelW = Math.max(2, Math.round(width * 0.12));
-      const wheelH = Math.max(2, Math.round(height * 0.1));
-      const wheelY = bodyY + bodyH;
-      const wheelGap = Math.round((width - wheelW * 2) / 3);
-      const wheelX1 = wheelGap;
-      const wheelX2 = width - wheelGap - wheelW;
-      g.beginFill(0x333333);
-      g.drawRect(wheelX1, wheelY - Math.round(wheelH / 2), wheelW, wheelH);
-      g.drawRect(wheelX2, wheelY - Math.round(wheelH / 2), wheelW, wheelH);
-      g.endFill();
+      G.drawRgvDeviceGraphics(g, width, height, color);
       const rt = PIXI.RenderTexture.create({ width: width, height: height });
       this.pixiApp.renderer.render(g, rt);
       return rt;
     },
-    updateRgvTextureColor(sprite, color) {
-      const key = Math.round(sprite.width) + '-' + Math.round(sprite.height) + '-' + color;
-      let tex = this.pixiRgvColorTextureMap.get(key);
+    applyCachedColoredDeviceTexture(sprite, color, cacheMap, createColoredTex, repositionText) {
+      const w = Math.round(sprite.width);
+      const h = Math.round(sprite.height);
+      const key = w + '-' + h + '-' + color;
+      let tex = cacheMap.get(key);
       if (!tex) {
-        tex = this.createRgvTextureColoredDevice(Math.round(sprite.width), Math.round(sprite.height), color);
-        this.pixiRgvColorTextureMap.set(key, tex);
+        tex = createColoredTex(w, h, color);
+        cacheMap.set(key, tex);
       }
       sprite.texture = tex;
-      if (sprite.textObj) {
-        const fill = this.getContrastColor(color);
-        sprite.textObj.style.fill = fill;
-        sprite.textObj.style.stroke = (fill === '#000000' ? '#ffffff' : '#000000');
-        sprite.textObj.style.strokeThickness = 1;
+      const textObj = sprite.textObj;
+      if (!textObj) {
+        return;
       }
+      const fill = this.getContrastColor(color);
+      textObj.style.fill = fill;
+      textObj.style.stroke = fill === '#000000' ? '#ffffff' : '#000000';
+      this.applyEditorLikeTrackDeviceTextStyle(textObj);
+      if (repositionText) {
+        textObj.position.set(sprite.width / 2, sprite.height / 2);
+      }
+    },
+    updateRgvTextureColor(sprite, color) {
+      this.applyCachedColoredDeviceTexture(
+        sprite,
+        color,
+        this.pixiRgvColorTextureMap,
+        this.createRgvTextureColoredDevice,
+        true
+      );
     },
     updateCrnTextureColor(sprite, color) {
-      const key = Math.round(sprite.width) + '-' + Math.round(sprite.height) + '-' + color;
-      let tex = this.pixiCrnColorTextureMap.get(key);
-      if (!tex) {
-        tex = this.createCrnTextureColoredDevice(Math.round(sprite.width), Math.round(sprite.height), color);
-        this.pixiCrnColorTextureMap.set(key, tex);
-      }
-      sprite.texture = tex;
-      if (sprite.textObj) {
-        const fill = this.getContrastColor(color);
-        sprite.textObj.style.fill = fill;
-        sprite.textObj.style.stroke = (fill === '#000000' ? '#ffffff' : '#000000');
-        sprite.textObj.style.strokeThickness = 1;
-      }
+      this.applyCachedColoredDeviceTexture(
+        sprite,
+        color,
+        this.pixiCrnColorTextureMap,
+        this.createCrnTextureColoredDevice,
+        false
+      );
     },
     getContrastColor(color) {
-      const r = (color >> 16) & 0xFF;
-      const g = (color >> 8) & 0xFF;
-      const b = color & 0xFF;
+      const r = (color >> 16) & 0xff;
+      const g = (color >> 8) & 0xff;
+      const b = color & 0xff;
       const brightness = (r * 299 + g * 587 + b * 114) / 1000;
       return brightness > 150 ? '#000000' : '#ffffff';
     },
     getStationStatusColor(status) {
       const colorMap = this.stationStatusColors || this.getDefaultStationStatusColors();
-      if (status && colorMap[status] != null) { return colorMap[status]; }
+      if (status && colorMap[status] != null) {
+        return colorMap[status];
+      }
       return colorMap['site-unauto'] != null ? colorMap['site-unauto'] : 0xb8b8b8;
     },
     resolveStationStatus(item) {
+      if (item && item.error > 0) { return 'site-error'; }
       const status = item && (item.siteStatus != null ? item.siteStatus : item.stationStatus);
-      const taskNo = this.parseStationTaskNo(item && (item.workNo != null ? item.workNo : item.taskNo));
+      const taskNo = this.parseStationTaskNo(
+        item && (item.workNo != null ? item.workNo : item.taskNo)
+      );
       const autoing = !!(item && item.autoing);
       const loading = !!(item && item.loading);
       const runBlock = !!(item && item.runBlock);
       const enableIn = !!(item && item.enableIn);
-      if (taskNo === 9998 || enableIn) { return 'site-enable-in'; }
+      if (taskNo === 9998 || enableIn) {
+        return 'site-enable-in';
+      }
       if (autoing && loading && taskNo > 0 && !runBlock) {
         const taskClass = this.getStationTaskClass(taskNo);
-        if (taskClass) { return taskClass; }
+        if (taskClass) {
+          return taskClass;
+        }
       }
-      if (status) { return status; }
-      if (autoing && loading && taskNo > 0 && runBlock) { return 'site-run-block'; }
-      if (autoing && loading && taskNo > 0) { return 'site-auto-run-id'; }
-      if (autoing && loading) { return 'site-auto-run'; }
-      if (autoing && taskNo > 0) { return 'site-auto-id'; }
-      if (autoing) { return 'site-auto'; }
+      if (status) {
+        return status;
+      }
+      if (autoing && loading && taskNo > 0 && runBlock) {
+        return 'site-run-block';
+      }
+      if (autoing && loading && taskNo > 0) {
+        return 'site-auto-run-id';
+      }
+      if (autoing && loading) {
+        return 'site-auto-run';
+      }
+      if (autoing && taskNo > 0) {
+        return 'site-auto-id';
+      }
+      if (autoing) {
+        return 'site-auto';
+      }
       return 'site-unauto';
     },
     parseStationTaskNo(value) {
@@ -1538,33 +2116,63 @@
       return isNaN(taskNo) ? 0 : taskNo;
     },
     getStationTaskClass(taskNo) {
-      if (!(taskNo > 0)) { return null; }
+      if (!(taskNo > 0)) {
+        return null;
+      }
       const range = this.stationTaskRange || {};
-      if (this.isTaskNoInRange(taskNo, range.inbound)) { return 'machine-pakin'; }
-      if (this.isTaskNoInRange(taskNo, range.outbound)) { return 'machine-pakout'; }
+      if (this.isTaskNoInRange(taskNo, range.inbound)) {
+        return 'machine-pakin';
+      }
+      if (this.isTaskNoInRange(taskNo, range.outbound)) {
+        return 'machine-pakout';
+      }
       return null;
     },
     isTaskNoInRange(taskNo, range) {
-      if (!range) { return false; }
+      if (!range) {
+        return false;
+      }
       const start = parseInt(range.start, 10);
       const end = parseInt(range.end, 10);
-      if (isNaN(start) || isNaN(end)) { return false; }
+      if (isNaN(start) || isNaN(end)) {
+        return false;
+      }
       return taskNo >= start && taskNo <= end;
     },
     getCrnStatusColor(status) {
-      if (status === "machine-auto") { return 0x21BA45; }
-      if (status === "machine-un-auto") { return 0xBBBBBB; }
-      if (status === "machine-error") { return 0xDB2828; }
-      if (status === "machine-pakin") { return 0x30bffc; }
-      if (status === "machine-pakout") { return 0x97b400; }
-      return 0xBBBBBB;
+      if (status === 'machine-auto') {
+        return 0x21ba45;
+      }
+      if (status === 'machine-un-auto') {
+        return 0xbbbbbb;
+      }
+      if (status === 'machine-error') {
+        return 0xdb2828;
+      }
+      if (status === 'machine-pakin') {
+        return 0x30bffc;
+      }
+      if (status === 'machine-pakout') {
+        return 0x97b400;
+      }
+      return 0xbbbbbb;
     },
     getRgvStatusColor(status) {
-      if (status === "idle") { return 0x21BA45; }
-      if (status === "working") { return 0xffd60b; }
-      if (status === "waiting") { return 0xffd60b; }
-      if (status === "fetching") { return 0xffd60b; }
-      if (status === "putting") { return 0xffd60b; }
+      if (status === 'idle') {
+        return 0x21ba45;
+      }
+      if (status === 'working') {
+        return 0xffd60b;
+      }
+      if (status === 'waiting') {
+        return 0xffd60b;
+      }
+      if (status === 'fetching') {
+        return 0xffd60b;
+      }
+      if (status === 'putting') {
+        return 0xffd60b;
+      }
       return 0xb8b8b8;
     },
     getSprite(item, pointerDownEvent) {
@@ -1577,51 +2185,76 @@
         const key = Math.round(item.width) + '-' + Math.round(item.height) + '-' + 0x00ff7f;
         let texture = this.pixiDevpTextureMap.get(key);
         if (!texture) {
-          texture = this.createDevpTextureColoredRect(Math.round(item.width), Math.round(item.height), 0x00ff7f);
+          texture = this.createDevpTextureColoredRect(
+            Math.round(item.width),
+            Math.round(item.height),
+            0x00ff7f
+          );
           this.pixiDevpTextureMap.set(key, texture);
         }
         sprite = new PIXI.Sprite(texture);
         sprite._kind = 'devp';
-        const directionOverlay = this.createStationDirectionOverlay(item.width, item.height, item.stationDirectionList);
+        const directionOverlay = this.createStationDirectionOverlay(
+          item.width,
+          item.height,
+          item.stationDirectionList
+        );
         if (directionOverlay) {
           directionOverlay.visible = this.showStationDirection;
           sprite.addChild(directionOverlay);
         }
         sprite.directionObj = directionOverlay;
-        sprite._stationDirectionList = Array.isArray(item.stationDirectionList) ? item.stationDirectionList.slice() : [];
+        sprite._stationDirectionList = Array.isArray(item.stationDirectionList)
+          ? item.stationDirectionList.slice()
+          : [];
         let siteId = this.getStationId(value);
-        if (siteId === -1) { siteId = item.data; }
-        const style = new PIXI.TextStyle({ fontFamily: 'Arial', fontSize: 10, fill: '#000000', stroke: '#ffffff', strokeThickness: 1 });
+        // item.value: '{"bridgeStationIds":[1188,1186],"autoBridge":1,"direction":["left","right"]}'
+        if (siteId === -1) {
+          siteId = item.data || '';
+        }
+        const style = new PIXI.TextStyle({
+          fontFamily: 'Arial',
+          fontSize: 10,
+          fill: '#000000',
+          stroke: '#ffffff',
+          strokeThickness: 1
+        });
         const text = new PIXI.Text(String(siteId), style);
         text.anchor.set(0.5);
         text.position.set(sprite.width / 2, sprite.height / 2);
         sprite.addChild(text);
         sprite.textObj = text;
         const stationIdInt = parseInt(siteId, 10);
-        if (!isNaN(stationIdInt)) { this.pixiStaMap.set(stationIdInt, sprite); }
+        if (!isNaN(stationIdInt)) {
+          this.pixiStaMap.set(stationIdInt, sprite);
+        }
         sprite._stationId = isNaN(stationIdInt) ? null : stationIdInt;
         sprite._baseColor = 0x00ff7f;
         sprite._loopHighlighted = false;
         sprite.interactive = true;
         sprite.buttonMode = true;
         sprite.on('pointerdown', () => {
-          if (window.gsap) { window.gsap.killTweensOf(sprite); }
+          if (window.gsap) {
+            window.gsap.killTweensOf(sprite);
+          }
           sprite.alpha = 1;
           const id = parseInt(siteId, 10);
-          if (!isNaN(id)) { this.$emit('station-click', id); }
+          if (!isNaN(id)) {
+            this.$emit('station-click', id);
+          }
         });
       } else if (item.type == 'crn') {
         sprite = this.createTrackSprite(item.width, item.height, item.trackMask);
         sprite._kind = 'crn-track';
-        if (this.getDeviceNo(value) > 0) { this.crnList.push(item); }
+        this.crnList.push(item);
       } else if (item.type == 'dualCrn') {
         sprite = this.createTrackSprite(item.width, item.height, item.trackMask);
         sprite._kind = 'crn-track';
-        if (this.getDeviceNo(value) > 0) { this.dualCrnList.push(item); }
+        this.dualCrnList.push(item);
       } else if (item.type == 'rgv') {
         sprite = this.createTrackSprite(item.width, item.height, item.trackMask);
         sprite._kind = 'rgv-track';
-        if (this.getDeviceNo(value) > 0) { this.rgvList.push(item); }
+        this.rgvList.push(item);
       } else {
         return null;
       }
@@ -1631,172 +2264,172 @@
     collectTrackItem(item) {
       const value = item.value;
       if (item.type === 'crn') {
-        if (this.getDeviceNo(value) > 0) { this.crnList.push(item); }
+        if (this.getDeviceNo(value) > 0) {
+          this.crnList.push(item);
+        }
       } else if (item.type === 'dualCrn') {
-        if (this.getDeviceNo(value) > 0) { this.dualCrnList.push(item); }
+        if (this.getDeviceNo(value) > 0) {
+          this.dualCrnList.push(item);
+        }
       } else if (item.type === 'rgv') {
-        if (this.getDeviceNo(value) > 0) { this.rgvList.push(item); }
+        if (this.getDeviceNo(value) > 0) {
+          this.rgvList.push(item);
+        }
       }
     },
     isTrackType(cell) {
-      return cell && (cell.type === 'crn' || cell.type === 'dualCrn' || cell.type === 'rgv');
+      return (
+        cell &&
+        (cell.type === 'crn' ||
+          cell.type === 'dualCrn' ||
+          cell.type === 'rgv' ||
+          cell.type === 'annulus')
+      );
     },
     resolveMergedCell(map, rowIndex, colIndex) {
-      if (!map || rowIndex < 0 || colIndex < 0) { return null; }
+      if (!map || rowIndex < 0 || colIndex < 0) {
+        return null;
+      }
       const row = map[rowIndex];
-      if (!row || colIndex >= row.length) { return null; }
+      if (!row || colIndex >= row.length) {
+        return null;
+      }
       const cell = row[colIndex];
-      if (!cell) { return null; }
-      if (!cell.isMergedPart && cell.type !== 'merge') { return cell; }
+      if (!cell) {
+        return null;
+      }
+      if (!cell.isMergedPart && cell.type !== 'merge') {
+        return cell;
+      }
       if (cell.isMergedPart) {
         for (let c = colIndex - 1; c >= 0; c--) {
           const left = row[c];
-          if (!left) { continue; }
-          if (!left.isMergedPart && left.type !== 'merge' && left.posX === cell.posX) { return left; }
+          if (!left) {
+            continue;
+          }
+          if (!left.isMergedPart && left.type !== 'merge' && left.posX === cell.posX) {
+            return left;
+          }
         }
       }
       if (cell.type === 'merge') {
         for (let r = rowIndex - 1; r >= 0; r--) {
           const upRow = map[r];
-          if (!upRow || colIndex >= upRow.length) { continue; }
+          if (!upRow || colIndex >= upRow.length) {
+            continue;
+          }
           const up = upRow[colIndex];
-          if (!up) { continue; }
-          if (up.type !== 'merge') { return up; }
+          if (!up) {
+            continue;
+          }
+          if (up.type !== 'merge') {
+            return up;
+          }
         }
       }
       return null;
     },
-    getTrackMask(map, rowIndex, colIndex) {
-      const TRACK_N = 1;
-      const TRACK_E = 2;
-      const TRACK_S = 4;
-      const TRACK_W = 8;
-      const baseRow = map[rowIndex];
-      if (!baseRow) { return 0; }
-      const base = baseRow[colIndex];
-      if (!this.isTrackType(base)) { return 0; }
-      const rowSpan = base.rowSpan || 1;
-      const colSpan = base.colSpan || 1;
-      let mask = 0;
-      const n = this.resolveMergedCell(map, rowIndex - 1, colIndex);
-      const s = this.resolveMergedCell(map, rowIndex + rowSpan, colIndex);
-      const w = this.resolveMergedCell(map, rowIndex, colIndex - 1);
-      const e = this.resolveMergedCell(map, rowIndex, colIndex + colSpan);
-      if (n && n !== base && this.isTrackType(n)) { mask |= TRACK_N; }
-      if (e && e !== base && this.isTrackType(e)) { mask |= TRACK_E; }
-      if (s && s !== base && this.isTrackType(s)) { mask |= TRACK_S; }
-      if (w && w !== base && this.isTrackType(w)) { mask |= TRACK_W; }
-      if (mask === 0) { mask = TRACK_E | TRACK_W; }
-      return mask;
-    },
+    // getTrackMask(map, rowIndex, colIndex) {
+    //   const TRACK_N = 1;
+    //   const TRACK_E = 2;
+    //   const TRACK_S = 4;
+    //   const TRACK_W = 8;
+    //   const baseRow = map[rowIndex];
+    //   if (!baseRow) {
+    //     return 0;
+    //   }
+    //   const base = baseRow[colIndex];
+    //   if (!this.isTrackType(base)) {
+    //     return 0;
+    //   }
+    //   const rowSpan = base.rowSpan || 1;
+    //   const colSpan = base.colSpan || 1;
+    //   let mask = 0;
+    //   const n = this.resolveMergedCell(map, rowIndex - 1, colIndex);
+    //   const s = this.resolveMergedCell(map, rowIndex + rowSpan, colIndex);
+    //   const w = this.resolveMergedCell(map, rowIndex, colIndex - 1);
+    //   const e = this.resolveMergedCell(map, rowIndex, colIndex + colSpan);
+    //   if (n && n !== base && this.isTrackType(n)) {
+    //     mask |= TRACK_N;
+    //   }
+    //   if (e && e !== base && this.isTrackType(e)) {
+    //     mask |= TRACK_E;
+    //   }
+    //   if (s && s !== base && this.isTrackType(s)) {
+    //     mask |= TRACK_S;
+    //   }
+    //   if (w && w !== base && this.isTrackType(w)) {
+    //     mask |= TRACK_W;
+    //   }
+    //   if (mask === 0) {
+    //     mask = TRACK_E | TRACK_W;
+    //   }
+    //   return mask;
+    // },
     drawTracks(map) {
-      if (!this.tracksGraphics) { return; }
+      if (!this.tracksGraphics) {
+        return;
+      }
+
+      const centerOf = (cell) => ({
+        x: cell.x + cell.width / 2,
+        y: cell.y + cell.height / 2
+      });
+
       this.tracksGraphics.clear();
-      const rail = 3;
+      const rail = 2;
       const color = 0x555555;
-      this.tracksGraphics.lineStyle({ width: rail, color: color, alpha: 1, cap: PIXI.LINE_CAP.ROUND, join: PIXI.LINE_JOIN.ROUND });
-      const drawn = new Set();
-      const toKey = (p) => {
-        const x = Math.round(p.x * 100) / 100;
-        const y = Math.round(p.y * 100) / 100;
-        return x + "," + y;
-      };
-      const edgeKey = (a, b) => {
-        const ka = toKey(a);
-        const kb = toKey(b);
-        return ka < kb ? (ka + "|" + kb) : (kb + "|" + ka);
-      };
-      const centerOf = (cell) => ({ x: cell.posX + cell.width / 2, y: cell.posY + cell.height / 2 });
 
-      for (let r = 0; r < map.length; r++) {
-        const row = map[r];
-        if (!row) { continue; }
-        for (let c = 0; c < row.length; c++) {
-          const cell = row[c];
-          if (!cell || cell.type === 'merge' || !this.isTrackType(cell) || cell.isMergedPart) { continue; }
-          const rowSpan = cell.rowSpan || 1;
-          const colSpan = cell.colSpan || 1;
-          const n = this.resolveMergedCell(map, r - 1, c);
-          const s = this.resolveMergedCell(map, r + rowSpan, c);
-          const w = this.resolveMergedCell(map, r, c - 1);
-          const e = this.resolveMergedCell(map, r, c + colSpan);
-          const hasN = n && this.isTrackType(n);
-          const hasE = e && this.isTrackType(e);
-          const hasS = s && this.isTrackType(s);
-          const hasW = w && this.isTrackType(w);
-          const count = (hasN ? 1 : 0) + (hasE ? 1 : 0) + (hasS ? 1 : 0) + (hasW ? 1 : 0);
-          const straight = (hasN && hasS) || (hasE && hasW);
-          if (count === 2 && !straight) {
-            const cPos = centerOf(cell);
-            let p1 = null;
-            let p2 = null;
-            if (hasN && hasE) { p1 = centerOf(n); p2 = centerOf(e); }
-            else if (hasE && hasS) { p1 = centerOf(e); p2 = centerOf(s); }
-            else if (hasS && hasW) { p1 = centerOf(s); p2 = centerOf(w); }
-            else if (hasW && hasN) { p1 = centerOf(w); p2 = centerOf(n); }
-            if (p1 && p2) {
-              const k1 = edgeKey(cPos, p1);
-              const k2 = edgeKey(cPos, p2);
-              if (!drawn.has(k1) || !drawn.has(k2)) {
-                this.tracksGraphics.moveTo(p1.x, p1.y);
-                this.tracksGraphics.lineTo(cPos.x, cPos.y);
-                this.tracksGraphics.lineTo(p2.x, p2.y);
-              }
-              drawn.add(k1);
-              drawn.add(k2);
-            }
+      map.forEach((item) => {
+        this.tracksGraphics.lineStyle({
+          width: rail,
+          color: G.TYPE_META[item.type].border,
+          alpha: 1,
+          cap: PIXI.LINE_CAP.ROUND,
+          join: PIXI.LINE_JOIN.ROUND
+        });
+        if (['crn', 'rgv', 'dualCrn'].includes(item.type)) {
+          if (item.width > item.height) {
+            // 姘村钩
+            this.tracksGraphics.moveTo(item.x, item.y + item.height / 2);
+            this.tracksGraphics.lineTo(item.x + item.width, item.y + item.height / 2);
+            this.tracksGraphics.endFill();
+          } else {
+            // 鍨傜洿
+            this.tracksGraphics.moveTo(item.x + item.width / 2, item.y);
+            this.tracksGraphics.lineTo(item.x + item.width / 2, item.y + item.height);
+            this.tracksGraphics.endFill();
           }
+          // this.tracksGraphics.moveTo(cPos.x, cPos.y);
+          // this.tracksGraphics.lineTo(cPos.x + item.width, cPos.y);
+          // this.tracksGraphics.endFill();
+        } else if (item.type === 'annulus') {
+          G.strokeAnnulusDualOutline(this.tracksGraphics, item, item.shape || 'rect');
         }
-      }
-
-      for (let r = 0; r < map.length; r++) {
-        const row = map[r];
-        if (!row) { continue; }
-        for (let c = 0; c < row.length; c++) {
-          const cell = row[c];
-          if (!cell || cell.type === 'merge' || !this.isTrackType(cell) || cell.isMergedPart) { continue; }
-          const cPos = centerOf(cell);
-          const rowSpan = cell.rowSpan || 1;
-          const colSpan = cell.colSpan || 1;
-          const e = this.resolveMergedCell(map, r, c + colSpan);
-          const s = this.resolveMergedCell(map, r + rowSpan, c);
-          if (e && this.isTrackType(e)) {
-            const p = centerOf(e);
-            const k = edgeKey(cPos, p);
-            if (!drawn.has(k)) {
-              this.tracksGraphics.moveTo(cPos.x, cPos.y);
-              this.tracksGraphics.lineTo(p.x, p.y);
-              drawn.add(k);
-            }
-          }
-          if (s && this.isTrackType(s)) {
-            const p = centerOf(s);
-            const k = edgeKey(cPos, p);
-            if (!drawn.has(k)) {
-              this.tracksGraphics.moveTo(cPos.x, cPos.y);
-              this.tracksGraphics.lineTo(p.x, p.y);
-              drawn.add(k);
-            }
-          }
-        }
-      }
+      });
     },
     updateColor(sprite, color) {
       if (sprite && sprite._kind === 'devp') {
         const key = sprite.width + '-' + sprite.height + '-' + color;
         let texture = this.pixiDevpTextureMap.get(key);
         if (!texture) {
-          texture = this.createDevpTextureColoredRect(Math.round(sprite.width), Math.round(sprite.height), color);
+          texture = this.createDevpTextureColoredRect(
+            Math.round(sprite.width),
+            Math.round(sprite.height),
+            color
+          );
           this.pixiDevpTextureMap.set(key, texture);
         }
         const textObj = sprite.textObj;
         sprite.texture = texture;
         if (textObj) {
-          if (textObj.parent !== sprite) { sprite.addChild(textObj); }
+          if (textObj.parent !== sprite) {
+            sprite.addChild(textObj);
+          }
           textObj.position.set(sprite.width / 2, sprite.height / 2);
           const fill = this.getContrastColor(color);
           textObj.style.fill = fill;
-          textObj.style.stroke = (fill === '#000000' ? '#ffffff' : '#000000');
+          textObj.style.stroke = fill === '#000000' ? '#ffffff' : '#000000';
           textObj.style.strokeThickness = 1;
         }
         return;
@@ -1804,7 +2437,9 @@
       sprite.tint = color;
     },
     setStationBaseColor(sprite, color) {
-      if (!sprite) { return; }
+      if (!sprite) {
+        return;
+      }
       sprite._baseColor = color;
       if (this.isStationInHoverLoop(sprite)) {
         this.applyHighlightColor(sprite);
@@ -1814,31 +2449,43 @@
       }
     },
     applyHighlightColor(sprite) {
-      if (!sprite) { return; }
+      if (!sprite) {
+        return;
+      }
       this.updateColor(sprite, this.loopHighlightColor);
       sprite._loopHighlighted = true;
     },
     isStationInHoverLoop(sprite) {
-      if (!sprite || sprite._stationId == null || !this.hoverLoopStationIdSet) { return false; }
+      if (!sprite || sprite._stationId == null || !this.hoverLoopStationIdSet) {
+        return false;
+      }
       return this.hoverLoopStationIdSet.has(sprite._stationId);
     },
     buildStationIdSet(stationIdList) {
       const set = new Set();
-      if (!Array.isArray(stationIdList)) { return set; }
+      if (!Array.isArray(stationIdList)) {
+        return set;
+      }
       stationIdList.forEach((id) => {
         const v = parseInt(id, 10);
-        if (!isNaN(v)) { set.add(v); }
+        if (!isNaN(v)) {
+          set.add(v);
+        }
       });
       return set;
     },
     applyLoopStationHighlight() {
-      if (!this.pixiStaMap) { return; }
+      if (!this.pixiStaMap) {
+        return;
+      }
       this.pixiStaMap.forEach((sprite) => {
-        if (!sprite) { return; }
+        if (!sprite) {
+          return;
+        }
         if (this.isStationInHoverLoop(sprite)) {
           this.applyHighlightColor(sprite);
         } else if (sprite._loopHighlighted) {
-          const baseColor = (typeof sprite._baseColor === 'number') ? sprite._baseColor : 0xb8b8b8;
+          const baseColor = typeof sprite._baseColor === 'number' ? sprite._baseColor : 0xb8b8b8;
           this.updateColor(sprite, baseColor);
           sprite._loopHighlighted = false;
         }
@@ -1847,8 +2494,10 @@
     clearLoopStationHighlight() {
       if (this.pixiStaMap) {
         this.pixiStaMap.forEach((sprite) => {
-          if (!sprite || !sprite._loopHighlighted) { return; }
-          const baseColor = (typeof sprite._baseColor === 'number') ? sprite._baseColor : 0xb8b8b8;
+          if (!sprite || !sprite._loopHighlighted) {
+            return;
+          }
+          const baseColor = typeof sprite._baseColor === 'number' ? sprite._baseColor : 0xb8b8b8;
           this.updateColor(sprite, baseColor);
           sprite._loopHighlighted = false;
         });
@@ -1856,131 +2505,10 @@
       this.hoverLoopNo = null;
       this.hoverLoopStationIdSet = new Set();
     },
-    clearTraceOverlay() {
-      if (window.gsap && this.tracePulseTween) {
-        try { this.tracePulseTween.kill(); } catch (e) {}
-      }
-      this.tracePulseTween = null;
-      if (!this.traceOverlayContainer) { return; }
-      const children = this.traceOverlayContainer.removeChildren();
-      children.forEach((child) => {
-        if (child && typeof child.destroy === 'function') {
-          child.destroy({ children: true, texture: false, baseTexture: false });
-        }
-      });
-    },
-    normalizeTraceOverlay(trace) {
-      if (!trace) { return null; }
-      const taskNo = parseInt(trace.taskNo, 10);
-      return {
-        taskNo: isNaN(taskNo) ? null : taskNo,
-        status: trace.status || '',
-        currentStationId: this.parseStationTaskNo(trace.currentStationId),
-        finalTargetStationId: this.parseStationTaskNo(trace.finalTargetStationId),
-        blockedStationId: this.parseStationTaskNo(trace.blockedStationId),
-        passedStationIds: this.normalizeTraceStationIds(trace.passedStationIds),
-        pendingStationIds: this.normalizeTraceStationIds(trace.pendingStationIds),
-        latestAppendedPath: this.normalizeTraceStationIds(trace.latestIssuedSegmentPath || trace.latestAppendedPath)
-      };
-    },
-    normalizeTraceStationIds(list) {
-      if (!Array.isArray(list)) { return []; }
-      const result = [];
-      list.forEach((item) => {
-        const stationId = parseInt(item, 10);
-        if (!isNaN(stationId)) { result.push(stationId); }
-      });
-      return result;
-    },
-    getStationCenter(stationId) {
-      if (stationId == null || !this.pixiStaMap) { return null; }
-      const sprite = this.pixiStaMap.get(parseInt(stationId, 10));
-      if (!sprite) { return null; }
-      return {
-        x: sprite.x + sprite.width / 2,
-        y: sprite.y + sprite.height / 2
-      };
-    },
-    drawTracePairs(graphics, stationIds, color, width, alpha) {
-      if (!graphics || !Array.isArray(stationIds) || stationIds.length < 2) { return; }
-      graphics.lineStyle({ width: width, color: color, alpha: alpha, cap: PIXI.LINE_CAP.ROUND, join: PIXI.LINE_JOIN.ROUND });
-      for (let i = 1; i < stationIds.length; i++) {
-        const prev = this.getStationCenter(stationIds[i - 1]);
-        const curr = this.getStationCenter(stationIds[i]);
-        if (!prev || !curr) { continue; }
-        graphics.moveTo(prev.x, prev.y);
-        graphics.lineTo(curr.x, curr.y);
-      }
-    },
-    drawTraceMarker(container, stationId, options) {
-      const point = this.getStationCenter(stationId);
-      if (!container || !point) { return null; }
-      const marker = new PIXI.Container();
-      const ring = new PIXI.Graphics();
-      const fill = new PIXI.Graphics();
-      const radius = options && options.radius ? options.radius : 18;
-      const color = options && options.color != null ? options.color : 0x1d4ed8;
-      ring.lineStyle(3, color, 0.95);
-      ring.drawCircle(0, 0, radius);
-      fill.beginFill(color, 0.18);
-      fill.drawCircle(0, 0, Math.max(6, radius * 0.42));
-      fill.endFill();
-      marker.addChild(ring);
-      marker.addChild(fill);
-      marker.position.set(point.x, point.y);
-      container.addChild(marker);
-      return marker;
-    },
-    buildPendingTraceSequence(trace) {
-      const pending = this.normalizeTraceStationIds(trace && trace.pendingStationIds);
-      const currentStationId = this.parseStationTaskNo(trace && trace.currentStationId);
-      if (pending.length === 0) {
-        return currentStationId > 0 ? [currentStationId] : [];
-      }
-      if (currentStationId > 0 && pending[0] !== currentStationId) {
-        return [currentStationId].concat(pending);
-      }
-      return pending;
-    },
-    renderTraceOverlay() {
-      if (!this.traceOverlayContainer) { return; }
-      this.clearTraceOverlay();
-      const trace = this.normalizeTraceOverlay(this.traceOverlay);
-      if (!trace || !this.pixiStaMap || this.pixiStaMap.size === 0) { return; }
-
-      const graphics = new PIXI.Graphics();
-      this.drawTracePairs(graphics, trace.passedStationIds, 0x2563eb, 5, 0.95);
-      this.drawTracePairs(graphics, this.buildPendingTraceSequence(trace), 0xf97316, 4, 0.9);
-      this.drawTracePairs(graphics, trace.latestAppendedPath, 0xfacc15, 7, 0.88);
-      this.traceOverlayContainer.addChild(graphics);
-
-      const currentMarker = this.drawTraceMarker(this.traceOverlayContainer, trace.currentStationId, {
-        color: 0x2563eb,
-        radius: 20
-      });
-      if (currentMarker && window.gsap) {
-        this.tracePulseTween = window.gsap.to(currentMarker.scale, {
-          x: 1.18,
-          y: 1.18,
-          duration: 0.55,
-          repeat: -1,
-          yoyo: true,
-          ease: 'sine.inOut'
-        });
-      }
-
-      if (trace.blockedStationId > 0) {
-        const blockedMarker = this.drawTraceMarker(this.traceOverlayContainer, trace.blockedStationId, {
-          color: 0xdc2626,
-          radius: 22
-        });
-        if (blockedMarker) {
-          blockedMarker.alpha = 0.95;
-        }
-      }
-    },
     handleLoopCardEnter(loopItem) {
-      if (!loopItem) { return; }
+      if (!loopItem) {
+        return;
+      }
       this.hoverLoopNo = loopItem.loopNo;
       this.hoverLoopStationIdSet = this.buildStationIdSet(loopItem.stationIdList);
       this.applyLoopStationHighlight();
@@ -1995,21 +2523,56 @@
       }
     },
     isJson(str) {
-      try { JSON.parse(str); return true; } catch (e) { return false; }
+      try {
+        JSON.parse(str);
+        return true;
+      } catch (e) {
+        return false;
+      }
     },
     getDeviceNo(obj) {
-      if (this.isJson(obj)) { let data = JSON.parse(obj); if (data.deviceNo == null || data.deviceNo == undefined) { return -1; } return data.deviceNo; } else { return -1; }
+      if (this.isJson(obj)) {
+        let data = JSON.parse(obj);
+        if (data.deviceNo == null || data.deviceNo == undefined) {
+          return -1;
+        }
+        return data.deviceNo;
+      } else {
+        return -1;
+      }
     },
     getTaskNo(obj) {
-      if (this.isJson(obj)) { let data = JSON.parse(obj); if (data.taskNo == null || data.taskNo == undefined) { return -1; } return data.taskNo; } else { return -1; }
+      if (this.isJson(obj)) {
+        let data = JSON.parse(obj);
+        if (data.taskNo == null || data.taskNo == undefined) {
+          return -1;
+        }
+        return data.taskNo;
+      } else {
+        return -1;
+      }
     },
     getStationId(obj) {
-      if (this.isJson(obj)) { let data = JSON.parse(obj); if (data.stationId == null || data.stationId == undefined) { return -1; } return data.stationId; } else { return -1; }
+      if (this.isJson(obj)) {
+        let data = JSON.parse(obj);
+        if (data.stationId == null || data.stationId == undefined) {
+          return -1;
+        }
+        return data.stationId;
+      } else {
+        return -1;
+      }
     },
     parseMapValue(obj) {
-      if (obj == null) { return null; }
-      if (typeof obj === 'object') { return obj; }
-      if (!this.isJson(obj)) { return null; }
+      if (obj == null) {
+        return null;
+      }
+      if (typeof obj === 'object') {
+        return obj;
+      }
+      if (!this.isJson(obj)) {
+        return null;
+      }
       try {
         return JSON.parse(obj);
       } catch (e) {
@@ -2038,8 +2601,15 @@
       const result = [];
       const seen = new Set();
       rawList.forEach((item) => {
-        const key = aliasMap[String(item || '').trim().toLowerCase()];
-        if (!key || seen.has(key)) { return; }
+        const key =
+          aliasMap[
+            String(item || '')
+              .trim()
+              .toLowerCase()
+          ];
+        if (!key || seen.has(key)) {
+          return;
+        }
         seen.add(key);
         result.push(key);
       });
@@ -2048,15 +2618,29 @@
     resolveStationDirectionList(map, rowIndex, colIndex, item) {
       const valueObj = this.parseMapValue(item && item.value);
       const fromValue = this.normalizeDirectionList(valueObj && valueObj.direction);
-      if (fromValue.length > 0) { return fromValue; }
+      if (fromValue.length > 0) {
+        return fromValue;
+      }
       const rowSpan = item && item.rowSpan ? item.rowSpan : 1;
       const colSpan = item && item.colSpan ? item.colSpan : 1;
       const fallback = [];
       const candidateList = [
-        { key: 'top', cell: this.resolveMergedCell(map, rowIndex - 1, colIndex) },
-        { key: 'right', cell: this.resolveMergedCell(map, rowIndex, colIndex + colSpan) },
-        { key: 'bottom', cell: this.resolveMergedCell(map, rowIndex + rowSpan, colIndex) },
-        { key: 'left', cell: this.resolveMergedCell(map, rowIndex, colIndex - 1) }
+        {
+          key: 'top',
+          cell: this.resolveMergedCell(map, rowIndex - 1, colIndex)
+        },
+        {
+          key: 'right',
+          cell: this.resolveMergedCell(map, rowIndex, colIndex + colSpan)
+        },
+        {
+          key: 'bottom',
+          cell: this.resolveMergedCell(map, rowIndex + rowSpan, colIndex)
+        },
+        {
+          key: 'left',
+          cell: this.resolveMergedCell(map, rowIndex, colIndex - 1)
+        }
       ];
       candidateList.forEach((candidate) => {
         if (this.isStationDirectionNeighbor(candidate.cell)) {
@@ -2066,12 +2650,18 @@
       return fallback;
     },
     isStationDirectionNeighbor(cell) {
-      if (!cell) { return false; }
-      if (cell.type === 'devp') { return true; }
+      if (!cell) {
+        return false;
+      }
+      if (cell.type === 'devp') {
+        return true;
+      }
       return this.isTrackType(cell);
     },
     createStationDirectionOverlay(width, height, directionList) {
-      if (!Array.isArray(directionList) || directionList.length === 0) { return null; }
+      if (!Array.isArray(directionList) || directionList.length === 0) {
+        return null;
+      }
       const container = new PIXI.Container();
       const arrowSize = Math.max(4, Math.min(width, height) * 0.22);
       const margin = Math.max(2, Math.min(width, height) * 0.12);
@@ -2083,7 +2673,9 @@
       return container;
     },
     drawStationDirectionArrow(graphics, width, height, direction, size, margin) {
-      if (!graphics) { return; }
+      if (!graphics) {
+        return;
+      }
       const halfBase = Math.max(2, size * 0.45);
       const stemLen = Math.max(3, size * 0.7);
       const centerX = width / 2;
@@ -2093,7 +2685,10 @@
         const tipY = margin;
         const baseY = margin + size;
         const stemY = Math.min(centerY - 2, baseY + stemLen);
-        if (stemY > baseY) { graphics.moveTo(centerX, stemY); graphics.lineTo(centerX, baseY); }
+        if (stemY > baseY) {
+          graphics.moveTo(centerX, stemY);
+          graphics.lineTo(centerX, baseY);
+        }
         graphics.moveTo(centerX, tipY);
         graphics.lineTo(centerX - halfBase, baseY);
         graphics.lineTo(centerX + halfBase, baseY);
@@ -2101,7 +2696,10 @@
         const tipX = width - margin;
         const baseX = width - margin - size;
         const stemX = Math.max(centerX + 2, baseX - stemLen);
-        if (stemX < baseX) { graphics.moveTo(stemX, centerY); graphics.lineTo(baseX, centerY); }
+        if (stemX < baseX) {
+          graphics.moveTo(stemX, centerY);
+          graphics.lineTo(baseX, centerY);
+        }
         graphics.moveTo(tipX, centerY);
         graphics.lineTo(baseX, centerY - halfBase);
         graphics.lineTo(baseX, centerY + halfBase);
@@ -2109,7 +2707,10 @@
         const tipY = height - margin;
         const baseY = height - margin - size;
         const stemY = Math.max(centerY + 2, baseY - stemLen);
-        if (stemY < baseY) { graphics.moveTo(centerX, stemY); graphics.lineTo(centerX, baseY); }
+        if (stemY < baseY) {
+          graphics.moveTo(centerX, stemY);
+          graphics.lineTo(centerX, baseY);
+        }
         graphics.moveTo(centerX, tipY);
         graphics.lineTo(centerX - halfBase, baseY);
         graphics.lineTo(centerX + halfBase, baseY);
@@ -2117,7 +2718,10 @@
         const tipX = margin;
         const baseX = margin + size;
         const stemX = Math.min(centerX - 2, baseX + stemLen);
-        if (stemX > baseX) { graphics.moveTo(stemX, centerY); graphics.lineTo(baseX, centerY); }
+        if (stemX > baseX) {
+          graphics.moveTo(stemX, centerY);
+          graphics.lineTo(baseX, centerY);
+        }
         graphics.moveTo(tipX, centerY);
         graphics.lineTo(baseX, centerY - halfBase);
         graphics.lineTo(baseX, centerY + halfBase);
@@ -2126,17 +2730,31 @@
       graphics.endFill();
     },
     applyStationDirectionVisibility() {
-      if (!this.pixiStaMap) { return; }
+      if (!this.pixiStaMap) {
+        return;
+      }
       this.pixiStaMap.forEach((sprite) => {
-        if (!sprite || !sprite.directionObj) { return; }
+        if (!sprite || !sprite.directionObj) {
+          return;
+        }
         sprite.directionObj.visible = this.showStationDirection;
       });
     },
     getTrackSiteNo(obj) {
-      if (this.isJson(obj)) { let data = JSON.parse(obj); if (data.trackSiteNo == null || data.trackSiteNo == undefined) { return -1; } return data.trackSiteNo; } else { return -1; }
+      if (this.isJson(obj)) {
+        let data = JSON.parse(obj);
+        if (data.trackSiteNo == null || data.trackSiteNo == undefined) {
+          return -1;
+        }
+        return data.trackSiteNo;
+      } else {
+        return -1;
+      }
     },
     buildShelfHitGrid(map, rowHeights, rowOffsets) {
-      if (!map || !Array.isArray(map)) { return; }
+      if (!map || !Array.isArray(map)) {
+        return;
+      }
       this.mapRowOffsets = Array.isArray(rowOffsets) ? rowOffsets.slice() : [];
       this.mapRowHeights = Array.isArray(rowHeights) ? rowHeights.slice() : [];
       const rowColOffsets = [];
@@ -2145,7 +2763,9 @@
       let maxCols = 0;
       for (let r = 0; r < map.length; r++) {
         const row = map[r];
-        if (row && row.length > maxCols) { maxCols = row.length; }
+        if (row && row.length > maxCols) {
+          maxCols = row.length;
+        }
         rowShelfCells[r] = [];
       }
       const colWidths = new Array(maxCols);
@@ -2153,13 +2773,18 @@
         let w = null;
         for (let r = 0; r < map.length; r++) {
           const cell = map[r] && map[r][c];
-          if (!cell) { continue; }
+          if (!cell) {
+            continue;
+          }
           if (cell.cellWidth != null && cell.cellWidth !== '') {
             const base = Number(cell.cellWidth);
-            if (isFinite(base) && base > 0) { w = base / 40; break; }
+            if (isFinite(base) && base > 0) {
+              w = base / 40;
+              break;
+            }
           }
         }
-        colWidths[c] = (w && isFinite(w) && w > 0) ? w : 25;
+        colWidths[c] = w && isFinite(w) && w > 0 ? w : 25;
       }
       const colOffsets = new Array(maxCols);
       let xCursor = 0;
@@ -2180,9 +2805,11 @@
           let w = null;
           if (cell && cell.cellWidth != null && cell.cellWidth !== '') {
             const base = Number(cell.cellWidth);
-            if (isFinite(base) && base > 0) { w = base / 40; }
+            if (isFinite(base) && base > 0) {
+              w = base / 40;
+            }
           }
-          widths[c] = (w && isFinite(w) && w > 0) ? w : 25;
+          widths[c] = w && isFinite(w) && w > 0 ? w : 25;
         }
         const offsets = new Array(row.length);
         let x = 0;
@@ -2201,16 +2828,32 @@
 
       for (let r = 0; r < map.length; r++) {
         const row = map[r];
-        if (!row) { continue; }
+        if (!row) {
+          continue;
+        }
         for (let c = 0; c < row.length; c++) {
           const cell = row[c];
-          if (!cell || cell.type !== 'shelf') { continue; }
-          const startRow = this.findIndexByOffsets(this.mapRowOffsets, this.mapRowHeights, cell.posY + 0.01);
-          const endRow = this.findIndexByOffsets(this.mapRowOffsets, this.mapRowHeights, cell.posY + cell.height - 0.01);
-          if (startRow < 0) { continue; }
+          if (!cell || cell.type !== 'shelf') {
+            continue;
+          }
+          const startRow = this.findIndexByOffsets(
+            this.mapRowOffsets,
+            this.mapRowHeights,
+            cell.posY + 0.01
+          );
+          const endRow = this.findIndexByOffsets(
+            this.mapRowOffsets,
+            this.mapRowHeights,
+            cell.posY + cell.height - 0.01
+          );
+          if (startRow < 0) {
+            continue;
+          }
           const last = endRow >= 0 ? endRow : startRow;
           for (let rr = startRow; rr <= last; rr++) {
-            if (!rowShelfCells[rr]) { rowShelfCells[rr] = []; }
+            if (!rowShelfCells[rr]) {
+              rowShelfCells[rr] = [];
+            }
             rowShelfCells[rr].push(cell);
           }
         }
@@ -2222,7 +2865,9 @@
         this.shelfCullRaf = null;
       }
       this.shelfChunkList = [];
-      if (!this.shelvesContainer) { return; }
+      if (!this.shelvesContainer) {
+        return;
+      }
       const children = this.shelvesContainer.removeChildren();
       children.forEach((child) => {
         if (child && typeof child.destroy === 'function') {
@@ -2232,15 +2877,26 @@
     },
     buildShelfChunks(map, contentW, contentH) {
       this.clearShelfChunks();
-      if (!this.pixiApp || !this.pixiApp.renderer || !this.shelvesContainer || !Array.isArray(map)) { return; }
+      if (
+        !this.pixiApp ||
+        !this.pixiApp.renderer ||
+        !this.shelvesContainer ||
+        !Array.isArray(map)
+      ) {
+        return;
+      }
       const chunkSize = Math.max(256, parseInt(this.shelfChunkSize, 10) || 2048);
       const chunkMap = new Map();
       for (let r = 0; r < map.length; r++) {
         const row = map[r];
-        if (!row) { continue; }
+        if (!row) {
+          continue;
+        }
         for (let c = 0; c < row.length; c++) {
           const cell = row[c];
-          if (!cell || cell.type !== 'shelf' || cell.type === 'merge') { continue; }
+          if (!cell || cell.type !== 'shelf' || cell.type === 'merge') {
+            continue;
+          }
           const startChunkX = Math.floor(cell.posX / chunkSize);
           const endChunkX = Math.floor((cell.posX + Math.max(1, cell.width) - 0.01) / chunkSize);
           const startChunkY = Math.floor(cell.posY / chunkSize);
@@ -2298,7 +2954,9 @@
       this.updateVisibleShelfChunks();
     },
     getViewportLocalBounds(padding) {
-      if (!this.mapRoot || !this.pixiApp) { return null; }
+      if (!this.mapRoot || !this.pixiApp) {
+        return null;
+      }
       const viewport = this.getViewportSize();
       const pad = Math.max(0, Number(padding) || 0);
       const points = [
@@ -2313,23 +2971,40 @@
       let maxY = -Infinity;
       points.forEach((point) => {
         const local = this.mapRoot.toLocal(point);
-        if (local.x < minX) { minX = local.x; }
-        if (local.y < minY) { minY = local.y; }
-        if (local.x > maxX) { maxX = local.x; }
-        if (local.y > maxY) { maxY = local.y; }
+        if (local.x < minX) {
+          minX = local.x;
+        }
+        if (local.y < minY) {
+          minY = local.y;
+        }
+        if (local.x > maxX) {
+          maxX = local.x;
+        }
+        if (local.y > maxY) {
+          maxY = local.y;
+        }
       });
-      if (!isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY)) { return null; }
+      if (!isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY)) {
+        return null;
+      }
       return { minX: minX, minY: minY, maxX: maxX, maxY: maxY };
     },
     updateVisibleShelfChunks() {
-      if (!this.shelfChunkList || this.shelfChunkList.length === 0) { return; }
+      if (!this.shelfChunkList || this.shelfChunkList.length === 0) {
+        return;
+      }
       const localBounds = this.getViewportLocalBounds(this.shelfCullPadding);
-      if (!localBounds) { return; }
+      if (!localBounds) {
+        return;
+      }
       for (let i = 0; i < this.shelfChunkList.length; i++) {
         const sprite = this.shelfChunkList[i];
         const bounds = sprite && sprite._chunkBounds;
-        if (!bounds) { continue; }
-        const visible = bounds.x < localBounds.maxX &&
+        if (!bounds) {
+          continue;
+        }
+        const visible =
+          bounds.x < localBounds.maxX &&
           bounds.x + bounds.width > localBounds.minX &&
           bounds.y < localBounds.maxY &&
           bounds.y + bounds.height > localBounds.minY;
@@ -2339,41 +3014,70 @@
       }
     },
     scheduleShelfChunkCulling() {
-      if (this.shelfCullRaf) { return; }
+      if (this.shelfCullRaf) {
+        return;
+      }
       this.shelfCullRaf = requestAnimationFrame(() => {
         this.shelfCullRaf = null;
         this.updateVisibleShelfChunks();
       });
     },
     findIndexByOffsets(offsets, sizes, value) {
-      if (!offsets || !sizes || offsets.length === 0) { return -1; }
+      if (!offsets || !sizes || offsets.length === 0) {
+        return -1;
+      }
       for (let i = 0; i < offsets.length; i++) {
         const start = offsets[i];
         const end = start + (sizes[i] || 0);
-        if (value >= start && value < end) { return i; }
+        if (value >= start && value < end) {
+          return i;
+        }
       }
       return -1;
     },
     updateShelfHoverFromPointer(globalPos) {
-      if (!this.map || !this.mapRoot) { return; }
-      if (!this.mapRowOffsets.length || !this.mapColOffsets.length) { return; }
+      if (!this.map || !this.mapRoot) {
+        return;
+      }
+      if (!this.mapRowOffsets.length || !this.mapColOffsets.length) {
+        return;
+      }
       const local = this.mapRoot.toLocal(new PIXI.Point(globalPos.x, globalPos.y));
       const rowIndex = this.findIndexByOffsets(this.mapRowOffsets, this.mapRowHeights, local.y);
-      if (rowIndex < 0) { if (this.hoveredShelfCell) { this.hoveredShelfCell = null; this.hideShelfTooltip(); } return; }
+
+      if (rowIndex < 0) {
+        if (this.hoveredShelfCell) {
+          this.hoveredShelfCell = null;
+          this.hideShelfTooltip();
+        }
+        return;
+      }
       let cell = null;
       if (this.mapRowShelfCells && this.mapRowShelfCells[rowIndex]) {
         const list = this.mapRowShelfCells[rowIndex];
         for (let i = 0; i < list.length; i++) {
           const it = list[i];
-          if (!it) { continue; }
-          if (local.x >= it.posX && local.x < it.posX + it.width &&
-              local.y >= it.posY && local.y < it.posY + it.height) {
+          if (!it) {
+            continue;
+          }
+          if (
+            local.x >= it.posX &&
+            local.x < it.posX + it.width &&
+            local.y >= it.posY &&
+            local.y < it.posY + it.height
+          ) {
             cell = it;
             break;
           }
         }
       }
-      if (!cell || cell.type !== 'shelf') { if (this.hoveredShelfCell) { this.hoveredShelfCell = null; this.hideShelfTooltip(); } return; }
+      if (!cell || cell.type !== 'shelf') {
+        if (this.hoveredShelfCell) {
+          this.hoveredShelfCell = null;
+          this.hideShelfTooltip();
+        }
+        return;
+      }
       if (this.hoveredShelfCell !== cell) {
         this.hoveredShelfCell = cell;
         this.shelfTooltip.item = cell;
@@ -2383,36 +3087,57 @@
       this.updateShelfTooltipPositionByGlobal(globalPos);
     },
     normalizeLocTypeKey(value) {
-      if (value == null) { return null; }
+      if (value == null) {
+        return null;
+      }
       const str = String(value).trim();
-      if (!str) { return null; }
-      const parts = str.split('-').filter(p => p !== '');
-      if (parts.length >= 3) { return parts.slice(0, parts.length - 1).join('-'); }
+      if (!str) {
+        return null;
+      }
+      const parts = str.split('-').filter((p) => p !== '');
+      if (parts.length >= 3) {
+        return parts.slice(0, parts.length - 1).join('-');
+      }
       return str;
     },
     loadLocList() {
-      if (!window.$ || typeof baseUrl === 'undefined') { return; }
-      if (this.locListLoading) { return; }
+      if (!window.$ || typeof baseUrl === 'undefined') {
+        return;
+      }
+      if (this.locListLoading) {
+        return;
+      }
       this.locListLoading = true;
       $.ajax({
-        url: baseUrl + "/console/map/locList",
-        headers: { 'token': localStorage.getItem('token') },
+        url: baseUrl + '/console/map/locList',
+        headers: { token: localStorage.getItem('token') },
         dataType: 'json',
         method: 'GET',
         success: (res) => {
           if (res && !Array.isArray(res)) {
-            if (res.code === 403) { parent.location.href = baseUrl + "/login"; return; }
-            if (res.code !== 200) { return; }
+            if (res.code === 403) {
+              parent.location.href = baseUrl + '/login';
+              return;
+            }
+            if (res.code !== 200) {
+              return;
+            }
           }
-          const list = Array.isArray(res) ? res : (res && res.code === 200 ? res.data : null);
-          if (!list || !Array.isArray(list)) { return; }
+          const list = Array.isArray(res) ? res : res && res.code === 200 ? res.data : null;
+          if (!list || !Array.isArray(list)) {
+            return;
+          }
           const map = new Map();
           list.forEach((item) => {
-            if (!item) { return; }
+            if (!item) {
+              return;
+            }
             const locType = item.locType != null ? item.locType : item.loc_type;
             if (locType != null && locType !== '') {
               const normalizedType = this.normalizeLocTypeKey(locType);
-              if (normalizedType && !map.has(normalizedType)) { map.set(normalizedType, item); }
+              if (normalizedType && !map.has(normalizedType)) {
+                map.set(normalizedType, item);
+              }
             }
           });
           this.locListMap = map;
@@ -2427,21 +3152,35 @@
       });
     },
     showShelfTooltip(e, item) {
-      if (!item) { return; }
-      if (!this.isShelfTooltipAllowed()) { this.hideShelfTooltip(); return; }
-      if (!this.locListLoaded && !this.locListLoading) { this.loadLocList(); }
+      if (!item) {
+        return;
+      }
+      if (!this.isShelfTooltipAllowed()) {
+        this.hideShelfTooltip();
+        return;
+      }
+      if (!this.locListLoaded && !this.locListLoading) {
+        this.loadLocList();
+      }
       this.shelfTooltip.item = item;
       this.shelfTooltip.text = this.getShelfArrangeInfo(item);
       this.updateShelfTooltipPosition(e);
       this.shelfTooltip.visible = true;
     },
     updateShelfTooltipPosition(e) {
-      if (!e || !e.data || !e.data.global) { return; }
+      if (!e || !e.data || !e.data.global) {
+        return;
+      }
       this.updateShelfTooltipPositionByGlobal(e.data.global);
     },
     updateShelfTooltipPositionByGlobal(globalPos) {
-      if (!this.isShelfTooltipAllowed()) { this.hideShelfTooltip(); return; }
-      if (!globalPos) { return; }
+      if (!this.isShelfTooltipAllowed()) {
+        this.hideShelfTooltip();
+        return;
+      }
+      if (!globalPos) {
+        return;
+      }
       this.shelfTooltip.x = globalPos.x + 12;
       this.shelfTooltip.y = globalPos.y + 12;
     },
@@ -2453,7 +3192,9 @@
       return this.getStageAbsScale() >= this.shelfTooltipMinScale;
     },
     getStageAbsScale() {
-      if (!this.pixiApp || !this.pixiApp.stage) { return 1; }
+      if (!this.pixiApp || !this.pixiApp.stage) {
+        return 1;
+      }
       return Math.abs(this.pixiApp.stage.scale.x || 1);
     },
     updateShelfTooltipVisibilityByScale() {
@@ -2463,37 +3204,57 @@
       }
     },
     getShelfArrangeInfo(item) {
-      const parts = [];
-      const matchKey = this.getShelfMatchKey(item);
-      if (matchKey != null) { parts.push('鍧愭爣:' + matchKey); }
-      const locInfo = (matchKey != null) ? this.locListMap.get(matchKey) : null;
-      if (locInfo) {
-        const locNo = locInfo.locNo != null ? locInfo.locNo : locInfo.loc_no;
-        const displayLocNo = this.stripLocLayer(locNo);
-        if (displayLocNo != null) { parts.push('鎺掑垪:' + displayLocNo); }
-      }
-      return parts.join('  ');
+      return item.value;
+      // const parts = [];
+      // const matchKey = this.getShelfMatchKey(item);
+      // if (matchKey != null) {
+      //   parts.push('鍧愭爣:' + matchKey);
+      // }
+      // const locInfo = matchKey != null ? this.locListMap.get(matchKey) : null;
+      // if (locInfo) {
+      //   const locNo = locInfo.locNo != null ? locInfo.locNo : locInfo.loc_no;
+      //   const displayLocNo = this.stripLocLayer(locNo);
+      //   if (displayLocNo != null) {
+      //     parts.push('鎺掑垪:' + displayLocNo);
+      //   }
+      // }
+      // return parts.join('  ');
     },
     getShelfMatchKey(item) {
-      if (!item) { return null; }
-      const direct = item.locType != null ? item.locType : (item.loc_type != null ? item.loc_type : null);
+      if (!item) {
+        return null;
+      }
+      const direct =
+        item.locType != null ? item.locType : item.loc_type != null ? item.loc_type : null;
       const directKey = this.normalizeLocTypeKey(direct);
-      if (directKey) { return directKey; }
+      if (directKey) {
+        return directKey;
+      }
       const rowIndex = item.rowIndex;
       const colIndex = item.colIndex;
-      if (rowIndex == null || colIndex == null) { return null; }
+      if (rowIndex == null || colIndex == null) {
+        return null;
+      }
       const key0 = rowIndex + '-' + colIndex;
       if (this.locListLoaded && this.locListMap && this.locListMap.size > 0) {
-        if (this.locListMap.has(key0)) { return key0; }
+        if (this.locListMap.has(key0)) {
+          return key0;
+        }
       }
       return null;
     },
     stripLocLayer(locNo) {
-      if (locNo == null) { return null; }
+      if (locNo == null) {
+        return null;
+      }
       const str = String(locNo).trim();
-      if (!str) { return null; }
-      const parts = str.split('-').filter(p => p !== '');
-      if (parts.length >= 3) { return parts.slice(0, parts.length - 1).join('-'); }
+      if (!str) {
+        return null;
+      }
+      const parts = str.split('-').filter((p) => p !== '');
+      if (parts.length >= 3) {
+        return parts.slice(0, parts.length - 1).join('-');
+      }
       return str;
     },
     shelfTooltipStyle() {
@@ -2519,63 +3280,51 @@
       const vh = viewport.height;
       const margin = 50;
       const mirrorSign = this.mapMirrorX ? -1 : 1;
-      const inverseRotation = -((this.mapRotation % 360) * Math.PI / 180);
+      const inverseRotation = -(((this.mapRotation % 360) * Math.PI) / 180);
       const tmpPoint = new PIXI.Point();
-      this.pixiStaMap && this.pixiStaMap.forEach((sprite) => {
-        const textObj = sprite && sprite.textObj;
-        if (!textObj) { return; }
-        const base = (textObj.style && textObj.style.fontSize) ? textObj.style.fontSize : 10;
-        let scale = minPx / (base * s);
-        if (!isFinite(scale)) { scale = 1; }
-        scale = Math.max(0.8, Math.min(scale, 3));
-        textObj.scale.set(scale * mirrorSign, scale);
-        textObj.rotation = inverseRotation;
-        textObj.position.set(sprite.width / 2, sprite.height / 2);
-        sprite.getGlobalPosition(tmpPoint);
-        const on = tmpPoint.x >= -margin && tmpPoint.y >= -margin && tmpPoint.x <= vw + margin && tmpPoint.y <= vh + margin;
-        textObj.visible = (s >= 0.25) && on;
-      });
-      this.pixiCrnMap && this.pixiCrnMap.forEach((sprite) => {
-        const textObj = sprite && sprite.textObj;
-        if (!textObj) { return; }
-        const base = (textObj.style && textObj.style.fontSize) ? textObj.style.fontSize : 12;
-        let scale = minPx / (base * s);
-        if (!isFinite(scale)) { scale = 1; }
-        scale = Math.max(0.8, Math.min(scale, 3));
-        textObj.scale.set(scale * mirrorSign, scale);
-        textObj.rotation = inverseRotation;
-        textObj.position.set(sprite.width / 2, sprite.height / 2);
-        sprite.getGlobalPosition(tmpPoint);
-        const on = tmpPoint.x >= -margin && tmpPoint.y >= -margin && tmpPoint.x <= vw + margin && tmpPoint.y <= vh + margin;
-        textObj.visible = (s >= 0.25) && on;
-      });
-      this.pixiDualCrnMap && this.pixiDualCrnMap.forEach((sprite) => {
-        const textObj = sprite && sprite.textObj;
-        if (!textObj) { return; }
-        const base = (textObj.style && textObj.style.fontSize) ? textObj.style.fontSize : 12;
-        let scale = minPx / (base * s);
-        if (!isFinite(scale)) { scale = 1; }
-        scale = Math.max(0.8, Math.min(scale, 3));
-        textObj.scale.set(scale * mirrorSign, scale);
-        textObj.rotation = inverseRotation;
-        textObj.position.set(sprite.width / 2, sprite.height / 2);
-        sprite.getGlobalPosition(tmpPoint);
-        const on = tmpPoint.x >= -margin && tmpPoint.y >= -margin && tmpPoint.x <= vw + margin && tmpPoint.y <= vh + margin;
-        textObj.visible = (s >= 0.25) && on;
-      });
-      this.pixiRgvMap && this.pixiRgvMap.forEach((sprite) => {
-        const textObj = sprite && sprite.textObj;
-        if (!textObj) { return; }
-        const base = (textObj.style && textObj.style.fontSize) ? textObj.style.fontSize : 12;
-        let scale = minPx / (base * s);
-        if (!isFinite(scale)) { scale = 1; }
-        scale = Math.max(0.8, Math.min(scale, 3));
-        textObj.scale.set(scale * mirrorSign, scale);
-        textObj.rotation = inverseRotation;
-        textObj.position.set(sprite.width / 2, sprite.height / 2);
-        sprite.getGlobalPosition(tmpPoint);
-        const on = tmpPoint.x >= -margin && tmpPoint.y >= -margin && tmpPoint.x <= vw + margin && tmpPoint.y <= vh + margin;
-        textObj.visible = (s >= 0.25) && on;
+      this.pixiStaMap &&
+        this.pixiStaMap.forEach((sprite) => {
+          const textObj = sprite && sprite.textObj;
+          if (!textObj) {
+            return;
+          }
+          const base = textObj.style && textObj.style.fontSize ? textObj.style.fontSize : 10;
+          let scale = minPx / (base * s);
+          if (!isFinite(scale)) {
+            scale = 1;
+          }
+          scale = Math.max(0.8, Math.min(scale, 3));
+          textObj.scale.set(mirrorSign, 1);
+          textObj.rotation = inverseRotation;
+          textObj.position.set(sprite.width / 2, sprite.height / 2);
+          sprite.getGlobalPosition(tmpPoint);
+          const on =
+            tmpPoint.x >= -margin &&
+            tmpPoint.y >= -margin &&
+            tmpPoint.x <= vw + margin &&
+            tmpPoint.y <= vh + margin;
+          textObj.visible = s >= 0.25 && on;
+        });
+      [this.pixiCrnMap, this.pixiDualCrnMap, this.pixiRgvMap].forEach((deviceMap) => {
+        deviceMap &&
+          deviceMap.forEach((sprite) => {
+            const textObj = sprite && sprite.textObj;
+            if (!textObj) {
+              return;
+            }
+            this.applyEditorLikeTrackDeviceTextStyle(textObj);
+            textObj.anchor.set(0.5);
+            textObj.rotation = 0;
+            textObj.scale.set(mirrorSign, 1);
+            textObj.position.set(sprite.width / 2, sprite.height / 2);
+            sprite.getGlobalPosition(tmpPoint);
+            const on =
+              tmpPoint.x >= -margin &&
+              tmpPoint.y >= -margin &&
+              tmpPoint.x <= vw + margin &&
+              tmpPoint.y <= vh + margin;
+            textObj.visible = s >= 0.25 && on;
+          });
       });
     },
     rotateMap() {
@@ -2598,8 +3347,11 @@
       this.saveMapTransformConfig();
     },
     openStationColorConfigPage() {
-      if (typeof window === 'undefined') { return; }
-      const url = (typeof baseUrl !== 'undefined' ? baseUrl : '') + '/views/watch/stationColorConfig.html';
+      if (typeof window === 'undefined') {
+        return;
+      }
+      const url =
+        (typeof baseUrl !== 'undefined' ? baseUrl : '') + '/views/watch/stationColorConfig.html';
       const layerInstance = (window.top && window.top.layer) || window.layer;
       if (layerInstance && typeof layerInstance.open === 'function') {
         layerInstance.open({
@@ -2616,13 +3368,19 @@
     },
     parseRotation(value) {
       const num = parseInt(value, 10);
-      if (!isFinite(num)) { return 0; }
+      if (!isFinite(num)) {
+        return 0;
+      }
       const rot = ((num % 360) + 360) % 360;
-      return (rot === 90 || rot === 180 || rot === 270) ? rot : 0;
+      return rot === 90 || rot === 180 || rot === 270 ? rot : 0;
     },
     parseMirror(value) {
-      if (value === true || value === false) { return value; }
-      if (value == null) { return false; }
+      if (value === true || value === false) {
+        return value;
+      }
+      if (value == null) {
+        return false;
+      }
       const str = String(value).toLowerCase();
       return str === '1' || str === 'true' || str === 'y';
     },
@@ -2632,11 +3390,12 @@
         'site-auto-run': 0xfa51f6,
         'site-auto-id': 0xc4c400,
         'site-auto-run-id': 0x30bffc,
-        'site-enable-in': 0xA81DEE,
+        'site-enable-in': 0xa81dee,
         'site-unauto': 0xb8b8b8,
         'machine-pakin': 0x30bffc,
         'machine-pakout': 0x97b400,
-        'site-run-block': 0xe69138
+        'site-run-block': 0xe69138,
+        'site-error': 0xDB2828
       };
     },
     parseColorConfigValue(value, fallback) {
@@ -2644,13 +3403,25 @@
         return value;
       }
       const str = String(value == null ? '' : value).trim();
-      if (!str) { return fallback; }
-      if (/^#[0-9a-fA-F]{6}$/.test(str)) { return parseInt(str.slice(1), 16); }
+      if (!str) {
+        return fallback;
+      }
+      if (/^#[0-9a-fA-F]{6}$/.test(str)) {
+        return parseInt(str.slice(1), 16);
+      }
       if (/^#[0-9a-fA-F]{3}$/.test(str)) {
-        const expanded = str.charAt(1) + str.charAt(1) + str.charAt(2) + str.charAt(2) + str.charAt(3) + str.charAt(3);
+        const expanded =
+          str.charAt(1) +
+          str.charAt(1) +
+          str.charAt(2) +
+          str.charAt(2) +
+          str.charAt(3) +
+          str.charAt(3);
         return parseInt(expanded, 16);
       }
-      if (/^0x[0-9a-fA-F]{6}$/i.test(str)) { return parseInt(str.slice(2), 16); }
+      if (/^0x[0-9a-fA-F]{6}$/i.test(str)) {
+        return parseInt(str.slice(2), 16);
+      }
       if (/^[0-9]+$/.test(str)) {
         const num = parseInt(str, 10);
         return isNaN(num) ? fallback : num;
@@ -2658,15 +3429,19 @@
       return fallback;
     },
     loadStationColorConfig() {
-      if (!window.$ || typeof baseUrl === 'undefined') { return; }
+      if (!window.$ || typeof baseUrl === 'undefined') {
+        return;
+      }
       $.ajax({
-        url: baseUrl + "/watch/stationColor/config/auth",
-        headers: { 'token': localStorage.getItem('token') },
+        url: baseUrl + '/watch/stationColor/config/auth',
+        headers: { token: localStorage.getItem('token') },
         dataType: 'json',
         method: 'GET',
         success: (res) => {
           if (!res || res.code !== 200 || !res.data) {
-            if (res && res.code === 403) { parent.location.href = baseUrl + "/login"; }
+            if (res && res.code === 403) {
+              parent.location.href = baseUrl + '/login';
+            }
             return;
           }
           this.applyStationColorConfigPayload(res.data);
@@ -2678,7 +3453,9 @@
       const nextColors = Object.assign({}, defaults);
       const items = Array.isArray(data.items) ? data.items : [];
       items.forEach((item) => {
-        if (!item || !item.status || defaults[item.status] == null) { return; }
+        if (!item || !item.status || defaults[item.status] == null) {
+          return;
+        }
         nextColors[item.status] = this.parseColorConfigValue(item.color, defaults[item.status]);
       });
       this.stationStatusColors = nextColors;
@@ -2708,31 +3485,44 @@
       return createList;
     },
     createMapConfigs(createList) {
-      if (!window.$ || typeof baseUrl === 'undefined' || !Array.isArray(createList) || createList.length === 0) { return; }
+      if (
+        !window.$ ||
+        typeof baseUrl === 'undefined' ||
+        !Array.isArray(createList) ||
+        createList.length === 0
+      ) {
+        return;
+      }
       createList.forEach((cfg) => {
         $.ajax({
-          url: baseUrl + "/config/add/auth",
-          headers: { 'token': localStorage.getItem('token') },
+          url: baseUrl + '/config/add/auth',
+          headers: { token: localStorage.getItem('token') },
           method: 'POST',
           data: cfg
         });
       });
     },
     loadMapTransformConfig() {
-      if (!window.$ || typeof baseUrl === 'undefined') { return; }
+      if (!window.$ || typeof baseUrl === 'undefined') {
+        return;
+      }
       $.ajax({
-        url: baseUrl + "/config/listAll/auth",
-        headers: { 'token': localStorage.getItem('token') },
+        url: baseUrl + '/config/listAll/auth',
+        headers: { token: localStorage.getItem('token') },
         dataType: 'json',
         method: 'GET',
         success: (res) => {
           if (!res || res.code !== 200 || !Array.isArray(res.data)) {
-            if (res && res.code === 403) { parent.location.href = baseUrl + "/login"; }
+            if (res && res.code === 403) {
+              parent.location.href = baseUrl + '/login';
+            }
             return;
           }
           const byCode = {};
           res.data.forEach((item) => {
-            if (item && item.code) { byCode[item.code] = item; }
+            if (item && item.code) {
+              byCode[item.code] = item;
+            }
           });
           const rotateCfg = byCode[this.mapConfigCodes.rotate];
           const mirrorCfg = byCode[this.mapConfigCodes.mirror];
@@ -2750,14 +3540,22 @@
       });
     },
     saveMapTransformConfig() {
-      if (!window.$ || typeof baseUrl === 'undefined') { return; }
+      if (!window.$ || typeof baseUrl === 'undefined') {
+        return;
+      }
       const updateList = [
-        { code: this.mapConfigCodes.rotate, value: String(this.mapRotation || 0) },
-        { code: this.mapConfigCodes.mirror, value: this.mapMirrorX ? '1' : '0' }
+        {
+          code: this.mapConfigCodes.rotate,
+          value: String(this.mapRotation || 0)
+        },
+        {
+          code: this.mapConfigCodes.mirror,
+          value: this.mapMirrorX ? '1' : '0'
+        }
       ];
       $.ajax({
-        url: baseUrl + "/config/updateBatch",
-        headers: { 'token': localStorage.getItem('token') },
+        url: baseUrl + '/config/updateBatch',
+        headers: { token: localStorage.getItem('token') },
         data: JSON.stringify(updateList),
         dataType: 'json',
         contentType: 'application/json;charset=UTF-8',
@@ -2773,11 +3571,15 @@
       return { width: swap ? h : w, height: swap ? w : h };
     },
     fitStageToContent() {
-      if (!this.pixiApp || !this.mapContentSize) { return; }
+      if (!this.pixiApp || !this.mapContentSize) {
+        return;
+      }
       const size = this.getTransformedContentSize();
       const contentW = size.width || 0;
       const contentH = size.height || 0;
-      if (contentW <= 0 || contentH <= 0) { return; }
+      if (contentW <= 0 || contentH <= 0) {
+        return;
+      }
       const viewport = this.getViewportSize();
       const vw = viewport.width;
       const vh = viewport.height;
@@ -2785,7 +3587,9 @@
       const availableW = Math.max(1, vw - padding.left - padding.right);
       const availableH = Math.max(1, vh - padding.top - padding.bottom);
       let scale = Math.min(availableW / contentW, availableH / contentH) * 0.95;
-      if (!isFinite(scale) || scale <= 0) { scale = 1; }
+      if (!isFinite(scale) || scale <= 0) {
+        scale = 1;
+      }
       const baseW = this.mapContentSize.width || contentW;
       const baseH = this.mapContentSize.height || contentH;
       const mirrorX = this.mapMirrorX ? -1 : 1;
@@ -2798,41 +3602,355 @@
       this.pixiApp.stage.setTransform(posX, posY, scaleX, scaleY, 0, 0, 0, 0, 0);
     },
     applyMapTransform(fitToView) {
-      if (!this.mapRoot || !this.mapContentSize) { return; }
+      if (!this.mapRoot || !this.mapContentSize) {
+        return;
+      }
       const contentW = this.mapContentSize.width || 0;
       const contentH = this.mapContentSize.height || 0;
-      if (contentW <= 0 || contentH <= 0) { return; }
+      if (contentW <= 0 || contentH <= 0) {
+        return;
+      }
       this.mapRoot.pivot.set(contentW / 2, contentH / 2);
       this.mapRoot.position.set(contentW / 2, contentH / 2);
-      this.mapRoot.rotation = (this.mapRotation % 360) * Math.PI / 180;
+      this.mapRoot.rotation = ((this.mapRotation % 360) * Math.PI) / 180;
       this.mapRoot.scale.set(1, 1);
-      if (fitToView) { this.fitStageToContent(); }
+      if (fitToView) {
+        this.fitStageToContent();
+      }
       this.scheduleAdjustLabels();
       this.scheduleShelfChunkCulling();
     },
     scheduleAdjustLabels() {
-      if (this.adjustLabelTimer) { clearTimeout(this.adjustLabelTimer); }
+      if (this.adjustLabelTimer) {
+        clearTimeout(this.adjustLabelTimer);
+      }
       this.adjustLabelTimer = setTimeout(() => {
         this.adjustLabelScale();
         this.updateShelfTooltipVisibilityByScale();
         this.adjustLabelTimer = null;
       }, 20);
+    },
+    // 鍒ゆ柇鏈夋病鏈夌Щ鍔ㄨ繃澶�
+    isMoveFinish(point, path, point2, path2) {
+      if (path.prev === path2 || path.prev?.prev === path2) {
+        return true;
+      }
+      if (path2 === path) {
+        if (Math.abs(path.x - path2.x) < EPSILON && Math.abs(path.y - path2.y) < EPSILON) {
+          return true;
+        }
+        const vectorPoint = G.normalizeVector(point, point2);
+        let vectorPath;
+        if (path.type === 'arc') {
+          vectorPath = G.normalizeVector(
+            { x: path.arcStartX, y: path.arcStartY },
+            { x: path.arcEndX, y: path.arcEndY }
+          );
+        } else {
+          vectorPath = G.normalizeVector({ x: path.startX, y: path.startY }, path);
+        }
+        return (
+          Math.sign(vectorPath.y) === -Math.sign(vectorPoint.y) ||
+          Math.sign(vectorPath.x) === -Math.sign(vectorPoint.x)
+        );
+      }
+      return false;
+    },
+    isPointOnSegment(point, A, B) {
+      // 璁$畻鍙夌Н锛屽垽鏂偣鏄惁鍦ㄧ洿绾緼B涓�
+      const crossProduct = (point.x - A.x) * (B.y - A.y) - (point.y - A.y) * (B.x - A.x);
+      // 涓嶅湪鐩寸嚎涓婏紝鍒欒偗瀹氫笉鍦ㄧ嚎娈典笂
+      if (Math.abs(crossProduct) > EPSILON) {
+        return false;
+      }
+      // 妫�鏌ョ偣鏄惁鍦ㄧ嚎娈礎鍜孊鐨勫潗鏍囪寖鍥村唴
+      const minX = Math.min(A.x, B.x);
+      const maxX = Math.max(A.x, B.x);
+      const minY = Math.min(A.y, B.y);
+      const maxY = Math.max(A.y, B.y);
+      // 浣跨敤瀹瑰樊纭繚鍖呮嫭绔偣
+      return (
+        point.x >= minX - EPSILON &&
+        point.x < maxX + EPSILON &&
+        point.y >= minY - EPSILON &&
+        point.y < maxY + EPSILON
+      );
+    },
+    getCurveDistance(p1, p2, path1, path2, p1Angle, p2Angle, distance = 0) {
+      // console.log('getCurveDistance', p1, p2, path1, path2, p1Angle, p2Angle, distance);
+      if (path1 === path2) {
+        if (path1.type === 'line') {
+          return G.calcDistance(p1, p2) + distance;
+        } else {
+          return Math.abs(((p2Angle - p1Angle) % (2 * Math.PI)) * path1.radius) + distance;
+        }
+      }
+      if (path1.type === 'line') {
+        const restDistance = G.calcDistance(p1, path1);
+        return this.getCurveDistance(
+          path1,
+          p2,
+          path1.next,
+          path2,
+          path2.startAngle,
+          p2Angle,
+          restDistance + distance
+        );
+      } else {
+        const startAngle = path1.startAngle;
+        const endAngle = path1.endAngle;
+        let tmpCurrentAngle = p1Angle || startAngle;
+        const currentAngle = this.getNormalizeAngle(tmpCurrentAngle, startAngle, endAngle);
+        const restDistance = Math.abs((endAngle - currentAngle) * path1.radius);
+        // console.log('鍦嗗姬' + path1.index, newStartAngle, newEndAngle, newCurrentAngle, restDistance)
+        return this.getCurveDistance(
+          { x: p1.arcEndX, y: p1.arcEndY },
+          p2,
+          path1.next,
+          path2,
+          endAngle,
+          p2Angle,
+          restDistance + distance
+        );
+      }
+    },
+    getNormalizeAngle(angle, startAngle, endAngle) {
+      if (angle < startAngle && angle < endAngle) {
+        return angle + 2 * Math.PI;
+      }
+      return angle;
+    },
+    pointToSegment(point, segStart, segEnd) {
+      // 瑙f瀯鍧愭爣
+      const { x: px, y: py } = point;
+      const { x: ax, y: ay } = segStart;
+      const { x: bx, y: by } = segEnd;
+
+      // 鍚戦噺 AB 鍜� AP
+      const abx = bx - ax;
+      const aby = by - ay;
+      const apx = px - ax;
+      const apy = py - ay;
+
+      // 绾挎闀垮害鐨勫钩鏂�
+      const abLenSq = abx * abx + aby * aby;
+
+      // 绾挎閫�鍖栦负鐐� A (鎴� B)
+      if (abLenSq === 0) {
+        return Math.hypot(apx, apy);
+      }
+
+      // 璁$畻鎶曞奖鍙傛暟 t锛屽苟閽冲埗鍒� [0, 1]
+      let t = (apx * abx + apy * aby) / abLenSq;
+      t = Math.max(0, Math.min(1, t));
+
+      // 绾挎涓婄鐐� P 鏈�杩戠殑鐐瑰潗鏍�
+      const closestX = ax + t * abx;
+      const closestY = ay + t * aby;
+
+      return { x: closestX, y: closestY, t };
+    },
+    getMappingInfo(sprite) {
+      if (!sprite) {
+        return false;
+      }
+      let minDistance = Infinity;
+      let minPath;
+      let x, y, angle;
+
+      sprite.trackInfo.pathList.forEach((path, index) => {
+        if (path.type === 'line') {
+          // 姹傜嚎娈靛埌鏌愮偣鐨勮窛绂�
+          const pointInSegment = this.pointToSegment(
+            sprite,
+            { x: path.startX, y: path.startY },
+            path
+          );
+          const distance = G.calcDistance(pointInSegment, sprite);
+          if (distance < minDistance) {
+            minDistance = distance;
+            minPath = path;
+            x = pointInSegment.x;
+            y = pointInSegment.y;
+          }
+        } else {
+          const toCenter = G.calcDistance(path, sprite);
+          const distance = Math.abs(path.radius - toCenter);
+          if (distance < minDistance) {
+            const vector = G.normalizeVector(path, sprite);
+            minDistance = distance;
+            minPath = path;
+            x = path.x + vector.x * path.radius;
+            y = path.y + vector.y * path.radius;
+            angle = Math.atan2(vector.y, vector.x);
+          }
+        }
+      });
+
+      if (sprite.trackInfo.type === 'annulus' && minPath && x != null && y != null) {
+        const c = G.centerAnnulusBandPoint(sprite.trackInfo, x, y, minPath);
+        x = c.x;
+        y = c.y;
+      }
+
+      return { x, y, angle, path: minPath };
+    },
+    distanceBasedEasingSigmoid(remaining, threshold = 1, steepness = 10, maxSpeedChange = 0.3) {
+      // 姝や箖缂撳姩鍑芥暟锛屼娇鐢⊿igmoid鍑芥暟浣滀负鏍稿績锛歠(x) = 1 / (1 + e^(-k*(x - threshold)))
+      // remaining: 杈撳叆鍊�
+      // threshold: 鍑芥暟涓績鐐癸紝杈撳嚭涓�1
+      // steepness: 鏇茬嚎闄″抄搴︼紝鎺у埗杩囨浮鍖虹殑瀹藉害
+      const exponent = -steepness * (remaining - threshold);
+      const sigmoid = 1 / (1 + Math.exp(exponent));
+
+      // 灏哠igmoid杈撳嚭浠嶽0,1]鏄犲皠鍒伴�熷害鑼冨洿锛屼緥濡俒0.7, 1.3]
+      const minSpeed = 1 - maxSpeedChange;
+      const maxSpeed = 1 + maxSpeedChange;
+      return minSpeed + (maxSpeed - minSpeed) * sigmoid;
+    },
+    // Poll
+    startAnnulusDevicePoll() {
+      if (!this.annulusPoller) {
+        this.annulusPoller = new Poller({
+          periodMs: 1000,
+          alpha: 0.2,
+          fetchFn: (poller) => this.getAnnulusDeviceInfo(poller)
+        });
+      }
+      this.annulusPoller.start();
+    },
+    stopAnnulusDevicePoll() {
+      if (this.annulusPoller) {
+        this.annulusPoller.stop();
+      }
+    },
+    async getAnnulusDeviceInfo(poller) {
+      if (poller.abortController) {
+        try {
+          poller.abortController.abort();
+        } catch (e) {}
+      }
+      poller.abortController = new AbortController();
+
+      const res = await fetch('http://127.0.0.1:9091/rs-car/rgv/ring/through/rgv/position/data', {
+        method: 'POST',
+        signal: poller.abortController.signal
+      });
+      if (!res.ok) {
+        throw new Error(`getAnnulusDeviceInfo http ${res.status}`);
+      }
+      const json = await res.json();
+      this.setDeviceInfoByBarcode('rgv', json);
+    },
+    //todo锛� 娴嬭瘯浠g爜
+    async fakeMove(newList = [0, 10, 20, 30, 40, 50], newRaw = {}) {
+      const finalList = newList;
+      const rawByBarCode = {
+        index: 17,
+        modeColor: '#4169E1',
+        statusColor: '#27AE60',
+        rgvPos: 0,
+        rgvPosMax: ['el_1775520471475', 0, 100]
+      };
+      const createTimeout = () => {
+        const p1 = new Promise((res, rej) => {
+          setTimeout(() => {
+            res(1);
+          }, 1000);
+        });
+        return p1;
+      };
+      for await (const p of finalList) {
+        await createTimeout();
+        this.setDeviceInfoByBarcode(newRaw.type || 'rgv', [
+          { ...rawByBarCode, ...newRaw, rgvPos: p * 17370 }
+        ]);
+      }
+    },
+    createFakeButton() {
+      if (!this.mapRoot || typeof PIXI === 'undefined') {
+        return;
+      }
+      if (this.fakeOriginButton) {
+        try {
+          this.fakeOriginButton.destroy({ children: true });
+        } catch (e) {}
+        this.fakeOriginButton = null;
+      }
+      const bw = 76;
+      const bh = 30;
+      const bg = new PIXI.Graphics();
+      bg.lineStyle(1, 0xffffff, 0.65);
+      bg.beginFill(0x2563c7, 0.95);
+      bg.drawRect(0, 0, bw, bh);
+      bg.endFill();
+      const textStyle = new PIXI.TextStyle({
+        fontFamily: 'Arial',
+        fontSize: 12,
+        fill: '#ffffff',
+        align: 'center'
+      });
+      const label = new PIXI.Text('妯℃嫙杩愬姩', textStyle);
+      label.anchor.set(0.5);
+      label.position.set(bw / 2, bh / 2);
+      const btn = new PIXI.Container();
+      btn.addChild(bg);
+      btn.addChild(label);
+      btn.position.set(200, 200);
+      btn.interactive = true;
+      btn.buttonMode = true;
+      btn.hitArea = new PIXI.Rectangle(0, 0, bw, bh);
+      btn.on('pointertap', (e) => {
+        this.fakeMoveButtonClick();
+      });
+      this.mapRoot.addChild(btn);
+
+      btn.zIndex = 9999;
+    },
+    async fakeMoveButtonClick() {
+      const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
+      // 璁惧17
+      const list = [];
+      for (let i = 0; i <= 7; i++) {
+        list.push(i * 10);
+      }
+      const p1 = this.fakeMove(list);
+      const list2 = [];
+      p1.then(async () => {
+        await sleep(1000);
+        await this.fakeMove([70], { index: 17, statusColor: '#a5d6f7' });
+        await sleep(1000);
+        await this.fakeMove([80], { index: 17, statusColor: '#a5d6f7' });
+        await sleep(1000);
+        await this.fakeMove([80], { index: 17, statusColor: '#245a9a' });
+      }, 1000);
+
+      // 璁惧18
+      for (let i = 0; i < 7; i++) {
+        list2.push(i * 7);
+      }
+      list2.push(47.8);
+      const p2 = this.fakeMove(list2, { index: 18 });
+      p2.then(async () => {
+        await sleep(1000);
+        await this.fakeMove([47.8], { index: 18, statusColor: '#a5d6f7' });
+        await sleep(1000);
+        await this.fakeMove([52.8, 57.8, 62.8, 63.5], { index: 18, statusColor: '#a5d6f7' });
+        await sleep(1000);
+        await this.fakeMove([63.5], { index: 18, statusColor: '#245a9a' });
+      });
+
+      // 璁惧16
+      let list3 = [];
+      for (let i = 0; i <= 10; i++) {
+        list3.push(i * 10);
+      }
+      for (let i = 9; i >= 0; i--) {
+        list3.push(i * 10);
+      }
+      await this.fakeMove(list3, { index: 16, statusColor: '#a5d6f7', type: 'crn' });
+      await sleep(1000);
+      await this.fakeMove([0], { index: 16, statusColor: '#245a9a', type: 'crn' });
     }
   }
 });
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/main/webapp/components/MapCanvasBak.js b/src/main/webapp/components/MapCanvasBak.js
new file mode 100644
index 0000000..83eee99
--- /dev/null
+++ b/src/main/webapp/components/MapCanvasBak.js
@@ -0,0 +1,258 @@
+//MapCanvas.js 閲屽簾寮冪殑鍑芥暟锛屾殏鏃朵繚鐣欏湪杩欓噷锛屾柟渚垮悗缁鏈夐渶瑕佸啀鎭㈠
+
+// function getMappingInfo(point) {
+//   let angle, path, vector, x, y;
+//   this.allSmoothList.find((smoothList) => {
+//     const smoothLength = smoothList.length;
+//     const existPath = smoothList.find((currentPath, i) => {
+//       const prevPath = smoothList[(i - 1 + smoothLength) % smoothLength];
+//       if (currentPath.type === "line") {
+//         const prevPathEndPoint =
+//           prevPath.type === "arc"
+//             ? {
+//               x: prevPath.arcEndX,
+//               y: prevPath.arcEndY,
+//             }
+//             : prevPath;
+//         return this.isPointOnSegment(point, prevPathEndPoint, currentPath);
+//       } else {
+//         const start = {
+//           x: currentPath.arcStartX,
+//           y: currentPath.arcStartY,
+//         };
+//         const middle = {
+//           x: currentPath.arcMiddleX,
+//           y: currentPath.arcMiddleY,
+//         };
+//         const end = { x: currentPath.arcEndX, y: currentPath.arcEndY };
+//         return (
+//           this.isPointOnSegment(point, start, middle) ||
+//           this.isPointOnSegment(point, middle, end)
+//         );
+//       }
+//     });
+//     if (!existPath) {
+//       return false;
+//     }
+//     path = existPath;
+//     vector = this.normalizeVector(existPath, point);
+//     if (existPath.type === "arc") {
+//       x = existPath.x + vector.x * existPath.radius;
+//       y = existPath.y + vector.y * existPath.radius;
+//       angle = Math.atan2(vector.y, vector.x);
+//     } else {
+//       x = point.x;
+//       y = point.y;
+//       angle = null;
+//     }
+//     return true;
+//   });
+//   return {
+//     angle,
+//     path,
+//     x,
+//     y,
+//     vector,
+//   };
+// }
+
+// setDeviceInfo(type, res) {
+//   const deviceTypeInfo = this.DEVICE_MAP[type];
+//   let devices = Array.isArray(res) ? res : res && res.code === 200 ? res.data : null;
+//   if (!devices) {
+//     return;
+//   }
+//   for (var i = 0; i < devices.length; i++) {
+//     const device = devices[i];
+//     const id = parseInt(device[deviceTypeInfo.idName]);
+//     const sprite = deviceTypeInfo.pixiMap.get(id);
+//     if (!sprite) {
+//       continue;
+//     }
+//     // 鎵惧埌sprite浜嗗氨鏇存柊棰滆壊鍜屾枃瀛�
+//     const taskNo = device.taskNo;
+//     if (taskNo != null && taskNo > 0) {
+//       sprite.textObj.text = id + '(' + taskNo + ')';
+//     } else {
+//       sprite.textObj.text = String(id);
+//     }
+//     this.applyEditorLikeTrackDeviceTextStyle(sprite.textObj);
+//     const status = device[deviceTypeInfo.statusInfo.name];
+//     const statusColor = deviceTypeInfo.statusInfo.getStatus(status);
+//     deviceTypeInfo.statusInfo.updateTextureColor(sprite, statusColor);
+
+//     const oldSprite = {
+//       ...sprite,
+//       x: sprite.x,
+//       y: sprite.y,
+//       mappingInfo: { ...sprite.mappingInfo }
+//     };
+//     // 鍚庣浼犺繃鏉ョ殑鏄乏涓婅鐨勭偣锛岃繖閲屽彇涓績鐐�
+//     device.width = sprite.width;
+//     device.height = sprite.height;
+//     const sourcePoint = {
+//       x: device.x + device.width / 2,
+//       y: device.y + device.height / 2
+//     };
+//     const mappingInfo = this.getMappingInfo({ ...device, ...sourcePoint });
+//     sprite.mappingInfo = mappingInfo;
+//     sprite.time = Date.now();
+//     if (!oldSprite.time) {
+//       sprite.x = sprite.mappingInfo.x;
+//       sprite.y = sprite.mappingInfo.y;
+//       sprite.rotation = sprite.mappingInfo.rotate;
+//       sprite.path = sprite.mappingInfo.path;
+//       sprite.vector = sprite.mappingInfo.vector;
+//       continue;
+//     }
+//     // const curveDistance = this.getCurveDistance(
+//     //   oldSprite.mappingInfo,sprite.mappingInfo,
+//     //   oldSprite.mappingInfo.path,sprite.mappingInfo.path,
+//     //   oldSprite.mappingInfo.angle,sprite.mappingInfo.angle
+//     // );
+//     const curveDistance = G.calcDistance(oldSprite.mappingInfo, sprite.mappingInfo);
+//     const deltaTime = (sprite.time - oldSprite.time) / 1000 || 0;
+//     if (deltaTime > 0 && deltaTime <= 10 && curveDistance > 0) {
+//       // 淇濆瓨鏈甯ч棿闅旓紝渚� ticker 鍒ゆ柇鏁版嵁鏂伴矞搴�
+//       sprite.lastDeltaTime = deltaTime;
+//       // 鐢ㄦ寚鏁扮Щ鍔ㄥ钩鍧囷紙伪=0.35锛変唬鏇跨獥鍙e潎鍊硷紝瀵归�熷害鍙樺寲鍝嶅簲鏇村揩
+//       const v = curveDistance / deltaTime;
+//       const alpha = 0.35;
+//       sprite.maV =
+//         typeof sprite.maV === 'number' && isFinite(sprite.maV)
+//           ? sprite.maV * (1 - alpha) + v * alpha
+//           : v;
+//     }
+//     if (curveDistance < EPSILON) {
+//       // 宸叉棤鍓╀綑璺▼鎴栨柊鏁版嵁涓庡綋鍓嶇洰鏍囦竴鑷达細蹇呴』鏀跺熬骞剁Щ闄� ticker锛屽惁鍒欎細涓�鐩磋窇
+//       this.finishDeviceMotion(sprite);
+//     } else {
+//       sprite.isFinish = false;
+//       if (sprite.ticker) {
+//         // 宸叉湁鎻掑�煎洖璋冿細mappingInfo 宸插湪涓婃柟鏇存柊锛岀户缁拷鏂扮洰鏍囷紱鍕� return锛堜細涓柇鍏跺畠璁惧锛�
+//         continue;
+//       }
+//       const Gm = window.BasMapTrackGeometry;
+//       const trackElForMove =
+//         this.map2 && sprite.pathId != null
+//           ? this.map2.find((item) => item && item.id === sprite.pathId)
+//           : null;
+//       const pathListForMove =
+//         trackElForMove &&
+//         trackElForMove.pathList &&
+//         trackElForMove.pathList.length
+//           ? trackElForMove.pathList
+//           : null;
+//       if (!pathListForMove || !Gm) {
+//         this.finishDeviceMotion(sprite);
+//         continue;
+//       }
+//       const fn = () => {
+//         if (sprite.isFinish) {
+//           return;
+//         }
+//         const restDistance = G.calcDistance(sprite, sprite.mappingInfo);
+//         if (restDistance <= EPSILON) {
+//           this.finishDeviceMotion(sprite);
+//           return;
+//         }
+//         const dtMs =
+//           this.pixiApp && this.pixiApp.ticker && typeof this.pixiApp.ticker.deltaMS === 'number'
+//             ? this.pixiApp.ticker.deltaMS
+//             : 16.667;
+//         const dt = Math.max(0, dtMs) / 1000;
+//         if (dt <= 0) {
+//           return;
+//         }
+//         const baseV =
+//           typeof sprite.maV === 'number' && isFinite(sprite.maV) && sprite.maV > 0
+//             ? sprite.maV
+//             : Math.max(sprite.width * 2, 20);
+//         // 鏁版嵁鏂伴矞搴﹀垽鏂細
+//         // 鏈嶅姟鍣ㄥ抚闂撮殧鍐呮湁鏂版暟鎹埌鏉� 鈫� 鍏ㄩ�熻拷鐩爣锛堜笉鍑忛�燂紝閬垮厤涓嬩竴甯�"杩借刀鎶栧姩"锛�
+//         // 鏁版嵁鍋滄鎺ㄩ�� 鈫� 绾挎�у噺閫熸敹鏁涘埌缁堢偣
+//         const msSinceUpdate = sprite.time ? Date.now() - sprite.time : 0;
+//         const typicalIntervalMs = (sprite.lastDeltaTime || 1) * 1000;
+//         const isStale = msSinceUpdate > typicalIntervalMs * 1.5;
+//         const easing = isStale ? Math.min(1.0, restDistance / Math.max(sprite.width, 1)) : 1.0;
+//         const smoothDistance = Math.min(restDistance, dt * baseV * easing);
+//         const path = sprite.path;
+//         const movePoint =
+//           trackElForMove &&
+//           trackElForMove.type === 'annulus' &&
+//           Gm.snapToAnnulusOuterPath
+//             ? Gm.snapToAnnulusOuterPath(sprite.x, sprite.y, path)
+//             : sprite;
+//         const angle = Math.atan2(sprite.y - path.y, sprite.x - path.x);
+//         const p = Gm.getPositionAfterMove({
+//           point: movePoint,
+//           pathList: pathListForMove,
+//           path,
+//           deltaDistance: smoothDistance,
+//           angle
+//         });
+//         let px = p.x;
+//         let py = p.y;
+//         if (
+//           trackElForMove &&
+//           trackElForMove.type === 'annulus' &&
+//           Gm.centerAnnulusBandPoint
+//         ) {
+//           const c = Gm.centerAnnulusBandPoint(trackElForMove, p.x, p.y, p.path);
+//           px = c.x;
+//           py = c.y;
+//         }
+//         sprite.path = p.path;
+//         sprite.x = px;
+//         sprite.y = py;
+//         sprite.rotation = this.getRotate(p, p.path);
+//         const restDistanceAfter = G.calcDistance(
+//           { x: sprite.x, y: sprite.y },
+//           sprite.mappingInfo
+//         );
+//         if (restDistanceAfter <= EPSILON || smoothDistance >= restDistance) {
+//           this.finishDeviceMotion(sprite);
+//           return;
+//         }
+//       };
+//       sprite.ticker = fn;
+//       this.pixiApp.ticker.add(sprite.ticker);
+//     }
+//   }
+//   this.scheduleAdjustLabels();
+// },
+
+// const list = [{
+//   x: 853.92,
+//   y: 457
+// }, {
+//   x: 953.92,
+//   y: 457
+// }, {
+//   x: 1053.92,
+//   y: 457
+// }, {
+//   x: 1153.92,
+//   y: 457
+// }, {
+//   x: 1253.92,
+//   y: 457
+// }, {
+//   x: 1353.92,
+//   y: 457
+// }, {
+//   x: 1453.92,
+//   y: 457
+// }, {
+//   x: 1553.92,
+//   y: 457
+// }, {
+//   x: 1653.92,
+//   y: 457
+// }];
+// const raw = {
+//   crnStatus: 'machine-auto',
+//   offset: 1,
+//   crnId: 15,
+//   pathId: 'el_1775197504724'
+// };
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 = '';
+      }
+    }
+  });
 })();
diff --git a/src/main/webapp/static/js/basMap/mapTrackGeometry.js b/src/main/webapp/static/js/basMap/mapTrackGeometry.js
new file mode 100644
index 0000000..8d31dc3
--- /dev/null
+++ b/src/main/webapp/static/js/basMap/mapTrackGeometry.js
@@ -0,0 +1,1195 @@
+/**
+ * 鐜┛ / 骞虫粦杞ㄩ亾鍑犱綍锛氱洿瑙掑杈瑰舰杞渾寮ц矾寰勩�丳IXI 缁樺埗銆佽澶囨湞鍚戠瓑銆�
+ * 渚� basMap 缂栬緫鍣ㄤ笌鐩戞帶 MapCanvas 鍏辩敤銆傞渶鍦ㄩ〉闈腑鍏堜簬 editor.js / MapCanvas.js 寮曞叆銆�
+ * 璁惧澶栬锛圕RN / RGV锛夌粯鍒朵笌 MapCanvas 璐村浘涓�鑷达紝瑙� drawCrnDeviceGraphics / drawRgvDeviceGraphics銆�
+ */
+(function (global) {
+  'use strict';
+
+  var TYPE_META = {
+    shelf: {
+      label: '璐ф灦',
+      shortLabel: 'SHELF',
+      fill: 0x7d96bf,
+      border: 0x4f6486
+    },
+    repairHub: {
+      label: '缁翠慨绔欏彴',
+      shortLabel: 'HUB',
+      fill: 0x8eb89a,
+      border: 0x4a6b55
+    },
+    devp: {
+      label: '杈撻�佺嚎',
+      shortLabel: 'DEVP',
+      fill: 0xf0b06f,
+      border: 0xa45f21
+    },
+    crn: {
+      label: '鍫嗗灈鏈鸿建閬�',
+      shortLabel: 'CRN',
+      fill: 0x68bfd0,
+      border: 0x1d6e81,
+      alpha: 0.12
+    },
+    dualCrn: {
+      label: '鍙屽伐浣嶈建閬�',
+      shortLabel: 'DCRN',
+      fill: 0x54c1a4,
+      border: 0x0f7b62,
+      alpha: 0.12
+    },
+    rgv: {
+      label: 'RGV杞ㄩ亾',
+      shortLabel: 'RGV',
+      fill: 0xc691e9,
+      border: 0x744b98,
+      alpha: 0.12
+    },
+    annulus: {
+      label: '鐜┛',
+      shortLabel: 'ANNULUS',
+      fill: 0xe6b3b3,
+      border: 0xcc6666,
+      alpha: 0
+    }
+  };
+
+  function normalizeVector(p1, p2) {
+    var dx = p2.x - p1.x;
+    var dy = p2.y - p1.y;
+    var length = Math.sqrt(dx * dx + dy * dy);
+    return { x: dx / length, y: dy / length };
+  }
+
+  function calcDistance(p1, p2) {
+    return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
+  }
+
+  function safeParseJson(text) {
+    if (!text || typeof text !== 'string') {
+      return null;
+    }
+    try {
+      return JSON.parse(text);
+    } catch (e) {
+      return null;
+    }
+  }
+
+  /** @param {string|object|null|undefined} raw */
+  function parseDeviceFormValue(raw) {
+    if (!raw) {
+      return null;
+    }
+    if (typeof raw === 'object') {
+      return raw;
+    }
+    return safeParseJson(raw);
+  }
+
+  /**
+   * 璁惧闀垮锛氭湁 `deviceLength`/`deviceWidth` 鍒欑敤鍍忕礌鍊硷紝鍚﹀垯鐢ㄨ建閬撹嚜鍔ㄥ昂瀵搞��
+   * @param {object|null|undefined} item
+   * @param {number} fallbackAlong
+   * @param {number} fallbackAcross
+   * @returns {{ along: number, across: number }}
+   */
+  function normalizeDeviceSizeOverride(item, fallbackAlong, fallbackAcross) {
+    if (!item) {
+      return { along: fallbackAlong, across: fallbackAcross };
+    }
+    var overrideAlong = Math.round(Number(item.deviceLength));
+    var overrideAcross = Math.round(Number(item.deviceWidth));
+    var along = isFinite(overrideAlong) && overrideAlong > 0 ? overrideAlong : fallbackAlong;
+    var across = isFinite(overrideAcross) && overrideAcross > 0 ? overrideAcross : fallbackAcross;
+    return {
+      along: Math.max(2, along),
+      across: Math.max(2, across)
+    };
+  }
+
+  function getNormalizeAngle(angle, startAngle, endAngle) {
+    // 灏� angle 褰掍竴鍖栧埌 [startAngle, startAngle + 2*PI) 鑼冨洿
+    var twoPi = 2 * Math.PI;
+    var rangeStart = startAngle;
+
+    var normalized = angle % twoPi;
+    if (normalized < 0) {
+      normalized += twoPi;
+    }
+
+    var normalizedStart = rangeStart % twoPi;
+    if (normalizedStart < 0) {
+      normalizedStart += twoPi;
+    }
+
+    // 璁$畻鐩稿鍋忕Щ
+    var offset = normalized - normalizedStart;
+    if (offset < 0) {
+      offset += twoPi;
+    }
+    return rangeStart + offset;
+  }
+
+  function getPositionAfterMove(params) {
+    var point = params.point;
+    var pathList = params.pathList;
+    var deltaDistance = params.deltaDistance;
+    var angle = params.angle;
+    var path, pathIndex;
+    if (params.path) {
+      path = params.path;
+      pathIndex = pathList.indexOf(path);
+    } else {
+      pathIndex = params.pathIndex;
+      path = pathList[pathIndex];
+    }
+    var nextIndex = (pathIndex + 1) % pathList.length;
+    var prevIndex = (pathIndex - 1 + pathList.length) % pathList.length;
+    var isBackward = deltaDistance < 0;
+    var moveDistance = Math.abs(deltaDistance);
+    if (moveDistance === 0) {
+      return {
+        x: point.x,
+        y: point.y,
+        path: path,
+        angle: angle
+      };
+    }
+
+    if (path.type === 'line') {
+      var targetPoint = isBackward ? { x: path.startX, y: path.startY } : { x: path.x, y: path.y };
+      var restDistance = calcDistance(point, targetPoint);
+      var vector = normalizeVector(point, targetPoint);
+      if (moveDistance < restDistance) {
+        // vector 鎸囧悜鏈鐩爣绔紙姝e悜涓虹粓鐐癸紝鍙嶅悜涓鸿捣鐐癸級銆傚弽鍚戞椂 deltaDistance<0锛�
+        // 蹇呴』鐢� +moveDistance 娌� vector 璧帮紝涓嶈兘鐢� vector*deltaDistance锛堣礋鍙蜂細鎶婁綅绉绘姌鍚戝彟涓�绔級銆�
+        var stepAlong = isBackward ? moveDistance : deltaDistance;
+        var x = point.x + vector.x * stepAlong;
+        var y = point.y + vector.y * stepAlong;
+        return { x: x, y: y, path: path };
+      }
+      // pathList绛変簬1璇存槑灏�1鏉¤建閬擄紝鐩存帴杩斿洖瀵瑰簲绔偣
+      if (pathList.length === 1) {
+        return {
+          x: targetPoint.x,
+          y: targetPoint.y,
+          path: path
+        };
+      }
+      return getPositionAfterMove({
+        point: targetPoint,
+        pathList: pathList,
+        pathIndex: isBackward ? prevIndex : nextIndex,
+        deltaDistance: isBackward ? deltaDistance + restDistance : deltaDistance - restDistance
+      });
+    }
+
+    var startAngle = path.startAngle;
+    var endAngle = path.endAngle;
+    var inferredAngle = Math.atan2(point.y - path.y, point.x - path.x);
+    var tmpCurrentAngle = angle != null ? angle : inferredAngle;
+    var currentAngle = getNormalizeAngle(tmpCurrentAngle, startAngle, endAngle);
+    var restDistance2 = isBackward
+      ? Math.abs((currentAngle - startAngle) * path.radius)
+      : Math.abs((endAngle - currentAngle) * path.radius);
+    if (moveDistance < restDistance2) {
+      var deltaAngle = (deltaDistance / path.radius) * -path.crossProduct;
+      var newAngle = currentAngle + deltaAngle;
+      return {
+        x: path.x + path.radius * Math.cos(newAngle),
+        y: path.y + path.radius * Math.sin(newAngle),
+        path: path,
+        angle: newAngle
+      };
+    }
+    return getPositionAfterMove({
+      point: isBackward
+        ? { x: path.arcStartX, y: path.arcStartY }
+        : { x: path.arcEndX, y: path.arcEndY },
+      pathList: pathList,
+      pathIndex: isBackward ? prevIndex : nextIndex,
+      deltaDistance: isBackward ? deltaDistance + restDistance2 : deltaDistance - restDistance2,
+      angle: isBackward ? path.startAngle : path.endAngle
+    });
+  }
+
+  function getAllDistance(pathList) {
+    var totalDistance = 0;
+    pathList.forEach(function (path) {
+      if (path.type === 'line') {
+        totalDistance += calcDistance(path, { x: path.startX, y: path.startY });
+      } else {
+        totalDistance += Math.abs(
+          ((path.endAngle - path.startAngle) % (Math.PI * 2)) * path.radius
+        );
+      }
+    });
+    return totalDistance;
+  }
+
+  /** 鐩稿杞ㄩ亾绐勮竟锛堝帤搴︽柟鍚戯級鏁翠綋缂╁皬杞︿綋锛岄伩鍏嶆按骞�/鍨傜洿杞ㄩ亾涓婅创婊¤建閬撳甫 */
+  var TRACK_DEVICE_BOX_SCALE = 0.9;
+
+  /**
+   * 鐜┛鍗曚晶鍐呯缉璺濈锛氭寜杞﹀ across 鐨勬瘮渚嬩及绠楋紙浼樺厛 `value.deviceList[].deviceWidth`锛屽惁鍒欑幆绌胯嚜鍔ㄨ鍒欙級锛屼笖涓嶄綆浜庝笅闄愩��
+   * 鍏冪礌鍙 `annulusBandInset`锛堥潪璐燂級瑕嗙洊銆�
+   */
+  var ANNULUS_INSET_FROM_DEVICE_RATIO = 0.26;
+  /** @type {number} 榛樿鏈�灏忓唴缂╋紙鍍忕礌锛� */
+  var ANNULUS_INSET_MIN_PIXELS = 5;
+
+  /**
+   * 鐩寸嚎杞ㄩ亾涓婅澶囧浘鏍囧儚绱犲昂瀵革紙娌胯建閬撴柟鍚� 脳 鍨傜洿杞ㄩ亾鏂瑰悜锛夈��
+   * 涓� drawCrnDeviceGraphics / drawRgvDeviceGraphics 鐨� width銆乭eight 璇箟涓�鑷达紝涓嶅啀鍦ㄥ埆澶勫仛銆屽啀涔� 2銆嶃��
+   * @param {{ width: number, height: number }} rect 杞ㄩ亾澶栨帴鐭╁舰
+   * @param {"crn"|"dualCrn"|"rgv"} trackType
+   * @returns {{ along: number, across: number }}
+   */
+  function getDevicePixelBoxForTrack(rect, trackType) {
+    var shortLen = Math.min(rect.width, rect.height);
+    var across = shortLen;
+    var along = shortLen * 2;
+    along = Math.round(along * TRACK_DEVICE_BOX_SCALE);
+    across = Math.round(across * TRACK_DEVICE_BOX_SCALE);
+    return {
+      along: Math.max(2, along),
+      across: Math.max(2, across)
+    };
+  }
+
+  /**
+   * 杞ㄩ亾涓婅澶囥�岃嚜鍔ㄣ�嶅儚绱犲昂瀵革紙娌胯建閬撴柟鍚� 脳 鍨傜洿杞ㄩ亾鏂瑰悜锛夈��
+   * - 鐩寸嚎杞ㄩ亾锛氭潵鑷� getDevicePixelBoxForTrack
+   * - 鐜┛锛氭寜鍘嗗彶閫昏緫缂╂斁鍒� shortLength 鐨� 15%
+   * @param {{ type: string, width: number, height: number, pathList?: any[] }} rect
+   * @returns {{ along: number, across: number } | null}
+   */
+  function getAutoTrackDeviceBox(rect) {
+    if (!rect) {
+      return null;
+    }
+    var rectW = Number(rect.width);
+    var rectH = Number(rect.height);
+    if (!isFinite(rectW) || !isFinite(rectH) || rectW <= 0 || rectH <= 0) {
+      return null;
+    }
+    if (rect.type === 'annulus') {
+      var shortLength = Math.min(rectW, rectH);
+      var box = getDevicePixelBoxForTrack({ width: shortLength, height: shortLength }, 'rgv');
+      var scale = (shortLength * 0.15) / box.across;
+      return {
+        along: Math.max(2, Math.round(box.along * scale)),
+        across: Math.max(2, Math.round(box.across * scale))
+      };
+    }
+    return getDevicePixelBoxForTrack({ width: rectW, height: rectH }, rect.type);
+  }
+
+  function getDeviceInfo(rect) {
+    var isHorizontal = rect.width > rect.height;
+    var longLength = isHorizontal ? rect.width : rect.height;
+    var shortLength = isHorizontal ? rect.height : rect.width;
+    var deviceForm = parseDeviceFormValue(rect.value);
+    if (!deviceForm || !deviceForm.deviceList || deviceForm.deviceList.length === 0) {
+      return deviceForm;
+    }
+
+    if (rect.type === 'annulus') {
+      var pathList = rect.pathList || [];
+      if (!pathList.length) {
+        return deviceForm;
+      }
+      var allDistance = getAllDistance(pathList);
+      var autoBox = getAutoTrackDeviceBox(rect);
+      var ab = annulusBandContext(rect, rect.shape || 'rect');
+      deviceForm.deviceList.forEach(function (item) {
+        var deltaDistance = (allDistance * item.progress) / 100;
+        var startPoint = {
+          x: pathList[0].arcStartX,
+          y: pathList[0].arcStartY
+        };
+        var moved = getPositionAfterMove({
+          point: startPoint,
+          pathList: pathList,
+          pathIndex: 0,
+          deltaDistance: deltaDistance
+        });
+        var centered = shiftAnnulusPointToBandCenter(
+          moved.x,
+          moved.y,
+          moved.path,
+          ab.inset,
+          ab.refInside
+        );
+        var fallbackAlong = autoBox ? autoBox.along : Math.max(2, Math.round(shortLength * 0.3));
+        var fallbackAcross = autoBox ? autoBox.across : Math.max(2, Math.round(shortLength * 0.15));
+        var size = normalizeDeviceSizeOverride(item, fallbackAlong, fallbackAcross);
+        var width = size.along;
+        var height = size.across;
+        item.x = centered.x;
+        item.y = centered.y;
+        item.path = moved.path;
+        item.width = width;
+        item.height = height;
+      });
+      return deviceForm;
+    }
+
+    var box = getAutoTrackDeviceBox(rect) || getDevicePixelBoxForTrack(rect, rect.type);
+    deviceForm.deviceList.forEach(function (item) {
+      var size = normalizeDeviceSizeOverride(item, box.along, box.across);
+      var inset = (shortLength - size.across) / 2;
+      var distance = (item.progress * (longLength - 2 * shortLength)) / 100;
+      if (isHorizontal) {
+        item.x = rect.x + distance;
+        item.y = rect.y + inset;
+        item.width = size.along;
+        item.height = size.across;
+      } else {
+        item.x = rect.x + inset;
+        item.y = rect.y + distance;
+        item.width = size.across;
+        item.height = size.along;
+      }
+    });
+    return deviceForm;
+  }
+
+  function getLShapePointList(sprite) {
+    var turningPoint;
+    var LPointList;
+    if (!sprite.turningPoint) {
+      var rate = 1 / 3;
+      var minDistance = Math.min(sprite.width, sprite.height);
+      var shortDistance = rate * minDistance;
+      var longDistance = sprite.height - shortDistance;
+
+      if (sprite.shape === 'L1') {
+        turningPoint = {
+          x: sprite.x + shortDistance,
+          y: sprite.y + longDistance
+        };
+      } else if (sprite.shape === 'L2') {
+        turningPoint = {
+          x: sprite.x + sprite.width - shortDistance,
+          y: sprite.y + sprite.height - longDistance
+        };
+      } else if (sprite.shape === 'L3') {
+        turningPoint = {
+          x: sprite.x + shortDistance,
+          y: sprite.y + sprite.height - longDistance
+        };
+      } else if (sprite.shape === 'L4') {
+        turningPoint = {
+          x: sprite.x + sprite.width - shortDistance,
+          y: sprite.y + longDistance
+        };
+      }
+    } else {
+      turningPoint = sprite.turningPoint;
+    }
+    sprite.turningPoint = turningPoint;
+
+    if (sprite.shape === 'L1') {
+      LPointList = [
+        { x: sprite.x, y: sprite.y, direction: 'up' },
+        { x: turningPoint.x, y: sprite.y, direction: 'right' },
+        Object.assign({}, turningPoint, { direction: 'down' }),
+        { x: sprite.x + sprite.width, y: turningPoint.y, direction: 'right' },
+        {
+          x: sprite.x + sprite.width,
+          y: sprite.y + sprite.height,
+          direction: 'down'
+        },
+        { x: sprite.x, y: sprite.y + sprite.height, direction: 'left' }
+      ];
+    } else if (sprite.shape === 'L2') {
+      LPointList = [
+        {
+          x: sprite.x + sprite.width,
+          y: sprite.y + sprite.height,
+          direction: 'bottom'
+        },
+        { x: turningPoint.x, y: sprite.y + sprite.height, direction: 'left' },
+        Object.assign({}, turningPoint, { direction: 'up' }),
+        { x: sprite.x, y: turningPoint.y, direction: 'left' },
+        { x: sprite.x, y: sprite.y, direction: 'up' },
+        { x: sprite.x + sprite.width, y: sprite.y, direction: 'right' }
+      ];
+    } else if (sprite.shape === 'L3') {
+      LPointList = [
+        { x: sprite.x, y: sprite.y + sprite.height, direction: 'bottom' },
+        { x: turningPoint.x, y: sprite.y + sprite.height, direction: 'right' },
+        Object.assign({}, turningPoint, { direction: 'up' }),
+        { x: sprite.x + sprite.width, y: turningPoint.y, direction: 'right' },
+        { x: sprite.x + sprite.width, y: sprite.y, direction: 'up' },
+        { x: sprite.x, y: sprite.y, direction: 'left' }
+      ];
+    } else {
+      LPointList = [
+        { x: sprite.x + sprite.width, y: sprite.y, direction: 'up' },
+        { x: turningPoint.x, y: sprite.y, direction: 'left' },
+        Object.assign({}, turningPoint, { direction: 'down' }),
+        { x: sprite.x, y: turningPoint.y, direction: 'left' },
+        { x: sprite.x, y: sprite.y + sprite.height, direction: 'down' },
+        {
+          x: sprite.x + sprite.width,
+          y: sprite.y + sprite.height,
+          direction: 'right'
+        }
+      ];
+    }
+
+    return LPointList;
+  }
+
+  /**
+   * 灏勭嚎娉曞垽鐐规槸鍚﹀湪绠�鍗曞杈瑰舰鍐咃紙鐢ㄤ簬鎵惧弬鑰冨唴鐐癸紝绠楀唴鍚戞硶鍚戯級銆�
+   * @param {{ x: number, y: number }} pt
+   * @param {{ x: number, y: number }[]} ring
+   * @returns {boolean}
+   */
+  function pointInPolygon(pt, ring) {
+    var inside = false;
+    var n = ring.length;
+    var i;
+    for (i = 0; i < n; i++) {
+      var a = ring[i];
+      var b = ring[(i + 1) % n];
+      var intersects =
+        a.y > pt.y !== b.y > pt.y &&
+        pt.x < ((b.x - a.x) * (pt.y - a.y)) / (b.y - a.y + 1e-12) + a.x;
+      if (intersects) {
+        inside = !inside;
+      }
+    }
+    return inside;
+  }
+
+  /**
+   * 鍦ㄥ皷鐐圭幆鍖呭洿鐩掑唴閲囨牱锛屽緱鍒板杈瑰舰鍐呴儴涓�鐐癸紝渚涚洿绾挎涓婂垽鏂�屾寚鍚戝唴渚с�嶇殑娉曞悜銆�
+   * @param {{ x: number, y: number }[]} pointList
+   * @returns {{ x: number, y: number }}
+   */
+  function findInteriorRefPoint(pointList) {
+    var minX = Infinity;
+    var minY = Infinity;
+    var maxX = -Infinity;
+    var maxY = -Infinity;
+    var i;
+    for (i = 0; i < pointList.length; i++) {
+      var p = pointList[i];
+      minX = Math.min(minX, p.x);
+      minY = Math.min(minY, p.y);
+      maxX = Math.max(maxX, p.x);
+      maxY = Math.max(maxY, p.y);
+    }
+    var cx = (minX + maxX) / 2;
+    var cy = (minY + maxY) / 2;
+    if (pointInPolygon({ x: cx, y: cy }, pointList)) {
+      return { x: cx, y: cy };
+    }
+    var g;
+    for (g = 1; g <= 6; g++) {
+      var sj;
+      for (sj = 1; sj < g; sj++) {
+        var si;
+        for (si = 1; si < g; si++) {
+          var tx = minX + ((maxX - minX) * si) / g;
+          var ty = minY + ((maxY - minY) * sj) / g;
+          if (pointInPolygon({ x: tx, y: ty }, pointList)) {
+            return { x: tx, y: ty };
+          }
+        }
+      }
+    }
+    return { x: cx, y: cy };
+  }
+
+  /**
+   * 鏈夊悜杈� (ax,ay)鈫�(bx,by) 娌跨幆鍓嶈繘鏂瑰悜鐨勫崟浣嶅唴鍚戞硶鍚戦噺锛堥�嗘椂閽堢幆鏃跺唴渚у湪鍓嶈繘鏂瑰悜宸︿晶锛夈��
+   * @param {number} ax
+   * @param {number} ay
+   * @param {number} bx
+   * @param {number} by
+   * @param {boolean} ccwPolygon 灏栫偣鐜湁鍚戦潰绉� >0 鏃朵负閫嗘椂閽�
+   * @returns {{ x: number, y: number }}
+   */
+  function inwardNormalUnitForward(ax, ay, bx, by, ccwPolygon) {
+    var dx = bx - ax;
+    var dy = by - ay;
+    var len = Math.sqrt(dx * dx + dy * dy) || 1;
+    var ux = dx / len;
+    var uy = dy / len;
+    if (ccwPolygon) {
+      return { x: -uy, y: ux };
+    }
+    return { x: uy, y: -ux };
+  }
+
+  /**
+   * 涓ょ洿绾� p1+t*u1 涓� p2+s*u2 鐨勪氦鐐癸紙u 宸蹭负鍗曚綅鍚戦噺鍒� t 涓哄嚑浣曢暱搴︼級銆�
+   * @param {{ x: number, y: number }} p1
+   * @param {{ x: number, y: number }} u1
+   * @param {{ x: number, y: number }} p2
+   * @param {{ x: number, y: number }} u2
+   * @returns {{ x: number, y: number }}
+   */
+  function intersectLines(p1, u1, p2, u2) {
+    var cr = u1.x * u2.y - u1.y * u2.x;
+    if (Math.abs(cr) < 1e-9) {
+      return { x: p1.x, y: p1.y };
+    }
+    var dx = p2.x - p1.x;
+    var dy = p2.y - p1.y;
+    var t = (dx * u2.y - dy * u2.x) / cr;
+    return { x: p1.x + t * u1.x, y: p1.y + t * u1.y };
+  }
+
+  /**
+   * 鐩磋椤剁偣鐜悜鍐呭亸绉汇��
+   * 鍑硅锛氭部鐜墠杩涘悜閲� vIn=curr鈭抪rev銆乿Out=next鈭抍urr 鐨勫弶绉笌鐜悜涓�鑷存椂鍒ゅ畾锛圕CW 鐜笂鍑歌 cross>0銆佸嚬瑙� cross<0锛夛紝
+   * 鏂伴《鐐逛负 curr+inset*(nIn+nOut)锛涘嚫瑙掍负涓ら偦杈瑰钩绉诲悗鐨勭洿绾夸氦鐐广��
+   * 锛坰moothRightAnglePath 鐢ㄧ殑鏄� curr鈫抪rev 涓� curr鈫抧ext锛屼笌銆屾部鐜墠杩涖�嶅樊涓�绗﹀彿锛屼笉鑳藉鐢ㄥ叾鍙夌Н鍒ゅ嚬鍑搞�傦級
+   * @param {{ x: number, y: number, direction?: string }[]} sharpCorners
+   * @param {number} inset
+   * @returns {{ x: number, y: number, direction?: string }[]}
+   */
+  function offsetOrthogonalSharpRing(sharpCorners, inset) {
+    var area2 = 0;
+    var nj;
+    for (nj = 0; nj < sharpCorners.length; nj++) {
+      var pj0 = sharpCorners[nj];
+      var pj1 = sharpCorners[(nj + 1) % sharpCorners.length];
+      area2 += pj0.x * pj1.y - pj1.x * pj0.y;
+    }
+    var ccwPolygon = area2 > 0;
+    var n = sharpCorners.length;
+    var out = [];
+    var i;
+    for (i = 0; i < n; i++) {
+      var prev = sharpCorners[(i - 1 + n) % n];
+      var curr = sharpCorners[i];
+      var next = sharpCorners[(i + 1) % n];
+      var vInx = curr.x - prev.x;
+      var vIny = curr.y - prev.y;
+      var vOutx = next.x - curr.x;
+      var vOuty = next.y - curr.y;
+      var crossWalk = vInx * vOuty - vIny * vOutx;
+      var reflex = ccwPolygon ? crossWalk < 0 : crossWalk > 0;
+      var nIn = inwardNormalUnitForward(prev.x, prev.y, curr.x, curr.y, ccwPolygon);
+      var nOut = inwardNormalUnitForward(curr.x, curr.y, next.x, next.y, ccwPolygon);
+      var ix;
+      var iy;
+      if (reflex) {
+        ix = curr.x + (nIn.x + nOut.x) * inset;
+        iy = curr.y + (nIn.y + nOut.y) * inset;
+      } else {
+        var lenIn = Math.sqrt(vInx * vInx + vIny * vIny) || 1;
+        var lenOut = Math.sqrt(vOutx * vOutx + vOuty * vOuty) || 1;
+        var uIn = { x: vInx / lenIn, y: vIny / lenIn };
+        var uOut = { x: vOutx / lenOut, y: vOuty / lenOut };
+        var oIn = { x: prev.x + nIn.x * inset, y: prev.y + nIn.y * inset };
+        var oOut = { x: curr.x + nOut.x * inset, y: curr.y + nOut.y * inset };
+        var inter = intersectLines(oIn, uIn, oOut, uOut);
+        ix = inter.x;
+        iy = inter.y;
+      }
+      out.push({ x: ix, y: iy, direction: sharpCorners[i].direction });
+    }
+    return out;
+  }
+
+  /**
+   * 瑙f瀽鐜┛鍗曚晶鍐呯缉鍍忕礌璺濈锛涜嫢浼犲叆 `precomputedSharp` 鍒欎笉鍐嶉噸澶嶆眰灏栫偣鐜��
+   * @param {{ width: number, height: number, shape?: string, annulusBandInset?: number, value?: string|object }} sprite
+   * @param {{ x: number, y: number }[]} [precomputedSharp]
+   * @returns {number}
+   */
+  // function resolveAnnulusBandInset(sprite, precomputedSharp) {
+  //   var override = sprite && sprite.annulusBandInset;
+  //   if (typeof override === 'number' && isFinite(override) && override >= 0) {
+  //     return override;
+  //   }
+  //   var tmp = { type: 'annulus', width: sprite.width, height: sprite.height };
+  //   var box = getAutoTrackDeviceBox(tmp);
+  //   var shortLen = Math.min(Number(sprite.width) || 0, Number(sprite.height) || 0);
+  //   var fallbackAlong = box ? box.along : Math.max(2, Math.round(shortLen * 0.3));
+  //   var fallbackAcross = box ? box.across : Math.max(2, Math.round(shortLen * 0.15));
+  //   var form = parseDeviceFormValue(sprite && sprite.value);
+  //   var across = fallbackAcross;
+  //   console.log('fallbackAcross', across);
+  //   if (form && form.deviceList && form.deviceList.length) {
+  //     var di;
+  //     for (di = 0; di < form.deviceList.length; di++) {
+  //       var sz = normalizeDeviceSizeOverride(form.deviceList[di], fallbackAlong, fallbackAcross);
+  //       if (sz.across > across) {
+  //         across = sz.across;
+  //       }
+  //     }
+  //   }
+  //   var inset = Math.max(
+  //     ANNULUS_INSET_MIN_PIXELS,
+  //     Math.round(across * ANNULUS_INSET_FROM_DEVICE_RATIO)
+  //   );
+  //   var maxByBox = Math.floor(Math.min(sprite.width, sprite.height) / 2) - 3;
+  //   var ring = precomputedSharp || getSharpCornerList(sprite, sprite.shape || 'rect');
+  //   var minEdge = Infinity;
+  //   var ri;
+  //   for (ri = 0; ri < ring.length; ri++) {
+  //     var d = calcDistance(ring[ri], ring[(ri + 1) % ring.length]);
+  //     if (d > 0 && d < minEdge) {
+  //       minEdge = d;
+  //     }
+  //   }
+  //   var maxByEdge = minEdge === Infinity ? inset : Math.floor(minEdge / 2) - 1;
+  //   var cap = Math.min(isFinite(maxByBox) ? maxByBox : inset, isFinite(maxByEdge) ? maxByEdge : inset);
+  //   if (cap > 0 && inset > cap) {
+  //     inset = cap;
+  //   }
+  //   return Math.max(0, inset);
+  // }
+
+  /**
+   * 鐜┛鍏辩敤锛氬皷鐐圭幆銆佸唴渚у弬鑰冪偣銆乮nset锛堝彧绠椾竴閬嶅皷鐐癸紝閬垮厤澶氬閲嶅 getSharpCornerList锛夈��
+   * @param {object} sprite x/y/width/height/shape/turningPoint/annulusBandInset
+   * @param {string} defaultShape
+   * @returns {{ sharp: object[], refInside: {x:number,y:number}, inset: number }}
+   */
+  function annulusBandContext(sprite, defaultShape) {
+    sprite.shape = sprite.shape || defaultShape;
+    var sharp = getSharpCornerList(sprite, defaultShape);
+    return {
+      sharp: sharp,
+      refInside: findInteriorRefPoint(sharp),
+      inset: 6
+    };
+  }
+
+  /**
+   * 鐢卞厓绱犲鎺ユ涓庡舰鐘跺緱鍒版湭鍦嗚鍓嶇殑椤剁偣鐜紙rect 鎴� L1鈥揕4锛夈��
+   * @param {{ x: number, y: number, width: number, height: number, shape?: string, turningPoint?: object }} sprite
+   * @param {string} defaultShape
+   * @returns {{ x: number, y: number, direction?: string }[]}
+   */
+  function getSharpCornerList(sprite, defaultShape) {
+    var rectPointList = [
+      { x: sprite.x, y: sprite.y, direction: 'right' },
+      { x: sprite.x + sprite.width, y: sprite.y, direction: 'down' },
+      {
+        x: sprite.x + sprite.width,
+        y: sprite.y + sprite.height,
+        direction: 'left'
+      },
+      { x: sprite.x, y: sprite.y + sprite.height, direction: 'up' }
+    ];
+    var shape = sprite.shape || defaultShape;
+    return shape === 'rect' ? rectPointList : getLShapePointList(sprite);
+  }
+
+  function getIsStillHalf(target) {
+    var curList = getLShapePointList(target);
+    var count = 0;
+    for (var i = 0; i < curList.length; i++) {
+      var p0 = curList[i];
+      var p1 = curList[(i + 1) % curList.length];
+      var p2 = curList[(i + 2) % curList.length];
+      var p3 = curList[(i + 3) % curList.length];
+
+      var d1 = calcDistance(p0, p1);
+      var d2 = calcDistance(p1, p2);
+      var d3 = calcDistance(p2, p3);
+      if (target.halfList.includes((i + 1) % curList.length) && d2 <= d1 && d2 <= d3) {
+        count += 1;
+      }
+    }
+    return count === 2;
+  }
+
+  function setRadiusInPoint(pointList, sprite) {
+    sprite.halfList = [];
+    var pointLength = pointList.length;
+    var i;
+    for (i = 0; i < pointLength; i++) {
+      var currentPoint = pointList[i];
+      var next1 = pointList[(i + 1) % pointLength];
+      var next2 = pointList[(i + 2) % pointLength];
+      var next3 = pointList[(i + 3) % pointLength];
+      var v1 = normalizeVector(currentPoint, next1);
+      var v2 = normalizeVector(next1, next2);
+      var v3 = normalizeVector(next2, next3);
+      var finalV = { x: v1.x + v2.x + v3.x, y: v1.y + v2.y + v3.y };
+      var distance = finalV.x * finalV.x + finalV.y * finalV.y;
+      var d1 = calcDistance(currentPoint, next1);
+      var d2 = calcDistance(next1, next2);
+      var d3 = calcDistance(next2, next3);
+      if (distance === 1 && d2 <= d1 && d2 <= d3) {
+        var radius = d2 / 2;
+        next1.isHalf = true;
+        next2.isHalf = true;
+        next1.radius = radius;
+        next2.radius = radius;
+        sprite.halfList.push((i + 1) % pointLength);
+      }
+    }
+    for (i = 0; i < pointLength; i++) {
+      currentPoint = pointList[i];
+      if (currentPoint.isHalf) {
+        continue;
+      }
+      var prevPoint = pointList[(i - 1 + pointLength) % pointLength];
+      var nextPoint = pointList[(i + 1 + pointLength) % pointLength];
+      var rad = Math.min.apply(
+        Math,
+        [prevPoint.radius, nextPoint.radius].filter(function (item) {
+          return item;
+        })
+      );
+      currentPoint.radius = rad;
+    }
+  }
+
+  function smoothRightAnglePath(points) {
+    var smoothedPath = [];
+    var n = points.length;
+    var i;
+    for (i = 0; i < n; i++) {
+      var prev = points[(i - 1 + n) % n];
+      var curr = points[i];
+      var next = points[(i + 1) % n];
+      var radius = curr.radius;
+      var toPrev = normalizeVector(curr, prev);
+      var toNext = normalizeVector(curr, next);
+      var dotProduct = toPrev.x * toNext.x + toPrev.y * toNext.y;
+      var isRightAngle = Math.abs(dotProduct) < 0.1;
+
+      if (isRightAngle) {
+        var arcStart = {
+          x: curr.x + toPrev.x * radius,
+          y: curr.y + toPrev.y * radius
+        };
+        var arcEnd = {
+          x: curr.x + toNext.x * radius,
+          y: curr.y + toNext.y * radius
+        };
+        var arcCenter = {
+          x: curr.x + toPrev.x * radius + toNext.x * radius,
+          y: curr.y + toPrev.y * radius + toNext.y * radius
+        };
+        var startAngle = Math.atan2(arcStart.y - arcCenter.y, arcStart.x - arcCenter.x);
+        var endAngle = Math.atan2(arcEnd.y - arcCenter.y, arcEnd.x - arcCenter.x);
+        var crossProduct = toPrev.x * toNext.y - toPrev.y * toNext.x;
+        if (crossProduct > 0) {
+          if (endAngle > startAngle) {
+            endAngle -= Math.PI * 2;
+          }
+        } else {
+          if (endAngle < startAngle) {
+            endAngle += Math.PI * 2;
+          }
+        }
+        smoothedPath.push({
+          x: arcCenter.x,
+          y: arcCenter.y,
+          radius: radius,
+          startAngle: startAngle,
+          endAngle: endAngle,
+          type: 'arc',
+          arcStartX: arcStart.x,
+          arcStartY: arcStart.y,
+          arcEndX: arcEnd.x,
+          arcEndY: arcEnd.y,
+          crossProduct: crossProduct
+        });
+      }
+    }
+    var length = smoothedPath.length;
+    var newSmoothedPath = [];
+    for (i = 0; i < length; i++) {
+      var currArc = smoothedPath[i];
+      var nextArc = smoothedPath[(i + 1 + length) % length];
+      newSmoothedPath.push(currArc);
+      if (currArc.arcEndX !== nextArc.arcStartX || currArc.arcEndY !== nextArc.arcStartY) {
+        newSmoothedPath.push({
+          startX: currArc.arcEndX,
+          startY: currArc.arcEndY,
+          type: 'line',
+          x: nextArc.arcStartX,
+          y: nextArc.arcStartY
+        });
+      }
+    }
+    return newSmoothedPath;
+  }
+
+  /**
+   * 浠呮弿杈�/濉厖杞粨锛氭部 pathList 璧扮瑪骞� closePath锛堜笉 endFill锛屼究浜庤繛缁鍦堟弿杈癸級銆�
+   * @param {PIXI.Graphics} smoothedGraphics
+   * @param {object[]} path
+   */
+  function traceSmoothedPath(smoothedGraphics, path) {
+    if (!path || !path.length) {
+      return;
+    }
+    var startPoint = path[0];
+    if (startPoint.type === 'arc') {
+      smoothedGraphics.moveTo(startPoint.arcStartX, startPoint.arcStartY);
+    } else {
+      smoothedGraphics.moveTo(startPoint.x, startPoint.y);
+    }
+    var i;
+    for (i = 0; i < path.length; i++) {
+      var point = path[i];
+      if (point.type === 'line') {
+        smoothedGraphics.lineTo(point.x, point.y);
+      } else if (point.type === 'arc') {
+        smoothedGraphics.arc(
+          point.x,
+          point.y,
+          point.radius,
+          point.startAngle,
+          point.endAngle,
+          point.endAngle < point.startAngle
+        );
+      }
+    }
+    smoothedGraphics.closePath();
+  }
+
+  /**
+   * 濉厖闂悎澶栧湀骞� endFill锛堣皟鐢ㄦ柟闇�宸� beginFill锛夈��
+   * @param {PIXI.Graphics} smoothedGraphics
+   * @param {object[]} path
+   */
+  function drawSmoothedPath(smoothedGraphics, path) {
+    traceSmoothedPath(smoothedGraphics, path);
+    smoothedGraphics.endFill();
+  }
+
+  /**
+   * PIXI beginHole 瑕佹眰娲炰笌澶栬疆寤撶粫鍚戠浉鍙嶏細娈甸�嗗簭涓斿渾寮ц捣缁堢偣瀵硅皟銆�
+   * @param {object[]} path smoothRightAnglePath 缁撴灉
+   * @returns {object[]}
+   */
+  function reverseSmoothedSegmentsForHole(path) {
+    var n = path.length;
+    var out = [];
+    var i;
+    for (i = n - 1; i >= 0; i--) {
+      var seg = path[i];
+      if (seg.type === 'line') {
+        out.push({
+          type: 'line',
+          startX: seg.x,
+          startY: seg.y,
+          x: seg.startX,
+          y: seg.startY
+        });
+      } else if (seg.type === 'arc') {
+        out.push({
+          type: 'arc',
+          x: seg.x,
+          y: seg.y,
+          radius: seg.radius,
+          startAngle: seg.endAngle,
+          endAngle: seg.startAngle,
+          arcStartX: seg.arcEndX,
+          arcStartY: seg.arcEndY,
+          arcEndX: seg.arcStartX,
+          arcEndY: seg.arcStartY,
+          crossProduct: seg.crossProduct
+        });
+      }
+    }
+    return out;
+  }
+
+  /**
+   * 澶栧~鍏� + 鍐呮礊锛堝弻杞ㄥ甫锛夛紝璋冪敤鏂归渶宸� beginFill銆�
+   * @param {PIXI.Graphics} smoothedGraphics
+   * @param {object[]} outerPath
+   * @param {object[]} innerPath
+   */
+  function drawSmoothedPathWithHole(smoothedGraphics, outerPath, innerPath) {
+    traceSmoothedPath(smoothedGraphics, outerPath);
+    smoothedGraphics.beginHole();
+    traceSmoothedPath(smoothedGraphics, reverseSmoothedSegmentsForHole(innerPath));
+    smoothedGraphics.endHole();
+    smoothedGraphics.endFill();
+  }
+
+  /**
+   * 鐢熸垚澶栧湀涓庡唴鍦� smooth pathList锛涘唴鍦堢敱姝d氦灏栫偣鐜� offset 鍚庡啀 setRadius/smooth銆�
+   * @param {{ x: number, y: number, width: number, height: number, shape?: string, turningPoint?: object, annulusBandInset?: number }} sprite
+   * @param {string} defaultShape
+   * @returns {{ outerPath: object[], innerPath: object[]|null, inset: number }}
+   */
+  function buildOuterAndInnerPaths(sprite, defaultShape) {
+    var ctx = annulusBandContext(sprite, defaultShape);
+    var sharp = ctx.sharp;
+    var sharpClone = sharp.map(function (p) {
+      return { x: p.x, y: p.y, direction: p.direction };
+    });
+    setRadiusInPoint(sharp, sprite);
+    var outerPath = smoothRightAnglePath(sharp);
+    var inset = ctx.inset;
+    var minSide = Math.min(sprite.width, sprite.height);
+    if (inset <= 0 || minSide <= inset * 2 + 4) {
+      return { outerPath: outerPath, innerPath: null, inset: inset };
+    }
+    var innerSharp = offsetOrthogonalSharpRing(sharpClone, inset);
+    var innerMeta = {};
+    setRadiusInPoint(innerSharp, innerMeta);
+    var innerPath = smoothRightAnglePath(innerSharp);
+    return { outerPath: outerPath, innerPath: innerPath, inset: inset };
+  }
+
+  /**
+   * 澶栬疆寤撲笂涓�鐐规部娉曞悜锛堢洿绾挎锛夋垨寰勫悜锛堝渾寮ф锛夊唴绉� half锛岃惤鍦ㄥ弻杞ㄥ嚑浣曚腑绾裤��
+   * @param {number} x
+   * @param {number} y
+   * @param {object} path 褰撳墠娈� line|arc
+   * @param {number} inset 鍗曚晶甯﹀
+   * @param {{ x: number, y: number }} refInside 澶氳竟褰㈠唴鍙傝�冪偣
+   * @returns {{ x: number, y: number }}
+   */
+  function shiftAnnulusPointToBandCenter(x, y, path, inset, refInside) {
+    var half = inset / 2;
+    if (half <= 0) {
+      return { x: x, y: y };
+    }
+    if (path.type === 'line') {
+      var tx = path.x - path.startX;
+      var ty = path.y - path.startY;
+      var tlen = Math.sqrt(tx * tx + ty * ty) || 1;
+      var nx = -ty / tlen;
+      var ny = tx / tlen;
+      var mx = (path.startX + path.x) / 2;
+      var my = (path.startY + path.y) / 2;
+      if ((refInside.x - mx) * nx + (refInside.y - my) * ny < 0) {
+        nx = -nx;
+        ny = -ny;
+      }
+      return { x: x + nx * half, y: y + ny * half };
+    }
+    if (path.type === 'arc') {
+      var ox = x - path.x;
+      var oy = y - path.y;
+      var olen = Math.sqrt(ox * ox + oy * oy) || 1;
+      return { x: x - (ox / olen) * half, y: y - (oy / olen) * half };
+    }
+    return { x: x, y: y };
+  }
+
+  /**
+   * 灏嗗鍦� path 涓婂潗鏍囨槧灏勫埌杞ㄩ亾甯︿腑绾匡紙鐩戞帶/鏉$爜涓庣紪杈戝櫒涓�鑷达級銆�
+   * @param {{ type: string, x: number, y: number, width: number, height: number, shape?: string, turningPoint?: object, annulusBandInset?: number }} element
+   * @param {number} x
+   * @param {number} y
+   * @param {object} path
+   * @returns {{ x: number, y: number }}
+   */
+  function centerAnnulusBandPoint(element, x, y, path) {
+    if (!element || element.type !== 'annulus' || path == null) {
+      return { x: x, y: y };
+    }
+    var ctx = annulusBandContext(element, element.shape || 'rect');
+    return shiftAnnulusPointToBandCenter(x, y, path, ctx.inset, ctx.refInside);
+  }
+
+  /**
+   * 灏嗗潗鏍囧帇鍥炵幆绌垮鍦� path锛堢洿绾挎鎶曞奖銆佸渾寮ф钀藉埌鍗婂緞涓婏級銆�
+   * sprite 宸插仛 centerAnnulusBandPoint 鏃朵笉鑳界洿鎺ヤ綔涓� getPositionAfterMove 鐨勮捣鐐癸紝鍚﹀垯浼氭部銆岀偣鈫掓缁堢偣銆嶆枩绉绘紓绉汇��
+   * @param {number} x
+   * @param {number} y
+   * @param {{ type: string, startX?: number, startY?: number, x?: number, y?: number, radius?: number }|null|undefined} path
+   * @returns {{ x: number, y: number }}
+   */
+  function snapToAnnulusOuterPath(x, y, path) {
+    if (!path) {
+      return { x: x, y: y };
+    }
+    if (path.type === 'line') {
+      var sx = path.startX;
+      var sy = path.startY;
+      var ex = path.x;
+      var ey = path.y;
+      var dx = ex - sx;
+      var dy = ey - sy;
+      var len2 = dx * dx + dy * dy;
+      if (len2 < 1e-12) {
+        return { x: sx, y: sy };
+      }
+      var t = ((x - sx) * dx + (y - sy) * dy) / len2;
+      t = Math.max(0, Math.min(1, t));
+      return { x: sx + t * dx, y: sy + t * dy };
+    }
+    if (path.type === 'arc') {
+      var ox = path.x;
+      var oy = path.y;
+      var r = path.radius;
+      if (!isFinite(r) || r <= 0) {
+        return { x: x, y: y };
+      }
+      var ang = getNormalizeAngle(Math.atan2(y - oy, x - ox), path.startAngle, path.endAngle);
+      return { x: ox + r * Math.cos(ang), y: oy + r * Math.sin(ang) };
+    }
+    return { x: x, y: y };
+  }
+
+  /**
+   * 鐩戞帶杞ㄩ亾灞傦細宸蹭繚瀛樼殑 pathList 鎻忓鍦� + 鎸夊嚑浣曢噸绠楀唴鍦堝啀鎻忎竴鍦堛��
+   * @param {PIXI.Graphics} smoothedGraphics
+   * @param {{ pathList: object[], x: number, y: number, width: number, height: number, shape?: string, turningPoint?: object, annulusBandInset?: number }} item
+   * @param {string} defaultShape
+   */
+  function strokeAnnulusDualOutline(smoothedGraphics, item, defaultShape) {
+    var outerPath = item && item.pathList;
+    if (!outerPath || !outerPath.length) {
+      return;
+    }
+    var tmp = {
+      x: item.x,
+      y: item.y,
+      width: item.width,
+      height: item.height,
+      shape: item.shape,
+      turningPoint: item.turningPoint,
+      annulusBandInset: item.annulusBandInset
+    };
+    var pair = buildOuterAndInnerPaths(tmp, item.shape || defaultShape);
+    traceSmoothedPath(smoothedGraphics, outerPath);
+    if (pair.innerPath && pair.innerPath.length) {
+      traceSmoothedPath(smoothedGraphics, pair.innerPath);
+    }
+  }
+
+  /**
+   * 缂栬緫鍣ㄧ幆绌垮~鍏咃細鍐欏洖 sprite.pathList锛堝鍦堬級锛岀粯鍒跺濉�+鍐呮礊銆�
+   * PIXI 涓甫娲炲~鍏呴�氬父涓嶄細缁欐礊杈圭晫鎻忚竟锛屾晠鍦� endFill 鍚庡啀娌垮唴鍦� trace 涓�閬嶏紝涓庣洃鎺ц建 strokeAnnulusDualOutline 涓�鑷达紝
+   * 浣� trackLayer / guideLayer / selectionLayer 鍧囪兘鐪嬪埌鍐呭湀杞粨銆�
+   * @param {PIXI.Graphics} smoothedGraphics
+   * @param {object} sprite 鍏冪礌鎴栭瑙� rect
+   * @param {string} defaultShape 濡� annulusShape
+   */
+  function startDrawSmoothedPath(smoothedGraphics, sprite, defaultShape) {
+    var pair = buildOuterAndInnerPaths(sprite, defaultShape);
+    sprite.pathList = pair.outerPath;
+    if (!pair.innerPath || !pair.innerPath.length) {
+      drawSmoothedPath(smoothedGraphics, pair.outerPath);
+      return;
+    }
+    drawSmoothedPathWithHole(smoothedGraphics, pair.outerPath, pair.innerPath);
+    traceSmoothedPath(smoothedGraphics, pair.innerPath);
+  }
+
+  function getRotate(point, path) {
+    var vector = normalizeVector(point, path);
+    if (path.type === 'arc') {
+      var angleToCenter = Math.atan2(vector.y, vector.x);
+      return angleToCenter + (Math.PI / 2) * path.crossProduct;
+    }
+    return Math.atan2(vector.y, vector.x);
+  }
+
+  /**
+   * 鍫嗗灈鏈� / 鍙屽伐浣嶈澶囧浘鏍囷紙涓� MapCanvas#createCrnTexture / createCrnTextureColoredDevice 涓�鑷达級
+   * @param {PIXI.Graphics} g
+   * @param {number} deviceWidth 鍥炬爣鎬诲锛堝儚绱狅紝涓� getDevicePixelBoxForTrack.along 涓�鑷达級
+   * @param {number} deviceHeight 鍥炬爣鎬婚珮锛堜笌 getDevicePixelBoxForTrack.across 涓�鑷达級
+   * @param {number} bodyColor 椹鹃┒鑸卞~鍏呰壊
+   */
+  function drawCrnDeviceGraphics(g, deviceWidth, deviceHeight, bodyColor) {
+    var yTop = Math.round(deviceHeight * 0.1);
+    g.beginFill(0x999999);
+    g.drawRect(2, yTop, 3, deviceHeight - yTop - 2);
+    g.drawRect(deviceWidth - 5, yTop, 3, deviceHeight - yTop - 2);
+    g.endFill();
+    g.beginFill(0x999999);
+    g.drawRect(0, yTop, deviceWidth, 3);
+    g.endFill();
+    var cabW = Math.round(deviceWidth * 0.68);
+    var cabH = Math.round(deviceHeight * 0.38);
+    var cabX = Math.round((deviceWidth - cabW) / 2);
+    var cabY = Math.round(deviceHeight * 0.52 - cabH / 2);
+    g.beginFill(bodyColor);
+    g.drawRect(cabX, cabY, cabW, cabH);
+    g.endFill();
+    var winW = Math.round(cabW * 0.6);
+    var winH = Math.round(cabH * 0.45);
+    var winX = cabX + Math.round((cabW - winW) / 2);
+    var winY = cabY + Math.round((cabH - winH) / 2);
+    g.beginFill(0xd0e8ff);
+    g.drawRect(winX, winY, winW, winH);
+    g.endFill();
+    var forkW = Math.round(deviceWidth * 0.8);
+    var forkH = Math.max(2, Math.round(deviceHeight * 0.08));
+    var forkX = Math.round((deviceWidth - forkW) / 2);
+    var forkY = cabY + cabH;
+    g.beginFill(0x666666);
+    g.drawRect(forkX, forkY, forkW, forkH);
+    g.endFill();
+  }
+
+  /**
+   * RGV 璁惧鍥炬爣锛堜笌 MapCanvas#createRgvTextureColoredDevice 涓�鑷达級
+   * @param {PIXI.Graphics} g
+   * @param {number} width
+   * @param {number} height
+   * @param {number} bodyColor 杞︿綋濉厖鑹�
+   */
+  function drawRgvDeviceGraphics(g, width, height, bodyColor) {
+    var bodyW = Math.round(width * 0.8);
+    var bodyH = Math.round(height * 0.55);
+    var bodyX = Math.round((width - bodyW) / 2);
+    var bodyY = Math.round((height - bodyH) / 2);
+    g.beginFill(bodyColor);
+    g.drawRect(bodyX, bodyY, bodyW, bodyH);
+    g.endFill();
+    var winW = Math.round(bodyW * 0.55);
+    var winH = Math.round(bodyH * 0.45);
+    var winX = bodyX + Math.round((bodyW - winW) / 2);
+    var winY = bodyY + Math.round((bodyH - winH) / 2);
+    g.beginFill(0xd0e8ff);
+    g.drawRect(winX, winY, winW, winH);
+    g.endFill();
+    var wheelW = Math.max(2, Math.round(width * 0.12));
+    var wheelH = Math.max(2, Math.round(height * 0.1));
+    var wheelY = bodyY + bodyH;
+    var wheelGap = Math.round((width - wheelW * 2) / 3);
+    var wheelX1 = wheelGap;
+    var wheelX2 = width - wheelGap - wheelW;
+    g.beginFill(0x333333);
+    g.drawRect(wheelX1, wheelY - Math.round(wheelH / 2), wheelW, wheelH);
+    g.drawRect(wheelX2, wheelY - Math.round(wheelH / 2), wheelW, wheelH);
+    g.endFill();
+  }
+
+  global.BasMapTrackGeometry = {
+    TYPE_META: TYPE_META,
+    normalizeVector: normalizeVector,
+    calcDistance: calcDistance,
+    safeParseJson: safeParseJson,
+    getNormalizeAngle: getNormalizeAngle,
+    getPositionAfterMove: getPositionAfterMove,
+    getAllDistance: getAllDistance,
+    getDeviceInfo: getDeviceInfo,
+    getDevicePixelBoxForTrack: getDevicePixelBoxForTrack,
+    getAutoTrackDeviceBox: getAutoTrackDeviceBox,
+    getLShapePointList: getLShapePointList,
+    getIsStillHalf: getIsStillHalf,
+    setRadiusInPoint: setRadiusInPoint,
+    smoothRightAnglePath: smoothRightAnglePath,
+    drawSmoothedPath: drawSmoothedPath,
+    traceSmoothedPath: traceSmoothedPath,
+    strokeAnnulusDualOutline: strokeAnnulusDualOutline,
+    centerAnnulusBandPoint: centerAnnulusBandPoint,
+    snapToAnnulusOuterPath: snapToAnnulusOuterPath,
+    startDrawSmoothedPath: startDrawSmoothedPath,
+    getRotate: getRotate,
+    drawCrnDeviceGraphics: drawCrnDeviceGraphics,
+    drawRgvDeviceGraphics: drawRgvDeviceGraphics
+  };
+})(typeof window !== 'undefined' ? window : this);
diff --git a/src/main/webapp/views/basMap/editor.html b/src/main/webapp/views/basMap/editor.html
index 1f1679d..73a836f 100644
--- a/src/main/webapp/views/basMap/editor.html
+++ b/src/main/webapp/views/basMap/editor.html
@@ -1,6 +1,6 @@
 <!DOCTYPE html>
 <html lang="zh-CN">
-<head>
+  <head>
     <meta charset="utf-8">
     <title>鑷敱鐢诲竷鍦板浘缂栬緫鍣�</title>
     <meta name="renderer" content="webkit">
@@ -9,797 +9,1178 @@
     <link rel="stylesheet" href="../../static/vue/element/element.css">
     <link rel="stylesheet" href="../../static/css/cool.css">
     <style>
-        :root {
-            --page-bg:
-                radial-gradient(1180px 540px at -10% -16%, rgba(24, 113, 181, 0.14), transparent 58%),
-                radial-gradient(920px 480px at 110% -12%, rgba(14, 148, 136, 0.12), transparent 56%),
-                linear-gradient(180deg, #eef4f9 0%, #f8fbfd 100%);
-            --card-bg: rgba(255, 255, 255, 0.94);
-            --card-border: rgba(216, 226, 238, 0.96);
-            --text-main: #213448;
-            --text-sub: #63788e;
-            --primary: #2f79d6;
-            --accent: #169a82;
-            --warn: #f08a3c;
-            --danger: #d85a5a;
-        }
+      :root {
+        --page-bg:
+          radial-gradient(1180px 540px at -10% -16%, rgba(24, 113, 181, 0.14), transparent 58%),
+          radial-gradient(920px 480px at 110% -12%, rgba(14, 148, 136, 0.12), transparent 56%),
+          linear-gradient(180deg, #eef4f9 0%, #f8fbfd 100%);
+        --card-bg: rgba(255, 255, 255, 0.94);
+        --card-border: rgba(216, 226, 238, 0.96);
+        --text-main: #213448;
+        --text-sub: #63788e;
+        --primary: #2f79d6;
+        --accent: #169a82;
+        --warn: #f08a3c;
+        --danger: #d85a5a;
+      }
 
-        [v-cloak] { display: none; }
+      [v-cloak] {
+        display: none;
+      }
 
-        html, body {
-            margin: 0;
-            height: 100%;
-            font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif;
-            color: var(--text-main);
-            background: var(--page-bg);
-            overflow: hidden;
-        }
+      html,
+      body {
+        margin: 0;
+        height: 100%;
+        font-family: 'Avenir Next', 'PingFang SC', 'Microsoft YaHei', sans-serif;
+        color: var(--text-main);
+        background: var(--page-bg);
+        overflow: hidden;
+      }
 
-        .editor-shell {
-            width: 100%;
-            height: 100vh;
-            margin: 0 auto;
-            padding: 8px;
-            box-sizing: border-box;
-            display: flex;
-            flex-direction: column;
-            gap: 0;
-        }
+      .editor-shell {
+        width: 100%;
+        height: 100vh;
+        margin: 0 auto;
+        padding: 8px;
+        box-sizing: border-box;
+        display: flex;
+        flex-direction: column;
+        gap: 0;
+      }
 
-        .panel-card {
-            border-radius: 24px;
-            border: 1px solid var(--card-border);
-            background: var(--card-bg);
-            box-shadow: 0 16px 32px rgba(39, 62, 92, 0.08);
-        }
+      .panel-card {
+        border-radius: 24px;
+        border: 1px solid var(--card-border);
+        background: var(--card-bg);
+        box-shadow: 0 16px 32px rgba(39, 62, 92, 0.08);
+      }
 
-        .workspace {
-            min-height: 0;
-            flex: 1 1 auto;
-            display: flex;
-        }
+      .workspace {
+        min-height: 0;
+        flex: 1 1 auto;
+        display: flex;
+      }
 
-        .panel-card {
-            display: flex;
-            flex-direction: column;
-            overflow: hidden;
-        }
+      .panel-card {
+        display: flex;
+        flex-direction: column;
+        overflow: hidden;
+      }
 
-        .panel-head {
-            display: flex;
-            align-items: center;
-            justify-content: space-between;
-            gap: 10px;
-            padding: 18px 20px 14px;
-            border-bottom: 1px solid rgba(221, 230, 239, 0.94);
-        }
+      .panel-head {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        gap: 10px;
+        padding: 18px 20px 14px;
+        border-bottom: 1px solid rgba(221, 230, 239, 0.94);
+      }
 
-        .panel-head h2 {
-            margin: 0;
-            font-size: 16px;
-            font-weight: 700;
-        }
+      .panel-head h2 {
+        margin: 0;
+        font-size: 16px;
+        font-weight: 700;
+      }
 
-        .panel-body {
-            padding: 16px 18px 18px;
-            display: flex;
-            flex-direction: column;
-            gap: 14px;
-            min-height: 0;
-            overflow: auto;
-        }
+      .panel-body {
+        padding: 16px 18px 18px;
+        display: flex;
+        flex-direction: column;
+        gap: 14px;
+        min-height: 0;
+        overflow: auto;
+      }
 
-        .tool-section,
-        .status-stack,
-        .action-list {
-            display: flex;
-            flex-direction: column;
-            gap: 8px;
-        }
+      .tool-section,
+      .status-stack,
+      .action-list {
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+      }
 
-        .tool-section-label {
-            font-size: 12px;
-            color: var(--text-sub);
-            letter-spacing: 0.08em;
-            text-transform: uppercase;
-        }
+      .tool-section-label {
+        font-size: 12px;
+        color: var(--text-sub);
+        letter-spacing: 0.08em;
+        text-transform: uppercase;
+      }
 
-        .tool-grid {
-            display: grid;
-            grid-template-columns: repeat(2, minmax(0, 1fr));
-            gap: 8px;
-        }
+      .tool-section-label-sub {
+        font-size: 12px;
+        color: var(--text-sub);
+        letter-spacing: 0.08em;
+        text-transform: uppercase;
+        line-height: 1.75;
+      }
 
-        .tool-card-btn {
-            appearance: none;
-            border: 1px solid rgba(193, 205, 219, 0.9);
-            background: rgba(255, 255, 255, 0.96);
-            border-radius: 14px;
-            padding: 10px 12px;
-            text-align: left;
-            cursor: pointer;
-            transition: all 0.18s ease;
-            color: var(--text-main);
-            display: flex;
-            flex-direction: column;
-            gap: 4px;
-            min-height: 68px;
-        }
+      .tool-grid {
+        display: grid;
+        grid-template-columns: repeat(2, minmax(0, 1fr));
+        gap: 8px;
+      }
 
-        .tool-card-btn.active {
-            border-color: rgba(55, 127, 212, 0.45);
-            background: rgba(235, 244, 253, 0.98);
-            box-shadow: 0 8px 18px rgba(47, 121, 214, 0.14);
-        }
+      .tool-el-popover {
+        height: 100%;
+      }
 
-        .tool-card-btn strong {
-            font-size: 13px;
-            font-weight: 700;
-        }
+      .tool-el-popover > .el-popover__reference-wrapper {
+        display: block;
+        height: 100%;
+      }
 
-        .tool-card-btn span {
-            font-size: 12px;
-            color: var(--text-sub);
-            line-height: 1.5;
-        }
+      .tool-el-popover .tool-card-btn {
+        height: 100%;
+      }
 
-        .status-card,
-        .selection-summary,
-        .note-card {
-            border-radius: 16px;
-            border: 1px solid rgba(218, 227, 236, 0.92);
-            background: rgba(248, 251, 254, 0.92);
-            padding: 12px 14px;
-        }
+      .tool-card-btn {
+        appearance: none;
+        border: 1px solid rgba(193, 205, 219, 0.9);
+        background: rgba(255, 255, 255, 0.96);
+        border-radius: 14px;
+        padding: 10px 12px;
+        text-align: left;
+        cursor: pointer;
+        transition: all 0.18s ease;
+        color: var(--text-main);
+        display: flex;
+        flex-direction: column;
+        gap: 4px;
+        min-height: 68px;
+      }
 
-        .status-card strong,
-        .selection-summary strong,
-        .note-card strong {
-            display: block;
-            font-size: 13px;
-            margin-bottom: 6px;
-        }
+      .tool-card-btn.active {
+        border-color: rgba(55, 127, 212, 0.45);
+        background: rgba(235, 244, 253, 0.98);
+        box-shadow: 0 8px 18px rgba(47, 121, 214, 0.14);
+      }
 
-        .status-card span,
-        .selection-summary span,
-        .note-card span {
-            display: block;
-            font-size: 12px;
-            color: var(--text-sub);
-            line-height: 1.65;
-        }
+      .tool-card-btn strong {
+        font-size: 13px;
+        font-weight: 700;
+      }
 
-        .selection-summary strong {
-            font-size: 14px;
-            color: var(--text-main);
-        }
+      .tool-card-btn span {
+        font-size: 12px;
+        color: var(--text-sub);
+        line-height: 1.5;
+      }
 
-        .canvas-toolbar {
-            padding: 16px 18px 14px;
-            border-bottom: 1px solid rgba(221, 230, 239, 0.94);
-            display: flex;
-            align-items: flex-start;
-            justify-content: space-between;
-            gap: 12px;
-            flex-wrap: wrap;
-        }
+      .status-card,
+      .selection-summary,
+      .note-card {
+        border-radius: 16px;
+        border: 1px solid rgba(218, 227, 236, 0.92);
+        background: rgba(248, 251, 254, 0.92);
+        padding: 12px 14px;
+      }
 
-        .canvas-toolbar-main {
-            flex: 1 1 420px;
-            display: flex;
-            flex-direction: column;
-            gap: 8px;
-        }
+      .status-card strong,
+      .selection-summary strong,
+      .note-card strong {
+        display: block;
+        font-size: 13px;
+        margin-bottom: 6px;
+      }
 
-        .canvas-toolbar-title {
-            display: flex;
-            flex-direction: column;
-            gap: 4px;
-        }
+      .status-card span,
+      .selection-summary span,
+      .note-card span {
+        display: block;
+        font-size: 12px;
+        color: var(--text-sub);
+        line-height: 1.65;
+      }
 
-        .canvas-toolbar-title h1 {
-            margin: 0;
-            font-size: 24px;
-            font-weight: 700;
-            letter-spacing: 0.3px;
-        }
+      .selection-summary strong {
+        font-size: 14px;
+        color: var(--text-main);
+      }
 
-        .canvas-toolbar-title span {
-            font-size: 13px;
-            line-height: 1.65;
-            color: var(--text-sub);
-        }
+      .canvas-toolbar {
+        padding: 16px 18px 14px;
+        border-bottom: 1px solid rgba(221, 230, 239, 0.94);
+        display: flex;
+        align-items: flex-start;
+        justify-content: space-between;
+        gap: 12px;
+        flex-wrap: wrap;
+      }
 
-        .canvas-toolbar-meta,
-        .canvas-toolbar-actions {
-            display: flex;
-            align-items: center;
-            gap: 8px;
-            flex-wrap: wrap;
-        }
+      .canvas-toolbar-main {
+        flex: 1 1 420px;
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+      }
 
-        .canvas-toolbar-actions {
-            justify-content: flex-end;
-            flex: 0 1 760px;
-        }
+      .canvas-toolbar-title {
+        display: flex;
+        flex-direction: column;
+        gap: 4px;
+      }
 
-        .canvas-toolbar-actions .el-input__inner,
-        .canvas-toolbar-actions .el-button {
-            border-radius: 10px;
-        }
+      .canvas-toolbar-title h1 {
+        margin: 0;
+        font-size: 24px;
+        font-weight: 700;
+        letter-spacing: 0.3px;
+      }
 
-        .canvas-meta {
-            font-size: 12px;
-            color: var(--text-sub);
-        }
+      .canvas-toolbar-title span {
+        font-size: 13px;
+        line-height: 1.65;
+        color: var(--text-sub);
+      }
 
-        .canvas-card {
-            flex: 1 1 auto;
-            min-width: 0;
-            min-height: 0;
-        }
+      .canvas-toolbar-meta,
+      .canvas-toolbar-actions {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        flex-wrap: wrap;
+      }
 
-        .canvas-wrap {
-            position: relative;
-            flex: 1 1 auto;
-            min-height: 0;
-            background:
-                linear-gradient(180deg, rgba(245, 249, 252, 0.95) 0%, rgba(251, 252, 254, 0.98) 100%);
-        }
+      .canvas-toolbar-actions {
+        justify-content: flex-end;
+        flex: 0 1 760px;
+      }
 
-        .canvas-stage {
-            position: absolute;
-            inset: 0;
-            overflow: hidden;
-            background: #f6f9fc;
-        }
+      .canvas-toolbar-actions .el-input__inner,
+      .canvas-toolbar-actions .el-button {
+        border-radius: 10px;
+      }
 
-        .canvas-host {
-            position: absolute;
-            inset: 0;
-            background: #f6f9fc;
-        }
+      .canvas-meta {
+        font-size: 12px;
+        color: var(--text-sub);
+      }
 
-        .canvas-overlay-layer {
-            position: absolute;
-            inset: 0;
-            pointer-events: none;
-            z-index: 5;
-        }
+      .canvas-card {
+        flex: 1 1 auto;
+        min-width: 0;
+        min-height: 0;
+      }
 
-        .canvas-loading-mask {
-            position: absolute;
-            inset: 0;
-            z-index: 4;
-            display: flex;
-            align-items: center;
-            justify-content: center;
-            background: rgba(246, 249, 252, 0.82);
-            backdrop-filter: blur(2px);
-        }
+      .canvas-wrap {
+        position: relative;
+        flex: 1 1 auto;
+        min-height: 0;
+        background: linear-gradient(
+          180deg,
+          rgba(245, 249, 252, 0.95) 0%,
+          rgba(251, 252, 254, 0.98) 100%
+        );
+      }
 
-        .canvas-loading-card {
-            display: flex;
-            flex-direction: column;
-            align-items: center;
-            gap: 8px;
-            min-width: 220px;
-            padding: 18px 22px;
-            border-radius: 18px;
-            background: rgba(255, 255, 255, 0.96);
-            border: 1px solid rgba(55, 127, 212, 0.16);
-            box-shadow: 0 18px 40px rgba(66, 94, 136, 0.12);
-            color: var(--text-main);
-        }
+      .canvas-stage {
+        position: absolute;
+        inset: 0;
+        overflow: hidden;
+        background: #f6f9fc;
+      }
 
-        .canvas-loading-card strong {
-            font-size: 18px;
-        }
+      .canvas-host {
+        position: absolute;
+        inset: 0;
+        background: #f6f9fc;
+      }
 
-        .canvas-loading-card span {
-            font-size: 13px;
-            color: var(--text-sub);
-        }
+      .canvas-overlay-layer {
+        position: absolute;
+        inset: 0;
+        pointer-events: none;
+        z-index: 5;
+      }
 
-        .overlay-panel {
-            position: absolute;
-            top: 14px;
-            bottom: 14px;
-            width: 300px;
-            display: flex;
-            flex-direction: column;
-            border-radius: 22px;
-            border: 1px solid rgba(214, 224, 236, 0.96);
-            background: #ffffff;
-            box-shadow: 0 8px 18px rgba(31, 55, 82, 0.06);
-            overflow: hidden;
-            pointer-events: auto;
-            contain: layout paint;
-        }
+      .canvas-loading-mask {
+        position: absolute;
+        inset: 0;
+        z-index: 4;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        background: rgba(246, 249, 252, 0.82);
+        backdrop-filter: blur(2px);
+      }
 
+      .canvas-loading-card {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        gap: 8px;
+        min-width: 220px;
+        padding: 18px 22px;
+        border-radius: 18px;
+        background: rgba(255, 255, 255, 0.96);
+        border: 1px solid rgba(55, 127, 212, 0.16);
+        box-shadow: 0 18px 40px rgba(66, 94, 136, 0.12);
+        color: var(--text-main);
+      }
+
+      .canvas-loading-card strong {
+        font-size: 18px;
+      }
+
+      .canvas-loading-card span {
+        font-size: 13px;
+        color: var(--text-sub);
+      }
+
+      .overlay-panel {
+        position: absolute;
+        top: 14px;
+        bottom: 14px;
+        width: 300px;
+        display: flex;
+        flex-direction: column;
+        border-radius: 22px;
+        border: 1px solid rgba(214, 224, 236, 0.96);
+        background: #ffffff;
+        box-shadow: 0 8px 18px rgba(31, 55, 82, 0.06);
+        overflow: hidden;
+        pointer-events: auto;
+        contain: layout paint;
+      }
+
+      .overlay-left {
+        left: 14px;
+      }
+
+      .overlay-right {
+        right: 14px;
+        width: 340px;
+      }
+
+      .overlay-panel.collapsed {
+        width: 68px;
+        bottom: auto;
+      }
+
+      .overlay-panel.collapsed .panel-body {
+        display: none;
+      }
+
+      .overlay-panel.collapsed .panel-head {
+        align-items: flex-start;
+        padding: 12px 10px;
+      }
+
+      .overlay-panel.collapsed .panel-head h2 {
+        writing-mode: vertical-rl;
+        text-orientation: mixed;
+        font-size: 14px;
+        line-height: 1;
+      }
+
+      .overlay-panel.collapsed .canvas-meta {
+        display: none;
+      }
+
+      .panel-head-actions {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+      }
+
+      .panel-toggle {
+        appearance: none;
+        border: 1px solid rgba(190, 203, 217, 0.96);
+        background: rgba(255, 255, 255, 0.96);
+        color: var(--text-main);
+        width: 30px;
+        height: 30px;
+        border-radius: 10px;
+        cursor: pointer;
+        font-size: 14px;
+        font-weight: 700;
+        line-height: 1;
+      }
+
+      .panel-toggle:hover {
+        border-color: rgba(55, 127, 212, 0.4);
+        color: var(--primary);
+      }
+
+      .prop-grid {
+        display: grid;
+        grid-template-columns: repeat(2, minmax(0, 1fr));
+        gap: 10px;
+      }
+
+      .prop-grid .device-divider {
+        margin: 0px 0 6px;
+      }
+
+      .prop-grid .span-2 {
+        grid-column: span 2;
+      }
+
+      .field-stack {
+        display: flex;
+        flex-direction: column;
+        gap: 6px;
+      }
+
+      .field-label {
+        font-size: 12px;
+        color: var(--text-sub);
+        line-height: 1.4;
+      }
+
+      .field-required {
+        color: #d85b52;
+        font-weight: 700;
+      }
+
+      .field-help {
+        font-size: 12px;
+        color: var(--text-sub);
+        line-height: 1.6;
+      }
+
+      .direction-grid {
+        display: grid;
+        grid-template-columns: repeat(4, minmax(0, 1fr));
+        gap: 8px;
+      }
+
+      .direction-chip {
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        gap: 6px;
+        min-height: 34px;
+        border: 1px solid rgba(55, 127, 212, 0.18);
+        border-radius: 12px;
+        background: #fff;
+        color: var(--text-main);
+        cursor: pointer;
+        transition: all 0.16s ease;
+      }
+
+      .direction-chip:hover {
+        border-color: rgba(55, 127, 212, 0.45);
+        color: var(--primary);
+      }
+
+      .direction-chip.active {
+        border-color: rgba(55, 127, 212, 0.85);
+        background: rgba(85, 145, 227, 0.12);
+        color: var(--primary);
+        box-shadow: inset 0 0 0 1px rgba(85, 145, 227, 0.1);
+      }
+
+      .direction-arrow {
+        font-size: 16px;
+        font-weight: 700;
+        line-height: 1;
+      }
+
+      .check-grid {
+        display: grid;
+        grid-template-columns: repeat(2, minmax(0, 1fr));
+        gap: 8px 12px;
+      }
+
+      .json-box {
+        font-family: Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
+      }
+
+      .footer-note {
+        font-size: 12px;
+        color: var(--text-sub);
+        line-height: 1.75;
+      }
+
+      @media (max-width: 1380px) {
         .overlay-left {
-            left: 14px;
+          width: 260px;
         }
 
         .overlay-right {
-            right: 14px;
-            width: 340px;
+          width: 300px;
+        }
+      }
+
+      @media (max-width: 980px) {
+        .canvas-card {
+          min-height: 0;
         }
 
-        .overlay-panel.collapsed {
-            width: 68px;
-            bottom: auto;
+        .canvas-wrap {
+          min-height: 0;
         }
 
-        .overlay-panel.collapsed .panel-body {
-            display: none;
+        .overlay-left,
+        .overlay-right {
+          width: min(280px, calc(100% - 28px));
         }
-
-        .overlay-panel.collapsed .panel-head {
-            align-items: flex-start;
-            padding: 12px 10px;
-        }
-
-        .overlay-panel.collapsed .panel-head h2 {
-            writing-mode: vertical-rl;
-            text-orientation: mixed;
-            font-size: 14px;
-            line-height: 1;
-        }
-
-        .overlay-panel.collapsed .canvas-meta {
-            display: none;
-        }
-
-        .panel-head-actions {
-            display: flex;
-            align-items: center;
-            gap: 8px;
-        }
-
-        .panel-toggle {
-            appearance: none;
-            border: 1px solid rgba(190, 203, 217, 0.96);
-            background: rgba(255, 255, 255, 0.96);
-            color: var(--text-main);
-            width: 30px;
-            height: 30px;
-            border-radius: 10px;
-            cursor: pointer;
-            font-size: 14px;
-            font-weight: 700;
-            line-height: 1;
-        }
-
-        .panel-toggle:hover {
-            border-color: rgba(55, 127, 212, 0.4);
-            color: var(--primary);
-        }
-
-        .prop-grid {
-            display: grid;
-            grid-template-columns: repeat(2, minmax(0, 1fr));
-            gap: 10px;
-        }
-
-        .prop-grid .span-2 {
-            grid-column: span 2;
-        }
-
-        .field-stack {
-            display: flex;
-            flex-direction: column;
-            gap: 6px;
-        }
-
-        .field-label {
-            font-size: 12px;
-            color: var(--text-sub);
-            line-height: 1.4;
-        }
-
-        .field-required {
-            color: #d85b52;
-            font-weight: 700;
-        }
-
-        .field-help {
-            font-size: 12px;
-            color: var(--text-sub);
-            line-height: 1.6;
-        }
-
-        .direction-grid {
-            display: grid;
-            grid-template-columns: repeat(4, minmax(0, 1fr));
-            gap: 8px;
-        }
-
-        .direction-chip {
-            display: inline-flex;
-            align-items: center;
-            justify-content: center;
-            gap: 6px;
-            min-height: 34px;
-            border: 1px solid rgba(55, 127, 212, 0.18);
-            border-radius: 12px;
-            background: #fff;
-            color: var(--text-main);
-            cursor: pointer;
-            transition: all 0.16s ease;
-        }
-
-        .direction-chip:hover {
-            border-color: rgba(55, 127, 212, 0.45);
-            color: var(--primary);
-        }
-
-        .direction-chip.active {
-            border-color: rgba(55, 127, 212, 0.85);
-            background: rgba(85, 145, 227, 0.12);
-            color: var(--primary);
-            box-shadow: inset 0 0 0 1px rgba(85, 145, 227, 0.1);
-        }
-
-        .direction-arrow {
-            font-size: 16px;
-            font-weight: 700;
-            line-height: 1;
-        }
-
-        .check-grid {
-            display: grid;
-            grid-template-columns: repeat(2, minmax(0, 1fr));
-            gap: 8px 12px;
-        }
-
-        .json-box {
-            font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace;
-        }
-
-        .footer-note {
-            font-size: 12px;
-            color: var(--text-sub);
-            line-height: 1.75;
-        }
-
-        @media (max-width: 1380px) {
-            .overlay-left {
-                width: 260px;
-            }
-
-            .overlay-right {
-                width: 300px;
-            }
-        }
-
-        @media (max-width: 980px) {
-            .canvas-card {
-                min-height: 0;
-            }
-
-            .canvas-wrap {
-                min-height: 0;
-            }
-
-            .overlay-left,
-            .overlay-right {
-                width: min(280px, calc(100% - 28px));
-            }
-        }
+      }
     </style>
-</head>
-<body>
-<div id="app" class="editor-shell" v-cloak>
-    <section class="workspace">
+  </head>
+
+  <body>
+    <div id="app" class="editor-shell" v-cloak>
+      <section class="workspace">
         <main class="panel-card canvas-panel canvas-card">
-            <div class="canvas-toolbar">
-                    <div class="canvas-toolbar-main">
-                        <div class="canvas-toolbar-title">
-                            <h1>WCS鍦板浘缂栬緫鍣�</h1>
-                        </div>
-                    <div class="canvas-toolbar-meta">
-                        <span class="canvas-meta">妤煎眰: {{ currentLev ? currentLev + 'F' : '--' }}</span>
-                        <span class="canvas-meta">缂╂斁: {{ viewPercent }}%</span>
-                        <span class="canvas-meta">妯″紡: {{ toolLabel(activeTool) }}</span>
-                        <span class="canvas-meta">閫変腑: {{ selectedIds.length }}</span>
-                        <span class="canvas-meta">鍏冪礌: {{ doc ? doc.elements.length : 0 }}</span>
-                        <span class="canvas-meta">鐢诲竷: {{ Math.round(doc ? doc.canvasWidth : 0) }} x {{ Math.round(doc ? doc.canvasHeight : 0) }}</span>
-                        <span class="canvas-meta">娓叉煋鍊嶇巼: {{ formatNumber(pixiResolution) }}x</span>
-                        <span class="canvas-meta">FPS: {{ fpsText }}</span>
-                    </div>
-                </div>
-                <div class="canvas-toolbar-actions">
-                    <el-select v-model="floorPickerLev" size="small" placeholder="閫夋嫨妤煎眰" @change="handleFloorChange" style="width: 120px;">
-                        <el-option v-for="lev in levOptions" :key="'lev-' + lev" :label="lev + 'F'" :value="lev"></el-option>
-                    </el-select>
-                    <el-button size="small" plain @click="openBlankDialog">鏂板缓鑷敱鐢诲竷</el-button>
-                    <el-button size="small" plain @click="triggerImportExcel">瀵煎叆 Excel</el-button>
-                    <el-button size="small" plain @click="triggerImportMap">瀵煎叆鍦板浘</el-button>
-                    <el-button size="small" plain @click="exportMapPackage">瀵煎嚭鍦板浘</el-button>
-                    <el-button size="small" plain @click="loadCurrentFloor">閲嶆柊璇诲彇</el-button>
-                    <el-button size="small" plain @click="fitContent">閫傞厤鍏ㄥ浘</el-button>
-                    <el-button size="small" plain @click="resetView">鍥炲埌鐢诲竷</el-button>
-                    <el-button size="small" @click="undo" :disabled="undoStack.length === 0">鎾ら攢</el-button>
-                    <el-button size="small" @click="redo" :disabled="redoStack.length === 0">閲嶅仛</el-button>
-                    <el-button size="small" type="primary" plain :loading="savingAll" :disabled="dirtyDraftCount === 0 || saving" @click="saveAllDocs">淇濆瓨鍏ㄩ儴妤煎眰<span v-if="dirtyDraftCount > 0">({{ dirtyDraftCount }})</span></el-button>
-                    <el-button size="small" type="primary" :loading="saving" :disabled="savingAll" @click="saveDoc">淇濆瓨褰撳墠妤煎眰</el-button>
-                </div>
+          <div class="canvas-toolbar">
+            <div class="canvas-toolbar-main">
+              <div class="canvas-toolbar-title">
+                <h1>PixiJS 鑷敱鐢诲竷鍦板浘缂栬緫鍣�</h1>
+              </div>
+              <div class="canvas-toolbar-meta">
+                <span class="canvas-meta">妤煎眰: {{ currentLev ? currentLev + 'F' : '--' }}</span>
+                <span class="canvas-meta">缂╂斁: {{ viewPercent }}%</span>
+                <span class="canvas-meta">妯″紡: {{ toolLabel(activeTool) }}</span>
+                <span class="canvas-meta">閫変腑: {{ selectedIds.length }}</span>
+                <span class="canvas-meta">鍏冪礌: {{ doc ? doc.elements.length : 0 }}</span>
+                <span class="canvas-meta"
+                  >鐢诲竷: {{ Math.round(doc ? doc.canvasWidth : 0) }} x {{ Math.round(doc ?
+                  doc.canvasHeight : 0) }}</span
+                >
+                <span class="canvas-meta">娓叉煋鍊嶇巼: {{ formatNumber(pixiResolution) }}x</span>
+                <span class="canvas-meta">FPS: {{ fpsText }}</span>
+              </div>
             </div>
-                <div class="canvas-wrap">
-                    <div class="canvas-stage" ref="canvasStage">
-                        <div class="canvas-host" ref="canvasHost"></div>
-                        <div v-if="loadingFloor" class="canvas-loading-mask">
-                            <div class="canvas-loading-card">
-                                <strong>姝e湪鍔犺浇 {{ switchingFloorLev || floorPickerLev || currentLev || '--' }}F</strong>
-                                <span>鐢诲竷鍜岀紦瀛樻鍦ㄥ垏鎹紝璇风◢鍊欍��</span>
-                            </div>
-                        </div>
-                    </div>
-                <div class="canvas-overlay-layer">
-                    <aside class="overlay-panel overlay-left" :class="{ collapsed: toolPanelCollapsed }">
-                        <div class="panel-head">
-                            <h2>宸ュ叿闈㈡澘</h2>
-                            <div class="panel-head-actions">
-                                <span class="canvas-meta">{{ toolLabel(activeTool) }}</span>
-                                <button type="button" class="panel-toggle" @click="toggleToolPanel">{{ toolPanelCollapsed ? '>' : '<' }}</button>
-                            </div>
-                        </div>
-                        <div class="panel-body">
-                            <div class="tool-section">
-                                <div class="tool-section-label">浜や簰</div>
-                                <div class="tool-grid">
-                                    <button
-                                        v-for="tool in interactionTools"
-                                        :key="tool.key"
-                                        type="button"
-                                        class="tool-card-btn"
-                                        :class="{ active: activeTool === tool.key }"
-                                        @click="setTool(tool.key)">
-                                        <strong>{{ tool.label }}</strong>
-                                        <span>{{ tool.desc }}</span>
-                                    </button>
-                                </div>
-                            </div>
-
-                            <div class="tool-section">
-                                <div class="tool-section-label">缁樺埗鍏冪礌</div>
-                                <div class="tool-grid">
-                                    <button
-                                        v-for="tool in drawTools"
-                                        :key="tool.key"
-                                        type="button"
-                                        class="tool-card-btn"
-                                        :class="{ active: activeTool === tool.key }"
-                                        @click="setTool(tool.key)">
-                                        <strong>{{ tool.label }}</strong>
-                                        <span>{{ tool.desc }}</span>
-                                    </button>
-                                </div>
-                            </div>
-
-                            <div class="tool-section">
-                                <div class="tool-section-label">缂栬緫鍔ㄤ綔</div>
-                                <div class="action-list">
-                                    <el-button size="small" plain @click="copySelection" :disabled="selectedIds.length === 0">澶嶅埗</el-button>
-                                    <el-button size="small" plain @click="pasteClipboard" :disabled="clipboard.length === 0">绮樿创</el-button>
-                                    <el-button size="small" plain @click="duplicateSelection" :disabled="selectedIds.length === 0">澶嶅埗鍋忕Щ</el-button>
-                                    <el-button size="small" plain @click="fitSelection" :disabled="selectedIds.length === 0">鑱氱劍閫変腑</el-button>
-                                    <el-button size="small" type="danger" plain @click="deleteSelection" :disabled="selectedIds.length === 0">鍒犻櫎閫変腑</el-button>
-                                </div>
-                            </div>
-
-                            <div class="status-stack">
-                                <div class="status-card">
-                                    <strong>蹇嵎閿�</strong>
-                                    <span>`Delete` 鍒犻櫎锛宍Ctrl/Cmd + Z` 鎾ら攢锛宍Ctrl/Cmd + Shift + Z` / `Ctrl/Cmd + Y` 閲嶅仛銆�</span>
-                                    <span>`Ctrl/Cmd + C / V` 澶嶅埗绮樿创锛屾寜浣忕┖鏍煎彲涓存椂鎷栧姩鐢诲竷锛宍Shift + 鐐瑰嚮` 鍙鍑忓崟涓�変腑銆�</span>
-                                    <span>`闃靛垪` 宸ュ叿: 鍏堥�変腑涓�涓揣鏋� / 杞ㄩ亾妯℃澘锛屽啀鎷栦竴鏉℃按骞虫垨绔栫洿绾胯嚜鍔ㄨˉ榻愪竴鎺掞紱璐ф灦浼氭寜 `鎺�-鍒梎 瑙勫垯缁х画缂栧彿銆�</span>
-                                </div>
-                                <div class="status-card">
-                                    <strong>褰撳墠鐘舵��</strong>
-                                    <span>妤煎眰: {{ currentLev ? currentLev + 'F' : '--' }}</span>
-                                    <span>鎸囬拡: {{ pointerStatus }}</span>
-                                    <span v-if="arrayPreviewCount > 0">闃靛垪棰勮: 灏嗙敓鎴� {{ arrayPreviewCount }} 涓�</span>
-                                    <span>鏈繚瀛�: {{ isDirty ? '鏄�' : '鍚�' }}</span>
-                                </div>
-                            </div>
-                        </div>
-                    </aside>
-
-                    <aside class="overlay-panel overlay-right" :class="{ collapsed: inspectorPanelCollapsed }">
-                        <div class="panel-head">
-                            <h2>灞炴�ч潰鏉�</h2>
-                            <div class="panel-head-actions">
-                                <span class="canvas-meta" v-if="singleSelectedElement">{{ singleSelectedElement.type }}</span>
-                                <span class="canvas-meta" v-else>{{ selectedIds.length > 1 ? '澶氶��' : '鐢诲竷' }}</span>
-                                <button type="button" class="panel-toggle" @click="toggleInspectorPanel">{{ inspectorPanelCollapsed ? '<' : '>' }}</button>
-                            </div>
-                        </div>
-                        <div class="panel-body">
-                            <div class="selection-summary">
-                                <strong v-if="singleSelectedElement">鍗曞厓绱犵紪杈�</strong>
-                                <strong v-else-if="selectedIds.length > 1">澶氶�夌紪杈�</strong>
-                                <strong v-else>鏈�変腑鍏冪礌</strong>
-                                <span v-if="singleSelectedElement">浣嶇疆 {{ formatNumber(singleSelectedElement.x) }}, {{ formatNumber(singleSelectedElement.y) }} | 灏哄 {{ formatNumber(singleSelectedElement.width) }} x {{ formatNumber(singleSelectedElement.height) }}</span>
-                                <span v-else-if="selectedIds.length > 1">褰撳墠宸查�� {{ selectedIds.length }} 涓厓绱狅紝鍙暣浣撶Щ鍔ㄣ�佸鍒舵垨鍒犻櫎銆�</span>
-                                <span v-else-if="activeTool === 'array'">鍏堥�変腑涓�涓揣鏋� / 杞ㄩ亾鍏冪礌锛屽啀鎷栦竴鏉$嚎鐢熸垚闃靛垪銆�</span>
-                                <span v-else>閫夋嫨宸ュ叿鍚庣偣鍑诲厓绱狅紝鎴栧垏鎹㈡閫夊伐鍏锋鍑轰竴缁勫厓绱犮��</span>
-                            </div>
-
-                            <div class="tool-section">
-                                <div class="tool-section-label">鐢诲竷璁剧疆</div>
-                                <div class="prop-grid">
-                                    <el-input v-model.trim="canvasForm.width" size="small" placeholder="鐢诲竷瀹藉害"></el-input>
-                                    <el-input v-model.trim="canvasForm.height" size="small" placeholder="鐢诲竷楂樺害"></el-input>
-                                    <el-button class="span-2" size="small" plain @click="applyCanvasSize">搴旂敤鐢诲竷灏哄</el-button>
-                                </div>
-                            </div>
-
-                            <template v-if="singleSelectedElement">
-                                <div class="tool-section">
-                                    <div class="tool-section-label">鍑犱綍灞炴��</div>
-                                    <div class="prop-grid">
-                                        <el-input size="small" :value="singleSelectedElement.type" disabled></el-input>
-                                        <el-input size="small" :value="singleSelectedElement.id" disabled></el-input>
-                                        <el-input v-model.trim="geometryForm.x" size="small" placeholder="X"></el-input>
-                                        <el-input v-model.trim="geometryForm.y" size="small" placeholder="Y"></el-input>
-                                        <el-input v-model.trim="geometryForm.width" size="small" placeholder="瀹藉害"></el-input>
-                                        <el-input v-model.trim="geometryForm.height" size="small" placeholder="楂樺害"></el-input>
-                                        <el-button class="span-2" size="small" plain @click="applyGeometry">搴旂敤鍑犱綍</el-button>
-                                    </div>
-                                </div>
-
-                                <div v-if="singleSelectedElement.type === 'devp'" class="tool-section">
-                                    <div class="tool-section-label">杈撻�佺珯鐐归厤缃�</div>
-                                    <div class="prop-grid">
-                                        <div class="field-stack">
-                                            <span class="field-label">绔欏彿</span>
-                                            <el-input v-model.trim="devpForm.stationId" size="small" placeholder="璇疯緭鍏ヨ緭閫佺珯鐐圭珯鍙�"></el-input>
-                                        </div>
-                                        <div class="field-stack">
-                                            <span class="field-label">PLC 缂栧彿</span>
-                                            <el-input v-model.trim="devpForm.deviceNo" size="small" placeholder="璇疯緭鍏ヨ緭閫佺珯鐐� PLC 缂栧彿"></el-input>
-                                        </div>
-                                        <div class="field-stack span-2">
-                                            <span class="field-label">鏂瑰悜</span>
-                                            <div class="direction-grid">
-                                                <button
-                                                    v-for="item in devpDirectionOptions"
-                                                    :key="item.key"
-                                                    type="button"
-                                                    class="direction-chip"
-                                                    :class="{ active: isDevpDirectionActive(item.key) }"
-                                                    @click="toggleDevpDirection(item.key)">
-                                                    <span class="direction-arrow">{{ item.arrow }}</span>
-                                                    <span>{{ item.label }}</span>
-                                                </button>
-                                            </div>
-                                            <div class="field-help">鐐瑰嚮绠ご鍒囨崲鏂瑰悜锛屽彲鍚屾椂閫夋嫨澶氫釜鏂瑰悜銆�</div>
-                                        </div>
-                                        <div class="field-stack span-2">
-                                            <span class="field-label">绔欑偣绫诲瀷</span>
-                                            <div class="check-grid">
-                                                <el-checkbox v-model="devpForm.isBarcodeStation">鏉$爜绔�</el-checkbox>
-                                                <el-checkbox v-model="devpForm.isInStation">鍏ョ珯鐐�</el-checkbox>
-                                                <el-checkbox v-model="devpForm.isOutStation">鍑虹珯鐐�</el-checkbox>
-                                                <el-checkbox v-model="devpForm.runBlockReassign">鍫靛閲嶅垎閰�</el-checkbox>
-                                                <el-checkbox v-model="devpForm.isOutOrder">鍑哄簱鎺掑簭</el-checkbox>
-                                                <el-checkbox v-model="devpForm.isLiftTransfer">椤跺崌绉绘牻</el-checkbox>
-                                            </div>
-                                        </div>
-                                        <div class="field-stack">
-                                            <span class="field-label">鏉$爜绱㈠紩<span v-if="devpRequiresBarcodeIndex" class="field-required"> 蹇呭~</span></span>
-                                            <el-input v-model.trim="devpForm.barcodeIdx" size="small" placeholder="鏉$爜绔欐椂蹇呭~锛屼緥濡� 1"></el-input>
-                                        </div>
-                                        <div class="field-stack">
-                                            <span class="field-label">鏉$爜绔欑珯鍙�<span v-if="devpRequiresBarcodeLink" class="field-required"> 蹇呭~</span></span>
-                                            <el-input v-model.trim="devpForm.barcodeStation" size="small" placeholder="鍏ョ珯鐐规椂蹇呭~锛屽~鍐欐潯鐮佺珯绔欏彿"></el-input>
-                                        </div>
-                                        <div class="field-stack">
-                                            <span class="field-label">鏉$爜绔� PLC 缂栧彿<span v-if="devpRequiresBarcodeLink" class="field-required"> 蹇呭~</span></span>
-                                            <el-input v-model.trim="devpForm.barcodeStationDeviceNo" size="small" placeholder="鍏ョ珯鐐规椂蹇呭~锛屽~鍐欐潯鐮佺珯 PLC 缂栧彿"></el-input>
-                                        </div>
-                                        <div class="field-stack">
-                                            <span class="field-label">閫�鍥炵珯绔欏彿<span v-if="devpRequiresBackStation" class="field-required"> 蹇呭~</span></span>
-                                            <el-input v-model.trim="devpForm.backStation" size="small" placeholder="鏉$爜绔欐椂蹇呭~锛屽~鍐欓��鍥炵珯绔欏彿"></el-input>
-                                        </div>
-                                        <div class="field-stack">
-                                            <span class="field-label">閫�鍥炵珯 PLC 缂栧彿<span v-if="devpRequiresBackStation" class="field-required"> 蹇呭~</span></span>
-                                            <el-input v-model.trim="devpForm.backStationDeviceNo" size="small" placeholder="鏉$爜绔欐椂蹇呭~锛屽~鍐欓��鍥炵珯 PLC 缂栧彿"></el-input>
-                                        </div>
-                                        <div class="footer-note span-2">
-                                            鍕鹃�夆�滃叆绔欑偣鈥濆悗锛屽繀椤诲~鍐欐潯鐮佺珯绔欏彿鍜屾潯鐮佺珯 PLC 缂栧彿銆�
-                                            鍕鹃�夆�滄潯鐮佺珯鈥濆悗锛屽繀椤诲~鍐欐潯鐮佺储寮曘�侀��鍥炵珯绔欏彿鍜岄��鍥炵珯 PLC 缂栧彿銆�
-                                        </div>
-                                        <el-button class="span-2" size="small" type="primary" plain @click="applyDevpForm">搴旂敤杈撻�佺嚎閰嶇疆</el-button>
-                                    </div>
-                                </div>
-
-                                <div v-if="singleSelectedDeviceElement" class="tool-section">
-                                    <div class="tool-section-label">{{ getDeviceConfigLabel(singleSelectedDeviceElement.type) }}</div>
-                                    <div class="prop-grid">
-                                        <el-input size="small" :value="getDeviceConfigKeyLabel(singleSelectedDeviceElement.type, deviceForm.valueKey)" disabled></el-input>
-                                        <el-input v-model.trim="deviceForm.deviceNo" size="small" placeholder="璁惧缂栧彿"></el-input>
-                                        <el-button class="span-2" size="small" type="primary" plain @click="applyDeviceForm">搴旂敤璁惧鍙傛暟</el-button>
-                                    </div>
-                                    <div class="footer-note" style="padding-top: 8px;">
-                                        杩欓噷鍙敼璁惧缂栧彿鐩稿叧閿紝鍘熷 JSON 閲岀殑鍏朵粬瀛楁浼氫繚鐣欙紱涓嬮潰浠嶅彲鐩存帴鏌ョ湅鎴栨墜宸ヤ慨鏀� JSON銆�
-                                    </div>
-                                </div>
-
-                                <div class="tool-section">
-                                    <div class="tool-section-label">{{ singleSelectedElement.type === 'devp' ? '鍘熷 JSON 棰勮' : (singleSelectedDeviceElement ? '鍘熷 JSON 棰勮 / 鎵嬪伐缂栬緫' : '鍊� / JSON 缂栬緫') }}</div>
-                                    <el-input
-                                        class="json-box"
-                                        type="textarea"
-                                        :rows="8"
-                                        v-model="valueEditorText"
-                                        :readonly="singleSelectedElement.type === 'devp'">
-                                    </el-input>
-                                    <el-button
-                                        v-if="singleSelectedElement.type !== 'devp'"
-                                        size="small"
-                                        type="primary"
-                                        plain
-                                        @click="applyRawValue"
-                                    >搴旂敤鍊�</el-button>
-                                </div>
-                            </template>
-
-                            <div v-if="selectedShelfElements.length > 0" class="tool-section">
-                                <div class="tool-section-label">璐ф灦鑷姩濉厖</div>
-                                <div class="prop-grid">
-                                    <el-input v-model.trim="shelfFillForm.startValue" size="small" placeholder="璧峰鍊硷紝渚嬪 12-1"></el-input>
-                                    <el-input size="small" :value="'宸查�夎揣鏋� ' + selectedShelfElements.length + ' 涓�'" disabled></el-input>
-                                    <el-select v-model="shelfFillForm.rowStep" size="small" placeholder="鎺掓柟鍚�">
-                                        <el-option label="涓婂埌涓嬮�掑噺" value="desc"></el-option>
-                                        <el-option label="涓婂埌涓嬮�掑" value="asc"></el-option>
-                                    </el-select>
-                                    <el-select v-model="shelfFillForm.colStep" size="small" placeholder="鍒楁柟鍚�">
-                                        <el-option label="宸﹀埌鍙抽�掑" value="asc"></el-option>
-                                        <el-option label="宸﹀埌鍙抽�掑噺" value="desc"></el-option>
-                                    </el-select>
-                                    <el-button class="span-2" size="small" type="primary" plain @click="applyShelfAutoFill">鎸夋帓鍒楀~鍏呰揣鏋跺��</el-button>
-                                </div>
-                                <div class="footer-note" style="padding-top: 8px;">
-                                    浼氭寜閫変腑璐ф灦鐨勫疄闄呯┖闂存帓鍒楀垎缁勫~鍏呫�傞粯璁よ鍒欐槸涓婂埌涓嬫帓鍙烽�掑噺銆佸乏鍒板彸鍒楀彿閫掑銆�
-                                </div>
-                            </div>
-                        </div>
-                    </aside>
-                </div>
+            <div class="canvas-toolbar-actions">
+              <el-select
+                v-model="floorPickerLev"
+                size="small"
+                placeholder="閫夋嫨妤煎眰"
+                @change="handleFloorChange"
+                style="width: 120px"
+              >
+                <el-option
+                  v-for="lev in levOptions"
+                  :key="'lev-' + lev"
+                  :label="lev + 'F'"
+                  :value="lev"
+                ></el-option>
+              </el-select>
+              <el-button size="small" plain @click="openBlankDialog">鏂板缓鑷敱鐢诲竷</el-button>
+              <!-- <el-button size="small" plain @click="triggerImportExcel">瀵煎叆 Excel</el-button> -->
+              <el-button size="small" plain @click="triggerImportMap">瀵煎叆鍦板浘</el-button>
+              <el-button size="small" plain @click="exportMapPackage">瀵煎嚭鍦板浘</el-button>
+              <el-button size="small" plain @click="loadCurrentFloor">閲嶆柊璇诲彇</el-button>
+              <el-button size="small" plain @click="fitContent">閫傞厤鍏ㄥ浘</el-button>
+              <el-button size="small" plain @click="resetView">鍥炲埌鐢诲竷</el-button>
+              <el-button size="small" @click="undo" :disabled="undoStack.length === 0"
+                >鎾ら攢</el-button
+              >
+              <el-button size="small" @click="redo" :disabled="redoStack.length === 0"
+                >閲嶅仛</el-button
+              >
+              <el-button
+                size="small"
+                type="primary"
+                plain
+                :loading="savingAll"
+                :disabled="dirtyDraftCount === 0 || saving"
+                @click="saveAllDocs"
+                >淇濆瓨鍏ㄩ儴妤煎眰<span v-if="dirtyDraftCount > 0"
+                  >({{ dirtyDraftCount }})</span
+                ></el-button
+              >
+              <el-button
+                size="small"
+                type="primary"
+                :loading="saving"
+                :disabled="savingAll"
+                @click="saveDoc"
+                >淇濆瓨褰撳墠妤煎眰</el-button
+              >
             </div>
+          </div>
+          <div class="canvas-wrap">
+            <div class="canvas-stage" ref="canvasStage">
+              <div class="canvas-host" ref="canvasHost"></div>
+              <div v-if="loadingFloor" class="canvas-loading-mask">
+                <div class="canvas-loading-card">
+                  <strong
+                    >姝e湪鍔犺浇 {{ switchingFloorLev || floorPickerLev || currentLev || '--'
+                    }}F</strong
+                  >
+                  <span>鐢诲竷鍜岀紦瀛樻鍦ㄥ垏鎹紝璇风◢鍊欍��</span>
+                </div>
+              </div>
+            </div>
+            <div class="canvas-overlay-layer">
+              <aside class="overlay-panel overlay-left" :class="{ collapsed: toolPanelCollapsed }">
+                <div class="panel-head">
+                  <h2>宸ュ叿闈㈡澘</h2>
+                  <div class="panel-head-actions">
+                    <span class="canvas-meta">{{ toolLabel(activeTool) }}</span>
+                    <button type="button" class="panel-toggle" @click="toggleToolPanel">
+                      {{ toolPanelCollapsed ? '>' : '<' }}
+                    </button>
+                  </div>
+                </div>
+                <div class="panel-body">
+                  <div class="tool-section">
+                    <div class="tool-section-label">浜や簰</div>
+                    <div class="tool-grid">
+                      <button
+                        v-for="tool in interactionTools"
+                        :key="tool.key"
+                        type="button"
+                        class="tool-card-btn"
+                        :class="{ active: activeTool === tool.key }"
+                        @click="setTool(tool.key)"
+                      >
+                        <strong>{{ tool.label }}</strong>
+                        <span>{{ tool.desc }}</span>
+                      </button>
+                    </div>
+                  </div>
+
+                  <div class="tool-section">
+                    <div class="tool-section-label">缁樺埗鍏冪礌</div>
+                    <div class="tool-grid">
+                      <template v-for="tool in drawTools">
+                        <el-popover
+                          class="tool-el-popover"
+                          v-if="tool.key === 'annulus'"
+                          :key="tool.key"
+                          placement="right"
+                          title="璇烽�夋嫨缁樺埗鐨勫舰鐘�"
+                          width="520"
+                          trigger="click"
+                        >
+                          <el-radio-group v-model="annulusShape" size="mini">
+                            <el-radio-button label="rect">鍦嗚鐭╁舰</el-radio-button>
+                            <el-radio-button label="L1">鍦嗚L鍨�</el-radio-button>
+                            <el-radio-button label="L2">鍦嗚L鍨嬫棆杞�90掳</el-radio-button>
+                            <el-radio-button label="L3">鍦嗚L鍨嬫棆杞�180掳</el-radio-button>
+                            <el-radio-button label="L4">鍦嗚L鍨嬫棆杞�270掳</el-radio-button>
+                          </el-radio-group>
+                          <button
+                            slot="reference"
+                            type="button"
+                            class="tool-card-btn"
+                            :class="{ active: activeTool === tool.key }"
+                            @click="setTool(tool.key)"
+                          >
+                            <strong>{{ tool.label }}</strong>
+                            <span>{{ tool.desc }}</span>
+                          </button>
+                        </el-popover>
+                        <button
+                          v-else
+                          :key="tool.key"
+                          type="button"
+                          class="tool-card-btn"
+                          :class="{ active: activeTool === tool.key }"
+                          @click="setTool(tool.key)"
+                        >
+                          <strong>{{ tool.label }}</strong>
+                          <span>{{ tool.desc }}</span>
+                        </button>
+                      </template>
+                    </div>
+                  </div>
+
+                  <div class="tool-section">
+                    <div class="tool-section-label">缂栬緫鍔ㄤ綔</div>
+                    <div class="action-list">
+                      <el-button
+                        size="small"
+                        plain
+                        @click="copySelection"
+                        :disabled="selectedIds.length === 0"
+                        >澶嶅埗</el-button
+                      >
+                      <el-button
+                        size="small"
+                        plain
+                        @click="pasteClipboard"
+                        :disabled="clipboard.length === 0"
+                        >绮樿创</el-button
+                      >
+                      <el-button
+                        size="small"
+                        plain
+                        @click="duplicateSelection"
+                        :disabled="selectedIds.length === 0"
+                        >澶嶅埗鍋忕Щ</el-button
+                      >
+                      <el-button
+                        size="small"
+                        plain
+                        @click="fitSelection"
+                        :disabled="selectedIds.length === 0"
+                        >鑱氱劍閫変腑</el-button
+                      >
+                      <el-button
+                        size="small"
+                        type="danger"
+                        plain
+                        @click="deleteSelection"
+                        :disabled="selectedIds.length === 0"
+                        >鍒犻櫎閫変腑</el-button
+                      >
+                    </div>
+                  </div>
+
+                  <div class="status-stack">
+                    <div class="status-card">
+                      <strong>蹇嵎閿�</strong>
+                      <span
+                        >`Delete` 鍒犻櫎锛宍Ctrl/Cmd + Z` 鎾ら攢锛宍Ctrl/Cmd + Shift + Z` / `Ctrl/Cmd + Y`
+                        閲嶅仛銆�</span
+                      >
+                      <span
+                        >`Ctrl/Cmd + C / V` 澶嶅埗绮樿创锛屾寜浣忕┖鏍煎彲涓存椂鎷栧姩鐢诲竷锛宍Shift + 鐐瑰嚮`
+                        鍙鍑忓崟涓�変腑銆�</span
+                      >
+                      <span
+                        >`闃靛垪` 宸ュ叿: 鍏堥�変腑涓�涓揣鏋� /
+                        缁翠慨绔欏彴妯℃澘锛屽啀鎷栦竴鏉℃按骞虫垨绔栫洿绾胯嚜鍔ㄨˉ榻愪竴鎺掞紱璐ф灦浼氭寜 `鎺�-鍒梎
+                        瑙勫垯缁х画缂栧彿銆�</span
+                      >
+                    </div>
+                    <div class="status-card">
+                      <strong>褰撳墠鐘舵��</strong>
+                      <span>妤煎眰: {{ currentLev ? currentLev + 'F' : '--' }}</span>
+                      <span>鎸囬拡: {{ pointerStatus }}</span>
+                      <span v-if="arrayPreviewCount > 0"
+                        >闃靛垪棰勮: 灏嗙敓鎴� {{ arrayPreviewCount }} 涓�</span
+                      >
+                      <span>鏈繚瀛�: {{ isDirty ? '鏄�' : '鍚�' }}</span>
+                    </div>
+                  </div>
+                </div>
+              </aside>
+
+              <aside
+                class="overlay-panel overlay-right"
+                :class="{ collapsed: inspectorPanelCollapsed }"
+              >
+                <div class="panel-head">
+                  <h2>灞炴�ч潰鏉�</h2>
+                  <div class="panel-head-actions">
+                    <span class="canvas-meta" v-if="singleSelectedElement"
+                      >{{ singleSelectedElement.type }}</span
+                    >
+                    <span class="canvas-meta" v-else
+                      >{{ selectedIds.length > 1 ? '澶氶��' : '鐢诲竷' }}</span
+                    >
+                    <button type="button" class="panel-toggle" @click="toggleInspectorPanel">
+                      {{ inspectorPanelCollapsed ? '<' : '>' }}
+                    </button>
+                  </div>
+                </div>
+                <div class="panel-body">
+                  <div class="selection-summary">
+                    <strong v-if="singleSelectedElement">鍗曞厓绱犵紪杈�</strong>
+                    <strong v-else-if="selectedIds.length > 1">澶氶�夌紪杈�</strong>
+                    <strong v-else>鏈�変腑鍏冪礌</strong>
+                    <span v-if="singleSelectedElement"
+                      >浣嶇疆 {{ formatNumber(singleSelectedElement.x) }}, {{
+                      formatNumber(singleSelectedElement.y) }} | 灏哄 {{
+                      formatNumber(singleSelectedElement.width) }} x {{
+                      formatNumber(singleSelectedElement.height) }}</span
+                    >
+                    <span v-else-if="selectedIds.length > 1"
+                      >褰撳墠宸查�� {{ selectedIds.length }} 涓厓绱狅紝鍙暣浣撶Щ鍔ㄣ�佸鍒舵垨鍒犻櫎銆�</span
+                    >
+                    <span v-else-if="activeTool === 'array'"
+                      >鍏堥�変腑涓�涓揣鏋� / 缁翠慨绔欏彴鍏冪礌锛屽啀鎷栦竴鏉$嚎鐢熸垚闃靛垪銆�</span
+                    >
+                    <span v-else>閫夋嫨宸ュ叿鍚庣偣鍑诲厓绱狅紝鎴栧垏鎹㈡閫夊伐鍏锋鍑轰竴缁勫厓绱犮��</span>
+                  </div>
+
+                  <div class="tool-section">
+                    <div class="tool-section-label">鐢诲竷璁剧疆</div>
+                    <div class="prop-grid">
+                      <el-input
+                        v-model.trim="canvasForm.width"
+                        size="small"
+                        placeholder="鐢诲竷瀹藉害"
+                      ></el-input>
+                      <el-input
+                        v-model.trim="canvasForm.height"
+                        size="small"
+                        placeholder="鐢诲竷楂樺害"
+                      ></el-input>
+                      <el-button class="span-2" size="small" plain @click="applyCanvasSize"
+                        >搴旂敤鐢诲竷灏哄</el-button
+                      >
+                    </div>
+                  </div>
+
+                  <template v-if="singleSelectedElement">
+                    <div class="tool-section">
+                      <div class="tool-section-label">鍑犱綍灞炴��</div>
+                      <div class="prop-grid">
+                        <el-input
+                          size="small"
+                          :value="singleSelectedElement.type"
+                          disabled
+                        ></el-input>
+                        <el-input
+                          size="small"
+                          :value="singleSelectedElement.id"
+                          disabled
+                        ></el-input>
+                        <el-input
+                          v-model.trim="geometryForm.x"
+                          size="small"
+                          placeholder="X"
+                        ></el-input>
+                        <el-input
+                          v-model.trim="geometryForm.y"
+                          size="small"
+                          placeholder="Y"
+                        ></el-input>
+                        <el-input
+                          v-model.trim="geometryForm.width"
+                          size="small"
+                          placeholder="瀹藉害"
+                        ></el-input>
+                        <el-input
+                          v-model.trim="geometryForm.height"
+                          size="small"
+                          placeholder="楂樺害"
+                        ></el-input>
+                        <el-button class="span-2" size="small" plain @click="applyGeometry"
+                          >搴旂敤鍑犱綍</el-button
+                        >
+                      </div>
+                    </div>
+
+                    <div v-if="singleSelectedElement.type === 'devp'" class="tool-section">
+                      <div class="tool-section-label">杈撻�佺珯鐐归厤缃�</div>
+                      <div class="prop-grid">
+                        <div class="field-stack">
+                          <span class="field-label">绔欏彿</span>
+                          <el-input
+                            v-model.trim="devpForm.stationId"
+                            size="small"
+                            placeholder="璇疯緭鍏ヨ緭閫佺珯鐐圭珯鍙�"
+                          ></el-input>
+                        </div>
+                        <div class="field-stack">
+                          <span class="field-label">PLC 缂栧彿</span>
+                          <el-input
+                            v-model.trim="devpForm.deviceNo"
+                            size="small"
+                            placeholder="璇疯緭鍏ヨ緭閫佺珯鐐� PLC 缂栧彿"
+                          ></el-input>
+                        </div>
+                        <div class="field-stack span-2">
+                          <span class="field-label">鏂瑰悜</span>
+                          <div class="direction-grid">
+                            <button
+                              v-for="item in devpDirectionOptions"
+                              :key="item.key"
+                              type="button"
+                              class="direction-chip"
+                              :class="{ active: isDevpDirectionActive(item.key) }"
+                              @click="toggleDevpDirection(item.key)"
+                            >
+                              <span class="direction-arrow">{{ item.arrow }}</span>
+                              <span>{{ item.label }}</span>
+                            </button>
+                          </div>
+                          <div class="field-help">鐐瑰嚮绠ご鍒囨崲鏂瑰悜锛屽彲鍚屾椂閫夋嫨澶氫釜鏂瑰悜銆�</div>
+                        </div>
+                        <div class="field-stack span-2">
+                          <span class="field-label">绔欑偣绫诲瀷</span>
+                          <div class="check-grid">
+                            <el-checkbox v-model="devpForm.isBarcodeStation">鏉$爜绔�</el-checkbox>
+                            <el-checkbox v-model="devpForm.isInStation">鍏ョ珯鐐�</el-checkbox>
+                            <el-checkbox v-model="devpForm.isOutStation">鍑虹珯鐐�</el-checkbox>
+                            <el-checkbox v-model="devpForm.runBlockReassign"
+                              >鍫靛閲嶅垎閰�</el-checkbox
+                            >
+                            <el-checkbox v-model="devpForm.isOutOrder">鍑哄簱鎺掑簭</el-checkbox>
+                            <el-checkbox v-model="devpForm.isLiftTransfer">椤跺崌绉绘牻</el-checkbox>
+                          </div>
+                        </div>
+                        <div class="field-stack">
+                          <span class="field-label"
+                            >鏉$爜绱㈠紩<span v-if="devpRequiresBarcodeIndex" class="field-required">
+                              蹇呭~</span
+                            ></span
+                          >
+                          <el-input
+                            v-model.trim="devpForm.barcodeIdx"
+                            size="small"
+                            placeholder="鏉$爜绔欐椂蹇呭~锛屼緥濡� 1"
+                          ></el-input>
+                        </div>
+                        <div class="field-stack">
+                          <span class="field-label"
+                            >鏉$爜绔欑珯鍙�<span v-if="devpRequiresBarcodeLink" class="field-required">
+                              蹇呭~</span
+                            ></span
+                          >
+                          <el-input
+                            v-model.trim="devpForm.barcodeStation"
+                            size="small"
+                            placeholder="鍏ョ珯鐐规椂蹇呭~锛屽~鍐欐潯鐮佺珯绔欏彿"
+                          ></el-input>
+                        </div>
+                        <div class="field-stack">
+                          <span class="field-label"
+                            >鏉$爜绔� PLC 缂栧彿<span
+                              v-if="devpRequiresBarcodeLink"
+                              class="field-required"
+                            >
+                              蹇呭~</span
+                            ></span
+                          >
+                          <el-input
+                            v-model.trim="devpForm.barcodeStationDeviceNo"
+                            size="small"
+                            placeholder="鍏ョ珯鐐规椂蹇呭~锛屽~鍐欐潯鐮佺珯 PLC 缂栧彿"
+                          ></el-input>
+                        </div>
+                        <div class="field-stack">
+                          <span class="field-label"
+                            >閫�鍥炵珯绔欏彿<span v-if="devpRequiresBackStation" class="field-required">
+                              蹇呭~</span
+                            ></span
+                          >
+                          <el-input
+                            v-model.trim="devpForm.backStation"
+                            size="small"
+                            placeholder="鏉$爜绔欐椂蹇呭~锛屽~鍐欓��鍥炵珯绔欏彿"
+                          ></el-input>
+                        </div>
+                        <div class="field-stack">
+                          <span class="field-label"
+                            >閫�鍥炵珯 PLC 缂栧彿<span
+                              v-if="devpRequiresBackStation"
+                              class="field-required"
+                            >
+                              蹇呭~</span
+                            ></span
+                          >
+                          <el-input
+                            v-model.trim="devpForm.backStationDeviceNo"
+                            size="small"
+                            placeholder="鏉$爜绔欐椂蹇呭~锛屽~鍐欓��鍥炵珯 PLC 缂栧彿"
+                          ></el-input>
+                        </div>
+                        <div class="footer-note span-2">
+                          鍕鹃�夆�滃叆绔欑偣鈥濆悗锛屽繀椤诲~鍐欐潯鐮佺珯绔欏彿鍜屾潯鐮佺珯 PLC 缂栧彿銆�
+                          鍕鹃�夆�滄潯鐮佺珯鈥濆悗锛屽繀椤诲~鍐欐潯鐮佺储寮曘�侀��鍥炵珯绔欏彿鍜岄��鍥炵珯 PLC 缂栧彿銆�
+                        </div>
+                        <el-button
+                          class="span-2"
+                          size="small"
+                          type="primary"
+                          plain
+                          @click="applyDevpForm"
+                          >搴旂敤杈撻�佺嚎閰嶇疆</el-button
+                        >
+                      </div>
+                    </div>
+
+                    <div v-if="singleSelectedDeviceElement" class="tool-section">
+                      <div class="tool-section-label">
+                        {{ getDeviceConfigLabel(singleSelectedDeviceElement.type) }}
+                      </div>
+                      <div class="prop-grid">
+                        <el-input size="small" value="杞ㄩ亾ID" disabled></el-input>
+                        <el-input
+                          v-model.trim="deviceForm.trackId"
+                          size="small"
+                          placeholder="杞ㄩ亾ID锛堥粯璁ら�掑锛屼粠 1 寮�濮嬶級"
+                        ></el-input>
+                        <el-input size="small" value="鏉$爜璧峰鍊�" disabled></el-input>
+                        <el-input
+                          v-model.trim="deviceForm.barCodeStart"
+                          size="small"
+                          placeholder="杞ㄩ亾鏉$爜璧峰鍊硷紝榛樿 0"
+                        ></el-input>
+                        <el-input size="small" value="鏉$爜缁撴潫鍊�" disabled></el-input>
+                        <el-input
+                          v-model.trim="deviceForm.barCodeEnd"
+                          size="small"
+                          placeholder="杞ㄩ亾鏉$爜缁撴潫鍊硷紝榛樿 100000"
+                        ></el-input>
+                        <div
+                          class="tool-section-label-sub span-2"
+                          v-if="deviceForm.deviceList.length > 0"
+                        >
+                          璁惧鍒楄〃
+                        </div>
+                        <template v-for="(item, index) in deviceForm.deviceList">
+                          <el-input
+                            size="small"
+                            :value="getDeviceConfigKeyLabel(singleSelectedDeviceElement.type, item.valueKey)"
+                            disabled
+                          ></el-input>
+                          <el-input
+                            v-model.trim="item.deviceNo"
+                            size="small"
+                            placeholder="璁惧缂栧彿"
+                          ></el-input>
+                          <el-input size="small" value="璧峰浣嶇疆" disabled></el-input>
+                          <div
+                            style="display: flex; align-items: center; gap: 10px; margin-left: 12px"
+                          >
+                            <el-slider
+                              v-model="item.progress"
+                              :max="100"
+                              :min="0"
+                              show-tooltip
+                              style="flex: 1"
+                            ></el-slider>
+                            <span
+                              style="
+                                font-size: 12px;
+                                color: var(--text-sub);
+                                width: 30px;
+                                text-align: right;
+                              "
+                              >{{ item.progress }}%</span
+                            >
+                          </div>
+
+                          <el-input size="small" value="璁惧闀�(娌胯建閬�)" disabled></el-input>
+                          <el-input
+                            v-model.trim="item.deviceLength"
+                            size="small"
+                            :placeholder="getDeviceLengthPlaceholder()"
+                          ></el-input>
+                          <el-input size="small" value="璁惧瀹�(鍨傜洿杞ㄩ亾)" disabled></el-input>
+                          <el-input
+                            v-model.trim="item.deviceWidth"
+                            size="small"
+                            :placeholder="getDeviceWidthPlaceholder()"
+                          ></el-input>
+                          <!-- <div class="field-help span-2">榛樿(鑷姩): 闀� {{ getAutoTrackDeviceLengthValue() }} | 瀹� {{ getAutoTrackDeviceWidthValue() }}</div> -->
+                          <el-divider
+                            class="device-divider span-2"
+                            v-if="index < deviceForm.deviceList.length - 1"
+                          ></el-divider>
+                        </template>
+
+                        <el-button size="small" type="primary" plain @click="addDeviceForm"
+                          >娣诲姞璁惧</el-button
+                        >
+                        <el-button size="small" type="primary" plain @click="applyDeviceForm"
+                          >搴旂敤璁惧鍙傛暟</el-button
+                        >
+                      </div>
+                      <div class="footer-note" style="padding-top: 8px">
+                        杩欓噷鍙敼璁惧缂栧彿鐩稿叧閿紝鍘熷 JSON
+                        閲岀殑鍏朵粬瀛楁浼氫繚鐣欙紱涓嬮潰浠嶅彲鐩存帴鏌ョ湅鎴栨墜宸ヤ慨鏀� JSON銆�
+                      </div>
+                    </div>
+
+                    <div class="tool-section">
+                      <div class="tool-section-label">
+                        {{ singleSelectedElement.type === 'devp' ? '鍘熷 JSON 棰勮' : (singleSelectedDeviceElement ? '鍘熷 JSON 棰勮 / 鎵嬪伐缂栬緫' : '鍊� / JSON 缂栬緫') }}
+                      </div>
+                      <el-input
+                        class="json-box"
+                        type="textarea"
+                        :rows="8"
+                        v-model="valueEditorText"
+                        :readonly="singleSelectedElement.type === 'devp'"
+                      >
+                      </el-input>
+                      <el-button
+                        v-if="singleSelectedElement.type !== 'devp'"
+                        size="small"
+                        type="primary"
+                        plain
+                        @click="applyRawValue"
+                        >搴旂敤鍊�</el-button
+                      >
+                    </div>
+                  </template>
+
+                  <div v-if="selectedShelfElements.length > 0" class="tool-section">
+                    <div class="tool-section-label">璐ф灦鑷姩濉厖</div>
+                    <div class="prop-grid">
+                      <el-input
+                        v-model.trim="shelfFillForm.startValue"
+                        size="small"
+                        placeholder="璧峰鍊硷紝渚嬪 12-1"
+                      ></el-input>
+                      <el-input
+                        size="small"
+                        :value="'宸查�夎揣鏋� ' + selectedShelfElements.length + ' 涓�'"
+                        disabled
+                      ></el-input>
+                      <el-select v-model="shelfFillForm.rowStep" size="small" placeholder="鎺掓柟鍚�">
+                        <el-option label="涓婂埌涓嬮�掑噺" value="desc"></el-option>
+                        <el-option label="涓婂埌涓嬮�掑" value="asc"></el-option>
+                      </el-select>
+                      <el-select v-model="shelfFillForm.colStep" size="small" placeholder="鍒楁柟鍚�">
+                        <el-option label="宸﹀埌鍙抽�掑" value="asc"></el-option>
+                        <el-option label="宸﹀埌鍙抽�掑噺" value="desc"></el-option>
+                      </el-select>
+                      <el-button
+                        class="span-2"
+                        size="small"
+                        type="primary"
+                        plain
+                        @click="applyShelfAutoFill"
+                        >鎸夋帓鍒楀~鍏呰揣鏋跺��</el-button
+                      >
+                    </div>
+                    <div class="footer-note" style="padding-top: 8px">
+                      浼氭寜閫変腑璐ф灦鐨勫疄闄呯┖闂存帓鍒楀垎缁勫~鍏呫�傞粯璁よ鍒欐槸涓婂埌涓嬫帓鍙烽�掑噺銆佸乏鍒板彸鍒楀彿閫掑銆�
+                    </div>
+                  </div>
+                </div>
+              </aside>
+            </div>
+          </div>
         </main>
-    </section>
+      </section>
 
-    <el-dialog title="鏂板缓鑷敱鐢诲竷" :visible.sync="blankDialogVisible" width="420px" class="dialog-panel" append-to-body>
+      <el-dialog
+        title="鏂板缓鑷敱鐢诲竷"
+        :visible.sync="blankDialogVisible"
+        width="420px"
+        class="dialog-panel"
+        append-to-body
+      >
         <el-form label-width="90px" size="small">
-            <el-form-item label="妤煎眰">
-                <el-input v-model.trim="blankForm.lev"></el-input>
-            </el-form-item>
-            <el-form-item label="瀹藉害">
-                <el-input v-model.trim="blankForm.width"></el-input>
-            </el-form-item>
-            <el-form-item label="楂樺害">
-                <el-input v-model.trim="blankForm.height"></el-input>
-            </el-form-item>
+          <el-form-item label="妤煎眰">
+            <el-input v-model.trim="blankForm.lev"></el-input>
+          </el-form-item>
+          <el-form-item label="瀹藉害">
+            <el-input v-model.trim="blankForm.width"></el-input>
+          </el-form-item>
+          <el-form-item label="楂樺害">
+            <el-input v-model.trim="blankForm.height"></el-input>
+          </el-form-item>
         </el-form>
         <div slot="footer">
-            <el-button @click="blankDialogVisible = false">鍙栨秷</el-button>
-            <el-button type="primary" @click="createBlankMap">鍒涘缓</el-button>
+          <el-button @click="blankDialogVisible = false">鍙栨秷</el-button>
+          <el-button type="primary" @click="createBlankMap">鍒涘缓</el-button>
         </div>
-    </el-dialog>
+      </el-dialog>
 
-    <input ref="importInput" type="file" style="display:none;" @change="handleImportExcel">
-    <input ref="mapImportInput" type="file" accept=".json,application/json" style="display:none;" @change="handleImportMap">
-</div>
+      <input ref="importInput" type="file" style="display: none" @change="handleImportExcel" />
+      <input
+        ref="mapImportInput"
+        type="file"
+        accept=".json,application/json"
+        style="display: none"
+        @change="handleImportMap"
+      />
+    </div>
 
-<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
-<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
-<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
-<script type="text/javascript" src="../../static/vue/element/element.js"></script>
-<script type="text/javascript" src="../../static/js/pixi-legacy.min.js"></script>
-<script type="text/javascript" src="../../static/js/basMap/editor.js?v=20260321e"></script>
-</body>
+    <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
+    <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
+    <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
+    <script type="text/javascript" src="../../static/vue/element/element.js"></script>
+    <script type="text/javascript" src="../../static/js/pixi-legacy.min.js"></script>
+    <script
+      type="text/javascript"
+      src="../../static/js/basMap/mapTrackGeometry.js?v=20260406b"
+    ></script>
+    <script type="text/javascript" src="../../static/js/basMap/editor.js?v=20260321e"></script>
+  </body>
 </html>
diff --git a/src/main/webapp/views/watch/console.html b/src/main/webapp/views/watch/console.html
index 284561b..5d2dde6 100644
--- a/src/main/webapp/views/watch/console.html
+++ b/src/main/webapp/views/watch/console.html
@@ -553,6 +553,7 @@
 		<script src="../../components/WatchDualCrnCard.js"></script>
 		<script src="../../components/DevpCard.js"></script>
 		<script src="../../components/WatchRgvCard.js"></script>
+    <script src="../../static/js/basMap/mapTrackGeometry.js?v=20260406a"></script>
 		<script src="../../components/MapCanvas.js?v=20260320_crn_size_fix1"></script>
 		<script>
 			let ws;
diff --git a/src/main/webapp/views/watch/fakeTrace.html b/src/main/webapp/views/watch/fakeTrace.html
index 2110046..5e191ac 100644
--- a/src/main/webapp/views/watch/fakeTrace.html
+++ b/src/main/webapp/views/watch/fakeTrace.html
@@ -517,6 +517,7 @@
     </div>
 </div>
 
+<script src="../../static/js/basMap/mapTrackGeometry.js?v=20260406a"></script>
 <script src="../../components/MapCanvas.js?v=20260320_crn_size_fix1"></script>
 <script>
     var fakeTraceWs = null;
diff --git a/src/main/webapp/views/watch/stationTrace.html b/src/main/webapp/views/watch/stationTrace.html
index 02f8cca..140c28d 100644
--- a/src/main/webapp/views/watch/stationTrace.html
+++ b/src/main/webapp/views/watch/stationTrace.html
@@ -567,6 +567,7 @@
     </div>
 </div>
 
+<script src="../../static/js/basMap/mapTrackGeometry.js?v=20260406a"></script>
 <script src="../../components/MapCanvas.js?v=20260319_station_trace_v1"></script>
 <script>
     var stationTraceWs = null;

--
Gitblit v1.9.1