js-draw 1.17.0 → 1.18.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 +70 -10
- package/dist/bundle.js +2 -2
- package/dist/cjs/Editor.d.ts +18 -20
- package/dist/cjs/Editor.js +5 -2
- package/dist/cjs/components/AbstractComponent.d.ts +17 -5
- package/dist/cjs/components/AbstractComponent.js +15 -15
- package/dist/cjs/components/Stroke.d.ts +4 -1
- package/dist/cjs/components/Stroke.js +158 -2
- package/dist/cjs/components/builders/PolylineBuilder.d.ts +1 -1
- package/dist/cjs/components/builders/PolylineBuilder.js +9 -2
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +44 -51
- package/dist/cjs/image/EditorImage.js +1 -1
- package/dist/cjs/localizations/de.js +1 -1
- package/dist/cjs/localizations/es.js +1 -1
- package/dist/cjs/testing/createEditor.d.ts +2 -2
- package/dist/cjs/testing/createEditor.js +2 -2
- package/dist/cjs/toolbar/IconProvider.d.ts +3 -1
- package/dist/cjs/toolbar/IconProvider.js +15 -3
- package/dist/cjs/toolbar/localization.d.ts +6 -1
- package/dist/cjs/toolbar/localization.js +7 -2
- package/dist/cjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
- package/dist/cjs/toolbar/widgets/EraserToolWidget.js +45 -5
- package/dist/cjs/toolbar/widgets/PenToolWidget.js +10 -3
- package/dist/cjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/keybindings.js +1 -1
- package/dist/cjs/tools/Eraser.d.ts +24 -4
- package/dist/cjs/tools/Eraser.js +107 -20
- package/dist/cjs/tools/PasteHandler.js +0 -1
- package/dist/cjs/tools/lib.d.ts +1 -4
- package/dist/cjs/tools/lib.js +2 -4
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +18 -20
- package/dist/mjs/Editor.mjs +5 -2
- package/dist/mjs/components/AbstractComponent.d.ts +17 -5
- package/dist/mjs/components/AbstractComponent.mjs +15 -15
- package/dist/mjs/components/Stroke.d.ts +4 -1
- package/dist/mjs/components/Stroke.mjs +159 -3
- package/dist/mjs/components/builders/PolylineBuilder.d.ts +1 -1
- package/dist/mjs/components/builders/PolylineBuilder.mjs +10 -3
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +45 -52
- package/dist/mjs/image/EditorImage.mjs +1 -1
- package/dist/mjs/localizations/de.mjs +1 -1
- package/dist/mjs/localizations/es.mjs +1 -1
- package/dist/mjs/testing/createEditor.d.ts +2 -2
- package/dist/mjs/testing/createEditor.mjs +2 -2
- package/dist/mjs/toolbar/IconProvider.d.ts +3 -1
- package/dist/mjs/toolbar/IconProvider.mjs +15 -3
- package/dist/mjs/toolbar/localization.d.ts +6 -1
- package/dist/mjs/toolbar/localization.mjs +7 -2
- package/dist/mjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
- package/dist/mjs/toolbar/widgets/EraserToolWidget.mjs +47 -6
- package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +10 -3
- package/dist/mjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/keybindings.mjs +1 -1
- package/dist/mjs/tools/Eraser.d.ts +24 -4
- package/dist/mjs/tools/Eraser.mjs +107 -21
- package/dist/mjs/tools/PasteHandler.mjs +0 -1
- package/dist/mjs/tools/lib.d.ts +1 -4
- package/dist/mjs/tools/lib.mjs +1 -4
- package/dist/mjs/version.mjs +1 -1
- package/package.json +3 -3
    
        package/dist/cjs/tools/lib.d.ts
    CHANGED
    
    | @@ -1,6 +1,3 @@ | |
| 1 | 
            -
            /**
         | 
| 2 | 
            -
             * @packageDocumentation
         | 
| 3 | 
            -
             */
         | 
| 4 1 | 
             
            export { default as BaseTool } from './BaseTool';
         | 
| 5 2 | 
             
            export { default as ToolController } from './ToolController';
         | 
| 6 3 | 
             
            export { default as ToolEnabledGroup } from './ToolEnabledGroup';
         | 
| @@ -11,7 +8,7 @@ export { default as PenTool, PenStyle } from './Pen'; | |
| 11 8 | 
             
            export { default as TextTool } from './TextTool';
         | 
| 12 9 | 
             
            export { default as SelectionTool } from './SelectionTool/SelectionTool';
         | 
| 13 10 | 
             
            export { default as SelectAllShortcutHandler } from './SelectionTool/SelectAllShortcutHandler';
         | 
| 14 | 
            -
            export { default as EraserTool } from './Eraser';
         | 
| 11 | 
            +
            export { default as EraserTool, EraserMode } from './Eraser';
         | 
| 15 12 | 
             
            export { default as PasteHandler } from './PasteHandler';
         | 
| 16 13 | 
             
            export { default as SoundUITool } from './SoundUITool';
         | 
| 17 14 | 
             
            export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
         | 
    
        package/dist/cjs/tools/lib.js
    CHANGED
    
    | @@ -1,12 +1,9 @@ | |
| 1 1 | 
             
            "use strict";
         | 
| 2 | 
            -
            /**
         | 
| 3 | 
            -
             * @packageDocumentation
         | 
| 4 | 
            -
             */
         | 
