@adecore/drawing 0.21.0 → 0.21.2
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.js +6 -6
- package/dist/paths.js +6 -6
- package/dist/reading-order.js +1 -1
- package/dist/svg.js +17 -18
- package/dist/text.js +1 -4
- package/package.json +1 -1
- package/src/geometry.ts +6 -6
- package/src/paths.ts +6 -6
- package/src/reading-order.ts +1 -1
- package/src/svg.ts +19 -19
- package/src/text.ts +1 -4
package/dist/geometry.js
CHANGED
|
@@ -60,8 +60,8 @@ export function distanceToSegment(point, a, b) {
|
|
|
60
60
|
if (length === 0) {
|
|
61
61
|
return Math.hypot(point.x - a.x, point.y - a.y);
|
|
62
62
|
}
|
|
63
|
-
const
|
|
64
|
-
return Math.hypot(point.x - (a.x +
|
|
63
|
+
const along = Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.y - a.y) * dy) / length));
|
|
64
|
+
return Math.hypot(point.x - (a.x + along * dx), point.y - (a.y + along * dy));
|
|
65
65
|
}
|
|
66
66
|
export function absolutePoints(element) {
|
|
67
67
|
if (element.kind !== 'line' && element.kind !== 'freehand') {
|
|
@@ -82,10 +82,10 @@ function cornersOfDiamond(rect) {
|
|
|
82
82
|
}
|
|
83
83
|
function insidePolygon(point, polygon) {
|
|
84
84
|
let inside = false;
|
|
85
|
-
for (let i = 0,
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
85
|
+
for (let i = 0, previous = polygon.length - 1; i < polygon.length; previous = i++) {
|
|
86
|
+
const corner = polygon[i];
|
|
87
|
+
const before = polygon[previous];
|
|
88
|
+
if (corner.y > point.y !== before.y > point.y && point.x < ((before.x - corner.x) * (point.y - corner.y)) / (before.y - corner.y) + corner.x) {
|
|
89
89
|
inside = !inside;
|
|
90
90
|
}
|
|
91
91
|
}
|
package/dist/paths.js
CHANGED
|
@@ -43,8 +43,8 @@ function optionsOf(element) {
|
|
|
43
43
|
}
|
|
44
44
|
// Rough has no rounded-rectangle primitive.
|
|
45
45
|
function roundedRectPath(w, h, radius) {
|
|
46
|
-
const
|
|
47
|
-
return `M ${
|
|
46
|
+
const corner = Math.min(radius, w / 2, h / 2);
|
|
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
|
export function outlineToPath(outline) {
|
|
50
50
|
if (outline.length === 0) {
|
|
@@ -78,10 +78,10 @@ export function pathsOfElement(element) {
|
|
|
78
78
|
}
|
|
79
79
|
// A note is a sheet of paper, not a drawn shape: no wobble, and an edge of its own paper color.
|
|
80
80
|
if (element.kind === 'note') {
|
|
81
|
-
const
|
|
81
|
+
const sheet = roundedRectPath(element.w, element.h, NOTE_RADIUS);
|
|
82
82
|
return [
|
|
83
|
-
{ d, role: 'fill', strokeWidth: 0, dash: null },
|
|
84
|
-
{ d, role: 'stroke', strokeWidth: element.strokeWidth, dash: null }
|
|
83
|
+
{ d: sheet, role: 'fill', strokeWidth: 0, dash: null },
|
|
84
|
+
{ d: sheet, role: 'stroke', strokeWidth: element.strokeWidth, dash: null }
|
|
85
85
|
];
|
|
86
86
|
}
|
|
87
87
|
const options = optionsOf(element);
|
|
@@ -111,7 +111,7 @@ export function pathsOfElement(element) {
|
|
|
111
111
|
})();
|
|
112
112
|
const paths = generator.toPaths(drawable).map((path) => ({
|
|
113
113
|
d: path.d,
|
|
114
|
-
role: path.fill === FILL
|
|
114
|
+
role: path.fill === FILL || path.stroke === FILL ? 'fill' : 'stroke',
|
|
115
115
|
strokeWidth: path.strokeWidth,
|
|
116
116
|
dash: path.stroke === STROKE ? dash : null
|
|
117
117
|
}));
|
package/dist/reading-order.js
CHANGED
|
@@ -41,7 +41,7 @@ function labelAt(point, elements, arrowId) {
|
|
|
41
41
|
}
|
|
42
42
|
/*
|
|
43
43
|
* A drawing as lines an agent can read: the texts top to bottom and left to right, then every
|
|
44
|
-
* arrow as the two things it connects.
|
|
44
|
+
* arrow as the two things it connects.
|
|
45
45
|
*/
|
|
46
46
|
export function readingOrder(elements) {
|
|
47
47
|
// A row is measured from its top text, so a staircase of texts cannot chain into one long line.
|
package/dist/svg.js
CHANGED
|
@@ -66,30 +66,29 @@ function textSvg(element, options) {
|
|
|
66
66
|
.join('');
|
|
67
67
|
return `<text font-family="${escapeXml(family)}" font-size="${element.size}" fill="${options.palette[element.stroke]}" text-anchor="${anchor}">${lines}</text>`;
|
|
68
68
|
}
|
|
69
|
-
function
|
|
69
|
+
function pathSvg(path, element, options) {
|
|
70
70
|
const note = element.kind === 'note';
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
if (path.role === 'stroke') {
|
|
72
|
+
const dash = path.dash ? ` stroke-dasharray="${path.dash.join(' ')}"` : '';
|
|
73
|
+
const line = note ? (options.edge ?? DEFAULT_EDGE)[element.fillColor ?? element.stroke] : options.palette[element.stroke];
|
|
74
|
+
return `<path d="${path.d}" fill="none" stroke="${line}" stroke-width="${path.strokeWidth}" stroke-linecap="round"${dash}/>`;
|
|
75
|
+
}
|
|
76
|
+
// A note is filled with its paper, which is a palette of pale sheets of its own.
|
|
77
|
+
const fill = note ? (options.paper ?? DEFAULT_PAPER) : options.palette;
|
|
78
|
+
const color = path.role === 'ink' ? options.palette[element.stroke] : fill[element.fillColor ?? element.stroke];
|
|
79
|
+
// A hachure fill is a bundle of lines, so it arrives as a stroke with its own width.
|
|
80
|
+
return path.strokeWidth > 0 && path.role === 'fill' && element.fill === 'hachure'
|
|
81
|
+
? `<path d="${path.d}" fill="none" stroke="${color}" stroke-width="${path.strokeWidth}"/>`
|
|
82
|
+
: `<path d="${path.d}" fill="${color}" stroke="none"/>`;
|
|
83
|
+
}
|
|
84
|
+
function elementSvg(element, options) {
|
|
73
85
|
const body = element.kind === 'text'
|
|
74
86
|
? textSvg(element, options)
|
|
75
87
|
: pathsOfElement(element)
|
|
76
|
-
.map((path) =>
|
|
77
|
-
const dash = path.dash ? ` stroke-dasharray="${path.dash.join(' ')}"` : '';
|
|
78
|
-
if (path.role === 'stroke') {
|
|
79
|
-
const line = note ? edge[element.fillColor ?? element.stroke] : options.palette[element.stroke];
|
|
80
|
-
return `<path d="${path.d}" fill="none" stroke="${line}" stroke-width="${path.strokeWidth}" stroke-linecap="round"${dash}/>`;
|
|
81
|
-
}
|
|
82
|
-
// A note is filled with its paper, which is a palette of pale sheets of its own.
|
|
83
|
-
const fill = note ? paper : options.palette;
|
|
84
|
-
const color = path.role === 'ink' ? options.palette[element.stroke] : fill[element.fillColor ?? element.stroke];
|
|
85
|
-
// A hachure fill is a bundle of lines, so it arrives as a stroke with its own width.
|
|
86
|
-
return path.strokeWidth > 0 && path.role === 'fill' && element.fill === 'hachure'
|
|
87
|
-
? `<path d="${path.d}" fill="none" stroke="${color}" stroke-width="${path.strokeWidth}"/>`
|
|
88
|
-
: `<path d="${path.d}" fill="${color}" stroke="none"/>`;
|
|
89
|
-
})
|
|
88
|
+
.map((path) => pathSvg(path, element, options))
|
|
90
89
|
.join('');
|
|
91
90
|
// The paper is drawn first and the note's own words go on top of it.
|
|
92
|
-
const written = note ? textSvg(element, options) : '';
|
|
91
|
+
const written = element.kind === 'note' ? textSvg(element, options) : '';
|
|
93
92
|
return `<g transform="${transformOf(element)}">${body}${written}</g>`;
|
|
94
93
|
}
|
|
95
94
|
/*
|
package/dist/text.js
CHANGED
|
@@ -20,10 +20,7 @@ export function writingFrameOf(element) {
|
|
|
20
20
|
export function textLines(text) {
|
|
21
21
|
return text.split('\n');
|
|
22
22
|
}
|
|
23
|
-
/*
|
|
24
|
-
* A measure for where no font exists, such as the daemon writing an SVG for an agent: a glyph is
|
|
25
|
-
* a little over half its size wide, which is what most faces come to on average.
|
|
26
|
-
*/
|
|
23
|
+
/* A measure for where no font exists, such as on a server: most faces average a glyph a little over half its size wide. */
|
|
27
24
|
export function approximateMeasure(size, font) {
|
|
28
25
|
const glyph = fontOf(font) === 'mono' ? 0.6 : 0.55;
|
|
29
26
|
return (line) => line.length * size * glyph;
|
package/package.json
CHANGED
package/src/geometry.ts
CHANGED
|
@@ -85,8 +85,8 @@ export function distanceToSegment(point: Point, a: Point, b: Point): number {
|
|
|
85
85
|
if (length === 0) {
|
|
86
86
|
return Math.hypot(point.x - a.x, point.y - a.y);
|
|
87
87
|
}
|
|
88
|
-
const
|
|
89
|
-
return Math.hypot(point.x - (a.x +
|
|
88
|
+
const along = Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.y - a.y) * dy) / length));
|
|
89
|
+
return Math.hypot(point.x - (a.x + along * dx), point.y - (a.y + along * dy));
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
export function absolutePoints(element: DrawingElement): Point[] {
|
|
@@ -111,10 +111,10 @@ function cornersOfDiamond(rect: Rect): Point[] {
|
|
|
111
111
|
|
|
112
112
|
function insidePolygon(point: Point, polygon: readonly Point[]): boolean {
|
|
113
113
|
let inside = false;
|
|
114
|
-
for (let i = 0,
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
114
|
+
for (let i = 0, previous = polygon.length - 1; i < polygon.length; previous = i++) {
|
|
115
|
+
const corner = polygon[i]!;
|
|
116
|
+
const before = polygon[previous]!;
|
|
117
|
+
if (corner.y > point.y !== before.y > point.y && point.x < ((before.x - corner.x) * (point.y - corner.y)) / (before.y - corner.y) + corner.x) {
|
|
118
118
|
inside = !inside;
|
|
119
119
|
}
|
|
120
120
|
}
|
package/src/paths.ts
CHANGED
|
@@ -66,8 +66,8 @@ function optionsOf(element: DrawingElement): Options {
|
|
|
66
66
|
|
|
67
67
|
// Rough has no rounded-rectangle primitive.
|
|
68
68
|
function roundedRectPath(w: number, h: number, radius: number): string {
|
|
69
|
-
const
|
|
70
|
-
return `M ${
|
|
69
|
+
const corner = Math.min(radius, w / 2, h / 2);
|
|
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
|
export function outlineToPath(outline: readonly (readonly number[])[]): string {
|
|
@@ -105,10 +105,10 @@ export function pathsOfElement(element: DrawingElement): ElementPath[] {
|
|
|
105
105
|
}
|
|
106
106
|
// A note is a sheet of paper, not a drawn shape: no wobble, and an edge of its own paper color.
|
|
107
107
|
if (element.kind === 'note') {
|
|
108
|
-
const
|
|
108
|
+
const sheet = roundedRectPath(element.w, element.h, NOTE_RADIUS);
|
|
109
109
|
return [
|
|
110
|
-
{ d, role: 'fill', strokeWidth: 0, dash: null },
|
|
111
|
-
{ d, role: 'stroke', strokeWidth: element.strokeWidth, dash: null }
|
|
110
|
+
{ d: sheet, role: 'fill', strokeWidth: 0, dash: null },
|
|
111
|
+
{ d: sheet, role: 'stroke', strokeWidth: element.strokeWidth, dash: null }
|
|
112
112
|
];
|
|
113
113
|
}
|
|
114
114
|
const options = optionsOf(element);
|
|
@@ -144,7 +144,7 @@ export function pathsOfElement(element: DrawingElement): ElementPath[] {
|
|
|
144
144
|
})();
|
|
145
145
|
const paths: ElementPath[] = generator.toPaths(drawable).map((path) => ({
|
|
146
146
|
d: path.d,
|
|
147
|
-
role: path.fill === FILL
|
|
147
|
+
role: path.fill === FILL || path.stroke === FILL ? 'fill' : 'stroke',
|
|
148
148
|
strokeWidth: path.strokeWidth,
|
|
149
149
|
dash: path.stroke === STROKE ? dash : null
|
|
150
150
|
}));
|
package/src/reading-order.ts
CHANGED
|
@@ -48,7 +48,7 @@ function labelAt(point: Point, elements: readonly DrawingElement[], arrowId: str
|
|
|
48
48
|
|
|
49
49
|
/*
|
|
50
50
|
* A drawing as lines an agent can read: the texts top to bottom and left to right, then every
|
|
51
|
-
* arrow as the two things it connects.
|
|
51
|
+
* arrow as the two things it connects.
|
|
52
52
|
*/
|
|
53
53
|
export function readingOrder(elements: readonly DrawingElement[]): string[] {
|
|
54
54
|
// A row is measured from its top text, so a staircase of texts cannot chain into one long line.
|
package/src/svg.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DrawingColor, DrawingElement, DrawingFont } from './protocol.ts';
|
|
2
2
|
import { boundsOfElements, type Rect } from './geometry.ts';
|
|
3
|
-
import { pathsOfElement } from './paths.ts';
|
|
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 {
|
|
@@ -93,31 +93,31 @@ function textSvg(element: WrittenElement, options: SvgOptions): string {
|
|
|
93
93
|
return `<text font-family="${escapeXml(family)}" font-size="${element.size}" fill="${options.palette[element.stroke]}" text-anchor="${anchor}">${lines}</text>`;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
function
|
|
96
|
+
function pathSvg(path: ElementPath, element: DrawingElement, options: SvgOptions): string {
|
|
97
97
|
const note = element.kind === 'note';
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
if (path.role === 'stroke') {
|
|
99
|
+
const dash = path.dash ? ` stroke-dasharray="${path.dash.join(' ')}"` : '';
|
|
100
|
+
const line = note ? (options.edge ?? DEFAULT_EDGE)[element.fillColor ?? element.stroke] : options.palette[element.stroke];
|
|
101
|
+
return `<path d="${path.d}" fill="none" stroke="${line}" stroke-width="${path.strokeWidth}" stroke-linecap="round"${dash}/>`;
|
|
102
|
+
}
|
|
103
|
+
// A note is filled with its paper, which is a palette of pale sheets of its own.
|
|
104
|
+
const fill = note ? (options.paper ?? DEFAULT_PAPER) : options.palette;
|
|
105
|
+
const color = path.role === 'ink' ? options.palette[element.stroke] : fill[element.fillColor ?? element.stroke];
|
|
106
|
+
// A hachure fill is a bundle of lines, so it arrives as a stroke with its own width.
|
|
107
|
+
return path.strokeWidth > 0 && path.role === 'fill' && element.fill === 'hachure'
|
|
108
|
+
? `<path d="${path.d}" fill="none" stroke="${color}" stroke-width="${path.strokeWidth}"/>`
|
|
109
|
+
: `<path d="${path.d}" fill="${color}" stroke="none"/>`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function elementSvg(element: DrawingElement, options: SvgOptions): string {
|
|
100
113
|
const body =
|
|
101
114
|
element.kind === 'text'
|
|
102
115
|
? textSvg(element, options)
|
|
103
116
|
: pathsOfElement(element)
|
|
104
|
-
.map((path) =>
|
|
105
|
-
const dash = path.dash ? ` stroke-dasharray="${path.dash.join(' ')}"` : '';
|
|
106
|
-
if (path.role === 'stroke') {
|
|
107
|
-
const line = note ? edge[element.fillColor ?? element.stroke] : options.palette[element.stroke];
|
|
108
|
-
return `<path d="${path.d}" fill="none" stroke="${line}" stroke-width="${path.strokeWidth}" stroke-linecap="round"${dash}/>`;
|
|
109
|
-
}
|
|
110
|
-
// A note is filled with its paper, which is a palette of pale sheets of its own.
|
|
111
|
-
const fill = note ? paper : options.palette;
|
|
112
|
-
const color = path.role === 'ink' ? options.palette[element.stroke] : fill[element.fillColor ?? element.stroke];
|
|
113
|
-
// A hachure fill is a bundle of lines, so it arrives as a stroke with its own width.
|
|
114
|
-
return path.strokeWidth > 0 && path.role === 'fill' && element.fill === 'hachure'
|
|
115
|
-
? `<path d="${path.d}" fill="none" stroke="${color}" stroke-width="${path.strokeWidth}"/>`
|
|
116
|
-
: `<path d="${path.d}" fill="${color}" stroke="none"/>`;
|
|
117
|
-
})
|
|
117
|
+
.map((path) => pathSvg(path, element, options))
|
|
118
118
|
.join('');
|
|
119
119
|
// The paper is drawn first and the note's own words go on top of it.
|
|
120
|
-
const written = note ? textSvg(element, options) : '';
|
|
120
|
+
const written = element.kind === 'note' ? textSvg(element, options) : '';
|
|
121
121
|
return `<g transform="${transformOf(element)}">${body}${written}</g>`;
|
|
122
122
|
}
|
|
123
123
|
|
package/src/text.ts
CHANGED
|
@@ -32,10 +32,7 @@ export function textLines(text: string): string[] {
|
|
|
32
32
|
|
|
33
33
|
export type MeasureLine = (line: string) => number;
|
|
34
34
|
|
|
35
|
-
/*
|
|
36
|
-
* A measure for where no font exists, such as the daemon writing an SVG for an agent: a glyph is
|
|
37
|
-
* a little over half its size wide, which is what most faces come to on average.
|
|
38
|
-
*/
|
|
35
|
+
/* A measure for where no font exists, such as on a server: most faces average a glyph a little over half its size wide. */
|
|
39
36
|
export function approximateMeasure(size: number, font: DrawingFont | undefined): MeasureLine {
|
|
40
37
|
const glyph = fontOf(font) === 'mono' ? 0.6 : 0.55;
|
|
41
38
|
return (line) => line.length * size * glyph;
|