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;
|