@crewlethq/icons 0.15.0 → 0.15.1

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/README.md CHANGED
@@ -166,11 +166,15 @@ import { CrewletCharacter, CREWLET_CHARACTERS, isCrewletCharacter } from '@crewl
166
166
  | Prop | Type | Default | Purpose |
167
167
  | ---- | ---- | ------- | ------- |
168
168
  | `character` | `CrewletCharacterId`, one of `CREWLET_CHARACTERS` | required | Which character to draw. `crewlet`, the first, is the mark itself. |
169
- | `detail` | `'full' \| 'compact'` | `'full'` | `full` draws the keyline floating off the body and the gap round the visor; `compact` leaves both out and opens the eyes, for a character too small for a hairline gap to read. |
169
+ | `detail` | `'full' \| 'compact'` | `'full'` | `full` draws the keyline floating off the body and the gap round the visor; `compact` leaves both out, with every other cut as fine as them (the page's fold), and opens the eyes, for a character too small for a hairline gap to read. |
170
170
 
171
- Every character is cut from the mark's own rules: every corner chamfered at 45 degrees, a keyline at the mark's own proportions, a visor with two upright slits measured from the visor itself, a V notch on a flat crown and block legs under a flat base. A character is framed square and centred on what it draws, so it sits in the middle of any box it is given. Its face and holes are cut out of the body with a mask rather than painted over it, so it is transparent where it is cut and stands on any ground; every instance takes its own mask ids.
171
+ Every character is cut from the mark's own rules: every corner chamfered at 45 degrees, a keyline at the mark's own proportions, a visor with two upright slits measured from the visor itself, a V notch on a flat crown and block legs under a flat base.
172
172
 
173
- An id is what a configuration stores, so the set is a contract: `CREWLET_CHARACTERS` lists the ids in the order a picker offers them, `CREWLET_CHARACTER_GEOMETRY` gives each one's name and a few words on its shape, and `isCrewletCharacter` tells an id this build draws from anything else read off the wire. The geometry lives in `src/crewletCharacters.ts`; the original Crewlet is drawn from `svg/crewlet-icon.svg` itself, which `test/characters.test.mjs` holds. `@crewlethq/ui`'s `Avatar` draws a character on a plate in an agent's hue.
173
+ A character is framed square on its optical centre: the point halfway between the middle of its bounding box and the centre of area of its silhouette, in the smallest square round that point that holds everything it draws, the keyline's mitred corners included. A symmetric silhouette is centred geometrically; a heart over thin legs, a speech bubble over its tail or a triangle over its apex moves half the way toward its mass, so it sits in the middle of any box it is given.
174
+
175
+ Its face and holes are cut into its own paths, wound against the outline round them, and never painted or masked. A cut shows whatever the character stands on (a badge's plate, a chart's card, a hovered row) in every colour and theme, and nothing a host stylesheet does to the paths in its zone can fill one. It carries no mask and no id, so any number of copies, rendered together or apart, can share a page.
176
+
177
+ An id is what a configuration stores, so the set is a contract: `CREWLET_CHARACTERS` lists the ids in the order a picker offers them, `CREWLET_CHARACTER_GEOMETRY` gives each one's name and a few words on its shape, and `isCrewletCharacter` tells an id this build draws from anything else read off the wire. The geometry lives in `src/crewletCharacters.ts`, the union and the cuts in `src/region.ts` and the frame and the face in `src/characterDrawing.ts`; the original Crewlet is drawn from `svg/crewlet-icon.svg` itself, which `test/characters.test.mjs` holds. `@crewlethq/ui`'s `Avatar` draws a character on a plate in an agent's hue.
174
178
 
175
179
  ## Stylesheets
176
180
 
@@ -12,7 +12,7 @@ import { SVGProps } from 'react';
12
12
  * in it. `cut` is that rule as a function.
13
13
  * - THE KEYLINE. An outline floats off the body with a transparent gap, as the
14
14
  * mark is drawn: a line of KEYLINE_LINE at KEYLINE_GAP, the mark's own
15
- * proportions. It is drawn at full detail only (see CrewletCharacter).
15
+ * proportions. It is drawn at full detail only (see characterDrawing.ts).
16
16
  * - THE VISOR. One band with two upright slits, each slit's size and place
17
17
  * measured from the band itself (EYE), so every face matches the mark's at
18
18
  * whatever body it sits on.
@@ -22,14 +22,19 @@ import { SVGProps } from 'react';
22
22
  * THIS FILE IS THE GEOMETRY'S ONE SOURCE. Nothing regenerates it, so an edit
23
23
  * here is the edit. Every outline is a polygon on a stage 100 units wide whose
24
24
  * ground is at GROUND, written clockwise with the chamfer each corner takes;
25
- * the paths are computed once, when the module loads. The original Crewlet
26
- * is the exception: it IS the mark, so it is drawn from the mark's own paths
27
- * (generated from svg/crewlet-icon.svg at build) placed on the same stage.
25
+ * the polygons are computed when the module loads, and everything measured
26
+ * off them (the union, the keyline, the box) the first time it is asked for.
27
+ * The original Crewlet is the exception: it IS the mark, so it is drawn from
28
+ * the mark's own paths (generated from svg/crewlet-icon.svg at build) placed
29
+ * on the same stage.
28
30
  *
29
- * Every outline of one character is drawn as ONE union: the parts overlap and
30
- * share a winding, so a leg under a body fills as part of it, and the keyline
31
- * goes round the whole figure rather than round each part.
31
+ * Every outline of one character is drawn as ONE union (see region.ts): the
32
+ * parts overlap, so a leg under a body fills as part of it, and the keyline
33
+ * goes round the whole figure rather than round each part. The face and the
34
+ * holes are cut out of that union as geometry, never painted over it (see
35
+ * characterDrawing.ts).
32
36
  */
37
+
33
38
  /** The characters, in the order a picker offers them. The first is the mark. */
34
39
  declare const CREWLET_CHARACTERS: readonly ["crewlet", "hexlet", "peaklet", "prismlet", "towerlet", "bricklet", "gemlet", "coglet", "pluslet", "shieldlet", "stacklet", "sparklet", "cloudlet", "foxlet", "rocketlet", "crownlet", "cactlet", "wisplet", "heartlet", "flasklet", "chiplet", "duolet", "chatlet", "pagelet", "archlet", "dashlet", "octlet", "conelet", "hivelet", "folderlet"];
35
40
  /** One character's id, which is what a configuration stores. */
@@ -48,7 +53,7 @@ interface Described {
48
53
  readonly name: string;
49
54
  /** What its outline is, in a few words, for a description: `Hexagon with pointed flanks`. */
50
55
  readonly shape: string;
51
- /** Everything it draws, keyline included, which is what a frame centres on. */
56
+ /** Everything it draws, keyline included, which is what its frame has to hold. */
52
57
  readonly bounds: CharacterBox;
53
58
  }
54
59
  /** A character drawn from polygons on the stage. */
@@ -80,10 +85,10 @@ declare const CREWLET_CHARACTER_GEOMETRY: Readonly<Record<CrewletCharacterId, Cr
80
85
  * How much of a character is drawn.
81
86
  *
82
87
  * `full` is the mark's own drawing: the keyline floating off the body, and the
83
- * gap round the visor. `compact` leaves both out and opens the eyes a little,
84
- * for a character drawn too small for a hairline gap to read (an avatar under
85
- * 56px): at that size the keyline turns into a blur round the body rather
86
- * than a line.
88
+ * gap round the visor. `compact` leaves both out, with every other cut as fine
89
+ * as them, and opens the eyes a little, for a character drawn too small for a
90
+ * hairline gap to read (an avatar under 56px): at that size the keyline turns
91
+ * into a blur round the body rather than a line.
87
92
  */
88
93
  type CrewletCharacterDetail = 'full' | 'compact';
89
94
  interface CrewletCharacterProps extends Omit<SVGProps<SVGSVGElement>, 'children' | 'viewBox'> {
@@ -95,6 +100,12 @@ interface CrewletCharacterProps extends Omit<SVGProps<SVGSVGElement>, 'children'
95
100
  /**
96
101
  * One Crewlet character, in the current text colour, centred in a square.
97
102
  *
103
+ * It is framed on its optical centre and its face and holes are cut through
104
+ * it (see `characterDrawing.ts`), so it sits in the middle of whatever box a
105
+ * consumer puts it in and shows that box's ground through every cut. It
106
+ * carries no mask and no id, so nothing a host stylesheet paints reaches a
107
+ * hole and any number of copies can share a page.
108
+ *
98
109
  * It is a picture and says nothing on its own: it is hidden from assistive
99
110
  * technology, and whatever draws it beside a name (an avatar, a picker's
100
111
  * option) carries that name. Colour it with CSS `color` on it or any ancestor.
@@ -1 +1 @@
1
- export { CREWLET_CHARACTERS, CREWLET_CHARACTER_GEOMETRY, CrewletCharacter_default as CrewletCharacter, isCrewletCharacter } from './chunk-7RRFA6BN.js';
1
+ export { CREWLET_CHARACTERS, CREWLET_CHARACTER_GEOMETRY, CrewletCharacter_default as CrewletCharacter, isCrewletCharacter } from './chunk-X7FKGFKM.js';
@@ -1,14 +1,264 @@
1
- import { useId } from 'react';
2
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
-
4
- // src/CrewletCharacter.tsx
1
+ import { jsx } from 'react/jsx-runtime';
5
2
 
6
3
  // src/generated/crewletMark.ts
7
4
  var CREWLET_MARK = {
8
5
  paths: ["M543.42 868.92 l-34.44 -0.24 0 -58.77 0 -58.77 -28.92 0 -28.80 0 -40.20 -39.61 c-119.78 -118.25 -181.49 -179.97 -204.30 -204.30 -15.87 -16.93 -82.75 -96.74 -92.51 -110.38 l-3.76 -5.17 26.80 -18.46 c14.69 -10.11 34.09 -23.51 43.14 -29.86 55.25 -38.32 70.29 -48.43 71.12 -47.49 0.47 0.47 11.64 13.87 24.69 29.74 13.17 15.87 27.98 33.62 32.80 39.50 4.94 5.88 21.63 26.10 37.15 44.90 15.52 18.81 28.56 34.21 29.15 34.21 0.47 0.12 0.82 -30.44 0.82 -67.83 l0 -68.06 37.26 -34.56 c43.38 -40.20 61.83 -57.13 75.11 -69 l9.52 -8.46 261.78 0 261.90 0 47.14 45.73 c25.98 25.16 52.43 51.02 58.66 57.36 l11.52 11.52 0.24 67.94 0.35 67.83 31.74 -40.79 c17.51 -22.33 34.91 -45.02 38.91 -50.19 15.16 -20.34 44.32 -57.72 45.14 -58.07 0.71 -0.24 104.97 75.82 126.83 92.51 2.23 1.65 4.58 3.41 5.17 3.76 1.65 1.18 -7.99 14.22 -46.08 62.07 -35.38 44.32 -49.84 61.13 -92.39 106.97 -19.28 20.81 -38.91 41.73 -148.70 157.40 l-34.68 36.56 -26.45 0.12 c-19.04 0 -26.92 0.35 -27.98 1.41 -1.06 1.06 -1.41 15.05 -1.41 55.72 l0 54.31 -34.91 0.47 c-19.28 0.24 -53.72 0.24 -76.41 0 l-41.49 -0.47 0 -55.72 0 -55.72 -99.33 0 -99.33 0 -0.24 56.19 -0.35 56.07 -39.97 0 c-21.98 0 -55.48 -0.12 -74.29 -0.35z m94.74 -13.87 l7.17 -0.82 0 -54.89 c0 -49.02 0.24 -55.13 1.88 -55.72 1.06 -0.35 51.84 -0.71 112.85 -0.71 l110.97 0 0 55.84 0 55.84 62.89 0 62.89 0 0 -55.84 0 -55.84 31.50 0 31.50 0 14.69 -14.93 c30.80 -31.50 113.20 -118.49 144.47 -152.58 48.66 -53.01 81.34 -90.28 104.85 -119.43 22.80 -28.45 44.43 -56.42 44.43 -57.48 0 -0.59 -2 -2.47 -4.35 -4.23 -2.47 -1.76 -7.05 -5.17 -10.34 -7.64 -3.17 -2.35 -16.93 -12.34 -30.56 -22.22 -13.52 -9.87 -33.50 -24.57 -44.43 -32.68 -16.34 -12.11 -19.98 -14.34 -21.39 -13.17 -0.94 0.82 -5.88 6.82 -10.81 13.40 -4.94 6.47 -13.99 18.46 -20.10 26.57 -6.23 8.11 -26.57 34.44 -45.37 58.54 -18.81 24.10 -36.56 47.02 -39.50 50.78 -5.29 7.05 -5.29 7.05 -11.40 7.05 l-6.23 0 0 -79.58 0 -79.58 -37.85 -37.03 c-20.92 -20.45 -45.14 -44.08 -53.95 -52.66 l-15.99 -15.63 -256.84 0 -256.73 0 -47.49 44.20 c-26.21 24.33 -51.13 47.49 -55.60 51.37 l-7.88 7.05 0 79.11 0 79.23 -6.35 0 -6.35 0 -48.66 -58.54 c-26.80 -32.09 -55.95 -67.24 -64.89 -78.17 -8.93 -10.81 -16.69 -19.63 -17.16 -19.63 -0.94 0 -8.58 5.17 -34.32 22.92 -34.21 23.74 -46.08 31.86 -62.65 43.26 -10.34 7.05 -19.28 13.28 -19.87 13.87 -0.59 0.47 2.35 5.05 6.47 10.11 13.87 16.93 53.13 63.12 66.77 78.40 21.86 24.57 83.81 87.69 145.41 148.23 31.62 31.03 68.65 67.36 82.28 80.76 l24.69 24.45 33.50 0.24 33.38 0.35 0.35 59.13 0.24 59.01 53.37 0 c29.27 0 56.54 -0.35 60.54 -0.71z", "M535.78 783.81 l-0.35 -59.13 -36.67 0 -36.67 0 -6.82 -6.47 c-48.78 -46.43 -176.91 -173.03 -210.41 -207.82 -24.69 -25.74 -75.70 -81.70 -93.69 -102.85 -3.29 -3.88 -5.29 -7.17 -4.70 -7.76 1.29 -1.06 58.30 -40.32 82.05 -56.42 l16.34 -11.05 3.64 4.47 c11.05 13.87 76.99 92.51 98.98 118.14 l25.74 29.97 15.63 0.35 15.52 0.35 0 -82.87 0 -82.75 50.55 -46.67 c27.86 -25.74 50.55 -47.14 50.55 -47.72 0 -0.59 46.78 -1.06 111.55 -1.06 l111.55 0 15.52 14.81 c15.28 14.58 15.52 14.69 17.98 12.58 1.29 -1.18 8.46 -7.76 15.75 -14.58 l13.40 -12.46 17.75 -0.71 c9.76 -0.47 58.77 -0.82 108.85 -0.82 l90.98 0 14.34 13.75 c7.76 7.64 24.57 23.86 37.15 36.20 12.58 12.34 29.15 28.45 36.67 35.85 l13.87 13.40 0.24 82.87 0.35 82.99 14.22 0.35 14.34 0.35 4.47 -5.64 c2.47 -3.06 11.52 -14.11 19.98 -24.45 8.46 -10.34 30.56 -38.32 49.14 -62.30 43.85 -56.42 46.20 -59.36 48.43 -59.36 2.12 0 4.82 1.88 47.96 33.27 43.02 31.27 45.37 33.03 45.37 34.09 0 0.82 -39.03 46.20 -62.42 72.53 -62.42 70.29 -106.50 117.67 -184.08 197.60 l-50.78 52.43 -13.28 0.71 c-7.29 0.47 -23.98 0.82 -37.03 0.82 l-23.86 0 0 57.01 0 57.01 -49.37 0 -49.37 0 0 -56.42 0 -56.42 -114.37 0 c-63.01 0 -120.13 0.35 -126.95 0.82 l-12.58 0.71 0 55.72 0 55.60 -47.61 0 -47.61 0 -0.24 -59.01z m443.16 -373.22 l-0.35 -119.66 -223.93 0.12 -223.93 0.12 -0.35 119.43 -0.24 119.55 224.52 0 224.52 0 -0.24 -119.55z", "M546.36 516.39 c-0.24 -0.59 -0.47 -4.94 -0.35 -9.76 0 -4.82 0.12 -52.54 0.35 -106.15 l0.24 -97.21 209.59 0.24 209.47 0.35 0.35 106.73 0.24 106.62 -209.71 0 c-115.43 0 -209.94 -0.35 -210.18 -0.82z m121.07 -100.50 l-0.35 -66.18 -34.32 -0.35 -34.44 -0.24 0 66.41 0 66.41 34.68 0 34.68 0 -0.24 -66.06z m260.96 0 l-0.35 -66.18 -34.09 0 -34.09 0 0 10.93 c0 5.88 0.12 35.62 0.35 66.06 l0.24 55.25 34.09 0 34.09 0 -0.24 -66.06z"],
9
- box: { left: 110.48999999999992, top: 196.30999999999992, right: 1389.0800000000002, bottom: 869.2899999999998 }
6
+ box: { left: 110.48999999999992, top: 196.30999999999992, right: 1389.0800000000002, bottom: 869.2899999999998 },
7
+ centroid: { x: 755.6746766446299, y: 497.3295661269271 }
10
8
  };
11
9
 
10
+ // src/region.ts
11
+ var twiceArea = (ring) => ring.reduce((sum, p, i) => {
12
+ const q = ring[(i + 1) % ring.length];
13
+ return sum + p[0] * q[1] - q[0] * p[1];
14
+ }, 0);
15
+ function collect(rings, add, into) {
16
+ for (const ring of rings) {
17
+ ring.forEach((p, i) => {
18
+ const q = ring[(i + 1) % ring.length];
19
+ if (p[1] === q[1]) return;
20
+ into.push(
21
+ p[1] < q[1] ? { x0: p[0], y0: p[1], x1: q[0], y1: q[1], wind: 1, add } : { x0: q[0], y0: q[1], x1: p[0], y1: p[1], wind: -1, add }
22
+ );
23
+ });
24
+ }
25
+ }
26
+ var xAt = (e, y) => y === e.y0 ? e.x0 : y === e.y1 ? e.x1 : e.x0 + (y - e.y0) * (e.x1 - e.x0) / (e.y1 - e.y0);
27
+ function heights(edges) {
28
+ const ys = /* @__PURE__ */ new Set();
29
+ for (const e of edges) {
30
+ ys.add(e.y0);
31
+ ys.add(e.y1);
32
+ }
33
+ for (let i = 0; i < edges.length; i += 1) {
34
+ const a = edges[i];
35
+ for (let j = i + 1; j < edges.length && edges[j].y0 < a.y1; j += 1) {
36
+ const b = edges[j];
37
+ const top = Math.max(a.y0, b.y0);
38
+ const bottom = Math.min(a.y1, b.y1);
39
+ if (bottom <= top) continue;
40
+ const above = xAt(a, top) - xAt(b, top);
41
+ const below = xAt(a, bottom) - xAt(b, bottom);
42
+ if (above * below < 0) ys.add(top + (bottom - top) * above / (above - below));
43
+ }
44
+ }
45
+ return [...ys].sort((p, q) => p - q);
46
+ }
47
+ var SNAP = 1e-9;
48
+ function snapper() {
49
+ const seen = /* @__PURE__ */ new Map();
50
+ return (y, x) => {
51
+ let xs = seen.get(y);
52
+ if (!xs) seen.set(y, xs = []);
53
+ for (const known of xs) if (Math.abs(known - x) <= SNAP) return known;
54
+ xs.push(x);
55
+ return x;
56
+ };
57
+ }
58
+ function spansOf(active, top, bottom, snap) {
59
+ const at = active.map((e) => ({ e, a: snap(top, xAt(e, top)), b: snap(bottom, xAt(e, bottom)) })).sort((p, q) => p.a + p.b - (q.a + q.b));
60
+ const spans = [];
61
+ let added = 0;
62
+ let removed = 0;
63
+ let inside = false;
64
+ let from = [0, 0];
65
+ for (let i = 0; i < at.length; ) {
66
+ const { a, b } = at[i];
67
+ for (; i < at.length && at[i].a === a && at[i].b === b; i += 1) {
68
+ const { e } = at[i];
69
+ if (e.add) added += e.wind;
70
+ else removed += e.wind;
71
+ }
72
+ const now = added !== 0 && removed === 0;
73
+ if (now && !inside) from = [a, b];
74
+ else if (!now && inside) spans.push({ left: from, right: [a, b] });
75
+ inside = now;
76
+ }
77
+ return spans;
78
+ }
79
+ function minus(a, b) {
80
+ const out = [];
81
+ let j = 0;
82
+ for (const [start, end] of a) {
83
+ let from = start;
84
+ while (j < b.length && b[j][1] <= from) j += 1;
85
+ for (let k = j; k < b.length && b[k][0] < end && from < end; k += 1) {
86
+ const [bStart, bEnd] = b[k];
87
+ if (bStart > from) out.push([from, bStart]);
88
+ from = Math.max(from, bEnd);
89
+ }
90
+ if (from < end) out.push([from, end]);
91
+ }
92
+ return out;
93
+ }
94
+ function boundaryOf(ys, slabs) {
95
+ const out = [];
96
+ slabs.forEach((spans, k) => {
97
+ const top = ys[k];
98
+ const bottom = ys[k + 1];
99
+ for (const { left, right } of spans) {
100
+ out.push([
101
+ [right[0], top],
102
+ [right[1], bottom]
103
+ ]);
104
+ out.push([
105
+ [left[1], bottom],
106
+ [left[0], top]
107
+ ]);
108
+ }
109
+ });
110
+ ys.forEach((y, k) => {
111
+ const above = k > 0 ? slabs[k - 1].map(({ left, right }) => [left[1], right[1]]) : [];
112
+ const below = k < slabs.length ? slabs[k].map(({ left, right }) => [left[0], right[0]]) : [];
113
+ for (const [a, b] of minus(below, above)) {
114
+ out.push([
115
+ [a, y],
116
+ [b, y]
117
+ ]);
118
+ }
119
+ for (const [a, b] of minus(above, below)) {
120
+ out.push([
121
+ [b, y],
122
+ [a, y]
123
+ ]);
124
+ }
125
+ });
126
+ return out.filter(([a, b]) => a[0] !== b[0] || a[1] !== b[1]);
127
+ }
128
+ var EPSILON = 1e-9;
129
+ function simplify(ring) {
130
+ let points = [...ring];
131
+ let changed = true;
132
+ while (changed && points.length >= 3) {
133
+ changed = false;
134
+ const kept = [];
135
+ for (let i = 0; i < points.length; i += 1) {
136
+ const prev = kept.length > 0 ? kept[kept.length - 1] : points[points.length - 1];
137
+ const p = points[i];
138
+ const next = points[(i + 1) % points.length];
139
+ const ux = p[0] - prev[0];
140
+ const uy = p[1] - prev[1];
141
+ const vx = next[0] - p[0];
142
+ const vy = next[1] - p[1];
143
+ const short = Math.hypot(ux, uy) < EPSILON;
144
+ const straight = Math.abs(ux * vy - uy * vx) < EPSILON;
145
+ if (short || straight) {
146
+ changed = true;
147
+ continue;
148
+ }
149
+ kept.push(p);
150
+ }
151
+ points = kept;
152
+ }
153
+ return points;
154
+ }
155
+ function chain(segments) {
156
+ const key = (p) => `${p[0]} ${p[1]}`;
157
+ const from = /* @__PURE__ */ new Map();
158
+ segments.forEach(([a], i) => {
159
+ const list = from.get(key(a));
160
+ if (list) list.push(i);
161
+ else from.set(key(a), [i]);
162
+ });
163
+ const used = new Uint8Array(segments.length);
164
+ const rings = [];
165
+ segments.forEach((first, i) => {
166
+ if (used[i]) return;
167
+ const ring = [];
168
+ let at = i;
169
+ while (at !== void 0) {
170
+ used[at] = 1;
171
+ const [a, b] = segments[at];
172
+ ring.push(a);
173
+ if (b[0] === first[0][0] && b[1] === first[0][1]) break;
174
+ at = from.get(key(b))?.find((n) => !used[n]);
175
+ }
176
+ rings.push(ring);
177
+ });
178
+ return rings.map(simplify).filter((ring) => ring.length >= 3 && Math.abs(twiceArea(ring)) > EPSILON);
179
+ }
180
+ function region(add, remove = []) {
181
+ const edges = [];
182
+ collect(add, true, edges);
183
+ collect(remove, false, edges);
184
+ edges.sort((p, q) => p.y0 - q.y0);
185
+ const ys = heights(edges);
186
+ const snap = snapper();
187
+ const slabs = [];
188
+ let next = 0;
189
+ let active = [];
190
+ for (let k = 0; k + 1 < ys.length; k += 1) {
191
+ const top = ys[k];
192
+ const bottom = ys[k + 1];
193
+ while (next < edges.length && edges[next].y0 <= top) active.push(edges[next++]);
194
+ active = active.filter((e) => e.y1 > top);
195
+ slabs.push(spansOf(active, top, bottom, snap));
196
+ }
197
+ return chain(boundaryOf(ys, slabs));
198
+ }
199
+ var areaOf = (rings) => rings.reduce((sum, ring) => sum + twiceArea(ring), 0) / 2;
200
+ function centroidOf(rings) {
201
+ let x = 0;
202
+ let y = 0;
203
+ for (const ring of rings) {
204
+ ring.forEach((p, i) => {
205
+ const q = ring[(i + 1) % ring.length];
206
+ const cross = p[0] * q[1] - q[0] * p[1];
207
+ x += (p[0] + q[0]) * cross;
208
+ y += (p[1] + q[1]) * cross;
209
+ });
210
+ }
211
+ const six = 6 * areaOf(rings);
212
+ return [x / six, y / six];
213
+ }
214
+ var clockwise = (ring) => twiceArea(ring) < 0 ? ring.reverse() : ring;
215
+ function strokeBand(rings, distance, miterLimit = 4) {
216
+ const pieces = [];
217
+ for (const ring of rings) {
218
+ ring.forEach((p, i) => {
219
+ const q = ring[(i + 1) % ring.length];
220
+ const r = ring[(i + 2) % ring.length];
221
+ const length = Math.hypot(q[0] - p[0], q[1] - p[1]);
222
+ const nx = (q[1] - p[1]) / length;
223
+ const ny = -(q[0] - p[0]) / length;
224
+ const onward = Math.hypot(r[0] - q[0], r[1] - q[1]);
225
+ const mx = (r[1] - q[1]) / onward;
226
+ const my = -(r[0] - q[0]) / onward;
227
+ const d = distance;
228
+ pieces.push(
229
+ clockwise([
230
+ [p[0] + nx * d, p[1] + ny * d],
231
+ [q[0] + nx * d, q[1] + ny * d],
232
+ [q[0] - nx * d, q[1] - ny * d],
233
+ [p[0] - nx * d, p[1] - ny * d]
234
+ ])
235
+ );
236
+ const turn = (q[0] - p[0]) * (r[1] - q[1]) - (q[1] - p[1]) * (r[0] - q[0]);
237
+ if (turn === 0) return;
238
+ const side = turn > 0 ? 1 : -1;
239
+ const a = [q[0] + side * nx * d, q[1] + side * ny * d];
240
+ const b = [q[0] + side * mx * d, q[1] + side * my * d];
241
+ const sx = nx + mx;
242
+ const sy = ny + my;
243
+ const reach = sx * sx + sy * sy;
244
+ if (reach > 0 && 2 / Math.sqrt(reach) <= miterLimit) {
245
+ const tip = [q[0] + side * sx * 2 * d / reach, q[1] + side * sy * 2 * d / reach];
246
+ pieces.push(clockwise([q, a, tip, b]));
247
+ } else {
248
+ pieces.push(clockwise([q, a, b]));
249
+ }
250
+ });
251
+ }
252
+ return pieces;
253
+ }
254
+ var round = (n) => Math.round(n * 100) / 100;
255
+ function pathOfRings(rings) {
256
+ return rings.map((ring) => {
257
+ const points = ring.map(([x, y]) => `${round(x)} ${round(y)}`).filter((p, i, all) => p !== all[i - 1]);
258
+ return `M${points.join("L")}Z`;
259
+ }).join("");
260
+ }
261
+
12
262
  // src/crewletCharacters.ts
13
263
  var CREWLET_CHARACTERS = [
14
264
  "crewlet",
@@ -47,13 +297,14 @@ var GROUND = 96;
47
297
  var KEYLINE_LINE = 1.35;
48
298
  var KEYLINE_GAP = 0.9;
49
299
  var VISOR_GAP = 1.3;
300
+ var HAIRLINE = 2;
50
301
  var EYE = {
51
302
  full: { width: 0.164, height: 0.62 },
52
303
  compact: { width: 0.19, height: 0.68 },
53
304
  offset: 0.312
54
305
  };
55
306
  var NOTCH = { half: 3.2, depth: 2.6 };
56
- var round = (n) => Math.round(n * 100) / 100;
307
+ var round2 = (n) => Math.round(n * 100) / 100;
57
308
  function cut(points, chamfer) {
58
309
  const out = [];
59
310
  points.forEach((vertex, i) => {
@@ -72,14 +323,10 @@ function cut(points, chamfer) {
72
323
  });
73
324
  return out;
74
325
  }
75
- var pathOf = (points) => `M${points.map(([x, y]) => `${round(x)} ${round(y)}`).join("L")}Z`;
76
- var area = (points) => points.reduce((sum, p, i) => {
77
- const q = points[(i + 1) % points.length];
78
- return sum + p[0] * q[1] - q[0] * p[1];
79
- }, 0);
326
+ var pathOf = (points) => `M${points.map(([x, y]) => `${round2(x)} ${round2(y)}`).join("L")}Z`;
80
327
  function poly(points, chamfer = 0) {
81
328
  const chamfers = typeof chamfer === "number" ? points.map(() => chamfer) : chamfer;
82
- return area(points) < 0 ? pathOf(cut([...points].reverse(), [...chamfers].reverse())) : pathOf(cut(points, chamfers));
329
+ return twiceArea(points) < 0 ? cut([...points].reverse(), [...chamfers].reverse()) : cut(points, chamfers);
83
330
  }
84
331
  var rect = (x, y, width, height, chamfer = 0) => poly(
85
332
  [
@@ -110,45 +357,56 @@ function gear(cx, cy, outer, inner, teeth) {
110
357
  }
111
358
  return poly(points);
112
359
  }
113
- function boxOf(paths, grow) {
114
- const numbers = paths.join(" ").match(/-?\d*\.?\d+/g).map(Number);
115
- let left = Infinity;
116
- let top = Infinity;
117
- let right = -Infinity;
118
- let bottom = -Infinity;
119
- for (let i = 0; i < numbers.length; i += 2) {
120
- left = Math.min(left, numbers[i]);
121
- right = Math.max(right, numbers[i]);
122
- top = Math.min(top, numbers[i + 1]);
123
- bottom = Math.max(bottom, numbers[i + 1]);
124
- }
125
- return { x: left - grow, y: top - grow, width: right - left + 2 * grow, height: bottom - top + 2 * grow };
360
+ function boxOf(rings) {
361
+ const points = rings.flat();
362
+ const xs = points.map(([x]) => x);
363
+ const ys = points.map(([, y]) => y);
364
+ const left = Math.min(...xs);
365
+ const top = Math.min(...ys);
366
+ return { x: left, y: top, width: Math.max(...xs) - left, height: Math.max(...ys) - top };
126
367
  }
127
368
  var box = ([x, y, width, height]) => ({ x, y, width, height });
369
+ var RINGS = /* @__PURE__ */ new WeakMap();
128
370
  function drawn(spec) {
129
- return {
371
+ const holes = spec.holes ?? [];
372
+ let bounds;
373
+ const geometry = {
130
374
  kind: "drawn",
131
375
  name: spec.name,
132
376
  shape: spec.shape,
133
- shapes: spec.shapes,
134
- holes: spec.holes ?? [],
377
+ shapes: spec.shapes.map(pathOf),
378
+ holes: holes.map(pathOf),
135
379
  visor: box(spec.visor),
136
380
  notch: spec.notch === void 0 ? null : [spec.notchX ?? 50, spec.notch],
137
- // The keyline reaches its gap plus its line past the outline.
138
- bounds: boxOf(spec.shapes, KEYLINE_GAP + KEYLINE_LINE)
381
+ // It takes the union, so it is measured the first time it is asked for.
382
+ get bounds() {
383
+ return bounds ??= boxOf([...silhouetteOf(geometry), ...keylineReachOf(geometry)]);
384
+ }
139
385
  };
386
+ RINGS.set(geometry, { parts: spec.shapes, holes });
387
+ return geometry;
388
+ }
389
+ var ringsOf = (geometry) => RINGS.get(geometry);
390
+ function silhouetteOf(geometry) {
391
+ const rings = ringsOf(geometry);
392
+ return rings.silhouette ??= region(rings.parts);
393
+ }
394
+ function keylineReachOf(geometry) {
395
+ const rings = ringsOf(geometry);
396
+ return rings.reach ??= strokeBand(silhouetteOf(geometry), KEYLINE_GAP + KEYLINE_LINE);
140
397
  }
141
398
  var MARK_SCALE = 0.095;
142
399
  var MARK_CENTRE = 760;
143
400
  var MARK_FEET = 869;
401
+ var markOnStage = (x, y) => [
402
+ 50 + (x - MARK_CENTRE) * MARK_SCALE,
403
+ GROUND + (y - MARK_FEET) * MARK_SCALE
404
+ ];
405
+ var MARK_CENTROID = markOnStage(CREWLET_MARK.centroid.x, CREWLET_MARK.centroid.y);
144
406
  function mark() {
145
407
  const { paths, box: own } = CREWLET_MARK;
146
- const onStage = (x, y) => [
147
- 50 + (x - MARK_CENTRE) * MARK_SCALE,
148
- GROUND + (y - MARK_FEET) * MARK_SCALE
149
- ];
150
- const [left, top] = onStage(own.left, own.top);
151
- const [right, bottom] = onStage(own.right, own.bottom);
408
+ const [left, top] = markOnStage(own.left, own.top);
409
+ const [right, bottom] = markOnStage(own.right, own.bottom);
152
410
  return {
153
411
  kind: "mark",
154
412
  name: "Crewlet",
@@ -659,89 +917,103 @@ var CREWLET_CHARACTER_GEOMETRY = {
659
917
  visor: [24, 40, 52, 22]
660
918
  })
661
919
  };
662
- var round2 = (n) => Math.round(n * 100) / 100;
663
- function viewBoxOf({ x, y, width, height }) {
664
- const side = Math.max(width, height);
665
- return [x + width / 2 - side / 2, y + height / 2 - side / 2, side, side].map(round2).join(" ");
920
+
921
+ // src/characterDrawing.ts
922
+ var OPTICAL_PULL = 0.5;
923
+ var rect2 = (x, y, width, height) => [
924
+ [x, y],
925
+ [x + width, y],
926
+ [x + width, y + height],
927
+ [x, y + height]
928
+ ];
929
+ function widthOf(ring) {
930
+ let least = Infinity;
931
+ ring.forEach((p, i) => {
932
+ const q = ring[(i + 1) % ring.length];
933
+ const length = Math.hypot(q[0] - p[0], q[1] - p[1]);
934
+ if (length === 0) return;
935
+ const depth = Math.max(
936
+ ...ring.map((r) => Math.abs((q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0])) / length)
937
+ );
938
+ least = Math.min(least, depth);
939
+ });
940
+ return least;
666
941
  }
667
- function Drawn({ geometry, detail, id }) {
668
- const { shapes, holes, visor, notch } = geometry;
942
+ function cutsOf(geometry, detail) {
943
+ const { visor, notch } = geometry;
669
944
  const full = detail === "full";
670
- const d = shapes.join("");
671
945
  const eye = full ? EYE.full : EYE.compact;
672
946
  const eyeWidth = visor.width * eye.width;
673
947
  const eyeHeight = visor.height * eye.height;
674
948
  const eyeTop = visor.y + (visor.height - eyeHeight) / 2;
675
949
  const middle = visor.x + visor.width / 2;
676
950
  const offset = visor.width * EYE.offset;
677
- const reach = { x: -40, y: -40, width: 180, height: 180 };
678
- const face = `${id}-face`;
679
- const ring = `${id}-ring`;
680
- return /* @__PURE__ */ jsxs(Fragment, { children: [
681
- /* @__PURE__ */ jsxs("defs", { children: [
682
- /* @__PURE__ */ jsxs("mask", { id: face, maskUnits: "userSpaceOnUse", ...reach, children: [
683
- /* @__PURE__ */ jsx("rect", { ...reach, fill: "#fff" }),
684
- /* @__PURE__ */ jsxs("g", { fill: "#000", children: [
685
- [middle - offset, middle + offset].map((centre) => /* @__PURE__ */ jsx(
686
- "rect",
687
- {
688
- x: round2(centre - eyeWidth / 2),
689
- y: round2(eyeTop),
690
- width: round2(eyeWidth),
691
- height: round2(eyeHeight)
692
- },
693
- centre
694
- )),
695
- notch && /* @__PURE__ */ jsx(
696
- "path",
697
- {
698
- d: `M${round2(notch[0] - NOTCH.half)} ${round2(notch[1] - 1)}L${round2(notch[0] + NOTCH.half)} ${round2(
699
- notch[1] - 1
700
- )}L${round2(notch[0])} ${round2(notch[1] + NOTCH.depth)}Z`
701
- }
702
- ),
703
- holes.map((hole) => /* @__PURE__ */ jsx("path", { d: hole }, hole))
704
- ] }),
705
- full && /* @__PURE__ */ jsx(
706
- "rect",
707
- {
708
- x: visor.x,
709
- y: visor.y,
710
- width: visor.width,
711
- height: visor.height,
712
- fill: "none",
713
- stroke: "#000",
714
- strokeWidth: VISOR_GAP
715
- }
716
- )
717
- ] }),
718
- full && /* @__PURE__ */ jsxs("mask", { id: ring, maskUnits: "userSpaceOnUse", ...reach, children: [
719
- /* @__PURE__ */ jsx("rect", { ...reach, fill: "#fff" }),
720
- /* @__PURE__ */ jsx("path", { d, fill: "#000", stroke: "#000", strokeWidth: KEYLINE_GAP * 2, strokeLinejoin: "miter" })
721
- ] })
722
- ] }),
723
- full && /* @__PURE__ */ jsx(
724
- "path",
725
- {
726
- d,
727
- mask: `url(#${ring})`,
728
- fill: "none",
729
- stroke: "currentColor",
730
- strokeWidth: (KEYLINE_GAP + KEYLINE_LINE) * 2,
731
- strokeLinejoin: "miter"
732
- }
733
- ),
734
- /* @__PURE__ */ jsx("path", { d, mask: `url(#${face})`, fill: "currentColor" })
735
- ] });
951
+ const cuts = [middle - offset, middle + offset].map(
952
+ (centre) => rect2(centre - eyeWidth / 2, eyeTop, eyeWidth, eyeHeight)
953
+ );
954
+ if (notch) {
955
+ cuts.push([
956
+ [notch[0] - NOTCH.half, notch[1] - 1],
957
+ [notch[0] + NOTCH.half, notch[1] - 1],
958
+ [notch[0], notch[1] + NOTCH.depth]
959
+ ]);
960
+ }
961
+ for (const hole of ringsOf(geometry).holes) if (full || widthOf(hole) > HAIRLINE) cuts.push(hole);
962
+ if (full) {
963
+ const half = VISOR_GAP / 2;
964
+ cuts.push(rect2(visor.x - half, visor.y - half, visor.width + VISOR_GAP, visor.height + VISOR_GAP));
965
+ cuts.push([...rect2(visor.x + half, visor.y + half, visor.width - VISOR_GAP, visor.height - VISOR_GAP)].reverse());
966
+ }
967
+ return cuts;
968
+ }
969
+ function frameOf(bounds, centroid) {
970
+ const boxX = bounds.x + bounds.width / 2;
971
+ const boxY = bounds.y + bounds.height / 2;
972
+ const x = boxX + OPTICAL_PULL * (centroid[0] - boxX);
973
+ const y = boxY + OPTICAL_PULL * (centroid[1] - boxY);
974
+ const side = Math.max(bounds.width + 2 * Math.abs(x - boxX), bounds.height + 2 * Math.abs(y - boxY));
975
+ return { x: x - side / 2, y: y - side / 2, width: side, height: side };
976
+ }
977
+ function drawnDrawing(geometry) {
978
+ const { parts } = ringsOf(geometry);
979
+ const silhouette = silhouetteOf(geometry);
980
+ const made = {};
981
+ const body = (detail) => pathOfRings(region(parts, cutsOf(geometry, detail)));
982
+ const keyline = () => pathOfRings(
983
+ region([...silhouette, ...keylineReachOf(geometry)], [...silhouette, ...strokeBand(silhouette, KEYLINE_GAP)])
984
+ );
985
+ return {
986
+ frame: frameOf(geometry.bounds, centroidOf(silhouette)),
987
+ paths: (detail) => made[detail] ??= detail === "full" ? [keyline(), body("full")] : [body("compact")]
988
+ };
989
+ }
990
+ var drawings = /* @__PURE__ */ new Map();
991
+ function drawingOf(character) {
992
+ let drawing = drawings.get(character);
993
+ if (drawing) return drawing;
994
+ const geometry = CREWLET_CHARACTER_GEOMETRY[character];
995
+ if (geometry.kind === "mark") {
996
+ const full = [geometry.keyline, ...geometry.body];
997
+ drawing = {
998
+ frame: frameOf(geometry.bounds, MARK_CENTROID),
999
+ paths: (detail) => detail === "full" ? full : geometry.body
1000
+ };
1001
+ } else {
1002
+ drawing = drawnDrawing(geometry);
1003
+ }
1004
+ drawings.set(character, drawing);
1005
+ return drawing;
736
1006
  }
1007
+ var round3 = (n) => Math.round(n * 100) / 100;
737
1008
  var CrewletCharacter = ({ character, detail = "full", ...rest }) => {
738
- const id = `crewlet-character-${useId().replace(/[^A-Za-z0-9_-]/g, "")}`;
739
1009
  const geometry = CREWLET_CHARACTER_GEOMETRY[character];
1010
+ const { frame, paths } = drawingOf(character);
1011
+ const drawn2 = paths(detail).map((d) => /* @__PURE__ */ jsx("path", { d }, d));
740
1012
  return /* @__PURE__ */ jsx(
741
1013
  "svg",
742
1014
  {
743
1015
  xmlns: "http://www.w3.org/2000/svg",
744
- viewBox: viewBoxOf(geometry.bounds),
1016
+ viewBox: [frame.x, frame.y, frame.width, frame.height].map(round3).join(" "),
745
1017
  width: "1em",
746
1018
  height: "1em",
747
1019
  "aria-hidden": "true",
@@ -749,10 +1021,7 @@ var CrewletCharacter = ({ character, detail = "full", ...rest }) => {
749
1021
  "data-character": character,
750
1022
  "data-detail": detail,
751
1023
  ...rest,
752
- children: geometry.kind === "mark" ? /* @__PURE__ */ jsxs("g", { transform: geometry.transform, fill: "currentColor", children: [
753
- detail === "full" && /* @__PURE__ */ jsx("path", { d: geometry.keyline }),
754
- geometry.body.map((d) => /* @__PURE__ */ jsx("path", { d }, d))
755
- ] }) : /* @__PURE__ */ jsx(Drawn, { geometry, detail, id })
1024
+ children: /* @__PURE__ */ jsx("g", { fill: "currentColor", transform: geometry.kind === "mark" ? geometry.transform : void 0, children: drawn2 })
756
1025
  }
757
1026
  );
758
1027
  };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { CREWLET_CHARACTERS, CREWLET_CHARACTER_GEOMETRY, CrewletCharacter_default as CrewletCharacter, isCrewletCharacter } from './chunk-7RRFA6BN.js';
1
+ export { CREWLET_CHARACTERS, CREWLET_CHARACTER_GEOMETRY, CrewletCharacter_default as CrewletCharacter, isCrewletCharacter } from './chunk-X7FKGFKM.js';
2
2
  import { cssLength } from './chunk-TOSJN7HA.js';
3
3
  import { jsxs, jsx } from 'react/jsx-runtime';
4
4
  import './CrewletFigure-4PMMCAFG.css';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewlethq/icons",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Crewlet signature illustrated icons, vendor marks, and the Lucide glyph set as React components.",
5
5
  "license": "MIT AND ISC",
6
6
  "repository": {