@yam8d/m8-sdk 0.5.3 → 0.5.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yam8d/m8-sdk",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Client SDK for apps that read and drive a Dirtywave M8, inside a yam8d External Apps iframe or standalone over Web Serial, WebUSB or Web MIDI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/types/client.d.ts CHANGED
@@ -33,7 +33,7 @@ export interface M8Client {
33
33
  /** Standalone: the API the device is connected through, null while disconnected (always null in host mode). */
34
34
  readonly deviceTransport: M8DeviceTransport | null;
35
35
  navigateToView(viewName: string): Promise<boolean>;
36
- navigateTo(x: number, y: number): Promise<void>;
36
+ navigateTo(x: number, y: number): Promise<boolean>;
37
37
  setValueToHex(targetHex: number): Promise<boolean>;
38
38
  setValueToInt(targetInt: number): Promise<boolean>;
39
39
  setValueFloat(targetFloat: number): Promise<boolean>;
@@ -0,0 +1,60 @@
1
+ export type CursorDir = 'up' | 'down' | 'left' | 'right';
2
+ export type CellRect = {
3
+ x: number;
4
+ y: number;
5
+ w: number;
6
+ h: number;
7
+ };
8
+ /** The cells and moves of one layout. A move to `null` means the key did not move the cursor. */
9
+ export interface CursorLayout {
10
+ readonly cells: Map<string, CellRect>;
11
+ readonly moves: Map<string, Partial<Record<CursorDir, string | null>>>;
12
+ readonly unreachable: Set<string>;
13
+ /** Texts that failed once: one more failure makes them unreachable. */
14
+ readonly suspects: Set<string>;
15
+ }
16
+ export type CursorMapData = {
17
+ version: 1;
18
+ layouts: {
19
+ key: string;
20
+ cells: string[];
21
+ moves: [string, CursorDir, string | null][];
22
+ unreachable: string[];
23
+ suspects?: string[];
24
+ }[];
25
+ };
26
+ export interface CursorMap {
27
+ /** The layout, or undefined when nothing was learned for it. */
28
+ get(key: string): CursorLayout | undefined;
29
+ recordCell(key: string, rect: CellRect): void;
30
+ /** `to` null: the key was pressed and the cursor did not move. */
31
+ recordMove(key: string, from: CellRect, dir: CursorDir, to: CellRect | null): void;
32
+ /** A navigation could not reach this text: the second time, it is unreachable. */
33
+ markUnreachable(key: string, textKey: string): void;
34
+ toJSON(): CursorMapData;
35
+ }
36
+ export declare const DIRS: readonly CursorDir[];
37
+ export declare const cellId: (rect: CellRect) => string;
38
+ export declare const parseCellId: (id: string) => CellRect | null;
39
+ export declare const createCursorMap: (options?: {
40
+ data?: CursorMapData | null;
41
+ onChange?: () => void;
42
+ }) => CursorMap;
43
+ /** Whether a move landed on the wrong side (Up landing lower...): a wrap, not a move on this screen. */
44
+ export declare const wentBackwards: (from: CellRect, dir: CursorDir, to: CellRect) => boolean;
45
+ /**
46
+ * Where a key not learned yet probably goes: Up/Down to the nearest cell above/below that shares
47
+ * columns with this one, Left/Right to the nearest cell of the same row. Null when there is none.
48
+ */
49
+ export declare const guessMove: (layout: CursorLayout, from: CellRect, dir: CursorDir) => string | null;
50
+ /**
51
+ * Cheapest key sequence from `fromId` to a cell that `isGoal` accepts, over the learned moves and,
52
+ * for keys not learned yet from a cell, the move `guessMove` expects (marked `guessed`). Moves
53
+ * listed in `skip` (`"<cellId>:<dir>"`) or refused by `forbid` are not used, learned or guessed,
54
+ * nor are learned moves that went backwards (a phrase wrapping to the next one). Null when no path exists.
55
+ */
56
+ export declare const findPath: (layout: CursorLayout, fromId: string, isGoal: (id: string) => boolean, skip?: ReadonlySet<string>, forbid?: (rect: CellRect, dir: CursorDir) => boolean) => {
57
+ dir: CursorDir;
58
+ to: string;
59
+ guessed: boolean;
60
+ }[] | null;
@@ -1,15 +1,22 @@
1
1
  import type { EngineBus } from './bus';
2
+ import { type CursorMap } from './cursorMap';
3
+ import { type Point } from './cursorTargets';
2
4
  import type { M8Store } from './store';
3
- export type Point = {
4
- x: number;
5
- y: number;
6
- };
5
+ export type { Point } from './cursorTargets';
7
6
  export interface CursorNavigator {
8
- /** Moves the cursor towards `target` (M8 pixel coordinates). Resolves when it stops. */
9
- navigateTo(target: Point): Promise<void>;
7
+ /**
8
+ * Moves the cursor to the cell at or nearest to `target` (M8 pixel coordinates). Resolves true
9
+ * when it got there, false when it stopped on the closest cell it could reach (or was cancelled).
10
+ */
11
+ navigateTo(target: Point): Promise<boolean>;
10
12
  dispose(): void;
11
13
  }
12
- export declare const createCursorNavigator: ({ bus, store }: {
14
+ export interface CursorNavigatorDeps {
13
15
  bus: EngineBus;
14
16
  store: M8Store;
15
- }) => CursorNavigator;
17
+ /** The screen text (one string per row, columns kept), used to find the cells near a click. */
18
+ getScreenLines?: () => string[];
19
+ /** Where the learned layouts are kept; a new empty map by default. */
20
+ cursorMap?: CursorMap;
21
+ }
22
+ export declare const createCursorNavigator: ({ bus, store, getScreenLines, cursorMap, }: CursorNavigatorDeps) => CursorNavigator;
@@ -0,0 +1,86 @@
1
+ import { type CellRect, type CursorLayout } from './cursorMap';
2
+ import type { M8CellMetrics } from './store';
3
+ export type Point = {
4
+ x: number;
5
+ y: number;
6
+ };
7
+ /**
8
+ * Where the cursor should go. `cell`: a learned cursor cell (reached when the cursor is that cell).
9
+ * `text`: text no learned cell covers yet; `rect` is the character aimed at (reached when the
10
+ * cursor covers it), `run` the whole run (one run can be two cells, as an FX command and value).
11
+ * `point`: no text near the point, or selection mode (reached when the cursor covers the point).
12
+ */
13
+ export type CursorGoal = {
14
+ kind: 'cell';
15
+ id: string;
16
+ rect: CellRect;
17
+ } | {
18
+ kind: 'text';
19
+ key: string;
20
+ rect: CellRect;
21
+ run: CellRect;
22
+ row: number;
23
+ } | {
24
+ kind: 'point';
25
+ rect: CellRect;
26
+ };
27
+ export type TextRun = {
28
+ key: string;
29
+ rect: CellRect;
30
+ row: number;
31
+ col: number;
32
+ text: string;
33
+ };
34
+ export type GridRows = {
35
+ first: number;
36
+ last: number;
37
+ numberWidth: number;
38
+ };
39
+ /** The first and last rows that start with a row number, on grid views that have one; else null. */
40
+ export declare const gridRows: (viewName: string | null, lines: readonly string[]) => GridRows | null;
41
+ /** A cursor rectangle as the character cells it covers (column, row, width in characters). */
42
+ export declare const snapToGrid: (rect: CellRect, metrics: M8CellMetrics) => CellRect;
43
+ /** The screen row of a cursor cell. */
44
+ export declare const rowOf: (rect: CellRect, metrics: M8CellMetrics) => number;
45
+ /** First column of the track panel, or null when the layout has no 40 columns. */
46
+ export declare const panelColumnFor: (screenWidth: number, metrics: M8CellMetrics) => number | null;
47
+ /**
48
+ * The labels that start the rows (words with a letter outside hex digits, two characters or more).
49
+ * Values, row numbers and markers are left out, so the signature changes with the layout
50
+ * (instrument type, modulator type) but not with the song.
51
+ */
52
+ export declare const layoutSignature: (lines: readonly string[], panelColumn: number | null) => string;
53
+ /**
54
+ * Runs of text on the screen, in M8 pixels. Column 0 (the playback marker margin) is left out, and
55
+ * so are the characters `covered` says belong to a known cell.
56
+ */
57
+ export declare const textRuns: (lines: readonly string[], metrics: M8CellMetrics, panelColumn: number | null, covered?: (row: number, col: number) => boolean) => TextRun[];
58
+ /** Whether the screen has text under a cursor cell (the same columns as textUnderCursor). */
59
+ export declare const cellHasText: (rect: CellRect, lines: readonly string[], metrics: M8CellMetrics) => boolean;
60
+ /** Whether `cursor` covers `target` horizontally (at least half a character, or half the target). */
61
+ export declare const coversX: (cursor: CellRect, target: CellRect, metrics: M8CellMetrics) => boolean;
62
+ /** Whether `cursor` covers `target` vertically (at least half a row, or half the target). */
63
+ export declare const coversY: (cursor: CellRect, target: CellRect, metrics: M8CellMetrics) => boolean;
64
+ export declare const covers: (cursor: CellRect, target: CellRect, metrics: M8CellMetrics) => boolean;
65
+ /** How far the cursor is from a target: the gap between the rectangles, plus a small centre term to break ties. */
66
+ export declare const rectDistance: (a: CellRect, b: CellRect) => number;
67
+ export declare const pointGoal: (point: Point) => CursorGoal;
68
+ /** Whether the cursor rectangle has reached the goal. */
69
+ export declare const goalReached: (goal: CursorGoal, cursor: CellRect, metrics: M8CellMetrics) => boolean;
70
+ /**
71
+ * What a click can go to: the learned cells with text on screen, and the runs of the text they do
72
+ * not cover (an FX value stays a candidate once only its command is learned).
73
+ */
74
+ export declare const candidates: (layout: CursorLayout | undefined, lines: readonly string[], metrics: M8CellMetrics, panelColumn: number | null, grid?: GridRows | null) => {
75
+ cells: {
76
+ id: string;
77
+ rect: CellRect;
78
+ }[];
79
+ runs: TextRun[];
80
+ };
81
+ /** The candidate nearest to `point`, or a point goal when the screen offers none. */
82
+ export declare const pickGoal: (point: Point, layout: CursorLayout | undefined, lines: readonly string[], metrics: M8CellMetrics, panelColumn: number | null, grid?: GridRows | null,
83
+ /** Texts to leave out besides the unreachable ones (those that failed in this navigation). */
84
+ skip?: ReadonlySet<string>,
85
+ /** After a text failed: only learned cells and texts beside the click (not the next label down the column). */
86
+ knownFirst?: boolean) => CursorGoal;
@@ -1,5 +1,6 @@
1
1
  import type { M8HostEvents, M8HostMethods, M8State } from '../types';
2
2
  import type { EngineBus } from './bus';
3
+ import { type CursorMap } from './cursorMap';
3
4
  import { type Point } from './cursorNavigator';
4
5
  import { type MacroRunner } from './macroRunner';
5
6
  import { type M8Store } from './store';
@@ -10,6 +11,8 @@ export interface M8EngineOptions {
10
11
  cancelMacroOnKeydown?: boolean;
11
12
  /** Use this store instead of a new one (for tests, or to keep state across reconnects). */
12
13
  store?: M8Store;
14
+ /** Where the cursor navigator keeps the layouts it learns (share one to keep them across reconnects). */
15
+ cursorMap?: CursorMap;
13
16
  }
14
17
  export interface M8Engine {
15
18
  readonly bus: EngineBus;
@@ -17,8 +20,8 @@ export interface M8Engine {
17
20
  /** The SDK methods, as an iframe client or the standalone client calls them. */
18
21
  readonly methods: M8HostMethods;
19
22
  readonly macro: MacroRunner;
20
- /** Moves the cursor towards a point in M8 pixel coordinates. */
21
- navigateTo(point: Point): Promise<void>;
23
+ /** Moves the cursor to the cell at or nearest to a point in M8 pixel coordinates. Resolves true when it got there. */
24
+ navigateTo(point: Point): Promise<boolean>;
22
25
  /** Resolves true when the view is reached. */
23
26
  navigateToView(viewName: string): Promise<boolean>;
24
27
  getState(): M8State;
@@ -11,7 +11,7 @@ export type HostMethodsDeps = {
11
11
  navigateTo: (point: {
12
12
  x: number;
13
13
  y: number;
14
- }) => Promise<void>;
14
+ }) => Promise<boolean>;
15
15
  enableMidiTransport: () => Promise<{
16
16
  ok: boolean;
17
17
  error?: string;
@@ -1,6 +1,7 @@
1
1
  export type { EngineBus, ProtocolEventBus } from './bus';
2
2
  export { connectWithRetry } from './connectWithRetry';
3
3
  export { type ConnectedBus, connection, decode7BitPacked, type M8ConnectOptions, type M8DeviceTransport } from './connection';
4
+ export { type CellRect, type CursorDir, type CursorLayout, type CursorMap, type CursorMapData, createCursorMap } from './cursorMap';
4
5
  export { type CursorNavigator, createCursorNavigator, type Point } from './cursorNavigator';
5
6
  export { createM8Engine, type M8Engine, type M8EngineOptions } from './engine';
6
7
  export { createEventBus } from './eventBus';
@@ -16,6 +16,6 @@ export interface ViewNavigationDeps {
16
16
  /** The 24 text rows of the screen, to find buttons by their label. */
17
17
  getScreenLines?: () => string[];
18
18
  /** Moves the cursor to a text grid cell. Without it, button edges are not used. */
19
- moveCursor?: (x: number, y: number) => Promise<void>;
19
+ moveCursor?: (x: number, y: number) => Promise<unknown>;
20
20
  }
21
21
  export declare const createViewNavigation: ({ store, runner, getScreenLines, moveCursor }: ViewNavigationDeps) => ViewNavigation;
package/types/types.d.ts CHANGED
@@ -74,7 +74,8 @@ export interface M8TransportEvent {
74
74
  }
75
75
  export interface M8HostMethods {
76
76
  navigateToView(viewName: string): Promise<boolean>;
77
- navigateTo(x: number, y: number): Promise<void>;
77
+ /** Moves the cursor to the cell at or nearest to a text grid position. Resolves true when it got there. */
78
+ navigateTo(x: number, y: number): Promise<boolean>;
78
79
  setValueToHex(targetHex: number): Promise<boolean>;
79
80
  setValueToInt(targetInt: number): Promise<boolean>;
80
81
  setValueFloat(targetFloat: number): Promise<boolean>;