/** * 环穿 / 平滑轨道几何:直角多边形转圆弧路径、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; } 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 }; })(typeof window !== 'undefined' ? window : this);