entangled-worlds-orbiters-shared 5.0.0 → 6.0.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 (57) hide show
  1. package/README.md +3 -4
  2. package/dist/config/index.d.ts +1 -1
  3. package/dist/config/index.js +1 -1
  4. package/dist/config/textureConfig.d.ts +2 -15
  5. package/dist/config/textureConfig.js +1 -43
  6. package/dist/loaders/index.d.ts +0 -2
  7. package/dist/loaders/index.js +0 -1
  8. package/dist/loaders/ktx2.js +2 -2
  9. package/dist/shortcuts/chord.d.ts +24 -2
  10. package/dist/shortcuts/chord.js +86 -8
  11. package/dist/shortcuts/format.d.ts +7 -4
  12. package/dist/shortcuts/format.js +40 -14
  13. package/dist/shortcuts/index.d.ts +1 -1
  14. package/dist/shortcuts/index.js +4 -3
  15. package/dist/shortcuts/match.d.ts +3 -1
  16. package/dist/shortcuts/match.js +14 -8
  17. package/dist/sky/geometry.d.ts +31 -0
  18. package/dist/sky/geometry.js +84 -0
  19. package/dist/sky/index.d.ts +6 -0
  20. package/dist/sky/index.js +6 -0
  21. package/dist/sky/params.d.ts +44 -0
  22. package/dist/sky/params.js +52 -0
  23. package/dist/sky/shaders.d.ts +14 -0
  24. package/dist/sky/shaders.js +149 -0
  25. package/dist/sky/sky.d.ts +24 -0
  26. package/dist/sky/sky.js +268 -0
  27. package/dist/sky/worldSkyFile.d.ts +16 -0
  28. package/dist/sky/worldSkyFile.js +51 -0
  29. package/dist/star/bounds.d.ts +1 -1
  30. package/dist/star/bounds.js +1 -1
  31. package/dist/star/params.d.ts +1 -1
  32. package/dist/star/params.js +1 -1
  33. package/dist/star/starBody.d.ts +1 -1
  34. package/dist/star/starManager.d.ts +1 -1
  35. package/package.json +8 -2
  36. package/public/config/textures.json +0 -73
  37. package/src/config/index.ts +0 -2
  38. package/src/config/textureConfig.ts +2 -53
  39. package/src/loaders/index.ts +0 -2
  40. package/src/loaders/ktx2.ts +2 -2
  41. package/src/shortcuts/chord.ts +96 -6
  42. package/src/shortcuts/format.ts +44 -17
  43. package/src/shortcuts/index.ts +7 -2
  44. package/src/shortcuts/match.ts +13 -8
  45. package/src/sky/geometry.ts +99 -0
  46. package/src/sky/index.ts +16 -0
  47. package/src/sky/params.ts +89 -0
  48. package/src/sky/shaders.ts +159 -0
  49. package/src/sky/sky.ts +330 -0
  50. package/src/sky/worldSkyFile.ts +60 -0
  51. package/src/star/bounds.ts +1 -1
  52. package/src/star/params.ts +1 -1
  53. package/src/star/starBody.ts +1 -1
  54. package/src/star/starManager.ts +1 -1
  55. package/dist/loaders/cubeMapLoader.d.ts +0 -58
  56. package/dist/loaders/cubeMapLoader.js +0 -291
  57. package/src/loaders/cubeMapLoader.ts +0 -342
package/README.md CHANGED
@@ -6,7 +6,7 @@ A shared library containing core components for 3D world rendering, moons system
6
6
 
7
7
  - **World System**: Core world rendering with textured spheres, normal materials, GLB models, and cloud shells
8
8
  - **Moons System**: Orbital moon generation and management
9
- - **Loaders**: Texture, GLB, and cubemap loading utilities
9
+ - **Loaders**: Texture and GLB loading utilities
10
10
  - **Core Rendering**: Lighting, Fresnel effects, and material utilities
11
11
  - **Configuration**: Texture and configuration management system
12
12
 
@@ -57,7 +57,6 @@ import * as EntangledWorlds from 'entangled-worlds-orbiters-shared';
57
57
  - `moonsGeneration` - Moon generation logic
58
58
 
59
59
  ### Loaders
60
- - `cubeMapLoader` - Skybox loading
61
60
  - `glbLoader` - GLB loading utilities
62
61
  - `bodyTextureLoader` - Texture loading
63
62
 
