js-draw
Version:
Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.
180 lines (179 loc) • 6.81 kB
JavaScript
import waitForAll from '../util/waitForAll.mjs';
import Command from './Command.mjs';
import SerializableCommand from './SerializableCommand.mjs';
class NonSerializableUnion extends Command {
constructor(commands, applyChunkSize, descriptionOverride) {
super();
this.commands = commands;
this.applyChunkSize = applyChunkSize;
this.descriptionOverride = descriptionOverride;
}
apply(editor) {
if (this.applyChunkSize === undefined) {
const results = this.commands.map((cmd) => cmd.apply(editor));
return waitForAll(results);
}
else {
return editor.asyncApplyCommands(this.commands, this.applyChunkSize);
}
}
unapply(editor) {
const commands = [...this.commands];
commands.reverse();
if (this.applyChunkSize === undefined) {
const results = commands.map((cmd) => cmd.unapply(editor));
return waitForAll(results);
}
else {
return editor.asyncUnapplyCommands(commands, this.applyChunkSize, false);
}
}
onDrop(editor) {
this.commands.forEach((command) => command.onDrop(editor));
}
description(editor, localizationTable) {
if (this.descriptionOverride) {
return this.descriptionOverride;
}
const descriptions = [];
let lastDescription = null;
let duplicateDescriptionCount = 0;
let handledCommandCount = 0;
for (const part of this.commands) {
const description = part.description(editor, localizationTable);
if (description !== lastDescription && lastDescription !== null) {
descriptions.push(localizationTable.unionOf(lastDescription, duplicateDescriptionCount));
lastDescription = null;
duplicateDescriptionCount = 0;
}
duplicateDescriptionCount++;
handledCommandCount++;
lastDescription ??= description;
// Long descriptions aren't very useful to the user.
const maxDescriptionLength = 12;
if (descriptions.length > maxDescriptionLength) {
break;
}
}
if (duplicateDescriptionCount > 1) {
descriptions.push(localizationTable.unionOf(lastDescription, duplicateDescriptionCount));
}
else if (duplicateDescriptionCount === 1) {
descriptions.push(lastDescription);
}
if (handledCommandCount < this.commands.length) {
descriptions.push(localizationTable.andNMoreCommands(this.commands.length - handledCommandCount));
}
return descriptions.join(', ');
}
}
class SerializableUnion extends SerializableCommand {
constructor(commands, applyChunkSize, descriptionOverride) {
super('union');
this.commands = commands;
this.applyChunkSize = applyChunkSize;
this.descriptionOverride = descriptionOverride;
this.nonserializableCommand = new NonSerializableUnion(commands, applyChunkSize, descriptionOverride);
}
serializeToJSON() {
if (this.serializedData) {
return this.serializedData;
}
return {
applyChunkSize: this.applyChunkSize,
data: this.commands.map((command) => command.serialize()),
description: this.descriptionOverride,
};
}
apply(editor) {
// Cache this' serialized form -- applying this may change how commands serialize.
this.serializedData = this.serializeToJSON();
return this.nonserializableCommand.apply(editor);
}
unapply(editor) {
return this.nonserializableCommand.unapply(editor);
}
onDrop(editor) {
this.nonserializableCommand.onDrop(editor);
}
description(editor, localizationTable) {
return this.nonserializableCommand.description(editor, localizationTable);
}
}
/**
* Creates a single command from `commands`. This is useful when undoing should undo *all* commands
* in `commands` at once, rather than one at a time.
*
* @example
*
* ```ts,runnable
* import { Editor, pathToRenderable, Stroke, uniteCommands } from 'js-draw';
* import { Path, Color4 } from '@js-draw/math';
*
* const editor = new Editor(document.body);
* editor.addToolbar();
*
* // Create strokes!
* const strokes = [];
* for (let i = 0; i < 10; i++) {
* const renderablePath = pathToRenderable(
* Path.fromString(`M0,${i * 10} L100,100 L300,30 z`),
* { fill: Color4.transparent, stroke: { color: Color4.red, width: 1, } }
* );
* strokes.push(new Stroke([ renderablePath ]));
* }
*
* // Convert to commands
* const addStrokesCommands = strokes.map(stroke => editor.image.addElement(stroke));
*
* // Apply all as a single undoable command (try applying each in a loop instead!)
* await editor.dispatch(uniteCommands(addStrokesCommands));
*
* // The second parameter to uniteCommands is for very large numbers of commands, when
* // applying them shouldn't be done all at once (which would block the UI).
*
* // The second parameter to uniteCommands is for very large numbers of commands, when
* // applying them shouldn't be done all at once (which would block the UI).
* ```
*/
const uniteCommands = (commands, options) => {
let allSerializable = true;
for (const command of commands) {
if (!(command instanceof SerializableCommand)) {
allSerializable = false;
break;
}
}
let applyChunkSize;
let description;
if (typeof options === 'number') {
applyChunkSize = options;
}
else {
applyChunkSize = options?.applyChunkSize;
description = options?.description;
}
if (!allSerializable) {
return new NonSerializableUnion(commands, applyChunkSize, description);
}
else {
const castedCommands = commands;
return new SerializableUnion(castedCommands, applyChunkSize, description);
}
};
SerializableCommand.register('union', (data, editor) => {
if (typeof data.data.length !== 'number') {
throw new Error('Unions of commands must serialize to lists of serialization data.');
}
const applyChunkSize = data.applyChunkSize;
if (typeof applyChunkSize !== 'number' && applyChunkSize !== undefined) {
throw new Error('serialized applyChunkSize is neither undefined nor a number.');
}
const description = typeof data.description === 'string' ? data.description : undefined;
const commands = [];
for (const part of data.data) {
commands.push(SerializableCommand.deserialize(part, editor));
}
return uniteCommands(commands, { applyChunkSize, description });
});
export default uniteCommands;