js-draw 1.16.0 → 1.17.0
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/Editor.css +11 -0
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +79 -6
- package/dist/cjs/Editor.js +114 -91
- package/dist/cjs/Pointer.d.ts +2 -1
- package/dist/cjs/Pointer.js +9 -2
- package/dist/cjs/commands/localization.d.ts +1 -0
- package/dist/cjs/commands/localization.js +1 -0
- package/dist/cjs/commands/uniteCommands.d.ts +5 -1
- package/dist/cjs/commands/uniteCommands.js +33 -7
- package/dist/cjs/components/TextComponent.d.ts +36 -1
- package/dist/cjs/components/TextComponent.js +39 -1
- package/dist/cjs/components/builders/ArrowBuilder.js +1 -1
- package/dist/cjs/components/builders/PolylineBuilder.d.ts +35 -0
- package/dist/cjs/components/builders/PolylineBuilder.js +115 -0
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +1 -1
- package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +1 -1
- package/dist/cjs/components/lib.d.ts +1 -0
- package/dist/cjs/components/lib.js +3 -1
- package/dist/cjs/components/util/StrokeSmoother.js +4 -4
- package/dist/cjs/image/EditorImage.d.ts +4 -1
- package/dist/cjs/image/EditorImage.js +4 -1
- package/dist/cjs/inputEvents.d.ts +11 -1
- package/dist/cjs/localizations/comments.d.ts +3 -0
- package/dist/cjs/localizations/comments.js +3 -0
- package/dist/cjs/localizations/de.js +0 -2
- package/dist/cjs/localizations/es.js +2 -2
- package/dist/cjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
- package/dist/cjs/rendering/renderers/CanvasRenderer.js +16 -0
- package/dist/cjs/rendering/renderers/SVGRenderer.js +1 -1
- package/dist/cjs/toolbar/IconProvider.d.ts +6 -3
- package/dist/cjs/toolbar/IconProvider.js +6 -4
- package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +24 -1
- package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
- package/dist/cjs/toolbar/widgets/PenToolWidget.js +7 -1
- package/dist/cjs/tools/Eraser.js +1 -1
- package/dist/cjs/tools/InputFilter/InputStabilizer.js +3 -3
- package/dist/cjs/tools/PasteHandler.js +40 -10
- package/dist/cjs/tools/Pen.js +2 -2
- package/dist/cjs/tools/SelectionTool/SelectionTool.js +23 -4
- package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +1 -1
- package/dist/cjs/tools/ToolController.d.ts +17 -1
- package/dist/cjs/tools/ToolController.js +21 -8
- package/dist/cjs/tools/localization.d.ts +2 -2
- package/dist/cjs/tools/localization.js +2 -2
- package/dist/cjs/util/ClipboardHandler.d.ts +27 -0
- package/dist/cjs/util/ClipboardHandler.js +205 -0
- package/dist/cjs/util/ClipboardHandler.test.d.ts +1 -0
- package/dist/cjs/version.d.ts +5 -0
- package/dist/cjs/version.js +6 -1
- package/dist/mjs/Editor.d.ts +79 -6
- package/dist/mjs/Editor.mjs +114 -91
- package/dist/mjs/Pointer.d.ts +2 -1
- package/dist/mjs/Pointer.mjs +9 -2
- package/dist/mjs/commands/localization.d.ts +1 -0
- package/dist/mjs/commands/localization.mjs +1 -0
- package/dist/mjs/commands/uniteCommands.d.ts +5 -1
- package/dist/mjs/commands/uniteCommands.mjs +33 -7
- package/dist/mjs/components/TextComponent.d.ts +36 -1
- package/dist/mjs/components/TextComponent.mjs +40 -2
- package/dist/mjs/components/builders/ArrowBuilder.mjs +1 -1
- package/dist/mjs/components/builders/PolylineBuilder.d.ts +35 -0
- package/dist/mjs/components/builders/PolylineBuilder.mjs +108 -0
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +1 -1
- package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +1 -1
- package/dist/mjs/components/lib.d.ts +1 -0
- package/dist/mjs/components/lib.mjs +1 -0
- package/dist/mjs/components/util/StrokeSmoother.mjs +4 -4
- package/dist/mjs/image/EditorImage.d.ts +4 -1
- package/dist/mjs/image/EditorImage.mjs +4 -1
- package/dist/mjs/inputEvents.d.ts +11 -1
- package/dist/mjs/localizations/comments.d.ts +3 -0
- package/dist/mjs/localizations/comments.mjs +3 -0
- package/dist/mjs/localizations/de.mjs +0 -2
- package/dist/mjs/localizations/es.mjs +2 -2
- package/dist/mjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
- package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +16 -0
- package/dist/mjs/rendering/renderers/SVGRenderer.mjs +1 -1
- package/dist/mjs/toolbar/IconProvider.d.ts +6 -3
- package/dist/mjs/toolbar/IconProvider.mjs +6 -4
- package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +24 -1
- package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
- package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +7 -1
- package/dist/mjs/tools/Eraser.mjs +1 -1
- package/dist/mjs/tools/InputFilter/InputStabilizer.mjs +3 -3
- package/dist/mjs/tools/PasteHandler.mjs +40 -10
- package/dist/mjs/tools/Pen.mjs +2 -2
- package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +23 -4
- package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +1 -1
- package/dist/mjs/tools/ToolController.d.ts +17 -1
- package/dist/mjs/tools/ToolController.mjs +21 -8
- package/dist/mjs/tools/localization.d.ts +2 -2
- package/dist/mjs/tools/localization.mjs +2 -2
- package/dist/mjs/util/ClipboardHandler.d.ts +27 -0
- package/dist/mjs/util/ClipboardHandler.mjs +200 -0
- package/dist/mjs/util/ClipboardHandler.test.d.ts +1 -0
- package/dist/mjs/version.d.ts +5 -0
- package/dist/mjs/version.mjs +6 -1
- package/package.json +6 -6
- package/src/Editor.scss +10 -0
- package/src/toolbar/EdgeToolbar.scss +2 -0
- package/src/tools/SoundUITool.scss +4 -1
@@ -0,0 +1,108 @@
|
|
1
|
+
import { Rect2, Color4, PathCommandType } from '@js-draw/math';
|
2
|
+
import Stroke from '../Stroke.mjs';
|
3
|
+
import Viewport from '../../Viewport.mjs';
|
4
|
+
import makeShapeFitAutocorrect from './autocorrect/makeShapeFitAutocorrect.mjs';
|
5
|
+
/**
|
6
|
+
* Creates strokes from line segments rather than Bézier curves.
|
7
|
+
*
|
8
|
+
* @beta Output behavior may change significantly between versions. For now, intended for debugging.
|
9
|
+
*/
|
10
|
+
export const makePolylineBuilder = makeShapeFitAutocorrect((initialPoint, viewport) => {
|
11
|
+
const minFit = viewport.getSizeOfPixelOnCanvas();
|
12
|
+
return new PolylineBuilder(initialPoint, minFit, viewport);
|
13
|
+
});
|
14
|
+
export default class PolylineBuilder {
|
15
|
+
constructor(startPoint, minFitAllowed, viewport) {
|
16
|
+
this.minFitAllowed = minFitAllowed;
|
17
|
+
this.viewport = viewport;
|
18
|
+
this.parts = [];
|
19
|
+
this.widthAverageNumSamples = 1;
|
20
|
+
this.averageWidth = startPoint.width;
|
21
|
+
this.startPoint = {
|
22
|
+
...startPoint,
|
23
|
+
pos: this.roundPoint(startPoint.pos),
|
24
|
+
};
|
25
|
+
this.lastPoint = this.startPoint.pos;
|
26
|
+
this.bbox = new Rect2(this.startPoint.pos.x, this.startPoint.pos.y, 0, 0);
|
27
|
+
this.parts = [
|
28
|
+
{
|
29
|
+
kind: PathCommandType.MoveTo,
|
30
|
+
point: this.startPoint.pos,
|
31
|
+
},
|
32
|
+
];
|
33
|
+
}
|
34
|
+
getBBox() {
|
35
|
+
return this.bbox.grownBy(this.averageWidth);
|
36
|
+
}
|
37
|
+
getRenderingStyle() {
|
38
|
+
return {
|
39
|
+
fill: Color4.transparent,
|
40
|
+
stroke: {
|
41
|
+
color: this.startPoint.color,
|
42
|
+
width: this.roundDistance(this.averageWidth),
|
43
|
+
}
|
44
|
+
};
|
45
|
+
}
|
46
|
+
previewCurrentPath() {
|
47
|
+
const startPoint = this.startPoint.pos;
|
48
|
+
const commands = [...this.parts];
|
49
|
+
// TODO: For now, this is necesary for the path to be visible.
|
50
|
+
if (commands.length <= 1) {
|
51
|
+
commands.push({
|
52
|
+
kind: PathCommandType.LineTo,
|
53
|
+
point: startPoint,
|
54
|
+
});
|
55
|
+
}
|
56
|
+
return {
|
57
|
+
startPoint,
|
58
|
+
commands,
|
59
|
+
style: this.getRenderingStyle(),
|
60
|
+
};
|
61
|
+
}
|
62
|
+
previewFullPath() {
|
63
|
+
return [this.previewCurrentPath()];
|
64
|
+
}
|
65
|
+
preview(renderer) {
|
66
|
+
const paths = this.previewFullPath();
|
67
|
+
if (paths) {
|
68
|
+
const approxBBox = this.viewport.visibleRect;
|
69
|
+
renderer.startObject(approxBBox);
|
70
|
+
for (const path of paths) {
|
71
|
+
renderer.drawPath(path);
|
72
|
+
}
|
73
|
+
renderer.endObject();
|
74
|
+
}
|
75
|
+
}
|
76
|
+
build() {
|
77
|
+
return new Stroke(this.previewFullPath());
|
78
|
+
}
|
79
|
+
getMinFit() {
|
80
|
+
let minFit = Math.min(this.minFitAllowed, this.averageWidth / 3);
|
81
|
+
if (minFit < 1e-10) {
|
82
|
+
minFit = this.minFitAllowed;
|
83
|
+
}
|
84
|
+
return minFit;
|
85
|
+
}
|
86
|
+
roundPoint(point) {
|
87
|
+
const minFit = this.getMinFit();
|
88
|
+
return Viewport.roundPoint(point, minFit);
|
89
|
+
}
|
90
|
+
roundDistance(dist) {
|
91
|
+
const minFit = this.getMinFit();
|
92
|
+
return Viewport.roundPoint(dist, minFit);
|
93
|
+
}
|
94
|
+
addPoint(newPoint) {
|
95
|
+
this.widthAverageNumSamples++;
|
96
|
+
this.averageWidth =
|
97
|
+
this.averageWidth * (this.widthAverageNumSamples - 1) / this.widthAverageNumSamples
|
98
|
+
+ newPoint.width / this.widthAverageNumSamples;
|
99
|
+
const roundedPoint = this.roundPoint(newPoint.pos);
|
100
|
+
if (!roundedPoint.eq(this.lastPoint)) {
|
101
|
+
this.parts.push({
|
102
|
+
kind: PathCommandType.LineTo,
|
103
|
+
point: this.roundPoint(newPoint.pos),
|
104
|
+
});
|
105
|
+
this.bbox = this.bbox.grownToPoint(roundedPoint);
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
@@ -285,7 +285,7 @@ export default class PressureSensitiveFreehandLineBuilder {
|
|
285
285
|
// Approximate the normal at the location of the control point
|
286
286
|
let projectionT = bezier.project(controlPoint.xy).t;
|
287
287
|
if (!projectionT) {
|
288
|
-
if (startPt.
|
288
|
+
if (startPt.squareDistanceTo(controlPoint) < endPt.squareDistanceTo(controlPoint)) {
|
289
289
|
projectionT = 0.1;
|
290
290
|
}
|
291
291
|
else {
|
@@ -83,7 +83,7 @@ class ShapeFitBuilder {
|
|
83
83
|
// Find the closest point to the startPoint
|
84
84
|
for (let i = 0; i < templatePoints.length; i++) {
|
85
85
|
const current = templatePoints[i];
|
86
|
-
const currentSqrDist = current.
|
86
|
+
const currentSqrDist = current.squareDistanceTo(startPoint);
|
87
87
|
if (!closestToFirst || currentSqrDist < closestToFirstSqrDist) {
|
88
88
|
closestToFirstSqrDist = currentSqrDist;
|
89
89
|
closestToFirst = current;
|
@@ -1,5 +1,6 @@
|
|
1
1
|
export * from './builders/types';
|
2
2
|
export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
|
3
|
+
export { makePolylineBuilder } from './builders/PolylineBuilder';
|
3
4
|
export { makePressureSensitiveFreehandLineBuilder } from './builders/PressureSensitiveFreehandLineBuilder';
|
4
5
|
export { makeOutlinedCircleBuilder } from './builders/CircleBuilder';
|
5
6
|
export { default as StrokeSmoother, Curve as StrokeSmootherCurve } from './util/StrokeSmoother';
|
@@ -1,5 +1,6 @@
|
|
1
1
|
export * from './builders/types.mjs';
|
2
2
|
export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder.mjs';
|
3
|
+
export { makePolylineBuilder } from './builders/PolylineBuilder.mjs';
|
3
4
|
export { makePressureSensitiveFreehandLineBuilder } from './builders/PressureSensitiveFreehandLineBuilder.mjs';
|
4
5
|
export { makeOutlinedCircleBuilder } from './builders/CircleBuilder.mjs';
|
5
6
|
export { default as StrokeSmoother } from './util/StrokeSmoother.mjs';
|
@@ -38,8 +38,8 @@ export class StrokeSmoother {
|
|
38
38
|
const startPt = this.currentCurve.p0;
|
39
39
|
const controlPt = this.currentCurve.p1;
|
40
40
|
const endPt = this.currentCurve.p2;
|
41
|
-
const toControlDist = startPt.
|
42
|
-
const toEndDist = endPt.
|
41
|
+
const toControlDist = startPt.distanceTo(controlPt);
|
42
|
+
const toEndDist = endPt.distanceTo(controlPt);
|
43
43
|
return toControlDist + toEndDist;
|
44
44
|
}
|
45
45
|
finalizeCurrentCurve() {
|
@@ -96,7 +96,7 @@ export class StrokeSmoother {
|
|
96
96
|
return;
|
97
97
|
}
|
98
98
|
const threshold = Math.min(this.lastPoint.width, newPoint.width) / 3;
|
99
|
-
const shouldSnapToInitial = this.startPoint.pos.
|
99
|
+
const shouldSnapToInitial = this.startPoint.pos.distanceTo(newPoint.pos) < threshold
|
100
100
|
&& this.isFirstSegment;
|
101
101
|
// Snap to the starting point if the stroke is contained within a small ball centered
|
102
102
|
// at the starting point.
|
@@ -147,7 +147,7 @@ export class StrokeSmoother {
|
|
147
147
|
const maxRelativeLength = 1.7;
|
148
148
|
const segmentStart = this.buffer[0];
|
149
149
|
const segmentEnd = newPoint.pos;
|
150
|
-
const startEndDist = segmentEnd.
|
150
|
+
const startEndDist = segmentEnd.distanceTo(segmentStart);
|
151
151
|
const maxControlPointDist = maxRelativeLength * startEndDist;
|
152
152
|
// Exit in cases where we would divide by zero
|
153
153
|
if (maxControlPointDist === 0 || exitingVec.magnitude() === 0 || !isFinite(exitingVec.magnitude())) {
|
@@ -105,7 +105,10 @@ export default class EditorImage {
|
|
105
105
|
getImportExportRect(): Rect2;
|
106
106
|
/**
|
107
107
|
* Sets the import/export rectangle to the given `imageRect`. Disables
|
108
|
-
* autoresize
|
108
|
+
* autoresize if it was previously enabled.
|
109
|
+
*
|
110
|
+
* **Note**: The import/export rectangle is the same as the size of any
|
111
|
+
* {@link BackgroundComponent}s (and other components that auto-resize).
|
109
112
|
*/
|
110
113
|
setImportExportRect(imageRect: Rect2): SerializableCommand;
|
111
114
|
/** @see {@link setAutoresizeEnabled} */
|
@@ -205,7 +205,10 @@ class EditorImage {
|
|
205
205
|
}
|
206
206
|
/**
|
207
207
|
* Sets the import/export rectangle to the given `imageRect`. Disables
|
208
|
-
* autoresize
|
208
|
+
* autoresize if it was previously enabled.
|
209
|
+
*
|
210
|
+
* **Note**: The import/export rectangle is the same as the size of any
|
211
|
+
* {@link BackgroundComponent}s (and other components that auto-resize).
|
209
212
|
*/
|
210
213
|
setImportExportRect(imageRect) {
|
211
214
|
return _a.SetImportExportRectCommand.of(this, imageRect, false);
|
@@ -53,7 +53,7 @@ export interface KeyUpEvent extends BaseKeyEvent {
|
|
53
53
|
}
|
54
54
|
export interface CopyEvent {
|
55
55
|
readonly kind: InputEvtType.CopyEvent;
|
56
|
-
setData(mime: string, data: string): void;
|
56
|
+
setData(mime: string, data: string | Promise<Blob>): void;
|
57
57
|
}
|
58
58
|
export interface PasteEvent {
|
59
59
|
readonly kind: InputEvtType.PasteEvent;
|
@@ -76,7 +76,17 @@ export interface PointerMoveEvt extends PointerEvtBase {
|
|
76
76
|
export interface PointerUpEvt extends PointerEvtBase {
|
77
77
|
readonly kind: InputEvtType.PointerUpEvt;
|
78
78
|
}
|
79
|
+
/**
|
80
|
+
* An internal `js-draw` pointer event type.
|
81
|
+
*
|
82
|
+
* This **is not** the same as a DOM pointer event.
|
83
|
+
*/
|
79
84
|
export type PointerEvt = PointerDownEvt | PointerMoveEvt | PointerUpEvt;
|
85
|
+
/**
|
86
|
+
* An internal `js-draw` input event type.
|
87
|
+
*
|
88
|
+
* These are not DOM events.
|
89
|
+
*/
|
80
90
|
export type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt | CopyEvent | PasteEvent;
|
81
91
|
export declare const keyUpEventFromHTMLEvent: (event: KeyboardEvent) => KeyUpEvent;
|
82
92
|
export declare const keyPressEventFromHTMLEvent: (event: KeyboardEvent) => KeyPressEvent;
|
@@ -1,6 +1,9 @@
|
|
1
1
|
import { EditorLocalization } from '../localization';
|
2
2
|
/**
|
3
3
|
* Comments to help translators create translations.
|
4
|
+
*
|
5
|
+
* The key for each comment should be the same as is used in the
|
6
|
+
* translation and original source records.
|
4
7
|
*/
|
5
8
|
declare const comments: Partial<Record<keyof EditorLocalization, string>>;
|
6
9
|
export default comments;
|
@@ -109,8 +109,6 @@ const localization = {
|
|
109
109
|
soundExplorer: 'Klangbasierte Bilderkundung',
|
110
110
|
disableAccessibilityExploreTool: 'Deaktiviere klangbasierte Erkundung',
|
111
111
|
enableAccessibilityExploreTool: 'Aktiviere klangbasierte Erkundung',
|
112
|
-
copied: (count, description) => `${count} ${description} kopiert`,
|
113
|
-
pasted: (count, description) => `${count} ${description} eingefügt`,
|
114
112
|
unionOf: (actionDescription, actionCount) => `Vereinigung: ${actionCount} ${actionDescription}`,
|
115
113
|
emptyBackground: 'Leerer Hintergrund',
|
116
114
|
filledBackgroundWithColor: (color) => `Gefüllter Hintergrund (${color})`,
|
@@ -61,8 +61,8 @@ const localization = {
|
|
61
61
|
toNextMatch: 'Próxima',
|
62
62
|
closeDialog: 'Cerrar',
|
63
63
|
anyDevicePanning: 'Mover la pantalla con todo dispotivo',
|
64
|
-
copied: (count
|
65
|
-
pasted: (count
|
64
|
+
copied: (count) => `${count} cosas fueron copiados`,
|
65
|
+
pasted: (count) => count === 1 ? 'Pegado' : `${count} cosas fueron pegados`,
|
66
66
|
toolEnabledAnnouncement: (toolName) => `${toolName} fue activado`,
|
67
67
|
toolDisabledAnnouncement: (toolName) => `${toolName} fue desactivado`,
|
68
68
|
resizeOutputCommand: (newSize) => `Tamaño de imagen fue cambiado a ${newSize.w}x${newSize.h}`,
|
@@ -58,4 +58,11 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
58
58
|
endObject(): void;
|
59
59
|
drawPoints(...points: Point2[]): void;
|
60
60
|
isTooSmallToRender(rect: Rect2): boolean;
|
61
|
+
static fromViewport(exportViewport: Viewport, options?: {
|
62
|
+
canvasSize?: Vec2;
|
63
|
+
maxCanvasDimen?: number;
|
64
|
+
}): {
|
65
|
+
renderer: CanvasRenderer;
|
66
|
+
element: HTMLCanvasElement;
|
67
|
+
};
|
61
68
|
}
|
@@ -245,4 +245,20 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
245
245
|
const anyTooSmall = Math.abs(diagonal.x) < anyDimenMinSize || Math.abs(diagonal.y) < anyDimenMinSize;
|
246
246
|
return bothTooSmall || anyTooSmall;
|
247
247
|
}
|
248
|
+
// @internal
|
249
|
+
static fromViewport(exportViewport, options = {}) {
|
250
|
+
const canvas = document.createElement('canvas');
|
251
|
+
const exportRectSize = exportViewport.getScreenRectSize();
|
252
|
+
let canvasSize = options.canvasSize ?? exportRectSize;
|
253
|
+
if (options.maxCanvasDimen && canvasSize.maximumEntryMagnitude() > options.maxCanvasDimen) {
|
254
|
+
canvasSize = canvasSize.times(options.maxCanvasDimen / canvasSize.maximumEntryMagnitude());
|
255
|
+
}
|
256
|
+
canvas.width = canvasSize.x;
|
257
|
+
canvas.height = canvasSize.y;
|
258
|
+
const ctx = canvas.getContext('2d');
|
259
|
+
// Scale to ensure that the entire output is visible.
|
260
|
+
const scaleFactor = Math.min(canvasSize.x / exportRectSize.x, canvasSize.y / exportRectSize.y);
|
261
|
+
ctx.scale(scaleFactor, scaleFactor);
|
262
|
+
return { renderer: new CanvasRenderer(ctx, exportViewport), element: canvas };
|
263
|
+
}
|
248
264
|
}
|
@@ -110,7 +110,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
110
110
|
}
|
111
111
|
if (style.stroke) {
|
112
112
|
pathElem.setAttribute('stroke', style.stroke.color.toHexString());
|
113
|
-
pathElem.setAttribute('stroke-width', toRoundedString(style.stroke.width));
|
113
|
+
pathElem.setAttribute('stroke-width', toRoundedString(style.stroke.width * this.getSizeOfCanvasPixelOnScreen()));
|
114
114
|
}
|
115
115
|
this.elem.appendChild(pathElem);
|
116
116
|
this.objectElems?.push(pathElem);
|
@@ -3,8 +3,9 @@ import TextRenderingStyle from '../rendering/TextRenderingStyle';
|
|
3
3
|
import { PenStyle } from '../tools/Pen';
|
4
4
|
export type IconElemType = HTMLImageElement | SVGElement;
|
5
5
|
/**
|
6
|
-
* Provides icons that can be used in the toolbar
|
7
|
-
*
|
6
|
+
* Provides icons that can be used in the toolbar and other locations.
|
7
|
+
*
|
8
|
+
* To customize the icons used by the editor, extend this class and override methods.
|
8
9
|
*
|
9
10
|
* @example
|
10
11
|
* ```ts,runnable
|
@@ -12,7 +13,7 @@ export type IconElemType = HTMLImageElement | SVGElement;
|
|
12
13
|
*
|
13
14
|
* class CustomIconProvider extends jsdraw.IconProvider {
|
14
15
|
* // Use '☺' instead of the default dropdown symbol.
|
15
|
-
* public makeDropdownIcon() {
|
16
|
+
* public override makeDropdownIcon() {
|
16
17
|
* const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
17
18
|
* icon.innerHTML = `
|
18
19
|
* <text x='5' y='55' style='fill: var(--icon-color); font-size: 50pt;'>☺</text>
|
@@ -24,6 +25,8 @@ export type IconElemType = HTMLImageElement | SVGElement;
|
|
24
25
|
*
|
25
26
|
* const icons = new CustomIconProvider();
|
26
27
|
* const editor = new jsdraw.Editor(document.body, {
|
28
|
+
* // The icon pack to use is specified through the editor's initial
|
29
|
+
* // configuration object:
|
27
30
|
* iconProvider: icons,
|
28
31
|
* });
|
29
32
|
*
|
@@ -57,8 +57,9 @@ const makeRedoIcon = (mirror) => {
|
|
57
57
|
return icon;
|
58
58
|
};
|
59
59
|
/**
|
60
|
-
* Provides icons that can be used in the toolbar
|
61
|
-
*
|
60
|
+
* Provides icons that can be used in the toolbar and other locations.
|
61
|
+
*
|
62
|
+
* To customize the icons used by the editor, extend this class and override methods.
|
62
63
|
*
|
63
64
|
* @example
|
64
65
|
* ```ts,runnable
|
@@ -66,7 +67,7 @@ const makeRedoIcon = (mirror) => {
|
|
66
67
|
*
|
67
68
|
* class CustomIconProvider extends jsdraw.IconProvider {
|
68
69
|
* // Use '☺' instead of the default dropdown symbol.
|
69
|
-
* public makeDropdownIcon() {
|
70
|
+
* public override makeDropdownIcon() {
|
70
71
|
* const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
71
72
|
* icon.innerHTML = `
|
72
73
|
* <text x='5' y='55' style='fill: var(--icon-color); font-size: 50pt;'>☺</text>
|
@@ -78,6 +79,8 @@ const makeRedoIcon = (mirror) => {
|
|
78
79
|
*
|
79
80
|
* const icons = new CustomIconProvider();
|
80
81
|
* const editor = new jsdraw.Editor(document.body, {
|
82
|
+
* // The icon pack to use is specified through the editor's initial
|
83
|
+
* // configuration object:
|
81
84
|
* iconProvider: icons,
|
82
85
|
* });
|
83
86
|
*
|
@@ -92,7 +95,6 @@ class IconProvider {
|
|
92
95
|
makeUndoIcon() {
|
93
96
|
return makeRedoIcon(true);
|
94
97
|
}
|
95
|
-
// @param mirror - reflect across the x-axis. This parameter is internal.
|
96
98
|
// @returns a redo icon.
|
97
99
|
makeRedoIcon() {
|
98
100
|
return makeRedoIcon(false);
|
@@ -166,7 +166,30 @@ class DocumentPropertiesWidget extends BaseWidget {
|
|
166
166
|
row.replaceChildren(label, input);
|
167
167
|
return {
|
168
168
|
setValue: (value) => {
|
169
|
-
|
169
|
+
// Slightly improve the case where the user tries to change the
|
170
|
+
// first digit of a dimension like 600.
|
171
|
+
//
|
172
|
+
// As changing the value also gives the image zero size (which is unsupported,
|
173
|
+
// .setValue is called immediately). We work around this by trying to select
|
174
|
+
// the added/changed digits.
|
175
|
+
//
|
176
|
+
// See https://github.com/personalizedrefrigerator/js-draw/issues/58.
|
177
|
+
if (document.activeElement === input && input.value.match(/^0*$/)) {
|
178
|
+
// We need to switch to type="text" and back to type="number" because
|
179
|
+
// number inputs don't support selection.
|
180
|
+
//
|
181
|
+
// See https://stackoverflow.com/q/22381837
|
182
|
+
const originalValue = input.value;
|
183
|
+
input.type = 'text';
|
184
|
+
input.value = value.toString();
|
185
|
+
// Select the added digits
|
186
|
+
const lengthToSelect = Math.max(1, input.value.length - originalValue.length);
|
187
|
+
input.setSelectionRange(0, lengthToSelect);
|
188
|
+
input.type = 'number';
|
189
|
+
}
|
190
|
+
else {
|
191
|
+
input.value = value.toString();
|
192
|
+
}
|
170
193
|
},
|
171
194
|
setIsAutomaticSize: (automatic) => {
|
172
195
|
input.disabled = automatic;
|
@@ -15,7 +15,7 @@ export interface PenTypeRecord {
|
|
15
15
|
export default class PenToolWidget extends BaseToolWidget {
|
16
16
|
private tool;
|
17
17
|
private updateInputs;
|
18
|
-
protected penTypes: PenTypeRecord[];
|
18
|
+
protected penTypes: Readonly<PenTypeRecord>[];
|
19
19
|
protected shapelikeIDs: string[];
|
20
20
|
private static idCounter;
|
21
21
|
constructor(editor: Editor, tool: Pen, localization?: ToolbarLocalization);
|
@@ -19,8 +19,11 @@ class PenToolWidget extends BaseToolWidget {
|
|
19
19
|
this.updateInputs = () => { };
|
20
20
|
// Pen types that correspond to
|
21
21
|
this.shapelikeIDs = ['pressure-sensitive-pen', 'freehand-pen'];
|
22
|
+
// Additional client-specified pens.
|
23
|
+
const additionalPens = editor.getCurrentSettings().pens?.additionalPenTypes ?? [];
|
22
24
|
// Default pen types
|
23
25
|
this.penTypes = [
|
26
|
+
// Non-shape pens
|
24
27
|
{
|
25
28
|
name: this.localizationTable.flatTipPen,
|
26
29
|
id: 'pressure-sensitive-pen',
|
@@ -31,6 +34,8 @@ class PenToolWidget extends BaseToolWidget {
|
|
31
34
|
id: 'freehand-pen',
|
32
35
|
factory: makeFreehandLineBuilder,
|
33
36
|
},
|
37
|
+
...(additionalPens.filter(pen => !pen.isShapeBuilder)),
|
38
|
+
// Shape pens
|
34
39
|
{
|
35
40
|
name: this.localizationTable.arrowPen,
|
36
41
|
id: 'arrow',
|
@@ -60,7 +65,8 @@ class PenToolWidget extends BaseToolWidget {
|
|
60
65
|
id: 'outlined-circle',
|
61
66
|
isShapeBuilder: true,
|
62
67
|
factory: makeOutlinedCircleBuilder,
|
63
|
-
}
|
68
|
+
},
|
69
|
+
...(additionalPens.filter(pen => pen.isShapeBuilder)),
|
64
70
|
];
|
65
71
|
this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
|
66
72
|
if (toolEvt.kind !== EditorEventType.ToolUpdated) {
|
@@ -45,7 +45,7 @@ export default class Eraser extends BaseTool {
|
|
45
45
|
return Rect2.fromCorners(centerPoint.minus(halfSize), centerPoint.plus(halfSize));
|
46
46
|
}
|
47
47
|
eraseTo(currentPoint) {
|
48
|
-
if (!this.isFirstEraseEvt && currentPoint.
|
48
|
+
if (!this.isFirstEraseEvt && currentPoint.distanceTo(this.lastPoint) === 0) {
|
49
49
|
return;
|
50
50
|
}
|
51
51
|
this.isFirstEraseEvt = false;
|
@@ -8,10 +8,10 @@ var StabilizerType;
|
|
8
8
|
})(StabilizerType || (StabilizerType = {}));
|
9
9
|
const defaultOptions = {
|
10
10
|
kind: StabilizerType.IntertialStabilizer,
|
11
|
-
mass: 0.4,
|
12
|
-
springConstant: 100.0,
|
11
|
+
mass: 0.4, // kg
|
12
|
+
springConstant: 100.0, // N/m
|
13
13
|
frictionCoefficient: 0.28,
|
14
|
-
maxPointDist: 10,
|
14
|
+
maxPointDist: 10, // screen units
|
15
15
|
inertiaFraction: 0.75,
|
16
16
|
minSimilarityToFinalize: 0.0,
|
17
17
|
velocityDecayFactor: 0.1,
|
@@ -23,8 +23,29 @@ export default class PasteHandler extends BaseTool {
|
|
23
23
|
// @internal
|
24
24
|
onPaste(event) {
|
25
25
|
const mime = event.mime.toLowerCase();
|
26
|
-
|
27
|
-
|
26
|
+
const svgData = (() => {
|
27
|
+
if (mime === 'image/svg+xml') {
|
28
|
+
return event.data;
|
29
|
+
}
|
30
|
+
if (mime !== 'text/html') {
|
31
|
+
return false;
|
32
|
+
}
|
33
|
+
// text/html is sometimes handlable SVG data. Use a hueristic
|
34
|
+
// to determine if this is the case:
|
35
|
+
// We use [^] and not . so that newlines are included.
|
36
|
+
const match = event.data.match(/^[^]{0,200}<svg.*/i); // [^]{0,200} <- Allow for metadata near start
|
37
|
+
if (!match) {
|
38
|
+
return false;
|
39
|
+
}
|
40
|
+
// Extract the SVG element from the pasted data
|
41
|
+
let svgEnd = event.data.toLowerCase().lastIndexOf('</svg>');
|
42
|
+
if (svgEnd === -1)
|
43
|
+
svgEnd = event.data.length;
|
44
|
+
return event.data.substring(event.data.search(/<svg/i), svgEnd);
|
45
|
+
})();
|
46
|
+
if (svgData) {
|
47
|
+
console.log('svgpaste', svgData);
|
48
|
+
void this.doSVGPaste(svgData);
|
28
49
|
return true;
|
29
50
|
}
|
30
51
|
else if (mime === 'text/plain') {
|
@@ -38,16 +59,21 @@ export default class PasteHandler extends BaseTool {
|
|
38
59
|
return false;
|
39
60
|
}
|
40
61
|
async addComponentsFromPaste(components) {
|
41
|
-
await this.editor.addAndCenterComponents(components);
|
62
|
+
await this.editor.addAndCenterComponents(components, true, this.editor.localization.pasted(components.length));
|
42
63
|
}
|
43
64
|
async doSVGPaste(data) {
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
65
|
+
this.editor.showLoadingWarning(0);
|
66
|
+
try {
|
67
|
+
const loader = SVGLoader.fromString(data, true);
|
68
|
+
const components = [];
|
69
|
+
await loader.start((component) => {
|
70
|
+
components.push(component);
|
71
|
+
}, (_countProcessed, _totalToProcess) => null);
|
72
|
+
await this.addComponentsFromPaste(components);
|
73
|
+
}
|
74
|
+
finally {
|
75
|
+
this.editor.hideLoadingWarning();
|
76
|
+
}
|
51
77
|
}
|
52
78
|
async doTextPaste(text) {
|
53
79
|
const textTools = this.editor.toolController.getMatchingTools(TextTool);
|
@@ -62,6 +88,10 @@ export default class PasteHandler extends BaseTool {
|
|
62
88
|
});
|
63
89
|
const defaultTextStyle = { size: 12, fontFamily: 'sans', renderingStyle: { fill: Color4.red } };
|
64
90
|
const pastedTextStyle = textTools[0]?.getTextStyle() ?? defaultTextStyle;
|
91
|
+
// Don't paste text that would be invisible.
|
92
|
+
if (text.trim() === '') {
|
93
|
+
return;
|
94
|
+
}
|
65
95
|
const lines = text.split('\n');
|
66
96
|
await this.addComponentsFromPaste([TextComponent.fromLines(lines, Mat33.identity, pastedTextStyle)]);
|
67
97
|
}
|
package/dist/mjs/tools/Pen.mjs
CHANGED
@@ -97,8 +97,8 @@ export default class Pen extends BaseTool {
|
|
97
97
|
this.currentDeviceType = current.device;
|
98
98
|
if (this.shapeAutocompletionEnabled) {
|
99
99
|
const stationaryDetectionConfig = {
|
100
|
-
maxSpeed: 8.5,
|
101
|
-
maxRadius: 11,
|
100
|
+
maxSpeed: 8.5, // screenPx/s
|
101
|
+
maxRadius: 11, // screenPx
|
102
102
|
minTimeSeconds: 0.5, // s
|
103
103
|
};
|
104
104
|
this.stationaryDetector = new StationaryPenDetector(current, stationaryDetectionConfig, pointer => this.autocorrectShape(pointer));
|
@@ -2,6 +2,7 @@ import { Mat33, Vec2 } from '@js-draw/math';
|
|
2
2
|
import { EditorEventType } from '../../types.mjs';
|
3
3
|
import Viewport from '../../Viewport.mjs';
|
4
4
|
import BaseTool from '../BaseTool.mjs';
|
5
|
+
import CanvasRenderer from '../../rendering/renderers/CanvasRenderer.mjs';
|
5
6
|
import SVGRenderer from '../../rendering/renderers/SVGRenderer.mjs';
|
6
7
|
import Selection from './Selection.mjs';
|
7
8
|
import TextComponent from '../../components/TextComponent.mjs';
|
@@ -370,19 +371,37 @@ class SelectionTool extends BaseTool {
|
|
370
371
|
return false;
|
371
372
|
}
|
372
373
|
const exportViewport = new Viewport(() => { });
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
374
|
+
const selectionScreenSize = this.selectionBox.getScreenRegion().size.times(this.editor.display.getDevicePixelRatio());
|
375
|
+
// Update the viewport to have screen size roughly equal to the size of the selection box
|
376
|
+
let scaleFactor = selectionScreenSize.maximumEntryMagnitude() / (bbox.size.maximumEntryMagnitude() || 1);
|
377
|
+
// Round to a nearby power of two
|
378
|
+
scaleFactor = Math.pow(2, Math.ceil(Math.log2(scaleFactor)));
|
379
|
+
exportViewport.updateScreenSize(bbox.size.times(scaleFactor));
|
380
|
+
exportViewport.resetTransform(Mat33.scaling2D(scaleFactor)
|
381
|
+
// Move the selection onto the screen
|
382
|
+
.rightMul(Mat33.translation(bbox.topLeft.times(-1))));
|
383
|
+
const { element: svgExportElem, renderer: svgRenderer } = SVGRenderer.fromViewport(exportViewport, { sanitize: true, useViewBoxForPositioning: true });
|
384
|
+
const { element: canvas, renderer: canvasRenderer } = CanvasRenderer.fromViewport(exportViewport, { maxCanvasDimen: 4096 });
|
377
385
|
const text = [];
|
378
386
|
for (const elem of selectedElems) {
|
379
387
|
elem.render(svgRenderer);
|
388
|
+
elem.render(canvasRenderer);
|
380
389
|
if (elem instanceof TextComponent) {
|
381
390
|
text.push(elem.getText());
|
382
391
|
}
|
383
392
|
}
|
384
393
|
event.setData('image/svg+xml', svgExportElem.outerHTML);
|
385
394
|
event.setData('text/html', svgExportElem.outerHTML);
|
395
|
+
event.setData('image/png', new Promise((resolve, reject) => {
|
396
|
+
canvas.toBlob((blob) => {
|
397
|
+
if (blob) {
|
398
|
+
resolve(blob);
|
399
|
+
}
|
400
|
+
else {
|
401
|
+
reject('Failed to convert canvas to blob.');
|
402
|
+
}
|
403
|
+
}, 'image/png');
|
404
|
+
}));
|
386
405
|
if (text.length > 0) {
|
387
406
|
event.setData('text/plain', text.join('\n'));
|
388
407
|
}
|
@@ -23,7 +23,7 @@ export default class ToPointerAutoscroller {
|
|
23
23
|
return Vec2.zero;
|
24
24
|
}
|
25
25
|
const closestEdgePoint = autoscrollBoundary.getClosestPointOnBoundaryTo(screenPoint);
|
26
|
-
const distToEdge = closestEdgePoint.
|
26
|
+
const distToEdge = closestEdgePoint.distanceTo(screenPoint);
|
27
27
|
const toEdge = closestEdgePoint.minus(screenPoint);
|
28
28
|
// Go faster for points further away from the boundary.
|
29
29
|
const maximumScaleFactor = 1.25;
|