/**
|
* 环穿 / 平滑轨道几何:直角多边形转圆弧路径、PIXI 绘制、设备朝向等。
|
* 环穿持久化的 pathList 为中轨;外圈、内圈仅在绘制时由 buildAnnulusPaths 计算。
|
* 供 basMap 编辑器与监控 MapCanvas 共用。需在页面中先于 editor.js / MapCanvas.js 引入。
|
*/
|
(function (global) {
|
'use strict';
|
|
var TYPE_META = {
|
shelf: {
|
label: '货架',
|
shortLabel: 'SHELF',
|
fill: 0x7d96bf,
|
border: 0x4f6486
|
},
|
repairHub: {
|
label: '维修站台',
|
shortLabel: 'HUB',
|
fill: 0x8eb89a,
|
border: 0x4a6b55
|
},
|
devp: {
|
label: '输送线',
|
shortLabel: 'DEVP',
|
fill: 0xf0b06f,
|
border: 0xa45f21
|
},
|
crn: {
|
label: '堆垛机轨道',
|
shortLabel: 'CRN',
|
fill: 0x68bfd0,
|
border: 0x1d6e81,
|
alpha: 0.12
|
},
|
dualCrn: {
|
label: '双工位轨道',
|
shortLabel: 'DCRN',
|
fill: 0x54c1a4,
|
border: 0x0f7b62,
|
alpha: 0.12
|
},
|
rgv: {
|
label: 'RGV轨道',
|
shortLabel: 'RGV',
|
fill: 0xc691e9,
|
border: 0x744b98,
|
alpha: 0.12
|
},
|
annulus: {
|
label: '环穿',
|
shortLabel: 'ANNULUS',
|
fill: 0xe6b3b3,
|
border: 0xcc6666,
|
alpha: 0
|
}
|
};
|
|
function normalizeVector(p1, p2) {
|
var dx = p2.x - p1.x;
|
var dy = p2.y - p1.y;
|
var length = Math.sqrt(dx * dx + dy * dy);
|
return { x: dx / length, y: dy / length };
|
}
|
|
function calcDistance(p1, p2) {
|
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
|
}
|
|
function safeParseJson(text) {
|
if (!text) {
|
return null;
|
}
|
if (typeof text === 'object') {
|
return text;
|
}
|
if (typeof text !== 'string') {
|
return null;
|
}
|
try {
|
return JSON.parse(text);
|
} catch (e) {
|
return null;
|
}
|
}
|
|
function getNormalizeAngle(angle, startAngle, endAngle) {
|
// 将 angle 归一化到 [startAngle, startAngle + 2*PI) 范围
|
var twoPi = 2 * Math.PI;
|
var rangeStart = startAngle;
|
|
var normalized = angle % twoPi;
|
if (normalized < 0) {
|
normalized += twoPi;
|
}
|
|
var normalizedStart = rangeStart % twoPi;
|
if (normalizedStart < 0) {
|
normalizedStart += twoPi;
|
}
|
|
// 计算相对偏移
|
var offset = normalized - normalizedStart;
|
if (offset < 0) {
|
offset += twoPi;
|
}
|
return rangeStart + offset;
|
}
|
|
function getPositionAfterMove(params) {
|
var point = params.point;
|
var pathList = params.pathList;
|
var deltaDistance = params.deltaDistance;
|
var angle = params.angle;
|
var path, pathIndex;
|
if (params.path) {
|
path = params.path;
|
pathIndex = pathList.indexOf(path);
|
} else {
|
pathIndex = params.pathIndex;
|
path = pathList[pathIndex];
|
}
|
var nextIndex = (pathIndex + 1) % pathList.length;
|
var prevIndex = (pathIndex - 1 + pathList.length) % pathList.length;
|
var isBackward = deltaDistance < 0;
|
var moveDistance = Math.abs(deltaDistance);
|
if (moveDistance === 0) {
|
return {
|
x: point.x,
|
y: point.y,
|
path: path,
|
angle: angle
|
};
|
}
|
|
if (path.type === 'line') {
|
var targetPoint = isBackward ? { x: path.startX, y: path.startY } : { x: path.x, y: path.y };
|
var restDistance = calcDistance(point, targetPoint);
|
var vector = normalizeVector(point, targetPoint);
|
if (moveDistance < restDistance) {
|
// vector 指向本段目标端(正向为终点,反向为起点)。反向时 deltaDistance<0,
|
// 必须用 +moveDistance 沿 vector 走,不能用 vector*deltaDistance(负号会把位移折向另一端)。
|
var stepAlong = isBackward ? moveDistance : deltaDistance;
|
var x = point.x + vector.x * stepAlong;
|
var y = point.y + vector.y * stepAlong;
|
return { x: x, y: y, path: path };
|
}
|
// pathList等于1说明就1条轨道,直接返回对应端点
|
if (pathList.length === 1) {
|
return {
|
x: targetPoint.x,
|
y: targetPoint.y,
|
path: path
|
};
|
}
|
return getPositionAfterMove({
|
point: targetPoint,
|
pathList: pathList,
|
pathIndex: isBackward ? prevIndex : nextIndex,
|
deltaDistance: isBackward ? deltaDistance + restDistance : deltaDistance - restDistance
|
});
|
}
|
|
var startAngle = path.startAngle;
|
var endAngle = path.endAngle;
|
var inferredAngle = Math.atan2(point.y - path.y, point.x - path.x);
|
var tmpCurrentAngle = angle != null ? angle : inferredAngle;
|
var currentAngle = getNormalizeAngle(tmpCurrentAngle, startAngle, endAngle);
|
var restDistance2 = isBackward
|
? Math.abs((currentAngle - startAngle) * path.radius)
|
: Math.abs((endAngle - currentAngle) * path.radius);
|
if (moveDistance < restDistance2) {
|
var deltaAngle = (deltaDistance / path.radius) * -path.crossProduct;
|
var newAngle = currentAngle + deltaAngle;
|
return {
|
x: path.x + path.radius * Math.cos(newAngle),
|
y: path.y + path.radius * Math.sin(newAngle),
|
path: path,
|
angle: newAngle
|
};
|
}
|
return getPositionAfterMove({
|
point: isBackward
|
? { x: path.arcStartX, y: path.arcStartY }
|
: { x: path.arcEndX, y: path.arcEndY },
|
pathList: pathList,
|
pathIndex: isBackward ? prevIndex : nextIndex,
|
deltaDistance: isBackward ? deltaDistance + restDistance2 : deltaDistance - restDistance2,
|
angle: isBackward ? path.startAngle : path.endAngle
|
});
|
}
|
|
function getAllDistance(pathList) {
|
var totalDistance = 0;
|
pathList.forEach(function (path) {
|
if (path.type === 'line') {
|
totalDistance += calcDistance(path, { x: path.startX, y: path.startY });
|
} else {
|
totalDistance += Math.abs(
|
((path.endAngle - path.startAngle) % (Math.PI * 2)) * path.radius
|
);
|
}
|
});
|
return totalDistance;
|
}
|
|
/** 相对轨道窄边(厚度方向)整体缩小车体,避免水平/垂直轨道上贴满轨道带 */
|
var TRACK_DEVICE_BOX_SCALE = 0.9;
|
|
/**
|
* 环穿单侧内缩距离:按车宽 across 的比例估算(优先 `value.deviceList[].deviceWidth`,否则环穿自动规则),且不低于下限。
|
* 元素可设 `annulusBandInset`(非负)覆盖。
|
*/
|
var ANNULUS_INSET_FROM_DEVICE_RATIO = 0.26;
|
/** @type {number} 默认最小内缩(像素) */
|
var ANNULUS_INSET_MIN_PIXELS = 5;
|
|
/**
|
* 轨道上设备「自动」像素尺寸(沿轨道方向 × 垂直轨道方向)。
|
* 直线轨道:短边 × TRACK_DEVICE_BOX_SCALE 为 across,along 为其 2 倍(与 drawCrnDeviceGraphics / drawRgvDeviceGraphics 一致)。
|
* 环穿:先按同上得到基准盒,再缩放到短边的 15%。
|
* @param {{ type: string, width: number, height: number, pathList?: any[] }} rect
|
* @returns {{ along: number, across: number } | null}
|
*/
|
function getAutoTrackDeviceBox(rect) {
|
if (!rect) {
|
return null;
|
}
|
var rectW = Number(rect.width);
|
var rectH = Number(rect.height);
|
if (!isFinite(rectW) || !isFinite(rectH) || rectW <= 0 || rectH <= 0) {
|
return null;
|
}
|
var shortLen = Math.min(rectW, rectH);
|
var across = Math.round(shortLen * TRACK_DEVICE_BOX_SCALE);
|
var along = Math.round(shortLen * 2 * TRACK_DEVICE_BOX_SCALE);
|
if (rect.type === 'annulus') {
|
var scale = (shortLen * 0.15) / across;
|
return {
|
along: Math.max(2, Math.round(along * scale)),
|
across: Math.max(2, Math.round(across * scale))
|
};
|
}
|
return { along, across };
|
}
|
|
function getDeviceInfo(rect) {
|
var isHorizontal = rect.width > rect.height;
|
var longLength = isHorizontal ? rect.width : rect.height;
|
var shortLength = isHorizontal ? rect.height : rect.width;
|
var deviceForm = safeParseJson(rect.value);
|
if (!deviceForm || !deviceForm.deviceList || deviceForm.deviceList.length === 0) {
|
return deviceForm;
|
}
|
|
if (rect.type === 'annulus') {
|
var pathList = rect.pathList || [];
|
if (!pathList.length) {
|
return deviceForm;
|
}
|
var allDistance = getAllDistance(pathList);
|
deviceForm.deviceList.forEach(function (item) {
|
var deltaDistance = (allDistance * item.progress) / 100;
|
var startPoint = {
|
x: pathList[0].arcStartX,
|
y: pathList[0].arcStartY
|
};
|
var moved = getPositionAfterMove({
|
point: startPoint,
|
pathList: pathList,
|
pathIndex: 0,
|
deltaDistance: deltaDistance
|
});
|
item.x = moved.x;
|
item.y = moved.y;
|
item.path = moved.path;
|
item.width = item.deviceLength;
|
item.height = item.deviceWidth;
|
});
|
return deviceForm;
|
}
|
|
// 线段轨道
|
deviceForm.deviceList.forEach(function (item) {
|
var inset = (shortLength - item.deviceLength) / 2;
|
var distance = (item.progress * (longLength - 2 * shortLength)) / 100;
|
if (isHorizontal) {
|
item.x = rect.x + distance;
|
item.y = rect.y + inset;
|
item.width = item.deviceLength;
|
item.height = item.deviceWidth;
|
} else {
|
item.x = rect.x + inset;
|
item.y = rect.y + distance;
|
item.width = item.deviceLength;
|
item.height = item.deviceWidth;
|
}
|
});
|
return deviceForm;
|
}
|
|
function getLShapePointList(sprite) {
|
var turningPoint;
|
var LPointList;
|
if (!sprite.turningPoint) {
|
var rate = 1 / 3;
|
var minDistance = Math.min(sprite.width, sprite.height);
|
var shortDistance = rate * minDistance;
|
var longDistance = sprite.height - shortDistance;
|
|
if (sprite.shape === 'L1') {
|
turningPoint = {
|
x: sprite.x + shortDistance,
|
y: sprite.y + longDistance
|
};
|
} else if (sprite.shape === 'L2') {
|
turningPoint = {
|
x: sprite.x + sprite.width - shortDistance,
|
y: sprite.y + sprite.height - longDistance
|
};
|
} else if (sprite.shape === 'L3') {
|
turningPoint = {
|
x: sprite.x + shortDistance,
|
y: sprite.y + sprite.height - longDistance
|
};
|
} else if (sprite.shape === 'L4') {
|
turningPoint = {
|
x: sprite.x + sprite.width - shortDistance,
|
y: sprite.y + longDistance
|
};
|
}
|
} else {
|
turningPoint = sprite.turningPoint;
|
}
|
sprite.turningPoint = turningPoint;
|
|
if (sprite.shape === 'L1') {
|
LPointList = [
|
{ x: sprite.x, y: sprite.y, direction: 'up' },
|
{ x: turningPoint.x, y: sprite.y, direction: 'right' },
|
Object.assign({}, turningPoint, { direction: 'down' }),
|
{ x: sprite.x + sprite.width, y: turningPoint.y, direction: 'right' },
|
{
|
x: sprite.x + sprite.width,
|
y: sprite.y + sprite.height,
|
direction: 'down'
|
},
|
{ x: sprite.x, y: sprite.y + sprite.height, direction: 'left' }
|
];
|
} else if (sprite.shape === 'L2') {
|
LPointList = [
|
{
|
x: sprite.x + sprite.width,
|
y: sprite.y + sprite.height,
|
direction: 'bottom'
|
},
|
{ x: turningPoint.x, y: sprite.y + sprite.height, direction: 'left' },
|
Object.assign({}, turningPoint, { direction: 'up' }),
|
{ x: sprite.x, y: turningPoint.y, direction: 'left' },
|
{ x: sprite.x, y: sprite.y, direction: 'up' },
|
{ x: sprite.x + sprite.width, y: sprite.y, direction: 'right' }
|
];
|
} else if (sprite.shape === 'L3') {
|
LPointList = [
|
{ x: sprite.x, y: sprite.y + sprite.height, direction: 'bottom' },
|
{ x: turningPoint.x, y: sprite.y + sprite.height, direction: 'right' },
|
Object.assign({}, turningPoint, { direction: 'up' }),
|
{ x: sprite.x + sprite.width, y: turningPoint.y, direction: 'right' },
|
{ x: sprite.x + sprite.width, y: sprite.y, direction: 'up' },
|
{ x: sprite.x, y: sprite.y, direction: 'left' }
|
];
|
} else {
|
LPointList = [
|
{ x: sprite.x + sprite.width, y: sprite.y, direction: 'up' },
|
{ x: turningPoint.x, y: sprite.y, direction: 'left' },
|
Object.assign({}, turningPoint, { direction: 'down' }),
|
{ x: sprite.x, y: turningPoint.y, direction: 'left' },
|
{ x: sprite.x, y: sprite.y + sprite.height, direction: 'down' },
|
{
|
x: sprite.x + sprite.width,
|
y: sprite.y + sprite.height,
|
direction: 'right'
|
}
|
];
|
}
|
|
return LPointList;
|
}
|
|
/**
|
* 有向边 (ax,ay)→(bx,by) 沿环前进方向的单位内向法向量(逆时针环时内侧在前进方向左侧)。
|
* @param {number} ax
|
* @param {number} ay
|
* @param {number} bx
|
* @param {number} by
|
* @param {boolean} ccwPolygon 尖点环有向面积 >0 时为逆时针
|
* @returns {{ x: number, y: number }}
|
*/
|
function inwardNormalUnitForward(ax, ay, bx, by, ccwPolygon) {
|
var dx = bx - ax;
|
var dy = by - ay;
|
var len = Math.sqrt(dx * dx + dy * dy) || 1;
|
var ux = dx / len;
|
var uy = dy / len;
|
if (ccwPolygon) {
|
return { x: -uy, y: ux };
|
}
|
return { x: uy, y: -ux };
|
}
|
|
/**
|
* 两直线 p1+t*u1 与 p2+s*u2 的交点(u 已为单位向量则 t 为几何长度)。
|
* @param {{ x: number, y: number }} p1
|
* @param {{ x: number, y: number }} u1
|
* @param {{ x: number, y: number }} p2
|
* @param {{ x: number, y: number }} u2
|
* @returns {{ x: number, y: number }}
|
*/
|
function intersectLines(p1, u1, p2, u2) {
|
var cr = u1.x * u2.y - u1.y * u2.x;
|
if (Math.abs(cr) < 1e-9) {
|
return { x: p1.x, y: p1.y };
|
}
|
var dx = p2.x - p1.x;
|
var dy = p2.y - p1.y;
|
var t = (dx * u2.y - dy * u2.x) / cr;
|
return { x: p1.x + t * u1.x, y: p1.y + t * u1.y };
|
}
|
|
/**
|
* 直角顶点环向内偏移。
|
* 凹角:沿环前进向量 vIn=curr−prev、vOut=next−curr 的叉积与环向一致时判定(CCW 环上凸角 cross>0、凹角 cross<0),
|
* 新顶点为 curr+inset*(nIn+nOut);凸角为两邻边平移后的直线交点。
|
* (smoothRightAnglePath 用的是 curr→prev 与 curr→next,与「沿环前进」差一符号,不能复用其叉积判凹凸。)
|
* @param {{ x: number, y: number, direction?: string }[]} sharpCorners
|
* @param {number} inset
|
* @returns {{ x: number, y: number, direction?: string }[]}
|
*/
|
function offsetOrthogonalSharpRing(sharpCorners, inset) {
|
var area2 = 0;
|
var nj;
|
for (nj = 0; nj < sharpCorners.length; nj++) {
|
var pj0 = sharpCorners[nj];
|
var pj1 = sharpCorners[(nj + 1) % sharpCorners.length];
|
area2 += pj0.x * pj1.y - pj1.x * pj0.y;
|
}
|
var ccwPolygon = area2 > 0;
|
var n = sharpCorners.length;
|
var out = [];
|
var i;
|
for (i = 0; i < n; i++) {
|
var prev = sharpCorners[(i - 1 + n) % n];
|
var curr = sharpCorners[i];
|
var next = sharpCorners[(i + 1) % n];
|
var vInx = curr.x - prev.x;
|
var vIny = curr.y - prev.y;
|
var vOutx = next.x - curr.x;
|
var vOuty = next.y - curr.y;
|
var crossWalk = vInx * vOuty - vIny * vOutx;
|
var reflex = ccwPolygon ? crossWalk < 0 : crossWalk > 0;
|
var nIn = inwardNormalUnitForward(prev.x, prev.y, curr.x, curr.y, ccwPolygon);
|
var nOut = inwardNormalUnitForward(curr.x, curr.y, next.x, next.y, ccwPolygon);
|
var ix;
|
var iy;
|
if (reflex) {
|
ix = curr.x + (nIn.x + nOut.x) * inset;
|
iy = curr.y + (nIn.y + nOut.y) * inset;
|
} else {
|
var lenIn = Math.sqrt(vInx * vInx + vIny * vIny) || 1;
|
var lenOut = Math.sqrt(vOutx * vOutx + vOuty * vOuty) || 1;
|
var uIn = { x: vInx / lenIn, y: vIny / lenIn };
|
var uOut = { x: vOutx / lenOut, y: vOuty / lenOut };
|
var oIn = { x: prev.x + nIn.x * inset, y: prev.y + nIn.y * inset };
|
var oOut = { x: curr.x + nOut.x * inset, y: curr.y + nOut.y * inset };
|
var inter = intersectLines(oIn, uIn, oOut, uOut);
|
ix = inter.x;
|
iy = inter.y;
|
}
|
out.push({ x: ix, y: iy, direction: sharpCorners[i].direction });
|
}
|
return out;
|
}
|
|
// 暂时注释掉。以后再考虑加上。先不删除。
|
/**
|
* 解析环穿单侧内缩像素距离;若传入 `precomputedSharp` 则不再重复求尖点环。
|
* @param {{ width: number, height: number, shape?: string, annulusBandInset?: number, value?: string|object }} sprite
|
* @param {{ x: number, y: number }[]} [precomputedSharp]
|
* @returns {number}
|
*/
|
// function resolveAnnulusBandInset(sprite, precomputedSharp) {
|
// var override = sprite && sprite.annulusBandInset;
|
// if (typeof override === 'number' && isFinite(override) && override >= 0) {
|
// return override;
|
// }
|
// var tmp = { type: 'annulus', width: sprite.width, height: sprite.height };
|
// var box = getAutoTrackDeviceBox(tmp);
|
// var shortLen = Math.min(Number(sprite.width) || 0, Number(sprite.height) || 0);
|
// var fallbackAlong = box ? box.along : Math.max(2, Math.round(shortLen * 0.3));
|
// var fallbackAcross = box ? box.across : Math.max(2, Math.round(shortLen * 0.15));
|
// var form = safeParseJson(sprite && sprite.value);
|
// var across = fallbackAcross;
|
// console.log('fallbackAcross', across);
|
// if (form && form.deviceList && form.deviceList.length) {
|
// var di;
|
// for (di = 0; di < form.deviceList.length; di++) {
|
// var sz = normalizeDeviceSizeOverride(form.deviceList[di], fallbackAlong, fallbackAcross);
|
// if (sz.across > across) {
|
// across = sz.across;
|
// }
|
// }
|
// }
|
// var inset = Math.max(
|
// ANNULUS_INSET_MIN_PIXELS,
|
// Math.round(across * ANNULUS_INSET_FROM_DEVICE_RATIO)
|
// );
|
// var maxByBox = Math.floor(Math.min(sprite.width, sprite.height) / 2) - 3;
|
// var ring = precomputedSharp || getSharpCornerList(sprite, sprite.shape || 'rect');
|
// var minEdge = Infinity;
|
// var ri;
|
// for (ri = 0; ri < ring.length; ri++) {
|
// var d = calcDistance(ring[ri], ring[(ri + 1) % ring.length]);
|
// if (d > 0 && d < minEdge) {
|
// minEdge = d;
|
// }
|
// }
|
// var maxByEdge = minEdge === Infinity ? inset : Math.floor(minEdge / 2) - 1;
|
// var cap = Math.min(isFinite(maxByBox) ? maxByBox : inset, isFinite(maxByEdge) ? maxByEdge : inset);
|
// if (cap > 0 && inset > cap) {
|
// inset = cap;
|
// }
|
// return Math.max(0, inset);
|
// }
|
|
/**
|
* 环穿共用:尖点环、inset(只算一遍尖点)。
|
* @param {object} sprite x/y/width/height/shape/turningPoint/annulusBandInset
|
* @param {string} defaultShape
|
* @returns {{ sharp: object[], inset: number }}
|
*/
|
function annulusBandContext(sprite, defaultShape) {
|
sprite.shape = sprite.shape || defaultShape;
|
var sharp = getSharpCornerList(sprite, defaultShape);
|
return {
|
sharp: sharp,
|
inset: 6
|
};
|
}
|
|
/**
|
* 由元素外接框与形状得到未圆角前的顶点环(rect 或 L1–L4)。
|
* @param {{ x: number, y: number, width: number, height: number, shape?: string, turningPoint?: object }} sprite
|
* @param {string} defaultShape
|
* @returns {{ x: number, y: number, direction?: string }[]}
|
*/
|
function getSharpCornerList(sprite, defaultShape) {
|
var rectPointList = [
|
{ x: sprite.x, y: sprite.y, direction: 'right' },
|
{ x: sprite.x + sprite.width, y: sprite.y, direction: 'down' },
|
{
|
x: sprite.x + sprite.width,
|
y: sprite.y + sprite.height,
|
direction: 'left'
|
},
|
{ x: sprite.x, y: sprite.y + sprite.height, direction: 'up' }
|
];
|
var shape = sprite.shape || defaultShape;
|
return shape === 'rect' ? rectPointList : getLShapePointList(sprite);
|
}
|
|
function getIsStillHalf(target) {
|
var curList = getLShapePointList(target);
|
var count = 0;
|
for (var i = 0; i < curList.length; i++) {
|
var p0 = curList[i];
|
var p1 = curList[(i + 1) % curList.length];
|
var p2 = curList[(i + 2) % curList.length];
|
var p3 = curList[(i + 3) % curList.length];
|
|
var d1 = calcDistance(p0, p1);
|
var d2 = calcDistance(p1, p2);
|
var d3 = calcDistance(p2, p3);
|
if (target.halfList.includes((i + 1) % curList.length) && d2 <= d1 && d2 <= d3) {
|
count += 1;
|
}
|
}
|
return count === 2;
|
}
|
|
/**
|
* 计算点 (sx,sy) 和 (mx,my) 在直线段 path0 上的带符号投影距离差。
|
* 返回值 >0 表示 mx,my 在 sx,sy 前方(沿路径方向)。
|
* @param {{ type: string, startX: number, startY: number, x: number, y: number }} path0 直线段路径
|
* @param {number} sx 起点 x
|
* @param {number} sy 起点 y
|
* @param {number} mx 终点 x
|
* @param {number} my 终点 y
|
* @returns {number|null} 带符号距离差,非线段或零长度时返回 null
|
*/
|
function lineSignedRemainAlong(path0, sx, sy, mx, my) {
|
if (!path0 || path0.type !== 'line') {
|
return null;
|
}
|
var start = { x: path0.startX, y: path0.startY };
|
var end = { x: path0.x, y: path0.y };
|
var segLen = calcDistance(start, end);
|
if (!(segLen > 0)) {
|
return null;
|
}
|
var vector = normalizeVector(start, end);
|
var clampT = function (px, py) {
|
var t = ((px - start.x) * vector.x + (py - start.y) * vector.y) * segLen;
|
return Math.max(0, Math.min(segLen, t));
|
};
|
return clampT(mx, my) - clampT(sx, sy);
|
}
|
|
function setRadiusInPoint(pointList, sprite) {
|
sprite.halfList = [];
|
var pointLength = pointList.length;
|
var i;
|
for (i = 0; i < pointLength; i++) {
|
var currentPoint = pointList[i];
|
var next1 = pointList[(i + 1) % pointLength];
|
var next2 = pointList[(i + 2) % pointLength];
|
var next3 = pointList[(i + 3) % pointLength];
|
var v1 = normalizeVector(currentPoint, next1);
|
var v2 = normalizeVector(next1, next2);
|
var v3 = normalizeVector(next2, next3);
|
var finalV = { x: v1.x + v2.x + v3.x, y: v1.y + v2.y + v3.y };
|
var distance = finalV.x * finalV.x + finalV.y * finalV.y;
|
var d1 = calcDistance(currentPoint, next1);
|
var d2 = calcDistance(next1, next2);
|
var d3 = calcDistance(next2, next3);
|
if (distance === 1 && d2 <= d1 && d2 <= d3) {
|
var radius = d2 / 2;
|
next1.isHalf = true;
|
next2.isHalf = true;
|
next1.radius = radius;
|
next2.radius = radius;
|
sprite.halfList.push((i + 1) % pointLength);
|
}
|
}
|
for (i = 0; i < pointLength; i++) {
|
currentPoint = pointList[i];
|
if (currentPoint.isHalf) {
|
continue;
|
}
|
var prevPoint = pointList[(i - 1 + pointLength) % pointLength];
|
var nextPoint = pointList[(i + 1 + pointLength) % pointLength];
|
var rad = Math.min.apply(
|
Math,
|
[prevPoint.radius, nextPoint.radius].filter(function (item) {
|
return item;
|
})
|
);
|
currentPoint.radius = rad;
|
}
|
}
|
|
function smoothRightAnglePath(points) {
|
var smoothedPath = [];
|
var n = points.length;
|
var i;
|
for (i = 0; i < n; i++) {
|
var prev = points[(i - 1 + n) % n];
|
var curr = points[i];
|
var next = points[(i + 1) % n];
|
var radius = curr.radius;
|
var toPrev = normalizeVector(curr, prev);
|
var toNext = normalizeVector(curr, next);
|
var dotProduct = toPrev.x * toNext.x + toPrev.y * toNext.y;
|
var isRightAngle = Math.abs(dotProduct) < 0.1;
|
|
if (isRightAngle) {
|
var arcStart = {
|
x: curr.x + toPrev.x * radius,
|
y: curr.y + toPrev.y * radius
|
};
|
var arcEnd = {
|
x: curr.x + toNext.x * radius,
|
y: curr.y + toNext.y * radius
|
};
|
var arcCenter = {
|
x: curr.x + toPrev.x * radius + toNext.x * radius,
|
y: curr.y + toPrev.y * radius + toNext.y * radius
|
};
|
var startAngle = Math.atan2(arcStart.y - arcCenter.y, arcStart.x - arcCenter.x);
|
var endAngle = Math.atan2(arcEnd.y - arcCenter.y, arcEnd.x - arcCenter.x);
|
var crossProduct = toPrev.x * toNext.y - toPrev.y * toNext.x;
|
if (crossProduct > 0) {
|
if (endAngle > startAngle) {
|
endAngle -= Math.PI * 2;
|
}
|
} else {
|
if (endAngle < startAngle) {
|
endAngle += Math.PI * 2;
|
}
|
}
|
smoothedPath.push({
|
x: arcCenter.x,
|
y: arcCenter.y,
|
radius: radius,
|
startAngle: startAngle,
|
endAngle: endAngle,
|
type: 'arc',
|
arcStartX: arcStart.x,
|
arcStartY: arcStart.y,
|
arcEndX: arcEnd.x,
|
arcEndY: arcEnd.y,
|
crossProduct: crossProduct
|
});
|
}
|
}
|
var length = smoothedPath.length;
|
var newSmoothedPath = [];
|
for (i = 0; i < length; i++) {
|
var currArc = smoothedPath[i];
|
var nextArc = smoothedPath[(i + 1 + length) % length];
|
newSmoothedPath.push(currArc);
|
if (currArc.arcEndX !== nextArc.arcStartX || currArc.arcEndY !== nextArc.arcStartY) {
|
newSmoothedPath.push({
|
startX: currArc.arcEndX,
|
startY: currArc.arcEndY,
|
type: 'line',
|
x: nextArc.arcStartX,
|
y: nextArc.arcStartY
|
});
|
}
|
}
|
return newSmoothedPath;
|
}
|
|
/**
|
* 仅描边/填充轮廓:沿 pathList 走笔并 closePath(不 endFill,便于连续多圈描边)。
|
* @param {PIXI.Graphics} smoothedGraphics
|
* @param {object[]} path
|
*/
|
function traceSmoothedPath(smoothedGraphics, path) {
|
if (!path || !path.length) {
|
return;
|
}
|
var startPoint = path[0];
|
if (startPoint.type === 'arc') {
|
smoothedGraphics.moveTo(startPoint.arcStartX, startPoint.arcStartY);
|
} else {
|
smoothedGraphics.moveTo(startPoint.x, startPoint.y);
|
}
|
var i;
|
for (i = 0; i < path.length; i++) {
|
var point = path[i];
|
if (point.type === 'line') {
|
smoothedGraphics.lineTo(point.x, point.y);
|
} else if (point.type === 'arc') {
|
smoothedGraphics.arc(
|
point.x,
|
point.y,
|
point.radius,
|
point.startAngle,
|
point.endAngle,
|
point.endAngle < point.startAngle
|
);
|
}
|
}
|
smoothedGraphics.closePath();
|
}
|
|
/**
|
* 填充闭合外圈并 endFill(调用方需已 beginFill)。
|
* @param {PIXI.Graphics} smoothedGraphics
|
* @param {object[]} path
|
*/
|
function drawSmoothedPath(smoothedGraphics, path) {
|
traceSmoothedPath(smoothedGraphics, path);
|
smoothedGraphics.endFill();
|
}
|
|
/**
|
* PIXI beginHole 要求洞与外轮廓绕向相反:段逆序且圆弧起终点对调。
|
* @param {object[]} path smoothRightAnglePath 结果
|
* @returns {object[]}
|
*/
|
function reverseSmoothedSegmentsForHole(path) {
|
var n = path.length;
|
var out = [];
|
var i;
|
for (i = n - 1; i >= 0; i--) {
|
var seg = path[i];
|
if (seg.type === 'line') {
|
out.push({
|
type: 'line',
|
startX: seg.x,
|
startY: seg.y,
|
x: seg.startX,
|
y: seg.startY
|
});
|
} else if (seg.type === 'arc') {
|
out.push({
|
type: 'arc',
|
x: seg.x,
|
y: seg.y,
|
radius: seg.radius,
|
startAngle: seg.endAngle,
|
endAngle: seg.startAngle,
|
arcStartX: seg.arcEndX,
|
arcStartY: seg.arcEndY,
|
arcEndX: seg.arcStartX,
|
arcEndY: seg.arcStartY,
|
crossProduct: seg.crossProduct
|
});
|
}
|
}
|
return out;
|
}
|
|
/**
|
* 外填充 + 内洞(双轨带),调用方需已 beginFill。
|
* @param {PIXI.Graphics} smoothedGraphics
|
* @param {object[]} outerPath
|
* @param {object[]} innerPath
|
*/
|
function drawSmoothedPathWithHole(smoothedGraphics, outerPath, innerPath) {
|
traceSmoothedPath(smoothedGraphics, outerPath);
|
smoothedGraphics.beginHole();
|
traceSmoothedPath(smoothedGraphics, reverseSmoothedSegmentsForHole(innerPath));
|
smoothedGraphics.endHole();
|
smoothedGraphics.endFill();
|
}
|
|
/**
|
* 外圈、中轨、内圈:外/内仅用于绘制;中轨为 pathList 与运动学所用。
|
* @param {{ x: number, y: number, width: number, height: number, shape?: string, turningPoint?: object, annulusBandInset?: number }} sprite
|
* @param {string} defaultShape
|
* @returns {{ outerPath: object[], innerPath: object[]|null, centerPath: object[], inset: number }}
|
*/
|
function buildAnnulusPaths(sprite, defaultShape) {
|
var ctx = annulusBandContext(sprite, defaultShape);
|
var sharp = ctx.sharp;
|
var sharpClone = sharp.map(function (p) {
|
return { x: p.x, y: p.y, direction: p.direction };
|
});
|
setRadiusInPoint(sharp, sprite);
|
var outerPath = smoothRightAnglePath(sharp);
|
var inset = ctx.inset;
|
var minSide = Math.min(sprite.width, sprite.height);
|
if (inset <= 0 || minSide <= inset * 2 + 4) {
|
return { outerPath: outerPath, innerPath: null, centerPath: outerPath, inset: inset };
|
}
|
var centerSharp = offsetOrthogonalSharpRing(
|
sharpClone.map(function (p) {
|
return { x: p.x, y: p.y, direction: p.direction };
|
}),
|
inset / 2
|
);
|
var centerMeta = {};
|
setRadiusInPoint(centerSharp, centerMeta);
|
var centerPath = smoothRightAnglePath(centerSharp);
|
var innerSharp = offsetOrthogonalSharpRing(sharpClone, inset);
|
var innerMeta = {};
|
setRadiusInPoint(innerSharp, innerMeta);
|
var innerPath = smoothRightAnglePath(innerSharp);
|
return { outerPath: outerPath, innerPath: innerPath, centerPath: centerPath, inset: inset };
|
}
|
|
/**
|
* 将坐标压回环穿 pathList(中轨)当前段上。
|
* @param {number} x
|
* @param {number} y
|
* @param {{ type: string, startX?: number, startY?: number, x?: number, y?: number, radius?: number }|null|undefined} path
|
* @returns {{ x: number, y: number }}
|
*/
|
function snapToAnnulusPath(trackInfo, x, y, path) {
|
if (!path || trackInfo.type !== 'annulus') {
|
return { x: x, y: y };
|
}
|
if (path.type === 'line') {
|
var sx = path.startX;
|
var sy = path.startY;
|
var ex = path.x;
|
var ey = path.y;
|
var dx = ex - sx;
|
var dy = ey - sy;
|
var len2 = dx * dx + dy * dy;
|
if (len2 < 1e-12) {
|
return { x: sx, y: sy };
|
}
|
var t = ((x - sx) * dx + (y - sy) * dy) / len2;
|
t = Math.max(0, Math.min(1, t));
|
return { x: sx + t * dx, y: sy + t * dy };
|
}
|
if (path.type === 'arc') {
|
var ox = path.x;
|
var oy = path.y;
|
var r = path.radius;
|
if (!isFinite(r) || r <= 0) {
|
return { x: x, y: y };
|
}
|
var ang = getNormalizeAngle(Math.atan2(y - oy, x - ox), path.startAngle, path.endAngle);
|
return { x: ox + r * Math.cos(ang), y: oy + r * Math.sin(ang) };
|
}
|
return { x: x, y: y };
|
}
|
|
/**
|
* 监控轨道层:按几何算外圈、内圈并描边(pathList 为中轨,不用于此描边)。
|
* @param {PIXI.Graphics} smoothedGraphics
|
* @param {{ pathList: object[], x: number, y: number, width: number, height: number, shape?: string, turningPoint?: object, annulusBandInset?: number }} item
|
* @param {string} defaultShape
|
*/
|
function strokeAnnulusDualOutline(smoothedGraphics, item, defaultShape) {
|
var tmp = {
|
x: item.x,
|
y: item.y,
|
width: item.width,
|
height: item.height,
|
shape: item.shape,
|
turningPoint: item.turningPoint,
|
annulusBandInset: item.annulusBandInset
|
};
|
var pair = buildAnnulusPaths(tmp, item.shape || defaultShape);
|
traceSmoothedPath(smoothedGraphics, pair.outerPath);
|
if (pair.innerPath && pair.innerPath.length) {
|
traceSmoothedPath(smoothedGraphics, pair.innerPath);
|
}
|
}
|
|
/**
|
* 编辑器环穿:写回 sprite.pathList(中轨),绘制外填+内洞。
|
* @param {PIXI.Graphics} smoothedGraphics
|
* @param {object} sprite 元素或预览 rect
|
* @param {string} defaultShape 如 annulusShape
|
*/
|
function startDrawSmoothedPath(smoothedGraphics, sprite, defaultShape) {
|
var pair = buildAnnulusPaths(sprite, defaultShape);
|
sprite.pathList = pair.centerPath;
|
if (!pair.innerPath || !pair.innerPath.length) {
|
drawSmoothedPath(smoothedGraphics, pair.outerPath);
|
return;
|
}
|
drawSmoothedPathWithHole(smoothedGraphics, pair.outerPath, pair.innerPath);
|
traceSmoothedPath(smoothedGraphics, pair.innerPath);
|
}
|
|
function getRotate(point, path) {
|
if (path.type === 'line') {
|
var tdx = path.x - path.startX;
|
var tdy = path.y - path.startY;
|
return Math.atan2(tdy, tdx);
|
}
|
var vector = normalizeVector(point, path);
|
var angleToCenter = Math.atan2(vector.y, vector.x);
|
return angleToCenter + (Math.PI / 2) * path.crossProduct;
|
}
|
|
/**
|
* 堆垛机 / 双工位设备图标(与 MapCanvas#createCrnTexture / createCrnTextureColoredDevice 一致)
|
* @param {PIXI.Graphics} g
|
* @param {number} deviceWidth 图标总宽(像素,与 getAutoTrackDeviceBox.along 一致)
|
* @param {number} deviceHeight 图标总高(与 getAutoTrackDeviceBox.across 一致)
|
* @param {number} bodyColor 驾驶舱填充色
|
*/
|
function drawCrnDeviceGraphics(g, deviceWidth, deviceHeight, bodyColor) {
|
var yTop = Math.round(deviceHeight * 0.1);
|
g.beginFill(0x999999);
|
g.drawRect(2, yTop, 3, deviceHeight - yTop - 2);
|
g.drawRect(deviceWidth - 5, yTop, 3, deviceHeight - yTop - 2);
|
g.endFill();
|
g.beginFill(0x999999);
|
g.drawRect(0, yTop, deviceWidth, 3);
|
g.endFill();
|
var cabW = Math.round(deviceWidth * 0.68);
|
var cabH = Math.round(deviceHeight * 0.38);
|
var cabX = Math.round((deviceWidth - cabW) / 2);
|
var cabY = Math.round(deviceHeight * 0.52 - cabH / 2);
|
g.beginFill(bodyColor);
|
g.drawRect(cabX, cabY, cabW, cabH);
|
g.endFill();
|
var winW = Math.round(cabW * 0.6);
|
var winH = Math.round(cabH * 0.45);
|
var winX = cabX + Math.round((cabW - winW) / 2);
|
var winY = cabY + Math.round((cabH - winH) / 2);
|
g.beginFill(0xd0e8ff);
|
g.drawRect(winX, winY, winW, winH);
|
g.endFill();
|
var forkW = Math.round(deviceWidth * 0.8);
|
var forkH = Math.max(2, Math.round(deviceHeight * 0.08));
|
var forkX = Math.round((deviceWidth - forkW) / 2);
|
var forkY = cabY + cabH;
|
g.beginFill(0x666666);
|
g.drawRect(forkX, forkY, forkW, forkH);
|
g.endFill();
|
}
|
|
/**
|
* RGV 设备图标(与 MapCanvas#createRgvTextureColoredDevice 一致)
|
* @param {PIXI.Graphics} g
|
* @param {number} width
|
* @param {number} height
|
* @param {number} bodyColor 车体填充色
|
*/
|
function drawRgvDeviceGraphics(g, width, height, bodyColor) {
|
var bodyW = Math.round(width * 0.8);
|
var bodyH = Math.round(height * 0.55);
|
var bodyX = Math.round((width - bodyW) / 2);
|
var bodyY = Math.round((height - bodyH) / 2);
|
g.beginFill(bodyColor);
|
g.drawRect(bodyX, bodyY, bodyW, bodyH);
|
g.endFill();
|
var winW = Math.round(bodyW * 0.55);
|
var winH = Math.round(bodyH * 0.45);
|
var winX = bodyX + Math.round((bodyW - winW) / 2);
|
var winY = bodyY + Math.round((bodyH - winH) / 2);
|
g.beginFill(0xd0e8ff);
|
g.drawRect(winX, winY, winW, winH);
|
g.endFill();
|
var wheelW = Math.max(2, Math.round(width * 0.12));
|
var wheelH = Math.max(2, Math.round(height * 0.1));
|
var wheelY = bodyY + bodyH;
|
var wheelGap = Math.round((width - wheelW * 2) / 3);
|
var wheelX1 = wheelGap;
|
var wheelX2 = width - wheelGap - wheelW;
|
g.beginFill(0x333333);
|
g.drawRect(wheelX1, wheelY - Math.round(wheelH / 2), wheelW, wheelH);
|
g.drawRect(wheelX2, wheelY - Math.round(wheelH / 2), wheelW, wheelH);
|
g.endFill();
|
}
|
|
global.BasMapTrackGeometry = {
|
TYPE_META: TYPE_META,
|
normalizeVector: normalizeVector,
|
calcDistance: calcDistance,
|
safeParseJson: safeParseJson,
|
getNormalizeAngle: getNormalizeAngle,
|
getPositionAfterMove: getPositionAfterMove,
|
getAllDistance: getAllDistance,
|
getDeviceInfo: getDeviceInfo,
|
getAutoTrackDeviceBox: getAutoTrackDeviceBox,
|
getLShapePointList: getLShapePointList,
|
getIsStillHalf: getIsStillHalf,
|
setRadiusInPoint: setRadiusInPoint,
|
smoothRightAnglePath: smoothRightAnglePath,
|
drawSmoothedPath: drawSmoothedPath,
|
traceSmoothedPath: traceSmoothedPath,
|
strokeAnnulusDualOutline: strokeAnnulusDualOutline,
|
snapToAnnulusPath: snapToAnnulusPath,
|
startDrawSmoothedPath: startDrawSmoothedPath,
|
getRotate: getRotate,
|
drawCrnDeviceGraphics: drawCrnDeviceGraphics,
|
drawRgvDeviceGraphics: drawRgvDeviceGraphics,
|
lineSignedRemainAlong: lineSignedRemainAlong
|
};
|
})(typeof window !== 'undefined' ? window : this);
|