@motionscript/geo 0.0.0-stage → 0.1.0-alpha.3

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.
Files changed (108) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +201 -0
  3. package/dist/border/format.d.ts +73 -0
  4. package/dist/border/format.d.ts.map +1 -0
  5. package/dist/border/format.js +68 -0
  6. package/dist/border/format.js.map +1 -0
  7. package/dist/border/geo-border.d.ts +44 -0
  8. package/dist/border/geo-border.d.ts.map +1 -0
  9. package/dist/border/geo-border.js +234 -0
  10. package/dist/border/geo-border.js.map +1 -0
  11. package/dist/border/geometry.d.ts +40 -0
  12. package/dist/border/geometry.d.ts.map +1 -0
  13. package/dist/border/geometry.js +382 -0
  14. package/dist/border/geometry.js.map +1 -0
  15. package/dist/border/index.d.ts +15 -0
  16. package/dist/border/index.d.ts.map +1 -0
  17. package/dist/border/index.js +15 -0
  18. package/dist/border/index.js.map +1 -0
  19. package/dist/border/loader.d.ts +31 -0
  20. package/dist/border/loader.d.ts.map +1 -0
  21. package/dist/border/loader.js +87 -0
  22. package/dist/border/loader.js.map +1 -0
  23. package/dist/border/registry.d.ts +64 -0
  24. package/dist/border/registry.d.ts.map +1 -0
  25. package/dist/border/registry.js +152 -0
  26. package/dist/border/registry.js.map +1 -0
  27. package/dist/border/selection.d.ts +21 -0
  28. package/dist/border/selection.d.ts.map +1 -0
  29. package/dist/border/selection.js +86 -0
  30. package/dist/border/selection.js.map +1 -0
  31. package/dist/border/simplify.d.ts +12 -0
  32. package/dist/border/simplify.d.ts.map +1 -0
  33. package/dist/border/simplify.js +116 -0
  34. package/dist/border/simplify.js.map +1 -0
  35. package/dist/browser/index.js +25 -0
  36. package/dist/browser/index.js.map +7 -0
  37. package/dist/browser/manifest.json +11 -0
  38. package/dist/globe/atmosphere.d.ts +38 -0
  39. package/dist/globe/atmosphere.d.ts.map +1 -0
  40. package/dist/globe/atmosphere.js +91 -0
  41. package/dist/globe/atmosphere.js.map +1 -0
  42. package/dist/globe/borders.d.ts +39 -0
  43. package/dist/globe/borders.d.ts.map +1 -0
  44. package/dist/globe/borders.js +116 -0
  45. package/dist/globe/borders.js.map +1 -0
  46. package/dist/globe/data/ne-110m.d.ts +25 -0
  47. package/dist/globe/data/ne-110m.d.ts.map +1 -0
  48. package/dist/globe/data/ne-110m.js +198 -0
  49. package/dist/globe/data/ne-110m.js.map +1 -0
  50. package/dist/globe/globe-places.d.ts +140 -0
  51. package/dist/globe/globe-places.d.ts.map +1 -0
  52. package/dist/globe/globe-places.js +262 -0
  53. package/dist/globe/globe-places.js.map +1 -0
  54. package/dist/globe/globe.d.ts +193 -0
  55. package/dist/globe/globe.d.ts.map +1 -0
  56. package/dist/globe/globe.js +435 -0
  57. package/dist/globe/globe.js.map +1 -0
  58. package/dist/globe/index.d.ts +17 -0
  59. package/dist/globe/index.d.ts.map +1 -0
  60. package/dist/globe/index.js +17 -0
  61. package/dist/globe/index.js.map +1 -0
  62. package/dist/globe/places.d.ts +69 -0
  63. package/dist/globe/places.d.ts.map +1 -0
  64. package/dist/globe/places.js +94 -0
  65. package/dist/globe/places.js.map +1 -0
  66. package/dist/globe/projection.d.ts +182 -0
  67. package/dist/globe/projection.d.ts.map +1 -0
  68. package/dist/globe/projection.js +215 -0
  69. package/dist/globe/projection.js.map +1 -0
  70. package/dist/globe/world-map.d.ts +83 -0
  71. package/dist/globe/world-map.d.ts.map +1 -0
  72. package/dist/globe/world-map.js +169 -0
  73. package/dist/globe/world-map.js.map +1 -0
  74. package/dist/globe/world.d.ts +46 -0
  75. package/dist/globe/world.d.ts.map +1 -0
  76. package/dist/globe/world.js +71 -0
  77. package/dist/globe/world.js.map +1 -0
  78. package/dist/index.d.ts +5 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +6 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/nodes.d.ts +19 -0
  83. package/dist/nodes.d.ts.map +1 -0
  84. package/dist/nodes.js +19 -0
  85. package/dist/nodes.js.map +1 -0
  86. package/package.json +69 -3
  87. package/registry.json +31 -0
  88. package/src/border/format.ts +147 -0
  89. package/src/border/geo-border.ts +260 -0
  90. package/src/border/geometry.ts +463 -0
  91. package/src/border/index.ts +14 -0
  92. package/src/border/loader.ts +97 -0
  93. package/src/border/registry.ts +191 -0
  94. package/src/border/selection.ts +95 -0
  95. package/src/border/simplify.ts +110 -0
  96. package/src/globe/atmosphere.ts +98 -0
  97. package/src/globe/borders.ts +166 -0
  98. package/src/globe/data/ne-110m.ts +214 -0
  99. package/src/globe/globe-places.ts +352 -0
  100. package/src/globe/globe.ts +552 -0
  101. package/src/globe/index.ts +16 -0
  102. package/src/globe/places.ts +132 -0
  103. package/src/globe/projection.ts +261 -0
  104. package/src/globe/world-map.ts +227 -0
  105. package/src/globe/world.ts +111 -0
  106. package/src/index.ts +5 -0
  107. package/src/nodes.ts +19 -0
  108. package/README.md +0 -4
