@motionscript/geo 0.0.0-stage → 0.1.0-alpha.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.
Files changed (108) hide show
  1. package/CHANGELOG.md +5 -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 +68 -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,140 @@
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
+ * A marked point on the surface.
21
+ *
22
+ * `label` is **not drawn**. It names the tile in the inspector and the entries
23
+ * in an arc's endpoint menus, which is the whole of its job today — a label
24
+ * rendered against the globe is a separate problem (it has to face the camera,
25
+ * hide when it rotates behind the planet, and not collide with its neighbours),
26
+ * and storing the text now is what lets that arrive without a migration.
27
+ */
28
+ export interface GlobeMarker {
29
+ /** Identity across list edits — a tween matches like with like. */
30
+ id: string;
31
+ /** A name for this place. Editor-facing only; see the note above. */
32
+ label: string;
33
+ /** Degrees north, −90…90. */
34
+ lat: number;
35
+ /** Degrees east, −180…180. */
36
+ lon: number;
37
+ /** Any colour string the fill parser takes. Empty means the palette entry. */
38
+ color: string;
39
+ /** Radius as a fraction of the globe's own. */
40
+ size: number;
41
+ /** 0–1. A fade, unlike {@link enabled}, which is a switch. */
42
+ opacity: number;
43
+ /** Whether it draws. Off keeps the tile in place, editable, with its colour. */
44
+ enabled: boolean;
45
+ }
46
+ /**
47
+ * One end of an arc: a marker it is pinned to, or a place of its own.
48
+ *
49
+ * The pinned form is the one the tile offers first, and it is not merely
50
+ * convenient — an arc between two *marked* places moves when the place does,
51
+ * where a copied pair of coordinates silently stops matching the dot it was
52
+ * drawn to. That is the same reasoning a state machine's connection names its
53
+ * ends by state id rather than by position.
54
+ *
55
+ * The free form stays because not every end wants a dot on it, and because a
56
+ * list written by hand or by an agent reaches for a coordinate pair first.
57
+ */
58
+ export type ArcEnd = {
59
+ readonly kind: "marker";
60
+ readonly markerId: string;
61
+ } | {
62
+ readonly kind: "place";
63
+ readonly lat: number;
64
+ readonly lon: number;
65
+ };
66
+ /** A great-circle path between two ends. */
67
+ export interface GlobeArc {
68
+ id: string;
69
+ from: ArcEnd;
70
+ to: ArcEnd;
71
+ color: string;
72
+ /** Tube radius as a fraction of the globe's own. */
73
+ width: number;
74
+ /** How far the middle lifts off the surface, as a fraction of the radius. */
75
+ lift: number;
76
+ /**
77
+ * How much of the path is drawn, 0–1 — a line that draws itself on.
78
+ *
79
+ * A fraction of the *path* rather than a time, so it means the same thing
80
+ * whatever the arc's length and is the author's to tween from wherever they
81
+ * like. Zero draws nothing and is not an error.
82
+ */
83
+ progress: number;
84
+ enabled: boolean;
85
+ }
86
+ /**
87
+ * The palette an uncoloured marker takes its colour from, by position.
88
+ *
89
+ * Warm-to-cool rather than a spectrum: markers are read against a dark ocean and
90
+ * usually mean something ranked — the first is the subject and the rest are
91
+ * context — so the order runs from the one that carries furthest to the one that
92
+ * recedes. Six, then it wraps; a seventh place that has to stand out sets its
93
+ * own colour.
94
+ */
95
+ export declare const DEFAULT_MARKER_COLORS: readonly ["#ff3b30", "#ff9f0a", "#ffd60a", "#30d158", "#5ac8fa", "#bf5af2"];
96
+ /** The colour a marker at `index` is drawn in when it names none. */
97
+ export declare function markerColorAt(index: number): string;
98
+ /** The colour an arc falls back to. */
99
+ export declare const DEFAULT_ARC_COLOR = "#ffd60a";
100
+ /** A new marker tile: showing, opaque, and at the given place. */
101
+ export declare function createMarker(at?: {
102
+ lat: number;
103
+ lon: number;
104
+ }): GlobeMarker;
105
+ /** A new arc tile, between two ends. */
106
+ export declare function createArc(from: ArcEnd, to: ArcEnd): GlobeArc;
107
+ /** An end pinned to a marker. */
108
+ export declare function markerEnd(markerId: string): ArcEnd;
109
+ /** An end at a place of its own. */
110
+ export declare function placeEnd(lat: number, lon: number): ArcEnd;
111
+ /**
112
+ * Reads a stored marker list, defensively.
113
+ *
114
+ * Never throws, and drops only what it genuinely cannot place: the field is
115
+ * edited a keystroke at a time by hand as well as through the tiles, so it is
116
+ * invalid far more often than it is valid, and a reader that threw would blank
117
+ * the canvas on the way from `[` to `[{`.
118
+ *
119
+ * A row with **no position at all** is the one thing that is dropped rather than
120
+ * defaulted. Every other field has an honest default; a latitude does not, and
121
+ * putting an unplaced marker on the equator would draw a dot somewhere nobody
122
+ * asked for and call it the author's.
123
+ */
124
+ export declare function markersOf(value: unknown): GlobeMarker[];
125
+ /** Reads a stored arc list, on the same terms as {@link markersOf}. */
126
+ export declare function arcsOf(value: unknown): GlobeArc[];
127
+ /** Serializes a marker list into the stored string form. */
128
+ export declare function markersToValue(markers: readonly GlobeMarker[]): string;
129
+ /**
130
+ * Serializes an arc list, writing each end back in the shape it was authored in.
131
+ *
132
+ * A pinned end is a bare **string** and a free one a `[lat, lon]` **pair**,
133
+ * rather than the tagged union this module works in. Two reasons, and the second
134
+ * is the load-bearing one: those are the two shapes somebody writing this list
135
+ * by hand reaches for, and a stored document is read by whatever version opens
136
+ * it later — so the wire form is public API and a discriminant that only exists
137
+ * to make the TypeScript convenient does not belong in it.
138
+ */
139
+ export declare function arcsToValue(arcs: readonly GlobeArc[]): string;
140
+ //# sourceMappingURL=globe-places.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"globe-places.d.ts","sourceRoot":"","sources":["../../src/globe/globe-places.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;;;GAQG;AACH,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,EAAE,EAAE,MAAM,CAAA;IACV,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAA;IACb,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,8BAA8B;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAA;IACb,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAA;IACf,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,MAAM,GACd;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1E,4CAA4C;AAC5C,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;OAMG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,6EAOxB,CAAA;AAEV,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED,uCAAuC;AACvC,eAAO,MAAM,iBAAiB,YAAY,CAAA;AAuB1C,kEAAkE;AAClE,wBAAgB,YAAY,CAC1B,EAAE,GAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAuB,GACpD,WAAW,CAWb;AAED,wCAAwC;AACxC,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,QAAQ,CAW5D;AAED,iCAAiC;AACjC,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,oCAAoC;AACpC,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEzD;AAID;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,EAAE,CAoBvD;AAED,uEAAuE;AACvE,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,EAAE,CAkBjD;AAED,4DAA4D;AAC5D,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,MAAM,CAEtE;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,CAI7D"}
@@ -0,0 +1,262 @@
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
+ * The palette an uncoloured marker takes its colour from, by position.
21
+ *
22
+ * Warm-to-cool rather than a spectrum: markers are read against a dark ocean and
23
+ * usually mean something ranked — the first is the subject and the rest are
24
+ * context — so the order runs from the one that carries furthest to the one that
25
+ * recedes. Six, then it wraps; a seventh place that has to stand out sets its
26
+ * own colour.
27
+ */
28
+ export const DEFAULT_MARKER_COLORS = [
29
+ "#ff3b30",
30
+ "#ff9f0a",
31
+ "#ffd60a",
32
+ "#30d158",
33
+ "#5ac8fa",
34
+ "#bf5af2",
35
+ ];
36
+ /** The colour a marker at `index` is drawn in when it names none. */
37
+ export function markerColorAt(index) {
38
+ const palette = DEFAULT_MARKER_COLORS;
39
+ return palette[((index % palette.length) + palette.length) % palette.length];
40
+ }
41
+ /** The colour an arc falls back to. */
42
+ export const DEFAULT_ARC_COLOR = "#ffd60a";
43
+ const DEFAULT_MARKER_SIZE = 0.02;
44
+ const DEFAULT_ARC_WIDTH = 0.004;
45
+ const DEFAULT_ARC_LIFT = 0.25;
46
+ /**
47
+ * A fresh id for a tile.
48
+ *
49
+ * A counter plus a short random suffix rather than `crypto.randomUUID`, exactly
50
+ * as `graph-equations.ts` explains: this package loads in a bare Node process
51
+ * with no DOM and no assumed globals, and an id only has to be unique within one
52
+ * node's list. The suffix is what stops two clients editing the same scene
53
+ * colliding on `mk-3`.
54
+ *
55
+ * Never called during a build — only when an author adds a tile.
56
+ */
57
+ let placeSeq = 0;
58
+ function nextId(prefix) {
59
+ placeSeq += 1;
60
+ return `${prefix}-${placeSeq}-${Math.random().toString(36).slice(2, 8)}`;
61
+ }
62
+ /** A new marker tile: showing, opaque, and at the given place. */
63
+ export function createMarker(at = { lat: 0, lon: 0 }) {
64
+ return {
65
+ id: nextId("mk"),
66
+ label: "",
67
+ lat: at.lat,
68
+ lon: at.lon,
69
+ color: "",
70
+ size: DEFAULT_MARKER_SIZE,
71
+ opacity: 1,
72
+ enabled: true,
73
+ };
74
+ }
75
+ /** A new arc tile, between two ends. */
76
+ export function createArc(from, to) {
77
+ return {
78
+ id: nextId("arc"),
79
+ from,
80
+ to,
81
+ color: DEFAULT_ARC_COLOR,
82
+ width: DEFAULT_ARC_WIDTH,
83
+ lift: DEFAULT_ARC_LIFT,
84
+ progress: 1,
85
+ enabled: true,
86
+ };
87
+ }
88
+ /** An end pinned to a marker. */
89
+ export function markerEnd(markerId) {
90
+ return { kind: "marker", markerId };
91
+ }
92
+ /** An end at a place of its own. */
93
+ export function placeEnd(lat, lon) {
94
+ return { kind: "place", lat, lon };
95
+ }
96
+ // --- Reading ---------------------------------------------------------------
97
+ /**
98
+ * Reads a stored marker list, defensively.
99
+ *
100
+ * Never throws, and drops only what it genuinely cannot place: the field is
101
+ * edited a keystroke at a time by hand as well as through the tiles, so it is
102
+ * invalid far more often than it is valid, and a reader that threw would blank
103
+ * the canvas on the way from `[` to `[{`.
104
+ *
105
+ * A row with **no position at all** is the one thing that is dropped rather than
106
+ * defaulted. Every other field has an honest default; a latitude does not, and
107
+ * putting an unplaced marker on the equator would draw a dot somewhere nobody
108
+ * asked for and call it the author's.
109
+ */
110
+ export function markersOf(value) {
111
+ return rowsOf(value).flatMap((row, index) => {
112
+ const lat = finite(row.lat ?? row.latitude);
113
+ const lon = finite(row.lon ?? row.lng ?? row.longitude);
114
+ if (lat === null || lon === null)
115
+ return [];
116
+ return [
117
+ {
118
+ id: idOf(row.id, `mk:${index}`),
119
+ label: text(row.label ?? row.name) ?? "",
120
+ lat: clamp(lat, -90, 90),
121
+ lon: wrapLongitude(lon),
122
+ color: text(row.color) ?? "",
123
+ size: positive(row.size, DEFAULT_MARKER_SIZE),
124
+ opacity: unit(row.opacity),
125
+ // Absent means on, as everywhere else a switch is stored: only an
126
+ // explicit `false` turns a tile off.
127
+ enabled: row.enabled !== false,
128
+ },
129
+ ];
130
+ });
131
+ }
132
+ /** Reads a stored arc list, on the same terms as {@link markersOf}. */
133
+ export function arcsOf(value) {
134
+ return rowsOf(value).flatMap((row, index) => {
135
+ const from = endOf(row.from);
136
+ const to = endOf(row.to);
137
+ if (!from || !to)
138
+ return [];
139
+ return [
140
+ {
141
+ id: idOf(row.id, `arc:${index}`),
142
+ from,
143
+ to,
144
+ color: text(row.color) ?? DEFAULT_ARC_COLOR,
145
+ width: positive(row.width, DEFAULT_ARC_WIDTH),
146
+ lift: clamp(finite(row.lift) ?? DEFAULT_ARC_LIFT, 0, 4),
147
+ progress: unit(row.progress),
148
+ enabled: row.enabled !== false,
149
+ },
150
+ ];
151
+ });
152
+ }
153
+ /** Serializes a marker list into the stored string form. */
154
+ export function markersToValue(markers) {
155
+ return JSON.stringify(markers);
156
+ }
157
+ /**
158
+ * Serializes an arc list, writing each end back in the shape it was authored in.
159
+ *
160
+ * A pinned end is a bare **string** and a free one a `[lat, lon]` **pair**,
161
+ * rather than the tagged union this module works in. Two reasons, and the second
162
+ * is the load-bearing one: those are the two shapes somebody writing this list
163
+ * by hand reaches for, and a stored document is read by whatever version opens
164
+ * it later — so the wire form is public API and a discriminant that only exists
165
+ * to make the TypeScript convenient does not belong in it.
166
+ */
167
+ export function arcsToValue(arcs) {
168
+ return JSON.stringify(arcs.map((arc) => ({ ...arc, from: endToValue(arc.from), to: endToValue(arc.to) })));
169
+ }
170
+ function endToValue(end) {
171
+ return end.kind === "marker" ? end.markerId : [end.lat, end.lon];
172
+ }
173
+ /**
174
+ * One end, from any of the three shapes a stored list holds it in.
175
+ *
176
+ * A **string** is a marker id; a **pair** is `[lat, lon]`, which is how a
177
+ * coordinate is written everywhere outside a program; an **object** with `lat`
178
+ * and `lon` is the long form. Refusing any of them would make the field's most
179
+ * obvious content its most surprising failure.
180
+ */
181
+ function endOf(value) {
182
+ if (typeof value === "string") {
183
+ return value.trim() === "" ? null : markerEnd(value);
184
+ }
185
+ if (Array.isArray(value)) {
186
+ const lat = finite(value[0]);
187
+ const lon = finite(value[1]);
188
+ return lat === null || lon === null
189
+ ? null
190
+ : placeEnd(clamp(lat, -90, 90), wrapLongitude(lon));
191
+ }
192
+ if (typeof value !== "object" || value === null)
193
+ return null;
194
+ const row = value;
195
+ if (typeof row.markerId === "string")
196
+ return markerEnd(row.markerId);
197
+ const lat = finite(row.lat ?? row.latitude);
198
+ const lon = finite(row.lon ?? row.lng ?? row.longitude);
199
+ return lat === null || lon === null
200
+ ? null
201
+ : placeEnd(clamp(lat, -90, 90), wrapLongitude(lon));
202
+ }
203
+ function rowsOf(value) {
204
+ if (typeof value !== "string" || value.trim() === "")
205
+ return [];
206
+ try {
207
+ const parsed = JSON.parse(value);
208
+ if (!Array.isArray(parsed))
209
+ return [];
210
+ return parsed.filter((row) => typeof row === "object" && row !== null);
211
+ }
212
+ catch {
213
+ return [];
214
+ }
215
+ }
216
+ /**
217
+ * A row's identity, or one derived from where it sits.
218
+ *
219
+ * Positional rather than random, and the difference is the whole point: a random
220
+ * id here would be a different one on every read, and both the tile's React key
221
+ * and an arc's pin to a marker would change under the author's hands.
222
+ */
223
+ function idOf(value, positional) {
224
+ return text(value) ?? positional;
225
+ }
226
+ function text(value) {
227
+ return typeof value === "string" && value.trim() !== "" ? value : null;
228
+ }
229
+ function finite(value) {
230
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
231
+ }
232
+ /** A stored 0–1 value; anything unreadable is fully on. */
233
+ function unit(value) {
234
+ const n = finite(value);
235
+ return n === null ? 1 : clamp(n, 0, 1);
236
+ }
237
+ /** A stored size, which must be above zero to draw at all. */
238
+ function positive(value, fallback) {
239
+ const n = finite(value);
240
+ return n === null || n <= 0 ? fallback : n;
241
+ }
242
+ /**
243
+ * A longitude into −180…180.
244
+ *
245
+ * Wrapped rather than clamped, unlike latitude: 190°E is a real place (170°W)
246
+ * and clamping would put it on the antimeridian, where there is nothing north of
247
+ * the north pole for a latitude to mean.
248
+ *
249
+ * A value already in range is returned untouched — the modulo round-trip is not
250
+ * exact in binary floating point (139.7 comes back as 139.70000000000005), and a
251
+ * stored longitude that differs from the one the author typed shows up later as
252
+ * a diff nobody made.
253
+ */
254
+ function wrapLongitude(value) {
255
+ if (value >= -180 && value <= 180)
256
+ return value;
257
+ return ((((value + 180) % 360) + 360) % 360) - 180;
258
+ }
259
+ function clamp(value, min, max) {
260
+ return value < min ? min : value > max ? max : value;
261
+ }
262
+ //# sourceMappingURL=globe-places.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"globe-places.js","sourceRoot":"","sources":["../../src/globe/globe-places.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What an author places on a Globe — the marked points, and the arcs between\n * them — as the panel edits them and the stored field holds them.\n *\n * Beside `graph-equations.ts` and shaped like it, because it is the same\n * problem: an appearance value is `number | string | boolean`, so a *list* has\n * to arrive as one string that a mapper reads. What lives here is the authored\n * shape and its serialization, shared by the inspector's tiles and the node's\n * builder; what a frame actually draws is `nodes/globe/impl/places.ts`, which\n * resolves this into coordinates and clamps it to what the render can use.\n *\n * The split matters in one direction: **reading must be faithful and must not\n * mint identity**. The panel reads on every render and the builder on every\n * rebuild, so a random id here would be a different id every time — and two\n * things key off it (a tile's React key, and which arc endpoint points at which\n * marker). {@link createMarker} mints ids; the readers derive a positional one\n * and never invent.\n */\n\n/**\n * A marked point on the surface.\n *\n * `label` is **not drawn**. It names the tile in the inspector and the entries\n * in an arc's endpoint menus, which is the whole of its job today — a label\n * rendered against the globe is a separate problem (it has to face the camera,\n * hide when it rotates behind the planet, and not collide with its neighbours),\n * and storing the text now is what lets that arrive without a migration.\n */\nexport interface GlobeMarker {\n /** Identity across list edits — a tween matches like with like. */\n id: string\n /** A name for this place. Editor-facing only; see the note above. */\n label: string\n /** Degrees north, −90…90. */\n lat: number\n /** Degrees east, −180…180. */\n lon: number\n /** Any colour string the fill parser takes. Empty means the palette entry. */\n color: string\n /** Radius as a fraction of the globe's own. */\n size: number\n /** 0–1. A fade, unlike {@link enabled}, which is a switch. */\n opacity: number\n /** Whether it draws. Off keeps the tile in place, editable, with its colour. */\n enabled: boolean\n}\n\n/**\n * One end of an arc: a marker it is pinned to, or a place of its own.\n *\n * The pinned form is the one the tile offers first, and it is not merely\n * convenient — an arc between two *marked* places moves when the place does,\n * where a copied pair of coordinates silently stops matching the dot it was\n * drawn to. That is the same reasoning a state machine's connection names its\n * ends by state id rather than by position.\n *\n * The free form stays because not every end wants a dot on it, and because a\n * list written by hand or by an agent reaches for a coordinate pair first.\n */\nexport type ArcEnd =\n | { readonly kind: \"marker\"; readonly markerId: string }\n | { readonly kind: \"place\"; readonly lat: number; readonly lon: number }\n\n/** A great-circle path between two ends. */\nexport interface GlobeArc {\n id: string\n from: ArcEnd\n to: ArcEnd\n color: string\n /** Tube radius as a fraction of the globe's own. */\n width: number\n /** How far the middle lifts off the surface, as a fraction of the radius. */\n lift: number\n /**\n * How much of the path is drawn, 0–1 — a line that draws itself on.\n *\n * A fraction of the *path* rather than a time, so it means the same thing\n * whatever the arc's length and is the author's to tween from wherever they\n * like. Zero draws nothing and is not an error.\n */\n progress: number\n enabled: boolean\n}\n\n/**\n * The palette an uncoloured marker takes its colour from, by position.\n *\n * Warm-to-cool rather than a spectrum: markers are read against a dark ocean and\n * usually mean something ranked — the first is the subject and the rest are\n * context — so the order runs from the one that carries furthest to the one that\n * recedes. Six, then it wraps; a seventh place that has to stand out sets its\n * own colour.\n */\nexport const DEFAULT_MARKER_COLORS = [\n \"#ff3b30\",\n \"#ff9f0a\",\n \"#ffd60a\",\n \"#30d158\",\n \"#5ac8fa\",\n \"#bf5af2\",\n] as const\n\n/** The colour a marker at `index` is drawn in when it names none. */\nexport function markerColorAt(index: number): string {\n const palette = DEFAULT_MARKER_COLORS\n return palette[((index % palette.length) + palette.length) % palette.length]!\n}\n\n/** The colour an arc falls back to. */\nexport const DEFAULT_ARC_COLOR = \"#ffd60a\"\n\nconst DEFAULT_MARKER_SIZE = 0.02\nconst DEFAULT_ARC_WIDTH = 0.004\nconst DEFAULT_ARC_LIFT = 0.25\n\n/**\n * A fresh id for a tile.\n *\n * A counter plus a short random suffix rather than `crypto.randomUUID`, exactly\n * as `graph-equations.ts` explains: this package loads in a bare Node process\n * with no DOM and no assumed globals, and an id only has to be unique within one\n * node's list. The suffix is what stops two clients editing the same scene\n * colliding on `mk-3`.\n *\n * Never called during a build — only when an author adds a tile.\n */\nlet placeSeq = 0\nfunction nextId(prefix: string): string {\n placeSeq += 1\n return `${prefix}-${placeSeq}-${Math.random().toString(36).slice(2, 8)}`\n}\n\n/** A new marker tile: showing, opaque, and at the given place. */\nexport function createMarker(\n at: { lat: number; lon: number } = { lat: 0, lon: 0 }\n): GlobeMarker {\n return {\n id: nextId(\"mk\"),\n label: \"\",\n lat: at.lat,\n lon: at.lon,\n color: \"\",\n size: DEFAULT_MARKER_SIZE,\n opacity: 1,\n enabled: true,\n }\n}\n\n/** A new arc tile, between two ends. */\nexport function createArc(from: ArcEnd, to: ArcEnd): GlobeArc {\n return {\n id: nextId(\"arc\"),\n from,\n to,\n color: DEFAULT_ARC_COLOR,\n width: DEFAULT_ARC_WIDTH,\n lift: DEFAULT_ARC_LIFT,\n progress: 1,\n enabled: true,\n }\n}\n\n/** An end pinned to a marker. */\nexport function markerEnd(markerId: string): ArcEnd {\n return { kind: \"marker\", markerId }\n}\n\n/** An end at a place of its own. */\nexport function placeEnd(lat: number, lon: number): ArcEnd {\n return { kind: \"place\", lat, lon }\n}\n\n// --- Reading ---------------------------------------------------------------\n\n/**\n * Reads a stored marker list, defensively.\n *\n * Never throws, and drops only what it genuinely cannot place: the field is\n * edited a keystroke at a time by hand as well as through the tiles, so it is\n * invalid far more often than it is valid, and a reader that threw would blank\n * the canvas on the way from `[` to `[{`.\n *\n * A row with **no position at all** is the one thing that is dropped rather than\n * defaulted. Every other field has an honest default; a latitude does not, and\n * putting an unplaced marker on the equator would draw a dot somewhere nobody\n * asked for and call it the author's.\n */\nexport function markersOf(value: unknown): GlobeMarker[] {\n return rowsOf(value).flatMap((row, index) => {\n const lat = finite(row.lat ?? row.latitude)\n const lon = finite(row.lon ?? row.lng ?? row.longitude)\n if (lat === null || lon === null) return []\n return [\n {\n id: idOf(row.id, `mk:${index}`),\n label: text(row.label ?? row.name) ?? \"\",\n lat: clamp(lat, -90, 90),\n lon: wrapLongitude(lon),\n color: text(row.color) ?? \"\",\n size: positive(row.size, DEFAULT_MARKER_SIZE),\n opacity: unit(row.opacity),\n // Absent means on, as everywhere else a switch is stored: only an\n // explicit `false` turns a tile off.\n enabled: row.enabled !== false,\n },\n ]\n })\n}\n\n/** Reads a stored arc list, on the same terms as {@link markersOf}. */\nexport function arcsOf(value: unknown): GlobeArc[] {\n return rowsOf(value).flatMap((row, index) => {\n const from = endOf(row.from)\n const to = endOf(row.to)\n if (!from || !to) return []\n return [\n {\n id: idOf(row.id, `arc:${index}`),\n from,\n to,\n color: text(row.color) ?? DEFAULT_ARC_COLOR,\n width: positive(row.width, DEFAULT_ARC_WIDTH),\n lift: clamp(finite(row.lift) ?? DEFAULT_ARC_LIFT, 0, 4),\n progress: unit(row.progress),\n enabled: row.enabled !== false,\n },\n ]\n })\n}\n\n/** Serializes a marker list into the stored string form. */\nexport function markersToValue(markers: readonly GlobeMarker[]): string {\n return JSON.stringify(markers)\n}\n\n/**\n * Serializes an arc list, writing each end back in the shape it was authored in.\n *\n * A pinned end is a bare **string** and a free one a `[lat, lon]` **pair**,\n * rather than the tagged union this module works in. Two reasons, and the second\n * is the load-bearing one: those are the two shapes somebody writing this list\n * by hand reaches for, and a stored document is read by whatever version opens\n * it later — so the wire form is public API and a discriminant that only exists\n * to make the TypeScript convenient does not belong in it.\n */\nexport function arcsToValue(arcs: readonly GlobeArc[]): string {\n return JSON.stringify(\n arcs.map((arc) => ({ ...arc, from: endToValue(arc.from), to: endToValue(arc.to) }))\n )\n}\n\nfunction endToValue(end: ArcEnd): string | [number, number] {\n return end.kind === \"marker\" ? end.markerId : [end.lat, end.lon]\n}\n\n/**\n * One end, from any of the three shapes a stored list holds it in.\n *\n * A **string** is a marker id; a **pair** is `[lat, lon]`, which is how a\n * coordinate is written everywhere outside a program; an **object** with `lat`\n * and `lon` is the long form. Refusing any of them would make the field's most\n * obvious content its most surprising failure.\n */\nfunction endOf(value: unknown): ArcEnd | null {\n if (typeof value === \"string\") {\n return value.trim() === \"\" ? null : markerEnd(value)\n }\n if (Array.isArray(value)) {\n const lat = finite(value[0])\n const lon = finite(value[1])\n return lat === null || lon === null\n ? null\n : placeEnd(clamp(lat, -90, 90), wrapLongitude(lon))\n }\n if (typeof value !== \"object\" || value === null) return null\n const row = value as Record<string, unknown>\n if (typeof row.markerId === \"string\") return markerEnd(row.markerId)\n const lat = finite(row.lat ?? row.latitude)\n const lon = finite(row.lon ?? row.lng ?? row.longitude)\n return lat === null || lon === null\n ? null\n : placeEnd(clamp(lat, -90, 90), wrapLongitude(lon))\n}\n\n// --- Reading helpers -------------------------------------------------------\n\ntype Row = Record<string, unknown>\n\nfunction rowsOf(value: unknown): Row[] {\n if (typeof value !== \"string\" || value.trim() === \"\") return []\n try {\n const parsed: unknown = JSON.parse(value)\n if (!Array.isArray(parsed)) return []\n return parsed.filter(\n (row): row is Row => typeof row === \"object\" && row !== null\n )\n } catch {\n return []\n }\n}\n\n/**\n * A row's identity, or one derived from where it sits.\n *\n * Positional rather than random, and the difference is the whole point: a random\n * id here would be a different one on every read, and both the tile's React key\n * and an arc's pin to a marker would change under the author's hands.\n */\nfunction idOf(value: unknown, positional: string): string {\n return text(value) ?? positional\n}\n\nfunction text(value: unknown): string | null {\n return typeof value === \"string\" && value.trim() !== \"\" ? value : null\n}\n\nfunction finite(value: unknown): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null\n}\n\n/** A stored 0–1 value; anything unreadable is fully on. */\nfunction unit(value: unknown): number {\n const n = finite(value)\n return n === null ? 1 : clamp(n, 0, 1)\n}\n\n/** A stored size, which must be above zero to draw at all. */\nfunction positive(value: unknown, fallback: number): number {\n const n = finite(value)\n return n === null || n <= 0 ? fallback : n\n}\n\n/**\n * A longitude into −180…180.\n *\n * Wrapped rather than clamped, unlike latitude: 190°E is a real place (170°W)\n * and clamping would put it on the antimeridian, where there is nothing north of\n * the north pole for a latitude to mean.\n *\n * A value already in range is returned untouched — the modulo round-trip is not\n * exact in binary floating point (139.7 comes back as 139.70000000000005), and a\n * stored longitude that differs from the one the author typed shows up later as\n * a diff nobody made.\n */\nfunction wrapLongitude(value: number): number {\n if (value >= -180 && value <= 180) return value\n return ((((value + 180) % 360) + 360) % 360) - 180\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return value < min ? min : value > max ? max : value\n}\n"]}
@@ -0,0 +1,193 @@
1
+ import { type Command, type CommandArgs, Canvas3D, Scene3D, type Canvas3DProps, type Color, type NodeConfig } from "@motionscript/core";
2
+ import { type OrbitTarget } from "@motionscript/core/component";
3
+ export interface GlobeProps extends Canvas3DProps {
4
+ resolution: number;
5
+ ocean: Color;
6
+ land: Color;
7
+ border: Color;
8
+ borderWidth: number;
9
+ graticule: boolean;
10
+ graticuleColor: Color;
11
+ graticuleStep: number;
12
+ graticuleWidth: number;
13
+ orbit: number;
14
+ elevation: number;
15
+ zoom: number;
16
+ fov: number;
17
+ ambientIntensity: number;
18
+ ambientColor: Color;
19
+ keyIntensity: number;
20
+ keyColor: Color;
21
+ atmosphere: boolean;
22
+ atmosphereColor: Color;
23
+ atmosphereSize: number;
24
+ atmosphereFalloff: number;
25
+ atmosphereIntensity: number;
26
+ markers: string;
27
+ arcs: string;
28
+ }
29
+ /**
30
+ * A globe: country boundaries on a lit sphere, flown by an orbit camera.
31
+ *
32
+ * <Globe width="fill" height="fill" orbit={20} elevation={25} zoom={2.6} />
33
+ *
34
+ * Like the Protein and unlike the Canvas 3D viewport, it draws **one subject**
35
+ * derived from its own props rather than being a room the author fills. Four
36
+ * things about it are worth knowing before reading the parts:
37
+ *
38
+ * **The map is a texture, drawn in 2D.** See `world-map.ts`: a WebGL line
39
+ * ignores any width above one pixel, so borders drawn as 3D lines are hairlines
40
+ * forever. Baked, a border is an ordinary Skia stroke at any weight.
41
+ *
42
+ * **That texture is cached against its style, and the cache is not an
43
+ * optimisation.** motion-script re-rasterizes a surface every frame unless the
44
+ * descriptor says otherwise, and re-rasterizing this one means redrawing ten
45
+ * thousand paths, stalling on a GPU read-back and re-uploading a few megabytes —
46
+ * sixty times a second, for an identical image. {@link bakedMap} holds one
47
+ * descriptor per style and marks it `static`; the schema then refuses to animate
48
+ * any field that style is made of, so an export cannot mint a raster per frame.
49
+ *
50
+ * **The camera is three tweenable numbers, and two of them are places.** `orbit`
51
+ * is a longitude and `elevation` is a latitude — see `projection.ts`, which is
52
+ * where that convention is established and defended. There is no `OrbitControls`
53
+ * here for the reason the other 3D nodes have none: a rendered timeline has no
54
+ * pointer, and every frame has to be reproducible under scrubbing and export.
55
+ *
56
+ * **Markers and arcs are real 3D, never baked.** They are the animated content;
57
+ * putting them in the texture would run every frame of a marker's fade through a
58
+ * full re-rasterize.
59
+ */
60
+ export declare class Globe extends Canvas3D<GlobeProps> {
61
+ /**
62
+ * Texture width in pixels; the height is half of it.
63
+ *
64
+ * Structural, and capped in the schema rather than here. The buffer is
65
+ * re-uploaded to the GPU every frame even when its pixels are cached, so this
66
+ * is a per-frame bandwidth number as much as a sharpness one — 2048 is 8 MB a
67
+ * frame and already finer than the 110m boundaries it draws.
68
+ */
69
+ resolution: number;
70
+ ocean: Color;
71
+ land: Color;
72
+ /**
73
+ * The colour of every coast and border.
74
+ *
75
+ * Alpha works. It did not while the country paths carried the stroke — a
76
+ * border belongs to two countries, so it was drawn twice and came out at
77
+ * double density beside the coastlines. `borders.ts` deduplicates the lines
78
+ * and `world-map.ts` strokes them once.
79
+ */
80
+ border: Color;
81
+ borderWidth: number;
82
+ graticule: boolean;
83
+ graticuleColor: Color;
84
+ graticuleStep: number;
85
+ graticuleWidth: number;
86
+ /**
87
+ * The camera's heading, in degrees, in exactly the sense every other 3D node
88
+ * uses it — which is what keeps the canvas drag consistent with them.
89
+ *
90
+ * It is *not* a longitude: a camera at heading θ is centred over longitude −θ
91
+ * (see `orbitForLongitude` in `projection.ts`). Unbounded, like every other
92
+ * viewport's, because the drag wraps it into `[0, 360)` and a range here would
93
+ * clamp half of them.
94
+ */
95
+ orbit: number;
96
+ /**
97
+ * Degrees above the equator, which on a globe *is* the latitude the camera
98
+ * stands over — no conversion, unlike the heading. Held short of the poles by
99
+ * the schema, as every orbit camera's is.
100
+ */
101
+ elevation: number;
102
+ /** Distance from the centre, as a multiple of the globe's radius. */
103
+ zoom: number;
104
+ fov: number;
105
+ ambientIntensity: number;
106
+ ambientColor: Color;
107
+ keyIntensity: number;
108
+ keyColor: Color;
109
+ atmosphere: boolean;
110
+ atmosphereColor: Color;
111
+ /** The shell's radius as a multiple of the globe's. */
112
+ atmosphereSize: number;
113
+ /** How tightly the glow hugs the edge. Higher is a thinner rim. */
114
+ atmosphereFalloff: number;
115
+ atmosphereIntensity: number;
116
+ markers: string;
117
+ arcs: string;
118
+ constructor(props?: NodeConfig<Globe, GlobeProps>);
119
+ /**
120
+ * Frame the scene at a spherical camera placement. Every axis is optional,
121
+ * so "pull back" and "spin round" stay separate intentions — see
122
+ * {@link OrbitTarget}.
123
+ */
124
+ orbitTo(args: CommandArgs<{
125
+ target: OrbitTarget;
126
+ }> & {
127
+ duration: number;
128
+ }): Command<GlobeProps>;
129
+ /**
130
+ * Fly the camera to a place on the globe. `latitude`/`longitude` are the
131
+ * point to bring under the camera and `framing` the distance to settle at.
132
+ *
133
+ * **`arc` is what makes it a flight rather than a pan** — above `0` the
134
+ * distance bows outward at the midpoint by that fraction, so a long journey
135
+ * rises off the surface and settles back down rather than scraping the
136
+ * horizon the whole way. Longitude takes the **short way round**.
137
+ */
138
+ flyTo(args: CommandArgs<{
139
+ latitude: number;
140
+ longitude: number;
141
+ framing?: number;
142
+ arc?: number;
143
+ }> & {
144
+ duration?: number;
145
+ }): Command<GlobeProps>;
146
+ protected buildScene3D(): Scene3D;
147
+ /** The sphere, wearing the baked map. */
148
+ private addPlanet;
149
+ /** The halo. A shell, kept for its back faces — see `atmosphere.ts`. */
150
+ private addAtmosphere;
151
+ /**
152
+ * The markers, as one instanced draw.
153
+ *
154
+ * Instanced rather than a mesh apiece because a marker list is the one thing
155
+ * here with no ceiling — a network map is thousands of points, and a draw call
156
+ * each would not keep up. One geometry, one material, one upload.
157
+ *
158
+ * Note the material carries **no** `vertexColors`: an instanced mesh that asks
159
+ * for it makes the shader read a per-vertex `color` attribute the geometry does
160
+ * not have, gets zeroes, and every marker renders black. The per-instance
161
+ * colours ride the `colors` option instead, which is a different channel.
162
+ */
163
+ private addMarkers;
164
+ /** The arcs, one swept tube each. */
165
+ private addArcs;
166
+ /**
167
+ * The baked map, and the descriptor that lets it be baked once.
168
+ *
169
+ * Two caches are being kept in step here, and the key is what keeps them in
170
+ * step. This one holds the `Graphics2D` and its descriptor so the node hands
171
+ * the renderer the *same object* every frame — surface identity is object
172
+ * identity, so a fresh one each frame would defeat the texture cache and
173
+ * orphan a GPU texture per frame. The renderer's own `staticRasters` holds the
174
+ * rasterized pixels against the `identity` string below.
175
+ *
176
+ * They share one key, which is the point: edit a colour and this cache misses
177
+ * (a new `Graphics2D`) *and* that one misses (a new identity string). Derive
178
+ * the identity from anything else — a counter, the node id — and a colour
179
+ * change would serve the old pixels forever, because nothing invalidates
180
+ * `staticRasters` short of tearing down the render context.
181
+ *
182
+ * The descriptor is written out rather than built with `Tex.surface` because
183
+ * the builder's options do not include `static` or `identity`. Handing a bare
184
+ * `Graphics2D` to `fill` instead takes `resolveFill3D`'s surface path, which
185
+ * sets neither — and that is the per-frame read-back this whole arrangement
186
+ * exists to avoid.
187
+ */
188
+ private cache;
189
+ private bakedMap;
190
+ /** The style props as the one value the baker takes. Also the cache key. */
191
+ private mapStyle;
192
+ }
193
+ //# sourceMappingURL=globe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"globe.d.ts","sourceRoot":"","sources":["../../src/globe/globe.ts"],"names":[],"mappings":"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"}