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.
Files changed (63) hide show
  1. package/README.md +70 -10
  2. package/dist/bundle.js +2 -2
  3. package/dist/cjs/Editor.d.ts +18 -20
  4. package/dist/cjs/Editor.js +5 -2
  5. package/dist/cjs/components/AbstractComponent.d.ts +17 -5
  6. package/dist/cjs/components/AbstractComponent.js +15 -15
  7. package/dist/cjs/components/Stroke.d.ts +4 -1
  8. package/dist/cjs/components/Stroke.js +158 -2
  9. package/dist/cjs/components/builders/PolylineBuilder.d.ts +1 -1
  10. package/dist/cjs/components/builders/PolylineBuilder.js +9 -2
  11. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
  12. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +44 -51
  13. package/dist/cjs/image/EditorImage.js +1 -1
  14. package/dist/cjs/localizations/de.js +1 -1
  15. package/dist/cjs/localizations/es.js +1 -1
  16. package/dist/cjs/testing/createEditor.d.ts +2 -2
  17. package/dist/cjs/testing/createEditor.js +2 -2
  18. package/dist/cjs/toolbar/IconProvider.d.ts +3 -1
  19. package/dist/cjs/toolbar/IconProvider.js +15 -3
  20. package/dist/cjs/toolbar/localization.d.ts +6 -1
  21. package/dist/cjs/toolbar/localization.js +7 -2
  22. package/dist/cjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
  23. package/dist/cjs/toolbar/widgets/EraserToolWidget.js +45 -5
  24. package/dist/cjs/toolbar/widgets/PenToolWidget.js +10 -3
  25. package/dist/cjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
  26. package/dist/cjs/toolbar/widgets/keybindings.js +1 -1
  27. package/dist/cjs/tools/Eraser.d.ts +24 -4
  28. package/dist/cjs/tools/Eraser.js +107 -20
  29. package/dist/cjs/tools/PasteHandler.js +0 -1
  30. package/dist/cjs/tools/lib.d.ts +1 -4
  31. package/dist/cjs/tools/lib.js +2 -4
  32. package/dist/cjs/version.js +1 -1
  33. package/dist/mjs/Editor.d.ts +18 -20
  34. package/dist/mjs/Editor.mjs +5 -2
  35. package/dist/mjs/components/AbstractComponent.d.ts +17 -5
  36. package/dist/mjs/components/AbstractComponent.mjs +15 -15
  37. package/dist/mjs/components/Stroke.d.ts +4 -1
  38. package/dist/mjs/components/Stroke.mjs +159 -3
  39. package/dist/mjs/components/builders/PolylineBuilder.d.ts +1 -1
  40. package/dist/mjs/components/builders/PolylineBuilder.mjs +10 -3
  41. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
  42. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +45 -52
  43. package/dist/mjs/image/EditorImage.mjs +1 -1
  44. package/dist/mjs/localizations/de.mjs +1 -1
  45. package/dist/mjs/localizations/es.mjs +1 -1
  46. package/dist/mjs/testing/createEditor.d.ts +2 -2
  47. package/dist/mjs/testing/createEditor.mjs +2 -2
  48. package/dist/mjs/toolbar/IconProvider.d.ts +3 -1
  49. package/dist/mjs/toolbar/IconProvider.mjs +15 -3
  50. package/dist/mjs/toolbar/localization.d.ts +6 -1
  51. package/dist/mjs/toolbar/localization.mjs +7 -2
  52. package/dist/mjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
  53. package/dist/mjs/toolbar/widgets/EraserToolWidget.mjs +47 -6
  54. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +10 -3
  55. package/dist/mjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
  56. package/dist/mjs/toolbar/widgets/keybindings.mjs +1 -1
  57. package/dist/mjs/tools/Eraser.d.ts +24 -4
  58. package/dist/mjs/tools/Eraser.mjs +107 -21
  59. package/dist/mjs/tools/PasteHandler.mjs +0 -1
  60. package/dist/mjs/tools/lib.d.ts +1 -4
  61. package/dist/mjs/tools/lib.mjs +1 -4
  62. package/dist/mjs/version.mjs +1 -1
  63. package/package.json +3 -3
@@ -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';
@@ -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");
@@ -6,5 +6,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  * @internal
7
7
  */
8
8
  exports.default = {
9
- number: '1.17.0',
9
+ number: '1.18.0',
10
10
  };
@@ -79,30 +79,28 @@ export interface EditorSettings {
79
79
  * Configures the default pen tools.
80
80
  *
81
81
  * **Example**:
82
- * ```ts,runnable
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: readonly Readonly<PenTypeRecord>[];
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
- * [`docs/example/example.ts`](https://github.com/personalizedrefrigerator/js-draw/blob/main/docs/demo/example.ts).
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;
@@ -47,7 +47,7 @@ import ClipboardHandler from './util/ClipboardHandler.mjs';
47
47
  * ```
48
48
  *
49
49
  * See also
50
- * [`docs/example/example.ts`](https://github.com/personalizedrefrigerator/js-draw/blob/main/docs/demo/example.ts).
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: { additionalPenTypes: settings.pens?.additionalPenTypes ?? [], },
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
- protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
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;