numbl 0.4.4 → 0.4.5

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.
@@ -261,6 +261,17 @@ export type AxisLimitSpec = [number | null, number | null] | "auto";
261
261
  export type PlotInstruction = {
262
262
  type: "set_figure_handle";
263
263
  handle: number;
264
+ } | {
265
+ /** An HTML UI component (MATLAB `uihtml`): renders self-contained HTML
266
+ * markup in an iframe, bypassing the axes/trace model. `html` is the
267
+ * full HTMLSource string; `id` is a stable per-component key. `data`, when
268
+ * present, is the `Data` property JSON-encoded (via `jsonencode`); the
269
+ * renderer parses it and pushes it to the page's `htmlComponent` so the
270
+ * `setup`/`"DataChanged"` bridge fires (MATLAB `h.Data` → JavaScript). */
271
+ type: "uihtml";
272
+ id: string;
273
+ html: string;
274
+ data?: string;
264
275
  } | {
265
276
  type: "plot";
266
277
  traces: PlotTrace[];
package/dist-lib/lib.js CHANGED
@@ -44556,6 +44556,149 @@ for (const { name, min: min2, max: max2 } of INT_RANGES) {
44556
44556
  ]
44557
44557
  });
44558
44558
  }
44559
+ var TYPECAST_VIEWS = {
44560
+ uint8: (b) => new Uint8Array(b),
44561
+ int8: (b) => new Int8Array(b),
44562
+ uint16: (b) => new Uint16Array(b),
44563
+ int16: (b) => new Int16Array(b),
44564
+ uint32: (b) => new Uint32Array(b),
44565
+ int32: (b) => new Int32Array(b),
44566
+ single: (b) => new Float32Array(b),
44567
+ double: (b) => new Float64Array(b)
44568
+ };
44569
+ defineBuiltin({
44570
+ name: "typecast",
44571
+ help: {
44572
+ signatures: ["B = typecast(X, CLASS)"],
44573
+ description: "Reinterpret the raw bytes of numeric array X as CLASS (e.g. 'uint8', 'single', 'int32'). numbl stores all numerics as double, so X is treated as double-precision; this is mainly for serializing numeric data to bytes, e.g. typecast(double(x), 'uint8')."
44574
+ },
44575
+ cases: [
44576
+ {
44577
+ match: (argTypes) => {
44578
+ if (argTypes.length !== 2) return null;
44579
+ const a = argTypes[0];
44580
+ if (a.kind === "number" || a.kind === "boolean" || a.kind === "tensor" || a.kind === "complex_or_number")
44581
+ return [{ kind: "tensor", isComplex: false }];
44582
+ return null;
44583
+ },
44584
+ apply: (args) => {
44585
+ const X = args[0];
44586
+ const cls = toString(args[1]).toLowerCase();
44587
+ const makeView = TYPECAST_VIEWS[cls];
44588
+ if (!makeView)
44589
+ throw new RuntimeError(`typecast: unsupported class '${cls}'`);
44590
+ let src;
44591
+ if (isRuntimeNumber(X)) src = Float64Array.of(X);
44592
+ else if (isRuntimeLogical(X)) src = Float64Array.of(X ? 1 : 0);
44593
+ else if (isRuntimeTensor(X)) {
44594
+ if (X.imag)
44595
+ throw new RuntimeError("typecast: complex input not supported");
44596
+ src = X.data;
44597
+ } else {
44598
+ throw new RuntimeError("typecast: X must be a numeric array");
44599
+ }
44600
+ const buffer = src.buffer.slice(
44601
+ src.byteOffset,
44602
+ src.byteOffset + src.byteLength
44603
+ );
44604
+ const view = makeView(buffer);
44605
+ const out = allocFloat64Array(view.length);
44606
+ for (let i = 0; i < view.length; i++) out[i] = view[i];
44607
+ return RTV.tensor(out, [1, view.length]);
44608
+ }
44609
+ }
44610
+ ]
44611
+ });
44612
+ function jsonEncodeNumber(x) {
44613
+ return Number.isFinite(x) ? String(x) : "null";
44614
+ }
44615
+ function jsonEncodeTensor(v) {
44616
+ if (v.imag && !imagAllZero(v.imag))
44617
+ throw new RuntimeError("jsonencode: complex values are not supported");
44618
+ const data = v.data;
44619
+ const n = data.length;
44620
+ const enc = v._isLogical ? (x) => x ? "true" : "false" : jsonEncodeNumber;
44621
+ const shape = v.shape ?? [n];
44622
+ if (n === 1 && shape.every((s) => s === 1)) return enc(data[0]);
44623
+ if (n === 0) return "[]";
44624
+ const nonSingleton = shape.filter((s) => s > 1).length;
44625
+ if (shape.length <= 1 || nonSingleton <= 1) {
44626
+ const parts2 = [];
44627
+ for (let i = 0; i < n; i++) parts2.push(enc(data[i]));
44628
+ return "[" + parts2.join(",") + "]";
44629
+ }
44630
+ if (shape.length === 2) {
44631
+ const [m, cols] = shape;
44632
+ const rows = [];
44633
+ for (let i = 0; i < m; i++) {
44634
+ const rowParts = [];
44635
+ for (let j = 0; j < cols; j++) rowParts.push(enc(data[j * m + i]));
44636
+ rows.push("[" + rowParts.join(",") + "]");
44637
+ }
44638
+ return "[" + rows.join(",") + "]";
44639
+ }
44640
+ const parts = [];
44641
+ for (let i = 0; i < n; i++) parts.push(enc(data[i]));
44642
+ return "[" + parts.join(",") + "]";
44643
+ }
44644
+ function jsonEncodeCell(v) {
44645
+ if (v.data.length === 0) return "[]";
44646
+ return "[" + v.data.map(jsonEncodeValue).join(",") + "]";
44647
+ }
44648
+ function jsonEncodeStruct(v) {
44649
+ const parts = [];
44650
+ for (const [key, val] of v.fields) {
44651
+ parts.push(JSON.stringify(key) + ":" + jsonEncodeValue(val));
44652
+ }
44653
+ return "{" + parts.join(",") + "}";
44654
+ }
44655
+ function jsonEncodeValue(v) {
44656
+ if (v === void 0 || v === null) return "null";
44657
+ if (isRuntimeNumber(v)) return jsonEncodeNumber(v);
44658
+ if (isRuntimeLogical(v)) return v ? "true" : "false";
44659
+ if (isRuntimeString(v)) return JSON.stringify(v);
44660
+ if (isRuntimeChar(v)) {
44661
+ if (v.shape && v.shape.length === 2 && v.shape[0] > 1) {
44662
+ const rows = v.shape[0];
44663
+ const cols = v.shape[1];
44664
+ const out = [];
44665
+ for (let i = 0; i < rows; i++) {
44666
+ let row = "";
44667
+ for (let j = 0; j < cols; j++) row += v.value[j * rows + i] ?? "";
44668
+ out.push(JSON.stringify(row));
44669
+ }
44670
+ return "[" + out.join(",") + "]";
44671
+ }
44672
+ return JSON.stringify(v.value);
44673
+ }
44674
+ if (isRuntimeComplexNumber(v)) {
44675
+ if (v.im !== 0)
44676
+ throw new RuntimeError("jsonencode: complex values are not supported");
44677
+ return jsonEncodeNumber(v.re);
44678
+ }
44679
+ if (isRuntimeTensor(v)) return jsonEncodeTensor(v);
44680
+ if (isRuntimeCell(v)) return jsonEncodeCell(v);
44681
+ if (isRuntimeStruct(v)) return jsonEncodeStruct(v);
44682
+ if (isRuntimeStructArray(v))
44683
+ return "[" + v.elements.map(jsonEncodeStruct).join(",") + "]";
44684
+ throw new RuntimeError("jsonencode: unsupported value type");
44685
+ }
44686
+ defineBuiltin({
44687
+ name: "jsonencode",
44688
+ help: {
44689
+ signatures: ["txt = jsonencode(V)"],
44690
+ description: "Encode value V (struct, cell, char/string, logical, or numeric array) as a JSON-formatted char row vector."
44691
+ },
44692
+ cases: [
44693
+ {
44694
+ match: (argTypes) => {
44695
+ if (argTypes.length < 1) return null;
44696
+ return [{ kind: "char" }];
44697
+ },
44698
+ apply: (args) => RTV.char(jsonEncodeValue(args[0]))
44699
+ }
44700
+ ]
44701
+ });
44559
44702
  function idivideMode(args) {
44560
44703
  if (args.length < 3) return "fix";
44561
44704
  const m = args[2];
@@ -51289,7 +51432,15 @@ function dispatchPlotBuiltin(name, args, instructions, state) {
51289
51432
  return true;
51290
51433
  // ── Graphics ops: figure / labels / hold / layout ──────────────
51291
51434
  case "figure": {
51292
- const handle = args.length > 0 ? args[0] : 1;
51435
+ let handle;
51436
+ if (args.length > 0) {
51437
+ handle = toNumber(args[0]);
51438
+ state.maxFigureHandle = Math.max(state.maxFigureHandle ?? 0, handle);
51439
+ } else {
51440
+ handle = (state.maxFigureHandle ?? 0) + 1;
51441
+ state.maxFigureHandle = handle;
51442
+ }
51443
+ state.currentFigureHandle = handle;
51293
51444
  plotInstr(instructions, { type: "set_figure_handle", handle });
51294
51445
  return true;
51295
51446
  }
@@ -52236,6 +52387,10 @@ var SPECIAL_BUILTIN_NAMES = [
52236
52387
  "stream2",
52237
52388
  "ishold",
52238
52389
  "figure",
52390
+ "drawuihtml",
52391
+ "registeruihtmlcallback",
52392
+ "sendEventToHTMLSource",
52393
+ "uigridlayout",
52239
52394
  "subplot",
52240
52395
  "tiledlayout",
52241
52396
  "nexttile",
@@ -53585,15 +53740,68 @@ function registerSpecialBuiltins(rt) {
53585
53740
  return rt.ishold();
53586
53741
  });
53587
53742
  registerSpecial("figure", (nargout, args) => {
53588
- const handle = args.length > 0 ? args[0] : 1;
53589
53743
  dispatchPlotBuiltin(
53590
53744
  "figure",
53591
53745
  args.map(ensureRuntimeValue),
53592
53746
  rt.plotInstructions,
53593
53747
  rt
53594
53748
  );
53595
- return nargout >= 1 ? RTV.num(toNumber(ensureRuntimeValue(handle))) : void 0;
53749
+ return nargout >= 1 ? RTV.num(rt.currentFigureHandle) : void 0;
53750
+ });
53751
+ let uihtmlSeq = 0;
53752
+ registerSpecial("drawuihtml", (nargout, args) => {
53753
+ const html = args.length > 0 ? toString(ensureRuntimeValue(args[0])) : "";
53754
+ const data = args.length > 1 ? toString(ensureRuntimeValue(args[1])) : void 0;
53755
+ uihtmlSeq += 1;
53756
+ const id = "uh" + uihtmlSeq;
53757
+ rt.plotInstructions.push({
53758
+ type: "uihtml",
53759
+ id,
53760
+ html,
53761
+ ...data !== void 0 ? { data } : {}
53762
+ });
53763
+ return nargout >= 1 ? RTV.char(id) : void 0;
53764
+ });
53765
+ registerSpecial("registeruihtmlcallback", (_nargout, args) => {
53766
+ if (args.length < 3) return void 0;
53767
+ const compId = toString(ensureRuntimeValue(args[0]));
53768
+ const eventType = toString(ensureRuntimeValue(args[1]));
53769
+ const fcn = ensureRuntimeValue(args[2]);
53770
+ if (eventType !== "HTMLEventReceived" && eventType !== "DataChanged") {
53771
+ return void 0;
53772
+ }
53773
+ if (!isRuntimeFunction(fcn)) return void 0;
53774
+ const entry = rt.uihtmlCallbacks.get(compId) ?? {};
53775
+ const prev = entry[eventType];
53776
+ if (prev) decref(rt, prev);
53777
+ incref(fcn);
53778
+ entry[eventType] = fcn;
53779
+ rt.uihtmlCallbacks.set(compId, entry);
53780
+ return void 0;
53781
+ });
53782
+ registerSpecial("sendEventToHTMLSource", (_nargout, args) => {
53783
+ if (args.length < 2) {
53784
+ throw new RuntimeError("sendEventToHTMLSource requires src and name");
53785
+ }
53786
+ const src = ensureRuntimeValue(args[0]);
53787
+ if (!isRuntimeStruct(src) || !src.fields.has("ComponentId")) {
53788
+ throw new RuntimeError(
53789
+ "sendEventToHTMLSource: src must be the component passed to the callback"
53790
+ );
53791
+ }
53792
+ const compId = toString(src.fields.get("ComponentId"));
53793
+ const name = toString(ensureRuntimeValue(args[1]));
53794
+ const dataArg = args.length > 2 ? ensureRuntimeValue(args[2]) : RTV.num(0);
53795
+ const dataJson = toString(
53796
+ ensureRuntimeValue(rt.dispatch("jsonencode", 1, [dataArg]))
53797
+ );
53798
+ rt.onHtmlSourceEvent?.(compId, name, dataJson);
53799
+ return void 0;
53596
53800
  });
53801
+ registerSpecial(
53802
+ "uigridlayout",
53803
+ (nargout) => nargout >= 1 ? RTV.num(0) : void 0
53804
+ );
53597
53805
  const PLOT_RETURNS_ZERO = ["subplot", "tiledlayout", "nexttile", "legend"];
53598
53806
  for (const name of PLOT_RETURNS_ZERO) {
53599
53807
  registerSpecial(name, (nargout, args) => {
@@ -56434,6 +56642,19 @@ var figuresReducer = (state, action) => {
56434
56642
  switch (action.type) {
56435
56643
  case "set_figure_handle":
56436
56644
  return { ...state, currentHandle: action.handle };
56645
+ case "uihtml": {
56646
+ const fig = ensureFig(state);
56647
+ return {
56648
+ ...state,
56649
+ figs: {
56650
+ ...state.figs,
56651
+ [state.currentHandle]: {
56652
+ ...fig,
56653
+ uihtml: { id: action.id, html: action.html, data: action.data }
56654
+ }
56655
+ }
56656
+ };
56657
+ }
56437
56658
  case "set_hold":
56438
56659
  return updateAxes(state, { holdOn: action.value });
56439
56660
  case "plot": {
@@ -56874,6 +57095,7 @@ var Runtime = class _Runtime {
56874
57095
  this.profilingEnabled = !!options.profile;
56875
57096
  this.fileIO = options.fileIO;
56876
57097
  this.system = options.system;
57098
+ this.onHtmlSourceEvent = options.onHtmlSourceEvent;
56877
57099
  if (options.initialHoldState) {
56878
57100
  this.holdState = options.initialHoldState;
56879
57101
  }
@@ -56892,9 +57114,24 @@ var Runtime = class _Runtime {
56892
57114
  plotInstructions = [];
56893
57115
  variableValues = {};
56894
57116
  holdState = false;
57117
+ /** Figure-handle tracking for MATLAB-style `figure` allocation: `figure`
57118
+ * with no argument creates a new figure (`maxFigureHandle + 1`). */
57119
+ currentFigureHandle = 0;
57120
+ maxFigureHandle = 0;
56895
57121
  /** Monotonic id source for graphics handles whose trace can be live-updated
56896
57122
  * via `set` / `update_trace` (e.g. lines returned by `line`). */
56897
57123
  graphicsIdCounter = 1;
57124
+ /** Reverse channel for `uihtml` components (HTML → MATLAB). Maps a component
57125
+ * id (the `uihtml` instruction's `id`) to the callback handles registered
57126
+ * for it. When non-empty after a run, the host keeps this runtime alive so
57127
+ * iframe events can re-enter the interpreter and invoke these handles.
57128
+ * Handles are incref'd while stored (see registeruihtmlcallback). */
57129
+ uihtmlCallbacks = /* @__PURE__ */ new Map();
57130
+ /** Hook invoked by `sendEventToHTMLSource(src, name, data)` to push an event
57131
+ * from MATLAB back to a uihtml component's page. `dataJson` is the data
57132
+ * already `jsonencode`d. Set from ExecOptions; the host forwards it to the
57133
+ * iframe. */
57134
+ onHtmlSourceEvent;
56898
57135
  // tiledlayout/nexttile state. Reset by tiledlayout, advanced by nexttile.
56899
57136
  // mode: "fixed" uses the rows/cols verbatim; "flow"/"vertical"/"horizontal"
56900
57137
  // grow the grid as tiles are added.
@@ -57027,6 +57264,15 @@ var Runtime = class _Runtime {
57027
57264
  }
57028
57265
  }
57029
57266
  // ── Builtin initialization ──────────────────────────────────────────
57267
+ /** Register the implicit default figure (handle 1) the first time a graphics
57268
+ * op runs without an explicit `figure`, so a later no-arg `figure` allocates
57269
+ * a new handle instead of reusing 1 (MATLAB semantics). */
57270
+ ensureCurrentFigure() {
57271
+ if (!this.currentFigureHandle) {
57272
+ this.currentFigureHandle = 1;
57273
+ this.maxFigureHandle = Math.max(this.maxFigureHandle, 1);
57274
+ }
57275
+ }
57030
57276
  initBuiltins() {
57031
57277
  registerSpecialBuiltins(this);
57032
57278
  for (const name of PLOT_DISPATCH_NAMES) {
@@ -57172,6 +57418,22 @@ var Runtime = class _Runtime {
57172
57418
  this.builtins["stream2"] = (_nargout, args) => {
57173
57419
  return stream2Call(args.map(ensureRuntimeValue));
57174
57420
  };
57421
+ const NO_AUTO_FIGURE = /* @__PURE__ */ new Set(["figure", "close", "clf", "cla", "axis"]);
57422
+ for (const name of [
57423
+ ...PLOT_DISPATCH_NAMES,
57424
+ "fplot",
57425
+ "fplot3",
57426
+ "streamline",
57427
+ "stream2"
57428
+ ]) {
57429
+ if (NO_AUTO_FIGURE.has(name)) continue;
57430
+ const orig = this.builtins[name];
57431
+ if (!orig) continue;
57432
+ this.builtins[name] = (n, a) => {
57433
+ this.ensureCurrentFigure();
57434
+ return orig(n, a);
57435
+ };
57436
+ }
57175
57437
  }
57176
57438
  profileEnter(key) {
57177
57439
  if (!this.profilingEnabled) return;
@@ -63616,6 +63878,10 @@ end
63616
63878
  {
63617
63879
  name: "isIllConditioned.m",
63618
63880
  source: "function result = isIllConditioned(dA)\n result = dA.isIllCond;\nend\n"
63881
+ },
63882
+ {
63883
+ name: "uihtml.m",
63884
+ source: "classdef uihtml < handle\n %UIHTML Create an HTML UI component (numbl subset).\n % H = UIHTML('HTMLSource', HTML) renders the self-contained HTML string\n % HTML in the figure pane. An optional leading parent argument (e.g. a\n % figure) is accepted and ignored. Supported name-value options:\n % HTMLSource, Data, Position.\n %\n % H = UIHTML('HTMLSource', HTML, 'Data', X) also sends X into the page.\n % Mirroring MATLAB, X is encoded with jsonencode, parsed in the page with\n % JSON.parse, and set on the JavaScript `htmlComponent.Data` object,\n % firing any \"DataChanged\" listener registered in the page's\n % `function setup(htmlComponent)`. To update the data after construction,\n % set H.Data and call show(H) (numbl re-renders the component):\n %\n % h = uihtml('HTMLSource', html, 'Data', struct('n', 1));\n % h.Data = struct('n', 2);\n % show(h);\n %\n % numbl currently supports HTMLSource given as an HTML markup string (a\n % single self-contained document). HTML file paths and supporting files\n % are not yet supported.\n %\n % The reverse channel (page -> MATLAB) is supported for the IDE:\n % HTMLEventReceivedFcn fires when JS calls\n % htmlComponent.sendEventToMATLAB(name,data); inside the callback use\n % sendEventToHTMLSource(src,name,data) to send back to the page.\n % DataChangedFcn fires when JS sets htmlComponent.Data. Register callbacks\n % at construction (name-value) since numbl renders at construction.\n properties\n HTMLSource = ''\n Data = []\n Position = [100 100 100 100]\n HTMLEventReceivedFcn = []\n DataChangedFcn = []\n end\n methods\n function obj = uihtml(varargin)\n args = varargin;\n % Ignore an optional leading parent argument (anything that is not\n % a name-value name string).\n if numel(args) >= 1 && ~(ischar(args{1}) || isstring(args{1}))\n args = args(2:end);\n end\n for i = 1:2:numel(args) - 1\n name = args{i};\n val = args{i + 1};\n if strcmpi(name, 'HTMLSource')\n obj.HTMLSource = val;\n elseif strcmpi(name, 'Data')\n obj.Data = val;\n elseif strcmpi(name, 'Position')\n obj.Position = val;\n elseif strcmpi(name, 'HTMLEventReceivedFcn')\n obj.HTMLEventReceivedFcn = val;\n elseif strcmpi(name, 'DataChangedFcn')\n obj.DataChangedFcn = val;\n end\n end\n render(obj);\n end\n\n function show(obj)\n render(obj);\n end\n\n function render(obj)\n if isempty(obj.HTMLSource)\n return;\n end\n if isempty(obj.Data)\n id = drawuihtml(obj.HTMLSource);\n else\n id = drawuihtml(obj.HTMLSource, jsonencode(obj.Data));\n end\n if ~isempty(obj.HTMLEventReceivedFcn)\n registeruihtmlcallback(id, 'HTMLEventReceived', ...\n obj.HTMLEventReceivedFcn);\n end\n if ~isempty(obj.DataChangedFcn)\n registeruihtmlcallback(id, 'DataChanged', obj.DataChangedFcn);\n end\n end\n end\nend\n"
63619
63885
  }
63620
63886
  ];
63621
63887
 
@@ -91570,6 +91836,64 @@ function registerExecutorsForOpt(registry3, opt) {
91570
91836
  }
91571
91837
  }
91572
91838
 
91839
+ // src/numbl-core/runtime/uihtmlSession.ts
91840
+ function createUihtmlSession(rt, activeSpecials, onDrawnow) {
91841
+ function invokeHandle(fn, args) {
91842
+ if (fn.jsFn) {
91843
+ if (fn.jsFnExpectsNargout) fn.jsFn(0, ...args);
91844
+ else fn.jsFn(...args);
91845
+ return;
91846
+ }
91847
+ rt.dispatch(fn.name, 0, args);
91848
+ }
91849
+ function dispatchEvent(compId, eventType, payload) {
91850
+ const entry = rt.uihtmlCallbacks.get(compId);
91851
+ const fn = entry?.[eventType];
91852
+ if (!fn || !isRuntimeFunction(fn)) return;
91853
+ const saved = /* @__PURE__ */ new Map();
91854
+ for (const name of SPECIAL_BUILTIN_NAMES) {
91855
+ const ex = getIBuiltin(name);
91856
+ if (ex) saved.set(name, ex);
91857
+ }
91858
+ for (const ib of activeSpecials.values()) registerDynamicIBuiltin(ib);
91859
+ pushCurrentRuntime(rt);
91860
+ const before = rt.plotInstructions.length;
91861
+ try {
91862
+ const src = RTV.struct({ ComponentId: RTV.char(compId) });
91863
+ const dataRV = convertJsonValue(payload.data);
91864
+ const event = eventType === "HTMLEventReceived" ? RTV.struct({
91865
+ HTMLEventName: RTV.char(payload.name ?? ""),
91866
+ HTMLEventData: dataRV,
91867
+ Source: src,
91868
+ EventName: RTV.char("HTMLEventReceived")
91869
+ }) : RTV.struct({
91870
+ Data: dataRV,
91871
+ PreviousData: RTV.tensor(new Float64Array(0), [0, 0]),
91872
+ Source: src,
91873
+ EventName: RTV.char("DataChanged")
91874
+ });
91875
+ invokeHandle(fn, [src, event]);
91876
+ } finally {
91877
+ popCurrentRuntime(rt);
91878
+ for (const ib of saved.values()) registerDynamicIBuiltin(ib);
91879
+ }
91880
+ const newInstrs = rt.plotInstructions.slice(before);
91881
+ if (newInstrs.length && onDrawnow) onDrawnow(newInstrs);
91882
+ }
91883
+ function dispose() {
91884
+ for (const entry of rt.uihtmlCallbacks.values()) {
91885
+ if (entry.HTMLEventReceived) decref(rt, entry.HTMLEventReceived);
91886
+ if (entry.DataChanged) decref(rt, entry.DataChanged);
91887
+ }
91888
+ rt.uihtmlCallbacks.clear();
91889
+ }
91890
+ return {
91891
+ hasCallbacks: () => rt.uihtmlCallbacks.size > 0,
91892
+ dispatchEvent,
91893
+ dispose
91894
+ };
91895
+ }
91896
+
91573
91897
  // src/numbl-core/executeCode.ts
91574
91898
  globalThis.FloatXArray = Float64Array;
91575
91899
  var SHIM_SEARCH_PATH = "__numbl_shims__";
@@ -92021,6 +92345,18 @@ ${sections.join("\n\n")}` : `// No ${label} generated`;
92021
92345
  variableValues: interpreter.getVariableValues(),
92022
92346
  holdState: rt.holdState
92023
92347
  };
92348
+ if (rt.uihtmlCallbacks.size > 0) {
92349
+ const activeSpecials = /* @__PURE__ */ new Map();
92350
+ for (const name of SPECIAL_BUILTIN_NAMES) {
92351
+ const ex = getIBuiltin(name);
92352
+ if (ex) activeSpecials.set(name, ex);
92353
+ }
92354
+ result.uihtmlSession = createUihtmlSession(
92355
+ rt,
92356
+ activeSpecials,
92357
+ options.onDrawnow
92358
+ );
92359
+ }
92024
92360
  if (options.profile) {
92025
92361
  result.profileData = {
92026
92362
  executionTimeMs,
@@ -10,6 +10,8 @@ import type { FileIOAdapter } from "./fileIOAdapter.js";
10
10
  import type { SystemAdapter } from "./systemAdapter.js";
11
11
  import type { WorkspaceFile } from "../numbl-core/workspace/index.js";
12
12
  import type { NativeBridge } from "./workspace/index.js";
13
+ import { type UihtmlSession } from "./runtime/uihtmlSession.js";
14
+ export type { UihtmlSession } from "./runtime/uihtmlSession.js";
13
15
  export interface ExecOptions {
14
16
  onOutput?: (text: string) => void;
15
17
  onDrawnow?: (plotInstructions: PlotInstruction[]) => void;
@@ -48,6 +50,10 @@ export interface ExecOptions {
48
50
  implicitCwdPath?: string | null;
49
51
  /** SharedArrayBuffer for cooperative cancellation. Int32[0] != 0 means cancelled. */
50
52
  cancelSAB?: SharedArrayBuffer;
53
+ /** Hook for `sendEventToHTMLSource(src,name,data)` — pushes an event from a
54
+ * uihtml callback back to the component's page (MATLAB → JS). `dataJson` is
55
+ * the data already `jsonencode`d. The host forwards it to the iframe. */
56
+ onHtmlSourceEvent?: (compId: string, name: string, dataJson: string) => void;
51
57
  }
52
58
  export interface BuiltinProfileEntry {
53
59
  totalTimeMs: number;
@@ -94,5 +100,9 @@ export interface ExecResult {
94
100
  workspaceFiles?: WorkspaceFile[];
95
101
  /** Final implicit cwd path (for REPL persistence across commands). */
96
102
  implicitCwdPath?: string | null;
103
+ /** Present when the run left `uihtml` reverse-channel callbacks registered.
104
+ * The host retains this to dispatch later iframe events into the still-live
105
+ * interpreter (see UihtmlSession). Absent for normal runs (no overhead). */
106
+ uihtmlSession?: UihtmlSession;
97
107
  }
98
108
  export declare function executeCode(source: string, options?: ExecOptions, workspaceFiles?: WorkspaceFile[], mainFileName?: string, searchPaths?: string[], nativeBridge?: NativeBridge): ExecResult;
@@ -42,6 +42,12 @@ import type { PlotInstruction } from "../../graphics/types.js";
42
42
  export interface PlotDispatchState {
43
43
  holdState: boolean;
44
44
  tiledLayoutState: TiledLayoutState | null;
45
+ /** Current figure handle (0 = none created yet). */
46
+ currentFigureHandle?: number;
47
+ /** Highest figure handle allocated so far. `figure` with no argument
48
+ * creates a NEW figure with handle `maxFigureHandle + 1` (MATLAB
49
+ * semantics), rather than always reusing handle 1. */
50
+ maxFigureHandle?: number;
45
51
  }
46
52
  /** Active tiled-layout grid. `mode` controls how the grid grows: in
47
53
  * `flow` (default), nexttile expands rows/cols to fit; `vertical` and
@@ -32,9 +32,27 @@ export declare class Runtime {
32
32
  plotInstructions: PlotInstruction[];
33
33
  variableValues: Record<string, RuntimeValue>;
34
34
  holdState: boolean;
35
+ /** Figure-handle tracking for MATLAB-style `figure` allocation: `figure`
36
+ * with no argument creates a new figure (`maxFigureHandle + 1`). */
37
+ currentFigureHandle: number;
38
+ maxFigureHandle: number;
35
39
  /** Monotonic id source for graphics handles whose trace can be live-updated
36
40
  * via `set` / `update_trace` (e.g. lines returned by `line`). */
37
41
  graphicsIdCounter: number;
42
+ /** Reverse channel for `uihtml` components (HTML → MATLAB). Maps a component
43
+ * id (the `uihtml` instruction's `id`) to the callback handles registered
44
+ * for it. When non-empty after a run, the host keeps this runtime alive so
45
+ * iframe events can re-enter the interpreter and invoke these handles.
46
+ * Handles are incref'd while stored (see registeruihtmlcallback). */
47
+ uihtmlCallbacks: Map<string, {
48
+ HTMLEventReceived?: RuntimeValue;
49
+ DataChanged?: RuntimeValue;
50
+ }>;
51
+ /** Hook invoked by `sendEventToHTMLSource(src, name, data)` to push an event
52
+ * from MATLAB back to a uihtml component's page. `dataJson` is the data
53
+ * already `jsonencode`d. Set from ExecOptions; the host forwards it to the
54
+ * iframe. */
55
+ onHtmlSourceEvent?: (compId: string, name: string, dataJson: string) => void;
38
56
  tiledLayoutState: {
39
57
  rows: number;
40
58
  cols: number;
@@ -142,6 +160,10 @@ export declare class Runtime {
142
160
  * every value adopted into the scope is decref'd. Used by `execStmt`
143
161
  * to bound the lifetime of expression transients to one statement. */
144
162
  withScope<T>(fn: () => T): T;
163
+ /** Register the implicit default figure (handle 1) the first time a graphics
164
+ * op runs without an explicit `figure`, so a later no-arg `figure` allocates
165
+ * a new handle instead of reusing 1 (MATLAB semantics). */
166
+ ensureCurrentFigure(): void;
145
167
  private initBuiltins;
146
168
  profileEnter(key: string): void;
147
169
  profileLeave(): void;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * uihtml reverse channel (HTML → MATLAB) for a finished run.
3
+ *
4
+ * When a script creates a `uihtml` component with a callback
5
+ * (HTMLEventReceivedFcn / DataChangedFcn), the run leaves entries in
6
+ * `rt.uihtmlCallbacks`. `executeCode` then builds a `UihtmlSession` and returns
7
+ * it so the host (the worker) can keep the interpreter alive and re-enter it
8
+ * when an event arrives from the iframe — mirroring MATLAB, where the script
9
+ * returns but its callbacks keep firing.
10
+ *
11
+ * Dispatch re-activates the run's own special-builtin closures (captured at
12
+ * end-of-run, NOT re-registered — re-registering would reset their counters)
13
+ * and pushes the runtime so `disp`, plotting, and `sendEventToHTMLSource`
14
+ * inside the callback reach this runtime. This mirrors executeCode's own
15
+ * save/restore of SPECIAL_BUILTIN_NAMES.
16
+ */
17
+ import type { Runtime } from "./runtime.js";
18
+ import type { PlotInstruction } from "../../graphics/types.js";
19
+ import { type IBuiltin } from "../interpreter/builtins/types.js";
20
+ export interface UihtmlSession {
21
+ /** True while at least one component still has a registered callback. */
22
+ hasCallbacks(): boolean;
23
+ /** Dispatch an event from a component's page into MATLAB. `eventType` is
24
+ * "HTMLEventReceived" (JS `sendEventToMATLAB`) or "DataChanged" (JS set
25
+ * `htmlComponent.Data`). `payload.data` is the structured-clone'd JS value.
26
+ * New plot output is flushed via `onDrawnow`; outgoing
27
+ * `sendEventToHTMLSource` calls go through `rt.onHtmlSourceEvent`. */
28
+ dispatchEvent(compId: string, eventType: "HTMLEventReceived" | "DataChanged", payload: {
29
+ name?: string;
30
+ data: unknown;
31
+ }): void;
32
+ /** Release the retained callback handles (and their refs). */
33
+ dispose(): void;
34
+ }
35
+ /**
36
+ * Build a session over a runtime that registered uihtml callbacks.
37
+ * `activeSpecials` is a snapshot of this runtime's special-builtin closures,
38
+ * captured at end-of-run while they are still installed globally.
39
+ */
40
+ export declare function createUihtmlSession(rt: Runtime, activeSpecials: Map<string, IBuiltin>, onDrawnow?: (plotInstructions: PlotInstruction[]) => void): UihtmlSession;
@@ -1,2 +1,2 @@
1
1
  /** Numbl version, used for JIT disk cache invalidation. */
2
- export declare const NUMBL_VERSION = "0.4.4";
2
+ export declare const NUMBL_VERSION = "0.4.5";