Files
Nitro-V3/src/components/floorplan-editor/views/FloorplanTile.tsx
T
medievalshell 48ed3ad7ba fix: show furniture-occupied tiles in the floor plan editor
The editor never requested occupied tiles, so tiles holding furniture
were indistinguishable from empty floor and could be edited/voided.

- request GetOccupiedTilesMessageComposer when the editor opens
- handle RoomOccupiedTilesMessageEvent -> SET_OCCUPIED_TILES
- new Tile.occupied flag (kept separate from `blocked`/void): occupied
  tiles render with a distinct marker and are protected from PAINT/
  ERASE/ADJUST and brush-to-selection edits
- occupied is purely informational and never changes the saved tilemap
  (no accidental voiding of floor under furni)

Tests: reducer cases for SET_OCCUPIED_TILES + edit protection; container
test asserts the occupied event is non-destructive on save; route the
canvas pointer test through elementFromPoint (jsdom has no getScreenCTM).
2026-05-28 09:20:20 +02:00

146 lines
4.7 KiB
TypeScript

import { FC, memo } from 'react';
import { Tile } from '../state/types';
import { tileFill } from '../state/selectors';
import { TILE_SIZE } from '../state/constants';
import { tileToScreen } from '../hooks/usePointerToTile';
type Props = {
row: number;
col: number;
tile: Tile;
selected: boolean;
isDoor: boolean;
southH?: number;
westH?: number;
};
const HEIGHT_LIFT = TILE_SIZE / 8;
const diamondPoints = (row: number, col: number, h: number): string =>
{
const [ cx, cyBase ] = tileToScreen(row, col);
const cy = cyBase - h * HEIGHT_LIFT;
const half = TILE_SIZE / 2;
const quarter = TILE_SIZE / 4;
return `${ cx },${ cy - quarter } ${ cx + half },${ cy } ${ cx },${ cy + quarter } ${ cx - half },${ cy }`;
};
const darkenHex = (hex: string, factor: number): string =>
{
const h = hex.replace('#', '');
if(h.length !== 6) return hex;
const r = Math.max(0, Math.floor(parseInt(h.slice(0, 2), 16) * factor));
const g = Math.max(0, Math.floor(parseInt(h.slice(2, 4), 16) * factor));
const b = Math.max(0, Math.floor(parseInt(h.slice(4, 6), 16) * factor));
return `#${ [ r, g, b ].map(v => v.toString(16).padStart(2, '0')).join('') }`;
};
const southWallPoints = (cx: number, cy: number, drop: number): string =>
{
const half = TILE_SIZE / 2;
const quarter = TILE_SIZE / 4;
return `${ cx + half },${ cy } ${ cx + half },${ cy + drop } ${ cx },${ cy + quarter + drop } ${ cx },${ cy + quarter }`;
};
const westWallPoints = (cx: number, cy: number, drop: number): string =>
{
const half = TILE_SIZE / 2;
const quarter = TILE_SIZE / 4;
return `${ cx - half },${ cy } ${ cx },${ cy + quarter } ${ cx },${ cy + quarter + drop } ${ cx - half },${ cy + drop }`;
};
const FloorplanTileImpl: FC<Props> = ({ row, col, tile, selected, isDoor, southH = 0, westH = 0 }) =>
{
if(tile.blocked)
{
if(!selected) return null;
const points = diamondPoints(row, col, tile.h);
return (
<polygon
data-testid="selection-ring"
points={ points }
fill="rgba(250, 204, 21, 0.45)"
stroke="#facc15"
strokeWidth={ 1.5 }
strokeDasharray="3 2"
/>
);
}
const points = diamondPoints(row, col, tile.h);
const fill = tileFill(tile);
const [ cx, cyBase ] = tileToScreen(row, col);
const cy = cyBase - tile.h * HEIGHT_LIFT;
const southDrop = Math.max(0, tile.h - southH) * HEIGHT_LIFT;
const westDrop = Math.max(0, tile.h - westH) * HEIGHT_LIFT;
const southFill = (southDrop > 0) ? darkenHex(fill, 0.70) : null;
const westFill = (westDrop > 0) ? darkenHex(fill, 0.55) : null;
return (
<g>
{ southFill && (
<polygon
data-testid="tile-south-wall"
points={ southWallPoints(cx, cy, southDrop) }
fill={ southFill }
stroke="#222"
strokeWidth={ 0.5 }
/>
) }
{ westFill && (
<polygon
data-testid="tile-west-wall"
points={ westWallPoints(cx, cy, westDrop) }
fill={ westFill }
stroke="#222"
strokeWidth={ 0.5 }
/>
) }
<polygon
data-row={ row }
data-col={ col }
points={ points }
fill={ fill }
stroke="#222"
strokeWidth={ 0.5 }
/>
{ tile.occupied && (
<polygon
data-testid="occupied-marker"
points={ points }
fill="rgba(249, 115, 22, 0.40)"
stroke="#f97316"
strokeWidth={ 1 }
strokeDasharray="2 2"
pointerEvents="none"
/>
) }
{ selected && (
<polygon
data-testid="selection-ring"
data-row={ row }
data-col={ col }
points={ points }
fill="none"
stroke="#fff"
strokeWidth={ 2 }
strokeDasharray="3 2"
/>
) }
{ isDoor && (
<polygon
data-testid="door-marker"
data-row={ row }
data-col={ col }
points={ points }
fill="rgba(255,255,255,0.85)"
stroke="#000"
strokeWidth={ 1 }
/>
) }
</g>
);
};
export const FloorplanTile = memo(FloorplanTileImpl);