entangled-worlds-orbiters-shared 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/clock/Transport.d.ts +8 -2
- package/dist/clock/Transport.js +46 -7
- package/dist/clock/musical.js +13 -8
- package/dist/clock/sync.js +15 -2
- package/dist/loaders/glbLoader.js +17 -4
- package/dist/shortcuts/chord.d.ts +35 -0
- package/dist/shortcuts/chord.js +99 -0
- package/dist/shortcuts/entangledWorlds.d.ts +21 -0
- package/dist/shortcuts/entangledWorlds.js +40 -0
- package/dist/shortcuts/format.d.ts +44 -0
- package/dist/shortcuts/format.js +132 -0
- package/dist/shortcuts/index.d.ts +9 -0
- package/dist/shortcuts/index.js +13 -0
- package/dist/shortcuts/labels.d.ts +10 -0
- package/dist/shortcuts/labels.js +130 -0
- package/dist/shortcuts/match.d.ts +65 -0
- package/dist/shortcuts/match.js +145 -0
- package/dist/shortcuts/orbiters.d.ts +55 -0
- package/dist/shortcuts/orbiters.js +109 -0
- package/dist/shortcuts/overrides.d.ts +30 -0
- package/dist/shortcuts/overrides.js +81 -0
- package/dist/shortcuts/sections.d.ts +23 -0
- package/dist/shortcuts/sections.js +19 -0
- package/dist/shortcuts/site.d.ts +9 -0
- package/dist/shortcuts/site.js +25 -0
- package/package.json +5 -1
- package/src/clock/Transport.ts +37 -6
- package/src/clock/musical.ts +18 -8
- package/src/clock/sync.ts +15 -2
- package/src/loaders/glbLoader.ts +15 -6
- package/src/shortcuts/chord.ts +122 -0
- package/src/shortcuts/entangledWorlds.ts +51 -0
- package/src/shortcuts/format.ts +143 -0
- package/src/shortcuts/index.ts +69 -0
- package/src/shortcuts/labels.ts +139 -0
- package/src/shortcuts/match.ts +195 -0
- package/src/shortcuts/orbiters.ts +132 -0
- package/src/shortcuts/overrides.ts +110 -0
- package/src/shortcuts/sections.ts +36 -0
- package/src/shortcuts/site.ts +33 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// One key with its modifiers, written as a string: `Shift+KeyR`, `Mod+KeyZ`, `Shift?+Digit1`.
|
|
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:
|
|
5
|
+
//
|
|
6
|
+
// Shift, Alt, Ctrl, Meta held. One left out must NOT be held.
|
|
7
|
+
// Mod Ctrl or ⌘ (either, or both), the command key of every platform.
|
|
8
|
+
// Shift? (any, with ?) may be held or not: the action is the same either way.
|
|
9
|
+
//
|
|
10
|
+
// So `KeyQ` is Q alone, `Shift?+KeyQ` is Q with or without Shift, `Mod+Shift+KeyZ` is ⌘⇧Z or Ctrl⇧Z.
|
|
11
|
+
//
|
|
12
|
+
// Two chords with a space between are a sequence, pressed one after the other: `KeyG KeyH` is G,
|
|
13
|
+
// then H. A sequence is never one press, so `parseChord` names none; `parseKeys` reads both kinds.
|
|
14
|
+
|
|
15
|
+
/** What a modifier has to be for a chord to match: held, not held, or either. */
|
|
16
|
+
export type ModifierRule = 'held' | 'free' | 'any'
|
|
17
|
+
|
|
18
|
+
export interface Chord {
|
|
19
|
+
readonly code: string
|
|
20
|
+
readonly shift: ModifierRule
|
|
21
|
+
readonly alt: ModifierRule
|
|
22
|
+
readonly ctrl: ModifierRule
|
|
23
|
+
readonly meta: ModifierRule
|
|
24
|
+
/** Ctrl or ⌘: when `held`, at least one of the two is down, and neither is otherwise checked. */
|
|
25
|
+
readonly mod: ModifierRule
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The modifier state of a key press: the four flags of a `KeyboardEvent`. */
|
|
29
|
+
export interface ModifierState {
|
|
30
|
+
readonly shiftKey: boolean
|
|
31
|
+
readonly altKey: boolean
|
|
32
|
+
readonly ctrlKey: boolean
|
|
33
|
+
readonly metaKey: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MODIFIERS = { Shift: 'shift', Alt: 'alt', Ctrl: 'ctrl', Meta: 'meta', Mod: 'mod' } as const
|
|
37
|
+
type ModifierName = keyof typeof MODIFIERS
|
|
38
|
+
|
|
39
|
+
const CODE = /^[A-Za-z][A-Za-z0-9]*$/
|
|
40
|
+
|
|
41
|
+
const parsed = new Map<string, Chord | null>()
|
|
42
|
+
|
|
43
|
+
/** The chord a key string names, or `null` when it names none (an unknown modifier, a doubled one, no key). */
|
|
44
|
+
export function parseChord(keys: string): Chord | null {
|
|
45
|
+
const known = parsed.get(keys)
|
|
46
|
+
if (known !== undefined) return known
|
|
47
|
+
const chord = parse(keys)
|
|
48
|
+
parsed.set(keys, chord)
|
|
49
|
+
return chord
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parse(keys: string): Chord | null {
|
|
53
|
+
if (typeof keys !== 'string') return null
|
|
54
|
+
const parts = keys.split('+')
|
|
55
|
+
const code = parts.pop()
|
|
56
|
+
if (!code || !CODE.test(code)) return null
|
|
57
|
+
const rules: Record<(typeof MODIFIERS)[ModifierName], ModifierRule> = {
|
|
58
|
+
shift: 'free', alt: 'free', ctrl: 'free', meta: 'free', mod: 'free',
|
|
59
|
+
}
|
|
60
|
+
const seen = new Set<string>()
|
|
61
|
+
for (const part of parts) {
|
|
62
|
+
const optional = part.endsWith('?')
|
|
63
|
+
const name = (optional ? part.slice(0, -1) : part) as ModifierName
|
|
64
|
+
if (!Object.hasOwn(MODIFIERS, name) || seen.has(name)) return null
|
|
65
|
+
seen.add(name)
|
|
66
|
+
rules[MODIFIERS[name]] = optional ? 'any' : 'held'
|
|
67
|
+
}
|
|
68
|
+
// Mod already speaks for Ctrl and ⌘: naming one of them beside it says two things about one key.
|
|
69
|
+
if (rules.mod !== 'free' && (rules.ctrl !== 'free' || rules.meta !== 'free')) return null
|
|
70
|
+
return { code, ...rules }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const fits = (rule: ModifierRule, down: boolean) => rule === 'any' || (rule === 'held') === Boolean(down)
|
|
74
|
+
|
|
75
|
+
/** Whether a press of `code` with these modifiers is this chord. */
|
|
76
|
+
export function chordMatches(chord: Chord, code: string, state: ModifierState): boolean {
|
|
77
|
+
if (chord.code !== code) return false
|
|
78
|
+
if (!fits(chord.shift, state.shiftKey) || !fits(chord.alt, state.altKey)) return false
|
|
79
|
+
if (chord.mod === 'held') return Boolean(state.ctrlKey || state.metaKey)
|
|
80
|
+
if (chord.mod === 'any') return true
|
|
81
|
+
return fits(chord.ctrl, state.ctrlKey) && fits(chord.meta, state.metaKey)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A chord that needs Ctrl or ⌘: a command, never a character typed. */
|
|
85
|
+
export function isCommandChord(chord: Chord): boolean {
|
|
86
|
+
return chord.mod === 'held' || chord.ctrl === 'held' || chord.meta === 'held'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const STATES: ModifierState[] = Array.from({ length: 16 }, (_, i) => ({
|
|
90
|
+
shiftKey: Boolean(i & 1), altKey: Boolean(i & 2), ctrlKey: Boolean(i & 4), metaKey: Boolean(i & 8),
|
|
91
|
+
}))
|
|
92
|
+
|
|
93
|
+
/** Whether one press could be both chords — the test for a clash. */
|
|
94
|
+
export function chordsOverlap(a: Chord, b: Chord): boolean {
|
|
95
|
+
return a.code === b.code && STATES.some((state) => chordMatches(a, a.code, state) && chordMatches(b, b.code, state))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The chords a key string names, one or two (a sequence); `null` when it names none. */
|
|
99
|
+
export function parseKeys(keys: string): Chord[] | null {
|
|
100
|
+
if (typeof keys !== 'string') return null
|
|
101
|
+
const steps = keys.split(' ')
|
|
102
|
+
if (steps.length > 2) return null
|
|
103
|
+
const chords = steps.map(parseChord)
|
|
104
|
+
return chords.every((chord): chord is Chord => chord !== null) ? chords : null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether two key strings could answer the same presses — the test for a clash. A single key and a
|
|
109
|
+
* sequence that starts with it clash too: the single key would fire before the sequence went on.
|
|
110
|
+
*/
|
|
111
|
+
export function keysOverlap(a: string, b: string): boolean {
|
|
112
|
+
const as = parseKeys(a)
|
|
113
|
+
const bs = parseKeys(b)
|
|
114
|
+
if (!as || !bs) return false
|
|
115
|
+
if (as.length !== bs.length) return chordsOverlap(as[0], bs[0])
|
|
116
|
+
return as.every((chord, i) => chordsOverlap(chord, bs[i]))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const MODIFIER_CODES = new Set(['ShiftLeft', 'ShiftRight', 'AltLeft', 'AltRight', 'ControlLeft', 'ControlRight', 'MetaLeft', 'MetaRight', 'CapsLock', 'Fn', 'FnLock'])
|
|
120
|
+
|
|
121
|
+
/** A key that is only a modifier (or Caps Lock, Fn): never a shortcut's key on its own. */
|
|
122
|
+
export const isModifierCode = (code: string): boolean => MODIFIER_CODES.has(code)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The Entangled Worlds catalogues: the studio (where a world's clip is made) and the full-window
|
|
2
|
+
// player. Escape and Enter inside dialogs stay with the dialogs and are not listed here.
|
|
3
|
+
|
|
4
|
+
import type { ShortcutAction } from './match.js'
|
|
5
|
+
|
|
6
|
+
export const EW_STUDIO_SECTION = 'ew-studio'
|
|
7
|
+
export const EW_PLAYER_SECTION = 'ew-player'
|
|
8
|
+
|
|
9
|
+
/** The studio's actions all act on what is open in it; the player has no other scope. */
|
|
10
|
+
export type EntangledWorldsShortcut = ShortcutAction<'studio' | 'player'>
|
|
11
|
+
|
|
12
|
+
const studio = (id: string, label: string, defaultKeys: readonly string[], group: string, command?: false): EntangledWorldsShortcut =>
|
|
13
|
+
Object.freeze({
|
|
14
|
+
id: `ew.studio.${id}`, section: EW_STUDIO_SECTION, label, defaultKeys: Object.freeze([...defaultKeys]), scope: 'studio' as const,
|
|
15
|
+
group, ...(command === false ? { command } : {}),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
/** How the studio's command menu groups its actions, in its order. */
|
|
19
|
+
export const EW_STUDIO_COMMAND_GROUPS = Object.freeze([
|
|
20
|
+
{ id: 'edit', label: 'Edit' },
|
|
21
|
+
{ id: 'playback', label: 'Playback' },
|
|
22
|
+
{ id: 'steps', label: 'Steps' },
|
|
23
|
+
{ id: 'help', label: 'Help' },
|
|
24
|
+
] as const)
|
|
25
|
+
|
|
26
|
+
export const EW_STUDIO_SHORTCUTS: readonly EntangledWorldsShortcut[] = Object.freeze([
|
|
27
|
+
studio('undo', 'Undo', ['Mod+KeyZ'], 'edit'),
|
|
28
|
+
studio('redo', 'Redo', ['Mod+Shift+KeyZ'], 'edit'),
|
|
29
|
+
studio('duplicate', 'Duplicate the selection', ['Mod+KeyD'], 'edit'),
|
|
30
|
+
studio('copy', 'Copy the selection', ['Mod+KeyC'], 'edit'),
|
|
31
|
+
studio('paste', 'Paste', ['Mod+KeyV'], 'edit'),
|
|
32
|
+
studio('playPause', 'Play or pause', ['Shift?+Space'], 'playback'),
|
|
33
|
+
studio('remove', 'Remove the selection', ['Shift?+Delete', 'Shift?+Backspace'], 'edit'),
|
|
34
|
+
studio('step.audio', 'Audio step', ['Digit1', 'Numpad1'], 'steps'),
|
|
35
|
+
studio('step.animations', 'Animations step', ['Digit2', 'Numpad2'], 'steps'),
|
|
36
|
+
studio('step.third', 'Export step (Release where there is no export)', ['Digit3', 'Numpad3'], 'steps'),
|
|
37
|
+
studio('step.release', 'Release step', ['Digit4', 'Numpad4'], 'steps'),
|
|
38
|
+
studio('help', 'Keyboard shortcuts', ['Shift?+Slash'], 'help'),
|
|
39
|
+
// Opens the menu itself, so the menu does not list it.
|
|
40
|
+
studio('commands', 'Commands', ['Mod+KeyK'], 'help', false),
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
export const EW_PLAYER_SHORTCUTS: readonly EntangledWorldsShortcut[] = Object.freeze([
|
|
44
|
+
Object.freeze({
|
|
45
|
+
id: 'ew.player.playPause',
|
|
46
|
+
section: EW_PLAYER_SECTION,
|
|
47
|
+
label: 'Play or pause',
|
|
48
|
+
defaultKeys: Object.freeze(['Shift?+Alt?+Ctrl?+Meta?+Space']),
|
|
49
|
+
scope: 'player' as const,
|
|
50
|
+
}),
|
|
51
|
+
])
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// A key string for people: how a chord is shown (`⌘⇧Z`, `Ctrl+Shift+Z`, `Q`), and the key string a
|
|
2
|
+
// press makes when someone sets a key ("press a key to change").
|
|
3
|
+
|
|
4
|
+
import { isModifierCode, parseChord, type Chord } from './chord.js'
|
|
5
|
+
|
|
6
|
+
/** Apple keyboards show modifiers as symbols, and their command key is ⌘ (`Mod`); others write words and Ctrl. */
|
|
7
|
+
export type ShortcutPlatform = 'apple' | 'other'
|
|
8
|
+
|
|
9
|
+
/** The platform of this browser, by its own report; `other` outside a browser. */
|
|
10
|
+
export function shortcutPlatform(): ShortcutPlatform {
|
|
11
|
+
const nav = (globalThis as { navigator?: { platform?: string; userAgentData?: { platform?: string } } }).navigator
|
|
12
|
+
const name = nav?.userAgentData?.platform ?? nav?.platform ?? ''
|
|
13
|
+
return /mac|iphone|ipad|ipod/i.test(name) ? 'apple' : 'other'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The character a key place prints on this person's keyboard, when the browser can say
|
|
18
|
+
* (`navigator.keyboard.getLayoutMap()`, Chromium only); a key place it does not know is shown from
|
|
19
|
+
* its name.
|
|
20
|
+
*/
|
|
21
|
+
export type KeyLayout = ReadonlyMap<string, string>
|
|
22
|
+
|
|
23
|
+
const NAMED: Readonly<Record<string, string>> = {
|
|
24
|
+
Space: 'Space', Enter: 'Enter', Escape: 'Esc', Tab: 'Tab', Backspace: 'Backspace', Delete: 'Delete',
|
|
25
|
+
Insert: 'Insert', Home: 'Home', End: 'End', PageUp: 'Page up', PageDown: 'Page down',
|
|
26
|
+
ArrowUp: '↑', ArrowDown: '↓', ArrowLeft: '←', ArrowRight: '→',
|
|
27
|
+
Minus: '-', Equal: '=', BracketLeft: '[', BracketRight: ']', Backslash: '\\', Semicolon: ';', Quote: "'",
|
|
28
|
+
Backquote: '`', Comma: ',', Period: '.', Slash: '/', IntlBackslash: '§',
|
|
29
|
+
NumpadAdd: 'Num +', NumpadSubtract: 'Num -', NumpadMultiply: 'Num *', NumpadDivide: 'Num /',
|
|
30
|
+
NumpadDecimal: 'Num .', NumpadEnter: 'Num Enter',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What a key place is called on a keyboard: `KeyQ` → `Q`, `Digit1` → `1`, `Numpad4` → `Num 4`. */
|
|
34
|
+
export function keyLabel(code: string, layout?: KeyLayout): string {
|
|
35
|
+
const printed = layout?.get(code)
|
|
36
|
+
if (printed && printed.trim()) return printed.length === 1 ? printed.toUpperCase() : printed
|
|
37
|
+
if (/^Key[A-Z]$/.test(code)) return code.slice(3)
|
|
38
|
+
if (/^Digit\d$/.test(code)) return code.slice(5)
|
|
39
|
+
if (/^Numpad\d$/.test(code)) return `Num ${code.slice(6)}`
|
|
40
|
+
return NAMED[code] ?? code
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SYMBOLS = { ctrl: '⌃', alt: '⌥', shift: '⇧', meta: '⌘' } as const
|
|
44
|
+
const WORDS = { ctrl: 'Ctrl', alt: 'Alt', shift: 'Shift', meta: 'Meta' } as const
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The parts a chord is shown as, one per key to press: modifiers first, in the order the platform
|
|
48
|
+
* writes them, then the key. A modifier the chord merely allows (`Shift?`) is not shown — it is not
|
|
49
|
+
* a key anyone has to press. `null` for a key string that names no chord.
|
|
50
|
+
*/
|
|
51
|
+
export function chordParts(keys: string, platform: ShortcutPlatform = shortcutPlatform(), layout?: KeyLayout): string[] | null {
|
|
52
|
+
// A sequence: each key's parts, one after the other (`G`, `H`).
|
|
53
|
+
if (typeof keys === 'string' && keys.includes(' ')) {
|
|
54
|
+
const steps = keys.split(' ').map((step) => (step.includes(' ') ? null : chordParts(step, platform, layout)))
|
|
55
|
+
return steps.length === 2 && steps.every(Boolean) ? (steps as string[][]).flat() : null
|
|
56
|
+
}
|
|
57
|
+
const chord = parseChord(keys)
|
|
58
|
+
if (!chord) return null
|
|
59
|
+
const held = (rule: Chord['shift']) => rule === 'held'
|
|
60
|
+
const ctrl = held(chord.ctrl) || (held(chord.mod) && platform === 'other')
|
|
61
|
+
const meta = held(chord.meta) || (held(chord.mod) && platform === 'apple')
|
|
62
|
+
const names = platform === 'apple' ? SYMBOLS : WORDS
|
|
63
|
+
// Apple's order is ⌃⌥⇧⌘; elsewhere Ctrl, Alt, Shift (and the rare Meta last).
|
|
64
|
+
const parts: string[] = []
|
|
65
|
+
if (ctrl) parts.push(names.ctrl)
|
|
66
|
+
if (held(chord.alt)) parts.push(platform === 'apple' ? names.alt : 'Alt')
|
|
67
|
+
if (held(chord.shift)) parts.push(names.shift)
|
|
68
|
+
if (meta) parts.push(names.meta)
|
|
69
|
+
// The help key is `?` wherever it lets Shift be held (its place is `/`): show what people press.
|
|
70
|
+
parts.push(chord.code === 'Slash' && chord.shift === 'any' && !layout ? '?' : keyLabel(chord.code, layout))
|
|
71
|
+
return parts
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One line for a chord: `⌘⇧Z` on Apple keyboards, `Ctrl+Shift+Z` elsewhere. */
|
|
75
|
+
export function formatChord(keys: string, platform: ShortcutPlatform = shortcutPlatform(), layout?: KeyLayout): string {
|
|
76
|
+
if (keys.includes(' ')) return keys.split(' ').map((step) => formatChord(step, platform, layout)).join(' ')
|
|
77
|
+
const parts = chordParts(keys, platform, layout)
|
|
78
|
+
if (!parts) return keys
|
|
79
|
+
return parts.join(platform === 'apple' ? '' : '+')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The key string a press makes, for someone setting a key: exactly the modifiers held, the command
|
|
85
|
+
* key of their platform written `Mod` (⌘ on Apple, Ctrl elsewhere) so the key works on both. `null`
|
|
86
|
+
* while only modifiers are down, or for a press with no key place.
|
|
87
|
+
*/
|
|
88
|
+
export function chordFromEvent(
|
|
89
|
+
event: { code: string; shiftKey: boolean; altKey: boolean; ctrlKey: boolean; metaKey: boolean },
|
|
90
|
+
platform: ShortcutPlatform = shortcutPlatform(),
|
|
91
|
+
): string | null {
|
|
92
|
+
if (!event.code || isModifierCode(event.code)) return null
|
|
93
|
+
const parts: string[] = []
|
|
94
|
+
// ⌘ (Ctrl elsewhere) is written Mod so the key works on both — unless Ctrl and ⌘ are both held,
|
|
95
|
+
// which Mod cannot say: then both by name.
|
|
96
|
+
if (event.ctrlKey && event.metaKey) parts.push('Ctrl', 'Meta')
|
|
97
|
+
else if (platform === 'apple' ? event.metaKey : event.ctrlKey) parts.push('Mod')
|
|
98
|
+
else if (event.ctrlKey) parts.push('Ctrl')
|
|
99
|
+
else if (event.metaKey) parts.push('Meta')
|
|
100
|
+
if (event.altKey) parts.push('Alt')
|
|
101
|
+
if (event.shiftKey) parts.push('Shift')
|
|
102
|
+
parts.push(event.code)
|
|
103
|
+
const keys = parts.join('+')
|
|
104
|
+
return parseChord(keys) ? keys : null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const sameModifiers = (a: Chord, b: Chord) =>
|
|
108
|
+
a.shift === b.shift && a.alt === b.alt && a.ctrl === b.ctrl && a.meta === b.meta && a.mod === b.mod
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The keys of several actions shown as one ("1 2 3", "Q W E", "⌥ 0–9"): when each has exactly one
|
|
112
|
+
* key and they differ only in the last key, the shared modifiers once and then the keys. Otherwise
|
|
113
|
+
* every key of every action, as it is. Read from the keys in force, so a person's own keys show.
|
|
114
|
+
*/
|
|
115
|
+
export function mergedKeyParts(
|
|
116
|
+
keysPerAction: ReadonlyArray<readonly string[]>,
|
|
117
|
+
platform: ShortcutPlatform = shortcutPlatform(),
|
|
118
|
+
layout?: KeyLayout,
|
|
119
|
+
): string[][] {
|
|
120
|
+
const chords = keysPerAction.map((keys) => (keys.length === 1 ? parseChord(keys[0]) : null))
|
|
121
|
+
const first = chords[0]
|
|
122
|
+
if (keysPerAction.length > 1 && first && chords.every((c) => c && sameModifiers(c, first))) {
|
|
123
|
+
const modifiers = (chordParts(keysPerAction[0][0], platform, layout) ?? []).slice(0, -1)
|
|
124
|
+
const labels = chords.map((c) => keyLabel(c!.code, layout))
|
|
125
|
+
const digits = labels.join('')
|
|
126
|
+
// The whole number row, in the row's order: one range.
|
|
127
|
+
const tail = digits === '1234567890' ? ['0–9'] : [labels.join(' ')]
|
|
128
|
+
return [[...modifiers, ...tail]]
|
|
129
|
+
}
|
|
130
|
+
return keysPerAction.flatMap((keys) => keys.map((k) => chordParts(k, platform, layout) ?? [k]))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A key someone just pressed for an action, as it is stored: when every default key of the action
|
|
135
|
+
* lets Shift be held (`Shift?`, the play keys that work while selecting several Orbiters) and the
|
|
136
|
+
* press had no Shift, the new key lets it be held too, so the action keeps working the same way.
|
|
137
|
+
*/
|
|
138
|
+
export function recordedKeys(defaultKeys: readonly string[], pressed: string): string {
|
|
139
|
+
const chord = parseChord(pressed)
|
|
140
|
+
if (!chord || chord.shift !== 'free' || defaultKeys.length === 0) return pressed
|
|
141
|
+
const shiftFree = defaultKeys.every((keys) => parseChord(keys)?.shift === 'any')
|
|
142
|
+
return shiftFree ? `Shift?+${pressed}` : pressed
|
|
143
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Keyboard shortcuts, one system for every app: each app's catalogue of actions (id, section, label,
|
|
2
|
+
// default keys, scope) and one matcher that says which action a key press is. Keys are named by
|
|
3
|
+
// their place on the keyboard (`KeyboardEvent.code`). Framework-free: no React, no DOM beyond reading
|
|
4
|
+
// a key event.
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
parseChord,
|
|
8
|
+
parseKeys,
|
|
9
|
+
keysOverlap,
|
|
10
|
+
chordMatches,
|
|
11
|
+
chordsOverlap,
|
|
12
|
+
isCommandChord,
|
|
13
|
+
type Chord,
|
|
14
|
+
type ModifierRule,
|
|
15
|
+
type ModifierState,
|
|
16
|
+
} from './chord.js'
|
|
17
|
+
export {
|
|
18
|
+
matchShortcut,
|
|
19
|
+
createShortcutMatcher,
|
|
20
|
+
SEQUENCE_WAIT_MS,
|
|
21
|
+
resolveBindings,
|
|
22
|
+
findClashes,
|
|
23
|
+
claimKeys,
|
|
24
|
+
isKeyClaimed,
|
|
25
|
+
isTypingTarget,
|
|
26
|
+
type ShortcutAction,
|
|
27
|
+
type ShortcutBindings,
|
|
28
|
+
type ShortcutOverrides,
|
|
29
|
+
type ShortcutEvent,
|
|
30
|
+
} from './match.js'
|
|
31
|
+
export {
|
|
32
|
+
ORBITERS_PLAY_SHORTCUTS,
|
|
33
|
+
ORBITERS_STUDIO_SHORTCUTS,
|
|
34
|
+
ORBITERS_PLAY_SECTION,
|
|
35
|
+
ORBITERS_STUDIO_SECTION,
|
|
36
|
+
ORBITERS_KEYBOARD_SURFACE_KEYS,
|
|
37
|
+
ORBITERS_COMMAND_GROUPS,
|
|
38
|
+
ORBITERS_PLAY_SHEET,
|
|
39
|
+
type OrbitersShortcut,
|
|
40
|
+
type OrbitersShortcutScope,
|
|
41
|
+
} from './orbiters.js'
|
|
42
|
+
export {
|
|
43
|
+
shortcutPlatform,
|
|
44
|
+
keyLabel,
|
|
45
|
+
chordParts,
|
|
46
|
+
formatChord,
|
|
47
|
+
chordFromEvent,
|
|
48
|
+
mergedKeyParts,
|
|
49
|
+
recordedKeys,
|
|
50
|
+
type ShortcutPlatform,
|
|
51
|
+
type KeyLayout,
|
|
52
|
+
} from './format.js'
|
|
53
|
+
export { SHORTCUT_SECTIONS, SHORTCUT_APPS, type ShortcutSection, type ShortcutAppId } from './sections.js'
|
|
54
|
+
export { SITE_SHORTCUTS, SITE_SECTION, type SiteShortcut } from './site.js'
|
|
55
|
+
export {
|
|
56
|
+
EW_STUDIO_SHORTCUTS,
|
|
57
|
+
EW_PLAYER_SHORTCUTS,
|
|
58
|
+
EW_STUDIO_SECTION,
|
|
59
|
+
EW_PLAYER_SECTION,
|
|
60
|
+
EW_STUDIO_COMMAND_GROUPS,
|
|
61
|
+
type EntangledWorldsShortcut,
|
|
62
|
+
} from './entangledWorlds.js'
|
|
63
|
+
export { shortcutLabel, shortcutSectionLabel, shortcutName } from './labels.js'
|
|
64
|
+
export {
|
|
65
|
+
createShortcutOverridesSource,
|
|
66
|
+
parseOverrides,
|
|
67
|
+
type ShortcutOverridesSource,
|
|
68
|
+
type ReadAccountSettings,
|
|
69
|
+
} from './overrides.js'
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// What the actions and sections are called in each language the platform speaks, beside the
|
|
2
|
+
// English in the catalogues. One table, read by every app and the settings page, so a name is
|
|
3
|
+
// translated once.
|
|
4
|
+
|
|
5
|
+
import type { ShortcutAction } from './match.js'
|
|
6
|
+
|
|
7
|
+
type Names = Readonly<Record<string, string>>
|
|
8
|
+
|
|
9
|
+
const ES: Names = Object.freeze({
|
|
10
|
+
"ew.player.playPause": "Reproducir o pausar",
|
|
11
|
+
"ew.studio.commands": "Comandos",
|
|
12
|
+
"ew.studio.copy": "Copiar la selección",
|
|
13
|
+
"ew.studio.duplicate": "Duplicar la selección",
|
|
14
|
+
"ew.studio.help": "Atajos de teclado",
|
|
15
|
+
"ew.studio.paste": "Pegar",
|
|
16
|
+
"ew.studio.playPause": "Reproducir o pausar",
|
|
17
|
+
"ew.studio.redo": "Rehacer",
|
|
18
|
+
"ew.studio.remove": "Quitar la selección",
|
|
19
|
+
"ew.studio.step.animations": "Paso de animaciones",
|
|
20
|
+
"ew.studio.step.audio": "Paso de audio",
|
|
21
|
+
"ew.studio.step.release": "Paso de publicación",
|
|
22
|
+
"ew.studio.step.third": "Paso de exportación (Publicación donde no hay exportación)",
|
|
23
|
+
"ew.studio.undo": "Deshacer",
|
|
24
|
+
"orbiters.play.body.moon": "Luna",
|
|
25
|
+
"orbiters.play.body.star": "Estrella",
|
|
26
|
+
"orbiters.play.body.world": "Mundo",
|
|
27
|
+
"orbiters.play.dimension.1": "Primera dimensión",
|
|
28
|
+
"orbiters.play.dimension.2": "Segunda dimensión",
|
|
29
|
+
"orbiters.play.dimension.3": "Tercera dimensión",
|
|
30
|
+
"orbiters.play.help": "Atajos de teclado",
|
|
31
|
+
"orbiters.play.history.redo": "Rehacer",
|
|
32
|
+
"orbiters.play.history.undo": "Deshacer",
|
|
33
|
+
"orbiters.play.midiLearn": "MIDI learn",
|
|
34
|
+
"orbiters.play.panel.cosmicLfo": "Cosmic LFO",
|
|
35
|
+
"orbiters.play.panel.jamming": "Jamming",
|
|
36
|
+
"orbiters.play.panel.playback": "Reproducción",
|
|
37
|
+
"orbiters.play.panel.sensors": "Sensores",
|
|
38
|
+
"orbiters.play.performanceMode": "Modo rendimiento",
|
|
39
|
+
"orbiters.play.rec.hold": "Grabar mientras se mantiene",
|
|
40
|
+
"orbiters.play.rec.toggle": "Activar o desactivar Rec",
|
|
41
|
+
"orbiters.play.snapshot.recall.0": "Recuperar instantánea 0",
|
|
42
|
+
"orbiters.play.snapshot.recall.1": "Recuperar instantánea 1",
|
|
43
|
+
"orbiters.play.snapshot.recall.2": "Recuperar instantánea 2",
|
|
44
|
+
"orbiters.play.snapshot.recall.3": "Recuperar instantánea 3",
|
|
45
|
+
"orbiters.play.snapshot.recall.4": "Recuperar instantánea 4",
|
|
46
|
+
"orbiters.play.snapshot.recall.5": "Recuperar instantánea 5",
|
|
47
|
+
"orbiters.play.snapshot.recall.6": "Recuperar instantánea 6",
|
|
48
|
+
"orbiters.play.snapshot.recall.7": "Recuperar instantánea 7",
|
|
49
|
+
"orbiters.play.snapshot.recall.8": "Recuperar instantánea 8",
|
|
50
|
+
"orbiters.play.snapshot.recall.9": "Recuperar instantánea 9",
|
|
51
|
+
"orbiters.play.snapshot.store.0": "Guardar instantánea 0",
|
|
52
|
+
"orbiters.play.snapshot.store.1": "Guardar instantánea 1",
|
|
53
|
+
"orbiters.play.snapshot.store.2": "Guardar instantánea 2",
|
|
54
|
+
"orbiters.play.snapshot.store.3": "Guardar instantánea 3",
|
|
55
|
+
"orbiters.play.snapshot.store.4": "Guardar instantánea 4",
|
|
56
|
+
"orbiters.play.snapshot.store.5": "Guardar instantánea 5",
|
|
57
|
+
"orbiters.play.snapshot.store.6": "Guardar instantánea 6",
|
|
58
|
+
"orbiters.play.snapshot.store.7": "Guardar instantánea 7",
|
|
59
|
+
"orbiters.play.snapshot.store.8": "Guardar instantánea 8",
|
|
60
|
+
"orbiters.play.snapshot.store.9": "Guardar instantánea 9",
|
|
61
|
+
"orbiters.play.transport.restart": "Reproducir desde el principio",
|
|
62
|
+
"orbiters.play.transport.toggle": "Reproducir o pausar",
|
|
63
|
+
"orbiters.studio.map": "Mapear",
|
|
64
|
+
"orbiters.studio.redo": "Rehacer",
|
|
65
|
+
"orbiters.studio.undo": "Deshacer",
|
|
66
|
+
"site.commands": "Comandos",
|
|
67
|
+
"site.search": "Buscar",
|
|
68
|
+
"site.go.home": "Inicio",
|
|
69
|
+
"site.go.playground": "Playground",
|
|
70
|
+
"site.go.notifications": "Notificaciones",
|
|
71
|
+
"site.go.audios": "Mis audios",
|
|
72
|
+
"site.go.orbiters": "Mis Orbiters",
|
|
73
|
+
"site.go.worlds": "Mis Entangled Worlds",
|
|
74
|
+
"site.go.collections": "Mis colecciones",
|
|
75
|
+
"site.go.stars": "Mis estrellas",
|
|
76
|
+
"site.new.orbiter": "Nuevo Orbiter",
|
|
77
|
+
"site.new.audio": "Nuevo audio",
|
|
78
|
+
"site.new.world": "Nuevo Entangled World",
|
|
79
|
+
"site.settings": "Abrir la configuración",
|
|
80
|
+
"section:site": "Plantasia Space",
|
|
81
|
+
"app:plantasia-space": "Plantasia Space",
|
|
82
|
+
"app:orbiters": "Orbiters",
|
|
83
|
+
"app:entangled-worlds": "Entangled Worlds",
|
|
84
|
+
"part:orbiters-play": "Tocar",
|
|
85
|
+
"part:orbiters-studio": "Estudio",
|
|
86
|
+
"part:ew-studio": "Estudio",
|
|
87
|
+
"part:ew-player": "Reproductor",
|
|
88
|
+
"section:orbiters-play": "Orbiters: tocar",
|
|
89
|
+
"section:orbiters-studio": "Orbiter Studio",
|
|
90
|
+
"section:ew-studio": "Estudio de Entangled Worlds",
|
|
91
|
+
"section:ew-player": "Reproductor de Entangled Worlds",
|
|
92
|
+
"orbiters.play.screenCapture": "Captura de pantalla",
|
|
93
|
+
"orbiters.play.share": "Compartir",
|
|
94
|
+
"orbiters.play.fullscreen": "Pantalla completa",
|
|
95
|
+
"orbiters.play.commands": "Comandos",
|
|
96
|
+
"group:edit": "Edición",
|
|
97
|
+
"group:playback": "Reproducción",
|
|
98
|
+
"group:steps": "Pasos",
|
|
99
|
+
"group:help": "Ayuda",
|
|
100
|
+
"group:transport": "Transporte",
|
|
101
|
+
"group:view": "Vista",
|
|
102
|
+
"group:snapshots": "Instantáneas",
|
|
103
|
+
"group:panels": "Paneles",
|
|
104
|
+
"group:orbiter": "Orbiter",
|
|
105
|
+
"group:studio": "Estudio",
|
|
106
|
+
"row:playPause": "Reproducir / pausar",
|
|
107
|
+
"row:playFromStart": "Reproducir desde el inicio",
|
|
108
|
+
"row:recHeld": "REC mientras se mantiene",
|
|
109
|
+
"row:recLatch": "REC fijo",
|
|
110
|
+
"row:undo": "Deshacer",
|
|
111
|
+
"row:redo": "Rehacer",
|
|
112
|
+
"row:dimensions": "Dimensión I · II · III",
|
|
113
|
+
"row:bodies": "Mundo · Luna · Estrella",
|
|
114
|
+
"row:snapshotRecall": "Recuperar instantánea",
|
|
115
|
+
"row:snapshotStore": "Guardar instantánea",
|
|
116
|
+
"row:jamming": "Jamming",
|
|
117
|
+
"row:playback": "Reproducción",
|
|
118
|
+
"row:cosmicLfo": "Cosmic LFO",
|
|
119
|
+
"row:sensors": "Sensores",
|
|
120
|
+
"row:midiLearn": "MIDI learn",
|
|
121
|
+
"row:performanceMode": "Modo rendimiento",
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const NAMES: Readonly<Record<string, Names>> = Object.freeze({ es: ES })
|
|
125
|
+
|
|
126
|
+
/** An action's name in a language (`es`, `es-AR`), falling back to the catalogue's English. */
|
|
127
|
+
export function shortcutLabel(action: Pick<ShortcutAction, 'id' | 'label'>, locale?: string): string {
|
|
128
|
+
return NAMES[(locale ?? '').slice(0, 2).toLowerCase()]?.[action.id] ?? action.label
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A section's name in a language, falling back to the English. */
|
|
132
|
+
export function shortcutSectionLabel(section: { id: string; label: string }, locale?: string): string {
|
|
133
|
+
return NAMES[(locale ?? '').slice(0, 2).toLowerCase()]?.[`section:${section.id}`] ?? section.label
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Any other named thing of the shortcuts (a command group `group:<id>`, a sheet row `row:<id>`). */
|
|
137
|
+
export function shortcutName(key: string, english: string, locale?: string): string {
|
|
138
|
+
return NAMES[(locale ?? '').slice(0, 2).toLowerCase()]?.[key] ?? english
|
|
139
|
+
}
|