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