@m4l-jweb/wrapper 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 +1 -1
- package/src/core.ts +166 -16
- package/src/max.d.ts +9 -0
package/package.json
CHANGED
package/src/core.ts
CHANGED
|
@@ -76,15 +76,48 @@ function reload(): void {
|
|
|
76
76
|
*/
|
|
77
77
|
function anything(): void {}
|
|
78
78
|
|
|
79
|
+
/* ------------------------------------------------------------------ *
|
|
80
|
+
* Who to answer
|
|
81
|
+
*
|
|
82
|
+
* A reply normally goes out outlet 0, to the device's OWN [jweb]. But a floating
|
|
83
|
+
* window's [jweb] lives inside a subpatcher, so there is no cord to it - the
|
|
84
|
+
* wrapper reaches it BY NAME, through its [r window-read-<id>] (see loadWindows).
|
|
85
|
+
*
|
|
86
|
+
* So a handler must not hard-code outlet(0): when it is answering a WINDOW it has
|
|
87
|
+
* to route to that window's receiver instead. `window()` below sets replyWindow
|
|
88
|
+
* for the duration of a window message's dispatch, and reply() honours it - so the
|
|
89
|
+
* same get_state()/ui_ready() serve the device view AND any window, unchanged.
|
|
90
|
+
* ------------------------------------------------------------------ */
|
|
91
|
+
|
|
92
|
+
/** The window a reply should go to, or null for the device's own [jweb] (outlet 0). */
|
|
93
|
+
var replyWindow: string | null = null;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* outlet(0, selector, value) that follows replyWindow - to a window's
|
|
97
|
+
* [r window-read-<id>] when one is set, to the device's own [jweb] otherwise.
|
|
98
|
+
*
|
|
99
|
+
* It takes a FIXED (selector, value) rather than a rest arg, and that is
|
|
100
|
+
* deliberate: `outlet` and `messnamed` are Max HOST functions, and calling
|
|
101
|
+
* `.apply` on them is not reliable across Max builds - when it fails it fails
|
|
102
|
+
* SILENTLY, and an exception here takes the whole ui_ready handshake (mode, build,
|
|
103
|
+
* the state resend) down with it, with no symptom but a device that never fills in
|
|
104
|
+
* its header. Every reply the wrapper sends is one selector and one value, so this
|
|
105
|
+
* is all it ever needs.
|
|
106
|
+
*/
|
|
107
|
+
function reply(selector: string, value: unknown): void {
|
|
108
|
+
if (replyWindow !== null) messnamed("window-read-" + replyWindow, selector, value);
|
|
109
|
+
else outlet(0, selector, value);
|
|
110
|
+
}
|
|
111
|
+
|
|
79
112
|
/**
|
|
80
113
|
* The UI announces it finished loading. The page loads asynchronously, so never
|
|
81
114
|
* assume it was listening when state last changed - resend all of it.
|
|
82
115
|
*/
|
|
83
116
|
function ui_ready(): void {
|
|
84
|
-
|
|
117
|
+
reply("mode", MODE);
|
|
85
118
|
// The UI shows this next to its own baked-in version: a mismatch means a
|
|
86
119
|
// mixed install (stale .amxd instance vs newer extracted UI, or vice versa).
|
|
87
|
-
|
|
120
|
+
reply("build", buildStamp());
|
|
88
121
|
sendCurrentTempo(); // liveapi.ts
|
|
89
122
|
// The device resends its own state here. The page loads asynchronously, so
|
|
90
123
|
// anything sent before it was listening is simply gone.
|
|
@@ -119,7 +152,7 @@ function get_state(id: string): void {
|
|
|
119
152
|
// hands back is what came out of the set. An empty "{}" here after a reopen means
|
|
120
153
|
// the [pattr] did not save - which is a failure with no other symptom.
|
|
121
154
|
post("m4l-jweb: get_state " + id + " -> " + json + "\n");
|
|
122
|
-
|
|
155
|
+
reply("state_" + id, json);
|
|
123
156
|
} catch (e) {
|
|
124
157
|
post("m4l-jweb: get_state error for " + id + " - " + (e as Error).message + "\n");
|
|
125
158
|
}
|
|
@@ -146,11 +179,113 @@ function sync_state(id: string): void {
|
|
|
146
179
|
// bound to nothing), both look exactly like a successful write from here. What the
|
|
147
180
|
// dict says it holds is the only evidence.
|
|
148
181
|
post("m4l-jweb: sync_state " + id + " <- " + json + " (dict now " + d.stringify() + ")\n");
|
|
182
|
+
// BROADCAST the new value to every OTHER view - the device UI and any other
|
|
183
|
+
// window - so a live edit in one page appears in the rest at once. Without this
|
|
184
|
+
// a second page only sees a slot when it next asks for it (on load), which is
|
|
185
|
+
// why an edit in the floating window never reached the device view. The WRITER
|
|
186
|
+
// is skipped: it already applied the value optimistically, and echoing it back
|
|
187
|
+
// could revert the control mid-typing (stateStore has no echo guard).
|
|
188
|
+
broadcastState(id, d.stringify());
|
|
149
189
|
} catch (e) {
|
|
150
190
|
post("m4l-jweb: sync_state error for " + id + " - " + (e as Error).message + "\n");
|
|
151
191
|
}
|
|
152
192
|
}
|
|
153
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Push `state_<id> <json>` to every view except the one that wrote it (replyWindow:
|
|
196
|
+
* a window id, or null for the device view). A window is reached by name, the
|
|
197
|
+
* device view by outlet 0 - the same two paths reply() chooses between.
|
|
198
|
+
*/
|
|
199
|
+
function broadcastState(id: string, json: string): void {
|
|
200
|
+
if (replyWindow !== null) outlet(0, "state_" + id, json); // device view, unless it wrote
|
|
201
|
+
var ids = listWindowIds();
|
|
202
|
+
for (var i = 0; i < ids.length; i++) {
|
|
203
|
+
if (ids[i] !== replyWindow) messnamed("window-read-" + ids[i], "state_" + id, json);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* ------------------------------------------------------------------ *
|
|
208
|
+
* Native dial visibility (layout.native runtime show/hide) - SPIKE
|
|
209
|
+
*
|
|
210
|
+
* The app (useNativeVisibility) sends `native_show`/`native_hide <varname>` to hide a
|
|
211
|
+
* native `live.*` object the current state does not use - the dynamic visibility a
|
|
212
|
+
* static `presentation` attribute cannot give. We reach the object through the Maxobj
|
|
213
|
+
* API (`this.patcher.getnamed`, the same `this.patcher` deviceFolder() uses) and set
|
|
214
|
+
* its `hidden` flag, LOGGING what we find.
|
|
215
|
+
*
|
|
216
|
+
* The open question, and the whole point of the spike: whether `hidden` (or anything
|
|
217
|
+
* reachable from here) makes a native object leave the M4L device PRESENTATION view
|
|
218
|
+
* at runtime. A first attempt - a `[thispatcher]` running `script hide` - was tried
|
|
219
|
+
* and did NOT work (script acts on the patching canvas, not the presentation). If
|
|
220
|
+
* this one also fails, a frozen M4L device cannot hide a native object at runtime and
|
|
221
|
+
* the fallback is a build-time choice. Watch the Max console for the lines below.
|
|
222
|
+
* ------------------------------------------------------------------ */
|
|
223
|
+
|
|
224
|
+
function native_show(varname: string): void {
|
|
225
|
+
setNativeHidden(varname, 0);
|
|
226
|
+
}
|
|
227
|
+
function native_hide(varname: string): void {
|
|
228
|
+
setNativeHidden(varname, 1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function setNativeHidden(varname: string, hidden: number): void {
|
|
232
|
+
try {
|
|
233
|
+
// `this.patcher` is the device patcher (Max's global object IS the jsthis, so a
|
|
234
|
+
// plainly-called function still sees it - deviceFolder() relies on the same).
|
|
235
|
+
var obj = this.patcher.getnamed(varname);
|
|
236
|
+
if (!obj) {
|
|
237
|
+
post("m4l-jweb: native " + varname + " -> getnamed() null (no such scripting name)\n");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// `hidden` is the documented Maxobj visibility toggle. CONFIRMED in Live: it
|
|
241
|
+
// reaches the M4L PRESENTATION view, so a native object vanishes from the device
|
|
242
|
+
// view - which is what the two-screen panel flip (useNativePanel) rides on.
|
|
243
|
+
//
|
|
244
|
+
// Reposition/resize does NOT work the same way: setting `presentation_rect` at
|
|
245
|
+
// runtime is accepted but never redrawn in a frozen M4L device (measured). So
|
|
246
|
+
// there is no `native_rect` here - the panel LAYERS views and hides one, rather
|
|
247
|
+
// than reflowing objects, because hide/show is the only thing that takes.
|
|
248
|
+
obj.hidden = hidden;
|
|
249
|
+
} catch (e) {
|
|
250
|
+
post("m4l-jweb: native " + varname + " error: " + (e as Error).message + "\n");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* ------------------------------------------------------------------ *
|
|
255
|
+
* Floating-window messages
|
|
256
|
+
*
|
|
257
|
+
* A window's page uses the ORDINARY bridge (`outlet(sel, ...)`), so it emits bare
|
|
258
|
+
* selectors just like the device view. They cannot arrive here bare, though: this
|
|
259
|
+
* [js] already has a get_state/ui_ready/etc, and a window's must not be mistaken
|
|
260
|
+
* for the device view's. So the subpatcher TAGS them - `[prepend window <id>]` on
|
|
261
|
+
* the window's [jweb] outlet (see applyWindows in surface.mjs) - and they land
|
|
262
|
+
* here as `window <id> <selector> <args...>`, dispatched on the first word.
|
|
263
|
+
*
|
|
264
|
+
* We then dispatch the INNER selector through the very same handlers, with
|
|
265
|
+
* replyWindow set so any reply routes back to the window and not the device view.
|
|
266
|
+
* That is what gives a window access to the device's state: `get_state`/
|
|
267
|
+
* `sync_state` reach the shared [dict], and `state_<id>` comes back to the window.
|
|
268
|
+
* Anything the library does not know goes to the device's own onWindowMessage().
|
|
269
|
+
* ------------------------------------------------------------------ */
|
|
270
|
+
|
|
271
|
+
function window(id: string): void {
|
|
272
|
+
var selector = String(arguments[1]);
|
|
273
|
+
var args: unknown[] = [];
|
|
274
|
+
for (var i = 2; i < arguments.length; i++) args.push(arguments[i]);
|
|
275
|
+
|
|
276
|
+
var prev = replyWindow;
|
|
277
|
+
replyWindow = id;
|
|
278
|
+
try {
|
|
279
|
+
if (selector === "ui_ready") ui_ready();
|
|
280
|
+
else if (selector === "get_state") get_state(String(args[0]));
|
|
281
|
+
else if (selector === "sync_state") (sync_state as any).apply(null, args);
|
|
282
|
+
else if (typeof onWindowMessage === "function") (onWindowMessage as any).apply(null, [id, selector].concat(args as any[]));
|
|
283
|
+
else post("m4l-jweb: window " + id + " sent unhandled '" + selector + "'\n");
|
|
284
|
+
} finally {
|
|
285
|
+
replyWindow = prev;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
154
289
|
/* ------------------------------------------------------------------ *
|
|
155
290
|
* The self-extracting UI payload
|
|
156
291
|
*
|
|
@@ -179,29 +314,44 @@ function loadWebview(): void {
|
|
|
179
314
|
*
|
|
180
315
|
* A window's [jweb] lives inside a subpatcher, so there is no cord from this
|
|
181
316
|
* [js] to it. `messnamed` reaches the [r window-read-<id>] the build put next to
|
|
182
|
-
* it - naming the receiver instead of the cord.
|
|
183
|
-
*
|
|
184
|
-
* The window payloads are the extra payloads whose name is `<device>_<id>.html`
|
|
185
|
-
* (packageDevices names them; the device's own UI is UI_PAYLOAD and is not in
|
|
186
|
-
* this list). Strip that prefix EXPLICITLY: a regex like /^.*_/ is greedy, so a
|
|
187
|
-
* window id with an underscore in it - `edit_grid` - would arrive as `grid`, and
|
|
188
|
-
* the page would then load into a receiver nobody is listening on.
|
|
317
|
+
* it - naming the receiver instead of the cord. The ids come from listWindowIds().
|
|
189
318
|
*/
|
|
190
319
|
function loadWindows(): void {
|
|
191
|
-
if (typeof EXTRA_PAYLOAD_NAMES === "undefined") return;
|
|
192
320
|
var folder = deviceFolder();
|
|
193
321
|
if (!folder) return;
|
|
194
322
|
|
|
195
|
-
var
|
|
323
|
+
var ids = listWindowIds();
|
|
324
|
+
var prefix = windowPrefix();
|
|
325
|
+
for (var i = 0; i < ids.length; i++) {
|
|
326
|
+
var winId = ids[i];
|
|
327
|
+
var winUrl = encodeURI("file:///" + folder + "/" + prefix + winId + ".html") + "?v=" + encodeURIComponent(buildStamp());
|
|
328
|
+
messnamed("window-read-" + winId, "url", winUrl);
|
|
329
|
+
post("m4l-jweb: window " + winId + " -> " + winUrl + "\n");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** The `<device>_` prefix a window payload's name carries, so the device's own UI is not one. */
|
|
334
|
+
function windowPrefix(): string {
|
|
335
|
+
return typeof UI_PAYLOAD_NAME !== "undefined" ? UI_PAYLOAD_NAME.replace(/\.html$/, "") + "_" : "";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The id of every floating window this device has, from its extracted payloads.
|
|
340
|
+
*
|
|
341
|
+
* Strip the `<device>_` prefix EXPLICITLY: a regex like /^.*_/ is greedy, so a
|
|
342
|
+
* window id with an underscore in it - `edit_grid` - would come back as `grid`.
|
|
343
|
+
*/
|
|
344
|
+
function listWindowIds(): string[] {
|
|
345
|
+
var ids: string[] = [];
|
|
346
|
+
if (typeof EXTRA_PAYLOAD_NAMES === "undefined") return ids;
|
|
347
|
+
var prefix = windowPrefix();
|
|
196
348
|
for (var i = 0; i < EXTRA_PAYLOAD_NAMES.length; i++) {
|
|
197
349
|
var name = EXTRA_PAYLOAD_NAMES[i];
|
|
198
350
|
if (name.slice(-5) !== ".html") continue; // a sample, a preset, ...: not a window
|
|
199
351
|
if (prefix && name.slice(0, prefix.length) !== prefix) continue;
|
|
200
|
-
|
|
201
|
-
var winUrl = encodeURI("file:///" + folder + "/" + name) + "?v=" + encodeURIComponent(buildStamp());
|
|
202
|
-
messnamed("window-read-" + winId, "url", winUrl);
|
|
203
|
-
post("m4l-jweb: window " + winId + " -> " + winUrl + "\n");
|
|
352
|
+
ids.push(name.slice(prefix.length, name.length - 5));
|
|
204
353
|
}
|
|
354
|
+
return ids;
|
|
205
355
|
}
|
|
206
356
|
|
|
207
357
|
function resolveUiUrl(): string | null {
|
package/src/max.d.ts
CHANGED
|
@@ -187,3 +187,12 @@ declare function onMaxurlReply(responseDictName: string): boolean;
|
|
|
187
187
|
|
|
188
188
|
/** Live's tempo changed (and once on attach). */
|
|
189
189
|
declare function onTempoChange(bpm: number): void;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A floating window's page sent a selector the library does not handle itself
|
|
193
|
+
* (i.e. not ui_ready/get_state/sync_state). `windowId` is which window; reply with
|
|
194
|
+
* `outlet(0, ...)` and it routes back to that window automatically (see reply() in
|
|
195
|
+
* core.ts). This is how a window editor - a drum map, a browser - talks to the
|
|
196
|
+
* device beyond shared state.
|
|
197
|
+
*/
|
|
198
|
+
declare function onWindowMessage(windowId: string, selector: string, ...args: unknown[]): void;
|