From 464a49bb6d2e99b169f99ca28ae2e0b4025fe705 Mon Sep 17 00:00:00 2001 From: robotica4us-collab Date: Sat, 28 Mar 2026 22:08:05 -0500 Subject: [PATCH] fix(navigation): block desk_cubicle in nav grid with zero padding to prevent walk-through (#75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - desk_cubicle now has blocksNavigation: true with navPadding: 0 (tight blocking, no inflation — aisles stay clear) - buildNavGrid reads per-item navPadding from ITEM_METADATA - getDeskLocations targets y-5 (chair position, above desk blocked zone) - Agents route AROUND desks instead of through them - Chair stays passable so agents can reach their sitting position Fixes object passthrough for desks. Other large passable items (doors, lamps) unchanged — they remain non-blocking by design. Co-authored-by: Neo (subagent) Co-authored-by: Luke The Dev <252071647+iamlukethedev@users.noreply.github.com> --- src/features/retro-office/RetroOffice3D.tsx | 2 +- src/features/retro-office/core/geometry.ts | 4 ++-- src/features/retro-office/core/navigation.ts | 15 ++++++++------- tests/unit/navigation.navBlockers.test.ts | 14 ++++++++------ 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/features/retro-office/RetroOffice3D.tsx b/src/features/retro-office/RetroOffice3D.tsx index 2264424..153d664 100644 --- a/src/features/retro-office/RetroOffice3D.tsx +++ b/src/features/retro-office/RetroOffice3D.tsx @@ -4500,7 +4500,7 @@ export function RetroOffice3D({ const agent = renderAgentLookupRef.current.get(agentId); if (!agent) return; const tx = item.x + 40; - const ty = item.y + 40; + const ty = item.y - 5; const path = planPath(agent.x, agent.y, tx, ty); // Mutate the render agent ref directly so the tick loop picks it up. Object.assign(agent, { diff --git a/src/features/retro-office/core/geometry.ts b/src/features/retro-office/core/geometry.ts index 8c30c2d..79582c5 100644 --- a/src/features/retro-office/core/geometry.ts +++ b/src/features/retro-office/core/geometry.ts @@ -107,7 +107,7 @@ export const getItemBaseSize = (item: FurnitureItem) => { * This is the single source of truth for nav-blocking behaviour. `buildNavGrid` in * navigation.ts reads this instead of maintaining its own hardcoded type set. */ -export const ITEM_METADATA: Record = { +export const ITEM_METADATA: Record = { // ── structural ──────────────────────────────────────────────────────────── wall: { blocksNavigation: true }, door: { blocksNavigation: false }, // passable @@ -117,7 +117,7 @@ export const ITEM_METADATA: Record = { couch_v: { blocksNavigation: true }, beanbag: { blocksNavigation: true }, // large floor seat (issue #4) // ── desks / workstations ────────────────────────────────────────────────── - desk_cubicle: { blocksNavigation: false }, // agents stand at these; collision handled separately + desk_cubicle: { blocksNavigation: true, navPadding: 0 }, // blocks nav with zero padding (tight to desk body) executive_desk: { blocksNavigation: true }, // ── tables ──────────────────────────────────────────────────────────────── round_table: { blocksNavigation: true }, diff --git a/src/features/retro-office/core/navigation.ts b/src/features/retro-office/core/navigation.ts index d8415b7..952eb75 100644 --- a/src/features/retro-office/core/navigation.ts +++ b/src/features/retro-office/core/navigation.ts @@ -84,14 +84,15 @@ const itemBlocksNavigation = (type: string): boolean => export function buildNavGrid(furniture: FurnitureItem[]): NavGrid { const grid = new Uint8Array(GRID_COLS * GRID_ROWS); - const pad = GRID_CELL * 0.6; + const defaultPad = GRID_CELL * 0.6; for (const item of furniture) { if (!itemBlocksNavigation(item.type)) continue; + const itemPad = ITEM_METADATA[item.type]?.navPadding ?? defaultPad; const bounds = getItemBounds(item); - const x1 = bounds.x - pad; - const y1 = bounds.y - pad; - const x2 = bounds.x + bounds.w + pad; - const y2 = bounds.y + bounds.h + pad; + const x1 = bounds.x - itemPad; + const y1 = bounds.y - itemPad; + const x2 = bounds.x + bounds.w + itemPad; + const y2 = bounds.y + bounds.h + itemPad; const c1 = Math.max(0, Math.floor(x1 / GRID_CELL)); const c2 = Math.min(GRID_COLS - 1, Math.floor(x2 / GRID_CELL)); const r1 = Math.max(0, Math.floor(y1 / GRID_CELL)); @@ -302,7 +303,7 @@ export function astar( export const getDeskLocations = (items: FurnitureItem[]) => items .filter((item) => item.type === "desk_cubicle") - .map((item) => ({ x: item.x + 40, y: item.y + 40 })); + .map((item) => ({ x: item.x + 40, y: item.y - 5 })); export const getMeetingSeatLocations = (items: FurnitureItem[]) => { // Meeting seats are inferred from chair placement in the conference area so standup @@ -497,7 +498,7 @@ export const resolveDeskIndexForItem = ( if (deskLocations.length === 0) return -1; if (item.type === "desk_cubicle") { return deskLocations.findIndex( - (desk) => desk.x === item.x + 40 && desk.y === item.y + 40, + (desk) => desk.x === item.x + 40 && desk.y === item.y - 5, ); } diff --git a/tests/unit/navigation.navBlockers.test.ts b/tests/unit/navigation.navBlockers.test.ts index a2918f8..bb6f985 100644 --- a/tests/unit/navigation.navBlockers.test.ts +++ b/tests/unit/navigation.navBlockers.test.ts @@ -203,17 +203,19 @@ describe("buildNavGrid + astar – full pathfinding integration (issue #4)", () }); describe("buildNavGrid – specific non-blocking item types (issue #4)", () => { - it("desk_cubicle does NOT block navigation — agents stand at these", () => { + it("desk_cubicle blocks navigation with zero padding — agents route around desks", () => { /* - * desk_cubicle has blocksNavigation: false in ITEM_METADATA. - * Agents interact with desks by standing beside them; the desk itself - * is not a solid blocker in the pathfinding grid. + * desk_cubicle has blocksNavigation: true with navPadding: 0 in ITEM_METADATA. + * The desk body is blocked in the nav grid so agents route around it, + * but zero padding keeps aisles between desks navigable. + * Agents sit at the chair position (y - 5), which is above the blocked zone. */ const item = makeItem("desk_cubicle", 400, 300); const grid = buildNavGrid([item]); - expect(isBlocked(grid, 400, 300)).toBe(false); + expect(isBlocked(grid, 400, 300)).toBe(true); // Confirm via metadata as the authoritative source. - expect(ITEM_METADATA["desk_cubicle"]?.blocksNavigation).toBe(false); + expect(ITEM_METADATA["desk_cubicle"]?.blocksNavigation).toBe(true); + expect(ITEM_METADATA["desk_cubicle"]?.navPadding).toBe(0); }); it("door does NOT block navigation — agents must be able to walk through doors", () => {