From 95b3f2b37c0fac75a886effe756572d5ea1dcc37 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 27 Apr 2026 08:28:56 +0900 Subject: [PATCH] refactor(agent-office): rewrite TileMap with theme support and viewport culling --- src/pages/agent-office/canvas/TileMap.js | 148 +++++++++++------------ 1 file changed, 69 insertions(+), 79 deletions(-) diff --git a/src/pages/agent-office/canvas/TileMap.js b/src/pages/agent-office/canvas/TileMap.js index be1ae3f..7d892ef 100644 --- a/src/pages/agent-office/canvas/TileMap.js +++ b/src/pages/agent-office/canvas/TileMap.js @@ -1,90 +1,80 @@ -const WALL_COLOR = '#2a2a3a'; -const DESK_COLOR = '#6b5b3a'; -const DESK_TOP = '#8b7b5a'; -const TABLE_COLOR = '#5a4a2a'; -const SOFA_COLOR = '#884444'; -const MONITOR_COLOR = '#224466'; -const MONITOR_SCREEN = '#44aacc'; +// src/pages/agent-office/canvas/TileMap.js -export function drawTileMap(ctx, mapData, width, height) { - const { tileSize, cols, rows, layers, furniture, colors } = mapData; - const scaleX = width / (cols * tileSize); - const scaleY = height / (rows * tileSize); - const scale = Math.min(scaleX, scaleY); - - const offsetX = (width - cols * tileSize * scale) / 2; - const offsetY = (height - rows * tileSize * scale) / 2; - - ctx.save(); - ctx.translate(offsetX, offsetY); - ctx.scale(scale, scale); - - const floor = layers.floor; - for (let r = 0; r < rows; r++) { - for (let c = 0; c < cols; c++) { - const tile = floor[r][c]; - ctx.fillStyle = colors[String(tile)] || '#3a3a50'; - ctx.fillRect(c * tileSize, r * tileSize, tileSize, tileSize); - ctx.strokeStyle = 'rgba(255,255,255,0.03)'; - ctx.strokeRect(c * tileSize, r * tileSize, tileSize, tileSize); - } +/** + * 타일맵 렌더러 — 바닥, 벽, 그리드를 테마 팔레트로 렌더링 + * 가구는 FurnitureRenderer가 별도 처리 + */ +export class TileMap { + constructor(mapData) { + this.cols = mapData.cols; + this.rows = mapData.rows; + this.tileSize = mapData.tileSize; + this.floor = mapData.floor; + this.tileTypes = mapData.tileTypes; } - ctx.fillStyle = WALL_COLOR; - ctx.fillRect(0, 0, cols * tileSize, 4); + /** + * 바닥 + 벽 렌더링 + * @param {CanvasRenderingContext2D} ctx + * @param {object} theme - themes.js 에서 가져온 테마 객체 + * @param {number} scale - 줌 레벨 + * @param {number} offsetX - 패닝 X 오프셋 + * @param {number} offsetY - 패닝 Y 오프셋 + */ + render(ctx, theme, scale, offsetX, offsetY) { + const ts = this.tileSize * scale; - for (const f of furniture) { - const fx = f.x * tileSize; - const fy = f.y * tileSize; - const fw = (f.w || 2) * tileSize; - const fh = (f.h || 2) * tileSize; + for (let r = 0; r < this.rows; r++) { + for (let c = 0; c < this.cols; c++) { + const tileType = this.floor[r][c]; + const x = c * ts + offsetX; + const y = r * ts + offsetY; - if (f.type === 'desk') { - ctx.fillStyle = DESK_COLOR; - ctx.fillRect(fx, fy, fw, fh); - ctx.fillStyle = DESK_TOP; - ctx.fillRect(fx + 2, fy + 2, fw - 4, 6); - const mx = fx + fw / 2 - 8; - ctx.fillStyle = MONITOR_COLOR; - ctx.fillRect(mx, fy + 4, 16, 12); - ctx.fillStyle = MONITOR_SCREEN; - ctx.fillRect(mx + 2, fy + 6, 12, 8); - if (f.label) { - ctx.fillStyle = 'rgba(255,255,255,0.6)'; - ctx.font = '8px monospace'; - ctx.textAlign = 'center'; - ctx.fillText(f.label, fx + fw / 2, fy + fh + 12); + // 화면 밖이면 스킵 + if (x + ts < 0 || y + ts < 0 || x > ctx.canvas.width || y > ctx.canvas.height) continue; + + if (tileType === 0) { + // 벽 + ctx.fillStyle = theme.wall.color; + ctx.fillRect(x, y, ts, ts); + // 벽 하단 경계선 + ctx.fillStyle = theme.wall.border; + ctx.fillRect(x, y + ts - scale, ts, scale); + } else { + // 바닥 + const isBreak = this.tileTypes[String(tileType)] === 'floor_break'; + ctx.fillStyle = isBreak ? theme.floor.color2 : theme.floor.color1; + ctx.fillRect(x, y, ts, ts); + + // 체커보드 패턴 + if ((r + c) % 2 === 0) { + ctx.fillStyle = theme.floor.grid; + ctx.fillRect(x, y, ts, ts); + } + + // 그리드 선 + ctx.strokeStyle = theme.floor.grid; + ctx.lineWidth = scale * 0.5; + ctx.strokeRect(x, y, ts, ts); + } } - } else if (f.type === 'table') { - ctx.fillStyle = TABLE_COLOR; - ctx.fillRect(fx, fy, fw, fh); - ctx.fillStyle = '#7a6a4a'; - ctx.fillRect(fx + 4, fy + 4, fw - 8, fh - 8); - } else if (f.type === 'sofa') { - ctx.fillStyle = SOFA_COLOR; - ctx.fillRect(fx, fy, 48, 32); - ctx.fillStyle = '#aa5555'; - ctx.fillRect(fx + 4, fy + 4, 40, 24); - } else if (f.type === 'coffee') { - ctx.fillStyle = '#664422'; - ctx.fillRect(fx + 8, fy + 8, 16, 20); - ctx.fillStyle = '#886644'; - ctx.fillRect(fx + 6, fy + 6, 20, 4); } } - ctx.restore(); - return { scale, offsetX, offsetY, tileSize }; -} + /** 화면 좌표 → 타일 좌표 변환 */ + screenToTile(screenX, screenY, scale, offsetX, offsetY) { + const ts = this.tileSize * scale; + const col = Math.floor((screenX - offsetX) / ts); + const row = Math.floor((screenY - offsetY) / ts); + return { col, row }; + } -export function worldToTile(mapData, renderInfo, canvasX, canvasY) { - const { scale, offsetX, offsetY, tileSize } = renderInfo; - const wx = (canvasX - offsetX) / scale; - const wy = (canvasY - offsetY) / scale; - return { col: Math.floor(wx / tileSize), row: Math.floor(wy / tileSize), worldX: wx, worldY: wy }; -} - -export function tileToCanvas(mapData, renderInfo, col, row) { - const { scale, offsetX, offsetY, tileSize } = renderInfo; - return { x: offsetX + col * tileSize * scale + (tileSize * scale) / 2, y: offsetY + row * tileSize * scale + (tileSize * scale) / 2 }; + /** 타일 좌표 → 화면 좌표 (타일 중앙) */ + tileToScreen(col, row, scale, offsetX, offsetY) { + const ts = this.tileSize * scale; + return { + x: col * ts + offsetX + ts / 2, + y: row * ts + offsetY + ts / 2 + }; + } }