feat: 优化库位栅格绘制逻辑,支持自适应布局和透明背景,提升视觉效果

This commit is contained in:
xudan 2025-09-04 17:27:45 +08:00
parent e44b53f5c0
commit 1e86108745

View File

@ -19,7 +19,7 @@ function roundedRectPath(ctx: CanvasRenderingContext2D, x: number, y: number, w:
}
/**
* 2x3 associatedStorageLocations
* associatedStorageLocations
* Meta2D
*/
export function drawStorageGrid(
@ -36,54 +36,115 @@ export function drawStorageGrid(
const fontFamily = opts?.fontFamily ?? pen.calculative?.fontFamily;
// 尺寸与间距(世界坐标)
// 尺寸与间距(世界坐标)- 放大库位方框
const base = Math.min(w, h);
const cell = Math.max(12, Math.min(24, base * 0.60));
const gap = Math.max(2, Math.min(6, base * 0.08));
const gridCols = 3;
const gridRows = 2;
const gridW = gridCols * cell + (gridCols - 1) * gap;
const gridH = gridRows * cell + (gridRows - 1) * gap;
// 单个库位时使用更大的方框,多个库位时使用正常大小
const cell = Math.max(16, Math.min(32, base * 0.70)); // 基础库位方框大小
const gap = Math.max(1, Math.min(2, base * 0.03)); // 进一步减少间距
// 自适应网格布局:根据库位数量动态调整行列数,确保正方形背景
const totalSlots = assoc.length;
let gridCols: number;
let gridRows: number;
let isSingleSlot = false;
if (totalSlots === 1) {
gridCols = 1;
gridRows = 1; // 单个库位使用1x1布局突出显示
isSingleSlot = true;
} else if (totalSlots <= 2) {
gridCols = 2;
gridRows = 2; // 2个库位时使用2x2正方形
} else if (totalSlots <= 4) {
gridCols = 2;
gridRows = 2; // 3-4个库位时使用2x2正方形
} else if (totalSlots <= 9) {
gridCols = 3;
gridRows = 3; // 5-9个库位时使用3x3正方形
} else if (totalSlots <= 16) {
gridCols = 4;
gridRows = 4; // 10-16个库位时使用4x4正方形
} else {
gridCols = 5;
gridRows = 5; // 17+个库位时使用5x5正方形
}
// 根据库位数量调整方框大小和间距
let finalCell = cell;
let finalGap = gap;
if (isSingleSlot) {
// 单个库位时方框更大
finalCell = Math.max(20, Math.min(40, base * 0.85));
} else {
// 多个库位时边距更大,确保库位之间有明显间距
finalGap = Math.max(6, Math.min(12, base * 0.12));
}
const gridW = gridCols * finalCell + (gridCols - 1) * finalGap;
const gridH = gridRows * finalCell + (gridRows - 1) * finalGap;
// 确保背景是正方形,取较大的尺寸
// 单个库位时使用更小的正方形来突出显示
const squareSize = isSingleSlot ? Math.max(gridW, gridH) * 0.8 : Math.max(gridW, gridH);
const gridW_final = squareSize;
const gridH_final = squareSize;
// 右上角位置
const outer = gap;
// 右上角位置 - 进一步减少外边距
const outer = Math.max(0.5, gap * 0.3);
const gx = x + w + outer;
const gy = y - gridH - outer;
const gy = y - gridH_final - outer;
ctx.save();
// 背景底
// 背景底 - 透明背景,使用正方形尺寸
ctx.beginPath();
roundedRectPath(ctx, gx - gap * 0.5, gy - gap * 0.5, gridW + gap, gridH + gap, Math.min(6, cell * 0.4));
ctx.fillStyle = '#00000022';
roundedRectPath(
ctx,
gx - finalGap * 0.5,
gy - finalGap * 0.5,
gridW_final + finalGap,
gridH_final + finalGap,
Math.min(6, finalCell * 0.4)
);
ctx.fillStyle = 'transparent'; // 透明背景
ctx.fill();
ctx.strokeStyle = 'transparent'; // 透明边框
ctx.stroke();
// 前 5 个格子 + 最后一格可能显示 +N
// 计算库位在正方形背景中的居中位置
const actualGridW = gridCols * finalCell + (gridCols - 1) * finalGap;
const actualGridH = gridRows * finalCell + (gridRows - 1) * finalGap;
const offsetX = (gridW_final - actualGridW) / 2;
const offsetY = (gridH_final - actualGridH) / 2;
// 显示所有库位格子,最后一格可能显示 +N
const slots = assoc.slice(0, gridCols * gridRows);
slots.forEach((layerName, i) => {
const r = Math.floor(i / gridCols);
const c = i % gridCols;
const cx = gx + c * (cell + gap);
const cy = gy + r * (cell + gap);
// 库位位置应该是左上角坐标,不是中心点
const cx = gx + offsetX + c * (finalCell + finalGap);
const cy = gy + offsetY + r * (finalCell + finalGap);
ctx.beginPath();
roundedRectPath(ctx, cx, cy, cell, cell, Math.min(4, cell * 0.3));
roundedRectPath(ctx, cx, cy, finalCell, finalCell, Math.min(4, finalCell * 0.3));
// 解析状态
const state = opts?.stateResolver?.(pen.id!, layerName) ?? {};
const occupied = !!state.occupied;
const locked = !!state.locked;
// 填充颜色:占用为红色,否则默认灰底
ctx.fillStyle = occupied ? '#ff4d4f' : '#f5f5f5';
// 填充颜色:占用为红色,否则透明
ctx.fillStyle = occupied ? '#ff4d4f' : 'transparent';
ctx.fill();
ctx.strokeStyle = '#999999';
ctx.strokeStyle = 'transparent';
ctx.stroke();
// 若锁定,绘制锁图标角标(尽量不阻塞:未加载完成则跳过)
if (locked && lockedIcon.complete && lockedIcon.naturalWidth > 0) {
const pad = Math.max(1, Math.floor(cell * 0.08));
const pad = Math.max(1, Math.floor(finalCell * 0.08));
// 图标尺寸不超过 cell 内可用空间,且有最小可见阈值
const maxIcon = Math.max(0, cell - 2 * pad);
const iconSize = Math.min(14, Math.floor(cell * 0.7), maxIcon);
const maxIcon = Math.max(0, finalCell - 2 * pad);
const iconSize = Math.min(14, Math.floor(finalCell * 0.7), maxIcon);
// 将绘制范围裁剪到当前格子,确保不会溢出
ctx.save();
@ -101,20 +162,21 @@ export function drawStorageGrid(
const i = gridCols * gridRows - 1;
const r = Math.floor(i / gridCols);
const c = i % gridCols;
const cx = gx + c * (cell + gap);
const cy = gy + r * (cell + gap);
// 更多按钮位置也应该是左上角坐标
const cx = gx + offsetX + c * (finalCell + finalGap);
const cy = gy + offsetY + r * (finalCell + finalGap);
ctx.beginPath();
roundedRectPath(ctx, cx, cy, cell, cell, Math.min(4, cell * 0.3));
roundedRectPath(ctx, cx, cy, finalCell, finalCell, Math.min(4, finalCell * 0.3));
ctx.fillStyle = '#e6f4ff';
ctx.fill();
ctx.strokeStyle = '#1677ff';
ctx.stroke();
ctx.fillStyle = '#1677ff';
ctx.font = `${Math.floor(cell * 0.6)}px/${1} ${fontFamily ?? 'sans-serif'}`;
ctx.font = `${Math.floor(finalCell * 0.6)}px/${1} ${fontFamily ?? 'sans-serif'}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('+' + overflow, cx + cell / 2, cy + cell / 2);
ctx.fillText('+' + overflow, cx + finalCell / 2, cy + finalCell / 2);
}
ctx.restore();
@ -247,10 +309,10 @@ export function drawStorageBackground(ctx: CanvasRenderingContext2D, pen: MapPen
ctx.arcTo(x, y, x + r, y, r);
ctx.closePath();
// 填充半透明背景
ctx.fillStyle = '#00000022';
// 填充半透明背景 - 调整为更偏白色
ctx.fillStyle = 'transparent';
ctx.fill();
ctx.strokeStyle = '#999999';
ctx.strokeStyle = 'transparent';
ctx.stroke();
ctx.restore();
@ -272,48 +334,100 @@ export function createStorageLocationPens(
): MapPen[] {
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pointRect;
// 库位栅格参数
// 库位栅格参数 - 与drawStorageGrid保持一致
const base = Math.min(w, h);
const cell = Math.max(12, Math.min(24, base * 0.60));
const gap = Math.max(2, Math.min(6, base * 0.08));
const gridCols = 3;
const gridRows = 2;
const gridH = gridRows * cell + (gridRows - 1) * gap;
const cell = Math.max(16, Math.min(32, base * 0.70)); // 放大库位方框
const gap = Math.max(1, Math.min(2, base * 0.03)); // 进一步减少间距
// 自适应网格布局:根据库位数量动态调整行列数,确保正方形背景
const totalSlots = storageLocations.length;
let gridCols: number;
let gridRows: number;
let isSingleSlot = false;
if (totalSlots === 1) {
gridCols = 1;
gridRows = 1; // 单个库位使用1x1布局突出显示
isSingleSlot = true;
} else if (totalSlots <= 2) {
gridCols = 2;
gridRows = 2; // 2个库位时使用2x2正方形
} else if (totalSlots <= 4) {
gridCols = 2;
gridRows = 2; // 3-4个库位时使用2x2正方形
} else if (totalSlots <= 9) {
gridCols = 3;
gridRows = 3; // 5-9个库位时使用3x3正方形
} else if (totalSlots <= 16) {
gridCols = 4;
gridRows = 4; // 10-16个库位时使用4x4正方形
} else {
gridCols = 5;
gridRows = 5; // 17+个库位时使用5x5正方形
}
// 根据库位数量调整方框大小和间距
let finalCell = cell;
let finalGap = gap;
if (isSingleSlot) {
// 单个库位时方框更大
finalCell = Math.max(20, Math.min(40, base * 0.85));
} else {
// 多个库位时边距更大,确保库位之间有明显间距
finalGap = Math.max(6, Math.min(12, base * 0.12));
}
const gridW = gridCols * finalCell + (gridCols - 1) * finalGap;
const gridH = gridRows * finalCell + (gridRows - 1) * finalGap;
// 确保背景是正方形,取较大的尺寸
// 单个库位时使用更小的正方形来突出显示
const squareSize = isSingleSlot ? Math.max(gridW, gridH) * 0.8 : Math.max(gridW, gridH);
const gridW_final = squareSize;
const gridH_final = squareSize;
// 右上角位置 - getPointRect返回的是中心点坐标需要转换为左上角坐标
const outer = gap;
// 右上角位置 - getPointRect返回的是中心点坐标需要转换为左上角坐标,进一步减少外边距
const outer = Math.max(0.5, gap * 0.3);
const gx = x + w / 2 + outer;
const gy = y - h / 2 - gridH - outer;
const gy = y - h / 2 - gridH_final - outer;
const pens: MapPen[] = [];
// 创建库位区域背景矩形
// 创建库位区域背景矩形 - 使用正方形尺寸,透明背景
const backgroundPen: MapPen = {
id: `storage-bg-${pointId}`,
name: 'storage-background',
tags: ['storage-background', `point-${pointId}`],
x: gx - gap * 0.5,
y: gy - gap * 0.5,
width: gridCols * cell + (gridCols - 1) * gap + gap,
height: gridRows * cell + (gridRows - 1) * gap + gap,
x: gx - finalGap * 0.5,
y: gy - finalGap * 0.5,
width: gridW_final + finalGap,
height: gridH_final + finalGap,
lineWidth: 1,
background: '#00000022',
color: '#999999',
background: 'transparent', // 透明背景
color: 'transparent', // 透明边框
locked: LockState.DisableEdit,
visible: true,
borderRadius: Math.min(6, cell * 0.4),
borderRadius: Math.min(6, finalCell * 0.4),
};
pens.push(backgroundPen);
// 计算库位在正方形背景中的居中位置
const actualGridW = gridCols * finalCell + (gridCols - 1) * finalGap;
const actualGridH = gridRows * finalCell + (gridRows - 1) * finalGap;
const offsetX = (gridW_final - actualGridW) / 2;
const offsetY = (gridH_final - actualGridH) / 2;
// 创建库位pen对象
storageLocations.slice(0, gridCols * gridRows).forEach((locationName, i) => {
const r = Math.floor(i / gridCols);
const c = i % gridCols;
// 计算库位在背景矩形内的相对位置,然后加上背景矩形的起始位置
const relativeX = c * (cell + gap) + cell / 2;
const relativeY = r * (cell + gap) + cell / 2;
const cx = gx - gap * 0.5 + relativeX;
const cy = gy - gap * 0.5 + relativeY;
// 计算库位在背景矩形内的相对位置,然后加上背景矩形的起始位置和居中偏移
// 库位位置应该是左上角坐标,不是中心点
const relativeX = c * (finalCell + finalGap);
const relativeY = r * (finalCell + finalGap);
const cx = gx - finalGap * 0.5 + offsetX + relativeX;
const cy = gy - finalGap * 0.5 + offsetY + relativeY;
// 从storageStateMap获取库位状态
const inner = storageStateMap.get(pointId);
@ -327,14 +441,14 @@ export function createStorageLocationPens(
tags: ['storage-location', `point-${pointId}`],
x: cx,
y: cy,
width: cell,
height: cell,
width: finalCell,
height: finalCell,
lineWidth: 1,
background: occupied ? '#ff4d4f' : '#f5f5f5',
color: '#999999',
background: occupied ? '#ff4d4f' : 'transparent',
color: 'transparent',
locked: LockState.DisableEdit,
visible: true,
borderRadius: Math.min(4, cell * 0.3),
borderRadius: Math.min(4, finalCell * 0.3),
storageLocation: {
pointId,
locationName,
@ -353,11 +467,12 @@ export function createStorageLocationPens(
const i = gridCols * gridRows - 1;
const r = Math.floor(i / gridCols);
const c = i % gridCols;
// 计算库位在背景矩形内的相对位置,然后加上背景矩形的起始位置
const relativeX = c * (cell + gap) + cell / 2;
const relativeY = r * (cell + gap) + cell / 2;
const cx = gx - gap * 0.5 + relativeX;
const cy = gy - gap * 0.5 + relativeY;
// 计算库位在背景矩形内的相对位置,然后加上背景矩形的起始位置和居中偏移
// 更多按钮位置也应该是左上角坐标
const relativeX = c * (finalCell + finalGap);
const relativeY = r * (finalCell + finalGap);
const cx = gx - finalGap * 0.5 + offsetX + relativeX;
const cy = gy - finalGap * 0.5 + offsetY + relativeY;
const morePen: MapPen = {
id: `storage-more-${pointId}`,
@ -365,15 +480,15 @@ export function createStorageLocationPens(
tags: ['storage-more', `point-${pointId}`],
x: cx,
y: cy,
width: cell,
height: cell,
width: finalCell,
height: finalCell,
lineWidth: 1,
background: '#e6f4ff',
color: '#1677ff',
locked: LockState.DisableEdit,
visible: true,
text: `+${overflow}`,
fontSize: Math.floor(cell * 0.6),
fontSize: Math.floor(finalCell * 0.6),
textAlign: 'center',
textBaseline: 'middle',
storageLocation: {