The tilemap panel gains Fill and Pick: Fill floods the connected cells of the active layer that carry the same tile (edge neighbours only, bounded by the grid) as one undo step, Pick takes the tile of the clicked cell on the active layer, and holding Alt picks while any tool is active (#133)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c2fd536aaf
commit
0b32d3cbf3
9 files changed
+350
-20
No files matched your search
@@ -48,8 +48,55 @@ const TOOLS: { tool: TilemapTool; label: string; hint: string }[] = [
|
||||
{ tool: 'brush', label: 'Brush', hint: 'Drag over the map to paint a line of cells.' },
|
||||
{ tool: 'erase', label: 'Erase', hint: 'Drag over the map to clear cells.' },
|
||||
{ tool: 'rect', label: 'Rect', hint: 'Drag to fill the box you span.' },
|
||||
{ tool: 'fill', label: 'Fill', hint: 'Click to fill the connected cells of the same tile.' },
|
||||
{ tool: 'pick', label: 'Pick', hint: 'Click a cell to take its tile and paint on with it.' },
|
||||
];
|
||||
|
||||
/**
|
||||
* While Alt is held the tool is Pick, and this is the tool it came from – what releasing Alt
|
||||
* brings back (#133). Null while Alt is up.
|
||||
*/
|
||||
let heldFrom: TilemapTool | null = null;
|
||||
/** The painting tool the Pick button hands a picked tile to: the one in use before it. */
|
||||
let pickReturn: TilemapTool = 'brush';
|
||||
|
||||
function paintingTool(tool: TilemapTool): TilemapTool {
|
||||
return tool === 'brush' || tool === 'rect' || tool === 'fill' ? tool : 'brush';
|
||||
}
|
||||
|
||||
/** A tool button: a choice made by hand, so it also ends a held Alt – releasing it changes nothing. */
|
||||
export function chooseTool(state: EditorState, tool: TilemapTool): void {
|
||||
heldFrom = null;
|
||||
if (tool === 'pick' && state.tilemapPaint.tool !== 'pick')
|
||||
pickReturn = paintingTool(state.tilemapPaint.tool);
|
||||
state.setTilemapPaint({ tool });
|
||||
}
|
||||
|
||||
/**
|
||||
* A tile taken from the map. Picking is choosing a tile, so like a click in the palette it goes
|
||||
* on painting with it: back to the tool before Pick – unless Alt is held, whose release does that.
|
||||
*/
|
||||
export function pickTile(state: EditorState, tileId: string): void {
|
||||
state.setTilemapPaint(heldFrom ? { tileId } : { tileId, tool: pickReturn });
|
||||
}
|
||||
|
||||
/** Alt pressed or released (or the window left with it down, which never reports the release). */
|
||||
export function holdPick(state: EditorState, held: boolean): boolean {
|
||||
if (held) {
|
||||
const tool = state.tilemapPaint.tool;
|
||||
if (heldFrom || tool === 'pick' || !paintedTilemap(state)) return false;
|
||||
heldFrom = tool;
|
||||
state.setTilemapPaint({ tool: 'pick' });
|
||||
return true;
|
||||
}
|
||||
const back = heldFrom;
|
||||
heldFrom = null;
|
||||
if (!back || state.tilemapPaint.tool !== 'pick') return false;
|
||||
// With the map deselected meanwhile the panel is gone, and so is any tool but Select.
|
||||
state.setTilemapPaint({ tool: paintedTilemap(state) ? back : 'select' });
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The tilemap that is painted on: the one scene object selected, placed in the active scene. A
|
||||
* second object in the selection means there is no single map to paint, so there is none (#142).
|
||||
@@ -128,6 +175,14 @@ export class TilemapPanel extends Panel {
|
||||
ui().focus.changeHandlers.push(() => {
|
||||
if (this.rebuildWhenLeft && !ui().focus.isWithin(this)) this.sync();
|
||||
});
|
||||
// Holding Alt picks (#133). The focus manager only hears presses, so the release has its own
|
||||
// listener; a handled Alt is kept from the browser, which would otherwise open its menu bar.
|
||||
const onAlt = (held: boolean) => (event: KeyboardEvent) => {
|
||||
if (event.key === 'Alt' && !event.repeat && holdPick(state, held)) event.preventDefault();
|
||||
};
|
||||
window.addEventListener('keydown', onAlt(true));
|
||||
window.addEventListener('keyup', onAlt(false));
|
||||
window.addEventListener('blur', () => holdPick(state, false));
|
||||
this.sync();
|
||||
}
|
||||
|
||||
@@ -158,6 +213,7 @@ export class TilemapPanel extends Panel {
|
||||
const target = paintedTilemap(this.state);
|
||||
// With the panel gone nothing shows the brush any more, and a click on the next map would paint.
|
||||
// The tile and the layer stay as they were: they are what one comes back to.
|
||||
if (!target) heldFrom = null;
|
||||
if (!target && this.state.tilemapPaint.tool !== 'select') {
|
||||
this.state.setTilemapPaint({ tool: 'select' });
|
||||
return;
|
||||
@@ -210,27 +266,30 @@ function buildTilemapSection(host: InspectorHost, asset: TilemapAsset): void {
|
||||
built = widgets;
|
||||
|
||||
addTitle(host, 'PAINTING');
|
||||
const tools = new HStack({ gap: Theme.gap });
|
||||
for (const { tool, label } of TOOLS) {
|
||||
const button = new Button(label, { onClick: () => state.setTilemapPaint({ tool }) });
|
||||
button.toggled = paint.tool === tool;
|
||||
widgets.tools.set(tool, button);
|
||||
tools.add(button, 1);
|
||||
// Two rows of three: six in one row cut "Select" down to "Se…" at the panel's starting width.
|
||||
for (let start = 0; start < TOOLS.length; start += 3) {
|
||||
const tools = new HStack({ gap: Theme.gap });
|
||||
for (const { tool, label } of TOOLS.slice(start, start + 3)) {
|
||||
const button = new Button(label, { onClick: () => chooseTool(state, tool) });
|
||||
button.toggled = paint.tool === tool;
|
||||
widgets.tools.set(tool, button);
|
||||
tools.add(button, 1);
|
||||
}
|
||||
host.body.add(tools);
|
||||
}
|
||||
host.body.add(tools);
|
||||
addNote(host, TOOLS.find((t) => t.tool === paint.tool)?.hint ?? '', 'small');
|
||||
addNote(host, 'Shift-drag still pans the view.', 'small');
|
||||
addNote(host, 'Hold Alt to pick a tile. Shift-drag still pans the view.', 'small');
|
||||
|
||||
addTitle(host, 'TILES');
|
||||
const tiles = tileIdsOf(state);
|
||||
if (tiles.length === 0) addNote(host, 'The project has no tile yet.');
|
||||
else {
|
||||
// Choosing a tile is how one starts painting, so it reaches for the brush by itself.
|
||||
// Choosing a tile is how one starts painting, so it reaches for the brush by itself – and a
|
||||
// tile chosen while picking is a pick, too.
|
||||
widgets.palette = new TilePalette(state, tiles, paint.tileId, (tileId) =>
|
||||
state.setTilemapPaint({
|
||||
tileId,
|
||||
tool: paint.tool === 'select' || paint.tool === 'erase' ? 'brush' : paint.tool,
|
||||
}),
|
||||
paint.tool === 'pick'
|
||||
? pickTile(state, tileId)
|
||||
: state.setTilemapPaint({ tileId, tool: paintingTool(paint.tool) }),
|
||||
);
|
||||
host.body.add(widgets.palette);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user