@@ -99,7 +98,7 @@ The capability registry (`createCapabilityRegistry`, `validate` / `assertValid`,
99
98
  - `largestFunction`, `shrinkWasm` - The size check and the shrink step
100
99
 
101
100
  ### Keyboard shortcuts (`entangled-worlds-orbiters-shared/shortcuts`)
102
- - `ShortcutAction` - One catalogue entry: `id`, `label` (+ `names` by language), `defaultKeys` (by key place, `event.code`), `scope`
101
+ - `ShortcutAction` - One catalogue entry: `id`, `label` (+ `names` by language), `defaultKeys` (named as a US keyboard names them, `event.code`; a press matches by the character it types, by its place only where it types no US character, so keys work on any keyboard), `scope`
103
102
  - `matchShortcut(event, bindings)` - Which action a press is, or `null`: modifiers, a field being typed in and surface claims decided in one place
104
103
  - Key strings: `Shift+KeyR`, `Mod+KeyZ` (Ctrl or ⌘), `Shift?+KeyQ` (Shift may be held); `parseChord`, `chordsOverlap`
105
104
  - `resolveBindings(catalogue, overrides)`, `findClashes` - Defaults plus a person's overrides (`[{ id, keys }]` as their settings store them, `keys: []` unbinds), and the clash check
@@ -150,7 +149,7 @@ Defaults:
150
149
 
151
150
  Effects:
152
151
  - All URLs returned by `config/textureConfig` helpers are absolute.
153
- - Skybox loader and moons generation resolve relative paths against the configured base.
152
+ - Moons generation resolves relative paths against the configured base.
154
153
  - Cloud shell textures passed to `world.makeSphere` are resolved to absolute URLs.
155
154
 
156
155
  ## Peer Dependencies
@@ -1,3 +1,3 @@
1
- export { loadTextureConfig, getTextureOptions, getTextureChoices, getDefaultTexture, getSkyboxOptions, getDefaultSkybox, getTextureFallbackUrl } from './textureConfig.js';
1
+ export { loadTextureConfig, getTextureOptions, getTextureChoices, getDefaultTexture, getTextureFallbackUrl } from './textureConfig.js';
2
2
  export type { TextureConfig, TextureCategory, TextureItem, TextureChoice, TextureConfigMetadata } from './textureConfig.js';
3
3
  export { setPublicBase, getPublicBase, resolvePublicUrl } from './env.js';
@@ -1,4 +1,4 @@
1
1
  // Configuration exports
2
- export { loadTextureConfig, getTextureOptions, getTextureChoices, getDefaultTexture, getSkyboxOptions, getDefaultSkybox, getTextureFallbackUrl } from './textureConfig.js';
2
+ export { loadTextureConfig, getTextureOptions, getTextureChoices, getDefaultTexture, getTextureFallbackUrl } from './textureConfig.js';
3
3
  // runtime base helpers
4
4
  export { setPublicBase, getPublicBase, resolvePublicUrl } from './env.js';
@@ -9,12 +9,11 @@ export interface TextureItem {
9
9
  fallback?: string;
10
10
  tags?: string[];
11
11
  }
12
- /** One catalogue category (clouds, moons, skyboxes, …). */
12
+ /** One catalogue category (clouds, moons, …). */
13
13
  export interface TextureCategory {
14
14
  category?: string;
15
15
  basePath?: string;
16
16
  defaultTexture?: string;
17
- defaultSkybox?: string;
18
17
  items?: TextureItem[];
19
18
  }
20
19
  /** The texture catalogue as loaded from the config URL. */
@@ -70,7 +69,7 @@ export type TextureChoice = {
70
69
  export declare function getTextureChoices(category: string, config?: TextureConfig | null): Promise<TextureChoice[]>;
71
70
  /**
72
71
  * Get texture options for a specific category (clouds, moons, etc.)
73
- * @param {string} category - Texture category (clouds, moons, skyboxes)
72
+ * @param {string} category - Texture category (clouds, moons)
74
73
  * @param {TextureConfig} config - Optional pre-loaded config object
75
74
  * @returns {Promise<Record<string, string>>} `{ label: url }`, suitable for dropdown option lists
76
75
  */
@@ -114,18 +113,6 @@ export declare function getTextureUrl(category: string, textureId: string, confi
114
113
  * @returns {Promise<TextureItem[]>} Array of matching texture definitions
115
114
  */
116
115
  export declare function searchTexturesByTags(category: string, tags: string[], config?: TextureConfig | null): Promise<TextureItem[]>;
117
- /**
118
- * Get skybox options formatted for GUI dropdowns
119
- * @param {TextureConfig} config - Optional pre-loaded config object
120
- * @returns {Promise<Record<string, string>>} `{ label: id }`
121
- */
122
- export declare function getSkyboxOptions(config?: TextureConfig | null): Promise<Record<string, string>>;
123
- /**
124
- * Get default skybox ID
125
- * @param {TextureConfig} config - Optional pre-loaded config object
126
- * @returns {Promise<string>} Default skybox ID
127
- */
128
- export declare function getDefaultSkybox(config?: TextureConfig | null): Promise<string>;
129
116
  /**
130
117
  * Reload configuration (force refresh)
131
118
  * @param {string} configUrl - Override config URL
@@ -117,7 +117,7 @@ export async function getTextureChoices(category, config) {
117
117
  }
118
118
  /**
119
119
  * Get texture options for a specific category (clouds, moons, etc.)
120
- * @param {string} category - Texture category (clouds, moons, skyboxes)
120
+ * @param {string} category - Texture category (clouds, moons)
121
121
  * @param {TextureConfig} config - Optional pre-loaded config object
122
122
  * @returns {Promise<Record<string, string>>} `{ label: url }`, suitable for dropdown option lists
123
123
  */
@@ -215,40 +215,6 @@ export async function searchTexturesByTags(category, tags, config) {
215
215
  }
216
216
  return categoryConfig.items.filter(item => item.tags && tags.some(tag => item.tags.includes(tag)));
217
217
  }
218
- /**
219
- * Get skybox options formatted for GUI dropdowns
220
- * @param {TextureConfig} config - Optional pre-loaded config object
221
- * @returns {Promise<Record<string, string>>} `{ label: id }`
222
- */
223
- export async function getSkyboxOptions(config) {
224
- const textureConfig = await resolveConfig(config);
225
- const skyboxConfig = textureConfig.textures.skyboxes;
226
- if (!skyboxConfig) {
227
- console.warn('[TextureConfig] Skyboxes category not found');
228
- return {};
229
- }
230
- const options = {};
231
- // Reads `items` unguarded, unlike the other readers: a skyboxes category without items throws here.
232
- skyboxConfig.items.forEach(item => {
233
- const label = item.label || item.id;
234
- const id = item.id;
235
- options[label] = id;
236
- });
237
- return options;
238
- }
239
- /**
240
- * Get default skybox ID
241
- * @param {TextureConfig} config - Optional pre-loaded config object
242
- * @returns {Promise<string>} Default skybox ID
243
- */
244
- export async function getDefaultSkybox(config) {
245
- const textureConfig = await resolveConfig(config);
246
- const skyboxConfig = textureConfig.textures.skyboxes;
247
- if (!skyboxConfig) {
248
- return '001'; // fallback
249
- }
250
- return skyboxConfig.defaultSkybox || '001';
251
- }
252
218
  /**
253
219
  * Hardcoded fallback configuration (minimal)
254
220
  * Used when remote config loading fails
@@ -275,14 +241,6 @@ function getHardcodedFallback() {
275
241
  items: [
276
242
  { id: "default-moon", filename: "moon-equirectangular.ktx2", label: "Default Moon", tags: ["lunar"] }
277
243
  ]
278
- },
279
- skyboxes: {
280
- category: "environment",
281
- basePath: "/skybox",
282
- defaultSkybox: "001",
283
- items: [
284
- { id: "001", label: "Deep Space", tags: ["space"] }
285
- ]
286
244
  }
287
245
  }
288
246
  };
@@ -1,5 +1,3 @@
1
- export { loadCubeMap, setBackground, normalizeSkycubeName, buildSkycubefaceKeys } from './cubeMapLoader.js';
2
1
  export { loadGLB } from './glbLoader.js';
3
2
  export { loadTexture } from './bodyTextureLoader.js';
4
3
  export { getKTX2LoaderForRenderer, disposeKTX2LoaderForRenderer } from './ktx2.js';
5
- export type { SkyboxResource } from './cubeMapLoader.js';
@@ -1,5 +1,4 @@
1
1
  // Loaders exports
2
- export { loadCubeMap, setBackground, normalizeSkycubeName, buildSkycubefaceKeys } from './cubeMapLoader.js';
3
2
  export { loadGLB } from './glbLoader.js';
4
3
  export { loadTexture } from './bodyTextureLoader.js';
5
4
  export { getKTX2LoaderForRenderer, disposeKTX2LoaderForRenderer } from './ktx2.js';
@@ -4,12 +4,12 @@ import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
4
4
  *
5
5
  * `KTX2Loader.detectSupport(renderer)` binds the loader's Basis transcoder worker pool to ONE WebGL
6
6
  * context. A single shared loader therefore only decodes textures for the FIRST renderer it saw —
7
- * every other view's planets/skyboxes silently fail and render untextured (dark). That breaks any
7
+ * every other view's planets silently fail and render untextured (dark). That breaks any
8
8
  * multi-view page: N orbiters in one realm, or N Entangled World planets shown together.
9
9
  *
10
10
  * Keying the loader by renderer gives each view its own correctly-bound loader. Single-renderer apps
11
11
  * (a single orbiter, today's Entangled World) get exactly ONE entry, so behaviour is byte-identical.
12
- * (Was: a single `window.__EW_KTX2_LOADER` global shared by bodyTexture/glb/cubeMap loaders.)
12
+ * (Was: a single `window.__EW_KTX2_LOADER` global shared by the bodyTexture/glb loaders.)
13
13
  */
14
14
  const loadersByRenderer = new WeakMap();
15
15
  function makeLoader(renderer) {
@@ -18,8 +18,30 @@ export interface ModifierState {
18
18
  }
19
19
  /** The chord a key string names, or `null` when it names none (an unknown modifier, a doubled one, no key). */
20
20
  export declare function parseChord(keys: string): Chord | null;
21
- /** Whether a press of `code` with these modifiers is this chord. */
22
- export declare function chordMatches(chord: Chord, code: string, state: ModifierState): boolean;
21
+ /** A press as a chord reads it: the modifiers, the key's place and, when the browser says, the character it typed. */
22
+ export interface ChordPress extends ModifierState {
23
+ readonly code: string;
24
+ readonly key?: string;
25
+ }
26
+ /** How a press is this chord: by the character it typed, by the key's place, or not at all. */
27
+ export type ChordMatch = 'character' | 'place' | null;
28
+ /**
29
+ * How a press is this chord. `character` beats `place`: on a Spanish keyboard Shift+7 types `/`, so it
30
+ * is the `/` key before it is the `7` key. A press with no `key` (an old caller) matches by place only.
31
+ */
32
+ export declare function chordMatch(chord: Chord, press: ChordPress): ChordMatch;
33
+ /** Whether a press of `code` with these modifiers (and, when given, the character it typed) is this chord. */
34
+ export declare function chordMatches(chord: Chord, code: string, state: ModifierState & {
35
+ readonly key?: string;
36
+ }): boolean;
37
+ /**
38
+ * The key string a typed character names (`/` → `Slash`, `?` → `Shift+Slash`, `q` → `KeyQ`), for
39
+ * someone setting a key; `null` for a character a US keyboard does not have, or none.
40
+ */
41
+ export declare function typedKey(key: string | undefined): {
42
+ readonly code: string;
43
+ readonly shift?: boolean;
44
+ } | null;
23
45
  /** A chord that needs Ctrl or ⌘: a command, never a character typed. */
24
46
  export declare function isCommandChord(chord: Chord): boolean;
25
47
  /** Whether one press could be both chords — the test for a clash. */
@@ -1,7 +1,13 @@
1
1
  // One key with its modifiers, written as a string: `Shift+KeyR`, `Mod+KeyZ`, `Shift?+Digit1`.
2
2
  //
3
- // The last part is the key's PLACE (`KeyboardEvent.code`), so a French or German keyboard keeps the
4
- // same layout: the key, not the letter it prints. Before it, the modifiers:
3
+ // The last part names the key the way a US keyboard does (`KeyboardEvent.code`: `KeyQ`, `Slash`,
4
+ // `Digit1`). A press matches it by the CHARACTER it types wherever it types one, so `/` is the key
5
+ // that prints `/` on this person's keyboard (Shift+7 on a Spanish one), and `KeyQ` is the key that
6
+ // prints Q (top left on a French one). Only where the key types no character the US keyboard has
7
+ // (a Greek or Cyrillic letter, ⌥ on a Mac changing it, a dead key, no letter on a letter key) is the
8
+ // key matched by its place.
9
+ // Digits match by character first, then by place: a French keyboard types `&` there.
10
+ // Before the key, the modifiers:
5
11
  //
6
12
  // Shift, Alt, Ctrl, Meta held. One left out must NOT be held.
7
13
  // Mod Ctrl or ⌘ (either, or both), the command key of every platform.
@@ -48,18 +54,90 @@ function parse(keys) {
48
54
  return { code, ...rules };
49
55
  }
50
56
  const fits = (rule, down) => rule === 'any' || (rule === 'held') === Boolean(down);
51
- /** Whether a press of `code` with these modifiers is this chord. */
52
- export function chordMatches(chord, code, state) {
53
- if (chord.code !== code)
54
- return false;
55
- if (!fits(chord.shift, state.shiftKey) || !fits(chord.alt, state.altKey))
56
- return false;
57
+ /**
58
+ * The punctuation keys a chord may name, and what a US keyboard types there without and with Shift:
59
+ * what the chord means on every keyboard. With these, Shift is part of the character (`?` is
60
+ * `Shift+Slash`), so a keyboard that needs Shift, or AltGr, for a `/` still types the `/`.
61
+ */
62
+ const PUNCTUATION = {
63
+ Minus: ['-', '_'], Equal: ['=', '+'], BracketLeft: ['[', '{'], BracketRight: [']', '}'], Backslash: ['\\', '|'],
64
+ Semicolon: [';', ':'], Quote: ["'", '"'], Backquote: ['`', '~'], Comma: [',', '<'], Period: ['.', '>'], Slash: ['/', '?'],
65
+ };
66
+ /** The code a US keyboard types each punctuation character at, and whether it takes Shift. */
67
+ const PUNCTUATION_PLACE = new Map(Object.entries(PUNCTUATION).flatMap(([code, [plain, shifted]]) => [[plain, [code, false]], [shifted, [code, true]]]));
68
+ /** The character a press typed, when it is one a US keyboard has (printable ASCII); else `null`. */
69
+ function typed(key) {
70
+ return typeof key === 'string' && key.length === 1 && key >= '!' && key <= '~' ? key : null;
71
+ }
72
+ function commandFits(chord, state) {
57
73
  if (chord.mod === 'held')
58
74
  return Boolean(state.ctrlKey || state.metaKey);
59
75
  if (chord.mod === 'any')
60
76
  return true;
61
77
  return fits(chord.ctrl, state.ctrlKey) && fits(chord.meta, state.metaKey);
62
78
  }
79
+ function placeMatch(chord, code, state) {
80
+ return chord.code === code && fits(chord.shift, state.shiftKey) && fits(chord.alt, state.altKey) && commandFits(chord, state);
81
+ }
82
+ /**
83
+ * How a press is this chord. `character` beats `place`: on a Spanish keyboard Shift+7 types `/`, so it
84
+ * is the `/` key before it is the `7` key. A press with no `key` (an old caller) matches by place only.
85
+ */
86
+ export function chordMatch(chord, press) {
87
+ const char = typed(press.key);
88
+ if (char === null)
89
+ return placeMatch(chord, press.code, press) ? 'place' : null;
90
+ const punctuation = PUNCTUATION[chord.code];
91
+ if (punctuation) {
92
+ // Shift is in the character (a Spanish `/` is Shift+7); with ⌘ some browsers report the key
93
+ // unshifted, so `⇧⌘,` is also a `,` with Shift held. Alt only when the chord asks for it (AltGr
94
+ // and ⌥ type characters).
95
+ const [plain, shifted] = punctuation;
96
+ const asShifted = char === shifted || (isCommandChord(chord) && char === plain && press.shiftKey);
97
+ const ok = chord.shift === 'held' ? asShifted : chord.shift === 'any' ? char === plain || asShifted : char === plain;
98
+ // A command (⇧⌘,) where this keyboard types another character there: the key's place, as menus do.
99
+ if (!ok)
100
+ return isCommandChord(chord) && placeMatch(chord, press.code, press) ? 'place' : null;
101
+ if (chord.alt === 'held' && !press.altKey)
102
+ return null;
103
+ // AltGr arrives as Ctrl+Alt on Windows: that Ctrl typed the character, it is no command.
104
+ const altGr = press.altKey && press.ctrlKey && !press.metaKey;
105
+ return commandFits(chord, { ctrlKey: press.ctrlKey && !altGr, metaKey: press.metaKey, shiftKey: press.shiftKey, altKey: press.altKey }) ? 'character' : null;
106
+ }
107
+ const letter = /^Key([A-Z])$/.exec(chord.code)?.[1];
108
+ if (letter) {
109
+ // A key that types no letter (`;` where Q is on a Greek keyboard) is read by its place.
110
+ if (!/^[a-zA-Z]$/.test(char))
111
+ return placeMatch(chord, press.code, press) ? 'place' : null;
112
+ if (char.toUpperCase() !== letter)
113
+ return null;
114
+ return fits(chord.shift, press.shiftKey) && fits(chord.alt, press.altKey) && commandFits(chord, press) ? 'character' : null;
115
+ }
116
+ const digit = /^Digit(\d)$/.exec(chord.code)?.[1];
117
+ // A digit typed; where the keyboard types something else there (`&` on a French one), its place.
118
+ if (digit && char === digit && fits(chord.shift, press.shiftKey) && fits(chord.alt, press.altKey) && commandFits(chord, press))
119
+ return 'character';
120
+ return placeMatch(chord, press.code, press) ? 'place' : null;
121
+ }
122
+ /** Whether a press of `code` with these modifiers (and, when given, the character it typed) is this chord. */
123
+ export function chordMatches(chord, code, state) {
124
+ // Field by field: a browser's KeyboardEvent keeps them on its prototype, where a spread does not reach.
125
+ const { shiftKey, altKey, ctrlKey, metaKey, key } = state;
126
+ return chordMatch(chord, { code, key, shiftKey, altKey, ctrlKey, metaKey }) !== null;
127
+ }
128
+ /**
129
+ * The key string a typed character names (`/` → `Slash`, `?` → `Shift+Slash`, `q` → `KeyQ`), for
130
+ * someone setting a key; `null` for a character a US keyboard does not have, or none.
131
+ */
132
+ export function typedKey(key) {
133
+ const char = typed(key);
134
+ if (char === null)
135
+ return null;
136
+ if (/^[a-zA-Z]$/.test(char))
137
+ return { code: `Key${char.toUpperCase()}` };
138
+ const place = PUNCTUATION_PLACE.get(char);
139
+ return place ? { code: place[0], shift: place[1] } : null;
140
+ }
63
141
  /** A chord that needs Ctrl or ⌘: a command, never a character typed. */
64
142
  export function isCommandChord(chord) {
65
143
  return chord.mod === 'held' || chord.ctrl === 'held' || chord.meta === 'held';
@@ -4,8 +4,8 @@ export type ShortcutPlatform = 'apple' | 'other';
4
4
  export declare function shortcutPlatform(): ShortcutPlatform;
5
5
  /**
6
6
  * The character a key place prints on this person's keyboard, when the browser can say
7
- * (`navigator.keyboard.getLayoutMap()`, Chromium only); a key place it does not know is shown from
8
- * its name.
7
+ * (`navigator.keyboard.getLayoutMap()`, Chromium only). Only keys matched by their place use it:
8
+ * a letter, digit or punctuation key matches the character it names, so it shows that character.
9
9
  */
10
10
  export type KeyLayout = ReadonlyMap<string, string>;
11
11
  /** What a key place is called on a keyboard: `KeyQ` → `Q`, `Digit1` → `1`, `Numpad4` → `Num 4`. */
@@ -20,11 +20,14 @@ export declare function chordParts(keys: string, platform?: ShortcutPlatform, la
20
20
  export declare function formatChord(keys: string, platform?: ShortcutPlatform, layout?: KeyLayout): string;
21
21
  /**
22
22
  * The key string a press makes, for someone setting a key: exactly the modifiers held, the command
23
- * key of their platform written `Mod` (⌘ on Apple, Ctrl elsewhere) so the key works on both. `null`
24
- * while only modifiers are down, or for a press with no key place.
23
+ * key of their platform written `Mod` (⌘ on Apple, Ctrl elsewhere) so the key works on both. A letter
24
+ * or punctuation character typed is written as the key that types it on a US keyboard (`/` is
25
+ * `Slash` whichever keys typed it), the way the matcher reads it back. `null` while only modifiers
26
+ * are down, or for a press with no key place.
25
27
  */
26
28
  export declare function chordFromEvent(event: {
27
29
  code: string;
30
+ key?: string;
28
31
  shiftKey: boolean;
29
32
  altKey: boolean;
30
33
  ctrlKey: boolean;
@@ -1,6 +1,6 @@
1
1
  // A key string for people: how a chord is shown (`⌘⇧Z`, `Ctrl+Shift+Z`, `Q`), and the key string a
2
2
  // press makes when someone sets a key ("press a key to change").
3
- import { isModifierCode, parseChord } from './chord.js';
3
+ import { isModifierCode, parseChord, typedKey } from './chord.js';
4
4
  /** The platform of this browser, by its own report; `other` outside a browser. */
5
5
  export function shortcutPlatform() {
6
6
  const nav = globalThis.navigator;
@@ -18,7 +18,8 @@ const NAMED = {
18
18
  };
19
19
  /** What a key place is called on a keyboard: `KeyQ` → `Q`, `Digit1` → `1`, `Numpad4` → `Num 4`. */
20
20
  export function keyLabel(code, layout) {
21
- const printed = layout?.get(code);
21
+ const byCharacter = /^(Key[A-Z]|Digit\d)$/.test(code) || typedKey(NAMED[code])?.code === code;
22
+ const printed = byCharacter ? undefined : layout?.get(code);
22
23
  if (printed && printed.trim())
23
24
  return printed.length === 1 ? printed.toUpperCase() : printed;
24
25
  if (/^Key[A-Z]$/.test(code))
@@ -29,6 +30,11 @@ export function keyLabel(code, layout) {
29
30
  return `Num ${code.slice(6)}`;
30
31
  return NAMED[code] ?? code;
31
32
  }
33
+ /** What a US keyboard types with Shift at each punctuation key. */
34
+ const SHIFTED = {
35
+ Minus: '_', Equal: '+', BracketLeft: '{', BracketRight: '}', Backslash: '|', Semicolon: ':', Quote: '"',
36
+ Backquote: '~', Comma: '<', Period: '>', Slash: '?',
37
+ };
32
38
  const SYMBOLS = { ctrl: '⌃', alt: '⌥', shift: '⇧', meta: '⌘' };
33
39
  const WORDS = { ctrl: 'Ctrl', alt: 'Alt', shift: 'Shift', meta: 'Meta' };
34
40
  /**
@@ -55,12 +61,16 @@ export function chordParts(keys, platform = shortcutPlatform(), layout) {
55
61
  parts.push(names.ctrl);
56
62
  if (held(chord.alt))
57
63
  parts.push(platform === 'apple' ? names.alt : 'Alt');
58
- if (held(chord.shift))
64
+ // A punctuation key is the character it types, Shift included: `Shift+Slash` is `?`.
65
+ // A command (⇧⌘,) is shown as written: its Shift is a key to press, not part of a character.
66
+ const punctuation = !ctrl && !meta && typedKey(NAMED[chord.code])?.code === chord.code ? SHIFTED[chord.code] : undefined;
67
+ if (held(chord.shift) && !punctuation)
59
68
  parts.push(names.shift);
60
69
  if (meta)
61
70
  parts.push(names.meta);
62
- // The help key is `?` wherever it lets Shift be held (its place is `/`): show what people press.
63
- parts.push(chord.code === 'Slash' && chord.shift === 'any' && !layout ? '?' : keyLabel(chord.code, layout));
71
+ // The help key is `?` wherever it lets Shift be held (`/` or `?`): show what people press.
72
+ const shifted = punctuation && (held(chord.shift) || (chord.code === 'Slash' && chord.shift === 'any'));
73
+ parts.push(shifted ? punctuation : keyLabel(chord.code, layout));
64
74
  return parts;
65
75
  }
66
76
  /** One line for a chord: `⌘⇧Z` on Apple keyboards, `Ctrl+Shift+Z` elsewhere. */
@@ -74,28 +84,44 @@ export function formatChord(keys, platform = shortcutPlatform(), layout) {
74
84
  }
75
85
  /**
76
86
  * The key string a press makes, for someone setting a key: exactly the modifiers held, the command
77
- * key of their platform written `Mod` (⌘ on Apple, Ctrl elsewhere) so the key works on both. `null`
78
- * while only modifiers are down, or for a press with no key place.
87
+ * key of their platform written `Mod` (⌘ on Apple, Ctrl elsewhere) so the key works on both. A letter
88
+ * or punctuation character typed is written as the key that types it on a US keyboard (`/` is
89
+ * `Slash` whichever keys typed it), the way the matcher reads it back. `null` while only modifiers
90
+ * are down, or for a press with no key place.
79
91
  */
80
92
  export function chordFromEvent(event, platform = shortcutPlatform()) {
81
93
  if (!event.code || isModifierCode(event.code))
82
94
  return null;
95
+ const typed = typedKey(event.key);
96
+ const punctuation = typed?.shift !== undefined;
97
+ // A punctuation character carries its own Shift, and the AltGr or ⌥ that typed it is no modifier.
98
+ const altGr = punctuation && event.altKey && event.ctrlKey && !event.metaKey;
99
+ // Field by field: a browser's KeyboardEvent keeps them on its prototype, where a spread does not reach.
100
+ const press = punctuation
101
+ ? {
102
+ // With ⌘ or Ctrl some browsers report the key unshifted: the Shift held is then part of the chord.
103
+ shiftKey: Boolean(typed.shift) || (event.shiftKey && !altGr && (event.ctrlKey || event.metaKey)),
104
+ altKey: false,
105
+ ctrlKey: altGr ? false : event.ctrlKey,
106
+ metaKey: event.metaKey,
107
+ }
108
+ : event;
83
109
  const parts = [];
84
110
  // ⌘ (Ctrl elsewhere) is written Mod so the key works on both — unless Ctrl and ⌘ are both held,
85
111
  // which Mod cannot say: then both by name.
86
- if (event.ctrlKey && event.metaKey)
112
+ if (press.ctrlKey && press.metaKey)
87
113
  parts.push('Ctrl', 'Meta');
88
- else if (platform === 'apple' ? event.metaKey : event.ctrlKey)
114
+ else if (platform === 'apple' ? press.metaKey : press.ctrlKey)
89
115
  parts.push('Mod');
90
- else if (event.ctrlKey)
116
+ else if (press.ctrlKey)
91
117
  parts.push('Ctrl');
92
- else if (event.metaKey)
118
+ else if (press.metaKey)
93
119
  parts.push('Meta');
94
- if (event.altKey)
120
+ if (press.altKey)
95
121
  parts.push('Alt');
96
- if (event.shiftKey)
122
+ if (press.shiftKey)
97
123
  parts.push('Shift');
98
- parts.push(event.code);
124
+ parts.push(typed?.code ?? event.code);
99
125
  const keys = parts.join('+');
100
126
  return parseChord(keys) ? keys : null;
101
127
  }
@@ -1,4 +1,4 @@
1
- export { parseChord, parseKeys, keysOverlap, chordMatches, chordsOverlap, isCommandChord, type Chord, type ModifierRule, type ModifierState, } from './chord.js';
1
+ export { parseChord, parseKeys, keysOverlap, chordMatch, chordMatches, chordsOverlap, typedKey, isCommandChord, type Chord, type ModifierRule, type ModifierState, type ChordMatch, type ChordPress, } from './chord.js';
2
2
  export { matchShortcut, createShortcutMatcher, SEQUENCE_WAIT_MS, resolveBindings, findClashes, claimKeys, isKeyClaimed, isTypingTarget, type ShortcutAction, type ShortcutNames, type ShortcutBindings, type ShortcutOverrides, type ShortcutEvent, } from './match.js';
3
3
  export { shortcutPlatform, keyLabel, chordParts, formatChord, chordFromEvent, mergedKeyParts, recordedKeys, type ShortcutPlatform, type KeyLayout, } from './format.js';
4
4
  export { EMPTY_CATALOGUE, parseCatalogue, localName, localAppName, localPartName, shortcutApps, sectionActions, findSection, type ShortcutCatalogue, type ShortcutSection, type ShortcutNamed, type ShortcutSheetColumn, type ShortcutSheetRow, } from './catalogue.js';
@@ -1,8 +1,9 @@
1
1
  // Keyboard shortcuts, one system for every app: the reader of the shortcut catalogue (every app's
2
2
  // actions and default keys, served by the backend) and of a person's own keys, and one matcher that
3
- // says which action a key press is. Keys are named by their place on the keyboard
4
- // (`KeyboardEvent.code`). Framework-free: no React, no DOM beyond reading a key event.
5
- export { parseChord, parseKeys, keysOverlap, chordMatches, chordsOverlap, isCommandChord, } from './chord.js';
3
+ // says which action a key press is. Keys are named as a US keyboard names them
4
+ // (`KeyboardEvent.code`) and matched by the character a press types, so they work on any keyboard.
5
+ // Framework-free: no React, no DOM beyond reading a key event.
6
+ export { parseChord, parseKeys, keysOverlap, chordMatch, chordMatches, chordsOverlap, typedKey, isCommandChord, } from './chord.js';
6
7
  export { matchShortcut, createShortcutMatcher, SEQUENCE_WAIT_MS, resolveBindings, findClashes, claimKeys, isKeyClaimed, isTypingTarget, } from './match.js';
7
8
  export { shortcutPlatform, keyLabel, chordParts, formatChord, chordFromEvent, mergedKeyParts, recordedKeys, } from './format.js';
8
9
  export { EMPTY_CATALOGUE, parseCatalogue, localName, localAppName, localPartName, shortcutApps, sectionActions, findSection, } from './catalogue.js';
@@ -58,7 +58,9 @@ export declare function isKeyClaimed(event: Pick<ShortcutEvent, 'key' | 'metaKey
58
58
  export declare function isTypingTarget(target: unknown): boolean;
59
59
  /**
60
60
  * The action this press is, or `null`. Not one while the person is typing in a field, nor a key a
61
- * surface holds; a widget that reads characters keeps its characters but not the ⌘ commands.
61
+ * surface holds; a widget that reads characters keeps its characters but not the ⌘ commands. A key
62
+ * that matches by the character typed wins over one that only matches by the key's place, whatever
63
+ * their order (Shift+7 types `/` on a Spanish keyboard: the `/` action, not the `7` one).
62
64
  */
63
65
  export declare function matchShortcut(event: ShortcutEvent, bindings: ShortcutBindings): string | null;
64
66
  /** How long the second key of a sequence (`G`, then `H`) may follow the first. */
@@ -1,7 +1,7 @@
1
1
  // Which action a key press is. Each app keeps what its actions DO; this only answers which one a
2
2
  // press names, so every app reads keys the same way: the modifiers, a field being typed in, and a
3
3
  // surface that holds letters of its own are decided here, once.
4
- import { chordMatches, isCommandChord, isModifierCode, keysOverlap, parseChord, parseKeys } from './chord.js';
4
+ import { chordMatch, chordMatches, isCommandChord, isModifierCode, keysOverlap, parseChord, parseKeys } from './chord.js';
5
5
  /**
6
6
  * Every action's keys: its defaults, or the person's override (unknown actions and keys that name no
7
7
  * chord are dropped). Catalogue order is kept, so the earlier catalogue wins where two match one press.
@@ -81,23 +81,29 @@ function readsCharacters(target) {
81
81
  }
82
82
  /**
83
83
  * The action this press is, or `null`. Not one while the person is typing in a field, nor a key a
84
- * surface holds; a widget that reads characters keeps its characters but not the ⌘ commands.
84
+ * surface holds; a widget that reads characters keeps its characters but not the ⌘ commands. A key
85
+ * that matches by the character typed wins over one that only matches by the key's place, whatever
86
+ * their order (Shift+7 types `/` on a Spanish keyboard: the `/` action, not the `7` one).
85
87
  */
86
88
  export function matchShortcut(event, bindings) {
87
89
  if (!event.code || isTypingTarget(event.target) || isKeyClaimed(event))
88
90
  return null;
89
91
  const characters = readsCharacters(event.target);
92
+ let byPlace = null;
90
93
  for (const [actionId, keys] of bindings) {
91
94
  for (const key of keys) {
92
95
  const chord = parseChord(key);
93
- if (!chord || !chordMatches(chord, event.code, event))
96
+ const how = chord && chordMatch(chord, event);
97
+ if (!chord || !how)
94
98
  continue;
95
- if (characters && !isCommandChord(chord))
96
- return null;
97
- return actionId;
99
+ if (how === 'character')
100
+ return characters && !isCommandChord(chord) ? null : actionId;
101
+ byPlace ??= { id: actionId, command: isCommandChord(chord) };
98
102
  }
99
103
  }
100
- return null;
104
+ if (!byPlace || (characters && !byPlace.command))
105
+ return null;
106
+ return byPlace.id;
101
107
  }
102
108
  // ---------------------------------------------------------------------------------------------
103
109
  // Sequences
@@ -137,7 +143,7 @@ export function createShortcutMatcher(waitMs = SEQUENCE_WAIT_MS, now = Date.now)
137
143
  return id;
138
144
  }
139
145
  if (known.some(([, [one]]) => chordMatches(one, event.code, event))) {
140
- first = { code: event.code, shiftKey: event.shiftKey, altKey: event.altKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey, at: now() };
146
+ first = { code: event.code, key: event.key, shiftKey: event.shiftKey, altKey: event.altKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey, at: now() };
141
147
  return null;
142
148
  }
143
149
  return matchShortcut(event, bindings);
@@ -0,0 +1,31 @@
1
+ import * as THREE from 'three';
2
+ import type { WorldSky } from './worldSkyFile.js';
3
+ /** The band's frame: `basis` maps band space (x toward the core, y the band's pole) to the scene. */
4
+ export interface BandFrame {
5
+ basis: THREE.Matrix4;
6
+ core: THREE.Vector3;
7
+ }
8
+ /** An equatorial direction in the scene frame (y = celestial north). */
9
+ export declare function equatorial(raDeg: number, decDeg: number, out?: THREE.Vector3): THREE.Vector3;
10
+ /** A seeded band, tilted `tiltDeg` from the equator, its core at a seeded longitude. */
11
+ export declare function seededBandFrame(seed: number, tiltDeg: number): BandFrame;
12
+ /**
13
+ * The Milky Way: the galactic plane (J2000 north pole and centre). From every catalogued host it
14
+ * is the same plane to well under a degree, so it holds for every world sky.
15
+ */
16
+ export declare function galacticBandFrame(): BandFrame;
17
+ /**
18
+ * The star buffer, exactly `count` stars (the device's star budget). A world sky first: its
19
+ * catalogue stars copied as they are (brightest first, so a small budget keeps the brightest), then
20
+ * its stand-ins, then seeded faint fill; a seeded sky is `count` seeded stars. Stars gather into the
21
+ * band by `band`.
22
+ */
23
+ export declare function buildStarGeometry(opts: {
24
+ seed: number;
25
+ count: number;
26
+ band: number;
27
+ frame: BandFrame;
28
+ world?: WorldSky | null;
29
+ }): THREE.BufferGeometry;
30
+ /** The constellation figures of a world sky, straight from the file. */
31
+ export declare function buildLineGeometry(world: WorldSky): THREE.BufferGeometry;