js-draw 1.27.2 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/Editor.css +1 -1
- package/dist/bundle.js +28 -28
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +7 -2
- package/dist/cjs/Editor.js +11 -5
- package/dist/cjs/SVGLoader/SVGLoader.d.ts +21 -0
- package/dist/cjs/SVGLoader/SVGLoader.js +74 -47
- package/dist/cjs/SVGLoader/SVGLoader.plugins.test.d.ts +1 -0
- package/dist/cjs/Viewport.js +2 -32
- package/dist/cjs/commands/Duplicate.d.ts +7 -4
- package/dist/cjs/commands/Duplicate.js +48 -7
- package/dist/cjs/commands/Duplicate.test.d.ts +1 -0
- package/dist/cjs/commands/Erase.d.ts +1 -1
- package/dist/cjs/commands/Erase.js +2 -2
- package/dist/cjs/commands/localization.d.ts +2 -2
- package/dist/cjs/commands/localization.js +2 -2
- package/dist/cjs/components/AbstractComponent.d.ts +7 -0
- package/dist/cjs/components/AbstractComponent.js +16 -2
- package/dist/cjs/components/Stroke.d.ts +21 -1
- package/dist/cjs/components/Stroke.js +29 -0
- package/dist/cjs/components/TextComponent.d.ts +2 -2
- package/dist/cjs/components/TextComponent.js +2 -2
- package/dist/cjs/image/EditorImage.d.ts +17 -9
- package/dist/cjs/image/EditorImage.js +33 -17
- package/dist/cjs/lib.d.ts +1 -1
- package/dist/cjs/localizations/de.js +2 -2
- package/dist/cjs/rendering/RenderingStyle.d.ts +7 -6
- package/dist/cjs/rendering/lib.d.ts +1 -1
- package/dist/cjs/rendering/renderers/AbstractRenderer.js +4 -0
- package/dist/cjs/rendering/renderers/CanvasRenderer.d.ts +9 -0
- package/dist/cjs/rendering/renderers/CanvasRenderer.js +14 -0
- package/dist/cjs/rendering/renderers/SVGRenderer.d.ts +18 -0
- package/dist/cjs/rendering/renderers/SVGRenderer.js +21 -1
- package/dist/cjs/toolbar/utils/HelpDisplay.js +6 -4
- package/dist/cjs/toolbar/utils/localization.d.ts +1 -0
- package/dist/cjs/toolbar/utils/localization.js +1 -0
- package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +1 -1
- package/dist/cjs/toolbar/widgets/InsertImageWidget/InsertImageWidget.js +1 -1
- package/dist/cjs/toolbar/widgets/components/makeGridSelector.js +1 -1
- package/dist/cjs/tools/Eraser.js +3 -3
- package/dist/cjs/tools/FindTool.js +1 -1
- package/dist/cjs/tools/PasteHandler.js +4 -1
- package/dist/cjs/tools/Pen.js +1 -1
- package/dist/cjs/tools/SelectionTool/SelectAllShortcutHandler.js +1 -1
- package/dist/cjs/tools/SelectionTool/Selection.js +23 -10
- package/dist/cjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.js +1 -1
- package/dist/cjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.js +1 -1
- package/dist/cjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.js +1 -1
- package/dist/cjs/tools/SelectionTool/SelectionTool.js +3 -2
- package/dist/cjs/tools/SoundUITool.js +1 -1
- package/dist/cjs/tools/TextTool.js +2 -2
- package/dist/cjs/util/assertions.d.ts +6 -0
- package/dist/cjs/util/assertions.js +18 -0
- package/dist/cjs/util/describeTransformation.d.ts +12 -0
- package/dist/cjs/util/describeTransformation.js +44 -0
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +7 -2
- package/dist/mjs/Editor.mjs +11 -5
- package/dist/mjs/SVGLoader/SVGLoader.d.ts +21 -0
- package/dist/mjs/SVGLoader/SVGLoader.mjs +74 -47
- package/dist/mjs/SVGLoader/SVGLoader.plugins.test.d.ts +1 -0
- package/dist/mjs/Viewport.mjs +2 -32
- package/dist/mjs/commands/Duplicate.d.ts +7 -4
- package/dist/mjs/commands/Duplicate.mjs +48 -7
- package/dist/mjs/commands/Duplicate.test.d.ts +1 -0
- package/dist/mjs/commands/Erase.d.ts +1 -1
- package/dist/mjs/commands/Erase.mjs +2 -2
- package/dist/mjs/commands/localization.d.ts +2 -2
- package/dist/mjs/commands/localization.mjs +2 -2
- package/dist/mjs/components/AbstractComponent.d.ts +7 -0
- package/dist/mjs/components/AbstractComponent.mjs +17 -3
- package/dist/mjs/components/Stroke.d.ts +21 -1
- package/dist/mjs/components/Stroke.mjs +31 -2
- package/dist/mjs/components/TextComponent.d.ts +2 -2
- package/dist/mjs/components/TextComponent.mjs +2 -2
- package/dist/mjs/image/EditorImage.d.ts +17 -9
- package/dist/mjs/image/EditorImage.mjs +33 -17
- package/dist/mjs/lib.d.ts +1 -1
- package/dist/mjs/localizations/de.mjs +2 -2
- package/dist/mjs/rendering/RenderingStyle.d.ts +7 -6
- package/dist/mjs/rendering/lib.d.ts +1 -1
- package/dist/mjs/rendering/renderers/AbstractRenderer.mjs +4 -0
- package/dist/mjs/rendering/renderers/CanvasRenderer.d.ts +9 -0
- package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +14 -0
- package/dist/mjs/rendering/renderers/SVGRenderer.d.ts +18 -0
- package/dist/mjs/rendering/renderers/SVGRenderer.mjs +21 -1
- package/dist/mjs/toolbar/utils/HelpDisplay.mjs +6 -4
- package/dist/mjs/toolbar/utils/localization.d.ts +1 -0
- package/dist/mjs/toolbar/utils/localization.mjs +1 -0
- package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +1 -1
- package/dist/mjs/toolbar/widgets/InsertImageWidget/InsertImageWidget.mjs +1 -1
- package/dist/mjs/toolbar/widgets/components/makeGridSelector.mjs +1 -1
- package/dist/mjs/tools/Eraser.mjs +3 -3
- package/dist/mjs/tools/FindTool.mjs +1 -1
- package/dist/mjs/tools/PasteHandler.mjs +4 -1
- package/dist/mjs/tools/Pen.mjs +1 -1
- package/dist/mjs/tools/SelectionTool/SelectAllShortcutHandler.mjs +1 -1
- package/dist/mjs/tools/SelectionTool/Selection.mjs +23 -10
- package/dist/mjs/tools/SelectionTool/SelectionBuilders/LassoSelectionBuilder.mjs +1 -1
- package/dist/mjs/tools/SelectionTool/SelectionBuilders/RectSelectionBuilder.mjs +1 -1
- package/dist/mjs/tools/SelectionTool/SelectionBuilders/SelectionBuilder.mjs +1 -1
- package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +3 -2
- package/dist/mjs/tools/SoundUITool.mjs +1 -1
- package/dist/mjs/tools/TextTool.mjs +2 -2
- package/dist/mjs/util/assertions.d.ts +6 -0
- package/dist/mjs/util/assertions.mjs +16 -0
- package/dist/mjs/util/describeTransformation.d.ts +12 -0
- package/dist/mjs/util/describeTransformation.mjs +42 -0
- package/dist/mjs/version.mjs +1 -1
- package/package.json +4 -4
- package/src/toolbar/utils/HelpDisplay.scss +7 -1
@@ -11,6 +11,25 @@ export type SVGLoaderUnknownStyleAttribute = {
|
|
11
11
|
value: string;
|
12
12
|
priority?: string;
|
13
13
|
};
|
14
|
+
export interface SVGLoaderControl {
|
15
|
+
/** Call this to add a component to the editor. */
|
16
|
+
addComponent: ComponentAddedListener;
|
17
|
+
}
|
18
|
+
/**
|
19
|
+
* Loads custom components from an SVG image.
|
20
|
+
* @see SVGLoader.fromString
|
21
|
+
*/
|
22
|
+
export interface SVGLoaderPlugin {
|
23
|
+
/**
|
24
|
+
* Called when the {@link SVGLoader} encounters a `node`.
|
25
|
+
*
|
26
|
+
* Call `loader.addComponent` to add new components to the image.
|
27
|
+
*
|
28
|
+
* Returning `true` prevents the {@link SVGLoader} from doing further
|
29
|
+
* processing on the node.
|
30
|
+
*/
|
31
|
+
visit(node: Element, loader: SVGLoaderControl): Promise<boolean>;
|
32
|
+
}
|
14
33
|
export declare enum SVGLoaderLoadMethod {
|
15
34
|
IFrame = "iframe",
|
16
35
|
DOMParser = "domparser"
|
@@ -18,6 +37,7 @@ export declare enum SVGLoaderLoadMethod {
|
|
18
37
|
export interface SVGLoaderOptions {
|
19
38
|
sanitize?: boolean;
|
20
39
|
disableUnknownObjectWarnings?: boolean;
|
40
|
+
plugins?: SVGLoaderPlugin[];
|
21
41
|
loadMethod?: SVGLoaderLoadMethod;
|
22
42
|
}
|
23
43
|
export default class SVGLoader implements ImageLoader {
|
@@ -31,6 +51,7 @@ export default class SVGLoader implements ImageLoader {
|
|
31
51
|
private rootViewBox;
|
32
52
|
private readonly storeUnknown;
|
33
53
|
private readonly disableUnknownObjectWarnings;
|
54
|
+
private readonly plugins;
|
34
55
|
private constructor();
|
35
56
|
private getStyle;
|
36
57
|
private strokeDataFromElem;
|
@@ -39,6 +39,7 @@ export default class SVGLoader {
|
|
39
39
|
this.totalToProcess = 0;
|
40
40
|
this.containerGroupIDs = [];
|
41
41
|
this.encounteredIDs = [];
|
42
|
+
this.plugins = options.plugins ?? [];
|
42
43
|
this.storeUnknown = !(options.sanitize ?? false);
|
43
44
|
this.disableUnknownObjectWarnings = !!options.disableUnknownObjectWarnings;
|
44
45
|
}
|
@@ -434,56 +435,78 @@ export default class SVGLoader {
|
|
434
435
|
async visit(node) {
|
435
436
|
this.totalToProcess += node.childElementCount;
|
436
437
|
let visitChildren = true;
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
438
|
+
const visitPlugin = async () => {
|
439
|
+
for (const plugin of this.plugins) {
|
440
|
+
const processed = await plugin.visit(node, {
|
441
|
+
addComponent: (component) => {
|
442
|
+
return this.onAddComponent?.(component);
|
443
|
+
},
|
444
|
+
});
|
445
|
+
if (processed) {
|
441
446
|
visitChildren = false;
|
447
|
+
return true;
|
442
448
|
}
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
else {
|
453
|
-
await this.addPath(node);
|
454
|
-
}
|
455
|
-
break;
|
456
|
-
case 'text':
|
457
|
-
await this.addText(node);
|
458
|
-
visitChildren = false;
|
459
|
-
break;
|
460
|
-
case 'image':
|
461
|
-
await this.addImage(node);
|
462
|
-
// Images should not have children.
|
463
|
-
visitChildren = false;
|
464
|
-
break;
|
465
|
-
case 'svg':
|
466
|
-
this.updateViewBox(node);
|
467
|
-
this.updateSVGAttrs(node);
|
468
|
-
break;
|
469
|
-
case 'style':
|
470
|
-
// Keeping unnecessary style sheets can cause the browser to keep all
|
471
|
-
// SVG elements *referenced* by the style sheet in some browsers.
|
472
|
-
//
|
473
|
-
// Only keep the style sheet if it won't be discarded on save.
|
474
|
-
if (node.getAttribute('id') !== renderedStylesheetId) {
|
475
|
-
await this.addUnknownNode(node);
|
476
|
-
}
|
477
|
-
break;
|
478
|
-
default:
|
479
|
-
if (!this.disableUnknownObjectWarnings) {
|
480
|
-
console.warn('Unknown SVG element,', node, node.tagName);
|
481
|
-
if (!(node instanceof SVGElement)) {
|
482
|
-
console.warn('Element', node, 'is not an SVGElement!', this.storeUnknown ? 'Continuing anyway.' : 'Skipping.');
|
449
|
+
}
|
450
|
+
return false;
|
451
|
+
};
|
452
|
+
const visitBuiltIn = async () => {
|
453
|
+
switch (node.tagName.toLowerCase()) {
|
454
|
+
case 'g':
|
455
|
+
if (node.classList.contains(imageBackgroundCSSClassName)) {
|
456
|
+
await this.addBackground(node);
|
457
|
+
visitChildren = false;
|
483
458
|
}
|
484
|
-
|
485
|
-
|
486
|
-
|
459
|
+
else {
|
460
|
+
await this.startGroup(node);
|
461
|
+
}
|
462
|
+
// Otherwise, continue -- visit the node's children.
|
463
|
+
break;
|
464
|
+
case 'path':
|
465
|
+
if (node.classList.contains(imageBackgroundCSSClassName)) {
|
466
|
+
await this.addBackground(node);
|
467
|
+
}
|
468
|
+
else {
|
469
|
+
await this.addPath(node);
|
470
|
+
}
|
471
|
+
break;
|
472
|
+
case 'text':
|
473
|
+
await this.addText(node);
|
474
|
+
visitChildren = false;
|
475
|
+
break;
|
476
|
+
case 'image':
|
477
|
+
await this.addImage(node);
|
478
|
+
// Images should not have children.
|
479
|
+
visitChildren = false;
|
480
|
+
break;
|
481
|
+
case 'svg':
|
482
|
+
this.updateViewBox(node);
|
483
|
+
this.updateSVGAttrs(node);
|
484
|
+
break;
|
485
|
+
case 'style':
|
486
|
+
// Keeping unnecessary style sheets can cause the browser to keep all
|
487
|
+
// SVG elements *referenced* by the style sheet in some browsers.
|
488
|
+
//
|
489
|
+
// Only keep the style sheet if it won't be discarded on save.
|
490
|
+
if (node.getAttribute('id') !== renderedStylesheetId) {
|
491
|
+
await this.addUnknownNode(node);
|
492
|
+
}
|
493
|
+
break;
|
494
|
+
default:
|
495
|
+
if (!this.disableUnknownObjectWarnings) {
|
496
|
+
console.warn('Unknown SVG element,', node, node.tagName);
|
497
|
+
if (!(node instanceof SVGElement)) {
|
498
|
+
console.warn('Element', node, 'is not an SVGElement!', this.storeUnknown ? 'Continuing anyway.' : 'Skipping.');
|
499
|
+
}
|
500
|
+
}
|
501
|
+
await this.addUnknownNode(node);
|
502
|
+
return;
|
503
|
+
}
|
504
|
+
};
|
505
|
+
if (await visitPlugin()) {
|
506
|
+
visitChildren = false;
|
507
|
+
}
|
508
|
+
else {
|
509
|
+
await visitBuiltIn();
|
487
510
|
}
|
488
511
|
if (visitChildren) {
|
489
512
|
for (const child of node.children) {
|
@@ -602,17 +625,21 @@ export default class SVGLoader {
|
|
602
625
|
// Handle options
|
603
626
|
let sanitize;
|
604
627
|
let disableUnknownObjectWarnings;
|
628
|
+
let plugins;
|
605
629
|
if (typeof options === 'boolean') {
|
606
630
|
sanitize = options;
|
607
631
|
disableUnknownObjectWarnings = false;
|
632
|
+
plugins = [];
|
608
633
|
}
|
609
634
|
else {
|
610
635
|
sanitize = options.sanitize ?? false;
|
611
636
|
disableUnknownObjectWarnings = options.disableUnknownObjectWarnings ?? false;
|
637
|
+
plugins = options.plugins;
|
612
638
|
}
|
613
639
|
return new SVGLoader(svgElem, cleanUp, {
|
614
640
|
sanitize,
|
615
641
|
disableUnknownObjectWarnings,
|
642
|
+
plugins,
|
616
643
|
});
|
617
644
|
}
|
618
645
|
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
package/dist/mjs/Viewport.mjs
CHANGED
@@ -12,6 +12,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
12
12
|
var _inverseTransform, _a;
|
13
13
|
import Command from './commands/Command.mjs';
|
14
14
|
import { Mat33, Rect2, Vec2, Vec3 } from '@js-draw/math';
|
15
|
+
import describeTransformation from './util/describeTransformation.mjs';
|
15
16
|
export class ViewportTransform extends Command {
|
16
17
|
}
|
17
18
|
export class Viewport {
|
@@ -234,38 +235,7 @@ Viewport.ViewportTransform = (_a = class extends ViewportTransform {
|
|
234
235
|
editor.queueRerender();
|
235
236
|
}
|
236
237
|
description(editor, localizationTable) {
|
237
|
-
|
238
|
-
// Describe the transformation's affect on the viewport (note that transformation transforms
|
239
|
-
// the **elements** within the viewport). Assumes the transformation only does rotation/scale/translation.
|
240
|
-
const origVec = editor.viewport.visibleRect.center;
|
241
|
-
const linearTransformedVec = this.transform.transformVec3(Vec2.unitX);
|
242
|
-
const affineTransformedVec = this.transform.transformVec2(origVec);
|
243
|
-
const scale = linearTransformedVec.magnitude();
|
244
|
-
const rotation = (180 / Math.PI) * linearTransformedVec.angle();
|
245
|
-
const translation = affineTransformedVec.minus(origVec);
|
246
|
-
if (scale > 1.2) {
|
247
|
-
result.push(localizationTable.zoomedIn);
|
248
|
-
}
|
249
|
-
else if (scale < 0.8) {
|
250
|
-
result.push(localizationTable.zoomedOut);
|
251
|
-
}
|
252
|
-
if (Math.floor(Math.abs(rotation)) > 0) {
|
253
|
-
result.push(localizationTable.rotatedBy(Math.round(rotation)));
|
254
|
-
}
|
255
|
-
const minTranslation = 1e-4;
|
256
|
-
if (translation.x > minTranslation) {
|
257
|
-
result.push(localizationTable.movedLeft);
|
258
|
-
}
|
259
|
-
else if (translation.x < -minTranslation) {
|
260
|
-
result.push(localizationTable.movedRight);
|
261
|
-
}
|
262
|
-
if (translation.y < -minTranslation) {
|
263
|
-
result.push(localizationTable.movedDown);
|
264
|
-
}
|
265
|
-
else if (translation.y > minTranslation) {
|
266
|
-
result.push(localizationTable.movedUp);
|
267
|
-
}
|
268
|
-
return result.join('; ');
|
238
|
+
return describeTransformation(editor.viewport.visibleRect.center, this.transform, true, localizationTable);
|
269
239
|
}
|
270
240
|
},
|
271
241
|
_inverseTransform = new WeakMap(),
|
@@ -12,7 +12,7 @@ import SerializableCommand from './SerializableCommand';
|
|
12
12
|
*
|
13
13
|
* // Find all elements intersecting the rectangle with top left (0,0) and
|
14
14
|
* // (width,height)=(100,100).
|
15
|
-
* const elems = editor.image.
|
15
|
+
* const elems = editor.image.getComponentsIntersecting(
|
16
16
|
* new Rect2(0, 0, 100, 100)
|
17
17
|
* );
|
18
18
|
*
|
@@ -23,16 +23,19 @@ import SerializableCommand from './SerializableCommand';
|
|
23
23
|
* editor.dispatch(duplicateElems);
|
24
24
|
* ```
|
25
25
|
*
|
26
|
-
* @see {@link Editor.dispatch} {@link EditorImage.
|
26
|
+
* @see {@link Editor.dispatch} {@link EditorImage.getComponentsIntersecting}
|
27
27
|
*/
|
28
28
|
export default class Duplicate extends SerializableCommand {
|
29
29
|
private toDuplicate;
|
30
30
|
private duplicates;
|
31
31
|
private reverse;
|
32
|
-
constructor(toDuplicate: AbstractComponent[]);
|
32
|
+
constructor(toDuplicate: AbstractComponent[], idsForDuplicates?: string[]);
|
33
33
|
apply(editor: Editor): void;
|
34
34
|
unapply(editor: Editor): void;
|
35
35
|
onDrop(editor: Editor): void;
|
36
36
|
description(_editor: Editor, localizationTable: EditorLocalization): string;
|
37
|
-
protected serializeToJSON():
|
37
|
+
protected serializeToJSON(): {
|
38
|
+
originalIds: string[];
|
39
|
+
cloneIds: string[];
|
40
|
+
};
|
38
41
|
}
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import describeComponentList from '../components/util/describeComponentList.mjs';
|
2
|
+
import { assertIsStringArray } from '../util/assertions.mjs';
|
2
3
|
import Erase from './Erase.mjs';
|
3
4
|
import SerializableCommand from './SerializableCommand.mjs';
|
4
5
|
/**
|
@@ -11,7 +12,7 @@ import SerializableCommand from './SerializableCommand.mjs';
|
|
11
12
|
*
|
12
13
|
* // Find all elements intersecting the rectangle with top left (0,0) and
|
13
14
|
* // (width,height)=(100,100).
|
14
|
-
* const elems = editor.image.
|
15
|
+
* const elems = editor.image.getComponentsIntersecting(
|
15
16
|
* new Rect2(0, 0, 100, 100)
|
16
17
|
* );
|
17
18
|
*
|
@@ -22,13 +23,24 @@ import SerializableCommand from './SerializableCommand.mjs';
|
|
22
23
|
* editor.dispatch(duplicateElems);
|
23
24
|
* ```
|
24
25
|
*
|
25
|
-
* @see {@link Editor.dispatch} {@link EditorImage.
|
26
|
+
* @see {@link Editor.dispatch} {@link EditorImage.getComponentsIntersecting}
|
26
27
|
*/
|
27
28
|
class Duplicate extends SerializableCommand {
|
28
|
-
constructor(toDuplicate
|
29
|
+
constructor(toDuplicate,
|
30
|
+
// @internal -- IDs given to the duplicate elements
|
31
|
+
idsForDuplicates) {
|
29
32
|
super('duplicate');
|
30
33
|
this.toDuplicate = toDuplicate;
|
31
|
-
this.duplicates = toDuplicate.map((elem) =>
|
34
|
+
this.duplicates = toDuplicate.map((elem, idx) => {
|
35
|
+
// For collaborative editing, it's important for the clones to have
|
36
|
+
// the same IDs as the originals
|
37
|
+
if (idsForDuplicates && idsForDuplicates[idx]) {
|
38
|
+
return elem.cloneWithId(idsForDuplicates[idx]);
|
39
|
+
}
|
40
|
+
else {
|
41
|
+
return elem.clone();
|
42
|
+
}
|
43
|
+
});
|
32
44
|
this.reverse = new Erase(this.duplicates);
|
33
45
|
}
|
34
46
|
apply(editor) {
|
@@ -47,13 +59,42 @@ class Duplicate extends SerializableCommand {
|
|
47
59
|
return localizationTable.duplicateAction(describeComponentList(localizationTable, this.duplicates) ?? localizationTable.elements, this.duplicates.length);
|
48
60
|
}
|
49
61
|
serializeToJSON() {
|
50
|
-
return
|
62
|
+
return {
|
63
|
+
originalIds: this.toDuplicate.map((elem) => elem.getId()),
|
64
|
+
cloneIds: this.duplicates.map((elem) => elem.getId()),
|
65
|
+
};
|
51
66
|
}
|
52
67
|
}
|
53
68
|
(() => {
|
54
69
|
SerializableCommand.register('duplicate', (json, editor) => {
|
55
|
-
|
56
|
-
|
70
|
+
let originalIds;
|
71
|
+
let cloneIds;
|
72
|
+
// Compatibility with older editors
|
73
|
+
if (Array.isArray(json)) {
|
74
|
+
originalIds = json;
|
75
|
+
cloneIds = [];
|
76
|
+
}
|
77
|
+
else {
|
78
|
+
originalIds = json.originalIds;
|
79
|
+
cloneIds = json.cloneIds;
|
80
|
+
}
|
81
|
+
assertIsStringArray(originalIds);
|
82
|
+
assertIsStringArray(cloneIds);
|
83
|
+
// Resolve to elements -- only keep the elements that can be found in the image.
|
84
|
+
const resolvedElements = [];
|
85
|
+
const filteredCloneIds = [];
|
86
|
+
for (let i = 0; i < originalIds.length; i++) {
|
87
|
+
const originalId = originalIds[i];
|
88
|
+
const foundElement = editor.image.lookupElement(originalId);
|
89
|
+
if (!foundElement) {
|
90
|
+
console.warn('Duplicate command: Could not find element with ID', originalId);
|
91
|
+
}
|
92
|
+
else {
|
93
|
+
filteredCloneIds.push(cloneIds[i]);
|
94
|
+
resolvedElements.push(foundElement);
|
95
|
+
}
|
96
|
+
}
|
97
|
+
return new Duplicate(resolvedElements, filteredCloneIds);
|
57
98
|
});
|
58
99
|
})();
|
59
100
|
export default Duplicate;
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -31,7 +31,7 @@ import SerializableCommand from './SerializableCommand';
|
|
31
31
|
*
|
32
32
|
* // Find all elements intersecting the rectangle with top left (-10,-30) and
|
33
33
|
* // (width,height)=(50,100).
|
34
|
-
* const elems = editor.image.
|
34
|
+
* const elems = editor.image.getComponentsIntersecting(
|
35
35
|
* new Rect2(-10, -30, 50, 100)
|
36
36
|
* );
|
37
37
|
*
|
@@ -31,7 +31,7 @@ import SerializableCommand from './SerializableCommand.mjs';
|
|
31
31
|
*
|
32
32
|
* // Find all elements intersecting the rectangle with top left (-10,-30) and
|
33
33
|
* // (width,height)=(50,100).
|
34
|
-
* const elems = editor.image.
|
34
|
+
* const elems = editor.image.getComponentsIntersecting(
|
35
35
|
* new Rect2(-10, -30, 50, 100)
|
36
36
|
* );
|
37
37
|
*
|
@@ -63,7 +63,7 @@ class Erase extends SerializableCommand {
|
|
63
63
|
unapply(editor) {
|
64
64
|
for (const part of this.toRemove) {
|
65
65
|
if (!editor.image.findParent(part)) {
|
66
|
-
EditorImage.
|
66
|
+
EditorImage.addComponent(part).apply(editor);
|
67
67
|
}
|
68
68
|
}
|
69
69
|
this.applied = false;
|
@@ -11,11 +11,11 @@ export interface CommandLocalization {
|
|
11
11
|
duplicatedNoElements: string;
|
12
12
|
elements: string;
|
13
13
|
updatedViewport: string;
|
14
|
-
transformedElements: (elemCount: number) => string;
|
14
|
+
transformedElements: (elemCount: number, transformDescription: string) => string;
|
15
15
|
resizeOutputCommand: (newSize: Rect2) => string;
|
16
16
|
enabledAutoresizeOutputCommand: string;
|
17
17
|
disabledAutoresizeOutputCommand: string;
|
18
|
-
|
18
|
+
addComponentAction: (elemDescription: string) => string;
|
19
19
|
eraseAction: (elemDescription: string, numElems: number) => string;
|
20
20
|
duplicateAction: (elemDescription: string, count: number) => string;
|
21
21
|
inverseOf: (actionDescription: string) => string;
|
@@ -1,10 +1,10 @@
|
|
1
1
|
export const defaultCommandLocalization = {
|
2
2
|
updatedViewport: 'Transformed Viewport',
|
3
|
-
transformedElements: (elemCount) => `Transformed ${elemCount} element${elemCount === 1 ? '' : 's'}`,
|
3
|
+
transformedElements: (elemCount, action) => `Transformed ${elemCount} element${elemCount === 1 ? '' : 's'} (${action})`,
|
4
4
|
resizeOutputCommand: (newSize) => `Resized image to ${newSize.w}x${newSize.h}`,
|
5
5
|
enabledAutoresizeOutputCommand: 'Enabled output autoresize',
|
6
6
|
disabledAutoresizeOutputCommand: 'Disabled output autoresize',
|
7
|
-
|
7
|
+
addComponentAction: (componentDescription) => `Added ${componentDescription}`,
|
8
8
|
eraseAction: (componentDescription, numElems) => `Erased ${numElems} ${componentDescription}`,
|
9
9
|
duplicateAction: (componentDescription, numElems) => `Duplicated ${numElems} ${componentDescription}`,
|
10
10
|
unionOf: (actionDescription, actionCount) => `Union: ${actionCount} ${actionDescription}`,
|
@@ -158,6 +158,13 @@ export default abstract class AbstractComponent {
|
|
158
158
|
abstract description(localizationTable: ImageComponentLocalization): string;
|
159
159
|
protected abstract createClone(): AbstractComponent;
|
160
160
|
clone(): AbstractComponent;
|
161
|
+
/**
|
162
|
+
* Creates a copy of this component with a particular `id`.
|
163
|
+
* This is used internally by {@link Duplicate} when deserializing.
|
164
|
+
*
|
165
|
+
* @internal -- users of the library shouldn't need this.
|
166
|
+
*/
|
167
|
+
cloneWithId(cloneId: string): AbstractComponent;
|
161
168
|
/**
|
162
169
|
* **Optional method**: Divides this component into sections roughly along the given path,
|
163
170
|
* removing parts that are roughly within `shape`.
|
@@ -5,8 +5,10 @@ var __setFunctionName = (this && this.__setFunctionName) || function (f, name, p
|
|
5
5
|
var _a;
|
6
6
|
import SerializableCommand from '../commands/SerializableCommand.mjs';
|
7
7
|
import EditorImage from '../image/EditorImage.mjs';
|
8
|
-
import { Mat33 } from '@js-draw/math';
|
8
|
+
import { Mat33, Vec2 } from '@js-draw/math';
|
9
9
|
import UnresolvedSerializableCommand from '../commands/UnresolvedCommand.mjs';
|
10
|
+
import describeTransformation from '../util/describeTransformation.mjs';
|
11
|
+
import { assertIsString } from '../util/assertions.mjs';
|
10
12
|
export var ComponentSizingMode;
|
11
13
|
(function (ComponentSizingMode) {
|
12
14
|
/** The default. The compnent gets its size from its bounding box. */
|
@@ -201,6 +203,17 @@ class AbstractComponent {
|
|
201
203
|
}
|
202
204
|
return clone;
|
203
205
|
}
|
206
|
+
/**
|
207
|
+
* Creates a copy of this component with a particular `id`.
|
208
|
+
* This is used internally by {@link Duplicate} when deserializing.
|
209
|
+
*
|
210
|
+
* @internal -- users of the library shouldn't need this.
|
211
|
+
*/
|
212
|
+
cloneWithId(cloneId) {
|
213
|
+
const clone = this.clone();
|
214
|
+
clone.id = cloneId;
|
215
|
+
return clone;
|
216
|
+
}
|
204
217
|
// Convert the component to an object that can be passed to
|
205
218
|
// `JSON.stringify`.
|
206
219
|
//
|
@@ -244,6 +257,7 @@ class AbstractComponent {
|
|
244
257
|
if (AbstractComponent.isNotDeserializable(json)) {
|
245
258
|
throw new Error(`Element with data ${json} cannot be deserialized.`);
|
246
259
|
}
|
260
|
+
assertIsString(json.id);
|
247
261
|
const instance = this.deserializationCallbacks[json.name](json.data);
|
248
262
|
instance.id = json.id;
|
249
263
|
if (isFinite(json.zIndex)) {
|
@@ -307,7 +321,7 @@ AbstractComponent.TransformElementCommand = (_a = class extends UnresolvedSerial
|
|
307
321
|
}
|
308
322
|
// Add the element back to the document.
|
309
323
|
if (hadParent) {
|
310
|
-
EditorImage.
|
324
|
+
EditorImage.addComponent(this.component).apply(editor);
|
311
325
|
}
|
312
326
|
}
|
313
327
|
apply(editor) {
|
@@ -321,7 +335,7 @@ AbstractComponent.TransformElementCommand = (_a = class extends UnresolvedSerial
|
|
321
335
|
editor.queueRerender();
|
322
336
|
}
|
323
337
|
description(_editor, localizationTable) {
|
324
|
-
return localizationTable.transformedElements(1);
|
338
|
+
return localizationTable.transformedElements(1, describeTransformation(Vec2.zero, this.affineTransfm, false, localizationTable));
|
325
339
|
}
|
326
340
|
serializeToJSON() {
|
327
341
|
return {
|
@@ -1,7 +1,8 @@
|
|
1
1
|
import SerializableCommand from '../commands/SerializableCommand';
|
2
|
-
import { Mat33, Path, Rect2, LineSegment2 } from '@js-draw/math';
|
2
|
+
import { Mat33, Path, Rect2, LineSegment2, Color4 } from '@js-draw/math';
|
3
3
|
import Editor from '../Editor';
|
4
4
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
5
|
+
import { StrokeStyle } from '../rendering/RenderingStyle';
|
5
6
|
import AbstractComponent from './AbstractComponent';
|
6
7
|
import { ImageComponentLocalization } from './localization';
|
7
8
|
import RestyleableComponent, { ComponentStyle } from './RestylableComponent';
|
@@ -48,6 +49,25 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
|
|
48
49
|
* ```
|
49
50
|
*/
|
50
51
|
constructor(parts: RenderablePathSpec[], initialZIndex?: number);
|
52
|
+
/**
|
53
|
+
* Creates a new `Stroke` from a {@link Path} and `style`. Strokes created
|
54
|
+
* with this method have transparent fill.
|
55
|
+
*
|
56
|
+
* Example:
|
57
|
+
* ```ts,runnable
|
58
|
+
* import { Editor, Stroke, Color4 } from 'js-draw';
|
59
|
+
* const editor = new Editor(document.body);
|
60
|
+
* ---visible---
|
61
|
+
* const stroke = Stroke.fromStroked('m0,0 l10,10', { width: 10, color: Color4.red });
|
62
|
+
* editor.dispatch(editor.image.addComponent(stroke));
|
63
|
+
* ```
|
64
|
+
* Notice that `path` can be a string that specifies an SVG path
|
65
|
+
*
|
66
|
+
* @see fromFilled
|
67
|
+
*/
|
68
|
+
static fromStroked(path: Path | string, style: StrokeStyle): Stroke;
|
69
|
+
/** @see fromStroked */
|
70
|
+
static fromFilled(path: Path | string, fill: Color4): Stroke;
|
51
71
|
getStyle(): ComponentStyle;
|
52
72
|
updateStyle(style: ComponentStyle): SerializableCommand;
|
53
73
|
forceStyle(style: ComponentStyle, editor: Editor | null): void;
|
@@ -1,5 +1,5 @@
|
|
1
|
-
import { Path, Rect2, PathCommandType, comparePathIndices, stepPathIndexBy, } from '@js-draw/math';
|
2
|
-
import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle.mjs';
|
1
|
+
import { Path, Rect2, PathCommandType, comparePathIndices, stepPathIndexBy, Color4, } from '@js-draw/math';
|
2
|
+
import { styleFromJSON, styleToJSON, } from '../rendering/RenderingStyle.mjs';
|
3
3
|
import AbstractComponent from './AbstractComponent.mjs';
|
4
4
|
import { createRestyleComponentCommand, } from './RestylableComponent.mjs';
|
5
5
|
import { pathFromRenderable, pathToRenderable, simplifyPathToFullScreenOrEmpty, } from '../rendering/RenderablePathSpec.mjs';
|
@@ -69,6 +69,35 @@ export default class Stroke extends AbstractComponent {
|
|
69
69
|
}
|
70
70
|
this.contentBBox ??= Rect2.empty;
|
71
71
|
}
|
72
|
+
/**
|
73
|
+
* Creates a new `Stroke` from a {@link Path} and `style`. Strokes created
|
74
|
+
* with this method have transparent fill.
|
75
|
+
*
|
76
|
+
* Example:
|
77
|
+
* ```ts,runnable
|
78
|
+
* import { Editor, Stroke, Color4 } from 'js-draw';
|
79
|
+
* const editor = new Editor(document.body);
|
80
|
+
* ---visible---
|
81
|
+
* const stroke = Stroke.fromStroked('m0,0 l10,10', { width: 10, color: Color4.red });
|
82
|
+
* editor.dispatch(editor.image.addComponent(stroke));
|
83
|
+
* ```
|
84
|
+
* Notice that `path` can be a string that specifies an SVG path
|
85
|
+
*
|
86
|
+
* @see fromFilled
|
87
|
+
*/
|
88
|
+
static fromStroked(path, style) {
|
89
|
+
if (typeof path === 'string') {
|
90
|
+
path = Path.fromString(path);
|
91
|
+
}
|
92
|
+
return new Stroke([pathToRenderable(path, { fill: Color4.transparent, stroke: style })]);
|
93
|
+
}
|
94
|
+
/** @see fromStroked */
|
95
|
+
static fromFilled(path, fill) {
|
96
|
+
if (typeof path === 'string') {
|
97
|
+
path = Path.fromString(path);
|
98
|
+
}
|
99
|
+
return new Stroke([pathToRenderable(path, { fill })]);
|
100
|
+
}
|
72
101
|
getStyle() {
|
73
102
|
if (this.parts.length === 0) {
|
74
103
|
return {};
|
@@ -38,7 +38,7 @@ type TextElement = TextComponent | string;
|
|
38
38
|
* };
|
39
39
|
*
|
40
40
|
* editor.dispatch(
|
41
|
-
* editor.image.
|
41
|
+
* editor.image.addComponent(new TextComponent(['Hello, world'], positioning1, style)),
|
42
42
|
* );
|
43
43
|
*
|
44
44
|
*
|
@@ -49,7 +49,7 @@ type TextElement = TextComponent | string;
|
|
49
49
|
* // is placed directly after 'Test'.
|
50
50
|
* const positioning2 = Mat33.translation(Vec2.of(10, 50));
|
51
51
|
* editor.dispatch(
|
52
|
-
* editor.image.
|
52
|
+
* editor.image.addComponent(
|
53
53
|
* new TextComponent([ new TextComponent(['Test'], positioning1, style), '[Test]' ], positioning2, style)
|
54
54
|
* ),
|
55
55
|
* );
|
@@ -40,7 +40,7 @@ const defaultTextStyle = {
|
|
40
40
|
* };
|
41
41
|
*
|
42
42
|
* editor.dispatch(
|
43
|
-
* editor.image.
|
43
|
+
* editor.image.addComponent(new TextComponent(['Hello, world'], positioning1, style)),
|
44
44
|
* );
|
45
45
|
*
|
46
46
|
*
|
@@ -51,7 +51,7 @@ const defaultTextStyle = {
|
|
51
51
|
* // is placed directly after 'Test'.
|
52
52
|
* const positioning2 = Mat33.translation(Vec2.of(10, 50));
|
53
53
|
* editor.dispatch(
|
54
|
-
* editor.image.
|
54
|
+
* editor.image.addComponent(
|
55
55
|
* new TextComponent([ new TextComponent(['Test'], positioning1, style), '[Test]' ], positioning2, style)
|
56
56
|
* ),
|
57
57
|
* );
|