| | |
| | | (function () { |
| | | var FREE_EDITOR_MODE = 'free-v1'; |
| | | var MAP_TRANSFER_FORMAT = 'bas-map-editor-transfer-v2'; |
| | | var HISTORY_LIMIT = 60; |
| | | var DEFAULT_CANVAS_WIDTH = 6400; |
| | | var DEFAULT_CANVAS_HEIGHT = 3600; |
| | | var MIN_ELEMENT_SIZE = 24; |
| | | var HANDLE_SCREEN_SIZE = 10; |
| | | var DRAG_START_THRESHOLD = 5; |
| | | var EDGE_SNAP_SCREEN_TOLERANCE = 8; |
| | | var COORD_EPSILON = 0.01; |
| | | var DEFERRED_STATIC_REBUILD_DELAY = 120; |
| | | var PAN_LABEL_REFRESH_DELAY = 160; |
| | | var ZOOM_REFRESH_DELAY = 220; |
| | | var POINTER_STATUS_UPDATE_INTERVAL = 48; |
| | | var SPATIAL_BUCKET_SIZE = 240; |
| | | var STATIC_VIEW_PADDING = 120; |
| | | var MIN_LABEL_SCALE = 0.17; |
| | | var ABS_MIN_LABEL_SCREEN_WIDTH = 26; |
| | | var ABS_MIN_LABEL_SCREEN_HEIGHT = 14; |
| | | var STATIC_SPRITE_SCALE_THRESHOLD = 0.85; |
| | | var STATIC_SIMPLIFY_SCALE_THRESHOLD = 0.22; |
| | | var DENSE_SIMPLIFY_SCALE_THRESHOLD = 0.8; |
| | | var DENSE_SIMPLIFY_ELEMENT_THRESHOLD = 1200; |
| | | var DENSE_LABEL_HIDE_SCALE_THRESHOLD = 1.05; |
| | | var DENSE_LABEL_HIDE_ELEMENT_THRESHOLD = 1200; |
| | | var STATIC_SPRITE_POOL_SLACK = 96; |
| | | var MIN_LABEL_COUNT = 180; |
| | | var MAX_LABEL_COUNT = 360; |
| | | var SHOW_CANVAS_ELEMENT_LABELS = true; |
| | | var DRAW_TYPES = ['shelf', 'devp', 'crn', 'dualCrn', 'rgv']; |
| | | var ARRAY_TEMPLATE_TYPES = ['shelf', 'crn', 'dualCrn', 'rgv']; |
| | | var DEVICE_CONFIG_TYPES = ['crn', 'dualCrn', 'rgv']; |
| | | var DEVP_DIRECTION_OPTIONS = [ |
| | | { key: 'top', label: '上', arrow: '↑' }, |
| | | { key: 'right', label: '右', arrow: '→' }, |
| | | { key: 'bottom', label: '下', arrow: '↓' }, |
| | | { key: 'left', label: '左', arrow: '←' } |
| | | ]; |
| | | var idSeed = Date.now(); |
| | | var FREE_EDITOR_MODE = 'free-v1'; |
| | | var MAP_TRANSFER_FORMAT = 'bas-map-editor-transfer-v2'; |
| | | var HISTORY_LIMIT = 60; |
| | | var DEFAULT_CANVAS_WIDTH = 6400; |
| | | var DEFAULT_CANVAS_HEIGHT = 3600; |
| | | var MIN_ELEMENT_SIZE = 24; |
| | | var HANDLE_SCREEN_SIZE = 10; |
| | | var DRAG_START_THRESHOLD = 5; |
| | | var EDGE_SNAP_SCREEN_TOLERANCE = 8; |
| | | var COORD_EPSILON = 0.01; |
| | | var DEFERRED_STATIC_REBUILD_DELAY = 120; |
| | | var PAN_LABEL_REFRESH_DELAY = 160; |
| | | var ZOOM_REFRESH_DELAY = 220; |
| | | var POINTER_STATUS_UPDATE_INTERVAL = 48; |
| | | var SPATIAL_BUCKET_SIZE = 240; |
| | | var STATIC_VIEW_PADDING = 120; |
| | | var MIN_LABEL_SCALE = 0.17; |
| | | var ABS_MIN_LABEL_SCREEN_WIDTH = 26; |
| | | var ABS_MIN_LABEL_SCREEN_HEIGHT = 14; |
| | | var STATIC_SPRITE_SCALE_THRESHOLD = 0.85; |
| | | var STATIC_SIMPLIFY_SCALE_THRESHOLD = 0.22; |
| | | var DENSE_SIMPLIFY_SCALE_THRESHOLD = 0.8; |
| | | var DENSE_SIMPLIFY_ELEMENT_THRESHOLD = 1200; |
| | | var DENSE_LABEL_HIDE_SCALE_THRESHOLD = 1.05; |
| | | var DENSE_LABEL_HIDE_ELEMENT_THRESHOLD = 1200; |
| | | var STATIC_SPRITE_POOL_SLACK = 96; |
| | | var MIN_LABEL_COUNT = 180; |
| | | var MAX_LABEL_COUNT = 360; |
| | | var DRAW_TYPES = ['shelf', 'repairHub', 'devp', 'crn', 'dualCrn', 'rgv', 'annulus']; |
| | | var ARRAY_TEMPLATE_TYPES = ['shelf', 'repairHub']; |
| | | var DEVICE_CONFIG_TYPES = ['crn', 'dualCrn', 'rgv', 'annulus']; |
| | | var DEVP_DIRECTION_OPTIONS = [ |
| | | { key: 'top', label: '上', arrow: '↑' }, |
| | | { key: 'right', label: '右', arrow: '→' }, |
| | | { key: 'bottom', label: '下', arrow: '↓' }, |
| | | { key: 'left', label: '左', arrow: '←' } |
| | | ]; |
| | | var idSeed = Date.now(); |
| | | |
| | | var TYPE_META = { |
| | | shelf: { label: '货架', shortLabel: 'SHELF', fill: 0x7d96bf, border: 0x4f6486 }, |
| | | devp: { label: '输送线', shortLabel: 'DEVP', fill: 0xf0b06f, border: 0xa45f21 }, |
| | | crn: { label: '堆垛机轨道', shortLabel: 'CRN', fill: 0x68bfd0, border: 0x1d6e81 }, |
| | | dualCrn: { label: '双工位轨道', shortLabel: 'DCRN', fill: 0x54c1a4, border: 0x0f7b62 }, |
| | | rgv: { label: 'RGV轨道', shortLabel: 'RGV', fill: 0xc691e9, border: 0x744b98 } |
| | | var G = window.BasMapTrackGeometry; |
| | | if (!G) { |
| | | throw new Error('mapTrackGeometry.js must be loaded before editor.js'); |
| | | } |
| | | |
| | | function nextId() { |
| | | idSeed += 1; |
| | | return 'el_' + idSeed; |
| | | } |
| | | |
| | | function deepClone(obj) { |
| | | return JSON.parse(JSON.stringify(obj == null ? null : obj)); |
| | | } |
| | | |
| | | function padNumber(value) { |
| | | return value < 10 ? '0' + value : String(value); |
| | | } |
| | | |
| | | function authHeaders() { |
| | | return { |
| | | token: localStorage.getItem('token') |
| | | }; |
| | | } |
| | | |
| | | function getQueryParam(name) { |
| | | var search = window.location.search || ''; |
| | | if (!search) { |
| | | return ''; |
| | | } |
| | | var target = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| | | var match = search.match(new RegExp('(?:[?&])' + target + '=([^&]*)')); |
| | | return match ? decodeURIComponent(match[1]) : ''; |
| | | } |
| | | |
| | | function toNumber(value, defaultValue) { |
| | | if (value === null || value === undefined || value === '') { |
| | | return defaultValue; |
| | | } |
| | | var parsed = Number(value); |
| | | return isFinite(parsed) ? parsed : defaultValue; |
| | | } |
| | | |
| | | function toInt(value, defaultValue) { |
| | | return Math.round(toNumber(value, defaultValue)); |
| | | } |
| | | |
| | | function clamp(value, min, max) { |
| | | return Math.max(min, Math.min(max, value)); |
| | | } |
| | | |
| | | function roundCoord(value) { |
| | | return Math.round(value * 1000) / 1000; |
| | | } |
| | | |
| | | function normalizeValue(value) { |
| | | if (value === null || value === undefined) { |
| | | return ''; |
| | | } |
| | | return typeof value === 'string' ? value : JSON.stringify(value); |
| | | } |
| | | |
| | | function parseShelfLocationValue(value) { |
| | | var text = normalizeValue(value).trim(); |
| | | var matched = text.match(/^(-?\d+)\s*-\s*(-?\d+)$/); |
| | | if (!matched) { |
| | | return null; |
| | | } |
| | | return { |
| | | row: toInt(matched[1], 0), |
| | | col: toInt(matched[2], 0) |
| | | }; |
| | | } |
| | | |
| | | function formatShelfLocationValue(row, col) { |
| | | return String(toInt(row, 0)) + '-' + String(toInt(col, 0)); |
| | | } |
| | | |
| | | function isShelfLikeNodeType(type) { |
| | | return type === 'shelf' || type === 'repairHub'; |
| | | } |
| | | |
| | | function safeParseJson(text) { |
| | | if (!text || typeof text !== 'string') { |
| | | return null; |
| | | } |
| | | try { |
| | | return JSON.parse(text); |
| | | } catch (e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | function boolFlag(value) { |
| | | return value === true || value === 1 || value === '1'; |
| | | } |
| | | |
| | | function normalizeDirectionList(direction) { |
| | | var list = Array.isArray(direction) ? direction : String(direction || '').split(/[,\s|/]+/); |
| | | var result = []; |
| | | var seen = {}; |
| | | for (var i = 0; i < list.length; i++) { |
| | | var item = String(list[i] || '') |
| | | .trim() |
| | | .toLowerCase(); |
| | | if (!item || seen[item]) { |
| | | continue; |
| | | } |
| | | seen[item] = true; |
| | | result.push(item); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | function directionTokenToArrow(token) { |
| | | if (token === 'top' || token === 'up' || token === 'north' || token === 'n') { |
| | | return '↑'; |
| | | } |
| | | if (token === 'right' || token === 'east' || token === 'e') { |
| | | return '→'; |
| | | } |
| | | if (token === 'bottom' || token === 'down' || token === 'south' || token === 's') { |
| | | return '↓'; |
| | | } |
| | | if (token === 'left' || token === 'west' || token === 'w') { |
| | | return '←'; |
| | | } |
| | | return ''; |
| | | } |
| | | |
| | | function formatDirectionArrows(direction) { |
| | | var list = normalizeDirectionList(direction); |
| | | var arrows = []; |
| | | for (var i = 0; i < list.length; i++) { |
| | | var arrow = directionTokenToArrow(list[i]); |
| | | if (arrow) { |
| | | arrows.push(arrow); |
| | | } |
| | | } |
| | | return arrows.join(''); |
| | | } |
| | | |
| | | function isDeviceConfigType(type) { |
| | | return DEVICE_CONFIG_TYPES.indexOf(type) >= 0; |
| | | } |
| | | |
| | | function pickDeviceValueKey(type, json) { |
| | | if (type === 'crn' || type === 'dualCrn') { |
| | | return 'crnNo'; |
| | | } |
| | | if (type === 'rgv') { |
| | | return 'rgvNo'; |
| | | } |
| | | return 'deviceNo'; |
| | | } |
| | | |
| | | function isInputLike(target) { |
| | | if (!target || !target.tagName) { |
| | | return false; |
| | | } |
| | | var tag = String(target.tagName || '').toLowerCase(); |
| | | return tag === 'input' || tag === 'textarea' || tag === 'select' || !!target.isContentEditable; |
| | | } |
| | | |
| | | function rectsOverlap(a, b) { |
| | | return ( |
| | | a.x < b.x + b.width - COORD_EPSILON && |
| | | a.x + a.width > b.x + COORD_EPSILON && |
| | | a.y < b.y + b.height - COORD_EPSILON && |
| | | a.y + a.height > b.y + COORD_EPSILON |
| | | ); |
| | | } |
| | | |
| | | function rectIntersects(a, b) { |
| | | return ( |
| | | a.x <= b.x + b.width && a.x + a.width >= b.x && a.y <= b.y + b.height && a.y + a.height >= b.y |
| | | ); |
| | | } |
| | | |
| | | function isRectWithinCanvas(rect, canvasWidth, canvasHeight) { |
| | | return ( |
| | | rect.x >= -COORD_EPSILON && |
| | | rect.y >= -COORD_EPSILON && |
| | | rect.x + rect.width <= canvasWidth + COORD_EPSILON && |
| | | rect.y + rect.height <= canvasHeight + COORD_EPSILON |
| | | ); |
| | | } |
| | | |
| | | function findDocOverlapId(doc) { |
| | | if (!doc || !doc.elements || !doc.elements.length) { |
| | | return ''; |
| | | } |
| | | var buckets = {}; |
| | | var elements = doc.elements; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | var minX = Math.floor(element.x / SPATIAL_BUCKET_SIZE); |
| | | var maxX = Math.floor((element.x + element.width) / SPATIAL_BUCKET_SIZE); |
| | | var minY = Math.floor(element.y / SPATIAL_BUCKET_SIZE); |
| | | var maxY = Math.floor((element.y + element.height) / SPATIAL_BUCKET_SIZE); |
| | | for (var bx = minX; bx <= maxX; bx++) { |
| | | for (var by = minY; by <= maxY; by++) { |
| | | var key = bucketKey(bx, by); |
| | | var bucket = buckets[key]; |
| | | if (!bucket || !bucket.length) { |
| | | continue; |
| | | } |
| | | for (var j = 0; j < bucket.length; j++) { |
| | | if (rectsOverlap(element, bucket[j])) { |
| | | return element.id || 'el_' + i; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | for (bx = minX; bx <= maxX; bx++) { |
| | | for (by = minY; by <= maxY; by++) { |
| | | key = bucketKey(bx, by); |
| | | if (!buckets[key]) { |
| | | buckets[key] = []; |
| | | } |
| | | buckets[key].push(element); |
| | | } |
| | | } |
| | | } |
| | | return ''; |
| | | } |
| | | |
| | | function buildRectFromPoints(a, b) { |
| | | var left = Math.min(a.x, b.x); |
| | | var top = Math.min(a.y, b.y); |
| | | var right = Math.max(a.x, b.x); |
| | | var bottom = Math.max(a.y, b.y); |
| | | return { |
| | | x: roundCoord(left), |
| | | y: roundCoord(top), |
| | | width: roundCoord(right - left), |
| | | height: roundCoord(bottom - top) |
| | | }; |
| | | } |
| | | |
| | | function getTypeMeta(type) { |
| | | return G.TYPE_META[type] || G.TYPE_META.shelf; |
| | | } |
| | | |
| | | function rangesNearOrOverlap(a1, a2, b1, b2, tolerance) { |
| | | return a1 <= b2 + tolerance && a2 >= b1 - tolerance; |
| | | } |
| | | |
| | | function bucketKey(x, y) { |
| | | return x + ':' + y; |
| | | } |
| | | |
| | | function getPreferredResolution() { |
| | | return Math.min(window.devicePixelRatio || 1, 1.25); |
| | | } |
| | | |
| | | function drawElementByType(graphics, element, type, style) { |
| | | const cameraScale = this.camera.scale; |
| | | const rect = element; |
| | | |
| | | const drawDeviceList = () => { |
| | | const deviceForm = G.safeParseJson(rect.value); |
| | | if (!deviceForm || !deviceForm.deviceList || deviceForm.deviceList.length === 0) { |
| | | return; |
| | | } |
| | | |
| | | const fontSize = Math.max(10 / cameraScale, 6); |
| | | const textStyle = new PIXI.TextStyle({ |
| | | fontFamily: 'Arial', |
| | | fontSize: fontSize, |
| | | fill: '#000000', |
| | | stroke: '#ffffff', |
| | | strokeThickness: Math.max(1 / cameraScale, 0.5), |
| | | align: 'center' |
| | | }); |
| | | |
| | | const getDeviceNoText = (item) => { |
| | | if (item == null) return ''; |
| | | if (item.deviceNo != null && String(item.deviceNo).trim() !== '') { |
| | | return String(item.deviceNo).trim(); |
| | | } |
| | | // 兼容历史字段 |
| | | if (item.crnNo != null && String(item.crnNo).trim() !== '') { |
| | | return String(item.crnNo).trim(); |
| | | } |
| | | if (item.rgvNo != null && String(item.rgvNo).trim() !== '') { |
| | | return String(item.rgvNo).trim(); |
| | | } |
| | | return ''; |
| | | }; |
| | | |
| | | const newDeviceInfo = G.getDeviceInfo(rect); |
| | | newDeviceInfo.deviceList.forEach((item) => { |
| | | // annulus 轨道上放置的是 rgv 设备 |
| | | const deviceType = type === 'annulus' ? 'rgv' : type; |
| | | const deviceContainer = new PIXI.Container(); |
| | | const isHorizontal = rect.width > rect.height; |
| | | const centerX = type === 'annulus' ? item.x : item.x + item.width / 2; |
| | | const centerY = type === 'annulus' ? item.y : item.y + item.height / 2; |
| | | |
| | | // 与 getDeviceInfo / getDevicePixelBoxForTrack 一致 |
| | | const drawW = Math.max(2, Math.round(item.width)); |
| | | const drawH = Math.max(2, Math.round(item.height)); |
| | | |
| | | deviceContainer.pivot.set(drawW / 2, drawH / 2); |
| | | deviceContainer.position.set(centerX, centerY); |
| | | deviceContainer.rotation = |
| | | type === 'annulus' |
| | | ? G.getRotate({ x: item.x, y: item.y }, item.path) |
| | | : isHorizontal |
| | | ? 0 |
| | | : Math.PI / 2; |
| | | |
| | | const deviceGraphics = new PIXI.Graphics(); |
| | | if (deviceType === 'rgv') { |
| | | G.drawRgvDeviceGraphics(deviceGraphics, drawW, drawH, 0x245a9a); |
| | | } else if (deviceType === 'crn' || deviceType === 'dualCrn') { |
| | | G.drawCrnDeviceGraphics(deviceGraphics, drawW, drawH, 0x245a9a); |
| | | } else { |
| | | // fallback: keep previous minimal shape for unknown types |
| | | const radius = Math.max(6 / cameraScale, 2); |
| | | deviceGraphics.beginFill(style.fill.color, 0.92); |
| | | deviceGraphics.lineStyle(style.line.width, style.line.color, style.line.alpha); |
| | | deviceGraphics.drawRoundedRect(0, 0, drawW, drawH, radius); |
| | | deviceGraphics.endFill(); |
| | | } |
| | | deviceContainer.addChild(deviceGraphics); |
| | | |
| | | const txt = getDeviceNoText(item); |
| | | if (txt) { |
| | | const text = new PIXI.Text(txt, textStyle); |
| | | text.anchor.set(0.5); |
| | | text.position.set(drawW / 2, drawH / 2); |
| | | deviceContainer.addChild(text); |
| | | } |
| | | |
| | | graphics.addChild(deviceContainer); |
| | | }); |
| | | }; |
| | | |
| | | function nextId() { |
| | | idSeed += 1; |
| | | return 'el_' + idSeed; |
| | | } |
| | | graphics.lineStyle(style.line.width, style.line.color, style.line.alpha); |
| | | graphics.beginFill(style.fill.color, style.fill.alpha); |
| | | if (type === 'annulus') { |
| | | G.startDrawSmoothedPath(graphics, element, element.shape || this.annulusShape); |
| | | drawDeviceList(rect); |
| | | } else if (isDeviceConfigType(type)) { |
| | | graphics.lineStyle(0); |
| | | graphics.beginFill(style.fill.color, style.fill.alpha); |
| | | graphics.drawRoundedRect( |
| | | rect.x, |
| | | rect.y, |
| | | rect.width, |
| | | rect.height, |
| | | Math.max(6 / cameraScale, 2) |
| | | ); |
| | | graphics.endFill(); |
| | | graphics.lineStyle(style.line.width, style.line.color, style.line.alpha); |
| | | const isHorizontal = rect.width > rect.height; |
| | | |
| | | function deepClone(obj) { |
| | | return JSON.parse(JSON.stringify(obj == null ? null : obj)); |
| | | if (isHorizontal) { |
| | | const center = rect.height / 2; |
| | | graphics.moveTo(rect.x, rect.y + center); |
| | | graphics.lineTo(rect.x + rect.width, rect.y + center); |
| | | } else { |
| | | const center = rect.width / 2; |
| | | graphics.moveTo(rect.x + center, rect.y); |
| | | graphics.lineTo(rect.x + center, rect.y + rect.height); |
| | | } |
| | | drawDeviceList(rect); |
| | | } else { |
| | | graphics.drawRoundedRect( |
| | | element.x, |
| | | element.y, |
| | | element.width, |
| | | element.height, |
| | | Math.max(6 / cameraScale, 2) |
| | | ); |
| | | } |
| | | graphics.endFill(); |
| | | } |
| | | |
| | | function padNumber(value) { |
| | | return value < 10 ? ('0' + value) : String(value); |
| | | } |
| | | |
| | | function authHeaders() { |
| | | return { |
| | | token: localStorage.getItem('token') |
| | | }; |
| | | } |
| | | |
| | | function getQueryParam(name) { |
| | | var search = window.location.search || ''; |
| | | if (!search) { |
| | | return ''; |
| | | new Vue({ |
| | | el: '#app', |
| | | data: function () { |
| | | return { |
| | | remoteLevOptions: [], |
| | | levOptions: [], |
| | | currentLev: null, |
| | | floorPickerLev: null, |
| | | draftDocs: {}, |
| | | doc: null, |
| | | activeTool: 'select', |
| | | toolPanelCollapsed: false, |
| | | inspectorPanelCollapsed: false, |
| | | interactionTools: [ |
| | | { |
| | | key: 'select', |
| | | label: '选择 / 移动', |
| | | desc: '点击元素选择,拖拽移动,空白处拖动画布' |
| | | }, |
| | | { key: 'marquee', label: '框选', desc: '在画布中框选一组元素' }, |
| | | { |
| | | key: 'array', |
| | | label: '阵列', |
| | | desc: '选中货架或维修站台模板后拖线生成一排,按排-列续号' |
| | | }, |
| | | { key: 'pan', label: '平移', desc: '专门用于拖动画布和观察全图' } |
| | | ], |
| | | drawTools: [ |
| | | { key: 'shelf', label: '货架', desc: '自由拉出货架矩形' }, |
| | | { key: 'repairHub', label: '维修站台', desc: '自由拉出维修站台矩形' }, |
| | | { key: 'devp', label: '输送线', desc: '拉出站点 / 输送线矩形' }, |
| | | { key: 'crn', label: '堆垛机', desc: '拉出堆垛机轨道矩形' }, |
| | | { key: 'dualCrn', label: '双工位堆垛机', desc: '拉出双工位轨道矩形' }, |
| | | { key: 'rgv', label: 'RGV', desc: '拉出 RGV 轨道矩形' }, |
| | | { key: 'annulus', label: '环穿', desc: '拉出环穿轨道矩形' } |
| | | ], |
| | | pixiApp: null, |
| | | mapRoot: null, |
| | | gridLayer: null, |
| | | trackLayer: null, |
| | | nodeLayer: null, |
| | | patchObjectLayer: null, |
| | | activeLayer: null, |
| | | labelLayer: null, |
| | | selectionLayer: null, |
| | | guideLayer: null, |
| | | guideText: null, |
| | | hoverLayer: null, |
| | | labelPool: [], |
| | | renderQueued: false, |
| | | gridSceneDirty: true, |
| | | staticSceneDirty: true, |
| | | spatialIndexDirty: true, |
| | | spatialBuckets: null, |
| | | gridRenderRect: null, |
| | | gridRenderKey: '', |
| | | staticRenderRect: null, |
| | | staticRenderKey: '', |
| | | staticExcludedKey: '', |
| | | camera: { |
| | | x: 80, |
| | | y: 80, |
| | | scale: 1 |
| | | }, |
| | | viewZoom: 1, |
| | | selectedIds: [], |
| | | clipboard: [], |
| | | hoverElementId: '', |
| | | pointerStatus: '--', |
| | | lastPointerStatusUpdateTs: 0, |
| | | pixiResolution: getPreferredResolution(), |
| | | fpsValue: 0, |
| | | fpsFrameCount: 0, |
| | | fpsSampleStartTs: 0, |
| | | fpsTickerHandler: null, |
| | | interactionState: null, |
| | | isZooming: false, |
| | | isPanning: false, |
| | | zoomRefreshTimer: null, |
| | | panRefreshTimer: null, |
| | | pendingViewportRefresh: false, |
| | | pendingStaticCommit: null, |
| | | deferredStaticRebuildTimer: null, |
| | | currentPointerId: null, |
| | | boundCanvasHandlers: null, |
| | | boundWindowHandlers: null, |
| | | resizeObserver: null, |
| | | labelCapability: { |
| | | maxWidth: 0, |
| | | maxHeight: 0 |
| | | }, |
| | | labelCapabilityDirty: true, |
| | | undoStack: [], |
| | | redoStack: [], |
| | | savedSnapshot: '', |
| | | isDirty: false, |
| | | saving: false, |
| | | savingAll: false, |
| | | loadingFloor: false, |
| | | switchingFloorLev: null, |
| | | floorRequestSeq: 0, |
| | | activeFloorRequestSeq: 0, |
| | | blankDialogVisible: false, |
| | | blankForm: { |
| | | lev: '', |
| | | width: String(DEFAULT_CANVAS_WIDTH), |
| | | height: String(DEFAULT_CANVAS_HEIGHT) |
| | | }, |
| | | canvasForm: { |
| | | width: String(DEFAULT_CANVAS_WIDTH), |
| | | height: String(DEFAULT_CANVAS_HEIGHT) |
| | | }, |
| | | geometryForm: { |
| | | x: '', |
| | | y: '', |
| | | width: '', |
| | | height: '' |
| | | }, |
| | | devpForm: { |
| | | stationId: '', |
| | | deviceNo: '', |
| | | direction: [], |
| | | isBarcodeStation: false, |
| | | barcodeIdx: '', |
| | | backStation: '', |
| | | backStationDeviceNo: '', |
| | | isInStation: false, |
| | | barcodeStation: '', |
| | | barcodeStationDeviceNo: '', |
| | | isOutStation: false, |
| | | runBlockReassign: false, |
| | | isOutOrder: false, |
| | | isLiftTransfer: false |
| | | }, |
| | | deviceForm: { |
| | | trackId: '', |
| | | barCodeStart: 0, |
| | | barCodeEnd: 100000, |
| | | deviceList: [] |
| | | }, |
| | | devpDirectionOptions: DEVP_DIRECTION_OPTIONS, |
| | | shelfFillForm: { |
| | | startValue: '', |
| | | rowStep: 'desc', |
| | | colStep: 'asc' |
| | | }, |
| | | valueEditorText: '', |
| | | spacePressed: false, |
| | | lastCursor: 'default', |
| | | annulusShape: 'rect' |
| | | }; |
| | | }, |
| | | computed: { |
| | | singleSelectedElement: function () { |
| | | if (!this.doc || this.selectedIds.length !== 1) { |
| | | return null; |
| | | } |
| | | var target = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| | | var match = search.match(new RegExp('(?:[?&])' + target + '=([^&]*)')); |
| | | return match ? decodeURIComponent(match[1]) : ''; |
| | | } |
| | | |
| | | function toNumber(value, defaultValue) { |
| | | if (value === null || value === undefined || value === '') { |
| | | return defaultValue; |
| | | return this.findElementById(this.selectedIds[0]); |
| | | }, |
| | | singleSelectedDeviceElement: function () { |
| | | if (!this.singleSelectedElement || !isDeviceConfigType(this.singleSelectedElement.type)) { |
| | | return null; |
| | | } |
| | | var parsed = Number(value); |
| | | return isFinite(parsed) ? parsed : defaultValue; |
| | | } |
| | | |
| | | function toInt(value, defaultValue) { |
| | | return Math.round(toNumber(value, defaultValue)); |
| | | } |
| | | |
| | | function clamp(value, min, max) { |
| | | return Math.max(min, Math.min(max, value)); |
| | | } |
| | | |
| | | function roundCoord(value) { |
| | | return Math.round(value * 1000) / 1000; |
| | | } |
| | | |
| | | function normalizeValue(value) { |
| | | if (value === null || value === undefined) { |
| | | return ''; |
| | | return this.singleSelectedElement; |
| | | }, |
| | | selectedShelfElements: function () { |
| | | if (!this.doc || !this.selectedIds.length) { |
| | | return []; |
| | | } |
| | | return typeof value === 'string' ? value : JSON.stringify(value); |
| | | } |
| | | |
| | | function parseShelfLocationValue(value) { |
| | | var text = normalizeValue(value).trim(); |
| | | var matched = text.match(/^(-?\d+)\s*-\s*(-?\d+)$/); |
| | | if (!matched) { |
| | | return null; |
| | | return this.getSelectedElements().filter(function (item) { |
| | | return item && isShelfLikeNodeType(item.type); |
| | | }); |
| | | }, |
| | | devpRequiresBarcodeLink: function () { |
| | | return !!(this.devpForm && this.devpForm.isInStation); |
| | | }, |
| | | devpRequiresBarcodeIndex: function () { |
| | | return !!(this.devpForm && this.devpForm.isBarcodeStation); |
| | | }, |
| | | devpRequiresBackStation: function () { |
| | | return !!(this.devpForm && this.devpForm.isBarcodeStation); |
| | | }, |
| | | arrayPreviewCount: function () { |
| | | if (!this.interactionState || this.interactionState.type !== 'array') { |
| | | return 0; |
| | | } |
| | | return { |
| | | row: toInt(matched[1], 0), |
| | | col: toInt(matched[2], 0) |
| | | }; |
| | | } |
| | | |
| | | function formatShelfLocationValue(row, col) { |
| | | return String(toInt(row, 0)) + '-' + String(toInt(col, 0)); |
| | | } |
| | | |
| | | function safeParseJson(text) { |
| | | if (!text || typeof text !== 'string') { |
| | | return null; |
| | | } |
| | | try { |
| | | return JSON.parse(text); |
| | | } catch (e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | function boolFlag(value) { |
| | | return value === true || value === 1 || value === '1'; |
| | | } |
| | | |
| | | function normalizeDirectionList(direction) { |
| | | var list = Array.isArray(direction) ? direction : String(direction || '').split(/[,\s|/]+/); |
| | | return this.interactionState.previewItems ? this.interactionState.previewItems.length : 0; |
| | | }, |
| | | viewPercent: function () { |
| | | return Math.round(this.viewZoom * 100); |
| | | }, |
| | | fpsText: function () { |
| | | return this.fpsValue > 0 ? String(this.fpsValue) : '--'; |
| | | }, |
| | | dirtyDraftLevs: function () { |
| | | var result = []; |
| | | var seen = {}; |
| | | if (this.doc && this.doc.lev && this.isDirty) { |
| | | var currentLev = toInt(this.doc.lev, 0); |
| | | if (currentLev > 0) { |
| | | seen[currentLev] = true; |
| | | result.push(currentLev); |
| | | } |
| | | } |
| | | var self = this; |
| | | Object.keys(this.draftDocs || {}).forEach(function (key) { |
| | | var lev = toInt(key, 0); |
| | | if (lev <= 0 || seen[lev]) { |
| | | return; |
| | | } |
| | | if (self.hasDirtyDraft(lev)) { |
| | | seen[lev] = true; |
| | | result.push(lev); |
| | | } |
| | | }); |
| | | result.sort(function (a, b) { |
| | | return a - b; |
| | | }); |
| | | return result; |
| | | }, |
| | | dirtyDraftCount: function () { |
| | | return this.dirtyDraftLevs.length; |
| | | } |
| | | }, |
| | | mounted: function () { |
| | | this.initPixi(); |
| | | this.attachEvents(); |
| | | this.loadLevOptions(); |
| | | var lev = toInt(getQueryParam('lev'), 0); |
| | | if (lev > 0) { |
| | | this.floorPickerLev = lev; |
| | | this.fetchFloor(lev); |
| | | } else { |
| | | this.createLocalBlankDoc(1, DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT, ''); |
| | | } |
| | | }, |
| | | beforeDestroy: function () { |
| | | this.detachEvents(); |
| | | if (this.zoomRefreshTimer) { |
| | | window.clearTimeout(this.zoomRefreshTimer); |
| | | this.zoomRefreshTimer = null; |
| | | } |
| | | if (this.panRefreshTimer) { |
| | | window.clearTimeout(this.panRefreshTimer); |
| | | this.panRefreshTimer = null; |
| | | } |
| | | this.clearDeferredStaticCommit(); |
| | | this.stopFpsTicker(); |
| | | if (this.pixiApp) { |
| | | this.pixiApp.destroy(true, { children: true }); |
| | | this.pixiApp = null; |
| | | } |
| | | }, |
| | | methods: { |
| | | showMessage: function (type, message) { |
| | | if (this.$message) { |
| | | this.$message({ type: type, message: message }); |
| | | } |
| | | }, |
| | | formatNumber: function (value) { |
| | | var num = toNumber(value, 0); |
| | | if (Math.abs(num) >= 1000 || num === Math.round(num)) { |
| | | return String(Math.round(num)); |
| | | } |
| | | return String(Math.round(num * 100) / 100); |
| | | }, |
| | | syncFloorQueryParam: function (lev) { |
| | | lev = toInt(lev, 0); |
| | | if (lev <= 0 || !window.history || !window.history.replaceState || !window.URL) { |
| | | return; |
| | | } |
| | | try { |
| | | var url = new URL(window.location.href); |
| | | url.searchParams.set('lev', String(lev)); |
| | | window.history.replaceState(null, '', url.toString()); |
| | | } catch (e) { |
| | | // Ignore URL sync failures and keep editor usable. |
| | | } |
| | | }, |
| | | toolLabel: function (tool) { |
| | | var list = this.interactionTools.concat(this.drawTools); |
| | | for (var i = 0; i < list.length; i++) { |
| | | var item = String(list[i] || '').trim().toLowerCase(); |
| | | if (!item || seen[item]) { |
| | | continue; |
| | | if (list[i].key === tool) { |
| | | return list[i].label; |
| | | } |
| | | } |
| | | return tool || '--'; |
| | | }, |
| | | toggleToolPanel: function () { |
| | | this.toolPanelCollapsed = !this.toolPanelCollapsed; |
| | | }, |
| | | toggleInspectorPanel: function () { |
| | | this.inspectorPanelCollapsed = !this.inspectorPanelCollapsed; |
| | | }, |
| | | startFpsTicker: function () { |
| | | if (!this.pixiApp || !this.pixiApp.ticker || this.fpsTickerHandler) { |
| | | return; |
| | | } |
| | | var self = this; |
| | | this.fpsValue = 0; |
| | | this.fpsFrameCount = 0; |
| | | this.fpsSampleStartTs = |
| | | window.performance && performance.now ? performance.now() : Date.now(); |
| | | this.fpsTickerHandler = function () { |
| | | var now = window.performance && performance.now ? performance.now() : Date.now(); |
| | | self.fpsFrameCount += 1; |
| | | var elapsed = now - self.fpsSampleStartTs; |
| | | if (elapsed < 400) { |
| | | return; |
| | | } |
| | | self.fpsValue = Math.max(0, Math.round((self.fpsFrameCount * 1000) / elapsed)); |
| | | self.fpsFrameCount = 0; |
| | | self.fpsSampleStartTs = now; |
| | | }; |
| | | this.pixiApp.ticker.add(this.fpsTickerHandler); |
| | | }, |
| | | stopFpsTicker: function () { |
| | | if (this.pixiApp && this.pixiApp.ticker && this.fpsTickerHandler) { |
| | | this.pixiApp.ticker.remove(this.fpsTickerHandler); |
| | | } |
| | | this.fpsTickerHandler = null; |
| | | this.fpsFrameCount = 0; |
| | | this.fpsSampleStartTs = 0; |
| | | }, |
| | | initPixi: function () { |
| | | var host = this.$refs.canvasHost; |
| | | if (!host) { |
| | | return; |
| | | } |
| | | var resolution = getPreferredResolution(); |
| | | this.pixiResolution = resolution; |
| | | var app = new PIXI.Application({ |
| | | width: Math.max(host.clientWidth, 320), |
| | | height: Math.max(host.clientHeight, 320), |
| | | antialias: false, |
| | | autoDensity: true, |
| | | backgroundAlpha: 1, |
| | | backgroundColor: 0xf6f9fc, |
| | | resolution: resolution, |
| | | powerPreference: 'high-performance' |
| | | }); |
| | | host.innerHTML = ''; |
| | | host.appendChild(app.view); |
| | | app.view.style.width = '100%'; |
| | | app.view.style.height = '100%'; |
| | | app.view.style.touchAction = 'none'; |
| | | app.view.style.background = '#f6f9fc'; |
| | | app.renderer.roundPixels = true; |
| | | |
| | | this.pixiApp = app; |
| | | this.mapRoot = new PIXI.Container(); |
| | | app.stage.addChild(this.mapRoot); |
| | | |
| | | this.gridLayer = new PIXI.Graphics(); |
| | | this.staticLayer = new PIXI.Container(); |
| | | this.staticTrackSpriteLayer = null; |
| | | this.staticNodeSpriteLayer = null; |
| | | this.trackLayer = new PIXI.Graphics(); |
| | | this.nodeLayer = new PIXI.Graphics(); |
| | | this.eraseLayer = new PIXI.Graphics(); |
| | | this.patchObjectLayer = new PIXI.Graphics(); |
| | | this.activeLayer = new PIXI.Graphics(); |
| | | this.labelLayer = new PIXI.Container(); |
| | | this.selectionLayer = new PIXI.Graphics(); |
| | | this.guideLayer = new PIXI.Graphics(); |
| | | this.guideText = new PIXI.Text('', { |
| | | fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif', |
| | | fontSize: 14, |
| | | fontWeight: '700', |
| | | fill: 0x1f4f86, |
| | | stroke: 0xffffff, |
| | | strokeThickness: 4, |
| | | lineJoin: 'round' |
| | | }); |
| | | this.guideText.anchor.set(0.5, 1); |
| | | this.guideText.visible = false; |
| | | this.hoverLayer = new PIXI.Graphics(); |
| | | this.staticTrackSpritePool = []; |
| | | this.staticNodeSpritePool = []; |
| | | |
| | | this.mapRoot.addChild(this.gridLayer); |
| | | this.staticTrackSpriteLayer = new PIXI.ParticleContainer( |
| | | 12000, |
| | | { |
| | | position: true, |
| | | scale: true, |
| | | alpha: true, |
| | | tint: true |
| | | }, |
| | | 16384, |
| | | true |
| | | ); |
| | | this.staticNodeSpriteLayer = new PIXI.ParticleContainer( |
| | | 12000, |
| | | { |
| | | position: true, |
| | | scale: true, |
| | | alpha: true, |
| | | tint: true |
| | | }, |
| | | 16384, |
| | | true |
| | | ); |
| | | this.staticLayer.addChild(this.staticTrackSpriteLayer); |
| | | this.staticLayer.addChild(this.trackLayer); |
| | | this.staticLayer.addChild(this.staticNodeSpriteLayer); |
| | | this.staticLayer.addChild(this.nodeLayer); |
| | | this.mapRoot.addChild(this.staticLayer); |
| | | this.mapRoot.addChild(this.eraseLayer); |
| | | this.mapRoot.addChild(this.patchObjectLayer); |
| | | this.mapRoot.addChild(this.activeLayer); |
| | | this.mapRoot.addChild(this.labelLayer); |
| | | this.mapRoot.addChild(this.hoverLayer); |
| | | this.mapRoot.addChild(this.selectionLayer); |
| | | this.mapRoot.addChild(this.guideLayer); |
| | | this.mapRoot.addChild(this.guideText); |
| | | |
| | | this.boundCanvasHandlers = { |
| | | pointerdown: this.onCanvasPointerDown.bind(this), |
| | | wheel: this.onCanvasWheel.bind(this) |
| | | }; |
| | | app.view.addEventListener('pointerdown', this.boundCanvasHandlers.pointerdown); |
| | | app.view.addEventListener('wheel', this.boundCanvasHandlers.wheel, { |
| | | passive: false |
| | | }); |
| | | |
| | | if (window.ResizeObserver) { |
| | | this.resizeObserver = new ResizeObserver(this.handleResize.bind(this)); |
| | | this.resizeObserver.observe(host); |
| | | } |
| | | this.startFpsTicker(); |
| | | this.handleResize(); |
| | | }, |
| | | attachEvents: function () { |
| | | this.boundWindowHandlers = { |
| | | pointermove: this.onWindowPointerMove.bind(this), |
| | | pointerup: this.onWindowPointerUp.bind(this), |
| | | pointercancel: this.onWindowPointerUp.bind(this), |
| | | keydown: this.onWindowKeyDown.bind(this), |
| | | keyup: this.onWindowKeyUp.bind(this), |
| | | beforeunload: this.onBeforeUnload.bind(this), |
| | | resize: this.handleResize.bind(this) |
| | | }; |
| | | window.addEventListener('pointermove', this.boundWindowHandlers.pointermove); |
| | | window.addEventListener('pointerup', this.boundWindowHandlers.pointerup); |
| | | window.addEventListener('pointercancel', this.boundWindowHandlers.pointercancel); |
| | | window.addEventListener('keydown', this.boundWindowHandlers.keydown); |
| | | window.addEventListener('keyup', this.boundWindowHandlers.keyup); |
| | | window.addEventListener('beforeunload', this.boundWindowHandlers.beforeunload); |
| | | window.addEventListener('resize', this.boundWindowHandlers.resize); |
| | | }, |
| | | detachEvents: function () { |
| | | if (this.pixiApp && this.boundCanvasHandlers) { |
| | | this.pixiApp.view.removeEventListener( |
| | | 'pointerdown', |
| | | this.boundCanvasHandlers.pointerdown |
| | | ); |
| | | this.pixiApp.view.removeEventListener('wheel', this.boundCanvasHandlers.wheel); |
| | | } |
| | | if (this.resizeObserver) { |
| | | this.resizeObserver.disconnect(); |
| | | this.resizeObserver = null; |
| | | } |
| | | if (!this.boundWindowHandlers) { |
| | | return; |
| | | } |
| | | window.removeEventListener('pointermove', this.boundWindowHandlers.pointermove); |
| | | window.removeEventListener('pointerup', this.boundWindowHandlers.pointerup); |
| | | window.removeEventListener('pointercancel', this.boundWindowHandlers.pointercancel); |
| | | window.removeEventListener('keydown', this.boundWindowHandlers.keydown); |
| | | window.removeEventListener('keyup', this.boundWindowHandlers.keyup); |
| | | window.removeEventListener('beforeunload', this.boundWindowHandlers.beforeunload); |
| | | window.removeEventListener('resize', this.boundWindowHandlers.resize); |
| | | }, |
| | | handleResize: function () { |
| | | if (!this.pixiApp || !this.$refs.canvasHost) { |
| | | return; |
| | | } |
| | | var host = this.$refs.canvasHost; |
| | | var width = Math.max(host.clientWidth, 320); |
| | | var height = Math.max(host.clientHeight, 320); |
| | | this.pixiApp.renderer.resize(width, height); |
| | | this.markGridSceneDirty(); |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | }, |
| | | loadLevOptions: function () { |
| | | var self = this; |
| | | $.ajax({ |
| | | url: baseUrl + '/basMap/getLevList', |
| | | method: 'GET', |
| | | headers: authHeaders(), |
| | | success: function (res) { |
| | | if (res && res.code === 200 && Array.isArray(res.data)) { |
| | | self.remoteLevOptions = res.data |
| | | .map(function (item) { |
| | | return toInt(item, 0); |
| | | }) |
| | | .filter(function (item) { |
| | | return item > 0; |
| | | }); |
| | | self.refreshLevOptions(); |
| | | } |
| | | seen[item] = true; |
| | | result.push(item); |
| | | } |
| | | }); |
| | | }, |
| | | refreshLevOptions: function () { |
| | | var set = {}; |
| | | var result = []; |
| | | var pushLev = function (lev) { |
| | | lev = toInt(lev, 0); |
| | | if (lev <= 0 || set[lev]) { |
| | | return; |
| | | } |
| | | set[lev] = true; |
| | | result.push(lev); |
| | | }; |
| | | this.remoteLevOptions.forEach(pushLev); |
| | | Object.keys(this.draftDocs || {}).forEach(pushLev); |
| | | pushLev(this.currentLev); |
| | | pushLev(this.floorPickerLev); |
| | | result.sort(function (a, b) { |
| | | return a - b; |
| | | }); |
| | | this.levOptions = result; |
| | | }, |
| | | exportDoc: function (doc) { |
| | | var source = doc || this.doc || {}; |
| | | return { |
| | | lev: toInt(source.lev, 0), |
| | | editorMode: FREE_EDITOR_MODE, |
| | | canvasWidth: roundCoord(toNumber(source.canvasWidth, DEFAULT_CANVAS_WIDTH)), |
| | | canvasHeight: roundCoord(toNumber(source.canvasHeight, DEFAULT_CANVAS_HEIGHT)), |
| | | elements: (source.elements || []).map(function (item, index) { |
| | | const info = { |
| | | id: item && item.id ? String(item.id) : 'el_' + (index + 1), |
| | | type: DRAW_TYPES.indexOf(item && item.type) >= 0 ? item.type : 'shelf', |
| | | x: roundCoord(Math.max(0, toNumber(item && item.x, 0))), |
| | | y: roundCoord(Math.max(0, toNumber(item && item.y, 0))), |
| | | width: roundCoord( |
| | | Math.max(MIN_ELEMENT_SIZE, toNumber(item && item.width, MIN_ELEMENT_SIZE)) |
| | | ), |
| | | height: roundCoord( |
| | | Math.max(MIN_ELEMENT_SIZE, toNumber(item && item.height, MIN_ELEMENT_SIZE)) |
| | | ), |
| | | value: normalizeValue(item && item.value), |
| | | shape: item.shape, |
| | | pathList: item.pathList, |
| | | turningPoint: item.turningPoint |
| | | ? { x: toNumber(item.turningPoint.x, 0), y: toNumber(item.turningPoint.y, 0) } |
| | | : undefined, |
| | | annulusBandInset: |
| | | item.annulusBandInset != null ? toNumber(item.annulusBandInset, 0) : undefined |
| | | }; |
| | | return info; |
| | | }) |
| | | }; |
| | | }, |
| | | normalizeDoc: function (doc) { |
| | | var normalized = this.exportDoc(doc || {}); |
| | | if (normalized.lev <= 0) { |
| | | normalized.lev = toInt(this.currentLev, 1) || 1; |
| | | } |
| | | return normalized; |
| | | }, |
| | | snapshotDoc: function (doc) { |
| | | return JSON.stringify(this.exportDoc(doc)); |
| | | }, |
| | | syncDirty: function () { |
| | | var currentSnapshot = this.snapshotDoc(this.doc); |
| | | this.isDirty = currentSnapshot !== this.savedSnapshot; |
| | | }, |
| | | setDraftDocEntry: function (lev, doc, savedSnapshot) { |
| | | lev = toInt(lev, 0); |
| | | if (lev <= 0 || !doc) { |
| | | return; |
| | | } |
| | | var entry = { |
| | | doc: this.exportDoc(doc), |
| | | savedSnapshot: savedSnapshot != null ? savedSnapshot : '' |
| | | }; |
| | | if (this.$set) { |
| | | this.$set(this.draftDocs, lev, entry); |
| | | } else { |
| | | this.draftDocs[lev] = entry; |
| | | } |
| | | }, |
| | | removeDraftDocEntry: function (lev) { |
| | | lev = toInt(lev, 0); |
| | | if (lev <= 0) { |
| | | return; |
| | | } |
| | | if (this.$delete) { |
| | | this.$delete(this.draftDocs, lev); |
| | | } else { |
| | | delete this.draftDocs[lev]; |
| | | } |
| | | }, |
| | | cacheCurrentDraft: function () { |
| | | if (!this.doc || !this.doc.lev) { |
| | | return; |
| | | } |
| | | this.setDraftDocEntry(this.doc.lev, this.doc, this.savedSnapshot); |
| | | this.refreshLevOptions(); |
| | | }, |
| | | clearCurrentDraftIfSaved: function () { |
| | | if (!this.doc || !this.doc.lev) { |
| | | return; |
| | | } |
| | | this.setDraftDocEntry(this.doc.lev, this.doc, this.savedSnapshot); |
| | | }, |
| | | clearFloorTransientState: function () { |
| | | this.clearDeferredStaticCommit(); |
| | | this.interactionState = null; |
| | | this.currentPointerId = null; |
| | | this.hoverElementId = ''; |
| | | this.pointerStatus = '--'; |
| | | this.lastPointerStatusUpdateTs = 0; |
| | | this.selectedIds = []; |
| | | this.isPanning = false; |
| | | this.isZooming = false; |
| | | this.pendingViewportRefresh = false; |
| | | if (this.zoomRefreshTimer) { |
| | | window.clearTimeout(this.zoomRefreshTimer); |
| | | this.zoomRefreshTimer = null; |
| | | } |
| | | if (this.panRefreshTimer) { |
| | | window.clearTimeout(this.panRefreshTimer); |
| | | this.panRefreshTimer = null; |
| | | } |
| | | }, |
| | | resetRenderLayers: function () { |
| | | if (this.gridLayer) { |
| | | this.gridLayer.clear(); |
| | | } |
| | | if (this.trackLayer) { |
| | | this.trackLayer.clear(); |
| | | this.trackLayer.removeChildren(); |
| | | } |
| | | if (this.nodeLayer) { |
| | | this.nodeLayer.clear(); |
| | | this.nodeLayer.removeChildren(); |
| | | } |
| | | if (this.eraseLayer) { |
| | | this.eraseLayer.clear(); |
| | | } |
| | | if (this.patchObjectLayer) { |
| | | this.patchObjectLayer.clear(); |
| | | this.patchObjectLayer.removeChildren(); |
| | | } |
| | | if (this.activeLayer) { |
| | | this.activeLayer.clear(); |
| | | this.activeLayer.removeChildren(); |
| | | } |
| | | if (this.selectionLayer) { |
| | | this.selectionLayer.clear(); |
| | | this.selectionLayer.removeChildren(); |
| | | } |
| | | if (this.guideLayer) { |
| | | this.guideLayer.clear(); |
| | | this.guideLayer.removeChildren(); |
| | | } |
| | | if (this.hoverLayer) { |
| | | this.hoverLayer.clear(); |
| | | } |
| | | if (this.guideText) { |
| | | this.guideText.visible = false; |
| | | this.guideText.text = ''; |
| | | } |
| | | if (this.labelLayer) { |
| | | this.labelLayer.visible = false; |
| | | } |
| | | for (var i = 0; i < this.labelPool.length; i++) { |
| | | this.labelPool[i].visible = false; |
| | | this.labelPool[i].text = ''; |
| | | } |
| | | this.hideUnusedStaticSprites(this.staticTrackSpritePool || [], 0); |
| | | this.hideUnusedStaticSprites(this.staticNodeSpritePool || [], 0); |
| | | if (this.staticTrackSpriteLayer) { |
| | | this.staticTrackSpriteLayer.removeChildren(); |
| | | this.staticTrackSpritePool = []; |
| | | } |
| | | if (this.staticNodeSpriteLayer) { |
| | | this.staticNodeSpriteLayer.removeChildren(); |
| | | this.staticNodeSpritePool = []; |
| | | } |
| | | if (this.staticTrackSpriteLayer) { |
| | | this.staticTrackSpriteLayer.visible = false; |
| | | } |
| | | if (this.staticNodeSpriteLayer) { |
| | | this.staticNodeSpriteLayer.visible = false; |
| | | } |
| | | }, |
| | | hasDirtyDraft: function (lev) { |
| | | lev = toInt(lev, 0); |
| | | if (lev <= 0) { |
| | | return false; |
| | | } |
| | | var entry = this.draftDocs[lev]; |
| | | if (!entry || !entry.doc) { |
| | | return false; |
| | | } |
| | | var snapshot = this.snapshotDoc(entry.doc); |
| | | return snapshot !== (entry.savedSnapshot || ''); |
| | | }, |
| | | markStaticSceneDirty: function () { |
| | | this.staticSceneDirty = true; |
| | | }, |
| | | markGridSceneDirty: function () { |
| | | this.gridSceneDirty = true; |
| | | }, |
| | | clearRenderCaches: function () { |
| | | this.gridRenderRect = null; |
| | | this.gridRenderKey = ''; |
| | | this.staticRenderRect = null; |
| | | this.staticRenderKey = ''; |
| | | this.staticExcludedKey = ''; |
| | | }, |
| | | scheduleZoomRefresh: function () { |
| | | if (this.zoomRefreshTimer) { |
| | | window.clearTimeout(this.zoomRefreshTimer); |
| | | } |
| | | this.isZooming = true; |
| | | this.zoomRefreshTimer = window.setTimeout( |
| | | function () { |
| | | this.zoomRefreshTimer = null; |
| | | this.isZooming = false; |
| | | if (this.isPanning || (this.interactionState && this.interactionState.type === 'pan')) { |
| | | this.pendingViewportRefresh = true; |
| | | return; |
| | | } |
| | | this.markGridSceneDirty(); |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | }.bind(this), |
| | | ZOOM_REFRESH_DELAY |
| | | ); |
| | | }, |
| | | cancelPanRefresh: function () { |
| | | if (this.panRefreshTimer) { |
| | | window.clearTimeout(this.panRefreshTimer); |
| | | this.panRefreshTimer = null; |
| | | } |
| | | }, |
| | | schedulePanRefresh: function () { |
| | | this.cancelPanRefresh(); |
| | | this.isPanning = true; |
| | | this.panRefreshTimer = window.setTimeout( |
| | | function () { |
| | | this.panRefreshTimer = null; |
| | | this.isPanning = false; |
| | | if (this.pendingViewportRefresh) { |
| | | this.pendingViewportRefresh = false; |
| | | this.markGridSceneDirty(); |
| | | this.markStaticSceneDirty(); |
| | | } |
| | | this.scheduleRender(); |
| | | }.bind(this), |
| | | PAN_LABEL_REFRESH_DELAY |
| | | ); |
| | | }, |
| | | rebuildLabelCapability: function () { |
| | | var maxWidth = 0; |
| | | var maxHeight = 0; |
| | | var elements = this.doc && this.doc.elements ? this.doc.elements : []; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | if (element.width > maxWidth) { |
| | | maxWidth = element.width; |
| | | } |
| | | if (element.height > maxHeight) { |
| | | maxHeight = element.height; |
| | | } |
| | | } |
| | | this.labelCapability = { |
| | | maxWidth: maxWidth, |
| | | maxHeight: maxHeight |
| | | }; |
| | | this.labelCapabilityDirty = false; |
| | | }, |
| | | ensureLabelCapability: function () { |
| | | if (this.labelCapabilityDirty) { |
| | | this.rebuildLabelCapability(); |
| | | } |
| | | return this.labelCapability; |
| | | }, |
| | | markSpatialIndexDirty: function () { |
| | | this.spatialIndexDirty = true; |
| | | }, |
| | | rebuildSpatialIndex: function () { |
| | | var buckets = {}; |
| | | var elements = this.doc && this.doc.elements ? this.doc.elements : []; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | var minX = Math.floor(element.x / SPATIAL_BUCKET_SIZE); |
| | | var maxX = Math.floor((element.x + element.width) / SPATIAL_BUCKET_SIZE); |
| | | var minY = Math.floor(element.y / SPATIAL_BUCKET_SIZE); |
| | | var maxY = Math.floor((element.y + element.height) / SPATIAL_BUCKET_SIZE); |
| | | for (var bx = minX; bx <= maxX; bx++) { |
| | | for (var by = minY; by <= maxY; by++) { |
| | | var key = bucketKey(bx, by); |
| | | if (!buckets[key]) { |
| | | buckets[key] = []; |
| | | } |
| | | buckets[key].push(element); |
| | | } |
| | | } |
| | | } |
| | | this.spatialBuckets = buckets; |
| | | this.spatialIndexDirty = false; |
| | | }, |
| | | ensureSpatialIndex: function () { |
| | | if (this.spatialIndexDirty || !this.spatialBuckets) { |
| | | this.rebuildSpatialIndex(); |
| | | } |
| | | }, |
| | | querySpatialCandidates: function (rect, padding, excludeIds) { |
| | | if (!this.doc || !rect) { |
| | | return []; |
| | | } |
| | | this.ensureSpatialIndex(); |
| | | var excludeMap = {}; |
| | | excludeIds = excludeIds || []; |
| | | for (var i = 0; i < excludeIds.length; i++) { |
| | | excludeMap[excludeIds[i]] = true; |
| | | } |
| | | var seen = {}; |
| | | var result = []; |
| | | var pad = Math.max(0, padding || 0); |
| | | var minX = Math.floor((rect.x - pad) / SPATIAL_BUCKET_SIZE); |
| | | var maxX = Math.floor((rect.x + rect.width + pad) / SPATIAL_BUCKET_SIZE); |
| | | var minY = Math.floor((rect.y - pad) / SPATIAL_BUCKET_SIZE); |
| | | var maxY = Math.floor((rect.y + rect.height + pad) / SPATIAL_BUCKET_SIZE); |
| | | for (var bx = minX; bx <= maxX; bx++) { |
| | | for (var by = minY; by <= maxY; by++) { |
| | | var key = bucketKey(bx, by); |
| | | var bucket = this.spatialBuckets[key]; |
| | | if (!bucket || !bucket.length) { |
| | | continue; |
| | | } |
| | | for (var j = 0; j < bucket.length; j++) { |
| | | var element = bucket[j]; |
| | | if (!element || seen[element.id] || excludeMap[element.id]) { |
| | | continue; |
| | | } |
| | | seen[element.id] = true; |
| | | result.push(element); |
| | | } |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | function directionTokenToArrow(token) { |
| | | if (token === 'top' || token === 'up' || token === 'north' || token === 'n') { |
| | | return '↑'; |
| | | }, |
| | | cancelDeferredStaticRebuild: function () { |
| | | if (this.deferredStaticRebuildTimer) { |
| | | window.clearTimeout(this.deferredStaticRebuildTimer); |
| | | this.deferredStaticRebuildTimer = null; |
| | | } |
| | | if (token === 'right' || token === 'east' || token === 'e') { |
| | | return '→'; |
| | | } |
| | | if (token === 'bottom' || token === 'down' || token === 'south' || token === 's') { |
| | | return '↓'; |
| | | } |
| | | if (token === 'left' || token === 'west' || token === 'w') { |
| | | return '←'; |
| | | } |
| | | return ''; |
| | | } |
| | | |
| | | function formatDirectionArrows(direction) { |
| | | var list = normalizeDirectionList(direction); |
| | | var arrows = []; |
| | | for (var i = 0; i < list.length; i++) { |
| | | var arrow = directionTokenToArrow(list[i]); |
| | | if (arrow) { |
| | | arrows.push(arrow); |
| | | } |
| | | } |
| | | return arrows.join(''); |
| | | } |
| | | |
| | | function isDeviceConfigType(type) { |
| | | return DEVICE_CONFIG_TYPES.indexOf(type) >= 0; |
| | | } |
| | | |
| | | function pickDeviceValueKey(type, json) { |
| | | if (json && json.deviceNo != null) { |
| | | return 'deviceNo'; |
| | | } |
| | | if ((type === 'crn' || type === 'dualCrn') && json && json.crnNo != null) { |
| | | return 'crnNo'; |
| | | } |
| | | if (type === 'rgv' && json && json.rgvNo != null) { |
| | | return 'rgvNo'; |
| | | } |
| | | return 'deviceNo'; |
| | | } |
| | | |
| | | function isInputLike(target) { |
| | | if (!target || !target.tagName) { |
| | | return false; |
| | | } |
| | | var tag = String(target.tagName || '').toLowerCase(); |
| | | return tag === 'input' || tag === 'textarea' || tag === 'select' || !!target.isContentEditable; |
| | | } |
| | | |
| | | function rectsOverlap(a, b) { |
| | | return a.x < b.x + b.width - COORD_EPSILON && a.x + a.width > b.x + COORD_EPSILON |
| | | && a.y < b.y + b.height - COORD_EPSILON && a.y + a.height > b.y + COORD_EPSILON; |
| | | } |
| | | |
| | | function rectIntersects(a, b) { |
| | | return a.x <= b.x + b.width && a.x + a.width >= b.x |
| | | && a.y <= b.y + b.height && a.y + a.height >= b.y; |
| | | } |
| | | |
| | | function isRectWithinCanvas(rect, canvasWidth, canvasHeight) { |
| | | return rect.x >= -COORD_EPSILON && rect.y >= -COORD_EPSILON |
| | | && rect.x + rect.width <= canvasWidth + COORD_EPSILON |
| | | && rect.y + rect.height <= canvasHeight + COORD_EPSILON; |
| | | } |
| | | |
| | | function findDocOverlapId(doc) { |
| | | if (!doc || !doc.elements || !doc.elements.length) { |
| | | return ''; |
| | | } |
| | | var buckets = {}; |
| | | var elements = doc.elements; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | var minX = Math.floor(element.x / SPATIAL_BUCKET_SIZE); |
| | | var maxX = Math.floor((element.x + element.width) / SPATIAL_BUCKET_SIZE); |
| | | var minY = Math.floor(element.y / SPATIAL_BUCKET_SIZE); |
| | | var maxY = Math.floor((element.y + element.height) / SPATIAL_BUCKET_SIZE); |
| | | for (var bx = minX; bx <= maxX; bx++) { |
| | | for (var by = minY; by <= maxY; by++) { |
| | | var key = bucketKey(bx, by); |
| | | var bucket = buckets[key]; |
| | | if (!bucket || !bucket.length) { |
| | | continue; |
| | | } |
| | | for (var j = 0; j < bucket.length; j++) { |
| | | if (rectsOverlap(element, bucket[j])) { |
| | | return element.id || ('el_' + i); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | for (bx = minX; bx <= maxX; bx++) { |
| | | for (by = minY; by <= maxY; by++) { |
| | | key = bucketKey(bx, by); |
| | | if (!buckets[key]) { |
| | | buckets[key] = []; |
| | | } |
| | | buckets[key].push(element); |
| | | } |
| | | } |
| | | } |
| | | return ''; |
| | | } |
| | | |
| | | function buildRectFromPoints(a, b) { |
| | | var left = Math.min(a.x, b.x); |
| | | var top = Math.min(a.y, b.y); |
| | | var right = Math.max(a.x, b.x); |
| | | var bottom = Math.max(a.y, b.y); |
| | | return { |
| | | x: roundCoord(left), |
| | | y: roundCoord(top), |
| | | width: roundCoord(right - left), |
| | | height: roundCoord(bottom - top) |
| | | }; |
| | | } |
| | | |
| | | function getTypeMeta(type) { |
| | | return TYPE_META[type] || TYPE_META.shelf; |
| | | } |
| | | |
| | | function rangesNearOrOverlap(a1, a2, b1, b2, tolerance) { |
| | | return a1 <= b2 + tolerance && a2 >= b1 - tolerance; |
| | | } |
| | | |
| | | function bucketKey(x, y) { |
| | | return x + ':' + y; |
| | | } |
| | | |
| | | function getPreferredResolution() { |
| | | return Math.min(window.devicePixelRatio || 1, 1.25); |
| | | } |
| | | |
| | | new Vue({ |
| | | el: '#app', |
| | | data: function () { |
| | | }, |
| | | stageDeferredStaticCommit: function (ids, eraseRects) { |
| | | this.pendingStaticCommit = { |
| | | ids: (ids || []).slice(), |
| | | eraseRects: (eraseRects || []).map(function (item) { |
| | | return { |
| | | remoteLevOptions: [], |
| | | levOptions: [], |
| | | currentLev: null, |
| | | floorPickerLev: null, |
| | | draftDocs: {}, |
| | | doc: null, |
| | | activeTool: 'select', |
| | | toolPanelCollapsed: false, |
| | | inspectorPanelCollapsed: false, |
| | | interactionTools: [ |
| | | { key: 'select', label: '选择 / 移动', desc: '点击元素选择,拖拽移动,空白处拖动画布' }, |
| | | { key: 'marquee', label: '框选', desc: '在画布中框选一组元素' }, |
| | | { key: 'array', label: '阵列', desc: '选中一个货架 / 轨道后拖一条线自动生成一排' }, |
| | | { key: 'pan', label: '平移', desc: '专门用于拖动画布和观察全图' } |
| | | ], |
| | | drawTools: [ |
| | | { key: 'shelf', label: '货架', desc: '自由拉出货架矩形' }, |
| | | { key: 'devp', label: '输送线', desc: '拉出站点 / 输送线矩形' }, |
| | | { key: 'crn', label: 'CRN', desc: '拉出堆垛机轨道矩形' }, |
| | | { key: 'dualCrn', label: '双工位', desc: '拉出双工位轨道矩形' }, |
| | | { key: 'rgv', label: 'RGV', desc: '拉出 RGV 轨道矩形' } |
| | | ], |
| | | pixiApp: null, |
| | | mapRoot: null, |
| | | gridLayer: null, |
| | | trackLayer: null, |
| | | nodeLayer: null, |
| | | patchObjectLayer: null, |
| | | activeLayer: null, |
| | | labelLayer: null, |
| | | selectionLayer: null, |
| | | guideLayer: null, |
| | | guideText: null, |
| | | hoverLayer: null, |
| | | labelPool: [], |
| | | renderQueued: false, |
| | | gridSceneDirty: true, |
| | | staticSceneDirty: true, |
| | | spatialIndexDirty: true, |
| | | spatialBuckets: null, |
| | | gridRenderRect: null, |
| | | gridRenderKey: '', |
| | | staticRenderRect: null, |
| | | staticRenderKey: '', |
| | | staticExcludedKey: '', |
| | | camera: { |
| | | x: 80, |
| | | y: 80, |
| | | scale: 1 |
| | | }, |
| | | viewZoom: 1, |
| | | selectedIds: [], |
| | | clipboard: [], |
| | | hoverElementId: '', |
| | | pointerStatus: '--', |
| | | lastPointerStatusUpdateTs: 0, |
| | | pixiResolution: getPreferredResolution(), |
| | | fpsValue: 0, |
| | | fpsFrameCount: 0, |
| | | fpsSampleStartTs: 0, |
| | | fpsTickerHandler: null, |
| | | interactionState: null, |
| | | isZooming: false, |
| | | isPanning: false, |
| | | zoomRefreshTimer: null, |
| | | panRefreshTimer: null, |
| | | pendingViewportRefresh: false, |
| | | pendingStaticCommit: null, |
| | | deferredStaticRebuildTimer: null, |
| | | currentPointerId: null, |
| | | boundCanvasHandlers: null, |
| | | boundWindowHandlers: null, |
| | | resizeObserver: null, |
| | | labelCapability: { |
| | | maxWidth: 0, |
| | | maxHeight: 0 |
| | | }, |
| | | labelCapabilityDirty: true, |
| | | undoStack: [], |
| | | redoStack: [], |
| | | savedSnapshot: '', |
| | | isDirty: false, |
| | | saving: false, |
| | | savingAll: false, |
| | | loadingFloor: false, |
| | | switchingFloorLev: null, |
| | | floorRequestSeq: 0, |
| | | activeFloorRequestSeq: 0, |
| | | blankDialogVisible: false, |
| | | blankForm: { |
| | | lev: '', |
| | | width: String(DEFAULT_CANVAS_WIDTH), |
| | | height: String(DEFAULT_CANVAS_HEIGHT) |
| | | }, |
| | | canvasForm: { |
| | | width: String(DEFAULT_CANVAS_WIDTH), |
| | | height: String(DEFAULT_CANVAS_HEIGHT) |
| | | }, |
| | | geometryForm: { |
| | | x: '', |
| | | y: '', |
| | | width: '', |
| | | height: '' |
| | | }, |
| | | devpForm: { |
| | | stationId: '', |
| | | deviceNo: '', |
| | | direction: [], |
| | | isBarcodeStation: false, |
| | | barcodeIdx: '', |
| | | backStation: '', |
| | | backStationDeviceNo: '', |
| | | isInStation: false, |
| | | barcodeStation: '', |
| | | barcodeStationDeviceNo: '', |
| | | isOutStation: false, |
| | | runBlockReassign: false, |
| | | isOutOrder: false, |
| | | isLiftTransfer: false |
| | | }, |
| | | deviceForm: { |
| | | valueKey: '', |
| | | deviceNo: '' |
| | | }, |
| | | devpDirectionOptions: DEVP_DIRECTION_OPTIONS, |
| | | shelfFillForm: { |
| | | startValue: '', |
| | | rowStep: 'desc', |
| | | colStep: 'asc' |
| | | }, |
| | | valueEditorText: '', |
| | | spacePressed: false, |
| | | lastCursor: 'default' |
| | | x: item.x, |
| | | y: item.y, |
| | | width: item.width, |
| | | height: item.height |
| | | }; |
| | | }, |
| | | computed: { |
| | | singleSelectedElement: function () { |
| | | if (!this.doc || this.selectedIds.length !== 1) { |
| | | return null; |
| | | } |
| | | return this.findElementById(this.selectedIds[0]); |
| | | }, |
| | | singleSelectedDeviceElement: function () { |
| | | if (!this.singleSelectedElement || !isDeviceConfigType(this.singleSelectedElement.type)) { |
| | | return null; |
| | | } |
| | | return this.singleSelectedElement; |
| | | }, |
| | | selectedShelfElements: function () { |
| | | if (!this.doc || !this.selectedIds.length) { |
| | | return []; |
| | | } |
| | | return this.getSelectedElements().filter(function (item) { |
| | | return item && item.type === 'shelf'; |
| | | }); |
| | | }, |
| | | devpRequiresBarcodeLink: function () { |
| | | return !!(this.devpForm && this.devpForm.isInStation); |
| | | }, |
| | | devpRequiresBarcodeIndex: function () { |
| | | return !!(this.devpForm && this.devpForm.isBarcodeStation); |
| | | }, |
| | | devpRequiresBackStation: function () { |
| | | return !!(this.devpForm && this.devpForm.isBarcodeStation); |
| | | }, |
| | | arrayPreviewCount: function () { |
| | | if (!this.interactionState || this.interactionState.type !== 'array') { |
| | | return 0; |
| | | } |
| | | return this.interactionState.previewItems ? this.interactionState.previewItems.length : 0; |
| | | }, |
| | | viewPercent: function () { |
| | | return Math.round(this.viewZoom * 100); |
| | | }, |
| | | fpsText: function () { |
| | | return this.fpsValue > 0 ? String(this.fpsValue) : '--'; |
| | | }, |
| | | dirtyDraftLevs: function () { |
| | | var result = []; |
| | | var seen = {}; |
| | | if (this.doc && this.doc.lev && this.isDirty) { |
| | | var currentLev = toInt(this.doc.lev, 0); |
| | | if (currentLev > 0) { |
| | | seen[currentLev] = true; |
| | | result.push(currentLev); |
| | | } |
| | | } |
| | | var self = this; |
| | | Object.keys(this.draftDocs || {}).forEach(function (key) { |
| | | var lev = toInt(key, 0); |
| | | if (lev <= 0 || seen[lev]) { |
| | | return; |
| | | } |
| | | if (self.hasDirtyDraft(lev)) { |
| | | seen[lev] = true; |
| | | result.push(lev); |
| | | } |
| | | }); |
| | | result.sort(function (a, b) { return a - b; }); |
| | | return result; |
| | | }, |
| | | dirtyDraftCount: function () { |
| | | return this.dirtyDraftLevs.length; |
| | | } |
| | | }, |
| | | mounted: function () { |
| | | this.initPixi(); |
| | | this.attachEvents(); |
| | | this.loadLevOptions(); |
| | | var lev = toInt(getQueryParam('lev'), 0); |
| | | if (lev > 0) { |
| | | this.floorPickerLev = lev; |
| | | this.fetchFloor(lev); |
| | | } else { |
| | | this.createLocalBlankDoc(1, DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT, ''); |
| | | } |
| | | }, |
| | | beforeDestroy: function () { |
| | | this.detachEvents(); |
| | | if (this.zoomRefreshTimer) { |
| | | window.clearTimeout(this.zoomRefreshTimer); |
| | | this.zoomRefreshTimer = null; |
| | | } |
| | | if (this.panRefreshTimer) { |
| | | window.clearTimeout(this.panRefreshTimer); |
| | | this.panRefreshTimer = null; |
| | | } |
| | | this.clearDeferredStaticCommit(); |
| | | this.stopFpsTicker(); |
| | | if (this.pixiApp) { |
| | | this.pixiApp.destroy(true, { children: true }); |
| | | this.pixiApp = null; |
| | | } |
| | | }, |
| | | methods: { |
| | | showMessage: function (type, message) { |
| | | if (this.$message) { |
| | | this.$message({ type: type, message: message }); |
| | | } |
| | | }, |
| | | formatNumber: function (value) { |
| | | var num = toNumber(value, 0); |
| | | if (Math.abs(num) >= 1000 || num === Math.round(num)) { |
| | | return String(Math.round(num)); |
| | | } |
| | | return String(Math.round(num * 100) / 100); |
| | | }, |
| | | syncFloorQueryParam: function (lev) { |
| | | lev = toInt(lev, 0); |
| | | if (lev <= 0 || !window.history || !window.history.replaceState || !window.URL) { |
| | | return; |
| | | } |
| | | try { |
| | | var url = new URL(window.location.href); |
| | | url.searchParams.set('lev', String(lev)); |
| | | window.history.replaceState(null, '', url.toString()); |
| | | } catch (e) { |
| | | // Ignore URL sync failures and keep editor usable. |
| | | } |
| | | }, |
| | | toolLabel: function (tool) { |
| | | var list = this.interactionTools.concat(this.drawTools); |
| | | for (var i = 0; i < list.length; i++) { |
| | | if (list[i].key === tool) { |
| | | return list[i].label; |
| | | } |
| | | } |
| | | return tool || '--'; |
| | | }, |
| | | toggleToolPanel: function () { |
| | | this.toolPanelCollapsed = !this.toolPanelCollapsed; |
| | | }, |
| | | toggleInspectorPanel: function () { |
| | | this.inspectorPanelCollapsed = !this.inspectorPanelCollapsed; |
| | | }, |
| | | startFpsTicker: function () { |
| | | if (!this.pixiApp || !this.pixiApp.ticker || this.fpsTickerHandler) { |
| | | return; |
| | | } |
| | | var self = this; |
| | | this.fpsValue = 0; |
| | | this.fpsFrameCount = 0; |
| | | this.fpsSampleStartTs = (window.performance && performance.now) ? performance.now() : Date.now(); |
| | | this.fpsTickerHandler = function () { |
| | | var now = (window.performance && performance.now) ? performance.now() : Date.now(); |
| | | self.fpsFrameCount += 1; |
| | | var elapsed = now - self.fpsSampleStartTs; |
| | | if (elapsed < 400) { |
| | | return; |
| | | } |
| | | self.fpsValue = Math.max(0, Math.round(self.fpsFrameCount * 1000 / elapsed)); |
| | | self.fpsFrameCount = 0; |
| | | self.fpsSampleStartTs = now; |
| | | }; |
| | | this.pixiApp.ticker.add(this.fpsTickerHandler); |
| | | }, |
| | | stopFpsTicker: function () { |
| | | if (this.pixiApp && this.pixiApp.ticker && this.fpsTickerHandler) { |
| | | this.pixiApp.ticker.remove(this.fpsTickerHandler); |
| | | } |
| | | this.fpsTickerHandler = null; |
| | | this.fpsFrameCount = 0; |
| | | this.fpsSampleStartTs = 0; |
| | | }, |
| | | initPixi: function () { |
| | | var host = this.$refs.canvasHost; |
| | | if (!host) { |
| | | return; |
| | | } |
| | | var resolution = getPreferredResolution(); |
| | | this.pixiResolution = resolution; |
| | | var app = new PIXI.Application({ |
| | | width: Math.max(host.clientWidth, 320), |
| | | height: Math.max(host.clientHeight, 320), |
| | | antialias: false, |
| | | autoDensity: true, |
| | | backgroundAlpha: 1, |
| | | backgroundColor: 0xf6f9fc, |
| | | resolution: resolution, |
| | | powerPreference: 'high-performance' |
| | | }); |
| | | host.innerHTML = ''; |
| | | host.appendChild(app.view); |
| | | app.view.style.width = '100%'; |
| | | app.view.style.height = '100%'; |
| | | app.view.style.touchAction = 'none'; |
| | | app.view.style.background = '#f6f9fc'; |
| | | app.renderer.roundPixels = true; |
| | | |
| | | this.pixiApp = app; |
| | | this.mapRoot = new PIXI.Container(); |
| | | app.stage.addChild(this.mapRoot); |
| | | |
| | | this.gridLayer = new PIXI.Graphics(); |
| | | this.staticLayer = new PIXI.Container(); |
| | | this.staticTrackSpriteLayer = null; |
| | | this.staticNodeSpriteLayer = null; |
| | | this.trackLayer = new PIXI.Graphics(); |
| | | this.nodeLayer = new PIXI.Graphics(); |
| | | this.eraseLayer = new PIXI.Graphics(); |
| | | this.patchObjectLayer = new PIXI.Graphics(); |
| | | this.activeLayer = new PIXI.Graphics(); |
| | | this.labelLayer = new PIXI.Container(); |
| | | this.selectionLayer = new PIXI.Graphics(); |
| | | this.guideLayer = new PIXI.Graphics(); |
| | | this.guideText = new PIXI.Text('', { |
| | | fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif', |
| | | fontSize: 14, |
| | | fontWeight: '700', |
| | | fill: 0x1f4f86, |
| | | stroke: 0xffffff, |
| | | strokeThickness: 4, |
| | | lineJoin: 'round' |
| | | }); |
| | | this.guideText.anchor.set(0.5, 1); |
| | | this.guideText.visible = false; |
| | | this.hoverLayer = new PIXI.Graphics(); |
| | | this.staticTrackSpritePool = []; |
| | | this.staticNodeSpritePool = []; |
| | | |
| | | this.mapRoot.addChild(this.gridLayer); |
| | | this.staticTrackSpriteLayer = new PIXI.ParticleContainer(12000, { |
| | | position: true, |
| | | scale: true, |
| | | alpha: true, |
| | | tint: true |
| | | }, 16384, true); |
| | | this.staticNodeSpriteLayer = new PIXI.ParticleContainer(12000, { |
| | | position: true, |
| | | scale: true, |
| | | alpha: true, |
| | | tint: true |
| | | }, 16384, true); |
| | | this.staticLayer.addChild(this.staticTrackSpriteLayer); |
| | | this.staticLayer.addChild(this.trackLayer); |
| | | this.staticLayer.addChild(this.staticNodeSpriteLayer); |
| | | this.staticLayer.addChild(this.nodeLayer); |
| | | this.mapRoot.addChild(this.staticLayer); |
| | | this.mapRoot.addChild(this.eraseLayer); |
| | | this.mapRoot.addChild(this.patchObjectLayer); |
| | | this.mapRoot.addChild(this.activeLayer); |
| | | this.mapRoot.addChild(this.labelLayer); |
| | | this.mapRoot.addChild(this.hoverLayer); |
| | | this.mapRoot.addChild(this.selectionLayer); |
| | | this.mapRoot.addChild(this.guideLayer); |
| | | this.mapRoot.addChild(this.guideText); |
| | | |
| | | this.boundCanvasHandlers = { |
| | | pointerdown: this.onCanvasPointerDown.bind(this), |
| | | wheel: this.onCanvasWheel.bind(this) |
| | | }; |
| | | app.view.addEventListener('pointerdown', this.boundCanvasHandlers.pointerdown); |
| | | app.view.addEventListener('wheel', this.boundCanvasHandlers.wheel, { passive: false }); |
| | | |
| | | if (window.ResizeObserver) { |
| | | this.resizeObserver = new ResizeObserver(this.handleResize.bind(this)); |
| | | this.resizeObserver.observe(host); |
| | | } |
| | | this.startFpsTicker(); |
| | | this.handleResize(); |
| | | }, |
| | | attachEvents: function () { |
| | | this.boundWindowHandlers = { |
| | | pointermove: this.onWindowPointerMove.bind(this), |
| | | pointerup: this.onWindowPointerUp.bind(this), |
| | | pointercancel: this.onWindowPointerUp.bind(this), |
| | | keydown: this.onWindowKeyDown.bind(this), |
| | | keyup: this.onWindowKeyUp.bind(this), |
| | | beforeunload: this.onBeforeUnload.bind(this), |
| | | resize: this.handleResize.bind(this) |
| | | }; |
| | | window.addEventListener('pointermove', this.boundWindowHandlers.pointermove); |
| | | window.addEventListener('pointerup', this.boundWindowHandlers.pointerup); |
| | | window.addEventListener('pointercancel', this.boundWindowHandlers.pointercancel); |
| | | window.addEventListener('keydown', this.boundWindowHandlers.keydown); |
| | | window.addEventListener('keyup', this.boundWindowHandlers.keyup); |
| | | window.addEventListener('beforeunload', this.boundWindowHandlers.beforeunload); |
| | | window.addEventListener('resize', this.boundWindowHandlers.resize); |
| | | }, |
| | | detachEvents: function () { |
| | | if (this.pixiApp && this.boundCanvasHandlers) { |
| | | this.pixiApp.view.removeEventListener('pointerdown', this.boundCanvasHandlers.pointerdown); |
| | | this.pixiApp.view.removeEventListener('wheel', this.boundCanvasHandlers.wheel); |
| | | } |
| | | if (this.resizeObserver) { |
| | | this.resizeObserver.disconnect(); |
| | | this.resizeObserver = null; |
| | | } |
| | | if (!this.boundWindowHandlers) { |
| | | return; |
| | | } |
| | | window.removeEventListener('pointermove', this.boundWindowHandlers.pointermove); |
| | | window.removeEventListener('pointerup', this.boundWindowHandlers.pointerup); |
| | | window.removeEventListener('pointercancel', this.boundWindowHandlers.pointercancel); |
| | | window.removeEventListener('keydown', this.boundWindowHandlers.keydown); |
| | | window.removeEventListener('keyup', this.boundWindowHandlers.keyup); |
| | | window.removeEventListener('beforeunload', this.boundWindowHandlers.beforeunload); |
| | | window.removeEventListener('resize', this.boundWindowHandlers.resize); |
| | | }, |
| | | handleResize: function () { |
| | | if (!this.pixiApp || !this.$refs.canvasHost) { |
| | | return; |
| | | } |
| | | var host = this.$refs.canvasHost; |
| | | var width = Math.max(host.clientWidth, 320); |
| | | var height = Math.max(host.clientHeight, 320); |
| | | this.pixiApp.renderer.resize(width, height); |
| | | this.markGridSceneDirty(); |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | }, |
| | | loadLevOptions: function () { |
| | | var self = this; |
| | | $.ajax({ |
| | | url: baseUrl + '/basMap/getLevList', |
| | | method: 'GET', |
| | | headers: authHeaders(), |
| | | success: function (res) { |
| | | if (res && res.code === 200 && Array.isArray(res.data)) { |
| | | self.remoteLevOptions = res.data.map(function (item) { |
| | | return toInt(item, 0); |
| | | }).filter(function (item) { |
| | | return item > 0; |
| | | }); |
| | | self.refreshLevOptions(); |
| | | } |
| | | } |
| | | }); |
| | | }, |
| | | refreshLevOptions: function () { |
| | | var set = {}; |
| | | var result = []; |
| | | var pushLev = function (lev) { |
| | | lev = toInt(lev, 0); |
| | | if (lev <= 0 || set[lev]) { |
| | | return; |
| | | } |
| | | set[lev] = true; |
| | | result.push(lev); |
| | | }; |
| | | this.remoteLevOptions.forEach(pushLev); |
| | | Object.keys(this.draftDocs || {}).forEach(pushLev); |
| | | pushLev(this.currentLev); |
| | | pushLev(this.floorPickerLev); |
| | | result.sort(function (a, b) { return a - b; }); |
| | | this.levOptions = result; |
| | | }, |
| | | exportDoc: function (doc) { |
| | | var source = doc || this.doc || {}; |
| | | return { |
| | | lev: toInt(source.lev, 0), |
| | | editorMode: FREE_EDITOR_MODE, |
| | | canvasWidth: roundCoord(toNumber(source.canvasWidth, DEFAULT_CANVAS_WIDTH)), |
| | | canvasHeight: roundCoord(toNumber(source.canvasHeight, DEFAULT_CANVAS_HEIGHT)), |
| | | elements: (source.elements || []).map(function (item, index) { |
| | | return { |
| | | id: item && item.id ? String(item.id) : ('el_' + (index + 1)), |
| | | type: DRAW_TYPES.indexOf(item && item.type) >= 0 ? item.type : 'shelf', |
| | | x: roundCoord(Math.max(0, toNumber(item && item.x, 0))), |
| | | y: roundCoord(Math.max(0, toNumber(item && item.y, 0))), |
| | | width: roundCoord(Math.max(MIN_ELEMENT_SIZE, toNumber(item && item.width, MIN_ELEMENT_SIZE))), |
| | | height: roundCoord(Math.max(MIN_ELEMENT_SIZE, toNumber(item && item.height, MIN_ELEMENT_SIZE))), |
| | | value: normalizeValue(item && item.value) |
| | | }; |
| | | }) |
| | | }; |
| | | }, |
| | | normalizeDoc: function (doc) { |
| | | var normalized = this.exportDoc(doc || {}); |
| | | if (normalized.lev <= 0) { |
| | | normalized.lev = toInt(this.currentLev, 1) || 1; |
| | | } |
| | | return normalized; |
| | | }, |
| | | snapshotDoc: function (doc) { |
| | | return JSON.stringify(this.exportDoc(doc)); |
| | | }, |
| | | syncDirty: function () { |
| | | var currentSnapshot = this.snapshotDoc(this.doc); |
| | | this.isDirty = currentSnapshot !== this.savedSnapshot; |
| | | }, |
| | | setDraftDocEntry: function (lev, doc, savedSnapshot) { |
| | | lev = toInt(lev, 0); |
| | | if (lev <= 0 || !doc) { |
| | | return; |
| | | } |
| | | var entry = { |
| | | doc: this.exportDoc(doc), |
| | | savedSnapshot: savedSnapshot != null ? savedSnapshot : '' |
| | | }; |
| | | if (this.$set) { |
| | | this.$set(this.draftDocs, lev, entry); |
| | | } else { |
| | | this.draftDocs[lev] = entry; |
| | | } |
| | | }, |
| | | removeDraftDocEntry: function (lev) { |
| | | lev = toInt(lev, 0); |
| | | if (lev <= 0) { |
| | | return; |
| | | } |
| | | if (this.$delete) { |
| | | this.$delete(this.draftDocs, lev); |
| | | } else { |
| | | delete this.draftDocs[lev]; |
| | | } |
| | | }, |
| | | cacheCurrentDraft: function () { |
| | | if (!this.doc || !this.doc.lev) { |
| | | return; |
| | | } |
| | | this.setDraftDocEntry(this.doc.lev, this.doc, this.savedSnapshot); |
| | | this.refreshLevOptions(); |
| | | }, |
| | | clearCurrentDraftIfSaved: function () { |
| | | if (!this.doc || !this.doc.lev) { |
| | | return; |
| | | } |
| | | this.setDraftDocEntry(this.doc.lev, this.doc, this.savedSnapshot); |
| | | }, |
| | | clearFloorTransientState: function () { |
| | | this.clearDeferredStaticCommit(); |
| | | this.interactionState = null; |
| | | this.currentPointerId = null; |
| | | this.hoverElementId = ''; |
| | | this.pointerStatus = '--'; |
| | | this.lastPointerStatusUpdateTs = 0; |
| | | this.selectedIds = []; |
| | | this.isPanning = false; |
| | | this.isZooming = false; |
| | | this.pendingViewportRefresh = false; |
| | | if (this.zoomRefreshTimer) { |
| | | window.clearTimeout(this.zoomRefreshTimer); |
| | | this.zoomRefreshTimer = null; |
| | | } |
| | | if (this.panRefreshTimer) { |
| | | window.clearTimeout(this.panRefreshTimer); |
| | | this.panRefreshTimer = null; |
| | | } |
| | | }, |
| | | resetRenderLayers: function () { |
| | | if (this.gridLayer) { |
| | | this.gridLayer.clear(); |
| | | } |
| | | if (this.trackLayer) { |
| | | this.trackLayer.clear(); |
| | | } |
| | | if (this.nodeLayer) { |
| | | this.nodeLayer.clear(); |
| | | } |
| | | if (this.eraseLayer) { |
| | | this.eraseLayer.clear(); |
| | | } |
| | | if (this.patchObjectLayer) { |
| | | this.patchObjectLayer.clear(); |
| | | } |
| | | if (this.activeLayer) { |
| | | this.activeLayer.clear(); |
| | | } |
| | | if (this.selectionLayer) { |
| | | this.selectionLayer.clear(); |
| | | } |
| | | if (this.guideLayer) { |
| | | this.guideLayer.clear(); |
| | | } |
| | | if (this.hoverLayer) { |
| | | this.hoverLayer.clear(); |
| | | } |
| | | if (this.guideText) { |
| | | this.guideText.visible = false; |
| | | this.guideText.text = ''; |
| | | } |
| | | if (this.labelLayer) { |
| | | this.labelLayer.visible = false; |
| | | } |
| | | for (var i = 0; i < this.labelPool.length; i++) { |
| | | this.labelPool[i].visible = false; |
| | | this.labelPool[i].text = ''; |
| | | } |
| | | this.hideUnusedStaticSprites(this.staticTrackSpritePool || [], 0); |
| | | this.hideUnusedStaticSprites(this.staticNodeSpritePool || [], 0); |
| | | if (this.staticTrackSpriteLayer) { |
| | | this.staticTrackSpriteLayer.removeChildren(); |
| | | this.staticTrackSpritePool = []; |
| | | } |
| | | if (this.staticNodeSpriteLayer) { |
| | | this.staticNodeSpriteLayer.removeChildren(); |
| | | this.staticNodeSpritePool = []; |
| | | } |
| | | if (this.staticTrackSpriteLayer) { |
| | | this.staticTrackSpriteLayer.visible = false; |
| | | } |
| | | if (this.staticNodeSpriteLayer) { |
| | | this.staticNodeSpriteLayer.visible = false; |
| | | } |
| | | }, |
| | | hasDirtyDraft: function (lev) { |
| | | lev = toInt(lev, 0); |
| | | if (lev <= 0) { |
| | | return false; |
| | | } |
| | | var entry = this.draftDocs[lev]; |
| | | if (!entry || !entry.doc) { |
| | | return false; |
| | | } |
| | | var snapshot = this.snapshotDoc(entry.doc); |
| | | return snapshot !== (entry.savedSnapshot || ''); |
| | | }, |
| | | markStaticSceneDirty: function () { |
| | | this.staticSceneDirty = true; |
| | | }, |
| | | markGridSceneDirty: function () { |
| | | this.gridSceneDirty = true; |
| | | }, |
| | | clearRenderCaches: function () { |
| | | this.gridRenderRect = null; |
| | | this.gridRenderKey = ''; |
| | | this.staticRenderRect = null; |
| | | this.staticRenderKey = ''; |
| | | this.staticExcludedKey = ''; |
| | | }, |
| | | scheduleZoomRefresh: function () { |
| | | if (this.zoomRefreshTimer) { |
| | | window.clearTimeout(this.zoomRefreshTimer); |
| | | } |
| | | this.isZooming = true; |
| | | this.zoomRefreshTimer = window.setTimeout(function () { |
| | | this.zoomRefreshTimer = null; |
| | | this.isZooming = false; |
| | | if (this.isPanning || (this.interactionState && this.interactionState.type === 'pan')) { |
| | | this.pendingViewportRefresh = true; |
| | | return; |
| | | } |
| | | this.markGridSceneDirty(); |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | }.bind(this), ZOOM_REFRESH_DELAY); |
| | | }, |
| | | cancelPanRefresh: function () { |
| | | if (this.panRefreshTimer) { |
| | | window.clearTimeout(this.panRefreshTimer); |
| | | this.panRefreshTimer = null; |
| | | } |
| | | }, |
| | | schedulePanRefresh: function () { |
| | | this.cancelPanRefresh(); |
| | | this.isPanning = true; |
| | | this.panRefreshTimer = window.setTimeout(function () { |
| | | this.panRefreshTimer = null; |
| | | this.isPanning = false; |
| | | if (this.pendingViewportRefresh) { |
| | | this.pendingViewportRefresh = false; |
| | | this.markGridSceneDirty(); |
| | | this.markStaticSceneDirty(); |
| | | } |
| | | this.scheduleRender(); |
| | | }.bind(this), PAN_LABEL_REFRESH_DELAY); |
| | | }, |
| | | rebuildLabelCapability: function () { |
| | | var maxWidth = 0; |
| | | var maxHeight = 0; |
| | | var elements = this.doc && this.doc.elements ? this.doc.elements : []; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | if (element.width > maxWidth) { |
| | | maxWidth = element.width; |
| | | } |
| | | if (element.height > maxHeight) { |
| | | maxHeight = element.height; |
| | | } |
| | | } |
| | | this.labelCapability = { |
| | | maxWidth: maxWidth, |
| | | maxHeight: maxHeight |
| | | }; |
| | | this.labelCapabilityDirty = false; |
| | | }, |
| | | ensureLabelCapability: function () { |
| | | if (this.labelCapabilityDirty) { |
| | | this.rebuildLabelCapability(); |
| | | } |
| | | return this.labelCapability; |
| | | }, |
| | | markSpatialIndexDirty: function () { |
| | | this.spatialIndexDirty = true; |
| | | }, |
| | | rebuildSpatialIndex: function () { |
| | | var buckets = {}; |
| | | var elements = this.doc && this.doc.elements ? this.doc.elements : []; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | var minX = Math.floor(element.x / SPATIAL_BUCKET_SIZE); |
| | | var maxX = Math.floor((element.x + element.width) / SPATIAL_BUCKET_SIZE); |
| | | var minY = Math.floor(element.y / SPATIAL_BUCKET_SIZE); |
| | | var maxY = Math.floor((element.y + element.height) / SPATIAL_BUCKET_SIZE); |
| | | for (var bx = minX; bx <= maxX; bx++) { |
| | | for (var by = minY; by <= maxY; by++) { |
| | | var key = bucketKey(bx, by); |
| | | if (!buckets[key]) { |
| | | buckets[key] = []; |
| | | } |
| | | buckets[key].push(element); |
| | | } |
| | | } |
| | | } |
| | | this.spatialBuckets = buckets; |
| | | this.spatialIndexDirty = false; |
| | | }, |
| | | ensureSpatialIndex: function () { |
| | | if (this.spatialIndexDirty || !this.spatialBuckets) { |
| | | this.rebuildSpatialIndex(); |
| | | } |
| | | }, |
| | | querySpatialCandidates: function (rect, padding, excludeIds) { |
| | | if (!this.doc || !rect) { |
| | | return []; |
| | | } |
| | | this.ensureSpatialIndex(); |
| | | var excludeMap = {}; |
| | | excludeIds = excludeIds || []; |
| | | for (var i = 0; i < excludeIds.length; i++) { |
| | | excludeMap[excludeIds[i]] = true; |
| | | } |
| | | var seen = {}; |
| | | var result = []; |
| | | var pad = Math.max(0, padding || 0); |
| | | var minX = Math.floor((rect.x - pad) / SPATIAL_BUCKET_SIZE); |
| | | var maxX = Math.floor((rect.x + rect.width + pad) / SPATIAL_BUCKET_SIZE); |
| | | var minY = Math.floor((rect.y - pad) / SPATIAL_BUCKET_SIZE); |
| | | var maxY = Math.floor((rect.y + rect.height + pad) / SPATIAL_BUCKET_SIZE); |
| | | for (var bx = minX; bx <= maxX; bx++) { |
| | | for (var by = minY; by <= maxY; by++) { |
| | | var key = bucketKey(bx, by); |
| | | var bucket = this.spatialBuckets[key]; |
| | | if (!bucket || !bucket.length) { |
| | | continue; |
| | | } |
| | | for (var j = 0; j < bucket.length; j++) { |
| | | var element = bucket[j]; |
| | | if (!element || seen[element.id] || excludeMap[element.id]) { |
| | | continue; |
| | | } |
| | | seen[element.id] = true; |
| | | result.push(element); |
| | | } |
| | | } |
| | | } |
| | | return result; |
| | | }, |
| | | cancelDeferredStaticRebuild: function () { |
| | | if (this.deferredStaticRebuildTimer) { |
| | | window.clearTimeout(this.deferredStaticRebuildTimer); |
| | | this.deferredStaticRebuildTimer = null; |
| | | } |
| | | }, |
| | | stageDeferredStaticCommit: function (ids, eraseRects) { |
| | | this.pendingStaticCommit = { |
| | | ids: (ids || []).slice(), |
| | | eraseRects: (eraseRects || []).map(function (item) { |
| | | return { |
| | | x: item.x, |
| | | y: item.y, |
| | | width: item.width, |
| | | height: item.height |
| | | }; |
| | | }) |
| | | }; |
| | | }, |
| | | clearDeferredStaticCommit: function () { |
| | | this.cancelDeferredStaticRebuild(); |
| | | this.pendingStaticCommit = null; |
| | | }, |
| | | scheduleDeferredStaticRebuild: function () { |
| | | this.cancelDeferredStaticRebuild(); |
| | | this.deferredStaticRebuildTimer = window.setTimeout(function () { |
| | | this.deferredStaticRebuildTimer = null; |
| | | this.pendingStaticCommit = null; |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | }.bind(this), DEFERRED_STATIC_REBUILD_DELAY); |
| | | }, |
| | | selectionKey: function (ids) { |
| | | return (ids || []).slice().sort().join('|'); |
| | | }, |
| | | setSelectedIds: function (ids, options) { |
| | | options = options || {}; |
| | | var nextIds = (ids || []).filter(Boolean); |
| | | this.selectedIds = nextIds.slice(); |
| | | if (options.refreshInspector !== false) { |
| | | this.refreshInspector(); |
| | | } |
| | | }, |
| | | setCurrentDoc: function (doc, options) { |
| | | options = options || {}; |
| | | var normalized = this.normalizeDoc(doc); |
| | | this.clearFloorTransientState(); |
| | | this.resetRenderLayers(); |
| | | this.clearRenderCaches(); |
| | | this.doc = normalized; |
| | | this.markSpatialIndexDirty(); |
| | | this.labelCapabilityDirty = true; |
| | | this.pendingViewportRefresh = false; |
| | | this.currentLev = normalized.lev; |
| | | this.floorPickerLev = normalized.lev; |
| | | this.switchingFloorLev = null; |
| | | this.loadingFloor = false; |
| | | this.syncFloorQueryParam(normalized.lev); |
| | | this.markGridSceneDirty(); |
| | | this.markStaticSceneDirty(); |
| | | this.undoStack = []; |
| | | this.redoStack = []; |
| | | this.savedSnapshot = options.savedSnapshot != null ? options.savedSnapshot : this.snapshotDoc(normalized); |
| | | this.syncDirty(); |
| | | this.refreshInspector(); |
| | | this.refreshLevOptions(); |
| | | this.$nextTick(function () { |
| | | this.fitContent(); |
| | | this.scheduleRender(); |
| | | }.bind(this)); |
| | | }, |
| | | replaceDocFromSnapshot: function (snapshot) { |
| | | if (!snapshot) { |
| | | return; |
| | | } |
| | | try { |
| | | this.clearFloorTransientState(); |
| | | this.resetRenderLayers(); |
| | | this.clearRenderCaches(); |
| | | this.doc = this.normalizeDoc(JSON.parse(snapshot)); |
| | | this.markSpatialIndexDirty(); |
| | | this.labelCapabilityDirty = true; |
| | | this.pendingViewportRefresh = false; |
| | | } catch (e) { |
| | | this.showMessage('error', '历史记录恢复失败'); |
| | | return; |
| | | } |
| | | this.markGridSceneDirty(); |
| | | this.markStaticSceneDirty(); |
| | | this.floorPickerLev = this.doc.lev; |
| | | this.currentLev = this.doc.lev; |
| | | this.refreshInspector(); |
| | | this.syncDirty(); |
| | | this.cacheCurrentDraft(); |
| | | this.scheduleRender(); |
| | | }, |
| | | pushUndoSnapshot: function (snapshot) { |
| | | if (!snapshot) { |
| | | return; |
| | | } |
| | | if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length - 1] === snapshot) { |
| | | return; |
| | | } |
| | | this.undoStack.push(snapshot); |
| | | if (this.undoStack.length > HISTORY_LIMIT) { |
| | | this.undoStack.shift(); |
| | | } |
| | | }, |
| | | commitMutation: function (beforeSnapshot, options) { |
| | | options = options || {}; |
| | | var afterSnapshot = this.snapshotDoc(this.doc); |
| | | if (beforeSnapshot === afterSnapshot) { |
| | | this.scheduleRender(); |
| | | this.refreshInspector(); |
| | | return false; |
| | | } |
| | | this.pushUndoSnapshot(beforeSnapshot); |
| | | this.redoStack = []; |
| | | this.markSpatialIndexDirty(); |
| | | this.labelCapabilityDirty = true; |
| | | if (options.staticSceneDirty !== false) { |
| | | this.clearDeferredStaticCommit(); |
| | | this.markStaticSceneDirty(); |
| | | } |
| | | this.syncDirty(); |
| | | this.cacheCurrentDraft(); |
| | | this.refreshInspector(); |
| | | this.scheduleRender(); |
| | | return true; |
| | | }, |
| | | runMutation: function (mutator) { |
| | | if (!this.doc) { |
| | | return false; |
| | | } |
| | | var beforeSnapshot = this.snapshotDoc(this.doc); |
| | | mutator(); |
| | | return this.commitMutation(beforeSnapshot); |
| | | }, |
| | | undo: function () { |
| | | if (this.undoStack.length === 0 || !this.doc) { |
| | | return; |
| | | } |
| | | var currentSnapshot = this.snapshotDoc(this.doc); |
| | | var snapshot = this.undoStack.pop(); |
| | | this.redoStack.push(currentSnapshot); |
| | | this.replaceDocFromSnapshot(snapshot); |
| | | }, |
| | | redo: function () { |
| | | if (this.redoStack.length === 0 || !this.doc) { |
| | | return; |
| | | } |
| | | var currentSnapshot = this.snapshotDoc(this.doc); |
| | | var snapshot = this.redoStack.pop(); |
| | | this.pushUndoSnapshot(currentSnapshot); |
| | | this.replaceDocFromSnapshot(snapshot); |
| | | }, |
| | | createLocalBlankDoc: function (lev, width, height, savedSnapshot) { |
| | | var doc = { |
| | | lev: toInt(lev, 1), |
| | | editorMode: FREE_EDITOR_MODE, |
| | | canvasWidth: Math.max(MIN_ELEMENT_SIZE * 4, toNumber(width, DEFAULT_CANVAS_WIDTH)), |
| | | canvasHeight: Math.max(MIN_ELEMENT_SIZE * 4, toNumber(height, DEFAULT_CANVAS_HEIGHT)), |
| | | elements: [] |
| | | }; |
| | | this.setCurrentDoc(doc, { |
| | | savedSnapshot: savedSnapshot != null ? savedSnapshot : '' |
| | | }); |
| | | this.cacheCurrentDraft(); |
| | | this.syncDirty(); |
| | | }, |
| | | openBlankDialog: function () { |
| | | var lev = this.currentLev || 1; |
| | | this.blankForm = { |
| | | lev: String(lev), |
| | | width: String(Math.round(this.doc ? this.doc.canvasWidth : DEFAULT_CANVAS_WIDTH)), |
| | | height: String(Math.round(this.doc ? this.doc.canvasHeight : DEFAULT_CANVAS_HEIGHT)) |
| | | }; |
| | | this.blankDialogVisible = true; |
| | | }, |
| | | createBlankMap: function () { |
| | | var lev = toInt(this.blankForm.lev, 0); |
| | | var width = toNumber(this.blankForm.width, DEFAULT_CANVAS_WIDTH); |
| | | var height = toNumber(this.blankForm.height, DEFAULT_CANVAS_HEIGHT); |
| | | if (lev <= 0) { |
| | | this.showMessage('warning', '楼层不能为空'); |
| | | return; |
| | | } |
| | | if (width <= 0 || height <= 0) { |
| | | this.showMessage('warning', '画布尺寸必须大于 0'); |
| | | return; |
| | | } |
| | | this.blankDialogVisible = false; |
| | | this.createLocalBlankDoc(lev, width, height, ''); |
| | | }, |
| | | buildTransferPayload: function () { |
| | | var doc = this.exportDoc(this.doc); |
| | | return { |
| | | format: MAP_TRANSFER_FORMAT, |
| | | exportedAt: new Date().toISOString(), |
| | | source: { |
| | | lev: doc.lev, |
| | | editorMode: doc.editorMode |
| | | }, |
| | | docs: [doc] |
| | | }; |
| | | }, |
| | | buildTransferFilename: function (docs) { |
| | | var levs = (docs || []).map(function (item) { |
| | | return toInt(item && item.lev, 0); |
| | | }).filter(function (lev) { |
| | | return lev > 0; |
| | | }).sort(function (a, b) { |
| | | return a - b; |
| | | }); |
| | | var scope = levs.length <= 1 |
| | | ? (String(levs[0] || (this.currentLev || 1)) + 'F') |
| | | : ('all-' + levs.length + '-floors'); |
| | | var now = new Date(); |
| | | return [ |
| | | 'bas-map', |
| | | scope, |
| | | now.getFullYear(), |
| | | padNumber(now.getMonth() + 1), |
| | | padNumber(now.getDate()), |
| | | padNumber(now.getHours()), |
| | | padNumber(now.getMinutes()), |
| | | padNumber(now.getSeconds()) |
| | | ].join('-') + '.json'; |
| | | }, |
| | | requestEditorDoc: function (lev) { |
| | | return new Promise(function (resolve, reject) { |
| | | $.ajax({ |
| | | url: baseUrl + '/basMap/editor/' + lev + '/auth', |
| | | method: 'GET', |
| | | headers: authHeaders(), |
| | | success: function (res) { |
| | | if (!res || res.code !== 200 || !res.data) { |
| | | reject(new Error((res && res.msg) ? res.msg : ('加载 ' + lev + 'F 地图失败'))); |
| | | return; |
| | | } |
| | | resolve(res.data); |
| | | }, |
| | | error: function () { |
| | | reject(new Error('加载 ' + lev + 'F 地图失败')); |
| | | } |
| | | }); |
| | | }); |
| | | }, |
| | | collectAllTransferDocs: function () { |
| | | var self = this; |
| | | var levMap = {}; |
| | | (this.remoteLevOptions || []).forEach(function (lev) { |
| | | lev = toInt(lev, 0); |
| | | if (lev > 0) { |
| | | levMap[lev] = true; |
| | | } |
| | | }); |
| | | Object.keys(this.draftDocs || {}).forEach(function (key) { |
| | | var lev = toInt(key, 0); |
| | | if (lev > 0) { |
| | | levMap[lev] = true; |
| | | } |
| | | }); |
| | | if (this.doc && this.doc.lev) { |
| | | levMap[toInt(this.doc.lev, 0)] = true; |
| | | } |
| | | var levs = Object.keys(levMap).map(function (key) { |
| | | return toInt(key, 0); |
| | | }).filter(function (lev) { |
| | | return lev > 0; |
| | | }).sort(function (a, b) { |
| | | return a - b; |
| | | }); |
| | | if (!levs.length) { |
| | | return Promise.resolve([]); |
| | | } |
| | | return Promise.all(levs.map(function (lev) { |
| | | if (self.doc && self.doc.lev === lev) { |
| | | return Promise.resolve(self.exportDoc(self.doc)); |
| | | } |
| | | if (self.draftDocs[lev] && self.draftDocs[lev].doc) { |
| | | return Promise.resolve(self.exportDoc(self.draftDocs[lev].doc)); |
| | | } |
| | | return self.requestEditorDoc(lev).then(function (doc) { |
| | | return self.normalizeDoc(doc); |
| | | }); |
| | | })); |
| | | }, |
| | | exportMapPackage: function () { |
| | | var self = this; |
| | | if (!this.doc && (!this.remoteLevOptions || !this.remoteLevOptions.length)) { |
| | | this.showMessage('warning', '当前没有可导出的地图'); |
| | | return; |
| | | } |
| | | this.collectAllTransferDocs().then(function (docs) { |
| | | if (!docs || !docs.length) { |
| | | self.showMessage('warning', '当前没有可导出的地图'); |
| | | return; |
| | | } |
| | | var payload = { |
| | | format: MAP_TRANSFER_FORMAT, |
| | | exportedAt: new Date().toISOString(), |
| | | source: { |
| | | lev: self.currentLev || (docs[0] && docs[0].lev) || 1, |
| | | editorMode: FREE_EDITOR_MODE |
| | | }, |
| | | docs: docs.map(function (doc) { |
| | | return self.exportDoc(doc); |
| | | }) |
| | | }; |
| | | var blob = new Blob([JSON.stringify(payload, null, 2)], { |
| | | type: 'application/json;charset=utf-8' |
| | | }); |
| | | var href = window.URL.createObjectURL(blob); |
| | | var link = document.createElement('a'); |
| | | link.href = href; |
| | | link.download = self.buildTransferFilename(payload.docs); |
| | | document.body.appendChild(link); |
| | | link.click(); |
| | | document.body.removeChild(link); |
| | | window.setTimeout(function () { |
| | | window.URL.revokeObjectURL(href); |
| | | }, 0); |
| | | self.showMessage('success', '已导出 ' + payload.docs.length + ' 个楼层的地图包'); |
| | | }).catch(function (error) { |
| | | self.showMessage('error', error && error.message ? error.message : '导出地图失败'); |
| | | }); |
| | | }, |
| | | triggerImportMap: function () { |
| | | if (this.$refs.mapImportInput) { |
| | | this.$refs.mapImportInput.value = ''; |
| | | this.$refs.mapImportInput.click(); |
| | | } |
| | | }, |
| | | parseTransferPackage: function (raw) { |
| | | if (!raw) { |
| | | return null; |
| | | } |
| | | if (raw.format === MAP_TRANSFER_FORMAT && Array.isArray(raw.docs) && raw.docs.length) { |
| | | return { |
| | | docs: raw.docs, |
| | | activeLev: toInt(raw.source && raw.source.lev, 0) |
| | | }; |
| | | } |
| | | if ((raw.format === 'bas-map-editor-transfer-v1' || raw.format === MAP_TRANSFER_FORMAT) && raw.doc) { |
| | | return { |
| | | docs: [raw.doc], |
| | | activeLev: toInt(raw.source && raw.source.lev, 0) |
| | | }; |
| | | } |
| | | if (raw.editorMode === FREE_EDITOR_MODE && Array.isArray(raw.elements)) { |
| | | return { |
| | | docs: [raw], |
| | | activeLev: toInt(raw.lev, 0) |
| | | }; |
| | | } |
| | | return null; |
| | | }, |
| | | importMapPackage: function (payload, options) { |
| | | options = options || {}; |
| | | if (!payload || !Array.isArray(payload.docs) || !payload.docs.length) { |
| | | this.showMessage('error', '导入文件格式不正确'); |
| | | return; |
| | | } |
| | | if (this.isDirty && options.skipConfirm !== true) { |
| | | if (!window.confirm('导入地图会替换当前编辑态未保存内容,是否继续?')) { |
| | | return; |
| | | } |
| | | } |
| | | if (this.doc) { |
| | | this.cacheCurrentDraft(); |
| | | } |
| | | var self = this; |
| | | var normalizedDocs = payload.docs.map(function (item) { |
| | | return self.normalizeDoc(item); |
| | | }).sort(function (a, b) { |
| | | return toInt(a.lev, 0) - toInt(b.lev, 0); |
| | | }); |
| | | normalizedDocs.forEach(function (doc) { |
| | | self.setDraftDocEntry(doc.lev, doc, ''); |
| | | }); |
| | | var activeLev = toInt(payload.activeLev, 0); |
| | | var targetDoc = normalizedDocs[0]; |
| | | for (var i = 0; i < normalizedDocs.length; i++) { |
| | | if (normalizedDocs[i].lev === activeLev) { |
| | | targetDoc = normalizedDocs[i]; |
| | | break; |
| | | } |
| | | } |
| | | this.refreshLevOptions(); |
| | | this.floorPickerLev = targetDoc.lev; |
| | | this.setCurrentDoc(targetDoc, { savedSnapshot: '' }); |
| | | if (normalizedDocs.length > 1) { |
| | | this.showMessage('success', '地图包已导入 ' + normalizedDocs.length + ' 个楼层,可点击“保存全部楼层”落库'); |
| | | return; |
| | | } |
| | | this.showMessage('success', '地图包已导入,保存后才会覆盖运行地图'); |
| | | }, |
| | | handleImportMap: function (event) { |
| | | var file = event && event.target && event.target.files ? event.target.files[0] : null; |
| | | if (!file) { |
| | | return; |
| | | } |
| | | var self = this; |
| | | var reader = new FileReader(); |
| | | reader.onload = function (loadEvent) { |
| | | try { |
| | | var text = loadEvent && loadEvent.target ? loadEvent.target.result : ''; |
| | | var raw = JSON.parse(text || '{}'); |
| | | var payload = self.parseTransferPackage(raw); |
| | | self.importMapPackage(payload); |
| | | } catch (e) { |
| | | self.showMessage('error', '地图文件解析失败'); |
| | | } |
| | | }; |
| | | reader.onerror = function () { |
| | | self.showMessage('error', '地图文件读取失败'); |
| | | }; |
| | | reader.readAsText(file, 'utf-8'); |
| | | }, |
| | | triggerImportExcel: function () { |
| | | if (this.$refs.importInput) { |
| | | this.$refs.importInput.value = ''; |
| | | this.$refs.importInput.click(); |
| | | } |
| | | }, |
| | | handleImportExcel: function (event) { |
| | | var self = this; |
| | | var file = event && event.target && event.target.files ? event.target.files[0] : null; |
| | | if (!file) { |
| | | return; |
| | | } |
| | | var formData = new FormData(); |
| | | formData.append('file', file); |
| | | $.ajax({ |
| | | url: baseUrl + '/basMap/editor/importExcel/auth', |
| | | method: 'POST', |
| | | headers: authHeaders(), |
| | | data: formData, |
| | | processData: false, |
| | | contentType: false, |
| | | success: function (res) { |
| | | if (!res || res.code !== 200 || !Array.isArray(res.data) || res.data.length === 0) { |
| | | self.showMessage('error', (res && res.msg) ? res.msg : 'Excel 导入失败'); |
| | | return; |
| | | } |
| | | res.data.forEach(function (item) { |
| | | var doc = self.normalizeDoc(item); |
| | | self.setDraftDocEntry(doc.lev, doc, ''); |
| | | }); |
| | | self.refreshLevOptions(); |
| | | self.floorPickerLev = toInt(res.data[0].lev, 0); |
| | | self.setCurrentDoc(res.data[0], { savedSnapshot: '' }); |
| | | self.showMessage('success', 'Excel 已导入到编辑器,保存后才会覆盖运行地图'); |
| | | }, |
| | | error: function () { |
| | | self.showMessage('error', 'Excel 导入失败'); |
| | | } |
| | | }); |
| | | }, |
| | | handleFloorChange: function (lev) { |
| | | lev = toInt(lev, 0); |
| | | if (lev <= 0) { |
| | | return; |
| | | } |
| | | this.floorPickerLev = lev; |
| | | if (this.doc && this.doc.lev === lev && !this.loadingFloor) { |
| | | this.switchingFloorLev = null; |
| | | return; |
| | | } |
| | | if (this.doc) { |
| | | this.cacheCurrentDraft(); |
| | | } |
| | | this.clearFloorTransientState(); |
| | | this.resetRenderLayers(); |
| | | this.switchingFloorLev = lev; |
| | | this.markGridSceneDirty(); |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | this.fetchFloor(lev); |
| | | }, |
| | | loadCurrentFloor: function () { |
| | | if (!this.currentLev) { |
| | | this.showMessage('warning', '请先选择楼层'); |
| | | return; |
| | | } |
| | | if (this.isDirty && !window.confirm('重新读取会丢弃当前楼层未保存的自由画布编辑,是否继续?')) { |
| | | return; |
| | | } |
| | | this.removeDraftDocEntry(this.currentLev); |
| | | this.refreshLevOptions(); |
| | | this.fetchFloor(this.currentLev); |
| | | }, |
| | | fetchFloor: function (lev) { |
| | | var self = this; |
| | | lev = toInt(lev, 0); |
| | | if (lev <= 0) { |
| | | return; |
| | | } |
| | | var requestSeq = ++this.floorRequestSeq; |
| | | this.activeFloorRequestSeq = requestSeq; |
| | | this.loadingFloor = true; |
| | | this.switchingFloorLev = lev; |
| | | $.ajax({ |
| | | url: baseUrl + '/basMap/editor/' + lev + '/auth', |
| | | method: 'GET', |
| | | headers: authHeaders(), |
| | | success: function (res) { |
| | | if (requestSeq !== self.activeFloorRequestSeq) { |
| | | return; |
| | | } |
| | | self.loadingFloor = false; |
| | | if (!res || res.code !== 200 || !res.data) { |
| | | self.switchingFloorLev = null; |
| | | self.floorPickerLev = self.currentLev; |
| | | self.markGridSceneDirty(); |
| | | self.markStaticSceneDirty(); |
| | | self.scheduleRender(); |
| | | self.showMessage('error', (res && res.msg) ? res.msg : '加载地图失败'); |
| | | return; |
| | | } |
| | | var normalized = self.normalizeDoc(res.data); |
| | | self.setDraftDocEntry(normalized.lev, normalized, self.snapshotDoc(normalized)); |
| | | self.setCurrentDoc(normalized, { |
| | | savedSnapshot: self.snapshotDoc(normalized) |
| | | }); |
| | | }, |
| | | error: function () { |
| | | if (requestSeq !== self.activeFloorRequestSeq) { |
| | | return; |
| | | } |
| | | self.loadingFloor = false; |
| | | self.switchingFloorLev = null; |
| | | self.floorPickerLev = self.currentLev; |
| | | self.markGridSceneDirty(); |
| | | self.markStaticSceneDirty(); |
| | | self.scheduleRender(); |
| | | self.showMessage('error', '加载地图失败'); |
| | | } |
| | | }); |
| | | }, |
| | | validateDocBeforeSave: function (doc) { |
| | | var source = this.normalizeDoc(doc); |
| | | if (!source || !source.lev) { |
| | | return '楼层不能为空'; |
| | | } |
| | | if (toNumber(source.canvasWidth, 0) <= 0 || toNumber(source.canvasHeight, 0) <= 0) { |
| | | return '画布尺寸必须大于 0'; |
| | | } |
| | | var elements = source.elements || []; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | if (element.width <= 0 || element.height <= 0) { |
| | | return '存在尺寸无效的元素'; |
| | | } |
| | | if (element.x < 0 || element.y < 0) { |
| | | return '元素坐标不能小于 0'; |
| | | } |
| | | if (!isRectWithinCanvas(element, source.canvasWidth, source.canvasHeight)) { |
| | | return '存在超出画布边界的元素: ' + element.id; |
| | | } |
| | | if (element.type === 'devp') { |
| | | var value = safeParseJson(element.value); |
| | | if (!value || toInt(value.stationId, 0) <= 0 || toInt(value.deviceNo, 0) <= 0) { |
| | | return '输送线元素必须配置有效的 stationId 和 deviceNo'; |
| | | } |
| | | } |
| | | } |
| | | var overlapId = findDocOverlapId(source); |
| | | if (overlapId) { |
| | | return '存在重叠元素: ' + overlapId; |
| | | } |
| | | return ''; |
| | | }, |
| | | validateBeforeSave: function () { |
| | | return this.validateDocBeforeSave(this.doc); |
| | | }, |
| | | requestSaveDoc: function (doc) { |
| | | return new Promise(function (resolve, reject) { |
| | | $.ajax({ |
| | | url: baseUrl + '/basMap/editor/save/auth', |
| | | method: 'POST', |
| | | headers: $.extend({ |
| | | 'Content-Type': 'application/json;charset=UTF-8' |
| | | }, authHeaders()), |
| | | data: JSON.stringify(doc), |
| | | success: function (res) { |
| | | if (!res || res.code !== 200) { |
| | | reject(new Error((res && res.msg) ? res.msg : '保存失败')); |
| | | return; |
| | | } |
| | | resolve(res); |
| | | }, |
| | | error: function () { |
| | | reject(new Error('保存失败')); |
| | | } |
| | | }); |
| | | }); |
| | | }, |
| | | collectDirtyDocsForSave: function () { |
| | | var result = []; |
| | | var seen = {}; |
| | | if (this.doc && this.doc.lev && this.isDirty) { |
| | | var currentDoc = this.exportDoc(this.doc); |
| | | result.push(currentDoc); |
| | | seen[currentDoc.lev] = true; |
| | | } |
| | | var self = this; |
| | | Object.keys(this.draftDocs || {}).forEach(function (key) { |
| | | var lev = toInt(key, 0); |
| | | if (lev <= 0 || seen[lev]) { |
| | | return; |
| | | } |
| | | var entry = self.draftDocs[lev]; |
| | | if (!entry || !entry.doc) { |
| | | return; |
| | | } |
| | | var snapshot = self.snapshotDoc(entry.doc); |
| | | if (snapshot === (entry.savedSnapshot || '')) { |
| | | return; |
| | | } |
| | | var doc = self.exportDoc(entry.doc); |
| | | result.push(doc); |
| | | seen[doc.lev] = true; |
| | | }); |
| | | result.sort(function (a, b) { |
| | | return toInt(a.lev, 0) - toInt(b.lev, 0); |
| | | }); |
| | | return result; |
| | | }, |
| | | markDocSavedState: function (doc) { |
| | | var normalized = this.normalizeDoc(doc); |
| | | var savedSnapshot = this.snapshotDoc(normalized); |
| | | this.setDraftDocEntry(normalized.lev, normalized, savedSnapshot); |
| | | if (this.doc && this.doc.lev === normalized.lev) { |
| | | this.savedSnapshot = savedSnapshot; |
| | | this.syncDirty(); |
| | | } |
| | | }, |
| | | saveDoc: function () { |
| | | var self = this; |
| | | if (!this.doc) { |
| | | return; |
| | | } |
| | | var error = this.validateBeforeSave(); |
| | | if (error) { |
| | | this.showMessage('warning', error); |
| | | return; |
| | | } |
| | | this.saving = true; |
| | | var payload = this.exportDoc(this.doc); |
| | | this.requestSaveDoc(payload).then(function () { |
| | | self.saving = false; |
| | | self.savedSnapshot = self.snapshotDoc(self.doc); |
| | | self.syncDirty(); |
| | | self.clearCurrentDraftIfSaved(); |
| | | self.refreshLevOptions(); |
| | | self.showMessage('success', '当前楼层已保存并编译到运行地图'); |
| | | }).catch(function (error) { |
| | | self.saving = false; |
| | | self.showMessage('error', error && error.message ? error.message : '保存失败'); |
| | | }); |
| | | }, |
| | | saveAllDocs: function () { |
| | | var self = this; |
| | | if (this.saving || this.savingAll) { |
| | | return; |
| | | } |
| | | var docs = this.collectDirtyDocsForSave(); |
| | | if (!docs.length) { |
| | | this.showMessage('warning', '当前没有需要保存的楼层'); |
| | | return; |
| | | } |
| | | if (docs.length > 1 && !window.confirm('将保存 ' + docs.length + ' 个楼层到运行地图,是否继续?')) { |
| | | return; |
| | | } |
| | | for (var i = 0; i < docs.length; i++) { |
| | | var error = this.validateDocBeforeSave(docs[i]); |
| | | if (error) { |
| | | this.showMessage('warning', docs[i].lev + 'F 保存前校验失败: ' + error); |
| | | return; |
| | | } |
| | | } |
| | | this.savingAll = true; |
| | | var index = 0; |
| | | var total = docs.length; |
| | | var next = function () { |
| | | if (index >= total) { |
| | | self.savingAll = false; |
| | | self.refreshLevOptions(); |
| | | self.showMessage('success', '已保存 ' + total + ' 个楼层到运行地图'); |
| | | return; |
| | | } |
| | | var doc = docs[index++]; |
| | | self.requestSaveDoc(doc).then(function () { |
| | | self.markDocSavedState(doc); |
| | | next(); |
| | | }).catch(function (error) { |
| | | self.savingAll = false; |
| | | self.showMessage('error', doc.lev + 'F 保存失败: ' + (error && error.message ? error.message : '保存失败')); |
| | | }); |
| | | }; |
| | | next(); |
| | | }, |
| | | setTool: function (tool) { |
| | | this.activeTool = tool; |
| | | this.updateCursor(); |
| | | }, |
| | | findElementById: function (id) { |
| | | if (!this.doc || !id) { |
| | | return null; |
| | | } |
| | | var elements = this.doc.elements || []; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | if (elements[i].id === id) { |
| | | return elements[i]; |
| | | } |
| | | } |
| | | return null; |
| | | }, |
| | | getSelectedElements: function () { |
| | | var self = this; |
| | | return this.selectedIds.map(function (id) { |
| | | return self.findElementById(id); |
| | | }).filter(Boolean); |
| | | }, |
| | | refreshInspector: function () { |
| | | var element = this.singleSelectedElement; |
| | | if (!this.doc) { |
| | | this.canvasForm = { |
| | | width: String(DEFAULT_CANVAS_WIDTH), |
| | | height: String(DEFAULT_CANVAS_HEIGHT) |
| | | }; |
| | | this.valueEditorText = ''; |
| | | this.resetDevpForm(); |
| | | this.resetDeviceForm(); |
| | | return; |
| | | } |
| | | this.canvasForm = { |
| | | width: String(Math.round(this.doc.canvasWidth)), |
| | | height: String(Math.round(this.doc.canvasHeight)) |
| | | }; |
| | | if (!element) { |
| | | this.geometryForm = { x: '', y: '', width: '', height: '' }; |
| | | this.valueEditorText = ''; |
| | | this.resetDevpForm(); |
| | | this.resetDeviceForm(); |
| | | return; |
| | | } |
| | | this.geometryForm = { |
| | | x: String(this.formatNumber(element.x)), |
| | | y: String(this.formatNumber(element.y)), |
| | | width: String(this.formatNumber(element.width)), |
| | | height: String(this.formatNumber(element.height)) |
| | | }; |
| | | this.valueEditorText = element.value || ''; |
| | | if (element.type === 'devp') { |
| | | this.loadDevpForm(element.value); |
| | | } else { |
| | | this.resetDevpForm(); |
| | | } |
| | | if (isDeviceConfigType(element.type)) { |
| | | this.loadDeviceForm(element.type, element.value); |
| | | } else { |
| | | this.resetDeviceForm(); |
| | | } |
| | | this.ensureShelfFillStartValue(); |
| | | }, |
| | | resetDevpForm: function () { |
| | | this.devpForm = { |
| | | stationId: '', |
| | | deviceNo: '', |
| | | direction: [], |
| | | isBarcodeStation: false, |
| | | barcodeIdx: '', |
| | | backStation: '', |
| | | backStationDeviceNo: '', |
| | | isInStation: false, |
| | | barcodeStation: '', |
| | | barcodeStationDeviceNo: '', |
| | | isOutStation: false, |
| | | runBlockReassign: false, |
| | | isOutOrder: false, |
| | | isLiftTransfer: false |
| | | }; |
| | | }, |
| | | resetDeviceForm: function () { |
| | | this.deviceForm = { |
| | | valueKey: '', |
| | | deviceNo: '' |
| | | }; |
| | | }, |
| | | ensureShelfFillStartValue: function () { |
| | | var element = this.singleSelectedElement; |
| | | if (!element || element.type !== 'shelf') { |
| | | return; |
| | | } |
| | | if (!this.shelfFillForm.startValue || !parseShelfLocationValue(this.shelfFillForm.startValue)) { |
| | | this.shelfFillForm.startValue = normalizeValue(element.value || ''); |
| | | } |
| | | }, |
| | | loadDevpForm: function (value) { |
| | | this.resetDevpForm(); |
| | | var json = safeParseJson(value); |
| | | if (!json) { |
| | | return; |
| | | } |
| | | this.devpForm.stationId = json.stationId != null ? String(json.stationId) : ''; |
| | | this.devpForm.deviceNo = json.deviceNo != null ? String(json.deviceNo) : ''; |
| | | this.devpForm.direction = normalizeDirectionList(json.direction); |
| | | this.devpForm.isBarcodeStation = boolFlag(json.isBarcodeStation); |
| | | this.devpForm.barcodeIdx = json.barcodeIdx != null ? String(json.barcodeIdx) : ''; |
| | | this.devpForm.backStation = json.backStation != null ? String(json.backStation) : ''; |
| | | this.devpForm.backStationDeviceNo = json.backStationDeviceNo != null ? String(json.backStationDeviceNo) : ''; |
| | | this.devpForm.isInStation = boolFlag(json.isInStation); |
| | | this.devpForm.barcodeStation = json.barcodeStation != null ? String(json.barcodeStation) : ''; |
| | | this.devpForm.barcodeStationDeviceNo = json.barcodeStationDeviceNo != null ? String(json.barcodeStationDeviceNo) : ''; |
| | | this.devpForm.isOutStation = boolFlag(json.isOutStation); |
| | | this.devpForm.runBlockReassign = boolFlag(json.runBlockReassign); |
| | | this.devpForm.isOutOrder = boolFlag(json.isOutOrder); |
| | | this.devpForm.isLiftTransfer = boolFlag(json.isLiftTransfer); |
| | | }, |
| | | getDeviceConfigLabel: function (type) { |
| | | var meta = getTypeMeta(type); |
| | | return meta.label + '参数'; |
| | | }, |
| | | getDeviceConfigKeyLabel: function (type, valueKey) { |
| | | if (valueKey === 'crnNo') { |
| | | return 'crnNo'; |
| | | } |
| | | if (valueKey === 'rgvNo') { |
| | | return 'rgvNo'; |
| | | } |
| | | return type === 'rgv' ? 'deviceNo / rgvNo' : 'deviceNo / crnNo'; |
| | | }, |
| | | loadDeviceForm: function (type, value) { |
| | | this.resetDeviceForm(); |
| | | if (!isDeviceConfigType(type)) { |
| | | return; |
| | | } |
| | | var json = safeParseJson(value); |
| | | var valueKey = pickDeviceValueKey(type, json); |
| | | var deviceNo = ''; |
| | | if (json && json[valueKey] != null) { |
| | | deviceNo = String(json[valueKey]); |
| | | } |
| | | this.deviceForm = { |
| | | valueKey: valueKey, |
| | | deviceNo: deviceNo |
| | | }; |
| | | }, |
| | | isDevpDirectionActive: function (directionKey) { |
| | | return this.devpForm.direction.indexOf(directionKey) >= 0; |
| | | }, |
| | | toggleDevpDirection: function (directionKey) { |
| | | if (!directionKey) { |
| | | return; |
| | | } |
| | | var next = this.devpForm.direction.slice(); |
| | | var index = next.indexOf(directionKey); |
| | | if (index >= 0) { |
| | | next.splice(index, 1); |
| | | } else { |
| | | next.push(directionKey); |
| | | } |
| | | this.devpForm.direction = DEVP_DIRECTION_OPTIONS.map(function (item) { |
| | | return item.key; |
| | | }).filter(function (item) { |
| | | return next.indexOf(item) >= 0; |
| | | }); |
| | | }, |
| | | applyCanvasSize: function () { |
| | | var self = this; |
| | | if (!this.doc) { |
| | | return; |
| | | } |
| | | var width = toNumber(this.canvasForm.width, 0); |
| | | var height = toNumber(this.canvasForm.height, 0); |
| | | if (width <= 0 || height <= 0) { |
| | | this.showMessage('warning', '画布尺寸必须大于 0'); |
| | | return; |
| | | } |
| | | var bounds = this.getElementBounds((this.doc.elements || []).map(function (item) { |
| | | return item.id; |
| | | })); |
| | | if (bounds && (width < bounds.x + bounds.width || height < bounds.y + bounds.height)) { |
| | | this.showMessage('warning', '画布不能小于当前元素占用范围'); |
| | | return; |
| | | } |
| | | this.runMutation(function () { |
| | | self.doc.canvasWidth = roundCoord(width); |
| | | self.doc.canvasHeight = roundCoord(height); |
| | | }); |
| | | }, |
| | | applyGeometry: function () { |
| | | var self = this; |
| | | var element = this.singleSelectedElement; |
| | | if (!element) { |
| | | return; |
| | | } |
| | | var next = { |
| | | x: roundCoord(Math.max(0, toNumber(this.geometryForm.x, element.x))), |
| | | y: roundCoord(Math.max(0, toNumber(this.geometryForm.y, element.y))), |
| | | width: roundCoord(Math.max(MIN_ELEMENT_SIZE, toNumber(this.geometryForm.width, element.width))), |
| | | height: roundCoord(Math.max(MIN_ELEMENT_SIZE, toNumber(this.geometryForm.height, element.height))) |
| | | }; |
| | | if (!this.isWithinCanvas(next)) { |
| | | this.showMessage('warning', '几何属性超出当前画布范围'); |
| | | return; |
| | | } |
| | | var preview = deepClone(element); |
| | | preview.x = next.x; |
| | | preview.y = next.y; |
| | | preview.width = next.width; |
| | | preview.height = next.height; |
| | | if (this.hasOverlap(preview, [preview.id])) { |
| | | this.showMessage('warning', '调整后会与其他元素重叠'); |
| | | return; |
| | | } |
| | | this.runMutation(function () { |
| | | element.x = next.x; |
| | | element.y = next.y; |
| | | element.width = next.width; |
| | | element.height = next.height; |
| | | }); |
| | | }, |
| | | applyRawValue: function () { |
| | | var self = this; |
| | | var element = this.singleSelectedElement; |
| | | if (!element || element.type === 'devp') { |
| | | return; |
| | | } |
| | | this.runMutation(function () { |
| | | element.value = normalizeValue(self.valueEditorText); |
| | | }); |
| | | }, |
| | | applyDeviceForm: function () { |
| | | var self = this; |
| | | var element = this.singleSelectedDeviceElement; |
| | | if (!element) { |
| | | return; |
| | | } |
| | | var deviceNo = toInt(this.deviceForm.deviceNo, 0); |
| | | if (deviceNo <= 0) { |
| | | this.showMessage('warning', '设备编号必须大于 0'); |
| | | return; |
| | | } |
| | | var valueKey = this.deviceForm.valueKey || pickDeviceValueKey(element.type, safeParseJson(element.value)); |
| | | this.runMutation(function () { |
| | | var payload = safeParseJson(element.value) || {}; |
| | | delete payload.deviceNo; |
| | | delete payload.crnNo; |
| | | delete payload.rgvNo; |
| | | payload[valueKey] = deviceNo; |
| | | element.value = JSON.stringify(payload); |
| | | self.valueEditorText = element.value; |
| | | }); |
| | | }, |
| | | applyDevpForm: function () { |
| | | var self = this; |
| | | var element = this.singleSelectedElement; |
| | | if (!element || element.type !== 'devp') { |
| | | return; |
| | | } |
| | | var stationId = toInt(this.devpForm.stationId, 0); |
| | | var deviceNo = toInt(this.devpForm.deviceNo, 0); |
| | | if (stationId <= 0 || deviceNo <= 0) { |
| | | this.showMessage('warning', '站号和 PLC 编号必须大于 0'); |
| | | return; |
| | | } |
| | | var payload = { |
| | | stationId: stationId, |
| | | deviceNo: deviceNo |
| | | }; |
| | | var directionList = normalizeDirectionList(this.devpForm.direction); |
| | | if (directionList.length > 0) { |
| | | payload.direction = directionList; |
| | | } |
| | | var barcodeIdx = this.devpForm.barcodeIdx === '' ? 0 : toInt(this.devpForm.barcodeIdx, 0); |
| | | var backStation = this.devpForm.backStation === '' ? 0 : toInt(this.devpForm.backStation, 0); |
| | | var backStationDeviceNo = this.devpForm.backStationDeviceNo === '' ? 0 : toInt(this.devpForm.backStationDeviceNo, 0); |
| | | var barcodeStation = this.devpForm.barcodeStation === '' ? 0 : toInt(this.devpForm.barcodeStation, 0); |
| | | var barcodeStationDeviceNo = this.devpForm.barcodeStationDeviceNo === '' ? 0 : toInt(this.devpForm.barcodeStationDeviceNo, 0); |
| | | if (this.devpForm.isInStation && (barcodeStation <= 0 || barcodeStationDeviceNo <= 0)) { |
| | | this.showMessage('warning', '入站点必须填写条码站和条码站 PLC 编号'); |
| | | return; |
| | | } |
| | | if (this.devpForm.isBarcodeStation && (backStation <= 0 || backStationDeviceNo <= 0 || barcodeIdx <= 0)) { |
| | | this.showMessage('warning', '条码站必须填写条码索引、退回站和退回站 PLC 编号'); |
| | | return; |
| | | } |
| | | if (this.devpForm.isBarcodeStation) { |
| | | payload.isBarcodeStation = 1; |
| | | } |
| | | if (barcodeIdx > 0) { |
| | | payload.barcodeIdx = barcodeIdx; |
| | | } |
| | | if (backStation > 0) { |
| | | payload.backStation = backStation; |
| | | } |
| | | if (backStationDeviceNo > 0) { |
| | | payload.backStationDeviceNo = backStationDeviceNo; |
| | | } |
| | | if (this.devpForm.isInStation) { |
| | | payload.isInStation = 1; |
| | | } |
| | | if (barcodeStation > 0) { |
| | | payload.barcodeStation = barcodeStation; |
| | | } |
| | | if (barcodeStationDeviceNo > 0) { |
| | | payload.barcodeStationDeviceNo = barcodeStationDeviceNo; |
| | | } |
| | | if (this.devpForm.isOutStation) { |
| | | payload.isOutStation = 1; |
| | | } |
| | | if (this.devpForm.runBlockReassign) { |
| | | payload.runBlockReassign = 1; |
| | | } |
| | | if (this.devpForm.isOutOrder) { |
| | | payload.isOutOrder = 1; |
| | | } |
| | | if (this.devpForm.isLiftTransfer) { |
| | | payload.isLiftTransfer = 1; |
| | | } |
| | | this.runMutation(function () { |
| | | element.value = JSON.stringify(payload); |
| | | self.valueEditorText = element.value; |
| | | }); |
| | | }, |
| | | deleteSelection: function () { |
| | | var self = this; |
| | | if (!this.doc || this.selectedIds.length === 0) { |
| | | return; |
| | | } |
| | | var ids = this.selectedIds.slice(); |
| | | this.runMutation(function () { |
| | | self.doc.elements = self.doc.elements.filter(function (item) { |
| | | return ids.indexOf(item.id) === -1; |
| | | }); |
| | | self.selectedIds = []; |
| | | }); |
| | | }, |
| | | copySelection: function () { |
| | | var elements = this.getSelectedElements(); |
| | | if (!elements.length) { |
| | | return; |
| | | } |
| | | this.clipboard = deepClone(elements); |
| | | this.showMessage('success', '已复制 ' + elements.length + ' 个元素'); |
| | | }, |
| | | getElementListBounds: function (elements) { |
| | | if (!elements || !elements.length) { |
| | | return null; |
| | | } |
| | | var minX = elements[0].x; |
| | | var minY = elements[0].y; |
| | | var maxX = elements[0].x + elements[0].width; |
| | | var maxY = elements[0].y + elements[0].height; |
| | | for (var i = 1; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | minX = Math.min(minX, element.x); |
| | | minY = Math.min(minY, element.y); |
| | | maxX = Math.max(maxX, element.x + element.width); |
| | | maxY = Math.max(maxY, element.y + element.height); |
| | | } |
| | | return { |
| | | x: minX, |
| | | y: minY, |
| | | width: maxX - minX, |
| | | height: maxY - minY |
| | | }; |
| | | }, |
| | | getPasteTargetWorld: function () { |
| | | if (!this.doc) { |
| | | return { x: 0, y: 0 }; |
| | | } |
| | | var visible = this.getVisibleCanvasRect ? this.getVisibleCanvasRect() : this.getVisibleWorldRect(); |
| | | var fallback = { |
| | | x: visible.x + visible.width / 2, |
| | | y: visible.y + visible.height / 2 |
| | | }; |
| | | if (!this.lastPointerWorld) { |
| | | return fallback; |
| | | } |
| | | return { |
| | | x: clamp(this.lastPointerWorld.x, 0, this.doc.canvasWidth), |
| | | y: clamp(this.lastPointerWorld.y, 0, this.doc.canvasHeight), |
| | | screenX: this.lastPointerWorld.screenX, |
| | | screenY: this.lastPointerWorld.screenY |
| | | }; |
| | | }, |
| | | pasteClipboard: function () { |
| | | var self = this; |
| | | if (!this.doc || !this.clipboard.length) { |
| | | return; |
| | | } |
| | | var sourceBounds = this.getElementListBounds(this.clipboard); |
| | | if (!sourceBounds) { |
| | | return; |
| | | } |
| | | var target = this.getPasteTargetWorld(); |
| | | var offsetX = target.x - (sourceBounds.x + sourceBounds.width / 2); |
| | | var offsetY = target.y - (sourceBounds.y + sourceBounds.height / 2); |
| | | var minOffsetX = -sourceBounds.x; |
| | | var maxOffsetX = this.doc.canvasWidth - (sourceBounds.x + sourceBounds.width); |
| | | var minOffsetY = -sourceBounds.y; |
| | | var maxOffsetY = this.doc.canvasHeight - (sourceBounds.y + sourceBounds.height); |
| | | offsetX = clamp(offsetX, minOffsetX, maxOffsetX); |
| | | offsetY = clamp(offsetY, minOffsetY, maxOffsetY); |
| | | var copies = deepClone(this.clipboard).map(function (item) { |
| | | item.id = nextId(); |
| | | item.x = roundCoord(item.x + offsetX); |
| | | item.y = roundCoord(item.y + offsetY); |
| | | return item; |
| | | }); |
| | | if (!this.canPlaceElements(copies, [])) { |
| | | this.showMessage('warning', '粘贴后的元素与现有元素重叠或超出画布'); |
| | | return; |
| | | } |
| | | this.runMutation(function () { |
| | | self.doc.elements = self.doc.elements.concat(copies); |
| | | self.selectedIds = copies.map(function (item) { return item.id; }); |
| | | }); |
| | | }, |
| | | canArrayFromElement: function (element) { |
| | | return !!(element && ARRAY_TEMPLATE_TYPES.indexOf(element.type) >= 0); |
| | | }, |
| | | getShelfFillSteps: function () { |
| | | return { |
| | | row: this.shelfFillForm.rowStep === 'asc' ? 1 : -1, |
| | | col: this.shelfFillForm.colStep === 'desc' ? -1 : 1 |
| | | }; |
| | | }, |
| | | applyShelfSequenceToArrayCopies: function (template, copies) { |
| | | if (!template || template.type !== 'shelf' || !copies || !copies.length) { |
| | | return copies; |
| | | } |
| | | var base = parseShelfLocationValue(template.value) || parseShelfLocationValue(this.shelfFillForm.startValue); |
| | | if (!base) { |
| | | return copies; |
| | | } |
| | | var steps = this.getShelfFillSteps(); |
| | | var horizontal = Math.abs(copies[0].x - template.x) >= Math.abs(copies[0].y - template.y); |
| | | var direction = 1; |
| | | if (horizontal) { |
| | | direction = copies[0].x >= template.x ? 1 : -1; |
| | | } else { |
| | | direction = copies[0].y >= template.y ? 1 : -1; |
| | | } |
| | | for (var i = 0; i < copies.length; i++) { |
| | | var offset = i + 1; |
| | | var row = base.row; |
| | | var col = base.col; |
| | | if (horizontal) { |
| | | col = base.col + steps.col * direction * offset; |
| | | } else { |
| | | row = base.row + steps.row * direction * offset; |
| | | } |
| | | copies[i].value = formatShelfLocationValue(row, col); |
| | | } |
| | | return copies; |
| | | }, |
| | | buildShelfGridAssignments: function (elements) { |
| | | if (!elements || !elements.length) { |
| | | return null; |
| | | } |
| | | var clusterAxis = function (list, axis, sizeKey) { |
| | | var sorted = list.map(function (item) { |
| | | return { |
| | | id: item.id, |
| | | center: item[axis] + item[sizeKey] / 2, |
| | | size: item[sizeKey] |
| | | }; |
| | | }).sort(function (a, b) { |
| | | return a.center - b.center; |
| | | }); |
| | | var avgSize = sorted.reduce(function (sum, item) { |
| | | return sum + item.size; |
| | | }, 0) / sorted.length; |
| | | var tolerance = Math.max(6, avgSize * 0.45); |
| | | var groups = []; |
| | | for (var i = 0; i < sorted.length; i++) { |
| | | var current = sorted[i]; |
| | | var last = groups.length ? groups[groups.length - 1] : null; |
| | | if (!last || Math.abs(current.center - last.center) > tolerance) { |
| | | groups.push({ |
| | | center: current.center, |
| | | items: [current] |
| | | }); |
| | | } else { |
| | | last.items.push(current); |
| | | last.center = last.items.reduce(function (sum, item) { |
| | | return sum + item.center; |
| | | }, 0) / last.items.length; |
| | | } |
| | | } |
| | | var indexById = {}; |
| | | for (var groupIndex = 0; groupIndex < groups.length; groupIndex++) { |
| | | for (var itemIndex = 0; itemIndex < groups[groupIndex].items.length; itemIndex++) { |
| | | indexById[groups[groupIndex].items[itemIndex].id] = groupIndex; |
| | | } |
| | | } |
| | | return indexById; |
| | | }; |
| | | return { |
| | | rowById: clusterAxis(elements, 'y', 'height'), |
| | | colById: clusterAxis(elements, 'x', 'width') |
| | | }; |
| | | }, |
| | | applyShelfAutoFill: function () { |
| | | var self = this; |
| | | var shelves = this.selectedShelfElements.slice(); |
| | | if (!shelves.length) { |
| | | this.showMessage('warning', '请先选中至少一个货架'); |
| | | return; |
| | | } |
| | | var start = parseShelfLocationValue(this.shelfFillForm.startValue); |
| | | if (!start) { |
| | | this.showMessage('warning', '起始值格式必须是 排-列,例如 12-1'); |
| | | return; |
| | | } |
| | | var grid = this.buildShelfGridAssignments(shelves); |
| | | if (!grid) { |
| | | return; |
| | | } |
| | | var steps = this.getShelfFillSteps(); |
| | | this.runMutation(function () { |
| | | shelves.forEach(function (item) { |
| | | var rowIndex = grid.rowById[item.id] || 0; |
| | | var colIndex = grid.colById[item.id] || 0; |
| | | item.value = formatShelfLocationValue( |
| | | start.row + rowIndex * steps.row, |
| | | start.col + colIndex * steps.col |
| | | ); |
| | | }); |
| | | if (self.singleSelectedElement && self.singleSelectedElement.type === 'shelf') { |
| | | self.valueEditorText = self.singleSelectedElement.value || ''; |
| | | } |
| | | }); |
| | | }, |
| | | buildArrayCopies: function (template, startWorld, currentWorld) { |
| | | if (!this.doc || !template || !startWorld || !currentWorld || !this.canArrayFromElement(template)) { |
| | | return []; |
| | | } |
| | | var deltaX = currentWorld.x - startWorld.x; |
| | | var deltaY = currentWorld.y - startWorld.y; |
| | | if (Math.abs(deltaX) < COORD_EPSILON && Math.abs(deltaY) < COORD_EPSILON) { |
| | | return []; |
| | | } |
| | | var horizontal = Math.abs(deltaX) >= Math.abs(deltaY); |
| | | var step = horizontal ? template.width : template.height; |
| | | if (step <= COORD_EPSILON) { |
| | | return []; |
| | | } |
| | | var direction = (horizontal ? deltaX : deltaY) >= 0 ? 1 : -1; |
| | | var distance; |
| | | if (horizontal) { |
| | | distance = direction > 0 |
| | | ? currentWorld.x - (template.x + template.width) |
| | | : template.x - currentWorld.x; |
| | | } else { |
| | | distance = direction > 0 |
| | | ? currentWorld.y - (template.y + template.height) |
| | | : template.y - currentWorld.y; |
| | | } |
| | | var count = Math.max(0, Math.floor((distance + step * 0.5) / step)); |
| | | if (count <= 0) { |
| | | return []; |
| | | } |
| | | var copies = []; |
| | | for (var i = 1; i <= count; i++) { |
| | | copies.push({ |
| | | type: template.type, |
| | | x: roundCoord(template.x + (horizontal ? direction * template.width * i : 0)), |
| | | y: roundCoord(template.y + (horizontal ? 0 : direction * template.height * i)), |
| | | width: template.width, |
| | | height: template.height, |
| | | value: template.value |
| | | }); |
| | | } |
| | | return this.applyShelfSequenceToArrayCopies(template, copies); |
| | | }, |
| | | duplicateSelection: function () { |
| | | this.copySelection(); |
| | | this.pasteClipboard(); |
| | | }, |
| | | getElementBounds: function (ids) { |
| | | if (!this.doc) { |
| | | return null; |
| | | } |
| | | var elements = ids && ids.length ? this.getSelectedElements() : (this.doc.elements || []); |
| | | if (ids && ids.length) { |
| | | elements = ids.map(function (id) { |
| | | return this.findElementById(id); |
| | | }, this).filter(Boolean); |
| | | } |
| | | if (!elements.length) { |
| | | return null; |
| | | } |
| | | var minX = elements[0].x; |
| | | var minY = elements[0].y; |
| | | var maxX = elements[0].x + elements[0].width; |
| | | var maxY = elements[0].y + elements[0].height; |
| | | for (var i = 1; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | minX = Math.min(minX, element.x); |
| | | minY = Math.min(minY, element.y); |
| | | maxX = Math.max(maxX, element.x + element.width); |
| | | maxY = Math.max(maxY, element.y + element.height); |
| | | } |
| | | return { |
| | | x: minX, |
| | | y: minY, |
| | | width: maxX - minX, |
| | | height: maxY - minY |
| | | }; |
| | | }, |
| | | fitContent: function () { |
| | | if (!this.doc || !this.pixiApp) { |
| | | return; |
| | | } |
| | | var contentBounds = this.getElementBounds(); |
| | | if (contentBounds && contentBounds.width > 0 && contentBounds.height > 0) { |
| | | this.fitRect(contentBounds, this.pixiApp.renderer.width, this.pixiApp.renderer.height); |
| | | return; |
| | | } |
| | | this.fitCanvas(); |
| | | }, |
| | | fitCanvas: function () { |
| | | if (!this.doc || !this.pixiApp) { |
| | | return; |
| | | } |
| | | var renderer = this.pixiApp.renderer; |
| | | var target = { |
| | | x: 0, |
| | | y: 0, |
| | | width: Math.max(1, this.doc.canvasWidth), |
| | | height: Math.max(1, this.doc.canvasHeight) |
| | | }; |
| | | this.fitRect(target, renderer.width, renderer.height); |
| | | }, |
| | | fitSelection: function () { |
| | | if (!this.selectedIds.length || !this.pixiApp) { |
| | | return; |
| | | } |
| | | var bounds = this.getElementBounds(this.selectedIds); |
| | | if (!bounds) { |
| | | return; |
| | | } |
| | | this.fitRect(bounds, this.pixiApp.renderer.width, this.pixiApp.renderer.height); |
| | | }, |
| | | fitRect: function (rect, viewportWidth, viewportHeight) { |
| | | var padding = 80; |
| | | var scale = Math.min( |
| | | (viewportWidth - padding * 2) / Math.max(rect.width, 1), |
| | | (viewportHeight - padding * 2) / Math.max(rect.height, 1) |
| | | ); |
| | | scale = clamp(scale, 0.06, 4); |
| | | this.camera.scale = scale; |
| | | this.camera.x = Math.round((viewportWidth - rect.width * scale) / 2 - rect.x * scale); |
| | | this.camera.y = Math.round((viewportHeight - rect.height * scale) / 2 - rect.y * scale); |
| | | this.viewZoom = scale; |
| | | this.markGridSceneDirty(); |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | }, |
| | | resetView: function () { |
| | | this.fitCanvas(); |
| | | }, |
| | | getVisibleWorldRect: function () { |
| | | if (!this.pixiApp) { |
| | | return { |
| | | x: 0, |
| | | y: 0, |
| | | width: 0, |
| | | height: 0 |
| | | }; |
| | | } |
| | | return { |
| | | x: (-this.camera.x) / this.camera.scale, |
| | | y: (-this.camera.y) / this.camera.scale, |
| | | width: this.pixiApp.renderer.width / this.camera.scale, |
| | | height: this.pixiApp.renderer.height / this.camera.scale |
| | | }; |
| | | }, |
| | | getVisibleCanvasRect: function () { |
| | | if (!this.doc) { |
| | | return { |
| | | x: 0, |
| | | y: 0, |
| | | width: 0, |
| | | height: 0 |
| | | }; |
| | | } |
| | | var visible = this.getVisibleWorldRect(); |
| | | var left = clamp(visible.x, 0, this.doc.canvasWidth); |
| | | var top = clamp(visible.y, 0, this.doc.canvasHeight); |
| | | var right = clamp(visible.x + visible.width, 0, this.doc.canvasWidth); |
| | | var bottom = clamp(visible.y + visible.height, 0, this.doc.canvasHeight); |
| | | return { |
| | | x: left, |
| | | y: top, |
| | | width: Math.max(0, right - left), |
| | | height: Math.max(0, bottom - top) |
| | | }; |
| | | }, |
| | | getWorldRectWithPadding: function (screenPadding) { |
| | | if (!this.doc) { |
| | | return { |
| | | x: 0, |
| | | y: 0, |
| | | width: 0, |
| | | height: 0 |
| | | }; |
| | | } |
| | | var visible = this.getVisibleWorldRect(); |
| | | var padding = Math.max(screenPadding / this.camera.scale, 24); |
| | | var left = Math.max(0, visible.x - padding); |
| | | var top = Math.max(0, visible.y - padding); |
| | | var right = Math.min(this.doc.canvasWidth, visible.x + visible.width + padding); |
| | | var bottom = Math.min(this.doc.canvasHeight, visible.y + visible.height + padding); |
| | | return { |
| | | x: left, |
| | | y: top, |
| | | width: Math.max(0, right - left), |
| | | height: Math.max(0, bottom - top) |
| | | }; |
| | | }, |
| | | worldRectContains: function (outer, inner) { |
| | | if (!outer || !inner) { |
| | | return false; |
| | | } |
| | | return inner.x >= outer.x - COORD_EPSILON |
| | | && inner.y >= outer.y - COORD_EPSILON |
| | | && inner.x + inner.width <= outer.x + outer.width + COORD_EPSILON |
| | | && inner.y + inner.height <= outer.y + outer.height + COORD_EPSILON; |
| | | }, |
| | | getGridRenderKey: function () { |
| | | var minorStep = this.camera.scale > 1.5 ? 50 : (this.camera.scale > 0.45 ? 100 : 200); |
| | | return minorStep + '|' + (Math.round(this.camera.scale * 8) / 8); |
| | | }, |
| | | getStaticRenderKey: function () { |
| | | return (this.camera.scale >= 0.85 ? 'round' : 'flat') + '|' + (Math.round(this.camera.scale * 8) / 8); |
| | | }, |
| | | scheduleRender: function () { |
| | | if (this.renderQueued) { |
| | | return; |
| | | } |
| | | this.renderQueued = true; |
| | | window.requestAnimationFrame(function () { |
| | | this.renderQueued = false; |
| | | this.renderScene(); |
| | | }.bind(this)); |
| | | }, |
| | | renderScene: function () { |
| | | if (!this.pixiApp || !this.doc) { |
| | | return; |
| | | } |
| | | this.mapRoot.position.set(this.camera.x, this.camera.y); |
| | | this.mapRoot.scale.set(this.camera.scale, this.camera.scale); |
| | | this.viewZoom = this.camera.scale; |
| | | var visible = this.getVisibleCanvasRect(); |
| | | var viewportSettled = !this.isZooming && !this.isPanning && !(this.interactionState && this.interactionState.type === 'pan'); |
| | | var gridKeyChanged = this.gridRenderKey !== this.getGridRenderKey(); |
| | | if (this.gridSceneDirty || !this.gridRenderRect || (viewportSettled && gridKeyChanged) || (viewportSettled && !this.worldRectContains(this.gridRenderRect, visible))) { |
| | | this.renderGrid(this.getWorldRectWithPadding(STATIC_VIEW_PADDING)); |
| | | this.gridSceneDirty = false; |
| | | } |
| | | var excludedKey = this.selectionKey(this.getStaticExcludedIds()); |
| | | var staticKeyChanged = this.staticRenderKey !== this.getStaticRenderKey(); |
| | | if (this.staticSceneDirty || !this.staticRenderRect || (viewportSettled && staticKeyChanged) |
| | | || this.staticExcludedKey !== excludedKey || (viewportSettled && !this.worldRectContains(this.staticRenderRect, visible))) { |
| | | this.renderStaticElements(this.getWorldRectWithPadding(STATIC_VIEW_PADDING), excludedKey); |
| | | this.staticSceneDirty = false; |
| | | } |
| | | this.renderActiveElements(); |
| | | this.renderLabels(); |
| | | this.renderHover(); |
| | | this.renderSelection(); |
| | | this.renderGuide(); |
| | | this.updateCursor(); |
| | | }, |
| | | getStaticExcludedIds: function () { |
| | | if (!this.interactionState) { |
| | | return []; |
| | | } |
| | | if (this.interactionState.type === 'move' && this.selectedIds.length) { |
| | | return this.selectedIds.slice(); |
| | | } |
| | | if (this.interactionState.type === 'resize' && this.interactionState.elementId) { |
| | | return [this.interactionState.elementId]; |
| | | } |
| | | return []; |
| | | }, |
| | | getRenderableElements: function (excludeIds, renderRect) { |
| | | if (!this.doc) { |
| | | return []; |
| | | } |
| | | var rect = renderRect || this.getWorldRectWithPadding(STATIC_VIEW_PADDING); |
| | | var candidates = this.querySpatialCandidates(rect, 0, excludeIds); |
| | | var result = []; |
| | | for (var i = 0; i < candidates.length; i++) { |
| | | if (rectIntersects(rect, candidates[i])) { |
| | | result.push(candidates[i]); |
| | | } |
| | | } |
| | | return result; |
| | | }, |
| | | renderGrid: function (renderRect) { |
| | | if (!this.gridLayer || !this.doc) { |
| | | return; |
| | | } |
| | | var visible = renderRect || this.getVisibleWorldRect(); |
| | | var width = this.doc.canvasWidth; |
| | | var height = this.doc.canvasHeight; |
| | | var minorStep = this.camera.scale > 1.5 ? 50 : (this.camera.scale > 0.45 ? 100 : 200); |
| | | var majorStep = minorStep * 5; |
| | | var lineWidth = 1 / this.camera.scale; |
| | | var xStart = Math.max(0, Math.floor(visible.x / minorStep) * minorStep); |
| | | var yStart = Math.max(0, Math.floor(visible.y / minorStep) * minorStep); |
| | | var xEnd = Math.min(width, visible.x + visible.width); |
| | | var yEnd = Math.min(height, visible.y + visible.height); |
| | | |
| | | this.gridLayer.clear(); |
| | | this.gridLayer.beginFill(0xfafcff, 1); |
| | | this.gridLayer.drawRect(0, 0, width, height); |
| | | this.gridLayer.endFill(); |
| | | |
| | | this.gridLayer.lineStyle(lineWidth, 0xdbe4ee, 1); |
| | | this.gridLayer.drawRect(0, 0, width, height); |
| | | |
| | | for (var x = xStart; x <= xEnd; x += minorStep) { |
| | | var colorX = (x % majorStep === 0) ? 0xc9d7e6 : 0xe4ebf3; |
| | | this.gridLayer.lineStyle(lineWidth, colorX, x % majorStep === 0 ? 0.95 : 0.75); |
| | | this.gridLayer.moveTo(x, 0); |
| | | this.gridLayer.lineTo(x, height); |
| | | } |
| | | for (var y = yStart; y <= yEnd; y += minorStep) { |
| | | var colorY = (y % majorStep === 0) ? 0xc9d7e6 : 0xe4ebf3; |
| | | this.gridLayer.lineStyle(lineWidth, colorY, y % majorStep === 0 ? 0.95 : 0.75); |
| | | this.gridLayer.moveTo(0, y); |
| | | this.gridLayer.lineTo(width, y); |
| | | } |
| | | this.gridRenderRect = { |
| | | x: visible.x, |
| | | y: visible.y, |
| | | width: visible.width, |
| | | height: visible.height |
| | | }; |
| | | this.gridRenderKey = this.getGridRenderKey(); |
| | | }, |
| | | drawGridPatch: function (rects, layer) { |
| | | if (!this.doc || !layer || !rects || !rects.length) { |
| | | return; |
| | | } |
| | | var width = this.doc.canvasWidth; |
| | | var height = this.doc.canvasHeight; |
| | | var minorStep = this.camera.scale > 1.5 ? 50 : (this.camera.scale > 0.45 ? 100 : 200); |
| | | var majorStep = minorStep * 5; |
| | | var lineWidth = 1 / this.camera.scale; |
| | | for (var i = 0; i < rects.length; i++) { |
| | | var rect = rects[i]; |
| | | var left = clamp(rect.x - lineWidth, 0, width); |
| | | var top = clamp(rect.y - lineWidth, 0, height); |
| | | var right = clamp(rect.x + rect.width + lineWidth, 0, width); |
| | | var bottom = clamp(rect.y + rect.height + lineWidth, 0, height); |
| | | if (right <= left || bottom <= top) { |
| | | continue; |
| | | } |
| | | layer.lineStyle(0, 0, 0, 0); |
| | | layer.beginFill(0xfafcff, 1); |
| | | layer.drawRect(left, top, right - left, bottom - top); |
| | | layer.endFill(); |
| | | if (right - left < minorStep || bottom - top < minorStep) { |
| | | continue; |
| | | } |
| | | var xStart = Math.floor(left / minorStep) * minorStep; |
| | | var yStart = Math.floor(top / minorStep) * minorStep; |
| | | for (var x = xStart; x <= right; x += minorStep) { |
| | | if (x < left || x > right) { |
| | | continue; |
| | | } |
| | | var colorX = (x % majorStep === 0) ? 0xc9d7e6 : 0xe4ebf3; |
| | | layer.lineStyle(lineWidth, colorX, x % majorStep === 0 ? 0.95 : 0.75); |
| | | layer.moveTo(x, top); |
| | | layer.lineTo(x, bottom); |
| | | } |
| | | for (var y = yStart; y <= bottom; y += minorStep) { |
| | | if (y < top || y > bottom) { |
| | | continue; |
| | | } |
| | | var colorY = (y % majorStep === 0) ? 0xc9d7e6 : 0xe4ebf3; |
| | | layer.lineStyle(lineWidth, colorY, y % majorStep === 0 ? 0.95 : 0.75); |
| | | layer.moveTo(left, y); |
| | | layer.lineTo(right, y); |
| | | } |
| | | } |
| | | }, |
| | | drawPatchObjects: function (rects, excludeIds) { |
| | | if (!rects || !rects.length || !this.patchObjectLayer) { |
| | | return; |
| | | } |
| | | var seen = {}; |
| | | var elements = []; |
| | | for (var i = 0; i < rects.length; i++) { |
| | | var candidates = this.querySpatialCandidates(rects[i], 0, excludeIds); |
| | | for (var j = 0; j < candidates.length; j++) { |
| | | var item = candidates[j]; |
| | | if (!seen[item.id] && rectIntersects(rects[i], item)) { |
| | | seen[item.id] = true; |
| | | elements.push(item); |
| | | } |
| | | } |
| | | } |
| | | if (!elements.length) { |
| | | return; |
| | | } |
| | | this.drawElementsToLayers(elements, this.patchObjectLayer, this.patchObjectLayer); |
| | | }, |
| | | drawElementsToLayers: function (elements, trackLayer, nodeLayer) { |
| | | var lineWidth = 1 / this.camera.scale; |
| | | var useRounded = this.camera.scale >= 0.85; |
| | | var radius = Math.max(6 / this.camera.scale, 2); |
| | | var buckets = {}; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | var bucketKey = (element.type === 'shelf' ? 'node' : 'track') + ':' + element.type; |
| | | if (!buckets[bucketKey]) { |
| | | buckets[bucketKey] = []; |
| | | } |
| | | buckets[bucketKey].push(element); |
| | | } |
| | | for (var bucketKey in buckets) { |
| | | if (!buckets.hasOwnProperty(bucketKey)) { |
| | | continue; |
| | | } |
| | | var parts = bucketKey.split(':'); |
| | | var type = parts[1]; |
| | | var meta = getTypeMeta(type); |
| | | var layer = parts[0] === 'node' ? nodeLayer : trackLayer; |
| | | layer.lineStyle(lineWidth, meta.border, 1); |
| | | layer.beginFill(meta.fill, 0.92); |
| | | var bucket = buckets[bucketKey]; |
| | | for (var j = 0; j < bucket.length; j++) { |
| | | var item = bucket[j]; |
| | | if (useRounded) { |
| | | layer.drawRoundedRect(item.x, item.y, item.width, item.height, radius); |
| | | } else { |
| | | layer.drawRect(item.x, item.y, item.width, item.height); |
| | | } |
| | | } |
| | | layer.endFill(); |
| | | } |
| | | }, |
| | | ensureStaticSprite: function (poolName, index) { |
| | | var pool = poolName === 'node' ? this.staticNodeSpritePool : this.staticTrackSpritePool; |
| | | var layer = poolName === 'node' ? this.staticNodeSpriteLayer : this.staticTrackSpriteLayer; |
| | | if (pool[index]) { |
| | | return pool[index]; |
| | | } |
| | | var sprite = new PIXI.Sprite(PIXI.Texture.WHITE); |
| | | sprite.position.set(0, 0); |
| | | sprite.anchor.set(0, 0); |
| | | sprite.visible = false; |
| | | sprite.alpha = 0; |
| | | layer.addChild(sprite); |
| | | pool[index] = sprite; |
| | | return sprite; |
| | | }, |
| | | hideUnusedStaticSprites: function (pool, fromIndex) { |
| | | for (var i = fromIndex; i < pool.length; i++) { |
| | | pool[i].visible = false; |
| | | pool[i].alpha = 0; |
| | | pool[i].width = 0; |
| | | pool[i].height = 0; |
| | | pool[i].position.set(-99999, -99999); |
| | | } |
| | | }, |
| | | pruneStaticSpritePool: function (poolName, keepCount, slack) { |
| | | var pool = poolName === 'node' ? this.staticNodeSpritePool : this.staticTrackSpritePool; |
| | | var layer = poolName === 'node' ? this.staticNodeSpriteLayer : this.staticTrackSpriteLayer; |
| | | var target = Math.max(0, keepCount + Math.max(0, slack || 0)); |
| | | if (!pool || !layer || pool.length <= target) { |
| | | return; |
| | | } |
| | | for (var i = pool.length - 1; i >= target; i--) { |
| | | var sprite = pool[i]; |
| | | layer.removeChild(sprite); |
| | | if (sprite && sprite.destroy) { |
| | | sprite.destroy(); |
| | | } |
| | | pool.pop(); |
| | | } |
| | | }, |
| | | drawElementsToSpriteLayers: function (elements) { |
| | | var trackCount = 0; |
| | | var nodeCount = 0; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var item = elements[i]; |
| | | var meta = getTypeMeta(item.type); |
| | | var poolName = item.type === 'shelf' ? 'node' : 'track'; |
| | | var sprite = this.ensureStaticSprite(poolName, poolName === 'node' ? nodeCount : trackCount); |
| | | sprite.visible = true; |
| | | sprite.position.set(item.x, item.y); |
| | | sprite.width = item.width; |
| | | sprite.height = item.height; |
| | | sprite.tint = meta.fill; |
| | | sprite.alpha = 0.92; |
| | | if (poolName === 'node') { |
| | | nodeCount += 1; |
| | | } else { |
| | | trackCount += 1; |
| | | } |
| | | } |
| | | this.hideUnusedStaticSprites(this.staticTrackSpritePool, trackCount); |
| | | this.hideUnusedStaticSprites(this.staticNodeSpritePool, nodeCount); |
| | | if (this.camera.scale < DENSE_SIMPLIFY_SCALE_THRESHOLD) { |
| | | this.pruneStaticSpritePool('track', trackCount, STATIC_SPRITE_POOL_SLACK); |
| | | this.pruneStaticSpritePool('node', nodeCount, STATIC_SPRITE_POOL_SLACK); |
| | | } |
| | | }, |
| | | simplifyRenderableElements: function (elements) { |
| | | if (!elements || elements.length < 2) { |
| | | return elements || []; |
| | | } |
| | | var sorted = elements.slice().sort(function (a, b) { |
| | | if (a.type !== b.type) { |
| | | return a.type < b.type ? -1 : 1; |
| | | } |
| | | if (Math.abs(a.y - b.y) > COORD_EPSILON) { |
| | | return a.y - b.y; |
| | | } |
| | | if (Math.abs(a.height - b.height) > COORD_EPSILON) { |
| | | return a.height - b.height; |
| | | } |
| | | return a.x - b.x; |
| | | }); |
| | | var result = []; |
| | | var current = null; |
| | | for (var i = 0; i < sorted.length; i++) { |
| | | var item = sorted[i]; |
| | | if (!current) { |
| | | current = { |
| | | type: item.type, |
| | | x: item.x, |
| | | y: item.y, |
| | | width: item.width, |
| | | height: item.height |
| | | }; |
| | | continue; |
| | | } |
| | | var currentRight = current.x + current.width; |
| | | var itemRight = item.x + item.width; |
| | | var sameBand = current.type === item.type |
| | | && Math.abs(current.y - item.y) <= 0.5 |
| | | && Math.abs(current.height - item.height) <= 0.5; |
| | | var joinable = item.x <= currentRight + 0.5; |
| | | if (sameBand && joinable) { |
| | | current.width = roundCoord(Math.max(currentRight, itemRight) - current.x); |
| | | } else { |
| | | result.push(current); |
| | | current = { |
| | | type: item.type, |
| | | x: item.x, |
| | | y: item.y, |
| | | width: item.width, |
| | | height: item.height |
| | | }; |
| | | } |
| | | } |
| | | if (current) { |
| | | result.push(current); |
| | | } |
| | | return result; |
| | | }, |
| | | renderStaticElements: function (renderRect, excludedKey) { |
| | | if (!this.doc) { |
| | | return; |
| | | } |
| | | this.trackLayer.clear(); |
| | | this.nodeLayer.clear(); |
| | | this.eraseLayer.clear(); |
| | | this.patchObjectLayer.clear(); |
| | | var renderableElements = this.getRenderableElements(this.getStaticExcludedIds(), renderRect); |
| | | var useSpriteMode = this.camera.scale < STATIC_SPRITE_SCALE_THRESHOLD; |
| | | var shouldSimplify = this.camera.scale < STATIC_SIMPLIFY_SCALE_THRESHOLD |
| | | || (this.camera.scale < DENSE_SIMPLIFY_SCALE_THRESHOLD && renderableElements.length > DENSE_SIMPLIFY_ELEMENT_THRESHOLD); |
| | | this.staticTrackSpriteLayer.visible = useSpriteMode; |
| | | this.staticNodeSpriteLayer.visible = useSpriteMode; |
| | | this.trackLayer.visible = !useSpriteMode; |
| | | this.nodeLayer.visible = !useSpriteMode; |
| | | if (useSpriteMode) { |
| | | if (shouldSimplify) { |
| | | renderableElements = this.simplifyRenderableElements(renderableElements); |
| | | } |
| | | this.drawElementsToSpriteLayers(renderableElements); |
| | | } else { |
| | | this.hideUnusedStaticSprites(this.staticTrackSpritePool, 0); |
| | | this.hideUnusedStaticSprites(this.staticNodeSpritePool, 0); |
| | | this.drawElementsToLayers(renderableElements, this.trackLayer, this.nodeLayer); |
| | | } |
| | | var rect = renderRect || this.getWorldRectWithPadding(STATIC_VIEW_PADDING); |
| | | this.staticRenderRect = { |
| | | x: rect.x, |
| | | y: rect.y, |
| | | width: rect.width, |
| | | height: rect.height |
| | | }; |
| | | this.staticRenderKey = this.getStaticRenderKey(); |
| | | this.staticExcludedKey = excludedKey != null ? excludedKey : this.selectionKey(this.getStaticExcludedIds()); |
| | | this.pendingStaticCommit = null; |
| | | }, |
| | | renderActiveElements: function () { |
| | | this.activeLayer.clear(); |
| | | this.eraseLayer.clear(); |
| | | this.patchObjectLayer.clear(); |
| | | var activeIds = this.getStaticExcludedIds(); |
| | | if (!activeIds.length) { |
| | | return; |
| | | } |
| | | var activeElements = []; |
| | | for (var idx = 0; idx < activeIds.length; idx++) { |
| | | var element = this.findElementById(activeIds[idx]); |
| | | if (element) { |
| | | activeElements.push(element); |
| | | } |
| | | } |
| | | if (!activeElements.length) { |
| | | return; |
| | | } |
| | | this.drawElementsToLayers(activeElements, this.activeLayer, this.activeLayer); |
| | | }, |
| | | getLabelText: function (element) { |
| | | var meta = getTypeMeta(element.type); |
| | | var value = safeParseJson(element.value); |
| | | if (element.type === 'devp' && value) { |
| | | var station = value.stationId != null ? String(value.stationId) : ''; |
| | | var arrows = formatDirectionArrows(value.direction); |
| | | if (station && arrows) { |
| | | return element.height > element.width * 1.15 ? (station + '\n' + arrows) : (station + ' ' + arrows); |
| | | } |
| | | if (station) { |
| | | return station; |
| | | } |
| | | if (arrows) { |
| | | return arrows; |
| | | } |
| | | return meta.shortLabel; |
| | | } |
| | | if ((element.type === 'crn' || element.type === 'dualCrn' || element.type === 'rgv') && value) { |
| | | if (value.deviceNo != null) { |
| | | return meta.shortLabel + ' ' + value.deviceNo; |
| | | } |
| | | if (value.crnNo != null) { |
| | | return meta.shortLabel + ' ' + value.crnNo; |
| | | } |
| | | if (value.rgvNo != null) { |
| | | return meta.shortLabel + ' ' + value.rgvNo; |
| | | } |
| | | } |
| | | if (element.value && element.value.length <= 18 && element.value.indexOf('{') !== 0) { |
| | | return element.value; |
| | | } |
| | | return meta.shortLabel; |
| | | }, |
| | | ensureLabelSprite: function (index) { |
| | | if (this.labelPool[index]) { |
| | | return this.labelPool[index]; |
| | | } |
| | | var label = new PIXI.Text('', { |
| | | fontFamily: 'Avenir Next, PingFang SC, Microsoft YaHei, sans-serif', |
| | | fontSize: 12, |
| | | fontWeight: '600', |
| | | fill: 0x223448, |
| | | align: 'center' |
| | | }); |
| | | label.anchor.set(0.5); |
| | | this.labelLayer.addChild(label); |
| | | this.labelPool[index] = label; |
| | | return label; |
| | | }, |
| | | getLabelRenderBudget: function () { |
| | | if (!this.pixiApp || !this.pixiApp.renderer) { |
| | | return MIN_LABEL_COUNT; |
| | | } |
| | | var renderer = this.pixiApp.renderer; |
| | | var viewportArea = renderer.width * renderer.height; |
| | | return clamp(Math.round(viewportArea / 12000), MIN_LABEL_COUNT, MAX_LABEL_COUNT); |
| | | }, |
| | | getLabelMinScreenWidth: function (text) { |
| | | var lines = String(text || '').split('\n'); |
| | | var length = 0; |
| | | for (var i = 0; i < lines.length; i++) { |
| | | length = Math.max(length, String(lines[i] || '').trim().length); |
| | | } |
| | | if (length <= 4) { |
| | | return 26; |
| | | } |
| | | if (length <= 8) { |
| | | return 40; |
| | | } |
| | | if (length <= 12) { |
| | | return 52; |
| | | } |
| | | return 64; |
| | | }, |
| | | getLabelMinScreenHeight: function (text) { |
| | | var lines = String(text || '').split('\n'); |
| | | var length = 0; |
| | | for (var i = 0; i < lines.length; i++) { |
| | | length = Math.max(length, String(lines[i] || '').trim().length); |
| | | } |
| | | var lineHeight = length <= 4 ? 14 : 18; |
| | | return lineHeight * Math.max(lines.length, 1); |
| | | }, |
| | | renderLabels: function () { |
| | | if (!this.doc) { |
| | | return; |
| | | } |
| | | if (!SHOW_CANVAS_ELEMENT_LABELS) { |
| | | this.labelLayer.visible = false; |
| | | for (var hiddenIdx = 0; hiddenIdx < this.labelPool.length; hiddenIdx++) { |
| | | this.labelPool[hiddenIdx].visible = false; |
| | | } |
| | | return; |
| | | } |
| | | var capability = this.ensureLabelCapability(); |
| | | if (capability.maxWidth * this.camera.scale < ABS_MIN_LABEL_SCREEN_WIDTH |
| | | || capability.maxHeight * this.camera.scale < ABS_MIN_LABEL_SCREEN_HEIGHT) { |
| | | this.labelLayer.visible = false; |
| | | return; |
| | | } |
| | | if (this.isZooming || this.isPanning || this.camera.scale < MIN_LABEL_SCALE |
| | | || (this.interactionState && (this.interactionState.type === 'move' || this.interactionState.type === 'resize' || this.interactionState.type === 'pan'))) { |
| | | this.labelLayer.visible = false; |
| | | return; |
| | | } |
| | | this.labelLayer.visible = true; |
| | | var visible = this.getVisibleWorldRect(); |
| | | var elements = this.querySpatialCandidates(visible, 0, []); |
| | | if (elements.length > DENSE_LABEL_HIDE_ELEMENT_THRESHOLD && this.camera.scale < DENSE_LABEL_HIDE_SCALE_THRESHOLD) { |
| | | this.labelLayer.visible = false; |
| | | return; |
| | | } |
| | | var hasRoomForAnyLabel = false; |
| | | for (var roomIdx = 0; roomIdx < elements.length; roomIdx++) { |
| | | var candidate = elements[roomIdx]; |
| | | if (candidate.width * this.camera.scale >= ABS_MIN_LABEL_SCREEN_WIDTH |
| | | && candidate.height * this.camera.scale >= ABS_MIN_LABEL_SCREEN_HEIGHT) { |
| | | hasRoomForAnyLabel = true; |
| | | break; |
| | | } |
| | | } |
| | | if (!hasRoomForAnyLabel) { |
| | | this.labelLayer.visible = false; |
| | | return; |
| | | } |
| | | var visibleElements = []; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | var text = this.getLabelText(element); |
| | | if (!text) { |
| | | continue; |
| | | } |
| | | if (!rectIntersects(visible, element)) { |
| | | continue; |
| | | } |
| | | if (element.width * this.camera.scale < this.getLabelMinScreenWidth(text) || element.height * this.camera.scale < this.getLabelMinScreenHeight(text)) { |
| | | continue; |
| | | } |
| | | visibleElements.push({ |
| | | element: element, |
| | | text: text |
| | | }); |
| | | } |
| | | visibleElements.sort(function (a, b) { |
| | | return (b.element.width * b.element.height) - (a.element.width * a.element.height); |
| | | }); |
| | | var labelBudget = this.getLabelRenderBudget(); |
| | | if (visibleElements.length > labelBudget) { |
| | | visibleElements = visibleElements.slice(0, labelBudget); |
| | | } |
| | | for (var j = 0; j < visibleElements.length; j++) { |
| | | var item = visibleElements[j].element; |
| | | var label = this.ensureLabelSprite(j); |
| | | label.visible = true; |
| | | label.text = visibleElements[j].text; |
| | | label.position.set(item.x + item.width / 2, item.y + item.height / 2); |
| | | label.scale.set(1 / this.camera.scale, 1 / this.camera.scale); |
| | | label.alpha = this.selectedIds.indexOf(item.id) >= 0 ? 1 : 0.88; |
| | | } |
| | | for (var k = visibleElements.length; k < this.labelPool.length; k++) { |
| | | this.labelPool[k].visible = false; |
| | | } |
| | | }, |
| | | renderHover: function () { |
| | | this.hoverLayer.clear(); |
| | | if (this.interactionState || !this.hoverElementId || this.selectedIds.indexOf(this.hoverElementId) >= 0) { |
| | | return; |
| | | } |
| | | var element = this.findElementById(this.hoverElementId); |
| | | if (!element) { |
| | | return; |
| | | } |
| | | var lineWidth = 2 / this.camera.scale; |
| | | this.hoverLayer.lineStyle(lineWidth, 0x2f79d6, 0.95); |
| | | this.hoverLayer.drawRoundedRect(element.x, element.y, element.width, element.height, Math.max(6 / this.camera.scale, 2)); |
| | | }, |
| | | renderSelection: function () { |
| | | this.selectionLayer.clear(); |
| | | if (!this.selectedIds.length || (this.interactionState && (this.interactionState.type === 'move' || this.interactionState.type === 'resize'))) { |
| | | return; |
| | | } |
| | | var elements = this.getSelectedElements(); |
| | | var lineWidth = 2 / this.camera.scale; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | this.selectionLayer.lineStyle(lineWidth, 0x2568b8, 1); |
| | | this.selectionLayer.beginFill(0x2f79d6, 0.07); |
| | | this.selectionLayer.drawRoundedRect(element.x, element.y, element.width, element.height, Math.max(6 / this.camera.scale, 2)); |
| | | this.selectionLayer.endFill(); |
| | | } |
| | | if (elements.length !== 1) { |
| | | return; |
| | | } |
| | | var handleSize = HANDLE_SCREEN_SIZE / this.camera.scale; |
| | | var handlePositions = this.getHandlePositions(elements[0]); |
| | | this.selectionLayer.lineStyle(1 / this.camera.scale, 0x1d5ea9, 1); |
| | | this.selectionLayer.beginFill(0xffffff, 1); |
| | | for (var key in handlePositions) { |
| | | if (!handlePositions.hasOwnProperty(key)) { |
| | | continue; |
| | | } |
| | | var pos = handlePositions[key]; |
| | | this.selectionLayer.drawRect(pos.x - handleSize / 2, pos.y - handleSize / 2, handleSize, handleSize); |
| | | } |
| | | this.selectionLayer.endFill(); |
| | | }, |
| | | renderGuide: function () { |
| | | this.guideLayer.clear(); |
| | | if (this.guideText) { |
| | | this.guideText.visible = false; |
| | | } |
| | | if (!this.interactionState) { |
| | | return; |
| | | } |
| | | var state = this.interactionState; |
| | | if (state.type === 'draw' && state.rect && state.rect.width > 0 && state.rect.height > 0) { |
| | | var drawMeta = getTypeMeta(state.elementType); |
| | | this.guideLayer.lineStyle(2 / this.camera.scale, drawMeta.border, 0.95); |
| | | this.guideLayer.beginFill(drawMeta.fill, 0.18); |
| | | this.guideLayer.drawRoundedRect(state.rect.x, state.rect.y, state.rect.width, state.rect.height, Math.max(6 / this.camera.scale, 2)); |
| | | this.guideLayer.endFill(); |
| | | return; |
| | | } |
| | | if (state.type === 'array' && state.template) { |
| | | var previewItems = state.previewItems || []; |
| | | var arrayMeta = getTypeMeta(state.template.type); |
| | | var lineWidth = 2 / this.camera.scale; |
| | | var templateCenterX = state.template.x + state.template.width / 2; |
| | | var templateCenterY = state.template.y + state.template.height / 2; |
| | | this.guideLayer.lineStyle(lineWidth, arrayMeta.border, 0.9); |
| | | this.guideLayer.moveTo(templateCenterX, templateCenterY); |
| | | this.guideLayer.lineTo(state.currentWorld.x, state.currentWorld.y); |
| | | if (!previewItems.length) { |
| | | return; |
| | | } |
| | | this.guideLayer.lineStyle(1 / this.camera.scale, arrayMeta.border, 0.8); |
| | | this.guideLayer.beginFill(arrayMeta.fill, 0.2); |
| | | for (var previewIndex = 0; previewIndex < previewItems.length; previewIndex++) { |
| | | var preview = previewItems[previewIndex]; |
| | | this.guideLayer.drawRoundedRect(preview.x, preview.y, preview.width, preview.height, Math.max(6 / this.camera.scale, 2)); |
| | | } |
| | | this.guideLayer.endFill(); |
| | | if (this.guideText) { |
| | | this.guideText.text = '将生成 ' + previewItems.length + ' 个'; |
| | | this.guideText.position.set(state.currentWorld.x, state.currentWorld.y - 10 / this.camera.scale); |
| | | this.guideText.scale.set(1 / this.camera.scale); |
| | | this.guideText.visible = true; |
| | | } |
| | | return; |
| | | } |
| | | if (state.type === 'marquee') { |
| | | var rect = buildRectFromPoints(state.startWorld, state.currentWorld); |
| | | if (rect.width <= 0 || rect.height <= 0) { |
| | | return; |
| | | } |
| | | this.guideLayer.lineStyle(2 / this.camera.scale, 0x2f79d6, 0.92); |
| | | this.guideLayer.beginFill(0x2f79d6, 0.06); |
| | | this.guideLayer.drawRect(rect.x, rect.y, rect.width, rect.height); |
| | | this.guideLayer.endFill(); |
| | | } |
| | | }, |
| | | pointerToWorld: function (event) { |
| | | var rect = this.pixiApp.view.getBoundingClientRect(); |
| | | var screenX = event.clientX - rect.left; |
| | | var screenY = event.clientY - rect.top; |
| | | return { |
| | | screenX: screenX, |
| | | screenY: screenY, |
| | | x: roundCoord((screenX - this.camera.x) / this.camera.scale), |
| | | y: roundCoord((screenY - this.camera.y) / this.camera.scale) |
| | | }; |
| | | }, |
| | | isWithinCanvas: function (rect) { |
| | | if (!this.doc) { |
| | | return false; |
| | | } |
| | | return rect.x >= -COORD_EPSILON && rect.y >= -COORD_EPSILON |
| | | && rect.x + rect.width <= this.doc.canvasWidth + COORD_EPSILON |
| | | && rect.y + rect.height <= this.doc.canvasHeight + COORD_EPSILON; |
| | | }, |
| | | canPlaceElements: function (elements, excludeIds) { |
| | | excludeIds = excludeIds || []; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | if (!this.isWithinCanvas(elements[i])) { |
| | | return false; |
| | | } |
| | | if (this.hasOverlap(elements[i], excludeIds.concat([elements[i].id]))) { |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | }, |
| | | hasOverlap: function (candidate, excludeIds) { |
| | | if (!this.doc) { |
| | | return false; |
| | | } |
| | | var elements = this.querySpatialCandidates(candidate, COORD_EPSILON, excludeIds); |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var item = elements[i]; |
| | | if (rectsOverlap(candidate, item)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | }, |
| | | snapToleranceWorld: function () { |
| | | return Math.max(1, EDGE_SNAP_SCREEN_TOLERANCE / this.camera.scale); |
| | | }, |
| | | collectMoveSnap: function (baseItems, dx, dy, excludeIds) { |
| | | if (!this.doc || !baseItems || !baseItems.length) { |
| | | return { dx: 0, dy: 0 }; |
| | | } |
| | | var tolerance = this.snapToleranceWorld(); |
| | | var bestDx = null; |
| | | var bestDy = null; |
| | | for (var i = 0; i < baseItems.length; i++) { |
| | | var moving = baseItems[i]; |
| | | var movedLeft = moving.x + dx; |
| | | var movedRight = movedLeft + moving.width; |
| | | var movedTop = moving.y + dy; |
| | | var movedBottom = movedTop + moving.height; |
| | | var candidates = this.querySpatialCandidates({ |
| | | x: movedLeft, |
| | | y: movedTop, |
| | | width: moving.width, |
| | | height: moving.height |
| | | }, tolerance, excludeIds); |
| | | for (var j = 0; j < candidates.length; j++) { |
| | | var other = candidates[j]; |
| | | var otherLeft = other.x; |
| | | var otherRight = other.x + other.width; |
| | | var otherTop = other.y; |
| | | var otherBottom = other.y + other.height; |
| | | if (rangesNearOrOverlap(movedTop, movedBottom, otherTop, otherBottom, tolerance)) { |
| | | var horizontalCandidates = [ |
| | | otherLeft - movedRight, |
| | | otherRight - movedLeft, |
| | | otherLeft - movedLeft, |
| | | otherRight - movedRight |
| | | ]; |
| | | for (var hx = 0; hx < horizontalCandidates.length; hx++) { |
| | | var deltaX = horizontalCandidates[hx]; |
| | | if (Math.abs(deltaX) <= tolerance && (bestDx === null || Math.abs(deltaX) < Math.abs(bestDx))) { |
| | | bestDx = deltaX; |
| | | } |
| | | } |
| | | } |
| | | if (rangesNearOrOverlap(movedLeft, movedRight, otherLeft, otherRight, tolerance)) { |
| | | var verticalCandidates = [ |
| | | otherTop - movedBottom, |
| | | otherBottom - movedTop, |
| | | otherTop - movedTop, |
| | | otherBottom - movedBottom |
| | | ]; |
| | | for (var vy = 0; vy < verticalCandidates.length; vy++) { |
| | | var deltaY = verticalCandidates[vy]; |
| | | if (Math.abs(deltaY) <= tolerance && (bestDy === null || Math.abs(deltaY) < Math.abs(bestDy))) { |
| | | bestDy = deltaY; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return { |
| | | dx: bestDx == null ? 0 : bestDx, |
| | | dy: bestDy == null ? 0 : bestDy |
| | | }; |
| | | }, |
| | | collectResizeSnap: function (rect, handle, excludeIds) { |
| | | if (!this.doc || !rect) { |
| | | return null; |
| | | } |
| | | var tolerance = this.snapToleranceWorld(); |
| | | var left = rect.x; |
| | | var right = rect.x + rect.width; |
| | | var top = rect.y; |
| | | var bottom = rect.y + rect.height; |
| | | var bestLeft = null; |
| | | var bestRight = null; |
| | | var bestTop = null; |
| | | var bestBottom = null; |
| | | function pickBest(current, candidate) { |
| | | if (candidate == null) { |
| | | return current; |
| | | } |
| | | if (current == null || Math.abs(candidate) < Math.abs(current)) { |
| | | return candidate; |
| | | } |
| | | return current; |
| | | } |
| | | if (handle.indexOf('w') >= 0) { |
| | | bestLeft = pickBest(bestLeft, -left); |
| | | } |
| | | if (handle.indexOf('e') >= 0) { |
| | | bestRight = pickBest(bestRight, this.doc.canvasWidth - right); |
| | | } |
| | | if (handle.indexOf('n') >= 0) { |
| | | bestTop = pickBest(bestTop, -top); |
| | | } |
| | | if (handle.indexOf('s') >= 0) { |
| | | bestBottom = pickBest(bestBottom, this.doc.canvasHeight - bottom); |
| | | } |
| | | var elements = this.querySpatialCandidates(rect, tolerance, excludeIds); |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var other = elements[i]; |
| | | var otherLeft = other.x; |
| | | var otherRight = other.x + other.width; |
| | | var otherTop = other.y; |
| | | var otherBottom = other.y + other.height; |
| | | if (rangesNearOrOverlap(top, bottom, otherTop, otherBottom, tolerance)) { |
| | | if (handle.indexOf('w') >= 0) { |
| | | bestLeft = pickBest(bestLeft, otherLeft - left); |
| | | bestLeft = pickBest(bestLeft, otherRight - left); |
| | | } |
| | | if (handle.indexOf('e') >= 0) { |
| | | bestRight = pickBest(bestRight, otherLeft - right); |
| | | bestRight = pickBest(bestRight, otherRight - right); |
| | | } |
| | | } |
| | | if (rangesNearOrOverlap(left, right, otherLeft, otherRight, tolerance)) { |
| | | if (handle.indexOf('n') >= 0) { |
| | | bestTop = pickBest(bestTop, otherTop - top); |
| | | bestTop = pickBest(bestTop, otherBottom - top); |
| | | } |
| | | if (handle.indexOf('s') >= 0) { |
| | | bestBottom = pickBest(bestBottom, otherTop - bottom); |
| | | bestBottom = pickBest(bestBottom, otherBottom - bottom); |
| | | } |
| | | } |
| | | } |
| | | if (bestLeft != null && Math.abs(bestLeft) > tolerance) { |
| | | bestLeft = null; |
| | | } |
| | | if (bestRight != null && Math.abs(bestRight) > tolerance) { |
| | | bestRight = null; |
| | | } |
| | | if (bestTop != null && Math.abs(bestTop) > tolerance) { |
| | | bestTop = null; |
| | | } |
| | | if (bestBottom != null && Math.abs(bestBottom) > tolerance) { |
| | | bestBottom = null; |
| | | } |
| | | return { |
| | | left: bestLeft, |
| | | right: bestRight, |
| | | top: bestTop, |
| | | bottom: bestBottom |
| | | }; |
| | | }, |
| | | hitTestElement: function (point) { |
| | | if (!this.doc) { |
| | | return null; |
| | | } |
| | | var candidates = this.querySpatialCandidates({ |
| | | x: point.x, |
| | | y: point.y, |
| | | width: 0, |
| | | height: 0 |
| | | }, 0, []); |
| | | if (!candidates.length) { |
| | | return null; |
| | | } |
| | | var candidateMap = {}; |
| | | for (var c = 0; c < candidates.length; c++) { |
| | | candidateMap[candidates[c].id] = true; |
| | | } |
| | | var elements = this.doc.elements || []; |
| | | for (var i = elements.length - 1; i >= 0; i--) { |
| | | var element = elements[i]; |
| | | if (!candidateMap[element.id]) { |
| | | continue; |
| | | } |
| | | if (point.x >= element.x && point.x <= element.x + element.width |
| | | && point.y >= element.y && point.y <= element.y + element.height) { |
| | | return element; |
| | | } |
| | | } |
| | | return null; |
| | | }, |
| | | getHandlePositions: function (element) { |
| | | var x = element.x; |
| | | var y = element.y; |
| | | var w = element.width; |
| | | var h = element.height; |
| | | var cx = x + w / 2; |
| | | var cy = y + h / 2; |
| | | return { |
| | | nw: { x: x, y: y }, |
| | | n: { x: cx, y: y }, |
| | | ne: { x: x + w, y: y }, |
| | | e: { x: x + w, y: cy }, |
| | | se: { x: x + w, y: y + h }, |
| | | s: { x: cx, y: y + h }, |
| | | sw: { x: x, y: y + h }, |
| | | w: { x: x, y: cy } |
| | | }; |
| | | }, |
| | | getResizeHandleAt: function (point, element) { |
| | | var handlePositions = this.getHandlePositions(element); |
| | | var baseTolerance = HANDLE_SCREEN_SIZE / this.camera.scale; |
| | | var sizeLimitedTolerance = Math.max(Math.min(element.width, element.height) / 4, 3 / this.camera.scale); |
| | | var tolerance = Math.min(baseTolerance, sizeLimitedTolerance); |
| | | var bestHandle = ''; |
| | | var bestDistance = Infinity; |
| | | for (var key in handlePositions) { |
| | | if (!handlePositions.hasOwnProperty(key)) { |
| | | continue; |
| | | } |
| | | var pos = handlePositions[key]; |
| | | var dx = Math.abs(point.x - pos.x); |
| | | var dy = Math.abs(point.y - pos.y); |
| | | if (dx <= tolerance && dy <= tolerance) { |
| | | var distance = dx + dy; |
| | | if (distance < bestDistance) { |
| | | bestDistance = distance; |
| | | bestHandle = key; |
| | | } |
| | | } |
| | | } |
| | | return bestHandle; |
| | | }, |
| | | cursorForHandle: function (handle) { |
| | | if (handle === 'nw' || handle === 'se') { |
| | | return 'nwse-resize'; |
| | | } |
| | | if (handle === 'ne' || handle === 'sw') { |
| | | return 'nesw-resize'; |
| | | } |
| | | if (handle === 'n' || handle === 's') { |
| | | return 'ns-resize'; |
| | | } |
| | | if (handle === 'e' || handle === 'w') { |
| | | return 'ew-resize'; |
| | | } |
| | | return 'default'; |
| | | }, |
| | | updateCursor: function () { |
| | | if (!this.pixiApp) { |
| | | return; |
| | | } |
| | | var cursor = 'default'; |
| | | if (this.interactionState) { |
| | | if (this.interactionState.type === 'pan') { |
| | | cursor = 'grabbing'; |
| | | } else if (this.interactionState.type === 'draw' || this.interactionState.type === 'marquee') { |
| | | cursor = 'crosshair'; |
| | | } else if (this.interactionState.type === 'array') { |
| | | cursor = 'crosshair'; |
| | | } else if (this.interactionState.type === 'move') { |
| | | cursor = 'move'; |
| | | } else if (this.interactionState.type === 'movePending') { |
| | | cursor = 'grab'; |
| | | } else if (this.interactionState.type === 'resize') { |
| | | cursor = this.cursorForHandle(this.interactionState.handle); |
| | | } |
| | | } else if (this.spacePressed || this.activeTool === 'pan') { |
| | | cursor = 'grab'; |
| | | } else if (DRAW_TYPES.indexOf(this.activeTool) >= 0 || this.activeTool === 'marquee' || this.activeTool === 'array') { |
| | | cursor = 'crosshair'; |
| | | } else if (this.singleSelectedElement) { |
| | | var point = this.lastPointerWorld || null; |
| | | if (point) { |
| | | var handle = this.getResizeHandleAt(point, this.singleSelectedElement); |
| | | cursor = handle ? this.cursorForHandle(handle) : 'default'; |
| | | } |
| | | if (cursor === 'default' && this.hoverElementId) { |
| | | cursor = 'move'; |
| | | } else if (cursor === 'default') { |
| | | cursor = 'grab'; |
| | | } |
| | | } else { |
| | | cursor = this.hoverElementId ? 'move' : 'grab'; |
| | | } |
| | | if (cursor !== this.lastCursor) { |
| | | this.lastCursor = cursor; |
| | | this.pixiApp.view.style.cursor = cursor; |
| | | } |
| | | }, |
| | | startPan: function (point) { |
| | | this.cancelDeferredStaticRebuild(); |
| | | this.cancelPanRefresh(); |
| | | if (this.zoomRefreshTimer) { |
| | | window.clearTimeout(this.zoomRefreshTimer); |
| | | this.zoomRefreshTimer = null; |
| | | this.isZooming = false; |
| | | this.pendingViewportRefresh = true; |
| | | } |
| | | this.isPanning = true; |
| | | this.interactionState = { |
| | | type: 'pan', |
| | | startScreen: { |
| | | x: point.screenX, |
| | | y: point.screenY |
| | | }, |
| | | startCamera: { |
| | | x: this.camera.x, |
| | | y: this.camera.y |
| | | } |
| | | }; |
| | | this.updateCursor(); |
| | | }, |
| | | startMarquee: function (point, additive) { |
| | | this.cancelDeferredStaticRebuild(); |
| | | this.interactionState = { |
| | | type: 'marquee', |
| | | additive: !!additive, |
| | | startWorld: { x: point.x, y: point.y }, |
| | | currentWorld: { x: point.x, y: point.y } |
| | | }; |
| | | this.updateCursor(); |
| | | }, |
| | | startDraw: function (point) { |
| | | this.cancelDeferredStaticRebuild(); |
| | | this.interactionState = { |
| | | type: 'draw', |
| | | beforeSnapshot: this.snapshotDoc(this.doc), |
| | | elementType: this.activeTool, |
| | | startWorld: { x: point.x, y: point.y }, |
| | | rect: { x: point.x, y: point.y, width: 0, height: 0 } |
| | | }; |
| | | this.updateCursor(); |
| | | }, |
| | | startArray: function (point, element) { |
| | | if (!this.canArrayFromElement(element)) { |
| | | this.showMessage('warning', '阵列工具当前只支持货架、CRN、双工位和 RGV'); |
| | | return; |
| | | } |
| | | this.cancelDeferredStaticRebuild(); |
| | | this.interactionState = { |
| | | type: 'array', |
| | | beforeSnapshot: this.snapshotDoc(this.doc), |
| | | template: { |
| | | id: element.id, |
| | | type: element.type, |
| | | x: element.x, |
| | | y: element.y, |
| | | width: element.width, |
| | | height: element.height, |
| | | value: element.value |
| | | }, |
| | | startWorld: { x: point.x, y: point.y }, |
| | | currentWorld: { x: point.x, y: point.y }, |
| | | previewItems: [] |
| | | }; |
| | | this.updateCursor(); |
| | | }, |
| | | startMove: function (point) { |
| | | var selected = this.getSelectedElements(); |
| | | if (!selected.length) { |
| | | return; |
| | | } |
| | | this.cancelDeferredStaticRebuild(); |
| | | var baseItems = selected.map(function (item) { |
| | | return { |
| | | id: item.id, |
| | | x: item.x, |
| | | y: item.y, |
| | | width: item.width, |
| | | height: item.height, |
| | | value: item.value, |
| | | type: item.type |
| | | }; |
| | | }); |
| | | this.interactionState = { |
| | | type: 'movePending', |
| | | beforeSnapshot: this.snapshotDoc(this.doc), |
| | | startScreen: { x: point.screenX, y: point.screenY }, |
| | | startWorld: { x: point.x, y: point.y }, |
| | | baseItems: baseItems |
| | | }; |
| | | this.updateCursor(); |
| | | }, |
| | | startResize: function (point, element, handle) { |
| | | this.cancelDeferredStaticRebuild(); |
| | | this.interactionState = { |
| | | type: 'resize', |
| | | handle: handle, |
| | | elementId: element.id, |
| | | beforeSnapshot: this.snapshotDoc(this.doc), |
| | | baseRect: { |
| | | x: element.x, |
| | | y: element.y, |
| | | width: element.width, |
| | | height: element.height |
| | | } |
| | | }; |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | this.updateCursor(); |
| | | }, |
| | | onCanvasPointerDown: function (event) { |
| | | if (!this.doc || !this.pixiApp) { |
| | | return; |
| | | } |
| | | if (event.button !== 0 && event.button !== 1) { |
| | | return; |
| | | } |
| | | if (this.pixiApp.view.setPointerCapture && event.pointerId != null) { |
| | | try { |
| | | this.pixiApp.view.setPointerCapture(event.pointerId); |
| | | } catch (ignore) { |
| | | } |
| | | } |
| | | this.currentPointerId = event.pointerId; |
| | | var point = this.pointerToWorld(event); |
| | | this.lastPointerWorld = point; |
| | | this.pointerStatus = this.formatNumber(point.x) + ', ' + this.formatNumber(point.y); |
| | | if (this.spacePressed || this.activeTool === 'pan' || event.button === 1) { |
| | | this.startPan(point); |
| | | return; |
| | | } |
| | | if (DRAW_TYPES.indexOf(this.activeTool) >= 0) { |
| | | this.startDraw(point); |
| | | return; |
| | | } |
| | | if (this.activeTool === 'marquee') { |
| | | this.startMarquee(point, event.shiftKey); |
| | | return; |
| | | } |
| | | if (this.activeTool === 'array') { |
| | | var arrayHit = this.hitTestElement(point); |
| | | var arrayTemplate = arrayHit || this.singleSelectedElement; |
| | | if (arrayHit && this.selectedIds.indexOf(arrayHit.id) < 0) { |
| | | this.setSelectedIds([arrayHit.id]); |
| | | arrayTemplate = arrayHit; |
| | | } |
| | | if (!arrayTemplate) { |
| | | this.showMessage('warning', '请先选中一个货架或轨道作为阵列模板'); |
| | | return; |
| | | } |
| | | this.startArray(point, arrayTemplate); |
| | | return; |
| | | } |
| | | |
| | | var selected = this.singleSelectedElement; |
| | | var handle = selected ? this.getResizeHandleAt(point, selected) : ''; |
| | | if (handle) { |
| | | this.startResize(point, selected, handle); |
| | | return; |
| | | } |
| | | |
| | | var hit = this.hitTestElement(point); |
| | | if (hit) { |
| | | if (event.shiftKey) { |
| | | var index = this.selectedIds.indexOf(hit.id); |
| | | if (index >= 0) { |
| | | var nextIds = this.selectedIds.slice(); |
| | | nextIds.splice(index, 1); |
| | | this.setSelectedIds(nextIds); |
| | | } else { |
| | | this.setSelectedIds(this.selectedIds.concat([hit.id])); |
| | | } |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | if (this.selectedIds.indexOf(hit.id) < 0) { |
| | | this.setSelectedIds([hit.id]); |
| | | this.scheduleRender(); |
| | | } |
| | | this.startMove(point); |
| | | return; |
| | | } |
| | | |
| | | if (this.selectedIds.length) { |
| | | this.setSelectedIds([]); |
| | | this.scheduleRender(); |
| | | } |
| | | this.startPan(point); |
| | | }, |
| | | onCanvasWheel: function (event) { |
| | | if (!this.pixiApp || !this.doc) { |
| | | return; |
| | | } |
| | | event.preventDefault(); |
| | | var point = this.pointerToWorld(event); |
| | | var delta = event.deltaY < 0 ? 1.12 : 0.89; |
| | | var nextScale = clamp(this.camera.scale * delta, 0.06, 4); |
| | | this.camera.scale = nextScale; |
| | | this.camera.x = Math.round(point.screenX - point.x * nextScale); |
| | | this.camera.y = Math.round(point.screenY - point.y * nextScale); |
| | | this.viewZoom = nextScale; |
| | | this.scheduleZoomRefresh(); |
| | | this.scheduleRender(); |
| | | }, |
| | | onWindowPointerMove: function (event) { |
| | | if (!this.pixiApp || !this.doc) { |
| | | return; |
| | | } |
| | | var point = this.pointerToWorld(event); |
| | | this.lastPointerWorld = point; |
| | | var pointerText = this.formatNumber(point.x) + ', ' + this.formatNumber(point.y); |
| | | var now = (window.performance && performance.now) ? performance.now() : Date.now(); |
| | | if (pointerText !== this.pointerStatus && (now - this.lastPointerStatusUpdateTs >= POINTER_STATUS_UPDATE_INTERVAL || this.pointerStatus === '--')) { |
| | | this.pointerStatus = pointerText; |
| | | this.lastPointerStatusUpdateTs = now; |
| | | } |
| | | if (!this.interactionState) { |
| | | var hover = this.hitTestElement(point); |
| | | var hoverId = hover ? hover.id : ''; |
| | | if (hoverId !== this.hoverElementId) { |
| | | this.hoverElementId = hoverId; |
| | | this.scheduleRender(); |
| | | } |
| | | this.updateCursor(); |
| | | return; |
| | | } |
| | | |
| | | var state = this.interactionState; |
| | | if (state.type === 'pan') { |
| | | this.camera.x = Math.round(state.startCamera.x + (point.screenX - state.startScreen.x)); |
| | | this.camera.y = Math.round(state.startCamera.y + (point.screenY - state.startScreen.y)); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'marquee') { |
| | | state.currentWorld = { x: point.x, y: point.y }; |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'draw') { |
| | | var rawRect = buildRectFromPoints(state.startWorld, point); |
| | | var clipped = { |
| | | x: clamp(rawRect.x, 0, this.doc.canvasWidth), |
| | | y: clamp(rawRect.y, 0, this.doc.canvasHeight), |
| | | width: clamp(rawRect.width, 0, this.doc.canvasWidth), |
| | | height: clamp(rawRect.height, 0, this.doc.canvasHeight) |
| | | }; |
| | | if (clipped.x + clipped.width > this.doc.canvasWidth) { |
| | | clipped.width = roundCoord(this.doc.canvasWidth - clipped.x); |
| | | } |
| | | if (clipped.y + clipped.height > this.doc.canvasHeight) { |
| | | clipped.height = roundCoord(this.doc.canvasHeight - clipped.y); |
| | | } |
| | | state.rect = clipped; |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | if (state.type === 'array') { |
| | | state.currentWorld = { x: point.x, y: point.y }; |
| | | state.previewItems = this.buildArrayCopies(state.template, state.startWorld, state.currentWorld); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'movePending') { |
| | | var dragDistance = Math.max(Math.abs(point.screenX - state.startScreen.x), Math.abs(point.screenY - state.startScreen.y)); |
| | | if (dragDistance < DRAG_START_THRESHOLD) { |
| | | return; |
| | | } |
| | | state.type = 'move'; |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | this.updateCursor(); |
| | | } |
| | | |
| | | if (state.type === 'move') { |
| | | var dx = point.x - state.startWorld.x; |
| | | var dy = point.y - state.startWorld.y; |
| | | var minDx = -Infinity; |
| | | var maxDx = Infinity; |
| | | var minDy = -Infinity; |
| | | var maxDy = Infinity; |
| | | for (var i = 0; i < state.baseItems.length; i++) { |
| | | var base = state.baseItems[i]; |
| | | minDx = Math.max(minDx, -base.x); |
| | | minDy = Math.max(minDy, -base.y); |
| | | maxDx = Math.min(maxDx, this.doc.canvasWidth - (base.x + base.width)); |
| | | maxDy = Math.min(maxDy, this.doc.canvasHeight - (base.y + base.height)); |
| | | } |
| | | dx = clamp(dx, minDx, maxDx); |
| | | dy = clamp(dy, minDy, maxDy); |
| | | var snapDelta = this.collectMoveSnap(state.baseItems, dx, dy, this.selectedIds.slice()); |
| | | dx = clamp(dx + snapDelta.dx, minDx, maxDx); |
| | | dy = clamp(dy + snapDelta.dy, minDy, maxDy); |
| | | for (var j = 0; j < state.baseItems.length; j++) { |
| | | var baseItem = state.baseItems[j]; |
| | | var element = this.findElementById(baseItem.id); |
| | | if (!element) { |
| | | continue; |
| | | } |
| | | element.x = roundCoord(baseItem.x + dx); |
| | | element.y = roundCoord(baseItem.y + dy); |
| | | } |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'resize') { |
| | | var target = this.findElementById(state.elementId); |
| | | if (!target) { |
| | | return; |
| | | } |
| | | var baseRect = state.baseRect; |
| | | var left = baseRect.x; |
| | | var right = baseRect.x + baseRect.width; |
| | | var top = baseRect.y; |
| | | var bottom = baseRect.y + baseRect.height; |
| | | if (state.handle.indexOf('w') >= 0) { |
| | | left = clamp(point.x, 0, right - MIN_ELEMENT_SIZE); |
| | | } |
| | | if (state.handle.indexOf('e') >= 0) { |
| | | right = clamp(point.x, left + MIN_ELEMENT_SIZE, this.doc.canvasWidth); |
| | | } |
| | | if (state.handle.indexOf('n') >= 0) { |
| | | top = clamp(point.y, 0, bottom - MIN_ELEMENT_SIZE); |
| | | } |
| | | if (state.handle.indexOf('s') >= 0) { |
| | | bottom = clamp(point.y, top + MIN_ELEMENT_SIZE, this.doc.canvasHeight); |
| | | } |
| | | var snapped = this.collectResizeSnap({ |
| | | x: left, |
| | | y: top, |
| | | width: right - left, |
| | | height: bottom - top |
| | | }, state.handle, [target.id]); |
| | | if (snapped) { |
| | | if (state.handle.indexOf('w') >= 0 && snapped.left != null) { |
| | | left = clamp(left + snapped.left, 0, right - MIN_ELEMENT_SIZE); |
| | | } |
| | | if (state.handle.indexOf('e') >= 0 && snapped.right != null) { |
| | | right = clamp(right + snapped.right, left + MIN_ELEMENT_SIZE, this.doc.canvasWidth); |
| | | } |
| | | if (state.handle.indexOf('n') >= 0 && snapped.top != null) { |
| | | top = clamp(top + snapped.top, 0, bottom - MIN_ELEMENT_SIZE); |
| | | } |
| | | if (state.handle.indexOf('s') >= 0 && snapped.bottom != null) { |
| | | bottom = clamp(bottom + snapped.bottom, top + MIN_ELEMENT_SIZE, this.doc.canvasHeight); |
| | | } |
| | | } |
| | | target.x = roundCoord(left); |
| | | target.y = roundCoord(top); |
| | | target.width = roundCoord(right - left); |
| | | target.height = roundCoord(bottom - top); |
| | | this.scheduleRender(); |
| | | } |
| | | }, |
| | | onWindowPointerUp: function (event) { |
| | | if (!this.interactionState) { |
| | | return; |
| | | } |
| | | if (this.currentPointerId != null && event.pointerId != null && this.currentPointerId !== event.pointerId) { |
| | | return; |
| | | } |
| | | if (this.pixiApp && this.pixiApp.view.releasePointerCapture && event.pointerId != null) { |
| | | try { |
| | | this.pixiApp.view.releasePointerCapture(event.pointerId); |
| | | } catch (ignore) { |
| | | } |
| | | } |
| | | this.currentPointerId = null; |
| | | |
| | | var state = this.interactionState; |
| | | this.interactionState = null; |
| | | |
| | | if (state.type === 'pan') { |
| | | this.updateCursor(); |
| | | this.schedulePanRefresh(); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'marquee') { |
| | | var rect = buildRectFromPoints(state.startWorld, state.currentWorld); |
| | | if (rect.width > 2 && rect.height > 2) { |
| | | var matched = (this.doc.elements || []).filter(function (item) { |
| | | return rectIntersects(rect, item); |
| | | }).map(function (item) { |
| | | return item.id; |
| | | }); |
| | | this.setSelectedIds(state.additive ? Array.from(new Set(this.selectedIds.concat(matched))) : matched); |
| | | } |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'movePending') { |
| | | this.updateCursor(); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'draw') { |
| | | var drawRect = state.rect; |
| | | if (drawRect && drawRect.width >= MIN_ELEMENT_SIZE && drawRect.height >= MIN_ELEMENT_SIZE) { |
| | | var newElement = { |
| | | id: nextId(), |
| | | type: state.elementType, |
| | | x: roundCoord(drawRect.x), |
| | | y: roundCoord(drawRect.y), |
| | | width: roundCoord(drawRect.width), |
| | | height: roundCoord(drawRect.height), |
| | | value: '' |
| | | }; |
| | | if (this.hasOverlap(newElement, [])) { |
| | | this.showMessage('warning', '新元素不能与已有元素重叠'); |
| | | } else if (!this.isWithinCanvas(newElement)) { |
| | | this.showMessage('warning', '新元素超出画布范围'); |
| | | } else { |
| | | this.doc.elements.push(newElement); |
| | | this.selectedIds = [newElement.id]; |
| | | this.commitMutation(state.beforeSnapshot); |
| | | this.refreshInspector(); |
| | | return; |
| | | } |
| | | } |
| | | this.refreshInspector(); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | if (state.type === 'array') { |
| | | var copies = state.previewItems && state.previewItems.length |
| | | ? state.previewItems |
| | | : this.buildArrayCopies(state.template, state.startWorld, state.currentWorld || state.startWorld); |
| | | if (!copies.length) { |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | if (!this.canPlaceElements(copies, [])) { |
| | | this.showMessage('warning', '阵列生成后会重叠或超出画布,已取消'); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | var finalizedCopies = copies.map(function (item) { |
| | | return $.extend({}, item, { id: nextId() }); |
| | | }); |
| | | var self = this; |
| | | this.runMutation(function () { |
| | | self.doc.elements = self.doc.elements.concat(finalizedCopies); |
| | | self.selectedIds = [finalizedCopies[finalizedCopies.length - 1].id]; |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'move') { |
| | | var movedElements = this.getSelectedElements(); |
| | | if (!this.canPlaceElements(movedElements, this.selectedIds.slice())) { |
| | | for (var i = 0; i < state.baseItems.length; i++) { |
| | | var base = state.baseItems[i]; |
| | | var element = this.findElementById(base.id); |
| | | if (!element) { |
| | | continue; |
| | | } |
| | | element.x = base.x; |
| | | element.y = base.y; |
| | | } |
| | | this.showMessage('warning', '移动后会重叠或超出画布,已恢复'); |
| | | this.refreshInspector(); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | if (!this.commitMutation(state.beforeSnapshot)) { |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'resize') { |
| | | var resized = this.findElementById(state.elementId); |
| | | if (resized) { |
| | | if (!this.isWithinCanvas(resized) || this.hasOverlap(resized, [resized.id])) { |
| | | resized.x = state.baseRect.x; |
| | | resized.y = state.baseRect.y; |
| | | resized.width = state.baseRect.width; |
| | | resized.height = state.baseRect.height; |
| | | this.showMessage('warning', '缩放后会重叠或超出画布,已恢复'); |
| | | this.refreshInspector(); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | } |
| | | if (!this.commitMutation(state.beforeSnapshot)) { |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | this.scheduleRender(); |
| | | }, |
| | | onWindowKeyDown: function (event) { |
| | | if (event.key === ' ' && !isInputLike(event.target)) { |
| | | this.spacePressed = true; |
| | | this.updateCursor(); |
| | | event.preventDefault(); |
| | | } |
| | | if (!this.doc) { |
| | | return; |
| | | } |
| | | if (isInputLike(event.target)) { |
| | | return; |
| | | } |
| | | var ctrl = event.ctrlKey || event.metaKey; |
| | | if (event.key === 'Delete' || event.key === 'Backspace') { |
| | | event.preventDefault(); |
| | | this.deleteSelection(); |
| | | return; |
| | | } |
| | | if (ctrl && (event.key === 'z' || event.key === 'Z')) { |
| | | event.preventDefault(); |
| | | if (event.shiftKey) { |
| | | this.redo(); |
| | | } else { |
| | | this.undo(); |
| | | } |
| | | return; |
| | | } |
| | | if (ctrl && (event.key === 'y' || event.key === 'Y')) { |
| | | event.preventDefault(); |
| | | this.redo(); |
| | | return; |
| | | } |
| | | if (ctrl && (event.key === 'c' || event.key === 'C')) { |
| | | event.preventDefault(); |
| | | this.copySelection(); |
| | | return; |
| | | } |
| | | if (ctrl && (event.key === 'v' || event.key === 'V')) { |
| | | event.preventDefault(); |
| | | this.pasteClipboard(); |
| | | return; |
| | | } |
| | | if (event.key === 'Escape') { |
| | | this.interactionState = null; |
| | | this.setSelectedIds([]); |
| | | this.hoverElementId = ''; |
| | | this.scheduleRender(); |
| | | } |
| | | }, |
| | | onWindowKeyUp: function (event) { |
| | | if (event.key === ' ') { |
| | | this.spacePressed = false; |
| | | this.updateCursor(); |
| | | } |
| | | }, |
| | | onBeforeUnload: function (event) { |
| | | if (!this.isDirty) { |
| | | return; |
| | | } |
| | | event.preventDefault(); |
| | | event.returnValue = ''; |
| | | } |
| | | }) |
| | | }; |
| | | }, |
| | | clearDeferredStaticCommit: function () { |
| | | this.cancelDeferredStaticRebuild(); |
| | | this.pendingStaticCommit = null; |
| | | }, |
| | | scheduleDeferredStaticRebuild: function () { |
| | | this.cancelDeferredStaticRebuild(); |
| | | this.deferredStaticRebuildTimer = window.setTimeout( |
| | | function () { |
| | | this.deferredStaticRebuildTimer = null; |
| | | this.pendingStaticCommit = null; |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | }.bind(this), |
| | | DEFERRED_STATIC_REBUILD_DELAY |
| | | ); |
| | | }, |
| | | selectionKey: function (ids) { |
| | | return (ids || []).slice().sort().join('|'); |
| | | }, |
| | | setSelectedIds: function (ids, options) { |
| | | options = options || {}; |
| | | var nextIds = (ids || []).filter(Boolean); |
| | | this.selectedIds = nextIds.slice(); |
| | | if (options.refreshInspector !== false) { |
| | | this.refreshInspector(); |
| | | } |
| | | }); |
| | | }, |
| | | setCurrentDoc: function (doc, options) { |
| | | options = options || {}; |
| | | var normalized = this.normalizeDoc(doc); |
| | | this.clearFloorTransientState(); |
| | | this.resetRenderLayers(); |
| | | this.clearRenderCaches(); |
| | | this.doc = normalized; |
| | | this.markSpatialIndexDirty(); |
| | | this.labelCapabilityDirty = true; |
| | | this.pendingViewportRefresh = false; |
| | | this.currentLev = normalized.lev; |
| | | this.floorPickerLev = normalized.lev; |
| | | this.switchingFloorLev = null; |
| | | this.loadingFloor = false; |
| | | this.syncFloorQueryParam(normalized.lev); |
| | | this.markGridSceneDirty(); |
| | | this.markStaticSceneDirty(); |
| | | this.undoStack = []; |
| | | this.redoStack = []; |
| | | this.savedSnapshot = |
| | | options.savedSnapshot != null ? options.savedSnapshot : this.snapshotDoc(normalized); |
| | | this.syncDirty(); |
| | | this.refreshInspector(); |
| | | this.refreshLevOptions(); |
| | | this.$nextTick( |
| | | function () { |
| | | this.fitContent(); |
| | | this.scheduleRender(); |
| | | }.bind(this) |
| | | ); |
| | | }, |
| | | replaceDocFromSnapshot: function (snapshot) { |
| | | if (!snapshot) { |
| | | return; |
| | | } |
| | | try { |
| | | this.clearFloorTransientState(); |
| | | this.resetRenderLayers(); |
| | | this.clearRenderCaches(); |
| | | this.doc = this.normalizeDoc(JSON.parse(snapshot)); |
| | | this.markSpatialIndexDirty(); |
| | | this.labelCapabilityDirty = true; |
| | | this.pendingViewportRefresh = false; |
| | | } catch (e) { |
| | | this.showMessage('error', '历史记录恢复失败'); |
| | | return; |
| | | } |
| | | this.markGridSceneDirty(); |
| | | this.markStaticSceneDirty(); |
| | | this.floorPickerLev = this.doc.lev; |
| | | this.currentLev = this.doc.lev; |
| | | this.refreshInspector(); |
| | | this.syncDirty(); |
| | | this.cacheCurrentDraft(); |
| | | this.scheduleRender(); |
| | | }, |
| | | pushUndoSnapshot: function (snapshot) { |
| | | if (!snapshot) { |
| | | return; |
| | | } |
| | | if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length - 1] === snapshot) { |
| | | return; |
| | | } |
| | | this.undoStack.push(snapshot); |
| | | if (this.undoStack.length > HISTORY_LIMIT) { |
| | | this.undoStack.shift(); |
| | | } |
| | | }, |
| | | commitMutation: function (beforeSnapshot, options) { |
| | | options = options || {}; |
| | | var afterSnapshot = this.snapshotDoc(this.doc); |
| | | if (beforeSnapshot === afterSnapshot) { |
| | | this.scheduleRender(); |
| | | this.refreshInspector(); |
| | | return false; |
| | | } |
| | | this.pushUndoSnapshot(beforeSnapshot); |
| | | this.redoStack = []; |
| | | this.markSpatialIndexDirty(); |
| | | this.labelCapabilityDirty = true; |
| | | if (options.staticSceneDirty !== false) { |
| | | this.clearDeferredStaticCommit(); |
| | | this.markStaticSceneDirty(); |
| | | } |
| | | this.syncDirty(); |
| | | this.cacheCurrentDraft(); |
| | | this.refreshInspector(); |
| | | this.scheduleRender(); |
| | | return true; |
| | | }, |
| | | runMutation: function (mutator) { |
| | | if (!this.doc) { |
| | | return false; |
| | | } |
| | | var beforeSnapshot = this.snapshotDoc(this.doc); |
| | | mutator(); |
| | | return this.commitMutation(beforeSnapshot); |
| | | }, |
| | | undo: function () { |
| | | if (this.undoStack.length === 0 || !this.doc) { |
| | | return; |
| | | } |
| | | var currentSnapshot = this.snapshotDoc(this.doc); |
| | | var snapshot = this.undoStack.pop(); |
| | | this.redoStack.push(currentSnapshot); |
| | | this.replaceDocFromSnapshot(snapshot); |
| | | }, |
| | | redo: function () { |
| | | if (this.redoStack.length === 0 || !this.doc) { |
| | | return; |
| | | } |
| | | var currentSnapshot = this.snapshotDoc(this.doc); |
| | | var snapshot = this.redoStack.pop(); |
| | | this.pushUndoSnapshot(currentSnapshot); |
| | | this.replaceDocFromSnapshot(snapshot); |
| | | }, |
| | | createLocalBlankDoc: function (lev, width, height, savedSnapshot) { |
| | | var doc = { |
| | | lev: toInt(lev, 1), |
| | | editorMode: FREE_EDITOR_MODE, |
| | | canvasWidth: Math.max(MIN_ELEMENT_SIZE * 4, toNumber(width, DEFAULT_CANVAS_WIDTH)), |
| | | canvasHeight: Math.max(MIN_ELEMENT_SIZE * 4, toNumber(height, DEFAULT_CANVAS_HEIGHT)), |
| | | elements: [] |
| | | }; |
| | | this.setCurrentDoc(doc, { |
| | | savedSnapshot: savedSnapshot != null ? savedSnapshot : '' |
| | | }); |
| | | this.cacheCurrentDraft(); |
| | | this.syncDirty(); |
| | | }, |
| | | openBlankDialog: function () { |
| | | var lev = this.currentLev || 1; |
| | | this.blankForm = { |
| | | lev: String(lev), |
| | | width: String(Math.round(this.doc ? this.doc.canvasWidth : DEFAULT_CANVAS_WIDTH)), |
| | | height: String(Math.round(this.doc ? this.doc.canvasHeight : DEFAULT_CANVAS_HEIGHT)) |
| | | }; |
| | | this.blankDialogVisible = true; |
| | | }, |
| | | createBlankMap: function () { |
| | | var lev = toInt(this.blankForm.lev, 0); |
| | | var width = toNumber(this.blankForm.width, DEFAULT_CANVAS_WIDTH); |
| | | var height = toNumber(this.blankForm.height, DEFAULT_CANVAS_HEIGHT); |
| | | if (lev <= 0) { |
| | | this.showMessage('warning', '楼层不能为空'); |
| | | return; |
| | | } |
| | | if (width <= 0 || height <= 0) { |
| | | this.showMessage('warning', '画布尺寸必须大于 0'); |
| | | return; |
| | | } |
| | | this.blankDialogVisible = false; |
| | | this.createLocalBlankDoc(lev, width, height, ''); |
| | | }, |
| | | buildTransferPayload: function () { |
| | | var doc = this.exportDoc(this.doc); |
| | | return { |
| | | format: MAP_TRANSFER_FORMAT, |
| | | exportedAt: new Date().toISOString(), |
| | | source: { |
| | | lev: doc.lev, |
| | | editorMode: doc.editorMode |
| | | }, |
| | | docs: [doc] |
| | | }; |
| | | }, |
| | | buildTransferFilename: function (docs) { |
| | | var levs = (docs || []) |
| | | .map(function (item) { |
| | | return toInt(item && item.lev, 0); |
| | | }) |
| | | .filter(function (lev) { |
| | | return lev > 0; |
| | | }) |
| | | .sort(function (a, b) { |
| | | return a - b; |
| | | }); |
| | | var scope = |
| | | levs.length <= 1 |
| | | ? String(levs[0] || this.currentLev || 1) + 'F' |
| | | : 'all-' + levs.length + '-floors'; |
| | | var now = new Date(); |
| | | return ( |
| | | [ |
| | | 'bas-map', |
| | | scope, |
| | | now.getFullYear(), |
| | | padNumber(now.getMonth() + 1), |
| | | padNumber(now.getDate()), |
| | | padNumber(now.getHours()), |
| | | padNumber(now.getMinutes()), |
| | | padNumber(now.getSeconds()) |
| | | ].join('-') + '.json' |
| | | ); |
| | | }, |
| | | requestEditorDoc: function (lev) { |
| | | return new Promise(function (resolve, reject) { |
| | | $.ajax({ |
| | | url: baseUrl + '/basMap/editor/' + lev + '/auth', |
| | | method: 'GET', |
| | | headers: authHeaders(), |
| | | success: function (res) { |
| | | if (!res || res.code !== 200 || !res.data) { |
| | | reject(new Error(res && res.msg ? res.msg : '加载 ' + lev + 'F 地图失败')); |
| | | return; |
| | | } |
| | | resolve(res.data); |
| | | }, |
| | | error: function () { |
| | | reject(new Error('加载 ' + lev + 'F 地图失败')); |
| | | } |
| | | }); |
| | | }); |
| | | }, |
| | | collectAllTransferDocs: function () { |
| | | var self = this; |
| | | var levMap = {}; |
| | | (this.remoteLevOptions || []).forEach(function (lev) { |
| | | lev = toInt(lev, 0); |
| | | if (lev > 0) { |
| | | levMap[lev] = true; |
| | | } |
| | | }); |
| | | Object.keys(this.draftDocs || {}).forEach(function (key) { |
| | | var lev = toInt(key, 0); |
| | | if (lev > 0) { |
| | | levMap[lev] = true; |
| | | } |
| | | }); |
| | | if (this.doc && this.doc.lev) { |
| | | levMap[toInt(this.doc.lev, 0)] = true; |
| | | } |
| | | var levs = Object.keys(levMap) |
| | | .map(function (key) { |
| | | return toInt(key, 0); |
| | | }) |
| | | .filter(function (lev) { |
| | | return lev > 0; |
| | | }) |
| | | .sort(function (a, b) { |
| | | return a - b; |
| | | }); |
| | | if (!levs.length) { |
| | | return Promise.resolve([]); |
| | | } |
| | | return Promise.all( |
| | | levs.map(function (lev) { |
| | | if (self.doc && self.doc.lev === lev) { |
| | | return Promise.resolve(self.exportDoc(self.doc)); |
| | | } |
| | | if (self.draftDocs[lev] && self.draftDocs[lev].doc) { |
| | | return Promise.resolve(self.exportDoc(self.draftDocs[lev].doc)); |
| | | } |
| | | return self.requestEditorDoc(lev).then(function (doc) { |
| | | return self.normalizeDoc(doc); |
| | | }); |
| | | }) |
| | | ); |
| | | }, |
| | | exportMapPackage: function () { |
| | | var self = this; |
| | | if (!this.doc && (!this.remoteLevOptions || !this.remoteLevOptions.length)) { |
| | | this.showMessage('warning', '当前没有可导出的地图'); |
| | | return; |
| | | } |
| | | this.collectAllTransferDocs() |
| | | .then(function (docs) { |
| | | if (!docs || !docs.length) { |
| | | self.showMessage('warning', '当前没有可导出的地图'); |
| | | return; |
| | | } |
| | | var payload = { |
| | | format: MAP_TRANSFER_FORMAT, |
| | | exportedAt: new Date().toISOString(), |
| | | source: { |
| | | lev: self.currentLev || (docs[0] && docs[0].lev) || 1, |
| | | editorMode: FREE_EDITOR_MODE |
| | | }, |
| | | docs: docs.map(function (doc) { |
| | | return self.exportDoc(doc); |
| | | }) |
| | | }; |
| | | var blob = new Blob([JSON.stringify(payload, null, 2)], { |
| | | type: 'application/json;charset=utf-8' |
| | | }); |
| | | var href = window.URL.createObjectURL(blob); |
| | | var link = document.createElement('a'); |
| | | link.href = href; |
| | | link.download = self.buildTransferFilename(payload.docs); |
| | | document.body.appendChild(link); |
| | | link.click(); |
| | | document.body.removeChild(link); |
| | | window.setTimeout(function () { |
| | | window.URL.revokeObjectURL(href); |
| | | }, 0); |
| | | self.showMessage('success', '已导出 ' + payload.docs.length + ' 个楼层的地图包'); |
| | | }) |
| | | .catch(function (error) { |
| | | self.showMessage('error', error && error.message ? error.message : '导出地图失败'); |
| | | }); |
| | | }, |
| | | triggerImportMap: function () { |
| | | if (this.$refs.mapImportInput) { |
| | | this.$refs.mapImportInput.value = ''; |
| | | this.$refs.mapImportInput.click(); |
| | | } |
| | | }, |
| | | parseTransferPackage: function (raw) { |
| | | if (!raw) { |
| | | return null; |
| | | } |
| | | if (raw.format === MAP_TRANSFER_FORMAT && Array.isArray(raw.docs) && raw.docs.length) { |
| | | return { |
| | | docs: raw.docs, |
| | | activeLev: toInt(raw.source && raw.source.lev, 0) |
| | | }; |
| | | } |
| | | if ( |
| | | (raw.format === 'bas-map-editor-transfer-v1' || raw.format === MAP_TRANSFER_FORMAT) && |
| | | raw.doc |
| | | ) { |
| | | return { |
| | | docs: [raw.doc], |
| | | activeLev: toInt(raw.source && raw.source.lev, 0) |
| | | }; |
| | | } |
| | | if (raw.editorMode === FREE_EDITOR_MODE && Array.isArray(raw.elements)) { |
| | | return { |
| | | docs: [raw], |
| | | activeLev: toInt(raw.lev, 0) |
| | | }; |
| | | } |
| | | return null; |
| | | }, |
| | | importMapPackage: function (payload, options) { |
| | | options = options || {}; |
| | | if (!payload || !Array.isArray(payload.docs) || !payload.docs.length) { |
| | | this.showMessage('error', '导入文件格式不正确'); |
| | | return; |
| | | } |
| | | if (this.isDirty && options.skipConfirm !== true) { |
| | | if (!window.confirm('导入地图会替换当前编辑态未保存内容,是否继续?')) { |
| | | return; |
| | | } |
| | | } |
| | | if (this.doc) { |
| | | this.cacheCurrentDraft(); |
| | | } |
| | | var self = this; |
| | | var normalizedDocs = payload.docs |
| | | .map(function (item) { |
| | | return self.normalizeDoc(item); |
| | | }) |
| | | .sort(function (a, b) { |
| | | return toInt(a.lev, 0) - toInt(b.lev, 0); |
| | | }); |
| | | normalizedDocs.forEach(function (doc) { |
| | | self.setDraftDocEntry(doc.lev, doc, ''); |
| | | }); |
| | | var activeLev = toInt(payload.activeLev, 0); |
| | | var targetDoc = normalizedDocs[0]; |
| | | for (var i = 0; i < normalizedDocs.length; i++) { |
| | | if (normalizedDocs[i].lev === activeLev) { |
| | | targetDoc = normalizedDocs[i]; |
| | | break; |
| | | } |
| | | } |
| | | this.refreshLevOptions(); |
| | | this.floorPickerLev = targetDoc.lev; |
| | | this.setCurrentDoc(targetDoc, { savedSnapshot: '' }); |
| | | if (normalizedDocs.length > 1) { |
| | | this.showMessage( |
| | | 'success', |
| | | '地图包已导入 ' + normalizedDocs.length + ' 个楼层,可点击“保存全部楼层”落库' |
| | | ); |
| | | return; |
| | | } |
| | | this.showMessage('success', '地图包已导入,保存后才会覆盖运行地图'); |
| | | }, |
| | | handleImportMap: function (event) { |
| | | var file = event && event.target && event.target.files ? event.target.files[0] : null; |
| | | if (!file) { |
| | | return; |
| | | } |
| | | var self = this; |
| | | var reader = new FileReader(); |
| | | reader.onload = function (loadEvent) { |
| | | try { |
| | | var text = loadEvent && loadEvent.target ? loadEvent.target.result : ''; |
| | | var raw = JSON.parse(text || '{}'); |
| | | var payload = self.parseTransferPackage(raw); |
| | | self.importMapPackage(payload); |
| | | } catch (e) { |
| | | self.showMessage('error', '地图文件解析失败'); |
| | | } |
| | | }; |
| | | reader.onerror = function () { |
| | | self.showMessage('error', '地图文件读取失败'); |
| | | }; |
| | | reader.readAsText(file, 'utf-8'); |
| | | }, |
| | | triggerImportExcel: function () { |
| | | if (this.$refs.importInput) { |
| | | this.$refs.importInput.value = ''; |
| | | this.$refs.importInput.click(); |
| | | } |
| | | }, |
| | | handleImportExcel: function (event) { |
| | | var self = this; |
| | | var file = event && event.target && event.target.files ? event.target.files[0] : null; |
| | | if (!file) { |
| | | return; |
| | | } |
| | | var formData = new FormData(); |
| | | formData.append('file', file); |
| | | $.ajax({ |
| | | url: baseUrl + '/basMap/editor/importExcel/auth', |
| | | method: 'POST', |
| | | headers: authHeaders(), |
| | | data: formData, |
| | | processData: false, |
| | | contentType: false, |
| | | success: function (res) { |
| | | if (!res || res.code !== 200 || !Array.isArray(res.data) || res.data.length === 0) { |
| | | self.showMessage('error', res && res.msg ? res.msg : 'Excel 导入失败'); |
| | | return; |
| | | } |
| | | res.data.forEach(function (item) { |
| | | var doc = self.normalizeDoc(item); |
| | | self.setDraftDocEntry(doc.lev, doc, ''); |
| | | }); |
| | | self.refreshLevOptions(); |
| | | self.floorPickerLev = toInt(res.data[0].lev, 0); |
| | | self.setCurrentDoc(res.data[0], { savedSnapshot: '' }); |
| | | self.showMessage('success', 'Excel 已导入到编辑器,保存后才会覆盖运行地图'); |
| | | }, |
| | | error: function () { |
| | | self.showMessage('error', 'Excel 导入失败'); |
| | | } |
| | | }); |
| | | }, |
| | | handleFloorChange: function (lev) { |
| | | lev = toInt(lev, 0); |
| | | if (lev <= 0) { |
| | | return; |
| | | } |
| | | this.floorPickerLev = lev; |
| | | if (this.doc && this.doc.lev === lev && !this.loadingFloor) { |
| | | this.switchingFloorLev = null; |
| | | return; |
| | | } |
| | | if (this.doc) { |
| | | this.cacheCurrentDraft(); |
| | | } |
| | | this.clearFloorTransientState(); |
| | | this.resetRenderLayers(); |
| | | this.switchingFloorLev = lev; |
| | | this.markGridSceneDirty(); |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | this.fetchFloor(lev); |
| | | }, |
| | | loadCurrentFloor: function () { |
| | | if (!this.currentLev) { |
| | | this.showMessage('warning', '请先选择楼层'); |
| | | return; |
| | | } |
| | | if ( |
| | | this.isDirty && |
| | | !window.confirm('重新读取会丢弃当前楼层未保存的自由画布编辑,是否继续?') |
| | | ) { |
| | | return; |
| | | } |
| | | this.removeDraftDocEntry(this.currentLev); |
| | | this.refreshLevOptions(); |
| | | this.fetchFloor(this.currentLev); |
| | | }, |
| | | fetchFloor: function (lev) { |
| | | var self = this; |
| | | lev = toInt(lev, 0); |
| | | if (lev <= 0) { |
| | | return; |
| | | } |
| | | var requestSeq = ++this.floorRequestSeq; |
| | | this.activeFloorRequestSeq = requestSeq; |
| | | this.loadingFloor = true; |
| | | this.switchingFloorLev = lev; |
| | | $.ajax({ |
| | | url: baseUrl + '/basMap/editor/' + lev + '/auth', |
| | | method: 'GET', |
| | | headers: authHeaders(), |
| | | success: function (res) { |
| | | if (requestSeq !== self.activeFloorRequestSeq) { |
| | | return; |
| | | } |
| | | self.loadingFloor = false; |
| | | if (!res || res.code !== 200 || !res.data) { |
| | | self.switchingFloorLev = null; |
| | | self.floorPickerLev = self.currentLev; |
| | | self.markGridSceneDirty(); |
| | | self.markStaticSceneDirty(); |
| | | self.scheduleRender(); |
| | | self.showMessage('error', res && res.msg ? res.msg : '加载地图失败'); |
| | | return; |
| | | } |
| | | var normalized = self.normalizeDoc(res.data); |
| | | self.setDraftDocEntry(normalized.lev, normalized, self.snapshotDoc(normalized)); |
| | | self.setCurrentDoc(normalized, { |
| | | savedSnapshot: self.snapshotDoc(normalized) |
| | | }); |
| | | }, |
| | | error: function () { |
| | | if (requestSeq !== self.activeFloorRequestSeq) { |
| | | return; |
| | | } |
| | | self.loadingFloor = false; |
| | | self.switchingFloorLev = null; |
| | | self.floorPickerLev = self.currentLev; |
| | | self.markGridSceneDirty(); |
| | | self.markStaticSceneDirty(); |
| | | self.scheduleRender(); |
| | | self.showMessage('error', '加载地图失败'); |
| | | } |
| | | }); |
| | | }, |
| | | validateDocBeforeSave: function (doc) { |
| | | var source = this.normalizeDoc(doc); |
| | | if (!source || !source.lev) { |
| | | return '楼层不能为空'; |
| | | } |
| | | if (toNumber(source.canvasWidth, 0) <= 0 || toNumber(source.canvasHeight, 0) <= 0) { |
| | | return '画布尺寸必须大于 0'; |
| | | } |
| | | var elements = source.elements || []; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | if (element.width <= 0 || element.height <= 0) { |
| | | return '存在尺寸无效的元素'; |
| | | } |
| | | if (element.x < 0 || element.y < 0) { |
| | | return '元素坐标不能小于 0'; |
| | | } |
| | | if (!isRectWithinCanvas(element, source.canvasWidth, source.canvasHeight)) { |
| | | return '存在超出画布边界的元素: ' + element.id; |
| | | } |
| | | if (element.type === 'devp') { |
| | | var value = safeParseJson(element.value); |
| | | if (!value || toInt(value.stationId, 0) <= 0 || toInt(value.deviceNo, 0) <= 0) { |
| | | return '输送线元素必须配置有效的 stationId 和 deviceNo'; |
| | | } |
| | | } |
| | | } |
| | | var overlapId = findDocOverlapId(source); |
| | | if (overlapId) { |
| | | return '存在重叠元素: ' + overlapId; |
| | | } |
| | | return ''; |
| | | }, |
| | | validateBeforeSave: function () { |
| | | return this.validateDocBeforeSave(this.doc); |
| | | }, |
| | | requestSaveDoc: function (doc) { |
| | | return new Promise(function (resolve, reject) { |
| | | $.ajax({ |
| | | url: baseUrl + '/basMap/editor/save/auth', |
| | | method: 'POST', |
| | | headers: $.extend( |
| | | { |
| | | 'Content-Type': 'application/json;charset=UTF-8' |
| | | }, |
| | | authHeaders() |
| | | ), |
| | | data: JSON.stringify(doc), |
| | | success: function (res) { |
| | | if (!res || res.code !== 200) { |
| | | reject(new Error(res && res.msg ? res.msg : '保存失败')); |
| | | return; |
| | | } |
| | | resolve(res); |
| | | }, |
| | | error: function () { |
| | | reject(new Error('保存失败')); |
| | | } |
| | | }); |
| | | }); |
| | | }, |
| | | collectDirtyDocsForSave: function () { |
| | | var result = []; |
| | | var seen = {}; |
| | | if (this.doc && this.doc.lev && this.isDirty) { |
| | | var currentDoc = this.exportDoc(this.doc); |
| | | result.push(currentDoc); |
| | | seen[currentDoc.lev] = true; |
| | | } |
| | | var self = this; |
| | | Object.keys(this.draftDocs || {}).forEach(function (key) { |
| | | var lev = toInt(key, 0); |
| | | if (lev <= 0 || seen[lev]) { |
| | | return; |
| | | } |
| | | var entry = self.draftDocs[lev]; |
| | | if (!entry || !entry.doc) { |
| | | return; |
| | | } |
| | | var snapshot = self.snapshotDoc(entry.doc); |
| | | if (snapshot === (entry.savedSnapshot || '')) { |
| | | return; |
| | | } |
| | | var doc = self.exportDoc(entry.doc); |
| | | result.push(doc); |
| | | seen[doc.lev] = true; |
| | | }); |
| | | result.sort(function (a, b) { |
| | | return toInt(a.lev, 0) - toInt(b.lev, 0); |
| | | }); |
| | | return result; |
| | | }, |
| | | markDocSavedState: function (doc) { |
| | | var normalized = this.normalizeDoc(doc); |
| | | var savedSnapshot = this.snapshotDoc(normalized); |
| | | this.setDraftDocEntry(normalized.lev, normalized, savedSnapshot); |
| | | if (this.doc && this.doc.lev === normalized.lev) { |
| | | this.savedSnapshot = savedSnapshot; |
| | | this.syncDirty(); |
| | | } |
| | | }, |
| | | saveDoc: function () { |
| | | var self = this; |
| | | if (!this.doc) { |
| | | return; |
| | | } |
| | | var error = this.validateBeforeSave(); |
| | | if (error) { |
| | | this.showMessage('warning', error); |
| | | return; |
| | | } |
| | | this.saving = true; |
| | | var payload = this.exportDoc(this.doc); |
| | | this.requestSaveDoc(payload) |
| | | .then(function () { |
| | | self.saving = false; |
| | | self.savedSnapshot = self.snapshotDoc(self.doc); |
| | | self.syncDirty(); |
| | | self.clearCurrentDraftIfSaved(); |
| | | self.refreshLevOptions(); |
| | | self.showMessage('success', '当前楼层已保存并编译到运行地图'); |
| | | }) |
| | | .catch(function (error) { |
| | | self.saving = false; |
| | | self.showMessage('error', error && error.message ? error.message : '保存失败'); |
| | | }); |
| | | }, |
| | | saveAllDocs: function () { |
| | | var self = this; |
| | | if (this.saving || this.savingAll) { |
| | | return; |
| | | } |
| | | var docs = this.collectDirtyDocsForSave(); |
| | | if (!docs.length) { |
| | | this.showMessage('warning', '当前没有需要保存的楼层'); |
| | | return; |
| | | } |
| | | if ( |
| | | docs.length > 1 && |
| | | !window.confirm('将保存 ' + docs.length + ' 个楼层到运行地图,是否继续?') |
| | | ) { |
| | | return; |
| | | } |
| | | for (var i = 0; i < docs.length; i++) { |
| | | var error = this.validateDocBeforeSave(docs[i]); |
| | | if (error) { |
| | | this.showMessage('warning', docs[i].lev + 'F 保存前校验失败: ' + error); |
| | | return; |
| | | } |
| | | } |
| | | this.savingAll = true; |
| | | var index = 0; |
| | | var total = docs.length; |
| | | var next = function () { |
| | | if (index >= total) { |
| | | self.savingAll = false; |
| | | self.refreshLevOptions(); |
| | | self.showMessage('success', '已保存 ' + total + ' 个楼层到运行地图'); |
| | | return; |
| | | } |
| | | var doc = docs[index++]; |
| | | self |
| | | .requestSaveDoc(doc) |
| | | .then(function () { |
| | | self.markDocSavedState(doc); |
| | | next(); |
| | | }) |
| | | .catch(function (error) { |
| | | self.savingAll = false; |
| | | self.showMessage( |
| | | 'error', |
| | | doc.lev + 'F 保存失败: ' + (error && error.message ? error.message : '保存失败') |
| | | ); |
| | | }); |
| | | }; |
| | | next(); |
| | | }, |
| | | setTool: function (tool) { |
| | | this.activeTool = tool; |
| | | this.updateCursor(); |
| | | }, |
| | | findElementById: function (id) { |
| | | if (!this.doc || !id) { |
| | | return null; |
| | | } |
| | | var elements = this.doc.elements || []; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | if (elements[i].id === id) { |
| | | return elements[i]; |
| | | } |
| | | } |
| | | return null; |
| | | }, |
| | | getSelectedElements: function () { |
| | | var self = this; |
| | | return this.selectedIds |
| | | .map(function (id) { |
| | | return self.findElementById(id); |
| | | }) |
| | | .filter(Boolean); |
| | | }, |
| | | refreshInspector: function () { |
| | | var element = this.singleSelectedElement; |
| | | if (!this.doc) { |
| | | this.canvasForm = { |
| | | width: String(DEFAULT_CANVAS_WIDTH), |
| | | height: String(DEFAULT_CANVAS_HEIGHT) |
| | | }; |
| | | this.valueEditorText = ''; |
| | | this.resetDevpForm(); |
| | | this.resetDeviceForm(); |
| | | return; |
| | | } |
| | | this.canvasForm = { |
| | | width: String(Math.round(this.doc.canvasWidth)), |
| | | height: String(Math.round(this.doc.canvasHeight)) |
| | | }; |
| | | if (!element) { |
| | | this.geometryForm = { x: '', y: '', width: '', height: '' }; |
| | | this.valueEditorText = ''; |
| | | this.resetDevpForm(); |
| | | this.resetDeviceForm(); |
| | | return; |
| | | } |
| | | this.geometryForm = { |
| | | x: String(this.formatNumber(element.x)), |
| | | y: String(this.formatNumber(element.y)), |
| | | width: String(this.formatNumber(element.width)), |
| | | height: String(this.formatNumber(element.height)) |
| | | }; |
| | | this.valueEditorText = element.value || ''; |
| | | if (element.type === 'devp') { |
| | | this.loadDevpForm(element.value); |
| | | } else { |
| | | this.resetDevpForm(); |
| | | } |
| | | if (isDeviceConfigType(element.type)) { |
| | | this.loadDeviceForm(element.type, element.value); |
| | | } else { |
| | | this.resetDeviceForm(); |
| | | } |
| | | this.ensureShelfFillStartValue(); |
| | | }, |
| | | resetDevpForm: function () { |
| | | this.devpForm = { |
| | | stationId: '', |
| | | deviceNo: '', |
| | | direction: [], |
| | | isBarcodeStation: false, |
| | | barcodeIdx: '', |
| | | backStation: '', |
| | | backStationDeviceNo: '', |
| | | isInStation: false, |
| | | barcodeStation: '', |
| | | barcodeStationDeviceNo: '', |
| | | isOutStation: false, |
| | | runBlockReassign: false, |
| | | isOutOrder: false, |
| | | isLiftTransfer: false |
| | | }; |
| | | }, |
| | | resetDeviceForm: function () { |
| | | this.deviceForm = { |
| | | trackId: '', |
| | | barCodeStart: 0, |
| | | barCodeEnd: 100000, |
| | | deviceList: [ |
| | | { |
| | | valueKey: '', |
| | | deviceNo: '', |
| | | progress: 0, |
| | | deviceLength: '', |
| | | deviceWidth: '' |
| | | } |
| | | ] |
| | | }; |
| | | }, |
| | | ensureShelfFillStartValue: function () { |
| | | var element = this.singleSelectedElement; |
| | | if (!element || !isShelfLikeNodeType(element.type)) { |
| | | return; |
| | | } |
| | | if ( |
| | | !this.shelfFillForm.startValue || |
| | | !parseShelfLocationValue(this.shelfFillForm.startValue) |
| | | ) { |
| | | this.shelfFillForm.startValue = normalizeValue(element.value || ''); |
| | | } |
| | | }, |
| | | loadDevpForm: function (value) { |
| | | this.resetDevpForm(); |
| | | var json = safeParseJson(value); |
| | | if (!json) { |
| | | return; |
| | | } |
| | | this.devpForm.stationId = json.stationId != null ? String(json.stationId) : ''; |
| | | this.devpForm.deviceNo = json.deviceNo != null ? String(json.deviceNo) : ''; |
| | | this.devpForm.direction = normalizeDirectionList(json.direction); |
| | | this.devpForm.isBarcodeStation = boolFlag(json.isBarcodeStation); |
| | | this.devpForm.barcodeIdx = json.barcodeIdx != null ? String(json.barcodeIdx) : ''; |
| | | this.devpForm.backStation = json.backStation != null ? String(json.backStation) : ''; |
| | | this.devpForm.backStationDeviceNo = |
| | | json.backStationDeviceNo != null ? String(json.backStationDeviceNo) : ''; |
| | | this.devpForm.isInStation = boolFlag(json.isInStation); |
| | | this.devpForm.barcodeStation = |
| | | json.barcodeStation != null ? String(json.barcodeStation) : ''; |
| | | this.devpForm.barcodeStationDeviceNo = |
| | | json.barcodeStationDeviceNo != null ? String(json.barcodeStationDeviceNo) : ''; |
| | | this.devpForm.isOutStation = boolFlag(json.isOutStation); |
| | | this.devpForm.runBlockReassign = boolFlag(json.runBlockReassign); |
| | | this.devpForm.isOutOrder = boolFlag(json.isOutOrder); |
| | | this.devpForm.isLiftTransfer = boolFlag(json.isLiftTransfer); |
| | | }, |
| | | getDeviceConfigLabel: function (type) { |
| | | var meta = getTypeMeta(type); |
| | | return meta.label + '参数'; |
| | | }, |
| | | getDeviceConfigKeyLabel: function (type, valueKey) { |
| | | if (valueKey === 'crnNo') { |
| | | return 'crnNo'; |
| | | } |
| | | if (valueKey === 'rgvNo') { |
| | | return 'rgvNo'; |
| | | } |
| | | return type === 'rgv' ? 'deviceNo / rgvNo' : 'deviceNo / crnNo'; |
| | | }, |
| | | getAutoTrackDeviceBox: function () { |
| | | var element = this.singleSelectedDeviceElement; |
| | | if (!element || !G || !G.getAutoTrackDeviceBox) { |
| | | return null; |
| | | } |
| | | return G.getAutoTrackDeviceBox(element); |
| | | }, |
| | | getDeviceLengthPlaceholder: function () { |
| | | var box = this.getAutoTrackDeviceBox(); |
| | | return box && box.along ? '默认: ' + box.along : ''; |
| | | }, |
| | | getDeviceWidthPlaceholder: function () { |
| | | var box = this.getAutoTrackDeviceBox(); |
| | | return box && box.across ? '默认: ' + box.across : ''; |
| | | }, |
| | | loadDeviceForm: function (type, value) { |
| | | this.resetDeviceForm(); |
| | | if (!isDeviceConfigType(type)) { |
| | | return; |
| | | } |
| | | var json = safeParseJson(value) || {}; |
| | | this.deviceForm = { |
| | | trackId: '', |
| | | barCodeStart: 0, |
| | | barCodeEnd: 100000, |
| | | deviceList: [ |
| | | { |
| | | valueKey: '', |
| | | deviceNo: '', |
| | | progress: 0, |
| | | deviceLength: '', |
| | | deviceWidth: '' |
| | | } |
| | | ], |
| | | ...json |
| | | }; |
| | | var trackId = toInt(this.deviceForm.trackId, 0); |
| | | if (trackId <= 0) { |
| | | this.deviceForm.trackId = String(this.getNextDeviceTrackId(this.singleSelectedElement)); |
| | | } else { |
| | | this.deviceForm.trackId = String(trackId); |
| | | } |
| | | this.deviceForm.barCodeStart = toInt(this.deviceForm.barCodeStart, 0); |
| | | this.deviceForm.barCodeEnd = toInt(this.deviceForm.barCodeEnd, 100000); |
| | | }, |
| | | getNextDeviceTrackId: function (excludeElement) { |
| | | if (!this.doc || !Array.isArray(this.doc.elements)) { |
| | | return 1; |
| | | } |
| | | var excludeId = excludeElement && excludeElement.id ? String(excludeElement.id) : ''; |
| | | var maxValue = 0; |
| | | for (var i = 0; i < this.doc.elements.length; i++) { |
| | | var item = this.doc.elements[i]; |
| | | if (!item || (excludeId && item.id === excludeId) || !isDeviceConfigType(item.type)) { |
| | | continue; |
| | | } |
| | | var json = safeParseJson(item.value); |
| | | if (!json) { |
| | | continue; |
| | | } |
| | | var trackId = toInt(json.trackId, 0); |
| | | if (trackId > maxValue) { |
| | | maxValue = trackId; |
| | | } |
| | | } |
| | | return Math.max(1, maxValue + 1); |
| | | }, |
| | | isDevpDirectionActive: function (directionKey) { |
| | | return this.devpForm.direction.indexOf(directionKey) >= 0; |
| | | }, |
| | | toggleDevpDirection: function (directionKey) { |
| | | if (!directionKey) { |
| | | return; |
| | | } |
| | | var next = this.devpForm.direction.slice(); |
| | | var index = next.indexOf(directionKey); |
| | | if (index >= 0) { |
| | | next.splice(index, 1); |
| | | } else { |
| | | next.push(directionKey); |
| | | } |
| | | this.devpForm.direction = DEVP_DIRECTION_OPTIONS.map(function (item) { |
| | | return item.key; |
| | | }).filter(function (item) { |
| | | return next.indexOf(item) >= 0; |
| | | }); |
| | | }, |
| | | applyCanvasSize: function () { |
| | | var self = this; |
| | | if (!this.doc) { |
| | | return; |
| | | } |
| | | var width = toNumber(this.canvasForm.width, 0); |
| | | var height = toNumber(this.canvasForm.height, 0); |
| | | if (width <= 0 || height <= 0) { |
| | | this.showMessage('warning', '画布尺寸必须大于 0'); |
| | | return; |
| | | } |
| | | var bounds = this.getElementBounds( |
| | | (this.doc.elements || []).map(function (item) { |
| | | return item.id; |
| | | }) |
| | | ); |
| | | if (bounds && (width < bounds.x + bounds.width || height < bounds.y + bounds.height)) { |
| | | this.showMessage('warning', '画布不能小于当前元素占用范围'); |
| | | return; |
| | | } |
| | | this.runMutation(function () { |
| | | self.doc.canvasWidth = roundCoord(width); |
| | | self.doc.canvasHeight = roundCoord(height); |
| | | }); |
| | | }, |
| | | applyGeometry: function () { |
| | | var self = this; |
| | | var element = this.singleSelectedElement; |
| | | if (!element) { |
| | | return; |
| | | } |
| | | var next = { |
| | | x: roundCoord(Math.max(0, toNumber(this.geometryForm.x, element.x))), |
| | | y: roundCoord(Math.max(0, toNumber(this.geometryForm.y, element.y))), |
| | | width: roundCoord( |
| | | Math.max(MIN_ELEMENT_SIZE, toNumber(this.geometryForm.width, element.width)) |
| | | ), |
| | | height: roundCoord( |
| | | Math.max(MIN_ELEMENT_SIZE, toNumber(this.geometryForm.height, element.height)) |
| | | ) |
| | | }; |
| | | if (!this.isWithinCanvas(next)) { |
| | | this.showMessage('warning', '几何属性超出当前画布范围'); |
| | | return; |
| | | } |
| | | var preview = deepClone(element); |
| | | preview.x = next.x; |
| | | preview.y = next.y; |
| | | preview.width = next.width; |
| | | preview.height = next.height; |
| | | if (this.hasOverlap(preview, [preview.id])) { |
| | | this.showMessage('warning', '调整后会与其他元素重叠'); |
| | | return; |
| | | } |
| | | this.runMutation(function () { |
| | | element.x = next.x; |
| | | element.y = next.y; |
| | | element.width = next.width; |
| | | element.height = next.height; |
| | | }); |
| | | }, |
| | | applyRawValue: function () { |
| | | var self = this; |
| | | var element = this.singleSelectedElement; |
| | | if (!element || element.type === 'devp') { |
| | | return; |
| | | } |
| | | this.runMutation(function () { |
| | | element.value = normalizeValue(self.valueEditorText); |
| | | }); |
| | | }, |
| | | addDeviceForm: function () { |
| | | this.deviceForm.deviceList.push({ |
| | | valueKey: '', |
| | | deviceNo: '', |
| | | progress: 0, |
| | | deviceLength: '', |
| | | deviceWidth: '' |
| | | }); |
| | | }, |
| | | applyDeviceForm: function () { |
| | | var self = this; |
| | | var element = this.singleSelectedDeviceElement; |
| | | if (!element) { |
| | | return; |
| | | } |
| | | var trackId = toInt(this.deviceForm.trackId, 0); |
| | | if (trackId <= 0) { |
| | | this.showMessage('warning', '轨道ID必须大于 0'); |
| | | return; |
| | | } |
| | | if ( |
| | | !this.deviceForm.deviceList || |
| | | this.deviceForm.deviceList.length === 0 || |
| | | this.deviceForm.deviceList.some((item) => item.deviceNo === '') |
| | | ) { |
| | | this.showMessage('warning', '设备列表不能为空'); |
| | | return; |
| | | } |
| | | var valueKey = pickDeviceValueKey(element.type); |
| | | this.runMutation(function () { |
| | | var payload = safeParseJson(element.value) || {}; |
| | | delete payload.deviceNo; |
| | | delete payload.crnNo; |
| | | delete payload.rgvNo; |
| | | self.deviceForm.deviceList.forEach((item) => { |
| | | item.valueKey = valueKey; |
| | | // 允许通过属性面板覆盖默认设备像素尺寸(沿轨道/垂直轨道) |
| | | var deviceLength = toInt(item.deviceLength, 0); |
| | | var deviceWidth = toInt(item.deviceWidth, 0); |
| | | if (deviceLength > 0) { |
| | | item.deviceLength = String(deviceLength); |
| | | } else { |
| | | delete item.deviceLength; |
| | | } |
| | | if (deviceWidth > 0) { |
| | | item.deviceWidth = String(deviceWidth); |
| | | } else { |
| | | delete item.deviceWidth; |
| | | } |
| | | }); |
| | | self.deviceForm.trackId = String(trackId); |
| | | self.deviceForm.barCodeStart = toInt(self.deviceForm.barCodeStart, 0); |
| | | self.deviceForm.barCodeEnd = toInt(self.deviceForm.barCodeEnd, 100000); |
| | | element.value = JSON.stringify(self.deviceForm); |
| | | self.valueEditorText = element.value; |
| | | }); |
| | | }, |
| | | applyDevpForm: function () { |
| | | var self = this; |
| | | var element = this.singleSelectedElement; |
| | | if (!element || element.type !== 'devp') { |
| | | return; |
| | | } |
| | | var stationId = toInt(this.devpForm.stationId, 0); |
| | | var deviceNo = toInt(this.devpForm.deviceNo, 0); |
| | | if (stationId <= 0 || deviceNo <= 0) { |
| | | this.showMessage('warning', '站号和 PLC 编号必须大于 0'); |
| | | return; |
| | | } |
| | | var payload = { |
| | | stationId: stationId, |
| | | deviceNo: deviceNo |
| | | }; |
| | | var directionList = normalizeDirectionList(this.devpForm.direction); |
| | | if (directionList.length > 0) { |
| | | payload.direction = directionList; |
| | | } |
| | | var barcodeIdx = this.devpForm.barcodeIdx === '' ? 0 : toInt(this.devpForm.barcodeIdx, 0); |
| | | var backStation = |
| | | this.devpForm.backStation === '' ? 0 : toInt(this.devpForm.backStation, 0); |
| | | var backStationDeviceNo = |
| | | this.devpForm.backStationDeviceNo === '' |
| | | ? 0 |
| | | : toInt(this.devpForm.backStationDeviceNo, 0); |
| | | var barcodeStation = |
| | | this.devpForm.barcodeStation === '' ? 0 : toInt(this.devpForm.barcodeStation, 0); |
| | | var barcodeStationDeviceNo = |
| | | this.devpForm.barcodeStationDeviceNo === '' |
| | | ? 0 |
| | | : toInt(this.devpForm.barcodeStationDeviceNo, 0); |
| | | if (this.devpForm.isInStation && (barcodeStation <= 0 || barcodeStationDeviceNo <= 0)) { |
| | | this.showMessage('warning', '入站点必须填写条码站和条码站 PLC 编号'); |
| | | return; |
| | | } |
| | | if ( |
| | | this.devpForm.isBarcodeStation && |
| | | (backStation <= 0 || backStationDeviceNo <= 0 || barcodeIdx <= 0) |
| | | ) { |
| | | this.showMessage('warning', '条码站必须填写条码索引、退回站和退回站 PLC 编号'); |
| | | return; |
| | | } |
| | | if (this.devpForm.isBarcodeStation) { |
| | | payload.isBarcodeStation = 1; |
| | | } |
| | | if (barcodeIdx > 0) { |
| | | payload.barcodeIdx = barcodeIdx; |
| | | } |
| | | if (backStation > 0) { |
| | | payload.backStation = backStation; |
| | | } |
| | | if (backStationDeviceNo > 0) { |
| | | payload.backStationDeviceNo = backStationDeviceNo; |
| | | } |
| | | if (this.devpForm.isInStation) { |
| | | payload.isInStation = 1; |
| | | } |
| | | if (barcodeStation > 0) { |
| | | payload.barcodeStation = barcodeStation; |
| | | } |
| | | if (barcodeStationDeviceNo > 0) { |
| | | payload.barcodeStationDeviceNo = barcodeStationDeviceNo; |
| | | } |
| | | if (this.devpForm.isOutStation) { |
| | | payload.isOutStation = 1; |
| | | } |
| | | if (this.devpForm.runBlockReassign) { |
| | | payload.runBlockReassign = 1; |
| | | } |
| | | if (this.devpForm.isOutOrder) { |
| | | payload.isOutOrder = 1; |
| | | } |
| | | if (this.devpForm.isLiftTransfer) { |
| | | payload.isLiftTransfer = 1; |
| | | } |
| | | this.runMutation(function () { |
| | | element.value = JSON.stringify(payload); |
| | | self.valueEditorText = element.value; |
| | | }); |
| | | }, |
| | | deleteSelection: function () { |
| | | var self = this; |
| | | if (!this.doc || this.selectedIds.length === 0) { |
| | | return; |
| | | } |
| | | var ids = this.selectedIds.slice(); |
| | | this.runMutation(function () { |
| | | self.doc.elements = self.doc.elements.filter(function (item) { |
| | | return ids.indexOf(item.id) === -1; |
| | | }); |
| | | self.selectedIds = []; |
| | | }); |
| | | }, |
| | | copySelection: function () { |
| | | var elements = this.getSelectedElements(); |
| | | if (!elements.length) { |
| | | return; |
| | | } |
| | | this.clipboard = deepClone(elements); |
| | | this.showMessage('success', '已复制 ' + elements.length + ' 个元素'); |
| | | }, |
| | | getElementListBounds: function (elements) { |
| | | if (!elements || !elements.length) { |
| | | return null; |
| | | } |
| | | var minX = elements[0].x; |
| | | var minY = elements[0].y; |
| | | var maxX = elements[0].x + elements[0].width; |
| | | var maxY = elements[0].y + elements[0].height; |
| | | for (var i = 1; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | minX = Math.min(minX, element.x); |
| | | minY = Math.min(minY, element.y); |
| | | maxX = Math.max(maxX, element.x + element.width); |
| | | maxY = Math.max(maxY, element.y + element.height); |
| | | } |
| | | return { |
| | | x: minX, |
| | | y: minY, |
| | | width: maxX - minX, |
| | | height: maxY - minY |
| | | }; |
| | | }, |
| | | getPasteTargetWorld: function () { |
| | | if (!this.doc) { |
| | | return { x: 0, y: 0 }; |
| | | } |
| | | var visible = this.getVisibleCanvasRect |
| | | ? this.getVisibleCanvasRect() |
| | | : this.getVisibleWorldRect(); |
| | | var fallback = { |
| | | x: visible.x + visible.width / 2, |
| | | y: visible.y + visible.height / 2 |
| | | }; |
| | | if (!this.lastPointerWorld) { |
| | | return fallback; |
| | | } |
| | | return { |
| | | x: clamp(this.lastPointerWorld.x, 0, this.doc.canvasWidth), |
| | | y: clamp(this.lastPointerWorld.y, 0, this.doc.canvasHeight), |
| | | screenX: this.lastPointerWorld.screenX, |
| | | screenY: this.lastPointerWorld.screenY |
| | | }; |
| | | }, |
| | | pasteClipboard: function () { |
| | | var self = this; |
| | | if (!this.doc || !this.clipboard.length) { |
| | | return; |
| | | } |
| | | var sourceBounds = this.getElementListBounds(this.clipboard); |
| | | if (!sourceBounds) { |
| | | return; |
| | | } |
| | | var target = this.getPasteTargetWorld(); |
| | | var offsetX = target.x - (sourceBounds.x + sourceBounds.width / 2); |
| | | var offsetY = target.y - (sourceBounds.y + sourceBounds.height / 2); |
| | | var minOffsetX = -sourceBounds.x; |
| | | var maxOffsetX = this.doc.canvasWidth - (sourceBounds.x + sourceBounds.width); |
| | | var minOffsetY = -sourceBounds.y; |
| | | var maxOffsetY = this.doc.canvasHeight - (sourceBounds.y + sourceBounds.height); |
| | | offsetX = clamp(offsetX, minOffsetX, maxOffsetX); |
| | | offsetY = clamp(offsetY, minOffsetY, maxOffsetY); |
| | | var copies = deepClone(this.clipboard).map(function (item) { |
| | | item.id = nextId(); |
| | | item.x = roundCoord(item.x + offsetX); |
| | | item.y = roundCoord(item.y + offsetY); |
| | | return item; |
| | | }); |
| | | if (!this.canPlaceElements(copies, [])) { |
| | | this.showMessage('warning', '粘贴后的元素与现有元素重叠或超出画布'); |
| | | return; |
| | | } |
| | | this.runMutation(function () { |
| | | self.doc.elements = self.doc.elements.concat(copies); |
| | | self.selectedIds = copies.map(function (item) { |
| | | return item.id; |
| | | }); |
| | | }); |
| | | }, |
| | | canArrayFromElement: function (element) { |
| | | return !!(element && ARRAY_TEMPLATE_TYPES.indexOf(element.type) >= 0); |
| | | }, |
| | | getShelfFillSteps: function () { |
| | | return { |
| | | row: this.shelfFillForm.rowStep === 'asc' ? 1 : -1, |
| | | col: this.shelfFillForm.colStep === 'desc' ? -1 : 1 |
| | | }; |
| | | }, |
| | | applyShelfSequenceToArrayCopies: function (template, copies) { |
| | | if (!template || !isShelfLikeNodeType(template.type) || !copies || !copies.length) { |
| | | return copies; |
| | | } |
| | | var base = |
| | | parseShelfLocationValue(template.value) || |
| | | parseShelfLocationValue(this.shelfFillForm.startValue); |
| | | if (!base) { |
| | | return copies; |
| | | } |
| | | var steps = this.getShelfFillSteps(); |
| | | var horizontal = Math.abs(copies[0].x - template.x) >= Math.abs(copies[0].y - template.y); |
| | | var direction = 1; |
| | | if (horizontal) { |
| | | direction = copies[0].x >= template.x ? 1 : -1; |
| | | } else { |
| | | direction = copies[0].y >= template.y ? 1 : -1; |
| | | } |
| | | for (var i = 0; i < copies.length; i++) { |
| | | var offset = i + 1; |
| | | var row = base.row; |
| | | var col = base.col; |
| | | if (horizontal) { |
| | | col = base.col + steps.col * direction * offset; |
| | | } else { |
| | | row = base.row + steps.row * direction * offset; |
| | | } |
| | | copies[i].value = formatShelfLocationValue(row, col); |
| | | } |
| | | return copies; |
| | | }, |
| | | buildShelfGridAssignments: function (elements) { |
| | | if (!elements || !elements.length) { |
| | | return null; |
| | | } |
| | | var clusterAxis = function (list, axis, sizeKey) { |
| | | var sorted = list |
| | | .map(function (item) { |
| | | return { |
| | | id: item.id, |
| | | center: item[axis] + item[sizeKey] / 2, |
| | | size: item[sizeKey] |
| | | }; |
| | | }) |
| | | .sort(function (a, b) { |
| | | return a.center - b.center; |
| | | }); |
| | | var avgSize = |
| | | sorted.reduce(function (sum, item) { |
| | | return sum + item.size; |
| | | }, 0) / sorted.length; |
| | | var tolerance = Math.max(6, avgSize * 0.45); |
| | | var groups = []; |
| | | for (var i = 0; i < sorted.length; i++) { |
| | | var current = sorted[i]; |
| | | var last = groups.length ? groups[groups.length - 1] : null; |
| | | if (!last || Math.abs(current.center - last.center) > tolerance) { |
| | | groups.push({ |
| | | center: current.center, |
| | | items: [current] |
| | | }); |
| | | } else { |
| | | last.items.push(current); |
| | | last.center = |
| | | last.items.reduce(function (sum, item) { |
| | | return sum + item.center; |
| | | }, 0) / last.items.length; |
| | | } |
| | | } |
| | | var indexById = {}; |
| | | for (var groupIndex = 0; groupIndex < groups.length; groupIndex++) { |
| | | for (var itemIndex = 0; itemIndex < groups[groupIndex].items.length; itemIndex++) { |
| | | indexById[groups[groupIndex].items[itemIndex].id] = groupIndex; |
| | | } |
| | | } |
| | | return indexById; |
| | | }; |
| | | return { |
| | | rowById: clusterAxis(elements, 'y', 'height'), |
| | | colById: clusterAxis(elements, 'x', 'width') |
| | | }; |
| | | }, |
| | | applyShelfAutoFill: function () { |
| | | var self = this; |
| | | var shelves = this.selectedShelfElements.slice(); |
| | | if (!shelves.length) { |
| | | this.showMessage('warning', '请先选中至少一个货架或维修站台'); |
| | | return; |
| | | } |
| | | var start = parseShelfLocationValue(this.shelfFillForm.startValue); |
| | | if (!start) { |
| | | this.showMessage('warning', '起始值格式必须是 排-列,例如 12-1'); |
| | | return; |
| | | } |
| | | var grid = this.buildShelfGridAssignments(shelves); |
| | | if (!grid) { |
| | | return; |
| | | } |
| | | var steps = this.getShelfFillSteps(); |
| | | this.runMutation(function () { |
| | | shelves.forEach(function (item) { |
| | | var rowIndex = grid.rowById[item.id] || 0; |
| | | var colIndex = grid.colById[item.id] || 0; |
| | | item.value = formatShelfLocationValue( |
| | | start.row + rowIndex * steps.row, |
| | | start.col + colIndex * steps.col |
| | | ); |
| | | }); |
| | | if (self.singleSelectedElement && isShelfLikeNodeType(self.singleSelectedElement.type)) { |
| | | self.valueEditorText = self.singleSelectedElement.value || ''; |
| | | } |
| | | }); |
| | | }, |
| | | buildArrayCopies: function (template, startWorld, currentWorld) { |
| | | if ( |
| | | !this.doc || |
| | | !template || |
| | | !startWorld || |
| | | !currentWorld || |
| | | !this.canArrayFromElement(template) |
| | | ) { |
| | | return []; |
| | | } |
| | | var deltaX = currentWorld.x - startWorld.x; |
| | | var deltaY = currentWorld.y - startWorld.y; |
| | | if (Math.abs(deltaX) < COORD_EPSILON && Math.abs(deltaY) < COORD_EPSILON) { |
| | | return []; |
| | | } |
| | | var horizontal = Math.abs(deltaX) >= Math.abs(deltaY); |
| | | var step = horizontal ? template.width : template.height; |
| | | if (step <= COORD_EPSILON) { |
| | | return []; |
| | | } |
| | | var direction = (horizontal ? deltaX : deltaY) >= 0 ? 1 : -1; |
| | | var distance; |
| | | if (horizontal) { |
| | | distance = |
| | | direction > 0 |
| | | ? currentWorld.x - (template.x + template.width) |
| | | : template.x - currentWorld.x; |
| | | } else { |
| | | distance = |
| | | direction > 0 |
| | | ? currentWorld.y - (template.y + template.height) |
| | | : template.y - currentWorld.y; |
| | | } |
| | | var count = Math.max(0, Math.floor((distance + step * 0.5) / step)); |
| | | if (count <= 0) { |
| | | return []; |
| | | } |
| | | var copies = []; |
| | | for (var i = 1; i <= count; i++) { |
| | | copies.push({ |
| | | type: template.type, |
| | | x: roundCoord(template.x + (horizontal ? direction * template.width * i : 0)), |
| | | y: roundCoord(template.y + (horizontal ? 0 : direction * template.height * i)), |
| | | width: template.width, |
| | | height: template.height, |
| | | value: template.value |
| | | }); |
| | | } |
| | | return this.applyShelfSequenceToArrayCopies(template, copies); |
| | | }, |
| | | duplicateSelection: function () { |
| | | this.copySelection(); |
| | | this.pasteClipboard(); |
| | | }, |
| | | getElementBounds: function (ids) { |
| | | if (!this.doc) { |
| | | return null; |
| | | } |
| | | var elements = ids && ids.length ? this.getSelectedElements() : this.doc.elements || []; |
| | | if (ids && ids.length) { |
| | | elements = ids |
| | | .map(function (id) { |
| | | return this.findElementById(id); |
| | | }, this) |
| | | .filter(Boolean); |
| | | } |
| | | if (!elements.length) { |
| | | return null; |
| | | } |
| | | var minX = elements[0].x; |
| | | var minY = elements[0].y; |
| | | var maxX = elements[0].x + elements[0].width; |
| | | var maxY = elements[0].y + elements[0].height; |
| | | for (var i = 1; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | minX = Math.min(minX, element.x); |
| | | minY = Math.min(minY, element.y); |
| | | maxX = Math.max(maxX, element.x + element.width); |
| | | maxY = Math.max(maxY, element.y + element.height); |
| | | } |
| | | return { |
| | | x: minX, |
| | | y: minY, |
| | | width: maxX - minX, |
| | | height: maxY - minY |
| | | }; |
| | | }, |
| | | fitContent: function () { |
| | | if (!this.doc || !this.pixiApp) { |
| | | return; |
| | | } |
| | | var contentBounds = this.getElementBounds(); |
| | | if (contentBounds && contentBounds.width > 0 && contentBounds.height > 0) { |
| | | this.fitRect(contentBounds, this.pixiApp.renderer.width, this.pixiApp.renderer.height); |
| | | return; |
| | | } |
| | | this.fitCanvas(); |
| | | }, |
| | | fitCanvas: function () { |
| | | if (!this.doc || !this.pixiApp) { |
| | | return; |
| | | } |
| | | var renderer = this.pixiApp.renderer; |
| | | var target = { |
| | | x: 0, |
| | | y: 0, |
| | | width: Math.max(1, this.doc.canvasWidth), |
| | | height: Math.max(1, this.doc.canvasHeight) |
| | | }; |
| | | this.fitRect(target, renderer.width, renderer.height); |
| | | }, |
| | | fitSelection: function () { |
| | | if (!this.selectedIds.length || !this.pixiApp) { |
| | | return; |
| | | } |
| | | var bounds = this.getElementBounds(this.selectedIds); |
| | | if (!bounds) { |
| | | return; |
| | | } |
| | | this.fitRect(bounds, this.pixiApp.renderer.width, this.pixiApp.renderer.height); |
| | | }, |
| | | fitRect: function (rect, viewportWidth, viewportHeight) { |
| | | var padding = 80; |
| | | var scale = Math.min( |
| | | (viewportWidth - padding * 2) / Math.max(rect.width, 1), |
| | | (viewportHeight - padding * 2) / Math.max(rect.height, 1) |
| | | ); |
| | | scale = clamp(scale, 0.06, 4); |
| | | this.camera.scale = scale; |
| | | this.camera.x = Math.round((viewportWidth - rect.width * scale) / 2 - rect.x * scale); |
| | | this.camera.y = Math.round((viewportHeight - rect.height * scale) / 2 - rect.y * scale); |
| | | this.viewZoom = scale; |
| | | this.markGridSceneDirty(); |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | }, |
| | | resetView: function () { |
| | | this.fitCanvas(); |
| | | }, |
| | | getVisibleWorldRect: function () { |
| | | if (!this.pixiApp) { |
| | | return { |
| | | x: 0, |
| | | y: 0, |
| | | width: 0, |
| | | height: 0 |
| | | }; |
| | | } |
| | | return { |
| | | x: -this.camera.x / this.camera.scale, |
| | | y: -this.camera.y / this.camera.scale, |
| | | width: this.pixiApp.renderer.width / this.camera.scale, |
| | | height: this.pixiApp.renderer.height / this.camera.scale |
| | | }; |
| | | }, |
| | | getVisibleCanvasRect: function () { |
| | | if (!this.doc) { |
| | | return { |
| | | x: 0, |
| | | y: 0, |
| | | width: 0, |
| | | height: 0 |
| | | }; |
| | | } |
| | | var visible = this.getVisibleWorldRect(); |
| | | var left = clamp(visible.x, 0, this.doc.canvasWidth); |
| | | var top = clamp(visible.y, 0, this.doc.canvasHeight); |
| | | var right = clamp(visible.x + visible.width, 0, this.doc.canvasWidth); |
| | | var bottom = clamp(visible.y + visible.height, 0, this.doc.canvasHeight); |
| | | return { |
| | | x: left, |
| | | y: top, |
| | | width: Math.max(0, right - left), |
| | | height: Math.max(0, bottom - top) |
| | | }; |
| | | }, |
| | | getWorldRectWithPadding: function (screenPadding) { |
| | | if (!this.doc) { |
| | | return { |
| | | x: 0, |
| | | y: 0, |
| | | width: 0, |
| | | height: 0 |
| | | }; |
| | | } |
| | | var visible = this.getVisibleWorldRect(); |
| | | var padding = Math.max(screenPadding / this.camera.scale, 24); |
| | | var left = Math.max(0, visible.x - padding); |
| | | var top = Math.max(0, visible.y - padding); |
| | | var right = Math.min(this.doc.canvasWidth, visible.x + visible.width + padding); |
| | | var bottom = Math.min(this.doc.canvasHeight, visible.y + visible.height + padding); |
| | | return { |
| | | x: left, |
| | | y: top, |
| | | width: Math.max(0, right - left), |
| | | height: Math.max(0, bottom - top) |
| | | }; |
| | | }, |
| | | worldRectContains: function (outer, inner) { |
| | | if (!outer || !inner) { |
| | | return false; |
| | | } |
| | | return ( |
| | | inner.x >= outer.x - COORD_EPSILON && |
| | | inner.y >= outer.y - COORD_EPSILON && |
| | | inner.x + inner.width <= outer.x + outer.width + COORD_EPSILON && |
| | | inner.y + inner.height <= outer.y + outer.height + COORD_EPSILON |
| | | ); |
| | | }, |
| | | getGridRenderKey: function () { |
| | | var minorStep = this.camera.scale > 1.5 ? 50 : this.camera.scale > 0.45 ? 100 : 200; |
| | | return minorStep + '|' + Math.round(this.camera.scale * 8) / 8; |
| | | }, |
| | | getStaticRenderKey: function () { |
| | | return ( |
| | | (this.camera.scale >= 0.85 ? 'round' : 'flat') + |
| | | '|' + |
| | | Math.round(this.camera.scale * 8) / 8 |
| | | ); |
| | | }, |
| | | scheduleRender: function () { |
| | | if (this.renderQueued) { |
| | | return; |
| | | } |
| | | this.renderQueued = true; |
| | | window.requestAnimationFrame( |
| | | function () { |
| | | this.renderQueued = false; |
| | | this.renderScene(); |
| | | }.bind(this) |
| | | ); |
| | | }, |
| | | renderScene: function () { |
| | | if (!this.pixiApp || !this.doc) { |
| | | return; |
| | | } |
| | | this.mapRoot.position.set(this.camera.x, this.camera.y); |
| | | this.mapRoot.scale.set(this.camera.scale, this.camera.scale); |
| | | this.viewZoom = this.camera.scale; |
| | | var visible = this.getVisibleCanvasRect(); |
| | | var viewportSettled = |
| | | !this.isZooming && |
| | | !this.isPanning && |
| | | !(this.interactionState && this.interactionState.type === 'pan'); |
| | | var gridKeyChanged = this.gridRenderKey !== this.getGridRenderKey(); |
| | | if ( |
| | | this.gridSceneDirty || |
| | | !this.gridRenderRect || |
| | | (viewportSettled && gridKeyChanged) || |
| | | (viewportSettled && !this.worldRectContains(this.gridRenderRect, visible)) |
| | | ) { |
| | | this.renderGrid(this.getWorldRectWithPadding(STATIC_VIEW_PADDING)); |
| | | this.gridSceneDirty = false; |
| | | } |
| | | var excludedKey = this.selectionKey(this.getStaticExcludedIds()); |
| | | var staticKeyChanged = this.staticRenderKey !== this.getStaticRenderKey(); |
| | | if ( |
| | | this.staticSceneDirty || |
| | | !this.staticRenderRect || |
| | | (viewportSettled && staticKeyChanged) || |
| | | this.staticExcludedKey !== excludedKey || |
| | | (viewportSettled && !this.worldRectContains(this.staticRenderRect, visible)) |
| | | ) { |
| | | this.renderStaticElements(this.getWorldRectWithPadding(STATIC_VIEW_PADDING), excludedKey); |
| | | this.staticSceneDirty = false; |
| | | } |
| | | this.renderActiveElements(); |
| | | this.renderLabels(); |
| | | this.renderHover(); |
| | | this.renderSelection(); |
| | | this.renderGuide(); |
| | | this.updateCursor(); |
| | | }, |
| | | getStaticExcludedIds: function () { |
| | | if (!this.interactionState) { |
| | | return []; |
| | | } |
| | | if (this.interactionState.type === 'move' && this.selectedIds.length) { |
| | | return this.selectedIds.slice(); |
| | | } |
| | | if (this.interactionState.type === 'resize' && this.interactionState.elementId) { |
| | | return [this.interactionState.elementId]; |
| | | } |
| | | return []; |
| | | }, |
| | | getRenderableElements: function (excludeIds, renderRect) { |
| | | if (!this.doc) { |
| | | return []; |
| | | } |
| | | var rect = renderRect || this.getWorldRectWithPadding(STATIC_VIEW_PADDING); |
| | | var candidates = this.querySpatialCandidates(rect, 0, excludeIds); |
| | | var result = []; |
| | | for (var i = 0; i < candidates.length; i++) { |
| | | if (rectIntersects(rect, candidates[i])) { |
| | | result.push(candidates[i]); |
| | | } |
| | | } |
| | | return result; |
| | | }, |
| | | renderGrid: function (renderRect) { |
| | | if (!this.gridLayer || !this.doc) { |
| | | return; |
| | | } |
| | | var visible = renderRect || this.getVisibleWorldRect(); |
| | | var width = this.doc.canvasWidth; |
| | | var height = this.doc.canvasHeight; |
| | | var minorStep = this.camera.scale > 1.5 ? 50 : this.camera.scale > 0.45 ? 100 : 200; |
| | | var majorStep = minorStep * 5; |
| | | var lineWidth = 1 / this.camera.scale; |
| | | var xStart = Math.max(0, Math.floor(visible.x / minorStep) * minorStep); |
| | | var yStart = Math.max(0, Math.floor(visible.y / minorStep) * minorStep); |
| | | var xEnd = Math.min(width, visible.x + visible.width); |
| | | var yEnd = Math.min(height, visible.y + visible.height); |
| | | |
| | | this.gridLayer.clear(); |
| | | this.gridLayer.beginFill(0xfafcff, 1); |
| | | this.gridLayer.drawRect(0, 0, width, height); |
| | | this.gridLayer.endFill(); |
| | | |
| | | this.gridLayer.lineStyle(lineWidth, 0xdbe4ee, 1); |
| | | this.gridLayer.drawRect(0, 0, width, height); |
| | | |
| | | for (var x = xStart; x <= xEnd; x += minorStep) { |
| | | var colorX = x % majorStep === 0 ? 0xc9d7e6 : 0xe4ebf3; |
| | | this.gridLayer.lineStyle(lineWidth, colorX, x % majorStep === 0 ? 0.95 : 0.75); |
| | | this.gridLayer.moveTo(x, 0); |
| | | this.gridLayer.lineTo(x, height); |
| | | } |
| | | for (var y = yStart; y <= yEnd; y += minorStep) { |
| | | var colorY = y % majorStep === 0 ? 0xc9d7e6 : 0xe4ebf3; |
| | | this.gridLayer.lineStyle(lineWidth, colorY, y % majorStep === 0 ? 0.95 : 0.75); |
| | | this.gridLayer.moveTo(0, y); |
| | | this.gridLayer.lineTo(width, y); |
| | | } |
| | | this.gridRenderRect = { |
| | | x: visible.x, |
| | | y: visible.y, |
| | | width: visible.width, |
| | | height: visible.height |
| | | }; |
| | | this.gridRenderKey = this.getGridRenderKey(); |
| | | }, |
| | | drawGridPatch: function (rects, layer) { |
| | | if (!this.doc || !layer || !rects || !rects.length) { |
| | | return; |
| | | } |
| | | var width = this.doc.canvasWidth; |
| | | var height = this.doc.canvasHeight; |
| | | var minorStep = this.camera.scale > 1.5 ? 50 : this.camera.scale > 0.45 ? 100 : 200; |
| | | var majorStep = minorStep * 5; |
| | | var lineWidth = 1 / this.camera.scale; |
| | | for (var i = 0; i < rects.length; i++) { |
| | | var rect = rects[i]; |
| | | var left = clamp(rect.x - lineWidth, 0, width); |
| | | var top = clamp(rect.y - lineWidth, 0, height); |
| | | var right = clamp(rect.x + rect.width + lineWidth, 0, width); |
| | | var bottom = clamp(rect.y + rect.height + lineWidth, 0, height); |
| | | if (right <= left || bottom <= top) { |
| | | continue; |
| | | } |
| | | layer.lineStyle(0, 0, 0, 0); |
| | | layer.beginFill(0xfafcff, 1); |
| | | layer.drawRect(left, top, right - left, bottom - top); |
| | | layer.endFill(); |
| | | if (right - left < minorStep || bottom - top < minorStep) { |
| | | continue; |
| | | } |
| | | var xStart = Math.floor(left / minorStep) * minorStep; |
| | | var yStart = Math.floor(top / minorStep) * minorStep; |
| | | for (var x = xStart; x <= right; x += minorStep) { |
| | | if (x < left || x > right) { |
| | | continue; |
| | | } |
| | | var colorX = x % majorStep === 0 ? 0xc9d7e6 : 0xe4ebf3; |
| | | layer.lineStyle(lineWidth, colorX, x % majorStep === 0 ? 0.95 : 0.75); |
| | | layer.moveTo(x, top); |
| | | layer.lineTo(x, bottom); |
| | | } |
| | | for (var y = yStart; y <= bottom; y += minorStep) { |
| | | if (y < top || y > bottom) { |
| | | continue; |
| | | } |
| | | var colorY = y % majorStep === 0 ? 0xc9d7e6 : 0xe4ebf3; |
| | | layer.lineStyle(lineWidth, colorY, y % majorStep === 0 ? 0.95 : 0.75); |
| | | layer.moveTo(left, y); |
| | | layer.lineTo(right, y); |
| | | } |
| | | } |
| | | }, |
| | | drawPatchObjects: function (rects, excludeIds) { |
| | | if (!rects || !rects.length || !this.patchObjectLayer) { |
| | | return; |
| | | } |
| | | var seen = {}; |
| | | var elements = []; |
| | | for (var i = 0; i < rects.length; i++) { |
| | | var candidates = this.querySpatialCandidates(rects[i], 0, excludeIds); |
| | | for (var j = 0; j < candidates.length; j++) { |
| | | var item = candidates[j]; |
| | | if (!seen[item.id] && rectIntersects(rects[i], item)) { |
| | | seen[item.id] = true; |
| | | elements.push(item); |
| | | } |
| | | } |
| | | } |
| | | if (!elements.length) { |
| | | return; |
| | | } |
| | | this.drawElementsToLayers(elements, this.patchObjectLayer, this.patchObjectLayer); |
| | | }, |
| | | drawElementsToLayers: function (elements, trackLayer, nodeLayer) { |
| | | console.log('drawElementsToLayers'); |
| | | var lineWidth = 2 / this.camera.scale; |
| | | var buckets = {}; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | var bucketKey = |
| | | (isShelfLikeNodeType(element.type) ? 'node' : 'track') + ':' + element.type; |
| | | if (!buckets[bucketKey]) { |
| | | buckets[bucketKey] = []; |
| | | } |
| | | buckets[bucketKey].push(element); |
| | | } |
| | | for (var bucketKey in buckets) { |
| | | if (!buckets.hasOwnProperty(bucketKey)) { |
| | | continue; |
| | | } |
| | | var parts = bucketKey.split(':'); |
| | | var type = parts[1]; |
| | | var meta = getTypeMeta(type); |
| | | var layer = parts[0] === 'node' ? nodeLayer : trackLayer; |
| | | var bucket = buckets[bucketKey]; |
| | | for (var j = 0; j < bucket.length; j++) { |
| | | var item = bucket[j]; |
| | | drawElementByType.call(this, layer, item, type, { |
| | | line: { |
| | | width: lineWidth, |
| | | color: meta.border, |
| | | alpha: 0.95 |
| | | }, |
| | | fill: { |
| | | color: meta.fill, |
| | | alpha: meta.alpha ?? 0.92 |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | }, |
| | | ensureStaticSprite: function (poolName, index) { |
| | | var pool = poolName === 'node' ? this.staticNodeSpritePool : this.staticTrackSpritePool; |
| | | var layer = poolName === 'node' ? this.staticNodeSpriteLayer : this.staticTrackSpriteLayer; |
| | | if (pool[index]) { |
| | | return pool[index]; |
| | | } |
| | | var sprite = new PIXI.Sprite(PIXI.Texture.WHITE); |
| | | sprite.position.set(0, 0); |
| | | sprite.anchor.set(0, 0); |
| | | sprite.visible = false; |
| | | sprite.alpha = 0; |
| | | layer.addChild(sprite); |
| | | pool[index] = sprite; |
| | | return sprite; |
| | | }, |
| | | hideUnusedStaticSprites: function (pool, fromIndex) { |
| | | for (var i = fromIndex; i < pool.length; i++) { |
| | | pool[i].visible = false; |
| | | pool[i].alpha = 0; |
| | | pool[i].width = 0; |
| | | pool[i].height = 0; |
| | | pool[i].position.set(-99999, -99999); |
| | | } |
| | | }, |
| | | pruneStaticSpritePool: function (poolName, keepCount, slack) { |
| | | var pool = poolName === 'node' ? this.staticNodeSpritePool : this.staticTrackSpritePool; |
| | | var layer = poolName === 'node' ? this.staticNodeSpriteLayer : this.staticTrackSpriteLayer; |
| | | var target = Math.max(0, keepCount + Math.max(0, slack || 0)); |
| | | if (!pool || !layer || pool.length <= target) { |
| | | return; |
| | | } |
| | | for (var i = pool.length - 1; i >= target; i--) { |
| | | var sprite = pool[i]; |
| | | layer.removeChild(sprite); |
| | | if (sprite && sprite.destroy) { |
| | | sprite.destroy(); |
| | | } |
| | | pool.pop(); |
| | | } |
| | | }, |
| | | drawElementsToSpriteLayers: function (elements) { |
| | | var trackCount = 0; |
| | | var nodeCount = 0; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var item = elements[i]; |
| | | var meta = getTypeMeta(item.type); |
| | | var poolName = isShelfLikeNodeType(item.type) ? 'node' : 'track'; |
| | | var sprite = this.ensureStaticSprite( |
| | | poolName, |
| | | poolName === 'node' ? nodeCount : trackCount |
| | | ); |
| | | sprite.visible = true; |
| | | sprite.position.set(item.x, item.y); |
| | | sprite.width = item.width; |
| | | sprite.height = item.height; |
| | | sprite.tint = meta.fill; |
| | | sprite.alpha = meta.alpha ?? 1; |
| | | if (poolName === 'node') { |
| | | nodeCount += 1; |
| | | } else { |
| | | trackCount += 1; |
| | | } |
| | | } |
| | | this.hideUnusedStaticSprites(this.staticTrackSpritePool, trackCount); |
| | | this.hideUnusedStaticSprites(this.staticNodeSpritePool, nodeCount); |
| | | if (this.camera.scale < DENSE_SIMPLIFY_SCALE_THRESHOLD) { |
| | | this.pruneStaticSpritePool('track', trackCount, STATIC_SPRITE_POOL_SLACK); |
| | | this.pruneStaticSpritePool('node', nodeCount, STATIC_SPRITE_POOL_SLACK); |
| | | } |
| | | }, |
| | | simplifyRenderableElements: function (elements) { |
| | | if (!elements || elements.length < 2) { |
| | | return elements || []; |
| | | } |
| | | var sorted = elements.slice().sort(function (a, b) { |
| | | if (a.type !== b.type) { |
| | | return a.type < b.type ? -1 : 1; |
| | | } |
| | | if (Math.abs(a.y - b.y) > COORD_EPSILON) { |
| | | return a.y - b.y; |
| | | } |
| | | if (Math.abs(a.height - b.height) > COORD_EPSILON) { |
| | | return a.height - b.height; |
| | | } |
| | | return a.x - b.x; |
| | | }); |
| | | var result = []; |
| | | var current = null; |
| | | for (var i = 0; i < sorted.length; i++) { |
| | | var item = sorted[i]; |
| | | if (!current) { |
| | | current = { |
| | | type: item.type, |
| | | x: item.x, |
| | | y: item.y, |
| | | width: item.width, |
| | | height: item.height |
| | | }; |
| | | continue; |
| | | } |
| | | var currentRight = current.x + current.width; |
| | | var itemRight = item.x + item.width; |
| | | var sameBand = |
| | | current.type === item.type && |
| | | Math.abs(current.y - item.y) <= 0.5 && |
| | | Math.abs(current.height - item.height) <= 0.5; |
| | | var joinable = item.x <= currentRight + 0.5; |
| | | if (sameBand && joinable) { |
| | | current.width = roundCoord(Math.max(currentRight, itemRight) - current.x); |
| | | } else { |
| | | result.push(current); |
| | | current = { |
| | | type: item.type, |
| | | x: item.x, |
| | | y: item.y, |
| | | width: item.width, |
| | | height: item.height |
| | | }; |
| | | } |
| | | } |
| | | if (current) { |
| | | result.push(current); |
| | | } |
| | | return result; |
| | | }, |
| | | renderStaticElements: function (renderRect, excludedKey) { |
| | | if (!this.doc) { |
| | | return; |
| | | } |
| | | this.trackLayer.clear(); |
| | | this.trackLayer.removeChildren(); |
| | | this.nodeLayer.clear(); |
| | | this.nodeLayer.removeChildren(); |
| | | this.eraseLayer.clear(); |
| | | this.patchObjectLayer.clear(); |
| | | this.patchObjectLayer.removeChildren(); |
| | | var renderableElements = this.getRenderableElements( |
| | | this.getStaticExcludedIds(), |
| | | renderRect |
| | | ); |
| | | // 1. 筛选出 annulus 和设备轨道元素,因为 Sprite 不支持绘制中心线 |
| | | var annulusElements = []; |
| | | var deviceElements = []; |
| | | var normalElements = []; |
| | | for (var i = 0; i < renderableElements.length; i++) { |
| | | var el = renderableElements[i]; |
| | | if (el.type === 'annulus') { |
| | | annulusElements.push(el); |
| | | } else if (isDeviceConfigType(el.type)) { |
| | | deviceElements.push(el); |
| | | } else { |
| | | normalElements.push(el); |
| | | } |
| | | } |
| | | var hasAnnulus = annulusElements.length > 0; |
| | | var hasDeviceElements = deviceElements.length > 0; |
| | | |
| | | var useSpriteMode = this.camera.scale < STATIC_SPRITE_SCALE_THRESHOLD; |
| | | var shouldSimplify = |
| | | this.camera.scale < STATIC_SIMPLIFY_SCALE_THRESHOLD || |
| | | (this.camera.scale < DENSE_SIMPLIFY_SCALE_THRESHOLD && |
| | | renderableElements.length > DENSE_SIMPLIFY_ELEMENT_THRESHOLD); |
| | | |
| | | // 2. 重新设定层的可见性 |
| | | // Sprite层:仅在需要且有普通元素时显示 |
| | | this.staticTrackSpriteLayer.visible = useSpriteMode && normalElements.length > 0; |
| | | this.staticNodeSpriteLayer.visible = useSpriteMode && normalElements.length > 0; |
| | | |
| | | // Graphics层:如果不是 Sprite 模式,或者存在 annulus 或设备轨道元素,则必须显示 |
| | | this.trackLayer.visible = !useSpriteMode || hasAnnulus || hasDeviceElements; |
| | | this.nodeLayer.visible = !useSpriteMode; |
| | | |
| | | if (useSpriteMode) { |
| | | // 3. 绘制普通元素到 Sprite 层 |
| | | if (shouldSimplify) { |
| | | normalElements = this.simplifyRenderableElements(normalElements); |
| | | } |
| | | this.drawElementsToSpriteLayers(normalElements); |
| | | |
| | | // 4. 绘制 annulus 和设备轨道元素到 Graphics 层 |
| | | // 即使在缩略图模式下,这些元素也必须用 Graphics 绘制以保持中心线 |
| | | var graphicsElements = annulusElements.concat(deviceElements); |
| | | if (graphicsElements.length > 0) { |
| | | // 注意:drawElementsToLayers 会根据元素类型自动分配到 trackLayer 或 nodeLayer |
| | | // annulus 和设备轨道属于 track 类别,会绘制到 trackLayer |
| | | this.drawElementsToLayers(graphicsElements, this.trackLayer, this.trackLayer); |
| | | } |
| | | } else { |
| | | this.hideUnusedStaticSprites(this.staticTrackSpritePool, 0); |
| | | this.hideUnusedStaticSprites(this.staticNodeSpritePool, 0); |
| | | this.drawElementsToLayers(renderableElements, this.trackLayer, this.nodeLayer); |
| | | } |
| | | var rect = renderRect || this.getWorldRectWithPadding(STATIC_VIEW_PADDING); |
| | | this.staticRenderRect = { |
| | | x: rect.x, |
| | | y: rect.y, |
| | | width: rect.width, |
| | | height: rect.height |
| | | }; |
| | | this.staticRenderKey = this.getStaticRenderKey(); |
| | | this.staticExcludedKey = |
| | | excludedKey != null ? excludedKey : this.selectionKey(this.getStaticExcludedIds()); |
| | | this.pendingStaticCommit = null; |
| | | }, |
| | | renderActiveElements: function () { |
| | | this.activeLayer.clear(); |
| | | this.activeLayer.removeChildren(); |
| | | this.eraseLayer.clear(); |
| | | this.patchObjectLayer.clear(); |
| | | this.patchObjectLayer.removeChildren(); |
| | | var activeIds = this.getStaticExcludedIds(); |
| | | if (!activeIds.length) { |
| | | return; |
| | | } |
| | | var activeElements = []; |
| | | for (var idx = 0; idx < activeIds.length; idx++) { |
| | | var element = this.findElementById(activeIds[idx]); |
| | | if (element) { |
| | | activeElements.push(element); |
| | | } |
| | | } |
| | | if (!activeElements.length) { |
| | | return; |
| | | } |
| | | this.drawElementsToLayers(activeElements, this.activeLayer, this.activeLayer); |
| | | }, |
| | | getLabelText: function (element) { |
| | | var meta = getTypeMeta(element.type); |
| | | var value = safeParseJson(element.value); |
| | | if (element.type === 'devp' && value) { |
| | | var station = value.stationId != null ? String(value.stationId) : ''; |
| | | var arrows = formatDirectionArrows(value.direction); |
| | | if (station && arrows) { |
| | | return element.height > element.width * 1.15 |
| | | ? station + '\n' + arrows |
| | | : station + ' ' + arrows; |
| | | } |
| | | if (station) { |
| | | return station; |
| | | } |
| | | if (arrows) { |
| | | return arrows; |
| | | } |
| | | return meta.shortLabel; |
| | | } |
| | | if ( |
| | | (element.type === 'crn' || element.type === 'dualCrn' || element.type === 'rgv') && |
| | | value |
| | | ) { |
| | | if (value.deviceNo != null) { |
| | | return meta.shortLabel + ' ' + value.deviceNo; |
| | | } |
| | | if (value.crnNo != null) { |
| | | return meta.shortLabel + ' ' + value.crnNo; |
| | | } |
| | | if (value.rgvNo != null) { |
| | | return meta.shortLabel + ' ' + value.rgvNo; |
| | | } |
| | | } |
| | | if (element.value && element.value.length <= 18 && element.value.indexOf('{') !== 0) { |
| | | return element.value; |
| | | } |
| | | return meta.shortLabel; |
| | | }, |
| | | ensureLabelSprite: function (index) { |
| | | if (this.labelPool[index]) { |
| | | return this.labelPool[index]; |
| | | } |
| | | var label = new PIXI.Text('', { |
| | | fontFamily: 'Avenir Next, PingFang SC, Microsoft YaHei, sans-serif', |
| | | fontSize: 12, |
| | | fontWeight: '600', |
| | | fill: 0x223448, |
| | | align: 'center' |
| | | }); |
| | | label.anchor.set(0.5); |
| | | this.labelLayer.addChild(label); |
| | | this.labelPool[index] = label; |
| | | return label; |
| | | }, |
| | | getLabelRenderBudget: function () { |
| | | if (!this.pixiApp || !this.pixiApp.renderer) { |
| | | return MIN_LABEL_COUNT; |
| | | } |
| | | var renderer = this.pixiApp.renderer; |
| | | var viewportArea = renderer.width * renderer.height; |
| | | return clamp(Math.round(viewportArea / 12000), MIN_LABEL_COUNT, MAX_LABEL_COUNT); |
| | | }, |
| | | getLabelMinScreenWidth: function (text) { |
| | | var lines = String(text || '').split('\n'); |
| | | var length = 0; |
| | | for (var i = 0; i < lines.length; i++) { |
| | | length = Math.max(length, String(lines[i] || '').trim().length); |
| | | } |
| | | if (length <= 4) { |
| | | return 26; |
| | | } |
| | | if (length <= 8) { |
| | | return 40; |
| | | } |
| | | if (length <= 12) { |
| | | return 52; |
| | | } |
| | | return 64; |
| | | }, |
| | | getLabelMinScreenHeight: function (text) { |
| | | var lines = String(text || '').split('\n'); |
| | | var length = 0; |
| | | for (var i = 0; i < lines.length; i++) { |
| | | length = Math.max(length, String(lines[i] || '').trim().length); |
| | | } |
| | | var lineHeight = length <= 4 ? 14 : 18; |
| | | return lineHeight * Math.max(lines.length, 1); |
| | | }, |
| | | renderLabels: function () { |
| | | if (!this.doc) { |
| | | return; |
| | | } |
| | | var capability = this.ensureLabelCapability(); |
| | | if ( |
| | | capability.maxWidth * this.camera.scale < ABS_MIN_LABEL_SCREEN_WIDTH || |
| | | capability.maxHeight * this.camera.scale < ABS_MIN_LABEL_SCREEN_HEIGHT |
| | | ) { |
| | | this.labelLayer.visible = false; |
| | | return; |
| | | } |
| | | if ( |
| | | this.isZooming || |
| | | this.isPanning || |
| | | this.camera.scale < MIN_LABEL_SCALE || |
| | | (this.interactionState && |
| | | (this.interactionState.type === 'move' || |
| | | this.interactionState.type === 'resize' || |
| | | this.interactionState.type === 'pan')) |
| | | ) { |
| | | this.labelLayer.visible = false; |
| | | return; |
| | | } |
| | | this.labelLayer.visible = true; |
| | | var visible = this.getVisibleWorldRect(); |
| | | var elements = this.querySpatialCandidates(visible, 0, []); |
| | | if ( |
| | | elements.length > DENSE_LABEL_HIDE_ELEMENT_THRESHOLD && |
| | | this.camera.scale < DENSE_LABEL_HIDE_SCALE_THRESHOLD |
| | | ) { |
| | | this.labelLayer.visible = false; |
| | | return; |
| | | } |
| | | var hasRoomForAnyLabel = false; |
| | | for (var roomIdx = 0; roomIdx < elements.length; roomIdx++) { |
| | | var candidate = elements[roomIdx]; |
| | | if ( |
| | | candidate.width * this.camera.scale >= ABS_MIN_LABEL_SCREEN_WIDTH && |
| | | candidate.height * this.camera.scale >= ABS_MIN_LABEL_SCREEN_HEIGHT |
| | | ) { |
| | | hasRoomForAnyLabel = true; |
| | | break; |
| | | } |
| | | } |
| | | if (!hasRoomForAnyLabel) { |
| | | this.labelLayer.visible = false; |
| | | return; |
| | | } |
| | | var visibleElements = []; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | var text = this.getLabelText(element); |
| | | if (!text) { |
| | | continue; |
| | | } |
| | | if (!rectIntersects(visible, element)) { |
| | | continue; |
| | | } |
| | | if ( |
| | | element.width * this.camera.scale < this.getLabelMinScreenWidth(text) || |
| | | element.height * this.camera.scale < this.getLabelMinScreenHeight(text) |
| | | ) { |
| | | continue; |
| | | } |
| | | visibleElements.push({ |
| | | element: element, |
| | | text: text |
| | | }); |
| | | } |
| | | visibleElements.sort(function (a, b) { |
| | | return b.element.width * b.element.height - a.element.width * a.element.height; |
| | | }); |
| | | var labelBudget = this.getLabelRenderBudget(); |
| | | if (visibleElements.length > labelBudget) { |
| | | visibleElements = visibleElements.slice(0, labelBudget); |
| | | } |
| | | for (var j = 0; j < visibleElements.length; j++) { |
| | | var item = visibleElements[j].element; |
| | | var label = this.ensureLabelSprite(j); |
| | | label.visible = true; |
| | | label.text = visibleElements[j].text; |
| | | label.position.set(item.x + item.width / 2, item.y + item.height / 2); |
| | | label.scale.set(1 / this.camera.scale, 1 / this.camera.scale); |
| | | label.alpha = this.selectedIds.indexOf(item.id) >= 0 ? 1 : 0.88; |
| | | } |
| | | for (var k = visibleElements.length; k < this.labelPool.length; k++) { |
| | | this.labelPool[k].visible = false; |
| | | } |
| | | }, |
| | | renderHover: function () { |
| | | this.hoverLayer.clear(); |
| | | if ( |
| | | this.interactionState || |
| | | !this.hoverElementId || |
| | | this.selectedIds.indexOf(this.hoverElementId) >= 0 |
| | | ) { |
| | | return; |
| | | } |
| | | var element = this.findElementById(this.hoverElementId); |
| | | if (!element) { |
| | | return; |
| | | } |
| | | var lineWidth = 2 / this.camera.scale; |
| | | this.hoverLayer.lineStyle(lineWidth, 0x2f79d6, 0.95); |
| | | this.hoverLayer.drawRoundedRect( |
| | | element.x, |
| | | element.y, |
| | | element.width, |
| | | element.height, |
| | | Math.max(6 / this.camera.scale, 2) |
| | | ); |
| | | }, |
| | | renderSelection: function () { |
| | | console.log('renderSelection'); |
| | | this.selectionLayer.clear(); |
| | | this.selectionLayer.removeChildren(); |
| | | if ( |
| | | !this.selectedIds.length || |
| | | (this.interactionState && |
| | | (this.interactionState.type === 'move' || this.interactionState.type === 'resize')) |
| | | ) { |
| | | return; |
| | | } |
| | | var elements = this.getSelectedElements(); |
| | | var lineWidth = 2 / this.camera.scale; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var element = elements[i]; |
| | | drawElementByType.call(this, this.selectionLayer, element, element.type, { |
| | | line: { |
| | | width: lineWidth, |
| | | color: 0x2568b8, |
| | | alpha: 1 |
| | | }, |
| | | fill: { |
| | | color: 0x2f79d6, |
| | | alpha: 0.07 |
| | | } |
| | | }); |
| | | this.selectionLayer.endFill(); |
| | | } |
| | | if (elements.length !== 1) { |
| | | return; |
| | | } |
| | | var handleSize = HANDLE_SCREEN_SIZE / this.camera.scale; |
| | | var handlePositions = this.getHandlePositions(elements[0]); |
| | | this.selectionLayer.lineStyle(1 / this.camera.scale, 0x1d5ea9, 1); |
| | | this.selectionLayer.beginFill(0xffffff, 1); |
| | | for (var key in handlePositions) { |
| | | if (!handlePositions.hasOwnProperty(key)) { |
| | | continue; |
| | | } |
| | | var pos = handlePositions[key]; |
| | | this.selectionLayer.drawRect( |
| | | pos.x - handleSize / 2, |
| | | pos.y - handleSize / 2, |
| | | handleSize, |
| | | handleSize |
| | | ); |
| | | } |
| | | this.selectionLayer.endFill(); |
| | | }, |
| | | renderGuide: function () { |
| | | console.log('renderGuide'); |
| | | this.guideLayer.clear(); |
| | | this.guideLayer.removeChildren(); |
| | | if (this.guideText) { |
| | | this.guideText.visible = false; |
| | | } |
| | | if (!this.interactionState) { |
| | | return; |
| | | } |
| | | var state = this.interactionState; |
| | | if (state.type === 'draw' && state.rect && state.rect.width > 0 && state.rect.height > 0) { |
| | | var drawMeta = getTypeMeta(state.elementType); |
| | | drawElementByType.call(this, this.guideLayer, state.rect, state.elementType, { |
| | | line: { |
| | | width: 2 / this.camera.scale, |
| | | color: drawMeta.border, |
| | | alpha: 0.95 |
| | | }, |
| | | fill: { |
| | | color: drawMeta.fill, |
| | | alpha: drawMeta.alpha ?? 0.18 |
| | | } |
| | | }); |
| | | return; |
| | | } |
| | | if (state.type === 'array' && state.template) { |
| | | var previewItems = state.previewItems || []; |
| | | var arrayMeta = getTypeMeta(state.template.type); |
| | | var lineWidth = 2 / this.camera.scale; |
| | | var templateCenterX = state.template.x + state.template.width / 2; |
| | | var templateCenterY = state.template.y + state.template.height / 2; |
| | | this.guideLayer.lineStyle(lineWidth, arrayMeta.border, 0.9); |
| | | this.guideLayer.moveTo(templateCenterX, templateCenterY); |
| | | this.guideLayer.lineTo(state.currentWorld.x, state.currentWorld.y); |
| | | if (!previewItems.length) { |
| | | return; |
| | | } |
| | | this.guideLayer.lineStyle(1 / this.camera.scale, arrayMeta.border, 0.8); |
| | | this.guideLayer.beginFill(arrayMeta.fill, 0.2); |
| | | for (var previewIndex = 0; previewIndex < previewItems.length; previewIndex++) { |
| | | var preview = previewItems[previewIndex]; |
| | | this.guideLayer.drawRoundedRect( |
| | | preview.x, |
| | | preview.y, |
| | | preview.width, |
| | | preview.height, |
| | | Math.max(6 / this.camera.scale, 2) |
| | | ); |
| | | } |
| | | this.guideLayer.endFill(); |
| | | if (this.guideText) { |
| | | this.guideText.text = '将生成 ' + previewItems.length + ' 个'; |
| | | this.guideText.position.set( |
| | | state.currentWorld.x, |
| | | state.currentWorld.y - 10 / this.camera.scale |
| | | ); |
| | | this.guideText.scale.set(1 / this.camera.scale); |
| | | this.guideText.visible = true; |
| | | } |
| | | return; |
| | | } |
| | | if (state.type === 'marquee') { |
| | | var rect = buildRectFromPoints(state.startWorld, state.currentWorld); |
| | | if (rect.width <= 0 || rect.height <= 0) { |
| | | return; |
| | | } |
| | | this.guideLayer.lineStyle(2 / this.camera.scale, 0x2f79d6, 0.92); |
| | | this.guideLayer.beginFill(0x2f79d6, 0.06); |
| | | this.guideLayer.drawRect(rect.x, rect.y, rect.width, rect.height); |
| | | this.guideLayer.endFill(); |
| | | } |
| | | }, |
| | | pointerToWorld: function (event) { |
| | | var rect = this.pixiApp.view.getBoundingClientRect(); |
| | | var screenX = event.clientX - rect.left; |
| | | var screenY = event.clientY - rect.top; |
| | | return { |
| | | screenX: screenX, |
| | | screenY: screenY, |
| | | x: roundCoord((screenX - this.camera.x) / this.camera.scale), |
| | | y: roundCoord((screenY - this.camera.y) / this.camera.scale) |
| | | }; |
| | | }, |
| | | isWithinCanvas: function (rect) { |
| | | if (!this.doc) { |
| | | return false; |
| | | } |
| | | return ( |
| | | rect.x >= -COORD_EPSILON && |
| | | rect.y >= -COORD_EPSILON && |
| | | rect.x + rect.width <= this.doc.canvasWidth + COORD_EPSILON && |
| | | rect.y + rect.height <= this.doc.canvasHeight + COORD_EPSILON |
| | | ); |
| | | }, |
| | | canPlaceElements: function (elements, excludeIds) { |
| | | excludeIds = excludeIds || []; |
| | | for (var i = 0; i < elements.length; i++) { |
| | | if (!this.isWithinCanvas(elements[i])) { |
| | | return false; |
| | | } |
| | | if (this.hasOverlap(elements[i], excludeIds.concat([elements[i].id]))) { |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | }, |
| | | hasOverlap: function (candidate, excludeIds) { |
| | | if (!this.doc) { |
| | | return false; |
| | | } |
| | | var elements = this.querySpatialCandidates(candidate, COORD_EPSILON, excludeIds); |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var item = elements[i]; |
| | | if (rectsOverlap(candidate, item)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | }, |
| | | snapToleranceWorld: function () { |
| | | return Math.max(1, EDGE_SNAP_SCREEN_TOLERANCE / this.camera.scale); |
| | | }, |
| | | collectMoveSnap: function (baseItems, dx, dy, excludeIds) { |
| | | if (!this.doc || !baseItems || !baseItems.length) { |
| | | return { dx: 0, dy: 0 }; |
| | | } |
| | | var tolerance = this.snapToleranceWorld(); |
| | | var bestDx = null; |
| | | var bestDy = null; |
| | | for (var i = 0; i < baseItems.length; i++) { |
| | | var moving = baseItems[i]; |
| | | var movedLeft = moving.x + dx; |
| | | var movedRight = movedLeft + moving.width; |
| | | var movedTop = moving.y + dy; |
| | | var movedBottom = movedTop + moving.height; |
| | | var candidates = this.querySpatialCandidates( |
| | | { |
| | | x: movedLeft, |
| | | y: movedTop, |
| | | width: moving.width, |
| | | height: moving.height |
| | | }, |
| | | tolerance, |
| | | excludeIds |
| | | ); |
| | | for (var j = 0; j < candidates.length; j++) { |
| | | var other = candidates[j]; |
| | | var otherLeft = other.x; |
| | | var otherRight = other.x + other.width; |
| | | var otherTop = other.y; |
| | | var otherBottom = other.y + other.height; |
| | | if (rangesNearOrOverlap(movedTop, movedBottom, otherTop, otherBottom, tolerance)) { |
| | | var horizontalCandidates = [ |
| | | otherLeft - movedRight, |
| | | otherRight - movedLeft, |
| | | otherLeft - movedLeft, |
| | | otherRight - movedRight |
| | | ]; |
| | | for (var hx = 0; hx < horizontalCandidates.length; hx++) { |
| | | var deltaX = horizontalCandidates[hx]; |
| | | if ( |
| | | Math.abs(deltaX) <= tolerance && |
| | | (bestDx === null || Math.abs(deltaX) < Math.abs(bestDx)) |
| | | ) { |
| | | bestDx = deltaX; |
| | | } |
| | | } |
| | | } |
| | | if (rangesNearOrOverlap(movedLeft, movedRight, otherLeft, otherRight, tolerance)) { |
| | | var verticalCandidates = [ |
| | | otherTop - movedBottom, |
| | | otherBottom - movedTop, |
| | | otherTop - movedTop, |
| | | otherBottom - movedBottom |
| | | ]; |
| | | for (var vy = 0; vy < verticalCandidates.length; vy++) { |
| | | var deltaY = verticalCandidates[vy]; |
| | | if ( |
| | | Math.abs(deltaY) <= tolerance && |
| | | (bestDy === null || Math.abs(deltaY) < Math.abs(bestDy)) |
| | | ) { |
| | | bestDy = deltaY; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return { |
| | | dx: bestDx == null ? 0 : bestDx, |
| | | dy: bestDy == null ? 0 : bestDy |
| | | }; |
| | | }, |
| | | collectResizeSnap: function (rect, handle, excludeIds) { |
| | | if (!this.doc || !rect) { |
| | | return null; |
| | | } |
| | | var tolerance = this.snapToleranceWorld(); |
| | | var left = rect.x; |
| | | var right = rect.x + rect.width; |
| | | var top = rect.y; |
| | | var bottom = rect.y + rect.height; |
| | | var bestLeft = null; |
| | | var bestRight = null; |
| | | var bestTop = null; |
| | | var bestBottom = null; |
| | | function pickBest(current, candidate) { |
| | | if (candidate == null) { |
| | | return current; |
| | | } |
| | | if (current == null || Math.abs(candidate) < Math.abs(current)) { |
| | | return candidate; |
| | | } |
| | | return current; |
| | | } |
| | | if (handle.indexOf('w') >= 0) { |
| | | bestLeft = pickBest(bestLeft, -left); |
| | | } |
| | | if (handle.indexOf('e') >= 0) { |
| | | bestRight = pickBest(bestRight, this.doc.canvasWidth - right); |
| | | } |
| | | if (handle.indexOf('n') >= 0) { |
| | | bestTop = pickBest(bestTop, -top); |
| | | } |
| | | if (handle.indexOf('s') >= 0) { |
| | | bestBottom = pickBest(bestBottom, this.doc.canvasHeight - bottom); |
| | | } |
| | | var elements = this.querySpatialCandidates(rect, tolerance, excludeIds); |
| | | for (var i = 0; i < elements.length; i++) { |
| | | var other = elements[i]; |
| | | var otherLeft = other.x; |
| | | var otherRight = other.x + other.width; |
| | | var otherTop = other.y; |
| | | var otherBottom = other.y + other.height; |
| | | if (rangesNearOrOverlap(top, bottom, otherTop, otherBottom, tolerance)) { |
| | | if (handle.indexOf('w') >= 0) { |
| | | bestLeft = pickBest(bestLeft, otherLeft - left); |
| | | bestLeft = pickBest(bestLeft, otherRight - left); |
| | | } |
| | | if (handle.indexOf('e') >= 0) { |
| | | bestRight = pickBest(bestRight, otherLeft - right); |
| | | bestRight = pickBest(bestRight, otherRight - right); |
| | | } |
| | | } |
| | | if (rangesNearOrOverlap(left, right, otherLeft, otherRight, tolerance)) { |
| | | if (handle.indexOf('n') >= 0) { |
| | | bestTop = pickBest(bestTop, otherTop - top); |
| | | bestTop = pickBest(bestTop, otherBottom - top); |
| | | } |
| | | if (handle.indexOf('s') >= 0) { |
| | | bestBottom = pickBest(bestBottom, otherTop - bottom); |
| | | bestBottom = pickBest(bestBottom, otherBottom - bottom); |
| | | } |
| | | } |
| | | } |
| | | if (bestLeft != null && Math.abs(bestLeft) > tolerance) { |
| | | bestLeft = null; |
| | | } |
| | | if (bestRight != null && Math.abs(bestRight) > tolerance) { |
| | | bestRight = null; |
| | | } |
| | | if (bestTop != null && Math.abs(bestTop) > tolerance) { |
| | | bestTop = null; |
| | | } |
| | | if (bestBottom != null && Math.abs(bestBottom) > tolerance) { |
| | | bestBottom = null; |
| | | } |
| | | return { |
| | | left: bestLeft, |
| | | right: bestRight, |
| | | top: bestTop, |
| | | bottom: bestBottom |
| | | }; |
| | | }, |
| | | hitTestElement: function (point) { |
| | | if (!this.doc) { |
| | | return null; |
| | | } |
| | | var candidates = this.querySpatialCandidates( |
| | | { |
| | | x: point.x, |
| | | y: point.y, |
| | | width: 0, |
| | | height: 0 |
| | | }, |
| | | 0, |
| | | [] |
| | | ); |
| | | if (!candidates.length) { |
| | | return null; |
| | | } |
| | | var candidateMap = {}; |
| | | for (var c = 0; c < candidates.length; c++) { |
| | | candidateMap[candidates[c].id] = true; |
| | | } |
| | | var elements = this.doc.elements || []; |
| | | for (var i = elements.length - 1; i >= 0; i--) { |
| | | var element = elements[i]; |
| | | if (!candidateMap[element.id]) { |
| | | continue; |
| | | } |
| | | if ( |
| | | point.x >= element.x && |
| | | point.x <= element.x + element.width && |
| | | point.y >= element.y && |
| | | point.y <= element.y + element.height |
| | | ) { |
| | | return element; |
| | | } |
| | | } |
| | | return null; |
| | | }, |
| | | getHandlePositions: function (element) { |
| | | var x = element.x; |
| | | var y = element.y; |
| | | var w = element.width; |
| | | var h = element.height; |
| | | var cx = x + w / 2; |
| | | var cy = y + h / 2; |
| | | if (element.type === 'annulus' && element.shape !== 'rect') { |
| | | return { |
| | | nw: { x: x, y: y }, |
| | | ne: { x: x + w, y: y }, |
| | | se: { x: x + w, y: y + h }, |
| | | sw: { x: x, y: y + h }, |
| | | turningPoint: element.turningPoint, |
| | | shape: element.shape |
| | | }; |
| | | } |
| | | return { |
| | | nw: { x: x, y: y }, |
| | | n: { x: cx, y: y }, |
| | | ne: { x: x + w, y: y }, |
| | | e: { x: x + w, y: cy }, |
| | | se: { x: x + w, y: y + h }, |
| | | s: { x: cx, y: y + h }, |
| | | sw: { x: x, y: y + h }, |
| | | w: { x: x, y: cy } |
| | | }; |
| | | }, |
| | | getResizeHandleAt: function (point, element) { |
| | | var handlePositions = this.getHandlePositions(element); |
| | | var baseTolerance = HANDLE_SCREEN_SIZE / this.camera.scale; |
| | | var sizeLimitedTolerance = Math.max( |
| | | Math.min(element.width, element.height) / 4, |
| | | 3 / this.camera.scale |
| | | ); |
| | | var tolerance = Math.min(baseTolerance, sizeLimitedTolerance); |
| | | var bestHandle = ''; |
| | | var bestDistance = Infinity; |
| | | for (var key in handlePositions) { |
| | | if (!handlePositions.hasOwnProperty(key)) { |
| | | continue; |
| | | } |
| | | var pos = handlePositions[key]; |
| | | var dx = Math.abs(point.x - pos.x); |
| | | var dy = Math.abs(point.y - pos.y); |
| | | if (dx <= tolerance && dy <= tolerance) { |
| | | var distance = dx + dy; |
| | | if (distance < bestDistance) { |
| | | bestDistance = distance; |
| | | bestHandle = key; |
| | | } |
| | | } |
| | | } |
| | | return bestHandle; |
| | | }, |
| | | cursorForHandle: function (handle) { |
| | | if (handle === 'nw' || handle === 'se') { |
| | | return 'nwse-resize'; |
| | | } |
| | | if (handle === 'ne' || handle === 'sw') { |
| | | return 'nesw-resize'; |
| | | } |
| | | if (handle === 'n' || handle === 's') { |
| | | return 'ns-resize'; |
| | | } |
| | | if (handle === 'e' || handle === 'w') { |
| | | return 'ew-resize'; |
| | | } |
| | | if (handle === 'turningPoint') { |
| | | const element = this.singleSelectedElement || {}; |
| | | return element.shape === 'L1' || element.shape === 'L3' ? 'nesw-resize' : 'nwse-resize'; |
| | | } |
| | | return 'default'; |
| | | }, |
| | | updateCursor: function () { |
| | | if (!this.pixiApp) { |
| | | return; |
| | | } |
| | | var cursor = 'default'; |
| | | if (this.interactionState) { |
| | | if (this.interactionState.type === 'pan') { |
| | | cursor = 'grabbing'; |
| | | } else if ( |
| | | this.interactionState.type === 'draw' || |
| | | this.interactionState.type === 'marquee' |
| | | ) { |
| | | cursor = 'crosshair'; |
| | | } else if (this.interactionState.type === 'array') { |
| | | cursor = 'crosshair'; |
| | | } else if (this.interactionState.type === 'move') { |
| | | cursor = 'move'; |
| | | } else if (this.interactionState.type === 'movePending') { |
| | | cursor = 'grab'; |
| | | } else if (this.interactionState.type === 'resize') { |
| | | cursor = this.cursorForHandle(this.interactionState.handle); |
| | | } |
| | | } else if (this.spacePressed || this.activeTool === 'pan') { |
| | | cursor = 'grab'; |
| | | } else if ( |
| | | DRAW_TYPES.indexOf(this.activeTool) >= 0 || |
| | | this.activeTool === 'marquee' || |
| | | this.activeTool === 'array' |
| | | ) { |
| | | cursor = 'crosshair'; |
| | | } else if (this.singleSelectedElement) { |
| | | var point = this.lastPointerWorld || null; |
| | | if (point) { |
| | | var handle = this.getResizeHandleAt(point, this.singleSelectedElement); |
| | | cursor = handle ? this.cursorForHandle(handle) : 'default'; |
| | | } |
| | | if (cursor === 'default' && this.hoverElementId) { |
| | | cursor = 'move'; |
| | | } else if (cursor === 'default') { |
| | | cursor = 'grab'; |
| | | } |
| | | } else { |
| | | cursor = this.hoverElementId ? 'move' : 'grab'; |
| | | } |
| | | if (cursor !== this.lastCursor) { |
| | | this.lastCursor = cursor; |
| | | this.pixiApp.view.style.cursor = cursor; |
| | | } |
| | | }, |
| | | startPan: function (point) { |
| | | this.cancelDeferredStaticRebuild(); |
| | | this.cancelPanRefresh(); |
| | | if (this.zoomRefreshTimer) { |
| | | window.clearTimeout(this.zoomRefreshTimer); |
| | | this.zoomRefreshTimer = null; |
| | | this.isZooming = false; |
| | | this.pendingViewportRefresh = true; |
| | | } |
| | | this.isPanning = true; |
| | | this.interactionState = { |
| | | type: 'pan', |
| | | startScreen: { |
| | | x: point.screenX, |
| | | y: point.screenY |
| | | }, |
| | | startCamera: { |
| | | x: this.camera.x, |
| | | y: this.camera.y |
| | | } |
| | | }; |
| | | this.updateCursor(); |
| | | }, |
| | | startMarquee: function (point, additive) { |
| | | this.cancelDeferredStaticRebuild(); |
| | | this.interactionState = { |
| | | type: 'marquee', |
| | | additive: !!additive, |
| | | startWorld: { x: point.x, y: point.y }, |
| | | currentWorld: { x: point.x, y: point.y } |
| | | }; |
| | | this.updateCursor(); |
| | | }, |
| | | startDraw: function (point) { |
| | | this.cancelDeferredStaticRebuild(); |
| | | var initialRect = { x: point.x, y: point.y, width: 0, height: 0 }; |
| | | if (this.activeTool === 'annulus') { |
| | | initialRect.type = 'annulus'; |
| | | initialRect.shape = this.annulusShape; |
| | | } |
| | | this.interactionState = { |
| | | type: 'draw', |
| | | beforeSnapshot: this.snapshotDoc(this.doc), |
| | | elementType: this.activeTool, |
| | | startWorld: { x: point.x, y: point.y }, |
| | | rect: initialRect |
| | | }; |
| | | this.updateCursor(); |
| | | }, |
| | | startArray: function (point, element) { |
| | | if (!this.canArrayFromElement(element)) { |
| | | this.showMessage('warning', '阵列工具当前只支持货架与维修站台'); |
| | | return; |
| | | } |
| | | this.cancelDeferredStaticRebuild(); |
| | | this.interactionState = { |
| | | type: 'array', |
| | | beforeSnapshot: this.snapshotDoc(this.doc), |
| | | template: { |
| | | id: element.id, |
| | | type: element.type, |
| | | x: element.x, |
| | | y: element.y, |
| | | width: element.width, |
| | | height: element.height, |
| | | value: element.value |
| | | }, |
| | | startWorld: { x: point.x, y: point.y }, |
| | | currentWorld: { x: point.x, y: point.y }, |
| | | previewItems: [] |
| | | }; |
| | | this.updateCursor(); |
| | | }, |
| | | startMove: function (point) { |
| | | var selected = this.getSelectedElements(); |
| | | if (!selected.length) { |
| | | return; |
| | | } |
| | | this.cancelDeferredStaticRebuild(); |
| | | var baseItems = selected.map(function (item) { |
| | | return { |
| | | id: item.id, |
| | | x: item.x, |
| | | y: item.y, |
| | | width: item.width, |
| | | height: item.height, |
| | | value: item.value, |
| | | type: item.type, |
| | | turningPoint: item.turningPoint |
| | | ? { x: item.turningPoint.x, y: item.turningPoint.y } |
| | | : null |
| | | }; |
| | | }); |
| | | this.interactionState = { |
| | | type: 'movePending', |
| | | beforeSnapshot: this.snapshotDoc(this.doc), |
| | | startScreen: { x: point.screenX, y: point.screenY }, |
| | | startWorld: { x: point.x, y: point.y }, |
| | | baseItems: baseItems |
| | | }; |
| | | this.updateCursor(); |
| | | }, |
| | | startResize: function (point, element, handle) { |
| | | this.cancelDeferredStaticRebuild(); |
| | | this.interactionState = { |
| | | type: 'resize', |
| | | handle: handle, |
| | | elementId: element.id, |
| | | beforeSnapshot: this.snapshotDoc(this.doc), |
| | | startWorld: { x: point.x, y: point.y }, |
| | | baseRect: { |
| | | x: element.x, |
| | | y: element.y, |
| | | width: element.width, |
| | | height: element.height, |
| | | baseTurningPoint: element.turningPoint |
| | | ? { x: element.turningPoint.x, y: element.turningPoint.y } |
| | | : null, |
| | | shape: element.shape |
| | | } |
| | | }; |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | this.updateCursor(); |
| | | }, |
| | | onCanvasPointerDown: function (event) { |
| | | if (!this.doc || !this.pixiApp) { |
| | | return; |
| | | } |
| | | if (event.button !== 0 && event.button !== 1) { |
| | | return; |
| | | } |
| | | if (this.pixiApp.view.setPointerCapture && event.pointerId != null) { |
| | | try { |
| | | this.pixiApp.view.setPointerCapture(event.pointerId); |
| | | } catch (ignore) {} |
| | | } |
| | | this.currentPointerId = event.pointerId; |
| | | var point = this.pointerToWorld(event); |
| | | this.lastPointerWorld = point; |
| | | this.pointerStatus = this.formatNumber(point.x) + ', ' + this.formatNumber(point.y); |
| | | if (this.spacePressed || this.activeTool === 'pan' || event.button === 1) { |
| | | this.startPan(point); |
| | | return; |
| | | } |
| | | if (DRAW_TYPES.indexOf(this.activeTool) >= 0) { |
| | | this.startDraw(point); |
| | | return; |
| | | } |
| | | if (this.activeTool === 'marquee') { |
| | | this.startMarquee(point, event.shiftKey); |
| | | return; |
| | | } |
| | | if (this.activeTool === 'array') { |
| | | var arrayHit = this.hitTestElement(point); |
| | | var arrayTemplate = arrayHit || this.singleSelectedElement; |
| | | if (arrayHit && this.selectedIds.indexOf(arrayHit.id) < 0) { |
| | | this.setSelectedIds([arrayHit.id]); |
| | | arrayTemplate = arrayHit; |
| | | } |
| | | if (!arrayTemplate) { |
| | | this.showMessage('warning', '请先选中一个货架或维修站台作为阵列模板'); |
| | | return; |
| | | } |
| | | this.startArray(point, arrayTemplate); |
| | | return; |
| | | } |
| | | |
| | | var selected = this.singleSelectedElement; |
| | | var handle = selected ? this.getResizeHandleAt(point, selected) : ''; |
| | | if (handle) { |
| | | this.startResize(point, selected, handle); |
| | | return; |
| | | } |
| | | |
| | | var hit = this.hitTestElement(point); |
| | | if (hit) { |
| | | if (event.shiftKey) { |
| | | var index = this.selectedIds.indexOf(hit.id); |
| | | if (index >= 0) { |
| | | var nextIds = this.selectedIds.slice(); |
| | | nextIds.splice(index, 1); |
| | | this.setSelectedIds(nextIds); |
| | | } else { |
| | | this.setSelectedIds(this.selectedIds.concat([hit.id])); |
| | | } |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | if (this.selectedIds.indexOf(hit.id) < 0) { |
| | | this.setSelectedIds([hit.id]); |
| | | this.scheduleRender(); |
| | | } |
| | | this.startMove(point); |
| | | return; |
| | | } |
| | | |
| | | if (this.selectedIds.length) { |
| | | this.setSelectedIds([]); |
| | | this.scheduleRender(); |
| | | } |
| | | this.startPan(point); |
| | | }, |
| | | onCanvasWheel: function (event) { |
| | | if (!this.pixiApp || !this.doc) { |
| | | return; |
| | | } |
| | | event.preventDefault(); |
| | | var point = this.pointerToWorld(event); |
| | | var delta = event.deltaY < 0 ? 1.12 : 0.89; |
| | | var nextScale = clamp(this.camera.scale * delta, 0.06, 4); |
| | | this.camera.scale = nextScale; |
| | | this.camera.x = Math.round(point.screenX - point.x * nextScale); |
| | | this.camera.y = Math.round(point.screenY - point.y * nextScale); |
| | | this.viewZoom = nextScale; |
| | | this.scheduleZoomRefresh(); |
| | | this.scheduleRender(); |
| | | }, |
| | | onWindowPointerMove: function (event) { |
| | | if (!this.pixiApp || !this.doc) { |
| | | return; |
| | | } |
| | | var point = this.pointerToWorld(event); |
| | | this.lastPointerWorld = point; |
| | | var pointerText = this.formatNumber(point.x) + ', ' + this.formatNumber(point.y); |
| | | var now = window.performance && performance.now ? performance.now() : Date.now(); |
| | | if ( |
| | | pointerText !== this.pointerStatus && |
| | | (now - this.lastPointerStatusUpdateTs >= POINTER_STATUS_UPDATE_INTERVAL || |
| | | this.pointerStatus === '--') |
| | | ) { |
| | | this.pointerStatus = pointerText; |
| | | this.lastPointerStatusUpdateTs = now; |
| | | } |
| | | if (!this.interactionState) { |
| | | var hover = this.hitTestElement(point); |
| | | var hoverId = hover ? hover.id : ''; |
| | | if (hoverId !== this.hoverElementId) { |
| | | this.hoverElementId = hoverId; |
| | | this.scheduleRender(); |
| | | } |
| | | this.updateCursor(); |
| | | return; |
| | | } |
| | | |
| | | var state = this.interactionState; |
| | | if (state.type === 'pan') { |
| | | this.camera.x = Math.round(state.startCamera.x + (point.screenX - state.startScreen.x)); |
| | | this.camera.y = Math.round(state.startCamera.y + (point.screenY - state.startScreen.y)); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'marquee') { |
| | | state.currentWorld = { x: point.x, y: point.y }; |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'draw') { |
| | | var rawRect = buildRectFromPoints(state.startWorld, point); |
| | | var clipped = { |
| | | x: clamp(rawRect.x, 0, this.doc.canvasWidth), |
| | | y: clamp(rawRect.y, 0, this.doc.canvasHeight), |
| | | width: clamp(rawRect.width, 0, this.doc.canvasWidth), |
| | | height: clamp(rawRect.height, 0, this.doc.canvasHeight) |
| | | }; |
| | | if (clipped.x + clipped.width > this.doc.canvasWidth) { |
| | | clipped.width = roundCoord(this.doc.canvasWidth - clipped.x); |
| | | } |
| | | if (clipped.y + clipped.height > this.doc.canvasHeight) { |
| | | clipped.height = roundCoord(this.doc.canvasHeight - clipped.y); |
| | | } |
| | | if (state.elementType === 'annulus') { |
| | | clipped.type = 'annulus'; |
| | | clipped.shape = this.annulusShape; |
| | | } |
| | | state.rect = clipped; |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | if (state.type === 'array') { |
| | | state.currentWorld = { x: point.x, y: point.y }; |
| | | state.previewItems = this.buildArrayCopies( |
| | | state.template, |
| | | state.startWorld, |
| | | state.currentWorld |
| | | ); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'movePending') { |
| | | var dragDistance = Math.max( |
| | | Math.abs(point.screenX - state.startScreen.x), |
| | | Math.abs(point.screenY - state.startScreen.y) |
| | | ); |
| | | if (dragDistance < DRAG_START_THRESHOLD) { |
| | | return; |
| | | } |
| | | state.type = 'move'; |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | this.updateCursor(); |
| | | } |
| | | |
| | | if (state.type === 'move') { |
| | | var dx = point.x - state.startWorld.x; |
| | | var dy = point.y - state.startWorld.y; |
| | | var minDx = -Infinity; |
| | | var maxDx = Infinity; |
| | | var minDy = -Infinity; |
| | | var maxDy = Infinity; |
| | | for (var i = 0; i < state.baseItems.length; i++) { |
| | | var base = state.baseItems[i]; |
| | | minDx = Math.max(minDx, -base.x); |
| | | minDy = Math.max(minDy, -base.y); |
| | | maxDx = Math.min(maxDx, this.doc.canvasWidth - (base.x + base.width)); |
| | | maxDy = Math.min(maxDy, this.doc.canvasHeight - (base.y + base.height)); |
| | | } |
| | | dx = clamp(dx, minDx, maxDx); |
| | | dy = clamp(dy, minDy, maxDy); |
| | | var snapDelta = this.collectMoveSnap(state.baseItems, dx, dy, this.selectedIds.slice()); |
| | | dx = clamp(dx + snapDelta.dx, minDx, maxDx); |
| | | dy = clamp(dy + snapDelta.dy, minDy, maxDy); |
| | | for (var j = 0; j < state.baseItems.length; j++) { |
| | | var baseItem = state.baseItems[j]; |
| | | var element = this.findElementById(baseItem.id); |
| | | if (!element) { |
| | | continue; |
| | | } |
| | | element.x = roundCoord(baseItem.x + dx); |
| | | element.y = roundCoord(baseItem.y + dy); |
| | | if (element.type === 'annulus' && baseItem.turningPoint) { |
| | | if (!element.turningPoint) element.turningPoint = {}; |
| | | element.turningPoint.x = baseItem.turningPoint.x + dx; |
| | | element.turningPoint.y = baseItem.turningPoint.y + dy; |
| | | } |
| | | } |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'resize') { |
| | | var target = this.findElementById(state.elementId); |
| | | if (!target) { |
| | | return; |
| | | } |
| | | if (state.handle === 'turningPoint') { |
| | | var deltaX = point.x - state.startWorld.x; |
| | | var deltaY = point.y - state.startWorld.y; |
| | | var newTurningPoint = { |
| | | x: roundCoord(state.baseRect.baseTurningPoint.x + deltaX), |
| | | y: roundCoord(state.baseRect.baseTurningPoint.y + deltaY) |
| | | }; |
| | | // 边界约束:拐点必须在矩形内部,且不能导致臂长小于 MIN_ELEMENT_SIZE |
| | | var minX = target.x + MIN_ELEMENT_SIZE; |
| | | var maxX = target.x + target.width - MIN_ELEMENT_SIZE; |
| | | var minY = target.y + MIN_ELEMENT_SIZE; |
| | | var maxY = target.y + target.height - MIN_ELEMENT_SIZE; |
| | | newTurningPoint.x = clamp(newTurningPoint.x, minX, maxX); |
| | | newTurningPoint.y = clamp(newTurningPoint.y, minY, maxY); |
| | | // 额外保证两条臂至少为 MIN_ELEMENT_SIZE |
| | | var leftArm = newTurningPoint.x - target.x; |
| | | var rightArm = target.x + target.width - newTurningPoint.x; |
| | | var topArm = newTurningPoint.y - target.y; |
| | | var bottomArm = target.y + target.height - newTurningPoint.y; |
| | | if (leftArm < MIN_ELEMENT_SIZE) { |
| | | newTurningPoint.x = target.x + MIN_ELEMENT_SIZE; |
| | | } |
| | | if (rightArm < MIN_ELEMENT_SIZE) { |
| | | newTurningPoint.x = target.x + target.width - MIN_ELEMENT_SIZE; |
| | | } |
| | | if (topArm < MIN_ELEMENT_SIZE) { |
| | | newTurningPoint.y = target.y + MIN_ELEMENT_SIZE; |
| | | } |
| | | if (bottomArm < MIN_ELEMENT_SIZE) { |
| | | newTurningPoint.y = target.y + target.height - MIN_ELEMENT_SIZE; |
| | | } |
| | | const isStillHalf = G.getIsStillHalf({ |
| | | ...target, |
| | | turningPoint: { |
| | | x: newTurningPoint.x, |
| | | y: newTurningPoint.y |
| | | } |
| | | }); |
| | | if (!isStillHalf) { |
| | | return; |
| | | } |
| | | target.turningPoint = { |
| | | x: newTurningPoint.x, |
| | | y: newTurningPoint.y |
| | | }; |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | |
| | | var baseRect = state.baseRect; |
| | | var left = baseRect.x; |
| | | var right = baseRect.x + baseRect.width; |
| | | var top = baseRect.y; |
| | | var bottom = baseRect.y + baseRect.height; |
| | | if (state.handle.indexOf('w') >= 0) { |
| | | left = clamp(point.x, 0, right - MIN_ELEMENT_SIZE); |
| | | } |
| | | if (state.handle.indexOf('e') >= 0) { |
| | | right = clamp(point.x, left + MIN_ELEMENT_SIZE, this.doc.canvasWidth); |
| | | } |
| | | if (state.handle.indexOf('n') >= 0) { |
| | | top = clamp(point.y, 0, bottom - MIN_ELEMENT_SIZE); |
| | | } |
| | | if (state.handle.indexOf('s') >= 0) { |
| | | bottom = clamp(point.y, top + MIN_ELEMENT_SIZE, this.doc.canvasHeight); |
| | | } |
| | | var snapped = this.collectResizeSnap( |
| | | { |
| | | x: left, |
| | | y: top, |
| | | width: right - left, |
| | | height: bottom - top |
| | | }, |
| | | state.handle, |
| | | [target.id] |
| | | ); |
| | | if (snapped) { |
| | | if (state.handle.indexOf('w') >= 0 && snapped.left != null) { |
| | | left = clamp(left + snapped.left, 0, right - MIN_ELEMENT_SIZE); |
| | | } |
| | | if (state.handle.indexOf('e') >= 0 && snapped.right != null) { |
| | | right = clamp(right + snapped.right, left + MIN_ELEMENT_SIZE, this.doc.canvasWidth); |
| | | } |
| | | if (state.handle.indexOf('n') >= 0 && snapped.top != null) { |
| | | top = clamp(top + snapped.top, 0, bottom - MIN_ELEMENT_SIZE); |
| | | } |
| | | if (state.handle.indexOf('s') >= 0 && snapped.bottom != null) { |
| | | bottom = clamp( |
| | | bottom + snapped.bottom, |
| | | top + MIN_ELEMENT_SIZE, |
| | | this.doc.canvasHeight |
| | | ); |
| | | } |
| | | } |
| | | if (target.type === 'annulus' && target.shape !== 'rect') { |
| | | const isStillHalf = G.getIsStillHalf({ |
| | | ...target, |
| | | x: roundCoord(left), |
| | | y: roundCoord(top), |
| | | width: roundCoord(right - left), |
| | | height: roundCoord(bottom - top) |
| | | }); |
| | | if (!isStillHalf) { |
| | | return; |
| | | } |
| | | } |
| | | // 防止边越过 turningPoint 导致破坏 L 型结构,限制边距离 turningPoint 至少 MIN_ELEMENT_SIZE |
| | | if (target.type === 'annulus' && target.shape !== 'rect' && target.turningPoint) { |
| | | if (state.handle.indexOf('w') >= 0) { |
| | | left = Math.min(left, target.turningPoint.x - MIN_ELEMENT_SIZE); |
| | | } |
| | | if (state.handle.indexOf('e') >= 0) { |
| | | right = Math.max(right, target.turningPoint.x + MIN_ELEMENT_SIZE); |
| | | } |
| | | if (state.handle.indexOf('n') >= 0) { |
| | | top = Math.min(top, target.turningPoint.y - MIN_ELEMENT_SIZE); |
| | | } |
| | | if (state.handle.indexOf('s') >= 0) { |
| | | bottom = Math.max(bottom, target.turningPoint.y + MIN_ELEMENT_SIZE); |
| | | } |
| | | } |
| | | |
| | | target.x = roundCoord(left); |
| | | target.y = roundCoord(top); |
| | | target.width = roundCoord(right - left); |
| | | target.height = roundCoord(bottom - top); |
| | | this.scheduleRender(); |
| | | } |
| | | }, |
| | | onWindowPointerUp: function (event) { |
| | | if (!this.interactionState) { |
| | | return; |
| | | } |
| | | if ( |
| | | this.currentPointerId != null && |
| | | event.pointerId != null && |
| | | this.currentPointerId !== event.pointerId |
| | | ) { |
| | | return; |
| | | } |
| | | if (this.pixiApp && this.pixiApp.view.releasePointerCapture && event.pointerId != null) { |
| | | try { |
| | | this.pixiApp.view.releasePointerCapture(event.pointerId); |
| | | } catch (ignore) {} |
| | | } |
| | | this.currentPointerId = null; |
| | | |
| | | var state = this.interactionState; |
| | | this.interactionState = null; |
| | | |
| | | if (state.type === 'pan') { |
| | | this.updateCursor(); |
| | | this.schedulePanRefresh(); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'marquee') { |
| | | var rect = buildRectFromPoints(state.startWorld, state.currentWorld); |
| | | if (rect.width > 2 && rect.height > 2) { |
| | | var matched = (this.doc.elements || []) |
| | | .filter(function (item) { |
| | | return rectIntersects(rect, item); |
| | | }) |
| | | .map(function (item) { |
| | | return item.id; |
| | | }); |
| | | this.setSelectedIds( |
| | | state.additive ? Array.from(new Set(this.selectedIds.concat(matched))) : matched |
| | | ); |
| | | } |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'movePending') { |
| | | this.updateCursor(); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'draw') { |
| | | var drawRect = state.rect; |
| | | if ( |
| | | drawRect && |
| | | drawRect.width >= MIN_ELEMENT_SIZE && |
| | | drawRect.height >= MIN_ELEMENT_SIZE |
| | | ) { |
| | | var newElement = { |
| | | id: nextId(), |
| | | type: state.elementType, |
| | | x: roundCoord(drawRect.x), |
| | | y: roundCoord(drawRect.y), |
| | | width: roundCoord(drawRect.width), |
| | | height: roundCoord(drawRect.height), |
| | | value: '', |
| | | shape: drawRect.shape, |
| | | pathList: drawRect.pathList |
| | | }; |
| | | if (isDeviceConfigType(newElement.type)) { |
| | | newElement.value = JSON.stringify({ |
| | | trackId: this.getNextDeviceTrackId(null), |
| | | deviceList: [ |
| | | { |
| | | valueKey: '', |
| | | deviceNo: '', |
| | | progress: 0 |
| | | } |
| | | ] |
| | | }); |
| | | } |
| | | if (this.hasOverlap(newElement, [])) { |
| | | this.showMessage('warning', '新元素不能与已有元素重叠'); |
| | | } else if (!this.isWithinCanvas(newElement)) { |
| | | this.showMessage('warning', '新元素超出画布范围'); |
| | | } else { |
| | | this.doc.elements.push(newElement); |
| | | this.selectedIds = [newElement.id]; |
| | | this.commitMutation(state.beforeSnapshot); |
| | | this.refreshInspector(); |
| | | return; |
| | | } |
| | | } |
| | | this.refreshInspector(); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | if (state.type === 'array') { |
| | | var copies = |
| | | state.previewItems && state.previewItems.length |
| | | ? state.previewItems |
| | | : this.buildArrayCopies( |
| | | state.template, |
| | | state.startWorld, |
| | | state.currentWorld || state.startWorld |
| | | ); |
| | | if (!copies.length) { |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | if (!this.canPlaceElements(copies, [])) { |
| | | this.showMessage('warning', '阵列生成后会重叠或超出画布,已取消'); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | var finalizedCopies = copies.map(function (item) { |
| | | return $.extend({}, item, { id: nextId() }); |
| | | }); |
| | | var self = this; |
| | | this.runMutation(function () { |
| | | self.doc.elements = self.doc.elements.concat(finalizedCopies); |
| | | self.selectedIds = [finalizedCopies[finalizedCopies.length - 1].id]; |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'move') { |
| | | var movedElements = this.getSelectedElements(); |
| | | if (!this.canPlaceElements(movedElements, this.selectedIds.slice())) { |
| | | for (var i = 0; i < state.baseItems.length; i++) { |
| | | var base = state.baseItems[i]; |
| | | var element = this.findElementById(base.id); |
| | | if (!element) { |
| | | continue; |
| | | } |
| | | element.x = base.x; |
| | | element.y = base.y; |
| | | if (base.turningPoint) { |
| | | element.turningPoint = { |
| | | x: base.turningPoint.x, |
| | | y: base.turningPoint.y |
| | | }; |
| | | } else if (element.turningPoint) { |
| | | delete element.turningPoint; |
| | | } |
| | | } |
| | | this.showMessage('warning', '移动后会重叠或超出画布,已恢复'); |
| | | this.refreshInspector(); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | if (!this.commitMutation(state.beforeSnapshot)) { |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | if (state.type === 'resize') { |
| | | var resized = this.findElementById(state.elementId); |
| | | if (resized) { |
| | | if (!this.isWithinCanvas(resized) || this.hasOverlap(resized, [resized.id])) { |
| | | resized.x = state.baseRect.x; |
| | | resized.y = state.baseRect.y; |
| | | resized.width = state.baseRect.width; |
| | | resized.height = state.baseRect.height; |
| | | if (state.baseTurningPoint) { |
| | | resized.turningPoint = { |
| | | x: state.baseTurningPoint.x, |
| | | y: state.baseTurningPoint.y |
| | | }; |
| | | } else if (resized.turningPoint) { |
| | | delete resized.turningPoint; |
| | | } |
| | | this.showMessage('warning', '缩放后会重叠或超出画布,已恢复'); |
| | | this.refreshInspector(); |
| | | this.scheduleRender(); |
| | | return; |
| | | } |
| | | } |
| | | if (!this.commitMutation(state.beforeSnapshot)) { |
| | | this.markStaticSceneDirty(); |
| | | this.scheduleRender(); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | this.scheduleRender(); |
| | | }, |
| | | onWindowKeyDown: function (event) { |
| | | if (event.key === ' ' && !isInputLike(event.target)) { |
| | | this.spacePressed = true; |
| | | this.updateCursor(); |
| | | event.preventDefault(); |
| | | } |
| | | if (!this.doc) { |
| | | return; |
| | | } |
| | | if (isInputLike(event.target)) { |
| | | return; |
| | | } |
| | | var ctrl = event.ctrlKey || event.metaKey; |
| | | if (event.key === 'Delete' || event.key === 'Backspace') { |
| | | event.preventDefault(); |
| | | this.deleteSelection(); |
| | | return; |
| | | } |
| | | if (ctrl && (event.key === 'z' || event.key === 'Z')) { |
| | | event.preventDefault(); |
| | | if (event.shiftKey) { |
| | | this.redo(); |
| | | } else { |
| | | this.undo(); |
| | | } |
| | | return; |
| | | } |
| | | if (ctrl && (event.key === 'y' || event.key === 'Y')) { |
| | | event.preventDefault(); |
| | | this.redo(); |
| | | return; |
| | | } |
| | | if (ctrl && (event.key === 'c' || event.key === 'C')) { |
| | | event.preventDefault(); |
| | | this.copySelection(); |
| | | return; |
| | | } |
| | | if (ctrl && (event.key === 'v' || event.key === 'V')) { |
| | | event.preventDefault(); |
| | | this.pasteClipboard(); |
| | | return; |
| | | } |
| | | if (event.key === 'Escape') { |
| | | this.interactionState = null; |
| | | this.setSelectedIds([]); |
| | | this.hoverElementId = ''; |
| | | this.scheduleRender(); |
| | | } |
| | | }, |
| | | onWindowKeyUp: function (event) { |
| | | if (event.key === ' ') { |
| | | this.spacePressed = false; |
| | | this.updateCursor(); |
| | | } |
| | | }, |
| | | onBeforeUnload: function (event) { |
| | | if (!this.isDirty) { |
| | | return; |
| | | } |
| | | event.preventDefault(); |
| | | event.returnValue = ''; |
| | | } |
| | | } |
| | | }); |
| | | })(); |