feat(agent-office): add procedural furniture renderer with theme support
This commit is contained in:
209
src/pages/agent-office/canvas/FurnitureRenderer.js
Normal file
209
src/pages/agent-office/canvas/FurnitureRenderer.js
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
// src/pages/agent-office/canvas/FurnitureRenderer.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 가구 프로시저럴 렌더러 — 테마 팔레트 기반
|
||||||
|
* 각 가구 타입별 draw 함수, Y-sort를 위한 zY 반환
|
||||||
|
*/
|
||||||
|
export class FurnitureRenderer {
|
||||||
|
constructor(furnitureList, tileSize) {
|
||||||
|
this.furnitureList = furnitureList;
|
||||||
|
this.tileSize = tileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 모든 가구를 Y-sort 순서로 반환 (에이전트와 함께 정렬하기 위함)
|
||||||
|
* @returns {Array<{type, col, row, zY, draw: Function}>}
|
||||||
|
*/
|
||||||
|
getRenderables(theme, scale, offsetX, offsetY) {
|
||||||
|
const ts = this.tileSize * scale;
|
||||||
|
return this.furnitureList.map(f => ({
|
||||||
|
...f,
|
||||||
|
zY: f.row,
|
||||||
|
draw: (ctx) => this._drawFurniture(ctx, f, theme, ts, offsetX, offsetY)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawFurniture(ctx, f, theme, ts, ox, oy) {
|
||||||
|
const x = f.col * ts + ox;
|
||||||
|
const y = f.row * ts + oy;
|
||||||
|
|
||||||
|
switch (f.type) {
|
||||||
|
case 'desk_monitor': this._drawDesk(ctx, f, theme, ts, x, y); break;
|
||||||
|
case 'meeting_table': this._drawMeetingTable(ctx, f, theme, ts, x, y); break;
|
||||||
|
case 'sofa': this._drawSofa(ctx, theme, ts, x, y); break;
|
||||||
|
case 'coffee_machine':this._drawCoffeeMachine(ctx, theme, ts, x, y); break;
|
||||||
|
case 'bookshelf': this._drawBookshelf(ctx, f, theme, ts, x, y); break;
|
||||||
|
case 'plant': this._drawPlant(ctx, theme, ts, x, y); break;
|
||||||
|
case 'water_cooler': this._drawWaterCooler(ctx, theme, ts, x, y); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawDesk(ctx, f, theme, ts, x, y) {
|
||||||
|
// 책상 상판
|
||||||
|
const dw = ts * 2;
|
||||||
|
const dh = ts * 0.6;
|
||||||
|
ctx.fillStyle = theme.furniture.desk;
|
||||||
|
ctx.fillRect(x, y + ts * 0.2, dw, dh);
|
||||||
|
// 책상 다리
|
||||||
|
ctx.fillStyle = theme.wall.border;
|
||||||
|
ctx.fillRect(x + ts * 0.1, y + dh + ts * 0.2, ts * 0.15, ts * 0.3);
|
||||||
|
ctx.fillRect(x + dw - ts * 0.25, y + dh + ts * 0.2, ts * 0.15, ts * 0.3);
|
||||||
|
|
||||||
|
// 모니터들
|
||||||
|
const monCount = f.monitors || 1;
|
||||||
|
const monW = ts * 0.5;
|
||||||
|
const monH = ts * 0.4;
|
||||||
|
const totalW = monCount * monW + (monCount - 1) * ts * 0.1;
|
||||||
|
let monX = x + (dw - totalW) / 2;
|
||||||
|
|
||||||
|
for (let i = 0; i < monCount; i++) {
|
||||||
|
// 모니터 프레임
|
||||||
|
ctx.fillStyle = theme.furniture.monitor;
|
||||||
|
ctx.fillRect(monX, y - monH + ts * 0.2, monW, monH);
|
||||||
|
// 화면
|
||||||
|
ctx.fillStyle = theme.furniture.monitorScreen;
|
||||||
|
ctx.fillRect(monX + ts * 0.05, y - monH + ts * 0.25, monW - ts * 0.1, monH - ts * 0.1);
|
||||||
|
// 모니터 받침대
|
||||||
|
ctx.fillStyle = theme.furniture.monitor;
|
||||||
|
ctx.fillRect(monX + monW * 0.35, y + ts * 0.2 - ts * 0.05, monW * 0.3, ts * 0.08);
|
||||||
|
monX += monW + ts * 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 의자 (책상 아래)
|
||||||
|
ctx.fillStyle = theme.furniture.chair;
|
||||||
|
ctx.fillRect(x + dw * 0.35, y + ts, dw * 0.3, ts * 0.5);
|
||||||
|
ctx.fillRect(x + dw * 0.3, y + ts * 0.8, dw * 0.4, ts * 0.25);
|
||||||
|
|
||||||
|
// 에이전트별 악센트 소품
|
||||||
|
if (f.accent === 'instrument') {
|
||||||
|
// 음표 모양
|
||||||
|
ctx.fillStyle = theme.ui.accent;
|
||||||
|
ctx.fillRect(x + dw + ts * 0.2, y + ts * 0.3, ts * 0.1, ts * 0.5);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x + dw + ts * 0.2, y + ts * 0.8, ts * 0.15, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
} else if (f.accent === 'papers') {
|
||||||
|
// 서류 더미
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.fillRect(x + dw + ts * 0.1, y + ts * 0.3, ts * 0.35, ts * 0.45);
|
||||||
|
ctx.fillStyle = theme.text.label;
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
ctx.fillRect(x + dw + ts * 0.15, y + ts * 0.38 + i * ts * 0.1, ts * 0.25, ts * 0.02);
|
||||||
|
}
|
||||||
|
} else if (f.accent === 'briefcase') {
|
||||||
|
ctx.fillStyle = '#8B4513';
|
||||||
|
ctx.fillRect(x + dw + ts * 0.1, y + ts * 0.5, ts * 0.4, ts * 0.3);
|
||||||
|
ctx.fillStyle = '#D4A06A';
|
||||||
|
ctx.fillRect(x + dw + ts * 0.2, y + ts * 0.45, ts * 0.2, ts * 0.08);
|
||||||
|
} else if (f.accent === 'dice') {
|
||||||
|
ctx.fillStyle = '#ef4444';
|
||||||
|
ctx.fillRect(x + dw + ts * 0.15, y + ts * 0.4, ts * 0.3, ts * 0.3);
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x + dw + ts * 0.3, y + ts * 0.55, ts * 0.05, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawMeetingTable(ctx, f, theme, ts, x, y) {
|
||||||
|
const w = (f.width || 4) * ts;
|
||||||
|
const h = (f.height || 2) * ts;
|
||||||
|
// 테이블 상판
|
||||||
|
ctx.fillStyle = theme.furniture.table;
|
||||||
|
ctx.fillRect(x + ts * 0.1, y + ts * 0.1, w - ts * 0.2, h - ts * 0.2);
|
||||||
|
// 테이블 그림자
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.15)';
|
||||||
|
ctx.fillRect(x + ts * 0.15, y + h - ts * 0.1, w - ts * 0.25, ts * 0.1);
|
||||||
|
// 의자들 (상하 4개씩)
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const cx = x + ts * 0.5 + i * (w - ts) / 3;
|
||||||
|
ctx.fillStyle = theme.furniture.chair;
|
||||||
|
ctx.fillRect(cx, y - ts * 0.3, ts * 0.4, ts * 0.35);
|
||||||
|
ctx.fillRect(cx, y + h - ts * 0.05, ts * 0.4, ts * 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawSofa(ctx, theme, ts, x, y) {
|
||||||
|
ctx.fillStyle = theme.furniture.sofa;
|
||||||
|
ctx.fillRect(x, y, ts * 2, ts * 0.8);
|
||||||
|
// 등받이
|
||||||
|
ctx.fillStyle = theme.furniture.sofa;
|
||||||
|
ctx.fillRect(x, y - ts * 0.3, ts * 2, ts * 0.35);
|
||||||
|
// 쿠션 구분선
|
||||||
|
ctx.strokeStyle = theme.wall.border;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x + ts, y);
|
||||||
|
ctx.lineTo(x + ts, y + ts * 0.8);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawCoffeeMachine(ctx, theme, ts, x, y) {
|
||||||
|
ctx.fillStyle = theme.furniture.coffee;
|
||||||
|
ctx.fillRect(x + ts * 0.15, y, ts * 0.7, ts * 0.8);
|
||||||
|
// 디스펜서
|
||||||
|
ctx.fillStyle = theme.furniture.monitor;
|
||||||
|
ctx.fillRect(x + ts * 0.25, y + ts * 0.15, ts * 0.5, ts * 0.3);
|
||||||
|
// 커피 잔
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.fillRect(x + ts * 0.3, y + ts * 0.55, ts * 0.2, ts * 0.15);
|
||||||
|
// 스팀
|
||||||
|
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x + ts * 0.4, y + ts * 0.5);
|
||||||
|
ctx.quadraticCurveTo(x + ts * 0.45, y + ts * 0.35, x + ts * 0.4, y + ts * 0.2);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawBookshelf(ctx, f, theme, ts, x, y) {
|
||||||
|
const h = (f.height || 3) * ts;
|
||||||
|
ctx.fillStyle = theme.furniture.shelf;
|
||||||
|
ctx.fillRect(x, y, ts * 0.9, h);
|
||||||
|
// 선반 및 책
|
||||||
|
const bookColors = ['#aa4444', '#4444aa', '#44aa44', '#aaaa44', '#aa44aa', '#44aaaa'];
|
||||||
|
const shelfCount = f.height || 3;
|
||||||
|
for (let i = 0; i < shelfCount; i++) {
|
||||||
|
const sy = y + i * ts + ts * 0.1;
|
||||||
|
// 선반 판
|
||||||
|
ctx.fillStyle = theme.furniture.shelf;
|
||||||
|
ctx.fillRect(x, sy + ts * 0.7, ts * 0.9, ts * 0.05);
|
||||||
|
// 책들
|
||||||
|
for (let b = 0; b < 4; b++) {
|
||||||
|
ctx.fillStyle = bookColors[(i * 4 + b) % bookColors.length];
|
||||||
|
ctx.fillRect(x + ts * 0.05 + b * ts * 0.2, sy + ts * 0.1, ts * 0.15, ts * 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawPlant(ctx, theme, ts, x, y) {
|
||||||
|
// 화분
|
||||||
|
ctx.fillStyle = theme.decor.pot;
|
||||||
|
ctx.fillRect(x + ts * 0.25, y + ts * 0.6, ts * 0.5, ts * 0.35);
|
||||||
|
ctx.fillRect(x + ts * 0.2, y + ts * 0.55, ts * 0.6, ts * 0.1);
|
||||||
|
// 잎
|
||||||
|
ctx.fillStyle = theme.decor.plant;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(x + ts * 0.5, y + ts * 0.35, ts * 0.3, ts * 0.25, 0, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(x + ts * 0.35, y + ts * 0.25, ts * 0.15, ts * 0.2, -0.3, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(x + ts * 0.65, y + ts * 0.25, ts * 0.15, ts * 0.2, 0.3, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawWaterCooler(ctx, theme, ts, x, y) {
|
||||||
|
// 본체
|
||||||
|
ctx.fillStyle = theme.furniture.shelf;
|
||||||
|
ctx.fillRect(x + ts * 0.2, y + ts * 0.3, ts * 0.6, ts * 0.6);
|
||||||
|
// 물통
|
||||||
|
ctx.fillStyle = 'rgba(100,180,255,0.5)';
|
||||||
|
ctx.fillRect(x + ts * 0.3, y, ts * 0.4, ts * 0.35);
|
||||||
|
ctx.fillStyle = 'rgba(100,180,255,0.3)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x + ts * 0.5, y, ts * 0.2, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user