layer history and finally layer delete/merge

Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
This commit is contained in:
Victor Seiji Hariki 2022-12-05 23:22:19 -03:00
parent 0cc6f7660a
commit 62ddc38f01
6 changed files with 361 additions and 34 deletions

View file

@ -13,6 +13,11 @@
mask-image: url("/res/icons/file-plus.svg"); mask-image: url("/res/icons/file-plus.svg");
} }
.ui.icon > .icon-file-x {
-webkit-mask-image: url("/res/icons/file-x.svg");
mask-image: url("/res/icons/file-x.svg");
}
.ui.icon > .icon-chevron-down { .ui.icon > .icon-chevron-down {
-webkit-mask-image: url("/res/icons/chevron-down.svg"); -webkit-mask-image: url("/res/icons/chevron-down.svg");
mask-image: url("/res/icons/chevron-down.svg"); mask-image: url("/res/icons/chevron-down.svg");
@ -22,3 +27,13 @@
-webkit-mask-image: url("/res/icons/chevron-up.svg"); -webkit-mask-image: url("/res/icons/chevron-up.svg");
mask-image: url("/res/icons/chevron-up.svg"); mask-image: url("/res/icons/chevron-up.svg");
} }
.ui.icon > .icon-chevron-first {
-webkit-mask-image: url("/res/icons/chevron-first.svg");
mask-image: url("/res/icons/chevron-first.svg");
}
.ui.icon > .icon-chevron-flat-down {
-webkit-mask-image: url("/res/icons/chevron-first.svg");
mask-image: url("/res/icons/chevron-first.svg");
transform: rotate(-90deg);
}

View file

