From 83146fa85f996069f6b467a1f289ea02c0bdaffa Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Wed, 3 Jan 2024 20:51:17 +0200 Subject: [PATCH] Implement piano-style mapper ref #1 --- .../{index.spec.ts => keyboard.spec.ts} | 2 +- src/__tests__/piano.spec.ts | 81 +++++ src/coordinates.ts | 155 ++++++++ src/index.ts | 340 +----------------- src/keyboard.ts | 202 +++++++++++ src/piano.ts | 44 +++ 6 files changed, 486 insertions(+), 338 deletions(-) rename src/__tests__/{index.spec.ts => keyboard.spec.ts} (97%) create mode 100644 src/__tests__/piano.spec.ts create mode 100644 src/coordinates.ts create mode 100644 src/keyboard.ts create mode 100644 src/piano.ts diff --git a/src/__tests__/index.spec.ts b/src/__tests__/keyboard.spec.ts similarity index 97% rename from src/__tests__/index.spec.ts rename to src/__tests__/keyboard.spec.ts index 64dbb95..4384159 100644 --- a/src/__tests__/index.spec.ts +++ b/src/__tests__/keyboard.spec.ts @@ -1,5 +1,5 @@ import {describe, it, expect, vi} from 'vitest'; -import {CoordinateKeyboardEvent, Keyboard} from '..'; +import {CoordinateKeyboardEvent, Keyboard} from '../keyboard'; describe('Isomoprhic QWERTY keyboard', () => { it('only triggers once on multiple repeats of an event', () => { diff --git a/src/__tests__/piano.spec.ts b/src/__tests__/piano.spec.ts new file mode 100644 index 0000000..98fe064 --- /dev/null +++ b/src/__tests__/piano.spec.ts @@ -0,0 +1,81 @@ +import {describe, it, expect} from 'vitest'; +import {pianoMap} from '../piano'; +import {CODES_LAYER_1} from '../coordinates'; + +describe('Piano-style index mapper', () => { + it('maps a chromatic scale diatonically starting from KeyA', () => { + const {indexByCode, coordsByIndex} = pianoMap([ + 2, // C + 1, // C# + 2, // D + 1, // D# + 2, // E + 2, // F + 1, // F# + 2, // G + 1, // G# + 2, // A + 1, // A# + 2, // B + 2, // c + 1, // c# + 2, // d + 1, // d# + 2, // e + 2, // f + 1, // f# + 2, // g + 1, // g# + 2, // a + 1, // a# + 2, // b + ]); + const asdfRow = CODES_LAYER_1[2] + .slice(1) + .map(code => indexByCode.get(code!)); + const qwertyRow = CODES_LAYER_1[1] + .slice(1) + .map(code => indexByCode.get(code!)); + expect(coordsByIndex).toEqual([ + [0, 2, 1], + [1, 1, 1], + [1, 2, 1], + [2, 1, 1], + [2, 2, 1], + [3, 2, 1], + [4, 1, 1], + [4, 2, 1], + [5, 1, 1], + [5, 2, 1], + [6, 1, 1], + [6, 2, 1], + [7, 2, 1], + [8, 1, 1], + [8, 2, 1], + [9, 1, 1], + [9, 2, 1], + [10, 2, 1], + [11, 1, 1], + [11, 2, 1], + undefined, + undefined, + undefined, + undefined, + ]); + expect(asdfRow).toEqual([0, 2, 4, 5, 7, 9, 11, 12, 14, 16, 17, 19]); + expect(qwertyRow).toEqual([ + undefined, + 1, + 3, + undefined, + 6, + 8, + 10, + undefined, + 13, + 15, + undefined, + 18, + ]); + }); +}); diff --git a/src/coordinates.ts b/src/coordinates.ts new file mode 100644 index 0000000..4787795 --- /dev/null +++ b/src/coordinates.ts @@ -0,0 +1,155 @@ +/* +Split the keyboard into xy-planes along a z-coordinate for different contiguous regions of keys +*/ + +export type Coords3D = [number, number, number]; + +const ORIGIN_LAYER_0 = 0; +/** + * Key codes for the row consisting of Esc and FN keys. + */ +export const CODES_LAYER_0 = [ + [ + 'Escape', + 'F1', + 'F2', + 'F3', + 'F4', + 'F5', + 'F6', + 'F7', + 'F8', + 'F9', + 'F10', + 'F11', + 'F12', + ], +]; + +const ORIGIN_LAYER_1 = -1; +/** + * Key codes for the rows containing the digits, qwerty, asdf and zxcv. + */ +export const CODES_LAYER_1 = [ + [ + 'Backquote', + 'Digit1', + 'Digit2', + 'Digit3', + 'Digit4', + 'Digit5', + 'Digit6', + 'Digit7', + 'Digit8', + 'Digit9', + 'Digit0', + 'Minus', + 'Equal', + ], + [ + null, + 'KeyQ', + 'KeyW', + 'KeyE', + 'KeyR', + 'KeyT', + 'KeyY', + 'KeyU', + 'KeyI', + 'KeyO', + 'KeyP', + 'BracketLeft', + 'BracketRight', + ], + [ + null, + 'KeyA', + 'KeyS', + 'KeyD', + 'KeyF', + 'KeyG', + 'KeyH', + 'KeyJ', + 'KeyK', + 'KeyL', + 'Semicolon', + 'Quote', + 'Backslash', + ], + [ + 'IntlBackslash', + 'KeyZ', + 'KeyX', + 'KeyC', + 'KeyV', + 'KeyB', + 'KeyN', + 'KeyM', + 'Comma', + 'Period', + 'Slash', + ], +]; + +const ORIGIN_LAYER_2 = 0; +/** + * Key codes for the cluster of keys with Page Up/Down. + */ +export const CODES_LAYER_2 = [ + ['Insert', 'Home', 'PageUp'], + ['Delete', 'End', 'PageDown'], +]; + +const ORIGIN_LAYER_3 = 0; +/** + * Key codes for the numpad. + */ +export const CODES_LAYER_3 = [ + ['NumLock', 'NumpadDivide', 'NumpadMultiply', 'NumpadSubtract'], + ['Numpad7', 'Numpad8', 'Numpad9', 'NumpadAdd'], + ['Numpad4', 'Numpad5', 'Numpad6'], + ['Numpad1', 'Numpad2', 'Numpad3', 'NumpadEnter'], + ['Numpad0', null, 'NumpadDecimal'], +]; + +/** + * Mapping from key codes to coordinates of input device geometry. + */ +export const COORDS_BY_CODE: Map = new Map(); +CODES_LAYER_0.forEach((row, y) => + row.forEach((code, x) => COORDS_BY_CODE.set(code, [ORIGIN_LAYER_0 + x, y, 0])) +); +CODES_LAYER_1.forEach((row, y) => { + row.forEach((code, x) => { + if (code !== null) { + COORDS_BY_CODE.set(code, [ORIGIN_LAYER_1 + x, y, 1]); + } + }); +}); +CODES_LAYER_2.forEach((row, y) => + row.forEach((code, x) => COORDS_BY_CODE.set(code, [ORIGIN_LAYER_2 + x, y, 2])) +); +CODES_LAYER_3.forEach((row, y) => { + row.forEach((code, x) => { + if (code !== null) { + COORDS_BY_CODE.set(code, [ORIGIN_LAYER_3 + x, y, 3]); + } + }); +}); + +const CODE_BY_COORDS: Record = {}; + +for (const [code, xyz] of COORDS_BY_CODE) { + const key = xyz.join(','); + CODE_BY_COORDS[key] = code; +} + +/** + * Map 3D coordinates to key codes. + * @param xyz 3D coordinates of the physical key. + * @returns Key code associated with the coordinates or `undefined` if there is no association. + */ +export function codeByCoords(xyz: Coords3D): string | undefined { + const key = xyz.join(','); + return CODE_BY_COORDS[key]; +} diff --git a/src/index.ts b/src/index.ts index 58063b9..dd8c141 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,337 +1,3 @@ -/* -Split the keyboard into xy-planes along a z-coordinate for different contiguous regions of keys -*/ - -const ORIGIN_LAYER_0 = 0; -/** - * Key codes for the row consisting of Esc and FN keys. - */ -export const CODES_LAYER_0 = [ - [ - 'Escape', - 'F1', - 'F2', - 'F3', - 'F4', - 'F5', - 'F6', - 'F7', - 'F8', - 'F9', - 'F10', - 'F11', - 'F12', - ], -]; - -const ORIGIN_LAYER_1 = -1; -/** - * Key codes for the rows containing the digits, qwerty, asdf and zxcv. - */ -export const CODES_LAYER_1 = [ - [ - 'Backquote', - 'Digit1', - 'Digit2', - 'Digit3', - 'Digit4', - 'Digit5', - 'Digit6', - 'Digit7', - 'Digit8', - 'Digit9', - 'Digit0', - 'Minus', - 'Equal', - ], - [ - null, - 'KeyQ', - 'KeyW', - 'KeyE', - 'KeyR', - 'KeyT', - 'KeyY', - 'KeyU', - 'KeyI', - 'KeyO', - 'KeyP', - 'BracketLeft', - 'BracketRight', - ], - [ - null, - 'KeyA', - 'KeyS', - 'KeyD', - 'KeyF', - 'KeyG', - 'KeyH', - 'KeyJ', - 'KeyK', - 'KeyL', - 'Semicolon', - 'Quote', - 'Backslash', - ], - [ - 'IntlBackslash', - 'KeyZ', - 'KeyX', - 'KeyC', - 'KeyV', - 'KeyB', - 'KeyN', - 'KeyM', - 'Comma', - 'Period', - 'Slash', - ], -]; - -const ORIGIN_LAYER_2 = 0; -/** - * Key codes for the cluster of keys with Page Up/Down. - */ -export const CODES_LAYER_2 = [ - ['Insert', 'Home', 'PageUp'], - ['Delete', 'End', 'PageDown'], -]; - -const ORIGIN_LAYER_3 = 0; -/** - * Key codes for the numpad. - */ -export const CODES_LAYER_3 = [ - ['NumLock', 'NumpadDivide', 'NumpadMultiply', 'NumpadSubtract'], - ['Numpad7', 'Numpad8', 'Numpad9', 'NumpadAdd'], - ['Numpad4', 'Numpad5', 'Numpad6'], - ['Numpad1', 'Numpad2', 'Numpad3', 'NumpadEnter'], - ['Numpad0', null, 'NumpadDecimal'], -]; - -/** - * Mapping from key codes to coordinates of input device geometry. - */ -export const COORDS_BY_CODE: Map = new Map(); -CODES_LAYER_0.forEach((row, y) => - row.forEach((code, x) => COORDS_BY_CODE.set(code, [ORIGIN_LAYER_0 + x, y, 0])) -); -CODES_LAYER_1.forEach((row, y) => { - row.forEach((code, x) => { - if (code !== null) { - COORDS_BY_CODE.set(code, [ORIGIN_LAYER_1 + x, y, 1]); - } - }); -}); -CODES_LAYER_2.forEach((row, y) => - row.forEach((code, x) => COORDS_BY_CODE.set(code, [ORIGIN_LAYER_2 + x, y, 2])) -); -CODES_LAYER_3.forEach((row, y) => { - row.forEach((code, x) => { - if (code !== null) { - COORDS_BY_CODE.set(code, [ORIGIN_LAYER_3 + x, y, 3]); - } - }); -}); - -/** - * Keyboard event with a key code and coordinates if the device geometry can be figured out with any confidence. - */ -export type CoordinateKeyboardEvent = { - code: string; - coordinates?: number[]; -}; - -export type KeyupCallback = () => void; -export type KeydownCallback = (event: CoordinateKeyboardEvent) => KeyupCallback; - -/** - * Keyboard event listener that filters out repeated keydown events and normalizes keycodes to coordinates. - * The Shift keys toggle 'sustain'. - */ -export class Keyboard { - private keydownCallbacks: KeydownCallback[]; - private keyupCallbacks: Map; - private activeKeys: Set; - private pendingKeys: Set; - private stickyKeys: Set; - private _keydown?: (event: KeyboardEvent) => void; - private _keyup?: (event: KeyboardEvent) => void; - private log: (msg: string) => void; - - /** - * Construct a new keyboard event listener. - * @param autobind Start listening to "keydown" and "keyup" events immediately. - * @param log Logging function. - */ - constructor(autobind = false, log?: (msg: string) => void) { - this.keydownCallbacks = []; - this.keyupCallbacks = new Map(); - this.activeKeys = new Set(); - this.pendingKeys = new Set(); - this.stickyKeys = new Set(); - - if (autobind) { - this._keydown = this.keydown.bind(this); - this._keyup = this.keyup.bind(this); - window.addEventListener('keydown', this._keydown); - window.addEventListener('keyup', this._keyup); - } - - if (log === undefined) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - this.log = (msg: string) => {}; - } else { - this.log = log; - } - } - - /** - * Stop listening to "keydown" and "keyup" events if constructed with `autobind = true`. - */ - dispose() { - if (this._keydown) { - window.removeEventListener('keydown', this._keydown); - } - if (this._keyup) { - window.removeEventListener('keyup', this._keyup); - } - } - - /** - * Register a listener for processed keydown events. - * @param listener Function to call when a new key is pressed. - */ - addKeydownListener(listener: KeydownCallback) { - this.keydownCallbacks.push(listener); - } - - /** - * Unregister a listener for processed keydown events. - * @param listener Callback registered with {@link Keyboard.addKeydownListener}. - */ - removeEventListener(listener: KeydownCallback) { - this.keydownCallbacks.splice(this.keydownCallbacks.indexOf(listener), 1); - } - - private fireKeydown(event: CoordinateKeyboardEvent) { - event.coordinates = COORDS_BY_CODE.get(event.code); - const keyupCallbacks = this.keyupCallbacks.get(event.code) || []; - for (const callback of keyupCallbacks) { - console.warn('Unresolved keyup detected'); - callback(); - } - this.log( - `Firing keydown listeners with ${event.code} @ ${event.coordinates}` - ); - this.keydownCallbacks.forEach(callback => - keyupCallbacks.push(callback(event)) - ); - this.keyupCallbacks.set(event.code, keyupCallbacks); - } - - private fireKeyup(event: CoordinateKeyboardEvent) { - this.log(`Firing keyup listeners with ${event.code}`); - for (const callback of this.keyupCallbacks.get(event.code) || []) { - callback(); - } - this.keyupCallbacks.delete(event.code); - } - - /** - * Listener to be registered with `window.addEventListener("keydown", ...)`. - * @param event Keyboard event of a key being pressed down. - */ - keydown(event: KeyboardEvent) { - this.log(`${event.code} keydown received`); - if (event.ctrlKey || event.altKey || event.metaKey || event.repeat) { - this.log(`${event.code} keydown filtered out`); - return; - } - // The pending state isn't strictly necessary as we filter out repeated events, - // but it's kept in case event.repeat isn't 100% reliable. - if (event.key === 'Shift') { - for (const code of this.activeKeys) { - this.log(`Adding ${code} to pending state due to a 'Shift' press`); - this.pendingKeys.add(code); - } - return; - } - - if (this.stickyKeys.has(event.code)) { - this.log(`Stricky toggle for ${event.code}`); - this.activeKeys.delete(event.code); - this.stickyKeys.delete(event.code); - this.pendingKeys.delete(event.code); - this.fireKeyup(event); - return; - } - - if (this.pendingKeys.has(event.code)) { - this.log(`${event.code} is pending`); - return; - } - - if (this.activeKeys.has(event.code)) { - this.log(`${event.code} is already active`); - return; - } - - if (COORDS_BY_CODE.has(event.code)) { - this.log(`Adding ${event.code} to active state`); - this.activeKeys.add(event.code); - if (event.shiftKey) { - this.log( - `Adding ${event.code} to pending state due to being pressed with 'Shift'` - ); - this.pendingKeys.add(event.code); - } - this.fireKeydown(event); - return; - } - } - - /** - * Listener to be registered with `window.addEventListener("keyup", ...)`. - * @param event Keyboard event of a pressed key being released. - */ - keyup(event: KeyboardEvent) { - this.log(`${event.code} keyup received`); - if (event.shiftKey && this.activeKeys.has(event.code)) { - this.log( - `Sticking ${event.code} due being released while 'Shift' is pressed` - ); - this.stickyKeys.add(event.code); - } - if (this.pendingKeys.has(event.code)) { - this.log(`Promoting ${event.code} from pending to sticky`); - this.pendingKeys.delete(event.code); - this.stickyKeys.add(event.code); - } - if (this.stickyKeys.has(event.code)) { - this.log(`Not firing keyup due to ${event.code} being sticky`); - return; - } - - if (this.activeKeys.has(event.code)) { - this.activeKeys.delete(event.code); - this.fireKeyup(event); - return; - } - this.log(`${event.code} keyup fell through`); - } - - /** - * Release keys sustained due to being pressed down with 'Shift'. - */ - deactivate() { - this.log('Releasing all sustained and active keys'); - this.pendingKeys.clear(); - this.stickyKeys.clear(); - for (const code of this.activeKeys.keys()) { - this.fireKeyup({code}); - } - this.activeKeys.clear(); - } -} +export * from './coordinates'; +export * from './keyboard'; +export * from './piano'; diff --git a/src/keyboard.ts b/src/keyboard.ts new file mode 100644 index 0000000..c635993 --- /dev/null +++ b/src/keyboard.ts @@ -0,0 +1,202 @@ +import {COORDS_BY_CODE, Coords3D} from './coordinates'; + +/** + * Keyboard event with a key code and coordinates if the device geometry can be figured out with any confidence. + */ +export type CoordinateKeyboardEvent = { + code: string; + coordinates?: Coords3D; +}; + +export type KeyupCallback = () => void; +export type KeydownCallback = (event: CoordinateKeyboardEvent) => KeyupCallback; + +/** + * Keyboard event listener that filters out repeated keydown events and normalizes keycodes to coordinates. + * The Shift keys toggle 'sustain'. + */ +export class Keyboard { + private keydownCallbacks: KeydownCallback[]; + private keyupCallbacks: Map; + private activeKeys: Set; + private pendingKeys: Set; + private stickyKeys: Set; + private _keydown?: (event: KeyboardEvent) => void; + private _keyup?: (event: KeyboardEvent) => void; + private log: (msg: string) => void; + + /** + * Construct a new keyboard event listener. + * @param autobind Start listening to "keydown" and "keyup" events immediately. + * @param log Logging function. + */ + constructor(autobind = false, log?: (msg: string) => void) { + this.keydownCallbacks = []; + this.keyupCallbacks = new Map(); + this.activeKeys = new Set(); + this.pendingKeys = new Set(); + this.stickyKeys = new Set(); + + if (autobind) { + this._keydown = (event: KeyboardEvent) => this.keydown(event); + this._keyup = (event: KeyboardEvent) => this.keyup(event); + window.addEventListener('keydown', this._keydown); + window.addEventListener('keyup', this._keyup); + } + + if (log === undefined) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + this.log = (msg: string) => {}; + } else { + this.log = log; + } + } + + /** + * Stop listening to "keydown" and "keyup" events if constructed with `autobind = true`. + */ + dispose() { + if (this._keydown) { + window.removeEventListener('keydown', this._keydown); + } + if (this._keyup) { + window.removeEventListener('keyup', this._keyup); + } + } + + /** + * Register a listener for processed keydown events. + * @param listener Function to call when a new key is pressed. + */ + addKeydownListener(listener: KeydownCallback) { + this.keydownCallbacks.push(listener); + } + + /** + * Unregister a listener for processed keydown events. + * @param listener Callback registered with {@link Keyboard.addKeydownListener}. + */ + removeEventListener(listener: KeydownCallback) { + this.keydownCallbacks.splice(this.keydownCallbacks.indexOf(listener), 1); + } + + private fireKeydown(event: CoordinateKeyboardEvent) { + event.coordinates = COORDS_BY_CODE.get(event.code); + const keyupCallbacks = this.keyupCallbacks.get(event.code) || []; + for (const callback of keyupCallbacks) { + console.warn('Unresolved keyup detected'); + callback(); + } + this.log( + `Firing keydown listeners with ${event.code} @ ${event.coordinates}` + ); + this.keydownCallbacks.forEach(callback => + keyupCallbacks.push(callback(event)) + ); + this.keyupCallbacks.set(event.code, keyupCallbacks); + } + + private fireKeyup(event: CoordinateKeyboardEvent) { + this.log(`Firing keyup listeners with ${event.code}`); + for (const callback of this.keyupCallbacks.get(event.code) || []) { + callback(); + } + this.keyupCallbacks.delete(event.code); + } + + /** + * Listener to be registered with `window.addEventListener("keydown", ...)`. + * @param event Keyboard event of a key being pressed down. + */ + keydown(event: KeyboardEvent) { + this.log(`${event.code} keydown received`); + if (event.ctrlKey || event.altKey || event.metaKey || event.repeat) { + this.log(`${event.code} keydown filtered out`); + return; + } + // The pending state isn't strictly necessary as we filter out repeated events, + // but it's kept in case event.repeat isn't 100% reliable. + if (event.key === 'Shift') { + for (const code of this.activeKeys) { + this.log(`Adding ${code} to pending state due to a 'Shift' press`); + this.pendingKeys.add(code); + } + return; + } + + if (this.stickyKeys.has(event.code)) { + this.log(`Stricky toggle for ${event.code}`); + this.activeKeys.delete(event.code); + this.stickyKeys.delete(event.code); + this.pendingKeys.delete(event.code); + this.fireKeyup(event); + return; + } + + if (this.pendingKeys.has(event.code)) { + this.log(`${event.code} is pending`); + return; + } + + if (this.activeKeys.has(event.code)) { + this.log(`${event.code} is already active`); + return; + } + + if (COORDS_BY_CODE.has(event.code)) { + this.log(`Adding ${event.code} to active state`); + this.activeKeys.add(event.code); + if (event.shiftKey) { + this.log( + `Adding ${event.code} to pending state due to being pressed with 'Shift'` + ); + this.pendingKeys.add(event.code); + } + this.fireKeydown(event); + return; + } + } + + /** + * Listener to be registered with `window.addEventListener("keyup", ...)`. + * @param event Keyboard event of a pressed key being released. + */ + keyup(event: KeyboardEvent) { + this.log(`${event.code} keyup received`); + if (event.shiftKey && this.activeKeys.has(event.code)) { + this.log( + `Sticking ${event.code} due being released while 'Shift' is pressed` + ); + this.stickyKeys.add(event.code); + } + if (this.pendingKeys.has(event.code)) { + this.log(`Promoting ${event.code} from pending to sticky`); + this.pendingKeys.delete(event.code); + this.stickyKeys.add(event.code); + } + if (this.stickyKeys.has(event.code)) { + this.log(`Not firing keyup due to ${event.code} being sticky`); + return; + } + + if (this.activeKeys.has(event.code)) { + this.activeKeys.delete(event.code); + this.fireKeyup(event); + return; + } + this.log(`${event.code} keyup fell through`); + } + + /** + * Release keys sustained due to being pressed down with 'Shift'. + */ + deactivate() { + this.log('Releasing all sustained and active keys'); + this.pendingKeys.clear(); + this.stickyKeys.clear(); + for (const code of this.activeKeys.keys()) { + this.fireKeyup({code}); + } + this.activeKeys.clear(); + } +} diff --git a/src/piano.ts b/src/piano.ts new file mode 100644 index 0000000..b7b398d --- /dev/null +++ b/src/piano.ts @@ -0,0 +1,44 @@ +import {Coords3D, codeByCoords} from './coordinates'; + +/** + * Convert a linear sequence of note types into a piano-style layout + */ +export function pianoMap(ys: number[]) { + const nextXs = [-1, 0, 0, -1]; + const coordss: Coords3D[] = []; + for (const y of ys) { + const x = nextXs[y]; + coordss.push([x, y, 1]); + nextXs[y]++; + + // == Sync everything vertically + // A is before Z. S is after Z. + nextXs[2] = Math.max(nextXs[2], nextXs[3] + 1); + // Q before A. W is after A. + nextXs[1] = Math.max(nextXs[1], nextXs[2]); + // 1 is befor Q. 2 is after Q. + nextXs[0] = Math.max(nextXs[0], nextXs[1] + 1); + // Sync the other way too but with less "force". + nextXs[1] = Math.max(nextXs[1], nextXs[0] - 2); + nextXs[2] = Math.max(nextXs[2], nextXs[1] - 1); + nextXs[3] = Math.max(nextXs[3], nextXs[2] - 2); + } + + const indexByCode = new Map(); + const coordsByIndex: (Coords3D | undefined)[] = []; + let i = 0; + for (const xyz of coordss) { + const code = codeByCoords(xyz); + if (code === undefined) { + coordsByIndex.push(undefined); + } else { + indexByCode.set(code, i); + coordsByIndex.push(xyz); + } + i++; + } + return { + coordsByIndex, + indexByCode, + }; +}