@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m4l-jweb/build",
3
- "version": "0.6.5",
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.6.5"
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.6.0",
21
- "@m4l-jweb/surface": "^0.6.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.6.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",