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