@m4l-jweb/build 0.6.5 → 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/surface.mjs +145 -4
- package/templates/starter/package.json +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m4l-jweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "m4l-jweb: the CLI that builds and packages a device repo into installable Max for Live devices.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
"archiver": "^7.0.1",
|
|
34
34
|
"esbuild": "^0.25.0",
|
|
35
35
|
"typescript": "^5.7.0",
|
|
36
|
-
"@m4l-jweb/wrapper": "0.
|
|
36
|
+
"@m4l-jweb/wrapper": "0.7.0"
|
|
37
37
|
}
|
|
38
38
|
}
|
package/src/surface.mjs
CHANGED
|
@@ -124,7 +124,61 @@ export async function loadSurface(root, uiDir) {
|
|
|
124
124
|
* Generating the objects
|
|
125
125
|
* ------------------------------------------------------------------ */
|
|
126
126
|
|
|
127
|
-
const MAXCLASS = { dial: "live.dial", toggle: "live.toggle", menu: "live.menu" };
|
|
127
|
+
const MAXCLASS = { dial: "live.dial", toggle: "live.toggle", menu: "live.menu", button: "live.text" };
|
|
128
|
+
|
|
129
|
+
/* ------------------------------------------------------------------ *
|
|
130
|
+
* Native declarative layout (surface `layout.native`)
|
|
131
|
+
*
|
|
132
|
+
* A parameter listed in `layout.native` renders as a native `live.*` object IN
|
|
133
|
+
* THE DEVICE VIEW, next to a `[jweb]` shifted right to make room. This is a pure
|
|
134
|
+
* PRESENTATION overlay: the dial the compiler already emits gains three keys
|
|
135
|
+
* (`presentation`, `presentation_rect`, `varname`) and nothing about its wiring,
|
|
136
|
+
* its fan-out or the app's `set_<id>` route changes. A dial that carries no rect
|
|
137
|
+
* is exactly today's invisible object.
|
|
138
|
+
* ------------------------------------------------------------------ */
|
|
139
|
+
|
|
140
|
+
// The device view Live gives an M4L device. Height is fixed at ~169 px; the width
|
|
141
|
+
// is whatever the presentation content needs, which is the whole mechanism this
|
|
142
|
+
// relies on (Live recomputes device width from the presentation rects).
|
|
143
|
+
const DEVICE_H = 169;
|
|
144
|
+
const MARGIN = 8;
|
|
145
|
+
// Per-kind native sizes, from Max's own live.* defaults. A live.dial includes its
|
|
146
|
+
// own label under the knob, which is why it is taller than a bare toggle.
|
|
147
|
+
const NATIVE_SIZE = { dial: [44, 48], toggle: [44, 15], menu: [100, 15], button: [48, 15] };
|
|
148
|
+
// Vertical pitch: a dial is 48 px tall and wants 8 px of air beneath its label.
|
|
149
|
+
const PITCH_Y = 56;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* `id -> presentation_rect` for every native parameter, plus the zone's total
|
|
153
|
+
* width (how far `[jweb]` shifts right). A pure function of the declaration, so
|
|
154
|
+
* `tests/` can drive it directly.
|
|
155
|
+
*
|
|
156
|
+
* Column-major: fills `rows` down the first column, then starts a new column to
|
|
157
|
+
* the right. Adding a parameter at the end therefore never reshuffles the ones
|
|
158
|
+
* before it - the reading order stays stable.
|
|
159
|
+
*/
|
|
160
|
+
export function computeNativeSlots(surface) {
|
|
161
|
+
const native = surface?.layout?.native;
|
|
162
|
+
if (!native || native.params.length === 0) return { slots: new Map(), width: 0 };
|
|
163
|
+
const rows = native.rows ?? 3;
|
|
164
|
+
const slots = new Map();
|
|
165
|
+
let row = 0;
|
|
166
|
+
let colW = 0;
|
|
167
|
+
let x = MARGIN;
|
|
168
|
+
for (const id of native.params) {
|
|
169
|
+
const [w, h] = NATIVE_SIZE[surface.params[id].kind];
|
|
170
|
+
if (row >= rows) {
|
|
171
|
+
// Column full: step right by the widest box in the column just finished.
|
|
172
|
+
row = 0;
|
|
173
|
+
x += colW + MARGIN;
|
|
174
|
+
colW = 0;
|
|
175
|
+
}
|
|
176
|
+
slots.set(id, [x, MARGIN + row * PITCH_Y, w, h]);
|
|
177
|
+
colW = Math.max(colW, w);
|
|
178
|
+
row += 1;
|
|
179
|
+
}
|
|
180
|
+
return { slots, width: x + colW + MARGIN };
|
|
181
|
+
}
|
|
128
182
|
|
|
129
183
|
/**
|
|
130
184
|
* Max's `parameter_type`: 0 = float, 1 = int, 2 = enum.
|
|
@@ -134,7 +188,7 @@ const MAXCLASS = { dial: "live.dial", toggle: "live.toggle", menu: "live.menu" }
|
|
|
134
188
|
* where a float one would read "2.4 of [off 1/4 1/8 ...]".
|
|
135
189
|
*/
|
|
136
190
|
function parameterType(spec) {
|
|
137
|
-
if (spec.kind === "menu" || spec.kind === "toggle") return 2;
|
|
191
|
+
if (spec.kind === "menu" || spec.kind === "toggle" || spec.kind === "button") return 2;
|
|
138
192
|
return spec.step === 1 ? 1 : 0;
|
|
139
193
|
}
|
|
140
194
|
|
|
@@ -183,7 +237,7 @@ function unitAttrs(spec) {
|
|
|
183
237
|
|
|
184
238
|
/** The parameter's value as MAX stores it: numbers, always. */
|
|
185
239
|
function initialValue(spec) {
|
|
186
|
-
if (spec.kind === "toggle") return spec.default ? 1 : 0;
|
|
240
|
+
if (spec.kind === "toggle" || spec.kind === "button") return spec.default ? 1 : 0;
|
|
187
241
|
if (spec.kind === "menu") return spec.options.indexOf(spec.default);
|
|
188
242
|
return spec.default;
|
|
189
243
|
}
|
|
@@ -226,7 +280,7 @@ function parameterAttrs(id, spec) {
|
|
|
226
280
|
if (spec.steps !== undefined) attrs.parameter_steps = spec.steps;
|
|
227
281
|
}
|
|
228
282
|
|
|
229
|
-
if (spec.kind === "toggle") {
|
|
283
|
+
if (spec.kind === "toggle" || spec.kind === "button") {
|
|
230
284
|
attrs.parameter_mmax = 1;
|
|
231
285
|
attrs.parameter_enum = ["off", "on"];
|
|
232
286
|
}
|
|
@@ -239,6 +293,16 @@ function parameterAttrs(id, spec) {
|
|
|
239
293
|
return attrs;
|
|
240
294
|
}
|
|
241
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Box-level (not parameter) attributes a kind needs. A `button` is a `live.text`,
|
|
298
|
+
* which unlike a `live.toggle` carries VISIBLE TEXT: `text`/`texton` is the label it
|
|
299
|
+
* shows off/on, and `mode: 1` makes it a toggle rather than a momentary button.
|
|
300
|
+
*/
|
|
301
|
+
function kindBoxAttrs(spec) {
|
|
302
|
+
if (spec.kind === "button") return { text: spec.label, texton: spec.label, mode: 1 };
|
|
303
|
+
return {};
|
|
304
|
+
}
|
|
305
|
+
|
|
242
306
|
/**
|
|
243
307
|
* Compile the Surface into the patcher.
|
|
244
308
|
*
|
|
@@ -250,9 +314,29 @@ export function applySurface(ctx) {
|
|
|
250
314
|
const { boxes, lines, surface, jwebId } = ctx;
|
|
251
315
|
if (!surface || surface.ids.length === 0) return;
|
|
252
316
|
|
|
317
|
+
// Where the native dials go, if any were declared. `slots` is empty for a
|
|
318
|
+
// surface with no `layout`, and then this whole feature is inert - a param
|
|
319
|
+
// carries no presentation rect and stays the invisible object it is today.
|
|
320
|
+
const native = surface.layout?.native;
|
|
321
|
+
const { slots, width: nativeW } = computeNativeSlots(surface);
|
|
322
|
+
|
|
323
|
+
// The view switch (panel layouts) is NOT a grid dial: it is pinned to the
|
|
324
|
+
// top-right, over where the web UI paints its own switch button, so the control
|
|
325
|
+
// stays in one place when the app flips between the two views. It is excluded from
|
|
326
|
+
// `computeNativeSlots` (it is not in `native.params`), so give it a rect here.
|
|
327
|
+
const switchId = native?.switch;
|
|
328
|
+
const jwebBox = boxes.find((b) => b.box.id === jwebId)?.box;
|
|
329
|
+
const [, , jpw] = jwebBox?.presentation_rect ?? [0, 0, 420, DEVICE_H];
|
|
330
|
+
let switchRect = null;
|
|
331
|
+
if (switchId) {
|
|
332
|
+
const [sw, sh] = NATIVE_SIZE[surface.params[switchId].kind];
|
|
333
|
+
switchRect = [jpw - sw - MARGIN, MARGIN, sw, sh];
|
|
334
|
+
}
|
|
335
|
+
|
|
253
336
|
let x = 480;
|
|
254
337
|
for (const id of surface.ids) {
|
|
255
338
|
const spec = surface.params[id];
|
|
339
|
+
const rect = id === switchId ? switchRect : slots.get(id);
|
|
256
340
|
boxes.push({
|
|
257
341
|
box: {
|
|
258
342
|
id: paramObject(id),
|
|
@@ -262,6 +346,13 @@ export function applySurface(ctx) {
|
|
|
262
346
|
outlettype: [""],
|
|
263
347
|
parameter_enable: 1,
|
|
264
348
|
patching_rect: [x, 300, 44, 48],
|
|
349
|
+
...kindBoxAttrs(spec),
|
|
350
|
+
// A native param is shown in the device view: it gets a presentation rect
|
|
351
|
+
// and a scripting name (prefixed `param-` so it cannot collide with a state
|
|
352
|
+
// dict's `obj-state-<id>` varname). Everything else about the box - the
|
|
353
|
+
// wiring below, the fan-out, useParam() - is UNCHANGED. Presentation is
|
|
354
|
+
// purely a display overlay on the same graph.
|
|
355
|
+
...(rect ? { presentation: 1, presentation_rect: rect, varname: `param-${id}` } : {}),
|
|
265
356
|
saved_attribute_attributes: { valueof: parameterAttrs(id, spec) },
|
|
266
357
|
},
|
|
267
358
|
});
|
|
@@ -273,6 +364,26 @@ export function applySurface(ctx) {
|
|
|
273
364
|
x += 56;
|
|
274
365
|
}
|
|
275
366
|
|
|
367
|
+
// Position [jweb] for the native layout. WIDTH is preserved (React layouts were
|
|
368
|
+
// built for 420 px). A surface with no native content leaves [jweb] where the
|
|
369
|
+
// template put it.
|
|
370
|
+
if ((nativeW > 0 || switchId) && jwebBox) {
|
|
371
|
+
const [, py, pw, ph] = jwebBox.presentation_rect ?? [0, 0, 420, DEVICE_H];
|
|
372
|
+
if (native.panel) {
|
|
373
|
+
// LAYERED two-screen: [jweb] covers the whole device and the dials overlap its
|
|
374
|
+
// left. The app shows one layer at a time (useNativePanel), so the overlap is
|
|
375
|
+
// never seen - web mode fills the full width, no reserved strip.
|
|
376
|
+
jwebBox.presentation_rect = [0, py, Math.max(pw, nativeW), ph];
|
|
377
|
+
} else {
|
|
378
|
+
// Side-by-side: the dials to the left, [jweb] shifted right, both visible.
|
|
379
|
+
jwebBox.presentation_rect = [nativeW, py, pw, ph];
|
|
380
|
+
}
|
|
381
|
+
// Give [jweb] a scripting name so the wrapper can hide/show it at runtime
|
|
382
|
+
// (useNativePanel). Must equal JWEB_VARNAME in @m4l-jweb/surface - the one string
|
|
383
|
+
// the app and this codegen must agree on.
|
|
384
|
+
jwebBox.varname = "obj-jweb";
|
|
385
|
+
}
|
|
386
|
+
|
|
276
387
|
// Write direction: one route for every `set_<id>` the app can send. It goes at
|
|
277
388
|
// the END of the chain of routes (see claimAppMessages), so a chain that already
|
|
278
389
|
// took [jweb]'s outlet keeps it and hands us what it did not match.
|
|
@@ -295,6 +406,36 @@ export function applySurface(ctx) {
|
|
|
295
406
|
});
|
|
296
407
|
}
|
|
297
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Runtime show/hide of native dials - the `layout.native` visibility override.
|
|
411
|
+
*
|
|
412
|
+
* `layout.native` makes a parameter a native `live.*` object, and its presentation
|
|
413
|
+
* is STATIC: `presentation: 1` is stamped into the .amxd at build time, so every
|
|
414
|
+
* listed dial is always visible. A device like the fx line wants the opposite - a
|
|
415
|
+
* stage the current line does not name should not clutter the view, the way its old
|
|
416
|
+
* HTML slider was simply not rendered. React can hide a slider; a native object
|
|
417
|
+
* cannot hide itself, so the app drives it from outside via `native_show`/
|
|
418
|
+
* `native_hide <varname>` (useNativeVisibility).
|
|
419
|
+
*
|
|
420
|
+
* ------------------------------------------------------------------------------
|
|
421
|
+
* THIS IS A SPIKE, and its FIRST mechanism already FAILED IN LIVE.
|
|
422
|
+
*
|
|
423
|
+
* Attempt 1 (this codegen, now retired): a `[thispatcher]` running `script hide
|
|
424
|
+
* <varname>`. Verified in Live - the dials did NOT disappear. `script hide` acts on
|
|
425
|
+
* the PATCHING canvas; the M4L device view is the PRESENTATION, and `script` has no
|
|
426
|
+
* documented reach into it.
|
|
427
|
+
*
|
|
428
|
+
* Attempt 2 (current): the wrapper's [js] handles `native_show`/`native_hide` and
|
|
429
|
+
* manipulates the object through the Maxobj API (`this.patcher.getnamed(varname)`),
|
|
430
|
+
* with console diagnostics. So this codegen no longer claims the messages - it lets
|
|
431
|
+
* them fall through to the wrapper. See `native_show`/`native_hide` in core.ts.
|
|
432
|
+
*
|
|
433
|
+
* If attempt 2 also fails to reach the presentation, the honest conclusion is that a
|
|
434
|
+
* frozen M4L device cannot hide a native object at runtime, and the fallback is a
|
|
435
|
+
* build-time choice (fewer native params, or a device keeps HTML controls).
|
|
436
|
+
* ------------------------------------------------------------------------------
|
|
437
|
+
*/
|
|
438
|
+
|
|
298
439
|
/**
|
|
299
440
|
* Compile a declared `window` into the patcher: a subpatcher holding its own
|
|
300
441
|
* [jweb], and a [pcontrol] that opens it when the app asks.
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
"format": "prettier --write \"src/**/*.{ts,tsx,css}\" \"scripts/*.mjs\" \"patcher/*.mjs\""
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@m4l-jweb/bridge": "^0.
|
|
21
|
-
"@m4l-jweb/surface": "^0.
|
|
20
|
+
"@m4l-jweb/bridge": "^0.7.0",
|
|
21
|
+
"@m4l-jweb/surface": "^0.7.0",
|
|
22
22
|
"react": "^19.0.0",
|
|
23
23
|
"react-dom": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@m4l-jweb/build": "^0.
|
|
26
|
+
"@m4l-jweb/build": "^0.7.0",
|
|
27
27
|
"@types/node": "^22.0.0",
|
|
28
28
|
"@types/react": "^19.0.0",
|
|
29
29
|
"@types/react-dom": "^19.0.0",
|