From 1e861087456be1ddca32daf5e5ed350997e65ff3 Mon Sep 17 00:00:00 2001 From: xudan Date: Thu, 4 Sep 2025 17:27:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=BA=93=E4=BD=8D?= =?UTF-8?q?=E6=A0=85=E6=A0=BC=E7=BB=98=E5=88=B6=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=87=AA=E9=80=82=E5=BA=94=E5=B8=83=E5=B1=80?= =?UTF-8?q?=E5=92=8C=E9=80=8F=E6=98=8E=E8=83=8C=E6=99=AF=EF=BC=8C=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E8=A7=86=E8=A7=89=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/draw/storage-location-drawer.ts | 249 ++++++++++++++----- 1 file changed, 182 insertions(+), 67 deletions(-) diff --git a/src/services/draw/storage-location-drawer.ts b/src/services/draw/storage-location-drawer.ts index a42cb26..f47d8ed 100644 --- a/src/services/draw/storage-location-drawer.ts +++ b/src/services/draw/storage-location-drawer.ts @@ -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: {