@ -216,7 +216,7 @@
<button <button
type="button" type="button"
title="Add Layer" title="Add Layer"
onclick="uil.addLayer(null, 'New Layer')" onclick="commands.runCommand('addLayer', 'Added Layer')"
class="ui icon button"> class="ui icon button">
<div class="icon-file-plus"></div> <div class="icon-file-plus"></div>
</button> </button>
@ -224,7 +224,7 @@
<button <button
type="button" type="button"
title="Move Layer Up" title="Move Layer Up"
onclick="uil.moveLayerUp()" onclick="commands.runCommand('moveLayer', 'Moved Layer Up',{delta: 1})"
class="ui icon button"> class="ui icon button">
<div class="icon-chevron-up"></div> <div class="icon-chevron-up"></div>
</button> </button>
@ -232,10 +232,26 @@
<button <button
type="button" type="button"
title="Move Layer Down" title="Move Layer Down"
onclick="uil.moveLayerDown()" onclick="commands.runCommand('moveLayer', 'Moved Layer Down', {delta: -1})"
class="ui icon button"> class="ui icon button">
<div class="icon-chevron-down"></div> <div class="icon-chevron-down"></div>
</button> </button>
<button
type="button"
title="Merge Layer Down"
onclick="commands.runCommand('mergeLayer', 'Merged Layer Down')"
class="ui icon button">
<div class="icon-chevron-flat-down"></div>
</button>
<button
type="button"
title="Move Layer Down"
onclick="commands.runCommand('deleteLayer', 'Deleted Layer')"
class="ui icon button">
<div class="icon-file-x"></div>
</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -4,9 +4,6 @@
const _commands_events = new Observer(); const _commands_events = new Observer();
/** CommandNonExistentError */
class CommandNonExistentError extends Error {}
/** Global Commands Object */ /** Global Commands Object */
const commands = makeReadOnly( const commands = makeReadOnly(
{ {
@ -32,7 +29,14 @@ const commands = makeReadOnly(
*/ */
async undo(n = 1) { async undo(n = 1) {
for (var i = 0; i < n && this.current > -1; i++) { for (var i = 0; i < n && this.current > -1; i++) {
try {
await this._history[this._current--].undo(); await this._history[this._current--].undo();
} catch (e) {
console.warn("[commands] Failed to undo command");
console.warn(e);
this._current++;
break;
}
} }
}, },
/** /**
@ -42,7 +46,14 @@ const commands = makeReadOnly(
*/ */
async redo(n = 1) { async redo(n = 1) {
for (var i = 0; i < n && this.current + 1 < this._history.length; i++) { for (var i = 0; i < n && this.current + 1 < this._history.length; i++) {
try {
await this._history[++this._current].redo(); await this._history[++this._current].redo();
} catch {
console.warn("[commands] Failed to redo command");
console.warn(e);
this._current--;
break;
}
} }
}, },
@ -67,7 +78,7 @@ const commands = makeReadOnly(
* @returns {Command} * @returns {Command}
*/ */
createCommand(name, run, undo, redo = run) { createCommand(name, run, undo, redo = run) {
const command = async function runWrapper(title, options) { const command = async function runWrapper(title, options, extra) {
// Create copy of options and state object // Create copy of options and state object
const copy = {}; const copy = {};
Object.assign(copy, options); Object.assign(copy, options);
@ -93,11 +104,11 @@ const commands = makeReadOnly(
return; return;
} }
const undoWrapper = () => { const undoWrapper = async () => {
console.debug( console.debug(
`[commands] Undoing '${title}'[${name}], currently ${this._current}` `[commands] Undoing '${title}'[${name}], currently ${this._current}`
); );
undo(title, state); await undo(title, state);
_commands_events.emit({ _commands_events.emit({
id: entry.id, id: entry.id,
name, name,
@ -106,11 +117,11 @@ const commands = makeReadOnly(
current: this._current, current: this._current,
}); });
}; };
const redoWrapper = () => { const redoWrapper = async () => {
console.debug( console.debug(
`[commands] Redoing '${title}'[${name}], currently ${this._current}` `[commands] Redoing '${title}'[${name}], currently ${this._current}`
); );
redo(title, copy, state); await redo(title, copy, state);
_commands_events.emit({ _commands_events.emit({
id: entry.id, id: entry.id,
name, name,
@ -120,6 +131,11 @@ const commands = makeReadOnly(
}); });
}; };
entry.undo = undoWrapper;
entry.redo = redoWrapper;
if (!extra.recordHistory) return entry;
// Add to history // Add to history
if (commands._history.length > commands._current + 1) { if (commands._history.length > commands._current + 1) {
commands._history.forEach((entry, index) => { commands._history.forEach((entry, index) => {
@ -139,9 +155,6 @@ const commands = makeReadOnly(
commands._history.push(entry); commands._history.push(entry);
commands._current++; commands._current++;
entry.undo = undoWrapper;
entry.redo = redoWrapper;
_commands_events.emit({ _commands_events.emit({
id: entry.id, id: entry.id,
name, name,
@ -163,13 +176,16 @@ const commands = makeReadOnly(
* @param {string} name The name of the command to run * @param {string} name The name of the command to run
* @param {string} title The display name of the command on the history panel view * @param {string} title The display name of the command on the history panel view
* @param {any} options The options to be sent to the command to be run * @param {any} options The options to be sent to the command to be run
* @return {Promise<{undo: () => void, redo: () => void}>} The command's return value
*/ */
runCommand(name, title, options = null) { async runCommand(name, title, options = null, extra = {}) {
defaultOpt(extra, {
recordHistory: true,
});
if (!this._types[name]) if (!this._types[name])
throw new CommandNonExistentError( throw new ReferenceError(`[commands] Command '${name}' does not exist`);
`[commands] Command '${name}' does not exist`
); return this._types[name](title, options, extra);
this._types[name](title, options);
}, },
}, },
"commands", "commands",

View file

@ -99,6 +99,32 @@ const uil = {
const actionArray = document.createElement("div"); const actionArray = document.createElement("div");
actionArray.classList.add("actions"); actionArray.classList.add("actions");
if (uiLayer.deletable) {
const deleteButton = document.createElement("button");
deleteButton.addEventListener(
"click",
(evn) => {
commands.runCommand("deleteLayer", "Deleted Layer", {
layer: uiLayer,
});
},
{passive: false}
);
deleteButton.addEventListener(
"dblclick",
(evn) => {
evn.stopPropagation();
},
{passive: false}
);
deleteButton.title = "Delete Layer";
deleteButton.appendChild(document.createElement("div"));
deleteButton.classList.add("delete-btn");
actionArray.appendChild(deleteButton);
}
const hideButton = document.createElement("button"); const hideButton = document.createElement("button");
hideButton.addEventListener( hideButton.addEventListener(
"click", "click",
@ -111,6 +137,13 @@ const uil = {
}, },
{passive: false} {passive: false}
); );
hideButton.addEventListener(
"dblclick",
(evn) => {
evn.stopPropagation();
},
{passive: false}
);
hideButton.title = "Hide/Unhide Layer"; hideButton.title = "Hide/Unhide Layer";
hideButton.appendChild(document.createElement("div")); hideButton.appendChild(document.createElement("div"));
hideButton.classList.add("hide-btn"); hideButton.classList.add("hide-btn");
@ -121,13 +154,23 @@ const uil = {
if (layersEl.children[index]) if (layersEl.children[index])
layersEl.children[index].before(uiLayer.entry); layersEl.children[index].before(uiLayer.entry);
else layersEl.appendChild(uiLayer.entry); else layersEl.appendChild(uiLayer.entry);
} } else if (!layersEl.querySelector(`#ui-layer-${uiLayer.id}`)) {
// If layer exists but is not on the DOM, add it back
if (index === 0) layersEl.children[0].before(uiLayer.entry);
else layersEl.children[index - 1].after(uiLayer.entry);
} else {
// If the layer already exists, just move it here // If the layer already exists, just move it here
else {
layersEl.children[index].before(uiLayer.entry); layersEl.children[index].before(uiLayer.entry);
} }
}); });
// Deletes layer if not in array
for (var i = 0; i < layersEl.children.length; i++) {
if (!copy.find((l) => layersEl.children[i].id === `ui-layer-${l.id}`)) {
layersEl.children[i].remove();
}
}
// Synchronizes with the layer lib // Synchronizes with the layer lib
this.layers.forEach((uiLayer, index) => { this.layers.forEach((uiLayer, index) => {
if (index === 0) uiLayer.layer.moveAfter(bgLayer); if (index === 0) uiLayer.layer.moveAfter(bgLayer);
@ -138,11 +181,13 @@ const uil = {
/** /**
* Adds a user-manageable layer for image editing. * Adds a user-manageable layer for image editing.
* *
* Should not be called directly. Use the command instead.
*
* @param {string} group The group the layer belongs to. [does nothing for now] * @param {string} group The group the layer belongs to. [does nothing for now]
* @param {string} name The name of the new layer. * @param {string} name The name of the new layer.
* @returns * @returns
*/ */
addLayer(group, name) { _addLayer(group, name) {
const layer = imageCollection.registerLayer(null, { const layer = imageCollection.registerLayer(null, {
name, name,
after: after:
@ -180,12 +225,14 @@ const uil = {
}, },
/** /**
* Moves a layer to a specified position * Moves a layer to a specified position.
*
* Should not be called directly. Use the command instead.
* *
* @param {UserLayer} layer Layer to move * @param {UserLayer} layer Layer to move
* @param {number} position Position to move the layer to * @param {number} position Position to move the layer to
*/ */
moveLayerTo(layer, position) { _moveLayerTo(layer, position) {
if (position < 0 || position >= this.layers.length) if (position < 0 || position >= this.layers.length)
throw new RangeError("Position out of bounds"); throw new RangeError("Position out of bounds");
@ -203,27 +250,31 @@ const uil = {
throw new ReferenceError("Layer could not be found"); throw new ReferenceError("Layer could not be found");
}, },
/** /**
* Moves a layer up a single position * Moves a layer up a single position.
*
* Should not be called directly. Use the command instead.
* *
* @param {UserLayer} [layer=uil.active] Layer to move * @param {UserLayer} [layer=uil.active] Layer to move
*/ */
moveLayerUp(layer = uil.active) { _moveLayerUp(layer = uil.active) {
const index = this.layers.indexOf(layer); const index = this.layers.indexOf(layer);
if (index === -1) throw new ReferenceError("Layer could not be found"); if (index === -1) throw new ReferenceError("Layer could not be found");
try { try {
this.moveLayerTo(layer, index + 1); this._moveLayerTo(layer, index + 1);
} catch (e) {} } catch (e) {}
}, },
/** /**
* Moves a layer down a single position * Moves a layer down a single position.
*
* Should not be called directly. Use the command instead.
* *
* @param {UserLayer} [layer=uil.active] Layer to move * @param {UserLayer} [layer=uil.active] Layer to move
*/ */
moveLayerDown(layer = uil.active) { _moveLayerDown(layer = uil.active) {
const index = this.layers.indexOf(layer); const index = this.layers.indexOf(layer);
if (index === -1) throw new ReferenceError("Layer could not be found"); if (index === -1) throw new ReferenceError("Layer could not be found");
try { try {
this.moveLayerTo(layer, index - 1); this._moveLayerTo(layer, index - 1);
} catch (e) {} } catch (e) {}
}, },
/** /**
@ -266,4 +317,221 @@ const uil = {
return canvas; return canvas;
}, },
}; };
uil.addLayer(null, "Default Image Layer");
/**
* Command for creating a new layer
*/
commands.createCommand(
"addLayer",
(title, opt, state) => {
const options = Object.assign({}, opt) || {};
defaultOpt(options, {
group: null,
name: "New Layer",
deletable: true,
});
if (!state.layer) {
const {group, name} = options;
const layer = imageCollection.registerLayer(null, {
name,
after:
(uil.layers.length > 0 && uil.layers[uil.layers.length - 1].layer) ||
bgLayer,
});
state.layer = {
id: layer.id,
group,
name,
deletable: options.deletable,
_hidden: false,
set hidden(v) {
if (v) {
uil._hidden = true;
uil.layer.hide(v);
} else {
uil._hidden = false;
uil.layer.unhide(v);
}
},
get hidden() {
return uil._hidden;
},
entry: null,
layer,
};
}
uil.layers.push(state.layer);
uil._syncLayers();
uil.active = state.layer;
},
(title, state) => {
const index = uil.layers.findIndex((v) => v === state.layer);
if (index === -1) throw new ReferenceError("Layer could not be found");
if (uil.active === state.layer)
uil.active = uil.layers[index + 1] || uil.layers[index - 1];
uil.layers.splice(index, 1);
uil._syncLayers();
}
);
/**
* Command for moving a layer to a position
*/
commands.createCommand(
"moveLayer",
(title, opt, state) => {
const options = opt || {};
defaultOpt(options, {
layer: null,
to: null,
delta: null,
});
if (!state.layer) {
if (options.to === null && options.delta === null)
throw new Error(
"[layers.moveLayer] Options must contain one of {to?, delta?}"
);
const layer = options.layer || uil.active;
const index = uil.layers.indexOf(layer);
if (index === -1) throw new ReferenceError("Layer could not be found");
let position = options.to;
if (position === null) position = index + options.delta;
state.layer = layer;
state.oldposition = index;
state.position = position;
}
uil._moveLayerTo(state.layer, state.position);
},
(title, state) => {
uil._moveLayerTo(state.layer, state.oldposition);
}
);
/**
* Command for deleting a layer
*/
commands.createCommand(
"deleteLayer",
(title, opt, state) => {
const options = opt || {};
defaultOpt(options, {
layer: null,
});
if (!state.layer) {
const layer = options.layer || uil.active;
if (!layer.deletable)
throw new TypeError("[layer.deleteLayer] Layer is not deletable");
const index = uil.layers.indexOf(layer);
if (index === -1)
throw new ReferenceError(
"[layer.deleteLayer] Layer could not be found"
);
state.layer = layer;
state.position = index;
}
uil.layers.splice(state.position, 1);
uil.active = uil.layers[state.position - 1] || uil.layers[state.position];
uil._syncLayers();
state.layer.layer.hide();
},
(title, state) => {
uil.layers.splice(state.position, 0, state.layer);
uil.active = state.layer;
uil._syncLayers();
state.layer.layer.unhide();
}
);
/**
* Command for merging a layer into the layer below it
*/
commands.createCommand(
"mergeLayer",
async (title, opt, state) => {
const options = opt || {};
defaultOpt(options, {
layerS: null,
layerD: null,
});
const layerS = options.layer || uil.active;
if (!layerS.deletable)
throw new TypeError(
"[layer.mergeLayer] Layer is a root layer and cannot be merged"
);
const index = uil.layers.indexOf(layerS);
if (index === -1)
throw new ReferenceError("[layer.mergeLayer] Layer could not be found");
if (index === 0 && !options.layerD)
throw new ReferenceError(
"[layer.mergeLayer] No layer below source layer exists"
);
// Use layer under source layer to merge into if not given
const layerD = options.layerD || uil.layers[index - 1];
state.layerS = layerS;
state.layerD = layerD;
// REFERENCE: This is a great reference for metacommands (commands that use other commands)
// These commands should NOT record history as we are already executing a command
state.drawCommand = await commands.runCommand(
"drawImage",
"Merge Layer Draw",
{
image: state.layerS.layer.canvas,
x: 0,
y: 0,
ctx: state.layerD.layer.ctx,
},
{recordHistory: false}
);
state.delCommand = await commands.runCommand(
"deleteLayer",
"Merge Layer Delete",
{layer: state.layerS},
{recordHistory: false}
);
},
(title, state) => {
state.drawCommand.undo();
state.delCommand.undo();
},
(title, options, state) => {
state.drawCommand.redo();
state.delCommand.redo();
}
);
commands.runCommand(
"addLayer",
"Initial Layer Creation",
{name: "Default Image Layer", deletable: false},
{recordHistory: false}
);

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="17 18 11 12 17 6"></polyline>
<path d="M7 6v12"></path>
</svg>

After

Width:  |  Height:  |  Size: 267 B

7
res/icons/file-x.svg Normal file
View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="9.5" y1="12.5" x2="14.5" y2="17.5"></line>
<line x1="14.5" y1="12.5" x2="9.5" y2="17.5"></line>
</svg>

After

Width:  |  Height:  |  Size: 437 B