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:
@@ -14,7 +14,7 @@ export default function AgentOffice() {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
canvasRef, updateAgentState, setAgentNotification,
|
canvasRef, updateAgentState, setAgentNotification,
|
||||||
setTheme, setZoom, hitTest, getZoom
|
setTheme, setZoom, hitTest, getZoom, wasDragging
|
||||||
} = useOfficeCanvas();
|
} = useOfficeCanvas();
|
||||||
|
|
||||||
const [selectedAgent, setSelectedAgent] = useState(null);
|
const [selectedAgent, setSelectedAgent] = useState(null);
|
||||||
@@ -37,6 +37,7 @@ export default function AgentOffice() {
|
|||||||
|
|
||||||
// 캔버스 클릭 핸들러
|
// 캔버스 클릭 핸들러
|
||||||
const handleCanvasClick = useCallback((e) => {
|
const handleCanvasClick = useCallback((e) => {
|
||||||
|
if (wasDragging()) return; // 드래그 후 발생하는 클릭 무시
|
||||||
const result = hitTest(e.clientX, e.clientY);
|
const result = hitTest(e.clientX, e.clientY);
|
||||||
if (result.type === 'agent') {
|
if (result.type === 'agent') {
|
||||||
setSelectedAgent(result.id);
|
setSelectedAgent(result.id);
|
||||||
@@ -45,7 +46,7 @@ export default function AgentOffice() {
|
|||||||
} else {
|
} else {
|
||||||
setSelectedAgent(null);
|
setSelectedAgent(null);
|
||||||
}
|
}
|
||||||
}, [hitTest, clearNotifications, setAgentNotification]);
|
}, [hitTest, clearNotifications, setAgentNotification, wasDragging]);
|
||||||
|
|
||||||
// 테마 변경
|
// 테마 변경
|
||||||
const handleThemeChange = useCallback((name) => {
|
const handleThemeChange = useCallback((name) => {
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ export class OfficeRenderer {
|
|||||||
// 게임 루프
|
// 게임 루프
|
||||||
this._lastTime = 0;
|
this._lastTime = 0;
|
||||||
this._animId = null;
|
this._animId = null;
|
||||||
|
this._lastDpr = window.devicePixelRatio || 1;
|
||||||
|
|
||||||
|
// 드래그 감지
|
||||||
|
this._mouseDownPos = { x: 0, y: 0 };
|
||||||
|
this._wasDragging = false;
|
||||||
|
|
||||||
// 이벤트
|
// 이벤트
|
||||||
this._setupInputHandlers();
|
this._setupInputHandlers();
|
||||||
@@ -91,12 +96,17 @@ export class OfficeRenderer {
|
|||||||
if (e.button === 0) {
|
if (e.button === 0) {
|
||||||
this._isPanning = true;
|
this._isPanning = true;
|
||||||
this._panStart = { x: e.clientX - this.panX, y: e.clientY - this.panY };
|
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) => {
|
this._onMouseMove = (e) => {
|
||||||
if (this._isPanning) {
|
if (this._isPanning) {
|
||||||
this.panX = e.clientX - this._panStart.x;
|
this.panX = e.clientX - this._panStart.x;
|
||||||
this.panY = e.clientY - this._panStart.y;
|
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 = () => {
|
this._onMouseUp = () => {
|
||||||
@@ -244,11 +254,13 @@ export class OfficeRenderer {
|
|||||||
// 캔버스 크기 조정
|
// 캔버스 크기 조정
|
||||||
const displayW = this.canvas.clientWidth;
|
const displayW = this.canvas.clientWidth;
|
||||||
const displayH = this.canvas.clientHeight;
|
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.width = displayW * dpr;
|
||||||
this.canvas.height = displayH * 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.imageSmoothingEnabled = false;
|
||||||
ctx.clearRect(0, 0, displayW, displayH);
|
ctx.clearRect(0, 0, displayW, displayH);
|
||||||
@@ -287,6 +299,9 @@ export class OfficeRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 드래그 여부 반환 (클릭 이벤트 필터링용) */
|
||||||
|
wasDragging() { return this._wasDragging; }
|
||||||
|
|
||||||
/** 리사이즈 처리 */
|
/** 리사이즈 처리 */
|
||||||
resize() {
|
resize() {
|
||||||
// 다음 프레임에서 자동 조정됨 (_render에서 크기 체크)
|
// 다음 프레임에서 자동 조정됨 (_render에서 크기 체크)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export class Pathfinder {
|
|||||||
|
|
||||||
/** blocked 타일 세팅 (wall + furniture footprint) */
|
/** blocked 타일 세팅 (wall + furniture footprint) */
|
||||||
setBlocked(blockedList) {
|
setBlocked(blockedList) {
|
||||||
this.blocked.clear();
|
// Do NOT clear — setWalls already added wall tiles
|
||||||
for (const [col, row] of blockedList) {
|
for (const [col, row] of blockedList) {
|
||||||
this.blocked.add(`${col},${row}`);
|
this.blocked.add(`${col},${row}`);
|
||||||
}
|
}
|
||||||
@@ -65,9 +65,9 @@ export class Pathfinder {
|
|||||||
const nr = current.row + dr;
|
const nr = current.row + dr;
|
||||||
const nk = key(nc, nr);
|
const nk = key(nc, nr);
|
||||||
|
|
||||||
if (visited.has(nk) || this.isBlocked(nc, nr)) continue;
|
if (visited.has(nk)) continue;
|
||||||
// 골 지점은 blocked여도 이동 가능 (에이전트가 자기 자리에 앉으려면)
|
// 골 지점은 blocked여도 이동 가능 (에이전트가 자기 자리에 앉으려면)
|
||||||
if (nk !== goalKey && this.blocked.has(nk)) continue;
|
if (nk !== goalKey && this.isBlocked(nc, nr)) continue;
|
||||||
|
|
||||||
visited.add(nk);
|
visited.add(nk);
|
||||||
parent.set(nk, key(current.col, current.row));
|
parent.set(nk, key(current.col, current.row));
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export class TileMap {
|
|||||||
const x = c * ts + offsetX;
|
const x = c * ts + offsetX;
|
||||||
const y = r * ts + offsetY;
|
const y = r * ts + offsetY;
|
||||||
|
|
||||||
// 화면 밖이면 스킵
|
// 화면 밖이면 스킵 (CSS 공간 기준 — DPR 변환 적용된 좌표계)
|
||||||
if (x + ts < 0 || y + ts < 0 || x > ctx.canvas.width || y > ctx.canvas.height) continue;
|
if (x + ts < 0 || y + ts < 0 || x > ctx.canvas.clientWidth || y > ctx.canvas.clientHeight) continue;
|
||||||
|
|
||||||
if (tileType === 0) {
|
if (tileType === 0) {
|
||||||
// 벽
|
// 벽
|
||||||
|
|||||||
@@ -45,7 +45,12 @@ export default function TaskTab({ agentId, refreshTrigger }) {
|
|||||||
<span className="ao-task-time">{formatTime(task.created_at)}</span>
|
<span className="ao-task-time">{formatTime(task.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
{expanded === task.id && task.result_data && (
|
{expanded === task.id && task.result_data && (
|
||||||
<pre className="ao-task-result">{JSON.stringify(JSON.parse(task.result_data), null, 2)}</pre>
|
<pre className="ao-task-result">
|
||||||
|
{(() => {
|
||||||
|
try { return JSON.stringify(JSON.parse(task.result_data), null, 2); }
|
||||||
|
catch { return task.result_data; }
|
||||||
|
})()}
|
||||||
|
</pre>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ export function useOfficeCanvas() {
|
|||||||
return rendererRef.current?.zoom || 2;
|
return rendererRef.current?.zoom || 2;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const wasDragging = useCallback(() => {
|
||||||
|
return rendererRef.current?.wasDragging?.() || false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canvasRef,
|
canvasRef,
|
||||||
updateAgentState,
|
updateAgentState,
|
||||||
@@ -54,6 +58,7 @@ export function useOfficeCanvas() {
|
|||||||
setTheme,
|
setTheme,
|
||||||
setZoom,
|
setZoom,
|
||||||
hitTest,
|
hitTest,
|
||||||
getZoom
|
getZoom,
|
||||||
|
wasDragging
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user