@m4l-jweb/surface 0.6.0 → 0.7.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/package.json +2 -2
- package/src/index.ts +103 -3
- package/src/react.tsx +56 -0
- package/src/store.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m4l-jweb/surface",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "m4l-jweb: declare a device's Live parameters as code - the surface Push actually sees - plus a mocked-Live dev harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"react": ">=18",
|
|
27
|
-
"@m4l-jweb/bridge": "0.
|
|
27
|
+
"@m4l-jweb/bridge": "0.7.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependenciesMeta": {
|
|
30
30
|
"react": {
|
package/src/index.ts
CHANGED
|
@@ -92,10 +92,32 @@ export interface MenuSpec<O extends string = string> {
|
|
|
92
92
|
short: string;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
/**
|
|
96
|
+
* A LABELLED toggle button - a `live.text` in toggle mode, which a bare
|
|
97
|
+
* `live.toggle` cannot be: it carries visible text. The on/off value is the same as
|
|
98
|
+
* a toggle; `label` is what the button reads. Handy as a native view switch (a
|
|
99
|
+
* "Back" button) where the plain orange square of a toggle says nothing.
|
|
100
|
+
*/
|
|
101
|
+
export interface ButtonSpec {
|
|
102
|
+
kind: "button";
|
|
103
|
+
default: boolean;
|
|
104
|
+
/** The text on the button. */
|
|
105
|
+
label: string;
|
|
106
|
+
short: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type ParamSpec = DialSpec | ToggleSpec | MenuSpec | ButtonSpec;
|
|
96
110
|
|
|
97
111
|
/** The value type a given parameter carries. `useParam` will be typed by this. */
|
|
98
|
-
export type ParamValue<P extends ParamSpec> = P extends DialSpec
|
|
112
|
+
export type ParamValue<P extends ParamSpec> = P extends DialSpec
|
|
113
|
+
? number
|
|
114
|
+
: P extends ToggleSpec
|
|
115
|
+
? boolean
|
|
116
|
+
: P extends ButtonSpec
|
|
117
|
+
? boolean
|
|
118
|
+
: P extends MenuSpec<infer O>
|
|
119
|
+
? O
|
|
120
|
+
: never;
|
|
99
121
|
|
|
100
122
|
/* ------------------------------------------------------------------ *
|
|
101
123
|
* Windows and State
|
|
@@ -160,6 +182,11 @@ export const toggle = (spec: Omit<ToggleSpec, "kind">): ToggleSpec => ({
|
|
|
160
182
|
kind: "toggle",
|
|
161
183
|
...spec,
|
|
162
184
|
});
|
|
185
|
+
/** A labelled toggle button (`live.text`). Same on/off value as a toggle, with visible text. */
|
|
186
|
+
export const button = (spec: Omit<ButtonSpec, "kind">): ButtonSpec => ({
|
|
187
|
+
kind: "button",
|
|
188
|
+
...spec,
|
|
189
|
+
});
|
|
163
190
|
/**
|
|
164
191
|
* The options are spelled out rather than written as `Omit<MenuSpec<O>, "kind">`,
|
|
165
192
|
* and that is not a style choice: TypeScript cannot infer `O` THROUGH an `Omit`,
|
|
@@ -184,6 +211,47 @@ export interface Bank<K extends string> {
|
|
|
184
211
|
params: readonly K[];
|
|
185
212
|
}
|
|
186
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Which parameters render as NATIVE `live.*` objects in the device view, and how
|
|
216
|
+
* they are laid out.
|
|
217
|
+
*
|
|
218
|
+
* The compiler ALREADY generates a `live.dial` / `live.toggle` / `live.menu` for
|
|
219
|
+
* every declared parameter; they are invisible today only because they carry no
|
|
220
|
+
* `presentation` attribute, and Live shows the presentation view. Naming a
|
|
221
|
+
* parameter here makes the SAME object visible - a presentation overlay on codegen
|
|
222
|
+
* that already exists, with no wiring change: it is the same parameter, the same
|
|
223
|
+
* fan-out graph, `useParam()` still reads it, now drawn by Max instead of React.
|
|
224
|
+
*/
|
|
225
|
+
export interface NativeLayout<K extends string = string> {
|
|
226
|
+
/**
|
|
227
|
+
* In display order: fills rows top-to-bottom, then overflows into the next
|
|
228
|
+
* column (column-major, so adding a parameter does not reshuffle the rest).
|
|
229
|
+
*/
|
|
230
|
+
params: readonly K[];
|
|
231
|
+
/**
|
|
232
|
+
* Max rows per column. Default 3 - the device view is a fixed ~169 px tall and a
|
|
233
|
+
* `live.dial` needs a 56 px pitch, so only 3 fit vertically.
|
|
234
|
+
*/
|
|
235
|
+
rows?: number;
|
|
236
|
+
/**
|
|
237
|
+
* LAYERED "two screens" instead of side-by-side. When true, `[jweb]` is built
|
|
238
|
+
* full-width and the dials OVERLAP its left, and the app flips between them with
|
|
239
|
+
* `useNativePanel` (hide one layer, show the other) - because runtime reposition
|
|
240
|
+
* of native objects does not work in a frozen M4L device, only hide/show does.
|
|
241
|
+
* When false (the default), the dials sit BESIDE a right-shifted `[jweb]`, both
|
|
242
|
+
* visible at once.
|
|
243
|
+
*/
|
|
244
|
+
panel?: boolean;
|
|
245
|
+
/**
|
|
246
|
+
* A parameter that is the VIEW SWITCH, not a grid dial: pinned to the top-right
|
|
247
|
+
* (over the web UI's own switch button, so the control stays in one place across
|
|
248
|
+
* both views), kept out of the `params` grid, and shown in both modes. Meant for a
|
|
249
|
+
* toggle in a `panel` layout - the way back from the native panel, since the web
|
|
250
|
+
* UI is hidden there.
|
|
251
|
+
*/
|
|
252
|
+
switch?: K;
|
|
253
|
+
}
|
|
254
|
+
|
|
187
255
|
/**
|
|
188
256
|
* The declaration. `P`, `S` and `W` are inferred from what you write - they exist so
|
|
189
257
|
* that `useParam`, `useStateSync` and `useWindow` are typed against THIS surface: a
|
|
@@ -199,6 +267,8 @@ export interface SurfaceDef<
|
|
|
199
267
|
banks?: readonly Bank<Extract<keyof P, string>>[];
|
|
200
268
|
windows?: W;
|
|
201
269
|
state?: S;
|
|
270
|
+
/** Which parameters render as native Max objects in the device view. */
|
|
271
|
+
layout?: { native?: NativeLayout<Extract<keyof P, string>> };
|
|
202
272
|
}
|
|
203
273
|
|
|
204
274
|
export interface Surface<
|
|
@@ -265,9 +335,39 @@ export function defineSurface<
|
|
|
265
335
|
}
|
|
266
336
|
}
|
|
267
337
|
|
|
338
|
+
// A native layout may only name parameters that exist, and may not ask for more
|
|
339
|
+
// rows than the device view holds. Both throw here, at build time, for the same
|
|
340
|
+
// reason the bank checks do: a typo would otherwise generate a cord from a box
|
|
341
|
+
// that never gets a presentation rect, or overflow a 169 px view silently.
|
|
342
|
+
const native = def.layout?.native;
|
|
343
|
+
if (native) {
|
|
344
|
+
for (const id of native.params) {
|
|
345
|
+
if (!def.params[id]) throw new Error(`surface: layout.native names "${id}", which is not a declared parameter`);
|
|
346
|
+
}
|
|
347
|
+
const rows = native.rows ?? 3;
|
|
348
|
+
if (rows < 1 || rows > 3) throw new Error(`surface: layout.native.rows must be 1..3 - the device view is 169 px tall`);
|
|
349
|
+
if (native.switch !== undefined && !def.params[native.switch]) {
|
|
350
|
+
throw new Error(`surface: layout.native.switch names "${native.switch}", which is not a declared parameter`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
268
354
|
return { ...def, ids };
|
|
269
355
|
}
|
|
270
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Does this parameter render as a native Max object? App code uses it to stop
|
|
359
|
+
* drawing an HTML control the device view now owns. Cheap and honest: a parameter
|
|
360
|
+
* that is not in `layout.native` is still an HTML control, so `useParam()` stays
|
|
361
|
+
* the source of truth either way.
|
|
362
|
+
*/
|
|
363
|
+
export const isNative = (surface: Surface, id: string): boolean => !!surface.layout?.native?.params.includes(id as never);
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The scripting name the build gives `[jweb]` when a surface declares native
|
|
367
|
+
* layout, so the app can hide/show it at runtime (see `useNativePanel`).
|
|
368
|
+
*/
|
|
369
|
+
export const JWEB_VARNAME = "obj-jweb";
|
|
370
|
+
|
|
271
371
|
/** The default value of every parameter. The app's initial state, before Live replies. */
|
|
272
372
|
export function defaults<P extends Record<string, ParamSpec>>(surface: Surface<P>): { [K in keyof P]: ParamValue<P[K]> } {
|
|
273
373
|
const out = {} as { [K in keyof P]: ParamValue<P[K]> };
|
|
@@ -277,7 +377,7 @@ export function defaults<P extends Record<string, ParamSpec>>(surface: Surface<P
|
|
|
277
377
|
|
|
278
378
|
/** How a value is displayed - the parameter's own `format`, or a sane default. */
|
|
279
379
|
export function formatValue(spec: ParamSpec, value: unknown): string {
|
|
280
|
-
if (spec.kind === "toggle") return value ? "on" : "off";
|
|
380
|
+
if (spec.kind === "toggle" || spec.kind === "button") return value ? "on" : "off";
|
|
281
381
|
if (spec.kind === "menu") return String(value);
|
|
282
382
|
if (spec.format) return spec.format(Number(value));
|
|
283
383
|
const n = Number(value);
|
package/src/react.tsx
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
|
20
20
|
import { outlet } from "@m4l-jweb/bridge";
|
|
21
21
|
import type { ParamSpec, ParamValue, StateSpec, StateValue, Surface, WindowSpec } from "./index";
|
|
22
|
+
import { JWEB_VARNAME } from "./index";
|
|
22
23
|
import { paramStore, stateStore } from "./store";
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -67,6 +68,61 @@ export function useWindow<
|
|
|
67
68
|
);
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Show or hide a NATIVE dial in the device view at runtime.
|
|
73
|
+
*
|
|
74
|
+
* `layout.native` makes a parameter a native `live.*` object, but its presentation
|
|
75
|
+
* is STATIC - the dial is always visible. This is the runtime override: the app says
|
|
76
|
+
* which native params should be shown, and a `[thispatcher]` runs `script show`/
|
|
77
|
+
* `script hide` on the object by its scripting name (`param-<id>`, see
|
|
78
|
+
* `applyNativeControl` in @m4l-jweb/build). The parameter itself is untouched - a
|
|
79
|
+
* hidden dial still automates, MIDI-maps and reaches Push; only visibility changes.
|
|
80
|
+
*
|
|
81
|
+
* Returns a stable `(id, visible) => void`. Drive it from an effect that mirrors the
|
|
82
|
+
* app's own "which stages are active" state, e.g. `useEffect` over the shown set.
|
|
83
|
+
*/
|
|
84
|
+
export function useNativeVisibility<P extends Record<string, ParamSpec>>(
|
|
85
|
+
_surface: Surface<P>,
|
|
86
|
+
): (id: Extract<keyof P, string>, visible: boolean) => void {
|
|
87
|
+
return useCallback((id: Extract<keyof P, string>, visible: boolean) => {
|
|
88
|
+
// The varname applySurface() gave the object is `param-<id>`. Keep this prefix
|
|
89
|
+
// in step with that codegen - it is the one string both sides must agree on.
|
|
90
|
+
outlet(visible ? "native_show" : "native_hide", `param-${id}`);
|
|
91
|
+
}, []);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Flip the device view between the WEB UI and a NATIVE control panel - the "two
|
|
96
|
+
* screens" model. Runtime reposition/resize of presentation objects does NOT work in
|
|
97
|
+
* a frozen M4L device (measured: `presentation_rect` writes are stored but never
|
|
98
|
+
* redrawn), but `hidden` DOES. So instead of reflowing dials, we layer them:
|
|
99
|
+
*
|
|
100
|
+
* "web" - show [jweb] (full width), hide every native dial and the switch. The
|
|
101
|
+
* web UI paints its own switch button; the native one would only fight
|
|
102
|
+
* it for the same top-right spot.
|
|
103
|
+
* "native" - hide [jweb], show every native dial AND the switch - which is the way
|
|
104
|
+
* back, since the web UI is hidden here.
|
|
105
|
+
*
|
|
106
|
+
* Only `hidden` is used, so this actually works where reflow could not, and no layer
|
|
107
|
+
* is ever visible at the same time as another - so z-order never matters.
|
|
108
|
+
*/
|
|
109
|
+
export function useNativePanel<P extends Record<string, ParamSpec>>(
|
|
110
|
+
surface: Surface<P>,
|
|
111
|
+
): (mode: "web" | "native") => void {
|
|
112
|
+
return useCallback(
|
|
113
|
+
(mode: "web" | "native") => {
|
|
114
|
+
const native = surface.layout?.native;
|
|
115
|
+
if (!native) return;
|
|
116
|
+
const web = mode === "web";
|
|
117
|
+
const toggle = (varname: string) => outlet(web ? "native_hide" : "native_show", varname);
|
|
118
|
+
outlet(web ? "native_show" : "native_hide", JWEB_VARNAME);
|
|
119
|
+
for (const id of native.params as readonly string[]) toggle(`param-${id}`);
|
|
120
|
+
if (native.switch) toggle(`param-${native.switch}`);
|
|
121
|
+
},
|
|
122
|
+
[surface],
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
70
126
|
/**
|
|
71
127
|
* A two-way binding to a JSON state slot, persisted in the Live SET.
|
|
72
128
|
*
|
package/src/store.ts
CHANGED
|
@@ -40,7 +40,7 @@ const EPSILON = 1e-6;
|
|
|
40
40
|
|
|
41
41
|
/** Max stores every parameter as a NUMBER. A menu is an index into its options; a toggle is 0/1. */
|
|
42
42
|
export function toWire(spec: ParamSpec, value: unknown): number {
|
|
43
|
-
if (spec.kind === "toggle") return value ? 1 : 0;
|
|
43
|
+
if (spec.kind === "toggle" || spec.kind === "button") return value ? 1 : 0;
|
|
44
44
|
if (spec.kind === "menu") {
|
|
45
45
|
const i = spec.options.indexOf(String(value));
|
|
46
46
|
return i < 0 ? 0 : i;
|
|
@@ -50,7 +50,7 @@ export function toWire(spec: ParamSpec, value: unknown): number {
|
|
|
50
50
|
|
|
51
51
|
/** ...and back. An out-of-range menu index falls back to the default rather than `undefined`. */
|
|
52
52
|
export function fromWire(spec: ParamSpec, wire: number): unknown {
|
|
53
|
-
if (spec.kind === "toggle") return wire >= 0.5;
|
|
53
|
+
if (spec.kind === "toggle" || spec.kind === "button") return wire >= 0.5;
|
|
54
54
|
if (spec.kind === "menu") return spec.options[Math.round(wire)] ?? spec.default;
|
|
55
55
|
return wire;
|
|
56
56
|
}
|