js-draw 0.13.1 → 0.15.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/.github/ISSUE_TEMPLATE/translation.yml +8 -0
- package/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +4 -0
- package/dist/src/Color4.js +22 -0
- package/dist/src/Editor.d.ts +2 -1
- package/dist/src/Editor.js +14 -5
- package/dist/src/EditorImage.d.ts +1 -0
- package/dist/src/EditorImage.js +11 -0
- package/dist/src/SVGLoader.js +8 -2
- package/dist/src/Viewport.d.ts +1 -0
- package/dist/src/Viewport.js +6 -3
- package/dist/src/commands/UnresolvedCommand.d.ts +14 -0
- package/dist/src/commands/UnresolvedCommand.js +22 -0
- package/dist/src/commands/uniteCommands.js +4 -2
- package/dist/src/components/AbstractComponent.d.ts +0 -1
- package/dist/src/components/AbstractComponent.js +30 -50
- package/dist/src/components/RestylableComponent.d.ts +24 -0
- package/dist/src/components/RestylableComponent.js +80 -0
- package/dist/src/components/Stroke.d.ts +8 -1
- package/dist/src/components/Stroke.js +49 -1
- package/dist/src/components/TextComponent.d.ts +10 -10
- package/dist/src/components/TextComponent.js +46 -13
- package/dist/src/components/lib.d.ts +2 -1
- package/dist/src/components/lib.js +2 -1
- package/dist/src/components/localization.d.ts +1 -0
- package/dist/src/components/localization.js +1 -0
- package/dist/src/math/Path.js +10 -3
- package/dist/src/rendering/TextRenderingStyle.d.ts +23 -0
- package/dist/src/rendering/TextRenderingStyle.js +20 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +1 -1
- package/dist/src/toolbar/IconProvider.d.ts +30 -3
- package/dist/src/toolbar/IconProvider.js +37 -2
- package/dist/src/toolbar/localization.d.ts +1 -0
- package/dist/src/toolbar/localization.js +1 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +10 -4
- package/dist/src/toolbar/widgets/InsertImageWidget.js +2 -1
- package/dist/src/toolbar/widgets/SelectionToolWidget.js +77 -1
- package/dist/src/tools/Pen.js +2 -2
- package/dist/src/tools/SelectionTool/SelectAllShortcutHandler.d.ts +8 -0
- package/dist/src/tools/SelectionTool/SelectAllShortcutHandler.js +22 -0
- package/dist/src/tools/SelectionTool/Selection.d.ts +6 -0
- package/dist/src/tools/SelectionTool/Selection.js +13 -4
- package/dist/src/tools/SelectionTool/SelectionTool.js +9 -12
- package/dist/src/tools/SelectionTool/TransformMode.js +1 -1
- package/dist/src/tools/TextTool.d.ts +1 -1
- package/dist/src/tools/ToolController.js +2 -0
- package/dist/src/tools/lib.d.ts +1 -0
- package/dist/src/tools/lib.js +1 -0
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/tools/localization.js +1 -0
- package/package.json +1 -1
- package/src/Color4.test.ts +4 -0
- package/src/Color4.ts +26 -0
- package/src/Editor.toSVG.test.ts +1 -1
- package/src/Editor.ts +16 -5
- package/src/EditorImage.ts +13 -0
- package/src/SVGLoader.ts +11 -3
- package/src/Viewport.ts +7 -3
- package/src/commands/UnresolvedCommand.ts +37 -0
- package/src/commands/uniteCommands.ts +5 -2
- package/src/components/AbstractComponent.ts +36 -61
- package/src/components/RestylableComponent.ts +142 -0
- package/src/components/Stroke.test.ts +68 -0
- package/src/components/Stroke.ts +68 -2
- package/src/components/TextComponent.test.ts +56 -2
- package/src/components/TextComponent.ts +63 -25
- package/src/components/lib.ts +4 -1
- package/src/components/localization.ts +3 -0
- package/src/math/Path.toString.test.ts +10 -0
- package/src/math/Path.ts +11 -3
- package/src/math/Rect2.test.ts +18 -6
- package/src/rendering/TextRenderingStyle.ts +38 -0
- package/src/rendering/renderers/AbstractRenderer.ts +1 -1
- package/src/rendering/renderers/CanvasRenderer.ts +2 -1
- package/src/rendering/renderers/DummyRenderer.ts +1 -1
- package/src/rendering/renderers/SVGRenderer.ts +1 -1
- package/src/rendering/renderers/TextOnlyRenderer.ts +1 -1
- package/src/toolbar/IconProvider.ts +40 -7
- package/src/toolbar/localization.ts +2 -0
- package/src/toolbar/toolbar.css +3 -0
- package/src/toolbar/widgets/BaseWidget.ts +12 -4
- package/src/toolbar/widgets/InsertImageWidget.ts +2 -1
- package/src/toolbar/widgets/SelectionToolWidget.ts +95 -1
- package/src/tools/PanZoom.test.ts +2 -1
- package/src/tools/PasteHandler.ts +1 -1
- package/src/tools/Pen.ts +2 -2
- package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +28 -0
- package/src/tools/SelectionTool/Selection.ts +17 -6
- package/src/tools/SelectionTool/SelectionTool.ts +9 -13
- package/src/tools/SelectionTool/TransformMode.ts +1 -1
- package/src/tools/TextTool.ts +2 -1
- package/src/tools/ToolController.ts +2 -0
- package/src/tools/lib.ts +1 -0
- package/src/tools/localization.ts +2 -0
@@ -77,6 +77,17 @@ export default class Selection {
|
|
77
77
|
const scaleAndTranslateMat = this.transform.rightMul(rotationMatrix.inverse());
|
78
78
|
return this.originalRegion.transformedBoundingBox(scaleAndTranslateMat);
|
79
79
|
}
|
80
|
+
/**
|
81
|
+
* Computes and returns the bounding box of the selection without
|
82
|
+
* any additional padding. Computes directly from the elements that are selected.
|
83
|
+
* @internal
|
84
|
+
*/
|
85
|
+
computeTightBoundingBox() {
|
86
|
+
const bbox = this.selectedElems.reduce((accumulator, elem) => {
|
87
|
+
return (accumulator !== null && accumulator !== void 0 ? accumulator : elem.getBBox()).union(elem.getBBox());
|
88
|
+
}, null);
|
89
|
+
return bbox !== null && bbox !== void 0 ? bbox : Rect2.empty;
|
90
|
+
}
|
80
91
|
get regionRotation() {
|
81
92
|
return this.transform.transformVec3(Vec2.unitX).angle();
|
82
93
|
}
|
@@ -161,9 +172,7 @@ export default class Selection {
|
|
161
172
|
// Recompute this' region from the selected elements.
|
162
173
|
// Returns false if the selection is empty.
|
163
174
|
recomputeRegion() {
|
164
|
-
const newRegion = this.
|
165
|
-
return (accumulator !== null && accumulator !== void 0 ? accumulator : elem.getBBox()).union(elem.getBBox());
|
166
|
-
}, null);
|
175
|
+
const newRegion = this.computeTightBoundingBox();
|
167
176
|
if (!newRegion) {
|
168
177
|
this.cancelSelection();
|
169
178
|
return false;
|
@@ -431,7 +440,7 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand {
|
|
431
440
|
this.resolveToElems(editor);
|
432
441
|
(_b = this.selection) === null || _b === void 0 ? void 0 : _b.setTransform(this.fullTransform.inverse(), false);
|
433
442
|
(_c = this.selection) === null || _c === void 0 ? void 0 : _c.updateUI();
|
434
|
-
yield editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize);
|
443
|
+
yield editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize, true);
|
435
444
|
(_d = this.selection) === null || _d === void 0 ? void 0 : _d.setTransform(Mat33.identity);
|
436
445
|
(_e = this.selection) === null || _e === void 0 ? void 0 : _e.recomputeRegion();
|
437
446
|
(_f = this.selection) === null || _f === void 0 ? void 0 : _f.updateUI();
|
@@ -47,10 +47,12 @@ export default class SelectionTool extends BaseTool {
|
|
47
47
|
snapSelectionToGrid() {
|
48
48
|
if (!this.selectionBox)
|
49
49
|
throw new Error('No selection to snap!');
|
50
|
-
|
51
|
-
const
|
50
|
+
// Snap the top left corner of what we have selected.
|
51
|
+
const topLeftOfBBox = this.selectionBox.computeTightBoundingBox().topLeft;
|
52
|
+
const snappedTopLeft = this.editor.viewport.snapToGrid(topLeftOfBBox);
|
53
|
+
const snapDelta = snappedTopLeft.minus(topLeftOfBBox);
|
52
54
|
const oldTransform = this.selectionBox.getTransform();
|
53
|
-
this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(
|
55
|
+
this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(snapDelta)));
|
54
56
|
this.selectionBox.finalizeTransform();
|
55
57
|
}
|
56
58
|
onPointerDown({ allPointers, current }) {
|
@@ -169,7 +171,7 @@ export default class SelectionTool extends BaseTool {
|
|
169
171
|
this.expandingSelectionBox = false;
|
170
172
|
}
|
171
173
|
onKeyPress(event) {
|
172
|
-
if (event.key === 'Control') {
|
174
|
+
if (event.key === 'Control' || event.key === 'Meta') {
|
173
175
|
this.ctrlKeyPressed = true;
|
174
176
|
return true;
|
175
177
|
}
|
@@ -179,8 +181,7 @@ export default class SelectionTool extends BaseTool {
|
|
179
181
|
return true;
|
180
182
|
}
|
181
183
|
else if (event.key === 'a' && event.ctrlKey) {
|
182
|
-
|
183
|
-
// Return early to prevent 'a' from moving the selection/view.
|
184
|
+
this.setSelection(this.editor.image.getAllElements());
|
184
185
|
return true;
|
185
186
|
}
|
186
187
|
else if (event.ctrlKey) {
|
@@ -266,7 +267,7 @@ export default class SelectionTool extends BaseTool {
|
|
266
267
|
return handled;
|
267
268
|
}
|
268
269
|
onKeyUp(evt) {
|
269
|
-
if (evt.key === 'Control') {
|
270
|
+
if (evt.key === 'Control' || evt.key === 'Meta') {
|
270
271
|
this.ctrlKeyPressed = false;
|
271
272
|
return true;
|
272
273
|
}
|
@@ -281,10 +282,6 @@ export default class SelectionTool extends BaseTool {
|
|
281
282
|
});
|
282
283
|
return true;
|
283
284
|
}
|
284
|
-
else if (evt.key === 'a') {
|
285
|
-
this.setSelection(this.editor.image.getAllElements());
|
286
|
-
return true;
|
287
|
-
}
|
288
285
|
}
|
289
286
|
if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
|
290
287
|
this.selectionBox.finalizeTransform();
|
@@ -392,5 +389,5 @@ SelectionTool.handleableKeys = [
|
|
392
389
|
'e', 'j', 'ArrowDown',
|
393
390
|
'r', 'R',
|
394
391
|
'i', 'I', 'o', 'O',
|
395
|
-
'Control',
|
392
|
+
'Control', 'Meta',
|
396
393
|
];
|
@@ -51,7 +51,7 @@ export class ResizeTransformer {
|
|
51
51
|
// Round: If this isn't done, scaling can create numbers with long decimal representations.
|
52
52
|
// long decimal representations => large file sizes.
|
53
53
|
scale = scale.map(component => Viewport.roundScaleRatio(component, 2));
|
54
|
-
if (scale.x
|
54
|
+
if (scale.x !== 0 && scale.y !== 0) {
|
55
55
|
const origin = this.editor.viewport.roundPoint(this.selection.preTransformRegion.topLeft);
|
56
56
|
this.selection.setTransform(Mat33.scaling2D(scale, origin));
|
57
57
|
}
|
@@ -1,9 +1,9 @@
|
|
1
1
|
import Color4 from '../Color4';
|
2
|
-
import { TextStyle } from '../components/TextComponent';
|
3
2
|
import Editor from '../Editor';
|
4
3
|
import { PointerEvt } from '../types';
|
5
4
|
import BaseTool from './BaseTool';
|
6
5
|
import { ToolLocalization } from './localization';
|
6
|
+
import TextStyle from '../rendering/TextRenderingStyle';
|
7
7
|
export default class TextTool extends BaseTool {
|
8
8
|
private editor;
|
9
9
|
private localizationTable;
|
@@ -13,6 +13,7 @@ import PasteHandler from './PasteHandler';
|
|
13
13
|
import ToolbarShortcutHandler from './ToolbarShortcutHandler';
|
14
14
|
import { makePressureSensitiveFreehandLineBuilder } from '../components/builders/PressureSensitiveFreehandLineBuilder';
|
15
15
|
import FindTool from './FindTool';
|
16
|
+
import SelectAllShortcutHandler from './SelectionTool/SelectAllShortcutHandler';
|
16
17
|
export default class ToolController {
|
17
18
|
/** @internal */
|
18
19
|
constructor(editor, localization) {
|
@@ -43,6 +44,7 @@ export default class ToolController {
|
|
43
44
|
new ToolSwitcherShortcut(editor),
|
44
45
|
new FindTool(editor),
|
45
46
|
new PasteHandler(editor),
|
47
|
+
new SelectAllShortcutHandler(editor),
|
46
48
|
];
|
47
49
|
primaryTools.forEach(tool => tool.setToolGroup(primaryToolGroup));
|
48
50
|
panZoomTool.setEnabled(true);
|
package/dist/src/tools/lib.d.ts
CHANGED
@@ -10,6 +10,7 @@ export { default as PanZoomTool, PanZoomMode } from './PanZoom';
|
|
10
10
|
export { default as PenTool, PenStyle } from './Pen';
|
11
11
|
export { default as TextTool } from './TextTool';
|
12
12
|
export { default as SelectionTool } from './SelectionTool/SelectionTool';
|
13
|
+
export { default as SelectAllShortcutHandler } from './SelectionTool/SelectAllShortcutHandler';
|
13
14
|
export { default as EraserTool } from './Eraser';
|
14
15
|
export { default as PasteHandler } from './PasteHandler';
|
15
16
|
export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
|
package/dist/src/tools/lib.js
CHANGED
@@ -10,6 +10,7 @@ export { default as PanZoomTool, PanZoomMode } from './PanZoom';
|
|
10
10
|
export { default as PenTool } from './Pen';
|
11
11
|
export { default as TextTool } from './TextTool';
|
12
12
|
export { default as SelectionTool } from './SelectionTool/SelectionTool';
|
13
|
+
export { default as SelectAllShortcutHandler } from './SelectionTool/SelectAllShortcutHandler';
|
13
14
|
export { default as EraserTool } from './Eraser';
|
14
15
|
export { default as PasteHandler } from './PasteHandler';
|
15
16
|
export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
|
package/package.json
CHANGED
package/src/Color4.test.ts
CHANGED
package/src/Color4.ts
CHANGED
@@ -151,6 +151,32 @@ export default class Color4 {
|
|
151
151
|
);
|
152
152
|
}
|
153
153
|
|
154
|
+
/**
|
155
|
+
* @returns the component-wise average of `colors`, or `Color4.transparent` if `colors` is empty.
|
156
|
+
*/
|
157
|
+
public static average(colors: Color4[]) {
|
158
|
+
let averageA = 0;
|
159
|
+
let averageR = 0;
|
160
|
+
let averageG = 0;
|
161
|
+
let averageB = 0;
|
162
|
+
|
163
|
+
for (const color of colors) {
|
164
|
+
averageA += color.a;
|
165
|
+
averageR += color.r;
|
166
|
+
averageG += color.g;
|
167
|
+
averageB += color.b;
|
168
|
+
}
|
169
|
+
|
170
|
+
if (colors.length > 0) {
|
171
|
+
averageA /= colors.length;
|
172
|
+
averageR /= colors.length;
|
173
|
+
averageG /= colors.length;
|
174
|
+
averageB /= colors.length;
|
175
|
+
}
|
176
|
+
|
177
|
+
return new Color4(averageR, averageG, averageB, averageA);
|
178
|
+
}
|
179
|
+
|
154
180
|
private hexString: string|null = null;
|
155
181
|
|
156
182
|
/**
|
package/src/Editor.toSVG.test.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
import { TextStyle } from './components/TextComponent';
|
2
1
|
import { Color4, Mat33, Rect2, TextComponent, EditorImage, Vec2 } from './lib';
|
2
|
+
import TextStyle from './rendering/TextRenderingStyle';
|
3
3
|
import SVGLoader from './SVGLoader';
|
4
4
|
import createEditor from './testing/createEditor';
|
5
5
|
|
package/src/Editor.ts
CHANGED
@@ -315,7 +315,7 @@ export class Editor {
|
|
315
315
|
|
316
316
|
// Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
|
317
317
|
// pinch-zooming.
|
318
|
-
if (!evt.ctrlKey) {
|
318
|
+
if (!evt.ctrlKey && !evt.metaKey) {
|
319
319
|
if (!this.settings.wheelEventsEnabled) {
|
320
320
|
return;
|
321
321
|
} else if (this.settings.wheelEventsEnabled === 'only-if-focused') {
|
@@ -333,7 +333,7 @@ export class Editor {
|
|
333
333
|
delta = delta.times(100);
|
334
334
|
}
|
335
335
|
|
336
|
-
if (evt.ctrlKey) {
|
336
|
+
if (evt.ctrlKey || evt.metaKey) {
|
337
337
|
delta = Vec3.of(0, 0, evt.deltaY);
|
338
338
|
}
|
339
339
|
|
@@ -598,7 +598,7 @@ export class Editor {
|
|
598
598
|
} else if (this.toolController.dispatchInputEvent({
|
599
599
|
kind: InputEvtType.KeyPressEvent,
|
600
600
|
key: evt.key,
|
601
|
-
ctrlKey: evt.ctrlKey,
|
601
|
+
ctrlKey: evt.ctrlKey || evt.metaKey,
|
602
602
|
altKey: evt.altKey,
|
603
603
|
})) {
|
604
604
|
evt.preventDefault();
|
@@ -611,7 +611,7 @@ export class Editor {
|
|
611
611
|
if (this.toolController.dispatchInputEvent({
|
612
612
|
kind: InputEvtType.KeyUpEvent,
|
613
613
|
key: evt.key,
|
614
|
-
ctrlKey: evt.ctrlKey,
|
614
|
+
ctrlKey: evt.ctrlKey || evt.metaKey,
|
615
615
|
altKey: evt.altKey,
|
616
616
|
})) {
|
617
617
|
evt.preventDefault();
|
@@ -704,8 +704,14 @@ export class Editor {
|
|
704
704
|
return this.asyncApplyOrUnapplyCommands(commands, true, chunkSize);
|
705
705
|
}
|
706
706
|
|
707
|
+
// If `unapplyInReverseOrder`, commands are reversed before unapplying.
|
707
708
|
// @see {@link #asyncApplyOrUnapplyCommands }
|
708
|
-
public asyncUnapplyCommands(commands: Command[], chunkSize: number) {
|
709
|
+
public asyncUnapplyCommands(commands: Command[], chunkSize: number, unapplyInReverseOrder: boolean = false) {
|
710
|
+
if (unapplyInReverseOrder) {
|
711
|
+
commands = [ ...commands ]; // copy
|
712
|
+
commands.reverse();
|
713
|
+
}
|
714
|
+
|
709
715
|
return this.asyncApplyOrUnapplyCommands(commands, false, chunkSize);
|
710
716
|
}
|
711
717
|
|
@@ -745,6 +751,11 @@ export class Editor {
|
|
745
751
|
});
|
746
752
|
}
|
747
753
|
|
754
|
+
// @internal
|
755
|
+
public isRerenderQueued() {
|
756
|
+
return this.rerenderQueued;
|
757
|
+
}
|
758
|
+
|
748
759
|
public rerender(showImageBounds: boolean = true) {
|
749
760
|
this.display.startRerender();
|
750
761
|
|
package/src/EditorImage.ts
CHANGED
@@ -34,6 +34,19 @@ export default class EditorImage {
|
|
34
34
|
return null;
|
35
35
|
}
|
36
36
|
|
37
|
+
// Forces a re-render of `elem` when the image is next re-rendered as a whole.
|
38
|
+
// Does nothing if `elem` is not in this.
|
39
|
+
public queueRerenderOf(elem: AbstractComponent) {
|
40
|
+
// TODO: Make more efficient (e.g. increase IDs of all parents,
|
41
|
+
// make cache take into account last modified time instead of IDs, etc.)
|
42
|
+
const parent = this.findParent(elem);
|
43
|
+
|
44
|
+
if (parent) {
|
45
|
+
parent.remove();
|
46
|
+
this.addElementDirectly(elem);
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
37
50
|
/** @internal */
|
38
51
|
public renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport) {
|
39
52
|
cache.render(screenRenderer, this.root, viewport);
|
package/src/SVGLoader.ts
CHANGED
@@ -3,7 +3,7 @@ import AbstractComponent from './components/AbstractComponent';
|
|
3
3
|
import ImageComponent from './components/ImageComponent';
|
4
4
|
import Stroke from './components/Stroke';
|
5
5
|
import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
6
|
-
import TextComponent
|
6
|
+
import TextComponent from './components/TextComponent';
|
7
7
|
import UnknownSVGObject from './components/UnknownSVGObject';
|
8
8
|
import Mat33 from './math/Mat33';
|
9
9
|
import Path from './math/Path';
|
@@ -11,6 +11,7 @@ import Rect2 from './math/Rect2';
|
|
11
11
|
import { Vec2 } from './math/Vec2';
|
12
12
|
import { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
|
13
13
|
import RenderingStyle from './rendering/RenderingStyle';
|
14
|
+
import TextStyle from './rendering/TextRenderingStyle';
|
14
15
|
import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
|
15
16
|
|
16
17
|
type OnFinishListener = ()=> void;
|
@@ -244,7 +245,14 @@ export default class SVGLoader implements ImageLoader {
|
|
244
245
|
|
245
246
|
// Compute styles.
|
246
247
|
const computedStyles = window.getComputedStyle(elem);
|
247
|
-
const
|
248
|
+
const fontSizeExp = /^([-0-9.e]+)px/i;
|
249
|
+
|
250
|
+
// In some environments, computedStyles.fontSize can be increased by the system.
|
251
|
+
// Thus, to prevent text from growing on load/save, prefer .style.fontSize.
|
252
|
+
let fontSizeMatch = fontSizeExp.exec(elem.style.fontSize);
|
253
|
+
if (!fontSizeMatch) {
|
254
|
+
fontSizeMatch = fontSizeExp.exec(computedStyles.fontSize);
|
255
|
+
}
|
248
256
|
|
249
257
|
const supportedStyleAttrs = [
|
250
258
|
'fontFamily',
|
@@ -455,7 +463,7 @@ export default class SVGLoader implements ImageLoader {
|
|
455
463
|
<meta name='viewport' conent='width=device-width,initial-scale=1.0'/>
|
456
464
|
<meta charset='utf-8'/>
|
457
465
|
</head>
|
458
|
-
<body>
|
466
|
+
<body style='font-size: 12px;'>
|
459
467
|
<script>
|
460
468
|
console.error('JavaScript should not be able to run here!');
|
461
469
|
throw new Error(
|
package/src/Viewport.ts
CHANGED
@@ -157,14 +157,18 @@ export class Viewport {
|
|
157
157
|
* should return `100` because `100` is the nearest power of 10 to 101.
|
158
158
|
*/
|
159
159
|
public getScaleFactorToNearestPowerOfTen() {
|
160
|
+
return this.getScaleFactorToNearestPowerOf(10);
|
161
|
+
}
|
162
|
+
|
163
|
+
private getScaleFactorToNearestPowerOf(powerOf: number) {
|
160
164
|
const scaleFactor = this.getScaleFactor();
|
161
|
-
return Math.pow(
|
165
|
+
return Math.pow(powerOf, Math.round(Math.log(scaleFactor) / Math.log(powerOf)));
|
162
166
|
}
|
163
167
|
|
164
168
|
public snapToGrid(canvasPos: Point2) {
|
165
169
|
const snapCoordinate = (coordinate: number) => {
|
166
|
-
const scaleFactor = this.
|
167
|
-
const roundFactor = scaleFactor /
|
170
|
+
const scaleFactor = this.getScaleFactorToNearestPowerOf(2);
|
171
|
+
const roundFactor = scaleFactor / 50;
|
168
172
|
const snapped = Math.round(coordinate * roundFactor) / roundFactor;
|
169
173
|
|
170
174
|
return snapped;
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import EditorImage from '../EditorImage';
|
2
|
+
import AbstractComponent from '../components/AbstractComponent';
|
3
|
+
import SerializableCommand from './SerializableCommand';
|
4
|
+
|
5
|
+
export type ResolveFromComponentCallback = () => SerializableCommand;
|
6
|
+
|
7
|
+
/**
|
8
|
+
* A command that requires a component that may or may not be present in the editor when
|
9
|
+
* the command is created.
|
10
|
+
*/
|
11
|
+
export default abstract class UnresolvedSerializableCommand extends SerializableCommand {
|
12
|
+
protected component: AbstractComponent|null;
|
13
|
+
protected readonly componentID: string;
|
14
|
+
|
15
|
+
protected constructor(
|
16
|
+
commandId: string,
|
17
|
+
componentID: string,
|
18
|
+
component?: AbstractComponent
|
19
|
+
) {
|
20
|
+
super(commandId);
|
21
|
+
this.component = component ?? null;
|
22
|
+
this.componentID = componentID;
|
23
|
+
}
|
24
|
+
|
25
|
+
protected resolveComponent(image: EditorImage) {
|
26
|
+
if (this.component) {
|
27
|
+
return;
|
28
|
+
}
|
29
|
+
|
30
|
+
const component = image.lookupElement(this.componentID);
|
31
|
+
if (!component) {
|
32
|
+
throw new Error(`Unable to resolve component with ID ${this.componentID}`);
|
33
|
+
}
|
34
|
+
|
35
|
+
this.component = component;
|
36
|
+
}
|
37
|
+
}
|
@@ -32,11 +32,14 @@ class NonSerializableUnion extends Command {
|
|
32
32
|
}
|
33
33
|
|
34
34
|
public unapply(editor: Editor) {
|
35
|
+
const commands = [ ...this.commands ];
|
36
|
+
commands.reverse();
|
37
|
+
|
35
38
|
if (this.applyChunkSize === undefined) {
|
36
|
-
const results =
|
39
|
+
const results = commands.map(cmd => cmd.unapply(editor));
|
37
40
|
return NonSerializableUnion.waitForAll(results);
|
38
41
|
} else {
|
39
|
-
return editor.asyncUnapplyCommands(
|
42
|
+
return editor.asyncUnapplyCommands(commands, this.applyChunkSize, false);
|
40
43
|
}
|
41
44
|
}
|
42
45
|
|
@@ -7,6 +7,7 @@ import Rect2 from '../math/Rect2';
|
|
7
7
|
import { EditorLocalization } from '../localization';
|
8
8
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
9
9
|
import { ImageComponentLocalization } from './localization';
|
10
|
+
import UnresolvedSerializableCommand from '../commands/UnresolvedCommand';
|
10
11
|
|
11
12
|
export type LoadSaveData = (string[]|Record<symbol, string|number>);
|
12
13
|
export type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
|
@@ -132,12 +133,12 @@ export default abstract class AbstractComponent {
|
|
132
133
|
// Returns a command that, when applied, transforms this by [affineTransfm] and
|
133
134
|
// updates the editor.
|
134
135
|
public transformBy(affineTransfm: Mat33): SerializableCommand {
|
135
|
-
return new AbstractComponent.TransformElementCommand(affineTransfm, this);
|
136
|
+
return new AbstractComponent.TransformElementCommand(affineTransfm, this.getId(), this);
|
136
137
|
}
|
137
138
|
|
138
139
|
// Returns a command that updates this component's z-index.
|
139
140
|
public setZIndex(newZIndex: number): SerializableCommand {
|
140
|
-
return new AbstractComponent.TransformElementCommand(Mat33.identity, this, newZIndex);
|
141
|
+
return new AbstractComponent.TransformElementCommand(Mat33.identity, this.getId(), this, newZIndex);
|
141
142
|
}
|
142
143
|
|
143
144
|
// @returns true iff this component can be selected (e.g. by the selection tool.)
|
@@ -154,69 +155,42 @@ export default abstract class AbstractComponent {
|
|
154
155
|
|
155
156
|
private static transformElementCommandId = 'transform-element';
|
156
157
|
|
157
|
-
private static
|
158
|
-
private
|
158
|
+
private static TransformElementCommand = class extends UnresolvedSerializableCommand {
|
159
|
+
private origZIndex: number|null = null;
|
160
|
+
private targetZIndex: number;
|
159
161
|
|
162
|
+
// Construct a new TransformElementCommand. `component`, while optional, should
|
163
|
+
// be provided if available. If not provided, it will be fetched from the editor's
|
164
|
+
// document when the command is applied.
|
160
165
|
public constructor(
|
161
166
|
private affineTransfm: Mat33,
|
162
|
-
|
163
|
-
|
167
|
+
componentID: string,
|
168
|
+
component?: AbstractComponent,
|
169
|
+
targetZIndex?: number,
|
164
170
|
) {
|
165
|
-
super(AbstractComponent.transformElementCommandId);
|
166
|
-
|
167
|
-
|
168
|
-
private resolveCommand(editor: Editor) {
|
169
|
-
if (this.command) {
|
170
|
-
return;
|
171
|
-
}
|
171
|
+
super(AbstractComponent.transformElementCommandId, componentID, component);
|
172
|
+
this.targetZIndex = targetZIndex ?? AbstractComponent.zIndexCounter++;
|
172
173
|
|
173
|
-
|
174
|
-
if (
|
175
|
-
|
174
|
+
// Ensure that we keep drawing on top even after changing the z-index.
|
175
|
+
if (this.targetZIndex >= AbstractComponent.zIndexCounter) {
|
176
|
+
AbstractComponent.zIndexCounter = this.targetZIndex + 1;
|
176
177
|
}
|
177
|
-
this.command = new AbstractComponent.TransformElementCommand(
|
178
|
-
this.affineTransfm, component, this.targetZIndex
|
179
|
-
);
|
180
|
-
}
|
181
|
-
|
182
|
-
public apply(editor: Editor) {
|
183
|
-
this.resolveCommand(editor);
|
184
|
-
this.command!.apply(editor);
|
185
|
-
}
|
186
|
-
|
187
|
-
public unapply(editor: Editor) {
|
188
|
-
this.resolveCommand(editor);
|
189
|
-
this.command!.unapply(editor);
|
190
|
-
}
|
191
|
-
|
192
|
-
public description(_editor: Editor, localizationTable: EditorLocalization) {
|
193
|
-
return localizationTable.transformedElements(1);
|
194
|
-
}
|
195
|
-
|
196
|
-
protected serializeToJSON() {
|
197
|
-
return {
|
198
|
-
id: this.componentID,
|
199
|
-
transfm: this.affineTransfm.toArray(),
|
200
|
-
targetZIndex: this.targetZIndex,
|
201
|
-
};
|
202
178
|
}
|
203
|
-
};
|
204
179
|
|
205
|
-
|
206
|
-
|
207
|
-
|
180
|
+
protected resolveComponent(image: EditorImage): void {
|
181
|
+
if (this.component) {
|
182
|
+
return;
|
183
|
+
}
|
208
184
|
|
209
|
-
|
210
|
-
|
211
|
-
private component: AbstractComponent,
|
212
|
-
targetZIndex?: number,
|
213
|
-
) {
|
214
|
-
super(AbstractComponent.transformElementCommandId);
|
215
|
-
this.origZIndex = component.zIndex;
|
216
|
-
this.targetZIndex = targetZIndex ?? AbstractComponent.zIndexCounter++;
|
185
|
+
super.resolveComponent(image);
|
186
|
+
this.origZIndex = this.component!.getZIndex();
|
217
187
|
}
|
218
188
|
|
219
189
|
private updateTransform(editor: Editor, newTransfm: Mat33) {
|
190
|
+
if (!this.component) {
|
191
|
+
throw new Error('this.component is undefined or null!');
|
192
|
+
}
|
193
|
+
|
220
194
|
// Any parent should have only one direct child.
|
221
195
|
const parent = editor.image.findParent(this.component);
|
222
196
|
let hadParent = false;
|
@@ -235,13 +209,17 @@ export default abstract class AbstractComponent {
|
|
235
209
|
}
|
236
210
|
|
237
211
|
public apply(editor: Editor) {
|
238
|
-
this.
|
212
|
+
this.resolveComponent(editor.image);
|
213
|
+
|
214
|
+
this.component!.zIndex = this.targetZIndex;
|
239
215
|
this.updateTransform(editor, this.affineTransfm);
|
240
216
|
editor.queueRerender();
|
241
217
|
}
|
242
218
|
|
243
219
|
public unapply(editor: Editor) {
|
244
|
-
this.
|
220
|
+
this.resolveComponent(editor.image);
|
221
|
+
|
222
|
+
this.component!.zIndex = this.origZIndex!;
|
245
223
|
this.updateTransform(editor, this.affineTransfm.inverse());
|
246
224
|
editor.queueRerender();
|
247
225
|
}
|
@@ -252,16 +230,13 @@ export default abstract class AbstractComponent {
|
|
252
230
|
|
253
231
|
static {
|
254
232
|
SerializableCommand.register(AbstractComponent.transformElementCommandId, (json: any, editor: Editor) => {
|
255
|
-
const elem = editor.image.lookupElement(json.id);
|
233
|
+
const elem = editor.image.lookupElement(json.id) ?? undefined;
|
256
234
|
const transform = new Mat33(...(json.transfm as Mat33Array));
|
257
235
|
const targetZIndex = json.targetZIndex;
|
258
236
|
|
259
|
-
if (!elem) {
|
260
|
-
return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id, targetZIndex);
|
261
|
-
}
|
262
|
-
|
263
237
|
return new AbstractComponent.TransformElementCommand(
|
264
238
|
transform,
|
239
|
+
json.id,
|
265
240
|
elem,
|
266
241
|
targetZIndex,
|
267
242
|
);
|
@@ -270,7 +245,7 @@ export default abstract class AbstractComponent {
|
|
270
245
|
|
271
246
|
protected serializeToJSON() {
|
272
247
|
return {
|
273
|
-
id: this.
|
248
|
+
id: this.componentID,
|
274
249
|
transfm: this.affineTransfm.toArray(),
|
275
250
|
targetZIndex: this.targetZIndex,
|
276
251
|
};
|