js-draw 1.19.1 → 1.20.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 +51 -0
- package/dist/Editor.css +46 -5
- 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 +10 -2
- 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 +10 -2
- 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/package.json +2 -2
- package/src/toolbar/EdgeToolbar.scss +8 -3
- 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);
|