@@ -0,0 +1,352 @@
1
+ /**
2
+ * What an author places on a Globe — the marked points, and the arcs between
3
+ * them — as the panel edits them and the stored field holds them.
4
+ *
5
+ * Beside `graph-equations.ts` and shaped like it, because it is the same
6
+ * problem: an appearance value is `number | string | boolean`, so a *list* has
7
+ * to arrive as one string that a mapper reads. What lives here is the authored
8
+ * shape and its serialization, shared by the inspector's tiles and the node's
9
+ * builder; what a frame actually draws is `nodes/globe/impl/places.ts`, which
10
+ * resolves this into coordinates and clamps it to what the render can use.
11
+ *
12
+ * The split matters in one direction: **reading must be faithful and must not
13
+ * mint identity**. The panel reads on every render and the builder on every
14
+ * rebuild, so a random id here would be a different id every time — and two
15
+ * things key off it (a tile's React key, and which arc endpoint points at which
16
+ * marker). {@link createMarker} mints ids; the readers derive a positional one
17
+ * and never invent.
18
+ */
19
+
20
+ /**
21
+ * A marked point on the surface.
22
+ *
23
+ * `label` is **not drawn**. It names the tile in the inspector and the entries
24
+ * in an arc's endpoint menus, which is the whole of its job today — a label
25
+ * rendered against the globe is a separate problem (it has to face the camera,
26
+ * hide when it rotates behind the planet, and not collide with its neighbours),
27
+ * and storing the text now is what lets that arrive without a migration.
28
+ */
29
+ export interface GlobeMarker {
30
+ /** Identity across list edits — a tween matches like with like. */
31
+ id: string
32
+ /** A name for this place. Editor-facing only; see the note above. */
33
+ label: string
34
+ /** Degrees north, −90…90. */
35
+ lat: number
36
+ /** Degrees east, −180…180. */
37
+ lon: number
38
+ /** Any colour string the fill parser takes. Empty means the palette entry. */
39
+ color: string
40
+ /** Radius as a fraction of the globe's own. */
41
+ size: number
42
+ /** 0–1. A fade, unlike {@link enabled}, which is a switch. */
43
+ opacity: number
44
+ /** Whether it draws. Off keeps the tile in place, editable, with its colour. */
45
+ enabled: boolean
46
+ }
47
+
48
+ /**
49
+ * One end of an arc: a marker it is pinned to, or a place of its own.
50
+ *
51
+ * The pinned form is the one the tile offers first, and it is not merely
52
+ * convenient — an arc between two *marked* places moves when the place does,
53
+ * where a copied pair of coordinates silently stops matching the dot it was
54
+ * drawn to. That is the same reasoning a state machine's connection names its
55
+ * ends by state id rather than by position.
56
+ *
57
+ * The free form stays because not every end wants a dot on it, and because a
58
+ * list written by hand or by an agent reaches for a coordinate pair first.
59
+ */
60
+ export type ArcEnd =
61
+ | { readonly kind: "marker"; readonly markerId: string }
62
+ | { readonly kind: "place"; readonly lat: number; readonly lon: number }
63
+
64
+ /** A great-circle path between two ends. */
65
+ export interface GlobeArc {
66
+ id: string
67
+ from: ArcEnd
68
+ to: ArcEnd
69
+ color: string
70
+ /** Tube radius as a fraction of the globe's own. */
71
+ width: number
72
+ /** How far the middle lifts off the surface, as a fraction of the radius. */
73
+ lift: number
74
+ /**
75
+ * How much of the path is drawn, 0–1 — a line that draws itself on.
76
+ *
77
+ * A fraction of the *path* rather than a time, so it means the same thing
78
+ * whatever the arc's length and is the author's to tween from wherever they
79
+ * like. Zero draws nothing and is not an error.
80
+ */
81
+ progress: number
82
+ enabled: boolean
83
+ }
84
+
85
+ /**
86
+ * The palette an uncoloured marker takes its colour from, by position.
87
+ *
88
+ * Warm-to-cool rather than a spectrum: markers are read against a dark ocean and
89
+ * usually mean something ranked — the first is the subject and the rest are
90
+ * context — so the order runs from the one that carries furthest to the one that
91
+ * recedes. Six, then it wraps; a seventh place that has to stand out sets its
92
+ * own colour.
93
+ */
94
+ export const DEFAULT_MARKER_COLORS = [
95
+ "#ff3b30",
96
+ "#ff9f0a",
97
+ "#ffd60a",
98
+ "#30d158",
99
+ "#5ac8fa",
100
+ "#bf5af2",
101
+ ] as const
102
+
103
+ /** The colour a marker at `index` is drawn in when it names none. */
104
+ export function markerColorAt(index: number): string {
105
+ const palette = DEFAULT_MARKER_COLORS
106
+ return palette[((index % palette.length) + palette.length) % palette.length]!
107
+ }
108
+
109
+ /** The colour an arc falls back to. */
110
+ export const DEFAULT_ARC_COLOR = "#ffd60a"
111
+
112
+ const DEFAULT_MARKER_SIZE = 0.02
113
+ const DEFAULT_ARC_WIDTH = 0.004
114
+ const DEFAULT_ARC_LIFT = 0.25
115
+
116
+ /**
117
+ * A fresh id for a tile.
118
+ *
119
+ * A counter plus a short random suffix rather than `crypto.randomUUID`, exactly
120
+ * as `graph-equations.ts` explains: this package loads in a bare Node process
121
+ * with no DOM and no assumed globals, and an id only has to be unique within one
122
+ * node's list. The suffix is what stops two clients editing the same scene
123
+ * colliding on `mk-3`.
124
+ *
125
+ * Never called during a build — only when an author adds a tile.
126
+ */
127
+ let placeSeq = 0
128
+ function nextId(prefix: string): string {
129
+ placeSeq += 1
130
+ return `${prefix}-${placeSeq}-${Math.random().toString(36).slice(2, 8)}`
131
+ }
132
+
133
+ /** A new marker tile: showing, opaque, and at the given place. */
134
+ export function createMarker(
135
+ at: { lat: number; lon: number } = { lat: 0, lon: 0 }
136
+ ): GlobeMarker {
137
+ return {
138
+ id: nextId("mk"),
139
+ label: "",
140
+ lat: at.lat,
141
+ lon: at.lon,
142
+ color: "",
143
+ size: DEFAULT_MARKER_SIZE,
144
+ opacity: 1,
145
+ enabled: true,
146
+ }
147
+ }
148
+
149
+ /** A new arc tile, between two ends. */
150
+ export function createArc(from: ArcEnd, to: ArcEnd): GlobeArc {
151
+ return {
152
+ id: nextId("arc"),
153
+ from,
154
+ to,
155
+ color: DEFAULT_ARC_COLOR,
156
+ width: DEFAULT_ARC_WIDTH,
157
+ lift: DEFAULT_ARC_LIFT,
158
+ progress: 1,
159
+ enabled: true,
160
+ }
161
+ }
162
+
163
+ /** An end pinned to a marker. */
164
+ export function markerEnd(markerId: string): ArcEnd {
165
+ return { kind: "marker", markerId }
166
+ }
167
+
168
+ /** An end at a place of its own. */
169
+ export function placeEnd(lat: number, lon: number): ArcEnd {
170
+ return { kind: "place", lat, lon }
171
+ }
172
+
173
+ // --- Reading ---------------------------------------------------------------
174
+
175
+ /**
176
+ * Reads a stored marker list, defensively.
177
+ *
178
+ * Never throws, and drops only what it genuinely cannot place: the field is
179
+ * edited a keystroke at a time by hand as well as through the tiles, so it is
180
+ * invalid far more often than it is valid, and a reader that threw would blank
181
+ * the canvas on the way from `[` to `[{`.
182
+ *
183
+ * A row with **no position at all** is the one thing that is dropped rather than
184
+ * defaulted. Every other field has an honest default; a latitude does not, and
185
+ * putting an unplaced marker on the equator would draw a dot somewhere nobody
186
+ * asked for and call it the author's.
187
+ */
188
+ export function markersOf(value: unknown): GlobeMarker[] {
189
+ return rowsOf(value).flatMap((row, index) => {
190
+ const lat = finite(row.lat ?? row.latitude)
191
+ const lon = finite(row.lon ?? row.lng ?? row.longitude)
192
+ if (lat === null || lon === null) return []
193
+ return [
194
+ {
195
+ id: idOf(row.id, `mk:${index}`),
196
+ label: text(row.label ?? row.name) ?? "",
197
+ lat: clamp(lat, -90, 90),
198
+ lon: wrapLongitude(lon),
199
+ color: text(row.color) ?? "",
200
+ size: positive(row.size, DEFAULT_MARKER_SIZE),
201
+ opacity: unit(row.opacity),
202
+ // Absent means on, as everywhere else a switch is stored: only an
203
+ // explicit `false` turns a tile off.
204
+ enabled: row.enabled !== false,
205
+ },
206
+ ]
207
+ })
208
+ }
209
+
210
+ /** Reads a stored arc list, on the same terms as {@link markersOf}. */
211
+ export function arcsOf(value: unknown): GlobeArc[] {
212
+ return rowsOf(value).flatMap((row, index) => {
213
+ const from = endOf(row.from)
214
+ const to = endOf(row.to)
215
+ if (!from || !to) return []
216
+ return [
217
+ {
218
+ id: idOf(row.id, `arc:${index}`),
219
+ from,
220
+ to,
221
+ color: text(row.color) ?? DEFAULT_ARC_COLOR,
222
+ width: positive(row.width, DEFAULT_ARC_WIDTH),
223
+ lift: clamp(finite(row.lift) ?? DEFAULT_ARC_LIFT, 0, 4),
224
+ progress: unit(row.progress),
225
+ enabled: row.enabled !== false,
226
+ },
227
+ ]
228
+ })
229
+ }
230
+
231
+ /** Serializes a marker list into the stored string form. */
232
+ export function markersToValue(markers: readonly GlobeMarker[]): string {
233
+ return JSON.stringify(markers)
234
+ }
235
+
236
+ /**
237
+ * Serializes an arc list, writing each end back in the shape it was authored in.
238
+ *
239
+ * A pinned end is a bare **string** and a free one a `[lat, lon]` **pair**,
240
+ * rather than the tagged union this module works in. Two reasons, and the second
241
+ * is the load-bearing one: those are the two shapes somebody writing this list
242
+ * by hand reaches for, and a stored document is read by whatever version opens
243
+ * it later — so the wire form is public API and a discriminant that only exists
244
+ * to make the TypeScript convenient does not belong in it.
245
+ */
246
+ export function arcsToValue(arcs: readonly GlobeArc[]): string {
247
+ return JSON.stringify(
248
+ arcs.map((arc) => ({ ...arc, from: endToValue(arc.from), to: endToValue(arc.to) }))
249
+ )
250
+ }
251
+
252
+ function endToValue(end: ArcEnd): string | [number, number] {
253
+ return end.kind === "marker" ? end.markerId : [end.lat, end.lon]
254
+ }
255
+
256
+ /**
257
+ * One end, from any of the three shapes a stored list holds it in.
258
+ *
259
+ * A **string** is a marker id; a **pair** is `[lat, lon]`, which is how a
260
+ * coordinate is written everywhere outside a program; an **object** with `lat`
261
+ * and `lon` is the long form. Refusing any of them would make the field's most
262
+ * obvious content its most surprising failure.
263
+ */
264
+ function endOf(value: unknown): ArcEnd | null {
265
+ if (typeof value === "string") {
266
+ return value.trim() === "" ? null : markerEnd(value)
267
+ }
268
+ if (Array.isArray(value)) {
269
+ const lat = finite(value[0])
270
+ const lon = finite(value[1])
271
+ return lat === null || lon === null
272
+ ? null
273
+ : placeEnd(clamp(lat, -90, 90), wrapLongitude(lon))
274
+ }
275
+ if (typeof value !== "object" || value === null) return null
276
+ const row = value as Record<string, unknown>
277
+ if (typeof row.markerId === "string") return markerEnd(row.markerId)
278
+ const lat = finite(row.lat ?? row.latitude)
279
+ const lon = finite(row.lon ?? row.lng ?? row.longitude)
280
+ return lat === null || lon === null
281
+ ? null
282
+ : placeEnd(clamp(lat, -90, 90), wrapLongitude(lon))
283
+ }
284
+
285
+ // --- Reading helpers -------------------------------------------------------
286
+
287
+ type Row = Record<string, unknown>
288
+
289
+ function rowsOf(value: unknown): Row[] {
290
+ if (typeof value !== "string" || value.trim() === "") return []
291
+ try {
292
+ const parsed: unknown = JSON.parse(value)
293
+ if (!Array.isArray(parsed)) return []
294
+ return parsed.filter(
295
+ (row): row is Row => typeof row === "object" && row !== null
296
+ )
297
+ } catch {
298
+ return []
299
+ }
300
+ }
301
+
302
+ /**
303
+ * A row's identity, or one derived from where it sits.
304
+ *
305
+ * Positional rather than random, and the difference is the whole point: a random
306
+ * id here would be a different one on every read, and both the tile's React key
307
+ * and an arc's pin to a marker would change under the author's hands.
308
+ */
309
+ function idOf(value: unknown, positional: string): string {
310
+ return text(value) ?? positional
311
+ }
312
+
313
+ function text(value: unknown): string | null {
314
+ return typeof value === "string" && value.trim() !== "" ? value : null
315
+ }
316
+
317
+ function finite(value: unknown): number | null {
318
+ return typeof value === "number" && Number.isFinite(value) ? value : null
319
+ }
320
+
321
+ /** A stored 0–1 value; anything unreadable is fully on. */
322
+ function unit(value: unknown): number {
323
+ const n = finite(value)
324
+ return n === null ? 1 : clamp(n, 0, 1)
325
+ }
326
+
327
+ /** A stored size, which must be above zero to draw at all. */
328
+ function positive(value: unknown, fallback: number): number {
329
+ const n = finite(value)
330
+ return n === null || n <= 0 ? fallback : n
331
+ }
332
+
333
+ /**
334
+ * A longitude into −180…180.
335
+ *
336
+ * Wrapped rather than clamped, unlike latitude: 190°E is a real place (170°W)
337
+ * and clamping would put it on the antimeridian, where there is nothing north of
338
+ * the north pole for a latitude to mean.
339
+ *
340
+ * A value already in range is returned untouched — the modulo round-trip is not
341
+ * exact in binary floating point (139.7 comes back as 139.70000000000005), and a
342
+ * stored longitude that differs from the one the author typed shows up later as
343
+ * a diff nobody made.
344
+ */
345
+ function wrapLongitude(value: number): number {
346
+ if (value >= -180 && value <= 180) return value
347
+ return ((((value + 180) % 360) + 360) % 360) - 180
348
+ }
349
+
350
+ function clamp(value: number, min: number, max: number): number {
351
+ return value < min ? min : value > max ? max : value
352
+ }