const EPSILON = 1e-9; // 容差,用于处理浮点数精度问题
|
|
const nowMs = () =>
|
typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
|
|
const G = window.BasMapTrackGeometry;
|
if (!G) {
|
throw new Error('mapTrackGeometry.js must be loaded before MapCanvas.js');
|
}
|
|
// 仅本地测试用
|
const FAKE_MAX_LAYER = 1730000;
|
const FAKE_MAX_CRN_LAYER = 4800
|
/**
|
* 通用轮询器类
|
* 封装带请求取消、平滑延迟计算、错误退避的轮询逻辑
|
*/
|
class Poller {
|
/**
|
* @param {Object} options
|
* @param {Function} options.fetchFn - 数据获取函数。可返回 Promise<number> 作为本次耗时(ms)覆盖默认计时;否则由 Poller 自动计时
|
* @param {number} [options.periodMs=1000] - 目标轮询周期(毫秒)
|
* @param {number} [options.alpha=0.2] - EWMA平滑系数
|
*/
|
constructor(options) {
|
this.fetchFn = options.fetchFn;
|
this.periodMs = options.periodMs || 1000;
|
this.alpha = options.alpha || 0.2;
|
|
this.timer = null;
|
this.abortController = null;
|
this.inFlight = false;
|
this.ewmaMs = 0;
|
this.errorBackoffMs = 0;
|
}
|
|
start() {
|
if (this.timer) return;
|
this.errorBackoffMs = 0;
|
this.scheduleNext(0);
|
}
|
|
stop() {
|
if (this.timer) {
|
clearTimeout(this.timer);
|
this.timer = null;
|
}
|
if (this.abortController) {
|
try {
|
this.abortController.abort();
|
} catch (e) {}
|
this.abortController = null;
|
}
|
this.inFlight = false;
|
}
|
|
scheduleNext(delayMs) {
|
if (this.timer) clearTimeout(this.timer);
|
this.timer = setTimeout(() => this.pollOnce(), Math.max(0, delayMs));
|
}
|
|
computeNextDelay(lastDurationMs, hadError) {
|
const minMs = 0;
|
const maxMs = this.periodMs;
|
|
if (hadError) {
|
this.errorBackoffMs = this.periodMs;
|
return this.periodMs;
|
}
|
|
this.errorBackoffMs = 0;
|
|
const duration = Number.isFinite(lastDurationMs) ? lastDurationMs : 0;
|
this.ewmaMs = this.ewmaMs ? (1 - this.alpha) * this.ewmaMs + this.alpha * duration : duration;
|
|
const targetDelay = this.periodMs - this.ewmaMs;
|
const jitter = Math.floor(60 * (Math.random() - 0.5)); // +/- 30ms
|
return Math.min(maxMs, Math.max(minMs, Math.floor(targetDelay + jitter)));
|
}
|
|
async pollOnce() {
|
if (this.inFlight) return;
|
this.inFlight = true;
|
let hadError = false;
|
let durationMs = 0;
|
|
try {
|
const t0 = nowMs();
|
const ret = await this.fetchFn(this);
|
const t1 = nowMs();
|
durationMs = Number.isFinite(ret) ? ret : Math.max(0, Math.floor(t1 - t0));
|
} catch (e) {
|
hadError = true;
|
} finally {
|
this.inFlight = false;
|
const nextDelay = this.computeNextDelay(durationMs, hadError);
|
this.scheduleNext(nextDelay);
|
}
|
}
|
}
|
|
Vue.component('map-canvas', {
|
template: `
|
<div style="width: 100%; height: 100%; position: relative;">
|
<div ref="pixiView" style="position: absolute; inset: 0;"></div>
|
<div :style="cycleCapacityPanelStyle()">
|
<div style="display: flex; flex-direction: column; gap: 6px; align-items: flex-start;">
|
<div v-for="item in cycleCapacity.loopList"
|
:key="'loop-' + item.loopNo"
|
@mouseenter="handleLoopCardEnter(item)"
|
@mouseleave="handleLoopCardLeave(item)"
|
style="padding: 6px 10px; border-radius: 4px; background: rgba(11, 35, 58, 0.72); color: #fff; font-size: 12px; line-height: 1.4; white-space: nowrap; pointer-events: auto;">
|
圈{{ item.loopNo }} |
|
站点: {{ item.stationCount || 0 }} |
|
任务: {{ item.taskCount || 0 }} |
|
承载: {{ formatLoadPercent(item.currentLoad) }}
|
</div>
|
</div>
|
</div>
|
<div v-show="shelfTooltip.visible"
|
:style="shelfTooltipStyle()">
|
{{ shelfTooltip.text }}
|
</div>
|
<div style="position: absolute; top: 18px; right: 34px; z-index: 30; display: flex; flex-direction: column; align-items: flex-end; gap: 8px;">
|
<div :style="mapToolFpsStyle()" id="map-fps">FPS {{ mapFps }}</div>
|
<button type="button" @click="toggleMapToolPanel" :style="mapToolToggleStyle(showMapToolPanel)">{{ showMapToolPanel ? '收起操作' : '地图操作' }}</button>
|
<div v-show="showMapToolPanel" :style="mapToolBarStyle()">
|
<div :style="mapToolRowStyle()">
|
<button type="button" @click="toggleStationDirection" :style="mapToolButtonStyle(showStationDirection)">{{ showStationDirection ? '隐藏站点方向' : '显示站点方向' }}</button>
|
<button type="button" @click="resetMapView" :style="mapToolButtonStyle(false)">重置视图</button>
|
<button type="button" @click="rotateMap" :style="mapToolButtonStyle(false)">旋转</button>
|
<button type="button" @click="toggleMirror" :style="mapToolButtonStyle(mapMirrorX)">{{ mapMirrorX ? '取消镜像' : '镜像' }}</button>
|
</div>
|
<div :style="mapToolRowStyle()">
|
<button type="button" @click="openStationColorConfigPage" :style="mapToolButtonStyle(false)">站点颜色</button>
|
<button v-if="fakeOperationVisible" type="button" @click="openFakeOperationConfigPage" :style="mapToolButtonStyle(false)">仿真操作</button>
|
</div>
|
<div v-if="levList && levList.length > 1" :style="mapToolFloorSectionStyle()">
|
<div :style="mapToolSectionLabelStyle()">楼层</div>
|
<div :style="mapToolFloorListStyle()">
|
<button
|
v-for="floor in levList"
|
:key="'tool-floor-' + floor"
|
type="button"
|
@click="selectFloorFromTool(floor)"
|
:style="mapToolFloorButtonStyle(currentLev == floor)"
|
>{{ floor }}F</button>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
`,
|
props: [
|
'lev',
|
'levList',
|
'crnParam',
|
'rgvParam',
|
'devpParam',
|
'stationTaskRange',
|
'highlightOnParamChange',
|
'viewportPadding',
|
'hudPadding'
|
],
|
data() {
|
return {
|
map: [],
|
currentLev: 1,
|
mapFps: 0,
|
ws: null,
|
wsReconnectTimer: null,
|
wsReconnectAttempts: 0,
|
wsReconnectBaseDelay: 1000,
|
wsReconnectMaxDelay: 15000,
|
annulusPoller: null,
|
pixiApp: null,
|
pixiStageList: [],
|
pixiStaMap: new Map(),
|
pixiCrnMap: new Map(),
|
pixiDualCrnMap: new Map(),
|
pixiRgvMap: new Map(),
|
mapRoot: null,
|
mapRotation: 0,
|
mapMirrorX: false,
|
mapContentSize: { width: 0, height: 0 },
|
mapConfigCodes: {
|
rotate: 'map_canvas_rotation',
|
mirror: 'map_canvas_mirror_x'
|
},
|
pixiShelfMap: new Map(),
|
pixiTrackMap: new Map(),
|
pixiDevpTextureMap: new Map(),
|
pixiCrnColorTextureMap: new Map(),
|
pixiRgvColorTextureMap: new Map(),
|
shelfChunkList: [],
|
shelfChunkSize: 2048,
|
shelfCullPadding: 160,
|
shelfCullRaf: null,
|
locListMap: new Map(),
|
locListLoaded: false,
|
locListLoading: false,
|
mapRowOffsets: [],
|
mapRowHeights: [],
|
mapColOffsets: [],
|
mapColWidths: [],
|
mapRowColOffsets: [],
|
mapRowColWidths: [],
|
mapRowShelfCells: [],
|
hoveredShelfCell: null,
|
hoverPointer: { x: 0, y: 0 },
|
hoverRaf: null,
|
objectsContainer: null,
|
objectsContainer2: null,
|
tracksContainer: null,
|
tracksGraphics: null,
|
shelvesContainer: null,
|
graphicsCrn: null,
|
// graphicsCrnTrack: null,
|
// graphicsRgvTrack: null,
|
graphicsRgv: null,
|
shelfTooltip: {
|
visible: false,
|
x: 0,
|
y: 0,
|
text: '',
|
item: null
|
},
|
shelfTooltipMinScale: 0.4,
|
containerResizeObserver: null,
|
resizeDebounceTimer: null,
|
timer: null,
|
adjustLabelTimer: null,
|
isSwitchingFloor: false,
|
cycleCapacity: {
|
loopList: [],
|
totalStationCount: 0,
|
taskStationCount: 0,
|
currentLoad: 0
|
},
|
showMapToolPanel: false,
|
showStationDirection: false,
|
hoverLoopNo: null,
|
hoverLoopStationIdSet: new Set(),
|
loopHighlightColor: 0xfff34d,
|
stationDirectionColor: 0xff5a36,
|
stationStatusColors: {
|
'site-auto': 0x78ff81,
|
'site-auto-run': 0xfa51f6,
|
'site-auto-id': 0xc4c400,
|
'site-auto-run-id': 0x30bffc,
|
'site-enable-in': 0xa81dee,
|
'site-unauto': 0xb8b8b8,
|
'machine-pakin': 0x30bffc,
|
'machine-pakout': 0x97b400,
|
'site-run-block': 0xe69138,
|
'site-error': 0xdb2828
|
},
|
fakeOperationVisible: false
|
};
|
},
|
mounted() {
|
this.DEVICE_MAP = {
|
crn: {
|
createTexture: this.createCrnTexture,
|
graphics: this.graphicsCrn,
|
emitName: 'crn-click',
|
pixiMap: this.pixiCrnMap,
|
type: 'crn',
|
idName: 'crnId',
|
statusInfo: {
|
name: 'crnStatus',
|
getStatus: this.getCrnStatusColor,
|
updateTextureColor: this.updateCrnTextureColor
|
}
|
},
|
dualcrn: {
|
createTexture: this.createCrnTexture,
|
graphics: this.graphicsCrn,
|
emitName: 'dual-crn-click',
|
pixiMap: this.pixiDualCrnMap,
|
type: 'dualCrn',
|
idName: 'crnId',
|
statusInfo: {
|
name: 'crnStatus',
|
getStatus: this.getCrnStatusColor,
|
updateTextureColor: this.updateCrnTextureColor
|
}
|
},
|
rgv: {
|
createTexture: this.createRgvTexture,
|
graphics: this.graphicsRgv,
|
emitName: 'rgv-click',
|
pixiMap: this.pixiRgvMap,
|
type: 'rgv',
|
idName: 'rgvNo',
|
statusInfo: {
|
name: 'rgvStatus',
|
getStatus: this.getRgvStatusColor,
|
updateTextureColor: this.updateRgvTextureColor
|
}
|
}
|
};
|
this.DEVICE_MAP.dualCrn = this.DEVICE_MAP.dualcrn;
|
this.currentLev = this.lev || 1;
|
this.createMap();
|
this.startContainerResizeObserve();
|
this.loadMapTransformConfig();
|
this.loadStationColorConfig();
|
this.loadFakeProcessStatus();
|
this.loadLocList();
|
this.connectWs();
|
// 轨道依赖 /basMap/editor 的 map2;须在主地图 createMapData 完成后再拉取并 drawTracks,
|
// 否则 WebSocket 先到会 clear tracksGraphics,把先返回的编辑器轨道擦掉。
|
|
this.timer = setInterval(() => {
|
this.getCrnInfo();
|
this.getDualCrnInfo();
|
this.getSiteInfo();
|
this.getCycleCapacityInfo();
|
this.getRgvInfo();
|
}, 1000);
|
|
setTimeout(()=>{
|
this.startAnnulusDevicePoll();
|
},1000)
|
|
// todo:测试代码
|
setTimeout(() => {
|
this.createFakeButton();
|
}, 1000);
|
},
|
beforeDestroy() {
|
if (this.timer) {
|
clearInterval(this.timer);
|
}
|
this.stopAnnulusDevicePoll();
|
|
if (this.hoverRaf) {
|
cancelAnimationFrame(this.hoverRaf);
|
this.hoverRaf = null;
|
}
|
if (this.shelfCullRaf) {
|
cancelAnimationFrame(this.shelfCullRaf);
|
this.shelfCullRaf = null;
|
}
|
if (this.resizeDebounceTimer) {
|
clearTimeout(this.resizeDebounceTimer);
|
this.resizeDebounceTimer = null;
|
}
|
if (window.gsap && this.pixiApp && this.pixiApp.stage) {
|
window.gsap.killTweensOf(this.pixiApp.stage.position);
|
}
|
if (this.pixiApp) {
|
this.pixiApp.destroy(true, { children: true });
|
}
|
if (this.containerResizeObserver) {
|
this.containerResizeObserver.disconnect();
|
this.containerResizeObserver = null;
|
}
|
window.removeEventListener('resize', this.scheduleResizeToContainer);
|
if (this.wsReconnectTimer) {
|
clearTimeout(this.wsReconnectTimer);
|
this.wsReconnectTimer = null;
|
}
|
if (
|
this.ws &&
|
(this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)
|
) {
|
try {
|
this.ws.close();
|
} catch (e) {}
|
}
|
},
|
watch: {
|
lev(newLev) {
|
if (newLev != null) {
|
this.changeFloor(newLev);
|
}
|
},
|
viewportPadding: {
|
deep: true,
|
handler(newVal, oldVal) {
|
if (
|
this.mapContentSize &&
|
this.mapContentSize.width > 0 &&
|
this.mapContentSize.height > 0
|
) {
|
this.adjustStageForViewportPadding(oldVal, newVal);
|
}
|
}
|
},
|
crnParam: {
|
deep: true,
|
handler(v) {
|
if (!this.highlightOnParamChange || !v || !v.crnNo || !this.pixiCrnMap) {
|
return;
|
}
|
this.highlightParamSpriteWithGsap(this.pixiCrnMap.get(parseInt(v.crnNo, 10)));
|
}
|
},
|
rgvParam: {
|
deep: true,
|
handler(v) {
|
if (!this.highlightOnParamChange || !v || !v.rgvNo || !this.pixiRgvMap) {
|
return;
|
}
|
this.highlightParamSpriteWithGsap(this.pixiRgvMap.get(parseInt(v.rgvNo, 10)));
|
}
|
},
|
devpParam: {
|
deep: true,
|
handler(v) {
|
if (!this.highlightOnParamChange || !v || !v.stationId || !this.pixiStaMap) {
|
return;
|
}
|
this.highlightParamSpriteWithGsap(this.pixiStaMap.get(parseInt(v.stationId, 10)));
|
}
|
}
|
},
|
methods: {
|
highlightParamSpriteWithGsap(sprite) {
|
if (!sprite || !window.gsap) {
|
return;
|
}
|
window.gsap.killTweensOf(sprite);
|
window.gsap.fromTo(
|
sprite,
|
{ alpha: 1 },
|
{ alpha: 0.2, yoyo: true, repeat: 6, duration: 0.15 }
|
);
|
},
|
/**
|
* 切换楼层或重载地图前的共用清场(可选:对设备精灵 kill GSAP)。
|
* @param {{ killDeviceGsap?: boolean, skipClearLoopHighlight?: boolean }} options
|
*/
|
clearMap() {
|
// const opts = options || {};
|
// if (!opts.skipClearLoopHighlight) {
|
// this.clearLoopStationHighlight();
|
// }
|
this.hideShelfTooltip();
|
this.hoveredShelfCell = null;
|
this.mapRowOffsets = [];
|
this.mapRowHeights = [];
|
this.mapColOffsets = [];
|
this.mapColWidths = [];
|
if (this.adjustLabelTimer) {
|
clearTimeout(this.adjustLabelTimer);
|
this.adjustLabelTimer = null;
|
}
|
if (window.gsap) {
|
[this.pixiStaMap].forEach((m) => {
|
m &&
|
m.forEach((s) => {
|
try {
|
window.gsap.killTweensOf(s);
|
} catch (e) {}
|
});
|
});
|
}
|
this.objectsContainer.removeChildren();
|
this.clearShelfChunks();
|
this.pixiStaMap = new Map();
|
this.pixiStageList = [];
|
},
|
clearMap2() {
|
if (window.gsap) {
|
[this.pixiCrnMap, this.pixiDualCrnMap, this.pixiRgvMap].forEach((m) => {
|
m &&
|
m.forEach((s) => {
|
try {
|
window.gsap.killTweensOf(s);
|
} catch (e) {}
|
});
|
});
|
}
|
this.objectsContainer2.removeChildren();
|
if (this.tracksContainer) {
|
this.tracksContainer.removeChildren();
|
}
|
if (this.tracksGraphics) {
|
this.tracksGraphics.clear();
|
}
|
this.pixiCrnMap.clear();
|
this.pixiDualCrnMap.clear();
|
this.pixiRgvMap.clear();
|
},
|
cycleCapacityPanelStyle() {
|
const hud = this.hudPadding || {};
|
const left = Math.max(14, Number(hud.left) || 0);
|
const rightReserve = 220;
|
return {
|
position: 'absolute',
|
top: '12px',
|
left: left + 'px',
|
zIndex: 30,
|
pointerEvents: 'none',
|
maxWidth: 'calc(100% - ' + (left + rightReserve) + 'px)'
|
};
|
},
|
mapToolBarStyle() {
|
return {
|
display: 'flex',
|
flexDirection: 'column',
|
gap: '8px',
|
alignItems: 'stretch',
|
padding: '7px',
|
borderRadius: '14px',
|
background: 'rgba(255, 255, 255, 0.72)',
|
border: '1px solid rgba(160, 180, 205, 0.3)',
|
boxShadow: '0 8px 20px rgba(37, 64, 97, 0.08)',
|
backdropFilter: 'blur(4px)'
|
};
|
},
|
mapToolRowStyle() {
|
return {
|
display: 'flex',
|
gap: '8px',
|
alignItems: 'center',
|
justifyContent: 'flex-end',
|
flexWrap: 'wrap'
|
};
|
},
|
mapToolFloorSectionStyle() {
|
return {
|
display: 'flex',
|
flexDirection: 'column',
|
gap: '4px',
|
paddingTop: '6px',
|
borderTop: '1px solid rgba(160, 180, 205, 0.22)'
|
};
|
},
|
mapToolSectionLabelStyle() {
|
return {
|
color: '#6a7f95',
|
fontSize: '10px',
|
lineHeight: '14px',
|
textAlign: 'right'
|
};
|
},
|
mapToolFloorListStyle() {
|
return {
|
display: 'flex',
|
flexDirection: 'column',
|
gap: '4px',
|
alignItems: 'stretch'
|
};
|
},
|
mapToolFpsStyle() {
|
return {
|
padding: '4px 10px',
|
borderRadius: '999px',
|
background: 'rgba(255, 255, 255, 0.7)',
|
border: '1px solid rgba(160, 180, 205, 0.28)',
|
color: '#48617c',
|
fontSize: '12px',
|
lineHeight: '18px',
|
letterSpacing: '0.04em',
|
boxShadow: '0 6px 16px rgba(37, 64, 97, 0.06)',
|
userSelect: 'none'
|
};
|
},
|
mapToolToggleStyle(active) {
|
return {
|
appearance: 'none',
|
border: '1px solid ' + (active ? 'rgba(96, 132, 170, 0.36)' : 'rgba(160, 180, 205, 0.3)'),
|
background: active ? 'rgba(235, 243, 251, 0.96)' : 'rgba(255, 255, 255, 0.82)',
|
color: '#46617b',
|
height: '30px',
|
padding: '0 12px',
|
borderRadius: '999px',
|
fontSize: '12px',
|
lineHeight: '30px',
|
cursor: 'pointer',
|
whiteSpace: 'nowrap',
|
boxShadow: '0 6px 16px rgba(37, 64, 97, 0.06)'
|
};
|
},
|
mapToolButtonStyle(active) {
|
return {
|
appearance: 'none',
|
border: '1px solid ' + (active ? 'rgba(255, 136, 93, 0.38)' : 'rgba(160, 180, 205, 0.3)'),
|
background: active ? 'rgba(255, 119, 77, 0.16)' : 'rgba(255, 255, 255, 0.88)',
|
color: active ? '#d85a31' : '#4d647d',
|
height: '30px',
|
padding: '0 12px',
|
borderRadius: '10px',
|
fontSize: '12px',
|
lineHeight: '30px',
|
cursor: 'pointer',
|
transition: 'all 0.2s ease',
|
boxShadow: active ? '0 4px 10px rgba(255, 119, 77, 0.12)' : 'none',
|
whiteSpace: 'nowrap'
|
};
|
},
|
mapToolFloorButtonStyle(active) {
|
return {
|
appearance: 'none',
|
border: '1px solid ' + (active ? 'rgba(96, 132, 170, 0.36)' : 'rgba(160, 180, 205, 0.3)'),
|
background: active ? 'rgba(235, 243, 251, 0.96)' : 'rgba(255, 255, 255, 0.88)',
|
color: active ? '#27425c' : '#4d647d',
|
minWidth: '44px',
|
height: '26px',
|
padding: '0 10px',
|
borderRadius: '8px',
|
fontSize: '11px',
|
lineHeight: '26px',
|
cursor: 'pointer',
|
fontWeight: '700',
|
boxShadow: active ? '0 4px 12px rgba(37, 64, 97, 0.08)' : 'none',
|
whiteSpace: 'nowrap'
|
};
|
},
|
toggleMapToolPanel() {
|
this.showMapToolPanel = !this.showMapToolPanel;
|
},
|
selectFloorFromTool(lev) {
|
if (lev == null || lev === this.currentLev) {
|
return;
|
}
|
this.$emit('switch-lev', lev);
|
},
|
createMap() {
|
this.pixiApp = new PIXI.Application({
|
backgroundColor: 0xf5f7f9,
|
antialias: false,
|
powerPreference: 'high-performance',
|
autoDensity: true,
|
resolution: Math.min(window.devicePixelRatio || 1, 2)
|
});
|
PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.LINEAR;
|
this.$refs.pixiView.appendChild(this.pixiApp.view);
|
this.pixiApp.view.style.width = '100%';
|
this.pixiApp.view.style.height = '100%';
|
this.pixiApp.view.style.display = 'block';
|
this.resizeToContainer();
|
window.addEventListener('resize', this.scheduleResizeToContainer);
|
// this.graphicsCrnTrack = this.createTrackTexture(25, 25, 10);
|
// this.graphicsRgvTrack = this.createTrackTexture(25, 25, 10);
|
this.objectsContainer = new PIXI.Container();
|
this.objectsContainer2 = new PIXI.Container();
|
this.tracksContainer = new PIXI.ParticleContainer(10000, {
|
scale: true,
|
position: true,
|
rotation: false,
|
uvs: false,
|
alpha: false
|
});
|
this.tracksGraphics = new PIXI.Graphics();
|
this.shelvesContainer = new PIXI.Container();
|
this.tracksContainer.autoResize = true;
|
this.mapRoot = new PIXI.Container();
|
this.pixiApp.stage.addChild(this.mapRoot);
|
this.mapRoot.addChild(this.tracksGraphics);
|
this.mapRoot.addChild(this.tracksContainer);
|
this.mapRoot.addChild(this.shelvesContainer);
|
this.mapRoot.addChild(this.objectsContainer);
|
this.mapRoot.addChild(this.objectsContainer2);
|
this.pixiApp.renderer.roundPixels = true;
|
this.hoveredShelfCell = null;
|
this.hoverPointer = { x: 0, y: 0 };
|
this.hoverRaf = null;
|
|
//*******************shelf hover*******************
|
this.pixiApp.renderer.plugins.interaction.on('pointermove', (event) => {
|
if (!this.isShelfTooltipAllowed()) {
|
this.hideShelfTooltip();
|
return;
|
}
|
if (!this.map || !this.mapRoot) {
|
return;
|
}
|
const pos = event.data.global;
|
this.hoverPointer.x = pos.x;
|
this.hoverPointer.y = pos.y;
|
if (this.hoverRaf) {
|
return;
|
}
|
this.hoverRaf = requestAnimationFrame(() => {
|
this.hoverRaf = null;
|
this.updateShelfHoverFromPointer(this.hoverPointer);
|
});
|
});
|
this.pixiApp.view.addEventListener('mouseleave', () => {
|
this.hoveredShelfCell = null;
|
this.hideShelfTooltip();
|
});
|
//*******************shelf hover*******************
|
let stageOriginalPos;
|
let mouseDownPoint;
|
let touchBlank = false;
|
this.pixiApp.renderer.plugins.interaction.on('pointerdown', (event) => {
|
const globalPos = event.data.global;
|
stageOriginalPos = [this.pixiApp.stage.position.x, this.pixiApp.stage.position.y];
|
mouseDownPoint = [globalPos.x, globalPos.y];
|
if (!event.target || (event.target && event.target._kind === 'shelf')) {
|
touchBlank = true;
|
}
|
});
|
this.pixiApp.renderer.plugins.interaction.on('pointermove', (event) => {
|
const globalPos = event.data.global;
|
if (touchBlank) {
|
const dx = globalPos.x - mouseDownPoint[0];
|
const dy = globalPos.y - mouseDownPoint[1];
|
this.pixiApp.stage.position.set(stageOriginalPos[0] + dx, stageOriginalPos[1] + dy);
|
this.scheduleAdjustLabels();
|
this.scheduleShelfChunkCulling();
|
}
|
});
|
this.pixiApp.renderer.plugins.interaction.on('pointerup', () => {
|
touchBlank = false;
|
});
|
|
//*******************缩放画布*******************
|
this.pixiApp.view.addEventListener('wheel', (event) => {
|
event.stopPropagation();
|
event.preventDefault();
|
const rect = this.pixiApp.view.getBoundingClientRect();
|
const sx = event.clientX - rect.left;
|
const sy = event.clientY - rect.top;
|
const oldZoomX = this.pixiApp.stage.scale.x || 1;
|
const oldZoomY = this.pixiApp.stage.scale.y || 1;
|
const oldZoomAbs = Math.abs(oldZoomX) || 1;
|
const delta = event.deltaY;
|
let newZoomAbs = oldZoomAbs * 0.999 ** delta;
|
const mirrorX = this.mapMirrorX ? -1 : 1;
|
const newZoomX = mirrorX * newZoomAbs;
|
const newZoomY = newZoomAbs;
|
const worldX = (sx - this.pixiApp.stage.position.x) / oldZoomX;
|
const worldY = (sy - this.pixiApp.stage.position.y) / oldZoomY;
|
const newPosX = sx - worldX * newZoomX;
|
const newPosY = sy - worldY * newZoomY;
|
this.pixiApp.stage.setTransform(newPosX, newPosY, newZoomX, newZoomY, 0, 0, 0, 0, 0);
|
this.scheduleAdjustLabels();
|
this.scheduleShelfChunkCulling();
|
});
|
//*******************缩放画布*******************
|
|
//*******************FPS*******************
|
let g_Time = 0;
|
let fpsLastUpdateTs = 0;
|
let fpsDeltaSumMs = 0;
|
let fpsFrameCount = 0;
|
const fpsUpdateInterval = 200;
|
const fpsElement = document.getElementById('map-fps');
|
this.pixiApp.ticker.add((delta) => {
|
const timeNow = nowMs();
|
const timeDiff = timeNow - g_Time;
|
g_Time = timeNow;
|
fpsDeltaSumMs += timeDiff;
|
fpsFrameCount += 1;
|
if (timeNow - fpsLastUpdateTs >= fpsUpdateInterval) {
|
const avgFps = fpsDeltaSumMs > 0 ? (fpsFrameCount * 1000) / fpsDeltaSumMs : 0;
|
// this.mapFps = Math.round(avgFps);
|
// 不走vue,直接操作dom降低性能消耗
|
fpsElement.innerText = 'FPS ' + Math.round(avgFps);
|
fpsDeltaSumMs = 0;
|
fpsFrameCount = 0;
|
fpsLastUpdateTs = timeNow;
|
}
|
});
|
//*******************FPS*******************
|
},
|
startContainerResizeObserve() {
|
if (typeof ResizeObserver === 'undefined' || !this.$el) {
|
return;
|
}
|
this.containerResizeObserver = new ResizeObserver(() => {
|
this.scheduleResizeToContainer();
|
});
|
this.containerResizeObserver.observe(this.$el);
|
},
|
scheduleResizeToContainer() {
|
if (this.resizeDebounceTimer) {
|
clearTimeout(this.resizeDebounceTimer);
|
}
|
this.resizeDebounceTimer = setTimeout(() => {
|
this.resizeDebounceTimer = null;
|
this.resizeToContainer();
|
}, 80);
|
},
|
getViewportSize() {
|
if (!this.pixiApp || !this.pixiApp.renderer) {
|
return { width: 0, height: 0 };
|
}
|
const screen = this.pixiApp.renderer.screen;
|
if (screen && screen.width > 0 && screen.height > 0) {
|
return { width: screen.width, height: screen.height };
|
}
|
const rect = this.pixiApp.view ? this.pixiApp.view.getBoundingClientRect() : null;
|
return { width: rect ? rect.width : 0, height: rect ? rect.height : 0 };
|
},
|
getViewportPadding() {
|
return this.normalizeViewportPadding(this.viewportPadding);
|
},
|
normalizeViewportPadding(padding) {
|
const source = padding || {};
|
const normalize = (value) => {
|
const num = Number(value);
|
return isFinite(num) && num > 0 ? num : 0;
|
};
|
return {
|
top: normalize(source.top),
|
right: normalize(source.right),
|
bottom: normalize(source.bottom),
|
left: normalize(source.left)
|
};
|
},
|
getViewportCenter(viewport, padding) {
|
const normalized = this.normalizeViewportPadding(padding);
|
const availableW = Math.max(1, viewport.width - normalized.left - normalized.right);
|
const availableH = Math.max(1, viewport.height - normalized.top - normalized.bottom);
|
return {
|
x: normalized.left + availableW / 2,
|
y: normalized.top + availableH / 2
|
};
|
},
|
adjustStageForViewportPadding(oldPadding, newPadding) {
|
if (!this.pixiApp || !this.pixiApp.stage) {
|
return;
|
}
|
const viewport = this.getViewportSize();
|
if (viewport.width <= 0 || viewport.height <= 0) {
|
return;
|
}
|
const prevCenter = this.getViewportCenter(viewport, oldPadding);
|
const nextCenter = this.getViewportCenter(viewport, newPadding);
|
const deltaX = nextCenter.x - prevCenter.x;
|
const deltaY = nextCenter.y - prevCenter.y;
|
if (Math.abs(deltaX) < 0.5 && Math.abs(deltaY) < 0.5) {
|
return;
|
}
|
const targetX = this.pixiApp.stage.position.x + deltaX;
|
const targetY = this.pixiApp.stage.position.y + deltaY;
|
if (window.gsap) {
|
window.gsap.killTweensOf(this.pixiApp.stage.position);
|
window.gsap.to(this.pixiApp.stage.position, {
|
x: targetX,
|
y: targetY,
|
duration: 0.18,
|
ease: 'power1.out',
|
onUpdate: () => {
|
this.scheduleAdjustLabels();
|
this.scheduleShelfChunkCulling();
|
},
|
onComplete: () => {
|
this.scheduleAdjustLabels();
|
this.scheduleShelfChunkCulling();
|
}
|
});
|
return;
|
}
|
this.pixiApp.stage.position.x = targetX;
|
this.pixiApp.stage.position.y = targetY;
|
this.scheduleAdjustLabels();
|
this.scheduleShelfChunkCulling();
|
},
|
resizeToContainer() {
|
const w = this.$el.clientWidth || 0;
|
const h = this.$el.clientHeight || 0;
|
if (w > 0 && h > 0 && this.pixiApp) {
|
const vw =
|
this.pixiApp.renderer && this.pixiApp.renderer.screen
|
? this.pixiApp.renderer.screen.width
|
: 0;
|
const vh =
|
this.pixiApp.renderer && this.pixiApp.renderer.screen
|
? this.pixiApp.renderer.screen.height
|
: 0;
|
if (vw === w && vh === h) {
|
return;
|
}
|
this.pixiApp.renderer.resize(w, h);
|
if (
|
this.mapContentSize &&
|
this.mapContentSize.width > 0 &&
|
this.mapContentSize.height > 0
|
) {
|
this.applyMapTransform(true);
|
}
|
}
|
},
|
getMap() {
|
this.sendWs(
|
JSON.stringify({
|
url: '/basMap/lev/' + this.currentLev + '/auth',
|
data: {}
|
})
|
);
|
this.setMap2();
|
},
|
changeFloor(lev) {
|
this.currentLev = lev;
|
this.clearLoopStationHighlight();
|
this.isSwitchingFloor = true;
|
this.getMap();
|
},
|
setMap(res) {
|
this.clearMap();
|
this.createMapData(JSON.parse(res.data));
|
},
|
// 轨道在Map2中
|
setMap2() {
|
$.ajax({
|
url: baseUrl + `/basMap/editor/${this.currentLev}/auth`,
|
headers: { token: localStorage.getItem('token') },
|
method: 'get',
|
success: function (res) {
|
if (res && res.code === 200 && res.data && res.data.elements) {
|
this.clearMap2();
|
this.createMap2Data(
|
res.data.elements.filter((item) =>
|
['crn', 'dualCrn', 'rgv', 'annulus'].includes(item.type)
|
)
|
);
|
}
|
}.bind(this)
|
});
|
},
|
createMapData(map) {
|
this.pixiStageList = [map.length];
|
|
const bayHeightList = this.initHeight(map);
|
const bayWidthList = this.initWidth(map);
|
map.forEach((item, index) => {
|
for (let idx = 0; idx < item.length; idx++) {
|
let val = item[idx];
|
if (val.cellHeight == undefined || val.cellHeight === '') {
|
val.cellHeight = bayHeightList[index];
|
}
|
if (val.cellWidth == undefined || val.cellWidth === '') {
|
val.cellWidth = bayWidthList[idx];
|
}
|
}
|
});
|
|
map.forEach((item, index) => {
|
for (let idx = 0; idx < item.length; idx++) {
|
let val = item[idx];
|
let cellWidth = val.cellWidth / 40;
|
let cellHeight = val.cellHeight / 8;
|
val.width = cellWidth;
|
val.height = cellHeight;
|
let mergeHeight = cellHeight;
|
if (val.rowSpan > 1) {
|
for (let i = 1; i < val.rowSpan; i++) {
|
let nextMerge = map[index + i][idx];
|
if (nextMerge.type != 'merge') {
|
continue;
|
}
|
let mergeCellHeight = nextMerge.cellHeight / 8;
|
mergeHeight += mergeCellHeight;
|
}
|
val.height = mergeHeight;
|
}
|
let mergeWidth = cellWidth;
|
if (val.colSpan > 1) {
|
for (let i = 1; i < val.colSpan; i++) {
|
let nextMerge = map[index][idx + i];
|
if (!nextMerge) {
|
continue;
|
}
|
let mergeCellWidth = nextMerge.cellWidth / 40;
|
mergeWidth += mergeCellWidth;
|
nextMerge.isMergedPart = true;
|
}
|
val.width = mergeWidth;
|
}
|
}
|
});
|
|
//
|
const rowHeightScaled = [];
|
for (let r = 0; r < map.length; r++) {
|
const h = bayHeightList[r];
|
if (h != null && h !== -1) {
|
rowHeightScaled[r] = h / 8;
|
} else {
|
let fallback = 0;
|
for (let c = 0; c < map[r].length; c++) {
|
const v = map[r][c];
|
if (v && v.type !== 'merge' && v.height != null && v.height > 0) {
|
fallback = v.height;
|
break;
|
}
|
}
|
rowHeightScaled[r] = fallback > 0 ? fallback : 25;
|
}
|
}
|
let yOffsets = [];
|
let yCursor = 0;
|
for (let r = 0; r < map.length; r++) {
|
yOffsets[r] = yCursor;
|
yCursor += rowHeightScaled[r] || 0;
|
}
|
|
map.forEach((row, rowIndex) => {
|
let xCursor = 0;
|
let anchorX = 0;
|
for (let colIndex = 0; colIndex < row.length; colIndex++) {
|
let val = row[colIndex];
|
let cellWidth = val.width;
|
let cellHeight = val.height;
|
val.rowIndex = rowIndex;
|
val.colIndex = colIndex;
|
if (val.isMergedPart) {
|
val.posX = anchorX;
|
val.posY = yOffsets[rowIndex];
|
continue;
|
}
|
val.posX = xCursor;
|
val.posY = yOffsets[rowIndex];
|
anchorX = xCursor;
|
if (val.colSpan > 1) {
|
for (let i = 1; i < val.colSpan; i++) {
|
const next = row[colIndex + i];
|
if (!next) {
|
break;
|
}
|
next.posX = anchorX;
|
next.posY = yOffsets[rowIndex];
|
}
|
}
|
xCursor += cellWidth;
|
}
|
});
|
|
map.forEach((row, rowIndex) => {
|
for (let colIndex = 0; colIndex < row.length; colIndex++) {
|
const val = row[colIndex];
|
if (!val || val.type !== 'devp' || val.type === 'merge') {
|
continue;
|
}
|
val.stationDirectionList = this.resolveStationDirectionList(map, rowIndex, colIndex, val);
|
}
|
});
|
|
map.forEach((item, index) => {
|
this.pixiStageList[index] = [item.length];
|
for (let idx = 0; idx < item.length; idx++) {
|
let val = item[idx];
|
val.rowIndex = index;
|
val.colIndex = idx;
|
if (val.type === 'merge') {
|
continue;
|
}
|
if (val.type == undefined || val.type === 'none') {
|
continue;
|
}
|
if (val.type === 'shelf') {
|
continue;
|
}
|
if (['crn', 'dualCrn', 'rgv', 'annulus'].includes(val.type)) {
|
continue;
|
}
|
let sprite = this.getSprite(val, (e) => {});
|
if (sprite == null) {
|
continue;
|
}
|
this.objectsContainer.addChild(sprite);
|
this.pixiStageList[index][idx] = sprite;
|
}
|
});
|
|
let contentW = 0;
|
let contentH = 0;
|
for (let r = 0; r < map.length; r++) {
|
for (let c = 0; c < map[r].length; c++) {
|
const cell = map[r][c];
|
if (!cell || cell.type === 'merge') {
|
continue;
|
}
|
const right = cell.posX + cell.width;
|
const bottom = cell.posY + cell.height;
|
if (right > contentW) {
|
contentW = right;
|
}
|
if (bottom > contentH) {
|
contentH = bottom;
|
}
|
}
|
}
|
this.mapContentSize = { width: contentW, height: contentH };
|
this.buildShelfChunks(map, contentW, contentH);
|
this.buildShelfHitGrid(map, rowHeightScaled, yOffsets);
|
this.applyMapTransform(true);
|
this.map = map;
|
this.isSwitchingFloor = false;
|
},
|
createMap2Data(map2) {
|
this.map2 = map2;
|
|
const handleDevice = (deviceTypeInfo, device, trackInfo) => {
|
// 使用 G.getDeviceInfo 已经计算好的尺寸(已通过 normalizeDeviceSizeOverride 处理 deviceLength/deviceWidth)
|
const along = device.width;
|
const across = device.height;
|
// 每个设备独立创建纹理,不要缓存复用,否则所有设备尺寸会相同
|
const graphics = deviceTypeInfo.createTexture(along, across);
|
let sprite = new PIXI.Sprite(graphics);
|
// anchor 居中后子坐标原点在纹理中心,编号须用 (0,0),见 positionSpriteLabelToTextureCenter
|
sprite.anchor.set(0.5);
|
const deviceNo = device.deviceNo || device[deviceTypeInfo.idName];
|
const style = new PIXI.TextStyle({
|
fontFamily: 'Arial',
|
fontSize: 12,
|
fill: '#000000',
|
stroke: '#ffffff',
|
strokeThickness: 1,
|
align: 'center'
|
});
|
const txt = deviceNo != null ? String(deviceNo) : '';
|
|
const text = new PIXI.Text(txt, style);
|
text.anchor.set(0.5);
|
sprite.addChild(text);
|
sprite.textObj = text;
|
this.positionSpriteLabelToTextureCenter(sprite);
|
|
// 这里item已经是中心点了,直接用就行
|
device.width = sprite.width;
|
device.height = sprite.height;
|
if (['crn', 'dualCrn', 'rgv'].includes(trackInfo.type)) {
|
const isHorizontal = trackInfo.width > trackInfo.height;
|
const startX = isHorizontal ? trackInfo.x : trackInfo.width / 2 + trackInfo.x;
|
const startY = isHorizontal ? trackInfo.height / 2 + trackInfo.y : trackInfo.y;
|
// 这些设备路径固定是一条直线,这里手动赋值路径
|
trackInfo.pathList = [
|
{
|
x: isHorizontal ? startX + trackInfo.width : startX,
|
y: isHorizontal ? startY : startY + trackInfo.height,
|
type: 'line',
|
startX,
|
startY
|
}
|
];
|
}
|
sprite.trackInfo = trackInfo;
|
const mappingInfo = this.getMappingInfo({ ...sprite, ...device });
|
sprite.mappingInfo = mappingInfo;
|
sprite.path = mappingInfo.path;
|
sprite.currentAngle = mappingInfo.angle;
|
// sprite.vector = mappingInfo.vector
|
// sprite.time = Date.now()
|
sprite.rotation = G.getRotate(mappingInfo, mappingInfo.path) || sprite.rotation;
|
sprite.x = mappingInfo.x;
|
sprite.y = mappingInfo.y;
|
sprite.interactive = true; // 必须要设置才能接收事件
|
sprite.buttonMode = true; // 让光标在hover时变为手型指事件
|
sprite.on('pointerdown', () => {
|
if (window.gsap) {
|
window.gsap.killTweensOf(sprite);
|
}
|
sprite.alpha = 1;
|
const id = parseInt(deviceNo, 10);
|
this.$emit(deviceTypeInfo.emitName, id);
|
});
|
|
deviceTypeInfo.pixiMap.set(parseInt(deviceNo), sprite);
|
this.objectsContainer2.addChild(sprite);
|
};
|
|
map2.forEach((item) => {
|
if (['crn', 'dualCrn', 'rgv', 'annulus'].includes(item.type)) {
|
const deviceForm = G.safeParseJson(item.value);
|
if (deviceForm && deviceForm.deviceList && deviceForm.deviceList.length > 0) {
|
const newDeviceInfo = G.getDeviceInfo(item);
|
newDeviceInfo.deviceList.forEach((device) => {
|
//注意:annulus里的设备是rgv!
|
device.type = item.type === 'annulus' ? 'rgv' : item.type;
|
|
// 修复:将非 annulus 轨道设备的左上角位置转换为中心点位置
|
// annulus 类型的设备位置已经是中心点,不需要转换
|
if (item.type !== 'annulus') {
|
device.x = device.x + device.width / 2;
|
device.y = device.y + device.height / 2;
|
}
|
|
handleDevice(this.DEVICE_MAP[device.type], device, item);
|
// this.objectsContainer.addChild(sprite);
|
});
|
}
|
}
|
});
|
|
this.drawTracks(map2);
|
this.scheduleAdjustLabels();
|
},
|
initWidth(map) {
|
let maxRow = map.length;
|
let maxBay = map[0].length;
|
let bayWidthList = [];
|
for (let bay = 0; bay < maxBay; bay++) {
|
let bayWidth = -1;
|
for (let row = 0; row < maxRow; row++) {
|
let val = map[row][bay];
|
if (val.cellWidth == undefined || val.cellWidth === '') {
|
continue;
|
}
|
bayWidth = Math.max(bayWidth, val.cellWidth);
|
break;
|
}
|
bayWidthList.push(bayWidth);
|
}
|
return bayWidthList;
|
},
|
initHeight(map) {
|
let maxRow = map.length;
|
let maxBay = map[0].length;
|
let bayHeightList = [];
|
for (let row = 0; row < maxRow; row++) {
|
let bayHeight = -1;
|
for (let bay = 0; bay < maxBay; bay++) {
|
let val = map[row][bay];
|
if (val.cellHeight == undefined || val.cellHeight === '') {
|
continue;
|
}
|
bayHeight = Math.max(bayHeight, val.cellHeight);
|
break;
|
}
|
bayHeightList.push(bayHeight);
|
}
|
return bayHeightList;
|
},
|
setSiteInfo(res) {
|
let sites = Array.isArray(res) ? res : res && res.code === 200 ? res.data : null;
|
if (res && !Array.isArray(res)) {
|
if (res.code === 403) {
|
parent.location.href = baseUrl + '/login';
|
return;
|
}
|
if (res.code !== 200) {
|
return;
|
}
|
}
|
if (!sites) {
|
return;
|
}
|
sites.forEach((item) => {
|
let id = item.siteId != null ? item.siteId : item.stationId;
|
let workNo = item.workNo != null ? item.workNo : item.taskNo;
|
if (id == null) {
|
return;
|
}
|
let sta = this.pixiStaMap.get(parseInt(id));
|
if (sta == undefined) {
|
return;
|
}
|
if (workNo != null && workNo > 0) {
|
sta.textObj.text = String(id) + '\n(' + workNo + ')';
|
} else {
|
sta.textObj.text = String(id);
|
}
|
if (sta.statusObj) {
|
this.objectsContainer.removeChild(sta.statusObj);
|
sta.statusObj = null;
|
if (sta.textObj.parent !== sta) {
|
sta.addChild(sta.textObj);
|
sta.textObj.position.set(sta.width / 2, sta.height / 2);
|
}
|
}
|
this.setStationBaseColor(sta, this.getStationStatusColor(this.resolveStationStatus(item)));
|
});
|
},
|
sendConsoleLatestPoll(url) {
|
if (this.isSwitchingFloor) {
|
return;
|
}
|
this.sendWs(JSON.stringify({ url, data: {} }));
|
},
|
getCrnInfo() {
|
this.sendConsoleLatestPoll('/console/latest/data/crn');
|
},
|
getDualCrnInfo() {
|
this.sendConsoleLatestPoll('/console/latest/data/dualcrn');
|
},
|
getSiteInfo() {
|
this.sendConsoleLatestPoll('/console/latest/data/station');
|
},
|
getRgvInfo() {
|
this.sendConsoleLatestPoll('/console/latest/data/rgv');
|
},
|
getCycleCapacityInfo() {
|
this.sendConsoleLatestPoll('/console/latest/data/station/cycle/capacity');
|
},
|
setCycleCapacityInfo(res) {
|
const payload = res && res.code === 200 ? res.data : null;
|
if (res && res.code === 403) {
|
parent.location.href = baseUrl + '/login';
|
return;
|
}
|
if (!payload) {
|
return;
|
}
|
const loopList = Array.isArray(payload.loopList) ? payload.loopList : [];
|
this.cycleCapacity = {
|
loopList: loopList,
|
totalStationCount: payload.totalStationCount || 0,
|
taskStationCount: payload.taskStationCount || 0,
|
currentLoad:
|
typeof payload.currentLoad === 'number'
|
? payload.currentLoad
|
: parseFloat(payload.currentLoad || 0)
|
};
|
if (this.hoverLoopNo != null) {
|
const targetLoop = loopList.find((v) => v && v.loopNo === this.hoverLoopNo);
|
if (targetLoop) {
|
this.hoverLoopStationIdSet = this.buildStationIdSet(targetLoop.stationIdList);
|
this.applyLoopStationHighlight();
|
} else {
|
this.clearLoopStationHighlight();
|
}
|
}
|
},
|
formatLoadPercent(load) {
|
let value = typeof load === 'number' ? load : parseFloat(load || 0);
|
if (!isFinite(value)) {
|
value = 0;
|
}
|
if (value < 0) {
|
value = 0;
|
}
|
if (value > 1) {
|
value = 1;
|
}
|
return (value * 100).toFixed(1) + '%';
|
},
|
/******************setDeviceInfo使用的函数:******************/
|
getAnnulusAwarePoint(trackInfo, x, y, path) {
|
return trackInfo.type === 'annulus'
|
? G.snapToAnnulusOuterPath(x, y, path)
|
: { x, y };
|
},
|
/** 环穿:把 getPositionAfterMove 的结果压到轨带中线;非环穿原样返回 */
|
applyAnnulusBandCenterToPosition(trackInfo, position) {
|
if (!position || trackInfo.type !== 'annulus') {
|
return position;
|
}
|
const c = G.centerAnnulusBandPoint(trackInfo, position.x, position.y, position.path);
|
return { ...position, x: c.x, y: c.y };
|
},
|
computeFinalPosition(trackInfo, point, pathList, path, deltaDistance, angle) {
|
const position = G.getPositionAfterMove({ point, pathList, path, deltaDistance, angle });
|
return this.applyAnnulusBandCenterToPosition(trackInfo, position);
|
},
|
calcSignedSegmentDelta(fromBarcode, toBarcode, trackInfo, totalSegmentCount) {
|
const from = Number(fromBarcode);
|
const to = Number(toBarcode);
|
if (!isFinite(from) || !isFinite(to)) {
|
return 0;
|
}
|
const raw = to - from;
|
if (trackInfo.type !== 'annulus') {
|
return raw;
|
}
|
const span = totalSegmentCount;
|
if (!(span > 0)) {
|
return raw;
|
}
|
const mod = (n, m) => ((n % m) + m) % m;
|
return mod(raw, span);
|
},
|
lineSignedRemainAlong(path0, sx, sy, mx, my) {
|
if (!path0 || path0.type !== 'line') {
|
return null;
|
}
|
const x0 = path0.startX;
|
const y0 = path0.startY;
|
const vx = path0.x - x0;
|
const vy = path0.y - y0;
|
const segLen = Math.sqrt(vx * vx + vy * vy);
|
if (!(segLen > EPSILON)) {
|
return null;
|
}
|
const clampT = (px, py) => {
|
const t = ((px - x0) * vx + (py - y0) * vy) / segLen;
|
return Math.max(0, Math.min(segLen, t));
|
};
|
return clampT(mx, my) - clampT(sx, sy);
|
},
|
finishDeviceMotion(sprite) {
|
if (!sprite || !sprite.mappingInfo) {
|
return false;
|
}
|
const mx = sprite.mappingInfo.x;
|
const my = sprite.mappingInfo.y;
|
if (!isFinite(mx) || !isFinite(my)) {
|
return false;
|
}
|
sprite.isFinish = true;
|
sprite.x = mx;
|
sprite.y = my;
|
sprite.path = sprite.mappingInfo.path || sprite.path;
|
sprite.rotation = G.getRotate(sprite.mappingInfo, sprite.mappingInfo.path) || sprite.rotation;
|
if (sprite.ticker) {
|
this.pixiApp.ticker.remove(sprite.ticker);
|
}
|
sprite.ticker = null;
|
sprite.tickerTime = 0;
|
sprite._motionSpeed = undefined;
|
return true;
|
},
|
/** Web 接口返回统一成设备数组 */
|
parseBarcodeDevicesResponse(res) {
|
return Array.isArray(res) ? res : res && res.code === 200 ? res.data : null;
|
},
|
/** 条码设备:编号文字 + 状态色 */
|
applyBarcodeSpriteAppearance(sprite, device, deviceTypeInfo) {
|
const id = +device.index;
|
const taskNo = device.taskNo;
|
if (taskNo != null && taskNo > 0) {
|
sprite.textObj.text = id + '(' + taskNo + ')';
|
} else {
|
sprite.textObj.text = String(id);
|
}
|
const statusColor = device.statusColor;
|
if (statusColor != null) {
|
deviceTypeInfo.statusInfo.updateTextureColor(sprite, statusColor);
|
}
|
},
|
/** 条码锚点对象(两处构造合并为同一形状) */
|
createBarcodeAnchorState(
|
trackId,
|
minBarcode,
|
maxBarcode,
|
totalSegmentCount,
|
barcode,
|
point,
|
path,
|
angle
|
) {
|
return {
|
barcode,
|
x: point.x,
|
y: point.y,
|
path,
|
angle,
|
trackId,
|
minBarcode,
|
maxBarcode,
|
totalSegmentCount
|
};
|
},
|
/** 首次有条码数据:按 rgvPos 落到轨道上并建立锚点 */
|
initializeBarcodeTrackSprite(
|
sprite,
|
device,
|
pathList,
|
allDistance,
|
minBarcode,
|
maxBarcode,
|
totalSegmentCount
|
) {
|
sprite.barcode = device.rgvPos;
|
sprite.time = nowMs();
|
let tmpPassedSegmentCount = device.rgvPos - minBarcode;
|
const passedSegmentCount =
|
tmpPassedSegmentCount < 0
|
? tmpPassedSegmentCount + totalSegmentCount
|
: tmpPassedSegmentCount;
|
const deltaDistance = (allDistance * passedSegmentCount) / totalSegmentCount;
|
const initPath = sprite.path;
|
const initMovePoint = this.getAnnulusAwarePoint(
|
sprite.trackInfo,
|
sprite.x,
|
sprite.y,
|
initPath
|
);
|
let mappingInfo = this.computeFinalPosition(
|
sprite.trackInfo,
|
initMovePoint,
|
pathList,
|
initPath,
|
deltaDistance,
|
sprite.currentAngle
|
);
|
sprite.x = mappingInfo.x;
|
sprite.y = mappingInfo.y;
|
sprite.path = mappingInfo.path;
|
sprite.rotation = G.getRotate(mappingInfo, mappingInfo.path) || sprite.rotation;
|
sprite.currentAngle = mappingInfo.angle;
|
sprite.mappingInfo = mappingInfo;
|
const anchorPoint = this.getAnnulusAwarePoint(
|
sprite.trackInfo,
|
mappingInfo.x,
|
mappingInfo.y,
|
mappingInfo.path
|
);
|
sprite._barcodeAnchor = this.createBarcodeAnchorState(
|
sprite.trackInfo.id,
|
minBarcode,
|
maxBarcode,
|
totalSegmentCount,
|
device.rgvPos,
|
anchorPoint,
|
mappingInfo.path,
|
mappingInfo.angle
|
);
|
},
|
/** 条码设备每帧插值移动(Pixi ticker 回调) */
|
tickBarcodeTrackSpriteMotion(sprite, pathList) {
|
if (sprite.isFinish) {
|
return;
|
}
|
const restDistance = G.calcDistance(sprite, sprite.mappingInfo);
|
if (restDistance <= EPSILON) {
|
this.finishDeviceMotion(sprite);
|
return;
|
}
|
const dtMs =
|
this.pixiApp && this.pixiApp.ticker && typeof this.pixiApp.ticker.deltaMS === 'number'
|
? this.pixiApp.ticker.deltaMS
|
: 16.667;
|
const dt = Math.max(0, dtMs) / 1000;
|
if (dt <= 0) {
|
return;
|
}
|
const baseV =
|
typeof sprite.maV === 'number' && isFinite(sprite.maV) && sprite.maV > 0
|
? sprite.maV
|
: Math.max(sprite.width * 2, 20);
|
const msSinceUpdate = sprite.time ? nowMs() - sprite.time : 0;
|
const typicalIntervalMs = (sprite.lastDeltaTime || 1) * 1000;
|
const isStale = msSinceUpdate > typicalIntervalMs * 2.65;
|
const slowRadius = Math.max(sprite.width * 6.5, 72);
|
const easing = isStale ? Math.min(1.0, Math.pow(restDistance / slowRadius, 0.45)) : 1.0;
|
const targetSpeed = baseV * easing;
|
if (typeof sprite._motionSpeed !== 'number' || !isFinite(sprite._motionSpeed)) {
|
sprite._motionSpeed = targetSpeed * 0.35;
|
}
|
const maxAccel = Math.max(baseV * 1.75, 22);
|
const dv = targetSpeed - sprite._motionSpeed;
|
const cap = maxAccel * dt;
|
sprite._motionSpeed += Math.max(-cap, Math.min(cap, dv));
|
const path = sprite.path;
|
const stepCap = Math.min(restDistance, Math.max(0, sprite._motionSpeed) * dt);
|
const singleLineTrack =
|
pathList.length === 1 && pathList[0].type === 'line' && sprite.trackInfo.type !== 'annulus';
|
let smoothDistance;
|
if (singleLineTrack) {
|
const remainAlong = this.lineSignedRemainAlong(
|
path,
|
sprite.x,
|
sprite.y,
|
sprite.mappingInfo.x,
|
sprite.mappingInfo.y
|
);
|
if (remainAlong != null) {
|
let stepMag = Math.min(stepCap, Math.abs(remainAlong), restDistance);
|
if (stepMag < EPSILON && restDistance > EPSILON) {
|
const x0 = path.startX;
|
const y0 = path.startY;
|
const vx = path.x - x0;
|
const vy = path.y - y0;
|
const sl = Math.sqrt(vx * vx + vy * vy);
|
if (sl > EPSILON) {
|
const ux = vx / sl;
|
const uy = vy / sl;
|
const wdx = sprite.mappingInfo.x - sprite.x;
|
const wdy = sprite.mappingInfo.y - sprite.y;
|
const hint = wdx * ux + wdy * uy;
|
stepMag = Math.min(stepCap, restDistance);
|
if (hint === 0) {
|
this.finishDeviceMotion(sprite);
|
return;
|
}
|
smoothDistance = Math.sign(hint) * stepMag;
|
} else {
|
this.finishDeviceMotion(sprite);
|
return;
|
}
|
} else {
|
smoothDistance = Math.sign(remainAlong) * stepMag;
|
}
|
} else {
|
smoothDistance = stepCap;
|
}
|
} else {
|
smoothDistance = stepCap;
|
}
|
const movePointBarcode = this.getAnnulusAwarePoint(
|
sprite.trackInfo,
|
sprite.x,
|
sprite.y,
|
path
|
);
|
const angle = Math.atan2(sprite.y - path.y, sprite.x - path.x);
|
const raw = G.getPositionAfterMove({
|
point: movePointBarcode,
|
pathList,
|
path,
|
deltaDistance: smoothDistance,
|
angle
|
});
|
const p = this.applyAnnulusBandCenterToPosition(sprite.trackInfo, raw);
|
sprite.path = p.path;
|
sprite.x = p.x;
|
sprite.y = p.y;
|
sprite.rotation = G.getRotate(raw, raw.path) || sprite.rotation;
|
const restDistanceAfter = G.calcDistance({ x: sprite.x, y: sprite.y }, sprite.mappingInfo);
|
if (restDistanceAfter <= EPSILON || Math.abs(smoothDistance) >= restDistance - EPSILON) {
|
this.finishDeviceMotion(sprite);
|
}
|
},
|
/**
|
* 根据服务端条码刷新 mappingInfo、速度与 ticker。
|
* rgvPosMax 通常是闭区间 [min, max],且 max 对应 100% 与 min 同一点(走一圈回到原点)。
|
* 因此按 “段数 = max - min” 换算距离。
|
*/
|
syncBarcodeTrackSprite(
|
sprite,
|
device,
|
oldSprite,
|
pathList,
|
allDistance,
|
minBarcode,
|
maxBarcode,
|
totalSegmentCount
|
) {
|
let anchor = sprite._barcodeAnchor;
|
const needResetAnchor =
|
!anchor ||
|
anchor.trackId !== sprite.trackInfo.id ||
|
anchor.minBarcode !== minBarcode ||
|
anchor.maxBarcode !== maxBarcode ||
|
anchor.totalSegmentCount !== totalSegmentCount;
|
if (needResetAnchor) {
|
const anchorPath = sprite.path;
|
const anchorPoint = this.getAnnulusAwarePoint(
|
sprite.trackInfo,
|
sprite.x,
|
sprite.y,
|
anchorPath
|
);
|
const anchorBarcode = oldSprite.barcode != null ? oldSprite.barcode : device.rgvPos;
|
anchor = this.createBarcodeAnchorState(
|
sprite.trackInfo.id,
|
minBarcode,
|
maxBarcode,
|
totalSegmentCount,
|
anchorBarcode,
|
anchorPoint,
|
anchorPath,
|
sprite.currentAngle
|
);
|
sprite._barcodeAnchor = anchor;
|
}
|
const passedSegmentCount = this.calcSignedSegmentDelta(
|
anchor.barcode,
|
device.rgvPos,
|
sprite.trackInfo,
|
totalSegmentCount
|
);
|
const deltaDistance = (allDistance * passedSegmentCount) / totalSegmentCount;
|
const path = anchor.path || sprite.path;
|
const barcodeMovePoint = { x: anchor.x, y: anchor.y };
|
let finalPosition = this.computeFinalPosition(
|
sprite.trackInfo,
|
barcodeMovePoint,
|
pathList,
|
path,
|
deltaDistance,
|
anchor.angle
|
);
|
let curveDistance = G.calcDistance(sprite, finalPosition);
|
if (!isFinite(curveDistance)) {
|
curveDistance = deltaDistance;
|
}
|
sprite.mappingInfo = finalPosition;
|
const now = nowMs();
|
const deltaTime = (now - oldSprite.time) / 1000 || 0;
|
sprite.time = now;
|
sprite.barcode = device.rgvPos;
|
if (deltaTime > 0 && deltaTime <= 10 && curveDistance > 0) {
|
sprite.lastDeltaTime = deltaTime;
|
const v = curveDistance / deltaTime;
|
const alpha = 0.2;
|
sprite.maV =
|
typeof sprite.maV === 'number' && isFinite(sprite.maV)
|
? sprite.maV * (1 - alpha) + v * alpha
|
: v;
|
}
|
if(device.index === 18) {
|
console.log("device",device,curveDistance,oldSprite.barcode, device.rgvPos,sprite.id)
|
}
|
if (curveDistance < EPSILON) {
|
this.finishDeviceMotion(sprite);
|
return;
|
}
|
sprite.isFinish = false;
|
if (sprite.ticker) {
|
return;
|
}
|
sprite.ticker = () => this.tickBarcodeTrackSpriteMotion(sprite, pathList);
|
this.pixiApp.ticker.add(sprite.ticker);
|
},
|
// 适配不同接口返回的数据结构
|
deviceAdapter(type, res, trackType) {
|
const devices = this.parseBarcodeDevicesResponse(res);
|
const deviceTypeInfo = this.DEVICE_MAP[type];
|
// 环穿接口
|
if (trackType === 'annulus') {
|
return devices.map((device) => {
|
const index = +device.index;
|
const sprite = deviceTypeInfo.pixiMap.get(index);
|
if (!sprite) {
|
return {};
|
}
|
const trackInfoParse = G.safeParseJson(sprite.trackInfo.value);
|
const statusColorStr = device.statusColor.split('#')[1];
|
const statusColor = parseInt(statusColorStr, 16);
|
return {
|
index,
|
statusColor,
|
sprite,
|
minBarcode: trackInfoParse.barCodeStart,
|
maxBarcode: trackInfoParse.barCodeEnd,
|
rgvPos: device.rgvPos,
|
taskNo: device.taskNo
|
};
|
});
|
}
|
// 原版接口
|
return devices.map((device) => {
|
const index = +device[deviceTypeInfo.idName];
|
const sprite = deviceTypeInfo.pixiMap.get(index);
|
if (!sprite) {
|
return {};
|
}
|
const trackInfoParse = G.safeParseJson(sprite.trackInfo.value);
|
const minBarcode = trackInfoParse.barCodeStart;
|
const maxBarcode = trackInfoParse.barCodeEnd;
|
const statusInfo = deviceTypeInfo.statusInfo;
|
const statusColor = statusInfo.getStatus(device[statusInfo.name]);
|
return {
|
index,
|
statusColor,
|
sprite,
|
minBarcode,
|
maxBarcode,
|
rgvPos: device.laserValue * FAKE_MAX_LAYER / FAKE_MAX_CRN_LAYER,
|
taskNo: device.taskNo
|
};
|
});
|
},
|
setDeviceInfoByBarcode(type, res, trackType = 'annulus') {
|
const devices = this.deviceAdapter(type, res, trackType);
|
const deviceTypeInfo = this.DEVICE_MAP[type];
|
for (let i = 0; i < devices.length; i++) {
|
const device = devices[i] || {};
|
const { sprite, minBarcode, maxBarcode } = device;
|
if (!sprite) {
|
continue;
|
}
|
this.applyBarcodeSpriteAppearance(sprite, device, deviceTypeInfo);
|
const pathList = sprite.trackInfo.pathList;
|
const allDistance = G.getAllDistance(pathList);
|
const totalSegmentCount = Math.max(1, maxBarcode - minBarcode);
|
const oldSprite = {
|
...sprite,
|
x: sprite.x,
|
y: sprite.y,
|
mappingInfo: { ...sprite.mappingInfo }
|
};
|
if (!sprite.barcode && sprite.barcode !== 0) {
|
this.initializeBarcodeTrackSprite(
|
sprite,
|
device,
|
pathList,
|
allDistance,
|
minBarcode,
|
maxBarcode,
|
totalSegmentCount
|
);
|
continue;
|
}
|
this.syncBarcodeTrackSprite(
|
sprite,
|
device,
|
oldSprite,
|
pathList,
|
allDistance,
|
minBarcode,
|
maxBarcode,
|
totalSegmentCount
|
);
|
}
|
this.scheduleAdjustLabels();
|
},
|
/******************setDeviceInfo相关函数结束******************/
|
webSocketOnOpen(e) {
|
if (this.wsReconnectTimer) {
|
clearTimeout(this.wsReconnectTimer);
|
this.wsReconnectTimer = null;
|
}
|
this.wsReconnectAttempts = 0;
|
this.getMap(this.currentLev);
|
this.getCycleCapacityInfo();
|
},
|
webSocketOnError(e) {
|
this.scheduleReconnect();
|
},
|
webSocketOnMessage(e) {
|
const result = JSON.parse(e.data);
|
if (
|
result.url === '/console/latest/data/station' ||
|
result.url === '/console/latest/data/site'
|
) {
|
// todo
|
// this.setSiteInfo(JSON.parse(result.data));
|
} else if (result.url === '/console/latest/data/crn') {
|
this.setDeviceInfoByBarcode('crn', JSON.parse(result.data), 'crn');
|
} else if (result.url === '/console/latest/data/dualcrn') {
|
this.setDeviceInfoByBarcode('dualCrn', JSON.parse(result.data), 'dualCrn');
|
} else if (result.url === '/console/latest/data/rgv') {
|
// this.setDeviceInfoByBarcode('rgv', JSON.parse(result.data), 'rgv');
|
} else if (result.url === '/console/latest/data/station/cycle/capacity') {
|
this.setCycleCapacityInfo(JSON.parse(result.data));
|
} else if (typeof result.url === 'string' && result.url.indexOf('/basMap/lev/') === 0) {
|
this.setMap(JSON.parse(result.data));
|
}
|
// else if (result.url === 'rgv/ring/through/rgv/position/data') {
|
// this.setDeviceInfoByBarcode('rgv', JSON.parse(result.data));
|
// }
|
},
|
webSocketClose(e) {
|
this.scheduleReconnect();
|
},
|
sendWs(message) {
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
this.ws.send(message);
|
}
|
},
|
connectWs() {
|
if (
|
this.ws &&
|
(this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)
|
) {
|
return;
|
}
|
this.ws = new WebSocket('ws://' + window.location.host + baseUrl + '/console/websocket');
|
this.ws.onopen = this.webSocketOnOpen;
|
this.ws.onerror = this.webSocketOnError;
|
this.ws.onmessage = this.webSocketOnMessage;
|
this.ws.onclose = this.webSocketClose;
|
},
|
scheduleReconnect() {
|
if (this.wsReconnectTimer) {
|
return;
|
}
|
const attempt = this.wsReconnectAttempts + 1;
|
const jitter = Math.floor(Math.random() * 300);
|
const delay =
|
Math.min(
|
this.wsReconnectMaxDelay,
|
this.wsReconnectBaseDelay * Math.pow(2, this.wsReconnectAttempts)
|
) + jitter;
|
this.wsReconnectTimer = setTimeout(() => {
|
this.wsReconnectTimer = null;
|
this.wsReconnectAttempts = attempt;
|
this.connectWs();
|
}, delay);
|
},
|
createShelfSprite(width, height) {
|
let idx = width + '-' + height;
|
let texture = this.pixiShelfMap.get(idx);
|
if (texture == undefined) {
|
let graphics = this.getContainer('shelf', width, height);
|
texture = this.pixiApp.renderer.generateTexture(graphics);
|
this.pixiShelfMap.set(idx, texture);
|
}
|
return new PIXI.Sprite(texture);
|
},
|
getContainer(type, width, height) {
|
let graphics = new PIXI.Graphics();
|
let drawBorder = true;
|
if (type == 'shelf') {
|
graphics.beginFill(0xb6e2e2);
|
} else if (type == 'devp') {
|
graphics.beginFill(0x00ff7f);
|
graphics.visible = true;
|
} else if (type == 'crn') {
|
graphics.beginFill(0xaaffff);
|
}
|
if (drawBorder) {
|
graphics.lineStyle(1, 0xffffff, 1);
|
graphics.drawRect(0, 0, width, height);
|
}
|
graphics.endFill();
|
return graphics;
|
},
|
createCrnTexture(width, height) {
|
const g = new PIXI.Graphics();
|
G.drawCrnDeviceGraphics(g, width, height, 0x245a9a);
|
const rt = PIXI.RenderTexture.create({
|
width: width,
|
height: height
|
});
|
this.pixiApp.renderer.render(g, rt);
|
return rt;
|
},
|
createCrnTextureColoredDevice(deviceWidth, height, color) {
|
const g = new PIXI.Graphics();
|
G.drawCrnDeviceGraphics(g, deviceWidth, height, color);
|
const rt = PIXI.RenderTexture.create({
|
width: deviceWidth,
|
height: height
|
});
|
this.pixiApp.renderer.render(g, rt);
|
return rt;
|
},
|
createDevpTextureColoredRect(width, height, color) {
|
const g = new PIXI.Graphics();
|
g.beginFill(color);
|
g.lineStyle(1, 0xffffff, 1);
|
g.drawRect(0, 0, width, height);
|
g.endFill();
|
const rt = PIXI.RenderTexture.create({ width: width, height: height });
|
this.pixiApp.renderer.render(g, rt);
|
return rt;
|
},
|
createRgvTexture(width, height) {
|
const g = new PIXI.Graphics();
|
G.drawRgvDeviceGraphics(g, width, height, 0x245a9a);
|
const rt = PIXI.RenderTexture.create({ width: width, height: height });
|
this.pixiApp.renderer.render(g, rt);
|
return rt;
|
},
|
createRgvTextureColoredDevice(width, height, color) {
|
const g = new PIXI.Graphics();
|
G.drawRgvDeviceGraphics(g, width, height, color);
|
const rt = PIXI.RenderTexture.create({ width: width, height: height });
|
this.pixiApp.renderer.render(g, rt);
|
return rt;
|
},
|
applyCachedColoredDeviceTexture(sprite, color, cacheMap, createColoredTex, repositionText) {
|
const w = Math.round(sprite.width);
|
const h = Math.round(sprite.height);
|
const key = w + '-' + h + '-' + color;
|
let tex = cacheMap.get(key);
|
if (!tex) {
|
tex = createColoredTex(w, h, color);
|
cacheMap.set(key, tex);
|
}
|
sprite.texture = tex;
|
const textObj = sprite.textObj;
|
if (!textObj) {
|
return;
|
}
|
const fill = this.getContrastColor(color);
|
textObj.style.fill = fill;
|
textObj.style.stroke = fill === '#000000' ? '#ffffff' : '#000000';
|
textObj.style.strokeThickness = 1;
|
if (repositionText) {
|
this.positionSpriteLabelToTextureCenter(sprite);
|
}
|
},
|
updateRgvTextureColor(sprite, color) {
|
this.applyCachedColoredDeviceTexture(
|
sprite,
|
color,
|
this.pixiRgvColorTextureMap,
|
this.createRgvTextureColoredDevice,
|
true
|
);
|
},
|
updateCrnTextureColor(sprite, color) {
|
this.applyCachedColoredDeviceTexture(
|
sprite,
|
color,
|
this.pixiCrnColorTextureMap,
|
this.createCrnTextureColoredDevice,
|
false
|
);
|
},
|
getContrastColor(color) {
|
const r = (color >> 16) & 0xff;
|
const g = (color >> 8) & 0xff;
|
const b = color & 0xff;
|
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
return brightness > 150 ? '#000000' : '#ffffff';
|
},
|
getStationStatusColor(status) {
|
const colorMap = this.stationStatusColors || this.getDefaultStationStatusColors();
|
if (status && colorMap[status] != null) {
|
return colorMap[status];
|
}
|
return colorMap['site-unauto'] != null ? colorMap['site-unauto'] : 0xb8b8b8;
|
},
|
resolveStationStatus(item) {
|
if (item && item.error > 0) {
|
return 'site-error';
|
}
|
const status = item && (item.siteStatus != null ? item.siteStatus : item.stationStatus);
|
const taskNo = this.parseStationTaskNo(
|
item && (item.workNo != null ? item.workNo : item.taskNo)
|
);
|
const autoing = !!(item && item.autoing);
|
const loading = !!(item && item.loading);
|
const runBlock = !!(item && item.runBlock);
|
const enableIn = !!(item && item.enableIn);
|
if (taskNo === 9998 || enableIn) {
|
return 'site-enable-in';
|
}
|
if (autoing && loading && taskNo > 0 && !runBlock) {
|
const taskClass = this.getStationTaskClass(taskNo);
|
if (taskClass) {
|
return taskClass;
|
}
|
}
|
if (status) {
|
return status;
|
}
|
if (autoing && loading && taskNo > 0 && runBlock) {
|
return 'site-run-block';
|
}
|
if (autoing && loading && taskNo > 0) {
|
return 'site-auto-run-id';
|
}
|
if (autoing && loading) {
|
return 'site-auto-run';
|
}
|
if (autoing && taskNo > 0) {
|
return 'site-auto-id';
|
}
|
if (autoing) {
|
return 'site-auto';
|
}
|
return 'site-unauto';
|
},
|
parseStationTaskNo(value) {
|
const taskNo = parseInt(value, 10);
|
return isNaN(taskNo) ? 0 : taskNo;
|
},
|
getStationTaskClass(taskNo) {
|
if (!(taskNo > 0)) {
|
return null;
|
}
|
const range = this.stationTaskRange || {};
|
if (this.isTaskNoInRange(taskNo, range.inbound)) {
|
return 'machine-pakin';
|
}
|
if (this.isTaskNoInRange(taskNo, range.outbound)) {
|
return 'machine-pakout';
|
}
|
return null;
|
},
|
isTaskNoInRange(taskNo, range) {
|
if (!range) {
|
return false;
|
}
|
const start = parseInt(range.start, 10);
|
const end = parseInt(range.end, 10);
|
if (isNaN(start) || isNaN(end)) {
|
return false;
|
}
|
return taskNo >= start && taskNo <= end;
|
},
|
getCrnStatusColor(status) {
|
if (status === 'machine-auto') {
|
return 0x21ba45;
|
}
|
if (status === 'machine-un-auto') {
|
return 0xbbbbbb;
|
}
|
if (status === 'machine-error') {
|
return 0xdb2828;
|
}
|
if (status === 'machine-pakin') {
|
return 0x30bffc;
|
}
|
if (status === 'machine-pakout') {
|
return 0x97b400;
|
}
|
return 0xbbbbbb;
|
},
|
getRgvStatusColor(status) {
|
if (status === 'idle') {
|
return 0x21ba45;
|
}
|
if (status === 'working') {
|
return 0xffd60b;
|
}
|
if (status === 'waiting') {
|
return 0xffd60b;
|
}
|
if (status === 'fetching') {
|
return 0xffd60b;
|
}
|
if (status === 'putting') {
|
return 0xffd60b;
|
}
|
return 0xb8b8b8;
|
},
|
getSprite(item, pointerDownEvent) {
|
let sprite;
|
let value = item.value;
|
if (item.type == 'shelf') {
|
sprite = this.createShelfSprite(item.width, item.height);
|
sprite._kind = 'shelf';
|
} else if (item.type == 'devp') {
|
const key = Math.round(item.width) + '-' + Math.round(item.height) + '-' + 0x00ff7f;
|
let texture = this.pixiDevpTextureMap.get(key);
|
if (!texture) {
|
texture = this.createDevpTextureColoredRect(
|
Math.round(item.width),
|
Math.round(item.height),
|
0x00ff7f
|
);
|
this.pixiDevpTextureMap.set(key, texture);
|
}
|
sprite = new PIXI.Sprite(texture);
|
sprite._kind = 'devp';
|
const directionOverlay = this.createStationDirectionOverlay(
|
item.width,
|
item.height,
|
item.stationDirectionList
|
);
|
if (directionOverlay) {
|
directionOverlay.visible = this.showStationDirection;
|
sprite.addChild(directionOverlay);
|
}
|
sprite.directionObj = directionOverlay;
|
sprite._stationDirectionList = Array.isArray(item.stationDirectionList)
|
? item.stationDirectionList.slice()
|
: [];
|
let siteId = this.getStationId(value);
|
// item.value: '{"bridgeStationIds":[1188,1186],"autoBridge":1,"direction":["left","right"]}'
|
if (siteId === -1) {
|
siteId = item.data || '';
|
}
|
const style = new PIXI.TextStyle({
|
fontFamily: 'Arial',
|
fontSize: 10,
|
fill: '#000000',
|
stroke: '#ffffff',
|
strokeThickness: 1,
|
align: 'center'
|
});
|
const text = new PIXI.Text(String(siteId), style);
|
text.anchor.set(0.5);
|
text.position.set(sprite.width / 2, sprite.height / 2);
|
sprite.addChild(text);
|
sprite.textObj = text;
|
const stationIdInt = parseInt(siteId, 10);
|
if (!isNaN(stationIdInt)) {
|
this.pixiStaMap.set(stationIdInt, sprite);
|
}
|
sprite._stationId = isNaN(stationIdInt) ? null : stationIdInt;
|
sprite._baseColor = 0x00ff7f;
|
sprite._loopHighlighted = false;
|
sprite.interactive = true;
|
sprite.buttonMode = true;
|
sprite.on('pointerdown', () => {
|
if (window.gsap) {
|
window.gsap.killTweensOf(sprite);
|
}
|
sprite.alpha = 1;
|
const id = parseInt(siteId, 10);
|
if (!isNaN(id)) {
|
this.$emit('station-click', id);
|
}
|
});
|
} else {
|
return null;
|
}
|
sprite.position.set(item.posX, item.posY);
|
return sprite;
|
},
|
isTrackType(cell) {
|
return (
|
cell &&
|
(cell.type === 'crn' ||
|
cell.type === 'dualCrn' ||
|
cell.type === 'rgv' ||
|
cell.type === 'annulus')
|
);
|
},
|
resolveMergedCell(map, rowIndex, colIndex) {
|
if (!map || rowIndex < 0 || colIndex < 0) {
|
return null;
|
}
|
const row = map[rowIndex];
|
if (!row || colIndex >= row.length) {
|
return null;
|
}
|
const cell = row[colIndex];
|
if (!cell) {
|
return null;
|
}
|
if (!cell.isMergedPart && cell.type !== 'merge') {
|
return cell;
|
}
|
if (cell.isMergedPart) {
|
for (let c = colIndex - 1; c >= 0; c--) {
|
const left = row[c];
|
if (!left) {
|
continue;
|
}
|
if (!left.isMergedPart && left.type !== 'merge' && left.posX === cell.posX) {
|
return left;
|
}
|
}
|
}
|
if (cell.type === 'merge') {
|
for (let r = rowIndex - 1; r >= 0; r--) {
|
const upRow = map[r];
|
if (!upRow || colIndex >= upRow.length) {
|
continue;
|
}
|
const up = upRow[colIndex];
|
if (!up) {
|
continue;
|
}
|
if (up.type !== 'merge') {
|
return up;
|
}
|
}
|
}
|
return null;
|
},
|
|
drawTracks(map) {
|
if (!this.tracksGraphics) {
|
return;
|
}
|
|
const centerOf = (cell) => ({
|
x: cell.x + cell.width / 2,
|
y: cell.y + cell.height / 2
|
});
|
|
this.tracksGraphics.clear();
|
const rail = 2;
|
const color = 0x555555;
|
|
map.forEach((item) => {
|
this.tracksGraphics.lineStyle({
|
width: rail,
|
color: G.TYPE_META[item.type].border,
|
alpha: 1,
|
cap: PIXI.LINE_CAP.ROUND,
|
join: PIXI.LINE_JOIN.ROUND
|
});
|
if (['crn', 'rgv', 'dualCrn'].includes(item.type)) {
|
if (item.width > item.height) {
|
// 水平
|
this.tracksGraphics.moveTo(item.x, item.y + item.height / 2);
|
this.tracksGraphics.lineTo(item.x + item.width, item.y + item.height / 2);
|
this.tracksGraphics.endFill();
|
} else {
|
// 垂直
|
this.tracksGraphics.moveTo(item.x + item.width / 2, item.y);
|
this.tracksGraphics.lineTo(item.x + item.width / 2, item.y + item.height);
|
this.tracksGraphics.endFill();
|
}
|
// this.tracksGraphics.moveTo(cPos.x, cPos.y);
|
// this.tracksGraphics.lineTo(cPos.x + item.width, cPos.y);
|
// this.tracksGraphics.endFill();
|
} else if (item.type === 'annulus') {
|
G.strokeAnnulusDualOutline(this.tracksGraphics, item, item.shape || 'rect');
|
}
|
});
|
},
|
updateColor(sprite, color) {
|
if (sprite && sprite._kind === 'devp') {
|
const key = sprite.width + '-' + sprite.height + '-' + color;
|
let texture = this.pixiDevpTextureMap.get(key);
|
if (!texture) {
|
texture = this.createDevpTextureColoredRect(
|
Math.round(sprite.width),
|
Math.round(sprite.height),
|
color
|
);
|
this.pixiDevpTextureMap.set(key, texture);
|
}
|
const textObj = sprite.textObj;
|
sprite.texture = texture;
|
if (textObj) {
|
if (textObj.parent !== sprite) {
|
sprite.addChild(textObj);
|
}
|
textObj.position.set(sprite.width / 2, sprite.height / 2);
|
const fill = this.getContrastColor(color);
|
textObj.style.fill = fill;
|
textObj.style.stroke = fill === '#000000' ? '#ffffff' : '#000000';
|
textObj.style.strokeThickness = 1;
|
}
|
return;
|
}
|
sprite.tint = color;
|
},
|
setStationBaseColor(sprite, color) {
|
if (!sprite) {
|
return;
|
}
|
sprite._baseColor = color;
|
if (this.isStationInHoverLoop(sprite)) {
|
this.applyHighlightColor(sprite);
|
} else {
|
this.updateColor(sprite, color);
|
sprite._loopHighlighted = false;
|
}
|
},
|
applyHighlightColor(sprite) {
|
if (!sprite) {
|
return;
|
}
|
this.updateColor(sprite, this.loopHighlightColor);
|
sprite._loopHighlighted = true;
|
},
|
isStationInHoverLoop(sprite) {
|
if (!sprite || sprite._stationId == null || !this.hoverLoopStationIdSet) {
|
return false;
|
}
|
return this.hoverLoopStationIdSet.has(sprite._stationId);
|
},
|
buildStationIdSet(stationIdList) {
|
const set = new Set();
|
if (!Array.isArray(stationIdList)) {
|
return set;
|
}
|
stationIdList.forEach((id) => {
|
const v = parseInt(id, 10);
|
if (!isNaN(v)) {
|
set.add(v);
|
}
|
});
|
return set;
|
},
|
applyLoopStationHighlight() {
|
if (!this.pixiStaMap) {
|
return;
|
}
|
this.pixiStaMap.forEach((sprite) => {
|
if (!sprite) {
|
return;
|
}
|
if (this.isStationInHoverLoop(sprite)) {
|
this.applyHighlightColor(sprite);
|
} else if (sprite._loopHighlighted) {
|
const baseColor = typeof sprite._baseColor === 'number' ? sprite._baseColor : 0xb8b8b8;
|
this.updateColor(sprite, baseColor);
|
sprite._loopHighlighted = false;
|
}
|
});
|
},
|
clearLoopStationHighlight() {
|
if (this.pixiStaMap) {
|
this.pixiStaMap.forEach((sprite) => {
|
if (!sprite || !sprite._loopHighlighted) {
|
return;
|
}
|
const baseColor = typeof sprite._baseColor === 'number' ? sprite._baseColor : 0xb8b8b8;
|
this.updateColor(sprite, baseColor);
|
sprite._loopHighlighted = false;
|
});
|
}
|
this.hoverLoopNo = null;
|
this.hoverLoopStationIdSet = new Set();
|
},
|
handleLoopCardEnter(loopItem) {
|
if (!loopItem) {
|
return;
|
}
|
this.hoverLoopNo = loopItem.loopNo;
|
this.hoverLoopStationIdSet = this.buildStationIdSet(loopItem.stationIdList);
|
this.applyLoopStationHighlight();
|
},
|
handleLoopCardLeave(loopItem) {
|
if (!loopItem) {
|
this.clearLoopStationHighlight();
|
return;
|
}
|
if (this.hoverLoopNo === loopItem.loopNo) {
|
this.clearLoopStationHighlight();
|
}
|
},
|
isJson(str) {
|
try {
|
JSON.parse(str);
|
return true;
|
} catch (e) {
|
return false;
|
}
|
},
|
getDeviceNo(obj) {
|
if (this.isJson(obj)) {
|
let data = JSON.parse(obj);
|
if (data.deviceNo == null || data.deviceNo == undefined) {
|
return -1;
|
}
|
return data.deviceNo;
|
} else {
|
return -1;
|
}
|
},
|
getTaskNo(obj) {
|
if (this.isJson(obj)) {
|
let data = JSON.parse(obj);
|
if (data.taskNo == null || data.taskNo == undefined) {
|
return -1;
|
}
|
return data.taskNo;
|
} else {
|
return -1;
|
}
|
},
|
getStationId(obj) {
|
if (this.isJson(obj)) {
|
let data = JSON.parse(obj);
|
if (data.stationId == null || data.stationId == undefined) {
|
return -1;
|
}
|
return data.stationId;
|
} else {
|
return -1;
|
}
|
},
|
parseMapValue(obj) {
|
if (obj == null) {
|
return null;
|
}
|
if (typeof obj === 'object') {
|
return obj;
|
}
|
if (!this.isJson(obj)) {
|
return null;
|
}
|
try {
|
return JSON.parse(obj);
|
} catch (e) {
|
return null;
|
}
|
},
|
normalizeDirectionList(direction) {
|
const aliasMap = {
|
top: 'top',
|
up: 'top',
|
north: 'top',
|
bottom: 'bottom',
|
down: 'bottom',
|
south: 'bottom',
|
left: 'left',
|
west: 'left',
|
right: 'right',
|
east: 'right'
|
};
|
let rawList = [];
|
if (Array.isArray(direction)) {
|
rawList = direction;
|
} else if (typeof direction === 'string') {
|
rawList = direction.split(/[,\s|/]+/);
|
}
|
const result = [];
|
const seen = new Set();
|
rawList.forEach((item) => {
|
const key =
|
aliasMap[
|
String(item || '')
|
.trim()
|
.toLowerCase()
|
];
|
if (!key || seen.has(key)) {
|
return;
|
}
|
seen.add(key);
|
result.push(key);
|
});
|
return result;
|
},
|
resolveStationDirectionList(map, rowIndex, colIndex, item) {
|
const valueObj = this.parseMapValue(item && item.value);
|
const fromValue = this.normalizeDirectionList(valueObj && valueObj.direction);
|
if (fromValue.length > 0) {
|
return fromValue;
|
}
|
const rowSpan = item && item.rowSpan ? item.rowSpan : 1;
|
const colSpan = item && item.colSpan ? item.colSpan : 1;
|
const fallback = [];
|
const candidateList = [
|
{
|
key: 'top',
|
cell: this.resolveMergedCell(map, rowIndex - 1, colIndex)
|
},
|
{
|
key: 'right',
|
cell: this.resolveMergedCell(map, rowIndex, colIndex + colSpan)
|
},
|
{
|
key: 'bottom',
|
cell: this.resolveMergedCell(map, rowIndex + rowSpan, colIndex)
|
},
|
{
|
key: 'left',
|
cell: this.resolveMergedCell(map, rowIndex, colIndex - 1)
|
}
|
];
|
candidateList.forEach((candidate) => {
|
if (this.isStationDirectionNeighbor(candidate.cell)) {
|
fallback.push(candidate.key);
|
}
|
});
|
return fallback;
|
},
|
isStationDirectionNeighbor(cell) {
|
if (!cell) {
|
return false;
|
}
|
if (cell.type === 'devp') {
|
return true;
|
}
|
return this.isTrackType(cell);
|
},
|
createStationDirectionOverlay(width, height, directionList) {
|
if (!Array.isArray(directionList) || directionList.length === 0) {
|
return null;
|
}
|
const container = new PIXI.Container();
|
const arrowSize = Math.max(4, Math.min(width, height) * 0.22);
|
const margin = Math.max(2, Math.min(width, height) * 0.12);
|
directionList.forEach((direction) => {
|
const arrow = new PIXI.Graphics();
|
this.drawStationDirectionArrow(arrow, width, height, direction, arrowSize, margin);
|
container.addChild(arrow);
|
});
|
return container;
|
},
|
drawStationDirectionArrow(graphics, width, height, direction, size, margin) {
|
if (!graphics) {
|
return;
|
}
|
const halfBase = Math.max(2, size * 0.45);
|
const stemLen = Math.max(3, size * 0.7);
|
const centerX = width / 2;
|
const centerY = height / 2;
|
graphics.beginFill(this.stationDirectionColor, 0.95);
|
if (direction === 'top') {
|
const tipY = margin;
|
const baseY = margin + size;
|
const stemY = Math.min(centerY - 2, baseY + stemLen);
|
if (stemY > baseY) {
|
graphics.moveTo(centerX, stemY);
|
graphics.lineTo(centerX, baseY);
|
}
|
graphics.moveTo(centerX, tipY);
|
graphics.lineTo(centerX - halfBase, baseY);
|
graphics.lineTo(centerX + halfBase, baseY);
|
} else if (direction === 'right') {
|
const tipX = width - margin;
|
const baseX = width - margin - size;
|
const stemX = Math.max(centerX + 2, baseX - stemLen);
|
if (stemX < baseX) {
|
graphics.moveTo(stemX, centerY);
|
graphics.lineTo(baseX, centerY);
|
}
|
graphics.moveTo(tipX, centerY);
|
graphics.lineTo(baseX, centerY - halfBase);
|
graphics.lineTo(baseX, centerY + halfBase);
|
} else if (direction === 'bottom') {
|
const tipY = height - margin;
|
const baseY = height - margin - size;
|
const stemY = Math.max(centerY + 2, baseY - stemLen);
|
if (stemY < baseY) {
|
graphics.moveTo(centerX, stemY);
|
graphics.lineTo(centerX, baseY);
|
}
|
graphics.moveTo(centerX, tipY);
|
graphics.lineTo(centerX - halfBase, baseY);
|
graphics.lineTo(centerX + halfBase, baseY);
|
} else if (direction === 'left') {
|
const tipX = margin;
|
const baseX = margin + size;
|
const stemX = Math.min(centerX - 2, baseX + stemLen);
|
if (stemX > baseX) {
|
graphics.moveTo(stemX, centerY);
|
graphics.lineTo(baseX, centerY);
|
}
|
graphics.moveTo(tipX, centerY);
|
graphics.lineTo(baseX, centerY - halfBase);
|
graphics.lineTo(baseX, centerY + halfBase);
|
}
|
graphics.closePath();
|
graphics.endFill();
|
},
|
applyStationDirectionVisibility() {
|
if (!this.pixiStaMap) {
|
return;
|
}
|
this.pixiStaMap.forEach((sprite) => {
|
if (!sprite || !sprite.directionObj) {
|
return;
|
}
|
sprite.directionObj.visible = this.showStationDirection;
|
});
|
},
|
getTrackSiteNo(obj) {
|
if (this.isJson(obj)) {
|
let data = JSON.parse(obj);
|
if (data.trackSiteNo == null || data.trackSiteNo == undefined) {
|
return -1;
|
}
|
return data.trackSiteNo;
|
} else {
|
return -1;
|
}
|
},
|
buildShelfHitGrid(map, rowHeights, rowOffsets) {
|
if (!map || !Array.isArray(map)) {
|
return;
|
}
|
this.mapRowOffsets = Array.isArray(rowOffsets) ? rowOffsets.slice() : [];
|
this.mapRowHeights = Array.isArray(rowHeights) ? rowHeights.slice() : [];
|
const rowColOffsets = [];
|
const rowColWidths = [];
|
const rowShelfCells = new Array(map.length);
|
let maxCols = 0;
|
for (let r = 0; r < map.length; r++) {
|
const row = map[r];
|
if (row && row.length > maxCols) {
|
maxCols = row.length;
|
}
|
rowShelfCells[r] = [];
|
}
|
const colWidths = new Array(maxCols);
|
for (let c = 0; c < maxCols; c++) {
|
let w = null;
|
for (let r = 0; r < map.length; r++) {
|
const cell = map[r] && map[r][c];
|
if (!cell) {
|
continue;
|
}
|
if (cell.cellWidth != null && cell.cellWidth !== '') {
|
const base = Number(cell.cellWidth);
|
if (isFinite(base) && base > 0) {
|
w = base / 40;
|
break;
|
}
|
}
|
}
|
colWidths[c] = w && isFinite(w) && w > 0 ? w : 25;
|
}
|
const colOffsets = new Array(maxCols);
|
let xCursor = 0;
|
for (let c = 0; c < maxCols; c++) {
|
colOffsets[c] = xCursor;
|
xCursor += colWidths[c];
|
}
|
for (let r = 0; r < map.length; r++) {
|
const row = map[r];
|
if (!row || row.length === 0) {
|
rowColOffsets[r] = [];
|
rowColWidths[r] = [];
|
continue;
|
}
|
const widths = new Array(row.length);
|
for (let c = 0; c < row.length; c++) {
|
const cell = row[c];
|
let w = null;
|
if (cell && cell.cellWidth != null && cell.cellWidth !== '') {
|
const base = Number(cell.cellWidth);
|
if (isFinite(base) && base > 0) {
|
w = base / 40;
|
}
|
}
|
widths[c] = w && isFinite(w) && w > 0 ? w : 25;
|
}
|
const offsets = new Array(row.length);
|
let x = 0;
|
for (let c = 0; c < row.length; c++) {
|
offsets[c] = x;
|
x += widths[c];
|
}
|
rowColOffsets[r] = offsets;
|
rowColWidths[r] = widths;
|
}
|
this.mapColWidths = colWidths;
|
this.mapColOffsets = colOffsets;
|
this.mapRowColOffsets = rowColOffsets;
|
this.mapRowColWidths = rowColWidths;
|
this.mapRowShelfCells = rowShelfCells;
|
|
for (let r = 0; r < map.length; r++) {
|
const row = map[r];
|
if (!row) {
|
continue;
|
}
|
for (let c = 0; c < row.length; c++) {
|
const cell = row[c];
|
if (!cell || cell.type !== 'shelf') {
|
continue;
|
}
|
const startRow = this.findIndexByOffsets(
|
this.mapRowOffsets,
|
this.mapRowHeights,
|
cell.posY + 0.01
|
);
|
const endRow = this.findIndexByOffsets(
|
this.mapRowOffsets,
|
this.mapRowHeights,
|
cell.posY + cell.height - 0.01
|
);
|
if (startRow < 0) {
|
continue;
|
}
|
const last = endRow >= 0 ? endRow : startRow;
|
for (let rr = startRow; rr <= last; rr++) {
|
if (!rowShelfCells[rr]) {
|
rowShelfCells[rr] = [];
|
}
|
rowShelfCells[rr].push(cell);
|
}
|
}
|
}
|
},
|
clearShelfChunks() {
|
if (this.shelfCullRaf) {
|
cancelAnimationFrame(this.shelfCullRaf);
|
this.shelfCullRaf = null;
|
}
|
this.shelfChunkList = [];
|
if (!this.shelvesContainer) {
|
return;
|
}
|
const children = this.shelvesContainer.removeChildren();
|
children.forEach((child) => {
|
if (child && typeof child.destroy === 'function') {
|
child.destroy({ children: true, texture: true, baseTexture: true });
|
}
|
});
|
},
|
buildShelfChunks(map, contentW, contentH) {
|
this.clearShelfChunks();
|
if (
|
!this.pixiApp ||
|
!this.pixiApp.renderer ||
|
!this.shelvesContainer ||
|
!Array.isArray(map)
|
) {
|
return;
|
}
|
const chunkSize = Math.max(256, parseInt(this.shelfChunkSize, 10) || 2048);
|
const chunkMap = new Map();
|
for (let r = 0; r < map.length; r++) {
|
const row = map[r];
|
if (!row) {
|
continue;
|
}
|
for (let c = 0; c < row.length; c++) {
|
const cell = row[c];
|
if (!cell || cell.type !== 'shelf' || cell.type === 'merge') {
|
continue;
|
}
|
const startChunkX = Math.floor(cell.posX / chunkSize);
|
const endChunkX = Math.floor((cell.posX + Math.max(1, cell.width) - 0.01) / chunkSize);
|
const startChunkY = Math.floor(cell.posY / chunkSize);
|
const endChunkY = Math.floor((cell.posY + Math.max(1, cell.height) - 0.01) / chunkSize);
|
for (let chunkY = startChunkY; chunkY <= endChunkY; chunkY++) {
|
for (let chunkX = startChunkX; chunkX <= endChunkX; chunkX++) {
|
const key = chunkX + ',' + chunkY;
|
let list = chunkMap.get(key);
|
if (!list) {
|
list = [];
|
chunkMap.set(key, list);
|
}
|
list.push(cell);
|
}
|
}
|
}
|
}
|
|
const chunkList = [];
|
chunkMap.forEach((cells, key) => {
|
const keyParts = key.split(',');
|
const chunkX = parseInt(keyParts[0], 10) || 0;
|
const chunkY = parseInt(keyParts[1], 10) || 0;
|
const chunkLeft = chunkX * chunkSize;
|
const chunkTop = chunkY * chunkSize;
|
const chunkWidth = Math.max(1, Math.min(chunkSize, contentW - chunkLeft));
|
const chunkHeight = Math.max(1, Math.min(chunkSize, contentH - chunkTop));
|
const graphics = new PIXI.Graphics();
|
graphics.beginFill(0xb6e2e2);
|
graphics.lineStyle(1, 0xffffff, 1);
|
for (let i = 0; i < cells.length; i++) {
|
const cell = cells[i];
|
graphics.drawRect(cell.posX - chunkLeft, cell.posY - chunkTop, cell.width, cell.height);
|
}
|
graphics.endFill();
|
const texture = this.pixiApp.renderer.generateTexture(
|
graphics,
|
PIXI.SCALE_MODES.LINEAR,
|
1,
|
new PIXI.Rectangle(0, 0, chunkWidth, chunkHeight)
|
);
|
graphics.destroy(true);
|
const sprite = new PIXI.Sprite(texture);
|
sprite.position.set(chunkLeft, chunkTop);
|
sprite._chunkBounds = {
|
x: chunkLeft,
|
y: chunkTop,
|
width: chunkWidth,
|
height: chunkHeight
|
};
|
this.shelvesContainer.addChild(sprite);
|
chunkList.push(sprite);
|
});
|
this.shelfChunkList = chunkList;
|
this.updateVisibleShelfChunks();
|
},
|
getViewportLocalBounds(padding) {
|
if (!this.mapRoot || !this.pixiApp) {
|
return null;
|
}
|
const viewport = this.getViewportSize();
|
const pad = Math.max(0, Number(padding) || 0);
|
const points = [
|
new PIXI.Point(-pad, -pad),
|
new PIXI.Point(viewport.width + pad, -pad),
|
new PIXI.Point(-pad, viewport.height + pad),
|
new PIXI.Point(viewport.width + pad, viewport.height + pad)
|
];
|
let minX = Infinity;
|
let minY = Infinity;
|
let maxX = -Infinity;
|
let maxY = -Infinity;
|
points.forEach((point) => {
|
const local = this.mapRoot.toLocal(point);
|
if (local.x < minX) {
|
minX = local.x;
|
}
|
if (local.y < minY) {
|
minY = local.y;
|
}
|
if (local.x > maxX) {
|
maxX = local.x;
|
}
|
if (local.y > maxY) {
|
maxY = local.y;
|
}
|
});
|
if (!isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY)) {
|
return null;
|
}
|
return { minX: minX, minY: minY, maxX: maxX, maxY: maxY };
|
},
|
updateVisibleShelfChunks() {
|
if (!this.shelfChunkList || this.shelfChunkList.length === 0) {
|
return;
|
}
|
const localBounds = this.getViewportLocalBounds(this.shelfCullPadding);
|
if (!localBounds) {
|
return;
|
}
|
for (let i = 0; i < this.shelfChunkList.length; i++) {
|
const sprite = this.shelfChunkList[i];
|
const bounds = sprite && sprite._chunkBounds;
|
if (!bounds) {
|
continue;
|
}
|
const visible =
|
bounds.x < localBounds.maxX &&
|
bounds.x + bounds.width > localBounds.minX &&
|
bounds.y < localBounds.maxY &&
|
bounds.y + bounds.height > localBounds.minY;
|
if (sprite.visible !== visible) {
|
sprite.visible = visible;
|
}
|
}
|
},
|
scheduleShelfChunkCulling() {
|
if (this.shelfCullRaf) {
|
return;
|
}
|
this.shelfCullRaf = requestAnimationFrame(() => {
|
this.shelfCullRaf = null;
|
this.updateVisibleShelfChunks();
|
});
|
},
|
findIndexByOffsets(offsets, sizes, value) {
|
if (!offsets || !sizes || offsets.length === 0) {
|
return -1;
|
}
|
for (let i = 0; i < offsets.length; i++) {
|
const start = offsets[i];
|
const end = start + (sizes[i] || 0);
|
if (value >= start && value < end) {
|
return i;
|
}
|
}
|
return -1;
|
},
|
updateShelfHoverFromPointer(globalPos) {
|
if (!this.map || !this.mapRoot) {
|
return;
|
}
|
if (!this.mapRowOffsets.length || !this.mapColOffsets.length) {
|
return;
|
}
|
const local = this.mapRoot.toLocal(new PIXI.Point(globalPos.x, globalPos.y));
|
const rowIndex = this.findIndexByOffsets(this.mapRowOffsets, this.mapRowHeights, local.y);
|
|
if (rowIndex < 0) {
|
if (this.hoveredShelfCell) {
|
this.hoveredShelfCell = null;
|
this.hideShelfTooltip();
|
}
|
return;
|
}
|
let cell = null;
|
if (this.mapRowShelfCells && this.mapRowShelfCells[rowIndex]) {
|
const list = this.mapRowShelfCells[rowIndex];
|
for (let i = 0; i < list.length; i++) {
|
const it = list[i];
|
if (!it) {
|
continue;
|
}
|
if (
|
local.x >= it.posX &&
|
local.x < it.posX + it.width &&
|
local.y >= it.posY &&
|
local.y < it.posY + it.height
|
) {
|
cell = it;
|
break;
|
}
|
}
|
}
|
if (!cell || cell.type !== 'shelf') {
|
if (this.hoveredShelfCell) {
|
this.hoveredShelfCell = null;
|
this.hideShelfTooltip();
|
}
|
return;
|
}
|
if (this.hoveredShelfCell !== cell) {
|
this.hoveredShelfCell = cell;
|
this.shelfTooltip.item = cell;
|
this.shelfTooltip.text = this.getShelfArrangeInfo(cell);
|
this.shelfTooltip.visible = true;
|
}
|
this.updateShelfTooltipPositionByGlobal(globalPos);
|
},
|
normalizeLocTypeKey(value) {
|
if (value == null) {
|
return null;
|
}
|
const str = String(value).trim();
|
if (!str) {
|
return null;
|
}
|
const parts = str.split('-').filter((p) => p !== '');
|
if (parts.length >= 3) {
|
return parts.slice(0, parts.length - 1).join('-');
|
}
|
return str;
|
},
|
loadLocList() {
|
if (!window.$ || typeof baseUrl === 'undefined') {
|
return;
|
}
|
if (this.locListLoading) {
|
return;
|
}
|
this.locListLoading = true;
|
$.ajax({
|
url: baseUrl + '/console/map/locList',
|
headers: { token: localStorage.getItem('token') },
|
dataType: 'json',
|
method: 'GET',
|
success: (res) => {
|
if (res && !Array.isArray(res)) {
|
if (res.code === 403) {
|
parent.location.href = baseUrl + '/login';
|
return;
|
}
|
if (res.code !== 200) {
|
return;
|
}
|
}
|
const list = Array.isArray(res) ? res : res && res.code === 200 ? res.data : null;
|
if (!list || !Array.isArray(list)) {
|
return;
|
}
|
const map = new Map();
|
list.forEach((item) => {
|
if (!item) {
|
return;
|
}
|
const locType = item.locType != null ? item.locType : item.loc_type;
|
if (locType != null && locType !== '') {
|
const normalizedType = this.normalizeLocTypeKey(locType);
|
if (normalizedType && !map.has(normalizedType)) {
|
map.set(normalizedType, item);
|
}
|
}
|
});
|
this.locListMap = map;
|
this.locListLoaded = true;
|
if (this.shelfTooltip.visible) {
|
this.shelfTooltip.text = this.getShelfArrangeInfo(this.shelfTooltip.item);
|
}
|
},
|
complete: () => {
|
this.locListLoading = false;
|
}
|
});
|
},
|
showShelfTooltip(e, item) {
|
if (!item) {
|
return;
|
}
|
if (!this.isShelfTooltipAllowed()) {
|
this.hideShelfTooltip();
|
return;
|
}
|
if (!this.locListLoaded && !this.locListLoading) {
|
this.loadLocList();
|
}
|
this.shelfTooltip.item = item;
|
this.shelfTooltip.text = this.getShelfArrangeInfo(item);
|
this.updateShelfTooltipPosition(e);
|
this.shelfTooltip.visible = true;
|
},
|
updateShelfTooltipPosition(e) {
|
if (!e || !e.data || !e.data.global) {
|
return;
|
}
|
this.updateShelfTooltipPositionByGlobal(e.data.global);
|
},
|
updateShelfTooltipPositionByGlobal(globalPos) {
|
if (!this.isShelfTooltipAllowed()) {
|
this.hideShelfTooltip();
|
return;
|
}
|
if (!globalPos) {
|
return;
|
}
|
this.shelfTooltip.x = globalPos.x + 12;
|
this.shelfTooltip.y = globalPos.y + 12;
|
},
|
hideShelfTooltip() {
|
this.shelfTooltip.visible = false;
|
this.shelfTooltip.item = null;
|
},
|
isShelfTooltipAllowed() {
|
return this.getStageAbsScale() >= this.shelfTooltipMinScale;
|
},
|
getStageAbsScale() {
|
if (!this.pixiApp || !this.pixiApp.stage) {
|
return 1;
|
}
|
return Math.abs(this.pixiApp.stage.scale.x || 1);
|
},
|
updateShelfTooltipVisibilityByScale() {
|
if (this.shelfTooltip.visible && !this.isShelfTooltipAllowed()) {
|
this.hideShelfTooltip();
|
this.hoveredShelfCell = null;
|
}
|
},
|
getShelfArrangeInfo(item) {
|
if (!item.value) {
|
return '';
|
}
|
const list = item.value.split('-');
|
return `${list[1]}-${list[0]}`;
|
// const parts = [];
|
// const matchKey = this.getShelfMatchKey(item);
|
// if (matchKey != null) {
|
// parts.push('坐标:' + matchKey);
|
// }
|
// const locInfo = matchKey != null ? this.locListMap.get(matchKey) : null;
|
// if (locInfo) {
|
// const locNo = locInfo.locNo != null ? locInfo.locNo : locInfo.loc_no;
|
// const displayLocNo = this.stripLocLayer(locNo);
|
// if (displayLocNo != null) {
|
// parts.push('排列:' + displayLocNo);
|
// }
|
// }
|
// return parts.join(' ');
|
},
|
getShelfMatchKey(item) {
|
if (!item) {
|
return null;
|
}
|
const direct =
|
item.locType != null ? item.locType : item.loc_type != null ? item.loc_type : null;
|
const directKey = this.normalizeLocTypeKey(direct);
|
if (directKey) {
|
return directKey;
|
}
|
const rowIndex = item.rowIndex;
|
const colIndex = item.colIndex;
|
if (rowIndex == null || colIndex == null) {
|
return null;
|
}
|
const key0 = rowIndex + '-' + colIndex;
|
if (this.locListLoaded && this.locListMap && this.locListMap.size > 0) {
|
if (this.locListMap.has(key0)) {
|
return key0;
|
}
|
}
|
return null;
|
},
|
stripLocLayer(locNo) {
|
if (locNo == null) {
|
return null;
|
}
|
const str = String(locNo).trim();
|
if (!str) {
|
return null;
|
}
|
const parts = str.split('-').filter((p) => p !== '');
|
if (parts.length >= 3) {
|
return parts.slice(0, parts.length - 1).join('-');
|
}
|
return str;
|
},
|
shelfTooltipStyle() {
|
return {
|
position: 'absolute',
|
left: this.shelfTooltip.x + 'px',
|
top: this.shelfTooltip.y + 'px',
|
background: 'rgba(0,0,0,0.75)',
|
color: '#ffffff',
|
padding: '4px 8px',
|
borderRadius: '4px',
|
fontSize: '12px',
|
pointerEvents: 'none',
|
whiteSpace: 'nowrap',
|
zIndex: 10
|
};
|
},
|
/** Pixi v5:Sprite.anchor 为 (0.5,0.5) 时局部原点在纹理中心,文字用 (0,0);站点等默认 anchor (0,0) 仍用宽高一半。 */
|
positionSpriteLabelToTextureCenter(sprite) {
|
const textObj = sprite && sprite.textObj;
|
if (!textObj) {
|
return;
|
}
|
const ax = sprite.anchor != null ? sprite.anchor.x : 0;
|
const ay = sprite.anchor != null ? sprite.anchor.y : 0;
|
const originAtTextureCenter =
|
Math.abs(ax - 0.5) < 0.001 && Math.abs(ay - 0.5) < 0.001;
|
if (originAtTextureCenter) {
|
textObj.position.set(0, 0);
|
} else {
|
textObj.position.set(sprite.width / 2, sprite.height / 2);
|
}
|
},
|
adjustLabelScale() {
|
const s = this.pixiApp && this.pixiApp.stage ? Math.abs(this.pixiApp.stage.scale.x || 1) : 1;
|
const viewport = this.getViewportSize();
|
const vw = viewport.width;
|
const vh = viewport.height;
|
const margin = 50;
|
const mirrorSign = this.mapMirrorX ? -1 : 1;
|
const inverseRotation = -((this.mapRotation % 360) * Math.PI) / 180;
|
const tmpPoint = new PIXI.Point();
|
// 标签随地图缩放(与设备/站点几何一致);仅校正水平镜像与地图旋转,不再按 stage 缩放做反向补偿。
|
const apply = (map) => {
|
map &&
|
map.forEach((sprite) => {
|
const textObj = sprite && sprite.textObj;
|
if (!textObj) {
|
return;
|
}
|
textObj.scale.set(mirrorSign, 1);
|
textObj.rotation = inverseRotation;
|
this.positionSpriteLabelToTextureCenter(sprite);
|
sprite.getGlobalPosition(tmpPoint);
|
const on =
|
tmpPoint.x >= -margin &&
|
tmpPoint.y >= -margin &&
|
tmpPoint.x <= vw + margin &&
|
tmpPoint.y <= vh + margin;
|
textObj.visible = s >= 0.25 && on;
|
});
|
};
|
apply(this.pixiStaMap);
|
apply(this.pixiCrnMap);
|
apply(this.pixiDualCrnMap);
|
apply(this.pixiRgvMap);
|
},
|
rotateMap() {
|
this.mapRotation = (this.mapRotation + 90) % 360;
|
this.applyMapTransform(true);
|
this.saveMapTransformConfig();
|
},
|
resetMapView() {
|
this.fitStageToContent();
|
this.scheduleAdjustLabels();
|
this.scheduleShelfChunkCulling();
|
},
|
toggleStationDirection() {
|
this.showStationDirection = !this.showStationDirection;
|
this.applyStationDirectionVisibility();
|
},
|
toggleMirror() {
|
this.mapMirrorX = !this.mapMirrorX;
|
this.applyMapTransform(true);
|
this.saveMapTransformConfig();
|
},
|
openStationColorConfigPage() {
|
if (typeof window === 'undefined') {
|
return;
|
}
|
const url =
|
(typeof baseUrl !== 'undefined' ? baseUrl : '') + '/views/watch/stationColorConfig.html';
|
const layerInstance = (window.top && window.top.layer) || window.layer;
|
if (layerInstance && typeof layerInstance.open === 'function') {
|
layerInstance.open({
|
type: 2,
|
title: '站点颜色配置',
|
maxmin: true,
|
area: ['980px', '760px'],
|
shadeClose: false,
|
content: url
|
});
|
return;
|
}
|
window.open(url, '_blank');
|
},
|
loadFakeProcessStatus() {
|
if (
|
typeof window === 'undefined' ||
|
typeof $ === 'undefined' ||
|
typeof baseUrl === 'undefined'
|
) {
|
this.fakeOperationVisible = false;
|
return;
|
}
|
$.ajax({
|
url: baseUrl + '/openapi/getFakeSystemRunStatus',
|
method: 'get',
|
success: function (res) {
|
const data = res && res.data ? res.data : null;
|
this.fakeOperationVisible = !!(data && data.isFake);
|
}.bind(this),
|
error: function () {
|
this.fakeOperationVisible = false;
|
}.bind(this)
|
});
|
},
|
openFakeOperationConfigPage() {
|
if (typeof window === 'undefined' || !this.fakeOperationVisible) {
|
return;
|
}
|
const url =
|
(typeof baseUrl !== 'undefined' ? baseUrl : '') + '/views/watch/fakeOperationConfig.html';
|
const layerInstance = (window.top && window.top.layer) || window.layer;
|
if (layerInstance && typeof layerInstance.open === 'function') {
|
layerInstance.open({
|
type: 2,
|
title: '仿真操作',
|
maxmin: true,
|
area: ['1180px', '820px'],
|
shadeClose: false,
|
content: url
|
});
|
return;
|
}
|
window.open(url, '_blank');
|
},
|
parseRotation(value) {
|
const num = parseInt(value, 10);
|
if (!isFinite(num)) {
|
return 0;
|
}
|
const rot = ((num % 360) + 360) % 360;
|
return rot === 90 || rot === 180 || rot === 270 ? rot : 0;
|
},
|
parseMirror(value) {
|
if (value === true || value === false) {
|
return value;
|
}
|
if (value == null) {
|
return false;
|
}
|
const str = String(value).toLowerCase();
|
return str === '1' || str === 'true' || str === 'y';
|
},
|
getDefaultStationStatusColors() {
|
return {
|
'site-auto': 0x78ff81,
|
'site-auto-run': 0xfa51f6,
|
'site-auto-id': 0xc4c400,
|
'site-auto-run-id': 0x30bffc,
|
'site-enable-in': 0xa81dee,
|
'site-unauto': 0xb8b8b8,
|
'machine-pakin': 0x30bffc,
|
'machine-pakout': 0x97b400,
|
'site-run-block': 0xe69138,
|
'site-error': 0xdb2828
|
};
|
},
|
parseColorConfigValue(value, fallback) {
|
if (typeof value === 'number' && isFinite(value)) {
|
return value;
|
}
|
const str = String(value == null ? '' : value).trim();
|
if (!str) {
|
return fallback;
|
}
|
if (/^#[0-9a-fA-F]{6}$/.test(str)) {
|
return parseInt(str.slice(1), 16);
|
}
|
if (/^#[0-9a-fA-F]{3}$/.test(str)) {
|
const expanded =
|
str.charAt(1) +
|
str.charAt(1) +
|
str.charAt(2) +
|
str.charAt(2) +
|
str.charAt(3) +
|
str.charAt(3);
|
return parseInt(expanded, 16);
|
}
|
if (/^0x[0-9a-fA-F]{6}$/i.test(str)) {
|
return parseInt(str.slice(2), 16);
|
}
|
if (/^[0-9]+$/.test(str)) {
|
const num = parseInt(str, 10);
|
return isNaN(num) ? fallback : num;
|
}
|
return fallback;
|
},
|
loadStationColorConfig() {
|
if (!window.$ || typeof baseUrl === 'undefined') {
|
return;
|
}
|
$.ajax({
|
url: baseUrl + '/watch/stationColor/config/auth',
|
headers: { token: localStorage.getItem('token') },
|
dataType: 'json',
|
method: 'GET',
|
success: (res) => {
|
if (!res || res.code !== 200 || !res.data) {
|
if (res && res.code === 403) {
|
parent.location.href = baseUrl + '/login';
|
}
|
return;
|
}
|
this.applyStationColorConfigPayload(res.data);
|
}
|
});
|
},
|
applyStationColorConfigPayload(data) {
|
const defaults = this.getDefaultStationStatusColors();
|
const nextColors = Object.assign({}, defaults);
|
const items = Array.isArray(data.items) ? data.items : [];
|
items.forEach((item) => {
|
if (!item || !item.status || defaults[item.status] == null) {
|
return;
|
}
|
nextColors[item.status] = this.parseColorConfigValue(item.color, defaults[item.status]);
|
});
|
this.stationStatusColors = nextColors;
|
},
|
buildMissingMapConfigList(byCode) {
|
const createList = [];
|
if (!byCode[this.mapConfigCodes.rotate]) {
|
createList.push({
|
name: '地图旋转',
|
code: this.mapConfigCodes.rotate,
|
value: String(this.mapRotation || 0),
|
type: 1,
|
status: 1,
|
selectType: 'map'
|
});
|
}
|
if (!byCode[this.mapConfigCodes.mirror]) {
|
createList.push({
|
name: '地图镜像',
|
code: this.mapConfigCodes.mirror,
|
value: this.mapMirrorX ? '1' : '0',
|
type: 1,
|
status: 1,
|
selectType: 'map'
|
});
|
}
|
return createList;
|
},
|
createMapConfigs(createList) {
|
if (
|
!window.$ ||
|
typeof baseUrl === 'undefined' ||
|
!Array.isArray(createList) ||
|
createList.length === 0
|
) {
|
return;
|
}
|
createList.forEach((cfg) => {
|
$.ajax({
|
url: baseUrl + '/config/add/auth',
|
headers: { token: localStorage.getItem('token') },
|
method: 'POST',
|
data: cfg
|
});
|
});
|
},
|
loadMapTransformConfig() {
|
if (!window.$ || typeof baseUrl === 'undefined') {
|
return;
|
}
|
$.ajax({
|
url: baseUrl + '/config/listAll/auth',
|
headers: { token: localStorage.getItem('token') },
|
dataType: 'json',
|
method: 'GET',
|
success: (res) => {
|
if (!res || res.code !== 200 || !Array.isArray(res.data)) {
|
if (res && res.code === 403) {
|
parent.location.href = baseUrl + '/login';
|
}
|
return;
|
}
|
const byCode = {};
|
res.data.forEach((item) => {
|
if (item && item.code) {
|
byCode[item.code] = item;
|
}
|
});
|
const rotateCfg = byCode[this.mapConfigCodes.rotate];
|
const mirrorCfg = byCode[this.mapConfigCodes.mirror];
|
if (rotateCfg && rotateCfg.value != null) {
|
this.mapRotation = this.parseRotation(rotateCfg.value);
|
}
|
if (mirrorCfg && mirrorCfg.value != null) {
|
this.mapMirrorX = this.parseMirror(mirrorCfg.value);
|
}
|
this.createMapConfigs(this.buildMissingMapConfigList(byCode));
|
if (this.mapContentSize && this.mapContentSize.width > 0) {
|
this.applyMapTransform(true);
|
}
|
}
|
});
|
},
|
saveMapTransformConfig() {
|
if (!window.$ || typeof baseUrl === 'undefined') {
|
return;
|
}
|
const updateList = [
|
{
|
code: this.mapConfigCodes.rotate,
|
value: String(this.mapRotation || 0)
|
},
|
{
|
code: this.mapConfigCodes.mirror,
|
value: this.mapMirrorX ? '1' : '0'
|
}
|
];
|
$.ajax({
|
url: baseUrl + '/config/updateBatch',
|
headers: { token: localStorage.getItem('token') },
|
data: JSON.stringify(updateList),
|
dataType: 'json',
|
contentType: 'application/json;charset=UTF-8',
|
method: 'POST'
|
});
|
},
|
getTransformedContentSize() {
|
const size = this.mapContentSize || { width: 0, height: 0 };
|
const w = size.width || 0;
|
const h = size.height || 0;
|
const rot = ((this.mapRotation % 360) + 360) % 360;
|
const swap = rot === 90 || rot === 270;
|
return { width: swap ? h : w, height: swap ? w : h };
|
},
|
fitStageToContent() {
|
if (!this.pixiApp || !this.mapContentSize) {
|
return;
|
}
|
const size = this.getTransformedContentSize();
|
const contentW = size.width || 0;
|
const contentH = size.height || 0;
|
if (contentW <= 0 || contentH <= 0) {
|
return;
|
}
|
const viewport = this.getViewportSize();
|
const vw = viewport.width;
|
const vh = viewport.height;
|
const padding = this.getViewportPadding();
|
const availableW = Math.max(1, vw - padding.left - padding.right);
|
const availableH = Math.max(1, vh - padding.top - padding.bottom);
|
let scale = Math.min(availableW / contentW, availableH / contentH) * 0.95;
|
if (!isFinite(scale) || scale <= 0) {
|
scale = 1;
|
}
|
const baseW = this.mapContentSize.width || contentW;
|
const baseH = this.mapContentSize.height || contentH;
|
const mirrorX = this.mapMirrorX ? -1 : 1;
|
const scaleX = scale * mirrorX;
|
const scaleY = scale;
|
const centerX = padding.left + availableW / 2;
|
const centerY = padding.top + availableH / 2;
|
const posX = centerX - (baseW / 2) * scaleX;
|
const posY = centerY - (baseH / 2) * scaleY;
|
this.pixiApp.stage.setTransform(posX, posY, scaleX, scaleY, 0, 0, 0, 0, 0);
|
},
|
applyMapTransform(fitToView) {
|
if (!this.mapRoot || !this.mapContentSize) {
|
return;
|
}
|
const contentW = this.mapContentSize.width || 0;
|
const contentH = this.mapContentSize.height || 0;
|
if (contentW <= 0 || contentH <= 0) {
|
return;
|
}
|
this.mapRoot.pivot.set(contentW / 2, contentH / 2);
|
this.mapRoot.position.set(contentW / 2, contentH / 2);
|
this.mapRoot.rotation = ((this.mapRotation % 360) * Math.PI) / 180;
|
this.mapRoot.scale.set(1, 1);
|
if (fitToView) {
|
this.fitStageToContent();
|
}
|
this.scheduleAdjustLabels();
|
this.scheduleShelfChunkCulling();
|
},
|
scheduleAdjustLabels() {
|
if (this.adjustLabelTimer) {
|
clearTimeout(this.adjustLabelTimer);
|
}
|
this.adjustLabelTimer = setTimeout(() => {
|
this.adjustLabelScale();
|
this.updateShelfTooltipVisibilityByScale();
|
this.adjustLabelTimer = null;
|
}, 20);
|
},
|
// 判断有没有移动过头
|
isMoveFinish(point, path, point2, path2) {
|
if (path.prev === path2 || path.prev?.prev === path2) {
|
return true;
|
}
|
if (path2 === path) {
|
if (Math.abs(path.x - path2.x) < EPSILON && Math.abs(path.y - path2.y) < EPSILON) {
|
return true;
|
}
|
const vectorPoint = G.normalizeVector(point, point2);
|
let vectorPath;
|
if (path.type === 'arc') {
|
vectorPath = G.normalizeVector(
|
{ x: path.arcStartX, y: path.arcStartY },
|
{ x: path.arcEndX, y: path.arcEndY }
|
);
|
} else {
|
vectorPath = G.normalizeVector({ x: path.startX, y: path.startY }, path);
|
}
|
return (
|
Math.sign(vectorPath.y) === -Math.sign(vectorPoint.y) ||
|
Math.sign(vectorPath.x) === -Math.sign(vectorPoint.x)
|
);
|
}
|
return false;
|
},
|
isPointOnSegment(point, A, B) {
|
// 计算叉积,判断点是否在直线AB上
|
const crossProduct = (point.x - A.x) * (B.y - A.y) - (point.y - A.y) * (B.x - A.x);
|
// 不在直线上,则肯定不在线段上
|
if (Math.abs(crossProduct) > EPSILON) {
|
return false;
|
}
|
// 检查点是否在线段A和B的坐标范围内
|
const minX = Math.min(A.x, B.x);
|
const maxX = Math.max(A.x, B.x);
|
const minY = Math.min(A.y, B.y);
|
const maxY = Math.max(A.y, B.y);
|
// 使用容差确保包括端点
|
return (
|
point.x >= minX - EPSILON &&
|
point.x < maxX + EPSILON &&
|
point.y >= minY - EPSILON &&
|
point.y < maxY + EPSILON
|
);
|
},
|
getCurveDistance(p1, p2, path1, path2, p1Angle, p2Angle, distance = 0) {
|
// console.log('getCurveDistance', p1, p2, path1, path2, p1Angle, p2Angle, distance);
|
if (path1 === path2) {
|
if (path1.type === 'line') {
|
return G.calcDistance(p1, p2) + distance;
|
} else {
|
return Math.abs(((p2Angle - p1Angle) % (2 * Math.PI)) * path1.radius) + distance;
|
}
|
}
|
if (path1.type === 'line') {
|
const restDistance = G.calcDistance(p1, path1);
|
return this.getCurveDistance(
|
path1,
|
p2,
|
path1.next,
|
path2,
|
path2.startAngle,
|
p2Angle,
|
restDistance + distance
|
);
|
} else {
|
const startAngle = path1.startAngle;
|
const endAngle = path1.endAngle;
|
let tmpCurrentAngle = p1Angle || startAngle;
|
const currentAngle = this.getNormalizeAngle(tmpCurrentAngle, startAngle, endAngle);
|
const restDistance = Math.abs((endAngle - currentAngle) * path1.radius);
|
// console.log('圆弧' + path1.index, newStartAngle, newEndAngle, newCurrentAngle, restDistance)
|
return this.getCurveDistance(
|
{ x: p1.arcEndX, y: p1.arcEndY },
|
p2,
|
path1.next,
|
path2,
|
endAngle,
|
p2Angle,
|
restDistance + distance
|
);
|
}
|
},
|
getNormalizeAngle(angle, startAngle, endAngle) {
|
if (angle < startAngle && angle < endAngle) {
|
return angle + 2 * Math.PI;
|
}
|
return angle;
|
},
|
pointToSegment(point, segStart, segEnd) {
|
// 解构坐标
|
const { x: px, y: py } = point;
|
const { x: ax, y: ay } = segStart;
|
const { x: bx, y: by } = segEnd;
|
|
// 向量 AB 和 AP
|
const abx = bx - ax;
|
const aby = by - ay;
|
const apx = px - ax;
|
const apy = py - ay;
|
|
// 线段长度的平方
|
const abLenSq = abx * abx + aby * aby;
|
|
// 线段退化为点 A (或 B)
|
if (abLenSq === 0) {
|
return Math.hypot(apx, apy);
|
}
|
|
// 计算投影参数 t,并钳制到 [0, 1]
|
let t = (apx * abx + apy * aby) / abLenSq;
|
t = Math.max(0, Math.min(1, t));
|
|
// 线段上离点 P 最近的点坐标
|
const closestX = ax + t * abx;
|
const closestY = ay + t * aby;
|
|
return { x: closestX, y: closestY, t };
|
},
|
getMappingInfo(sprite) {
|
if (!sprite) {
|
return false;
|
}
|
let minDistance = Infinity;
|
let minPath;
|
let x, y, angle;
|
|
sprite.trackInfo.pathList.forEach((path, index) => {
|
if (path.type === 'line') {
|
// 求线段到某点的距离
|
const pointInSegment = this.pointToSegment(
|
sprite,
|
{ x: path.startX, y: path.startY },
|
path
|
);
|
const distance = G.calcDistance(pointInSegment, sprite);
|
if (distance < minDistance) {
|
minDistance = distance;
|
minPath = path;
|
x = pointInSegment.x;
|
y = pointInSegment.y;
|
}
|
} else {
|
const toCenter = G.calcDistance(path, sprite);
|
const distance = Math.abs(path.radius - toCenter);
|
if (distance < minDistance) {
|
const vector = G.normalizeVector(path, sprite);
|
minDistance = distance;
|
minPath = path;
|
x = path.x + vector.x * path.radius;
|
y = path.y + vector.y * path.radius;
|
angle = Math.atan2(vector.y, vector.x);
|
}
|
}
|
});
|
|
if (sprite.trackInfo.type === 'annulus' && minPath && x != null && y != null) {
|
const c = G.centerAnnulusBandPoint(sprite.trackInfo, x, y, minPath);
|
x = c.x;
|
y = c.y;
|
}
|
|
return { x, y, angle, path: minPath };
|
},
|
distanceBasedEasingSigmoid(remaining, threshold = 1, steepness = 10, maxSpeedChange = 0.3) {
|
// 缓动函数,使用Sigmoid函数作为核心:f(x) = 1 / (1 + e^(-k*(x - threshold)))
|
// remaining: 输入值
|
// threshold: 函数中心点,输出为1
|
// steepness: 曲线陡峭度,控制过渡区的宽度
|
const exponent = -steepness * (remaining - threshold);
|
const sigmoid = 1 / (1 + Math.exp(exponent));
|
|
// 将Sigmoid输出从[0,1]映射到速度范围,例如[0.7, 1.3]
|
const minSpeed = 1 - maxSpeedChange;
|
const maxSpeed = 1 + maxSpeedChange;
|
return minSpeed + (maxSpeed - minSpeed) * sigmoid;
|
},
|
// Poll
|
startAnnulusDevicePoll() {
|
if (!this.annulusPoller) {
|
this.annulusPoller = new Poller({
|
periodMs: 1000,
|
alpha: 0.2,
|
fetchFn: (poller) => this.getAnnulusDeviceInfo(poller)
|
});
|
}
|
this.annulusPoller.start();
|
},
|
stopAnnulusDevicePoll() {
|
if (this.annulusPoller) {
|
this.annulusPoller.stop();
|
}
|
},
|
async getAnnulusDeviceInfo(poller) {
|
if (poller.abortController) {
|
try {
|
poller.abortController.abort();
|
} catch (e) {}
|
}
|
poller.abortController = new AbortController();
|
|
const res = await fetch('http://127.0.0.1:9091/rs-car/rgv/ring/through/rgv/position/data', {
|
method: 'POST',
|
signal: poller.abortController.signal
|
});
|
if (!res.ok) {
|
throw new Error(`getAnnulusDeviceInfo http ${res.status}`);
|
}
|
const json = await res.json();
|
this.setDeviceInfoByBarcode('rgv', json);
|
},
|
//todo: 测试代码
|
fakeSetSiteInfo(staSiteList) {
|
const staItem = {
|
autoing:true,
|
barcode:"",
|
emptyMk:false,
|
enableIn:false,
|
error:0,
|
fullPlt:false,
|
inBarcodeError:false,
|
inEnable:true,ioMode:2,loading:false,outEnable:true,palletHeight:0,runBlock:false,stationId:1102,stationStatus:"site-auto",taskNo:0}
|
const list = staSiteList.map(item => {
|
return {
|
...staItem,
|
...item
|
}
|
})
|
this.setSiteInfo(list);
|
},
|
async fakeMove(newList = [0, 10, 20, 30, 40, 50], newRaw = {}) {
|
const finalList = newList;
|
const rawByBarCode = {
|
index: 17,
|
modeColor: '#4169E1',
|
statusColor: '#27AE60',
|
rgvPos: 0,
|
rgvPosMax: ['el_1775520471475', 0, FAKE_MAX_LAYER]
|
};
|
const createTimeout = () => {
|
const p1 = new Promise((res, rej) => {
|
setTimeout(() => {
|
res(1);
|
}, 1000);
|
});
|
return p1;
|
};
|
for await (const p of finalList) {
|
await createTimeout();
|
this.setDeviceInfoByBarcode(newRaw.type || 'rgv', [
|
{ ...rawByBarCode, ...newRaw, rgvPos: (p * FAKE_MAX_LAYER) / 100 }
|
]);
|
}
|
},
|
createFakeButton() {
|
if (!this.mapRoot || typeof PIXI === 'undefined') {
|
return;
|
}
|
if (this.fakeOriginButton) {
|
try {
|
this.fakeOriginButton.destroy({ children: true });
|
} catch (e) {}
|
this.fakeOriginButton = null;
|
}
|
const bw = 76;
|
const bh = 30;
|
const bg = new PIXI.Graphics();
|
bg.lineStyle(1, 0xffffff, 0.65);
|
bg.beginFill(0x2563c7, 0.95);
|
bg.drawRect(0, 0, bw, bh);
|
bg.endFill();
|
const textStyle = new PIXI.TextStyle({
|
fontFamily: 'Arial',
|
fontSize: 12,
|
fill: '#ffffff',
|
align: 'center'
|
});
|
const label = new PIXI.Text('模拟运动(仅测试用)', textStyle);
|
label.anchor.set(0.5);
|
label.position.set(bw / 2, bh / 2);
|
const btn = new PIXI.Container();
|
btn.addChild(bg);
|
btn.addChild(label);
|
btn.position.set(200, 200);
|
btn.interactive = true;
|
btn.buttonMode = true;
|
btn.hitArea = new PIXI.Rectangle(0, 0, bw, bh);
|
btn.on('pointertap', (e) => {
|
this.fakeMoveButtonClick();
|
});
|
this.mapRoot.addChild(btn);
|
|
btn.zIndex = 9999;
|
},
|
async fakeMoveButtonClick() {
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
// 设备17
|
const list = [];
|
for (let i = 0; i <= 7; i++) {
|
list.push(i * 10);
|
}
|
const p1 = this.fakeMove(list);
|
const list2 = [];
|
p1.then(async () => {
|
await sleep(1000);
|
await this.fakeMove([70], { index: 17, statusColor: '#a5d6f7' });
|
await sleep(1000);
|
await this.fakeMove([80], { index: 17, statusColor: '#a5d6f7' });
|
await sleep(1000);
|
await this.fakeMove([80], { index: 17, statusColor: '#245a9a' });
|
}, 1000);
|
|
// 设备18
|
for (let i = 0; i <= 8; i++) {
|
list2.push(i * 2);
|
}
|
const firstPhasePos = 17
|
list2.push(firstPhasePos);
|
Promise.resolve().then( async ()=>{
|
await this.fakeMove(list2, { index: 18 });
|
await sleep(1000);
|
await this.fakeSetSiteInfo([{stationId:1211,stationStatus:"site-auto-run-id"}]);
|
await sleep(1000);
|
await this.fakeSetSiteInfo([{stationId:1209,stationStatus:"site-auto-run-id"},{stationId:1211,stationStatus:"site-auto"}]);
|
await sleep(1000);
|
await this.fakeSetSiteInfo([{stationId:1208,stationStatus:"site-auto-run-id"},{stationId:1209,stationStatus:"site-auto"}]);
|
await sleep(1000);
|
await this.fakeSetSiteInfo([{stationId:1208,stationStatus:"site-auto"}]);
|
await this.fakeMove([firstPhasePos], { index: 18 , statusColor: '#a5d6f7' });
|
const list2_2 = []
|
for (let i = 1; i <= 8; i++) {
|
list2_2.push(firstPhasePos + i * 5.5);
|
}
|
list2_2.push(62.7)
|
await this.fakeMove(list2_2, { index: 18 , statusColor: '#a5d6f7' });
|
await sleep(1000);
|
await this.fakeMove([list2_2.at(-1)], { index: 18 , statusColor: '#245a9a' });
|
await this.fakeSetSiteInfo([{stationId:1107,stationStatus:"site-auto-run-id"}]);
|
await sleep(1000);
|
await this.fakeSetSiteInfo([{stationId:1107,stationStatus:"site-auto"}]);
|
await sleep(1000);
|
const list_crn = []
|
for (let i = 0; i <= 7; i++) {
|
list_crn.push(i * 10);
|
}
|
await this.fakeMove(list_crn, { index: 15, statusColor: '#a5d6f7', type: 'crn' });
|
await this.fakeMove([list_crn.at(-1)], { index: 15, statusColor: '#245a9a', type: 'crn' });
|
})
|
// await this.fakeMove([47.8], { index: 18, statusColor: '#a5d6f7' });
|
// await sleep(1000);
|
// await this.fakeMove([52.8, 57.8, 62.8, 63.5], { index: 18, statusColor: '#a5d6f7' });
|
// await sleep(1000);
|
// await this.fakeMove([63.5], { index: 18, statusColor: '#245a9a' });
|
|
// 设备16
|
let list3 = [];
|
for (let i = 0; i <= 10; i++) {
|
list3.push(i * 10);
|
}
|
for (let i = 9; i >= 0; i--) {
|
list3.push(i * 10);
|
}
|
await this.fakeMove(list3, { index: 16, statusColor: '#a5d6f7', type: 'crn' });
|
await sleep(1000);
|
await this.fakeMove([0], { index: 16, statusColor: '#245a9a', type: 'crn' });
|
}
|
}
|
});
|