Lists duplicate, rename and edit values right in their rows
Name, opacity and Z each sat in a separate field below or beside
the list. You had to select a row first and then look elsewhere to
change it. Duplicating was a separate button or not possible at all.
Now the row itself carries everything.
- SortableList gets three optional callbacks. onDuplicate shows a
copy button between the eye and the delete button. onRename opens
a text field on double click, Enter or a click outside commits,
Esc cancels, an empty name is ignored. value puts a NumberField
into the row.
- Dragging on the number changes the value and never moves the row.
A whole drag is one undo step, the canvas follows live.
- Copy and delete only show on the hovered and the selected row.
Their space stays reserved, so nothing jumps on hover.
- Pixel layers: copy right above with the same pixels on all
frames, rename, opacity in percent. The Name and Opacity fields
below the list are gone.
- Pixel frames: copy right after, replaces the Duplicate button.
- Tilemap layers: copy with cells, Z and visibility, rename, Z in
the row. The Name and Z fields in the panel are gone.
- Vector shapes: copy at the same place and select it, rename.
- New in core: DuplicateVectorShapeCommand,
DuplicateTilemapLayerCommand, withLayerDuplicated and copyName
("X copy", "X copy 2"). copyName lives in src/core/util/names.ts
because the model never imports from commands/.
Findings:
- Double clicking a shape that is not selected yet only selects it.
The first click moves the list up, so the second click misses the
name. Module 47 selects the row first.
Tests:
- ./check.sh: 138 files, 2055 tests, CHECK EXIT 0 (before 2052).
- GM_STEPS=47,48 (fresh, twice): ALL PASSED, 91 checks (before 86).
- GM_STEPS=39,40,47,48: module 40 green, 47 died on the rename
check, which was a test fault and is fixed.
- Counter probes: a constant copyName fails the shape and pixel
layer tests. A dead copy hit fails the copy check in 47.
- Check total 1272 in README and wiki, computed (1266 + 6).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ae0247d86f
commit
bbbf34109b
19 files changed
+747
-153
No files matched your search
+54
-16
@@ -502,8 +502,6 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
addColor: side.addColorButton,
|
||||
removeColor: side.removeColorButton,
|
||||
createPalette: side.createPaletteButton,
|
||||
layerName: side.layerName,
|
||||
layerOpacity: side.layerOpacity,
|
||||
onion: side.onionBox,
|
||||
tiles: side.tilesBox,
|
||||
fps: side.fpsField,
|
||||
@@ -512,7 +510,6 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
resize: editor.pixelArtEditor.resizeButton,
|
||||
...Object.fromEntries([...side.toolButtons].map(([tool, button]) => [tool, button])),
|
||||
...Object.fromEntries(Object.entries(side.layerButtons).map(([k, b]) => [`layer:${k}`, b])),
|
||||
...Object.fromEntries(Object.entries(side.frameButtons).map(([k, b]) => [`frame:${k}`, b])),
|
||||
};
|
||||
// The plus in a list's header is no widget of its own.
|
||||
const list = name === 'layer:add' ? side.layers : name === 'frame:add' ? side.frames : null;
|
||||
@@ -537,19 +534,29 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
},
|
||||
/**
|
||||
* The centre of a layer's or frame's row, scrolled into view, with the centre of its picture;
|
||||
* `eye` is the layer's switch, `remove` the cross (null on the last one).
|
||||
* `eye` is the layer's switch, `duplicate` and `remove` the marks at the end (no cross on the
|
||||
* last one), `name` where a double-click edits the name, `opacity` the field with its text and
|
||||
* `renaming` the field a name is being edited in.
|
||||
*/
|
||||
layerRow: (layerId: string) => {
|
||||
const side = editor.pixelArtEditor.side;
|
||||
side.reveal(side.layers);
|
||||
const row = side.layers.rowGlobal(layerId);
|
||||
return row
|
||||
const opacity = side.layers.valueField(layerId);
|
||||
const renaming = side.layers.renameField;
|
||||
// On the name: the middle of the row may be the opacity field, which a press would scrub.
|
||||
const name = side.layers.nameGlobal(layerId);
|
||||
return row && name
|
||||
? {
|
||||
x: row.x + row.w / 2,
|
||||
y: row.y + row.h / 2,
|
||||
x: name.x,
|
||||
y: name.y,
|
||||
thumb: side.layers.thumbGlobal(layerId),
|
||||
name,
|
||||
eye: side.layers.eyeGlobal(layerId),
|
||||
duplicate: side.layers.duplicateGlobal(layerId),
|
||||
remove: side.layers.removeGlobal(layerId),
|
||||
opacity: opacity ? { ...centreOf(opacity), value: opacity.value } : null,
|
||||
renaming: renaming ? { ...centreOf(renaming), value: renaming.value } : null,
|
||||
}
|
||||
: null;
|
||||
},
|
||||
@@ -562,6 +569,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
x: row.x + row.w / 2,
|
||||
y: row.y + row.h / 2,
|
||||
thumb: side.frames.thumbGlobal(frameId),
|
||||
duplicate: side.frames.duplicateGlobal(frameId),
|
||||
remove: side.frames.removeGlobal(frameId),
|
||||
}
|
||||
: null;
|
||||
@@ -662,6 +670,14 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
shapeRowAt: (shapeId: string) => currentShapeList()?.rowGlobal(shapeId) ?? null,
|
||||
/** The cross on a shape's row. */
|
||||
shapeRemoveAt: (shapeId: string) => currentShapeList()?.removeGlobal(shapeId) ?? null,
|
||||
/** The duplicate mark on a shape's row, and where a double-click edits its name. */
|
||||
shapeDuplicateAt: (shapeId: string) => currentShapeList()?.duplicateGlobal(shapeId) ?? null,
|
||||
shapeNameAt: (shapeId: string) => currentShapeList()?.nameGlobal(shapeId) ?? null,
|
||||
/** The field a shape's name is being edited in, with what it holds; null while none is. */
|
||||
shapeRenaming: () => {
|
||||
const field = currentShapeList()?.renameField;
|
||||
return field ? { ...centreOf(field), value: field.value } : null;
|
||||
},
|
||||
/** The Paint tool (#186): its button, the "Handles" menu, and which part it shows. */
|
||||
paintButton: () => centreOf(vectorArtEditor.paintButton),
|
||||
paintTarget: () => ({
|
||||
@@ -784,15 +800,37 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
},
|
||||
/** The plus in the layer list's header. */
|
||||
addLayerAt: () => currentTilemapWidgets()?.layers.addGlobal() ?? null,
|
||||
/** The name field of the painted layer and the Show dropdown, with what they hold (#138). */
|
||||
/**
|
||||
* Where the painted layer's name stands in its row – a double-click there edits it (#138) –
|
||||
* with the name the row shows and the field it is being edited in, while it is.
|
||||
*/
|
||||
layerNameAt: () => {
|
||||
const field = currentTilemapWidgets()?.layerName;
|
||||
const widgets = currentTilemapWidgets();
|
||||
const target = paintedTilemap(state);
|
||||
if (!widgets || !target) return null;
|
||||
const layerId = paintedLayerId(state, target.asset);
|
||||
const at = widgets.layers.nameGlobal(layerId);
|
||||
const editing = widgets.layers.renameField;
|
||||
return at
|
||||
? {
|
||||
...at,
|
||||
value: findLayer(target.asset, layerId)?.name ?? '',
|
||||
editing: editing ? { ...centreOf(editing), value: editing.value } : null,
|
||||
}
|
||||
: null;
|
||||
},
|
||||
/** The Z field in the painted layer's row, with what it holds (#174). */
|
||||
layerZAt: () => {
|
||||
const widgets = currentTilemapWidgets();
|
||||
const target = paintedTilemap(state);
|
||||
const field = widgets && target && widgets.layers.valueField(paintedLayerId(state, target.asset));
|
||||
return field ? { ...centreOf(field), value: field.value } : null;
|
||||
},
|
||||
/** The Z field of the painted layer, with what it holds (#174). */
|
||||
layerZAt: () => {
|
||||
const field = currentTilemapWidgets()?.layerZ;
|
||||
return field ? { ...centreOf(field), value: field.value } : null;
|
||||
/** The duplicate mark on the painted layer's row. */
|
||||
duplicateLayerAt: () => {
|
||||
const widgets = currentTilemapWidgets();
|
||||
const target = paintedTilemap(state);
|
||||
return widgets && target ? widgets.layers.duplicateGlobal(paintedLayerId(state, target.asset)) : null;
|
||||
},
|
||||
focusAt: () => {
|
||||
const button = currentTilemapWidgets()?.focus;
|
||||
@@ -802,9 +840,9 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
layerRowAt: (index: number) => {
|
||||
const list = currentTilemapWidgets()?.layers;
|
||||
const id = list?.shownIds[index];
|
||||
const row = id ? list.rowGlobal(id) : null;
|
||||
if (!list || !id || !row) return null;
|
||||
const name = { x: row.x + row.w / 2, y: row.y + row.h / 2 };
|
||||
// On the name: the middle of the row may be the Z field, which a press would scrub.
|
||||
const name = id ? list.nameGlobal(id) : null;
|
||||
if (!list || !id || !name) return null;
|
||||
// `y` is the row's own line, so a check can tell whether it is inside the panel.
|
||||
return { id, y: name.y, name, visible: list.eyeGlobal(id) };
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user