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) };
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import {
|
||||
AddVectorShapeCommand,
|
||||
CombineVectorShapesCommand,
|
||||
DuplicateVectorShapeCommand,
|
||||
MoveVectorShapeCommand,
|
||||
RemoveVectorShapeCommand,
|
||||
RemoveVectorShapesCommand,
|
||||
@@ -387,12 +388,20 @@ function addShapeList(host: InspectorHost, asset: VectorArtAsset, selectedIds: r
|
||||
asset.id,
|
||||
asset.shapes.map((shape) => shape.id),
|
||||
selectedIds,
|
||||
(shapeId, toggle) =>
|
||||
toggle
|
||||
? host.state.toggleShape(asset.id, shapeId)
|
||||
: host.state.select(shapeSelection(asset.id, [shapeId])),
|
||||
(from, to) => void host.state.execute(new MoveVectorShapeCommand(asset.id, from, to)),
|
||||
(shapeId) => removeShape(host, asset.id, shapeId),
|
||||
{
|
||||
onPick: (shapeId, toggle) =>
|
||||
toggle
|
||||
? host.state.toggleShape(asset.id, shapeId)
|
||||
: host.state.select(shapeSelection(asset.id, [shapeId])),
|
||||
onReorder: (from, to) => void host.state.execute(new MoveVectorShapeCommand(asset.id, from, to)),
|
||||
onDuplicate: (shapeId) => {
|
||||
const command = new DuplicateVectorShapeCommand(asset.id, shapeId);
|
||||
if (host.state.execute(command)) host.state.select(shapeSelection(asset.id, [command.shapeId]));
|
||||
},
|
||||
onRemove: (shapeId) => removeShape(host, asset.id, shapeId),
|
||||
onRename: (shapeId, name) =>
|
||||
void host.state.execute(new RenameVectorShapeCommand(asset.id, shapeId, name)),
|
||||
},
|
||||
);
|
||||
built = list;
|
||||
host.body.add(list);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
import {
|
||||
AddTilemapLayerCommand,
|
||||
DuplicateTilemapLayerCommand,
|
||||
MoveTilemapLayerCommand,
|
||||
RemoveTilemapLayerCommand,
|
||||
RenameTilemapLayerCommand,
|
||||
@@ -25,11 +26,9 @@ import {
|
||||
Dropdown,
|
||||
HStack,
|
||||
Menu,
|
||||
NumberField,
|
||||
Panel,
|
||||
ScrollContainer,
|
||||
SortableList,
|
||||
TextField,
|
||||
Theme,
|
||||
ui,
|
||||
VStack,
|
||||
@@ -133,12 +132,11 @@ export interface TilemapPanelWidgets {
|
||||
tools: Map<TilemapTool, Button>;
|
||||
/** Null while the project has no tile. */
|
||||
palette: TilePalette | null;
|
||||
/** The layers, with the plus that adds one and a cross on each that removes it. */
|
||||
/**
|
||||
* The layers, with the plus that adds one. Each row names its layer (#138, a double-click edits the
|
||||
* name), holds its `z` above the map's own (#174) and duplicates or removes it.
|
||||
*/
|
||||
layers: SortableList;
|
||||
/** The name of the layer painted on (#138). */
|
||||
layerName: TextField;
|
||||
/** Its `z` above the map's own (#174). */
|
||||
layerZ: NumberField;
|
||||
/** How the other layers are shown in the scene view (#138). */
|
||||
focus: Dropdown;
|
||||
}
|
||||
@@ -258,22 +256,6 @@ function buildPanelContent(host: InspectorHost, asset: TilemapAsset): void {
|
||||
const { state } = host;
|
||||
const paint = state.tilemapPaint;
|
||||
const painted = paintedLayerId(state, asset);
|
||||
const paintedLayer = findLayer(asset, painted);
|
||||
const paintedName = paintedLayer?.name ?? '';
|
||||
const layerName: TextField = new TextField(paintedName, {
|
||||
onChange: (name) => {
|
||||
// The command rejects a blank name but cannot put the old text back into the field,
|
||||
// so an empty one is not sent at all.
|
||||
if (name.trim()) state.execute(new RenameTilemapLayerCommand(asset.id, painted, name));
|
||||
else layerName.setValue(paintedName);
|
||||
},
|
||||
});
|
||||
const layerZField = new NumberField(paintedLayer ? layerZ(paintedLayer) : 0, {
|
||||
onChange: (z) => {
|
||||
if (paintedLayer && z !== layerZ(paintedLayer))
|
||||
state.execute(new SetTilemapLayerZCommand(asset.id, painted, z));
|
||||
},
|
||||
});
|
||||
const focus: Dropdown = new Dropdown(FOCUS.find((f) => f.focus === paint.focus)?.label ?? '', {
|
||||
onClick: () =>
|
||||
new Menu(
|
||||
@@ -297,10 +279,23 @@ function buildPanelContent(host: InspectorHost, asset: TilemapAsset): void {
|
||||
new SetTilemapLayerHiddenCommand(asset.id, layerId, isLayerVisible(findLayer(asset, layerId)!)),
|
||||
),
|
||||
onAdd: () => state.execute(new AddTilemapLayerCommand(asset.id)),
|
||||
onDuplicate: (layerId) => {
|
||||
const command = new DuplicateTilemapLayerCommand(asset.id, layerId);
|
||||
if (state.execute(command)) state.setTilemapPaint({ layerId: command.layerId });
|
||||
},
|
||||
onRemove: (layerId) => state.execute(new RemoveTilemapLayerCommand(asset.id, layerId)),
|
||||
onRename: (layerId, name) => state.execute(new RenameTilemapLayerCommand(asset.id, layerId, name)),
|
||||
value: {
|
||||
get: (layerId) => {
|
||||
const layer = findLayer(asset, layerId);
|
||||
return layer ? layerZ(layer) : null;
|
||||
},
|
||||
set: (layerId, z) => {
|
||||
const layer = findLayer(asset, layerId);
|
||||
if (layer && z !== layerZ(layer)) state.execute(new SetTilemapLayerZCommand(asset.id, layerId, z));
|
||||
},
|
||||
},
|
||||
}),
|
||||
layerName,
|
||||
layerZ: layerZField,
|
||||
focus,
|
||||
};
|
||||
built = widgets;
|
||||
@@ -342,16 +337,8 @@ function buildPanelContent(host: InspectorHost, asset: TilemapAsset): void {
|
||||
[painted],
|
||||
);
|
||||
host.body.add(widgets.layers);
|
||||
addNote(host, 'Drag a row to draw it earlier or later.', 'small');
|
||||
// Below the list rather than in it: a press on a row picks the layer, and a text field in its
|
||||
// place would take the click that chooses it.
|
||||
const nameRow = labelledRow('Name').row;
|
||||
nameRow.add(layerName, 1);
|
||||
host.body.add(nameRow);
|
||||
const zRow = labelledRow('Z').row;
|
||||
zRow.add(layerZField, 1);
|
||||
host.body.add(zRow);
|
||||
addNote(host, 'Added to the map’s own Z, as a child’s is.', 'small');
|
||||
addNote(host, 'Drag a row to reorder, double-click a name to rename.', 'small');
|
||||
addNote(host, 'The number is the layer’s Z, added to the map’s.', 'small');
|
||||
const showRow = labelledRow('Show').row;
|
||||
showRow.add(focus, 1);
|
||||
host.body.add(showRow);
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
addFrame,
|
||||
addLayer,
|
||||
addPaletteColor,
|
||||
duplicateLayer,
|
||||
mergeLayerDown,
|
||||
moveFrame,
|
||||
moveLayer,
|
||||
@@ -69,12 +70,11 @@ export class PixelArtSide extends ScrollContainer {
|
||||
readonly removeColorButton = new Button('Remove last');
|
||||
readonly createPaletteButton = new Button('Create palette');
|
||||
private readonly paletteNote = new Label('', { style: 'muted' });
|
||||
/** Each row names its layer (a double-click edits it), holds its opacity in percent and duplicates it. */
|
||||
readonly layers: SortableList;
|
||||
readonly layerName = new TextField('');
|
||||
readonly layerOpacity = new NumberField(1, { min: 0, max: 1, step: 0.05, decimals: 2 });
|
||||
readonly layerButtons: Record<'merge', Button>;
|
||||
/** Each row duplicates its frame right after it. */
|
||||
readonly frames: SortableList;
|
||||
readonly frameButtons: Record<'duplicate', Button>;
|
||||
private readonly thumbs = new PixelThumbs();
|
||||
private readonly stopCels: () => void;
|
||||
readonly onionBox = new Checkbox(false);
|
||||
@@ -137,16 +137,25 @@ export class PixelArtSide extends ScrollContainer {
|
||||
this.withAsset((a) => a.layers[from] && moveLayer(state, a, a.layers[from].id, to)),
|
||||
onToggleHidden: (id) => this.toggleHidden(id),
|
||||
onAdd: () => this.addLayer(),
|
||||
onDuplicate: (id) => this.duplicateLayer(id),
|
||||
onRemove: (id) => this.withAsset((a) => removeLayer(state, a, id)),
|
||||
onRename: (id, name) => this.withAsset((a) => renameLayer(state, a, id, name)),
|
||||
value: {
|
||||
get: (id) => {
|
||||
const layer = this.canvas.asset()?.layers.find((l) => l.id === id);
|
||||
return layer ? Math.round(layer.opacity * 100) : null;
|
||||
},
|
||||
preview: (id, percent) => this.previewOpacity(id, percent / 100),
|
||||
set: (id, percent) => this.commitOpacity(id, percent / 100),
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
decimals: 0,
|
||||
suffix: '%',
|
||||
},
|
||||
thumb: (id, art, size) => this.withAsset((a) => this.paintThumb(a, art, size, id)),
|
||||
});
|
||||
c.add(this.layers);
|
||||
this.layerName.onChange = (name) =>
|
||||
this.withAsset((asset) => renameLayer(state, asset, this.canvas.layerId, name));
|
||||
this.layerOpacity.onNumberChange = (value) =>
|
||||
this.withAsset((asset) => setLayerOpacity(state, asset, this.canvas.layerId, value));
|
||||
c.add(row(new Label('Name', { style: 'muted' }), this.layerName));
|
||||
c.add(row(new Label('Opacity', { style: 'muted' }), this.layerOpacity));
|
||||
this.layerButtons = {
|
||||
merge: new Button('Merge down', {
|
||||
onClick: () => this.withAsset((a) => mergeLayerDown(state, a, this.canvas.layerId)),
|
||||
@@ -160,15 +169,12 @@ export class PixelArtSide extends ScrollContainer {
|
||||
onPick: (id) => this.canvas.selectFrame(id),
|
||||
onMove: (from, to) =>
|
||||
this.withAsset((a) => a.frames[from] && moveFrame(state, a, a.frames[from].id, to)),
|
||||
onAdd: () => this.addFrame(false),
|
||||
onAdd: () => this.addFrame(this.canvas.frameId, false),
|
||||
onDuplicate: (id) => this.addFrame(id, true),
|
||||
onRemove: (id) => this.withAsset((a) => removeFrame(state, a, id)),
|
||||
thumb: (id, art, size) => this.withAsset((a) => this.paintThumb(a, art, size, undefined, id)),
|
||||
});
|
||||
c.add(this.frames);
|
||||
this.frameButtons = {
|
||||
duplicate: new Button('Duplicate', { onClick: () => this.addFrame(true) }),
|
||||
};
|
||||
c.add(row(this.frameButtons.duplicate));
|
||||
|
||||
c.add(heading('View'));
|
||||
this.onionBox.onChange = (on) => {
|
||||
@@ -296,6 +302,36 @@ export class PixelArtSide extends ScrollContainer {
|
||||
if (id) this.canvas.selectLayer(id);
|
||||
}
|
||||
|
||||
private duplicateLayer(layerId: string): void {
|
||||
const asset = this.canvas.asset();
|
||||
if (!asset) return;
|
||||
const id = duplicateLayer(this.state, asset, layerId);
|
||||
if (id) this.canvas.selectLayer(id);
|
||||
}
|
||||
|
||||
/** The opacity a scrub started from, per layer – what the commit puts back first, as with a colour. */
|
||||
private readonly opacityBefore = new Map<string, number>();
|
||||
|
||||
/** The opacity under the scrub straight into the layer, so the canvas shows it before it is an undo step. */
|
||||
private previewOpacity(layerId: string, opacity: number): void {
|
||||
const layer = this.canvas.asset()?.layers.find((l) => l.id === layerId);
|
||||
if (!layer) return;
|
||||
if (!this.opacityBefore.has(layerId)) this.opacityBefore.set(layerId, layer.opacity);
|
||||
layer.opacity = opacity;
|
||||
this.state.preview();
|
||||
}
|
||||
|
||||
private commitOpacity(layerId: string, opacity: number): void {
|
||||
const asset = this.canvas.asset();
|
||||
const layer = asset?.layers.find((l) => l.id === layerId);
|
||||
const before = this.opacityBefore.get(layerId);
|
||||
this.opacityBefore.delete(layerId);
|
||||
if (!asset || !layer) return;
|
||||
if (before !== undefined) layer.opacity = before;
|
||||
if (opacity !== layer.opacity) setLayerOpacity(this.state, asset, layerId, opacity);
|
||||
else if (before !== undefined) this.state.preview();
|
||||
}
|
||||
|
||||
private toggleHidden(layerId: string): void {
|
||||
const asset = this.canvas.asset();
|
||||
const layer = asset?.layers.find((l) => l.id === layerId);
|
||||
@@ -314,10 +350,11 @@ export class PixelArtSide extends ScrollContainer {
|
||||
if (frame) this.thumbs.paint(art, size, asset, paletteColors(this.state.project, asset), frame, layerId);
|
||||
}
|
||||
|
||||
private addFrame(copy: boolean): void {
|
||||
/** A frame after `afterId`: empty, or a copy of it. */
|
||||
private addFrame(afterId: string, copy: boolean): void {
|
||||
const asset = this.canvas.asset();
|
||||
if (!asset) return;
|
||||
const id = addFrame(this.state, asset, this.canvas.frameId, copy);
|
||||
const id = addFrame(this.state, asset, afterId, copy);
|
||||
if (id) this.canvas.selectFrame(id);
|
||||
}
|
||||
|
||||
@@ -341,18 +378,10 @@ export class PixelArtSide extends ScrollContainer {
|
||||
|
||||
const layerId = this.canvas.layerId;
|
||||
this.layers.setItems(
|
||||
(asset?.layers ?? []).map((layer) => ({
|
||||
id: layer.id,
|
||||
label: layer.name,
|
||||
hint: layer.opacity < 1 ? `${Math.round(layer.opacity * 100)}%` : undefined,
|
||||
hidden: layer.hidden,
|
||||
})),
|
||||
(asset?.layers ?? []).map((layer) => ({ id: layer.id, label: layer.name, hidden: layer.hidden })),
|
||||
layerId ? [layerId] : [],
|
||||
);
|
||||
const layer = asset?.layers.find((l) => l.id === layerId);
|
||||
this.layerName.setValue(layer?.name ?? '');
|
||||
this.layerOpacity.setNumber(layer?.opacity ?? 1);
|
||||
const index = asset && layer ? asset.layers.indexOf(layer) : -1;
|
||||
const index = asset ? asset.layers.findIndex((l) => l.id === layerId) : -1;
|
||||
this.layerButtons.merge.setEnabled(index > 0);
|
||||
|
||||
const frameId = this.canvas.frameId;
|
||||
@@ -360,8 +389,6 @@ export class PixelArtSide extends ScrollContainer {
|
||||
(asset?.frames ?? []).map((frame, i) => ({ id: frame.id, label: frameLabel(i) })),
|
||||
frameId ? [frameId] : [],
|
||||
);
|
||||
const frameIndex = asset ? asset.frames.findIndex((f) => f.id === frameId) : -1;
|
||||
this.frameButtons.duplicate.setEnabled(frameIndex >= 0);
|
||||
this.relayoutContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
withFrameRemoved,
|
||||
withLayerAdded,
|
||||
withLayerChanged,
|
||||
withLayerDuplicated,
|
||||
withLayerMergedDown,
|
||||
withLayerMoved,
|
||||
withLayerRemoved,
|
||||
@@ -52,6 +53,12 @@ export function addLayer(state: EditorState, asset: PixelArtAsset, aboveId: stri
|
||||
return edit(state, asset, 'Add layer', (s) => withLayerAdded(s, id, aboveId)) ? id : '';
|
||||
}
|
||||
|
||||
/** A copy of the layer right above it; its id, so the tab can go on painting on the copy. */
|
||||
export function duplicateLayer(state: EditorState, asset: PixelArtAsset, layerId: string): string {
|
||||
const id = newId();
|
||||
return edit(state, asset, 'Duplicate layer', (s) => withLayerDuplicated(s, id, layerId)) ? id : '';
|
||||
}
|
||||
|
||||
export function removeLayer(state: EditorState, asset: PixelArtAsset, layerId: string): boolean {
|
||||
return edit(state, asset, 'Delete layer', (s) => withLayerRemoved(s, layerId));
|
||||
}
|
||||
|
||||
@@ -27,25 +27,32 @@ export class ShapeList extends SortableList {
|
||||
|
||||
/**
|
||||
* `selectedIds` are the rows marked. `onPick` is told the row pressed and whether it was pressed with
|
||||
* Ctrl or Shift, which adds the shape to the selection or takes it out (#187). `onRemove` is the
|
||||
* cross on a row. There is no plus: a shape is drawn with the Pen, and "Add shape" stays below.
|
||||
* Ctrl or Shift, which adds the shape to the selection or takes it out (#187). `onDuplicate` and
|
||||
* `onRemove` are the marks at the end of a row, `onRename` a name edited in its row. There is no
|
||||
* plus: a shape is drawn with the Pen, and "Add shape" stays below.
|
||||
*/
|
||||
constructor(
|
||||
private readonly state: EditorState,
|
||||
private readonly assetId: string,
|
||||
shapeIds: readonly string[],
|
||||
selectedIds: readonly string[],
|
||||
onPick: (shapeId: string, toggle: boolean) => void,
|
||||
onReorder: (from: number, to: number) => void,
|
||||
onRemove: (shapeId: string) => void,
|
||||
rows: {
|
||||
onPick: (shapeId: string, toggle: boolean) => void;
|
||||
onReorder: (from: number, to: number) => void;
|
||||
onDuplicate: (shapeId: string) => void;
|
||||
onRemove: (shapeId: string) => void;
|
||||
onRename: (shapeId: string, name: string) => void;
|
||||
},
|
||||
) {
|
||||
super({
|
||||
title: 'Shapes',
|
||||
reversed: true,
|
||||
multiSelect: true,
|
||||
onPick,
|
||||
onMove: onReorder,
|
||||
onRemove,
|
||||
onPick: rows.onPick,
|
||||
onMove: rows.onReorder,
|
||||
onDuplicate: rows.onDuplicate,
|
||||
onRemove: rows.onRemove,
|
||||
onRename: rows.onRename,
|
||||
thumb: (id, art, size) => this.paintSwatch(id, art, size),
|
||||
});
|
||||
const asset = this.asset();
|
||||
|
||||
Reference in new issue
Block a user