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");
}
.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 {
-webkit-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");
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
type="button"
title="Add Layer"
onclick="uil.addLayer(null, 'New Layer')"
onclick="commands.runCommand('addLayer', 'Added Layer')"
class="ui icon button">
<div class="icon-file-plus"></div>
</button>
@ -224,7 +224,7 @@
<button
type="button"
title="Move Layer Up"
onclick="uil.moveLayerUp()"
onclick="commands.runCommand('moveLayer', 'Moved Layer Up',{delta: 1})"
class="ui icon button">
<div class="icon-chevron-up"></div>
</button>
@ -232,10 +232,26 @@
<button
type="button"
title="Move Layer Down"
onclick="uil.moveLayerDown()"
onclick="commands.runCommand('moveLayer', 'Moved Layer Down', {delta: -1})"
class="ui icon button">
<div class="icon-chevron-down"></div>
</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>

View file

@ -4,9 +4,6 @@
const _commands_events = new Observer();
/** CommandNonExistentError */
class CommandNonExistentError extends Error {}
/** Global Commands Object */
const commands = makeReadOnly(
{
@ -32,7 +29,14 @@ const commands = makeReadOnly(
*/
async undo(n = 1) {
for (var i = 0; i < n && this.current > -1; i++) {
try {
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) {
for (var i = 0; i < n && this.current + 1 < this._history.length; i++) {
try {
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}
*/
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
const copy = {};
Object.assign(copy, options);
@ -93,11 +104,11 @@ const commands = makeReadOnly(
return;
}
const undoWrapper = () => {
const undoWrapper = async () => {
console.debug(
`[commands] Undoing '${title}'[${name}], currently ${this._current}`
);
undo(title, state);
await undo(title, state);
_commands_events.emit({
id: entry.id,
name,
@ -106,11 +117,11 @@ const commands = makeReadOnly(
current: this._current,
});
};
const redoWrapper = () => {
const redoWrapper = async () => {
console.debug(
`[commands] Redoing '${title}'[${name}], currently ${this._current}`
);
redo(title, copy, state);
await redo(title, copy, state);
_commands_events.emit({
id: entry.id,
name,
@ -120,6 +131,11 @@ const commands = makeReadOnly(
});
};
entry.undo = undoWrapper;
entry.redo = redoWrapper;
if (!extra.recordHistory) return entry;
// Add to history
if (commands._history.length > commands._current + 1) {
commands._history.forEach((entry, index) => {
@ -139,9 +155,6 @@ const commands = makeReadOnly(
commands._history.push(entry);
commands._current++;
entry.undo = undoWrapper;
entry.redo = redoWrapper;
_commands_events.emit({
id: entry.id,
name,
@ -163,13 +176,16 @@ const commands = makeReadOnly(
* @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 {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])
throw new CommandNonExistentError(
`[commands] Command '${name}' does not exist`
);
this._types[name](title, options);
throw new ReferenceError(`[commands] Command '${name}' does not exist`);
return this._types[name](title, options, extra);
},
},
"commands",

View file

@ -99,6 +99,32 @@ const uil = {
const actionArray = document.createElement("div");
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");
hideButton.addEventListener(
"click",
@ -111,6 +137,13 @@ const uil = {
},
{passive: false}
);
hideButton.addEventListener(
"dblclick",
(evn) => {
evn.stopPropagation();
},
{passive: false}
);
hideButton.title = "Hide/Unhide Layer";
hideButton.appendChild(document.createElement("div"));
hideButton.classList.add("hide-btn");
@ -121,13 +154,23 @@ const uil = {
if (layersEl.children[index])
layersEl.children[index].before(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
else {
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
this.layers.forEach((uiLayer, index) => {
if (index === 0) uiLayer.layer.moveAfter(bgLayer);
@ -138,11 +181,13 @@ const uil = {
/**
* 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} name The name of the new layer.
* @returns
*/
addLayer(group, name) {
_addLayer(group, name) {
const layer = imageCollection.registerLayer(null, {
name,
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 {number} position Position to move the layer to
*/
moveLayerTo(layer, position) {
_moveLayerTo(layer, position) {
if (position < 0 || position >= this.layers.length)
throw new RangeError("Position out of bounds");
@ -203,27 +250,31 @@ const uil = {
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
*/
moveLayerUp(layer = uil.active) {
_moveLayerUp(layer = uil.active) {
const index = this.layers.indexOf(layer);
if (index === -1) throw new ReferenceError("Layer could not be found");
try {
this.moveLayerTo(layer, index + 1);
this._moveLayerTo(layer, index + 1);
} 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
*/
moveLayerDown(layer = uil.active) {
_moveLayerDown(layer = uil.active) {
const index = this.layers.indexOf(layer);
if (index === -1) throw new ReferenceError("Layer could not be found");
try {
this.moveLayerTo(layer, index - 1);
this._moveLayerTo(layer, index - 1);
} catch (e) {}
},
/**
@ -266,4 +317,221 @@ const uil = {
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