js-draw 1.19.1 → 1.20.1
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +52 -1
- package/dist/Editor.css +49 -7
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/SVGLoader/index.js +3 -1
- package/dist/cjs/image/EditorImage.d.ts +2 -1
- package/dist/cjs/image/EditorImage.js +101 -5
- package/dist/cjs/rendering/renderers/CanvasRenderer.js +4 -4
- package/dist/cjs/toolbar/localization.d.ts +1 -0
- package/dist/cjs/toolbar/localization.js +1 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/ImageWrapper.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/ImageWrapper.js +7 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/index.js +11 -3
- package/dist/cjs/toolbar/widgets/components/makeFileInput.js +12 -1
- package/dist/cjs/toolbar/widgets/components/makeSnappedList.js +69 -4
- package/dist/cjs/tools/Eraser.js +22 -5
- package/dist/cjs/tools/PanZoom.d.ts +54 -0
- package/dist/cjs/tools/PanZoom.js +54 -2
- package/dist/cjs/util/ReactiveValue.d.ts +4 -0
- package/dist/cjs/util/ReactiveValue.js +5 -0
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/SVGLoader/index.mjs +3 -1
- package/dist/mjs/image/EditorImage.d.ts +2 -1
- package/dist/mjs/image/EditorImage.mjs +101 -5
- package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +4 -4
- package/dist/mjs/toolbar/localization.d.ts +1 -0
- package/dist/mjs/toolbar/localization.mjs +1 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/ImageWrapper.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/ImageWrapper.mjs +7 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/index.mjs +11 -3
- package/dist/mjs/toolbar/widgets/components/makeFileInput.mjs +12 -1
- package/dist/mjs/toolbar/widgets/components/makeSnappedList.mjs +69 -4
- package/dist/mjs/tools/Eraser.mjs +22 -5
- package/dist/mjs/tools/PanZoom.d.ts +54 -0
- package/dist/mjs/tools/PanZoom.mjs +54 -2
- package/dist/mjs/util/ReactiveValue.d.ts +4 -0
- package/dist/mjs/util/ReactiveValue.mjs +5 -0
- package/dist/mjs/version.mjs +1 -1
- package/docs/img/readme-images/unsupported-elements--in-editor.png +0 -0
- package/package.json +2 -2
- package/src/toolbar/EdgeToolbar.scss +8 -3
- package/src/toolbar/widgets/components/makeFileInput.scss +3 -2
- package/src/toolbar/widgets/components/makeSnappedList.scss +58 -12
@@ -11,8 +11,72 @@ const ReactiveValue_1 = require("../../../util/ReactiveValue");
|
|
11
11
|
const makeSnappedList = (itemsValue) => {
|
12
12
|
const container = document.createElement('div');
|
13
13
|
container.classList.add('toolbar-snapped-scroll-list');
|
14
|
+
const scroller = document.createElement('div');
|
15
|
+
scroller.classList.add('scroller');
|
14
16
|
const visibleIndex = ReactiveValue_1.MutableReactiveValue.fromInitialValue(0);
|
15
17
|
let observer = null;
|
18
|
+
const makePageMarkers = () => {
|
19
|
+
const markerContainer = document.createElement('div');
|
20
|
+
markerContainer.classList.add('page-markers');
|
21
|
+
// Keyboard focus should go to the main scrolling list.
|
22
|
+
// TODO: Does it make sense for the page marker list to be focusable?
|
23
|
+
markerContainer.setAttribute('tabindex', '-1');
|
24
|
+
const markers = [];
|
25
|
+
const pairedItems = ReactiveValue_1.ReactiveValue.union([visibleIndex, itemsValue]);
|
26
|
+
pairedItems.onUpdateAndNow(([currentVisibleIndex, items]) => {
|
27
|
+
let addedOrRemovedMarkers = false;
|
28
|
+
// Items may have been removed from the list of pages. Make the markers reflect that.
|
29
|
+
while (items.length < markers.length) {
|
30
|
+
markers.pop();
|
31
|
+
addedOrRemovedMarkers = true;
|
32
|
+
}
|
33
|
+
let activeMarker;
|
34
|
+
for (let i = 0; i < items.length; i++) {
|
35
|
+
let marker;
|
36
|
+
if (i >= markers.length) {
|
37
|
+
marker = document.createElement('div');
|
38
|
+
// Use a separate content element to increase the clickable size of
|
39
|
+
// the marker.
|
40
|
+
const content = document.createElement('div');
|
41
|
+
content.classList.add('content');
|
42
|
+
marker.replaceChildren(content);
|
43
|
+
markers.push(marker);
|
44
|
+
addedOrRemovedMarkers = true;
|
45
|
+
}
|
46
|
+
else {
|
47
|
+
marker = markers[i];
|
48
|
+
}
|
49
|
+
marker.classList.add('marker');
|
50
|
+
if (i === currentVisibleIndex) {
|
51
|
+
marker.classList.add('-active');
|
52
|
+
activeMarker = marker;
|
53
|
+
}
|
54
|
+
else {
|
55
|
+
marker.classList.remove('-active');
|
56
|
+
}
|
57
|
+
const markerIndex = i;
|
58
|
+
marker.onclick = () => {
|
59
|
+
wrappedItems.get()[markerIndex]?.element?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
60
|
+
};
|
61
|
+
}
|
62
|
+
// Only call .replaceChildren when necessary -- doing so on every change would
|
63
|
+
// break transitions.
|
64
|
+
if (addedOrRemovedMarkers) {
|
65
|
+
markerContainer.replaceChildren(...markers);
|
66
|
+
}
|
67
|
+
// Handles the case where there are many markers and the current is offscreen
|
68
|
+
if (activeMarker && markerContainer.scrollHeight > container.clientHeight) {
|
69
|
+
activeMarker.scrollIntoView({ block: 'nearest' });
|
70
|
+
}
|
71
|
+
if (markers.length === 1) {
|
72
|
+
markerContainer.classList.add('-one-element');
|
73
|
+
}
|
74
|
+
else {
|
75
|
+
markerContainer.classList.remove('-one-element');
|
76
|
+
}
|
77
|
+
});
|
78
|
+
return markerContainer;
|
79
|
+
};
|
16
80
|
const createObserver = () => {
|
17
81
|
observer = new IntersectionObserver((entries) => {
|
18
82
|
for (const entry of entries) {
|
@@ -28,7 +92,7 @@ const makeSnappedList = (itemsValue) => {
|
|
28
92
|
}, {
|
29
93
|
// Element to use as the boudning box with which to intersect.
|
30
94
|
// See https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
|
31
|
-
root:
|
95
|
+
root: scroller,
|
32
96
|
// Fraction of an element that must be visible to trigger the callback:
|
33
97
|
threshold: 0.9,
|
34
98
|
});
|
@@ -60,7 +124,7 @@ const makeSnappedList = (itemsValue) => {
|
|
60
124
|
for (const item of lastItems) {
|
61
125
|
observer?.unobserve(item.element);
|
62
126
|
}
|
63
|
-
|
127
|
+
scroller.replaceChildren();
|
64
128
|
// An observer is only necessary if there are multiple items to scroll through.
|
65
129
|
if (items.length > 1) {
|
66
130
|
createObserver();
|
@@ -76,7 +140,7 @@ const makeSnappedList = (itemsValue) => {
|
|
76
140
|
container.classList.remove('-empty');
|
77
141
|
}
|
78
142
|
for (const item of items) {
|
79
|
-
|
143
|
+
scroller.appendChild(item.element);
|
80
144
|
}
|
81
145
|
visibleIndex.set(0);
|
82
146
|
if (observer) {
|
@@ -94,7 +158,8 @@ const makeSnappedList = (itemsValue) => {
|
|
94
158
|
});
|
95
159
|
// makeSnappedList is generally shown within the toolbar. This allows users to
|
96
160
|
// scroll it with a touchpad.
|
97
|
-
(0, stopPropagationOfScrollingWheelEvents_1.default)(
|
161
|
+
(0, stopPropagationOfScrollingWheelEvents_1.default)(scroller);
|
162
|
+
container.replaceChildren(makePageMarkers(), scroller);
|
98
163
|
return {
|
99
164
|
container,
|
100
165
|
visibleItem,
|
package/dist/cjs/tools/Eraser.js
CHANGED
@@ -71,6 +71,7 @@ class Eraser extends BaseTool_1.default {
|
|
71
71
|
this.editor = editor;
|
72
72
|
this.lastPoint = null;
|
73
73
|
this.isFirstEraseEvt = true;
|
74
|
+
this.toAdd = new Set();
|
74
75
|
// Commands that each remove one element
|
75
76
|
this.eraseCommands = [];
|
76
77
|
this.addCommands = [];
|
@@ -180,15 +181,17 @@ class Eraser extends BaseTool_1.default {
|
|
180
181
|
newAddCommands.forEach(command => command.apply(this.editor));
|
181
182
|
const finalToErase = [];
|
182
183
|
for (const item of toErase) {
|
183
|
-
if (this.toAdd.
|
184
|
-
this.toAdd
|
184
|
+
if (this.toAdd.has(item)) {
|
185
|
+
this.toAdd.delete(item);
|
185
186
|
}
|
186
187
|
else {
|
187
188
|
finalToErase.push(item);
|
188
189
|
}
|
189
190
|
}
|
190
191
|
this.toRemove.push(...finalToErase);
|
191
|
-
|
192
|
+
for (const item of toAdd) {
|
193
|
+
this.toAdd.add(item);
|
194
|
+
}
|
192
195
|
this.eraseCommands.push(new Erase_1.default(finalToErase));
|
193
196
|
this.addCommands.push(...newAddCommands);
|
194
197
|
}
|
@@ -199,7 +202,7 @@ class Eraser extends BaseTool_1.default {
|
|
199
202
|
if (event.allPointers.length === 1 || event.current.device === Pointer_1.PointerDevice.Eraser) {
|
200
203
|
this.lastPoint = event.current.canvasPos;
|
201
204
|
this.toRemove = [];
|
202
|
-
this.toAdd
|
205
|
+
this.toAdd.clear();
|
203
206
|
this.isFirstEraseEvt = true;
|
204
207
|
this.drawPreviewAt(event.current.canvasPos);
|
205
208
|
return true;
|
@@ -215,7 +218,21 @@ class Eraser extends BaseTool_1.default {
|
|
215
218
|
const commands = [];
|
216
219
|
if (this.addCommands.length > 0) {
|
217
220
|
this.addCommands.forEach(cmd => cmd.unapply(this.editor));
|
218
|
-
|
221
|
+
// Remove items from toAdd that are also present in toRemove -- adding, then
|
222
|
+
// removing these does nothing, and can break undo/redo.
|
223
|
+
for (const item of this.toAdd) {
|
224
|
+
if (this.toRemove.includes(item)) {
|
225
|
+
this.toAdd.delete(item);
|
226
|
+
this.toRemove = this.toRemove.filter(other => other !== item);
|
227
|
+
}
|
228
|
+
}
|
229
|
+
for (const item of this.toRemove) {
|
230
|
+
if (this.toAdd.has(item)) {
|
231
|
+
this.toAdd.delete(item);
|
232
|
+
this.toRemove = this.toRemove.filter(other => other !== item);
|
233
|
+
}
|
234
|
+
}
|
235
|
+
commands.push(...[...this.toAdd].map(a => EditorImage_1.default.addElement(a)));
|
219
236
|
this.addCommands = [];
|
220
237
|
}
|
221
238
|
if (this.eraseCommands.length > 0) {
|
@@ -10,13 +10,27 @@ interface PinchData {
|
|
10
10
|
dist: number;
|
11
11
|
}
|
12
12
|
export declare enum PanZoomMode {
|
13
|
+
/** Touch gestures with a single pointer. Ignores non-touch gestures. */
|
13
14
|
OneFingerTouchGestures = 1,
|
15
|
+
/** Touch gestures with exactly two pointers. Ignores non-touch gestures. */
|
14
16
|
TwoFingerTouchGestures = 2,
|
15
17
|
RightClickDrags = 4,
|
18
|
+
/** Single-pointer gestures of *any* type (including touch). */
|
16
19
|
SinglePointerGestures = 8,
|
20
|
+
/** Keyboard navigation (e.g. LeftArrow to move left). */
|
17
21
|
Keyboard = 16,
|
22
|
+
/** If provided, prevents **this** tool from rotating the viewport (other tools may still do so). */
|
18
23
|
RotationLocked = 32
|
19
24
|
}
|
25
|
+
/**
|
26
|
+
* This tool moves the viewport in response to touchpad, touchscreen, mouse, and keyboard events.
|
27
|
+
*
|
28
|
+
* Which events are handled, and which are skipped, are determined by the tool's `mode`. For example,
|
29
|
+
* a `PanZoom` tool with `mode = PanZoomMode.TwoFingerTouchGestures|PanZoomMode.RightClickDrags` would
|
30
|
+
* respond to right-click drag events and two-finger touch gestures.
|
31
|
+
*
|
32
|
+
* @see {@link setModeEnabled}
|
33
|
+
*/
|
20
34
|
export default class PanZoom extends BaseTool {
|
21
35
|
private editor;
|
22
36
|
private mode;
|
@@ -58,8 +72,48 @@ export default class PanZoom extends BaseTool {
|
|
58
72
|
onWheel({ delta, screenPos }: WheelEvt): boolean;
|
59
73
|
onKeyPress(event: KeyPressEvent): boolean;
|
60
74
|
private isRotationLocked;
|
75
|
+
/**
|
76
|
+
* Changes the types of gestures used by this pan/zoom tool.
|
77
|
+
*
|
78
|
+
* @see {@link PanZoomMode} {@link setMode}
|
79
|
+
*
|
80
|
+
* @example
|
81
|
+
* ```ts,runnable
|
82
|
+
* import { Editor, PanZoomTool, PanZoomMode } from 'js-draw';
|
83
|
+
*
|
84
|
+
* const editor = new Editor(document.body);
|
85
|
+
*
|
86
|
+
* // By default, there are multiple PanZoom tools that handle different events.
|
87
|
+
* // This gets all PanZoomTools.
|
88
|
+
* const panZoomToolList = editor.toolController.getMatchingTools(PanZoomTool);
|
89
|
+
*
|
90
|
+
* // The first PanZoomTool is the highest priority -- by default,
|
91
|
+
* // this tool is responsible for handling multi-finger touch gestures.
|
92
|
+
* //
|
93
|
+
* // Lower-priority PanZoomTools handle one-finger touch gestures and
|
94
|
+
* // key-presses.
|
95
|
+
* const panZoomTool = panZoomToolList[0];
|
96
|
+
*
|
97
|
+
* // Lock rotation for multi-finger touch gestures.
|
98
|
+
* panZoomTool.setModeEnabled(PanZoomMode.RotationLocked, true);
|
99
|
+
* ```
|
100
|
+
*/
|
61
101
|
setModeEnabled(mode: PanZoomMode, enabled: boolean): void;
|
102
|
+
/**
|
103
|
+
* Sets all modes for this tool using a bitmask.
|
104
|
+
*
|
105
|
+
* @see {@link setModeEnabled}
|
106
|
+
*
|
107
|
+
* @example
|
108
|
+
* ```ts
|
109
|
+
* tool.setMode(PanZoomMode.RotationLocked|PanZoomMode.TwoFingerTouchGestures);
|
110
|
+
* ```
|
111
|
+
*/
|
62
112
|
setMode(mode: PanZoomMode): void;
|
113
|
+
/**
|
114
|
+
* Returns a bitmask indicating the currently-enabled modes.
|
115
|
+
* @see {@link setModeEnabled}
|
116
|
+
*/
|
63
117
|
getMode(): PanZoomMode;
|
64
118
|
}
|
65
119
|
export {};
|
@@ -13,11 +13,16 @@ const BaseTool_1 = __importDefault(require("./BaseTool"));
|
|
13
13
|
const keybindings_1 = require("./keybindings");
|
14
14
|
var PanZoomMode;
|
15
15
|
(function (PanZoomMode) {
|
16
|
+
/** Touch gestures with a single pointer. Ignores non-touch gestures. */
|
16
17
|
PanZoomMode[PanZoomMode["OneFingerTouchGestures"] = 1] = "OneFingerTouchGestures";
|
18
|
+
/** Touch gestures with exactly two pointers. Ignores non-touch gestures. */
|
17
19
|
PanZoomMode[PanZoomMode["TwoFingerTouchGestures"] = 2] = "TwoFingerTouchGestures";
|
18
20
|
PanZoomMode[PanZoomMode["RightClickDrags"] = 4] = "RightClickDrags";
|
21
|
+
/** Single-pointer gestures of *any* type (including touch). */
|
19
22
|
PanZoomMode[PanZoomMode["SinglePointerGestures"] = 8] = "SinglePointerGestures";
|
23
|
+
/** Keyboard navigation (e.g. LeftArrow to move left). */
|
20
24
|
PanZoomMode[PanZoomMode["Keyboard"] = 16] = "Keyboard";
|
25
|
+
/** If provided, prevents **this** tool from rotating the viewport (other tools may still do so). */
|
21
26
|
PanZoomMode[PanZoomMode["RotationLocked"] = 32] = "RotationLocked";
|
22
27
|
})(PanZoomMode || (exports.PanZoomMode = PanZoomMode = {}));
|
23
28
|
class InertialScroller {
|
@@ -65,6 +70,15 @@ class InertialScroller {
|
|
65
70
|
}
|
66
71
|
}
|
67
72
|
}
|
73
|
+
/**
|
74
|
+
* This tool moves the viewport in response to touchpad, touchscreen, mouse, and keyboard events.
|
75
|
+
*
|
76
|
+
* Which events are handled, and which are skipped, are determined by the tool's `mode`. For example,
|
77
|
+
* a `PanZoom` tool with `mode = PanZoomMode.TwoFingerTouchGestures|PanZoomMode.RightClickDrags` would
|
78
|
+
* respond to right-click drag events and two-finger touch gestures.
|
79
|
+
*
|
80
|
+
* @see {@link setModeEnabled}
|
81
|
+
*/
|
68
82
|
class PanZoom extends BaseTool_1.default {
|
69
83
|
constructor(editor, mode, description) {
|
70
84
|
super(editor.notifier, description);
|
@@ -428,8 +442,32 @@ class PanZoom extends BaseTool_1.default {
|
|
428
442
|
isRotationLocked() {
|
429
443
|
return !!(this.mode & PanZoomMode.RotationLocked);
|
430
444
|
}
|
431
|
-
|
432
|
-
|
445
|
+
/**
|
446
|
+
* Changes the types of gestures used by this pan/zoom tool.
|
447
|
+
*
|
448
|
+
* @see {@link PanZoomMode} {@link setMode}
|
449
|
+
*
|
450
|
+
* @example
|
451
|
+
* ```ts,runnable
|
452
|
+
* import { Editor, PanZoomTool, PanZoomMode } from 'js-draw';
|
453
|
+
*
|
454
|
+
* const editor = new Editor(document.body);
|
455
|
+
*
|
456
|
+
* // By default, there are multiple PanZoom tools that handle different events.
|
457
|
+
* // This gets all PanZoomTools.
|
458
|
+
* const panZoomToolList = editor.toolController.getMatchingTools(PanZoomTool);
|
459
|
+
*
|
460
|
+
* // The first PanZoomTool is the highest priority -- by default,
|
461
|
+
* // this tool is responsible for handling multi-finger touch gestures.
|
462
|
+
* //
|
463
|
+
* // Lower-priority PanZoomTools handle one-finger touch gestures and
|
464
|
+
* // key-presses.
|
465
|
+
* const panZoomTool = panZoomToolList[0];
|
466
|
+
*
|
467
|
+
* // Lock rotation for multi-finger touch gestures.
|
468
|
+
* panZoomTool.setModeEnabled(PanZoomMode.RotationLocked, true);
|
469
|
+
* ```
|
470
|
+
*/
|
433
471
|
setModeEnabled(mode, enabled) {
|
434
472
|
let newMode = this.mode;
|
435
473
|
if (enabled) {
|
@@ -440,6 +478,16 @@ class PanZoom extends BaseTool_1.default {
|
|
440
478
|
}
|
441
479
|
this.setMode(newMode);
|
442
480
|
}
|
481
|
+
/**
|
482
|
+
* Sets all modes for this tool using a bitmask.
|
483
|
+
*
|
484
|
+
* @see {@link setModeEnabled}
|
485
|
+
*
|
486
|
+
* @example
|
487
|
+
* ```ts
|
488
|
+
* tool.setMode(PanZoomMode.RotationLocked|PanZoomMode.TwoFingerTouchGestures);
|
489
|
+
* ```
|
490
|
+
*/
|
443
491
|
setMode(mode) {
|
444
492
|
if (mode !== this.mode) {
|
445
493
|
this.mode = mode;
|
@@ -449,6 +497,10 @@ class PanZoom extends BaseTool_1.default {
|
|
449
497
|
});
|
450
498
|
}
|
451
499
|
}
|
500
|
+
/**
|
501
|
+
* Returns a bitmask indicating the currently-enabled modes.
|
502
|
+
* @see {@link setModeEnabled}
|
503
|
+
*/
|
452
504
|
getMode() {
|
453
505
|
return this.mode;
|
454
506
|
}
|
@@ -2,6 +2,9 @@ type ListenerResult = {
|
|
2
2
|
remove(): void;
|
3
3
|
};
|
4
4
|
type UpdateCallback<T> = (value: T) => void;
|
5
|
+
type ReactiveValuesOf<T extends unknown[]> = {
|
6
|
+
[key in keyof T]: ReactiveValue<T[key]>;
|
7
|
+
};
|
5
8
|
/**
|
6
9
|
* A `ReactiveValue` is a value that
|
7
10
|
* - updates periodically,
|
@@ -56,6 +59,7 @@ export declare abstract class ReactiveValue<T> {
|
|
56
59
|
* Returns a reactive value derived from a single `source`.
|
57
60
|
*/
|
58
61
|
static map<A, B>(source: ReactiveValue<A>, map: (a: A) => B, inverseMap: (b: B) => A): MutableReactiveValue<B>;
|
62
|
+
static union<Values extends [...unknown[]]>(values: ReactiveValuesOf<Values>): ReactiveValue<Values>;
|
59
63
|
}
|
60
64
|
export declare abstract class MutableReactiveValue<T> extends ReactiveValue<T> {
|
61
65
|
/**
|
@@ -106,6 +106,11 @@ class ReactiveValue {
|
|
106
106
|
}
|
107
107
|
return result;
|
108
108
|
}
|
109
|
+
static union(values) {
|
110
|
+
return ReactiveValue.fromCallback(() => {
|
111
|
+
return values.map(value => value.get());
|
112
|
+
}, values);
|
113
|
+
}
|
109
114
|
}
|
110
115
|
exports.ReactiveValue = ReactiveValue;
|
111
116
|
class MutableReactiveValue extends ReactiveValue {
|
package/dist/cjs/version.js
CHANGED
@@ -47,7 +47,9 @@ export default class SVGLoader {
|
|
47
47
|
let fill = Color4.transparent;
|
48
48
|
let stroke;
|
49
49
|
// If possible, use computedStyles (allows property inheritance).
|
50
|
-
|
50
|
+
// Chromium, however, sets .fill to a falsy, but not undefined value in some cases where
|
51
|
+
// styles are available. As such, use || instead of ??.
|
52
|
+
const fillAttribute = node.getAttribute('fill') ?? (computedStyles?.fill || node.style?.fill);
|
51
53
|
if (fillAttribute) {
|
52
54
|
try {
|
53
55
|
fill = Color4.fromString(fillAttribute);
|
@@ -166,7 +166,7 @@ export default class EditorImage {
|
|
166
166
|
*
|
167
167
|
* @internal
|
168
168
|
*/
|
169
|
-
setDebugMode(newDebugMode: boolean): void;
|
169
|
+
static setDebugMode(newDebugMode: boolean): void;
|
170
170
|
private static SetImportExportRectCommand;
|
171
171
|
}
|
172
172
|
/**
|
@@ -214,6 +214,7 @@ export declare class ImageNode {
|
|
214
214
|
renderAllAsync(renderer: AbstractRenderer, preRenderComponent: PreRenderComponentCallback): Promise<boolean>;
|
215
215
|
render(renderer: AbstractRenderer, visibleRect?: Rect2): void;
|
216
216
|
renderDebugBoundingBoxes(renderer: AbstractRenderer, visibleRect: Rect2, depth?: number): void;
|
217
|
+
private checkRep;
|
217
218
|
}
|
218
219
|
/** An `ImageNode` that can properly handle fullscreen/data components. @internal */
|
219
220
|
export declare class RootImageNode extends ImageNode {
|
@@ -314,7 +314,7 @@ class EditorImage {
|
|
314
314
|
*
|
315
315
|
* @internal
|
316
316
|
*/
|
317
|
-
setDebugMode(newDebugMode) {
|
317
|
+
static setDebugMode(newDebugMode) {
|
318
318
|
debugMode = newDebugMode;
|
319
319
|
}
|
320
320
|
}
|
@@ -586,8 +586,8 @@ export class ImageNode {
|
|
586
586
|
const nodeForChildren = new ImageNode(this);
|
587
587
|
nodeForChildren.children = this.children;
|
588
588
|
this.children = [nodeForNewLeaf, nodeForChildren];
|
589
|
-
nodeForChildren.recomputeBBox(true);
|
590
589
|
nodeForChildren.updateParents();
|
590
|
+
nodeForChildren.recomputeBBox(true);
|
591
591
|
}
|
592
592
|
return nodeForNewLeaf.addLeaf(leaf);
|
593
593
|
}
|
@@ -604,6 +604,9 @@ export class ImageNode {
|
|
604
604
|
const newNode = ImageNode.createLeafNode(this, leaf);
|
605
605
|
this.children.push(newNode);
|
606
606
|
newNode.recomputeBBox(true);
|
607
|
+
if (this.children.length >= this.targetChildCount) {
|
608
|
+
this.rebalance();
|
609
|
+
}
|
607
610
|
return newNode;
|
608
611
|
}
|
609
612
|
// Creates a new leaf node with the given content.
|
@@ -636,6 +639,7 @@ export class ImageNode {
|
|
636
639
|
this.parent?.recomputeBBox(true);
|
637
640
|
}
|
638
641
|
}
|
642
|
+
this.checkRep();
|
639
643
|
}
|
640
644
|
// Grows this' bounding box to also include `other`.
|
641
645
|
// Always bubbles up.
|
@@ -659,10 +663,12 @@ export class ImageNode {
|
|
659
663
|
// Remove this' parent, if this' parent isn't the root.
|
660
664
|
const oldParent = this.parent;
|
661
665
|
if (oldParent.parent !== null) {
|
662
|
-
|
663
|
-
|
664
|
-
this.parent.children.push(this);
|
666
|
+
const newParent = oldParent.parent;
|
667
|
+
newParent.children = newParent.children.filter(c => c !== oldParent);
|
665
668
|
oldParent.parent = null;
|
669
|
+
oldParent.children = [];
|
670
|
+
this.parent = newParent;
|
671
|
+
newParent.children.push(this);
|
666
672
|
this.parent.recomputeBBox(false);
|
667
673
|
}
|
668
674
|
else if (this.content === null) {
|
@@ -672,10 +678,63 @@ export class ImageNode {
|
|
672
678
|
this.parent = null;
|
673
679
|
}
|
674
680
|
}
|
681
|
+
// Create virtual containers for children. Handles the case where there
|
682
|
+
// are many small, often non-overlapping children that we still want to be grouped.
|
683
|
+
if (this.children.length > this.targetChildCount * 10) {
|
684
|
+
const grid = this.getBBox().divideIntoGrid(4, 4);
|
685
|
+
const indexToCount = [];
|
686
|
+
while (indexToCount.length < grid.length) {
|
687
|
+
indexToCount.push(0);
|
688
|
+
}
|
689
|
+
for (const child of this.children) {
|
690
|
+
for (let i = 0; i < grid.length; i++) {
|
691
|
+
if (grid[i].containsRect(child.getBBox())) {
|
692
|
+
indexToCount[i]++;
|
693
|
+
}
|
694
|
+
}
|
695
|
+
}
|
696
|
+
let indexWithGreatest = 0;
|
697
|
+
let greatestCount = indexToCount[0];
|
698
|
+
for (let i = 1; i < indexToCount.length; i++) {
|
699
|
+
if (indexToCount[i] > greatestCount) {
|
700
|
+
indexWithGreatest = i;
|
701
|
+
greatestCount = indexToCount[i];
|
702
|
+
}
|
703
|
+
}
|
704
|
+
const targetGridSquare = grid[indexWithGreatest];
|
705
|
+
// Avoid clustering if just a few children would be grouped.
|
706
|
+
// Unnecessary clustering can lead to unnecessarily nested nodes.
|
707
|
+
if (greatestCount > 4) {
|
708
|
+
const newChildren = [];
|
709
|
+
const childNodeChildren = [];
|
710
|
+
for (const child of this.children) {
|
711
|
+
if (targetGridSquare.containsRect(child.getBBox())) {
|
712
|
+
childNodeChildren.push(child);
|
713
|
+
}
|
714
|
+
else {
|
715
|
+
newChildren.push(child);
|
716
|
+
}
|
717
|
+
}
|
718
|
+
if (childNodeChildren.length < this.children.length) {
|
719
|
+
this.children = newChildren;
|
720
|
+
const child = new ImageNode(this);
|
721
|
+
this.children.push(child);
|
722
|
+
child.children = childNodeChildren;
|
723
|
+
child.updateParents(false);
|
724
|
+
child.recomputeBBox(false);
|
725
|
+
child.rebalance();
|
726
|
+
}
|
727
|
+
}
|
728
|
+
}
|
729
|
+
// Empty?
|
730
|
+
if (this.parent && this.children.length === 0 && this.content === null) {
|
731
|
+
this.remove();
|
732
|
+
}
|
675
733
|
}
|
676
734
|
// Removes the parent-to-child link.
|
677
735
|
// Called internally by `.remove`
|
678
736
|
removeChild(child) {
|
737
|
+
this.checkRep();
|
679
738
|
const oldChildCount = this.children.length;
|
680
739
|
this.children = this.children.filter(node => {
|
681
740
|
return node !== child;
|
@@ -685,6 +744,8 @@ export class ImageNode {
|
|
685
744
|
child.rebalance();
|
686
745
|
});
|
687
746
|
this.recomputeBBox(true);
|
747
|
+
this.rebalance();
|
748
|
+
this.checkRep();
|
688
749
|
}
|
689
750
|
// Remove this node and all of its children
|
690
751
|
remove() {
|
@@ -699,6 +760,7 @@ export class ImageNode {
|
|
699
760
|
this.parent = null;
|
700
761
|
this.content = null;
|
701
762
|
this.children = [];
|
763
|
+
this.checkRep();
|
702
764
|
}
|
703
765
|
// Creates a (potentially incomplete) async rendering of this image.
|
704
766
|
// Returns false if stopped early
|
@@ -765,11 +827,45 @@ export class ImageNode {
|
|
765
827
|
const lineWidth = isLeaf ? 1 * pixelSize : 2 * pixelSize;
|
766
828
|
renderer.drawRect(bbox.intersection(visibleRect), lineWidth, { fill });
|
767
829
|
renderer.endObject();
|
830
|
+
if (bbox.maxDimension > visibleRect.maxDimension / 3) {
|
831
|
+
const textStyle = {
|
832
|
+
fontFamily: 'monospace',
|
833
|
+
size: bbox.minDimension / 20,
|
834
|
+
renderingStyle: { fill: Color4.red },
|
835
|
+
};
|
836
|
+
renderer.drawText(`Depth: ${depth}`, Mat33.translation(bbox.bottomLeft), textStyle);
|
837
|
+
}
|
768
838
|
// Render debug information for children
|
769
839
|
for (const child of this.children) {
|
770
840
|
child.renderDebugBoundingBoxes(renderer, visibleRect, depth + 1);
|
771
841
|
}
|
772
842
|
}
|
843
|
+
checkRep(depth = 0) {
|
844
|
+
// Slow -- disabld by default
|
845
|
+
if (debugMode) {
|
846
|
+
if (this.parent && !this.parent.children.includes(this)) {
|
847
|
+
throw new Error(`Parent does not have this node as a child. (depth: ${depth})`);
|
848
|
+
}
|
849
|
+
let expectedBBox = null;
|
850
|
+
const seenChildren = new Set();
|
851
|
+
for (const child of this.children) {
|
852
|
+
expectedBBox ??= child.getBBox();
|
853
|
+
expectedBBox = expectedBBox.union(child.getBBox());
|
854
|
+
if (child.parent !== this) {
|
855
|
+
throw new Error(`Child with bbox ${child.getBBox()} and ${child.children.length} has wrong parent (was ${child.parent}).`);
|
856
|
+
}
|
857
|
+
// Children should only be present once
|
858
|
+
if (seenChildren.has(child)) {
|
859
|
+
throw new Error(`Child ${child} is present twice or more in its parent's child list`);
|
860
|
+
}
|
861
|
+
seenChildren.add(child);
|
862
|
+
}
|
863
|
+
const tolerance = this.bbox.minDimension / 100;
|
864
|
+
if (expectedBBox && !this.bbox.eq(expectedBBox, tolerance)) {
|
865
|
+
throw new Error(`Wrong bounding box ${expectedBBox} \\neq ${this.bbox} (depth: ${depth})`);
|
866
|
+
}
|
867
|
+
}
|
868
|
+
}
|
773
869
|
}
|
774
870
|
ImageNode.idCounter = 0;
|
775
871
|
/** An `ImageNode` that can properly handle fullscreen/data components. @internal */
|
@@ -64,12 +64,12 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
64
64
|
setDraftMode(draftMode) {
|
65
65
|
if (draftMode) {
|
66
66
|
this.minSquareCurveApproxDist = 9;
|
67
|
-
this.minRenderSizeBothDimens =
|
68
|
-
this.minRenderSizeAnyDimen = 0.
|
67
|
+
this.minRenderSizeBothDimens = 1;
|
68
|
+
this.minRenderSizeAnyDimen = 0.1;
|
69
69
|
}
|
70
70
|
else {
|
71
71
|
this.minSquareCurveApproxDist = 0.5;
|
72
|
-
this.minRenderSizeBothDimens = 0.
|
72
|
+
this.minRenderSizeBothDimens = 0.1;
|
73
73
|
this.minRenderSizeAnyDimen = 1e-6;
|
74
74
|
}
|
75
75
|
}
|
@@ -238,7 +238,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
238
238
|
// @internal
|
239
239
|
isTooSmallToRender(rect) {
|
240
240
|
// Should we ignore all objects within this object's bbox?
|
241
|
-
const diagonal = rect.size.times(this.
|
241
|
+
const diagonal = rect.size.times(this.getSizeOfCanvasPixelOnScreen());
|
242
242
|
const bothDimenMinSize = this.minRenderSizeBothDimens;
|
243
243
|
const bothTooSmall = Math.abs(diagonal.x) < bothDimenMinSize && Math.abs(diagonal.y) < bothDimenMinSize;
|
244
244
|
const anyDimenMinSize = this.minRenderSizeAnyDimen;
|
@@ -58,6 +58,7 @@ export interface ToolbarLocalization extends ToolbarUtilsLocalization {
|
|
58
58
|
errorImageHasZeroSize: string;
|
59
59
|
describeTheImage: string;
|
60
60
|
fileInput__loading: string;
|
61
|
+
fileInput__andNMoreFiles: (count: number) => string;
|
61
62
|
penDropdown__baseHelpText: string;
|
62
63
|
penDropdown__colorHelpText: string;
|
63
64
|
penDropdown__thicknessHelpText: string;
|
@@ -59,6 +59,7 @@ export const defaultToolbarLocalization = {
|
|
59
59
|
errorImageHasZeroSize: 'Error: Image has zero size',
|
60
60
|
describeTheImage: 'Image description',
|
61
61
|
fileInput__loading: 'Loading...',
|
62
|
+
fileInput__andNMoreFiles: (n) => `(...${n} more)`,
|
62
63
|
// Help text
|
63
64
|
penDropdown__baseHelpText: 'This tool draws shapes or freehand lines.',
|
64
65
|
penDropdown__colorHelpText: 'Changes the pen\'s color',
|
@@ -29,6 +29,12 @@ export class ImageWrapper {
|
|
29
29
|
isChanged() {
|
30
30
|
return this.imageBase64Url !== this.originalSrc;
|
31
31
|
}
|
32
|
+
// Returns true if the current image is large enough to display a "decrease size"
|
33
|
+
// option.
|
34
|
+
isLarge() {
|
35
|
+
const largeImageThreshold = 0.12 * 1024 * 1024; // 0.12 MiB
|
36
|
+
return this.getBase64Url().length > largeImageThreshold;
|
37
|
+
}
|
32
38
|
getBase64Url() {
|
33
39
|
return this.imageBase64Url;
|
34
40
|
}
|
@@ -37,6 +43,7 @@ export class ImageWrapper {
|
|
37
43
|
}
|
38
44
|
setAltText(text) {
|
39
45
|
this.altText = text;
|
46
|
+
this.preview.alt = text;
|
40
47
|
}
|
41
48
|
static fromSrcAndPreview(initialBase64Src, preview, onUrlUpdate) {
|
42
49
|
return new ImageWrapper(initialBase64Src, preview, onUrlUpdate);
|