@almadar/ui 5.148.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.
Files changed (35) hide show
  1. package/dist/{GameAudioProvider-CPGwD49P.d.cts → GameAudioProvider-B48iXwz3.d.ts} +2 -50
  2. package/dist/{GameAudioProvider-CPGwD49P.d.ts → GameAudioProvider-CQAdPreB.d.cts} +2 -50
  3. package/dist/avl/index.cjs +2741 -158
  4. package/dist/avl/index.js +2741 -158
  5. package/dist/{avl-schema-parser-DVmrgdwc.d.cts → avl-schema-parser-CVzkNzg7.d.cts} +18 -9
  6. package/dist/{avl-schema-parser-BRJ77Yze.d.ts → avl-schema-parser-Cx_4SVg9.d.ts} +18 -9
  7. package/dist/{cn-BAn68sNO.d.cts → cn-BnAJZcNb.d.cts} +2 -8
  8. package/dist/{cn-CWxxLkri.d.ts → cn-DJTUjk1M.d.ts} +2 -8
  9. package/dist/components/index.cjs +2764 -158
  10. package/dist/components/index.d.cts +1077 -107
  11. package/dist/components/index.d.ts +1077 -107
  12. package/dist/components/index.js +2760 -160
  13. package/dist/hooks/index.cjs +1 -1
  14. package/dist/hooks/index.js +1 -1
  15. package/dist/lib/drawable/three/index.cjs +57 -30
  16. package/dist/lib/drawable/three/index.d.cts +4 -3
  17. package/dist/lib/drawable/three/index.d.ts +4 -3
  18. package/dist/lib/drawable/three/index.js +57 -30
  19. package/dist/lib/index.d.cts +3 -2
  20. package/dist/lib/index.d.ts +3 -2
  21. package/dist/marketing/index.cjs +1 -1
  22. package/dist/marketing/index.js +1 -1
  23. package/dist/{paintDispatch-Cb_hQj4Y.d.ts → paintDispatch-BjZjUbcb.d.cts} +63 -69
  24. package/dist/{paintDispatch-Cb_hQj4Y.d.cts → paintDispatch-CxdLjHQq.d.ts} +63 -69
  25. package/dist/providers/index.cjs +2734 -152
  26. package/dist/providers/index.d.cts +2 -1
  27. package/dist/providers/index.d.ts +2 -1
  28. package/dist/providers/index.js +2734 -152
  29. package/dist/runtime/index.cjs +2741 -158
  30. package/dist/runtime/index.d.cts +2 -2
  31. package/dist/runtime/index.d.ts +2 -2
  32. package/dist/runtime/index.js +2741 -158
  33. package/dist/types-B3nHgnMG.d.cts +51 -0
  34. package/dist/types-B3nHgnMG.d.ts +51 -0
  35. package/package.json +3 -3