| 5 2 | 
             
            var __importDefault = (this && this.__importDefault) || function (mod) {
         | 
| 6 3 | 
             
                return (mod && mod.__esModule) ? mod : { "default": mod };
         | 
| 7 4 | 
             
            };
         | 
| 8 5 | 
             
            Object.defineProperty(exports, "__esModule", { value: true });
         | 
| 9 | 
            -
            exports.ToolbarShortcutHandler = exports.SoundUITool = exports.PasteHandler = exports.EraserTool = exports.SelectAllShortcutHandler = exports.SelectionTool = exports.TextTool = exports.PenTool = exports.PanZoomMode = exports.PanZoomTool = exports.ToolSwitcherShortcut = exports.UndoRedoShortcut = exports.ToolEnabledGroup = exports.ToolController = exports.BaseTool = void 0;
         | 
| 6 | 
            +
            exports.ToolbarShortcutHandler = exports.SoundUITool = exports.PasteHandler = exports.EraserMode = exports.EraserTool = exports.SelectAllShortcutHandler = exports.SelectionTool = exports.TextTool = exports.PenTool = exports.PanZoomMode = exports.PanZoomTool = exports.ToolSwitcherShortcut = exports.UndoRedoShortcut = exports.ToolEnabledGroup = exports.ToolController = exports.BaseTool = void 0;
         | 
| 10 7 | 
             
            var BaseTool_1 = require("./BaseTool");
         | 
| 11 8 | 
             
            Object.defineProperty(exports, "BaseTool", { enumerable: true, get: function () { return __importDefault(BaseTool_1).default; } });
         | 
| 12 9 | 
             
            var ToolController_1 = require("./ToolController");
         | 
