@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 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 t = 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 + t * dx), point.y - (a.y + t * dy));
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, j = polygon.length - 1; i < polygon.length; j = i++) {
86
- const a = polygon[i];
87
- const b = polygon[j];
88
- if (a.y > point.y !== b.y > point.y && point.x < ((b.x - a.x) * (point.y - a.y)) / (b.y - a.y) + a.x) {
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 r = Math.min(radius, w / 2, h / 2);
47
- return `M ${r} 0 L ${w - r} 0 Q ${w} 0 ${w} ${r} L ${w} ${h - r} Q ${w} ${h} ${w - r} ${h} L ${r} ${h} Q 0 ${h} 0 ${h - r} L 0 ${r} Q 0 0 ${r} 0 Z`;
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 d = roundedRectPath(element.w, element.h, NOTE_RADIUS);
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 ? 'fill' : path.stroke === FILL ? 'fill' : 'stroke',
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
  }));
@@ -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. A diagram becomes a list without anyone looking at it.
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 elementSvg(element, options) {
69
+ function pathSvg(path, element, options) {
70
70
  const note = element.kind === 'note';
71
- const paper = options.paper ?? DEFAULT_PAPER;
72
- const edge = options.edge ?? DEFAULT_EDGE;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/drawing",
3
- "version": "0.21.0",
3
+ "version": "0.21.2",
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",
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 t = 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 + t * dx), point.y - (a.y + t * dy));
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, j = polygon.length - 1; i < polygon.length; j = i++) {
115
- const a = polygon[i]!;
116
- const b = polygon[j]!;
117
- if (a.y > point.y !== b.y > point.y && point.x < ((b.x - a.x) * (point.y - a.y)) / (b.y - a.y) + a.x) {
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 r = Math.min(radius, w / 2, h / 2);
70
- return `M ${r} 0 L ${w - r} 0 Q ${w} 0 ${w} ${r} L ${w} ${h - r} Q ${w} ${h} ${w - r} ${h} L ${r} ${h} Q 0 ${h} 0 ${h - r} L 0 ${r} Q 0 0 ${r} 0 Z`;
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 d = roundedRectPath(element.w, element.h, NOTE_RADIUS);
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 ? 'fill' : path.stroke === FILL ? 'fill' : 'stroke',
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
  }));
@@ -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. A diagram becomes a list without anyone looking at it.
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 elementSvg(element: DrawingElement, options: SvgOptions): string {
96
+ function pathSvg(path: ElementPath, element: DrawingElement, options: SvgOptions): string {
97
97
  const note = element.kind === 'note';
98
- const paper = options.paper ?? DEFAULT_PAPER;
99
- const edge = options.edge ?? DEFAULT_EDGE;
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;