@@ -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);
@@ -14117,9 +14117,10 @@ function Canvas({
14117
14117
  drawables: [...drawables ?? [], ...childDrawables],
14118
14118
  isLoading,
14119
14119
  cameraMode: to3DCameraMode(camera?.mode),
14120
- ...zoom !== void 0 ? { scale: zoom } : {},
14120
+ ...zoom !== void 0 ? { zoom } : {},
14121
14121
  ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
14122
14122
  ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
14123
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
14123
14124
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
14124
14125
  unitScale,
14125
14126
  backgroundColor,
@@ -15120,6 +15121,14 @@ function shapeBounds(shape) {
15120
15121
  w: shape.radius * 2 + 8,
15121
15122
  h: shape.radius * 2 + 8
15122
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
+ };
15123
15132
  case "rect":
15124
15133
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
15125
15134
  return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
@@ -15153,13 +15162,14 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
15153
15162
  ctx.closePath();
15154
15163
  ctx.fill();
15155
15164
  }
15156
- function drawShape(ctx, shape, width, height) {
15165
+ function drawShape(ctx, shape, width, height, allShapes) {
15157
15166
  ctx.save();
15158
15167
  const opacity = shape.opacity ?? 1;
15159
15168
  ctx.globalAlpha = opacity;
15160
15169
  const stroke = resolveColor2(shape.color, ctx, "#333333");
15161
15170
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
15162
15171
  ctx.lineWidth = shape.lineWidth ?? 2;
15172
+ if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
15163
15173
  switch (shape.type) {
15164
15174
  case "grid": {
15165
15175
  const step = shape.step ?? 40;
@@ -15225,6 +15235,20 @@ function drawShape(ctx, shape, width, height) {
15225
15235
  ctx.stroke();
15226
15236
  break;
15227
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
+ }
15228
15252
  case "rect": {
15229
15253
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
15230
15254
  if (fill) {
@@ -15271,21 +15295,153 @@ function drawShape(ctx, shape, width, height) {
15271
15295
  ctx.fillText(shape.text, shape.x, shape.y);
15272
15296
  break;
15273
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
+ }
15274
15331
  }
15275
15332
  ctx.restore();
15276
15333
  }
15277
- 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;
15278
15430
  var init_LearningCanvas = __esm({
15279
15431
  "components/learning/atoms/LearningCanvas.tsx"() {
15280
15432
  "use client";
15281
15433
  init_cn();
15282
15434
  init_useEventBus();
15435
+ DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
15436
+ TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
15283
15437
  LearningCanvas = ({
15284
15438
  className,
15285
15439
  width = 600,
15286
15440
  height = 400,
15287
15441
  backgroundColor,
15288
15442
  shapes = [],
15443
+ readouts,
15444
+ traces,
15289
15445
  interactive = false,
15290
15446
  animate = false,
15291
15447
  onShapeClick,
@@ -15311,6 +15467,12 @@ var init_LearningCanvas = __esm({
15311
15467
  }
15312
15468
  return -1;
15313
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]);
15314
15476
  const draw = React94.useCallback(() => {
15315
15477
  const canvas = canvasRef.current;
15316
15478
  if (!canvas) return;
@@ -15327,13 +15489,13 @@ var init_LearningCanvas = __esm({
15327
15489
  ctx.fillStyle = backgroundColor;
15328
15490
  ctx.fillRect(0, 0, width, height);
15329
15491
  }
15330
- for (const shape of shapes) {
15331
- 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);
15332
15494
  }
15333
- for (const shape of shapes) {
15334
- 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);
15335
15497
  }
15336
- }, [width, height, backgroundColor, shapes]);
15498
+ }, [width, height, backgroundColor, derivedShapes]);
15337
15499
  React94.useEffect(() => {
15338
15500
  draw();
15339
15501
  }, [draw]);
@@ -16883,7 +17045,363 @@ var init_ComponentPatterns = __esm({
16883
17045
  AlertPattern.displayName = "AlertPattern";
16884
17046
  }
16885
17047
  });
16886
- 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;
16887
17405
  var init_AlgorithmCanvas = __esm({
16888
17406
  "components/learning/molecules/AlgorithmCanvas.tsx"() {
16889
17407
  "use client";
@@ -16895,6 +17413,43 @@ var init_AlgorithmCanvas = __esm({
16895
17413
  DEFAULT_POINTER_COLOR = "#dc2626";
16896
17414
  POINTER_BAND = 34;
16897
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;
16898
17453
  AlgorithmCanvas = ({
16899
17454
  className,
16900
17455
  width = 600,
@@ -16904,6 +17459,14 @@ var init_AlgorithmCanvas = __esm({
16904
17459
  bars = [],
16905
17460
  cells = [],
16906
17461
  pointers = [],
17462
+ ranges = [],
17463
+ slots = [],
17464
+ slotOrientation = "horizontal",
17465
+ frames = [],
17466
+ buckets = [],
17467
+ auxBars = [],
17468
+ rowLabels = [],
17469
+ colLabels = [],
16907
17470
  shapes = [],
16908
17471
  interactive = false,
16909
17472
  animate = false,
@@ -16913,12 +17476,35 @@ var init_AlgorithmCanvas = __esm({
16913
17476
  }) => {
16914
17477
  const derivedShapes = React94.useMemo(() => {
16915
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
+ });
16916
17496
  if (bars.length > 0) {
17497
+ const panelYBars = panelY.bars;
16917
17498
  const slot = width / bars.length;
16918
17499
  const barW = slot * 0.8;
16919
17500
  const gap = slot * 0.1;
16920
- const baseline = height - POINTER_BAND;
16921
- 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);
16922
17508
  const maxV = Math.max(1, ...bars.map((b) => Number.isFinite(b.value) ? b.value : 0));
16923
17509
  bars.forEach((bar, i) => {
16924
17510
  const v = Number.isFinite(bar.value) ? bar.value : 0;
@@ -16948,6 +17534,89 @@ var init_AlgorithmCanvas = __esm({
16948
17534
  });
16949
17535
  }
16950
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
+ }
16951
17620
  pointers.forEach((p) => {
16952
17621
  if (p.index < 0 || p.index >= bars.length) return;
16953
17622
  const cx = p.index * slot + slot / 2;
@@ -16955,7 +17624,7 @@ var init_AlgorithmCanvas = __esm({
16955
17624
  out.push({
16956
17625
  type: "arrow",
16957
17626
  x1: cx,
16958
- y1: height - 6,
17627
+ y1: panelYBars + primaryH - 18,
16959
17628
  x2: cx,
16960
17629
  y2: baseline + 4,
16961
17630
  color,
@@ -16965,7 +17634,7 @@ var init_AlgorithmCanvas = __esm({
16965
17634
  out.push({
16966
17635
  type: "text",
16967
17636
  x: cx,
16968
- y: height - 22,
17637
+ y: panelYBars + primaryH - 8,
16969
17638
  text: p.label,
16970
17639
  color,
16971
17640
  fontSize: 11,
@@ -16974,14 +17643,111 @@ var init_AlgorithmCanvas = __esm({
16974
17643
  }
16975
17644
  });
16976
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
+ }
16977
17738
  if (cells.length > 0) {
17739
+ const panelYCells = panelY.cells;
16978
17740
  const maxCol = Math.max(0, ...cells.map((c) => c.col)) + 1;
16979
17741
  const maxRow = Math.max(0, ...cells.map((c) => c.row)) + 1;
16980
- const cw = width / maxCol;
16981
- 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;
16982
17748
  cells.forEach((c, i) => {
16983
- const x = c.col * cw;
16984
- const y = c.row * ch;
17749
+ const x = gridX0 + c.col * cw;
17750
+ const y = gridY0 + c.row * ch;
16985
17751
  const color = c.color ?? DEFAULT_CELL_COLOR;
16986
17752
  out.push({
16987
17753
  type: "rect",
@@ -17005,11 +17771,207 @@ var init_AlgorithmCanvas = __esm({
17005
17771
  align: "center"
17006
17772
  });
17007
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
+ }
17008
17970
  });
17009
17971
  }
17010
17972
  out.push(...shapes);
17011
17973
  return out;
17012
- }, [bars, cells, pointers, shapes, width, height]);
17974
+ }, [bars, cells, pointers, ranges, slots, slotOrientation, frames, buckets, auxBars, rowLabels, colLabels, shapes, width, height]);
17013
17975
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
17014
17976
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
17015
17977
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -17171,23 +18133,422 @@ var init_AuthLayout = __esm({
17171
18133
  AuthLayout.displayName = "AuthLayout";
17172
18134
  }
17173
18135
  });
17174
- var BiologyCanvas;
18136
+ function LearningScene3D({
18137
+ className,
18138
+ width = 600,
18139
+ height = 400,
18140
+ title,
18141
+ backgroundColor,
18142
+ drawables,
18143
+ camera,
18144
+ lighting,
18145
+ post,
18146
+ showGrid = false,
18147
+ shadows,
18148
+ interactive,
18149
+ isLoading,
18150
+ error,
18151
+ onItemClick
18152
+ }) {
18153
+ const instanceId = React94.useId().replace(/[^a-zA-Z0-9]/g, "");
18154
+ const clickEvent = onItemClick ? `LEARNING_SCENE_3D.ITEM_CLICK.${instanceId}` : void 0;
18155
+ const onItemClickRef = React94.useRef(onItemClick);
18156
+ onItemClickRef.current = onItemClick;
18157
+ useEventListener(`UI:${clickEvent ?? "LEARNING_SCENE_3D.ITEM_CLICK.__disabled"}`, (event) => {
18158
+ const unitId = event.payload?.unitId;
18159
+ if (typeof unitId === "string") onItemClickRef.current?.(unitId);
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
+ }
18167
+ const props3d = {
18168
+ drawables,
18169
+ isLoading,
18170
+ error: error ? error.message : null,
18171
+ cameraMode: camera?.mode ?? "perspective",
18172
+ ...camera?.zoom !== void 0 ? { zoom: camera.zoom } : {},
18173
+ ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
18174
+ ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
18175
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
18176
+ ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
18177
+ backgroundColor,
18178
+ showGrid,
18179
+ ...shadows !== void 0 ? { shadows } : {},
18180
+ ...interactive !== void 0 ? { controlsEnabled: interactive } : {},
18181
+ lighting,
18182
+ post,
18183
+ ...clickEvent !== void 0 ? { unitClickEvent: clickEvent } : {}
18184
+ };
18185
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
18186
+ title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
18187
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width, height, display: "flex" }, children: /* @__PURE__ */ jsxRuntime.jsx(React94.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(Canvas3DHost2, { ...props3d }) }) })
18188
+ ] }) });
18189
+ }
18190
+ function meshSphere(id, x, y, z, radius, color, opts) {
18191
+ return {
18192
+ type: "draw-mesh",
18193
+ ...id !== void 0 ? { id } : {},
18194
+ shape: opts?.shape ?? "sphere",
18195
+ position: { x, y, z },
18196
+ radius,
18197
+ pivot: "center",
18198
+ segments: opts?.segments ?? 24,
18199
+ material: { color, ...opts?.material },
18200
+ ...opts?.opacity !== void 0 ? { opacity: opts.opacity } : {}
18201
+ };
18202
+ }
18203
+ function axisRotation(from, to) {
18204
+ const wx = to[0] - from[0];
18205
+ const wy = to[2] - from[2];
18206
+ const wz = to[1] - from[1];
18207
+ const len = Math.sqrt(wx * wx + wy * wy + wz * wz);
18208
+ const tilt = Math.acos(Math.min(1, Math.max(-1, len > 0 ? wy / len : 1)));
18209
+ return [0, Math.atan2(wz, -wx), tilt];
18210
+ }
18211
+ function segmentLength(from, to) {
18212
+ const dx = to[0] - from[0];
18213
+ const dy = to[1] - from[1];
18214
+ const dz = to[2] - from[2];
18215
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
18216
+ }
18217
+ function cylinderBetween(from, to, radius, color) {
18218
+ const len = segmentLength(from, to);
18219
+ if (len < 1e-6) return null;
18220
+ return {
18221
+ type: "draw-mesh",
18222
+ shape: "cylinder",
18223
+ position: { x: (from[0] + to[0]) / 2, y: (from[1] + to[1]) / 2, z: (from[2] + to[2]) / 2 },
18224
+ radius,
18225
+ height: len,
18226
+ rotation: axisRotation(from, to),
18227
+ pivot: "center",
18228
+ segments: 12,
18229
+ material: { color }
18230
+ };
18231
+ }
18232
+ function arrowBetween(from, to, color, shaftRadius = 0.08, id) {
18233
+ const len = segmentLength(from, to);
18234
+ if (len < 1e-6) return null;
18235
+ const tipLen = Math.min(shaftRadius * 8, len * 0.35);
18236
+ const k = (len - tipLen) / len;
18237
+ const delta = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
18238
+ const shaftEnd = [delta[0] * k, delta[1] * k, delta[2] * k];
18239
+ const tipStart = shaftEnd;
18240
+ const tipEnd = delta;
18241
+ const shaft = cylinderBetween([0, 0, 0], shaftEnd, shaftRadius, color);
18242
+ const tipLenActual = segmentLength(tipStart, tipEnd);
18243
+ const tip = {
18244
+ type: "draw-mesh",
18245
+ shape: "cone",
18246
+ position: {
18247
+ x: (tipStart[0] + tipEnd[0]) / 2,
18248
+ y: (tipStart[1] + tipEnd[1]) / 2,
18249
+ z: (tipStart[2] + tipEnd[2]) / 2
18250
+ },
18251
+ radius: shaftRadius * 3,
18252
+ height: tipLenActual,
18253
+ rotation: axisRotation(tipStart, tipEnd),
18254
+ pivot: "center",
18255
+ segments: 12,
18256
+ material: { color }
18257
+ };
18258
+ return {
18259
+ type: "draw-group",
18260
+ ...id !== void 0 ? { id } : {},
18261
+ position: { x: from[0], y: from[1], z: from[2] },
18262
+ items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
18263
+ };
18264
+ }
18265
+ function billboardLabel(text, x, y, z, opts) {
18266
+ return {
18267
+ type: "draw-text",
18268
+ text,
18269
+ position: { x, y, z },
18270
+ color: opts?.color ?? "#111827"
18271
+ };
18272
+ }
18273
+ function labelColorForBackground(backgroundColor) {
18274
+ const m = /^#(?:([0-9a-f]{3})|([0-9a-f]{6}))$/i.exec(backgroundColor ?? "");
18275
+ if (!m) return "#111827";
18276
+ const hex = m[1] !== void 0 ? m[1].split("").map((c) => c + c).join("") : m[2];
18277
+ const r2 = parseInt(hex.slice(0, 2), 16) / 255;
18278
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
18279
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
18280
+ const luminance = 0.2126 * r2 + 0.7152 * g + 0.0722 * b;
18281
+ return luminance < 0.5 ? "#e5e7eb" : "#111827";
18282
+ }
18283
+ function get3DClickPayload(onShapeClick, idToIndex) {
18284
+ if (!onShapeClick) return void 0;
18285
+ return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
18286
+ }
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;
18494
+ var init_learningScene3D = __esm({
18495
+ "components/learning/molecules/learningScene3D.tsx"() {
18496
+ "use client";
18497
+ init_atoms();
18498
+ init_Stack();
18499
+ init_useEventBus();
18500
+ sceneLog = logger.createLogger("almadar:ui:learning-scene-3d");
18501
+ KNOWN_BLOOM_KEYS = /* @__PURE__ */ new Set(["intensity", "threshold", "smoothing"]);
18502
+ Canvas3DHost2 = React94.lazy(
18503
+ () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
18504
+ );
18505
+ LearningScene3D.displayName = "LearningScene3D";
18506
+ }
18507
+ });
18508
+ var biologyLog, BIO_BAND_COLORS, BIO_STAGE_FILL, BIO_STAGE_TEXT, BiologyCanvas;
17175
18509
  var init_BiologyCanvas = __esm({
17176
18510
  "components/learning/molecules/BiologyCanvas.tsx"() {
17177
18511
  "use client";
17178
18512
  init_atoms();
17179
18513
  init_Stack();
17180
18514
  init_LearningCanvas();
18515
+ init_learningScene3D();
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
+ };
17181
18528
  BiologyCanvas = ({
17182
18529
  className,
17183
18530
  width = 600,
17184
18531
  height = 400,
17185
18532
  title,
17186
18533
  backgroundColor,
18534
+ mode = "2d",
18535
+ camera,
18536
+ lighting,
18537
+ post,
17187
18538
  nodes = [],
17188
18539
  edges = [],
18540
+ compartments = [],
18541
+ bands = [],
18542
+ stages = [],
18543
+ stageStyle = "timeline",
18544
+ helix,
18545
+ helix3d,
17189
18546
  shapes = [],
17190
- interactive = false,
18547
+ readouts,
18548
+ traces,
18549
+ showGrid,
18550
+ shadows,
18551
+ interactive,
17191
18552
  animate = false,
17192
18553
  onShapeClick,
17193
18554
  isLoading,
@@ -17199,19 +18560,148 @@ var init_BiologyCanvas = __esm({
17199
18560
  for (const n of nodes) {
17200
18561
  if (n.id) nodeById.set(n.id, n);
17201
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
+ }
17202
18672
  for (const e of edges) {
17203
18673
  const a = nodeById.get(e.from);
17204
18674
  const b = nodeById.get(e.to);
17205
18675
  if (!a || !b) continue;
17206
- out.push({
17207
- type: "line",
17208
- x1: a.x,
17209
- y1: a.y,
17210
- x2: b.x,
17211
- y2: b.y,
17212
- color: e.color ?? "#9ca3af",
17213
- lineWidth: 2
17214
- });
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
+ }
17215
18705
  if (e.label) {
17216
18706
  out.push({
17217
18707
  type: "text",
@@ -17224,6 +18714,8 @@ var init_BiologyCanvas = __esm({
17224
18714
  }
17225
18715
  }
17226
18716
  for (const n of nodes) {
18717
+ const state = n.state ?? "default";
18718
+ const muted = state === "muted";
17227
18719
  out.push({
17228
18720
  type: "circle",
17229
18721
  x: n.x,
@@ -17231,23 +18723,197 @@ var init_BiologyCanvas = __esm({
17231
18723
  radius: n.radius ?? 16,
17232
18724
  color: n.color ?? "#16a34a",
17233
18725
  fill: `${n.color ?? "#16a34a"}33`,
17234
- id: n.id
18726
+ id: n.id,
18727
+ ...muted ? { opacity: 0.35 } : {}
17235
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
+ }
17236
18739
  if (n.label) {
17237
18740
  out.push({
17238
18741
  type: "text",
17239
18742
  x: n.x,
17240
18743
  y: n.y + (n.radius ?? 16) + 14,
17241
18744
  text: n.label,
18745
+ ...muted ? { opacity: 0.35 } : {},
17242
18746
  color: "#111827",
17243
18747
  fontSize: 12,
17244
18748
  align: "center"
17245
18749
  });
17246
18750
  }
17247
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
+ }
17248
18828
  out.push(...shapes);
17249
18829
  return out;
17250
- }, [nodes, edges, shapes]);
18830
+ }, [nodes, edges, compartments, bands, stages, stageStyle, helix, shapes, width, height]);
18831
+ const drawables3D = React94.useMemo(() => {
18832
+ if (mode !== "3d") return [];
18833
+ if (shapes.length > 0) {
18834
+ biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
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
+ }
18847
+ const out = [];
18848
+ const labelColor = labelColorForBackground(backgroundColor);
18849
+ const nodeById = /* @__PURE__ */ new Map();
18850
+ for (const n of nodes) {
18851
+ if (n.id) nodeById.set(n.id, n);
18852
+ }
18853
+ for (const e of edges) {
18854
+ const a = nodeById.get(e.from);
18855
+ const b = nodeById.get(e.to);
18856
+ if (!a || !b) continue;
18857
+ const edgeRadius = Math.max(0.04, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.12);
18858
+ const edge = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], edgeRadius, e.color ?? "#9ca3af");
18859
+ if (edge) out.push(edge);
18860
+ if (e.label) {
18861
+ out.push(
18862
+ billboardLabel(
18863
+ e.label,
18864
+ (a.x + b.x) / 2,
18865
+ (a.y + b.y) / 2,
18866
+ ((a.z ?? 0) + (b.z ?? 0)) / 2,
18867
+ { color: labelColor }
18868
+ )
18869
+ );
18870
+ }
18871
+ }
18872
+ for (const n of nodes) {
18873
+ const radius = n.radius ?? 0.5;
18874
+ const nz = n.z ?? 0;
18875
+ out.push(meshSphere(n.id, n.x, n.y, nz, radius, n.color ?? "#16a34a", { shape: n.shape, ...n.opacity !== void 0 ? { opacity: n.opacity } : {} }));
18876
+ if (n.label) {
18877
+ out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
18878
+ }
18879
+ }
18880
+ if (helix3d) {
18881
+ out.push(...helixDrawables(helix3d, { labelColor }));
18882
+ }
18883
+ return out;
18884
+ }, [mode, nodes, edges, shapes, compartments, bands, stages, helix, helix3d, animate, backgroundColor]);
18885
+ const nodeIndexById = React94.useMemo(() => {
18886
+ const m = /* @__PURE__ */ new Map();
18887
+ (helix3d?.rungs ?? []).forEach((rung, i) => {
18888
+ if (rung.id) m.set(rung.id, i);
18889
+ });
18890
+ nodes.forEach((n, i) => {
18891
+ if (n.id) m.set(n.id, i);
18892
+ });
18893
+ return m;
18894
+ }, [nodes, helix3d]);
18895
+ if (mode === "3d") {
18896
+ return /* @__PURE__ */ jsxRuntime.jsx(
18897
+ LearningScene3D,
18898
+ {
18899
+ className,
18900
+ width,
18901
+ height,
18902
+ title,
18903
+ backgroundColor,
18904
+ drawables: drawables3D,
18905
+ camera,
18906
+ lighting,
18907
+ post,
18908
+ showGrid,
18909
+ shadows,
18910
+ interactive,
18911
+ isLoading,
18912
+ error,
18913
+ onItemClick: get3DClickPayload(onShapeClick, nodeIndexById)
18914
+ }
18915
+ );
18916
+ }
17251
18917
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
17252
18918
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
17253
18919
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -17257,7 +18923,9 @@ var init_BiologyCanvas = __esm({
17257
18923
  height,
17258
18924
  backgroundColor,
17259
18925
  shapes: derivedShapes,
17260
- interactive,
18926
+ readouts,
18927
+ traces,
18928
+ interactive: interactive ?? false,
17261
18929
  animate,
17262
18930
  onShapeClick,
17263
18931
  isLoading,
@@ -24097,24 +25765,55 @@ var init_ChatBar = __esm({
24097
25765
  ChatBar.displayName = "ChatBar";
24098
25766
  }
24099
25767
  });
24100
- var ChemistryCanvas;
25768
+ function bondPerpendicular(a, b) {
25769
+ const dx = b[0] - a[0];
25770
+ const dy = b[1] - a[1];
25771
+ const px = dy;
25772
+ const py = -dx;
25773
+ const len = Math.sqrt(px * px + py * py);
25774
+ if (len < 1e-6) return [1, 0, 0];
25775
+ return [px / len, py / len, 0];
25776
+ }
25777
+ var chemistryLog, CHEM_BOND_STATE_COLOR, LONE_PAIR_ANGLES, ChemistryCanvas;
24101
25778
  var init_ChemistryCanvas = __esm({
24102
25779
  "components/learning/molecules/ChemistryCanvas.tsx"() {
24103
25780
  "use client";
24104
25781
  init_atoms();
24105
25782
  init_Stack();
24106
25783
  init_LearningCanvas();
25784
+ init_learningScene3D();
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];
24107
25793
  ChemistryCanvas = ({
24108
25794
  className,
24109
25795
  width = 600,
24110
25796
  height = 400,
24111
25797
  title,
24112
25798
  backgroundColor,
25799
+ mode = "2d",
25800
+ camera,
25801
+ lighting,
25802
+ post,
24113
25803
  atoms = [],
24114
25804
  bonds = [],
24115
25805
  arrows = [],
25806
+ bondStyle = "thick",
25807
+ containers = [],
25808
+ equation,
25809
+ equationColor,
25810
+ lattice3d,
24116
25811
  shapes = [],
24117
- interactive = false,
25812
+ readouts,
25813
+ traces,
25814
+ showGrid,
25815
+ shadows,
25816
+ interactive,
24118
25817
  animate = false,
24119
25818
  onShapeClick,
24120
25819
  isLoading,
@@ -24126,21 +25825,118 @@ var init_ChemistryCanvas = __esm({
24126
25825
  for (const a of atoms) {
24127
25826
  if (a.id) atomById.set(a.id, a);
24128
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
+ }
24129
25899
  for (const b of bonds) {
24130
25900
  const a = atomById.get(b.from);
24131
25901
  const c = atomById.get(b.to);
24132
25902
  if (!a || !c) continue;
24133
- const color = b.color ?? "#6b7280";
24134
- const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
24135
- out.push({
24136
- type: "line",
24137
- x1: a.x,
24138
- y1: a.y,
24139
- x2: c.x,
24140
- y2: c.y,
24141
- color,
24142
- lineWidth: strokeWidth
24143
- });
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
+ }
24144
25940
  }
24145
25941
  for (const a of arrows) {
24146
25942
  const angle = (a.angle ?? 0) * (Math.PI / 180);
@@ -24189,10 +25985,142 @@ var init_ChemistryCanvas = __esm({
24189
25985
  align: "center"
24190
25986
  });
24191
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
+ });
24192
26029
  }
24193
26030
  out.push(...shapes);
24194
26031
  return out;
24195
- }, [atoms, bonds, arrows, shapes]);
26032
+ }, [atoms, bonds, arrows, bondStyle, containers, equation, equationColor, shapes, width]);
26033
+ const drawables3D = React94.useMemo(() => {
26034
+ if (mode !== "3d") return [];
26035
+ if (shapes.length > 0) {
26036
+ chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
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
+ }
26044
+ const out = [];
26045
+ const labelColor = labelColorForBackground(backgroundColor);
26046
+ const atomById = /* @__PURE__ */ new Map();
26047
+ for (const a of atoms) {
26048
+ if (a.id) atomById.set(a.id, a);
26049
+ }
26050
+ for (const b of bonds) {
26051
+ const a = atomById.get(b.from);
26052
+ const c = atomById.get(b.to);
26053
+ if (!a || !c) continue;
26054
+ const color = b.color ?? "#6b7280";
26055
+ const from = [a.x, a.y, a.z ?? 0];
26056
+ const to = [c.x, c.y, c.z ?? 0];
26057
+ const perp = bondPerpendicular(from, to);
26058
+ const bondRadius = Math.max(0.06, Math.min(a.radius ?? 0.45, c.radius ?? 0.45) * 0.22);
26059
+ const step = bondRadius * 2.2;
26060
+ const offsets = b.type === "double" ? [-step, step] : b.type === "triple" ? [-step, 0, step] : [0];
26061
+ for (const off of offsets) {
26062
+ const bond = cylinderBetween(
26063
+ [from[0] + perp[0] * off, from[1] + perp[1] * off, from[2] + perp[2] * off],
26064
+ [to[0] + perp[0] * off, to[1] + perp[1] * off, to[2] + perp[2] * off],
26065
+ bondRadius,
26066
+ color
26067
+ );
26068
+ if (bond) out.push(bond);
26069
+ }
26070
+ }
26071
+ for (const a of arrows) {
26072
+ const angle = (a.angle ?? 0) * (Math.PI / 180);
26073
+ const len = a.length ?? 60;
26074
+ const x2 = a.x + Math.cos(angle) * len;
26075
+ const y2 = a.y + Math.sin(angle) * len;
26076
+ const arrow = arrowBetween([a.x, a.y, 0], [x2, y2, 0], a.color ?? "#dc2626");
26077
+ if (arrow) out.push(arrow);
26078
+ if (a.label) {
26079
+ out.push(billboardLabel(a.label, (a.x + x2) / 2, (a.y + y2) / 2, 0, { color: labelColor }));
26080
+ }
26081
+ }
26082
+ for (const a of atoms) {
26083
+ const radius = a.radius ?? 0.45;
26084
+ const az = a.z ?? 0;
26085
+ out.push(meshSphere(a.id, a.x, a.y, az, radius, a.color ?? "#2563eb"));
26086
+ if (a.element) {
26087
+ out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
26088
+ }
26089
+ }
26090
+ if (lattice3d) {
26091
+ out.push(...latticeDrawables(lattice3d, { labelColor }));
26092
+ }
26093
+ return out;
26094
+ }, [mode, atoms, bonds, arrows, shapes, containers, lattice3d, animate, backgroundColor]);
26095
+ const atomIndexById = React94.useMemo(() => {
26096
+ const m = /* @__PURE__ */ new Map();
26097
+ atoms.forEach((a, i) => {
26098
+ if (a.id) m.set(a.id, i);
26099
+ });
26100
+ return m;
26101
+ }, [atoms]);
26102
+ if (mode === "3d") {
26103
+ return /* @__PURE__ */ jsxRuntime.jsx(
26104
+ LearningScene3D,
26105
+ {
26106
+ className,
26107
+ width,
26108
+ height,
26109
+ title,
26110
+ backgroundColor,
26111
+ drawables: drawables3D,
26112
+ camera,
26113
+ lighting,
26114
+ post,
26115
+ showGrid,
26116
+ shadows,
26117
+ interactive,
26118
+ isLoading,
26119
+ error,
26120
+ onItemClick: get3DClickPayload(onShapeClick, atomIndexById)
26121
+ }
26122
+ );
26123
+ }
24196
26124
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
24197
26125
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
24198
26126
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -24202,7 +26130,9 @@ var init_ChemistryCanvas = __esm({
24202
26130
  height,
24203
26131
  backgroundColor,
24204
26132
  shapes: derivedShapes,
24205
- interactive,
26133
+ readouts,
26134
+ traces,
26135
+ interactive: interactive ?? false,
24206
26136
  animate,
24207
26137
  onShapeClick,
24208
26138
  isLoading,
@@ -30391,6 +32321,10 @@ var init_ProgressDots = __esm({
30391
32321
  ProgressDots.displayName = "ProgressDots";
30392
32322
  }
30393
32323
  });
32324
+ function formatTick(v) {
32325
+ if (Number.isInteger(v)) return String(v);
32326
+ return v.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
32327
+ }
30394
32328
  var MathCanvas;
30395
32329
  var init_MathCanvas = __esm({
30396
32330
  "components/learning/molecules/MathCanvas.tsx"() {
@@ -30410,10 +32344,19 @@ var init_MathCanvas = __esm({
30410
32344
  showAxes = true,
30411
32345
  showGrid = true,
30412
32346
  gridStep = 1,
32347
+ showTickLabels = false,
32348
+ showCurveLabels = false,
30413
32349
  curves = [],
30414
32350
  points = [],
30415
32351
  vectors = [],
32352
+ regions = [],
32353
+ bars = [],
32354
+ guides = [],
32355
+ angles = [],
32356
+ hops = [],
30416
32357
  shapes = [],
32358
+ readouts,
32359
+ traces,
30417
32360
  interactive = false,
30418
32361
  animate = false,
30419
32362
  onShapeClick,
@@ -30427,6 +32370,8 @@ var init_MathCanvas = __esm({
30427
32370
  const plotH = height - margin * 2;
30428
32371
  const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
30429
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)));
30430
32375
  if (showGrid) {
30431
32376
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep) {
30432
32377
  const px = mapX(x);
@@ -30437,14 +32382,99 @@ var init_MathCanvas = __esm({
30437
32382
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color: "#e5e7eb", lineWidth: 1 });
30438
32383
  }
30439
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
+ }
30440
32452
  if (showAxes) {
30441
- const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
30442
- const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
30443
32453
  out.push({ type: "line", x1: margin, y1: xAxisY, x2: width - margin, y2: xAxisY, color: "#374151", lineWidth: 2 });
30444
32454
  out.push({ type: "line", x1: yAxisX, y1: margin, x2: yAxisX, y2: height - margin, color: "#374151", lineWidth: 2 });
30445
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
+ }
30446
32475
  for (const curve of curves) {
30447
32476
  if (!curve.samples || curve.samples.length < 2) continue;
32477
+ let lastInRange;
30448
32478
  for (let i = 1; i < curve.samples.length; i++) {
30449
32479
  const a = curve.samples[i - 1];
30450
32480
  const b = curve.samples[i];
@@ -30456,19 +32486,97 @@ var init_MathCanvas = __esm({
30456
32486
  x2: mapX(b.x),
30457
32487
  y2: mapY(b.y),
30458
32488
  color: curve.color ?? "#2563eb",
30459
- 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"
30460
32567
  });
30461
32568
  }
30462
32569
  }
30463
32570
  for (const p of points) {
30464
32571
  if (p.x < xMin || p.x > xMax || p.y < yMin || p.y > yMax) continue;
32572
+ const isOpen = p.style === "open";
30465
32573
  out.push({
30466
32574
  type: "circle",
30467
32575
  x: mapX(p.x),
30468
32576
  y: mapY(p.y),
30469
32577
  radius: p.radius ?? 4,
30470
32578
  color: p.color ?? "#dc2626",
30471
- fill: p.color ?? "#dc2626"
32579
+ fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
30472
32580
  });
30473
32581
  if (p.label) {
30474
32582
  out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: "#111827", fontSize: 12 });
@@ -30487,7 +32595,28 @@ var init_MathCanvas = __esm({
30487
32595
  }
30488
32596
  out.push(...shapes);
30489
32597
  return out;
30490
- }, [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
+ ]);
30491
32620
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
30492
32621
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
30493
32622
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -30496,6 +32625,8 @@ var init_MathCanvas = __esm({
30496
32625
  width,
30497
32626
  height,
30498
32627
  shapes: derivedShapes,
32628
+ readouts,
32629
+ traces,
30499
32630
  interactive,
30500
32631
  animate,
30501
32632
  onShapeClick,
@@ -30507,27 +32638,355 @@ var init_MathCanvas = __esm({
30507
32638
  };
30508
32639
  }
30509
32640
  });
30510
- var 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;
30511
32950
  var init_PhysicsCanvas = __esm({
30512
32951
  "components/learning/molecules/PhysicsCanvas.tsx"() {
30513
32952
  "use client";
30514
32953
  init_atoms();
30515
32954
  init_Stack();
30516
32955
  init_LearningCanvas();
32956
+ init_learningScene3D();
32957
+ physicsLog2 = logger.createLogger("almadar:ui:physics-canvas");
32958
+ PHYSICS_LABEL_COLOR = "#374151";
30517
32959
  PhysicsCanvas = ({
30518
32960
  className,
30519
32961
  width = 600,
30520
32962
  height = 400,
30521
32963
  title,
30522
32964
  backgroundColor,
32965
+ mode = "2d",
32966
+ camera,
32967
+ lighting,
32968
+ post,
30523
32969
  bodies = [],
30524
32970
  constraints = [],
30525
32971
  showVelocity = true,
30526
32972
  showForces = false,
30527
32973
  velocityScale = 20,
30528
32974
  forceScale = 20,
32975
+ sceneObjects = [],
32976
+ trails = [],
32977
+ vectors = [],
32978
+ surface3d,
32979
+ vectors3d = [],
32980
+ vectorScale = 1,
32981
+ angles = [],
32982
+ field,
32983
+ meters = [],
30529
32984
  shapes = [],
30530
- interactive = false,
32985
+ readouts,
32986
+ traces,
32987
+ showGrid,
32988
+ shadows,
32989
+ interactive,
30531
32990
  animate = false,
30532
32991
  onShapeClick,
30533
32992
  isLoading,
@@ -30539,19 +32998,14 @@ var init_PhysicsCanvas = __esm({
30539
32998
  for (const b of bodies) {
30540
32999
  if (b.id) bodyById.set(b.id, b);
30541
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));
30542
33004
  for (const c of constraints) {
30543
33005
  const a = bodyById.get(c.from);
30544
33006
  const b = bodyById.get(c.to);
30545
33007
  if (!a || !b) continue;
30546
- out.push({
30547
- type: "line",
30548
- x1: a.x,
30549
- y1: a.y,
30550
- x2: b.x,
30551
- y2: b.y,
30552
- color: c.color ?? "#9ca3af",
30553
- lineWidth: 2
30554
- });
33008
+ out.push(...constraintShapes(c, a, b));
30555
33009
  }
30556
33010
  for (const b of bodies) {
30557
33011
  out.push({
@@ -30596,9 +33050,181 @@ var init_PhysicsCanvas = __esm({
30596
33050
  });
30597
33051
  }
30598
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));
30599
33056
  out.push(...shapes);
30600
33057
  return out;
30601
- }, [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
+ ]);
33075
+ const drawables3D = React94.useMemo(() => {
33076
+ if (mode !== "3d") return [];
33077
+ if (shapes.length > 0) {
33078
+ physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
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
+ }
33098
+ const out = [];
33099
+ const labelColor = labelColorForBackground(backgroundColor);
33100
+ const bodyById = /* @__PURE__ */ new Map();
33101
+ for (const b of bodies) {
33102
+ if (b.id) bodyById.set(b.id, b);
33103
+ }
33104
+ for (const c of constraints) {
33105
+ const a = bodyById.get(c.from);
33106
+ const b = bodyById.get(c.to);
33107
+ if (!a || !b) continue;
33108
+ const rodRadius = Math.max(0.05, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.15);
33109
+ const rod = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], rodRadius, c.color ?? "#9ca3af");
33110
+ if (rod) out.push(rod);
33111
+ }
33112
+ for (const b of bodies) {
33113
+ const radius = b.radius ?? 0.5;
33114
+ const bz = b.z ?? 0;
33115
+ out.push(meshSphere(b.id, b.x, b.y, bz, radius, b.color ?? "#2563eb"));
33116
+ if (b.label) {
33117
+ out.push(billboardLabel(b.label, b.x, b.y, bz + radius, { color: labelColor }));
33118
+ }
33119
+ const vx = b.vx ?? 0;
33120
+ const vy = b.vy ?? 0;
33121
+ const vz = b.vz ?? 0;
33122
+ const arrowRadius = Math.max(0.05, radius * 0.15);
33123
+ if (showVelocity && (vx !== 0 || vy !== 0 || vz !== 0)) {
33124
+ const arrow = arrowBetween(
33125
+ [b.x, b.y, bz],
33126
+ [b.x + vx * velocityScale, b.y + vy * velocityScale, bz + vz * velocityScale],
33127
+ "#16a34a",
33128
+ arrowRadius
33129
+ );
33130
+ if (arrow) out.push(arrow);
33131
+ }
33132
+ const fx = b.fx ?? 0;
33133
+ const fy = b.fy ?? 0;
33134
+ const fz = b.fz ?? 0;
33135
+ if (showForces && (fx !== 0 || fy !== 0 || fz !== 0)) {
33136
+ const arrow = arrowBetween(
33137
+ [b.x, b.y, bz],
33138
+ [b.x + fx * forceScale, b.y + fy * forceScale, bz + fz * forceScale],
33139
+ "#dc2626",
33140
+ arrowRadius
33141
+ );
33142
+ if (arrow) out.push(arrow);
33143
+ }
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
+ }
33174
+ return out;
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
+ ]);
33196
+ const bodyIndexById = React94.useMemo(() => {
33197
+ const m = /* @__PURE__ */ new Map();
33198
+ vectors3d.forEach((v, i) => {
33199
+ if (v.id) m.set(v.id, i);
33200
+ });
33201
+ bodies.forEach((b, i) => {
33202
+ if (b.id) m.set(b.id, i);
33203
+ });
33204
+ return m;
33205
+ }, [bodies, vectors3d]);
33206
+ if (mode === "3d") {
33207
+ return /* @__PURE__ */ jsxRuntime.jsx(
33208
+ LearningScene3D,
33209
+ {
33210
+ className,
33211
+ width,
33212
+ height,
33213
+ title,
33214
+ backgroundColor,
33215
+ drawables: drawables3D,
33216
+ camera,
33217
+ lighting,
33218
+ post,
33219
+ showGrid,
33220
+ shadows,
33221
+ interactive,
33222
+ isLoading,
33223
+ error,
33224
+ onItemClick: get3DClickPayload(onShapeClick, bodyIndexById)
33225
+ }
33226
+ );
33227
+ }
30602
33228
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
30603
33229
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
30604
33230
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -30608,7 +33234,9 @@ var init_PhysicsCanvas = __esm({
30608
33234
  height,
30609
33235
  backgroundColor,
30610
33236
  shapes: derivedShapes,
30611
- interactive,
33237
+ readouts,
33238
+ traces,
33239
+ interactive: interactive ?? false,
30612
33240
  animate,
30613
33241
  onShapeClick,
30614
33242
  isLoading,
@@ -30758,7 +33386,7 @@ function layoutFlow(nodeIds, adjacency, roots, width, height, margin) {
30758
33386
  }
30759
33387
  return nodeIds.map((id) => positions.get(id));
30760
33388
  }
30761
- function layoutTree2(nodeIds, adjacency, roots, width, height, margin) {
33389
+ function layoutTree3(nodeIds, adjacency, roots, width, height, margin) {
30762
33390
  const effectiveRoots = roots.length > 0 ? roots : [nodeIds[0]];
30763
33391
  const layers = assignLayers(nodeIds, adjacency, effectiveRoots);
30764
33392
  const maxLayer = Math.max(...Array.from(layers.values()));
@@ -30814,7 +33442,7 @@ function computeStaticLayout(mode, input) {
30814
33442
  const adjacency = buildAdjacency(nodeIds, edges);
30815
33443
  const roots = findRoots(nodeIds, adjacency);
30816
33444
  if (mode === "flow") return layoutFlow(nodeIds, adjacency, roots, width, height, margin);
30817
- if (mode === "tree") return layoutTree2(nodeIds, adjacency, roots, width, height, margin);
33445
+ if (mode === "tree") return layoutTree3(nodeIds, adjacency, roots, width, height, margin);
30818
33446
  return layoutRadial(nodeIds, adjacency, roots, width, height, margin);
30819
33447
  }
30820
33448
  var init_graphViewLayouts = __esm({
@@ -31226,12 +33854,12 @@ var init_MapView = __esm({
31226
33854
  shadowSize: [41, 41]
31227
33855
  });
31228
33856
  L.Marker.prototype.options.icon = defaultIcon;
31229
- const { useEffect: useEffect64, useRef: useRef64, useCallback: useCallback96, useState: useState100 } = React94__namespace.default;
33857
+ const { useEffect: useEffect64, useRef: useRef65, useCallback: useCallback96, useState: useState100 } = React94__namespace.default;
31230
33858
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
31231
33859
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
31232
33860
  function MapUpdater({ centerLat, centerLng, zoom }) {
31233
33861
  const map = useMap();
31234
- const prevRef = useRef64({ centerLat, centerLng, zoom });
33862
+ const prevRef = useRef65({ centerLat, centerLng, zoom });
31235
33863
  useEffect64(() => {
31236
33864
  const prev = prevRef.current;
31237
33865
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
@@ -32057,26 +34685,6 @@ var init_Lightbox = __esm({
32057
34685
  Lightbox.displayName = "Lightbox";
32058
34686
  }
32059
34687
  });
32060
- function useMediaQuery(query) {
32061
- const subscribe = React94.useCallback(
32062
- (onChange) => {
32063
- const mql = window.matchMedia(query);
32064
- mql.addEventListener("change", onChange);
32065
- return () => mql.removeEventListener("change", onChange);
32066
- },
32067
- [query]
32068
- );
32069
- return React94.useSyncExternalStore(
32070
- subscribe,
32071
- () => window.matchMedia(query).matches,
32072
- () => false
32073
- );
32074
- }
32075
- var init_useMediaQuery = __esm({
32076
- "hooks/useMediaQuery.ts"() {
32077
- "use client";
32078
- }
32079
- });
32080
34688
  function renderIconInput3(icon, props) {
32081
34689
  return typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, ...props });
32082
34690
  }
@@ -32115,8 +34723,8 @@ function TableView({
32115
34723
  columns,
32116
34724
  fields,
32117
34725
  itemActions,
32118
- maxInlineActions,
32119
- itemClickEvent,
34726
+ maxInlineActions: _maxInlineActions,
34727
+ itemClickEvent = "",
32120
34728
  selectable = false,
32121
34729
  selectEvent,
32122
34730
  selectedIds,
@@ -32166,7 +34774,6 @@ function TableView({
32166
34774
  const hasMore = pageSize > 0 && visibleCount < ordered2.length;
32167
34775
  const hasRenderProp = typeof children === "function";
32168
34776
  const idField = dndItemIdField ?? "id";
32169
- const isCoarsePointer = useMediaQuery("(pointer: coarse)");
32170
34777
  React94__namespace.default.useEffect(() => {
32171
34778
  tableViewLog.debug("render", {
32172
34779
  rowCount: data.length,
@@ -32208,21 +34815,14 @@ function TableView({
32208
34815
  const dir = sortColumn === (col.field ?? col.key) && sortDirection === "asc" ? "desc" : "asc";
32209
34816
  eventBus.emit(`UI:${sortEvent}`, { column: col.field ?? col.key, direction: dir });
32210
34817
  };
32211
- const handleActionClick = (action, row) => (e) => {
32212
- e.stopPropagation();
32213
- const payload = {
32214
- id: row.id,
32215
- row
32216
- };
32217
- eventBus.emit(`UI:${action.event}`, payload);
32218
- };
34818
+ const rowClickEvent = itemClickEvent || actionDefs.find((a) => a.variant !== "danger")?.event;
32219
34819
  const handleRowClick = (row) => () => {
32220
- if (!itemClickEvent) return;
34820
+ if (!rowClickEvent) return;
32221
34821
  const payload = {
32222
34822
  id: row.id,
32223
34823
  row
32224
34824
  };
32225
- eventBus.emit(`UI:${itemClickEvent}`, payload);
34825
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
32226
34826
  };
32227
34827
  const colFloors = React94__namespace.default.useMemo(
32228
34828
  () => colDefs.map((col) => {
@@ -32238,10 +34838,7 @@ function TableView({
32238
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;
32239
34839
  const lk = LOOKS[look];
32240
34840
  const hasActions = actionDefs.length > 0;
32241
- const effectiveMaxInline = isCoarsePointer ? 0 : maxInlineActions;
32242
- const inlineActionCount = hasActions ? effectiveMaxInline != null ? Math.min(actionDefs.length, effectiveMaxInline) : actionDefs.length : 0;
32243
- const hasOverflowActions = hasActions && effectiveMaxInline != null && actionDefs.length > effectiveMaxInline;
32244
- const actionsTrack = hasActions ? `${inlineActionCount * 6 + (hasOverflowActions ? 3 : 0)}rem` : null;
34841
+ const actionsTrack = hasActions ? "3rem" : null;
32245
34842
  const gridTemplateColumns = [
32246
34843
  selectable ? "auto" : null,
32247
34844
  ...colDefs.map((c, i) => c.width ?? `minmax(${colFloors[i]}ch, 1fr)`),
@@ -32288,7 +34885,7 @@ function TableView({
32288
34885
  col.key
32289
34886
  );
32290
34887
  }),
32291
- 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" })
32292
34889
  ]
32293
34890
  }
32294
34891
  );
@@ -32300,12 +34897,12 @@ function TableView({
32300
34897
  role: "row",
32301
34898
  "data-entity-row": true,
32302
34899
  "data-entity-id": id,
32303
- onClick: itemClickEvent ? handleRowClick(row) : void 0,
34900
+ onClick: rowClickEvent ? handleRowClick(row) : void 0,
32304
34901
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
32305
34902
  className: cn(
32306
34903
  "group items-center gap-3 transition-colors duration-fast",
32307
34904
  hasRenderProp ? "flex" : "grid",
32308
- itemClickEvent && "cursor-pointer",
34905
+ rowClickEvent && "cursor-pointer",
32309
34906
  lk.rowPad,
32310
34907
  lk.divider && "border-b border-[var(--color-border)]",
32311
34908
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -32313,7 +34910,7 @@ function TableView({
32313
34910
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
32314
34911
  ),
32315
34912
  children: [
32316
- 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(
32317
34914
  Checkbox,
32318
34915
  {
32319
34916
  checked: selected.has(id),
@@ -32334,53 +34931,37 @@ function TableView({
32334
34931
  }
32335
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);
32336
34933
  }),
32337
- hasActions && /* @__PURE__ */ jsxRuntime.jsxs(
34934
+ hasActions && /* @__PURE__ */ jsxRuntime.jsx(
32338
34935
  HStack,
32339
34936
  {
32340
34937
  gap: "xs",
32341
- onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
34938
+ onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0,
32342
34939
  className: cn(
32343
34940
  // Pinned: the fixed column tracks routinely overflow the caller's
32344
- // scroll container, which used to leave the actions off-screen.
32345
- // 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.
32346
34944
  "justify-end flex-shrink-0 sticky right-0 z-[1] transition-colors",
34945
+ "border-l border-[var(--color-border)]",
32347
34946
  lk.striped && index % 2 === 1 ? "bg-[var(--color-surface-subtle)]" : "bg-[var(--color-card)] group-hover:bg-[var(--color-surface-subtle)]"
32348
34947
  ),
32349
- children: [
32350
- (effectiveMaxInline != null ? actionDefs.slice(0, effectiveMaxInline) : actionDefs).map((action, i) => /* @__PURE__ */ jsxRuntime.jsxs(
32351
- Button,
32352
- {
32353
- variant: action.variant === "primary" ? "primary" : "ghost",
32354
- size: "sm",
32355
- onClick: handleActionClick(action, row),
32356
- "data-testid": `action-${action.event}`,
32357
- "data-row-id": String(row.id),
32358
- className: cn(action.variant === "danger" && "text-error hover:text-error hover:bg-error/10"),
32359
- children: [
32360
- action.icon && renderIconInput3(action.icon, { size: "xs", className: "mr-1" }),
32361
- action.label
32362
- ]
32363
- },
32364
- i
32365
- )),
32366
- effectiveMaxInline != null && actionDefs.length > effectiveMaxInline && /* @__PURE__ */ jsxRuntime.jsx(
32367
- Menu,
32368
- {
32369
- position: "bottom-end",
32370
- 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" }) }),
32371
- items: actionDefs.slice(effectiveMaxInline).map((action) => ({
32372
- label: action.label,
32373
- icon: action.icon,
32374
- event: action.event,
32375
- variant: action.variant === "danger" ? "danger" : "default",
32376
- onClick: () => eventBus.emit(`UI:${action.event}`, {
32377
- id: row.id,
32378
- row
32379
- })
32380
- }))
32381
- }
32382
- )
32383
- ]
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
+ )
32384
34965
  }
32385
34966
  )
32386
34967
  ]
@@ -32423,7 +35004,6 @@ var init_TableView = __esm({
32423
35004
  init_format();
32424
35005
  init_getNestedValue();
32425
35006
  init_useEventBus();
32426
- init_useMediaQuery();
32427
35007
  init_Box();
32428
35008
  init_Stack();
32429
35009
  init_Typography();
@@ -41645,7 +44225,7 @@ function getEnumOptions(field) {
41645
44225
  }));
41646
44226
  }
41647
44227
  const validation = field.validation;
41648
- if (validation?.enum && Array.isArray(validation.enum)) {
44228
+ if (validation?.enum && validation.enum.length > 0) {
41649
44229
  return validation.enum.map((v) => ({
41650
44230
  value: v,
41651
44231
  label: v.charAt(0).toUpperCase() + v.slice(1).replace(/_/g, " ")
@@ -47092,6 +49672,7 @@ var init_component_registry_generated = __esm({
47092
49672
  init_ActionTile();
47093
49673
  init_ActivationBlock();
47094
49674
  init_ComponentPatterns();
49675
+ init_AlgoGraphCanvas();
47095
49676
  init_AlgorithmCanvas();
47096
49677
  init_AnimatedCounter();
47097
49678
  init_AnimatedGraphic();
@@ -47355,6 +49936,7 @@ var init_component_registry_generated = __esm({
47355
49936
  "ActivationBlock": ActivationBlock,
47356
49937
  "Alert": AlertPattern,
47357
49938
  "AlertPattern": AlertPattern,
49939
+ "AlgoGraphCanvas": AlgoGraphCanvas,
47358
49940
  "AlgorithmCanvas": AlgorithmCanvas,
47359
49941
  "AnimatedCounter": AnimatedCounter,
47360
49942
  "AnimatedGraphic": AnimatedGraphic,
@@ -52227,9 +54809,9 @@ var log8 = logger.createLogger("almadar:ui:effects:client-handlers");
52227
54809
  function createClientEffectHandlers(options) {
52228
54810
  const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
52229
54811
  return {
52230
- emit: (event, payload) => {
54812
+ emit: (event, payload, source) => {
52231
54813
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
52232
- eventBus.emit(prefixedEvent, payload);
54814
+ eventBus.emit(prefixedEvent, payload, source);
52233
54815
  },
52234
54816
  persist: async () => {
52235
54817
  log8.warn("persist is server-side only, ignored on client");
@@ -52394,7 +54976,7 @@ function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
52394
54976
  }
52395
54977
  };
52396
54978
  ctx.emit = (event, payload) => {
52397
- emit(event, payload);
54979
+ emit(event, payload, { trait: traitName, tick: tick.name });
52398
54980
  };
52399
54981
  if (tick.guard !== void 0 && !evaluator.evaluateGuard(tick.guard, ctx)) {
52400
54982
  tickLog.debug("guard-blocked", { traitName, tick: tick.name, state: currentState });
@@ -52846,6 +55428,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
52846
55428
  }
52847
55429
  const effectContext = {
52848
55430
  traitName,
55431
+ orbitalName: orbitalsByTrait?.[traitName],
52849
55432
  state: previousState,
52850
55433
  transition: `${previousState}->${newState}`,
52851
55434
  linkedEntity,
@@ -52922,7 +55505,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
52922
55505
  });
52923
55506
  }
52924
55507
  return emittedDuringExec;
52925
- }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot]);
55508
+ }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot, orbitalsByTrait]);
52926
55509
  const runTickEffects = React94.useCallback((tick, binding) => {
52927
55510
  const traitName = binding.trait.name;
52928
55511
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
@@ -52958,9 +55541,9 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
52958
55541
  log: tickLog
52959
55542
  });
52960
55543
  }, [executeTransitionEffects, sharedEntityStore]);
52961
- const emitFromSharedWriter = React94.useCallback((event, payload) => {
55544
+ const emitFromSharedWriter = React94.useCallback((event, payload, source) => {
52962
55545
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
52963
- eventBus.emit(prefixedEvent, payload);
55546
+ eventBus.emit(prefixedEvent, payload, source);
52964
55547
  }, [eventBus]);
52965
55548
  React94.useEffect(() => {
52966
55549
  const scheduler = runtime.createTickScheduler();