| @@ -30,6 +27,7 @@ var SelectAllShortcutHandler_1 = require("./SelectionTool/SelectAllShortcutHandl | |
| 30 27 | 
             
            Object.defineProperty(exports, "SelectAllShortcutHandler", { enumerable: true, get: function () { return __importDefault(SelectAllShortcutHandler_1).default; } });
         | 
| 31 28 | 
             
            var Eraser_1 = require("./Eraser");
         | 
| 32 29 | 
             
            Object.defineProperty(exports, "EraserTool", { enumerable: true, get: function () { return __importDefault(Eraser_1).default; } });
         | 
| 30 | 
            +
            Object.defineProperty(exports, "EraserMode", { enumerable: true, get: function () { return Eraser_1.EraserMode; } });
         | 
| 33 31 | 
             
            var PasteHandler_1 = require("./PasteHandler");
         | 
| 34 32 | 
             
            Object.defineProperty(exports, "PasteHandler", { enumerable: true, get: function () { return __importDefault(PasteHandler_1).default; } });
         | 
| 35 33 | 
             
            var SoundUITool_1 = require("./SoundUITool");
         | 
    
        package/dist/cjs/version.js
    CHANGED
    
    
    
        package/dist/mjs/Editor.d.ts
    CHANGED
    
    | @@ -79,30 +79,28 @@ export interface EditorSettings { | |
| 79 79 | 
             
                 * Configures the default pen tools.
         | 
| 80 80 | 
             
                 *
         | 
| 81 81 | 
             
                 * **Example**:
         | 
| 82 | 
            -
                 *  | 
| 83 | 
            -
                 * import { Editor, makePolylineBuilder } from 'js-draw';
         | 
| 84 | 
            -
                 *
         | 
| 85 | 
            -
                 * const editor = new Editor(document.body, {
         | 
| 86 | 
            -
                 *     pens: {
         | 
| 87 | 
            -
                 *         additionalPenTypes: [{
         | 
| 88 | 
            -
                 *             name: 'Polyline (For debugging)',
         | 
| 89 | 
            -
                 *             id: 'custom-polyline',
         | 
| 90 | 
            -
                 *             factory: makePolylineBuilder,
         | 
| 91 | 
            -
                 *
         | 
| 92 | 
            -
                 *             // The pen doesn't create fixed shapes (e.g. squares, rectangles, etc)
         | 
| 93 | 
            -
                 *             // and so should go under the "pens" section.
         | 
| 94 | 
            -
                 *             isShapeBuilder: false,
         | 
| 95 | 
            -
                 *         }],
         | 
| 96 | 
            -
                 *     },
         | 
| 97 | 
            -
                 * });
         | 
| 98 | 
            -
                 * editor.addToolbar();
         | 
| 99 | 
            -
                 * ```
         | 
| 82 | 
            +
                 * [[include:doc-pages/inline-examples/editor-settings-polyline-pen.md]]
         | 
| 100 83 | 
             
                 */
         | 
| 101 84 | 
             
                pens: {
         | 
| 102 85 | 
             
                    /**
         | 
| 103 86 | 
             
                     * Additional pen types that can be selected in a toolbar.
         | 
| 104 87 | 
             
                     */
         | 
| 105 | 
            -
                    additionalPenTypes | 
| 88 | 
            +
                    additionalPenTypes?: readonly Readonly<PenTypeRecord>[];
         | 
| 89 | 
            +
                    /**
         | 
| 90 | 
            +
                     * Should return `true` if a pen type should be shown in the toolbar.
         | 
| 91 | 
            +
                     *
         | 
| 92 | 
            +
                     * @example
         | 
| 93 | 
            +
                     * ```ts,runnable
         | 
| 94 | 
            +
                     * import {Editor} from 'js-draw';
         | 
| 95 | 
            +
                     * const editor = new Editor(document.body, {
         | 
| 96 | 
            +
                     *   // Only allow selecting the polyline pen from the toolbar.
         | 
| 97 | 
            +
                     *   pens: { filterPenTypes: p => p.id === 'polyline-pen' },
         | 
| 98 | 
            +
                     * });
         | 
| 99 | 
            +
                     * editor.addToolbar();
         | 
| 100 | 
            +
                     * ```
         | 
| 101 | 
            +
                     * Notice that this setting only affects the toolbar GUI.
         | 
| 102 | 
            +
                     */
         | 
| 103 | 
            +
                    filterPenTypes?: (penType: PenTypeRecord) => boolean;
         | 
| 106 104 | 
             
                } | null;
         | 
| 107 105 | 
             
            }
         | 
| 108 106 | 
             
            /**
         | 
| @@ -123,7 +121,7 @@ export interface EditorSettings { | |
| 123 121 | 
             
             * ```
         | 
| 124 122 | 
             
             *
         | 
| 125 123 | 
             
             * See also
         | 
| 126 | 
            -
             * [` | 
| 124 | 
            +
             * * [`examples.md`](https://github.com/personalizedrefrigerator/js-draw/blob/main/docs/examples.md).
         | 
| 127 125 | 
             
             */
         | 
| 128 126 | 
             
            export declare class Editor {
         | 
| 129 127 | 
             
                private container;
         | 
    
        package/dist/mjs/Editor.mjs
    CHANGED
    
    | @@ -47,7 +47,7 @@ import  ClipboardHandler  from './util/ClipboardHandler.mjs'; | |
| 47 47 | 
             
             * ```
         | 
| 48 48 | 
             
             *
         | 
| 49 49 | 
             
             * See also
         | 
| 50 | 
            -
             * [` | 
| 50 | 
            +
             * * [`examples.md`](https://github.com/personalizedrefrigerator/js-draw/blob/main/docs/examples.md).
         | 
| 51 51 | 
             
             */
         | 
| 52 52 | 
             
            export class Editor {
         | 
| 53 53 | 
             
                /**
         | 
| @@ -109,7 +109,10 @@ export class Editor { | |
| 109 109 | 
             
                        iconProvider: settings.iconProvider ?? new IconProvider(),
         | 
| 110 110 | 
             
                        notices: [],
         | 
| 111 111 | 
             
                        appInfo: settings.appInfo ? { ...settings.appInfo } : null,
         | 
| 112 | 
            -
                        pens: { | 
| 112 | 
            +
                        pens: {
         | 
| 113 | 
            +
                            additionalPenTypes: settings.pens?.additionalPenTypes ?? [],
         | 
| 114 | 
            +
                            filterPenTypes: settings.pens?.filterPenTypes ?? (() => true)
         | 
| 115 | 
            +
                        },
         | 
| 113 116 | 
             
                    };
         | 
| 114 117 | 
             
                    // Validate settings
         | 
| 115 118 | 
             
                    if (this.settings.minZoom > this.settings.maxZoom) {
         | 
| @@ -1,8 +1,9 @@ | |
| 1 1 | 
             
            import SerializableCommand from '../commands/SerializableCommand';
         | 
| 2 2 | 
             
            import EditorImage from '../image/EditorImage';
         | 
| 3 | 
            -
            import { LineSegment2, Mat33, Rect2 } from '@js-draw/math';
         | 
| 3 | 
            +
            import { LineSegment2, Mat33, Path, Rect2 } from '@js-draw/math';
         | 
| 4 4 | 
             
            import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
         | 
| 5 5 | 
             
            import { ImageComponentLocalization } from './localization';
         | 
| 6 | 
            +
            import Viewport from '../Viewport';
         | 
| 6 7 | 
             
            export type LoadSaveData = (string[] | Record<symbol, string | number>);
         | 
| 7 8 | 
             
            export type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
         | 
| 8 9 | 
             
            export type DeserializeCallback = (data: string) => AbstractComponent;
         | 
| @@ -112,7 +113,9 @@ export default abstract class AbstractComponent { | |
| 112 113 | 
             
                 * this function.
         | 
| 113 114 | 
             
                 */
         | 
| 114 115 | 
             
                intersectsRect(rect: Rect2): boolean;
         | 
| 115 | 
            -
                 | 
| 116 | 
            +
                isSelectable(): boolean;
         | 
| 117 | 
            +
                isBackground(): boolean;
         | 
| 118 | 
            +
                getProportionalRenderingTime(): number;
         | 
| 116 119 | 
             
                protected abstract applyTransformation(affineTransfm: Mat33): void;
         | 
| 117 120 | 
             
                /**
         | 
| 118 121 | 
             
                 * Returns a command that, when applied, transforms this by [affineTransfm] and
         | 
| @@ -131,9 +134,6 @@ export default abstract class AbstractComponent { | |
| 131 134 | 
             
                 *                         this command.
         | 
| 132 135 | 
             
                 */
         | 
| 133 136 | 
             
                setZIndexAndTransformBy(affineTransfm: Mat33, newZIndex: number, originalZIndex?: number): SerializableCommand;
         | 
| 134 | 
            -
                isSelectable(): boolean;
         | 
| 135 | 
            -
                isBackground(): boolean;
         | 
| 136 | 
            -
                getProportionalRenderingTime(): number;
         | 
| 137 137 | 
             
                private static transformElementCommandId;
         | 
| 138 138 | 
             
                private static TransformElementCommand;
         | 
| 139 139 | 
             
                /**
         | 
| @@ -143,6 +143,18 @@ export default abstract class AbstractComponent { | |
| 143 143 | 
             
                abstract description(localizationTable: ImageComponentLocalization): string;
         | 
| 144 144 | 
             
                protected abstract createClone(): AbstractComponent;
         | 
| 145 145 | 
             
                clone(): AbstractComponent;
         | 
| 146 | 
            +
                /**
         | 
| 147 | 
            +
                 * **Optional method**: Divides this component into sections roughly along the given path,
         | 
| 148 | 
            +
                 * removing parts that are roughly within `shape`.
         | 
| 149 | 
            +
                 *
         | 
| 150 | 
            +
                 * **Notes**:
         | 
| 151 | 
            +
                 * - A default implementation may be provided for this method in the future. Until then,
         | 
| 152 | 
            +
                 *   this method is `undefined` if unsupported.
         | 
| 153 | 
            +
                 *
         | 
| 154 | 
            +
                 * `viewport` should be provided to determine how newly-added points should be rounded.
         | 
| 155 | 
            +
                 */
         | 
| 156 | 
            +
                withRegionErased?(shape: Path, viewport: Viewport): AbstractComponent[];
         | 
| 157 | 
            +
                protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
         | 
| 146 158 | 
             
                serialize(): {
         | 
| 147 159 | 
             
                    name: string;
         | 
| 148 160 | 
             
                    zIndex: number;
         | 
| @@ -136,6 +136,21 @@ class AbstractComponent { | |
| 136 136 | 
             
                    const testLines = rect.getEdges();
         | 
| 137 137 | 
             
                    return testLines.some(edge => this.intersects(edge));
         | 
| 138 138 | 
             
                }
         | 
| 139 | 
            +
                // @returns true iff this component can be selected (e.g. by the selection tool.)
         | 
| 140 | 
            +
                isSelectable() {
         | 
| 141 | 
            +
                    return true;
         | 
| 142 | 
            +
                }
         | 
| 143 | 
            +
                // @returns true iff this component should be added to the background, rather than the
         | 
| 144 | 
            +
                // foreground of the image.
         | 
| 145 | 
            +
                isBackground() {
         | 
| 146 | 
            +
                    return false;
         | 
| 147 | 
            +
                }
         | 
| 148 | 
            +
                // @returns an approximation of the proportional time it takes to render this component.
         | 
| 149 | 
            +
                // This is intended to be a rough estimate, but, for example, a stroke with two points sould have
         | 
| 150 | 
            +
                // a renderingWeight approximately twice that of a stroke with one point.
         | 
| 151 | 
            +
                getProportionalRenderingTime() {
         | 
| 152 | 
            +
                    return 1;
         | 
| 153 | 
            +
                }
         | 
| 139 154 | 
             
                /**
         | 
| 140 155 | 
             
                 * Returns a command that, when applied, transforms this by [affineTransfm] and
         | 
| 141 156 | 
             
                 * updates the editor.
         | 
| @@ -160,21 +175,6 @@ class AbstractComponent { | |
| 160 175 | 
             
                setZIndexAndTransformBy(affineTransfm, newZIndex, originalZIndex) {
         | 
| 161 176 | 
             
                    return new AbstractComponent.TransformElementCommand(affineTransfm, this.getId(), this, newZIndex, originalZIndex);
         | 
| 162 177 | 
             
                }
         | 
| 163 | 
            -
                // @returns true iff this component can be selected (e.g. by the selection tool.)
         | 
| 164 | 
            -
                isSelectable() {
         | 
| 165 | 
            -
                    return true;
         | 
| 166 | 
            -
                }
         | 
| 167 | 
            -
                // @returns true iff this component should be added to the background, rather than the
         | 
| 168 | 
            -
                // foreground of the image.
         | 
| 169 | 
            -
                isBackground() {
         | 
| 170 | 
            -
                    return false;
         | 
| 171 | 
            -
                }
         | 
| 172 | 
            -
                // @returns an approximation of the proportional time it takes to render this component.
         | 
| 173 | 
            -
                // This is intended to be a rough estimate, but, for example, a stroke with two points sould have
         | 
| 174 | 
            -
                // a renderingWeight approximately twice that of a stroke with one point.
         | 
| 175 | 
            -
                getProportionalRenderingTime() {
         | 
| 176 | 
            -
                    return 1;
         | 
| 177 | 
            -
                }
         | 
| 178 178 | 
             
                // Returns a copy of this component.
         | 
| 179 179 | 
             
                clone() {
         | 
| 180 180 | 
             
                    const clone = this.createClone();
         | 
| @@ -6,6 +6,7 @@ import AbstractComponent from './AbstractComponent'; | |
| 6 6 | 
             
            import { ImageComponentLocalization } from './localization';
         | 
| 7 7 | 
             
            import RestyleableComponent, { ComponentStyle } from './RestylableComponent';
         | 
| 8 8 | 
             
            import RenderablePathSpec, { RenderablePathSpecWithPath } from '../rendering/RenderablePathSpec';
         | 
| 9 | 
            +
            import Viewport from '../Viewport';
         | 
| 9 10 | 
             
            /**
         | 
| 10 11 | 
             
             * Represents an {@link AbstractComponent} made up of one or more {@link Path}s.
         | 
| 11 12 | 
             
             *
         | 
| @@ -46,10 +47,12 @@ export default class Stroke extends AbstractComponent implements RestyleableComp | |
| 46 47 | 
             
                 * ]);
         | 
| 47 48 | 
             
                 * ```
         | 
| 48 49 | 
             
                 */
         | 
| 49 | 
            -
                constructor(parts: RenderablePathSpec[]);
         | 
| 50 | 
            +
                constructor(parts: RenderablePathSpec[], initialZIndex?: number);
         | 
| 50 51 | 
             
                getStyle(): ComponentStyle;
         | 
| 51 52 | 
             
                updateStyle(style: ComponentStyle): SerializableCommand;
         | 
| 52 53 | 
             
                forceStyle(style: ComponentStyle, editor: Editor | null): void;
         | 
| 54 | 
            +
                /** @beta -- May fail for concave `path`s */
         | 
| 55 | 
            +
                withRegionErased(eraserPath: Path, viewport: Viewport): Stroke[];
         | 
| 53 56 | 
             
                intersects(line: LineSegment2): boolean;
         | 
| 54 57 | 
             
                intersectsRect(rect: Rect2): boolean;
         | 
| 55 58 | 
             
                private simplifiedPath;
         | 
| @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            import { Path, Rect2 } from '@js-draw/math';
         | 
| 1 | 
            +
            import { Path, Rect2, PathCommandType, comparePathIndices, stepPathIndexBy } from '@js-draw/math';
         | 
| 2 2 | 
             
            import  { styleFromJSON, styleToJSON }  from '../rendering/RenderingStyle.mjs';
         | 
| 3 3 | 
             
            import  AbstractComponent  from './AbstractComponent.mjs';
         | 
| 4 4 | 
             
            import  { createRestyleComponentCommand }  from './RestylableComponent.mjs';
         | 
| @@ -39,8 +39,8 @@ export default class Stroke extends AbstractComponent { | |
| 39 39 | 
             
                 * ]);
         | 
| 40 40 | 
             
                 * ```
         | 
| 41 41 | 
             
                 */
         | 
| 42 | 
            -
                constructor(parts) {
         | 
| 43 | 
            -
                    super('stroke');
         | 
| 42 | 
            +
                constructor(parts, initialZIndex) {
         | 
| 43 | 
            +
                    super('stroke', initialZIndex);
         | 
| 44 44 | 
             
                    // @internal
         | 
| 45 45 | 
             
                    // eslint-disable-next-line @typescript-eslint/prefer-as-const
         | 
| 46 46 | 
             
                    this.isRestylableComponent = true;
         | 
| @@ -118,6 +118,162 @@ export default class Stroke extends AbstractComponent { | |
| 118 118 | 
             
                        editor.queueRerender();
         | 
| 119 119 | 
             
                    }
         | 
| 120 120 | 
             
                }
         | 
| 121 | 
            +
                /** @beta -- May fail for concave `path`s */
         | 
| 122 | 
            +
                withRegionErased(eraserPath, viewport) {
         | 
| 123 | 
            +
                    const polyline = eraserPath.polylineApproximation();
         | 
| 124 | 
            +
                    const isPointInsideEraser = (point) => {
         | 
| 125 | 
            +
                        return eraserPath.closedContainsPoint(point);
         | 
| 126 | 
            +
                    };
         | 
| 127 | 
            +
                    const newStrokes = [];
         | 
| 128 | 
            +
                    let failedAssertions = false;
         | 
| 129 | 
            +
                    for (const part of this.parts) {
         | 
| 130 | 
            +
                        const path = part.path;
         | 
| 131 | 
            +
                        const makeStroke = (path) => {
         | 
| 132 | 
            +
                            if (part.style.fill.a > 0) {
         | 
| 133 | 
            +
                                // Remove visually empty paths.
         | 
| 134 | 
            +
                                if (path.parts.length < 1 || (path.parts.length === 1 && path.parts[0].kind === PathCommandType.LineTo)) {
         | 
| 135 | 
            +
                                    // TODO: If this isn't present, a very large number of strokes are created while erasing.
         | 
| 136 | 
            +
                                    return null;
         | 
| 137 | 
            +
                                }
         | 
| 138 | 
            +
                                else {
         | 
| 139 | 
            +
                                    // Filled paths must be closed (allows for optimizations elsewhere)
         | 
| 140 | 
            +
                                    path = path.asClosed();
         | 
| 141 | 
            +
                                }
         | 
| 142 | 
            +
                            }
         | 
| 143 | 
            +
                            if (isNaN(path.getExactBBox().area)) {
         | 
| 144 | 
            +
                                console.warn('Prevented creating a stroke with NaN area');
         | 
| 145 | 
            +
                                failedAssertions = true;
         | 
| 146 | 
            +
                                return null;
         | 
| 147 | 
            +
                            }
         | 
| 148 | 
            +
                            return new Stroke([pathToRenderable(path, part.style)], this.getZIndex());
         | 
| 149 | 
            +
                        };
         | 
| 150 | 
            +
                        const intersectionPoints = [];
         | 
| 151 | 
            +
                        // If stroked, finds intersections with the middle of the stroke.
         | 
| 152 | 
            +
                        // If filled, finds intersections with the edge of the stroke.
         | 
| 153 | 
            +
                        for (const segment of polyline) {
         | 
| 154 | 
            +
                            intersectionPoints.push(...path.intersection(segment));
         | 
| 155 | 
            +
                        }
         | 
| 156 | 
            +
                        // When stroked, if the stroke width is significantly larger than the eraser,
         | 
| 157 | 
            +
                        // it can't intersect both the edge of the stroke and its middle at the same time
         | 
| 158 | 
            +
                        // (generally, erasing is triggered by the eraser touching the edge of this stroke).
         | 
| 159 | 
            +
                        //
         | 
| 160 | 
            +
                        // As such, we also look for intersections along the edge of this, if none with the
         | 
| 161 | 
            +
                        // center were found, but only within a certain range of sizes because:
         | 
| 162 | 
            +
                        // 1. Intersection testing with stroked paths is generally much slower than with
         | 
| 163 | 
            +
                        //    non-stroked paths.
         | 
| 164 | 
            +
                        // 2. If zoomed in significantly, it's unlikely that the user wants to erase a large
         | 
| 165 | 
            +
                        //    part of the stroke.
         | 
| 166 | 
            +
                        let isErasingFromEdge = false;
         | 
| 167 | 
            +
                        if (intersectionPoints.length === 0
         | 
| 168 | 
            +
                            && part.style.stroke
         | 
| 169 | 
            +
                            && part.style.stroke.width > eraserPath.bbox.minDimension * 0.3
         | 
| 170 | 
            +
                            && part.style.stroke.width < eraserPath.bbox.maxDimension * 30) {
         | 
| 171 | 
            +
                            for (const segment of polyline) {
         | 
| 172 | 
            +
                                intersectionPoints.push(...path.intersection(segment, part.style.stroke.width / 2));
         | 
| 173 | 
            +
                            }
         | 
| 174 | 
            +
                            isErasingFromEdge = true;
         | 
| 175 | 
            +
                        }
         | 
| 176 | 
            +
                        // Sort first by curve index, then by parameter value
         | 
| 177 | 
            +
                        intersectionPoints.sort(comparePathIndices);
         | 
| 178 | 
            +
                        const isInsideJustBeforeFirst = (() => {
         | 
| 179 | 
            +
                            if (intersectionPoints.length === 0) {
         | 
| 180 | 
            +
                                return false;
         | 
| 181 | 
            +
                            }
         | 
| 182 | 
            +
                            // The eraser may not be near the center of the curve -- approximate.
         | 
| 183 | 
            +
                            if (isErasingFromEdge) {
         | 
| 184 | 
            +
                                return intersectionPoints[0].curveIndex === 0 && intersectionPoints[0].parameterValue <= 0;
         | 
| 185 | 
            +
                            }
         | 
| 186 | 
            +
                            const justBeforeFirstIntersection = stepPathIndexBy(intersectionPoints[0], -1e-10);
         | 
| 187 | 
            +
                            return isPointInsideEraser(path.at(justBeforeFirstIntersection));
         | 
| 188 | 
            +
                        })();
         | 
| 189 | 
            +
                        let intersectionCount = isInsideJustBeforeFirst ? 1 : 0;
         | 
| 190 | 
            +
                        const addNewPath = (path, knownToBeInside) => {
         | 
| 191 | 
            +
                            const component = makeStroke(path);
         | 
| 192 | 
            +
                            let isInside = intersectionCount % 2 === 1;
         | 
| 193 | 
            +
                            intersectionCount++;
         | 
| 194 | 
            +
                            if (knownToBeInside !== undefined) {
         | 
| 195 | 
            +
                                isInside = knownToBeInside;
         | 
| 196 | 
            +
                            }
         | 
| 197 | 
            +
                            // Here, we work around bugs in the underlying Bezier curve library
         | 
| 198 | 
            +
                            // (including https://github.com/Pomax/bezierjs/issues/179).
         | 
| 199 | 
            +
                            // Even if not all intersections are returned correctly, we still want
         | 
| 200 | 
            +
                            // isInside to be roughly correct.
         | 
| 201 | 
            +
                            if (knownToBeInside === undefined && !isInside && eraserPath.closedContainsPoint(path.getExactBBox().center)) {
         | 
| 202 | 
            +
                                isInside = !isInside;
         | 
| 203 | 
            +
                            }
         | 
| 204 | 
            +
                            if (!component) {
         | 
| 205 | 
            +
                                return;
         | 
| 206 | 
            +
                            }
         | 
| 207 | 
            +
                            // Assertion: Avoid deleting sections that are much larger than the eraser.
         | 
| 208 | 
            +
                            failedAssertions ||= isInside && path.getExactBBox().maxDimension > eraserPath.getExactBBox().maxDimension * 2;
         | 
| 209 | 
            +
                            if (!isInside) {
         | 
| 210 | 
            +
                                newStrokes.push(component);
         | 
| 211 | 
            +
                            }
         | 
| 212 | 
            +
                        };
         | 
| 213 | 
            +
                        if (part.style.fill.a === 0) { // Not filled?
         | 
| 214 | 
            +
                            // An additional case where we erase completely -- without the padding of the stroke,
         | 
| 215 | 
            +
                            // the path is smaller than the eraser (allows us to erase dots completely).
         | 
| 216 | 
            +
                            const shouldEraseCompletely = eraserPath.getExactBBox().maxDimension / 10 > path.getExactBBox().maxDimension;
         | 
| 217 | 
            +
                            if (!shouldEraseCompletely) {
         | 
| 218 | 
            +
                                const split = path.splitAt(intersectionPoints, { mapNewPoint: p => viewport.roundPoint(p) });
         | 
| 219 | 
            +
                                for (const splitPart of split) {
         | 
| 220 | 
            +
                                    addNewPath(splitPart);
         | 
| 221 | 
            +
                                }
         | 
| 222 | 
            +
                            }
         | 
| 223 | 
            +
                        }
         | 
| 224 | 
            +
                        else if (intersectionPoints.length >= 2 && intersectionPoints.length % 2 === 0) {
         | 
| 225 | 
            +
                            // TODO: Support subtractive erasing on small scales -- see https://github.com/personalizedrefrigerator/js-draw/pull/63/commits/568686e2384219ad0bb07617ea4efff1540aed00
         | 
| 226 | 
            +
                            //       for a broken implementation.
         | 
| 227 | 
            +
                            //
         | 
| 228 | 
            +
                            // We currently assume that a 4-point intersection means that the intersection
         | 
| 229 | 
            +
                            // looks similar to this:
         | 
| 230 | 
            +
                            //   -----------
         | 
| 231 | 
            +
                            //  |   STROKE  |
         | 
| 232 | 
            +
                            //  |           |
         | 
| 233 | 
            +
                            //%%x-----------x%%%%%%%
         | 
| 234 | 
            +
                            //%                    %
         | 
| 235 | 
            +
                            //%      ERASER        %
         | 
| 236 | 
            +
                            //%                    %
         | 
| 237 | 
            +
                            //%%x-----------x%%%%%%%
         | 
| 238 | 
            +
                            //  |   STROKE  |
         | 
| 239 | 
            +
                            //   -----------
         | 
| 240 | 
            +
                            //
         | 
| 241 | 
            +
                            // Our goal is to separate STROKE into the contiguous parts outside
         | 
| 242 | 
            +
                            // of the eraser (as shown above).
         | 
| 243 | 
            +
                            //
         | 
| 244 | 
            +
                            // To do this, we split STROKE at each intersection:
         | 
| 245 | 
            +
                            //   3 3 3 3 3 3
         | 
| 246 | 
            +
                            //  3   STROKE  3
         | 
| 247 | 
            +
                            //  3           3
         | 
| 248 | 
            +
                            //  x           x
         | 
| 249 | 
            +
                            //  2           4
         | 
| 250 | 
            +
                            //  2   STROKE  4
         | 
| 251 | 
            +
                            //  2           4
         | 
| 252 | 
            +
                            //  x           x
         | 
| 253 | 
            +
                            //  1   STROKE  5
         | 
| 254 | 
            +
                            //   . 5 5 5 5 5
         | 
| 255 | 
            +
                            //   ^
         | 
| 256 | 
            +
                            // Start
         | 
| 257 | 
            +
                            //
         | 
| 258 | 
            +
                            // The difficulty here is correctly pairing edges to create the the output
         | 
| 259 | 
            +
                            // strokes, particularly because we don't know the order of intersection points.
         | 
| 260 | 
            +
                            const parts = path.splitAt(intersectionPoints, { mapNewPoint: p => viewport.roundPoint(p) });
         | 
| 261 | 
            +
                            for (let i = 0; i < Math.floor(parts.length / 2); i++) {
         | 
| 262 | 
            +
                                addNewPath(parts[i].union(parts[parts.length - i - 1]).asClosed());
         | 
| 263 | 
            +
                            }
         | 
| 264 | 
            +
                            if (parts.length % 2 !== 0) {
         | 
| 265 | 
            +
                                addNewPath(parts[Math.floor(parts.length / 2)].asClosed());
         | 
| 266 | 
            +
                            }
         | 
| 267 | 
            +
                        }
         | 
| 268 | 
            +
                        else {
         | 
| 269 | 
            +
                            addNewPath(path, false);
         | 
| 270 | 
            +
                        }
         | 
| 271 | 
            +
                    }
         | 
| 272 | 
            +
                    if (failedAssertions) {
         | 
| 273 | 
            +
                        return [this];
         | 
| 274 | 
            +
                    }
         | 
| 275 | 
            +
                    return newStrokes;
         | 
| 276 | 
            +
                }
         | 
| 121 277 | 
             
                intersects(line) {
         | 
| 122 278 | 
             
                    for (const part of this.parts) {
         | 
| 123 279 | 
             
                        const strokeWidth = part.style.stroke?.width;
         | 
| @@ -9,7 +9,6 @@ import RenderingStyle from '../../rendering/RenderingStyle'; | |
| 9 9 | 
             
            /**
         | 
| 10 10 | 
             
             * Creates strokes from line segments rather than Bézier curves.
         | 
| 11 11 | 
             
             *
         | 
| 12 | 
            -
             * @beta Output behavior may change significantly between versions. For now, intended for debugging.
         | 
| 13 12 | 
             
             */
         | 
| 14 13 | 
             
            export declare const makePolylineBuilder: ComponentBuilderFactory;
         | 
| 15 14 | 
             
            export default class PolylineBuilder implements ComponentBuilder {
         | 
| @@ -21,6 +20,7 @@ export default class PolylineBuilder implements ComponentBuilder { | |
| 21 20 | 
             
                private widthAverageNumSamples;
         | 
| 22 21 | 
             
                private lastPoint;
         | 
| 23 22 | 
             
                private startPoint;
         | 
| 23 | 
            +
                private lastLineSegment;
         | 
| 24 24 | 
             
                constructor(startPoint: StrokeDataPoint, minFitAllowed: number, viewport: Viewport);
         | 
| 25 25 | 
             
                getBBox(): Rect2;
         | 
| 26 26 | 
             
                protected getRenderingStyle(): RenderingStyle;
         | 
| @@ -1,11 +1,10 @@ | |
| 1 | 
            -
            import { Rect2, Color4, PathCommandType } from '@js-draw/math';
         | 
| 1 | 
            +
            import { Rect2, Color4, PathCommandType, Vec2, LineSegment2 } from '@js-draw/math';
         | 
| 2 2 | 
             
            import  Stroke  from '../Stroke.mjs';
         | 
| 3 3 | 
             
            import  Viewport  from '../../Viewport.mjs';
         | 
| 4 4 | 
             
            import  makeShapeFitAutocorrect  from './autocorrect/makeShapeFitAutocorrect.mjs';
         | 
| 5 5 | 
             
            /**
         | 
| 6 6 | 
             
             * Creates strokes from line segments rather than Bézier curves.
         | 
| 7 7 | 
             
             *
         | 
| 8 | 
            -
             * @beta Output behavior may change significantly between versions. For now, intended for debugging.
         | 
| 9 8 | 
             
             */
         | 
| 10 9 | 
             
            export const makePolylineBuilder = makeShapeFitAutocorrect((initialPoint, viewport) => {
         | 
| 11 10 | 
             
                const minFit = viewport.getSizeOfPixelOnCanvas();
         | 
| @@ -17,6 +16,7 @@ export default class PolylineBuilder { | |
| 17 16 | 
             
                    this.viewport = viewport;
         | 
| 18 17 | 
             
                    this.parts = [];
         | 
| 19 18 | 
             
                    this.widthAverageNumSamples = 1;
         | 
| 19 | 
            +
                    this.lastLineSegment = null;
         | 
| 20 20 | 
             
                    this.averageWidth = startPoint.width;
         | 
| 21 21 | 
             
                    this.startPoint = {
         | 
| 22 22 | 
             
                        ...startPoint,
         | 
| @@ -50,7 +50,7 @@ export default class PolylineBuilder { | |
| 50 50 | 
             
                    if (commands.length <= 1) {
         | 
| 51 51 | 
             
                        commands.push({
         | 
| 52 52 | 
             
                            kind: PathCommandType.LineTo,
         | 
| 53 | 
            -
                            point: startPoint,
         | 
| 53 | 
            +
                            point: startPoint.plus(Vec2.of(this.averageWidth / 4, 0)),
         | 
| 54 54 | 
             
                        });
         | 
| 55 55 | 
             
                    }
         | 
| 56 56 | 
             
                    return {
         | 
| @@ -98,11 +98,18 @@ export default class PolylineBuilder { | |
| 98 98 | 
             
                            + newPoint.width / this.widthAverageNumSamples;
         | 
| 99 99 | 
             
                    const roundedPoint = this.roundPoint(newPoint.pos);
         | 
| 100 100 | 
             
                    if (!roundedPoint.eq(this.lastPoint)) {
         | 
| 101 | 
            +
                        // If almost exactly in the same line as the previous
         | 
| 102 | 
            +
                        if (this.lastLineSegment && this.lastLineSegment.direction.dot(roundedPoint.minus(this.lastPoint).normalized()) > 0.997) {
         | 
| 103 | 
            +
                            this.parts.pop();
         | 
| 104 | 
            +
                            this.lastPoint = this.lastLineSegment.p1;
         | 
| 105 | 
            +
                        }
         | 
| 101 106 | 
             
                        this.parts.push({
         | 
| 102 107 | 
             
                            kind: PathCommandType.LineTo,
         | 
| 103 108 | 
             
                            point: this.roundPoint(newPoint.pos),
         | 
| 104 109 | 
             
                        });
         | 
| 105 110 | 
             
                        this.bbox = this.bbox.grownToPoint(roundedPoint);
         | 
| 111 | 
            +
                        this.lastLineSegment = new LineSegment2(this.lastPoint, roundedPoint);
         | 
| 112 | 
            +
                        this.lastPoint = roundedPoint;
         | 
| 106 113 | 
             
                    }
         | 
| 107 114 | 
             
                }
         | 
| 108 115 | 
             
            }
         | 
| @@ -12,6 +12,7 @@ export default class PressureSensitiveFreehandLineBuilder implements ComponentBu | |
| 12 12 | 
             
                private isFirstSegment;
         | 
| 13 13 | 
             
                private pathStartConnector;
         | 
| 14 14 | 
             
                private mostRecentConnector;
         | 
| 15 | 
            +
                private nextCurveStartConnector;
         | 
| 15 16 | 
             
                private upperSegments;
         | 
| 16 17 | 
             
                private lowerSegments;
         | 
| 17 18 | 
             
                private lastUpperBezier;
         | 
| @@ -25,7 +26,6 @@ export default class PressureSensitiveFreehandLineBuilder implements ComponentBu | |
| 25 26 | 
             
                private getRenderingStyle;
         | 
| 26 27 | 
             
                private previewCurrentPath;
         | 
| 27 28 | 
             
                private previewFullPath;
         | 
| 28 | 
            -
                private previewStroke;
         | 
| 29 29 | 
             
                preview(renderer: AbstractRenderer): void;
         | 
| 30 30 | 
             
                build(): Stroke;
         | 
| 31 31 | 
             
                private roundPoint;
         |