A placed tilemap grows and shrinks at every edge: the new GRID EDGES block in the inspector adds or removes one column or row at Left, Top, Right or Bottom as one undo step, shifting the cells of all layers and moving the placement through its own matrix so the content stays in place on screen; removed painted cells are kept outside the grid and return when the edge grows again, and the Columns and Rows fields of a placement keep the content in place too (#136)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b32d3cbf3
commit
4728f4fab5
9 files changed
+588
-11
No files matched your search
@@ -6,6 +6,9 @@ import {
|
||||
RevertPositionToTemplateCommand,
|
||||
} from '../../core/commands/applyCommands';
|
||||
import { UnpackContainerCommand } from '../../core/commands/containerCommands';
|
||||
import { ResizeTilemapEdgeCommand } from '../../core/commands/tilemapCommands';
|
||||
import { TILEMAP_COLUMNS_KEY, TILEMAP_EDGES, TILEMAP_ROWS_KEY } from '../../core/model/tilemap';
|
||||
import { findUsages } from '../../core/model/usages';
|
||||
import {
|
||||
ClearOverrideCommand,
|
||||
MoveSceneObjectCommand,
|
||||
@@ -38,7 +41,7 @@ import {
|
||||
deviatesFromTemplate,
|
||||
resolveSceneObjectProps,
|
||||
} from '../../core/properties/resolve';
|
||||
import { Button } from '../../ui';
|
||||
import { Button, Theme } from '../../ui';
|
||||
import { createContainer, removeObjects } from '../actions';
|
||||
import { aspectActions, sizeCommand } from './aspectAction';
|
||||
import { defSignature } from './assetSection';
|
||||
@@ -47,7 +50,7 @@ import { addGap, addNameField, addNote, addTitle, type InspectorHost } from './h
|
||||
import type { EditorState, ListSlot } from '../state';
|
||||
import { parentRow } from './parentRow';
|
||||
import { listOpener, previewingHandlers, structFields } from './previewHandlers';
|
||||
import { overrideValue } from './valueSpecs';
|
||||
import { overrideValue, tilemapSizeValue } from './valueSpecs';
|
||||
import { buildVariable } from './variableRows';
|
||||
import { currentProps, currentRef, deviations, positionMoved, targetOf, viewOf } from './objectQueries';
|
||||
import { componentRow, OFFSET_AXES, offsetKey, POSITION_AXES } from './vectorRows';
|
||||
@@ -82,7 +85,9 @@ export function objectSignature(
|
||||
? 'instance'
|
||||
: '';
|
||||
const rows = schemaForObject(project, asset, object);
|
||||
return `${object.parentId}|${kind}|` + rows.map(defSignature).join(',');
|
||||
// A tilemap warns when it is placed more than once, so the number of placements shapes it too.
|
||||
const placed = asset.type === 'tilemap' ? `|placed:${findUsages(project, asset.id).length}` : '';
|
||||
return `${object.parentId}|${kind}${placed}|` + rows.map(defSignature).join(',');
|
||||
}
|
||||
|
||||
/** Several objects: what they are, and the actions that apply to all of them at once. */
|
||||
@@ -204,9 +209,36 @@ export function buildObjectSection(host: InspectorHost, holderId: string, object
|
||||
for (const def of own) propertyRow(host, holderId, objectId, asset, def, values, asset.type === 'behavior');
|
||||
markObjectRows(host, object, asset, target);
|
||||
|
||||
if (asset.type === 'tilemap') gridEdgesSection(host, holderId, object);
|
||||
if (asset.type === 'container') contentSection(host, holderId, object, asset.objects.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grows and shrinks the grid at any edge, one column or row per click, while the painted cells stay
|
||||
* where they are in the scene (#136). The cells belong to the asset, so they move in every other
|
||||
* placement too – which only this one's position is corrected for, hence the note.
|
||||
*/
|
||||
function gridEdgesSection(host: InspectorHost, holderId: string, object: SceneObject): void {
|
||||
addTitle(host, 'GRID EDGES');
|
||||
for (const edge of TILEMAP_EDGES) {
|
||||
const { row } = labelledRow(edge[0]!.toUpperCase() + edge.slice(1));
|
||||
for (const count of [-1, 1])
|
||||
row.add(
|
||||
new Button(count < 0 ? '−' : '+', {
|
||||
variant: 'ghost',
|
||||
minWidth: Theme.controlHeight,
|
||||
onClick: () => host.state.execute(new ResizeTilemapEdgeCommand(holderId, object.id, edge, count)),
|
||||
}),
|
||||
);
|
||||
host.body.add(row);
|
||||
}
|
||||
const others = findUsages(host.state.project, object.assetId).length - 1;
|
||||
if (others > 0) {
|
||||
addNote(host, `Placed ${others} more time${others === 1 ? '' : 's'}: at the left and top`, 'warning');
|
||||
addNote(host, 'edge the cells move there as well.', 'warning');
|
||||
} else addNote(host, 'The painted cells stay where they are in the scene.', 'small');
|
||||
}
|
||||
|
||||
function assetLink(host: InspectorHost, label: string, asset: Asset): void {
|
||||
const { row } = labelledRow(label);
|
||||
row.add(
|
||||
@@ -284,7 +316,10 @@ function propertyRow(
|
||||
variable = false,
|
||||
): FieldRow {
|
||||
const bases = () => deviations(host.state, holderId, objectId, def.key);
|
||||
const overrideSpec = overrideValue(host.state, holderId, objectId, def.key);
|
||||
const overrideSpec =
|
||||
owner.type === 'tilemap' && (def.key === TILEMAP_COLUMNS_KEY || def.key === TILEMAP_ROWS_KEY)
|
||||
? tilemapSizeValue(host.state, holderId, objectId, def.key)
|
||||
: overrideValue(host.state, holderId, objectId, def.key);
|
||||
const fieldSlot = (field: string): ListSlot => ({
|
||||
kind: 'object',
|
||||
holderId,
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { SetAssetPropertyCommand } from '../../core/commands/assetCommands';
|
||||
import { SetNodeParamCommand } from '../../core/commands/graphCommands';
|
||||
import { SetOverrideCommand } from '../../core/commands/sceneObjectCommands';
|
||||
import { heldPosition, placedGridSize, ResizeTilemapEdgeCommand } from '../../core/commands/tilemapCommands';
|
||||
import { findNode, type Graph, type GraphNode } from '../../core/graph/Graph';
|
||||
import type { Asset } from '../../core/model/Asset';
|
||||
import type { ObjectRef } from '../../core/model/instance';
|
||||
import type { Position } from '../../core/model/Properties';
|
||||
import { findAsset, findGraphAsset, type Project } from '../../core/model/Project';
|
||||
import { TILEMAP_COLUMNS_KEY, type TilemapEdge, tilemapTileSize } from '../../core/model/tilemap';
|
||||
import { coerceValue, schemaFor } from '../../core/properties/PropertySchema';
|
||||
import { resolveSceneObjectProps } from '../../core/properties/resolve';
|
||||
import type { EditorState } from '../state';
|
||||
import { currentProps, currentRef } from './objectQueries';
|
||||
import type { PreviewedValue } from './previewHandlers';
|
||||
@@ -36,6 +41,57 @@ export function overrideValue(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Columns or Rows of a placed tilemap (#136): the grid grows and shrinks at the right or bottom edge
|
||||
* while the painted cells stay where they are in the scene, so the origin moves along. The preview
|
||||
* works from where the edit started rather than step by step – dragging the value back and forth
|
||||
* would otherwise leave rounding drift behind, and restoring it before the commit would not be exact.
|
||||
*/
|
||||
export function tilemapSizeValue(
|
||||
state: EditorState,
|
||||
holderId: string,
|
||||
objectId: string,
|
||||
key: string,
|
||||
): PreviewedValue<ObjectRef> {
|
||||
const plain = overrideValue(state, holderId, objectId, key);
|
||||
const edge: TilemapEdge = key === TILEMAP_COLUMNS_KEY ? 'right' : 'bottom';
|
||||
let start: { position: Position; size: number } | undefined;
|
||||
// Writes the value, then puts the origin where the size now asks for, counted from the start.
|
||||
const holding = (ref: ObjectRef, write: () => void): void => {
|
||||
const asset = findAsset(state.project, ref.object.assetId);
|
||||
if (!asset) return write();
|
||||
start ??= {
|
||||
position: { ...ref.object.position },
|
||||
size: placedGridSize(state.project, asset, ref.object, key),
|
||||
};
|
||||
write();
|
||||
const props = resolveSceneObjectProps(state.project, asset, ref.object);
|
||||
const size = placedGridSize(state.project, asset, ref.object, key);
|
||||
ref.setPosition(
|
||||
heldPosition(asset, ref.object, start.position, edge, size - start.size, tilemapTileSize(props)),
|
||||
);
|
||||
};
|
||||
return {
|
||||
...plain,
|
||||
write: (ref, value) => holding(ref, () => ref.setOverride(key, value)),
|
||||
remove: (ref) => holding(ref, () => ref.setOverride(key, undefined)),
|
||||
commit: (value) => {
|
||||
start = undefined;
|
||||
const ref = currentRef(state, holderId, objectId);
|
||||
const asset = ref && findAsset(state.project, ref.object.assetId);
|
||||
const def = asset && schemaFor('tilemap').find((d) => d.key === key);
|
||||
if (!ref || !asset || !def) return plain.commit(value);
|
||||
const next = Number(coerceValue(def, value));
|
||||
return new ResizeTilemapEdgeCommand(
|
||||
holderId,
|
||||
objectId,
|
||||
edge,
|
||||
next - placedGridSize(state.project, asset, ref.object, key),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function nodeParamValue(
|
||||
state: EditorState,
|
||||
assetId: string,
|
||||
|
||||
Reference in new issue
Block a user