@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.
@@ -3431,7 +3431,7 @@ function useEventBus() {
3431
3431
  return {
3432
3432
  ...baseBus,
3433
3433
  emit: (type, payload, source) => {
3434
- if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
3434
+ if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".") && !source?.trait) {
3435
3435
  scopeLog.warn("emit:bare-key-no-scope", { type });
3436
3436
  }
3437
3437
  baseBus.emit(type, payload, source);
@@ -15121,6 +15121,14 @@ function shapeBounds(shape) {
15121
15121
  w: shape.radius * 2 + 8,
15122
15122
  h: shape.radius * 2 + 8
15123
15123
  };
15124
+ case "ellipse":
15125
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
15126
+ return {
15127
+ x: shape.x - shape.width / 2 - 4,
15128
+ y: shape.y - shape.height / 2 - 4,
15129
+ w: shape.width + 8,
15130
+ h: shape.height + 8
15131
+ };
15124
15132
  case "rect":
15125
15133
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
15126
15134
  return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
@@ -15154,13 +15162,14 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
15154
15162
  ctx.closePath();
15155
15163
  ctx.fill();
15156
15164
  }
15157
- function drawShape(ctx, shape, width, height) {
15165
+ function drawShape(ctx, shape, width, height, allShapes) {
15158
15166
  ctx.save();
15159
15167
  const opacity = shape.opacity ?? 1;
15160
15168
  ctx.globalAlpha = opacity;
15161
15169
  const stroke = resolveColor2(shape.color, ctx, "#333333");
15162
15170
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
15163
15171
  ctx.lineWidth = shape.lineWidth ?? 2;
15172
+ if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
15164
15173
  switch (shape.type) {
15165
15174
  case "grid": {
15166
15175
  const step = shape.step ?? 40;
@@ -15226,6 +15235,20 @@ function drawShape(ctx, shape, width, height) {
15226
15235
  ctx.stroke();
15227
15236
  break;
15228
15237
  }
15238
+ case "ellipse": {
15239
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
15240
+ const startAngle = (shape.startAngle ?? 0) * Math.PI / 180;
15241
+ const endAngle = (shape.endAngle ?? 360) * Math.PI / 180;
15242
+ ctx.beginPath();
15243
+ ctx.ellipse(shape.x, shape.y, shape.width / 2, shape.height / 2, 0, startAngle, endAngle);
15244
+ if (fill) {
15245
+ ctx.fillStyle = fill;
15246
+ ctx.fill();
15247
+ }
15248
+ ctx.strokeStyle = stroke;
15249
+ ctx.stroke();
15250
+ break;
15251
+ }
15229
15252
  case "rect": {
15230
15253
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
15231
15254
  if (fill) {
@@ -15272,21 +15295,153 @@ function drawShape(ctx, shape, width, height) {
15272
15295
  ctx.fillText(shape.text, shape.x, shape.y);
15273
15296
  break;
15274
15297
  }
15298
+ case "venn-region": {
15299
+ const resolveCircles = (ids) => (ids ?? []).flatMap((id) => {
15300
+ const c = allShapes.find((s) => s.type === "circle" && s.id === id);
15301
+ return c && c.x != null && c.y != null && c.radius != null ? [{ x: c.x, y: c.y, radius: c.radius }] : [];
15302
+ });
15303
+ const inside = resolveCircles(shape.inside);
15304
+ if (inside.length === 0) break;
15305
+ const outside = resolveCircles(shape.outside);
15306
+ const off = document.createElement("canvas");
15307
+ off.width = ctx.canvas.width;
15308
+ off.height = ctx.canvas.height;
15309
+ const octx = off.getContext("2d");
15310
+ if (!octx) break;
15311
+ octx.setTransform(ctx.getTransform());
15312
+ for (const c of inside) {
15313
+ const p = new Path2D();
15314
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
15315
+ octx.clip(p);
15316
+ }
15317
+ octx.fillStyle = fill ?? stroke;
15318
+ octx.fillRect(0, 0, width, height);
15319
+ octx.globalCompositeOperation = "destination-out";
15320
+ for (const c of outside) {
15321
+ const p = new Path2D();
15322
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
15323
+ octx.fill(p);
15324
+ }
15325
+ ctx.save();
15326
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
15327
+ ctx.drawImage(off, 0, 0);
15328
+ ctx.restore();
15329
+ break;
15330
+ }
15275
15331
  }
15276
15332
  ctx.restore();
15277
15333
  }
15278
- var LearningCanvas;
15334
+ function readoutShapes(readouts, width) {
15335
+ const out = [];
15336
+ const chipH = 18;
15337
+ const gap = 6;
15338
+ let rightEdge = width - 6;
15339
+ let rowY = 6;
15340
+ for (const readout of readouts) {
15341
+ const text = `${readout.label}: ${String(readout.value)}`;
15342
+ const chipW = Math.min(170, Math.max(34, text.length * 6 + 12));
15343
+ let chipX = rightEdge - chipW;
15344
+ if (chipX < 4) {
15345
+ rowY += chipH + 4;
15346
+ rightEdge = width - 6;
15347
+ chipX = rightEdge - chipW;
15348
+ }
15349
+ const color = readout.color ?? "#334155";
15350
+ out.push({ type: "rect", x: chipX, y: rowY, width: chipW, height: chipH, color, fill: color });
15351
+ out.push({
15352
+ type: "text",
15353
+ x: chipX + chipW / 2,
15354
+ y: rowY + chipH / 2,
15355
+ text,
15356
+ color: "#ffffff",
15357
+ fontSize: 10,
15358
+ align: "center"
15359
+ });
15360
+ rightEdge = chipX - gap;
15361
+ }
15362
+ return out;
15363
+ }
15364
+ function traceShapes(panel, k, width, height) {
15365
+ const w = panel.width ?? Math.round(width * 0.32);
15366
+ const h = panel.height ?? Math.round(height * 0.28);
15367
+ const x = panel.x ?? width - w - 8;
15368
+ const y = panel.y ?? height - h - 8 - k * (h + 8);
15369
+ const allSamples = panel.series.flatMap((series) => series.samples);
15370
+ let xLo = Math.min(...allSamples.map((p) => p.x));
15371
+ let xHi = Math.max(...allSamples.map((p) => p.x));
15372
+ let yLo = Math.min(...allSamples.map((p) => p.y));
15373
+ let yHi = Math.max(...allSamples.map((p) => p.y));
15374
+ if (xLo === xHi) {
15375
+ xLo -= 1;
15376
+ xHi += 1;
15377
+ }
15378
+ if (yLo === yHi) {
15379
+ yLo -= 1;
15380
+ yHi += 1;
15381
+ }
15382
+ const backgroundColor = panel.backgroundColor ?? "#ffffff";
15383
+ const frameColor = panel.frameColor ?? "#94a3b8";
15384
+ const out = [];
15385
+ out.push({
15386
+ type: "rect",
15387
+ x,
15388
+ y,
15389
+ width: w,
15390
+ height: h,
15391
+ color: backgroundColor,
15392
+ fill: backgroundColor,
15393
+ opacity: panel.backgroundOpacity ?? 0.85
15394
+ });
15395
+ out.push({ type: "rect", x, y, width: w, height: h, color: frameColor, lineWidth: 1 });
15396
+ panel.series.forEach((series, j) => {
15397
+ const color = series.color ?? TRACE_SERIES_COLORS[j % TRACE_SERIES_COLORS.length];
15398
+ const mapped = series.samples.map((p) => ({
15399
+ x: x + 4 + (p.x - xLo) / (xHi - xLo) * (w - 8),
15400
+ y: y + h - 4 - (p.y - yLo) / (yHi - yLo) * (h - 8)
15401
+ }));
15402
+ for (let i = 1; i < mapped.length; i++) {
15403
+ out.push({
15404
+ type: "line",
15405
+ x1: mapped[i - 1].x,
15406
+ y1: mapped[i - 1].y,
15407
+ x2: mapped[i].x,
15408
+ y2: mapped[i].y,
15409
+ color,
15410
+ lineWidth: 1.5
15411
+ });
15412
+ }
15413
+ if (mapped.length > 0) {
15414
+ const last = mapped[mapped.length - 1];
15415
+ out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
15416
+ }
15417
+ if (series.label) {
15418
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
15419
+ }
15420
+ });
15421
+ if (panel.yLabel) {
15422
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
15423
+ }
15424
+ if (panel.xLabel) {
15425
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
15426
+ }
15427
+ return out;
15428
+ }
15429
+ var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
15279
15430
  var init_LearningCanvas = __esm({
15280
15431
  "components/learning/atoms/LearningCanvas.tsx"() {
15281
15432
  "use client";
15282
15433
  init_cn();
15283
15434
  init_useEventBus();
15435
+ DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
15436
+ TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
15284
15437
  LearningCanvas = ({
15285
15438
  className,
15286
15439
  width = 600,
15287
15440
  height = 400,
15288
15441
  backgroundColor,
15289
15442
  shapes = [],
15443
+ readouts,
15444
+ traces,
15290
15445
  interactive = false,
15291
15446
  animate = false,
15292
15447
  onShapeClick,
@@ -15312,6 +15467,12 @@ var init_LearningCanvas = __esm({
15312
15467
  }
15313
15468
  return -1;
15314
15469
  }, [shapes]);
15470
+ const derivedShapes = React94.useMemo(() => {
15471
+ if (!traces?.length && !readouts?.length) return shapes;
15472
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
15473
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
15474
+ return [...shapes, ...traceOut, ...readoutOut];
15475
+ }, [shapes, traces, readouts, width, height]);
15315
15476
  const draw = React94.useCallback(() => {
15316
15477
  const canvas = canvasRef.current;
15317
15478
  if (!canvas) return;
@@ -15328,13 +15489,13 @@ var init_LearningCanvas = __esm({
15328
15489
  ctx.fillStyle = backgroundColor;
15329
15490
  ctx.fillRect(0, 0, width, height);
15330
15491
  }
15331
- for (const shape of shapes) {
15332
- if (shape.type !== "text") drawShape(ctx, shape, width, height);
15492
+ for (const shape of derivedShapes) {
15493
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
15333
15494
  }
15334
- for (const shape of shapes) {
15335
- if (shape.type === "text") drawShape(ctx, shape, width, height);
15495
+ for (const shape of derivedShapes) {
15496
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
15336
15497
  }
15337
- }, [width, height, backgroundColor, shapes]);
15498
+ }, [width, height, backgroundColor, derivedShapes]);
15338
15499
  React94.useEffect(() => {
15339
15500
  draw();
15340
15501
  }, [draw]);
@@ -16884,7 +17045,363 @@ var init_ComponentPatterns = __esm({
16884
17045
  AlertPattern.displayName = "AlertPattern";
16885
17046
  }
16886
17047
  });
16887
- var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, AlgorithmCanvas;
17048
+ function layoutCircle(nodes, width, height) {
17049
+ const cx = width / 2;
17050
+ const cy = height / 2;
17051
+ const radius = Math.max(10, Math.min(cx, cy) - 40);
17052
+ const positions = /* @__PURE__ */ new Map();
17053
+ const n = nodes.length;
17054
+ nodes.forEach((node, i) => {
17055
+ const angle = 2 * Math.PI * i / Math.max(n, 1) - Math.PI / 2;
17056
+ positions.set(node.id, { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) });
17057
+ });
17058
+ return positions;
17059
+ }
17060
+ function layoutTree2(nodes, edges, root, width, height) {
17061
+ const nodeIds = nodes.map((n) => n.id);
17062
+ const idSet = new Set(nodeIds);
17063
+ const childrenOf = /* @__PURE__ */ new Map();
17064
+ const hasIncoming = /* @__PURE__ */ new Set();
17065
+ for (const e of edges) {
17066
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
17067
+ const list = childrenOf.get(e.from) ?? [];
17068
+ list.push(e.to);
17069
+ childrenOf.set(e.from, list);
17070
+ hasIncoming.add(e.to);
17071
+ }
17072
+ const depth = /* @__PURE__ */ new Map();
17073
+ const treeChildren = /* @__PURE__ */ new Map();
17074
+ const visited = /* @__PURE__ */ new Set();
17075
+ const bfsFrom = (start) => {
17076
+ if (visited.has(start)) return;
17077
+ visited.add(start);
17078
+ depth.set(start, 0);
17079
+ const queue = [start];
17080
+ while (queue.length > 0) {
17081
+ const u = queue.shift();
17082
+ for (const v of childrenOf.get(u) ?? []) {
17083
+ if (visited.has(v)) continue;
17084
+ visited.add(v);
17085
+ depth.set(v, (depth.get(u) ?? 0) + 1);
17086
+ const list = treeChildren.get(u) ?? [];
17087
+ list.push(v);
17088
+ treeChildren.set(u, list);
17089
+ queue.push(v);
17090
+ }
17091
+ }
17092
+ };
17093
+ const primaryRoot = root && idSet.has(root) ? root : nodeIds.find((id) => !hasIncoming.has(id)) ?? nodeIds[0];
17094
+ const rootsOrder = [];
17095
+ if (primaryRoot !== void 0) {
17096
+ bfsFrom(primaryRoot);
17097
+ rootsOrder.push(primaryRoot);
17098
+ }
17099
+ for (const id of nodeIds) {
17100
+ if (!visited.has(id)) {
17101
+ bfsFrom(id);
17102
+ rootsOrder.push(id);
17103
+ }
17104
+ }
17105
+ let leafCounter = 0;
17106
+ const xSlot = /* @__PURE__ */ new Map();
17107
+ const assignXSlot = (u) => {
17108
+ const children = treeChildren.get(u) ?? [];
17109
+ if (children.length === 0) {
17110
+ const slot = leafCounter++;
17111
+ xSlot.set(u, slot);
17112
+ return slot;
17113
+ }
17114
+ const childSlots = children.map(assignXSlot);
17115
+ const avg = childSlots.reduce((a, b) => a + b, 0) / childSlots.length;
17116
+ xSlot.set(u, avg);
17117
+ return avg;
17118
+ };
17119
+ for (const r2 of rootsOrder) assignXSlot(r2);
17120
+ let maxDepth = 0;
17121
+ for (const d of depth.values()) maxDepth = Math.max(maxDepth, d);
17122
+ const colWidth = width / Math.max(1, leafCounter);
17123
+ const rowHeight = height / (maxDepth + 1);
17124
+ const positions = /* @__PURE__ */ new Map();
17125
+ for (const id of nodeIds) {
17126
+ const slot = xSlot.get(id) ?? 0;
17127
+ const d = depth.get(id) ?? 0;
17128
+ positions.set(id, { x: slot * colWidth + colWidth / 2, y: d * rowHeight + rowHeight / 2 });
17129
+ }
17130
+ return positions;
17131
+ }
17132
+ function layoutLayered(nodes, edges, width, height) {
17133
+ const nodeIds = nodes.map((n) => n.id);
17134
+ const idSet = new Set(nodeIds);
17135
+ const adj = /* @__PURE__ */ new Map();
17136
+ const remainingIndegree = /* @__PURE__ */ new Map();
17137
+ for (const id of nodeIds) remainingIndegree.set(id, 0);
17138
+ for (const e of edges) {
17139
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
17140
+ const list = adj.get(e.from) ?? [];
17141
+ list.push(e.to);
17142
+ adj.set(e.from, list);
17143
+ remainingIndegree.set(e.to, (remainingIndegree.get(e.to) ?? 0) + 1);
17144
+ }
17145
+ const layer = /* @__PURE__ */ new Map();
17146
+ const dequeued = /* @__PURE__ */ new Set();
17147
+ const queue = [];
17148
+ for (const id of nodeIds) {
17149
+ if ((remainingIndegree.get(id) ?? 0) === 0) {
17150
+ layer.set(id, 0);
17151
+ queue.push(id);
17152
+ }
17153
+ }
17154
+ while (queue.length > 0) {
17155
+ const u = queue.shift();
17156
+ dequeued.add(u);
17157
+ for (const v of adj.get(u) ?? []) {
17158
+ const candidate = (layer.get(u) ?? 0) + 1;
17159
+ layer.set(v, Math.max(layer.get(v) ?? 0, candidate));
17160
+ remainingIndegree.set(v, (remainingIndegree.get(v) ?? 0) - 1);
17161
+ if ((remainingIndegree.get(v) ?? 0) === 0 && !dequeued.has(v)) {
17162
+ queue.push(v);
17163
+ }
17164
+ }
17165
+ }
17166
+ let baseMaxLayer = 0;
17167
+ for (const id of nodeIds) {
17168
+ if (dequeued.has(id)) baseMaxLayer = Math.max(baseMaxLayer, layer.get(id) ?? 0);
17169
+ }
17170
+ const cycleLayer = baseMaxLayer + 1;
17171
+ let maxLayer = baseMaxLayer;
17172
+ for (const id of nodeIds) {
17173
+ if (!dequeued.has(id)) {
17174
+ layer.set(id, cycleLayer);
17175
+ maxLayer = cycleLayer;
17176
+ }
17177
+ }
17178
+ const colWidth = width / Math.max(1, maxLayer + 1);
17179
+ const byLayer = /* @__PURE__ */ new Map();
17180
+ for (const id of nodeIds) {
17181
+ const l = layer.get(id) ?? 0;
17182
+ const list = byLayer.get(l) ?? [];
17183
+ list.push(id);
17184
+ byLayer.set(l, list);
17185
+ }
17186
+ const positions = /* @__PURE__ */ new Map();
17187
+ for (const [l, ids] of byLayer) {
17188
+ const rowHeight = height / ids.length;
17189
+ ids.forEach((id, i) => {
17190
+ positions.set(id, { x: l * colWidth + colWidth / 2, y: i * rowHeight + rowHeight / 2 });
17191
+ });
17192
+ }
17193
+ return positions;
17194
+ }
17195
+ function computePositions(nodes, edges, layout, root, width, height) {
17196
+ switch (layout) {
17197
+ case "circle":
17198
+ return layoutCircle(nodes, width, height);
17199
+ case "tree":
17200
+ return layoutTree2(nodes, edges, root, width, height);
17201
+ case "layered":
17202
+ return layoutLayered(nodes, edges, width, height);
17203
+ case "manual":
17204
+ default: {
17205
+ const positions = /* @__PURE__ */ new Map();
17206
+ for (const n of nodes) positions.set(n.id, { x: n.x ?? 0, y: n.y ?? 0 });
17207
+ return positions;
17208
+ }
17209
+ }
17210
+ }
17211
+ var NODE_STATE_COLOR, EDGE_STATE_COLOR, DEFAULT_NODE_RADIUS, AlgoGraphCanvas;
17212
+ var init_AlgoGraphCanvas = __esm({
17213
+ "components/learning/molecules/AlgoGraphCanvas.tsx"() {
17214
+ "use client";
17215
+ init_atoms();
17216
+ init_Stack();
17217
+ init_LearningCanvas();
17218
+ NODE_STATE_COLOR = {
17219
+ unvisited: "#cbd5e1",
17220
+ frontier: "#f59e0b",
17221
+ current: "#ef4444",
17222
+ visited: "#22c55e",
17223
+ goal: "#8b5cf6",
17224
+ path: "#0ea5e9"
17225
+ };
17226
+ EDGE_STATE_COLOR = {
17227
+ default: "#9ca3af",
17228
+ tree: "#16a34a",
17229
+ relaxed: "#f59e0b",
17230
+ candidate: "#38bdf8",
17231
+ path: "#dc2626"
17232
+ };
17233
+ DEFAULT_NODE_RADIUS = 18;
17234
+ AlgoGraphCanvas = ({
17235
+ className,
17236
+ width = 600,
17237
+ height = 400,
17238
+ title,
17239
+ backgroundColor,
17240
+ nodes = [],
17241
+ edges = [],
17242
+ layout = "manual",
17243
+ root,
17244
+ shapes = [],
17245
+ interactive = false,
17246
+ animate = false,
17247
+ onShapeClick,
17248
+ onNodeClick,
17249
+ isLoading,
17250
+ error
17251
+ }) => {
17252
+ const nodeById = React94.useMemo(() => {
17253
+ const m = /* @__PURE__ */ new Map();
17254
+ for (const n of nodes) m.set(n.id, n);
17255
+ return m;
17256
+ }, [nodes]);
17257
+ const nodeIndexById = React94.useMemo(() => {
17258
+ const m = /* @__PURE__ */ new Map();
17259
+ nodes.forEach((n, i) => m.set(n.id, i));
17260
+ return m;
17261
+ }, [nodes]);
17262
+ const derivedShapes = React94.useMemo(() => {
17263
+ const out = [];
17264
+ const positions = computePositions(nodes, edges, layout, root, width, height);
17265
+ const edgeGeoms = [];
17266
+ for (const e of edges) {
17267
+ const a = nodeById.get(e.from);
17268
+ const b = nodeById.get(e.to);
17269
+ const posA = positions.get(e.from);
17270
+ const posB = positions.get(e.to);
17271
+ if (!a || !b || !posA || !posB) continue;
17272
+ const rA = a.radius ?? DEFAULT_NODE_RADIUS;
17273
+ const rB = b.radius ?? DEFAULT_NODE_RADIUS;
17274
+ const dx = posB.x - posA.x;
17275
+ const dy = posB.y - posA.y;
17276
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
17277
+ const ux = dx / dist;
17278
+ const uy = dy / dist;
17279
+ edgeGeoms.push({
17280
+ directed: e.directed ?? false,
17281
+ x1: posA.x + ux * rA,
17282
+ y1: posA.y + uy * rA,
17283
+ x2: posB.x - ux * rB,
17284
+ y2: posB.y - uy * rB,
17285
+ color: e.color ?? EDGE_STATE_COLOR[e.state ?? "default"],
17286
+ label: e.label ?? (e.weight != null ? String(e.weight) : void 0)
17287
+ });
17288
+ }
17289
+ for (const g of edgeGeoms) {
17290
+ out.push({
17291
+ type: g.directed ? "arrow" : "line",
17292
+ x1: g.x1,
17293
+ y1: g.y1,
17294
+ x2: g.x2,
17295
+ y2: g.y2,
17296
+ color: g.color,
17297
+ lineWidth: 2
17298
+ });
17299
+ }
17300
+ for (const g of edgeGeoms) {
17301
+ if (g.label === void 0) continue;
17302
+ const midX = (g.x1 + g.x2) / 2;
17303
+ const midY = (g.y1 + g.y2) / 2;
17304
+ const dx = g.x2 - g.x1;
17305
+ const dy = g.y2 - g.y1;
17306
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
17307
+ const perpX = -(dy / dist);
17308
+ const perpY = dx / dist;
17309
+ out.push({
17310
+ type: "text",
17311
+ x: midX + perpX * 10,
17312
+ y: midY + perpY * 10,
17313
+ text: g.label,
17314
+ fontSize: 11,
17315
+ align: "center",
17316
+ color: "#374151"
17317
+ });
17318
+ }
17319
+ const nodeGeoms = [];
17320
+ for (const n of nodes) {
17321
+ const pos = positions.get(n.id);
17322
+ if (!pos) continue;
17323
+ nodeGeoms.push({
17324
+ id: n.id,
17325
+ x: pos.x,
17326
+ y: pos.y,
17327
+ radius: n.radius ?? DEFAULT_NODE_RADIUS,
17328
+ color: n.color ?? NODE_STATE_COLOR[n.state ?? "unvisited"],
17329
+ label: n.label,
17330
+ badge: n.badge
17331
+ });
17332
+ }
17333
+ for (const g of nodeGeoms) {
17334
+ out.push({ type: "circle", id: g.id, x: g.x, y: g.y, radius: g.radius, color: g.color, fill: `${g.color}33` });
17335
+ }
17336
+ const badgeGeoms = [];
17337
+ for (const g of nodeGeoms) {
17338
+ if (!g.badge) continue;
17339
+ const w = Math.min(42, Math.max(18, g.badge.text.length * 6 + 10));
17340
+ badgeGeoms.push({
17341
+ cx: g.x + g.radius * 0.75,
17342
+ cy: g.y - g.radius * 0.75,
17343
+ w,
17344
+ h: 14,
17345
+ // Borderless pill: same color drives both stroke and fill.
17346
+ color: g.badge.color ?? "#1e293b",
17347
+ text: g.badge.text
17348
+ });
17349
+ }
17350
+ for (const b of badgeGeoms) {
17351
+ 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 });
17352
+ }
17353
+ for (const b of badgeGeoms) {
17354
+ out.push({ type: "text", x: b.cx, y: b.cy, text: b.text, fontSize: 9, align: "center", color: "#ffffff" });
17355
+ }
17356
+ for (const g of nodeGeoms) {
17357
+ if (g.label === void 0) continue;
17358
+ out.push({
17359
+ type: "text",
17360
+ x: g.x,
17361
+ y: g.y + g.radius + 14,
17362
+ text: g.label,
17363
+ fontSize: 12,
17364
+ align: "center",
17365
+ color: "#111827"
17366
+ });
17367
+ }
17368
+ out.push(...shapes);
17369
+ return out;
17370
+ }, [nodes, edges, layout, root, width, height, nodeById, shapes]);
17371
+ const handleShapeClick = React94.useCallback(
17372
+ (payload) => {
17373
+ if (payload.type === "circle" && payload.id) {
17374
+ const node = nodeById.get(payload.id);
17375
+ const idx = nodeIndexById.get(payload.id);
17376
+ if (node && idx !== void 0) {
17377
+ onNodeClick?.({ id: node.id, label: node.label, index: idx });
17378
+ }
17379
+ }
17380
+ onShapeClick?.(payload);
17381
+ },
17382
+ [nodeById, nodeIndexById, onNodeClick, onShapeClick]
17383
+ );
17384
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
17385
+ title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
17386
+ /* @__PURE__ */ jsxRuntime.jsx(
17387
+ LearningCanvas,
17388
+ {
17389
+ width,
17390
+ height,
17391
+ backgroundColor,
17392
+ shapes: derivedShapes,
17393
+ interactive,
17394
+ animate,
17395
+ onShapeClick: onShapeClick || onNodeClick ? handleShapeClick : void 0,
17396
+ isLoading,
17397
+ error
17398
+ }
17399
+ )
17400
+ ] }) });
17401
+ };
17402
+ }
17403
+ });
17404
+ 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;
16888
17405
  var init_AlgorithmCanvas = __esm({
16889
17406
  "components/learning/molecules/AlgorithmCanvas.tsx"() {
16890
17407
  "use client";
@@ -16896,6 +17413,43 @@ var init_AlgorithmCanvas = __esm({
16896
17413
  DEFAULT_POINTER_COLOR = "#dc2626";
16897
17414
  POINTER_BAND = 34;
16898
17415
  TOP_PAD = 26;
17416
+ PANEL_FAMILY_ORDER = ["bars", "slots", "cells", "buckets", "frames"];
17417
+ RANGE_COLOR_DEFAULT = "#3b82f6";
17418
+ RANGE_FILL_OPACITY = 0.15;
17419
+ BRACKET_TOP_OFFSET = 16;
17420
+ BRACKET_ROW_H = 14;
17421
+ BRACKET_TICK_H = 6;
17422
+ BRACKET_LABEL_OFFSET = 6;
17423
+ SLOT_EMPTY_FILL = "#f1f5f9";
17424
+ SLOT_EMPTY_STROKE = "#cbd5e1";
17425
+ SLOT_FILLED_STROKE = "#9ca3af";
17426
+ SLOT_HIGHLIGHT_DEFAULT = "#f59e0b";
17427
+ SLOT_VALUE_TEXT_COLOR = "#ffffff";
17428
+ FRAME_ACTIVE_COLOR = "#3b82f6";
17429
+ FRAME_RETURNING_COLOR = "#f59e0b";
17430
+ FRAME_DONE_COLOR = "#94a3b8";
17431
+ FRAME_LABEL_COLOR = "#ffffff";
17432
+ FRAME_DETAIL_COLOR = "#e2e8f0";
17433
+ FRAME_TWO_LINE_MIN_H = 22;
17434
+ BUCKET_INDEX_FILL = "#e2e8f0";
17435
+ BUCKET_INDEX_STROKE = "#9ca3af";
17436
+ BUCKET_INDEX_TEXT = "#374151";
17437
+ BUCKET_ENTRY_TEXT = "#ffffff";
17438
+ BUCKET_ENTRY_DEFAULT = "#3b82f6";
17439
+ BUCKET_ENTRY_HIGHLIGHT = "#f59e0b";
17440
+ BUCKET_ENTRY_PROBING = "#38bdf8";
17441
+ BUCKET_ENTRY_MIN_W = 24;
17442
+ BUCKET_ENTRY_MAX_W = 64;
17443
+ AXIS_LABEL_COLOR = "#6b7280";
17444
+ AXIS_LABEL_FONT_SIZE = 10;
17445
+ CORNER_TEXT_COLOR = "#111827";
17446
+ CORNER_FONT_SIZE = 7;
17447
+ CORNER_MIN_CELL = 28;
17448
+ CORNER_INSET_X = 3;
17449
+ CORNER_INSET_Y = 6;
17450
+ AUX_PRIMARY_RATIO = 0.6;
17451
+ AUX_LABEL_BAND = 18;
17452
+ AUX_BASELINE_PAD = 8;
16899
17453
  AlgorithmCanvas = ({
16900
17454
  className,
16901
17455
  width = 600,
@@ -16905,6 +17459,14 @@ var init_AlgorithmCanvas = __esm({
16905
17459
  bars = [],
16906
17460
  cells = [],
16907
17461
  pointers = [],
17462
+ ranges = [],
17463
+ slots = [],
17464
+ slotOrientation = "horizontal",
17465
+ frames = [],
17466
+ buckets = [],
17467
+ auxBars = [],
17468
+ rowLabels = [],
17469
+ colLabels = [],
16908
17470
  shapes = [],
16909
17471
  interactive = false,
16910
17472
  animate = false,
@@ -16914,12 +17476,35 @@ var init_AlgorithmCanvas = __esm({
16914
17476
  }) => {
16915
17477
  const derivedShapes = React94.useMemo(() => {
16916
17478
  const out = [];
17479
+ const presence = {
17480
+ bars: bars.length > 0,
17481
+ slots: slots.length > 0,
17482
+ cells: cells.length > 0,
17483
+ buckets: buckets.length > 0,
17484
+ frames: frames.length > 0
17485
+ };
17486
+ const panelCount = PANEL_FAMILY_ORDER.filter((f3) => presence[f3]).length;
17487
+ const panelHeight = height / Math.max(1, panelCount);
17488
+ const panelY = { bars: 0, slots: 0, cells: 0, buckets: 0, frames: 0 };
17489
+ let compactIndex = 0;
17490
+ PANEL_FAMILY_ORDER.forEach((f3) => {
17491
+ if (presence[f3]) {
17492
+ panelY[f3] = compactIndex * panelHeight;
17493
+ compactIndex += 1;
17494
+ }
17495
+ });
16917
17496
  if (bars.length > 0) {
17497
+ const panelYBars = panelY.bars;
16918
17498
  const slot = width / bars.length;
16919
17499
  const barW = slot * 0.8;
16920
17500
  const gap = slot * 0.1;
16921
- const baseline = height - POINTER_BAND;
16922
- const usableH = baseline - TOP_PAD;
17501
+ const bracketRanges = ranges.filter((r2) => r2.kind === "bracket");
17502
+ const bracketCount = bracketRanges.length;
17503
+ const bracketHeadroom = bracketCount > 0 ? BRACKET_TOP_OFFSET + bracketCount * BRACKET_ROW_H : 0;
17504
+ const hasAux = auxBars.length > 0;
17505
+ const primaryH = hasAux ? panelHeight * AUX_PRIMARY_RATIO : panelHeight;
17506
+ const baseline = panelYBars + primaryH - POINTER_BAND;
17507
+ const usableH = baseline - (panelYBars + TOP_PAD + bracketHeadroom);
16923
17508
  const maxV = Math.max(1, ...bars.map((b) => Number.isFinite(b.value) ? b.value : 0));
16924
17509
  bars.forEach((bar, i) => {
16925
17510
  const v = Number.isFinite(bar.value) ? bar.value : 0;
@@ -16949,6 +17534,89 @@ var init_AlgorithmCanvas = __esm({
16949
17534
  });
16950
17535
  }
16951
17536
  });
17537
+ ranges.forEach((r2) => {
17538
+ const kind = r2.kind ?? "fill";
17539
+ if (kind !== "fill") return;
17540
+ const color = r2.color ?? RANGE_COLOR_DEFAULT;
17541
+ out.push({
17542
+ type: "rect",
17543
+ x: r2.from * slot,
17544
+ y: panelYBars,
17545
+ width: (r2.to - r2.from + 1) * slot,
17546
+ height: primaryH,
17547
+ color,
17548
+ fill: color,
17549
+ opacity: RANGE_FILL_OPACITY
17550
+ });
17551
+ if (r2.label) {
17552
+ out.push({
17553
+ type: "text",
17554
+ x: r2.from * slot + 4,
17555
+ // Sits below the bracket block (if any) so fill and bracket labels never collide.
17556
+ y: panelYBars + 10 + bracketHeadroom,
17557
+ text: r2.label,
17558
+ color,
17559
+ fontSize: 10,
17560
+ align: "left"
17561
+ });
17562
+ }
17563
+ });
17564
+ bracketRanges.forEach((r2, i) => {
17565
+ const bracketY = panelYBars + BRACKET_TOP_OFFSET + i * BRACKET_ROW_H;
17566
+ const x1 = r2.from * slot + slot * 0.1;
17567
+ const x2 = (r2.to + 1) * slot - slot * 0.1;
17568
+ const color = r2.color ?? RANGE_COLOR_DEFAULT;
17569
+ out.push({ type: "line", x1, y1: bracketY, x2, y2: bracketY, color, lineWidth: 2 });
17570
+ out.push({ type: "line", x1, y1: bracketY, x2: x1, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
17571
+ out.push({ type: "line", x1: x2, y1: bracketY, x2, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
17572
+ if (r2.label) {
17573
+ out.push({
17574
+ type: "text",
17575
+ x: (x1 + x2) / 2,
17576
+ y: bracketY - BRACKET_LABEL_OFFSET,
17577
+ text: r2.label,
17578
+ color,
17579
+ fontSize: 10,
17580
+ align: "center"
17581
+ });
17582
+ }
17583
+ });
17584
+ if (hasAux) {
17585
+ const auxH = panelHeight - primaryH;
17586
+ const slot2 = width / auxBars.length;
17587
+ const auxBaseline = panelYBars + primaryH + auxH - AUX_BASELINE_PAD;
17588
+ const auxUsableH = auxBaseline - (panelYBars + primaryH + AUX_LABEL_BAND);
17589
+ const maxAuxV = Math.max(1, ...auxBars.map((b) => Number.isFinite(b.value) ? b.value : 0));
17590
+ auxBars.forEach((bar, i) => {
17591
+ const v = Number.isFinite(bar.value) ? bar.value : 0;
17592
+ const bh = Math.max(0, v / maxAuxV * auxUsableH);
17593
+ const x = i * slot2 + slot2 * 0.1;
17594
+ const w = slot2 * 0.8;
17595
+ const color = bar.color ?? DEFAULT_BAR_COLOR;
17596
+ out.push({
17597
+ type: "rect",
17598
+ id: `auxbar-${i}`,
17599
+ x,
17600
+ y: auxBaseline - bh,
17601
+ width: w,
17602
+ height: bh,
17603
+ color,
17604
+ fill: color
17605
+ });
17606
+ const label = bar.label ?? (auxBars.length <= 24 ? String(v) : void 0);
17607
+ if (label) {
17608
+ out.push({
17609
+ type: "text",
17610
+ x: x + w / 2,
17611
+ y: auxBaseline - bh - 8,
17612
+ text: label,
17613
+ color: "#374151",
17614
+ fontSize: 11,
17615
+ align: "center"
17616
+ });
17617
+ }
17618
+ });
17619
+ }
16952
17620
  pointers.forEach((p) => {
16953
17621
  if (p.index < 0 || p.index >= bars.length) return;
16954
17622
  const cx = p.index * slot + slot / 2;
@@ -16956,7 +17624,7 @@ var init_AlgorithmCanvas = __esm({
16956
17624
  out.push({
16957
17625
  type: "arrow",
16958
17626
  x1: cx,
16959
- y1: height - 6,
17627
+ y1: panelYBars + primaryH - 18,
16960
17628
  x2: cx,
16961
17629
  y2: baseline + 4,
16962
17630
  color,
@@ -16966,7 +17634,7 @@ var init_AlgorithmCanvas = __esm({
16966
17634
  out.push({
16967
17635
  type: "text",
16968
17636
  x: cx,
16969
- y: height - 22,
17637
+ y: panelYBars + primaryH - 8,
16970
17638
  text: p.label,
16971
17639
  color,
16972
17640
  fontSize: 11,
@@ -16975,14 +17643,111 @@ var init_AlgorithmCanvas = __esm({
16975
17643
  }
16976
17644
  });
16977
17645
  }
17646
+ if (slots.length > 0) {
17647
+ const panelYSlots = panelY.slots;
17648
+ const n = slots.length;
17649
+ const vertical = slotOrientation === "vertical";
17650
+ const vBoxH = panelHeight / n;
17651
+ const vBoxW = Math.min(width * 0.5, 120);
17652
+ const vBoxX = (width - vBoxW) / 2;
17653
+ const hCellW = width / n;
17654
+ const hBoxW = hCellW * 0.82;
17655
+ const hBoxH = Math.min(panelHeight * 0.6, 48);
17656
+ const hBoxY = panelYSlots + (panelHeight - hBoxH) / 2;
17657
+ 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 };
17658
+ slots.forEach((s, i) => {
17659
+ const box = slotBox(i);
17660
+ const state = s.state ?? "filled";
17661
+ const fill = state === "empty" ? SLOT_EMPTY_FILL : state === "highlight" ? s.color ?? SLOT_HIGHLIGHT_DEFAULT : s.color ?? DEFAULT_BAR_COLOR;
17662
+ const stroke = state === "empty" ? SLOT_EMPTY_STROKE : SLOT_FILLED_STROKE;
17663
+ out.push({
17664
+ type: "rect",
17665
+ id: `slot-${i}`,
17666
+ x: box.x,
17667
+ y: box.y,
17668
+ width: box.width,
17669
+ height: box.height,
17670
+ color: stroke,
17671
+ fill
17672
+ });
17673
+ if (s.value != null && state !== "empty") {
17674
+ out.push({
17675
+ type: "text",
17676
+ x: box.x + box.width / 2,
17677
+ y: box.y + box.height / 2,
17678
+ text: String(s.value),
17679
+ color: SLOT_VALUE_TEXT_COLOR,
17680
+ fontSize: 12,
17681
+ align: "center"
17682
+ });
17683
+ }
17684
+ });
17685
+ if (bars.length === 0) {
17686
+ pointers.forEach((p) => {
17687
+ if (p.index < 0 || p.index >= slots.length) return;
17688
+ const box = slotBox(p.index);
17689
+ const color = p.color ?? DEFAULT_POINTER_COLOR;
17690
+ if (vertical) {
17691
+ const cy = box.y + box.height / 2;
17692
+ out.push({
17693
+ type: "arrow",
17694
+ x1: box.x + box.width + 34,
17695
+ y1: cy,
17696
+ x2: box.x + box.width + 4,
17697
+ y2: cy,
17698
+ color,
17699
+ lineWidth: 2
17700
+ });
17701
+ if (p.label) {
17702
+ out.push({
17703
+ type: "text",
17704
+ x: box.x + box.width + 38,
17705
+ y: cy,
17706
+ text: p.label,
17707
+ color,
17708
+ fontSize: 11,
17709
+ align: "left"
17710
+ });
17711
+ }
17712
+ } else {
17713
+ const cx = box.x + box.width / 2;
17714
+ out.push({
17715
+ type: "arrow",
17716
+ x1: cx,
17717
+ y1: panelYSlots + panelHeight - 18,
17718
+ x2: cx,
17719
+ y2: box.y + box.height + 4,
17720
+ color,
17721
+ lineWidth: 2
17722
+ });
17723
+ if (p.label) {
17724
+ out.push({
17725
+ type: "text",
17726
+ x: cx,
17727
+ y: panelYSlots + panelHeight - 8,
17728
+ text: p.label,
17729
+ color,
17730
+ fontSize: 11,
17731
+ align: "center"
17732
+ });
17733
+ }
17734
+ }
17735
+ });
17736
+ }
17737
+ }
16978
17738
  if (cells.length > 0) {
17739
+ const panelYCells = panelY.cells;
16979
17740
  const maxCol = Math.max(0, ...cells.map((c) => c.col)) + 1;
16980
17741
  const maxRow = Math.max(0, ...cells.map((c) => c.row)) + 1;
16981
- const cw = width / maxCol;
16982
- const ch = height / maxRow;
17742
+ const colLabelH = colLabels.length > 0 ? 16 : 0;
17743
+ const rowLabelW = rowLabels.length > 0 ? 20 : 0;
17744
+ const gridX0 = rowLabelW;
17745
+ const gridY0 = panelYCells + colLabelH;
17746
+ const cw = (width - rowLabelW) / maxCol;
17747
+ const ch = (panelHeight - colLabelH) / maxRow;
16983
17748
  cells.forEach((c, i) => {
16984
- const x = c.col * cw;
16985
- const y = c.row * ch;
17749
+ const x = gridX0 + c.col * cw;
17750
+ const y = gridY0 + c.row * ch;
16986
17751
  const color = c.color ?? DEFAULT_CELL_COLOR;
16987
17752
  out.push({
16988
17753
  type: "rect",
@@ -17006,11 +17771,207 @@ var init_AlgorithmCanvas = __esm({
17006
17771
  align: "center"
17007
17772
  });
17008
17773
  }
17774
+ if (c.corner && cw >= CORNER_MIN_CELL && ch >= CORNER_MIN_CELL) {
17775
+ const { tl, tr, bl, br } = c.corner;
17776
+ if (tl) {
17777
+ out.push({
17778
+ type: "text",
17779
+ x: x + CORNER_INSET_X,
17780
+ y: y + CORNER_INSET_Y,
17781
+ text: tl,
17782
+ color: CORNER_TEXT_COLOR,
17783
+ fontSize: CORNER_FONT_SIZE,
17784
+ align: "left"
17785
+ });
17786
+ }
17787
+ if (tr) {
17788
+ out.push({
17789
+ type: "text",
17790
+ x: x + cw - CORNER_INSET_X,
17791
+ y: y + CORNER_INSET_Y,
17792
+ text: tr,
17793
+ color: CORNER_TEXT_COLOR,
17794
+ fontSize: CORNER_FONT_SIZE,
17795
+ align: "right"
17796
+ });
17797
+ }
17798
+ if (bl) {
17799
+ out.push({
17800
+ type: "text",
17801
+ x: x + CORNER_INSET_X,
17802
+ y: y + ch - CORNER_INSET_Y,
17803
+ text: bl,
17804
+ color: CORNER_TEXT_COLOR,
17805
+ fontSize: CORNER_FONT_SIZE,
17806
+ align: "left"
17807
+ });
17808
+ }
17809
+ if (br) {
17810
+ out.push({
17811
+ type: "text",
17812
+ x: x + cw - CORNER_INSET_X,
17813
+ y: y + ch - CORNER_INSET_Y,
17814
+ text: br,
17815
+ color: CORNER_TEXT_COLOR,
17816
+ fontSize: CORNER_FONT_SIZE,
17817
+ align: "right"
17818
+ });
17819
+ }
17820
+ }
17821
+ });
17822
+ colLabels.forEach((l) => {
17823
+ out.push({
17824
+ type: "text",
17825
+ x: gridX0 + l.index * cw + cw / 2,
17826
+ y: panelYCells + colLabelH / 2,
17827
+ text: l.text,
17828
+ color: l.color ?? AXIS_LABEL_COLOR,
17829
+ fontSize: AXIS_LABEL_FONT_SIZE,
17830
+ align: "center"
17831
+ });
17832
+ });
17833
+ rowLabels.forEach((l) => {
17834
+ out.push({
17835
+ type: "text",
17836
+ x: rowLabelW - 6,
17837
+ y: gridY0 + l.index * ch + ch / 2,
17838
+ text: l.text,
17839
+ color: l.color ?? AXIS_LABEL_COLOR,
17840
+ fontSize: AXIS_LABEL_FONT_SIZE,
17841
+ align: "right"
17842
+ });
17843
+ });
17844
+ }
17845
+ if (buckets.length > 0) {
17846
+ const panelYBuckets = panelY.buckets;
17847
+ const bucketCount = Math.max(0, ...buckets.map((b) => b.index)) + 1;
17848
+ const rowH = panelHeight / bucketCount;
17849
+ const indexColW = Math.min(width * 0.12, 40);
17850
+ const maxChainLen = Math.max(1, ...buckets.map((b) => b.entries.length));
17851
+ const entryW = Math.min(BUCKET_ENTRY_MAX_W, Math.max(BUCKET_ENTRY_MIN_W, (width - indexColW - 8) / maxChainLen));
17852
+ const maxVisible = Math.floor((width - indexColW - 4) / entryW);
17853
+ buckets.forEach((b) => {
17854
+ const rowY = panelYBuckets + b.index * rowH;
17855
+ out.push({
17856
+ type: "rect",
17857
+ id: `bucket-index-${b.index}`,
17858
+ x: 2,
17859
+ y: rowY + 2,
17860
+ width: indexColW - 4,
17861
+ height: rowH - 4,
17862
+ color: BUCKET_INDEX_STROKE,
17863
+ fill: BUCKET_INDEX_FILL
17864
+ });
17865
+ out.push({
17866
+ type: "text",
17867
+ x: 2 + (indexColW - 4) / 2,
17868
+ y: rowY + rowH / 2,
17869
+ text: String(b.index),
17870
+ color: BUCKET_INDEX_TEXT,
17871
+ fontSize: 10,
17872
+ align: "center"
17873
+ });
17874
+ const overflow = b.entries.length > maxVisible;
17875
+ const visibleCount = overflow ? Math.max(0, maxVisible - 1) : b.entries.length;
17876
+ for (let j = 0; j < visibleCount; j++) {
17877
+ const entry = b.entries[j];
17878
+ const ex = indexColW + 4 + j * entryW;
17879
+ const state = entry.state ?? "default";
17880
+ const fill = state === "highlight" ? entry.color ?? BUCKET_ENTRY_HIGHLIGHT : state === "probing" ? entry.color ?? BUCKET_ENTRY_PROBING : entry.color ?? BUCKET_ENTRY_DEFAULT;
17881
+ out.push({
17882
+ type: "rect",
17883
+ id: `bucket-${b.index}-${j}`,
17884
+ x: ex,
17885
+ y: rowY + 2,
17886
+ width: entryW - 2,
17887
+ height: rowH - 4,
17888
+ color: fill,
17889
+ fill
17890
+ });
17891
+ if (entryW >= 20 && rowH >= 16) {
17892
+ out.push({
17893
+ type: "text",
17894
+ x: ex + (entryW - 2) / 2,
17895
+ y: rowY + rowH / 2,
17896
+ text: entry.label,
17897
+ color: BUCKET_ENTRY_TEXT,
17898
+ fontSize: 10,
17899
+ align: "center"
17900
+ });
17901
+ }
17902
+ }
17903
+ if (overflow) {
17904
+ const ex = indexColW + 4 + visibleCount * entryW;
17905
+ out.push({
17906
+ type: "rect",
17907
+ id: `bucket-${b.index}-overflow`,
17908
+ x: ex,
17909
+ y: rowY + 2,
17910
+ width: entryW - 2,
17911
+ height: rowH - 4,
17912
+ color: BUCKET_ENTRY_DEFAULT,
17913
+ fill: BUCKET_ENTRY_DEFAULT
17914
+ });
17915
+ out.push({
17916
+ type: "text",
17917
+ x: ex + (entryW - 2) / 2,
17918
+ y: rowY + rowH / 2,
17919
+ text: `+${b.entries.length - visibleCount}`,
17920
+ color: BUCKET_ENTRY_TEXT,
17921
+ fontSize: 10,
17922
+ align: "center"
17923
+ });
17924
+ }
17925
+ });
17926
+ }
17927
+ if (frames.length > 0) {
17928
+ const panelYFrames = panelY.frames;
17929
+ const n = frames.length;
17930
+ const frameH = panelHeight / n;
17931
+ const x = 8;
17932
+ const w = width - 16;
17933
+ frames.forEach((f3, i) => {
17934
+ const y = panelYFrames + panelHeight - (i + 1) * frameH;
17935
+ const state = f3.state ?? "active";
17936
+ const fill = state === "returning" ? f3.color ?? FRAME_RETURNING_COLOR : state === "done" ? f3.color ?? FRAME_DONE_COLOR : f3.color ?? FRAME_ACTIVE_COLOR;
17937
+ out.push({ type: "rect", id: `frame-${i}`, x, y, width: w, height: frameH, color: fill, fill });
17938
+ if (frameH >= FRAME_TWO_LINE_MIN_H) {
17939
+ out.push({
17940
+ type: "text",
17941
+ x: 16,
17942
+ y: y + frameH * 0.35,
17943
+ text: f3.label,
17944
+ color: FRAME_LABEL_COLOR,
17945
+ fontSize: 10,
17946
+ align: "left"
17947
+ });
17948
+ if (f3.detail) {
17949
+ out.push({
17950
+ type: "text",
17951
+ x: 16,
17952
+ y: y + frameH * 0.7,
17953
+ text: f3.detail,
17954
+ color: FRAME_DETAIL_COLOR,
17955
+ fontSize: 10,
17956
+ align: "left"
17957
+ });
17958
+ }
17959
+ } else {
17960
+ out.push({
17961
+ type: "text",
17962
+ x: 16,
17963
+ y: y + frameH / 2,
17964
+ text: f3.label,
17965
+ color: FRAME_LABEL_COLOR,
17966
+ fontSize: 10,
17967
+ align: "left"
17968
+ });
17969
+ }
17009
17970
  });
17010
17971
  }
17011
17972
  out.push(...shapes);
17012
17973
  return out;
17013
- }, [bars, cells, pointers, shapes, width, height]);
17974
+ }, [bars, cells, pointers, ranges, slots, slotOrientation, frames, buckets, auxBars, rowLabels, colLabels, shapes, width, height]);
17014
17975
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
17015
17976
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
17016
17977
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -17197,6 +18158,12 @@ function LearningScene3D({
17197
18158
  const unitId = event.payload?.unitId;
17198
18159
  if (typeof unitId === "string") onItemClickRef.current?.(unitId);
17199
18160
  });
18161
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && post?.bloom) {
18162
+ const unknownKeys = Object.keys(post.bloom).filter((k) => !KNOWN_BLOOM_KEYS.has(k));
18163
+ if (unknownKeys.length > 0) {
18164
+ sceneLog.debug("post.bloom has unrecognized keys \u2014 only intensity/threshold/smoothing are read", { unknownKeys });
18165
+ }
18166
+ }
17200
18167
  const props3d = {
17201
18168
  drawables,
17202
18169
  isLoading,
@@ -17262,7 +18229,7 @@ function cylinderBetween(from, to, radius, color) {
17262
18229
  material: { color }
17263
18230
  };
17264
18231
  }
17265
- function arrowBetween(from, to, color, shaftRadius = 0.08) {
18232
+ function arrowBetween(from, to, color, shaftRadius = 0.08, id) {
17266
18233
  const len = segmentLength(from, to);
17267
18234
  if (len < 1e-6) return null;
17268
18235
  const tipLen = Math.min(shaftRadius * 8, len * 0.35);
@@ -17290,6 +18257,7 @@ function arrowBetween(from, to, color, shaftRadius = 0.08) {
17290
18257
  };
17291
18258
  return {
17292
18259
  type: "draw-group",
18260
+ ...id !== void 0 ? { id } : {},
17293
18261
  position: { x: from[0], y: from[1], z: from[2] },
17294
18262
  items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
17295
18263
  };
@@ -17316,20 +18284,228 @@ function get3DClickPayload(onShapeClick, idToIndex) {
17316
18284
  if (!onShapeClick) return void 0;
17317
18285
  return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
17318
18286
  }
17319
- var Canvas3DHost2;
18287
+ function polylineTube(points, radius, color, opts) {
18288
+ const maxSegments = opts?.maxSegments ?? 128;
18289
+ let pts = points;
18290
+ if (pts.length - 1 > maxSegments) {
18291
+ const step = (pts.length - 1) / maxSegments;
18292
+ const kept = [pts[0]];
18293
+ for (let s = 1; s < maxSegments; s++) kept.push(pts[Math.round(s * step)]);
18294
+ kept.push(pts[pts.length - 1]);
18295
+ pts = kept;
18296
+ }
18297
+ const out = [];
18298
+ for (let i = 0; i < pts.length - 1; i++) {
18299
+ const seg = cylinderBetween(pts[i], pts[i + 1], radius, color);
18300
+ if (seg) out.push(opts?.opacity !== void 0 ? { ...seg, opacity: opts.opacity } : seg);
18301
+ }
18302
+ return out;
18303
+ }
18304
+ function heightFieldMesh(spec) {
18305
+ const { nx, ny, heights, spacing = 1, x = 0, y = 0 } = spec;
18306
+ const flatShading = spec.flatShading ?? true;
18307
+ const vertices = [];
18308
+ for (let iy = 0; iy < ny; iy++) {
18309
+ for (let ix = 0; ix < nx; ix++) {
18310
+ vertices.push([
18311
+ x + (ix - (nx - 1) / 2) * spacing,
18312
+ y + (iy - (ny - 1) / 2) * spacing,
18313
+ heights[iy * nx + ix] ?? 0
18314
+ ]);
18315
+ }
18316
+ }
18317
+ const bands = [...spec.bands ?? []].sort((a, b) => (a.min ?? -Infinity) - (b.min ?? -Infinity));
18318
+ const facesByBand = /* @__PURE__ */ new Map();
18319
+ for (let iy = 0; iy < ny - 1; iy++) {
18320
+ for (let ix = 0; ix < nx - 1; ix++) {
18321
+ const v00 = iy * nx + ix;
18322
+ const v10 = iy * nx + ix + 1;
18323
+ const v01 = (iy + 1) * nx + ix;
18324
+ const v11 = (iy + 1) * nx + ix + 1;
18325
+ for (const face of [[v00, v10, v01], [v10, v11, v01]]) {
18326
+ const centroid = (vertices[face[0]][2] + vertices[face[1]][2] + vertices[face[2]][2]) / 3;
18327
+ let band = null;
18328
+ for (const b of bands) {
18329
+ if ((b.min ?? -Infinity) <= centroid) band = b;
18330
+ }
18331
+ const key = bands.length > 0 ? band : null;
18332
+ const list = facesByBand.get(key) ?? [];
18333
+ list.push(face);
18334
+ facesByBand.set(key, list);
18335
+ }
18336
+ }
18337
+ }
18338
+ const out = [];
18339
+ for (const [band, faces] of facesByBand) {
18340
+ if (faces.length === 0) continue;
18341
+ out.push({
18342
+ type: "draw-mesh",
18343
+ shape: "polyhedron",
18344
+ position: { x: 0, y: 0, z: 0 },
18345
+ vertices,
18346
+ faces,
18347
+ pivot: "center",
18348
+ material: { color: band?.color ?? spec.color ?? "#64748b", flatShading, side: "double" },
18349
+ ...spec.opacity !== void 0 ? { opacity: spec.opacity } : {}
18350
+ });
18351
+ }
18352
+ return out;
18353
+ }
18354
+ function arrowField(vectors, opts) {
18355
+ const scale = opts?.scale ?? 1;
18356
+ const out = [];
18357
+ for (const v of vectors) {
18358
+ const to = [
18359
+ v.from[0] + v.delta[0] * scale,
18360
+ v.from[1] + v.delta[1] * scale,
18361
+ v.from[2] + v.delta[2] * scale
18362
+ ];
18363
+ const arrow = arrowBetween(v.from, to, v.color ?? "#dc2626", v.width, v.id);
18364
+ if (arrow) out.push(arrow);
18365
+ if (v.label) out.push(billboardLabel(v.label, to[0], to[1], to[2], { color: opts?.labelColor }));
18366
+ }
18367
+ return out;
18368
+ }
18369
+ function helixDrawables(spec, opts) {
18370
+ const count = spec.count ?? spec.rungs?.length ?? 0;
18371
+ const rungs = Array.from({ length: count }, (_, i) => spec.rungs?.[i] ?? {});
18372
+ const radius = spec.radius ?? 1;
18373
+ const rise = spec.rise ?? 0.34;
18374
+ const twistRad = (spec.twistDeg ?? 36) * (Math.PI / 180);
18375
+ const strandAColor = spec.strandAColor ?? "#38bdf8";
18376
+ const strandBColor = spec.strandBColor ?? "#fb923c";
18377
+ const backboneRadius = spec.backboneRadius ?? 0.16;
18378
+ const rungRadius = spec.rungRadius ?? 0.12;
18379
+ const cx = spec.x ?? 0;
18380
+ const cy = spec.y ?? 0;
18381
+ const cz = spec.z ?? 0;
18382
+ const unwoundCount = spec.unwoundCount ?? 0;
18383
+ const unwindSpread = spec.unwindSpread ?? 1.8;
18384
+ const strandA = [];
18385
+ const strandB = [];
18386
+ for (let i = 0; i < count; i++) {
18387
+ const yi = cy + (i - (count - 1) / 2) * rise;
18388
+ const theta = i * twistRad;
18389
+ const s = i < unwoundCount ? unwindSpread : 1;
18390
+ strandA.push([cx + s * radius * Math.cos(theta), yi, cz + s * radius * Math.sin(theta)]);
18391
+ strandB.push([cx + s * radius * Math.cos(theta + Math.PI), yi, cz + s * radius * Math.sin(theta + Math.PI)]);
18392
+ }
18393
+ const out = [];
18394
+ for (let i = 0; i < count; i++) {
18395
+ out.push(meshSphere(`hx-a-${i}`, strandA[i][0], strandA[i][1], strandA[i][2], backboneRadius, strandAColor));
18396
+ out.push(meshSphere(`hx-b-${i}`, strandB[i][0], strandB[i][1], strandB[i][2], backboneRadius, strandBColor));
18397
+ if (i > 0) {
18398
+ const segA = cylinderBetween(strandA[i - 1], strandA[i], backboneRadius, strandAColor);
18399
+ if (segA) out.push(segA);
18400
+ const segB = cylinderBetween(strandB[i - 1], strandB[i], backboneRadius, strandBColor);
18401
+ if (segB) out.push(segB);
18402
+ }
18403
+ const rung = rungs[i];
18404
+ const rungColor = rung.color ?? "#94a3b8";
18405
+ const rod = cylinderBetween(strandA[i], strandB[i], rungRadius, rungColor);
18406
+ if (rod) out.push(rod);
18407
+ const mid = [
18408
+ (strandA[i][0] + strandB[i][0]) / 2,
18409
+ (strandA[i][1] + strandB[i][1]) / 2,
18410
+ (strandA[i][2] + strandB[i][2]) / 2
18411
+ ];
18412
+ const markerRadius = rung.radius ?? rungRadius;
18413
+ out.push(meshSphere(rung.id, mid[0], mid[1], mid[2], markerRadius, rungColor));
18414
+ if (rung.label) out.push(billboardLabel(rung.label, mid[0], mid[1], mid[2] + markerRadius, { color: opts?.labelColor }));
18415
+ }
18416
+ return out;
18417
+ }
18418
+ function latticeDrawables(spec, opts) {
18419
+ const nx = spec.nx ?? 2;
18420
+ const ny = spec.ny ?? 2;
18421
+ const nz = spec.nz ?? 2;
18422
+ const latticeConstant = spec.latticeConstant ?? 2;
18423
+ const bondRadius = spec.bondRadius ?? 0.06;
18424
+ const highlightCell = spec.highlightCell ?? false;
18425
+ const dimColor = spec.dimColor ?? "#475569";
18426
+ const showLabels = spec.showLabels ?? false;
18427
+ const selectedColor = spec.selectedColor ?? "#f59e0b";
18428
+ const posByKey = /* @__PURE__ */ new Map();
18429
+ const inCellByKey = /* @__PURE__ */ new Map();
18430
+ const out = [];
18431
+ for (const site of spec.basis) {
18432
+ const snx = site.xEdge ? nx + 1 : nx;
18433
+ const sny = site.yEdge ? ny + 1 : ny;
18434
+ const snz = site.zEdge ? nz + 1 : nz;
18435
+ for (let i = 0; i < snx; i++) {
18436
+ for (let j = 0; j < sny; j++) {
18437
+ for (let k = 0; k < snz; k++) {
18438
+ const key = `${site.key}-${i}-${j}-${k}`;
18439
+ const inCell = i + site.dx <= 1 && j + site.dy <= 1 && k + site.dz <= 1;
18440
+ const pos = [
18441
+ (i + site.dx) * latticeConstant - nx * latticeConstant / 2,
18442
+ (j + site.dy) * latticeConstant - ny * latticeConstant / 2,
18443
+ (k + site.dz) * latticeConstant - nz * latticeConstant / 2
18444
+ ];
18445
+ posByKey.set(key, pos);
18446
+ inCellByKey.set(key, inCell);
18447
+ const isSelected = spec.selectedId === `lat-${key}`;
18448
+ const color = isSelected ? selectedColor : highlightCell && !inCell ? dimColor : site.color ?? "#2563eb";
18449
+ const radius = (site.radius ?? 0.3) * (isSelected ? 1.4 : 1);
18450
+ out.push(meshSphere(`lat-${key}`, pos[0], pos[1], pos[2], radius, color));
18451
+ if (showLabels && site.element) {
18452
+ out.push(billboardLabel(site.element, pos[0], pos[1], pos[2] + radius, { color: opts?.labelColor }));
18453
+ }
18454
+ }
18455
+ }
18456
+ }
18457
+ }
18458
+ const basisByKey = new Map(spec.basis.map((s) => [s.key, s]));
18459
+ for (const bond of spec.bonds ?? []) {
18460
+ const fromSite = basisByKey.get(bond.from);
18461
+ const toSite = basisByKey.get(bond.to);
18462
+ if (!fromSite || !toSite) continue;
18463
+ const fnx = fromSite.xEdge ? nx + 1 : nx;
18464
+ const fny = fromSite.yEdge ? ny + 1 : ny;
18465
+ const fnz = fromSite.zEdge ? nz + 1 : nz;
18466
+ const tnx = toSite.xEdge ? nx + 1 : nx;
18467
+ const tny = toSite.yEdge ? ny + 1 : ny;
18468
+ const tnz = toSite.zEdge ? nz + 1 : nz;
18469
+ const bdx = bond.dx ?? 0;
18470
+ const bdy = bond.dy ?? 0;
18471
+ const bdz = bond.dz ?? 0;
18472
+ for (let i = 0; i < fnx; i++) {
18473
+ for (let j = 0; j < fny; j++) {
18474
+ for (let k = 0; k < fnz; k++) {
18475
+ const ti = i + bdx;
18476
+ const tj = j + bdy;
18477
+ const tk = k + bdz;
18478
+ if (ti < 0 || ti >= tnx || tj < 0 || tj >= tny || tk < 0 || tk >= tnz) continue;
18479
+ const fromKey = `${fromSite.key}-${i}-${j}-${k}`;
18480
+ const toKey = `${toSite.key}-${ti}-${tj}-${tk}`;
18481
+ const fromPos = posByKey.get(fromKey);
18482
+ const toPos = posByKey.get(toKey);
18483
+ if (!fromPos || !toPos) continue;
18484
+ const dimmed = highlightCell && !(inCellByKey.get(fromKey) && inCellByKey.get(toKey));
18485
+ const seg = cylinderBetween(fromPos, toPos, bondRadius, dimmed ? dimColor : bond.color ?? "#6b7280");
18486
+ if (seg) out.push(seg);
18487
+ }
18488
+ }
18489
+ }
18490
+ }
18491
+ return out;
18492
+ }
18493
+ var sceneLog, KNOWN_BLOOM_KEYS, Canvas3DHost2;
17320
18494
  var init_learningScene3D = __esm({
17321
18495
  "components/learning/molecules/learningScene3D.tsx"() {
17322
18496
  "use client";
17323
18497
  init_atoms();
17324
18498
  init_Stack();
17325
18499
  init_useEventBus();
18500
+ sceneLog = logger.createLogger("almadar:ui:learning-scene-3d");
18501
+ KNOWN_BLOOM_KEYS = /* @__PURE__ */ new Set(["intensity", "threshold", "smoothing"]);
17326
18502
  Canvas3DHost2 = React94.lazy(
17327
18503
  () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
17328
18504
  );
17329
18505
  LearningScene3D.displayName = "LearningScene3D";
17330
18506
  }
17331
18507
  });
17332
- var biologyLog, BiologyCanvas;
18508
+ var biologyLog, BIO_BAND_COLORS, BIO_STAGE_FILL, BIO_STAGE_TEXT, BiologyCanvas;
17333
18509
  var init_BiologyCanvas = __esm({
17334
18510
  "components/learning/molecules/BiologyCanvas.tsx"() {
17335
18511
  "use client";
@@ -17338,6 +18514,17 @@ var init_BiologyCanvas = __esm({
17338
18514
  init_LearningCanvas();
17339
18515
  init_learningScene3D();
17340
18516
  biologyLog = logger.createLogger("almadar:ui:biology-canvas");
18517
+ BIO_BAND_COLORS = ["#dcfce7", "#fef9c3", "#fee2e2", "#e0e7ff"];
18518
+ BIO_STAGE_FILL = {
18519
+ pending: "#e2e8f0",
18520
+ active: "#3b82f6",
18521
+ done: "#94a3b8"
18522
+ };
18523
+ BIO_STAGE_TEXT = {
18524
+ pending: "#64748b",
18525
+ active: "#ffffff",
18526
+ done: "#ffffff"
18527
+ };
17341
18528
  BiologyCanvas = ({
17342
18529
  className,
17343
18530
  width = 600,
@@ -17350,7 +18537,15 @@ var init_BiologyCanvas = __esm({
17350
18537
  post,
17351
18538
  nodes = [],
17352
18539
  edges = [],
18540
+ compartments = [],
18541
+ bands = [],
18542
+ stages = [],
18543
+ stageStyle = "timeline",
18544
+ helix,
18545
+ helix3d,
17353
18546
  shapes = [],
18547
+ readouts,
18548
+ traces,
17354
18549
  showGrid,
17355
18550
  shadows,
17356
18551
  interactive,
@@ -17365,19 +18560,148 @@ var init_BiologyCanvas = __esm({
17365
18560
  for (const n of nodes) {
17366
18561
  if (n.id) nodeById.set(n.id, n);
17367
18562
  }
18563
+ const bandCount = bands.length;
18564
+ for (let i = 0; i < bandCount; i++) {
18565
+ const band = bands[i];
18566
+ const bandColor = band.color ?? BIO_BAND_COLORS[i % BIO_BAND_COLORS.length];
18567
+ const bandY = i * height / bandCount;
18568
+ const bandH = height / bandCount;
18569
+ out.push({
18570
+ type: "rect",
18571
+ x: 0,
18572
+ y: bandY,
18573
+ width,
18574
+ height: bandH,
18575
+ color: bandColor,
18576
+ fill: bandColor,
18577
+ opacity: 0.45
18578
+ });
18579
+ if (band.label) {
18580
+ out.push({
18581
+ type: "text",
18582
+ x: 8,
18583
+ y: bandY + 14,
18584
+ text: band.label,
18585
+ color: "#6b7280",
18586
+ fontSize: 10
18587
+ });
18588
+ }
18589
+ }
18590
+ for (const c of compartments) {
18591
+ const color = c.color ?? "#16a34a";
18592
+ out.push({
18593
+ type: "ellipse",
18594
+ x: c.x,
18595
+ y: c.y,
18596
+ width: c.width,
18597
+ height: c.height,
18598
+ color,
18599
+ fill: c.fill ?? `${color}1A`,
18600
+ lineWidth: c.lineWidth ?? 2,
18601
+ ...c.dash ? { dash: c.dash } : {}
18602
+ });
18603
+ if (c.label) {
18604
+ out.push({
18605
+ type: "text",
18606
+ x: c.x,
18607
+ y: c.y - c.height / 2 + 14,
18608
+ text: c.label,
18609
+ color: "#111827",
18610
+ fontSize: 11,
18611
+ align: "center"
18612
+ });
18613
+ }
18614
+ }
18615
+ if (helix) {
18616
+ const hx = helix.x ?? 24;
18617
+ const hy = helix.y ?? height * 0.25;
18618
+ const hw = helix.width ?? width - 48;
18619
+ const hh = helix.height ?? height * 0.5;
18620
+ const rungs = helix.rungs;
18621
+ const n = rungs.length;
18622
+ const cy = hy + hh / 2;
18623
+ const colorA = helix.colorA ?? "#2563eb";
18624
+ const colorB = helix.colorB ?? "#dc2626";
18625
+ const rungColor = helix.rungColor ?? "#94a3b8";
18626
+ const fork = helix.fork ?? 0;
18627
+ const maxSep = Math.min(hh - 8, 96);
18628
+ const strandA = [];
18629
+ const strandB = [];
18630
+ const rungGeoms = [];
18631
+ for (let i = 0; i < n; i++) {
18632
+ const rx = hx + (i + 0.5) * hw / n;
18633
+ const t = (i + 0.5) / n;
18634
+ const paired = t >= fork;
18635
+ const sep = paired ? 28 : 28 + (maxSep - 28) * ((fork - t) / fork);
18636
+ strandA.push({ x: rx, y: cy - sep / 2 });
18637
+ strandB.push({ x: rx, y: cy + sep / 2 });
18638
+ rungGeoms.push({ rx, sep, rung: rungs[i], paired });
18639
+ }
18640
+ for (let i = 1; i < n; i++) {
18641
+ 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 });
18642
+ }
18643
+ for (let i = 1; i < n; i++) {
18644
+ 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 });
18645
+ }
18646
+ for (const g of rungGeoms) {
18647
+ const rColor = g.rung.color ?? (g.rung.state === "new" ? "#16a34a" : rungColor);
18648
+ const topY = cy - g.sep / 2;
18649
+ const bottomY = cy + g.sep / 2;
18650
+ if (g.paired) {
18651
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: bottomY, color: rColor });
18652
+ if (g.rung.a) {
18653
+ out.push({ type: "text", x: g.rx, y: cy - g.sep / 4, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
18654
+ }
18655
+ if (g.rung.b) {
18656
+ out.push({ type: "text", x: g.rx, y: cy + g.sep / 4, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
18657
+ }
18658
+ } else {
18659
+ const stubTopY = topY + 8;
18660
+ const stubBottomY = bottomY - 8;
18661
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: stubTopY, color: rColor });
18662
+ out.push({ type: "line", x1: g.rx, y1: bottomY, x2: g.rx, y2: stubBottomY, color: rColor });
18663
+ if (g.rung.a) {
18664
+ out.push({ type: "text", x: g.rx, y: stubTopY + 6, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
18665
+ }
18666
+ if (g.rung.b) {
18667
+ out.push({ type: "text", x: g.rx, y: stubBottomY - 6, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
18668
+ }
18669
+ }
18670
+ }
18671
+ }
17368
18672
  for (const e of edges) {
17369
18673
  const a = nodeById.get(e.from);
17370
18674
  const b = nodeById.get(e.to);
17371
18675
  if (!a || !b) continue;
17372
- out.push({
17373
- type: "line",
17374
- x1: a.x,
17375
- y1: a.y,
17376
- x2: b.x,
17377
- y2: b.y,
17378
- color: e.color ?? "#9ca3af",
17379
- lineWidth: 2
17380
- });
18676
+ const color = e.color ?? "#9ca3af";
18677
+ if (e.directed) {
18678
+ const rA = a.radius ?? 16;
18679
+ const rB = b.radius ?? 16;
18680
+ const dx = b.x - a.x;
18681
+ const dy = b.y - a.y;
18682
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
18683
+ const ux = dx / dist;
18684
+ const uy = dy / dist;
18685
+ out.push({
18686
+ type: "arrow",
18687
+ x1: a.x + ux * rA,
18688
+ y1: a.y + uy * rA,
18689
+ x2: b.x - ux * rB,
18690
+ y2: b.y - uy * rB,
18691
+ color,
18692
+ lineWidth: 2
18693
+ });
18694
+ } else {
18695
+ out.push({
18696
+ type: "line",
18697
+ x1: a.x,
18698
+ y1: a.y,
18699
+ x2: b.x,
18700
+ y2: b.y,
18701
+ color,
18702
+ lineWidth: 2
18703
+ });
18704
+ }
17381
18705
  if (e.label) {
17382
18706
  out.push({
17383
18707
  type: "text",
@@ -17390,6 +18714,8 @@ var init_BiologyCanvas = __esm({
17390
18714
  }
17391
18715
  }
17392
18716
  for (const n of nodes) {
18717
+ const state = n.state ?? "default";
18718
+ const muted = state === "muted";
17393
18719
  out.push({
17394
18720
  type: "circle",
17395
18721
  x: n.x,
@@ -17397,28 +18723,127 @@ var init_BiologyCanvas = __esm({
17397
18723
  radius: n.radius ?? 16,
17398
18724
  color: n.color ?? "#16a34a",
17399
18725
  fill: `${n.color ?? "#16a34a"}33`,
17400
- id: n.id
18726
+ id: n.id,
18727
+ ...muted ? { opacity: 0.35 } : {}
17401
18728
  });
18729
+ if (state === "highlight") {
18730
+ out.push({
18731
+ type: "circle",
18732
+ x: n.x,
18733
+ y: n.y,
18734
+ radius: (n.radius ?? 16) + 4,
18735
+ color: "#f59e0b",
18736
+ lineWidth: 2
18737
+ });
18738
+ }
17402
18739
  if (n.label) {
17403
18740
  out.push({
17404
18741
  type: "text",
17405
18742
  x: n.x,
17406
18743
  y: n.y + (n.radius ?? 16) + 14,
17407
18744
  text: n.label,
18745
+ ...muted ? { opacity: 0.35 } : {},
17408
18746
  color: "#111827",
17409
18747
  fontSize: 12,
17410
18748
  align: "center"
17411
18749
  });
17412
18750
  }
17413
18751
  }
18752
+ const stageCount = stages.length;
18753
+ if (stageCount > 0) {
18754
+ if (stageStyle === "ring") {
18755
+ const cx = width / 2;
18756
+ const cy = height / 2;
18757
+ const R = Math.min(width, height) / 2 - 48;
18758
+ const ringPoints = [];
18759
+ for (let i = 0; i < stageCount; i++) {
18760
+ const angleRad = (-90 + 360 * i / stageCount) * Math.PI / 180;
18761
+ ringPoints.push({ x: cx + R * Math.cos(angleRad), y: cy + R * Math.sin(angleRad) });
18762
+ }
18763
+ if (stageCount >= 2) {
18764
+ for (let i = 0; i < stageCount - 1; i++) {
18765
+ const p1 = ringPoints[i];
18766
+ const p2 = ringPoints[i + 1];
18767
+ const dx = p2.x - p1.x;
18768
+ const dy = p2.y - p1.y;
18769
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
18770
+ const ux = dx / dist;
18771
+ const uy = dy / dist;
18772
+ out.push({
18773
+ type: "arrow",
18774
+ x1: p1.x + ux * 46,
18775
+ y1: p1.y + uy * 46,
18776
+ x2: p2.x - ux * 46,
18777
+ y2: p2.y - uy * 46,
18778
+ color: "#94a3b8"
18779
+ });
18780
+ }
18781
+ }
18782
+ for (let i = 0; i < stageCount; i++) {
18783
+ const stage = stages[i];
18784
+ const state = stage.state ?? "pending";
18785
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
18786
+ const w = Math.max(26, Math.min(84, stage.label.length * 6 + 10));
18787
+ const h = 18;
18788
+ const p = ringPoints[i];
18789
+ out.push({ type: "rect", x: p.x - w / 2, y: p.y - h / 2, width: w, height: h, color: fill, fill });
18790
+ out.push({ type: "text", x: p.x, y: p.y, text: stage.label, color: BIO_STAGE_TEXT[state], fontSize: 10, align: "center" });
18791
+ }
18792
+ } else {
18793
+ const stripY = height - 32;
18794
+ const slotW = (width - 16) / stageCount;
18795
+ const chipGeoms = [];
18796
+ for (let i = 0; i < stageCount; i++) {
18797
+ chipGeoms.push({ x: 8 + i * slotW + 5, w: slotW - 10 });
18798
+ }
18799
+ for (let i = 0; i < stageCount - 1; i++) {
18800
+ const midY = stripY + 13;
18801
+ out.push({
18802
+ type: "arrow",
18803
+ x1: chipGeoms[i].x + chipGeoms[i].w,
18804
+ y1: midY,
18805
+ x2: chipGeoms[i + 1].x,
18806
+ y2: midY,
18807
+ color: "#94a3b8"
18808
+ });
18809
+ }
18810
+ for (let i = 0; i < stageCount; i++) {
18811
+ const stage = stages[i];
18812
+ const state = stage.state ?? "pending";
18813
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
18814
+ const g = chipGeoms[i];
18815
+ out.push({ type: "rect", x: g.x, y: stripY, width: g.w, height: 26, color: fill, fill });
18816
+ out.push({
18817
+ type: "text",
18818
+ x: g.x + g.w / 2,
18819
+ y: stripY + 13,
18820
+ text: stage.label,
18821
+ color: BIO_STAGE_TEXT[state],
18822
+ fontSize: 10,
18823
+ align: "center"
18824
+ });
18825
+ }
18826
+ }
18827
+ }
17414
18828
  out.push(...shapes);
17415
18829
  return out;
17416
- }, [nodes, edges, shapes]);
18830
+ }, [nodes, edges, compartments, bands, stages, stageStyle, helix, shapes, width, height]);
17417
18831
  const drawables3D = React94.useMemo(() => {
17418
18832
  if (mode !== "3d") return [];
17419
18833
  if (shapes.length > 0) {
17420
18834
  biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
17421
18835
  }
18836
+ if (compartments.length > 0 || bands.length > 0 || stages.length > 0 || helix) {
18837
+ biologyLog.debug("2D-only families ignored in 3D mode (pixel-authored 2D vocabulary)", {
18838
+ compartments: compartments.length,
18839
+ bands: bands.length,
18840
+ stages: stages.length,
18841
+ helix: helix != null
18842
+ });
18843
+ }
18844
+ if (animate) {
18845
+ biologyLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
18846
+ }
17422
18847
  const out = [];
17423
18848
  const labelColor = labelColorForBackground(backgroundColor);
17424
18849
  const nodeById = /* @__PURE__ */ new Map();
@@ -17452,15 +18877,21 @@ var init_BiologyCanvas = __esm({
17452
18877
  out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
17453
18878
  }
17454
18879
  }
18880
+ if (helix3d) {
18881
+ out.push(...helixDrawables(helix3d, { labelColor }));
18882
+ }
17455
18883
  return out;
17456
- }, [mode, nodes, edges, shapes, backgroundColor]);
18884
+ }, [mode, nodes, edges, shapes, compartments, bands, stages, helix, helix3d, animate, backgroundColor]);
17457
18885
  const nodeIndexById = React94.useMemo(() => {
17458
18886
  const m = /* @__PURE__ */ new Map();
18887
+ (helix3d?.rungs ?? []).forEach((rung, i) => {
18888
+ if (rung.id) m.set(rung.id, i);
18889
+ });
17459
18890
  nodes.forEach((n, i) => {
17460
18891
  if (n.id) m.set(n.id, i);
17461
18892
  });
17462
18893
  return m;
17463
- }, [nodes]);
18894
+ }, [nodes, helix3d]);
17464
18895
  if (mode === "3d") {
17465
18896
  return /* @__PURE__ */ jsxRuntime.jsx(
17466
18897
  LearningScene3D,
@@ -17492,6 +18923,8 @@ var init_BiologyCanvas = __esm({
17492
18923
  height,
17493
18924
  backgroundColor,
17494
18925
  shapes: derivedShapes,
18926
+ readouts,
18927
+ traces,
17495
18928
  interactive: interactive ?? false,
17496
18929
  animate,
17497
18930
  onShapeClick,
@@ -24341,7 +25774,7 @@ function bondPerpendicular(a, b) {
24341
25774
  if (len < 1e-6) return [1, 0, 0];
24342
25775
  return [px / len, py / len, 0];
24343
25776
  }
24344
- var chemistryLog, ChemistryCanvas;
25777
+ var chemistryLog, CHEM_BOND_STATE_COLOR, LONE_PAIR_ANGLES, ChemistryCanvas;
24345
25778
  var init_ChemistryCanvas = __esm({
24346
25779
  "components/learning/molecules/ChemistryCanvas.tsx"() {
24347
25780
  "use client";
@@ -24350,6 +25783,13 @@ var init_ChemistryCanvas = __esm({
24350
25783
  init_LearningCanvas();
24351
25784
  init_learningScene3D();
24352
25785
  chemistryLog = logger.createLogger("almadar:ui:chemistry-canvas");
25786
+ CHEM_BOND_STATE_COLOR = {
25787
+ default: "#6b7280",
25788
+ forming: "#16a34a",
25789
+ breaking: "#dc2626",
25790
+ highlight: "#f59e0b"
25791
+ };
25792
+ LONE_PAIR_ANGLES = [-90, 0, 90, 180];
24353
25793
  ChemistryCanvas = ({
24354
25794
  className,
24355
25795
  width = 600,
@@ -24363,7 +25803,14 @@ var init_ChemistryCanvas = __esm({
24363
25803
  atoms = [],
24364
25804
  bonds = [],
24365
25805
  arrows = [],
25806
+ bondStyle = "thick",
25807
+ containers = [],
25808
+ equation,
25809
+ equationColor,
25810
+ lattice3d,
24366
25811
  shapes = [],
25812
+ readouts,
25813
+ traces,
24367
25814
  showGrid,
24368
25815
  shadows,
24369
25816
  interactive,
@@ -24378,21 +25825,118 @@ var init_ChemistryCanvas = __esm({
24378
25825
  for (const a of atoms) {
24379
25826
  if (a.id) atomById.set(a.id, a);
24380
25827
  }
25828
+ for (const c of containers) {
25829
+ const color = c.color ?? "#64748b";
25830
+ if (c.level != null) {
25831
+ const lv = c.level;
25832
+ out.push({
25833
+ type: "rect",
25834
+ x: c.x + 1,
25835
+ y: c.y + c.height * (1 - lv),
25836
+ width: c.width - 2,
25837
+ height: c.height * lv - 1,
25838
+ color: c.levelColor ?? "#60a5fa",
25839
+ fill: c.levelColor ?? "#60a5fa",
25840
+ opacity: 0.5
25841
+ });
25842
+ }
25843
+ out.push({
25844
+ type: "rect",
25845
+ x: c.x,
25846
+ y: c.y,
25847
+ width: c.width,
25848
+ height: c.height,
25849
+ color,
25850
+ fill: c.fill,
25851
+ lineWidth: c.lineWidth ?? 2
25852
+ });
25853
+ const divider = c.divider ?? "none";
25854
+ if (divider !== "none") {
25855
+ out.push({
25856
+ type: "line",
25857
+ x1: c.x + c.width / 2,
25858
+ y1: c.y,
25859
+ x2: c.x + c.width / 2,
25860
+ y2: c.y + c.height,
25861
+ color: c.dividerColor ?? color,
25862
+ ...divider === "dashed" || divider === "dotted" ? { dash: divider } : {}
25863
+ });
25864
+ }
25865
+ if (c.leftLabel) {
25866
+ out.push({
25867
+ type: "text",
25868
+ x: c.x + c.width * 0.25,
25869
+ y: c.y + 12,
25870
+ text: c.leftLabel,
25871
+ color: "#374151",
25872
+ fontSize: 11,
25873
+ align: "center"
25874
+ });
25875
+ }
25876
+ if (c.rightLabel) {
25877
+ out.push({
25878
+ type: "text",
25879
+ x: c.x + c.width * 0.75,
25880
+ y: c.y + 12,
25881
+ text: c.rightLabel,
25882
+ color: "#374151",
25883
+ fontSize: 11,
25884
+ align: "center"
25885
+ });
25886
+ }
25887
+ if (c.label) {
25888
+ out.push({
25889
+ type: "text",
25890
+ x: c.x + c.width / 2,
25891
+ y: c.y + c.height + 12,
25892
+ text: c.label,
25893
+ color: "#111827",
25894
+ fontSize: 12,
25895
+ align: "center"
25896
+ });
25897
+ }
25898
+ }
24381
25899
  for (const b of bonds) {
24382
25900
  const a = atomById.get(b.from);
24383
25901
  const c = atomById.get(b.to);
24384
25902
  if (!a || !c) continue;
24385
- const color = b.color ?? "#6b7280";
24386
- const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
24387
- out.push({
24388
- type: "line",
24389
- x1: a.x,
24390
- y1: a.y,
24391
- x2: c.x,
24392
- y2: c.y,
24393
- color,
24394
- lineWidth: strokeWidth
24395
- });
25903
+ const state = b.state ?? "default";
25904
+ const color = b.color ?? CHEM_BOND_STATE_COLOR[state];
25905
+ const dash = state === "forming" || state === "breaking" ? "dashed" : void 0;
25906
+ if (bondStyle === "parallel") {
25907
+ const dx = c.x - a.x;
25908
+ const dy = c.y - a.y;
25909
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
25910
+ const ux = dx / dist;
25911
+ const uy = dy / dist;
25912
+ const px = -uy;
25913
+ const py = ux;
25914
+ const offsets = b.type === "double" ? [-3, 3] : b.type === "triple" ? [-4, 0, 4] : [0];
25915
+ for (const off of offsets) {
25916
+ out.push({
25917
+ type: "line",
25918
+ x1: a.x + px * off,
25919
+ y1: a.y + py * off,
25920
+ x2: c.x + px * off,
25921
+ y2: c.y + py * off,
25922
+ color,
25923
+ lineWidth: 2,
25924
+ ...dash ? { dash } : {}
25925
+ });
25926
+ }
25927
+ } else {
25928
+ const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
25929
+ out.push({
25930
+ type: "line",
25931
+ x1: a.x,
25932
+ y1: a.y,
25933
+ x2: c.x,
25934
+ y2: c.y,
25935
+ color,
25936
+ lineWidth: strokeWidth,
25937
+ ...dash ? { dash } : {}
25938
+ });
25939
+ }
24396
25940
  }
24397
25941
  for (const a of arrows) {
24398
25942
  const angle = (a.angle ?? 0) * (Math.PI / 180);
@@ -24441,15 +25985,62 @@ var init_ChemistryCanvas = __esm({
24441
25985
  align: "center"
24442
25986
  });
24443
25987
  }
25988
+ const r2 = a.radius ?? 14;
25989
+ if (a.charge) {
25990
+ out.push({
25991
+ type: "text",
25992
+ x: a.x + r2 * 0.85,
25993
+ y: a.y - r2 * 0.85,
25994
+ text: a.charge,
25995
+ color: "#111827",
25996
+ fontSize: 9,
25997
+ align: "left"
25998
+ });
25999
+ }
26000
+ const lonePairs = Math.max(0, Math.min(4, a.lonePairs ?? 0));
26001
+ for (let k = 0; k < lonePairs; k++) {
26002
+ const angleRad = LONE_PAIR_ANGLES[k] * Math.PI / 180;
26003
+ const cx = a.x + (r2 + 6) * Math.cos(angleRad);
26004
+ const cy = a.y + (r2 + 6) * Math.sin(angleRad);
26005
+ const perpX = -Math.sin(angleRad);
26006
+ const perpY = Math.cos(angleRad);
26007
+ for (const sign of [1, -1]) {
26008
+ out.push({
26009
+ type: "circle",
26010
+ x: cx + perpX * 2.5 * sign,
26011
+ y: cy + perpY * 2.5 * sign,
26012
+ radius: 1.5,
26013
+ color: "#374151",
26014
+ fill: "#374151"
26015
+ });
26016
+ }
26017
+ }
26018
+ }
26019
+ if (equation) {
26020
+ out.push({
26021
+ type: "text",
26022
+ x: width / 2,
26023
+ y: 14,
26024
+ text: equation,
26025
+ color: equationColor ?? "#111827",
26026
+ fontSize: 13,
26027
+ align: "center"
26028
+ });
24444
26029
  }
24445
26030
  out.push(...shapes);
24446
26031
  return out;
24447
- }, [atoms, bonds, arrows, shapes]);
26032
+ }, [atoms, bonds, arrows, bondStyle, containers, equation, equationColor, shapes, width]);
24448
26033
  const drawables3D = React94.useMemo(() => {
24449
26034
  if (mode !== "3d") return [];
24450
26035
  if (shapes.length > 0) {
24451
26036
  chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
24452
26037
  }
26038
+ if (containers.length > 0) {
26039
+ chemistryLog.debug("containers ignored in 3D mode (pixel-authored 2D vocabulary)", { count: containers.length });
26040
+ }
26041
+ if (animate) {
26042
+ chemistryLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
26043
+ }
24453
26044
  const out = [];
24454
26045
  const labelColor = labelColorForBackground(backgroundColor);
24455
26046
  const atomById = /* @__PURE__ */ new Map();
@@ -24496,8 +26087,11 @@ var init_ChemistryCanvas = __esm({
24496
26087
  out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
24497
26088
  }
24498
26089
  }
26090
+ if (lattice3d) {
26091
+ out.push(...latticeDrawables(lattice3d, { labelColor }));
26092
+ }
24499
26093
  return out;
24500
- }, [mode, atoms, bonds, arrows, shapes, backgroundColor]);
26094
+ }, [mode, atoms, bonds, arrows, shapes, containers, lattice3d, animate, backgroundColor]);
24501
26095
  const atomIndexById = React94.useMemo(() => {
24502
26096
  const m = /* @__PURE__ */ new Map();
24503
26097
  atoms.forEach((a, i) => {
@@ -24536,6 +26130,8 @@ var init_ChemistryCanvas = __esm({
24536
26130
  height,
24537
26131
  backgroundColor,
24538
26132
  shapes: derivedShapes,
26133
+ readouts,
26134
+ traces,
24539
26135
  interactive: interactive ?? false,
24540
26136
  animate,
24541
26137
  onShapeClick,
@@ -30725,6 +32321,10 @@ var init_ProgressDots = __esm({
30725
32321
  ProgressDots.displayName = "ProgressDots";
30726
32322
  }
30727
32323
  });
32324
+ function formatTick(v) {
32325
+ if (Number.isInteger(v)) return String(v);
32326
+ return v.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
32327
+ }
30728
32328
  var MathCanvas;
30729
32329
  var init_MathCanvas = __esm({
30730
32330
  "components/learning/molecules/MathCanvas.tsx"() {
@@ -30744,10 +32344,19 @@ var init_MathCanvas = __esm({
30744
32344
  showAxes = true,
30745
32345
  showGrid = true,
30746
32346
  gridStep = 1,
32347
+ showTickLabels = false,
32348
+ showCurveLabels = false,
30747
32349
  curves = [],
30748
32350
  points = [],
30749
32351
  vectors = [],
32352
+ regions = [],
32353
+ bars = [],
32354
+ guides = [],
32355
+ angles = [],
32356
+ hops = [],
30750
32357
  shapes = [],
32358
+ readouts,
32359
+ traces,
30751
32360
  interactive = false,
30752
32361
  animate = false,
30753
32362
  onShapeClick,
@@ -30761,6 +32370,8 @@ var init_MathCanvas = __esm({
30761
32370
  const plotH = height - margin * 2;
30762
32371
  const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
30763
32372
  const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
32373
+ const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
32374
+ const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
30764
32375
  if (showGrid) {
30765
32376
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep) {
30766
32377
  const px = mapX(x);
@@ -30771,14 +32382,99 @@ var init_MathCanvas = __esm({
30771
32382
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color: "#e5e7eb", lineWidth: 1 });
30772
32383
  }
30773
32384
  }
32385
+ if (showTickLabels) {
32386
+ const labelEveryX = Math.max(1, Math.ceil((xMax - xMin) / gridStep / Math.floor(plotW / 40)));
32387
+ let kx = 0;
32388
+ for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
32389
+ if (kx % labelEveryX === 0 && x !== 0) {
32390
+ out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: 10, align: "center" });
32391
+ }
32392
+ }
32393
+ const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
32394
+ let ky = 0;
32395
+ for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
32396
+ if (ky % labelEveryY === 0 && y !== 0) {
32397
+ out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: 10, align: "right" });
32398
+ }
32399
+ }
32400
+ if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
32401
+ out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: 10, align: "right" });
32402
+ }
32403
+ }
32404
+ for (const region of regions) {
32405
+ if (!region.samples || region.samples.length === 0) continue;
32406
+ const baseline = region.baseline ?? 0;
32407
+ const clampedPoint = (p) => ({
32408
+ x: mapX(Math.min(xMax, Math.max(xMin, p.x))),
32409
+ y: mapY(Math.min(yMax, Math.max(yMin, p.y)))
32410
+ });
32411
+ const upper = region.samples.map(clampedPoint);
32412
+ const first = region.samples[0];
32413
+ const last = region.samples[region.samples.length - 1];
32414
+ 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 })];
32415
+ const color = region.color ?? "#2563eb";
32416
+ out.push({
32417
+ type: "polygon",
32418
+ points: [...upper, ...closing],
32419
+ fill: color,
32420
+ color,
32421
+ opacity: region.opacity ?? 0.2,
32422
+ lineWidth: 1
32423
+ });
32424
+ if (region.label) {
32425
+ const mid = Math.floor(region.samples.length / 2);
32426
+ out.push({
32427
+ type: "text",
32428
+ x: mapX((first.x + last.x) / 2),
32429
+ y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
32430
+ text: region.label,
32431
+ color: "#111827",
32432
+ fontSize: 11
32433
+ });
32434
+ }
32435
+ }
32436
+ for (const bar of bars) {
32437
+ if (bar.x + bar.width < xMin || bar.x > xMax) continue;
32438
+ const y0 = bar.y0 ?? 0;
32439
+ const color = bar.color ?? "#93c5fd";
32440
+ out.push({
32441
+ type: "rect",
32442
+ x: mapX(bar.x),
32443
+ y: mapY(Math.max(y0, bar.y1)),
32444
+ width: mapX(bar.x + bar.width) - mapX(bar.x),
32445
+ height: Math.abs(mapY(bar.y1) - mapY(y0)),
32446
+ color,
32447
+ fill: color,
32448
+ opacity: bar.opacity ?? 0.5,
32449
+ lineWidth: 1
32450
+ });
32451
+ }
30774
32452
  if (showAxes) {
30775
- const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
30776
- const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
30777
32453
  out.push({ type: "line", x1: margin, y1: xAxisY, x2: width - margin, y2: xAxisY, color: "#374151", lineWidth: 2 });
30778
32454
  out.push({ type: "line", x1: yAxisX, y1: margin, x2: yAxisX, y2: height - margin, color: "#374151", lineWidth: 2 });
30779
32455
  }
32456
+ for (const guide of guides) {
32457
+ const color = guide.color ?? "#9ca3af";
32458
+ const dash = guide.dash ?? "dashed";
32459
+ if (guide.kind === "vline") {
32460
+ if (guide.at < xMin || guide.at > xMax) continue;
32461
+ const px = mapX(guide.at);
32462
+ out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
32463
+ if (guide.label) {
32464
+ out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color: "#111827", fontSize: 11 });
32465
+ }
32466
+ } else {
32467
+ if (guide.at < yMin || guide.at > yMax) continue;
32468
+ const py = mapY(guide.at);
32469
+ out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
32470
+ if (guide.label) {
32471
+ out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color: "#111827", fontSize: 11, align: "right" });
32472
+ }
32473
+ }
32474
+ }
30780
32475
  for (const curve of curves) {
30781
32476
  if (!curve.samples || curve.samples.length < 2) continue;
32477
+ let lastInRange;
30782
32478
  for (let i = 1; i < curve.samples.length; i++) {
30783
32479
  const a = curve.samples[i - 1];
30784
32480
  const b = curve.samples[i];
@@ -30790,19 +32486,97 @@ var init_MathCanvas = __esm({
30790
32486
  x2: mapX(b.x),
30791
32487
  y2: mapY(b.y),
30792
32488
  color: curve.color ?? "#2563eb",
30793
- lineWidth: 2
32489
+ lineWidth: 2,
32490
+ dash: curve.dash
32491
+ });
32492
+ lastInRange = b;
32493
+ }
32494
+ if (showCurveLabels && curve.label && lastInRange) {
32495
+ out.push({
32496
+ type: "text",
32497
+ x: mapX(lastInRange.x) + 6,
32498
+ y: mapY(lastInRange.y) - 6,
32499
+ text: curve.label,
32500
+ color: curve.color ?? "#2563eb",
32501
+ fontSize: 11
32502
+ });
32503
+ }
32504
+ }
32505
+ for (const hop of hops) {
32506
+ const x1 = mapX(hop.from);
32507
+ const x2 = mapX(hop.to);
32508
+ const peak = Math.min(36, plotH * 0.3);
32509
+ const color = hop.color ?? "#7c3aed";
32510
+ out.push({
32511
+ type: "ellipse",
32512
+ x: (x1 + x2) / 2,
32513
+ y: xAxisY,
32514
+ width: Math.abs(x2 - x1),
32515
+ height: 2 * peak,
32516
+ startAngle: 180,
32517
+ endAngle: 360,
32518
+ color
32519
+ });
32520
+ const s = Math.sign(hop.to - hop.from);
32521
+ out.push({
32522
+ type: "polygon",
32523
+ points: [
32524
+ { x: x2, y: xAxisY },
32525
+ { x: x2 - 4 * s, y: xAxisY - 7 },
32526
+ { x: x2 + 2 * s, y: xAxisY - 7 }
32527
+ ],
32528
+ fill: color,
32529
+ color
32530
+ });
32531
+ if (hop.label) {
32532
+ out.push({
32533
+ type: "text",
32534
+ x: (x1 + x2) / 2,
32535
+ y: xAxisY - peak - 8,
32536
+ text: hop.label,
32537
+ color: "#111827",
32538
+ fontSize: 10,
32539
+ align: "center"
32540
+ });
32541
+ }
32542
+ }
32543
+ for (const angle of angles) {
32544
+ const radius = angle.radius ?? 0.8;
32545
+ const color = angle.color ?? "#0ea5e9";
32546
+ out.push({
32547
+ type: "ellipse",
32548
+ x: mapX(angle.x),
32549
+ y: mapY(angle.y),
32550
+ width: 2 * radius * plotW / (xMax - xMin),
32551
+ height: 2 * radius * plotH / (yMax - yMin),
32552
+ startAngle: -angle.to,
32553
+ endAngle: -angle.from,
32554
+ color
32555
+ });
32556
+ if (angle.label) {
32557
+ const mid = (angle.from + angle.to) / 2;
32558
+ const rad = mid * Math.PI / 180;
32559
+ out.push({
32560
+ type: "text",
32561
+ x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
32562
+ y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
32563
+ text: angle.label,
32564
+ color: "#111827",
32565
+ fontSize: 11,
32566
+ align: "center"
30794
32567
  });
30795
32568
  }
30796
32569
  }
30797
32570
  for (const p of points) {
30798
32571
  if (p.x < xMin || p.x > xMax || p.y < yMin || p.y > yMax) continue;
32572
+ const isOpen = p.style === "open";
30799
32573
  out.push({
30800
32574
  type: "circle",
30801
32575
  x: mapX(p.x),
30802
32576
  y: mapY(p.y),
30803
32577
  radius: p.radius ?? 4,
30804
32578
  color: p.color ?? "#dc2626",
30805
- fill: p.color ?? "#dc2626"
32579
+ fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
30806
32580
  });
30807
32581
  if (p.label) {
30808
32582
  out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: "#111827", fontSize: 12 });
@@ -30821,7 +32595,28 @@ var init_MathCanvas = __esm({
30821
32595
  }
30822
32596
  out.push(...shapes);
30823
32597
  return out;
30824
- }, [width, height, xMin, xMax, yMin, yMax, showAxes, showGrid, gridStep, curves, points, vectors, shapes]);
32598
+ }, [
32599
+ width,
32600
+ height,
32601
+ xMin,
32602
+ xMax,
32603
+ yMin,
32604
+ yMax,
32605
+ showAxes,
32606
+ showGrid,
32607
+ gridStep,
32608
+ showTickLabels,
32609
+ showCurveLabels,
32610
+ curves,
32611
+ points,
32612
+ vectors,
32613
+ regions,
32614
+ bars,
32615
+ guides,
32616
+ angles,
32617
+ hops,
32618
+ shapes
32619
+ ]);
30825
32620
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
30826
32621
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
30827
32622
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -30830,6 +32625,8 @@ var init_MathCanvas = __esm({
30830
32625
  width,
30831
32626
  height,
30832
32627
  shapes: derivedShapes,
32628
+ readouts,
32629
+ traces,
30833
32630
  interactive,
30834
32631
  animate,
30835
32632
  onShapeClick,
@@ -30841,7 +32638,315 @@ var init_MathCanvas = __esm({
30841
32638
  };
30842
32639
  }
30843
32640
  });
30844
- var physicsLog2, PhysicsCanvas;
32641
+ function formatMeterValue(v) {
32642
+ return Number.isInteger(v) ? String(v) : String(Number(v.toFixed(2)));
32643
+ }
32644
+ function sceneObjectShapes(obj, canvasWidth, canvasHeight) {
32645
+ const out = [];
32646
+ const color = obj.color ?? "#334155";
32647
+ switch (obj.kind) {
32648
+ case "ground": {
32649
+ const xStart = obj.x1 ?? 0;
32650
+ const xEnd = obj.x2 ?? canvasWidth;
32651
+ const y = obj.y ?? 0;
32652
+ out.push({ type: "line", x1: xStart, y1: y, x2: xEnd, y2: y, color, lineWidth: 2 });
32653
+ for (let hx = xStart + 7; hx <= xEnd; hx += 14) {
32654
+ out.push({ type: "line", x1: hx, y1: y, x2: hx - 7, y2: y + 7, color, lineWidth: 1 });
32655
+ }
32656
+ if (obj.label) {
32657
+ out.push({
32658
+ type: "text",
32659
+ x: (xStart + xEnd) / 2,
32660
+ y: y - 10,
32661
+ text: obj.label,
32662
+ color: PHYSICS_LABEL_COLOR,
32663
+ fontSize: 11,
32664
+ align: "center"
32665
+ });
32666
+ }
32667
+ break;
32668
+ }
32669
+ case "wall": {
32670
+ const yStart = obj.y1 ?? 0;
32671
+ const yEnd = obj.y2 ?? canvasHeight;
32672
+ const x = obj.x ?? 0;
32673
+ out.push({ type: "line", x1: x, y1: yStart, x2: x, y2: yEnd, color, lineWidth: 2 });
32674
+ for (let hy = yStart + 7; hy <= yEnd; hy += 14) {
32675
+ out.push({ type: "line", x1: x, y1: hy, x2: x - 7, y2: hy + 7, color, lineWidth: 1 });
32676
+ }
32677
+ if (obj.label) {
32678
+ out.push({
32679
+ type: "text",
32680
+ x: x + 12,
32681
+ y: (yStart + yEnd) / 2,
32682
+ text: obj.label,
32683
+ color: PHYSICS_LABEL_COLOR,
32684
+ fontSize: 11,
32685
+ align: "left"
32686
+ });
32687
+ }
32688
+ break;
32689
+ }
32690
+ case "ramp": {
32691
+ const x1 = obj.x1 ?? 0;
32692
+ const y1 = obj.y1 ?? 0;
32693
+ const x2 = obj.x2 ?? canvasWidth;
32694
+ const y2 = obj.y2 ?? canvasHeight;
32695
+ out.push({
32696
+ type: "polygon",
32697
+ points: [
32698
+ { x: x1, y: y1 },
32699
+ { x: x2, y: y2 },
32700
+ { x: x1, y: y2 }
32701
+ ],
32702
+ color,
32703
+ fill: obj.fill ?? "#e2e8f0",
32704
+ lineWidth: 2
32705
+ });
32706
+ if (obj.label) {
32707
+ out.push({
32708
+ type: "text",
32709
+ x: (2 * x1 + x2) / 3,
32710
+ y: (y1 + 2 * y2) / 3,
32711
+ text: obj.label,
32712
+ color: PHYSICS_LABEL_COLOR,
32713
+ fontSize: 11,
32714
+ align: "center"
32715
+ });
32716
+ }
32717
+ break;
32718
+ }
32719
+ case "box": {
32720
+ const x = obj.x ?? 0;
32721
+ const y = obj.y ?? 0;
32722
+ const w = obj.width ?? 40;
32723
+ const h = obj.height ?? 40;
32724
+ out.push({ type: "rect", x, y, width: w, height: h, color, fill: obj.fill, lineWidth: 2 });
32725
+ if (obj.label) {
32726
+ out.push({
32727
+ type: "text",
32728
+ x: x + w / 2,
32729
+ y: y + h / 2,
32730
+ text: obj.label,
32731
+ color: PHYSICS_LABEL_COLOR,
32732
+ fontSize: 11,
32733
+ align: "center"
32734
+ });
32735
+ }
32736
+ break;
32737
+ }
32738
+ case "pivot": {
32739
+ const x = obj.x ?? 0;
32740
+ const y = obj.y ?? 0;
32741
+ out.push({ type: "circle", x, y, radius: 5, color, fill: color });
32742
+ out.push({ type: "line", x1: x - 14, y1: y - 8, x2: x + 14, y2: y - 8, color, lineWidth: 1 });
32743
+ for (let k = 0; k < 5; k++) {
32744
+ const hx = x - 14 + 7 * k;
32745
+ out.push({ type: "line", x1: hx, y1: y - 8, x2: hx - 6, y2: y - 14, color, lineWidth: 1 });
32746
+ }
32747
+ if (obj.label) {
32748
+ out.push({
32749
+ type: "text",
32750
+ x,
32751
+ y: y - 20,
32752
+ text: obj.label,
32753
+ color: PHYSICS_LABEL_COLOR,
32754
+ fontSize: 11,
32755
+ align: "center"
32756
+ });
32757
+ }
32758
+ break;
32759
+ }
32760
+ }
32761
+ return out;
32762
+ }
32763
+ function trailShapes(trail) {
32764
+ const n = trail.points.length;
32765
+ if (n < 2) return [];
32766
+ const color = trail.color ?? "#94a3b8";
32767
+ const lineWidth = trail.width ?? 2;
32768
+ const fade = trail.fade ?? true;
32769
+ const globalOpacity = trail.opacity ?? 1;
32770
+ const out = [];
32771
+ for (let i = 0; i < n - 1; i++) {
32772
+ const a = trail.points[i];
32773
+ const b = trail.points[i + 1];
32774
+ const segmentOpacity = fade ? 0.12 + 0.68 * i / (n - 1) : 0.6;
32775
+ out.push({
32776
+ type: "line",
32777
+ x1: a.x,
32778
+ y1: a.y,
32779
+ x2: b.x,
32780
+ y2: b.y,
32781
+ color,
32782
+ lineWidth,
32783
+ opacity: segmentOpacity * globalOpacity
32784
+ });
32785
+ }
32786
+ return out;
32787
+ }
32788
+ function constraintShapes(c, a, b) {
32789
+ const color = c.color ?? "#9ca3af";
32790
+ const kind = c.kind ?? "rod";
32791
+ if (kind === "rod") {
32792
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2 }];
32793
+ }
32794
+ if (kind === "string") {
32795
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2, dash: "dashed" }];
32796
+ }
32797
+ const COILS = 8;
32798
+ const AMP = 7;
32799
+ const LEAD = 10;
32800
+ const dx = b.x - a.x;
32801
+ const dy = b.y - a.y;
32802
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
32803
+ const ux = dx / dist;
32804
+ const uy = dy / dist;
32805
+ const perpX = -uy;
32806
+ const perpY = ux;
32807
+ const aPrime = { x: a.x + LEAD * ux, y: a.y + LEAD * uy };
32808
+ const bPrime = { x: b.x - LEAD * ux, y: b.y - LEAD * uy };
32809
+ const m = 2 * COILS;
32810
+ const polyline = [{ x: a.x, y: a.y }, aPrime];
32811
+ for (let j = 1; j <= m; j++) {
32812
+ const t = j / (m + 1);
32813
+ const baseX = aPrime.x + t * (bPrime.x - aPrime.x);
32814
+ const baseY = aPrime.y + t * (bPrime.y - aPrime.y);
32815
+ const sign = j % 2 === 0 ? 1 : -1;
32816
+ polyline.push({ x: baseX + sign * AMP * perpX, y: baseY + sign * AMP * perpY });
32817
+ }
32818
+ polyline.push(bPrime, { x: b.x, y: b.y });
32819
+ const out = [];
32820
+ for (let i = 1; i < polyline.length; i++) {
32821
+ out.push({
32822
+ type: "line",
32823
+ x1: polyline[i - 1].x,
32824
+ y1: polyline[i - 1].y,
32825
+ x2: polyline[i].x,
32826
+ y2: polyline[i].y,
32827
+ color,
32828
+ lineWidth: 2
32829
+ });
32830
+ }
32831
+ return out;
32832
+ }
32833
+ function vectorShapes(v, bodyById) {
32834
+ let ax;
32835
+ let ay;
32836
+ if (v.body) {
32837
+ const anchor = bodyById.get(v.body);
32838
+ if (!anchor) return [];
32839
+ ax = anchor.x;
32840
+ ay = anchor.y;
32841
+ } else {
32842
+ ax = v.x ?? 0;
32843
+ ay = v.y ?? 0;
32844
+ }
32845
+ const scale = v.scale ?? 1;
32846
+ const color = v.color ?? "#dc2626";
32847
+ const tx = ax + v.dx * scale;
32848
+ const ty = ay + v.dy * scale;
32849
+ const out = [{ type: "arrow", x1: ax, y1: ay, x2: tx, y2: ty, color, lineWidth: 2, dash: v.dash }];
32850
+ if (v.label) {
32851
+ const dist = Math.max(1e-6, Math.hypot(tx - ax, ty - ay));
32852
+ const ux = (tx - ax) / dist;
32853
+ const uy = (ty - ay) / dist;
32854
+ out.push({
32855
+ type: "text",
32856
+ x: tx + 8 * ux,
32857
+ y: ty + 8 * uy,
32858
+ text: v.label,
32859
+ color,
32860
+ fontSize: 11,
32861
+ align: "center"
32862
+ });
32863
+ }
32864
+ return out;
32865
+ }
32866
+ function angleMarkerShapes(a) {
32867
+ const radius = a.radius ?? 26;
32868
+ const color = a.color ?? "#0ea5e9";
32869
+ const out = [
32870
+ {
32871
+ type: "ellipse",
32872
+ x: a.x,
32873
+ y: a.y,
32874
+ width: radius * 2,
32875
+ height: radius * 2,
32876
+ startAngle: a.from,
32877
+ endAngle: a.to,
32878
+ color,
32879
+ lineWidth: 2
32880
+ }
32881
+ ];
32882
+ if (a.label) {
32883
+ const mid = (a.from + a.to) / 2 * (Math.PI / 180);
32884
+ out.push({
32885
+ type: "text",
32886
+ x: a.x + (radius + 13) * Math.cos(mid),
32887
+ y: a.y + (radius + 13) * Math.sin(mid),
32888
+ text: a.label,
32889
+ color,
32890
+ fontSize: 11,
32891
+ align: "center"
32892
+ });
32893
+ }
32894
+ return out;
32895
+ }
32896
+ function fieldShapes(field, canvasWidth, canvasHeight) {
32897
+ const spacing = field.spacing ?? 48;
32898
+ const size = field.size ?? 14;
32899
+ const color = field.color ?? "#94a3b8";
32900
+ const regionX = field.x ?? 0;
32901
+ const regionY = field.y ?? 0;
32902
+ const regionW = field.width ?? canvasWidth;
32903
+ const regionH = field.height ?? canvasHeight;
32904
+ const out = [];
32905
+ for (let gx = regionX + spacing / 2; gx < regionX + regionW; gx += spacing) {
32906
+ for (let gy = regionY + spacing / 2; gy < regionY + regionH; gy += spacing) {
32907
+ if (field.kind === "arrows") {
32908
+ const rad = (field.angle ?? 0) * Math.PI / 180;
32909
+ const hx = Math.cos(rad) * size / 2;
32910
+ const hy = Math.sin(rad) * size / 2;
32911
+ out.push({ type: "arrow", x1: gx - hx, y1: gy - hy, x2: gx + hx, y2: gy + hy, color, lineWidth: 2 });
32912
+ } else if (field.kind === "into") {
32913
+ const r2 = size / 3;
32914
+ const d = 0.6 * r2 * Math.SQRT1_2;
32915
+ out.push({ type: "circle", x: gx, y: gy, radius: r2, color });
32916
+ out.push({ type: "line", x1: gx - d, y1: gy - d, x2: gx + d, y2: gy + d, color, lineWidth: 1 });
32917
+ out.push({ type: "line", x1: gx - d, y1: gy + d, x2: gx + d, y2: gy - d, color, lineWidth: 1 });
32918
+ } else {
32919
+ const r2 = size / 3;
32920
+ out.push({ type: "circle", x: gx, y: gy, radius: r2, color });
32921
+ out.push({ type: "circle", x: gx, y: gy, radius: 1.5, color, fill: color });
32922
+ }
32923
+ }
32924
+ }
32925
+ return out;
32926
+ }
32927
+ function meterShapes(meters, canvasHeight) {
32928
+ const n = meters.length;
32929
+ const out = [];
32930
+ const sharedMax = Math.max(1e-6, ...meters.map((m) => m.value));
32931
+ meters.forEach((meter, i) => {
32932
+ const rowY = canvasHeight - 10 - 16 * (n - i);
32933
+ const color = meter.color ?? "#3b82f6";
32934
+ const M = meter.max ?? sharedMax;
32935
+ const w = Math.round(Math.min(1, Math.max(0, meter.value / M)) * 110);
32936
+ out.push({ type: "text", x: 8, y: rowY + 8, text: meter.label, color: PHYSICS_LABEL_COLOR, fontSize: 10 });
32937
+ out.push({ type: "rect", x: 52, y: rowY, width: w, height: 10, color, fill: color });
32938
+ out.push({
32939
+ type: "text",
32940
+ x: 166,
32941
+ y: rowY + 8,
32942
+ text: formatMeterValue(meter.value),
32943
+ color: "#6b7280",
32944
+ fontSize: 9
32945
+ });
32946
+ });
32947
+ return out;
32948
+ }
32949
+ var physicsLog2, PHYSICS_LABEL_COLOR, PhysicsCanvas;
30845
32950
  var init_PhysicsCanvas = __esm({
30846
32951
  "components/learning/molecules/PhysicsCanvas.tsx"() {
30847
32952
  "use client";
@@ -30850,6 +32955,7 @@ var init_PhysicsCanvas = __esm({
30850
32955
  init_LearningCanvas();
30851
32956
  init_learningScene3D();
30852
32957
  physicsLog2 = logger.createLogger("almadar:ui:physics-canvas");
32958
+ PHYSICS_LABEL_COLOR = "#374151";
30853
32959
  PhysicsCanvas = ({
30854
32960
  className,
30855
32961
  width = 600,
@@ -30866,7 +32972,18 @@ var init_PhysicsCanvas = __esm({
30866
32972
  showForces = false,
30867
32973
  velocityScale = 20,
30868
32974
  forceScale = 20,
32975
+ sceneObjects = [],
32976
+ trails = [],
32977
+ vectors = [],
32978
+ surface3d,
32979
+ vectors3d = [],
32980
+ vectorScale = 1,
32981
+ angles = [],
32982
+ field,
32983
+ meters = [],
30869
32984
  shapes = [],
32985
+ readouts,
32986
+ traces,
30870
32987
  showGrid,
30871
32988
  shadows,
30872
32989
  interactive,
@@ -30881,19 +32998,14 @@ var init_PhysicsCanvas = __esm({
30881
32998
  for (const b of bodies) {
30882
32999
  if (b.id) bodyById.set(b.id, b);
30883
33000
  }
33001
+ if (field) out.push(...fieldShapes(field, width, height));
33002
+ for (const obj of sceneObjects) out.push(...sceneObjectShapes(obj, width, height));
33003
+ for (const trail of trails) out.push(...trailShapes(trail));
30884
33004
  for (const c of constraints) {
30885
33005
  const a = bodyById.get(c.from);
30886
33006
  const b = bodyById.get(c.to);
30887
33007
  if (!a || !b) continue;
30888
- out.push({
30889
- type: "line",
30890
- x1: a.x,
30891
- y1: a.y,
30892
- x2: b.x,
30893
- y2: b.y,
30894
- color: c.color ?? "#9ca3af",
30895
- lineWidth: 2
30896
- });
33008
+ out.push(...constraintShapes(c, a, b));
30897
33009
  }
30898
33010
  for (const b of bodies) {
30899
33011
  out.push({
@@ -30938,14 +33050,51 @@ var init_PhysicsCanvas = __esm({
30938
33050
  });
30939
33051
  }
30940
33052
  }
33053
+ for (const v of vectors) out.push(...vectorShapes(v, bodyById));
33054
+ for (const a of angles) out.push(...angleMarkerShapes(a));
33055
+ if (meters.length > 0) out.push(...meterShapes(meters, height));
30941
33056
  out.push(...shapes);
30942
33057
  return out;
30943
- }, [bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes]);
33058
+ }, [
33059
+ bodies,
33060
+ constraints,
33061
+ showVelocity,
33062
+ showForces,
33063
+ velocityScale,
33064
+ forceScale,
33065
+ sceneObjects,
33066
+ trails,
33067
+ vectors,
33068
+ angles,
33069
+ field,
33070
+ meters,
33071
+ shapes,
33072
+ width,
33073
+ height
33074
+ ]);
30944
33075
  const drawables3D = React94.useMemo(() => {
30945
33076
  if (mode !== "3d") return [];
30946
33077
  if (shapes.length > 0) {
30947
33078
  physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
30948
33079
  }
33080
+ if (sceneObjects.length > 0) {
33081
+ physicsLog2.debug("sceneObjects ignored in 3D mode (pixel-authored 2D vocabulary)", { count: sceneObjects.length });
33082
+ }
33083
+ if (vectors.length > 0) {
33084
+ physicsLog2.debug("vectors ignored in 3D mode (pixel-authored 2D vocabulary)", { count: vectors.length });
33085
+ }
33086
+ if (angles.length > 0) {
33087
+ physicsLog2.debug("angles ignored in 3D mode (pixel-authored 2D vocabulary)", { count: angles.length });
33088
+ }
33089
+ if (field) {
33090
+ physicsLog2.debug("field ignored in 3D mode (pixel-authored 2D vocabulary)");
33091
+ }
33092
+ if (meters.length > 0) {
33093
+ physicsLog2.debug("meters ignored in 3D mode (pixel-authored 2D vocabulary)", { count: meters.length });
33094
+ }
33095
+ if (animate) {
33096
+ physicsLog2.debug("animate ignored in 3D mode (motion is entity-state driven)");
33097
+ }
30949
33098
  const out = [];
30950
33099
  const labelColor = labelColorForBackground(backgroundColor);
30951
33100
  const bodyById = /* @__PURE__ */ new Map();
@@ -30993,15 +33142,67 @@ var init_PhysicsCanvas = __esm({
30993
33142
  if (arrow) out.push(arrow);
30994
33143
  }
30995
33144
  }
33145
+ for (const trail of trails) {
33146
+ if (trail.fade !== void 0) {
33147
+ physicsLog2.debug("trail.fade ignored in 3D mode (2D-only fade curve \u2014 3D draws an opaque tube)", { id: trail.id });
33148
+ }
33149
+ const points = trail.points.map((p) => [p.x, p.y, p.z ?? 0]);
33150
+ out.push(
33151
+ ...polylineTube(points, trail.width ?? 0.05, trail.color ?? "#94a3b8", {
33152
+ ...trail.opacity !== void 0 ? { opacity: trail.opacity } : {}
33153
+ })
33154
+ );
33155
+ }
33156
+ if (surface3d) {
33157
+ out.push(...heightFieldMesh(surface3d));
33158
+ }
33159
+ if (vectors3d.length > 0) {
33160
+ out.push(
33161
+ ...arrowField(
33162
+ vectors3d.map((v) => ({
33163
+ id: v.id,
33164
+ from: [v.x, v.y, v.z ?? 0],
33165
+ delta: [v.dx, v.dy, v.dz ?? 0],
33166
+ color: v.color,
33167
+ label: v.label,
33168
+ width: v.width
33169
+ })),
33170
+ { scale: vectorScale, labelColor }
33171
+ )
33172
+ );
33173
+ }
30996
33174
  return out;
30997
- }, [mode, bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes, backgroundColor]);
33175
+ }, [
33176
+ mode,
33177
+ bodies,
33178
+ constraints,
33179
+ showVelocity,
33180
+ showForces,
33181
+ velocityScale,
33182
+ forceScale,
33183
+ shapes,
33184
+ sceneObjects,
33185
+ trails,
33186
+ vectors,
33187
+ surface3d,
33188
+ vectors3d,
33189
+ vectorScale,
33190
+ angles,
33191
+ field,
33192
+ meters,
33193
+ animate,
33194
+ backgroundColor
33195
+ ]);
30998
33196
  const bodyIndexById = React94.useMemo(() => {
30999
33197
  const m = /* @__PURE__ */ new Map();
33198
+ vectors3d.forEach((v, i) => {
33199
+ if (v.id) m.set(v.id, i);
33200
+ });
31000
33201
  bodies.forEach((b, i) => {
31001
33202
  if (b.id) m.set(b.id, i);
31002
33203
  });
31003
33204
  return m;
31004
- }, [bodies]);
33205
+ }, [bodies, vectors3d]);
31005
33206
  if (mode === "3d") {
31006
33207
  return /* @__PURE__ */ jsxRuntime.jsx(
31007
33208
  LearningScene3D,
@@ -31033,6 +33234,8 @@ var init_PhysicsCanvas = __esm({
31033
33234
  height,
31034
33235
  backgroundColor,
31035
33236
  shapes: derivedShapes,
33237
+ readouts,
33238
+ traces,
31036
33239
  interactive: interactive ?? false,
31037
33240
  animate,
31038
33241
  onShapeClick,
@@ -31183,7 +33386,7 @@ function layoutFlow(nodeIds, adjacency, roots, width, height, margin) {
31183
33386
  }
31184
33387
  return nodeIds.map((id) => positions.get(id));
31185
33388
  }
31186
- function layoutTree2(nodeIds, adjacency, roots, width, height, margin) {
33389
+ function layoutTree3(nodeIds, adjacency, roots, width, height, margin) {
31187
33390
  const effectiveRoots = roots.length > 0 ? roots : [nodeIds[0]];
31188
33391
  const layers = assignLayers(nodeIds, adjacency, effectiveRoots);
31189
33392
  const maxLayer = Math.max(...Array.from(layers.values()));
@@ -31239,7 +33442,7 @@ function computeStaticLayout(mode, input) {
31239
33442
  const adjacency = buildAdjacency(nodeIds, edges);
31240
33443
  const roots = findRoots(nodeIds, adjacency);
31241
33444
  if (mode === "flow") return layoutFlow(nodeIds, adjacency, roots, width, height, margin);
31242
- if (mode === "tree") return layoutTree2(nodeIds, adjacency, roots, width, height, margin);
33445
+ if (mode === "tree") return layoutTree3(nodeIds, adjacency, roots, width, height, margin);
31243
33446
  return layoutRadial(nodeIds, adjacency, roots, width, height, margin);
31244
33447
  }
31245
33448
  var init_graphViewLayouts = __esm({
@@ -32482,26 +34685,6 @@ var init_Lightbox = __esm({
32482
34685
  Lightbox.displayName = "Lightbox";
32483
34686
  }
32484
34687
  });
32485
- function useMediaQuery(query) {
32486
- const subscribe = React94.useCallback(
32487
- (onChange) => {
32488
- const mql = window.matchMedia(query);
32489
- mql.addEventListener("change", onChange);
32490
- return () => mql.removeEventListener("change", onChange);
32491
- },
32492
- [query]
32493
- );
32494
- return React94.useSyncExternalStore(
32495
- subscribe,
32496
- () => window.matchMedia(query).matches,
32497
- () => false
32498
- );
32499
- }
32500
- var init_useMediaQuery = __esm({
32501
- "hooks/useMediaQuery.ts"() {
32502
- "use client";
32503
- }
32504
- });
32505
34688
  function renderIconInput3(icon, props) {
32506
34689
  return typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, ...props });
32507
34690
  }
@@ -32540,8 +34723,8 @@ function TableView({
32540
34723
  columns,
32541
34724
  fields,
32542
34725
  itemActions,
32543
- maxInlineActions,
32544
- itemClickEvent,
34726
+ maxInlineActions: _maxInlineActions,
34727
+ itemClickEvent = "",
32545
34728
  selectable = false,
32546
34729
  selectEvent,
32547
34730
  selectedIds,
@@ -32591,7 +34774,6 @@ function TableView({
32591
34774
  const hasMore = pageSize > 0 && visibleCount < ordered2.length;
32592
34775
  const hasRenderProp = typeof children === "function";
32593
34776
  const idField = dndItemIdField ?? "id";
32594
- const isCoarsePointer = useMediaQuery("(pointer: coarse)");
32595
34777
  React94__namespace.default.useEffect(() => {
32596
34778
  tableViewLog.debug("render", {
32597
34779
  rowCount: data.length,
@@ -32633,21 +34815,14 @@ function TableView({
32633
34815
  const dir = sortColumn === (col.field ?? col.key) && sortDirection === "asc" ? "desc" : "asc";
32634
34816
  eventBus.emit(`UI:${sortEvent}`, { column: col.field ?? col.key, direction: dir });
32635
34817
  };
32636
- const handleActionClick = (action, row) => (e) => {
32637
- e.stopPropagation();
32638
- const payload = {
32639
- id: row.id,
32640
- row
32641
- };
32642
- eventBus.emit(`UI:${action.event}`, payload);
32643
- };
34818
+ const rowClickEvent = itemClickEvent || actionDefs.find((a) => a.variant !== "danger")?.event;
32644
34819
  const handleRowClick = (row) => () => {
32645
- if (!itemClickEvent) return;
34820
+ if (!rowClickEvent) return;
32646
34821
  const payload = {
32647
34822
  id: row.id,
32648
34823
  row
32649
34824
  };
32650
- eventBus.emit(`UI:${itemClickEvent}`, payload);
34825
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
32651
34826
  };
32652
34827
  const colFloors = React94__namespace.default.useMemo(
32653
34828
  () => colDefs.map((col) => {
@@ -32663,10 +34838,7 @@ function TableView({
32663
34838
  const statusNode = isLoading ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) }) : error ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "error", children: error.message }) }) : data.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-12", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: emptyMessage || t("empty.noItems") }) }) : null;
32664
34839
  const lk = LOOKS[look];
32665
34840
  const hasActions = actionDefs.length > 0;
32666
- const effectiveMaxInline = isCoarsePointer ? 0 : maxInlineActions;
32667
- const inlineActionCount = hasActions ? effectiveMaxInline != null ? Math.min(actionDefs.length, effectiveMaxInline) : actionDefs.length : 0;
32668
- const hasOverflowActions = hasActions && effectiveMaxInline != null && actionDefs.length > effectiveMaxInline;
32669
- const actionsTrack = hasActions ? `${inlineActionCount * 6 + (hasOverflowActions ? 3 : 0)}rem` : null;
34841
+ const actionsTrack = hasActions ? "3rem" : null;
32670
34842
  const gridTemplateColumns = [
32671
34843
  selectable ? "auto" : null,
32672
34844
  ...colDefs.map((c, i) => c.width ?? `minmax(${colFloors[i]}ch, 1fr)`),
@@ -32713,7 +34885,7 @@ function TableView({
32713
34885
  col.key
32714
34886
  );
32715
34887
  }),
32716
- hasActions && /* @__PURE__ */ jsxRuntime.jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)]" })
34888
+ hasActions && /* @__PURE__ */ jsxRuntime.jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)] border-l border-[var(--color-border)] h-full" })
32717
34889
  ]
32718
34890
  }
32719
34891
  );
@@ -32725,12 +34897,12 @@ function TableView({
32725
34897
  role: "row",
32726
34898
  "data-entity-row": true,
32727
34899
  "data-entity-id": id,
32728
- onClick: itemClickEvent ? handleRowClick(row) : void 0,
34900
+ onClick: rowClickEvent ? handleRowClick(row) : void 0,
32729
34901
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
32730
34902
  className: cn(
32731
34903
  "group items-center gap-3 transition-colors duration-fast",
32732
34904
  hasRenderProp ? "flex" : "grid",
32733
- itemClickEvent && "cursor-pointer",
34905
+ rowClickEvent && "cursor-pointer",
32734
34906
  lk.rowPad,
32735
34907
  lk.divider && "border-b border-[var(--color-border)]",
32736
34908
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -32738,7 +34910,7 @@ function TableView({
32738
34910
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
32739
34911
  ),
32740
34912
  children: [
32741
- selectable && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex items-center", onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsxRuntime.jsx(
34913
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex items-center", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsxRuntime.jsx(
32742
34914
  Checkbox,
32743
34915
  {
32744
34916
  checked: selected.has(id),
@@ -32759,53 +34931,37 @@ function TableView({
32759
34931
  }
32760
34932
  return /* @__PURE__ */ jsxRuntime.jsx(Box, { role: "cell", className: cellBase, children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-foreground", children: formatCell(raw, col.format) }) }, col.key);
32761
34933
  }),
32762
- hasActions && /* @__PURE__ */ jsxRuntime.jsxs(
34934
+ hasActions && /* @__PURE__ */ jsxRuntime.jsx(
32763
34935
  HStack,
32764
34936
  {
32765
34937
  gap: "xs",
32766
- onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
34938
+ onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0,
32767
34939
  className: cn(
32768
34940
  // Pinned: the fixed column tracks routinely overflow the caller's
32769
- // scroll container, which used to leave the actions off-screen.
32770
- // Opaque so scrolled cells pass underneath it.
34941
+ // scroll container, which would leave the kebab off-screen.
34942
+ // Opaque + hairline edge so it reads as a pinned column, not a
34943
+ // floating control, while scrolled cells pass underneath.
32771
34944
  "justify-end flex-shrink-0 sticky right-0 z-[1] transition-colors",
34945
+ "border-l border-[var(--color-border)]",
32772
34946
  lk.striped && index % 2 === 1 ? "bg-[var(--color-surface-subtle)]" : "bg-[var(--color-card)] group-hover:bg-[var(--color-surface-subtle)]"
32773
34947
  ),
32774
- children: [
32775
- (effectiveMaxInline != null ? actionDefs.slice(0, effectiveMaxInline) : actionDefs).map((action, i) => /* @__PURE__ */ jsxRuntime.jsxs(
32776
- Button,
32777
- {
32778
- variant: action.variant === "primary" ? "primary" : "ghost",
32779
- size: "sm",
32780
- onClick: handleActionClick(action, row),
32781
- "data-testid": `action-${action.event}`,
32782
- "data-row-id": String(row.id),
32783
- className: cn(action.variant === "danger" && "text-error hover:text-error hover:bg-error/10"),
32784
- children: [
32785
- action.icon && renderIconInput3(action.icon, { size: "xs", className: "mr-1" }),
32786
- action.label
32787
- ]
32788
- },
32789
- i
32790
- )),
32791
- effectiveMaxInline != null && actionDefs.length > effectiveMaxInline && /* @__PURE__ */ jsxRuntime.jsx(
32792
- Menu,
32793
- {
32794
- position: "bottom-end",
32795
- trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
32796
- items: actionDefs.slice(effectiveMaxInline).map((action) => ({
32797
- label: action.label,
32798
- icon: action.icon,
32799
- event: action.event,
32800
- variant: action.variant === "danger" ? "danger" : "default",
32801
- onClick: () => eventBus.emit(`UI:${action.event}`, {
32802
- id: row.id,
32803
- row
32804
- })
32805
- }))
32806
- }
32807
- )
32808
- ]
34948
+ children: /* @__PURE__ */ jsxRuntime.jsx(
34949
+ Menu,
34950
+ {
34951
+ position: "bottom-end",
34952
+ trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", "data-row-id": String(row.id), children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
34953
+ items: actionDefs.map((action) => ({
34954
+ label: action.label,
34955
+ icon: action.icon,
34956
+ event: action.event,
34957
+ variant: action.variant === "danger" ? "danger" : "default",
34958
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
34959
+ id: row.id,
34960
+ row
34961
+ })
34962
+ }))
34963
+ }
34964
+ )
32809
34965
  }
32810
34966
  )
32811
34967
  ]
@@ -32848,7 +35004,6 @@ var init_TableView = __esm({
32848
35004
  init_format();
32849
35005
  init_getNestedValue();
32850
35006
  init_useEventBus();
32851
- init_useMediaQuery();
32852
35007
  init_Box();
32853
35008
  init_Stack();
32854
35009
  init_Typography();
@@ -47517,6 +49672,7 @@ var init_component_registry_generated = __esm({
47517
49672
  init_ActionTile();
47518
49673
  init_ActivationBlock();
47519
49674
  init_ComponentPatterns();
49675
+ init_AlgoGraphCanvas();
47520
49676
  init_AlgorithmCanvas();
47521
49677
  init_AnimatedCounter();
47522
49678
  init_AnimatedGraphic();
@@ -47780,6 +49936,7 @@ var init_component_registry_generated = __esm({
47780
49936
  "ActivationBlock": ActivationBlock,
47781
49937
  "Alert": AlertPattern,
47782
49938
  "AlertPattern": AlertPattern,
49939
+ "AlgoGraphCanvas": AlgoGraphCanvas,
47783
49940
  "AlgorithmCanvas": AlgorithmCanvas,
47784
49941
  "AnimatedCounter": AnimatedCounter,
47785
49942
  "AnimatedGraphic": AnimatedGraphic,
@@ -52652,9 +54809,9 @@ var log8 = logger.createLogger("almadar:ui:effects:client-handlers");
52652
54809
  function createClientEffectHandlers(options) {
52653
54810
  const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
52654
54811
  return {
52655
- emit: (event, payload) => {
54812
+ emit: (event, payload, source) => {
52656
54813
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
52657
- eventBus.emit(prefixedEvent, payload);
54814
+ eventBus.emit(prefixedEvent, payload, source);
52658
54815
  },
52659
54816
  persist: async () => {
52660
54817
  log8.warn("persist is server-side only, ignored on client");
@@ -52819,7 +54976,7 @@ function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
52819
54976
  }
52820
54977
  };
52821
54978
  ctx.emit = (event, payload) => {
52822
- emit(event, payload);
54979
+ emit(event, payload, { trait: traitName, tick: tick.name });
52823
54980
  };
52824
54981
  if (tick.guard !== void 0 && !evaluator.evaluateGuard(tick.guard, ctx)) {
52825
54982
  tickLog.debug("guard-blocked", { traitName, tick: tick.name, state: currentState });
@@ -53271,6 +55428,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
53271
55428
  }
53272
55429
  const effectContext = {
53273
55430
  traitName,
55431
+ orbitalName: orbitalsByTrait?.[traitName],
53274
55432
  state: previousState,
53275
55433
  transition: `${previousState}->${newState}`,
53276
55434
  linkedEntity,
@@ -53347,7 +55505,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
53347
55505
  });
53348
55506
  }
53349
55507
  return emittedDuringExec;
53350
- }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot]);
55508
+ }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot, orbitalsByTrait]);
53351
55509
  const runTickEffects = React94.useCallback((tick, binding) => {
53352
55510
  const traitName = binding.trait.name;
53353
55511
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
@@ -53383,9 +55541,9 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
53383
55541
  log: tickLog
53384
55542
  });
53385
55543
  }, [executeTransitionEffects, sharedEntityStore]);
53386
- const emitFromSharedWriter = React94.useCallback((event, payload) => {
55544
+ const emitFromSharedWriter = React94.useCallback((event, payload, source) => {
53387
55545
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
53388
- eventBus.emit(prefixedEvent, payload);
55546
+ eventBus.emit(prefixedEvent, payload, source);
53389
55547
  }, [eventBus]);
53390
55548
  React94.useEffect(() => {
53391
55549
  const scheduler = runtime.createTickScheduler();