@roxyapi/ui 0.7.0 → 0.8.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/AGENTS.md +5 -1
- package/README.md +30 -1
- package/dist/cdn/components/bodygraph.js +54 -0
- package/dist/cdn/components/bodygraph.js.map +7 -0
- package/dist/cdn/components/forecast-timeline.js +45 -0
- package/dist/cdn/components/forecast-timeline.js.map +7 -0
- package/dist/cdn/roxy-ui.js +49 -40
- package/dist/cdn/roxy-ui.js.map +4 -4
- package/dist/components/bodygraph.d.ts +27 -0
- package/dist/components/bodygraph.d.ts.map +1 -0
- package/dist/components/bodygraph.js +11 -0
- package/dist/components/bodygraph.js.map +7 -0
- package/dist/components/forecast-timeline.d.ts +38 -0
- package/dist/components/forecast-timeline.d.ts.map +1 -0
- package/dist/components/forecast-timeline.js +2 -0
- package/dist/components/forecast-timeline.js.map +7 -0
- package/dist/index.cjs +45 -36
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +52 -43
- package/dist/index.js.map +4 -4
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.json +2 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/types.gen.d.ts +3994 -6
- package/dist/types/types.gen.d.ts.map +1 -1
- package/dist/utils/bodygraph-render.d.ts +105 -0
- package/dist/utils/bodygraph-render.d.ts.map +1 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/bodygraph.ts +390 -0
- package/src/components/forecast-timeline.ts +336 -0
- package/src/index.ts +4 -0
- package/src/manifest.ts +26 -0
- package/src/types/index.ts +1 -1
- package/src/types/types.gen.ts +4079 -6
- package/src/utils/bodygraph-render.ts +641 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
import type { TemplateResult } from 'lit';
|
|
2
|
+
import { nothing, svg } from 'lit';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fixed geometry and renderer for the Human Design bodygraph. The diagram is a
|
|
6
|
+
* standard, invariant layout: nine centers in canonical positions and shapes,
|
|
7
|
+
* wired by 36 channels that each join two gates, with the 64 gates at fixed
|
|
8
|
+
* points on the center edges, all overlaid on a front-facing human silhouette so
|
|
9
|
+
* each center lands on the body part it governs. Only which centers are defined
|
|
10
|
+
* and which channels and gates are active changes per chart, so this module holds
|
|
11
|
+
* the geometry and the {@link RoxyBodygraph} component supplies the live state
|
|
12
|
+
* from the /human-design/bodygraph response.
|
|
13
|
+
*
|
|
14
|
+
* @remarks Every point is authored in one normalized 0 to 100 canonical grid (x
|
|
15
|
+
* left to right, y top to bottom) taken from the reference Jovian Archive
|
|
16
|
+
* bodygraph, then scaled into {@link BODYGRAPH_VIEWBOX} by a single transform so
|
|
17
|
+
* the chart and the body share one coordinate space and scale together. The grid
|
|
18
|
+
* is sized so the nine centers fill the figure exactly as in the canonical
|
|
19
|
+
* "nine centers on the human body" diagram: the Head center at the crown, Ajna at
|
|
20
|
+
* the forehead, Throat at the neck, G at the chest, Heart at the right chest,
|
|
21
|
+
* Spleen on the left torso, Solar Plexus on the right torso (its mirror), Sacral
|
|
22
|
+
* at the lower abdomen, and Root at the pelvis. Two structural truths the layout
|
|
23
|
+
* preserves: the Spleen (left) and the Solar Plexus (right) are mirror images at
|
|
24
|
+
* the Sacral height, so the chart is narrow at the top and wide at the bottom;
|
|
25
|
+
* and the Heart sits low and to the right of the G center, above the Solar
|
|
26
|
+
* Plexus. The reference chart spreads the two side centers to the page edge for
|
|
27
|
+
* channel clarity; here they are shifted inward by a fixed amount so they rest on
|
|
28
|
+
* the torso sides inside the figure, with every channel topology preserved. Center shapes follow the canonical orientations (Head triangle
|
|
29
|
+
* up, Ajna triangle down, Throat and Sacral and Root squares, G diamond, Heart
|
|
30
|
+
* triangle pointing right, Spleen triangle pointing right with its base on the
|
|
31
|
+
* far-left edge, Solar Plexus its mirror). The 36 channel gate pairs are the
|
|
32
|
+
* gold-standard set used by the RoxyAPI Human Design engine.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export type BodygraphCenterId =
|
|
36
|
+
| 'head'
|
|
37
|
+
| 'ajna'
|
|
38
|
+
| 'throat'
|
|
39
|
+
| 'g'
|
|
40
|
+
| 'heart'
|
|
41
|
+
| 'sacral'
|
|
42
|
+
| 'solar-plexus'
|
|
43
|
+
| 'spleen'
|
|
44
|
+
| 'root';
|
|
45
|
+
|
|
46
|
+
interface Point {
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* One center's drawable geometry: its semantic traditional color, the polygon
|
|
53
|
+
* point list of its canonical shape, and the anchor where its name label sits.
|
|
54
|
+
* Shapes are explicit point lists so the triangle and diamond orientations match
|
|
55
|
+
* the canonical diagram exactly. Label anchors sit in the empty margins outside
|
|
56
|
+
* each shape so they never collide with the gate numbers printed on the edges.
|
|
57
|
+
*/
|
|
58
|
+
interface CenterGeometry {
|
|
59
|
+
id: BodygraphCenterId;
|
|
60
|
+
label: string;
|
|
61
|
+
color: CenterColor;
|
|
62
|
+
points: Point[];
|
|
63
|
+
labelAnchor: Point;
|
|
64
|
+
labelAlign: 'start' | 'middle' | 'end';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Traditional Human Design center color group. A defined center is filled with
|
|
69
|
+
* this semantic color (constant across light and dark, like chart data colors);
|
|
70
|
+
* an open center is left transparent with a thin theme-aware outline. The four
|
|
71
|
+
* groups mirror the canonical scheme: gold for the identity and inspiration
|
|
72
|
+
* centers (Head, G), green for the mental awareness center (Ajna), red for the
|
|
73
|
+
* life-force motors of will and vitality (Heart, Sacral), brown for the
|
|
74
|
+
* pressure, expression, and remaining awareness centers (Throat, Spleen, Solar
|
|
75
|
+
* Plexus, Root).
|
|
76
|
+
*/
|
|
77
|
+
export type CenterColor = 'gold' | 'green' | 'red' | 'brown';
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Viewport mapping from the normalized 0 to 100 grid into the SVG viewBox. The
|
|
81
|
+
* chart proper occupies the inner grid; {@link PAD} is the slim margin (in grid
|
|
82
|
+
* units) the body silhouette extends past the centers on every side, so the head
|
|
83
|
+
* rises just above the Head center, the hips sit just below Root, and the
|
|
84
|
+
* shoulders spread just outside Spleen and Solar Plexus. UNIT scales grid units
|
|
85
|
+
* to viewBox units; the chart keeps its natural narrow-top, wide-bottom shape
|
|
86
|
+
* because x and y share one scale.
|
|
87
|
+
*/
|
|
88
|
+
const UNIT = 4;
|
|
89
|
+
const PAD = 9;
|
|
90
|
+
const VIEW_W = (100 + 2 * PAD) * UNIT;
|
|
91
|
+
const VIEW_H = (100 + 2 * PAD) * UNIT;
|
|
92
|
+
|
|
93
|
+
/** Map a normalized grid point (0 to 100) into viewBox units. */
|
|
94
|
+
function g(x: number, y: number): Point {
|
|
95
|
+
return { x: (x + PAD) * UNIT, y: (y + PAD) * UNIT };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Chart horizontal center, the axis of symmetry for the body and the side centers. */
|
|
99
|
+
const AXIS = 50;
|
|
100
|
+
|
|
101
|
+
/** Reflect a normalized grid x across {@link AXIS}, the vertical axis of symmetry. */
|
|
102
|
+
const mirrorX = (x: number): number => 2 * AXIS - x;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Gate positions authored per center in the normalized 0 to 100 grid, before the
|
|
106
|
+
* mapping through {@link g}. Symmetry is structural, not hand-typed: every center
|
|
107
|
+
* on the central column is authored balanced about {@link AXIS}; the Spleen is
|
|
108
|
+
* authored once on the left torso and the Solar Plexus is derived as its exact
|
|
109
|
+
* mirror in {@link buildGatePoints}, so the two side centers can never drift out
|
|
110
|
+
* of alignment. The Heart is the one center the canonical bodygraph places off
|
|
111
|
+
* the axis (low and to the right of the G center, with no left counterpart).
|
|
112
|
+
* Within each center the gates follow the canonical reading order so the numbers
|
|
113
|
+
* print where a printed chart shows them.
|
|
114
|
+
*
|
|
115
|
+
* @remarks Solar Plexus is intentionally empty here and filled by reflecting
|
|
116
|
+
* Spleen; {@link SPLEEN_TO_SOLAR_PLEXUS} pairs each Spleen gate with the Solar
|
|
117
|
+
* Plexus gate at its mirror position (base-top to base-top, apex to apex).
|
|
118
|
+
*/
|
|
119
|
+
const GATES_BY_CENTER: Record<
|
|
120
|
+
BodygraphCenterId,
|
|
121
|
+
Record<number, [number, number]>
|
|
122
|
+
> = {
|
|
123
|
+
head: { 64: [45.5, 11.2], 61: [50, 11.2], 63: [54.5, 11.2] },
|
|
124
|
+
ajna: {
|
|
125
|
+
47: [45.3, 18.0],
|
|
126
|
+
24: [50, 18.0],
|
|
127
|
+
4: [54.7, 18.0],
|
|
128
|
+
17: [45.6, 20.6],
|
|
129
|
+
11: [54.4, 20.6],
|
|
130
|
+
43: [50, 25.3],
|
|
131
|
+
},
|
|
132
|
+
throat: {
|
|
133
|
+
62: [45.5, 32.3],
|
|
134
|
+
23: [50, 32.3],
|
|
135
|
+
56: [54.5, 32.3],
|
|
136
|
+
16: [42, 34.6],
|
|
137
|
+
35: [58, 34.6],
|
|
138
|
+
12: [58, 37.6],
|
|
139
|
+
20: [42, 40.6],
|
|
140
|
+
45: [58, 40.6],
|
|
141
|
+
31: [46, 42.4],
|
|
142
|
+
8: [50, 42.4],
|
|
143
|
+
33: [54, 42.4],
|
|
144
|
+
},
|
|
145
|
+
g: {
|
|
146
|
+
1: [50, 47.5],
|
|
147
|
+
7: [45.6, 50.3],
|
|
148
|
+
13: [54.4, 50.3],
|
|
149
|
+
10: [40, 53.3],
|
|
150
|
+
25: [60, 53.3],
|
|
151
|
+
15: [45.6, 56.6],
|
|
152
|
+
46: [54.4, 56.6],
|
|
153
|
+
2: [50, 59.0],
|
|
154
|
+
},
|
|
155
|
+
heart: {
|
|
156
|
+
21: [62.5, 58.5],
|
|
157
|
+
51: [62.5, 61.3],
|
|
158
|
+
26: [62.5, 64.1],
|
|
159
|
+
40: [73, 61.3],
|
|
160
|
+
},
|
|
161
|
+
spleen: {
|
|
162
|
+
48: [20, 70.6],
|
|
163
|
+
57: [24, 72.3],
|
|
164
|
+
44: [28, 74.0],
|
|
165
|
+
50: [32, 75.6],
|
|
166
|
+
32: [28, 77.2],
|
|
167
|
+
28: [24, 78.9],
|
|
168
|
+
18: [20, 80.6],
|
|
169
|
+
},
|
|
170
|
+
sacral: {
|
|
171
|
+
5: [45.5, 72.5],
|
|
172
|
+
14: [50, 72.5],
|
|
173
|
+
29: [54.5, 72.5],
|
|
174
|
+
34: [42, 75.6],
|
|
175
|
+
27: [42, 79.0],
|
|
176
|
+
59: [58, 79.0],
|
|
177
|
+
42: [45.5, 81.4],
|
|
178
|
+
3: [50, 81.4],
|
|
179
|
+
9: [54.5, 81.4],
|
|
180
|
+
},
|
|
181
|
+
'solar-plexus': {},
|
|
182
|
+
root: {
|
|
183
|
+
53: [45.5, 89.9],
|
|
184
|
+
60: [50, 89.9],
|
|
185
|
+
52: [54.5, 89.9],
|
|
186
|
+
54: [42, 92.7],
|
|
187
|
+
19: [58, 92.7],
|
|
188
|
+
38: [42, 95.3],
|
|
189
|
+
39: [58, 95.3],
|
|
190
|
+
58: [42, 98.0],
|
|
191
|
+
41: [58, 98.0],
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/** Spleen gate to the Solar Plexus gate at its mirror position (base-top, ..., apex, ..., base-bottom). */
|
|
196
|
+
const SPLEEN_TO_SOLAR_PLEXUS: Record<number, number> = {
|
|
197
|
+
48: 36,
|
|
198
|
+
57: 22,
|
|
199
|
+
44: 37,
|
|
200
|
+
50: 6,
|
|
201
|
+
32: 49,
|
|
202
|
+
28: 55,
|
|
203
|
+
18: 30,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Assemble the viewBox-space gate anchors and the gate to center index from
|
|
208
|
+
* {@link GATES_BY_CENTER}, filling the Solar Plexus by reflecting the Spleen so
|
|
209
|
+
* the two side centers are guaranteed mirror images. Each gate sits on the
|
|
210
|
+
* canonical edge of its center where its channels connect, so channel lines join
|
|
211
|
+
* gate to gate cleanly and the activated numbers print in their traditional spots.
|
|
212
|
+
*/
|
|
213
|
+
function buildGatePoints(): {
|
|
214
|
+
points: Record<number, Point>;
|
|
215
|
+
centerOf: Record<number, BodygraphCenterId>;
|
|
216
|
+
} {
|
|
217
|
+
const points: Record<number, Point> = {};
|
|
218
|
+
const centerOf: Record<number, BodygraphCenterId> = {};
|
|
219
|
+
for (const [spleenGate, [x, y]] of Object.entries(GATES_BY_CENTER.spleen)) {
|
|
220
|
+
GATES_BY_CENTER['solar-plexus'][
|
|
221
|
+
SPLEEN_TO_SOLAR_PLEXUS[Number(spleenGate)]
|
|
222
|
+
] = [mirrorX(x), y];
|
|
223
|
+
}
|
|
224
|
+
for (const [center, gates] of Object.entries(GATES_BY_CENTER) as Array<
|
|
225
|
+
[BodygraphCenterId, Record<number, [number, number]>]
|
|
226
|
+
>) {
|
|
227
|
+
for (const [gate, [x, y]] of Object.entries(gates)) {
|
|
228
|
+
points[Number(gate)] = g(x, y);
|
|
229
|
+
centerOf[Number(gate)] = center;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { points, centerOf };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The viewBox-space gate anchors ({@link GATE_POINTS}) and the gate to center
|
|
237
|
+
* index ({@link GATE_CENTER}). Exported so the geometry tests can assert the
|
|
238
|
+
* layout invariants (side-center mirror symmetry, central-column balance, gates
|
|
239
|
+
* inside their centers) without rendering.
|
|
240
|
+
*/
|
|
241
|
+
export const { points: GATE_POINTS, centerOf: GATE_CENTER } = buildGatePoints();
|
|
242
|
+
|
|
243
|
+
/** Horizontal axis of symmetry in viewBox units, the reflection axis for geometry tests. */
|
|
244
|
+
export const CHART_AXIS_X = VIEW_W / 2;
|
|
245
|
+
|
|
246
|
+
/** Build a polygon from normalized grid corner pairs, mapping each through {@link g}. */
|
|
247
|
+
function shape(corners: ReadonlyArray<readonly [number, number]>): Point[] {
|
|
248
|
+
return corners.map(([x, y]) => g(x, y));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** A square center, centered on {@link AXIS}, spanning the given half-width and y range. */
|
|
252
|
+
function squareShape(halfWidth: number, top: number, bottom: number): Point[] {
|
|
253
|
+
return shape([
|
|
254
|
+
[AXIS - halfWidth, top],
|
|
255
|
+
[AXIS + halfWidth, top],
|
|
256
|
+
[AXIS + halfWidth, bottom],
|
|
257
|
+
[AXIS - halfWidth, bottom],
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** The Spleen triangle (base on the far-left edge, apex pointing right toward center). */
|
|
262
|
+
const SPLEEN_SHAPE: ReadonlyArray<readonly [number, number]> = [
|
|
263
|
+
[18.4, 68.0],
|
|
264
|
+
[18.4, 81.8],
|
|
265
|
+
[34.7, 74.9],
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Center shapes in canonical orientation and color, labels anchored in the
|
|
270
|
+
* margins. Central-column centers are built centered on {@link AXIS}; the Solar
|
|
271
|
+
* Plexus shape is the Spleen reflected across the axis, so the side centers stay
|
|
272
|
+
* exact mirrors. The Heart is the deliberate off-axis exception.
|
|
273
|
+
*/
|
|
274
|
+
export const CENTER_GEOMETRY: readonly CenterGeometry[] = [
|
|
275
|
+
{
|
|
276
|
+
id: 'head',
|
|
277
|
+
label: 'Head',
|
|
278
|
+
color: 'gold',
|
|
279
|
+
points: shape([
|
|
280
|
+
[40.0, 14.3],
|
|
281
|
+
[60.0, 14.3],
|
|
282
|
+
[50.0, 6.0],
|
|
283
|
+
]),
|
|
284
|
+
labelAnchor: g(63, 9),
|
|
285
|
+
labelAlign: 'start',
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: 'ajna',
|
|
289
|
+
label: 'Ajna',
|
|
290
|
+
color: 'green',
|
|
291
|
+
points: shape([
|
|
292
|
+
[40.0, 15.6],
|
|
293
|
+
[60.0, 15.6],
|
|
294
|
+
[50.0, 27.6],
|
|
295
|
+
]),
|
|
296
|
+
labelAnchor: g(62, 21),
|
|
297
|
+
labelAlign: 'start',
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
id: 'throat',
|
|
301
|
+
label: 'Throat',
|
|
302
|
+
color: 'brown',
|
|
303
|
+
points: squareShape(9.5, 30.4, 43.6),
|
|
304
|
+
labelAnchor: g(83, 34),
|
|
305
|
+
labelAlign: 'start',
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: 'g',
|
|
309
|
+
label: 'G',
|
|
310
|
+
color: 'gold',
|
|
311
|
+
points: shape([
|
|
312
|
+
[50.0, 45.0],
|
|
313
|
+
[60.7, 53.3],
|
|
314
|
+
[50.0, 61.6],
|
|
315
|
+
[39.3, 53.3],
|
|
316
|
+
]),
|
|
317
|
+
labelAnchor: g(13, 51),
|
|
318
|
+
labelAlign: 'end',
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: 'heart',
|
|
322
|
+
label: 'Heart',
|
|
323
|
+
color: 'red',
|
|
324
|
+
points: shape([
|
|
325
|
+
[61.5, 57.0],
|
|
326
|
+
[76.5, 61.3],
|
|
327
|
+
[61.5, 65.6],
|
|
328
|
+
]),
|
|
329
|
+
labelAnchor: g(85, 56),
|
|
330
|
+
labelAlign: 'start',
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: 'spleen',
|
|
334
|
+
label: 'Spleen',
|
|
335
|
+
color: 'brown',
|
|
336
|
+
points: shape(SPLEEN_SHAPE),
|
|
337
|
+
labelAnchor: g(13, 70),
|
|
338
|
+
labelAlign: 'end',
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: 'sacral',
|
|
342
|
+
label: 'Sacral',
|
|
343
|
+
color: 'red',
|
|
344
|
+
points: squareShape(9.5, 70.6, 83.6),
|
|
345
|
+
// Lower-right, below the Solar Plexus: a left-side leader would clip the
|
|
346
|
+
// Spleen, which sits at the same height as the Sacral.
|
|
347
|
+
labelAnchor: g(85, 88),
|
|
348
|
+
labelAlign: 'start',
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
id: 'solar-plexus',
|
|
352
|
+
label: 'Solar Plexus',
|
|
353
|
+
color: 'brown',
|
|
354
|
+
points: shape(
|
|
355
|
+
SPLEEN_SHAPE.map(([x, y]) => [mirrorX(x), y] as [number, number]),
|
|
356
|
+
),
|
|
357
|
+
labelAnchor: g(87, 73),
|
|
358
|
+
labelAlign: 'start',
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: 'root',
|
|
362
|
+
label: 'Root',
|
|
363
|
+
color: 'brown',
|
|
364
|
+
points: squareShape(9.5, 87.9, 99.9),
|
|
365
|
+
labelAnchor: g(50, 103),
|
|
366
|
+
labelAlign: 'middle',
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* The 36 channels as ordered gate pairs. This is the canonical Human Design
|
|
372
|
+
* channel set; a channel is active only when both of its gates are activated,
|
|
373
|
+
* which the live response reports in its `channels` array. The static list lets
|
|
374
|
+
* the renderer draw every channel as a hanging (inactive) line and overlay the
|
|
375
|
+
* active ones, so an open bodygraph still shows its full wiring skeleton.
|
|
376
|
+
*/
|
|
377
|
+
export const CHANNEL_PAIRS: ReadonlyArray<readonly [number, number]> = [
|
|
378
|
+
[64, 47],
|
|
379
|
+
[61, 24],
|
|
380
|
+
[63, 4],
|
|
381
|
+
[17, 62],
|
|
382
|
+
[11, 56],
|
|
383
|
+
[43, 23],
|
|
384
|
+
[16, 48],
|
|
385
|
+
[20, 34],
|
|
386
|
+
[20, 10],
|
|
387
|
+
[7, 31],
|
|
388
|
+
[1, 8],
|
|
389
|
+
[13, 33],
|
|
390
|
+
[21, 45],
|
|
391
|
+
[12, 22],
|
|
392
|
+
[35, 36],
|
|
393
|
+
[57, 20],
|
|
394
|
+
[15, 5],
|
|
395
|
+
[2, 14],
|
|
396
|
+
[46, 29],
|
|
397
|
+
[34, 10],
|
|
398
|
+
[10, 57],
|
|
399
|
+
[25, 51],
|
|
400
|
+
[27, 50],
|
|
401
|
+
[57, 34],
|
|
402
|
+
[26, 44],
|
|
403
|
+
[18, 58],
|
|
404
|
+
[28, 38],
|
|
405
|
+
[32, 54],
|
|
406
|
+
[3, 60],
|
|
407
|
+
[9, 52],
|
|
408
|
+
[42, 53],
|
|
409
|
+
[59, 6],
|
|
410
|
+
[19, 49],
|
|
411
|
+
[39, 55],
|
|
412
|
+
[41, 30],
|
|
413
|
+
[37, 40],
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Front-facing standing figure behind the chart, mirror-symmetric about
|
|
418
|
+
* {@link AXIS}. Authored in the same normalized grid as the centers, so it scales
|
|
419
|
+
* with the chart and frames it as in the canonical "nine centers on the human
|
|
420
|
+
* body" diagram: a rounded head holding the Head center, a short neck at the
|
|
421
|
+
* Throat, shoulders that slope to arms hanging down the outside of the torso so
|
|
422
|
+
* their span frames the Spleen (left) and Solar Plexus (right), a torso that
|
|
423
|
+
* narrows at the waist below the chest, then hips ending at the pelvis just below
|
|
424
|
+
* the Root center. The outline is one closed loop: crown, temple, jaw, neck,
|
|
425
|
+
* shoulder, down the outer arm to the hand beside the hip, in under the hip to
|
|
426
|
+
* the pelvis hem, mirrored back up the left side. The right half is built from
|
|
427
|
+
* cubic beziers and reflected so the figure is exactly symmetric. Drawn first and
|
|
428
|
+
* behind everything so the centers, wiring, and gate numbers stay legible on top.
|
|
429
|
+
*/
|
|
430
|
+
const BODY_PATH = buildBodyPath();
|
|
431
|
+
|
|
432
|
+
function buildBodyPath(): string {
|
|
433
|
+
const m = mirrorX;
|
|
434
|
+
// Right-side outline in grid units (x, y) from the crown down to the
|
|
435
|
+
// pelvis-right corner: a start point, then triples of (ctrl1, ctrl2, end). A
|
|
436
|
+
// rounded head hugging the Head and Ajna centers, a narrowed neck, shoulders at
|
|
437
|
+
// their widest, the outer torso running just past the Spleen and Solar Plexus
|
|
438
|
+
// (right edges near x 82 at y 74), then waist and hip to a flat pelvis hem. The
|
|
439
|
+
// left side is the mirror and the hem is a straight line, so the figure reads as
|
|
440
|
+
// a torso, not a point.
|
|
441
|
+
const r: Array<[number, number]> = [
|
|
442
|
+
[50, -2], // crown apex (start)
|
|
443
|
+
[60, -2], // crown round (ctrl)
|
|
444
|
+
[60.5, 9], // head side (ctrl)
|
|
445
|
+
[57, 18], // brow, head holds Head + Ajna (end)
|
|
446
|
+
[56, 21], // cheek (ctrl)
|
|
447
|
+
[54, 24], // jaw (ctrl)
|
|
448
|
+
[52, 27], // neck right, narrowed (end)
|
|
449
|
+
[54, 28], // neck base (ctrl)
|
|
450
|
+
[64, 30], // trapezius slope (ctrl)
|
|
451
|
+
[80, 34], // shoulder / deltoid, the widest point (end)
|
|
452
|
+
[83.5, 40], // upper torso side (ctrl)
|
|
453
|
+
[84, 56], // outer torso, frames the side centers (ctrl)
|
|
454
|
+
[83, 74], // torso side past Spleen / Solar Plexus (end)
|
|
455
|
+
[82, 84], // waist (ctrl)
|
|
456
|
+
[76, 92], // hip (ctrl)
|
|
457
|
+
[68, 97], // hip (end)
|
|
458
|
+
[64, 99], // toward the pelvis (ctrl)
|
|
459
|
+
[62, 100], // pelvis (ctrl)
|
|
460
|
+
[60, 100], // pelvis-right corner (end)
|
|
461
|
+
];
|
|
462
|
+
const segs: string[] = [`M ${pt(r[0])}`];
|
|
463
|
+
// Walk the right side as cubic beziers, three points per segment.
|
|
464
|
+
for (let i = 1; i + 2 < r.length; i += 3) {
|
|
465
|
+
segs.push(`C ${pt(r[i])} ${pt(r[i + 1])} ${pt(r[i + 2])}`);
|
|
466
|
+
}
|
|
467
|
+
// Flat pelvis hem across to the mirrored corner.
|
|
468
|
+
segs.push(`L ${ptm(r[r.length - 1], m)}`);
|
|
469
|
+
// Mirror the right walk back up the left side to the crown.
|
|
470
|
+
for (let i = r.length - 3; i >= 1; i -= 3) {
|
|
471
|
+
segs.push(`C ${ptm(r[i + 1], m)} ${ptm(r[i], m)} ${ptm(r[i - 1], m)}`);
|
|
472
|
+
}
|
|
473
|
+
segs.push('Z');
|
|
474
|
+
return segs.join(' ');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function pt([x, y]: [number, number]): string {
|
|
478
|
+
const p = g(x, y);
|
|
479
|
+
return `${p.x} ${p.y}`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function ptm([x, y]: [number, number], m: (x: number) => number): string {
|
|
483
|
+
const p = g(m(x), y);
|
|
484
|
+
return `${p.x} ${p.y}`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function polygonPoints(pts: Point[]): string {
|
|
488
|
+
return pts.map((p) => `${p.x},${p.y}`).join(' ');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function pairKey(a: number, b: number): string {
|
|
492
|
+
return a < b ? `${a}-${b}` : `${b}-${a}`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Render the body silhouette behind the chart. */
|
|
496
|
+
function renderBody(): TemplateResult {
|
|
497
|
+
return svg`<path class="bg-body" d=${BODY_PATH} />`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Render every channel as a single line joining its two gates, so the wiring
|
|
502
|
+
* always reads as a connected diagram rather than stubs poking out of centers.
|
|
503
|
+
* Each of the 36 channels draws a faint full-length connector; a channel with
|
|
504
|
+
* both gates active is redrawn thick and solid (a defined channel); a channel
|
|
505
|
+
* with only one gate active lights that gate's half toward the midpoint over the
|
|
506
|
+
* connector (a hanging gate). This mirrors how a printed bodygraph colors a full
|
|
507
|
+
* channel only when both gates are active and shows a single hanging gate
|
|
508
|
+
* otherwise, while keeping every connection visible.
|
|
509
|
+
*/
|
|
510
|
+
function renderChannels(
|
|
511
|
+
activeChannels: Set<string>,
|
|
512
|
+
activeGates: Set<number>,
|
|
513
|
+
): TemplateResult[] {
|
|
514
|
+
const lines: TemplateResult[] = [];
|
|
515
|
+
for (const [a, b] of CHANNEL_PAIRS) {
|
|
516
|
+
const pa = GATE_POINTS[a];
|
|
517
|
+
const pb = GATE_POINTS[b];
|
|
518
|
+
if (!pa || !pb) continue;
|
|
519
|
+
lines.push(
|
|
520
|
+
svg`<line class="bg-channel" x1=${pa.x} y1=${pa.y} x2=${pb.x} y2=${pb.y} />`,
|
|
521
|
+
);
|
|
522
|
+
if (activeChannels.has(pairKey(a, b))) {
|
|
523
|
+
lines.push(
|
|
524
|
+
svg`<line class="bg-channel on" x1=${pa.x} y1=${pa.y} x2=${pb.x} y2=${pb.y} />`,
|
|
525
|
+
);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const mid = { x: (pa.x + pb.x) / 2, y: (pa.y + pb.y) / 2 };
|
|
529
|
+
if (activeGates.has(a)) {
|
|
530
|
+
lines.push(
|
|
531
|
+
svg`<line class="bg-half" x1=${pa.x} y1=${pa.y} x2=${mid.x} y2=${mid.y} />`,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
if (activeGates.has(b)) {
|
|
535
|
+
lines.push(
|
|
536
|
+
svg`<line class="bg-half" x1=${pb.x} y1=${pb.y} x2=${mid.x} y2=${mid.y} />`,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return lines;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Closest point to `p` on segment `a`-`b`, clamped to the segment ends. */
|
|
544
|
+
function closestPointOnSegment(p: Point, a: Point, b: Point): Point {
|
|
545
|
+
const dx = b.x - a.x;
|
|
546
|
+
const dy = b.y - a.y;
|
|
547
|
+
const len2 = dx * dx + dy * dy;
|
|
548
|
+
if (len2 === 0) return a;
|
|
549
|
+
const t = Math.max(
|
|
550
|
+
0,
|
|
551
|
+
Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2),
|
|
552
|
+
);
|
|
553
|
+
return { x: a.x + t * dx, y: a.y + t * dy };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Closest point on a closed polygon's perimeter to `p`, where a label leader should land. */
|
|
557
|
+
function closestPointOnPolygon(p: Point, poly: readonly Point[]): Point {
|
|
558
|
+
let best = poly[0];
|
|
559
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
560
|
+
for (let i = 0; i < poly.length; i++) {
|
|
561
|
+
const q = closestPointOnSegment(p, poly[i], poly[(i + 1) % poly.length]);
|
|
562
|
+
const d = (q.x - p.x) ** 2 + (q.y - p.y) ** 2;
|
|
563
|
+
if (d < bestDist) {
|
|
564
|
+
bestDist = d;
|
|
565
|
+
best = q;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return best;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Render the nine center shapes, filled with their semantic color when defined
|
|
573
|
+
* and outlined when open, each with a margin label tied to its shape by a thin
|
|
574
|
+
* leader line so the Heart and every other center is unambiguous regardless of
|
|
575
|
+
* whether it is defined.
|
|
576
|
+
*/
|
|
577
|
+
function renderCenters(defined: Set<BodygraphCenterId>): TemplateResult[] {
|
|
578
|
+
return CENTER_GEOMETRY.map((c) => {
|
|
579
|
+
const isDefined = defined.has(c.id);
|
|
580
|
+
const cls = `bg-center bg-${c.color}${isDefined ? ' defined' : ''}`;
|
|
581
|
+
const edge = closestPointOnPolygon(c.labelAnchor, c.points);
|
|
582
|
+
return svg`<g>
|
|
583
|
+
<line class="bg-leader" x1=${c.labelAnchor.x} y1=${c.labelAnchor.y} x2=${edge.x} y2=${edge.y} />
|
|
584
|
+
<polygon class=${cls} points=${polygonPoints(c.points)}><title>${c.label}: ${isDefined ? 'defined' : 'open'}</title></polygon>
|
|
585
|
+
<text class="bg-center-label" x=${c.labelAnchor.x} y=${c.labelAnchor.y} text-anchor=${c.labelAlign} dominant-baseline="central">${c.label}</text>
|
|
586
|
+
</g>`;
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Render the activated gate numbers at their fixed points. Numbers sit on top of
|
|
592
|
+
* the filled centers, so a halo (a wider, background-colored stroke under the
|
|
593
|
+
* fill) keeps them legible against any center color in both themes.
|
|
594
|
+
*/
|
|
595
|
+
function renderGateNumbers(
|
|
596
|
+
activeGates: Set<number>,
|
|
597
|
+
titles: Map<number, string>,
|
|
598
|
+
): TemplateResult[] {
|
|
599
|
+
const out: TemplateResult[] = [];
|
|
600
|
+
for (const [gate, p] of Object.entries(GATE_POINTS)) {
|
|
601
|
+
const num = Number(gate);
|
|
602
|
+
if (!activeGates.has(num)) continue;
|
|
603
|
+
const title = titles.get(num);
|
|
604
|
+
out.push(
|
|
605
|
+
svg`<text class="bg-gate" x=${p.x} y=${p.y} text-anchor="middle" dominant-baseline="central">${num}${title ? svg`<title>${title}</title>` : nothing}</text>`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
return out;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export interface BodygraphRenderInput {
|
|
612
|
+
definedCenters: Set<BodygraphCenterId>;
|
|
613
|
+
activeChannels: Set<string>;
|
|
614
|
+
activeGates: Set<number>;
|
|
615
|
+
gateTitles: Map<number, string>;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Build the lookup key for an active channel from its two gate numbers. */
|
|
619
|
+
export function channelKey(a: number, b: number): string {
|
|
620
|
+
return pairKey(a, b);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export const BODYGRAPH_VIEWBOX = `0 0 ${VIEW_W} ${VIEW_H}`;
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Render the full bodygraph SVG inner content for the given live state. The
|
|
627
|
+
* caller wraps it in an `<svg>` with {@link BODYGRAPH_VIEWBOX} and applies its
|
|
628
|
+
* own theming CSS. Draw order: body silhouette under channels under centers
|
|
629
|
+
* under gate numbers, so the body is the backdrop, the wiring sits behind the
|
|
630
|
+
* shapes, and the numbers stay legible on top.
|
|
631
|
+
*/
|
|
632
|
+
export function renderBodygraphSvg(
|
|
633
|
+
input: BodygraphRenderInput,
|
|
634
|
+
): TemplateResult {
|
|
635
|
+
return svg`
|
|
636
|
+
${renderBody()}
|
|
637
|
+
${renderChannels(input.activeChannels, input.activeGates)}
|
|
638
|
+
${renderCenters(input.definedCenters)}
|
|
639
|
+
${renderGateNumbers(input.activeGates, input.gateTitles)}
|
|
640
|
+
`;
|
|
641
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/sync-version.ts. Do not edit.
|
|
2
|
-
export const ROXY_UI_VERSION = '0.
|
|
2
|
+
export const ROXY_UI_VERSION = '0.8.0';
|