@adecore/drawing 0.23.0 → 0.24.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/dist/geometry.d.ts +2 -2
- package/dist/geometry.js +26 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/paths.d.ts +0 -4
- package/dist/paths.js +2 -2
- package/dist/svg.d.ts +3 -2
- package/dist/svg.js +6 -5
- package/package.json +2 -1
- package/src/geometry.ts +28 -3
- package/src/index.ts +5 -11
- package/src/paths.ts +2 -2
- package/src/svg.ts +13 -10
package/dist/geometry.d.ts
CHANGED
|
@@ -15,12 +15,12 @@ export declare function boundsOf(element: Pick<DrawingElement, 'x' | 'y' | 'w' |
|
|
|
15
15
|
export declare function centerOf(rect: Rect): Point;
|
|
16
16
|
export declare function roundPoint(point: Point): Point;
|
|
17
17
|
export declare function rotatePoint(point: Point, around: Point, angle: number): Point;
|
|
18
|
-
export declare function toLocal(element: DrawingElement, point: Point): Point;
|
|
19
18
|
export declare function unionOf(rects: readonly Rect[]): Rect | null;
|
|
20
19
|
export declare function boundsOfElements(elements: readonly DrawingElement[]): Rect | null;
|
|
20
|
+
export declare function drawnBoundsOf(element: DrawingElement): Rect;
|
|
21
|
+
export declare function drawnBoundsOfElements(elements: readonly DrawingElement[]): Rect | null;
|
|
21
22
|
export declare function intersects(a: Rect, b: Rect): boolean;
|
|
22
23
|
export declare function rectFromPoints(from: Point, to: Point): Rect;
|
|
23
|
-
export declare function distanceToSegment(point: Point, a: Point, b: Point): number;
|
|
24
24
|
export declare function absolutePoints(element: DrawingElement): Point[];
|
|
25
25
|
export declare function hitsElement(element: DrawingElement, point: Point, tolerance: number): boolean;
|
|
26
26
|
export declare function elementAt(elements: readonly DrawingElement[], point: Point, tolerance: number): DrawingElement | undefined;
|
package/dist/geometry.js
CHANGED
|
@@ -20,7 +20,7 @@ export function rotatePoint(point, around, angle) {
|
|
|
20
20
|
return { x: around.x + dx * cos - dy * sin, y: around.y + dx * sin + dy * cos };
|
|
21
21
|
}
|
|
22
22
|
/* A point in the element's own frame, so every hit test can pretend nothing is turned. */
|
|
23
|
-
|
|
23
|
+
function toLocal(element, point) {
|
|
24
24
|
return rotatePoint(point, centerOf(boundsOf(element)), -(element.angle ?? 0));
|
|
25
25
|
}
|
|
26
26
|
export function unionOf(rects) {
|
|
@@ -42,6 +42,29 @@ export function unionOf(rects) {
|
|
|
42
42
|
export function boundsOfElements(elements) {
|
|
43
43
|
return unionOf(elements.map(boundsOf));
|
|
44
44
|
}
|
|
45
|
+
/* The box around an element as it is drawn: the stored box, turned by its angle. */
|
|
46
|
+
export function drawnBoundsOf(element) {
|
|
47
|
+
const rect = boundsOf(element);
|
|
48
|
+
const angle = element.angle ?? 0;
|
|
49
|
+
if (angle === 0) {
|
|
50
|
+
return rect;
|
|
51
|
+
}
|
|
52
|
+
const center = centerOf(rect);
|
|
53
|
+
const corners = [
|
|
54
|
+
{ x: rect.x, y: rect.y },
|
|
55
|
+
{ x: rect.x + rect.w, y: rect.y },
|
|
56
|
+
{ x: rect.x + rect.w, y: rect.y + rect.h },
|
|
57
|
+
{ x: rect.x, y: rect.y + rect.h }
|
|
58
|
+
].map((corner) => rotatePoint(corner, center, angle));
|
|
59
|
+
const xs = corners.map((corner) => corner.x);
|
|
60
|
+
const ys = corners.map((corner) => corner.y);
|
|
61
|
+
const left = Math.min(...xs);
|
|
62
|
+
const top = Math.min(...ys);
|
|
63
|
+
return { x: left, y: top, w: Math.max(...xs) - left, h: Math.max(...ys) - top };
|
|
64
|
+
}
|
|
65
|
+
export function drawnBoundsOfElements(elements) {
|
|
66
|
+
return unionOf(elements.map(drawnBoundsOf));
|
|
67
|
+
}
|
|
45
68
|
export function intersects(a, b) {
|
|
46
69
|
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
47
70
|
}
|
|
@@ -53,7 +76,7 @@ export function rectFromPoints(from, to) {
|
|
|
53
76
|
h: Math.abs(to.y - from.y)
|
|
54
77
|
};
|
|
55
78
|
}
|
|
56
|
-
|
|
79
|
+
function distanceToSegment(point, a, b) {
|
|
57
80
|
const dx = b.x - a.x;
|
|
58
81
|
const dy = b.y - a.y;
|
|
59
82
|
const length = dx * dx + dy * dy;
|
|
@@ -154,7 +177,7 @@ export function elementAt(elements, point, tolerance) {
|
|
|
154
177
|
return undefined;
|
|
155
178
|
}
|
|
156
179
|
export function elementsIn(elements, rect) {
|
|
157
|
-
return elements.filter((element) => !element.locked && intersects(
|
|
180
|
+
return elements.filter((element) => !element.locked && intersects(drawnBoundsOf(element), rect));
|
|
158
181
|
}
|
|
159
182
|
export function handlePoint(rect, handle) {
|
|
160
183
|
const x = handle.includes('w') ? rect.x : handle.includes('e') ? rect.x + rect.w : rect.x + rect.w / 2;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { type Point, type Rect, RESIZE_HANDLES, type ResizeHandle, boundsOf, centerOf, roundPoint, rotatePoint,
|
|
2
|
-
export { type ElementPath,
|
|
1
|
+
export { type Point, type Rect, RESIZE_HANDLES, type ResizeHandle, boundsOf, centerOf, roundPoint, rotatePoint, unionOf, boundsOfElements, drawnBoundsOf, drawnBoundsOfElements, intersects, rectFromPoints, absolutePoints, elementAt, elementsIn, handlePoint, resizeRect, scaleElement } from './geometry.ts';
|
|
2
|
+
export { type ElementPath, pathsOfElement } from './paths.ts';
|
|
3
3
|
export { readingOrder } from './reading-order.ts';
|
|
4
|
-
export { type SvgOptions, DEFAULT_SVG_MARGIN, DEFAULT_PALETTE, DEFAULT_PAPER, DEFAULT_EDGE, toSvg } from './svg.ts';
|
|
5
|
-
export { LINE_HEIGHT, DEFAULT_FONT_STACKS, type WrittenElement, NOTE_PADDING,
|
|
4
|
+
export { type SvgOptions, DEFAULT_SVG_MARGIN, DEFAULT_PALETTE, DEFAULT_PAPER, DEFAULT_EDGE, escapeXml, toSvg } from './svg.ts';
|
|
5
|
+
export { LINE_HEIGHT, DEFAULT_FONT_STACKS, type WrittenElement, NOTE_PADDING, writingFrameOf, type MeasureLine, approximateMeasure, linesOf, fontOf } from './text.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { RESIZE_HANDLES, boundsOf, centerOf, roundPoint, rotatePoint,
|
|
2
|
-
export {
|
|
1
|
+
export { RESIZE_HANDLES, boundsOf, centerOf, roundPoint, rotatePoint, unionOf, boundsOfElements, drawnBoundsOf, drawnBoundsOfElements, intersects, rectFromPoints, absolutePoints, elementAt, elementsIn, handlePoint, resizeRect, scaleElement } from './geometry.js';
|
|
2
|
+
export { pathsOfElement } from './paths.js';
|
|
3
3
|
export { readingOrder } from './reading-order.js';
|
|
4
|
-
export { DEFAULT_SVG_MARGIN, DEFAULT_PALETTE, DEFAULT_PAPER, DEFAULT_EDGE, toSvg } from './svg.js';
|
|
5
|
-
export { LINE_HEIGHT, DEFAULT_FONT_STACKS, NOTE_PADDING,
|
|
4
|
+
export { DEFAULT_SVG_MARGIN, DEFAULT_PALETTE, DEFAULT_PAPER, DEFAULT_EDGE, escapeXml, toSvg } from './svg.js';
|
|
5
|
+
export { LINE_HEIGHT, DEFAULT_FONT_STACKS, NOTE_PADDING, writingFrameOf, approximateMeasure, linesOf, fontOf } from './text.js';
|
package/dist/paths.d.ts
CHANGED
|
@@ -5,8 +5,4 @@ export interface ElementPath {
|
|
|
5
5
|
strokeWidth: number;
|
|
6
6
|
dash: number[] | null;
|
|
7
7
|
}
|
|
8
|
-
export declare function outlineToPath(outline: readonly (readonly number[])[]): string;
|
|
9
|
-
export declare function freehandOutline(element: DrawingElement & {
|
|
10
|
-
kind: 'freehand';
|
|
11
|
-
}): string;
|
|
12
8
|
export declare function pathsOfElement(element: DrawingElement): ElementPath[];
|
package/dist/paths.js
CHANGED
|
@@ -46,14 +46,14 @@ function roundedRectPath(w, h, radius) {
|
|
|
46
46
|
const corner = Math.min(radius, w / 2, h / 2);
|
|
47
47
|
return `M ${corner} 0 L ${w - corner} 0 Q ${w} 0 ${w} ${corner} L ${w} ${h - corner} Q ${w} ${h} ${w - corner} ${h} L ${corner} ${h} Q 0 ${h} 0 ${h - corner} L 0 ${corner} Q 0 0 ${corner} 0 Z`;
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
function outlineToPath(outline) {
|
|
50
50
|
if (outline.length === 0) {
|
|
51
51
|
return '';
|
|
52
52
|
}
|
|
53
53
|
const [first, ...rest] = outline;
|
|
54
54
|
return `M ${first[0]} ${first[1]} ${rest.map((point) => `L ${point[0]} ${point[1]}`).join(' ')} Z`;
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
function freehandOutline(element) {
|
|
57
57
|
const points = element.points.map(([x, y, pressure]) => [x, y, pressure ?? 0.5]);
|
|
58
58
|
const outline = getStroke(points, {
|
|
59
59
|
size: element.strokeWidth * 4 + 2,
|
package/dist/svg.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DrawingColor, DrawingElement, DrawingFont } from './protocol.ts';
|
|
2
2
|
import { type MeasureLine, type WrittenElement } from './text.ts';
|
|
3
3
|
export interface SvgOptions {
|
|
4
|
-
palette
|
|
4
|
+
palette?: Record<DrawingColor, string>;
|
|
5
5
|
background?: string | null;
|
|
6
6
|
margin?: number;
|
|
7
7
|
fonts?: Partial<Record<DrawingFont, string>>;
|
|
@@ -13,4 +13,5 @@ export declare const DEFAULT_SVG_MARGIN = 32;
|
|
|
13
13
|
export declare const DEFAULT_PALETTE: Record<DrawingColor, string>;
|
|
14
14
|
export declare const DEFAULT_PAPER: Record<DrawingColor, string>;
|
|
15
15
|
export declare const DEFAULT_EDGE: Record<DrawingColor, string>;
|
|
16
|
-
export declare function
|
|
16
|
+
export declare function escapeXml(value: string): string;
|
|
17
|
+
export declare function toSvg(elements: readonly DrawingElement[], options?: SvgOptions): string;
|
package/dist/svg.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { drawnBoundsOfElements } from './geometry.js';
|
|
2
2
|
import { pathsOfElement } from './paths.js';
|
|
3
3
|
import { DEFAULT_FONT_STACKS, LINE_HEIGHT, approximateMeasure, fontOf, linesOf, writingFrameOf } from './text.js';
|
|
4
4
|
export const DEFAULT_SVG_MARGIN = 32;
|
|
@@ -44,7 +44,7 @@ export const DEFAULT_EDGE = {
|
|
|
44
44
|
// XML has no way to write a control character other than tab and the line breaks, not even escaped.
|
|
45
45
|
// oxlint-disable-next-line no-control-regex
|
|
46
46
|
const NOT_XML = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]/g;
|
|
47
|
-
function escapeXml(value) {
|
|
47
|
+
export function escapeXml(value) {
|
|
48
48
|
return value.replace(NOT_XML, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
49
49
|
}
|
|
50
50
|
function round(value) {
|
|
@@ -95,14 +95,15 @@ function elementSvg(element, options) {
|
|
|
95
95
|
* The whole drawing as one SVG, in the colors of the theme it is exported from. Fonts are named,
|
|
96
96
|
* never embedded: a file that opens outside the app falls back to a system face.
|
|
97
97
|
*/
|
|
98
|
-
export function toSvg(elements, options) {
|
|
98
|
+
export function toSvg(elements, options = {}) {
|
|
99
|
+
const paint = { ...options, palette: options.palette ?? DEFAULT_PALETTE };
|
|
99
100
|
const margin = options.margin ?? DEFAULT_SVG_MARGIN;
|
|
100
|
-
const bounds =
|
|
101
|
+
const bounds = drawnBoundsOfElements(elements) ?? { x: 0, y: 0, w: 1, h: 1 };
|
|
101
102
|
const x = round(bounds.x - margin);
|
|
102
103
|
const y = round(bounds.y - margin);
|
|
103
104
|
const w = round(bounds.w + margin * 2);
|
|
104
105
|
const h = round(bounds.h + margin * 2);
|
|
105
106
|
const paper = options.background ? `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${options.background}"/>` : '';
|
|
106
|
-
const body = elements.map((element) => elementSvg(element,
|
|
107
|
+
const body = elements.map((element) => elementSvg(element, paint)).join('');
|
|
107
108
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="${x} ${y} ${w} ${h}">${paper}${body}</svg>`;
|
|
108
109
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/drawing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Drawing schemas, geometry, deterministic paths, SVG export and reading order.",
|
|
5
5
|
"license": "FSL-1.1-MIT",
|
|
6
6
|
"author": "Bas Milius",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"dist",
|
|
20
20
|
"src",
|
|
21
21
|
"!src/**/*.test.ts",
|
|
22
|
+
"!src/**/__snapshots__",
|
|
22
23
|
"!src/fixtures"
|
|
23
24
|
],
|
|
24
25
|
"exports": {
|
package/src/geometry.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function rotatePoint(point: Point, around: Point, angle: number): Point {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/* A point in the element's own frame, so every hit test can pretend nothing is turned. */
|
|
43
|
-
|
|
43
|
+
function toLocal(element: DrawingElement, point: Point): Point {
|
|
44
44
|
return rotatePoint(point, centerOf(boundsOf(element)), -(element.angle ?? 0));
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -65,6 +65,31 @@ export function boundsOfElements(elements: readonly DrawingElement[]): Rect | nu
|
|
|
65
65
|
return unionOf(elements.map(boundsOf));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/* The box around an element as it is drawn: the stored box, turned by its angle. */
|
|
69
|
+
export function drawnBoundsOf(element: DrawingElement): Rect {
|
|
70
|
+
const rect = boundsOf(element);
|
|
71
|
+
const angle = element.angle ?? 0;
|
|
72
|
+
if (angle === 0) {
|
|
73
|
+
return rect;
|
|
74
|
+
}
|
|
75
|
+
const center = centerOf(rect);
|
|
76
|
+
const corners = [
|
|
77
|
+
{ x: rect.x, y: rect.y },
|
|
78
|
+
{ x: rect.x + rect.w, y: rect.y },
|
|
79
|
+
{ x: rect.x + rect.w, y: rect.y + rect.h },
|
|
80
|
+
{ x: rect.x, y: rect.y + rect.h }
|
|
81
|
+
].map((corner) => rotatePoint(corner, center, angle));
|
|
82
|
+
const xs = corners.map((corner) => corner.x);
|
|
83
|
+
const ys = corners.map((corner) => corner.y);
|
|
84
|
+
const left = Math.min(...xs);
|
|
85
|
+
const top = Math.min(...ys);
|
|
86
|
+
return { x: left, y: top, w: Math.max(...xs) - left, h: Math.max(...ys) - top };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function drawnBoundsOfElements(elements: readonly DrawingElement[]): Rect | null {
|
|
90
|
+
return unionOf(elements.map(drawnBoundsOf));
|
|
91
|
+
}
|
|
92
|
+
|
|
68
93
|
export function intersects(a: Rect, b: Rect): boolean {
|
|
69
94
|
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
70
95
|
}
|
|
@@ -78,7 +103,7 @@ export function rectFromPoints(from: Point, to: Point): Rect {
|
|
|
78
103
|
};
|
|
79
104
|
}
|
|
80
105
|
|
|
81
|
-
|
|
106
|
+
function distanceToSegment(point: Point, a: Point, b: Point): number {
|
|
82
107
|
const dx = b.x - a.x;
|
|
83
108
|
const dy = b.y - a.y;
|
|
84
109
|
const length = dx * dx + dy * dy;
|
|
@@ -189,7 +214,7 @@ export function elementAt(elements: readonly DrawingElement[], point: Point, tol
|
|
|
189
214
|
}
|
|
190
215
|
|
|
191
216
|
export function elementsIn(elements: readonly DrawingElement[], rect: Rect): DrawingElement[] {
|
|
192
|
-
return elements.filter((element) => !element.locked && intersects(
|
|
217
|
+
return elements.filter((element) => !element.locked && intersects(drawnBoundsOf(element), rect));
|
|
193
218
|
}
|
|
194
219
|
|
|
195
220
|
export function handlePoint(rect: Rect, handle: ResizeHandle): Point {
|
package/src/index.ts
CHANGED
|
@@ -7,36 +7,30 @@ export {
|
|
|
7
7
|
centerOf,
|
|
8
8
|
roundPoint,
|
|
9
9
|
rotatePoint,
|
|
10
|
-
toLocal,
|
|
11
10
|
unionOf,
|
|
12
11
|
boundsOfElements,
|
|
12
|
+
drawnBoundsOf,
|
|
13
|
+
drawnBoundsOfElements,
|
|
13
14
|
intersects,
|
|
14
15
|
rectFromPoints,
|
|
15
|
-
distanceToSegment,
|
|
16
16
|
absolutePoints,
|
|
17
|
-
hitsElement,
|
|
18
17
|
elementAt,
|
|
19
18
|
elementsIn,
|
|
20
19
|
handlePoint,
|
|
21
20
|
resizeRect,
|
|
22
|
-
scaleElement
|
|
23
|
-
arrowHead,
|
|
24
|
-
arrowHeadSize
|
|
21
|
+
scaleElement
|
|
25
22
|
} from './geometry.ts';
|
|
26
|
-
export { type ElementPath,
|
|
23
|
+
export { type ElementPath, pathsOfElement } from './paths.ts';
|
|
27
24
|
export { readingOrder } from './reading-order.ts';
|
|
28
|
-
export { type SvgOptions, DEFAULT_SVG_MARGIN, DEFAULT_PALETTE, DEFAULT_PAPER, DEFAULT_EDGE, toSvg } from './svg.ts';
|
|
25
|
+
export { type SvgOptions, DEFAULT_SVG_MARGIN, DEFAULT_PALETTE, DEFAULT_PAPER, DEFAULT_EDGE, escapeXml, toSvg } from './svg.ts';
|
|
29
26
|
export {
|
|
30
27
|
LINE_HEIGHT,
|
|
31
28
|
DEFAULT_FONT_STACKS,
|
|
32
29
|
type WrittenElement,
|
|
33
30
|
NOTE_PADDING,
|
|
34
|
-
NOTE_RADIUS,
|
|
35
31
|
writingFrameOf,
|
|
36
|
-
textLines,
|
|
37
32
|
type MeasureLine,
|
|
38
33
|
approximateMeasure,
|
|
39
|
-
wrapLines,
|
|
40
34
|
linesOf,
|
|
41
35
|
fontOf
|
|
42
36
|
} from './text.ts';
|
package/src/paths.ts
CHANGED
|
@@ -70,7 +70,7 @@ function roundedRectPath(w: number, h: number, radius: number): string {
|
|
|
70
70
|
return `M ${corner} 0 L ${w - corner} 0 Q ${w} 0 ${w} ${corner} L ${w} ${h - corner} Q ${w} ${h} ${w - corner} ${h} L ${corner} ${h} Q 0 ${h} 0 ${h - corner} L 0 ${corner} Q 0 0 ${corner} 0 Z`;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
function outlineToPath(outline: readonly (readonly number[])[]): string {
|
|
74
74
|
if (outline.length === 0) {
|
|
75
75
|
return '';
|
|
76
76
|
}
|
|
@@ -78,7 +78,7 @@ export function outlineToPath(outline: readonly (readonly number[])[]): string {
|
|
|
78
78
|
return `M ${first![0]} ${first![1]} ${rest.map((point) => `L ${point[0]} ${point[1]}`).join(' ')} Z`;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
function freehandOutline(element: DrawingElement & { kind: 'freehand' }): string {
|
|
82
82
|
const points = element.points.map(([x, y, pressure]) => [x, y, pressure ?? 0.5] as [number, number, number]);
|
|
83
83
|
const outline = getStroke(points, {
|
|
84
84
|
size: element.strokeWidth * 4 + 2,
|
package/src/svg.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { DrawingColor, DrawingElement, DrawingFont } from './protocol.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { drawnBoundsOfElements, type Rect } from './geometry.ts';
|
|
3
3
|
import { pathsOfElement, type ElementPath } from './paths.ts';
|
|
4
4
|
import { DEFAULT_FONT_STACKS, LINE_HEIGHT, approximateMeasure, fontOf, linesOf, writingFrameOf, type MeasureLine, type WrittenElement } from './text.ts';
|
|
5
5
|
|
|
6
6
|
export interface SvgOptions {
|
|
7
|
-
/* What every palette name is in the theme this export is made in. */
|
|
8
|
-
palette
|
|
7
|
+
/* What every palette name is in the theme this export is made in; without it the light theme's colors are used. */
|
|
8
|
+
palette?: Record<DrawingColor, string>;
|
|
9
9
|
/* The paper behind the drawing, or null for a transparent one. */
|
|
10
10
|
background?: string | null;
|
|
11
11
|
/* World units around the drawing, so nothing touches the edge. */
|
|
@@ -67,10 +67,12 @@ export const DEFAULT_EDGE: Record<DrawingColor, string> = {
|
|
|
67
67
|
// oxlint-disable-next-line no-control-regex
|
|
68
68
|
const NOT_XML = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]/g;
|
|
69
69
|
|
|
70
|
-
function escapeXml(value: string): string {
|
|
70
|
+
export function escapeXml(value: string): string {
|
|
71
71
|
return value.replace(NOT_XML, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
type PaintOptions = SvgOptions & Required<Pick<SvgOptions, 'palette'>>;
|
|
75
|
+
|
|
74
76
|
function round(value: number): number {
|
|
75
77
|
return Math.round(value * 100) / 100;
|
|
76
78
|
}
|
|
@@ -81,7 +83,7 @@ function transformOf(element: DrawingElement): string {
|
|
|
81
83
|
return `translate(${round(element.x)} ${round(element.y)})${turn}`;
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
function textSvg(element: WrittenElement, options:
|
|
86
|
+
function textSvg(element: WrittenElement, options: PaintOptions): string {
|
|
85
87
|
const family = options.fonts?.[fontOf(element.font)] ?? DEFAULT_FONT_STACKS[fontOf(element.font)];
|
|
86
88
|
const anchor = element.align === 'center' ? 'middle' : element.align === 'right' ? 'end' : 'start';
|
|
87
89
|
const frame = writingFrameOf(element);
|
|
@@ -93,7 +95,7 @@ function textSvg(element: WrittenElement, options: SvgOptions): string {
|
|
|
93
95
|
return `<text font-family="${escapeXml(family)}" font-size="${element.size}" fill="${options.palette[element.stroke]}" text-anchor="${anchor}">${lines}</text>`;
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
function pathSvg(path: ElementPath, element: DrawingElement, options:
|
|
98
|
+
function pathSvg(path: ElementPath, element: DrawingElement, options: PaintOptions): string {
|
|
97
99
|
const note = element.kind === 'note';
|
|
98
100
|
if (path.role === 'stroke') {
|
|
99
101
|
const dash = path.dash ? ` stroke-dasharray="${path.dash.join(' ')}"` : '';
|
|
@@ -109,7 +111,7 @@ function pathSvg(path: ElementPath, element: DrawingElement, options: SvgOptions
|
|
|
109
111
|
: `<path d="${path.d}" fill="${color}" stroke="none"/>`;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
function elementSvg(element: DrawingElement, options:
|
|
114
|
+
function elementSvg(element: DrawingElement, options: PaintOptions): string {
|
|
113
115
|
const body =
|
|
114
116
|
element.kind === 'text'
|
|
115
117
|
? textSvg(element, options)
|
|
@@ -125,14 +127,15 @@ function elementSvg(element: DrawingElement, options: SvgOptions): string {
|
|
|
125
127
|
* The whole drawing as one SVG, in the colors of the theme it is exported from. Fonts are named,
|
|
126
128
|
* never embedded: a file that opens outside the app falls back to a system face.
|
|
127
129
|
*/
|
|
128
|
-
export function toSvg(elements: readonly DrawingElement[], options: SvgOptions): string {
|
|
130
|
+
export function toSvg(elements: readonly DrawingElement[], options: SvgOptions = {}): string {
|
|
131
|
+
const paint: PaintOptions = { ...options, palette: options.palette ?? DEFAULT_PALETTE };
|
|
129
132
|
const margin = options.margin ?? DEFAULT_SVG_MARGIN;
|
|
130
|
-
const bounds: Rect =
|
|
133
|
+
const bounds: Rect = drawnBoundsOfElements(elements) ?? { x: 0, y: 0, w: 1, h: 1 };
|
|
131
134
|
const x = round(bounds.x - margin);
|
|
132
135
|
const y = round(bounds.y - margin);
|
|
133
136
|
const w = round(bounds.w + margin * 2);
|
|
134
137
|
const h = round(bounds.h + margin * 2);
|
|
135
138
|
const paper = options.background ? `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${options.background}"/>` : '';
|
|
136
|
-
const body = elements.map((element) => elementSvg(element,
|
|
139
|
+
const body = elements.map((element) => elementSvg(element, paint)).join('');
|
|
137
140
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="${x} ${y} ${w} ${h}">${paper}${body}</svg>`;
|
|
138
141
|
}
|