js-draw 1.19.1 → 1.20.1
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 +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);
         
     |