@almadar/ui 5.149.0 → 5.150.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/dist/avl/index.js CHANGED
@@ -3355,7 +3355,7 @@ function useEventBus() {
3355
3355
  return {
3356
3356
  ...baseBus,
3357
3357
  emit: (type, payload, source) => {
3358
- if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
3358
+ if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".") && !source?.trait) {
3359
3359
  scopeLog.warn("emit:bare-key-no-scope", { type });
3360
3360
  }
3361
3361
  baseBus.emit(type, payload, source);
@@ -15045,6 +15045,14 @@ function shapeBounds(shape) {
15045
15045
  w: shape.radius * 2 + 8,
15046
15046
  h: shape.radius * 2 + 8
15047
15047
  };
15048
+ case "ellipse":
15049
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
15050
+ return {
15051
+ x: shape.x - shape.width / 2 - 4,
15052
+ y: shape.y - shape.height / 2 - 4,
15053
+ w: shape.width + 8,
15054
+ h: shape.height + 8
15055
+ };
15048
15056
  case "rect":
15049
15057
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
15050
15058
  return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
@@ -15078,13 +15086,14 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
15078
15086
  ctx.closePath();
15079
15087
  ctx.fill();
15080
15088
  }
15081
- function drawShape(ctx, shape, width, height) {
15089
+ function drawShape(ctx, shape, width, height, allShapes) {
15082
15090
  ctx.save();
15083
15091
  const opacity = shape.opacity ?? 1;
15084
15092
  ctx.globalAlpha = opacity;
15085
15093
  const stroke = resolveColor2(shape.color, ctx, "#333333");
15086
15094
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
15087
15095
  ctx.lineWidth = shape.lineWidth ?? 2;
15096
+ if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
15088
15097
  switch (shape.type) {
15089
15098
  case "grid": {
15090
15099
  const step = shape.step ?? 40;
@@ -15150,6 +15159,20 @@ function drawShape(ctx, shape, width, height) {
15150
15159
  ctx.stroke();
15151
15160
  break;
15152
15161
  }
15162
+ case "ellipse": {
15163
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
15164
+ const startAngle = (shape.startAngle ?? 0) * Math.PI / 180;
15165
+ const endAngle = (shape.endAngle ?? 360) * Math.PI / 180;
15166
+ ctx.beginPath();
15167
+ ctx.ellipse(shape.x, shape.y, shape.width / 2, shape.height / 2, 0, startAngle, endAngle);
15168
+ if (fill) {
15169
+ ctx.fillStyle = fill;
15170
+ ctx.fill();
15171
+ }
15172
+ ctx.strokeStyle = stroke;
15173
+ ctx.stroke();
15174
+ break;
15175
+ }
15153
15176
  case "rect": {
15154
15177
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
15155
15178
  if (fill) {
@@ -15196,21 +15219,153 @@ function drawShape(ctx, shape, width, height) {
15196
15219
  ctx.fillText(shape.text, shape.x, shape.y);
15197
15220
  break;
15198
15221
  }
15222
+ case "venn-region": {
15223
+ const resolveCircles = (ids) => (ids ?? []).flatMap((id) => {
15224
+ const c = allShapes.find((s) => s.type === "circle" && s.id === id);
15225
+ return c && c.x != null && c.y != null && c.radius != null ? [{ x: c.x, y: c.y, radius: c.radius }] : [];
15226
+ });
15227
+ const inside = resolveCircles(shape.inside);
15228
+ if (inside.length === 0) break;
15229
+ const outside = resolveCircles(shape.outside);
15230
+ const off = document.createElement("canvas");
15231
+ off.width = ctx.canvas.width;
15232
+ off.height = ctx.canvas.height;
15233
+ const octx = off.getContext("2d");
15234
+ if (!octx) break;
15235
+ octx.setTransform(ctx.getTransform());
15236
+ for (const c of inside) {
15237
+ const p = new Path2D();
15238
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
15239
+ octx.clip(p);
15240
+ }
15241
+ octx.fillStyle = fill ?? stroke;
15242
+ octx.fillRect(0, 0, width, height);
15243
+ octx.globalCompositeOperation = "destination-out";
15244
+ for (const c of outside) {
15245
+ const p = new Path2D();
15246
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
15247
+ octx.fill(p);
15248
+ }
15249
+ ctx.save();
15250
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
15251
+ ctx.drawImage(off, 0, 0);
15252
+ ctx.restore();
15253
+ break;
15254
+ }
15199
15255
  }
15200
15256
  ctx.restore();
15201
15257
  }
15202
- var LearningCanvas;
15258
+ function readoutShapes(readouts, width) {
15259
+ const out = [];
15260
+ const chipH = 18;
15261
+ const gap = 6;
15262
+ let rightEdge = width - 6;
15263
+ let rowY = 6;
15264
+ for (const readout of readouts) {
15265
+ const text = `${readout.label}: ${String(readout.value)}`;
15266
+ const chipW = Math.min(170, Math.max(34, text.length * 6 + 12));
15267
+ let chipX = rightEdge - chipW;
15268
+ if (chipX < 4) {
15269
+ rowY += chipH + 4;
15270
+ rightEdge = width - 6;
15271
+ chipX = rightEdge - chipW;
15272
+ }
15273
+ const color = readout.color ?? "#334155";
15274
+ out.push({ type: "rect", x: chipX, y: rowY, width: chipW, height: chipH, color, fill: color });
15275
+ out.push({
15276
+ type: "text",
15277
+ x: chipX + chipW / 2,
15278
+ y: rowY + chipH / 2,
15279
+ text,
15280
+ color: "#ffffff",
15281
+ fontSize: 10,
15282
+ align: "center"
15283
+ });
15284
+ rightEdge = chipX - gap;
15285
+ }
15286
+ return out;
15287
+ }
15288
+ function traceShapes(panel, k, width, height) {
15289
+ const w = panel.width ?? Math.round(width * 0.32);
15290
+ const h = panel.height ?? Math.round(height * 0.28);
15291
+ const x = panel.x ?? width - w - 8;
15292
+ const y = panel.y ?? height - h - 8 - k * (h + 8);
15293
+ const allSamples = panel.series.flatMap((series) => series.samples);
15294
+ let xLo = Math.min(...allSamples.map((p) => p.x));
15295
+ let xHi = Math.max(...allSamples.map((p) => p.x));
15296
+ let yLo = Math.min(...allSamples.map((p) => p.y));
15297
+ let yHi = Math.max(...allSamples.map((p) => p.y));
15298
+ if (xLo === xHi) {
15299
+ xLo -= 1;
15300
+ xHi += 1;
15301
+ }
15302
+ if (yLo === yHi) {
15303
+ yLo -= 1;
15304
+ yHi += 1;
15305
+ }
15306
+ const backgroundColor = panel.backgroundColor ?? "#ffffff";
15307
+ const frameColor = panel.frameColor ?? "#94a3b8";
15308
+ const out = [];
15309
+ out.push({
15310
+ type: "rect",
15311
+ x,
15312
+ y,
15313
+ width: w,
15314
+ height: h,
15315
+ color: backgroundColor,
15316
+ fill: backgroundColor,
15317
+ opacity: panel.backgroundOpacity ?? 0.85
15318
+ });
15319
+ out.push({ type: "rect", x, y, width: w, height: h, color: frameColor, lineWidth: 1 });
15320
+ panel.series.forEach((series, j) => {
15321
+ const color = series.color ?? TRACE_SERIES_COLORS[j % TRACE_SERIES_COLORS.length];
15322
+ const mapped = series.samples.map((p) => ({
15323
+ x: x + 4 + (p.x - xLo) / (xHi - xLo) * (w - 8),
15324
+ y: y + h - 4 - (p.y - yLo) / (yHi - yLo) * (h - 8)
15325
+ }));
15326
+ for (let i = 1; i < mapped.length; i++) {
15327
+ out.push({
15328
+ type: "line",
15329
+ x1: mapped[i - 1].x,
15330
+ y1: mapped[i - 1].y,
15331
+ x2: mapped[i].x,
15332
+ y2: mapped[i].y,
15333
+ color,
15334
+ lineWidth: 1.5
15335
+ });
15336
+ }
15337
+ if (mapped.length > 0) {
15338
+ const last = mapped[mapped.length - 1];
15339
+ out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
15340
+ }
15341
+ if (series.label) {
15342
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
15343
+ }
15344
+ });
15345
+ if (panel.yLabel) {
15346
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
15347
+ }
15348
+ if (panel.xLabel) {
15349
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
15350
+ }
15351
+ return out;
15352
+ }
15353
+ var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
15203
15354
  var init_LearningCanvas = __esm({
15204
15355
  "components/learning/atoms/LearningCanvas.tsx"() {
15205
15356
  "use client";
15206
15357
  init_cn();
15207
15358
  init_useEventBus();
15359
+ DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
15360
+ TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
15208
15361
  LearningCanvas = ({
15209
15362
  className,
15210
15363
  width = 600,
15211
15364
  height = 400,
15212
15365
  backgroundColor,
15213
15366
  shapes = [],
15367
+ readouts,
15368
+ traces,
15214
15369
  interactive = false,
15215
15370
  animate = false,
15216
15371
  onShapeClick,
@@ -15236,6 +15391,12 @@ var init_LearningCanvas = __esm({
15236
15391
  }
15237
15392
  return -1;
15238
15393
  }, [shapes]);
15394
+ const derivedShapes = useMemo(() => {
15395
+ if (!traces?.length && !readouts?.length) return shapes;
15396
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
15397
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
15398
+ return [...shapes, ...traceOut, ...readoutOut];
15399
+ }, [shapes, traces, readouts, width, height]);
15239
15400
  const draw = useCallback(() => {
15240
15401
  const canvas = canvasRef.current;
15241
15402
  if (!canvas) return;
@@ -15252,13 +15413,13 @@ var init_LearningCanvas = __esm({
15252
15413
  ctx.fillStyle = backgroundColor;
15253
15414
  ctx.fillRect(0, 0, width, height);
15254
15415
  }
15255
- for (const shape of shapes) {
15256
- if (shape.type !== "text") drawShape(ctx, shape, width, height);
15416
+ for (const shape of derivedShapes) {
15417
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
15257
15418
  }
15258
- for (const shape of shapes) {
15259
- if (shape.type === "text") drawShape(ctx, shape, width, height);
15419
+ for (const shape of derivedShapes) {
15420
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
15260
15421
  }
15261
- }, [width, height, backgroundColor, shapes]);
15422
+ }, [width, height, backgroundColor, derivedShapes]);
15262
15423
  useEffect(() => {
15263
15424
  draw();
15264
15425
  }, [draw]);
@@ -16808,7 +16969,363 @@ var init_ComponentPatterns = __esm({
16808
16969
  AlertPattern.displayName = "AlertPattern";
16809
16970
  }
16810
16971
  });
16811
- var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, AlgorithmCanvas;
16972
+ function layoutCircle(nodes, width, height) {
16973
+ const cx = width / 2;
16974
+ const cy = height / 2;
16975
+ const radius = Math.max(10, Math.min(cx, cy) - 40);
16976
+ const positions = /* @__PURE__ */ new Map();
16977
+ const n = nodes.length;
16978
+ nodes.forEach((node, i) => {
16979
+ const angle = 2 * Math.PI * i / Math.max(n, 1) - Math.PI / 2;
16980
+ positions.set(node.id, { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) });
16981
+ });
16982
+ return positions;
16983
+ }
16984
+ function layoutTree2(nodes, edges, root, width, height) {
16985
+ const nodeIds = nodes.map((n) => n.id);
16986
+ const idSet = new Set(nodeIds);
16987
+ const childrenOf = /* @__PURE__ */ new Map();
16988
+ const hasIncoming = /* @__PURE__ */ new Set();
16989
+ for (const e of edges) {
16990
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
16991
+ const list = childrenOf.get(e.from) ?? [];
16992
+ list.push(e.to);
16993
+ childrenOf.set(e.from, list);
16994
+ hasIncoming.add(e.to);
16995
+ }
16996
+ const depth = /* @__PURE__ */ new Map();
16997
+ const treeChildren = /* @__PURE__ */ new Map();
16998
+ const visited = /* @__PURE__ */ new Set();
16999
+ const bfsFrom = (start) => {
17000
+ if (visited.has(start)) return;
17001
+ visited.add(start);
17002
+ depth.set(start, 0);
17003
+ const queue = [start];
17004
+ while (queue.length > 0) {
17005
+ const u = queue.shift();
17006
+ for (const v of childrenOf.get(u) ?? []) {
17007
+ if (visited.has(v)) continue;
17008
+ visited.add(v);
17009
+ depth.set(v, (depth.get(u) ?? 0) + 1);
17010
+ const list = treeChildren.get(u) ?? [];
17011
+ list.push(v);
17012
+ treeChildren.set(u, list);
17013
+ queue.push(v);
17014
+ }
17015
+ }
17016
+ };
17017
+ const primaryRoot = root && idSet.has(root) ? root : nodeIds.find((id) => !hasIncoming.has(id)) ?? nodeIds[0];
17018
+ const rootsOrder = [];
17019
+ if (primaryRoot !== void 0) {
17020
+ bfsFrom(primaryRoot);
17021
+ rootsOrder.push(primaryRoot);
17022
+ }
17023
+ for (const id of nodeIds) {
17024
+ if (!visited.has(id)) {
17025
+ bfsFrom(id);
17026
+ rootsOrder.push(id);
17027
+ }
17028
+ }
17029
+ let leafCounter = 0;
17030
+ const xSlot = /* @__PURE__ */ new Map();
17031
+ const assignXSlot = (u) => {
17032
+ const children = treeChildren.get(u) ?? [];
17033
+ if (children.length === 0) {
17034
+ const slot = leafCounter++;
17035
+ xSlot.set(u, slot);
17036
+ return slot;
17037
+ }
17038
+ const childSlots = children.map(assignXSlot);
17039
+ const avg = childSlots.reduce((a, b) => a + b, 0) / childSlots.length;
17040
+ xSlot.set(u, avg);
17041
+ return avg;
17042
+ };
17043
+ for (const r2 of rootsOrder) assignXSlot(r2);
17044
+ let maxDepth = 0;
17045
+ for (const d of depth.values()) maxDepth = Math.max(maxDepth, d);
17046
+ const colWidth = width / Math.max(1, leafCounter);
17047
+ const rowHeight = height / (maxDepth + 1);
17048
+ const positions = /* @__PURE__ */ new Map();
17049
+ for (const id of nodeIds) {
17050
+ const slot = xSlot.get(id) ?? 0;
17051
+ const d = depth.get(id) ?? 0;
17052
+ positions.set(id, { x: slot * colWidth + colWidth / 2, y: d * rowHeight + rowHeight / 2 });
17053
+ }
17054
+ return positions;
17055
+ }
17056
+ function layoutLayered(nodes, edges, width, height) {
17057
+ const nodeIds = nodes.map((n) => n.id);
17058
+ const idSet = new Set(nodeIds);
17059
+ const adj = /* @__PURE__ */ new Map();
17060
+ const remainingIndegree = /* @__PURE__ */ new Map();
17061
+ for (const id of nodeIds) remainingIndegree.set(id, 0);
17062
+ for (const e of edges) {
17063
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
17064
+ const list = adj.get(e.from) ?? [];
17065
+ list.push(e.to);
17066
+ adj.set(e.from, list);
17067
+ remainingIndegree.set(e.to, (remainingIndegree.get(e.to) ?? 0) + 1);
17068
+ }
17069
+ const layer = /* @__PURE__ */ new Map();
17070
+ const dequeued = /* @__PURE__ */ new Set();
17071
+ const queue = [];
17072
+ for (const id of nodeIds) {
17073
+ if ((remainingIndegree.get(id) ?? 0) === 0) {
17074
+ layer.set(id, 0);
17075
+ queue.push(id);
17076
+ }
17077
+ }
17078
+ while (queue.length > 0) {
17079
+ const u = queue.shift();
17080
+ dequeued.add(u);
17081
+ for (const v of adj.get(u) ?? []) {
17082
+ const candidate = (layer.get(u) ?? 0) + 1;
17083
+ layer.set(v, Math.max(layer.get(v) ?? 0, candidate));
17084
+ remainingIndegree.set(v, (remainingIndegree.get(v) ?? 0) - 1);
17085
+ if ((remainingIndegree.get(v) ?? 0) === 0 && !dequeued.has(v)) {
17086
+ queue.push(v);
17087
+ }
17088
+ }
17089
+ }
17090
+ let baseMaxLayer = 0;
17091
+ for (const id of nodeIds) {
17092
+ if (dequeued.has(id)) baseMaxLayer = Math.max(baseMaxLayer, layer.get(id) ?? 0);
17093
+ }
17094
+ const cycleLayer = baseMaxLayer + 1;
17095
+ let maxLayer = baseMaxLayer;
17096
+ for (const id of nodeIds) {
17097
+ if (!dequeued.has(id)) {
17098
+ layer.set(id, cycleLayer);
17099
+ maxLayer = cycleLayer;
17100
+ }
17101
+ }
17102
+ const colWidth = width / Math.max(1, maxLayer + 1);
17103
+ const byLayer = /* @__PURE__ */ new Map();
17104
+ for (const id of nodeIds) {
17105
+ const l = layer.get(id) ?? 0;
17106
+ const list = byLayer.get(l) ?? [];
17107
+ list.push(id);
17108
+ byLayer.set(l, list);
17109
+ }
17110
+ const positions = /* @__PURE__ */ new Map();
17111
+ for (const [l, ids] of byLayer) {
17112
+ const rowHeight = height / ids.length;
17113
+ ids.forEach((id, i) => {
17114
+ positions.set(id, { x: l * colWidth + colWidth / 2, y: i * rowHeight + rowHeight / 2 });
17115
+ });
17116
+ }
17117
+ return positions;
17118
+ }
17119
+ function computePositions(nodes, edges, layout, root, width, height) {
17120
+ switch (layout) {
17121
+ case "circle":
17122
+ return layoutCircle(nodes, width, height);
17123
+ case "tree":
17124
+ return layoutTree2(nodes, edges, root, width, height);
17125
+ case "layered":
17126
+ return layoutLayered(nodes, edges, width, height);
17127
+ case "manual":
17128
+ default: {
17129
+ const positions = /* @__PURE__ */ new Map();
17130
+ for (const n of nodes) positions.set(n.id, { x: n.x ?? 0, y: n.y ?? 0 });
17131
+ return positions;
17132
+ }
17133
+ }
17134
+ }
17135
+ var NODE_STATE_COLOR, EDGE_STATE_COLOR, DEFAULT_NODE_RADIUS, AlgoGraphCanvas;
17136
+ var init_AlgoGraphCanvas = __esm({
17137
+ "components/learning/molecules/AlgoGraphCanvas.tsx"() {
17138
+ "use client";
17139
+ init_atoms();
17140
+ init_Stack();
17141
+ init_LearningCanvas();
17142
+ NODE_STATE_COLOR = {
17143
+ unvisited: "#cbd5e1",
17144
+ frontier: "#f59e0b",
17145
+ current: "#ef4444",
17146
+ visited: "#22c55e",
17147
+ goal: "#8b5cf6",
17148
+ path: "#0ea5e9"
17149
+ };
17150
+ EDGE_STATE_COLOR = {
17151
+ default: "#9ca3af",
17152
+ tree: "#16a34a",
17153
+ relaxed: "#f59e0b",
17154
+ candidate: "#38bdf8",
17155
+ path: "#dc2626"
17156
+ };
17157
+ DEFAULT_NODE_RADIUS = 18;
17158
+ AlgoGraphCanvas = ({
17159
+ className,
17160
+ width = 600,
17161
+ height = 400,
17162
+ title,
17163
+ backgroundColor,
17164
+ nodes = [],
17165
+ edges = [],
17166
+ layout = "manual",
17167
+ root,
17168
+ shapes = [],
17169
+ interactive = false,
17170
+ animate = false,
17171
+ onShapeClick,
17172
+ onNodeClick,
17173
+ isLoading,
17174
+ error
17175
+ }) => {
17176
+ const nodeById = useMemo(() => {
17177
+ const m = /* @__PURE__ */ new Map();
17178
+ for (const n of nodes) m.set(n.id, n);
17179
+ return m;
17180
+ }, [nodes]);
17181
+ const nodeIndexById = useMemo(() => {
17182
+ const m = /* @__PURE__ */ new Map();
17183
+ nodes.forEach((n, i) => m.set(n.id, i));
17184
+ return m;
17185
+ }, [nodes]);
17186
+ const derivedShapes = useMemo(() => {
17187
+ const out = [];
17188
+ const positions = computePositions(nodes, edges, layout, root, width, height);
17189
+ const edgeGeoms = [];
17190
+ for (const e of edges) {
17191
+ const a = nodeById.get(e.from);
17192
+ const b = nodeById.get(e.to);
17193
+ const posA = positions.get(e.from);
17194
+ const posB = positions.get(e.to);
17195
+ if (!a || !b || !posA || !posB) continue;
17196
+ const rA = a.radius ?? DEFAULT_NODE_RADIUS;
17197
+ const rB = b.radius ?? DEFAULT_NODE_RADIUS;
17198
+ const dx = posB.x - posA.x;
17199
+ const dy = posB.y - posA.y;
17200
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
17201
+ const ux = dx / dist;
17202
+ const uy = dy / dist;
17203
+ edgeGeoms.push({
17204
+ directed: e.directed ?? false,
17205
+ x1: posA.x + ux * rA,
17206
+ y1: posA.y + uy * rA,
17207
+ x2: posB.x - ux * rB,
17208
+ y2: posB.y - uy * rB,
17209
+ color: e.color ?? EDGE_STATE_COLOR[e.state ?? "default"],
17210
+ label: e.label ?? (e.weight != null ? String(e.weight) : void 0)
17211
+ });
17212
+ }
17213
+ for (const g of edgeGeoms) {
17214
+ out.push({
17215
+ type: g.directed ? "arrow" : "line",
17216
+ x1: g.x1,
17217
+ y1: g.y1,
17218
+ x2: g.x2,
17219
+ y2: g.y2,
17220
+ color: g.color,
17221
+ lineWidth: 2
17222
+ });
17223
+ }
17224
+ for (const g of edgeGeoms) {
17225
+ if (g.label === void 0) continue;
17226
+ const midX = (g.x1 + g.x2) / 2;
17227
+ const midY = (g.y1 + g.y2) / 2;
17228
+ const dx = g.x2 - g.x1;
17229
+ const dy = g.y2 - g.y1;
17230
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
17231
+ const perpX = -(dy / dist);
17232
+ const perpY = dx / dist;
17233
+ out.push({
17234
+ type: "text",
17235
+ x: midX + perpX * 10,
17236
+ y: midY + perpY * 10,
17237
+ text: g.label,
17238
+ fontSize: 11,
17239
+ align: "center",
17240
+ color: "#374151"
17241
+ });
17242
+ }
17243
+ const nodeGeoms = [];
17244
+ for (const n of nodes) {
17245
+ const pos = positions.get(n.id);
17246
+ if (!pos) continue;
17247
+ nodeGeoms.push({
17248
+ id: n.id,
17249
+ x: pos.x,
17250
+ y: pos.y,
17251
+ radius: n.radius ?? DEFAULT_NODE_RADIUS,
17252
+ color: n.color ?? NODE_STATE_COLOR[n.state ?? "unvisited"],
17253
+ label: n.label,
17254
+ badge: n.badge
17255
+ });
17256
+ }
17257
+ for (const g of nodeGeoms) {
17258
+ out.push({ type: "circle", id: g.id, x: g.x, y: g.y, radius: g.radius, color: g.color, fill: `${g.color}33` });
17259
+ }
17260
+ const badgeGeoms = [];
17261
+ for (const g of nodeGeoms) {
17262
+ if (!g.badge) continue;
17263
+ const w = Math.min(42, Math.max(18, g.badge.text.length * 6 + 10));
17264
+ badgeGeoms.push({
17265
+ cx: g.x + g.radius * 0.75,
17266
+ cy: g.y - g.radius * 0.75,
17267
+ w,
17268
+ h: 14,
17269
+ // Borderless pill: same color drives both stroke and fill.
17270
+ color: g.badge.color ?? "#1e293b",
17271
+ text: g.badge.text
17272
+ });
17273
+ }
17274
+ for (const b of badgeGeoms) {
17275
+ out.push({ type: "rect", x: b.cx - b.w / 2, y: b.cy - b.h / 2, width: b.w, height: b.h, color: b.color, fill: b.color });
17276
+ }
17277
+ for (const b of badgeGeoms) {
17278
+ out.push({ type: "text", x: b.cx, y: b.cy, text: b.text, fontSize: 9, align: "center", color: "#ffffff" });
17279
+ }
17280
+ for (const g of nodeGeoms) {
17281
+ if (g.label === void 0) continue;
17282
+ out.push({
17283
+ type: "text",
17284
+ x: g.x,
17285
+ y: g.y + g.radius + 14,
17286
+ text: g.label,
17287
+ fontSize: 12,
17288
+ align: "center",
17289
+ color: "#111827"
17290
+ });
17291
+ }
17292
+ out.push(...shapes);
17293
+ return out;
17294
+ }, [nodes, edges, layout, root, width, height, nodeById, shapes]);
17295
+ const handleShapeClick = useCallback(
17296
+ (payload) => {
17297
+ if (payload.type === "circle" && payload.id) {
17298
+ const node = nodeById.get(payload.id);
17299
+ const idx = nodeIndexById.get(payload.id);
17300
+ if (node && idx !== void 0) {
17301
+ onNodeClick?.({ id: node.id, label: node.label, index: idx });
17302
+ }
17303
+ }
17304
+ onShapeClick?.(payload);
17305
+ },
17306
+ [nodeById, nodeIndexById, onNodeClick, onShapeClick]
17307
+ );
17308
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
17309
+ title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
17310
+ /* @__PURE__ */ jsx(
17311
+ LearningCanvas,
17312
+ {
17313
+ width,
17314
+ height,
17315
+ backgroundColor,
17316
+ shapes: derivedShapes,
17317
+ interactive,
17318
+ animate,
17319
+ onShapeClick: onShapeClick || onNodeClick ? handleShapeClick : void 0,
17320
+ isLoading,
17321
+ error
17322
+ }
17323
+ )
17324
+ ] }) });
17325
+ };
17326
+ }
17327
+ });
17328
+ var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, PANEL_FAMILY_ORDER, RANGE_COLOR_DEFAULT, RANGE_FILL_OPACITY, BRACKET_TOP_OFFSET, BRACKET_ROW_H, BRACKET_TICK_H, BRACKET_LABEL_OFFSET, SLOT_EMPTY_FILL, SLOT_EMPTY_STROKE, SLOT_FILLED_STROKE, SLOT_HIGHLIGHT_DEFAULT, SLOT_VALUE_TEXT_COLOR, FRAME_ACTIVE_COLOR, FRAME_RETURNING_COLOR, FRAME_DONE_COLOR, FRAME_LABEL_COLOR, FRAME_DETAIL_COLOR, FRAME_TWO_LINE_MIN_H, BUCKET_INDEX_FILL, BUCKET_INDEX_STROKE, BUCKET_INDEX_TEXT, BUCKET_ENTRY_TEXT, BUCKET_ENTRY_DEFAULT, BUCKET_ENTRY_HIGHLIGHT, BUCKET_ENTRY_PROBING, BUCKET_ENTRY_MIN_W, BUCKET_ENTRY_MAX_W, AXIS_LABEL_COLOR, AXIS_LABEL_FONT_SIZE, CORNER_TEXT_COLOR, CORNER_FONT_SIZE, CORNER_MIN_CELL, CORNER_INSET_X, CORNER_INSET_Y, AUX_PRIMARY_RATIO, AUX_LABEL_BAND, AUX_BASELINE_PAD, AlgorithmCanvas;
16812
17329
  var init_AlgorithmCanvas = __esm({
16813
17330
  "components/learning/molecules/AlgorithmCanvas.tsx"() {
16814
17331
  "use client";
@@ -16820,6 +17337,43 @@ var init_AlgorithmCanvas = __esm({
16820
17337
  DEFAULT_POINTER_COLOR = "#dc2626";
16821
17338
  POINTER_BAND = 34;
16822
17339
  TOP_PAD = 26;
17340
+ PANEL_FAMILY_ORDER = ["bars", "slots", "cells", "buckets", "frames"];
17341
+ RANGE_COLOR_DEFAULT = "#3b82f6";
17342
+ RANGE_FILL_OPACITY = 0.15;
17343
+ BRACKET_TOP_OFFSET = 16;
17344
+ BRACKET_ROW_H = 14;
17345
+ BRACKET_TICK_H = 6;
17346
+ BRACKET_LABEL_OFFSET = 6;
17347
+ SLOT_EMPTY_FILL = "#f1f5f9";
17348
+ SLOT_EMPTY_STROKE = "#cbd5e1";
17349
+ SLOT_FILLED_STROKE = "#9ca3af";
17350
+ SLOT_HIGHLIGHT_DEFAULT = "#f59e0b";
17351
+ SLOT_VALUE_TEXT_COLOR = "#ffffff";
17352
+ FRAME_ACTIVE_COLOR = "#3b82f6";
17353
+ FRAME_RETURNING_COLOR = "#f59e0b";
17354
+ FRAME_DONE_COLOR = "#94a3b8";
17355
+ FRAME_LABEL_COLOR = "#ffffff";
17356
+ FRAME_DETAIL_COLOR = "#e2e8f0";
17357
+ FRAME_TWO_LINE_MIN_H = 22;
17358
+ BUCKET_INDEX_FILL = "#e2e8f0";
17359
+ BUCKET_INDEX_STROKE = "#9ca3af";
17360
+ BUCKET_INDEX_TEXT = "#374151";
17361
+ BUCKET_ENTRY_TEXT = "#ffffff";
17362
+ BUCKET_ENTRY_DEFAULT = "#3b82f6";
17363
+ BUCKET_ENTRY_HIGHLIGHT = "#f59e0b";
17364
+ BUCKET_ENTRY_PROBING = "#38bdf8";
17365
+ BUCKET_ENTRY_MIN_W = 24;
17366
+ BUCKET_ENTRY_MAX_W = 64;
17367
+ AXIS_LABEL_COLOR = "#6b7280";
17368
+ AXIS_LABEL_FONT_SIZE = 10;
17369
+ CORNER_TEXT_COLOR = "#111827";
17370
+ CORNER_FONT_SIZE = 7;
17371
+ CORNER_MIN_CELL = 28;
17372
+ CORNER_INSET_X = 3;
17373
+ CORNER_INSET_Y = 6;
17374
+ AUX_PRIMARY_RATIO = 0.6;
17375
+ AUX_LABEL_BAND = 18;
17376
+ AUX_BASELINE_PAD = 8;
16823
17377
  AlgorithmCanvas = ({
16824
17378
  className,
16825
17379
  width = 600,
@@ -16829,6 +17383,14 @@ var init_AlgorithmCanvas = __esm({
16829
17383
  bars = [],
16830
17384
  cells = [],
16831
17385
  pointers = [],
17386
+ ranges = [],
17387
+ slots = [],
17388
+ slotOrientation = "horizontal",
17389
+ frames = [],
17390
+ buckets = [],
17391
+ auxBars = [],
17392
+ rowLabels = [],
17393
+ colLabels = [],
16832
17394
  shapes = [],
16833
17395
  interactive = false,
16834
17396
  animate = false,
@@ -16838,12 +17400,35 @@ var init_AlgorithmCanvas = __esm({
16838
17400
  }) => {
16839
17401
  const derivedShapes = useMemo(() => {
16840
17402
  const out = [];
17403
+ const presence = {
17404
+ bars: bars.length > 0,
17405
+ slots: slots.length > 0,
17406
+ cells: cells.length > 0,
17407
+ buckets: buckets.length > 0,
17408
+ frames: frames.length > 0
17409
+ };
17410
+ const panelCount = PANEL_FAMILY_ORDER.filter((f3) => presence[f3]).length;
17411
+ const panelHeight = height / Math.max(1, panelCount);
17412
+ const panelY = { bars: 0, slots: 0, cells: 0, buckets: 0, frames: 0 };
17413
+ let compactIndex = 0;
17414
+ PANEL_FAMILY_ORDER.forEach((f3) => {
17415
+ if (presence[f3]) {
17416
+ panelY[f3] = compactIndex * panelHeight;
17417
+ compactIndex += 1;
17418
+ }
17419
+ });
16841
17420
  if (bars.length > 0) {
17421
+ const panelYBars = panelY.bars;
16842
17422
  const slot = width / bars.length;
16843
17423
  const barW = slot * 0.8;
16844
17424
  const gap = slot * 0.1;
16845
- const baseline = height - POINTER_BAND;
16846
- const usableH = baseline - TOP_PAD;
17425
+ const bracketRanges = ranges.filter((r2) => r2.kind === "bracket");
17426
+ const bracketCount = bracketRanges.length;
17427
+ const bracketHeadroom = bracketCount > 0 ? BRACKET_TOP_OFFSET + bracketCount * BRACKET_ROW_H : 0;
17428
+ const hasAux = auxBars.length > 0;
17429
+ const primaryH = hasAux ? panelHeight * AUX_PRIMARY_RATIO : panelHeight;
17430
+ const baseline = panelYBars + primaryH - POINTER_BAND;
17431
+ const usableH = baseline - (panelYBars + TOP_PAD + bracketHeadroom);
16847
17432
  const maxV = Math.max(1, ...bars.map((b) => Number.isFinite(b.value) ? b.value : 0));
16848
17433
  bars.forEach((bar, i) => {
16849
17434
  const v = Number.isFinite(bar.value) ? bar.value : 0;
@@ -16873,6 +17458,89 @@ var init_AlgorithmCanvas = __esm({
16873
17458
  });
16874
17459
  }
16875
17460
  });
17461
+ ranges.forEach((r2) => {
17462
+ const kind = r2.kind ?? "fill";
17463
+ if (kind !== "fill") return;
17464
+ const color = r2.color ?? RANGE_COLOR_DEFAULT;
17465
+ out.push({
17466
+ type: "rect",
17467
+ x: r2.from * slot,
17468
+ y: panelYBars,
17469
+ width: (r2.to - r2.from + 1) * slot,
17470
+ height: primaryH,
17471
+ color,
17472
+ fill: color,
17473
+ opacity: RANGE_FILL_OPACITY
17474
+ });
17475
+ if (r2.label) {
17476
+ out.push({
17477
+ type: "text",
17478
+ x: r2.from * slot + 4,
17479
+ // Sits below the bracket block (if any) so fill and bracket labels never collide.
17480
+ y: panelYBars + 10 + bracketHeadroom,
17481
+ text: r2.label,
17482
+ color,
17483
+ fontSize: 10,
17484
+ align: "left"
17485
+ });
17486
+ }
17487
+ });
17488
+ bracketRanges.forEach((r2, i) => {
17489
+ const bracketY = panelYBars + BRACKET_TOP_OFFSET + i * BRACKET_ROW_H;
17490
+ const x1 = r2.from * slot + slot * 0.1;
17491
+ const x2 = (r2.to + 1) * slot - slot * 0.1;
17492
+ const color = r2.color ?? RANGE_COLOR_DEFAULT;
17493
+ out.push({ type: "line", x1, y1: bracketY, x2, y2: bracketY, color, lineWidth: 2 });
17494
+ out.push({ type: "line", x1, y1: bracketY, x2: x1, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
17495
+ out.push({ type: "line", x1: x2, y1: bracketY, x2, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
17496
+ if (r2.label) {
17497
+ out.push({
17498
+ type: "text",
17499
+ x: (x1 + x2) / 2,
17500
+ y: bracketY - BRACKET_LABEL_OFFSET,
17501
+ text: r2.label,
17502
+ color,
17503
+ fontSize: 10,
17504
+ align: "center"
17505
+ });
17506
+ }
17507
+ });
17508
+ if (hasAux) {
17509
+ const auxH = panelHeight - primaryH;
17510
+ const slot2 = width / auxBars.length;
17511
+ const auxBaseline = panelYBars + primaryH + auxH - AUX_BASELINE_PAD;
17512
+ const auxUsableH = auxBaseline - (panelYBars + primaryH + AUX_LABEL_BAND);
17513
+ const maxAuxV = Math.max(1, ...auxBars.map((b) => Number.isFinite(b.value) ? b.value : 0));
17514
+ auxBars.forEach((bar, i) => {
17515
+ const v = Number.isFinite(bar.value) ? bar.value : 0;
17516
+ const bh = Math.max(0, v / maxAuxV * auxUsableH);
17517
+ const x = i * slot2 + slot2 * 0.1;
17518
+ const w = slot2 * 0.8;
17519
+ const color = bar.color ?? DEFAULT_BAR_COLOR;
17520
+ out.push({
17521
+ type: "rect",
17522
+ id: `auxbar-${i}`,
17523
+ x,
17524
+ y: auxBaseline - bh,
17525
+ width: w,
17526
+ height: bh,
17527
+ color,
17528
+ fill: color
17529
+ });
17530
+ const label = bar.label ?? (auxBars.length <= 24 ? String(v) : void 0);
17531
+ if (label) {
17532
+ out.push({
17533
+ type: "text",
17534
+ x: x + w / 2,
17535
+ y: auxBaseline - bh - 8,
17536
+ text: label,
17537
+ color: "#374151",
17538
+ fontSize: 11,
17539
+ align: "center"
17540
+ });
17541
+ }
17542
+ });
17543
+ }
16876
17544
  pointers.forEach((p) => {
16877
17545
  if (p.index < 0 || p.index >= bars.length) return;
16878
17546
  const cx = p.index * slot + slot / 2;
@@ -16880,7 +17548,7 @@ var init_AlgorithmCanvas = __esm({
16880
17548
  out.push({
16881
17549
  type: "arrow",
16882
17550
  x1: cx,
16883
- y1: height - 6,
17551
+ y1: panelYBars + primaryH - 18,
16884
17552
  x2: cx,
16885
17553
  y2: baseline + 4,
16886
17554
  color,
@@ -16890,7 +17558,7 @@ var init_AlgorithmCanvas = __esm({
16890
17558
  out.push({
16891
17559
  type: "text",
16892
17560
  x: cx,
16893
- y: height - 22,
17561
+ y: panelYBars + primaryH - 8,
16894
17562
  text: p.label,
16895
17563
  color,
16896
17564
  fontSize: 11,
@@ -16899,14 +17567,111 @@ var init_AlgorithmCanvas = __esm({
16899
17567
  }
16900
17568
  });
16901
17569
  }
17570
+ if (slots.length > 0) {
17571
+ const panelYSlots = panelY.slots;
17572
+ const n = slots.length;
17573
+ const vertical = slotOrientation === "vertical";
17574
+ const vBoxH = panelHeight / n;
17575
+ const vBoxW = Math.min(width * 0.5, 120);
17576
+ const vBoxX = (width - vBoxW) / 2;
17577
+ const hCellW = width / n;
17578
+ const hBoxW = hCellW * 0.82;
17579
+ const hBoxH = Math.min(panelHeight * 0.6, 48);
17580
+ const hBoxY = panelYSlots + (panelHeight - hBoxH) / 2;
17581
+ const slotBox = (i) => vertical ? { x: vBoxX, y: panelYSlots + panelHeight - (i + 1) * vBoxH, width: vBoxW, height: vBoxH } : { x: i * hCellW + (hCellW - hBoxW) / 2, y: hBoxY, width: hBoxW, height: hBoxH };
17582
+ slots.forEach((s, i) => {
17583
+ const box = slotBox(i);
17584
+ const state = s.state ?? "filled";
17585
+ const fill = state === "empty" ? SLOT_EMPTY_FILL : state === "highlight" ? s.color ?? SLOT_HIGHLIGHT_DEFAULT : s.color ?? DEFAULT_BAR_COLOR;
17586
+ const stroke = state === "empty" ? SLOT_EMPTY_STROKE : SLOT_FILLED_STROKE;
17587
+ out.push({
17588
+ type: "rect",
17589
+ id: `slot-${i}`,
17590
+ x: box.x,
17591
+ y: box.y,
17592
+ width: box.width,
17593
+ height: box.height,
17594
+ color: stroke,
17595
+ fill
17596
+ });
17597
+ if (s.value != null && state !== "empty") {
17598
+ out.push({
17599
+ type: "text",
17600
+ x: box.x + box.width / 2,
17601
+ y: box.y + box.height / 2,
17602
+ text: String(s.value),
17603
+ color: SLOT_VALUE_TEXT_COLOR,
17604
+ fontSize: 12,
17605
+ align: "center"
17606
+ });
17607
+ }
17608
+ });
17609
+ if (bars.length === 0) {
17610
+ pointers.forEach((p) => {
17611
+ if (p.index < 0 || p.index >= slots.length) return;
17612
+ const box = slotBox(p.index);
17613
+ const color = p.color ?? DEFAULT_POINTER_COLOR;
17614
+ if (vertical) {
17615
+ const cy = box.y + box.height / 2;
17616
+ out.push({
17617
+ type: "arrow",
17618
+ x1: box.x + box.width + 34,
17619
+ y1: cy,
17620
+ x2: box.x + box.width + 4,
17621
+ y2: cy,
17622
+ color,
17623
+ lineWidth: 2
17624
+ });
17625
+ if (p.label) {
17626
+ out.push({
17627
+ type: "text",
17628
+ x: box.x + box.width + 38,
17629
+ y: cy,
17630
+ text: p.label,
17631
+ color,
17632
+ fontSize: 11,
17633
+ align: "left"
17634
+ });
17635
+ }
17636
+ } else {
17637
+ const cx = box.x + box.width / 2;
17638
+ out.push({
17639
+ type: "arrow",
17640
+ x1: cx,
17641
+ y1: panelYSlots + panelHeight - 18,
17642
+ x2: cx,
17643
+ y2: box.y + box.height + 4,
17644
+ color,
17645
+ lineWidth: 2
17646
+ });
17647
+ if (p.label) {
17648
+ out.push({
17649
+ type: "text",
17650
+ x: cx,
17651
+ y: panelYSlots + panelHeight - 8,
17652
+ text: p.label,
17653
+ color,
17654
+ fontSize: 11,
17655
+ align: "center"
17656
+ });
17657
+ }
17658
+ }
17659
+ });
17660
+ }
17661
+ }
16902
17662
  if (cells.length > 0) {
17663
+ const panelYCells = panelY.cells;
16903
17664
  const maxCol = Math.max(0, ...cells.map((c) => c.col)) + 1;
16904
17665
  const maxRow = Math.max(0, ...cells.map((c) => c.row)) + 1;
16905
- const cw = width / maxCol;
16906
- const ch = height / maxRow;
17666
+ const colLabelH = colLabels.length > 0 ? 16 : 0;
17667
+ const rowLabelW = rowLabels.length > 0 ? 20 : 0;
17668
+ const gridX0 = rowLabelW;
17669
+ const gridY0 = panelYCells + colLabelH;
17670
+ const cw = (width - rowLabelW) / maxCol;
17671
+ const ch = (panelHeight - colLabelH) / maxRow;
16907
17672
  cells.forEach((c, i) => {
16908
- const x = c.col * cw;
16909
- const y = c.row * ch;
17673
+ const x = gridX0 + c.col * cw;
17674
+ const y = gridY0 + c.row * ch;
16910
17675
  const color = c.color ?? DEFAULT_CELL_COLOR;
16911
17676
  out.push({
16912
17677
  type: "rect",
@@ -16930,11 +17695,207 @@ var init_AlgorithmCanvas = __esm({
16930
17695
  align: "center"
16931
17696
  });
16932
17697
  }
17698
+ if (c.corner && cw >= CORNER_MIN_CELL && ch >= CORNER_MIN_CELL) {
17699
+ const { tl, tr, bl, br } = c.corner;
17700
+ if (tl) {
17701
+ out.push({
17702
+ type: "text",
17703
+ x: x + CORNER_INSET_X,
17704
+ y: y + CORNER_INSET_Y,
17705
+ text: tl,
17706
+ color: CORNER_TEXT_COLOR,
17707
+ fontSize: CORNER_FONT_SIZE,
17708
+ align: "left"
17709
+ });
17710
+ }
17711
+ if (tr) {
17712
+ out.push({
17713
+ type: "text",
17714
+ x: x + cw - CORNER_INSET_X,
17715
+ y: y + CORNER_INSET_Y,
17716
+ text: tr,
17717
+ color: CORNER_TEXT_COLOR,
17718
+ fontSize: CORNER_FONT_SIZE,
17719
+ align: "right"
17720
+ });
17721
+ }
17722
+ if (bl) {
17723
+ out.push({
17724
+ type: "text",
17725
+ x: x + CORNER_INSET_X,
17726
+ y: y + ch - CORNER_INSET_Y,
17727
+ text: bl,
17728
+ color: CORNER_TEXT_COLOR,
17729
+ fontSize: CORNER_FONT_SIZE,
17730
+ align: "left"
17731
+ });
17732
+ }
17733
+ if (br) {
17734
+ out.push({
17735
+ type: "text",
17736
+ x: x + cw - CORNER_INSET_X,
17737
+ y: y + ch - CORNER_INSET_Y,
17738
+ text: br,
17739
+ color: CORNER_TEXT_COLOR,
17740
+ fontSize: CORNER_FONT_SIZE,
17741
+ align: "right"
17742
+ });
17743
+ }
17744
+ }
17745
+ });
17746
+ colLabels.forEach((l) => {
17747
+ out.push({
17748
+ type: "text",
17749
+ x: gridX0 + l.index * cw + cw / 2,
17750
+ y: panelYCells + colLabelH / 2,
17751
+ text: l.text,
17752
+ color: l.color ?? AXIS_LABEL_COLOR,
17753
+ fontSize: AXIS_LABEL_FONT_SIZE,
17754
+ align: "center"
17755
+ });
17756
+ });
17757
+ rowLabels.forEach((l) => {
17758
+ out.push({
17759
+ type: "text",
17760
+ x: rowLabelW - 6,
17761
+ y: gridY0 + l.index * ch + ch / 2,
17762
+ text: l.text,
17763
+ color: l.color ?? AXIS_LABEL_COLOR,
17764
+ fontSize: AXIS_LABEL_FONT_SIZE,
17765
+ align: "right"
17766
+ });
17767
+ });
17768
+ }
17769
+ if (buckets.length > 0) {
17770
+ const panelYBuckets = panelY.buckets;
17771
+ const bucketCount = Math.max(0, ...buckets.map((b) => b.index)) + 1;
17772
+ const rowH = panelHeight / bucketCount;
17773
+ const indexColW = Math.min(width * 0.12, 40);
17774
+ const maxChainLen = Math.max(1, ...buckets.map((b) => b.entries.length));
17775
+ const entryW = Math.min(BUCKET_ENTRY_MAX_W, Math.max(BUCKET_ENTRY_MIN_W, (width - indexColW - 8) / maxChainLen));
17776
+ const maxVisible = Math.floor((width - indexColW - 4) / entryW);
17777
+ buckets.forEach((b) => {
17778
+ const rowY = panelYBuckets + b.index * rowH;
17779
+ out.push({
17780
+ type: "rect",
17781
+ id: `bucket-index-${b.index}`,
17782
+ x: 2,
17783
+ y: rowY + 2,
17784
+ width: indexColW - 4,
17785
+ height: rowH - 4,
17786
+ color: BUCKET_INDEX_STROKE,
17787
+ fill: BUCKET_INDEX_FILL
17788
+ });
17789
+ out.push({
17790
+ type: "text",
17791
+ x: 2 + (indexColW - 4) / 2,
17792
+ y: rowY + rowH / 2,
17793
+ text: String(b.index),
17794
+ color: BUCKET_INDEX_TEXT,
17795
+ fontSize: 10,
17796
+ align: "center"
17797
+ });
17798
+ const overflow = b.entries.length > maxVisible;
17799
+ const visibleCount = overflow ? Math.max(0, maxVisible - 1) : b.entries.length;
17800
+ for (let j = 0; j < visibleCount; j++) {
17801
+ const entry = b.entries[j];
17802
+ const ex = indexColW + 4 + j * entryW;
17803
+ const state = entry.state ?? "default";
17804
+ const fill = state === "highlight" ? entry.color ?? BUCKET_ENTRY_HIGHLIGHT : state === "probing" ? entry.color ?? BUCKET_ENTRY_PROBING : entry.color ?? BUCKET_ENTRY_DEFAULT;
17805
+ out.push({
17806
+ type: "rect",
17807
+ id: `bucket-${b.index}-${j}`,
17808
+ x: ex,
17809
+ y: rowY + 2,
17810
+ width: entryW - 2,
17811
+ height: rowH - 4,
17812
+ color: fill,
17813
+ fill
17814
+ });
17815
+ if (entryW >= 20 && rowH >= 16) {
17816
+ out.push({
17817
+ type: "text",
17818
+ x: ex + (entryW - 2) / 2,
17819
+ y: rowY + rowH / 2,
17820
+ text: entry.label,
17821
+ color: BUCKET_ENTRY_TEXT,
17822
+ fontSize: 10,
17823
+ align: "center"
17824
+ });
17825
+ }
17826
+ }
17827
+ if (overflow) {
17828
+ const ex = indexColW + 4 + visibleCount * entryW;
17829
+ out.push({
17830
+ type: "rect",
17831
+ id: `bucket-${b.index}-overflow`,
17832
+ x: ex,
17833
+ y: rowY + 2,
17834
+ width: entryW - 2,
17835
+ height: rowH - 4,
17836
+ color: BUCKET_ENTRY_DEFAULT,
17837
+ fill: BUCKET_ENTRY_DEFAULT
17838
+ });
17839
+ out.push({
17840
+ type: "text",
17841
+ x: ex + (entryW - 2) / 2,
17842
+ y: rowY + rowH / 2,
17843
+ text: `+${b.entries.length - visibleCount}`,
17844
+ color: BUCKET_ENTRY_TEXT,
17845
+ fontSize: 10,
17846
+ align: "center"
17847
+ });
17848
+ }
17849
+ });
17850
+ }
17851
+ if (frames.length > 0) {
17852
+ const panelYFrames = panelY.frames;
17853
+ const n = frames.length;
17854
+ const frameH = panelHeight / n;
17855
+ const x = 8;
17856
+ const w = width - 16;
17857
+ frames.forEach((f3, i) => {
17858
+ const y = panelYFrames + panelHeight - (i + 1) * frameH;
17859
+ const state = f3.state ?? "active";
17860
+ const fill = state === "returning" ? f3.color ?? FRAME_RETURNING_COLOR : state === "done" ? f3.color ?? FRAME_DONE_COLOR : f3.color ?? FRAME_ACTIVE_COLOR;
17861
+ out.push({ type: "rect", id: `frame-${i}`, x, y, width: w, height: frameH, color: fill, fill });
17862
+ if (frameH >= FRAME_TWO_LINE_MIN_H) {
17863
+ out.push({
17864
+ type: "text",
17865
+ x: 16,
17866
+ y: y + frameH * 0.35,
17867
+ text: f3.label,
17868
+ color: FRAME_LABEL_COLOR,
17869
+ fontSize: 10,
17870
+ align: "left"
17871
+ });
17872
+ if (f3.detail) {
17873
+ out.push({
17874
+ type: "text",
17875
+ x: 16,
17876
+ y: y + frameH * 0.7,
17877
+ text: f3.detail,
17878
+ color: FRAME_DETAIL_COLOR,
17879
+ fontSize: 10,
17880
+ align: "left"
17881
+ });
17882
+ }
17883
+ } else {
17884
+ out.push({
17885
+ type: "text",
17886
+ x: 16,
17887
+ y: y + frameH / 2,
17888
+ text: f3.label,
17889
+ color: FRAME_LABEL_COLOR,
17890
+ fontSize: 10,
17891
+ align: "left"
17892
+ });
17893
+ }
16933
17894
  });
16934
17895
  }
16935
17896
  out.push(...shapes);
16936
17897
  return out;
16937
- }, [bars, cells, pointers, shapes, width, height]);
17898
+ }, [bars, cells, pointers, ranges, slots, slotOrientation, frames, buckets, auxBars, rowLabels, colLabels, shapes, width, height]);
16938
17899
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
16939
17900
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
16940
17901
  /* @__PURE__ */ jsx(
@@ -17121,6 +18082,12 @@ function LearningScene3D({
17121
18082
  const unitId = event.payload?.unitId;
17122
18083
  if (typeof unitId === "string") onItemClickRef.current?.(unitId);
17123
18084
  });
18085
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && post?.bloom) {
18086
+ const unknownKeys = Object.keys(post.bloom).filter((k) => !KNOWN_BLOOM_KEYS.has(k));
18087
+ if (unknownKeys.length > 0) {
18088
+ sceneLog.debug("post.bloom has unrecognized keys \u2014 only intensity/threshold/smoothing are read", { unknownKeys });
18089
+ }
18090
+ }
17124
18091
  const props3d = {
17125
18092
  drawables,
17126
18093
  isLoading,
@@ -17186,7 +18153,7 @@ function cylinderBetween(from, to, radius, color) {
17186
18153
  material: { color }
17187
18154
  };
17188
18155
  }
17189
- function arrowBetween(from, to, color, shaftRadius = 0.08) {
18156
+ function arrowBetween(from, to, color, shaftRadius = 0.08, id) {
17190
18157
  const len = segmentLength(from, to);
17191
18158
  if (len < 1e-6) return null;
17192
18159
  const tipLen = Math.min(shaftRadius * 8, len * 0.35);
@@ -17214,6 +18181,7 @@ function arrowBetween(from, to, color, shaftRadius = 0.08) {
17214
18181
  };
17215
18182
  return {
17216
18183
  type: "draw-group",
18184
+ ...id !== void 0 ? { id } : {},
17217
18185
  position: { x: from[0], y: from[1], z: from[2] },
17218
18186
  items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
17219
18187
  };
@@ -17240,20 +18208,228 @@ function get3DClickPayload(onShapeClick, idToIndex) {
17240
18208
  if (!onShapeClick) return void 0;
17241
18209
  return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
17242
18210
  }
17243
- var Canvas3DHost2;
18211
+ function polylineTube(points, radius, color, opts) {
18212
+ const maxSegments = opts?.maxSegments ?? 128;
18213
+ let pts = points;
18214
+ if (pts.length - 1 > maxSegments) {
18215
+ const step = (pts.length - 1) / maxSegments;
18216
+ const kept = [pts[0]];
18217
+ for (let s = 1; s < maxSegments; s++) kept.push(pts[Math.round(s * step)]);
18218
+ kept.push(pts[pts.length - 1]);
18219
+ pts = kept;
18220
+ }
18221
+ const out = [];
18222
+ for (let i = 0; i < pts.length - 1; i++) {
18223
+ const seg = cylinderBetween(pts[i], pts[i + 1], radius, color);
18224
+ if (seg) out.push(opts?.opacity !== void 0 ? { ...seg, opacity: opts.opacity } : seg);
18225
+ }
18226
+ return out;
18227
+ }
18228
+ function heightFieldMesh(spec) {
18229
+ const { nx, ny, heights, spacing = 1, x = 0, y = 0 } = spec;
18230
+ const flatShading = spec.flatShading ?? true;
18231
+ const vertices = [];
18232
+ for (let iy = 0; iy < ny; iy++) {
18233
+ for (let ix = 0; ix < nx; ix++) {
18234
+ vertices.push([
18235
+ x + (ix - (nx - 1) / 2) * spacing,
18236
+ y + (iy - (ny - 1) / 2) * spacing,
18237
+ heights[iy * nx + ix] ?? 0
18238
+ ]);
18239
+ }
18240
+ }
18241
+ const bands = [...spec.bands ?? []].sort((a, b) => (a.min ?? -Infinity) - (b.min ?? -Infinity));
18242
+ const facesByBand = /* @__PURE__ */ new Map();
18243
+ for (let iy = 0; iy < ny - 1; iy++) {
18244
+ for (let ix = 0; ix < nx - 1; ix++) {
18245
+ const v00 = iy * nx + ix;
18246
+ const v10 = iy * nx + ix + 1;
18247
+ const v01 = (iy + 1) * nx + ix;
18248
+ const v11 = (iy + 1) * nx + ix + 1;
18249
+ for (const face of [[v00, v10, v01], [v10, v11, v01]]) {
18250
+ const centroid = (vertices[face[0]][2] + vertices[face[1]][2] + vertices[face[2]][2]) / 3;
18251
+ let band = null;
18252
+ for (const b of bands) {
18253
+ if ((b.min ?? -Infinity) <= centroid) band = b;
18254
+ }
18255
+ const key = bands.length > 0 ? band : null;
18256
+ const list = facesByBand.get(key) ?? [];
18257
+ list.push(face);
18258
+ facesByBand.set(key, list);
18259
+ }
18260
+ }
18261
+ }
18262
+ const out = [];
18263
+ for (const [band, faces] of facesByBand) {
18264
+ if (faces.length === 0) continue;
18265
+ out.push({
18266
+ type: "draw-mesh",
18267
+ shape: "polyhedron",
18268
+ position: { x: 0, y: 0, z: 0 },
18269
+ vertices,
18270
+ faces,
18271
+ pivot: "center",
18272
+ material: { color: band?.color ?? spec.color ?? "#64748b", flatShading, side: "double" },
18273
+ ...spec.opacity !== void 0 ? { opacity: spec.opacity } : {}
18274
+ });
18275
+ }
18276
+ return out;
18277
+ }
18278
+ function arrowField(vectors, opts) {
18279
+ const scale = opts?.scale ?? 1;
18280
+ const out = [];
18281
+ for (const v of vectors) {
18282
+ const to = [
18283
+ v.from[0] + v.delta[0] * scale,
18284
+ v.from[1] + v.delta[1] * scale,
18285
+ v.from[2] + v.delta[2] * scale
18286
+ ];
18287
+ const arrow = arrowBetween(v.from, to, v.color ?? "#dc2626", v.width, v.id);
18288
+ if (arrow) out.push(arrow);
18289
+ if (v.label) out.push(billboardLabel(v.label, to[0], to[1], to[2], { color: opts?.labelColor }));
18290
+ }
18291
+ return out;
18292
+ }
18293
+ function helixDrawables(spec, opts) {
18294
+ const count = spec.count ?? spec.rungs?.length ?? 0;
18295
+ const rungs = Array.from({ length: count }, (_, i) => spec.rungs?.[i] ?? {});
18296
+ const radius = spec.radius ?? 1;
18297
+ const rise = spec.rise ?? 0.34;
18298
+ const twistRad = (spec.twistDeg ?? 36) * (Math.PI / 180);
18299
+ const strandAColor = spec.strandAColor ?? "#38bdf8";
18300
+ const strandBColor = spec.strandBColor ?? "#fb923c";
18301
+ const backboneRadius = spec.backboneRadius ?? 0.16;
18302
+ const rungRadius = spec.rungRadius ?? 0.12;
18303
+ const cx = spec.x ?? 0;
18304
+ const cy = spec.y ?? 0;
18305
+ const cz = spec.z ?? 0;
18306
+ const unwoundCount = spec.unwoundCount ?? 0;
18307
+ const unwindSpread = spec.unwindSpread ?? 1.8;
18308
+ const strandA = [];
18309
+ const strandB = [];
18310
+ for (let i = 0; i < count; i++) {
18311
+ const yi = cy + (i - (count - 1) / 2) * rise;
18312
+ const theta = i * twistRad;
18313
+ const s = i < unwoundCount ? unwindSpread : 1;
18314
+ strandA.push([cx + s * radius * Math.cos(theta), yi, cz + s * radius * Math.sin(theta)]);
18315
+ strandB.push([cx + s * radius * Math.cos(theta + Math.PI), yi, cz + s * radius * Math.sin(theta + Math.PI)]);
18316
+ }
18317
+ const out = [];
18318
+ for (let i = 0; i < count; i++) {
18319
+ out.push(meshSphere(`hx-a-${i}`, strandA[i][0], strandA[i][1], strandA[i][2], backboneRadius, strandAColor));
18320
+ out.push(meshSphere(`hx-b-${i}`, strandB[i][0], strandB[i][1], strandB[i][2], backboneRadius, strandBColor));
18321
+ if (i > 0) {
18322
+ const segA = cylinderBetween(strandA[i - 1], strandA[i], backboneRadius, strandAColor);
18323
+ if (segA) out.push(segA);
18324
+ const segB = cylinderBetween(strandB[i - 1], strandB[i], backboneRadius, strandBColor);
18325
+ if (segB) out.push(segB);
18326
+ }
18327
+ const rung = rungs[i];
18328
+ const rungColor = rung.color ?? "#94a3b8";
18329
+ const rod = cylinderBetween(strandA[i], strandB[i], rungRadius, rungColor);
18330
+ if (rod) out.push(rod);
18331
+ const mid = [
18332
+ (strandA[i][0] + strandB[i][0]) / 2,
18333
+ (strandA[i][1] + strandB[i][1]) / 2,
18334
+ (strandA[i][2] + strandB[i][2]) / 2
18335
+ ];
18336
+ const markerRadius = rung.radius ?? rungRadius;
18337
+ out.push(meshSphere(rung.id, mid[0], mid[1], mid[2], markerRadius, rungColor));
18338
+ if (rung.label) out.push(billboardLabel(rung.label, mid[0], mid[1], mid[2] + markerRadius, { color: opts?.labelColor }));
18339
+ }
18340
+ return out;
18341
+ }
18342
+ function latticeDrawables(spec, opts) {
18343
+ const nx = spec.nx ?? 2;
18344
+ const ny = spec.ny ?? 2;
18345
+ const nz = spec.nz ?? 2;
18346
+ const latticeConstant = spec.latticeConstant ?? 2;
18347
+ const bondRadius = spec.bondRadius ?? 0.06;
18348
+ const highlightCell = spec.highlightCell ?? false;
18349
+ const dimColor = spec.dimColor ?? "#475569";
18350
+ const showLabels = spec.showLabels ?? false;
18351
+ const selectedColor = spec.selectedColor ?? "#f59e0b";
18352
+ const posByKey = /* @__PURE__ */ new Map();
18353
+ const inCellByKey = /* @__PURE__ */ new Map();
18354
+ const out = [];
18355
+ for (const site of spec.basis) {
18356
+ const snx = site.xEdge ? nx + 1 : nx;
18357
+ const sny = site.yEdge ? ny + 1 : ny;
18358
+ const snz = site.zEdge ? nz + 1 : nz;
18359
+ for (let i = 0; i < snx; i++) {
18360
+ for (let j = 0; j < sny; j++) {
18361
+ for (let k = 0; k < snz; k++) {
18362
+ const key = `${site.key}-${i}-${j}-${k}`;
18363
+ const inCell = i + site.dx <= 1 && j + site.dy <= 1 && k + site.dz <= 1;
18364
+ const pos = [
18365
+ (i + site.dx) * latticeConstant - nx * latticeConstant / 2,
18366
+ (j + site.dy) * latticeConstant - ny * latticeConstant / 2,
18367
+ (k + site.dz) * latticeConstant - nz * latticeConstant / 2
18368
+ ];
18369
+ posByKey.set(key, pos);
18370
+ inCellByKey.set(key, inCell);
18371
+ const isSelected = spec.selectedId === `lat-${key}`;
18372
+ const color = isSelected ? selectedColor : highlightCell && !inCell ? dimColor : site.color ?? "#2563eb";
18373
+ const radius = (site.radius ?? 0.3) * (isSelected ? 1.4 : 1);
18374
+ out.push(meshSphere(`lat-${key}`, pos[0], pos[1], pos[2], radius, color));
18375
+ if (showLabels && site.element) {
18376
+ out.push(billboardLabel(site.element, pos[0], pos[1], pos[2] + radius, { color: opts?.labelColor }));
18377
+ }
18378
+ }
18379
+ }
18380
+ }
18381
+ }
18382
+ const basisByKey = new Map(spec.basis.map((s) => [s.key, s]));
18383
+ for (const bond of spec.bonds ?? []) {
18384
+ const fromSite = basisByKey.get(bond.from);
18385
+ const toSite = basisByKey.get(bond.to);
18386
+ if (!fromSite || !toSite) continue;
18387
+ const fnx = fromSite.xEdge ? nx + 1 : nx;
18388
+ const fny = fromSite.yEdge ? ny + 1 : ny;
18389
+ const fnz = fromSite.zEdge ? nz + 1 : nz;
18390
+ const tnx = toSite.xEdge ? nx + 1 : nx;
18391
+ const tny = toSite.yEdge ? ny + 1 : ny;
18392
+ const tnz = toSite.zEdge ? nz + 1 : nz;
18393
+ const bdx = bond.dx ?? 0;
18394
+ const bdy = bond.dy ?? 0;
18395
+ const bdz = bond.dz ?? 0;
18396
+ for (let i = 0; i < fnx; i++) {
18397
+ for (let j = 0; j < fny; j++) {
18398
+ for (let k = 0; k < fnz; k++) {
18399
+ const ti = i + bdx;
18400
+ const tj = j + bdy;
18401
+ const tk = k + bdz;
18402
+ if (ti < 0 || ti >= tnx || tj < 0 || tj >= tny || tk < 0 || tk >= tnz) continue;
18403
+ const fromKey = `${fromSite.key}-${i}-${j}-${k}`;
18404
+ const toKey = `${toSite.key}-${ti}-${tj}-${tk}`;
18405
+ const fromPos = posByKey.get(fromKey);
18406
+ const toPos = posByKey.get(toKey);
18407
+ if (!fromPos || !toPos) continue;
18408
+ const dimmed = highlightCell && !(inCellByKey.get(fromKey) && inCellByKey.get(toKey));
18409
+ const seg = cylinderBetween(fromPos, toPos, bondRadius, dimmed ? dimColor : bond.color ?? "#6b7280");
18410
+ if (seg) out.push(seg);
18411
+ }
18412
+ }
18413
+ }
18414
+ }
18415
+ return out;
18416
+ }
18417
+ var sceneLog, KNOWN_BLOOM_KEYS, Canvas3DHost2;
17244
18418
  var init_learningScene3D = __esm({
17245
18419
  "components/learning/molecules/learningScene3D.tsx"() {
17246
18420
  "use client";
17247
18421
  init_atoms();
17248
18422
  init_Stack();
17249
18423
  init_useEventBus();
18424
+ sceneLog = createLogger("almadar:ui:learning-scene-3d");
18425
+ KNOWN_BLOOM_KEYS = /* @__PURE__ */ new Set(["intensity", "threshold", "smoothing"]);
17250
18426
  Canvas3DHost2 = lazy(
17251
18427
  () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
17252
18428
  );
17253
18429
  LearningScene3D.displayName = "LearningScene3D";
17254
18430
  }
17255
18431
  });
17256
- var biologyLog, BiologyCanvas;
18432
+ var biologyLog, BIO_BAND_COLORS, BIO_STAGE_FILL, BIO_STAGE_TEXT, BiologyCanvas;
17257
18433
  var init_BiologyCanvas = __esm({
17258
18434
  "components/learning/molecules/BiologyCanvas.tsx"() {
17259
18435
  "use client";
@@ -17262,6 +18438,17 @@ var init_BiologyCanvas = __esm({
17262
18438
  init_LearningCanvas();
17263
18439
  init_learningScene3D();
17264
18440
  biologyLog = createLogger("almadar:ui:biology-canvas");
18441
+ BIO_BAND_COLORS = ["#dcfce7", "#fef9c3", "#fee2e2", "#e0e7ff"];
18442
+ BIO_STAGE_FILL = {
18443
+ pending: "#e2e8f0",
18444
+ active: "#3b82f6",
18445
+ done: "#94a3b8"
18446
+ };
18447
+ BIO_STAGE_TEXT = {
18448
+ pending: "#64748b",
18449
+ active: "#ffffff",
18450
+ done: "#ffffff"
18451
+ };
17265
18452
  BiologyCanvas = ({
17266
18453
  className,
17267
18454
  width = 600,
@@ -17274,7 +18461,15 @@ var init_BiologyCanvas = __esm({
17274
18461
  post,
17275
18462
  nodes = [],
17276
18463
  edges = [],
18464
+ compartments = [],
18465
+ bands = [],
18466
+ stages = [],
18467
+ stageStyle = "timeline",
18468
+ helix,
18469
+ helix3d,
17277
18470
  shapes = [],
18471
+ readouts,
18472
+ traces,
17278
18473
  showGrid,
17279
18474
  shadows,
17280
18475
  interactive,
@@ -17289,19 +18484,148 @@ var init_BiologyCanvas = __esm({
17289
18484
  for (const n of nodes) {
17290
18485
  if (n.id) nodeById.set(n.id, n);
17291
18486
  }
18487
+ const bandCount = bands.length;
18488
+ for (let i = 0; i < bandCount; i++) {
18489
+ const band = bands[i];
18490
+ const bandColor = band.color ?? BIO_BAND_COLORS[i % BIO_BAND_COLORS.length];
18491
+ const bandY = i * height / bandCount;
18492
+ const bandH = height / bandCount;
18493
+ out.push({
18494
+ type: "rect",
18495
+ x: 0,
18496
+ y: bandY,
18497
+ width,
18498
+ height: bandH,
18499
+ color: bandColor,
18500
+ fill: bandColor,
18501
+ opacity: 0.45
18502
+ });
18503
+ if (band.label) {
18504
+ out.push({
18505
+ type: "text",
18506
+ x: 8,
18507
+ y: bandY + 14,
18508
+ text: band.label,
18509
+ color: "#6b7280",
18510
+ fontSize: 10
18511
+ });
18512
+ }
18513
+ }
18514
+ for (const c of compartments) {
18515
+ const color = c.color ?? "#16a34a";
18516
+ out.push({
18517
+ type: "ellipse",
18518
+ x: c.x,
18519
+ y: c.y,
18520
+ width: c.width,
18521
+ height: c.height,
18522
+ color,
18523
+ fill: c.fill ?? `${color}1A`,
18524
+ lineWidth: c.lineWidth ?? 2,
18525
+ ...c.dash ? { dash: c.dash } : {}
18526
+ });
18527
+ if (c.label) {
18528
+ out.push({
18529
+ type: "text",
18530
+ x: c.x,
18531
+ y: c.y - c.height / 2 + 14,
18532
+ text: c.label,
18533
+ color: "#111827",
18534
+ fontSize: 11,
18535
+ align: "center"
18536
+ });
18537
+ }
18538
+ }
18539
+ if (helix) {
18540
+ const hx = helix.x ?? 24;
18541
+ const hy = helix.y ?? height * 0.25;
18542
+ const hw = helix.width ?? width - 48;
18543
+ const hh = helix.height ?? height * 0.5;
18544
+ const rungs = helix.rungs;
18545
+ const n = rungs.length;
18546
+ const cy = hy + hh / 2;
18547
+ const colorA = helix.colorA ?? "#2563eb";
18548
+ const colorB = helix.colorB ?? "#dc2626";
18549
+ const rungColor = helix.rungColor ?? "#94a3b8";
18550
+ const fork = helix.fork ?? 0;
18551
+ const maxSep = Math.min(hh - 8, 96);
18552
+ const strandA = [];
18553
+ const strandB = [];
18554
+ const rungGeoms = [];
18555
+ for (let i = 0; i < n; i++) {
18556
+ const rx = hx + (i + 0.5) * hw / n;
18557
+ const t = (i + 0.5) / n;
18558
+ const paired = t >= fork;
18559
+ const sep = paired ? 28 : 28 + (maxSep - 28) * ((fork - t) / fork);
18560
+ strandA.push({ x: rx, y: cy - sep / 2 });
18561
+ strandB.push({ x: rx, y: cy + sep / 2 });
18562
+ rungGeoms.push({ rx, sep, rung: rungs[i], paired });
18563
+ }
18564
+ for (let i = 1; i < n; i++) {
18565
+ out.push({ type: "line", x1: strandA[i - 1].x, y1: strandA[i - 1].y, x2: strandA[i].x, y2: strandA[i].y, color: colorA, lineWidth: 3 });
18566
+ }
18567
+ for (let i = 1; i < n; i++) {
18568
+ out.push({ type: "line", x1: strandB[i - 1].x, y1: strandB[i - 1].y, x2: strandB[i].x, y2: strandB[i].y, color: colorB, lineWidth: 3 });
18569
+ }
18570
+ for (const g of rungGeoms) {
18571
+ const rColor = g.rung.color ?? (g.rung.state === "new" ? "#16a34a" : rungColor);
18572
+ const topY = cy - g.sep / 2;
18573
+ const bottomY = cy + g.sep / 2;
18574
+ if (g.paired) {
18575
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: bottomY, color: rColor });
18576
+ if (g.rung.a) {
18577
+ out.push({ type: "text", x: g.rx, y: cy - g.sep / 4, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
18578
+ }
18579
+ if (g.rung.b) {
18580
+ out.push({ type: "text", x: g.rx, y: cy + g.sep / 4, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
18581
+ }
18582
+ } else {
18583
+ const stubTopY = topY + 8;
18584
+ const stubBottomY = bottomY - 8;
18585
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: stubTopY, color: rColor });
18586
+ out.push({ type: "line", x1: g.rx, y1: bottomY, x2: g.rx, y2: stubBottomY, color: rColor });
18587
+ if (g.rung.a) {
18588
+ out.push({ type: "text", x: g.rx, y: stubTopY + 6, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
18589
+ }
18590
+ if (g.rung.b) {
18591
+ out.push({ type: "text", x: g.rx, y: stubBottomY - 6, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
18592
+ }
18593
+ }
18594
+ }
18595
+ }
17292
18596
  for (const e of edges) {
17293
18597
  const a = nodeById.get(e.from);
17294
18598
  const b = nodeById.get(e.to);
17295
18599
  if (!a || !b) continue;
17296
- out.push({
17297
- type: "line",
17298
- x1: a.x,
17299
- y1: a.y,
17300
- x2: b.x,
17301
- y2: b.y,
17302
- color: e.color ?? "#9ca3af",
17303
- lineWidth: 2
17304
- });
18600
+ const color = e.color ?? "#9ca3af";
18601
+ if (e.directed) {
18602
+ const rA = a.radius ?? 16;
18603
+ const rB = b.radius ?? 16;
18604
+ const dx = b.x - a.x;
18605
+ const dy = b.y - a.y;
18606
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
18607
+ const ux = dx / dist;
18608
+ const uy = dy / dist;
18609
+ out.push({
18610
+ type: "arrow",
18611
+ x1: a.x + ux * rA,
18612
+ y1: a.y + uy * rA,
18613
+ x2: b.x - ux * rB,
18614
+ y2: b.y - uy * rB,
18615
+ color,
18616
+ lineWidth: 2
18617
+ });
18618
+ } else {
18619
+ out.push({
18620
+ type: "line",
18621
+ x1: a.x,
18622
+ y1: a.y,
18623
+ x2: b.x,
18624
+ y2: b.y,
18625
+ color,
18626
+ lineWidth: 2
18627
+ });
18628
+ }
17305
18629
  if (e.label) {
17306
18630
  out.push({
17307
18631
  type: "text",
@@ -17314,6 +18638,8 @@ var init_BiologyCanvas = __esm({
17314
18638
  }
17315
18639
  }
17316
18640
  for (const n of nodes) {
18641
+ const state = n.state ?? "default";
18642
+ const muted = state === "muted";
17317
18643
  out.push({
17318
18644
  type: "circle",
17319
18645
  x: n.x,
@@ -17321,28 +18647,127 @@ var init_BiologyCanvas = __esm({
17321
18647
  radius: n.radius ?? 16,
17322
18648
  color: n.color ?? "#16a34a",
17323
18649
  fill: `${n.color ?? "#16a34a"}33`,
17324
- id: n.id
18650
+ id: n.id,
18651
+ ...muted ? { opacity: 0.35 } : {}
17325
18652
  });
18653
+ if (state === "highlight") {
18654
+ out.push({
18655
+ type: "circle",
18656
+ x: n.x,
18657
+ y: n.y,
18658
+ radius: (n.radius ?? 16) + 4,
18659
+ color: "#f59e0b",
18660
+ lineWidth: 2
18661
+ });
18662
+ }
17326
18663
  if (n.label) {
17327
18664
  out.push({
17328
18665
  type: "text",
17329
18666
  x: n.x,
17330
18667
  y: n.y + (n.radius ?? 16) + 14,
17331
18668
  text: n.label,
18669
+ ...muted ? { opacity: 0.35 } : {},
17332
18670
  color: "#111827",
17333
18671
  fontSize: 12,
17334
18672
  align: "center"
17335
18673
  });
17336
18674
  }
17337
18675
  }
18676
+ const stageCount = stages.length;
18677
+ if (stageCount > 0) {
18678
+ if (stageStyle === "ring") {
18679
+ const cx = width / 2;
18680
+ const cy = height / 2;
18681
+ const R = Math.min(width, height) / 2 - 48;
18682
+ const ringPoints = [];
18683
+ for (let i = 0; i < stageCount; i++) {
18684
+ const angleRad = (-90 + 360 * i / stageCount) * Math.PI / 180;
18685
+ ringPoints.push({ x: cx + R * Math.cos(angleRad), y: cy + R * Math.sin(angleRad) });
18686
+ }
18687
+ if (stageCount >= 2) {
18688
+ for (let i = 0; i < stageCount - 1; i++) {
18689
+ const p1 = ringPoints[i];
18690
+ const p2 = ringPoints[i + 1];
18691
+ const dx = p2.x - p1.x;
18692
+ const dy = p2.y - p1.y;
18693
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
18694
+ const ux = dx / dist;
18695
+ const uy = dy / dist;
18696
+ out.push({
18697
+ type: "arrow",
18698
+ x1: p1.x + ux * 46,
18699
+ y1: p1.y + uy * 46,
18700
+ x2: p2.x - ux * 46,
18701
+ y2: p2.y - uy * 46,
18702
+ color: "#94a3b8"
18703
+ });
18704
+ }
18705
+ }
18706
+ for (let i = 0; i < stageCount; i++) {
18707
+ const stage = stages[i];
18708
+ const state = stage.state ?? "pending";
18709
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
18710
+ const w = Math.max(26, Math.min(84, stage.label.length * 6 + 10));
18711
+ const h = 18;
18712
+ const p = ringPoints[i];
18713
+ out.push({ type: "rect", x: p.x - w / 2, y: p.y - h / 2, width: w, height: h, color: fill, fill });
18714
+ out.push({ type: "text", x: p.x, y: p.y, text: stage.label, color: BIO_STAGE_TEXT[state], fontSize: 10, align: "center" });
18715
+ }
18716
+ } else {
18717
+ const stripY = height - 32;
18718
+ const slotW = (width - 16) / stageCount;
18719
+ const chipGeoms = [];
18720
+ for (let i = 0; i < stageCount; i++) {
18721
+ chipGeoms.push({ x: 8 + i * slotW + 5, w: slotW - 10 });
18722
+ }
18723
+ for (let i = 0; i < stageCount - 1; i++) {
18724
+ const midY = stripY + 13;
18725
+ out.push({
18726
+ type: "arrow",
18727
+ x1: chipGeoms[i].x + chipGeoms[i].w,
18728
+ y1: midY,
18729
+ x2: chipGeoms[i + 1].x,
18730
+ y2: midY,
18731
+ color: "#94a3b8"
18732
+ });
18733
+ }
18734
+ for (let i = 0; i < stageCount; i++) {
18735
+ const stage = stages[i];
18736
+ const state = stage.state ?? "pending";
18737
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
18738
+ const g = chipGeoms[i];
18739
+ out.push({ type: "rect", x: g.x, y: stripY, width: g.w, height: 26, color: fill, fill });
18740
+ out.push({
18741
+ type: "text",
18742
+ x: g.x + g.w / 2,
18743
+ y: stripY + 13,
18744
+ text: stage.label,
18745
+ color: BIO_STAGE_TEXT[state],
18746
+ fontSize: 10,
18747
+ align: "center"
18748
+ });
18749
+ }
18750
+ }
18751
+ }
17338
18752
  out.push(...shapes);
17339
18753
  return out;
17340
- }, [nodes, edges, shapes]);
18754
+ }, [nodes, edges, compartments, bands, stages, stageStyle, helix, shapes, width, height]);
17341
18755
  const drawables3D = useMemo(() => {
17342
18756
  if (mode !== "3d") return [];
17343
18757
  if (shapes.length > 0) {
17344
18758
  biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
17345
18759
  }
18760
+ if (compartments.length > 0 || bands.length > 0 || stages.length > 0 || helix) {
18761
+ biologyLog.debug("2D-only families ignored in 3D mode (pixel-authored 2D vocabulary)", {
18762
+ compartments: compartments.length,
18763
+ bands: bands.length,
18764
+ stages: stages.length,
18765
+ helix: helix != null
18766
+ });
18767
+ }
18768
+ if (animate) {
18769
+ biologyLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
18770
+ }
17346
18771
  const out = [];
17347
18772
  const labelColor = labelColorForBackground(backgroundColor);
17348
18773
  const nodeById = /* @__PURE__ */ new Map();
@@ -17376,15 +18801,21 @@ var init_BiologyCanvas = __esm({
17376
18801
  out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
17377
18802
  }
17378
18803
  }
18804
+ if (helix3d) {
18805
+ out.push(...helixDrawables(helix3d, { labelColor }));
18806
+ }
17379
18807
  return out;
17380
- }, [mode, nodes, edges, shapes, backgroundColor]);
18808
+ }, [mode, nodes, edges, shapes, compartments, bands, stages, helix, helix3d, animate, backgroundColor]);
17381
18809
  const nodeIndexById = useMemo(() => {
17382
18810
  const m = /* @__PURE__ */ new Map();
18811
+ (helix3d?.rungs ?? []).forEach((rung, i) => {
18812
+ if (rung.id) m.set(rung.id, i);
18813
+ });
17383
18814
  nodes.forEach((n, i) => {
17384
18815
  if (n.id) m.set(n.id, i);
17385
18816
  });
17386
18817
  return m;
17387
- }, [nodes]);
18818
+ }, [nodes, helix3d]);
17388
18819
  if (mode === "3d") {
17389
18820
  return /* @__PURE__ */ jsx(
17390
18821
  LearningScene3D,
@@ -17416,6 +18847,8 @@ var init_BiologyCanvas = __esm({
17416
18847
  height,
17417
18848
  backgroundColor,
17418
18849
  shapes: derivedShapes,
18850
+ readouts,
18851
+ traces,
17419
18852
  interactive: interactive ?? false,
17420
18853
  animate,
17421
18854
  onShapeClick,
@@ -24265,7 +25698,7 @@ function bondPerpendicular(a, b) {
24265
25698
  if (len < 1e-6) return [1, 0, 0];
24266
25699
  return [px / len, py / len, 0];
24267
25700
  }
24268
- var chemistryLog, ChemistryCanvas;
25701
+ var chemistryLog, CHEM_BOND_STATE_COLOR, LONE_PAIR_ANGLES, ChemistryCanvas;
24269
25702
  var init_ChemistryCanvas = __esm({
24270
25703
  "components/learning/molecules/ChemistryCanvas.tsx"() {
24271
25704
  "use client";
@@ -24274,6 +25707,13 @@ var init_ChemistryCanvas = __esm({
24274
25707
  init_LearningCanvas();
24275
25708
  init_learningScene3D();
24276
25709
  chemistryLog = createLogger("almadar:ui:chemistry-canvas");
25710
+ CHEM_BOND_STATE_COLOR = {
25711
+ default: "#6b7280",
25712
+ forming: "#16a34a",
25713
+ breaking: "#dc2626",
25714
+ highlight: "#f59e0b"
25715
+ };
25716
+ LONE_PAIR_ANGLES = [-90, 0, 90, 180];
24277
25717
  ChemistryCanvas = ({
24278
25718
  className,
24279
25719
  width = 600,
@@ -24287,7 +25727,14 @@ var init_ChemistryCanvas = __esm({
24287
25727
  atoms = [],
24288
25728
  bonds = [],
24289
25729
  arrows = [],
25730
+ bondStyle = "thick",
25731
+ containers = [],
25732
+ equation,
25733
+ equationColor,
25734
+ lattice3d,
24290
25735
  shapes = [],
25736
+ readouts,
25737
+ traces,
24291
25738
  showGrid,
24292
25739
  shadows,
24293
25740
  interactive,
@@ -24302,21 +25749,118 @@ var init_ChemistryCanvas = __esm({
24302
25749
  for (const a of atoms) {
24303
25750
  if (a.id) atomById.set(a.id, a);
24304
25751
  }
25752
+ for (const c of containers) {
25753
+ const color = c.color ?? "#64748b";
25754
+ if (c.level != null) {
25755
+ const lv = c.level;
25756
+ out.push({
25757
+ type: "rect",
25758
+ x: c.x + 1,
25759
+ y: c.y + c.height * (1 - lv),
25760
+ width: c.width - 2,
25761
+ height: c.height * lv - 1,
25762
+ color: c.levelColor ?? "#60a5fa",
25763
+ fill: c.levelColor ?? "#60a5fa",
25764
+ opacity: 0.5
25765
+ });
25766
+ }
25767
+ out.push({
25768
+ type: "rect",
25769
+ x: c.x,
25770
+ y: c.y,
25771
+ width: c.width,
25772
+ height: c.height,
25773
+ color,
25774
+ fill: c.fill,
25775
+ lineWidth: c.lineWidth ?? 2
25776
+ });
25777
+ const divider = c.divider ?? "none";
25778
+ if (divider !== "none") {
25779
+ out.push({
25780
+ type: "line",
25781
+ x1: c.x + c.width / 2,
25782
+ y1: c.y,
25783
+ x2: c.x + c.width / 2,
25784
+ y2: c.y + c.height,
25785
+ color: c.dividerColor ?? color,
25786
+ ...divider === "dashed" || divider === "dotted" ? { dash: divider } : {}
25787
+ });
25788
+ }
25789
+ if (c.leftLabel) {
25790
+ out.push({
25791
+ type: "text",
25792
+ x: c.x + c.width * 0.25,
25793
+ y: c.y + 12,
25794
+ text: c.leftLabel,
25795
+ color: "#374151",
25796
+ fontSize: 11,
25797
+ align: "center"
25798
+ });
25799
+ }
25800
+ if (c.rightLabel) {
25801
+ out.push({
25802
+ type: "text",
25803
+ x: c.x + c.width * 0.75,
25804
+ y: c.y + 12,
25805
+ text: c.rightLabel,
25806
+ color: "#374151",
25807
+ fontSize: 11,
25808
+ align: "center"
25809
+ });
25810
+ }
25811
+ if (c.label) {
25812
+ out.push({
25813
+ type: "text",
25814
+ x: c.x + c.width / 2,
25815
+ y: c.y + c.height + 12,
25816
+ text: c.label,
25817
+ color: "#111827",
25818
+ fontSize: 12,
25819
+ align: "center"
25820
+ });
25821
+ }
25822
+ }
24305
25823
  for (const b of bonds) {
24306
25824
  const a = atomById.get(b.from);
24307
25825
  const c = atomById.get(b.to);
24308
25826
  if (!a || !c) continue;
24309
- const color = b.color ?? "#6b7280";
24310
- const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
24311
- out.push({
24312
- type: "line",
24313
- x1: a.x,
24314
- y1: a.y,
24315
- x2: c.x,
24316
- y2: c.y,
24317
- color,
24318
- lineWidth: strokeWidth
24319
- });
25827
+ const state = b.state ?? "default";
25828
+ const color = b.color ?? CHEM_BOND_STATE_COLOR[state];
25829
+ const dash = state === "forming" || state === "breaking" ? "dashed" : void 0;
25830
+ if (bondStyle === "parallel") {
25831
+ const dx = c.x - a.x;
25832
+ const dy = c.y - a.y;
25833
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
25834
+ const ux = dx / dist;
25835
+ const uy = dy / dist;
25836
+ const px = -uy;
25837
+ const py = ux;
25838
+ const offsets = b.type === "double" ? [-3, 3] : b.type === "triple" ? [-4, 0, 4] : [0];
25839
+ for (const off of offsets) {
25840
+ out.push({
25841
+ type: "line",
25842
+ x1: a.x + px * off,
25843
+ y1: a.y + py * off,
25844
+ x2: c.x + px * off,
25845
+ y2: c.y + py * off,
25846
+ color,
25847
+ lineWidth: 2,
25848
+ ...dash ? { dash } : {}
25849
+ });
25850
+ }
25851
+ } else {
25852
+ const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
25853
+ out.push({
25854
+ type: "line",
25855
+ x1: a.x,
25856
+ y1: a.y,
25857
+ x2: c.x,
25858
+ y2: c.y,
25859
+ color,
25860
+ lineWidth: strokeWidth,
25861
+ ...dash ? { dash } : {}
25862
+ });
25863
+ }
24320
25864
  }
24321
25865
  for (const a of arrows) {
24322
25866
  const angle = (a.angle ?? 0) * (Math.PI / 180);
@@ -24365,15 +25909,62 @@ var init_ChemistryCanvas = __esm({
24365
25909
  align: "center"
24366
25910
  });
24367
25911
  }
25912
+ const r2 = a.radius ?? 14;
25913
+ if (a.charge) {
25914
+ out.push({
25915
+ type: "text",
25916
+ x: a.x + r2 * 0.85,
25917
+ y: a.y - r2 * 0.85,
25918
+ text: a.charge,
25919
+ color: "#111827",
25920
+ fontSize: 9,
25921
+ align: "left"
25922
+ });
25923
+ }
25924
+ const lonePairs = Math.max(0, Math.min(4, a.lonePairs ?? 0));
25925
+ for (let k = 0; k < lonePairs; k++) {
25926
+ const angleRad = LONE_PAIR_ANGLES[k] * Math.PI / 180;
25927
+ const cx = a.x + (r2 + 6) * Math.cos(angleRad);
25928
+ const cy = a.y + (r2 + 6) * Math.sin(angleRad);
25929
+ const perpX = -Math.sin(angleRad);
25930
+ const perpY = Math.cos(angleRad);
25931
+ for (const sign of [1, -1]) {
25932
+ out.push({
25933
+ type: "circle",
25934
+ x: cx + perpX * 2.5 * sign,
25935
+ y: cy + perpY * 2.5 * sign,
25936
+ radius: 1.5,
25937
+ color: "#374151",
25938
+ fill: "#374151"
25939
+ });
25940
+ }
25941
+ }
25942
+ }
25943
+ if (equation) {
25944
+ out.push({
25945
+ type: "text",
25946
+ x: width / 2,
25947
+ y: 14,
25948
+ text: equation,
25949
+ color: equationColor ?? "#111827",
25950
+ fontSize: 13,
25951
+ align: "center"
25952
+ });
24368
25953
  }
24369
25954
  out.push(...shapes);
24370
25955
  return out;
24371
- }, [atoms, bonds, arrows, shapes]);
25956
+ }, [atoms, bonds, arrows, bondStyle, containers, equation, equationColor, shapes, width]);
24372
25957
  const drawables3D = useMemo(() => {
24373
25958
  if (mode !== "3d") return [];
24374
25959
  if (shapes.length > 0) {
24375
25960
  chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
24376
25961
  }
25962
+ if (containers.length > 0) {
25963
+ chemistryLog.debug("containers ignored in 3D mode (pixel-authored 2D vocabulary)", { count: containers.length });
25964
+ }
25965
+ if (animate) {
25966
+ chemistryLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
25967
+ }
24377
25968
  const out = [];
24378
25969
  const labelColor = labelColorForBackground(backgroundColor);
24379
25970
  const atomById = /* @__PURE__ */ new Map();
@@ -24420,8 +26011,11 @@ var init_ChemistryCanvas = __esm({
24420
26011
  out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
24421
26012
  }
24422
26013
  }
26014
+ if (lattice3d) {
26015
+ out.push(...latticeDrawables(lattice3d, { labelColor }));
26016
+ }
24423
26017
  return out;
24424
- }, [mode, atoms, bonds, arrows, shapes, backgroundColor]);
26018
+ }, [mode, atoms, bonds, arrows, shapes, containers, lattice3d, animate, backgroundColor]);
24425
26019
  const atomIndexById = useMemo(() => {
24426
26020
  const m = /* @__PURE__ */ new Map();
24427
26021
  atoms.forEach((a, i) => {
@@ -24460,6 +26054,8 @@ var init_ChemistryCanvas = __esm({
24460
26054
  height,
24461
26055
  backgroundColor,
24462
26056
  shapes: derivedShapes,
26057
+ readouts,
26058
+ traces,
24463
26059
  interactive: interactive ?? false,
24464
26060
  animate,
24465
26061
  onShapeClick,
@@ -30649,6 +32245,10 @@ var init_ProgressDots = __esm({
30649
32245
  ProgressDots.displayName = "ProgressDots";
30650
32246
  }
30651
32247
  });
32248
+ function formatTick(v) {
32249
+ if (Number.isInteger(v)) return String(v);
32250
+ return v.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
32251
+ }
30652
32252
  var MathCanvas;
30653
32253
  var init_MathCanvas = __esm({
30654
32254
  "components/learning/molecules/MathCanvas.tsx"() {
@@ -30668,10 +32268,19 @@ var init_MathCanvas = __esm({
30668
32268
  showAxes = true,
30669
32269
  showGrid = true,
30670
32270
  gridStep = 1,
32271
+ showTickLabels = false,
32272
+ showCurveLabels = false,
30671
32273
  curves = [],
30672
32274
  points = [],
30673
32275
  vectors = [],
32276
+ regions = [],
32277
+ bars = [],
32278
+ guides = [],
32279
+ angles = [],
32280
+ hops = [],
30674
32281
  shapes = [],
32282
+ readouts,
32283
+ traces,
30675
32284
  interactive = false,
30676
32285
  animate = false,
30677
32286
  onShapeClick,
@@ -30685,6 +32294,8 @@ var init_MathCanvas = __esm({
30685
32294
  const plotH = height - margin * 2;
30686
32295
  const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
30687
32296
  const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
32297
+ const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
32298
+ const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
30688
32299
  if (showGrid) {
30689
32300
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep) {
30690
32301
  const px = mapX(x);
@@ -30695,14 +32306,99 @@ var init_MathCanvas = __esm({
30695
32306
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color: "#e5e7eb", lineWidth: 1 });
30696
32307
  }
30697
32308
  }
32309
+ if (showTickLabels) {
32310
+ const labelEveryX = Math.max(1, Math.ceil((xMax - xMin) / gridStep / Math.floor(plotW / 40)));
32311
+ let kx = 0;
32312
+ for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
32313
+ if (kx % labelEveryX === 0 && x !== 0) {
32314
+ out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: 10, align: "center" });
32315
+ }
32316
+ }
32317
+ const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
32318
+ let ky = 0;
32319
+ for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
32320
+ if (ky % labelEveryY === 0 && y !== 0) {
32321
+ out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: 10, align: "right" });
32322
+ }
32323
+ }
32324
+ if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
32325
+ out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: 10, align: "right" });
32326
+ }
32327
+ }
32328
+ for (const region of regions) {
32329
+ if (!region.samples || region.samples.length === 0) continue;
32330
+ const baseline = region.baseline ?? 0;
32331
+ const clampedPoint = (p) => ({
32332
+ x: mapX(Math.min(xMax, Math.max(xMin, p.x))),
32333
+ y: mapY(Math.min(yMax, Math.max(yMin, p.y)))
32334
+ });
32335
+ const upper = region.samples.map(clampedPoint);
32336
+ const first = region.samples[0];
32337
+ const last = region.samples[region.samples.length - 1];
32338
+ const closing = region.samples2 && region.samples2.length > 0 ? [...region.samples2].reverse().map(clampedPoint) : [clampedPoint({ x: last.x, y: baseline }), clampedPoint({ x: first.x, y: baseline })];
32339
+ const color = region.color ?? "#2563eb";
32340
+ out.push({
32341
+ type: "polygon",
32342
+ points: [...upper, ...closing],
32343
+ fill: color,
32344
+ color,
32345
+ opacity: region.opacity ?? 0.2,
32346
+ lineWidth: 1
32347
+ });
32348
+ if (region.label) {
32349
+ const mid = Math.floor(region.samples.length / 2);
32350
+ out.push({
32351
+ type: "text",
32352
+ x: mapX((first.x + last.x) / 2),
32353
+ y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
32354
+ text: region.label,
32355
+ color: "#111827",
32356
+ fontSize: 11
32357
+ });
32358
+ }
32359
+ }
32360
+ for (const bar of bars) {
32361
+ if (bar.x + bar.width < xMin || bar.x > xMax) continue;
32362
+ const y0 = bar.y0 ?? 0;
32363
+ const color = bar.color ?? "#93c5fd";
32364
+ out.push({
32365
+ type: "rect",
32366
+ x: mapX(bar.x),
32367
+ y: mapY(Math.max(y0, bar.y1)),
32368
+ width: mapX(bar.x + bar.width) - mapX(bar.x),
32369
+ height: Math.abs(mapY(bar.y1) - mapY(y0)),
32370
+ color,
32371
+ fill: color,
32372
+ opacity: bar.opacity ?? 0.5,
32373
+ lineWidth: 1
32374
+ });
32375
+ }
30698
32376
  if (showAxes) {
30699
- const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
30700
- const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
30701
32377
  out.push({ type: "line", x1: margin, y1: xAxisY, x2: width - margin, y2: xAxisY, color: "#374151", lineWidth: 2 });
30702
32378
  out.push({ type: "line", x1: yAxisX, y1: margin, x2: yAxisX, y2: height - margin, color: "#374151", lineWidth: 2 });
30703
32379
  }
32380
+ for (const guide of guides) {
32381
+ const color = guide.color ?? "#9ca3af";
32382
+ const dash = guide.dash ?? "dashed";
32383
+ if (guide.kind === "vline") {
32384
+ if (guide.at < xMin || guide.at > xMax) continue;
32385
+ const px = mapX(guide.at);
32386
+ out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
32387
+ if (guide.label) {
32388
+ out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color: "#111827", fontSize: 11 });
32389
+ }
32390
+ } else {
32391
+ if (guide.at < yMin || guide.at > yMax) continue;
32392
+ const py = mapY(guide.at);
32393
+ out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
32394
+ if (guide.label) {
32395
+ out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color: "#111827", fontSize: 11, align: "right" });
32396
+ }
32397
+ }
32398
+ }
30704
32399
  for (const curve of curves) {
30705
32400
  if (!curve.samples || curve.samples.length < 2) continue;
32401
+ let lastInRange;
30706
32402
  for (let i = 1; i < curve.samples.length; i++) {
30707
32403
  const a = curve.samples[i - 1];
30708
32404
  const b = curve.samples[i];
@@ -30714,19 +32410,97 @@ var init_MathCanvas = __esm({
30714
32410
  x2: mapX(b.x),
30715
32411
  y2: mapY(b.y),
30716
32412
  color: curve.color ?? "#2563eb",
30717
- lineWidth: 2
32413
+ lineWidth: 2,
32414
+ dash: curve.dash
32415
+ });
32416
+ lastInRange = b;
32417
+ }
32418
+ if (showCurveLabels && curve.label && lastInRange) {
32419
+ out.push({
32420
+ type: "text",
32421
+ x: mapX(lastInRange.x) + 6,
32422
+ y: mapY(lastInRange.y) - 6,
32423
+ text: curve.label,
32424
+ color: curve.color ?? "#2563eb",
32425
+ fontSize: 11
32426
+ });
32427
+ }
32428
+ }
32429
+ for (const hop of hops) {
32430
+ const x1 = mapX(hop.from);
32431
+ const x2 = mapX(hop.to);
32432
+ const peak = Math.min(36, plotH * 0.3);
32433
+ const color = hop.color ?? "#7c3aed";
32434
+ out.push({
32435
+ type: "ellipse",
32436
+ x: (x1 + x2) / 2,
32437
+ y: xAxisY,
32438
+ width: Math.abs(x2 - x1),
32439
+ height: 2 * peak,
32440
+ startAngle: 180,
32441
+ endAngle: 360,
32442
+ color
32443
+ });
32444
+ const s = Math.sign(hop.to - hop.from);
32445
+ out.push({
32446
+ type: "polygon",
32447
+ points: [
32448
+ { x: x2, y: xAxisY },
32449
+ { x: x2 - 4 * s, y: xAxisY - 7 },
32450
+ { x: x2 + 2 * s, y: xAxisY - 7 }
32451
+ ],
32452
+ fill: color,
32453
+ color
32454
+ });
32455
+ if (hop.label) {
32456
+ out.push({
32457
+ type: "text",
32458
+ x: (x1 + x2) / 2,
32459
+ y: xAxisY - peak - 8,
32460
+ text: hop.label,
32461
+ color: "#111827",
32462
+ fontSize: 10,
32463
+ align: "center"
32464
+ });
32465
+ }
32466
+ }
32467
+ for (const angle of angles) {
32468
+ const radius = angle.radius ?? 0.8;
32469
+ const color = angle.color ?? "#0ea5e9";
32470
+ out.push({
32471
+ type: "ellipse",
32472
+ x: mapX(angle.x),
32473
+ y: mapY(angle.y),
32474
+ width: 2 * radius * plotW / (xMax - xMin),
32475
+ height: 2 * radius * plotH / (yMax - yMin),
32476
+ startAngle: -angle.to,
32477
+ endAngle: -angle.from,
32478
+ color
32479
+ });
32480
+ if (angle.label) {
32481
+ const mid = (angle.from + angle.to) / 2;
32482
+ const rad = mid * Math.PI / 180;
32483
+ out.push({
32484
+ type: "text",
32485
+ x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
32486
+ y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
32487
+ text: angle.label,
32488
+ color: "#111827",
32489
+ fontSize: 11,
32490
+ align: "center"
30718
32491
  });
30719
32492
  }
30720
32493
  }
30721
32494
  for (const p of points) {
30722
32495
  if (p.x < xMin || p.x > xMax || p.y < yMin || p.y > yMax) continue;
32496
+ const isOpen = p.style === "open";
30723
32497
  out.push({
30724
32498
  type: "circle",
30725
32499
  x: mapX(p.x),
30726
32500
  y: mapY(p.y),
30727
32501
  radius: p.radius ?? 4,
30728
32502
  color: p.color ?? "#dc2626",
30729
- fill: p.color ?? "#dc2626"
32503
+ fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
30730
32504
  });
30731
32505
  if (p.label) {
30732
32506
  out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: "#111827", fontSize: 12 });
@@ -30745,7 +32519,28 @@ var init_MathCanvas = __esm({
30745
32519
  }
30746
32520
  out.push(...shapes);
30747
32521
  return out;
30748
- }, [width, height, xMin, xMax, yMin, yMax, showAxes, showGrid, gridStep, curves, points, vectors, shapes]);
32522
+ }, [
32523
+ width,
32524
+ height,
32525
+ xMin,
32526
+ xMax,
32527
+ yMin,
32528
+ yMax,
32529
+ showAxes,
32530
+ showGrid,
32531
+ gridStep,
32532
+ showTickLabels,
32533
+ showCurveLabels,
32534
+ curves,
32535
+ points,
32536
+ vectors,
32537
+ regions,
32538
+ bars,
32539
+ guides,
32540
+ angles,
32541
+ hops,
32542
+ shapes
32543
+ ]);
30749
32544
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
30750
32545
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
30751
32546
  /* @__PURE__ */ jsx(
@@ -30754,6 +32549,8 @@ var init_MathCanvas = __esm({
30754
32549
  width,
30755
32550
  height,
30756
32551
  shapes: derivedShapes,
32552
+ readouts,
32553
+ traces,
30757
32554
  interactive,
30758
32555
  animate,
30759
32556
  onShapeClick,
@@ -30765,7 +32562,315 @@ var init_MathCanvas = __esm({
30765
32562
  };
30766
32563
  }
30767
32564
  });
30768
- var physicsLog2, PhysicsCanvas;
32565
+ function formatMeterValue(v) {
32566
+ return Number.isInteger(v) ? String(v) : String(Number(v.toFixed(2)));
32567
+ }
32568
+ function sceneObjectShapes(obj, canvasWidth, canvasHeight) {
32569
+ const out = [];
32570
+ const color = obj.color ?? "#334155";
32571
+ switch (obj.kind) {
32572
+ case "ground": {
32573
+ const xStart = obj.x1 ?? 0;
32574
+ const xEnd = obj.x2 ?? canvasWidth;
32575
+ const y = obj.y ?? 0;
32576
+ out.push({ type: "line", x1: xStart, y1: y, x2: xEnd, y2: y, color, lineWidth: 2 });
32577
+ for (let hx = xStart + 7; hx <= xEnd; hx += 14) {
32578
+ out.push({ type: "line", x1: hx, y1: y, x2: hx - 7, y2: y + 7, color, lineWidth: 1 });
32579
+ }
32580
+ if (obj.label) {
32581
+ out.push({
32582
+ type: "text",
32583
+ x: (xStart + xEnd) / 2,
32584
+ y: y - 10,
32585
+ text: obj.label,
32586
+ color: PHYSICS_LABEL_COLOR,
32587
+ fontSize: 11,
32588
+ align: "center"
32589
+ });
32590
+ }
32591
+ break;
32592
+ }
32593
+ case "wall": {
32594
+ const yStart = obj.y1 ?? 0;
32595
+ const yEnd = obj.y2 ?? canvasHeight;
32596
+ const x = obj.x ?? 0;
32597
+ out.push({ type: "line", x1: x, y1: yStart, x2: x, y2: yEnd, color, lineWidth: 2 });
32598
+ for (let hy = yStart + 7; hy <= yEnd; hy += 14) {
32599
+ out.push({ type: "line", x1: x, y1: hy, x2: x - 7, y2: hy + 7, color, lineWidth: 1 });
32600
+ }
32601
+ if (obj.label) {
32602
+ out.push({
32603
+ type: "text",
32604
+ x: x + 12,
32605
+ y: (yStart + yEnd) / 2,
32606
+ text: obj.label,
32607
+ color: PHYSICS_LABEL_COLOR,
32608
+ fontSize: 11,
32609
+ align: "left"
32610
+ });
32611
+ }
32612
+ break;
32613
+ }
32614
+ case "ramp": {
32615
+ const x1 = obj.x1 ?? 0;
32616
+ const y1 = obj.y1 ?? 0;
32617
+ const x2 = obj.x2 ?? canvasWidth;
32618
+ const y2 = obj.y2 ?? canvasHeight;
32619
+ out.push({
32620
+ type: "polygon",
32621
+ points: [
32622
+ { x: x1, y: y1 },
32623
+ { x: x2, y: y2 },
32624
+ { x: x1, y: y2 }
32625
+ ],
32626
+ color,
32627
+ fill: obj.fill ?? "#e2e8f0",
32628
+ lineWidth: 2
32629
+ });
32630
+ if (obj.label) {
32631
+ out.push({
32632
+ type: "text",
32633
+ x: (2 * x1 + x2) / 3,
32634
+ y: (y1 + 2 * y2) / 3,
32635
+ text: obj.label,
32636
+ color: PHYSICS_LABEL_COLOR,
32637
+ fontSize: 11,
32638
+ align: "center"
32639
+ });
32640
+ }
32641
+ break;
32642
+ }
32643
+ case "box": {
32644
+ const x = obj.x ?? 0;
32645
+ const y = obj.y ?? 0;
32646
+ const w = obj.width ?? 40;
32647
+ const h = obj.height ?? 40;
32648
+ out.push({ type: "rect", x, y, width: w, height: h, color, fill: obj.fill, lineWidth: 2 });
32649
+ if (obj.label) {
32650
+ out.push({
32651
+ type: "text",
32652
+ x: x + w / 2,
32653
+ y: y + h / 2,
32654
+ text: obj.label,
32655
+ color: PHYSICS_LABEL_COLOR,
32656
+ fontSize: 11,
32657
+ align: "center"
32658
+ });
32659
+ }
32660
+ break;
32661
+ }
32662
+ case "pivot": {
32663
+ const x = obj.x ?? 0;
32664
+ const y = obj.y ?? 0;
32665
+ out.push({ type: "circle", x, y, radius: 5, color, fill: color });
32666
+ out.push({ type: "line", x1: x - 14, y1: y - 8, x2: x + 14, y2: y - 8, color, lineWidth: 1 });
32667
+ for (let k = 0; k < 5; k++) {
32668
+ const hx = x - 14 + 7 * k;
32669
+ out.push({ type: "line", x1: hx, y1: y - 8, x2: hx - 6, y2: y - 14, color, lineWidth: 1 });
32670
+ }
32671
+ if (obj.label) {
32672
+ out.push({
32673
+ type: "text",
32674
+ x,
32675
+ y: y - 20,
32676
+ text: obj.label,
32677
+ color: PHYSICS_LABEL_COLOR,
32678
+ fontSize: 11,
32679
+ align: "center"
32680
+ });
32681
+ }
32682
+ break;
32683
+ }
32684
+ }
32685
+ return out;
32686
+ }
32687
+ function trailShapes(trail) {
32688
+ const n = trail.points.length;
32689
+ if (n < 2) return [];
32690
+ const color = trail.color ?? "#94a3b8";
32691
+ const lineWidth = trail.width ?? 2;
32692
+ const fade = trail.fade ?? true;
32693
+ const globalOpacity = trail.opacity ?? 1;
32694
+ const out = [];
32695
+ for (let i = 0; i < n - 1; i++) {
32696
+ const a = trail.points[i];
32697
+ const b = trail.points[i + 1];
32698
+ const segmentOpacity = fade ? 0.12 + 0.68 * i / (n - 1) : 0.6;
32699
+ out.push({
32700
+ type: "line",
32701
+ x1: a.x,
32702
+ y1: a.y,
32703
+ x2: b.x,
32704
+ y2: b.y,
32705
+ color,
32706
+ lineWidth,
32707
+ opacity: segmentOpacity * globalOpacity
32708
+ });
32709
+ }
32710
+ return out;
32711
+ }
32712
+ function constraintShapes(c, a, b) {
32713
+ const color = c.color ?? "#9ca3af";
32714
+ const kind = c.kind ?? "rod";
32715
+ if (kind === "rod") {
32716
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2 }];
32717
+ }
32718
+ if (kind === "string") {
32719
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2, dash: "dashed" }];
32720
+ }
32721
+ const COILS = 8;
32722
+ const AMP = 7;
32723
+ const LEAD = 10;
32724
+ const dx = b.x - a.x;
32725
+ const dy = b.y - a.y;
32726
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
32727
+ const ux = dx / dist;
32728
+ const uy = dy / dist;
32729
+ const perpX = -uy;
32730
+ const perpY = ux;
32731
+ const aPrime = { x: a.x + LEAD * ux, y: a.y + LEAD * uy };
32732
+ const bPrime = { x: b.x - LEAD * ux, y: b.y - LEAD * uy };
32733
+ const m = 2 * COILS;
32734
+ const polyline = [{ x: a.x, y: a.y }, aPrime];
32735
+ for (let j = 1; j <= m; j++) {
32736
+ const t = j / (m + 1);
32737
+ const baseX = aPrime.x + t * (bPrime.x - aPrime.x);
32738
+ const baseY = aPrime.y + t * (bPrime.y - aPrime.y);
32739
+ const sign = j % 2 === 0 ? 1 : -1;
32740
+ polyline.push({ x: baseX + sign * AMP * perpX, y: baseY + sign * AMP * perpY });
32741
+ }
32742
+ polyline.push(bPrime, { x: b.x, y: b.y });
32743
+ const out = [];
32744
+ for (let i = 1; i < polyline.length; i++) {
32745
+ out.push({
32746
+ type: "line",
32747
+ x1: polyline[i - 1].x,
32748
+ y1: polyline[i - 1].y,
32749
+ x2: polyline[i].x,
32750
+ y2: polyline[i].y,
32751
+ color,
32752
+ lineWidth: 2
32753
+ });
32754
+ }
32755
+ return out;
32756
+ }
32757
+ function vectorShapes(v, bodyById) {
32758
+ let ax;
32759
+ let ay;
32760
+ if (v.body) {
32761
+ const anchor = bodyById.get(v.body);
32762
+ if (!anchor) return [];
32763
+ ax = anchor.x;
32764
+ ay = anchor.y;
32765
+ } else {
32766
+ ax = v.x ?? 0;
32767
+ ay = v.y ?? 0;
32768
+ }
32769
+ const scale = v.scale ?? 1;
32770
+ const color = v.color ?? "#dc2626";
32771
+ const tx = ax + v.dx * scale;
32772
+ const ty = ay + v.dy * scale;
32773
+ const out = [{ type: "arrow", x1: ax, y1: ay, x2: tx, y2: ty, color, lineWidth: 2, dash: v.dash }];
32774
+ if (v.label) {
32775
+ const dist = Math.max(1e-6, Math.hypot(tx - ax, ty - ay));
32776
+ const ux = (tx - ax) / dist;
32777
+ const uy = (ty - ay) / dist;
32778
+ out.push({
32779
+ type: "text",
32780
+ x: tx + 8 * ux,
32781
+ y: ty + 8 * uy,
32782
+ text: v.label,
32783
+ color,
32784
+ fontSize: 11,
32785
+ align: "center"
32786
+ });
32787
+ }
32788
+ return out;
32789
+ }
32790
+ function angleMarkerShapes(a) {
32791
+ const radius = a.radius ?? 26;
32792
+ const color = a.color ?? "#0ea5e9";
32793
+ const out = [
32794
+ {
32795
+ type: "ellipse",
32796
+ x: a.x,
32797
+ y: a.y,
32798
+ width: radius * 2,
32799
+ height: radius * 2,
32800
+ startAngle: a.from,
32801
+ endAngle: a.to,
32802
+ color,
32803
+ lineWidth: 2
32804
+ }
32805
+ ];
32806
+ if (a.label) {
32807
+ const mid = (a.from + a.to) / 2 * (Math.PI / 180);
32808
+ out.push({
32809
+ type: "text",
32810
+ x: a.x + (radius + 13) * Math.cos(mid),
32811
+ y: a.y + (radius + 13) * Math.sin(mid),
32812
+ text: a.label,
32813
+ color,
32814
+ fontSize: 11,
32815
+ align: "center"
32816
+ });
32817
+ }
32818
+ return out;
32819
+ }
32820
+ function fieldShapes(field, canvasWidth, canvasHeight) {
32821
+ const spacing = field.spacing ?? 48;
32822
+ const size = field.size ?? 14;
32823
+ const color = field.color ?? "#94a3b8";
32824
+ const regionX = field.x ?? 0;
32825
+ const regionY = field.y ?? 0;
32826
+ const regionW = field.width ?? canvasWidth;
32827
+ const regionH = field.height ?? canvasHeight;
32828
+ const out = [];
32829
+ for (let gx = regionX + spacing / 2; gx < regionX + regionW; gx += spacing) {
32830
+ for (let gy = regionY + spacing / 2; gy < regionY + regionH; gy += spacing) {
32831
+ if (field.kind === "arrows") {
32832
+ const rad = (field.angle ?? 0) * Math.PI / 180;
32833
+ const hx = Math.cos(rad) * size / 2;
32834
+ const hy = Math.sin(rad) * size / 2;
32835
+ out.push({ type: "arrow", x1: gx - hx, y1: gy - hy, x2: gx + hx, y2: gy + hy, color, lineWidth: 2 });
32836
+ } else if (field.kind === "into") {
32837
+ const r2 = size / 3;
32838
+ const d = 0.6 * r2 * Math.SQRT1_2;
32839
+ out.push({ type: "circle", x: gx, y: gy, radius: r2, color });
32840
+ out.push({ type: "line", x1: gx - d, y1: gy - d, x2: gx + d, y2: gy + d, color, lineWidth: 1 });
32841
+ out.push({ type: "line", x1: gx - d, y1: gy + d, x2: gx + d, y2: gy - d, color, lineWidth: 1 });
32842
+ } else {
32843
+ const r2 = size / 3;
32844
+ out.push({ type: "circle", x: gx, y: gy, radius: r2, color });
32845
+ out.push({ type: "circle", x: gx, y: gy, radius: 1.5, color, fill: color });
32846
+ }
32847
+ }
32848
+ }
32849
+ return out;
32850
+ }
32851
+ function meterShapes(meters, canvasHeight) {
32852
+ const n = meters.length;
32853
+ const out = [];
32854
+ const sharedMax = Math.max(1e-6, ...meters.map((m) => m.value));
32855
+ meters.forEach((meter, i) => {
32856
+ const rowY = canvasHeight - 10 - 16 * (n - i);
32857
+ const color = meter.color ?? "#3b82f6";
32858
+ const M = meter.max ?? sharedMax;
32859
+ const w = Math.round(Math.min(1, Math.max(0, meter.value / M)) * 110);
32860
+ out.push({ type: "text", x: 8, y: rowY + 8, text: meter.label, color: PHYSICS_LABEL_COLOR, fontSize: 10 });
32861
+ out.push({ type: "rect", x: 52, y: rowY, width: w, height: 10, color, fill: color });
32862
+ out.push({
32863
+ type: "text",
32864
+ x: 166,
32865
+ y: rowY + 8,
32866
+ text: formatMeterValue(meter.value),
32867
+ color: "#6b7280",
32868
+ fontSize: 9
32869
+ });
32870
+ });
32871
+ return out;
32872
+ }
32873
+ var physicsLog2, PHYSICS_LABEL_COLOR, PhysicsCanvas;
30769
32874
  var init_PhysicsCanvas = __esm({
30770
32875
  "components/learning/molecules/PhysicsCanvas.tsx"() {
30771
32876
  "use client";
@@ -30774,6 +32879,7 @@ var init_PhysicsCanvas = __esm({
30774
32879
  init_LearningCanvas();
30775
32880
  init_learningScene3D();
30776
32881
  physicsLog2 = createLogger("almadar:ui:physics-canvas");
32882
+ PHYSICS_LABEL_COLOR = "#374151";
30777
32883
  PhysicsCanvas = ({
30778
32884
  className,
30779
32885
  width = 600,
@@ -30790,7 +32896,18 @@ var init_PhysicsCanvas = __esm({
30790
32896
  showForces = false,
30791
32897
  velocityScale = 20,
30792
32898
  forceScale = 20,
32899
+ sceneObjects = [],
32900
+ trails = [],
32901
+ vectors = [],
32902
+ surface3d,
32903
+ vectors3d = [],
32904
+ vectorScale = 1,
32905
+ angles = [],
32906
+ field,
32907
+ meters = [],
30793
32908
  shapes = [],
32909
+ readouts,
32910
+ traces,
30794
32911
  showGrid,
30795
32912
  shadows,
30796
32913
  interactive,
@@ -30805,19 +32922,14 @@ var init_PhysicsCanvas = __esm({
30805
32922
  for (const b of bodies) {
30806
32923
  if (b.id) bodyById.set(b.id, b);
30807
32924
  }
32925
+ if (field) out.push(...fieldShapes(field, width, height));
32926
+ for (const obj of sceneObjects) out.push(...sceneObjectShapes(obj, width, height));
32927
+ for (const trail of trails) out.push(...trailShapes(trail));
30808
32928
  for (const c of constraints) {
30809
32929
  const a = bodyById.get(c.from);
30810
32930
  const b = bodyById.get(c.to);
30811
32931
  if (!a || !b) continue;
30812
- out.push({
30813
- type: "line",
30814
- x1: a.x,
30815
- y1: a.y,
30816
- x2: b.x,
30817
- y2: b.y,
30818
- color: c.color ?? "#9ca3af",
30819
- lineWidth: 2
30820
- });
32932
+ out.push(...constraintShapes(c, a, b));
30821
32933
  }
30822
32934
  for (const b of bodies) {
30823
32935
  out.push({
@@ -30862,14 +32974,51 @@ var init_PhysicsCanvas = __esm({
30862
32974
  });
30863
32975
  }
30864
32976
  }
32977
+ for (const v of vectors) out.push(...vectorShapes(v, bodyById));
32978
+ for (const a of angles) out.push(...angleMarkerShapes(a));
32979
+ if (meters.length > 0) out.push(...meterShapes(meters, height));
30865
32980
  out.push(...shapes);
30866
32981
  return out;
30867
- }, [bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes]);
32982
+ }, [
32983
+ bodies,
32984
+ constraints,
32985
+ showVelocity,
32986
+ showForces,
32987
+ velocityScale,
32988
+ forceScale,
32989
+ sceneObjects,
32990
+ trails,
32991
+ vectors,
32992
+ angles,
32993
+ field,
32994
+ meters,
32995
+ shapes,
32996
+ width,
32997
+ height
32998
+ ]);
30868
32999
  const drawables3D = useMemo(() => {
30869
33000
  if (mode !== "3d") return [];
30870
33001
  if (shapes.length > 0) {
30871
33002
  physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
30872
33003
  }
33004
+ if (sceneObjects.length > 0) {
33005
+ physicsLog2.debug("sceneObjects ignored in 3D mode (pixel-authored 2D vocabulary)", { count: sceneObjects.length });
33006
+ }
33007
+ if (vectors.length > 0) {
33008
+ physicsLog2.debug("vectors ignored in 3D mode (pixel-authored 2D vocabulary)", { count: vectors.length });
33009
+ }
33010
+ if (angles.length > 0) {
33011
+ physicsLog2.debug("angles ignored in 3D mode (pixel-authored 2D vocabulary)", { count: angles.length });
33012
+ }
33013
+ if (field) {
33014
+ physicsLog2.debug("field ignored in 3D mode (pixel-authored 2D vocabulary)");
33015
+ }
33016
+ if (meters.length > 0) {
33017
+ physicsLog2.debug("meters ignored in 3D mode (pixel-authored 2D vocabulary)", { count: meters.length });
33018
+ }
33019
+ if (animate) {
33020
+ physicsLog2.debug("animate ignored in 3D mode (motion is entity-state driven)");
33021
+ }
30873
33022
  const out = [];
30874
33023
  const labelColor = labelColorForBackground(backgroundColor);
30875
33024
  const bodyById = /* @__PURE__ */ new Map();
@@ -30917,15 +33066,67 @@ var init_PhysicsCanvas = __esm({
30917
33066
  if (arrow) out.push(arrow);
30918
33067
  }
30919
33068
  }
33069
+ for (const trail of trails) {
33070
+ if (trail.fade !== void 0) {
33071
+ physicsLog2.debug("trail.fade ignored in 3D mode (2D-only fade curve \u2014 3D draws an opaque tube)", { id: trail.id });
33072
+ }
33073
+ const points = trail.points.map((p) => [p.x, p.y, p.z ?? 0]);
33074
+ out.push(
33075
+ ...polylineTube(points, trail.width ?? 0.05, trail.color ?? "#94a3b8", {
33076
+ ...trail.opacity !== void 0 ? { opacity: trail.opacity } : {}
33077
+ })
33078
+ );
33079
+ }
33080
+ if (surface3d) {
33081
+ out.push(...heightFieldMesh(surface3d));
33082
+ }
33083
+ if (vectors3d.length > 0) {
33084
+ out.push(
33085
+ ...arrowField(
33086
+ vectors3d.map((v) => ({
33087
+ id: v.id,
33088
+ from: [v.x, v.y, v.z ?? 0],
33089
+ delta: [v.dx, v.dy, v.dz ?? 0],
33090
+ color: v.color,
33091
+ label: v.label,
33092
+ width: v.width
33093
+ })),
33094
+ { scale: vectorScale, labelColor }
33095
+ )
33096
+ );
33097
+ }
30920
33098
  return out;
30921
- }, [mode, bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes, backgroundColor]);
33099
+ }, [
33100
+ mode,
33101
+ bodies,
33102
+ constraints,
33103
+ showVelocity,
33104
+ showForces,
33105
+ velocityScale,
33106
+ forceScale,
33107
+ shapes,
33108
+ sceneObjects,
33109
+ trails,
33110
+ vectors,
33111
+ surface3d,
33112
+ vectors3d,
33113
+ vectorScale,
33114
+ angles,
33115
+ field,
33116
+ meters,
33117
+ animate,
33118
+ backgroundColor
33119
+ ]);
30922
33120
  const bodyIndexById = useMemo(() => {
30923
33121
  const m = /* @__PURE__ */ new Map();
33122
+ vectors3d.forEach((v, i) => {
33123
+ if (v.id) m.set(v.id, i);
33124
+ });
30924
33125
  bodies.forEach((b, i) => {
30925
33126
  if (b.id) m.set(b.id, i);
30926
33127
  });
30927
33128
  return m;
30928
- }, [bodies]);
33129
+ }, [bodies, vectors3d]);
30929
33130
  if (mode === "3d") {
30930
33131
  return /* @__PURE__ */ jsx(
30931
33132
  LearningScene3D,
@@ -30957,6 +33158,8 @@ var init_PhysicsCanvas = __esm({
30957
33158
  height,
30958
33159
  backgroundColor,
30959
33160
  shapes: derivedShapes,
33161
+ readouts,
33162
+ traces,
30960
33163
  interactive: interactive ?? false,
30961
33164
  animate,
30962
33165
  onShapeClick,
@@ -31107,7 +33310,7 @@ function layoutFlow(nodeIds, adjacency, roots, width, height, margin) {
31107
33310
  }
31108
33311
  return nodeIds.map((id) => positions.get(id));
31109
33312
  }
31110
- function layoutTree2(nodeIds, adjacency, roots, width, height, margin) {
33313
+ function layoutTree3(nodeIds, adjacency, roots, width, height, margin) {
31111
33314
  const effectiveRoots = roots.length > 0 ? roots : [nodeIds[0]];
31112
33315
  const layers = assignLayers(nodeIds, adjacency, effectiveRoots);
31113
33316
  const maxLayer = Math.max(...Array.from(layers.values()));
@@ -31163,7 +33366,7 @@ function computeStaticLayout(mode, input) {
31163
33366
  const adjacency = buildAdjacency(nodeIds, edges);
31164
33367
  const roots = findRoots(nodeIds, adjacency);
31165
33368
  if (mode === "flow") return layoutFlow(nodeIds, adjacency, roots, width, height, margin);
31166
- if (mode === "tree") return layoutTree2(nodeIds, adjacency, roots, width, height, margin);
33369
+ if (mode === "tree") return layoutTree3(nodeIds, adjacency, roots, width, height, margin);
31167
33370
  return layoutRadial(nodeIds, adjacency, roots, width, height, margin);
31168
33371
  }
31169
33372
  var init_graphViewLayouts = __esm({
@@ -32406,26 +34609,6 @@ var init_Lightbox = __esm({
32406
34609
  Lightbox.displayName = "Lightbox";
32407
34610
  }
32408
34611
  });
32409
- function useMediaQuery(query) {
32410
- const subscribe = useCallback(
32411
- (onChange) => {
32412
- const mql = window.matchMedia(query);
32413
- mql.addEventListener("change", onChange);
32414
- return () => mql.removeEventListener("change", onChange);
32415
- },
32416
- [query]
32417
- );
32418
- return useSyncExternalStore(
32419
- subscribe,
32420
- () => window.matchMedia(query).matches,
32421
- () => false
32422
- );
32423
- }
32424
- var init_useMediaQuery = __esm({
32425
- "hooks/useMediaQuery.ts"() {
32426
- "use client";
32427
- }
32428
- });
32429
34612
  function renderIconInput3(icon, props) {
32430
34613
  return typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsx(Icon, { icon, ...props });
32431
34614
  }
@@ -32464,8 +34647,8 @@ function TableView({
32464
34647
  columns,
32465
34648
  fields,
32466
34649
  itemActions,
32467
- maxInlineActions,
32468
- itemClickEvent,
34650
+ maxInlineActions: _maxInlineActions,
34651
+ itemClickEvent = "",
32469
34652
  selectable = false,
32470
34653
  selectEvent,
32471
34654
  selectedIds,
@@ -32515,7 +34698,6 @@ function TableView({
32515
34698
  const hasMore = pageSize > 0 && visibleCount < ordered2.length;
32516
34699
  const hasRenderProp = typeof children === "function";
32517
34700
  const idField = dndItemIdField ?? "id";
32518
- const isCoarsePointer = useMediaQuery("(pointer: coarse)");
32519
34701
  React94__default.useEffect(() => {
32520
34702
  tableViewLog.debug("render", {
32521
34703
  rowCount: data.length,
@@ -32557,21 +34739,14 @@ function TableView({
32557
34739
  const dir = sortColumn === (col.field ?? col.key) && sortDirection === "asc" ? "desc" : "asc";
32558
34740
  eventBus.emit(`UI:${sortEvent}`, { column: col.field ?? col.key, direction: dir });
32559
34741
  };
32560
- const handleActionClick = (action, row) => (e) => {
32561
- e.stopPropagation();
32562
- const payload = {
32563
- id: row.id,
32564
- row
32565
- };
32566
- eventBus.emit(`UI:${action.event}`, payload);
32567
- };
34742
+ const rowClickEvent = itemClickEvent || actionDefs.find((a) => a.variant !== "danger")?.event;
32568
34743
  const handleRowClick = (row) => () => {
32569
- if (!itemClickEvent) return;
34744
+ if (!rowClickEvent) return;
32570
34745
  const payload = {
32571
34746
  id: row.id,
32572
34747
  row
32573
34748
  };
32574
- eventBus.emit(`UI:${itemClickEvent}`, payload);
34749
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
32575
34750
  };
32576
34751
  const colFloors = React94__default.useMemo(
32577
34752
  () => colDefs.map((col) => {
@@ -32587,10 +34762,7 @@ function TableView({
32587
34762
  const statusNode = isLoading ? /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) }) : error ? /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "error", children: error.message }) }) : data.length === 0 ? /* @__PURE__ */ jsx(Box, { className: "text-center py-12", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: emptyMessage || t("empty.noItems") }) }) : null;
32588
34763
  const lk = LOOKS[look];
32589
34764
  const hasActions = actionDefs.length > 0;
32590
- const effectiveMaxInline = isCoarsePointer ? 0 : maxInlineActions;
32591
- const inlineActionCount = hasActions ? effectiveMaxInline != null ? Math.min(actionDefs.length, effectiveMaxInline) : actionDefs.length : 0;
32592
- const hasOverflowActions = hasActions && effectiveMaxInline != null && actionDefs.length > effectiveMaxInline;
32593
- const actionsTrack = hasActions ? `${inlineActionCount * 6 + (hasOverflowActions ? 3 : 0)}rem` : null;
34765
+ const actionsTrack = hasActions ? "3rem" : null;
32594
34766
  const gridTemplateColumns = [
32595
34767
  selectable ? "auto" : null,
32596
34768
  ...colDefs.map((c, i) => c.width ?? `minmax(${colFloors[i]}ch, 1fr)`),
@@ -32637,7 +34809,7 @@ function TableView({
32637
34809
  col.key
32638
34810
  );
32639
34811
  }),
32640
- hasActions && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)]" })
34812
+ hasActions && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)] border-l border-[var(--color-border)] h-full" })
32641
34813
  ]
32642
34814
  }
32643
34815
  );
@@ -32649,12 +34821,12 @@ function TableView({
32649
34821
  role: "row",
32650
34822
  "data-entity-row": true,
32651
34823
  "data-entity-id": id,
32652
- onClick: itemClickEvent ? handleRowClick(row) : void 0,
34824
+ onClick: rowClickEvent ? handleRowClick(row) : void 0,
32653
34825
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
32654
34826
  className: cn(
32655
34827
  "group items-center gap-3 transition-colors duration-fast",
32656
34828
  hasRenderProp ? "flex" : "grid",
32657
- itemClickEvent && "cursor-pointer",
34829
+ rowClickEvent && "cursor-pointer",
32658
34830
  lk.rowPad,
32659
34831
  lk.divider && "border-b border-[var(--color-border)]",
32660
34832
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -32662,7 +34834,7 @@ function TableView({
32662
34834
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
32663
34835
  ),
32664
34836
  children: [
32665
- selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
34837
+ selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
32666
34838
  Checkbox,
32667
34839
  {
32668
34840
  checked: selected.has(id),
@@ -32683,53 +34855,37 @@ function TableView({
32683
34855
  }
32684
34856
  return /* @__PURE__ */ jsx(Box, { role: "cell", className: cellBase, children: /* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children: formatCell(raw, col.format) }) }, col.key);
32685
34857
  }),
32686
- hasActions && /* @__PURE__ */ jsxs(
34858
+ hasActions && /* @__PURE__ */ jsx(
32687
34859
  HStack,
32688
34860
  {
32689
34861
  gap: "xs",
32690
- onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
34862
+ onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0,
32691
34863
  className: cn(
32692
34864
  // Pinned: the fixed column tracks routinely overflow the caller's
32693
- // scroll container, which used to leave the actions off-screen.
32694
- // Opaque so scrolled cells pass underneath it.
34865
+ // scroll container, which would leave the kebab off-screen.
34866
+ // Opaque + hairline edge so it reads as a pinned column, not a
34867
+ // floating control, while scrolled cells pass underneath.
32695
34868
  "justify-end flex-shrink-0 sticky right-0 z-[1] transition-colors",
34869
+ "border-l border-[var(--color-border)]",
32696
34870
  lk.striped && index % 2 === 1 ? "bg-[var(--color-surface-subtle)]" : "bg-[var(--color-card)] group-hover:bg-[var(--color-surface-subtle)]"
32697
34871
  ),
32698
- children: [
32699
- (effectiveMaxInline != null ? actionDefs.slice(0, effectiveMaxInline) : actionDefs).map((action, i) => /* @__PURE__ */ jsxs(
32700
- Button,
32701
- {
32702
- variant: action.variant === "primary" ? "primary" : "ghost",
32703
- size: "sm",
32704
- onClick: handleActionClick(action, row),
32705
- "data-testid": `action-${action.event}`,
32706
- "data-row-id": String(row.id),
32707
- className: cn(action.variant === "danger" && "text-error hover:text-error hover:bg-error/10"),
32708
- children: [
32709
- action.icon && renderIconInput3(action.icon, { size: "xs", className: "mr-1" }),
32710
- action.label
32711
- ]
32712
- },
32713
- i
32714
- )),
32715
- effectiveMaxInline != null && actionDefs.length > effectiveMaxInline && /* @__PURE__ */ jsx(
32716
- Menu,
32717
- {
32718
- position: "bottom-end",
32719
- trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
32720
- items: actionDefs.slice(effectiveMaxInline).map((action) => ({
32721
- label: action.label,
32722
- icon: action.icon,
32723
- event: action.event,
32724
- variant: action.variant === "danger" ? "danger" : "default",
32725
- onClick: () => eventBus.emit(`UI:${action.event}`, {
32726
- id: row.id,
32727
- row
32728
- })
32729
- }))
32730
- }
32731
- )
32732
- ]
34872
+ children: /* @__PURE__ */ jsx(
34873
+ Menu,
34874
+ {
34875
+ position: "bottom-end",
34876
+ trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", "data-row-id": String(row.id), children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
34877
+ items: actionDefs.map((action) => ({
34878
+ label: action.label,
34879
+ icon: action.icon,
34880
+ event: action.event,
34881
+ variant: action.variant === "danger" ? "danger" : "default",
34882
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
34883
+ id: row.id,
34884
+ row
34885
+ })
34886
+ }))
34887
+ }
34888
+ )
32733
34889
  }
32734
34890
  )
32735
34891
  ]
@@ -32772,7 +34928,6 @@ var init_TableView = __esm({
32772
34928
  init_format();
32773
34929
  init_getNestedValue();
32774
34930
  init_useEventBus();
32775
- init_useMediaQuery();
32776
34931
  init_Box();
32777
34932
  init_Stack();
32778
34933
  init_Typography();
@@ -47441,6 +49596,7 @@ var init_component_registry_generated = __esm({
47441
49596
  init_ActionTile();
47442
49597
  init_ActivationBlock();
47443
49598
  init_ComponentPatterns();
49599
+ init_AlgoGraphCanvas();
47444
49600
  init_AlgorithmCanvas();
47445
49601
  init_AnimatedCounter();
47446
49602
  init_AnimatedGraphic();
@@ -47704,6 +49860,7 @@ var init_component_registry_generated = __esm({
47704
49860
  "ActivationBlock": ActivationBlock,
47705
49861
  "Alert": AlertPattern,
47706
49862
  "AlertPattern": AlertPattern,
49863
+ "AlgoGraphCanvas": AlgoGraphCanvas,
47707
49864
  "AlgorithmCanvas": AlgorithmCanvas,
47708
49865
  "AnimatedCounter": AnimatedCounter,
47709
49866
  "AnimatedGraphic": AnimatedGraphic,
@@ -52576,9 +54733,9 @@ var log8 = createLogger("almadar:ui:effects:client-handlers");
52576
54733
  function createClientEffectHandlers(options) {
52577
54734
  const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
52578
54735
  return {
52579
- emit: (event, payload) => {
54736
+ emit: (event, payload, source) => {
52580
54737
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
52581
- eventBus.emit(prefixedEvent, payload);
54738
+ eventBus.emit(prefixedEvent, payload, source);
52582
54739
  },
52583
54740
  persist: async () => {
52584
54741
  log8.warn("persist is server-side only, ignored on client");
@@ -52743,7 +54900,7 @@ function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
52743
54900
  }
52744
54901
  };
52745
54902
  ctx.emit = (event, payload) => {
52746
- emit(event, payload);
54903
+ emit(event, payload, { trait: traitName, tick: tick.name });
52747
54904
  };
52748
54905
  if (tick.guard !== void 0 && !evaluateGuard(tick.guard, ctx)) {
52749
54906
  tickLog.debug("guard-blocked", { traitName, tick: tick.name, state: currentState });
@@ -53195,6 +55352,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
53195
55352
  }
53196
55353
  const effectContext = {
53197
55354
  traitName,
55355
+ orbitalName: orbitalsByTrait?.[traitName],
53198
55356
  state: previousState,
53199
55357
  transition: `${previousState}->${newState}`,
53200
55358
  linkedEntity,
@@ -53271,7 +55429,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
53271
55429
  });
53272
55430
  }
53273
55431
  return emittedDuringExec;
53274
- }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot]);
55432
+ }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot, orbitalsByTrait]);
53275
55433
  const runTickEffects = useCallback((tick, binding) => {
53276
55434
  const traitName = binding.trait.name;
53277
55435
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
@@ -53307,9 +55465,9 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
53307
55465
  log: tickLog
53308
55466
  });
53309
55467
  }, [executeTransitionEffects, sharedEntityStore]);
53310
- const emitFromSharedWriter = useCallback((event, payload) => {
55468
+ const emitFromSharedWriter = useCallback((event, payload, source) => {
53311
55469
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
53312
- eventBus.emit(prefixedEvent, payload);
55470
+ eventBus.emit(prefixedEvent, payload, source);
53313
55471
  }, [eventBus]);
53314
55472
  useEffect(() => {
53315
55473
  const scheduler = createTickScheduler();