fix(agent-office): critical bug fixes from code review — wall pathfinding, drag/click, DPR, culling

- Pathfinder.setBlocked: remove blocked.clear() to preserve wall tiles set by setWalls()
- Pathfinder.findPath: fix dead-code goal exception — remove redundant isBlocked check, keep goal-tile exception in single guard
- OfficeRenderer: track mouseDownPos/_wasDragging; expose wasDragging() method for click-after-drag suppression
- OfficeRenderer._render: track _lastDpr to detect monitor DPR changes; use setTransform instead of scale to avoid accumulation
- TileMap.render: use clientWidth/clientHeight for viewport culling (CSS space, not buffer pixels)
- TaskTab: wrap JSON.parse in try/catch to prevent crash on malformed result_data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 09:40:08 +09:00
parent 3e4f2e0934
commit 6cbdf95596
6 changed files with 37 additions and 11 deletions

View File

@@ -48,6 +48,11 @@ export class OfficeRenderer {
// 게임 루프
this._lastTime = 0;
this._animId = null;
this._lastDpr = window.devicePixelRatio || 1;
// 드래그 감지
this._mouseDownPos = { x: 0, y: 0 };
this._wasDragging = false;
// 이벤트
this._setupInputHandlers();
@@ -91,12 +96,17 @@ export class OfficeRenderer {
if (e.button === 0) {
this._isPanning = true;
this._panStart = { x: e.clientX - this.panX, y: e.clientY - this.panY };
this._mouseDownPos = { x: e.clientX, y: e.clientY };
this._wasDragging = false;
}
});
this._onMouseMove = (e) => {
if (this._isPanning) {
this.panX = e.clientX - this._panStart.x;
this.panY = e.clientY - this._panStart.y;
const dx = e.clientX - this._mouseDownPos.x;
const dy = e.clientY - this._mouseDownPos.y;
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) this._wasDragging = true;
}
};
this._onMouseUp = () => {
@@ -244,11 +254,13 @@ export class OfficeRenderer {
// 캔버스 크기 조정
const displayW = this.canvas.clientWidth;
const displayH = this.canvas.clientHeight;
if (this.canvas.width !== displayW * dpr || this.canvas.height !== displayH * dpr) {
if (this.canvas.width !== displayW * dpr || this.canvas.height !== displayH * dpr || this._lastDpr !== dpr) {
this.canvas.width = displayW * dpr;
this.canvas.height = displayH * dpr;
ctx.scale(dpr, dpr);
this._lastDpr = dpr;
}
// setTransform 방식으로 누적 없이 항상 올바른 변환 적용
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.imageSmoothingEnabled = false;
ctx.clearRect(0, 0, displayW, displayH);
@@ -287,6 +299,9 @@ export class OfficeRenderer {
}
}
/** 드래그 여부 반환 (클릭 이벤트 필터링용) */
wasDragging() { return this._wasDragging; }
/** 리사이즈 처리 */
resize() {
// 다음 프레임에서 자동 조정됨 (_render에서 크기 체크)

View File

@@ -13,7 +13,7 @@ export class Pathfinder {
/** blocked 타일 세팅 (wall + furniture footprint) */
setBlocked(blockedList) {
this.blocked.clear();
// Do NOT clear — setWalls already added wall tiles
for (const [col, row] of blockedList) {
this.blocked.add(`${col},${row}`);
}
@@ -65,9 +65,9 @@ export class Pathfinder {
const nr = current.row + dr;
const nk = key(nc, nr);
if (visited.has(nk) || this.isBlocked(nc, nr)) continue;
if (visited.has(nk)) continue;
// 골 지점은 blocked여도 이동 가능 (에이전트가 자기 자리에 앉으려면)
if (nk !== goalKey && this.blocked.has(nk)) continue;
if (nk !== goalKey && this.isBlocked(nc, nr)) continue;
visited.add(nk);
parent.set(nk, key(current.col, current.row));

View File

@@ -30,8 +30,8 @@ export class TileMap {
const x = c * ts + offsetX;
const y = r * ts + offsetY;
// 화면 밖이면 스킵
if (x + ts < 0 || y + ts < 0 || x > ctx.canvas.width || y > ctx.canvas.height) continue;
// 화면 밖이면 스킵 (CSS 공간 기준 — DPR 변환 적용된 좌표계)
if (x + ts < 0 || y + ts < 0 || x > ctx.canvas.clientWidth || y > ctx.canvas.clientHeight) continue;
if (tileType === 0) {
// 벽