Empty a deleted tile's cells on every tilemap (#268)
Deleting a tile left the cells painted with it in the tilemaps. They drew nothing, blocked nothing and did not show in the delete dialog, but still read as a tile for "Tile at" and stopped a fill. - DeleteAssetCommand empties the cells of a deleted tile on every layer of every tilemap in the same step. Undo puts the tile and its cells back together. - The delete dialog counts them, for example "3 cell(s) on 1 tilemap(s) are painted with it and become empty." (cellsPainted). - presentTile treats a cell naming a missing tile as empty. "Tile at" (World.tileAt) and floodFill use it, so cells already in existing files read and fill as empty. There is no migration. - The brush, the rectangle and the fill check the tile in hand. A tile deleted after it was picked no longer paints new invisible cells, the editor says "Choose a tile to paint with." instead. - Unchanged on purpose: the picker still reports such a cell as naming a tile that no longer exists, and erasing removes it. Tests: - ./check.sh: 195 files, 2823 tests, CHECK EXIT 0 (+6). - GM_STEPS=39,40: ALL PASSED, 100 checks (4 new in 40t). - Full e2e sum is now 1590 (1586 confirmed by CI on round 8, plus 4). - Counter-checks, all fail as expected: 5 in unit tests (delete without emptying, undo without cells, fill and "Tile at" without presentTile, wrong map count) and 3 in the browser (dialog without count, delete without emptying, brush without the check). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
14972d057f
commit
9b5eeaab57
11 files changed
+310
-38
No files matched your search
+21
-12
@@ -28,7 +28,13 @@ import { findAsset, findGraphAsset, type ObjectHolder, type Project } from '../c
|
||||
import type { SceneObject } from '../core/model/SceneObject';
|
||||
import { pixelArtRefIds } from '../core/model/pixelart';
|
||||
import { textureRefIds } from '../core/model/texture';
|
||||
import { animationsUsing, tilesUsing, findPropertyUsages, findUsages } from '../core/model/usages';
|
||||
import {
|
||||
animationsUsing,
|
||||
cellsPainted,
|
||||
tilesUsing,
|
||||
findPropertyUsages,
|
||||
findUsages,
|
||||
} from '../core/model/usages';
|
||||
import { Dialog } from '../ui';
|
||||
import { type EditorState, objectSelection } from './state';
|
||||
|
||||
@@ -189,20 +195,23 @@ export function confirmDeleteAsset(state: EditorState, assetId: string): void {
|
||||
const typed =
|
||||
asset.type === 'struct' ? structTypeUsages(state.project, assetId) : { places: 0, lists: 0, nodes: 0 };
|
||||
const lists = typed.lists > 0 ? `, ${typed.lists} list(s)` : '';
|
||||
const painted = asset.type === 'tile' ? cellsPainted(state.project, assetId) : { tilemaps: 0, cells: 0 };
|
||||
const detail =
|
||||
asset.type === 'scene'
|
||||
? `The scene and its ${asset.objects.length} object(s) are removed.`
|
||||
: eased.keys > 0 || eased.nodes > 0
|
||||
? `${eased.keys} animation key(s) and ${eased.nodes} Lerp node(s) use it and blend linearly instead.`
|
||||
: typed.places > 0 || typed.lists > 0 || typed.nodes > 0
|
||||
? `${typed.places} struct(s)${lists} and ${typed.nodes} node(s) use it and keep its fields as their own.`
|
||||
: references > 0 || animations > 0 || tiles > 0
|
||||
? wornDetail(references, animations, tiles, asset.type)
|
||||
: usages === 0
|
||||
? 'This cannot be undone with Ctrl+Z after reload.'
|
||||
: asset.type === 'container'
|
||||
? `${usages} placed container(s) are removed together with their content.`
|
||||
: `${usages} placed instance(s) are removed as well (scenes and container templates).`;
|
||||
: painted.cells > 0
|
||||
? `${painted.cells} cell(s) on ${painted.tilemaps} tilemap(s) are painted with it and become empty.`
|
||||
: eased.keys > 0 || eased.nodes > 0
|
||||
? `${eased.keys} animation key(s) and ${eased.nodes} Lerp node(s) use it and blend linearly instead.`
|
||||
: typed.places > 0 || typed.lists > 0 || typed.nodes > 0
|
||||
? `${typed.places} struct(s)${lists} and ${typed.nodes} node(s) use it and keep its fields as their own.`
|
||||
: references > 0 || animations > 0 || tiles > 0
|
||||
? wornDetail(references, animations, tiles, asset.type)
|
||||
: usages === 0
|
||||
? 'This cannot be undone with Ctrl+Z after reload.'
|
||||
: asset.type === 'container'
|
||||
? `${usages} placed container(s) are removed together with their content.`
|
||||
: `${usages} placed instance(s) are removed as well (scenes and container templates).`;
|
||||
new Dialog(`Delete "${asset.name}"?`, detail, [
|
||||
{ label: 'Cancel' },
|
||||
{ label: 'Delete', danger: true, onClick: () => state.execute(new DeleteAssetCommand(assetId)) },
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
findLayer,
|
||||
floodFill,
|
||||
inGrid,
|
||||
presentTile,
|
||||
tilemapColumns,
|
||||
tilemapRows,
|
||||
tilemapTileSize,
|
||||
@@ -140,12 +141,12 @@ export class TilemapPainter {
|
||||
/** Fills the connected cells of one tile in a single command, so one click is one undo step. */
|
||||
private fillFrom(target: PaintTarget, layer: TilemapLayer, at: Vec2): void {
|
||||
const state = this.host.state;
|
||||
const tileId = state.tilemapPaint.tileId;
|
||||
const tileId = presentTile(state.project, state.tilemapPaint.tileId);
|
||||
if (!tileId) {
|
||||
state.setNotice('Choose a tile to paint with.');
|
||||
return;
|
||||
}
|
||||
const cells = floodFill(target.item.props, layer, at, tileId);
|
||||
const cells = floodFill(state.project, target.item.props, layer, at, tileId);
|
||||
const command = paintTiles(
|
||||
layer,
|
||||
target.asset.id,
|
||||
@@ -157,7 +158,9 @@ export class TilemapPainter {
|
||||
|
||||
private startStroke(target: PaintTarget, global: Vec2): boolean {
|
||||
const paint = this.host.state.tilemapPaint;
|
||||
const tileId = paint.tool === 'erase' ? '' : paint.tileId;
|
||||
// The tile in hand may have been deleted since it was picked; painting it would leave cells that
|
||||
// draw nothing (#268).
|
||||
const tileId = paint.tool === 'erase' ? '' : presentTile(this.host.state.project, paint.tileId);
|
||||
if (paint.tool !== 'erase' && !tileId) {
|
||||
this.host.state.setNotice('Choose a tile to paint with.');
|
||||
return false;
|
||||
|
||||
Reference in new issue
Block a user