js-draw 0.13.1 → 0.14.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/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.js +4 -4
- package/dist/src/SVGLoader.js +8 -2
- package/dist/src/Viewport.d.ts +1 -0
- package/dist/src/Viewport.js +6 -3
- package/dist/src/math/Path.js +10 -3
- package/dist/src/toolbar/IconProvider.d.ts +28 -2
- package/dist/src/toolbar/IconProvider.js +27 -2
- package/dist/src/tools/SelectionTool/Selection.d.ts +6 -0
- package/dist/src/tools/SelectionTool/Selection.js +12 -3
- package/dist/src/tools/SelectionTool/SelectionTool.js +5 -3
- package/dist/src/tools/SelectionTool/TransformMode.js +1 -1
- package/package.json +1 -1
- package/src/Editor.ts +4 -4
- package/src/SVGLoader.ts +9 -2
- package/src/Viewport.ts +7 -3
- package/src/math/Path.toString.test.ts +10 -0
- package/src/math/Path.ts +11 -3
- package/src/toolbar/IconProvider.ts +28 -6
- package/src/toolbar/toolbar.css +3 -0
- package/src/tools/SelectionTool/Selection.ts +16 -5
- package/src/tools/SelectionTool/SelectionTool.ts +5 -4
- package/src/tools/SelectionTool/TransformMode.ts +1 -1
package/dist/src/Editor.js
CHANGED
@@ -196,7 +196,7 @@ export class Editor {
|
|
196
196
|
let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
|
197
197
|
// Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
|
198
198
|
// pinch-zooming.
|
199
|
-
if (!evt.ctrlKey) {
|
199
|
+
if (!evt.ctrlKey && !evt.metaKey) {
|
200
200
|
if (!this.settings.wheelEventsEnabled) {
|
201
201
|
return;
|
202
202
|
}
|
@@ -213,7 +213,7 @@ export class Editor {
|
|
213
213
|
else if (evt.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
214
214
|
delta = delta.times(100);
|
215
215
|
}
|
216
|
-
if (evt.ctrlKey) {
|
216
|
+
if (evt.ctrlKey || evt.metaKey) {
|
217
217
|
delta = Vec3.of(0, 0, evt.deltaY);
|
218
218
|
}
|
219
219
|
// Ensure that `pos` is relative to `this.container`
|
@@ -441,7 +441,7 @@ export class Editor {
|
|
441
441
|
else if (this.toolController.dispatchInputEvent({
|
442
442
|
kind: InputEvtType.KeyPressEvent,
|
443
443
|
key: evt.key,
|
444
|
-
ctrlKey: evt.ctrlKey,
|
444
|
+
ctrlKey: evt.ctrlKey || evt.metaKey,
|
445
445
|
altKey: evt.altKey,
|
446
446
|
})) {
|
447
447
|
evt.preventDefault();
|
@@ -454,7 +454,7 @@ export class Editor {
|
|
454
454
|
if (this.toolController.dispatchInputEvent({
|
455
455
|
kind: InputEvtType.KeyUpEvent,
|
456
456
|
key: evt.key,
|
457
|
-
ctrlKey: evt.ctrlKey,
|
457
|
+
ctrlKey: evt.ctrlKey || evt.metaKey,
|
458
458
|
altKey: evt.altKey,
|
459
459
|
})) {
|
460
460
|
evt.preventDefault();
|
package/dist/src/SVGLoader.js
CHANGED
@@ -202,7 +202,13 @@ export default class SVGLoader {
|
|
202
202
|
}
|
203
203
|
// Compute styles.
|
204
204
|
const computedStyles = window.getComputedStyle(elem);
|
205
|
-
const
|
205
|
+
const fontSizeExp = /^([-0-9.e]+)px/i;
|
206
|
+
// In some environments, computedStyles.fontSize can be increased by the system.
|
207
|
+
// Thus, to prevent text from growing on load/save, prefer .style.fontSize.
|
208
|
+
let fontSizeMatch = fontSizeExp.exec(elem.style.fontSize);
|
209
|
+
if (!fontSizeMatch) {
|
210
|
+
fontSizeMatch = fontSizeExp.exec(computedStyles.fontSize);
|
211
|
+
}
|
206
212
|
const supportedStyleAttrs = [
|
207
213
|
'fontFamily',
|
208
214
|
'transform',
|
@@ -390,7 +396,7 @@ export default class SVGLoader {
|
|
390
396
|
<meta name='viewport' conent='width=device-width,initial-scale=1.0'/>
|
391
397
|
<meta charset='utf-8'/>
|
392
398
|
</head>
|
393
|
-
<body>
|
399
|
+
<body style='font-size: 12px;'>
|
394
400
|
<script>
|
395
401
|
console.error('JavaScript should not be able to run here!');
|
396
402
|
throw new Error(
|
package/dist/src/Viewport.d.ts
CHANGED
@@ -44,6 +44,7 @@ export declare class Viewport {
|
|
44
44
|
* should return `100` because `100` is the nearest power of 10 to 101.
|
45
45
|
*/
|
46
46
|
getScaleFactorToNearestPowerOfTen(): number;
|
47
|
+
private getScaleFactorToNearestPowerOf;
|
47
48
|
snapToGrid(canvasPos: Point2): Vec3;
|
48
49
|
/** Returns the size of one screen pixel in canvas units. */
|
49
50
|
getSizeOfPixelOnCanvas(): number;
|
package/dist/src/Viewport.js
CHANGED
@@ -84,13 +84,16 @@ export class Viewport {
|
|
84
84
|
* should return `100` because `100` is the nearest power of 10 to 101.
|
85
85
|
*/
|
86
86
|
getScaleFactorToNearestPowerOfTen() {
|
87
|
+
return this.getScaleFactorToNearestPowerOf(10);
|
88
|
+
}
|
89
|
+
getScaleFactorToNearestPowerOf(powerOf) {
|
87
90
|
const scaleFactor = this.getScaleFactor();
|
88
|
-
return Math.pow(
|
91
|
+
return Math.pow(powerOf, Math.round(Math.log(scaleFactor) / Math.log(powerOf)));
|
89
92
|
}
|
90
93
|
snapToGrid(canvasPos) {
|
91
94
|
const snapCoordinate = (coordinate) => {
|
92
|
-
const scaleFactor = this.
|
93
|
-
const roundFactor = scaleFactor /
|
95
|
+
const scaleFactor = this.getScaleFactorToNearestPowerOf(2);
|
96
|
+
const roundFactor = scaleFactor / 50;
|
94
97
|
const snapped = Math.round(coordinate * roundFactor) / roundFactor;
|
95
98
|
return snapped;
|
96
99
|
};
|
package/dist/src/math/Path.js
CHANGED
@@ -347,6 +347,7 @@ export default class Path {
|
|
347
347
|
// @param onlyAbsCommands - True if we should avoid converting absolute coordinates to relative offsets -- such
|
348
348
|
// conversions can lead to smaller output strings, but also take time.
|
349
349
|
static toString(startPoint, parts, onlyAbsCommands) {
|
350
|
+
var _a;
|
350
351
|
const result = [];
|
351
352
|
let prevPoint;
|
352
353
|
const addCommand = (command, ...points) => {
|
@@ -382,7 +383,7 @@ export default class Path {
|
|
382
383
|
commandString = `${command.toLowerCase()}${relativeCommandParts.join(' ')}`;
|
383
384
|
}
|
384
385
|
// Don't add no-ops.
|
385
|
-
if (commandString === 'l0,0') {
|
386
|
+
if (commandString === 'l0,0' || commandString === 'm0,0') {
|
386
387
|
return;
|
387
388
|
}
|
388
389
|
result.push(commandString);
|
@@ -390,9 +391,15 @@ export default class Path {
|
|
390
391
|
prevPoint = points[points.length - 1];
|
391
392
|
}
|
392
393
|
};
|
393
|
-
|
394
|
+
// Don't add two moveTos in a row (this can happen if
|
395
|
+
// the start point corresponds to a moveTo _and_ the first command is
|
396
|
+
// also a moveTo)
|
397
|
+
if (((_a = parts[0]) === null || _a === void 0 ? void 0 : _a.kind) !== PathCommandType.MoveTo) {
|
398
|
+
addCommand('M', startPoint);
|
399
|
+
}
|
394
400
|
let exhaustivenessCheck;
|
395
|
-
for (
|
401
|
+
for (let i = 0; i < parts.length; i++) {
|
402
|
+
const part = parts[i];
|
396
403
|
switch (part.kind) {
|
397
404
|
case PathCommandType.MoveTo:
|
398
405
|
addCommand('M', part.point);
|
@@ -2,7 +2,34 @@ import Color4 from '../Color4';
|
|
2
2
|
import { ComponentBuilderFactory } from '../components/builders/types';
|
3
3
|
import { TextStyle } from '../components/TextComponent';
|
4
4
|
import Pen from '../tools/Pen';
|
5
|
-
type IconType =
|
5
|
+
export type IconType = HTMLImageElement | SVGElement;
|
6
|
+
/**
|
7
|
+
* Provides icons that can be used in the toolbar, etc.
|
8
|
+
* Extend this class and override methods to customize icons.
|
9
|
+
*
|
10
|
+
* @example
|
11
|
+
* ```ts
|
12
|
+
* class CustomIconProvider extends jsdraw.IconProvider {
|
13
|
+
* // Use '☺' instead of the default dropdown symbol.
|
14
|
+
* public makeDropdownIcon() {
|
15
|
+
* const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
16
|
+
* icon.innerHTML = `
|
17
|
+
* <text x='5' y='55' style='fill: var(--icon-color); font-size: 50pt;'>☺</text>
|
18
|
+
* `;
|
19
|
+
* icon.setAttribute('viewBox', '0 0 100 100');
|
20
|
+
* return icon;
|
21
|
+
* }
|
22
|
+
* }
|
23
|
+
*
|
24
|
+
* const icons = new CustomIconProvider();
|
25
|
+
* const editor = new jsdraw.Editor(document.body, {
|
26
|
+
* iconProvider: icons,
|
27
|
+
* });
|
28
|
+
*
|
29
|
+
* // Add a toolbar that uses these icons
|
30
|
+
* editor.addToolbar();
|
31
|
+
* ```
|
32
|
+
*/
|
6
33
|
export default class IconProvider {
|
7
34
|
makeUndoIcon(): IconType;
|
8
35
|
makeRedoIcon(mirror?: boolean): IconType;
|
@@ -30,4 +57,3 @@ export default class IconProvider {
|
|
30
57
|
makeDeleteSelectionIcon(): IconType;
|
31
58
|
makeSaveIcon(): IconType;
|
32
59
|
}
|
33
|
-
export {};
|
@@ -24,8 +24,33 @@ const checkerboardPatternDef = `
|
|
24
24
|
</pattern>
|
25
25
|
`;
|
26
26
|
const checkerboardPatternRef = 'url(#checkerboard)';
|
27
|
-
|
28
|
-
|
27
|
+
/**
|
28
|
+
* Provides icons that can be used in the toolbar, etc.
|
29
|
+
* Extend this class and override methods to customize icons.
|
30
|
+
*
|
31
|
+
* @example
|
32
|
+
* ```ts
|
33
|
+
* class CustomIconProvider extends jsdraw.IconProvider {
|
34
|
+
* // Use '☺' instead of the default dropdown symbol.
|
35
|
+
* public makeDropdownIcon() {
|
36
|
+
* const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
37
|
+
* icon.innerHTML = `
|
38
|
+
* <text x='5' y='55' style='fill: var(--icon-color); font-size: 50pt;'>☺</text>
|
39
|
+
* `;
|
40
|
+
* icon.setAttribute('viewBox', '0 0 100 100');
|
41
|
+
* return icon;
|
42
|
+
* }
|
43
|
+
* }
|
44
|
+
*
|
45
|
+
* const icons = new CustomIconProvider();
|
46
|
+
* const editor = new jsdraw.Editor(document.body, {
|
47
|
+
* iconProvider: icons,
|
48
|
+
* });
|
49
|
+
*
|
50
|
+
* // Add a toolbar that uses these icons
|
51
|
+
* editor.addToolbar();
|
52
|
+
* ```
|
53
|
+
*/
|
29
54
|
export default class IconProvider {
|
30
55
|
makeUndoIcon() {
|
31
56
|
return this.makeRedoIcon(true);
|
@@ -23,6 +23,12 @@ export default class Selection {
|
|
23
23
|
getTransform(): Mat33;
|
24
24
|
get preTransformRegion(): Rect2;
|
25
25
|
get region(): Rect2;
|
26
|
+
/**
|
27
|
+
* Computes and returns the bounding box of the selection without
|
28
|
+
* any additional padding. Computes directly from the elements that are selected.
|
29
|
+
* @internal
|
30
|
+
*/
|
31
|
+
computeTightBoundingBox(): Rect2;
|
26
32
|
get regionRotation(): number;
|
27
33
|
get preTransformedScreenRegion(): Rect2;
|
28
34
|
get preTransformedScreenRegionRotation(): number;
|
@@ -77,6 +77,17 @@ export default class Selection {
|
|
77
77
|
const scaleAndTranslateMat = this.transform.rightMul(rotationMatrix.inverse());
|
78
78
|
return this.originalRegion.transformedBoundingBox(scaleAndTranslateMat);
|
79
79
|
}
|
80
|
+
/**
|
81
|
+
* Computes and returns the bounding box of the selection without
|
82
|
+
* any additional padding. Computes directly from the elements that are selected.
|
83
|
+
* @internal
|
84
|
+
*/
|
85
|
+
computeTightBoundingBox() {
|
86
|
+
const bbox = this.selectedElems.reduce((accumulator, elem) => {
|
87
|
+
return (accumulator !== null && accumulator !== void 0 ? accumulator : elem.getBBox()).union(elem.getBBox());
|
88
|
+
}, null);
|
89
|
+
return bbox !== null && bbox !== void 0 ? bbox : Rect2.empty;
|
90
|
+
}
|
80
91
|
get regionRotation() {
|
81
92
|
return this.transform.transformVec3(Vec2.unitX).angle();
|
82
93
|
}
|
@@ -161,9 +172,7 @@ export default class Selection {
|
|
161
172
|
// Recompute this' region from the selected elements.
|
162
173
|
// Returns false if the selection is empty.
|
163
174
|
recomputeRegion() {
|
164
|
-
const newRegion = this.
|
165
|
-
return (accumulator !== null && accumulator !== void 0 ? accumulator : elem.getBBox()).union(elem.getBBox());
|
166
|
-
}, null);
|
175
|
+
const newRegion = this.computeTightBoundingBox();
|
167
176
|
if (!newRegion) {
|
168
177
|
this.cancelSelection();
|
169
178
|
return false;
|
@@ -47,10 +47,12 @@ export default class SelectionTool extends BaseTool {
|
|
47
47
|
snapSelectionToGrid() {
|
48
48
|
if (!this.selectionBox)
|
49
49
|
throw new Error('No selection to snap!');
|
50
|
-
|
51
|
-
const
|
50
|
+
// Snap the top left corner of what we have selected.
|
51
|
+
const topLeftOfBBox = this.selectionBox.computeTightBoundingBox().topLeft;
|
52
|
+
const snappedTopLeft = this.editor.viewport.snapToGrid(topLeftOfBBox);
|
53
|
+
const snapDelta = snappedTopLeft.minus(topLeftOfBBox);
|
52
54
|
const oldTransform = this.selectionBox.getTransform();
|
53
|
-
this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(
|
55
|
+
this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(snapDelta)));
|
54
56
|
this.selectionBox.finalizeTransform();
|
55
57
|
}
|
56
58
|
onPointerDown({ allPointers, current }) {
|
@@ -51,7 +51,7 @@ export class ResizeTransformer {
|
|
51
51
|
// Round: If this isn't done, scaling can create numbers with long decimal representations.
|
52
52
|
// long decimal representations => large file sizes.
|
53
53
|
scale = scale.map(component => Viewport.roundScaleRatio(component, 2));
|
54
|
-
if (scale.x
|
54
|
+
if (scale.x !== 0 && scale.y !== 0) {
|
55
55
|
const origin = this.editor.viewport.roundPoint(this.selection.preTransformRegion.topLeft);
|
56
56
|
this.selection.setTransform(Mat33.scaling2D(scale, origin));
|
57
57
|
}
|
package/package.json
CHANGED
package/src/Editor.ts
CHANGED
@@ -315,7 +315,7 @@ export class Editor {
|
|
315
315
|
|
316
316
|
// Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
|
317
317
|
// pinch-zooming.
|
318
|
-
if (!evt.ctrlKey) {
|
318
|
+
if (!evt.ctrlKey && !evt.metaKey) {
|
319
319
|
if (!this.settings.wheelEventsEnabled) {
|
320
320
|
return;
|
321
321
|
} else if (this.settings.wheelEventsEnabled === 'only-if-focused') {
|
@@ -333,7 +333,7 @@ export class Editor {
|
|
333
333
|
delta = delta.times(100);
|
334
334
|
}
|
335
335
|
|
336
|
-
if (evt.ctrlKey) {
|
336
|
+
if (evt.ctrlKey || evt.metaKey) {
|
337
337
|
delta = Vec3.of(0, 0, evt.deltaY);
|
338
338
|
}
|
339
339
|
|
@@ -598,7 +598,7 @@ export class Editor {
|
|
598
598
|
} else if (this.toolController.dispatchInputEvent({
|
599
599
|
kind: InputEvtType.KeyPressEvent,
|
600
600
|
key: evt.key,
|
601
|
-
ctrlKey: evt.ctrlKey,
|
601
|
+
ctrlKey: evt.ctrlKey || evt.metaKey,
|
602
602
|
altKey: evt.altKey,
|
603
603
|
})) {
|
604
604
|
evt.preventDefault();
|
@@ -611,7 +611,7 @@ export class Editor {
|
|
611
611
|
if (this.toolController.dispatchInputEvent({
|
612
612
|
kind: InputEvtType.KeyUpEvent,
|
613
613
|
key: evt.key,
|
614
|
-
ctrlKey: evt.ctrlKey,
|
614
|
+
ctrlKey: evt.ctrlKey || evt.metaKey,
|
615
615
|
altKey: evt.altKey,
|
616
616
|
})) {
|
617
617
|
evt.preventDefault();
|
package/src/SVGLoader.ts
CHANGED
@@ -244,7 +244,14 @@ export default class SVGLoader implements ImageLoader {
|
|
244
244
|
|
245
245
|
// Compute styles.
|
246
246
|
const computedStyles = window.getComputedStyle(elem);
|
247
|
-
const
|
247
|
+
const fontSizeExp = /^([-0-9.e]+)px/i;
|
248
|
+
|
249
|
+
// In some environments, computedStyles.fontSize can be increased by the system.
|
250
|
+
// Thus, to prevent text from growing on load/save, prefer .style.fontSize.
|
251
|
+
let fontSizeMatch = fontSizeExp.exec(elem.style.fontSize);
|
252
|
+
if (!fontSizeMatch) {
|
253
|
+
fontSizeMatch = fontSizeExp.exec(computedStyles.fontSize);
|
254
|
+
}
|
248
255
|
|
249
256
|
const supportedStyleAttrs = [
|
250
257
|
'fontFamily',
|
@@ -455,7 +462,7 @@ export default class SVGLoader implements ImageLoader {
|
|
455
462
|
<meta name='viewport' conent='width=device-width,initial-scale=1.0'/>
|
456
463
|
<meta charset='utf-8'/>
|
457
464
|
</head>
|
458
|
-
<body>
|
465
|
+
<body style='font-size: 12px;'>
|
459
466
|
<script>
|
460
467
|
console.error('JavaScript should not be able to run here!');
|
461
468
|
throw new Error(
|
package/src/Viewport.ts
CHANGED
@@ -157,14 +157,18 @@ export class Viewport {
|
|
157
157
|
* should return `100` because `100` is the nearest power of 10 to 101.
|
158
158
|
*/
|
159
159
|
public getScaleFactorToNearestPowerOfTen() {
|
160
|
+
return this.getScaleFactorToNearestPowerOf(10);
|
161
|
+
}
|
162
|
+
|
163
|
+
private getScaleFactorToNearestPowerOf(powerOf: number) {
|
160
164
|
const scaleFactor = this.getScaleFactor();
|
161
|
-
return Math.pow(
|
165
|
+
return Math.pow(powerOf, Math.round(Math.log(scaleFactor) / Math.log(powerOf)));
|
162
166
|
}
|
163
167
|
|
164
168
|
public snapToGrid(canvasPos: Point2) {
|
165
169
|
const snapCoordinate = (coordinate: number) => {
|
166
|
-
const scaleFactor = this.
|
167
|
-
const roundFactor = scaleFactor /
|
170
|
+
const scaleFactor = this.getScaleFactorToNearestPowerOf(2);
|
171
|
+
const roundFactor = scaleFactor / 50;
|
168
172
|
const snapped = Math.round(coordinate * roundFactor) / roundFactor;
|
169
173
|
|
170
174
|
return snapped;
|
@@ -64,4 +64,14 @@ describe('Path.toString', () => {
|
|
64
64
|
|
65
65
|
expect(path.toString(true)).toBe(path1.toString(true));
|
66
66
|
});
|
67
|
+
|
68
|
+
it('should remove no-op move-tos', () => {
|
69
|
+
const path1 = Path.fromString('M50,75m0,0q0,12.5 0,50q0,6.3 25,0');
|
70
|
+
path1['cachedStringVersion'] = null;
|
71
|
+
const path2 = Path.fromString('M150,175M150,175q0,12.5 0,50q0,6.3 25,0');
|
72
|
+
path2['cachedStringVersion'] = null;
|
73
|
+
|
74
|
+
expect(path1.toString()).toBe('M50,75q0,12.5 0,50q0,6.3 25,0');
|
75
|
+
expect(path2.toString()).toBe('M150,175q0,12.5 0,50q0,6.3 25,0');
|
76
|
+
});
|
67
77
|
});
|
package/src/math/Path.ts
CHANGED
@@ -493,7 +493,7 @@ export default class Path {
|
|
493
493
|
}
|
494
494
|
|
495
495
|
// Don't add no-ops.
|
496
|
-
if (commandString === 'l0,0') {
|
496
|
+
if (commandString === 'l0,0' || commandString === 'm0,0') {
|
497
497
|
return;
|
498
498
|
}
|
499
499
|
result.push(commandString);
|
@@ -503,9 +503,17 @@ export default class Path {
|
|
503
503
|
}
|
504
504
|
};
|
505
505
|
|
506
|
-
|
506
|
+
// Don't add two moveTos in a row (this can happen if
|
507
|
+
// the start point corresponds to a moveTo _and_ the first command is
|
508
|
+
// also a moveTo)
|
509
|
+
if (parts[0]?.kind !== PathCommandType.MoveTo) {
|
510
|
+
addCommand('M', startPoint);
|
511
|
+
}
|
512
|
+
|
507
513
|
let exhaustivenessCheck: never;
|
508
|
-
for (
|
514
|
+
for (let i = 0; i < parts.length; i++) {
|
515
|
+
const part = parts[i];
|
516
|
+
|
509
517
|
switch (part.kind) {
|
510
518
|
case PathCommandType.MoveTo:
|
511
519
|
addCommand('M', part.point);
|
@@ -8,10 +8,7 @@ import Pen from '../tools/Pen';
|
|
8
8
|
import { StrokeDataPoint } from '../types';
|
9
9
|
import Viewport from '../Viewport';
|
10
10
|
|
11
|
-
|
12
|
-
// Many of the icons were created with Inkscape.
|
13
|
-
|
14
|
-
type IconType = SVGSVGElement|HTMLImageElement;
|
11
|
+
export type IconType = HTMLImageElement|SVGElement;
|
15
12
|
|
16
13
|
const svgNamespace = 'http://www.w3.org/2000/svg';
|
17
14
|
const iconColorFill = `
|
@@ -35,8 +32,33 @@ const checkerboardPatternDef = `
|
|
35
32
|
`;
|
36
33
|
const checkerboardPatternRef = 'url(#checkerboard)';
|
37
34
|
|
38
|
-
|
39
|
-
|
35
|
+
/**
|
36
|
+
* Provides icons that can be used in the toolbar, etc.
|
37
|
+
* Extend this class and override methods to customize icons.
|
38
|
+
*
|
39
|
+
* @example
|
40
|
+
* ```ts
|
41
|
+
* class CustomIconProvider extends jsdraw.IconProvider {
|
42
|
+
* // Use '☺' instead of the default dropdown symbol.
|
43
|
+
* public makeDropdownIcon() {
|
44
|
+
* const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
45
|
+
* icon.innerHTML = `
|
46
|
+
* <text x='5' y='55' style='fill: var(--icon-color); font-size: 50pt;'>☺</text>
|
47
|
+
* `;
|
48
|
+
* icon.setAttribute('viewBox', '0 0 100 100');
|
49
|
+
* return icon;
|
50
|
+
* }
|
51
|
+
* }
|
52
|
+
*
|
53
|
+
* const icons = new CustomIconProvider();
|
54
|
+
* const editor = new jsdraw.Editor(document.body, {
|
55
|
+
* iconProvider: icons,
|
56
|
+
* });
|
57
|
+
*
|
58
|
+
* // Add a toolbar that uses these icons
|
59
|
+
* editor.addToolbar();
|
60
|
+
* ```
|
61
|
+
*/
|
40
62
|
export default class IconProvider {
|
41
63
|
|
42
64
|
public makeUndoIcon(): IconType {
|
package/src/toolbar/toolbar.css
CHANGED
@@ -33,6 +33,7 @@
|
|
33
33
|
|
34
34
|
.toolbar-dropdown .toolbar-button > .toolbar-icon {
|
35
35
|
max-width: 50px;
|
36
|
+
width: 100%;
|
36
37
|
}
|
37
38
|
|
38
39
|
.toolbar-button.disabled {
|
@@ -89,6 +90,8 @@
|
|
89
90
|
|
90
91
|
.toolbar-root .toolbar-icon {
|
91
92
|
flex-shrink: 1;
|
93
|
+
|
94
|
+
width: 100%;
|
92
95
|
min-width: 30px;
|
93
96
|
min-height: 30px;
|
94
97
|
}
|
@@ -121,6 +121,21 @@ export default class Selection {
|
|
121
121
|
return this.originalRegion.transformedBoundingBox(scaleAndTranslateMat);
|
122
122
|
}
|
123
123
|
|
124
|
+
/**
|
125
|
+
* Computes and returns the bounding box of the selection without
|
126
|
+
* any additional padding. Computes directly from the elements that are selected.
|
127
|
+
* @internal
|
128
|
+
*/
|
129
|
+
public computeTightBoundingBox() {
|
130
|
+
const bbox = this.selectedElems.reduce((
|
131
|
+
accumulator: Rect2|null, elem: AbstractComponent
|
132
|
+
): Rect2 => {
|
133
|
+
return (accumulator ?? elem.getBBox()).union(elem.getBBox());
|
134
|
+
}, null);
|
135
|
+
|
136
|
+
return bbox ?? Rect2.empty;
|
137
|
+
}
|
138
|
+
|
124
139
|
public get regionRotation(): number {
|
125
140
|
return this.transform.transformVec3(Vec2.unitX).angle();
|
126
141
|
}
|
@@ -328,11 +343,7 @@ export default class Selection {
|
|
328
343
|
// Recompute this' region from the selected elements.
|
329
344
|
// Returns false if the selection is empty.
|
330
345
|
public recomputeRegion(): boolean {
|
331
|
-
const newRegion = this.
|
332
|
-
accumulator: Rect2|null, elem: AbstractComponent
|
333
|
-
): Rect2 => {
|
334
|
-
return (accumulator ?? elem.getBBox()).union(elem.getBBox());
|
335
|
-
}, null);
|
346
|
+
const newRegion = this.computeTightBoundingBox();
|
336
347
|
|
337
348
|
if (!newRegion) {
|
338
349
|
this.cancelSelection();
|
@@ -62,12 +62,13 @@ export default class SelectionTool extends BaseTool {
|
|
62
62
|
private snapSelectionToGrid() {
|
63
63
|
if (!this.selectionBox) throw new Error('No selection to snap!');
|
64
64
|
|
65
|
-
|
66
|
-
const
|
67
|
-
|
65
|
+
// Snap the top left corner of what we have selected.
|
66
|
+
const topLeftOfBBox = this.selectionBox.computeTightBoundingBox().topLeft;
|
67
|
+
const snappedTopLeft = this.editor.viewport.snapToGrid(topLeftOfBBox);
|
68
|
+
const snapDelta = snappedTopLeft.minus(topLeftOfBBox);
|
68
69
|
|
69
70
|
const oldTransform = this.selectionBox.getTransform();
|
70
|
-
this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(
|
71
|
+
this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(snapDelta)));
|
71
72
|
this.selectionBox.finalizeTransform();
|
72
73
|
}
|
73
74
|
|
@@ -62,7 +62,7 @@ export class ResizeTransformer {
|
|
62
62
|
// long decimal representations => large file sizes.
|
63
63
|
scale = scale.map(component => Viewport.roundScaleRatio(component, 2));
|
64
64
|
|
65
|
-
if (scale.x
|
65
|
+
if (scale.x !== 0 && scale.y !== 0) {
|
66
66
|
const origin = this.editor.viewport.roundPoint(this.selection.preTransformRegion.topLeft);
|
67
67
|
this.selection.setTransform(Mat33.scaling2D(scale, origin));
|
68
68
|
}
|