js-draw 1.17.0 → 1.18.0

Sign up to get free protection for your applications and to get access to all the features.
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;