@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
package/dist/avl/index.js CHANGED
@@ -3355,7 +3355,7 @@ function useEventBus() {
3355
3355
  return {
3356
3356
  ...baseBus,
3357
3357
  emit: (type, payload, source) => {
3358
- if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
3358
+ if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".") && !source?.trait) {
3359
3359
  scopeLog.warn("emit:bare-key-no-scope", { type });
3360
3360
  }
3361
3361
  baseBus.emit(type, payload, source);
@@ -14041,9 +14041,10 @@ function Canvas({
14041
14041
  drawables: [...drawables ?? [], ...childDrawables],
14042
14042
  isLoading,
14043
14043
  cameraMode: to3DCameraMode(camera?.mode),
14044
- ...zoom !== void 0 ? { scale: zoom } : {},
14044
+ ...zoom !== void 0 ? { zoom } : {},
14045
14045
  ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
14046
14046
  ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
14047
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
14047
14048
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
14048
14049
  unitScale,
14049
14050
  backgroundColor,
@@ -15044,6 +15045,14 @@ function shapeBounds(shape) {
15044
15045
  w: shape.radius * 2 + 8,
15045
15046
  h: shape.radius * 2 + 8
15046
15047
  };
15048
+ case "ellipse":
15049
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
15050
+ return {
15051
+ x: shape.x - shape.width / 2 - 4,
15052
+ y: shape.y - shape.height / 2 - 4,
15053
+ w: shape.width + 8,
15054
+ h: shape.height + 8
15055
+ };
15047
15056
  case "rect":
15048
15057
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
15049
15058
  return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
@@ -15077,13 +15086,14 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
15077
15086
  ctx.closePath();
15078
15087
  ctx.fill();
15079
15088
  }
15080
- function drawShape(ctx, shape, width, height) {
15089
+ function drawShape(ctx, shape, width, height, allShapes) {
15081
15090
  ctx.save();
15082
15091
  const opacity = shape.opacity ?? 1;
15083
15092
  ctx.globalAlpha = opacity;
15084
15093
  const stroke = resolveColor2(shape.color, ctx, "#333333");
15085
15094
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
15086
15095
  ctx.lineWidth = shape.lineWidth ?? 2;
15096
+ if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
15087
15097
  switch (shape.type) {
15088
15098
  case "grid": {
15089
15099
  const step = shape.step ?? 40;
@@ -15149,6 +15159,20 @@ function drawShape(ctx, shape, width, height) {
15149
15159
  ctx.stroke();
15150
15160
  break;
15151
15161
  }
15162
+ case "ellipse": {
15163
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
15164
+ const startAngle = (shape.startAngle ?? 0) * Math.PI / 180;
15165
+ const endAngle = (shape.endAngle ?? 360) * Math.PI / 180;
15166
+ ctx.beginPath();
15167
+ ctx.ellipse(shape.x, shape.y, shape.width / 2, shape.height / 2, 0, startAngle, endAngle);
15168
+ if (fill) {
15169
+ ctx.fillStyle = fill;
15170
+ ctx.fill();
15171
+ }
15172
+ ctx.strokeStyle = stroke;
15173
+ ctx.stroke();
15174
+ break;
15175
+ }
15152
15176
  case "rect": {
15153
15177
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
15154
15178
  if (fill) {
@@ -15195,21 +15219,153 @@ function drawShape(ctx, shape, width, height) {
15195
15219
  ctx.fillText(shape.text, shape.x, shape.y);
15196
15220
  break;
15197
15221
  }
15222
+ case "venn-region": {
15223
+ const resolveCircles = (ids) => (ids ?? []).flatMap((id) => {
15224
+ const c = allShapes.find((s) => s.type === "circle" && s.id === id);
15225
+ return c && c.x != null && c.y != null && c.radius != null ? [{ x: c.x, y: c.y, radius: c.radius }] : [];
15226
+ });
15227
+ const inside = resolveCircles(shape.inside);
15228
+ if (inside.length === 0) break;
15229
+ const outside = resolveCircles(shape.outside);
15230
+ const off = document.createElement("canvas");
15231
+ off.width = ctx.canvas.width;
15232
+ off.height = ctx.canvas.height;
15233
+ const octx = off.getContext("2d");
15234
+ if (!octx) break;
15235
+ octx.setTransform(ctx.getTransform());
15236
+ for (const c of inside) {
15237
+ const p = new Path2D();
15238
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
15239
+ octx.clip(p);
15240
+ }
15241
+ octx.fillStyle = fill ?? stroke;
15242
+ octx.fillRect(0, 0, width, height);
15243
+ octx.globalCompositeOperation = "destination-out";
15244
+ for (const c of outside) {
15245
+ const p = new Path2D();
15246
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
15247
+ octx.fill(p);
15248
+ }
15249
+ ctx.save();
15250
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
15251
+ ctx.drawImage(off, 0, 0);
15252
+ ctx.restore();
15253
+ break;
15254
+ }
15198
15255
  }
15199
15256
  ctx.restore();
15200
15257
  }
15201
- var LearningCanvas;
15258
+ function readoutShapes(readouts, width) {
15259
+ const out = [];
15260
+ const chipH = 18;
15261
+ const gap = 6;
15262
+ let rightEdge = width - 6;
15263
+ let rowY = 6;
15264
+ for (const readout of readouts) {
15265
+ const text = `${readout.label}: ${String(readout.value)}`;
15266
+ const chipW = Math.min(170, Math.max(34, text.length * 6 + 12));
15267
+ let chipX = rightEdge - chipW;
15268
+ if (chipX < 4) {
15269
+ rowY += chipH + 4;
15270
+ rightEdge = width - 6;
15271
+ chipX = rightEdge - chipW;
15272
+ }
15273
+ const color = readout.color ?? "#334155";
15274
+ out.push({ type: "rect", x: chipX, y: rowY, width: chipW, height: chipH, color, fill: color });
15275
+ out.push({
15276
+ type: "text",
15277
+ x: chipX + chipW / 2,
15278
+ y: rowY + chipH / 2,
15279
+ text,
15280
+ color: "#ffffff",
15281
+ fontSize: 10,
15282
+ align: "center"
15283
+ });
15284
+ rightEdge = chipX - gap;
15285
+ }
15286
+ return out;
15287
+ }
15288
+ function traceShapes(panel, k, width, height) {
15289
+ const w = panel.width ?? Math.round(width * 0.32);
15290
+ const h = panel.height ?? Math.round(height * 0.28);
15291
+ const x = panel.x ?? width - w - 8;
15292
+ const y = panel.y ?? height - h - 8 - k * (h + 8);
15293
+ const allSamples = panel.series.flatMap((series) => series.samples);
15294
+ let xLo = Math.min(...allSamples.map((p) => p.x));
15295
+ let xHi = Math.max(...allSamples.map((p) => p.x));
15296
+ let yLo = Math.min(...allSamples.map((p) => p.y));
15297
+ let yHi = Math.max(...allSamples.map((p) => p.y));
15298
+ if (xLo === xHi) {
15299
+ xLo -= 1;
15300
+ xHi += 1;
15301
+ }
15302
+ if (yLo === yHi) {
15303
+ yLo -= 1;
15304
+ yHi += 1;
15305
+ }
15306
+ const backgroundColor = panel.backgroundColor ?? "#ffffff";
15307
+ const frameColor = panel.frameColor ?? "#94a3b8";
15308
+ const out = [];
15309
+ out.push({
15310
+ type: "rect",
15311
+ x,
15312
+ y,
15313
+ width: w,
15314
+ height: h,
15315
+ color: backgroundColor,
15316
+ fill: backgroundColor,
15317
+ opacity: panel.backgroundOpacity ?? 0.85
15318
+ });
15319
+ out.push({ type: "rect", x, y, width: w, height: h, color: frameColor, lineWidth: 1 });
15320
+ panel.series.forEach((series, j) => {
15321
+ const color = series.color ?? TRACE_SERIES_COLORS[j % TRACE_SERIES_COLORS.length];
15322
+ const mapped = series.samples.map((p) => ({
15323
+ x: x + 4 + (p.x - xLo) / (xHi - xLo) * (w - 8),
15324
+ y: y + h - 4 - (p.y - yLo) / (yHi - yLo) * (h - 8)
15325
+ }));
15326
+ for (let i = 1; i < mapped.length; i++) {
15327
+ out.push({
15328
+ type: "line",
15329
+ x1: mapped[i - 1].x,
15330
+ y1: mapped[i - 1].y,
15331
+ x2: mapped[i].x,
15332
+ y2: mapped[i].y,
15333
+ color,
15334
+ lineWidth: 1.5
15335
+ });
15336
+ }
15337
+ if (mapped.length > 0) {
15338
+ const last = mapped[mapped.length - 1];
15339
+ out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
15340
+ }
15341
+ if (series.label) {
15342
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
15343
+ }
15344
+ });
15345
+ if (panel.yLabel) {
15346
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
15347
+ }
15348
+ if (panel.xLabel) {
15349
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
15350
+ }
15351
+ return out;
15352
+ }
15353
+ var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
15202
15354
  var init_LearningCanvas = __esm({
15203
15355
  "components/learning/atoms/LearningCanvas.tsx"() {
15204
15356
  "use client";
15205
15357
  init_cn();
15206
15358
  init_useEventBus();
15359
+ DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
15360
+ TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
15207
15361
  LearningCanvas = ({
15208
15362
  className,
15209
15363
  width = 600,
15210
15364
  height = 400,
15211
15365
  backgroundColor,
15212
15366
  shapes = [],
15367
+ readouts,
15368
+ traces,
15213
15369
  interactive = false,
15214
15370
  animate = false,
15215
15371
  onShapeClick,
@@ -15235,6 +15391,12 @@ var init_LearningCanvas = __esm({
15235
15391
  }
15236
15392
  return -1;
15237
15393
  }, [shapes]);
15394
+ const derivedShapes = useMemo(() => {
15395
+ if (!traces?.length && !readouts?.length) return shapes;
15396
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
15397
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
15398
+ return [...shapes, ...traceOut, ...readoutOut];
15399
+ }, [shapes, traces, readouts, width, height]);
15238
15400
  const draw = useCallback(() => {
15239
15401
  const canvas = canvasRef.current;
15240
15402
  if (!canvas) return;
@@ -15251,13 +15413,13 @@ var init_LearningCanvas = __esm({
15251
15413
  ctx.fillStyle = backgroundColor;
15252
15414
  ctx.fillRect(0, 0, width, height);
15253
15415
  }
15254
- for (const shape of shapes) {
15255
- if (shape.type !== "text") drawShape(ctx, shape, width, height);
15416
+ for (const shape of derivedShapes) {
15417
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
15256
15418
  }
15257
- for (const shape of shapes) {
15258
- if (shape.type === "text") drawShape(ctx, shape, width, height);
15419
+ for (const shape of derivedShapes) {
15420
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
15259
15421
  }
15260
- }, [width, height, backgroundColor, shapes]);
15422
+ }, [width, height, backgroundColor, derivedShapes]);
15261
15423
  useEffect(() => {
15262
15424
  draw();
15263
15425
  }, [draw]);
@@ -16807,7 +16969,363 @@ var init_ComponentPatterns = __esm({
16807
16969
  AlertPattern.displayName = "AlertPattern";
16808
16970
  }
16809
16971
  });
16810
- var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, AlgorithmCanvas;
16972
+ function layoutCircle(nodes, width, height) {
16973
+ const cx = width / 2;
16974
+ const cy = height / 2;
16975
+ const radius = Math.max(10, Math.min(cx, cy) - 40);
16976
+ const positions = /* @__PURE__ */ new Map();
16977
+ const n = nodes.length;
16978
+ nodes.forEach((node, i) => {
16979
+ const angle = 2 * Math.PI * i / Math.max(n, 1) - Math.PI / 2;
16980
+ positions.set(node.id, { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) });
16981
+ });
16982
+ return positions;
16983
+ }
16984
+ function layoutTree2(nodes, edges, root, width, height) {
16985
+ const nodeIds = nodes.map((n) => n.id);
16986
+ const idSet = new Set(nodeIds);
16987
+ const childrenOf = /* @__PURE__ */ new Map();
16988
+ const hasIncoming = /* @__PURE__ */ new Set();
16989
+ for (const e of edges) {
16990
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
16991
+ const list = childrenOf.get(e.from) ?? [];
16992
+ list.push(e.to);
16993
+ childrenOf.set(e.from, list);
16994
+ hasIncoming.add(e.to);
16995
+ }
16996
+ const depth = /* @__PURE__ */ new Map();
16997
+ const treeChildren = /* @__PURE__ */ new Map();
16998
+ const visited = /* @__PURE__ */ new Set();
16999
+ const bfsFrom = (start) => {
17000
+ if (visited.has(start)) return;
17001
+ visited.add(start);
17002
+ depth.set(start, 0);
17003
+ const queue = [start];
17004
+ while (queue.length > 0) {
17005
+ const u = queue.shift();
17006
+ for (const v of childrenOf.get(u) ?? []) {
17007
+ if (visited.has(v)) continue;
17008
+ visited.add(v);
17009
+ depth.set(v, (depth.get(u) ?? 0) + 1);
17010
+ const list = treeChildren.get(u) ?? [];
17011
+ list.push(v);
17012
+ treeChildren.set(u, list);
17013
+ queue.push(v);
17014
+ }
17015
+ }
17016
+ };
17017
+ const primaryRoot = root && idSet.has(root) ? root : nodeIds.find((id) => !hasIncoming.has(id)) ?? nodeIds[0];
17018
+ const rootsOrder = [];
17019
+ if (primaryRoot !== void 0) {
17020
+ bfsFrom(primaryRoot);
17021
+ rootsOrder.push(primaryRoot);
17022
+ }
17023
+ for (const id of nodeIds) {
17024
+ if (!visited.has(id)) {
17025
+ bfsFrom(id);
17026
+ rootsOrder.push(id);
17027
+ }
17028
+ }
17029
+ let leafCounter = 0;
17030
+ const xSlot = /* @__PURE__ */ new Map();
17031
+ const assignXSlot = (u) => {
17032
+ const children = treeChildren.get(u) ?? [];
17033
+ if (children.length === 0) {
17034
+ const slot = leafCounter++;
17035
+ xSlot.set(u, slot);
17036
+ return slot;
17037
+ }
17038
+ const childSlots = children.map(assignXSlot);
17039
+ const avg = childSlots.reduce((a, b) => a + b, 0) / childSlots.length;
17040
+ xSlot.set(u, avg);
17041
+ return avg;
17042
+ };
17043
+ for (const r2 of rootsOrder) assignXSlot(r2);
17044
+ let maxDepth = 0;
17045
+ for (const d of depth.values()) maxDepth = Math.max(maxDepth, d);
17046
+ const colWidth = width / Math.max(1, leafCounter);
17047
+ const rowHeight = height / (maxDepth + 1);
17048
+ const positions = /* @__PURE__ */ new Map();
17049
+ for (const id of nodeIds) {
17050
+ const slot = xSlot.get(id) ?? 0;
17051
+ const d = depth.get(id) ?? 0;
17052
+ positions.set(id, { x: slot * colWidth + colWidth / 2, y: d * rowHeight + rowHeight / 2 });
17053
+ }
17054
+ return positions;
17055
+ }
17056
+ function layoutLayered(nodes, edges, width, height) {
17057
+ const nodeIds = nodes.map((n) => n.id);
17058
+ const idSet = new Set(nodeIds);
17059
+ const adj = /* @__PURE__ */ new Map();
17060
+ const remainingIndegree = /* @__PURE__ */ new Map();
17061
+ for (const id of nodeIds) remainingIndegree.set(id, 0);
17062
+ for (const e of edges) {
17063
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
17064
+ const list = adj.get(e.from) ?? [];
17065
+ list.push(e.to);
17066
+ adj.set(e.from, list);
17067
+ remainingIndegree.set(e.to, (remainingIndegree.get(e.to) ?? 0) + 1);
17068
+ }
17069
+ const layer = /* @__PURE__ */ new Map();
17070
+ const dequeued = /* @__PURE__ */ new Set();
17071
+ const queue = [];
17072
+ for (const id of nodeIds) {
17073
+ if ((remainingIndegree.get(id) ?? 0) === 0) {
17074
+ layer.set(id, 0);
17075
+ queue.push(id);
17076
+ }
17077
+ }
17078
+ while (queue.length > 0) {
17079
+ const u = queue.shift();
17080
+ dequeued.add(u);
17081
+ for (const v of adj.get(u) ?? []) {
17082
+ const candidate = (layer.get(u) ?? 0) + 1;
17083
+ layer.set(v, Math.max(layer.get(v) ?? 0, candidate));
17084
+ remainingIndegree.set(v, (remainingIndegree.get(v) ?? 0) - 1);
17085
+ if ((remainingIndegree.get(v) ?? 0) === 0 && !dequeued.has(v)) {
17086
+ queue.push(v);
17087
+ }
17088
+ }
17089
+ }
17090
+ let baseMaxLayer = 0;
17091
+ for (const id of nodeIds) {
17092
+ if (dequeued.has(id)) baseMaxLayer = Math.max(baseMaxLayer, layer.get(id) ?? 0);
17093
+ }
17094
+ const cycleLayer = baseMaxLayer + 1;
17095
+ let maxLayer = baseMaxLayer;
17096
+ for (const id of nodeIds) {
17097
+ if (!dequeued.has(id)) {
17098
+ layer.set(id, cycleLayer);
17099
+ maxLayer = cycleLayer;
17100
+ }
17101
+ }
17102
+ const colWidth = width / Math.max(1, maxLayer + 1);
17103
+ const byLayer = /* @__PURE__ */ new Map();
17104
+ for (const id of nodeIds) {
17105
+ const l = layer.get(id) ?? 0;
17106
+ const list = byLayer.get(l) ?? [];
17107
+ list.push(id);
17108
+ byLayer.set(l, list);
17109
+ }
17110
+ const positions = /* @__PURE__ */ new Map();
17111
+ for (const [l, ids] of byLayer) {
17112
+ const rowHeight = height / ids.length;
17113
+ ids.forEach((id, i) => {
17114
+ positions.set(id, { x: l * colWidth + colWidth / 2, y: i * rowHeight + rowHeight / 2 });
17115
+ });
17116
+ }
17117
+ return positions;
17118
+ }
17119
+ function computePositions(nodes, edges, layout, root, width, height) {
17120
+ switch (layout) {
17121
+ case "circle":
17122
+ return layoutCircle(nodes, width, height);
17123
+ case "tree":
17124
+ return layoutTree2(nodes, edges, root, width, height);
17125
+ case "layered":
17126
+ return layoutLayered(nodes, edges, width, height);
17127
+ case "manual":
17128
+ default: {
17129
+ const positions = /* @__PURE__ */ new Map();
17130
+ for (const n of nodes) positions.set(n.id, { x: n.x ?? 0, y: n.y ?? 0 });
17131
+ return positions;
17132
+ }
17133
+ }
17134
+ }
17135
+ var NODE_STATE_COLOR, EDGE_STATE_COLOR, DEFAULT_NODE_RADIUS, AlgoGraphCanvas;
17136
+ var init_AlgoGraphCanvas = __esm({
17137
+ "components/learning/molecules/AlgoGraphCanvas.tsx"() {
17138
+ "use client";
17139
+ init_atoms();
17140
+ init_Stack();
17141
+ init_LearningCanvas();
17142
+ NODE_STATE_COLOR = {
17143
+ unvisited: "#cbd5e1",
17144
+ frontier: "#f59e0b",
17145
+ current: "#ef4444",
17146
+ visited: "#22c55e",
17147
+ goal: "#8b5cf6",
17148
+ path: "#0ea5e9"
17149
+ };
17150
+ EDGE_STATE_COLOR = {
17151
+ default: "#9ca3af",
17152
+ tree: "#16a34a",
17153
+ relaxed: "#f59e0b",
17154
+ candidate: "#38bdf8",
17155
+ path: "#dc2626"
17156
+ };
17157
+ DEFAULT_NODE_RADIUS = 18;
17158
+ AlgoGraphCanvas = ({
17159
+ className,
17160
+ width = 600,
17161
+ height = 400,
17162
+ title,
17163
+ backgroundColor,
17164
+ nodes = [],
17165
+ edges = [],
17166
+ layout = "manual",
17167
+ root,
17168
+ shapes = [],
17169
+ interactive = false,
17170
+ animate = false,
17171
+ onShapeClick,
17172
+ onNodeClick,
17173
+ isLoading,
17174
+ error
17175
+ }) => {
17176
+ const nodeById = useMemo(() => {
17177
+ const m = /* @__PURE__ */ new Map();
17178
+ for (const n of nodes) m.set(n.id, n);
17179
+ return m;
17180
+ }, [nodes]);
17181
+ const nodeIndexById = useMemo(() => {
17182
+ const m = /* @__PURE__ */ new Map();
17183
+ nodes.forEach((n, i) => m.set(n.id, i));
17184
+ return m;
17185
+ }, [nodes]);
17186
+ const derivedShapes = useMemo(() => {
17187
+ const out = [];
17188
+ const positions = computePositions(nodes, edges, layout, root, width, height);
17189
+ const edgeGeoms = [];
17190
+ for (const e of edges) {
17191
+ const a = nodeById.get(e.from);
17192
+ const b = nodeById.get(e.to);
17193
+ const posA = positions.get(e.from);
17194
+ const posB = positions.get(e.to);
17195
+ if (!a || !b || !posA || !posB) continue;
17196
+ const rA = a.radius ?? DEFAULT_NODE_RADIUS;
17197
+ const rB = b.radius ?? DEFAULT_NODE_RADIUS;
17198
+ const dx = posB.x - posA.x;
17199
+ const dy = posB.y - posA.y;
17200
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
17201
+ const ux = dx / dist;
17202
+ const uy = dy / dist;
17203
+ edgeGeoms.push({
17204
+ directed: e.directed ?? false,
17205
+ x1: posA.x + ux * rA,
17206
+ y1: posA.y + uy * rA,
17207
+ x2: posB.x - ux * rB,
17208
+ y2: posB.y - uy * rB,
17209
+ color: e.color ?? EDGE_STATE_COLOR[e.state ?? "default"],
17210
+ label: e.label ?? (e.weight != null ? String(e.weight) : void 0)
17211
+ });
17212
+ }
17213
+ for (const g of edgeGeoms) {
17214
+ out.push({
17215
+ type: g.directed ? "arrow" : "line",
17216
+ x1: g.x1,
17217
+ y1: g.y1,
17218
+ x2: g.x2,
17219
+ y2: g.y2,
17220
+ color: g.color,
17221
+ lineWidth: 2
17222
+ });
17223
+ }
17224
+ for (const g of edgeGeoms) {
17225
+ if (g.label === void 0) continue;
17226
+ const midX = (g.x1 + g.x2) / 2;
17227
+ const midY = (g.y1 + g.y2) / 2;
17228
+ const dx = g.x2 - g.x1;
17229
+ const dy = g.y2 - g.y1;
17230
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
17231
+ const perpX = -(dy / dist);
17232
+ const perpY = dx / dist;
17233
+ out.push({
17234
+ type: "text",
17235
+ x: midX + perpX * 10,
17236
+ y: midY + perpY * 10,
17237
+ text: g.label,
17238
+ fontSize: 11,
17239
+ align: "center",
17240
+ color: "#374151"
17241
+ });
17242
+ }
17243
+ const nodeGeoms = [];
17244
+ for (const n of nodes) {
17245
+ const pos = positions.get(n.id);
17246
+ if (!pos) continue;
17247
+ nodeGeoms.push({
17248
+ id: n.id,
17249
+ x: pos.x,
17250
+ y: pos.y,
17251
+ radius: n.radius ?? DEFAULT_NODE_RADIUS,
17252
+ color: n.color ?? NODE_STATE_COLOR[n.state ?? "unvisited"],
17253
+ label: n.label,
17254
+ badge: n.badge
17255
+ });
17256
+ }
17257
+ for (const g of nodeGeoms) {
17258
+ out.push({ type: "circle", id: g.id, x: g.x, y: g.y, radius: g.radius, color: g.color, fill: `${g.color}33` });
17259
+ }
17260
+ const badgeGeoms = [];
17261
+ for (const g of nodeGeoms) {
17262
+ if (!g.badge) continue;
17263
+ const w = Math.min(42, Math.max(18, g.badge.text.length * 6 + 10));
17264
+ badgeGeoms.push({
17265
+ cx: g.x + g.radius * 0.75,
17266
+ cy: g.y - g.radius * 0.75,
17267
+ w,
17268
+ h: 14,
17269
+ // Borderless pill: same color drives both stroke and fill.
17270
+ color: g.badge.color ?? "#1e293b",
17271
+ text: g.badge.text
17272
+ });
17273
+ }
17274
+ for (const b of badgeGeoms) {
17275
+ out.push({ type: "rect", x: b.cx - b.w / 2, y: b.cy - b.h / 2, width: b.w, height: b.h, color: b.color, fill: b.color });
17276
+ }
17277
+ for (const b of badgeGeoms) {
17278
+ out.push({ type: "text", x: b.cx, y: b.cy, text: b.text, fontSize: 9, align: "center", color: "#ffffff" });
17279
+ }
17280
+ for (const g of nodeGeoms) {
17281
+ if (g.label === void 0) continue;
17282
+ out.push({
17283
+ type: "text",
17284
+ x: g.x,
17285
+ y: g.y + g.radius + 14,
17286
+ text: g.label,
17287
+ fontSize: 12,
17288
+ align: "center",
17289
+ color: "#111827"
17290
+ });
17291
+ }
17292
+ out.push(...shapes);
17293
+ return out;
17294
+ }, [nodes, edges, layout, root, width, height, nodeById, shapes]);
17295
+ const handleShapeClick = useCallback(
17296
+ (payload) => {
17297
+ if (payload.type === "circle" && payload.id) {
17298
+ const node = nodeById.get(payload.id);
17299
+ const idx = nodeIndexById.get(payload.id);
17300
+ if (node && idx !== void 0) {
17301
+ onNodeClick?.({ id: node.id, label: node.label, index: idx });
17302
+ }
17303
+ }
17304
+ onShapeClick?.(payload);
17305
+ },
17306
+ [nodeById, nodeIndexById, onNodeClick, onShapeClick]
17307
+ );
17308
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
17309
+ title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
17310
+ /* @__PURE__ */ jsx(
17311
+ LearningCanvas,
17312
+ {
17313
+ width,
17314
+ height,
17315
+ backgroundColor,
17316
+ shapes: derivedShapes,
17317
+ interactive,
17318
+ animate,
17319
+ onShapeClick: onShapeClick || onNodeClick ? handleShapeClick : void 0,
17320
+ isLoading,
17321
+ error
17322
+ }
17323
+ )
17324
+ ] }) });
17325
+ };
17326
+ }
17327
+ });
17328
+ var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, PANEL_FAMILY_ORDER, RANGE_COLOR_DEFAULT, RANGE_FILL_OPACITY, BRACKET_TOP_OFFSET, BRACKET_ROW_H, BRACKET_TICK_H, BRACKET_LABEL_OFFSET, SLOT_EMPTY_FILL, SLOT_EMPTY_STROKE, SLOT_FILLED_STROKE, SLOT_HIGHLIGHT_DEFAULT, SLOT_VALUE_TEXT_COLOR, FRAME_ACTIVE_COLOR, FRAME_RETURNING_COLOR, FRAME_DONE_COLOR, FRAME_LABEL_COLOR, FRAME_DETAIL_COLOR, FRAME_TWO_LINE_MIN_H, BUCKET_INDEX_FILL, BUCKET_INDEX_STROKE, BUCKET_INDEX_TEXT, BUCKET_ENTRY_TEXT, BUCKET_ENTRY_DEFAULT, BUCKET_ENTRY_HIGHLIGHT, BUCKET_ENTRY_PROBING, BUCKET_ENTRY_MIN_W, BUCKET_ENTRY_MAX_W, AXIS_LABEL_COLOR, AXIS_LABEL_FONT_SIZE, CORNER_TEXT_COLOR, CORNER_FONT_SIZE, CORNER_MIN_CELL, CORNER_INSET_X, CORNER_INSET_Y, AUX_PRIMARY_RATIO, AUX_LABEL_BAND, AUX_BASELINE_PAD, AlgorithmCanvas;
16811
17329
  var init_AlgorithmCanvas = __esm({
16812
17330
  "components/learning/molecules/AlgorithmCanvas.tsx"() {
16813
17331
  "use client";
@@ -16819,6 +17337,43 @@ var init_AlgorithmCanvas = __esm({
16819
17337
  DEFAULT_POINTER_COLOR = "#dc2626";
16820
17338
  POINTER_BAND = 34;
16821
17339
  TOP_PAD = 26;
17340
+ PANEL_FAMILY_ORDER = ["bars", "slots", "cells", "buckets", "frames"];
17341
+ RANGE_COLOR_DEFAULT = "#3b82f6";
17342
+ RANGE_FILL_OPACITY = 0.15;
17343
+ BRACKET_TOP_OFFSET = 16;
17344
+ BRACKET_ROW_H = 14;
17345
+ BRACKET_TICK_H = 6;
17346
+ BRACKET_LABEL_OFFSET = 6;
17347
+ SLOT_EMPTY_FILL = "#f1f5f9";
17348
+ SLOT_EMPTY_STROKE = "#cbd5e1";
17349
+ SLOT_FILLED_STROKE = "#9ca3af";
17350
+ SLOT_HIGHLIGHT_DEFAULT = "#f59e0b";
17351
+ SLOT_VALUE_TEXT_COLOR = "#ffffff";
17352
+ FRAME_ACTIVE_COLOR = "#3b82f6";
17353
+ FRAME_RETURNING_COLOR = "#f59e0b";
17354
+ FRAME_DONE_COLOR = "#94a3b8";
17355
+ FRAME_LABEL_COLOR = "#ffffff";
17356
+ FRAME_DETAIL_COLOR = "#e2e8f0";
17357
+ FRAME_TWO_LINE_MIN_H = 22;
17358
+ BUCKET_INDEX_FILL = "#e2e8f0";
17359
+ BUCKET_INDEX_STROKE = "#9ca3af";
17360
+ BUCKET_INDEX_TEXT = "#374151";
17361
+ BUCKET_ENTRY_TEXT = "#ffffff";
17362
+ BUCKET_ENTRY_DEFAULT = "#3b82f6";
17363
+ BUCKET_ENTRY_HIGHLIGHT = "#f59e0b";
17364
+ BUCKET_ENTRY_PROBING = "#38bdf8";
17365
+ BUCKET_ENTRY_MIN_W = 24;
17366
+ BUCKET_ENTRY_MAX_W = 64;
17367
+ AXIS_LABEL_COLOR = "#6b7280";
17368
+ AXIS_LABEL_FONT_SIZE = 10;
17369
+ CORNER_TEXT_COLOR = "#111827";
17370
+ CORNER_FONT_SIZE = 7;
17371
+ CORNER_MIN_CELL = 28;
17372
+ CORNER_INSET_X = 3;
17373
+ CORNER_INSET_Y = 6;
17374
+ AUX_PRIMARY_RATIO = 0.6;
17375
+ AUX_LABEL_BAND = 18;
17376
+ AUX_BASELINE_PAD = 8;
16822
17377
  AlgorithmCanvas = ({
16823
17378
  className,
16824
17379
  width = 600,
@@ -16828,6 +17383,14 @@ var init_AlgorithmCanvas = __esm({
16828
17383
  bars = [],
16829
17384
  cells = [],
16830
17385
  pointers = [],
17386
+ ranges = [],
17387
+ slots = [],
17388
+ slotOrientation = "horizontal",
17389
+ frames = [],
17390
+ buckets = [],
17391
+ auxBars = [],
17392
+ rowLabels = [],
17393
+ colLabels = [],
16831
17394
  shapes = [],
16832
17395
  interactive = false,
16833
17396
  animate = false,
@@ -16837,12 +17400,35 @@ var init_AlgorithmCanvas = __esm({
16837
17400
  }) => {
16838
17401
  const derivedShapes = useMemo(() => {
16839
17402
  const out = [];
17403
+ const presence = {
17404
+ bars: bars.length > 0,
17405
+ slots: slots.length > 0,
17406
+ cells: cells.length > 0,
17407
+ buckets: buckets.length > 0,
17408
+ frames: frames.length > 0
17409
+ };
17410
+ const panelCount = PANEL_FAMILY_ORDER.filter((f3) => presence[f3]).length;
17411
+ const panelHeight = height / Math.max(1, panelCount);
17412
+ const panelY = { bars: 0, slots: 0, cells: 0, buckets: 0, frames: 0 };
17413
+ let compactIndex = 0;
17414
+ PANEL_FAMILY_ORDER.forEach((f3) => {
17415
+ if (presence[f3]) {
17416
+ panelY[f3] = compactIndex * panelHeight;
17417
+ compactIndex += 1;
17418
+ }
17419
+ });
16840
17420
  if (bars.length > 0) {
17421
+ const panelYBars = panelY.bars;
16841
17422
  const slot = width / bars.length;
16842
17423
  const barW = slot * 0.8;
16843
17424
  const gap = slot * 0.1;
16844
- const baseline = height - POINTER_BAND;
16845
- const usableH = baseline - TOP_PAD;
17425
+ const bracketRanges = ranges.filter((r2) => r2.kind === "bracket");
17426
+ const bracketCount = bracketRanges.length;
17427
+ const bracketHeadroom = bracketCount > 0 ? BRACKET_TOP_OFFSET + bracketCount * BRACKET_ROW_H : 0;
17428
+ const hasAux = auxBars.length > 0;
17429
+ const primaryH = hasAux ? panelHeight * AUX_PRIMARY_RATIO : panelHeight;
17430
+ const baseline = panelYBars + primaryH - POINTER_BAND;
17431
+ const usableH = baseline - (panelYBars + TOP_PAD + bracketHeadroom);
16846
17432
  const maxV = Math.max(1, ...bars.map((b) => Number.isFinite(b.value) ? b.value : 0));
16847
17433
  bars.forEach((bar, i) => {
16848
17434
  const v = Number.isFinite(bar.value) ? bar.value : 0;
@@ -16872,6 +17458,89 @@ var init_AlgorithmCanvas = __esm({
16872
17458
  });
16873
17459
  }
16874
17460
  });
17461
+ ranges.forEach((r2) => {
17462
+ const kind = r2.kind ?? "fill";
17463
+ if (kind !== "fill") return;
17464
+ const color = r2.color ?? RANGE_COLOR_DEFAULT;
17465
+ out.push({
17466
+ type: "rect",
17467
+ x: r2.from * slot,
17468
+ y: panelYBars,
17469
+ width: (r2.to - r2.from + 1) * slot,
17470
+ height: primaryH,
17471
+ color,
17472
+ fill: color,
17473
+ opacity: RANGE_FILL_OPACITY
17474
+ });
17475
+ if (r2.label) {
17476
+ out.push({
17477
+ type: "text",
17478
+ x: r2.from * slot + 4,
17479
+ // Sits below the bracket block (if any) so fill and bracket labels never collide.
17480
+ y: panelYBars + 10 + bracketHeadroom,
17481
+ text: r2.label,
17482
+ color,
17483
+ fontSize: 10,
17484
+ align: "left"
17485
+ });
17486
+ }
17487
+ });
17488
+ bracketRanges.forEach((r2, i) => {
17489
+ const bracketY = panelYBars + BRACKET_TOP_OFFSET + i * BRACKET_ROW_H;
17490
+ const x1 = r2.from * slot + slot * 0.1;
17491
+ const x2 = (r2.to + 1) * slot - slot * 0.1;
17492
+ const color = r2.color ?? RANGE_COLOR_DEFAULT;
17493
+ out.push({ type: "line", x1, y1: bracketY, x2, y2: bracketY, color, lineWidth: 2 });
17494
+ out.push({ type: "line", x1, y1: bracketY, x2: x1, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
17495
+ out.push({ type: "line", x1: x2, y1: bracketY, x2, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
17496
+ if (r2.label) {
17497
+ out.push({
17498
+ type: "text",
17499
+ x: (x1 + x2) / 2,
17500
+ y: bracketY - BRACKET_LABEL_OFFSET,
17501
+ text: r2.label,
17502
+ color,
17503
+ fontSize: 10,
17504
+ align: "center"
17505
+ });
17506
+ }
17507
+ });
17508
+ if (hasAux) {
17509
+ const auxH = panelHeight - primaryH;
17510
+ const slot2 = width / auxBars.length;
17511
+ const auxBaseline = panelYBars + primaryH + auxH - AUX_BASELINE_PAD;
17512
+ const auxUsableH = auxBaseline - (panelYBars + primaryH + AUX_LABEL_BAND);
17513
+ const maxAuxV = Math.max(1, ...auxBars.map((b) => Number.isFinite(b.value) ? b.value : 0));
17514
+ auxBars.forEach((bar, i) => {
17515
+ const v = Number.isFinite(bar.value) ? bar.value : 0;
17516
+ const bh = Math.max(0, v / maxAuxV * auxUsableH);
17517
+ const x = i * slot2 + slot2 * 0.1;
17518
+ const w = slot2 * 0.8;
17519
+ const color = bar.color ?? DEFAULT_BAR_COLOR;
17520
+ out.push({
17521
+ type: "rect",
17522
+ id: `auxbar-${i}`,
17523
+ x,
17524
+ y: auxBaseline - bh,
17525
+ width: w,
17526
+ height: bh,
17527
+ color,
17528
+ fill: color
17529
+ });
17530
+ const label = bar.label ?? (auxBars.length <= 24 ? String(v) : void 0);
17531
+ if (label) {
17532
+ out.push({
17533
+ type: "text",
17534
+ x: x + w / 2,
17535
+ y: auxBaseline - bh - 8,
17536
+ text: label,
17537
+ color: "#374151",
17538
+ fontSize: 11,
17539
+ align: "center"
17540
+ });
17541
+ }
17542
+ });
17543
+ }
16875
17544
  pointers.forEach((p) => {
16876
17545
  if (p.index < 0 || p.index >= bars.length) return;
16877
17546
  const cx = p.index * slot + slot / 2;
@@ -16879,7 +17548,7 @@ var init_AlgorithmCanvas = __esm({
16879
17548
  out.push({
16880
17549
  type: "arrow",
16881
17550
  x1: cx,
16882
- y1: height - 6,
17551
+ y1: panelYBars + primaryH - 18,
16883
17552
  x2: cx,
16884
17553
  y2: baseline + 4,
16885
17554
  color,
@@ -16889,7 +17558,7 @@ var init_AlgorithmCanvas = __esm({
16889
17558
  out.push({
16890
17559
  type: "text",
16891
17560
  x: cx,
16892
- y: height - 22,
17561
+ y: panelYBars + primaryH - 8,
16893
17562
  text: p.label,
16894
17563
  color,
16895
17564
  fontSize: 11,
@@ -16898,14 +17567,111 @@ var init_AlgorithmCanvas = __esm({
16898
17567
  }
16899
17568
  });
16900
17569
  }
17570
+ if (slots.length > 0) {
17571
+ const panelYSlots = panelY.slots;
17572
+ const n = slots.length;
17573
+ const vertical = slotOrientation === "vertical";
17574
+ const vBoxH = panelHeight / n;
17575
+ const vBoxW = Math.min(width * 0.5, 120);
17576
+ const vBoxX = (width - vBoxW) / 2;
17577
+ const hCellW = width / n;
17578
+ const hBoxW = hCellW * 0.82;
17579
+ const hBoxH = Math.min(panelHeight * 0.6, 48);
17580
+ const hBoxY = panelYSlots + (panelHeight - hBoxH) / 2;
17581
+ const slotBox = (i) => vertical ? { x: vBoxX, y: panelYSlots + panelHeight - (i + 1) * vBoxH, width: vBoxW, height: vBoxH } : { x: i * hCellW + (hCellW - hBoxW) / 2, y: hBoxY, width: hBoxW, height: hBoxH };
17582
+ slots.forEach((s, i) => {
17583
+ const box = slotBox(i);
17584
+ const state = s.state ?? "filled";
17585
+ const fill = state === "empty" ? SLOT_EMPTY_FILL : state === "highlight" ? s.color ?? SLOT_HIGHLIGHT_DEFAULT : s.color ?? DEFAULT_BAR_COLOR;
17586
+ const stroke = state === "empty" ? SLOT_EMPTY_STROKE : SLOT_FILLED_STROKE;
17587
+ out.push({
17588
+ type: "rect",
17589
+ id: `slot-${i}`,
17590
+ x: box.x,
17591
+ y: box.y,
17592
+ width: box.width,
17593
+ height: box.height,
17594
+ color: stroke,
17595
+ fill
17596
+ });
17597
+ if (s.value != null && state !== "empty") {
17598
+ out.push({
17599
+ type: "text",
17600
+ x: box.x + box.width / 2,
17601
+ y: box.y + box.height / 2,
17602
+ text: String(s.value),
17603
+ color: SLOT_VALUE_TEXT_COLOR,
17604
+ fontSize: 12,
17605
+ align: "center"
17606
+ });
17607
+ }
17608
+ });
17609
+ if (bars.length === 0) {
17610
+ pointers.forEach((p) => {
17611
+ if (p.index < 0 || p.index >= slots.length) return;
17612
+ const box = slotBox(p.index);
17613
+ const color = p.color ?? DEFAULT_POINTER_COLOR;
17614
+ if (vertical) {
17615
+ const cy = box.y + box.height / 2;
17616
+ out.push({
17617
+ type: "arrow",
17618
+ x1: box.x + box.width + 34,
17619
+ y1: cy,
17620
+ x2: box.x + box.width + 4,
17621
+ y2: cy,
17622
+ color,
17623
+ lineWidth: 2
17624
+ });
17625
+ if (p.label) {
17626
+ out.push({
17627
+ type: "text",
17628
+ x: box.x + box.width + 38,
17629
+ y: cy,
17630
+ text: p.label,
17631
+ color,
17632
+ fontSize: 11,
17633
+ align: "left"
17634
+ });
17635
+ }
17636
+ } else {
17637
+ const cx = box.x + box.width / 2;
17638
+ out.push({
17639
+ type: "arrow",
17640
+ x1: cx,
17641
+ y1: panelYSlots + panelHeight - 18,
17642
+ x2: cx,
17643
+ y2: box.y + box.height + 4,
17644
+ color,
17645
+ lineWidth: 2
17646
+ });
17647
+ if (p.label) {
17648
+ out.push({
17649
+ type: "text",
17650
+ x: cx,
17651
+ y: panelYSlots + panelHeight - 8,
17652
+ text: p.label,
17653
+ color,
17654
+ fontSize: 11,
17655
+ align: "center"
17656
+ });
17657
+ }
17658
+ }
17659
+ });
17660
+ }
17661
+ }
16901
17662
  if (cells.length > 0) {
17663
+ const panelYCells = panelY.cells;
16902
17664
  const maxCol = Math.max(0, ...cells.map((c) => c.col)) + 1;
16903
17665
  const maxRow = Math.max(0, ...cells.map((c) => c.row)) + 1;
16904
- const cw = width / maxCol;
16905
- const ch = height / maxRow;
17666
+ const colLabelH = colLabels.length > 0 ? 16 : 0;
17667
+ const rowLabelW = rowLabels.length > 0 ? 20 : 0;
17668
+ const gridX0 = rowLabelW;
17669
+ const gridY0 = panelYCells + colLabelH;
17670
+ const cw = (width - rowLabelW) / maxCol;
17671
+ const ch = (panelHeight - colLabelH) / maxRow;
16906
17672
  cells.forEach((c, i) => {
16907
- const x = c.col * cw;
16908
- const y = c.row * ch;
17673
+ const x = gridX0 + c.col * cw;
17674
+ const y = gridY0 + c.row * ch;
16909
17675
  const color = c.color ?? DEFAULT_CELL_COLOR;
16910
17676
  out.push({
16911
17677
  type: "rect",
@@ -16929,11 +17695,207 @@ var init_AlgorithmCanvas = __esm({
16929
17695
  align: "center"
16930
17696
  });
16931
17697
  }
17698
+ if (c.corner && cw >= CORNER_MIN_CELL && ch >= CORNER_MIN_CELL) {
17699
+ const { tl, tr, bl, br } = c.corner;
17700
+ if (tl) {
17701
+ out.push({
17702
+ type: "text",
17703
+ x: x + CORNER_INSET_X,
17704
+ y: y + CORNER_INSET_Y,
17705
+ text: tl,
17706
+ color: CORNER_TEXT_COLOR,
17707
+ fontSize: CORNER_FONT_SIZE,
17708
+ align: "left"
17709
+ });
17710
+ }
17711
+ if (tr) {
17712
+ out.push({
17713
+ type: "text",
17714
+ x: x + cw - CORNER_INSET_X,
17715
+ y: y + CORNER_INSET_Y,
17716
+ text: tr,
17717
+ color: CORNER_TEXT_COLOR,
17718
+ fontSize: CORNER_FONT_SIZE,
17719
+ align: "right"
17720
+ });
17721
+ }
17722
+ if (bl) {
17723
+ out.push({
17724
+ type: "text",
17725
+ x: x + CORNER_INSET_X,
17726
+ y: y + ch - CORNER_INSET_Y,
17727
+ text: bl,
17728
+ color: CORNER_TEXT_COLOR,
17729
+ fontSize: CORNER_FONT_SIZE,
17730
+ align: "left"
17731
+ });
17732
+ }
17733
+ if (br) {
17734
+ out.push({
17735
+ type: "text",
17736
+ x: x + cw - CORNER_INSET_X,
17737
+ y: y + ch - CORNER_INSET_Y,
17738
+ text: br,
17739
+ color: CORNER_TEXT_COLOR,
17740
+ fontSize: CORNER_FONT_SIZE,
17741
+ align: "right"
17742
+ });
17743
+ }
17744
+ }
17745
+ });
17746
+ colLabels.forEach((l) => {
17747
+ out.push({
17748
+ type: "text",
17749
+ x: gridX0 + l.index * cw + cw / 2,
17750
+ y: panelYCells + colLabelH / 2,
17751
+ text: l.text,
17752
+ color: l.color ?? AXIS_LABEL_COLOR,
17753
+ fontSize: AXIS_LABEL_FONT_SIZE,
17754
+ align: "center"
17755
+ });
17756
+ });
17757
+ rowLabels.forEach((l) => {
17758
+ out.push({
17759
+ type: "text",
17760
+ x: rowLabelW - 6,
17761
+ y: gridY0 + l.index * ch + ch / 2,
17762
+ text: l.text,
17763
+ color: l.color ?? AXIS_LABEL_COLOR,
17764
+ fontSize: AXIS_LABEL_FONT_SIZE,
17765
+ align: "right"
17766
+ });
17767
+ });
17768
+ }
17769
+ if (buckets.length > 0) {
17770
+ const panelYBuckets = panelY.buckets;
17771
+ const bucketCount = Math.max(0, ...buckets.map((b) => b.index)) + 1;
17772
+ const rowH = panelHeight / bucketCount;
17773
+ const indexColW = Math.min(width * 0.12, 40);
17774
+ const maxChainLen = Math.max(1, ...buckets.map((b) => b.entries.length));
17775
+ const entryW = Math.min(BUCKET_ENTRY_MAX_W, Math.max(BUCKET_ENTRY_MIN_W, (width - indexColW - 8) / maxChainLen));
17776
+ const maxVisible = Math.floor((width - indexColW - 4) / entryW);
17777
+ buckets.forEach((b) => {
17778
+ const rowY = panelYBuckets + b.index * rowH;
17779
+ out.push({
17780
+ type: "rect",
17781
+ id: `bucket-index-${b.index}`,
17782
+ x: 2,
17783
+ y: rowY + 2,
17784
+ width: indexColW - 4,
17785
+ height: rowH - 4,
17786
+ color: BUCKET_INDEX_STROKE,
17787
+ fill: BUCKET_INDEX_FILL
17788
+ });
17789
+ out.push({
17790
+ type: "text",
17791
+ x: 2 + (indexColW - 4) / 2,
17792
+ y: rowY + rowH / 2,
17793
+ text: String(b.index),
17794
+ color: BUCKET_INDEX_TEXT,
17795
+ fontSize: 10,
17796
+ align: "center"
17797
+ });
17798
+ const overflow = b.entries.length > maxVisible;
17799
+ const visibleCount = overflow ? Math.max(0, maxVisible - 1) : b.entries.length;
17800
+ for (let j = 0; j < visibleCount; j++) {
17801
+ const entry = b.entries[j];
17802
+ const ex = indexColW + 4 + j * entryW;
17803
+ const state = entry.state ?? "default";
17804
+ const fill = state === "highlight" ? entry.color ?? BUCKET_ENTRY_HIGHLIGHT : state === "probing" ? entry.color ?? BUCKET_ENTRY_PROBING : entry.color ?? BUCKET_ENTRY_DEFAULT;
17805
+ out.push({
17806
+ type: "rect",
17807
+ id: `bucket-${b.index}-${j}`,
17808
+ x: ex,
17809
+ y: rowY + 2,
17810
+ width: entryW - 2,
17811
+ height: rowH - 4,
17812
+ color: fill,
17813
+ fill
17814
+ });
17815
+ if (entryW >= 20 && rowH >= 16) {
17816
+ out.push({
17817
+ type: "text",
17818
+ x: ex + (entryW - 2) / 2,
17819
+ y: rowY + rowH / 2,
17820
+ text: entry.label,
17821
+ color: BUCKET_ENTRY_TEXT,
17822
+ fontSize: 10,
17823
+ align: "center"
17824
+ });
17825
+ }
17826
+ }
17827
+ if (overflow) {
17828
+ const ex = indexColW + 4 + visibleCount * entryW;
17829
+ out.push({
17830
+ type: "rect",
17831
+ id: `bucket-${b.index}-overflow`,
17832
+ x: ex,
17833
+ y: rowY + 2,
17834
+ width: entryW - 2,
17835
+ height: rowH - 4,
17836
+ color: BUCKET_ENTRY_DEFAULT,
17837
+ fill: BUCKET_ENTRY_DEFAULT
17838
+ });
17839
+ out.push({
17840
+ type: "text",
17841
+ x: ex + (entryW - 2) / 2,
17842
+ y: rowY + rowH / 2,
17843
+ text: `+${b.entries.length - visibleCount}`,
17844
+ color: BUCKET_ENTRY_TEXT,
17845
+ fontSize: 10,
17846
+ align: "center"
17847
+ });
17848
+ }
17849
+ });
17850
+ }
17851
+ if (frames.length > 0) {
17852
+ const panelYFrames = panelY.frames;
17853
+ const n = frames.length;
17854
+ const frameH = panelHeight / n;
17855
+ const x = 8;
17856
+ const w = width - 16;
17857
+ frames.forEach((f3, i) => {
17858
+ const y = panelYFrames + panelHeight - (i + 1) * frameH;
17859
+ const state = f3.state ?? "active";
17860
+ const fill = state === "returning" ? f3.color ?? FRAME_RETURNING_COLOR : state === "done" ? f3.color ?? FRAME_DONE_COLOR : f3.color ?? FRAME_ACTIVE_COLOR;
17861
+ out.push({ type: "rect", id: `frame-${i}`, x, y, width: w, height: frameH, color: fill, fill });
17862
+ if (frameH >= FRAME_TWO_LINE_MIN_H) {
17863
+ out.push({
17864
+ type: "text",
17865
+ x: 16,
17866
+ y: y + frameH * 0.35,
17867
+ text: f3.label,
17868
+ color: FRAME_LABEL_COLOR,
17869
+ fontSize: 10,
17870
+ align: "left"
17871
+ });
17872
+ if (f3.detail) {
17873
+ out.push({
17874
+ type: "text",
17875
+ x: 16,
17876
+ y: y + frameH * 0.7,
17877
+ text: f3.detail,
17878
+ color: FRAME_DETAIL_COLOR,
17879
+ fontSize: 10,
17880
+ align: "left"
17881
+ });
17882
+ }
17883
+ } else {
17884
+ out.push({
17885
+ type: "text",
17886
+ x: 16,
17887
+ y: y + frameH / 2,
17888
+ text: f3.label,
17889
+ color: FRAME_LABEL_COLOR,
17890
+ fontSize: 10,
17891
+ align: "left"
17892
+ });
17893
+ }
16932
17894
  });
16933
17895
  }
16934
17896
  out.push(...shapes);
16935
17897
  return out;
16936
- }, [bars, cells, pointers, shapes, width, height]);
17898
+ }, [bars, cells, pointers, ranges, slots, slotOrientation, frames, buckets, auxBars, rowLabels, colLabels, shapes, width, height]);
16937
17899
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
16938
17900
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
16939
17901
  /* @__PURE__ */ jsx(
@@ -17095,23 +18057,422 @@ var init_AuthLayout = __esm({
17095
18057
  AuthLayout.displayName = "AuthLayout";
17096
18058
  }
17097
18059
  });
17098
- var BiologyCanvas;
18060
+ function LearningScene3D({
18061
+ className,
18062
+ width = 600,
18063
+ height = 400,
18064
+ title,
18065
+ backgroundColor,
18066
+ drawables,
18067
+ camera,
18068
+ lighting,
18069
+ post,
18070
+ showGrid = false,
18071
+ shadows,
18072
+ interactive,
18073
+ isLoading,
18074
+ error,
18075
+ onItemClick
18076
+ }) {
18077
+ const instanceId = useId().replace(/[^a-zA-Z0-9]/g, "");
18078
+ const clickEvent = onItemClick ? `LEARNING_SCENE_3D.ITEM_CLICK.${instanceId}` : void 0;
18079
+ const onItemClickRef = useRef(onItemClick);
18080
+ onItemClickRef.current = onItemClick;
18081
+ useEventListener(`UI:${clickEvent ?? "LEARNING_SCENE_3D.ITEM_CLICK.__disabled"}`, (event) => {
18082
+ const unitId = event.payload?.unitId;
18083
+ if (typeof unitId === "string") onItemClickRef.current?.(unitId);
18084
+ });
18085
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && post?.bloom) {
18086
+ const unknownKeys = Object.keys(post.bloom).filter((k) => !KNOWN_BLOOM_KEYS.has(k));
18087
+ if (unknownKeys.length > 0) {
18088
+ sceneLog.debug("post.bloom has unrecognized keys \u2014 only intensity/threshold/smoothing are read", { unknownKeys });
18089
+ }
18090
+ }
18091
+ const props3d = {
18092
+ drawables,
18093
+ isLoading,
18094
+ error: error ? error.message : null,
18095
+ cameraMode: camera?.mode ?? "perspective",
18096
+ ...camera?.zoom !== void 0 ? { zoom: camera.zoom } : {},
18097
+ ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
18098
+ ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
18099
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
18100
+ ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
18101
+ backgroundColor,
18102
+ showGrid,
18103
+ ...shadows !== void 0 ? { shadows } : {},
18104
+ ...interactive !== void 0 ? { controlsEnabled: interactive } : {},
18105
+ lighting,
18106
+ post,
18107
+ ...clickEvent !== void 0 ? { unitClickEvent: clickEvent } : {}
18108
+ };
18109
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
18110
+ title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
18111
+ /* @__PURE__ */ jsx("div", { style: { width, height, display: "flex" }, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(Canvas3DHost2, { ...props3d }) }) })
18112
+ ] }) });
18113
+ }
18114
+ function meshSphere(id, x, y, z, radius, color, opts) {
18115
+ return {
18116
+ type: "draw-mesh",
18117
+ ...id !== void 0 ? { id } : {},
18118
+ shape: opts?.shape ?? "sphere",
18119
+ position: { x, y, z },
18120
+ radius,
18121
+ pivot: "center",
18122
+ segments: opts?.segments ?? 24,
18123
+ material: { color, ...opts?.material },
18124
+ ...opts?.opacity !== void 0 ? { opacity: opts.opacity } : {}
18125
+ };
18126
+ }
18127
+ function axisRotation(from, to) {
18128
+ const wx = to[0] - from[0];
18129
+ const wy = to[2] - from[2];
18130
+ const wz = to[1] - from[1];
18131
+ const len = Math.sqrt(wx * wx + wy * wy + wz * wz);
18132
+ const tilt = Math.acos(Math.min(1, Math.max(-1, len > 0 ? wy / len : 1)));
18133
+ return [0, Math.atan2(wz, -wx), tilt];
18134
+ }
18135
+ function segmentLength(from, to) {
18136
+ const dx = to[0] - from[0];
18137
+ const dy = to[1] - from[1];
18138
+ const dz = to[2] - from[2];
18139
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
18140
+ }
18141
+ function cylinderBetween(from, to, radius, color) {
18142
+ const len = segmentLength(from, to);
18143
+ if (len < 1e-6) return null;
18144
+ return {
18145
+ type: "draw-mesh",
18146
+ shape: "cylinder",
18147
+ position: { x: (from[0] + to[0]) / 2, y: (from[1] + to[1]) / 2, z: (from[2] + to[2]) / 2 },
18148
+ radius,
18149
+ height: len,
18150
+ rotation: axisRotation(from, to),
18151
+ pivot: "center",
18152
+ segments: 12,
18153
+ material: { color }
18154
+ };
18155
+ }
18156
+ function arrowBetween(from, to, color, shaftRadius = 0.08, id) {
18157
+ const len = segmentLength(from, to);
18158
+ if (len < 1e-6) return null;
18159
+ const tipLen = Math.min(shaftRadius * 8, len * 0.35);
18160
+ const k = (len - tipLen) / len;
18161
+ const delta = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
18162
+ const shaftEnd = [delta[0] * k, delta[1] * k, delta[2] * k];
18163
+ const tipStart = shaftEnd;
18164
+ const tipEnd = delta;
18165
+ const shaft = cylinderBetween([0, 0, 0], shaftEnd, shaftRadius, color);
18166
+ const tipLenActual = segmentLength(tipStart, tipEnd);
18167
+ const tip = {
18168
+ type: "draw-mesh",
18169
+ shape: "cone",
18170
+ position: {
18171
+ x: (tipStart[0] + tipEnd[0]) / 2,
18172
+ y: (tipStart[1] + tipEnd[1]) / 2,
18173
+ z: (tipStart[2] + tipEnd[2]) / 2
18174
+ },
18175
+ radius: shaftRadius * 3,
18176
+ height: tipLenActual,
18177
+ rotation: axisRotation(tipStart, tipEnd),
18178
+ pivot: "center",
18179
+ segments: 12,
18180
+ material: { color }
18181
+ };
18182
+ return {
18183
+ type: "draw-group",
18184
+ ...id !== void 0 ? { id } : {},
18185
+ position: { x: from[0], y: from[1], z: from[2] },
18186
+ items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
18187
+ };
18188
+ }
18189
+ function billboardLabel(text, x, y, z, opts) {
18190
+ return {
18191
+ type: "draw-text",
18192
+ text,
18193
+ position: { x, y, z },
18194
+ color: opts?.color ?? "#111827"
18195
+ };
18196
+ }
18197
+ function labelColorForBackground(backgroundColor) {
18198
+ const m = /^#(?:([0-9a-f]{3})|([0-9a-f]{6}))$/i.exec(backgroundColor ?? "");
18199
+ if (!m) return "#111827";
18200
+ const hex = m[1] !== void 0 ? m[1].split("").map((c) => c + c).join("") : m[2];
18201
+ const r2 = parseInt(hex.slice(0, 2), 16) / 255;
18202
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
18203
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
18204
+ const luminance = 0.2126 * r2 + 0.7152 * g + 0.0722 * b;
18205
+ return luminance < 0.5 ? "#e5e7eb" : "#111827";
18206
+ }
18207
+ function get3DClickPayload(onShapeClick, idToIndex) {
18208
+ if (!onShapeClick) return void 0;
18209
+ return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
18210
+ }
18211
+ function polylineTube(points, radius, color, opts) {
18212
+ const maxSegments = opts?.maxSegments ?? 128;
18213
+ let pts = points;
18214
+ if (pts.length - 1 > maxSegments) {
18215
+ const step = (pts.length - 1) / maxSegments;
18216
+ const kept = [pts[0]];
18217
+ for (let s = 1; s < maxSegments; s++) kept.push(pts[Math.round(s * step)]);
18218
+ kept.push(pts[pts.length - 1]);
18219
+ pts = kept;
18220
+ }
18221
+ const out = [];
18222
+ for (let i = 0; i < pts.length - 1; i++) {
18223
+ const seg = cylinderBetween(pts[i], pts[i + 1], radius, color);
18224
+ if (seg) out.push(opts?.opacity !== void 0 ? { ...seg, opacity: opts.opacity } : seg);
18225
+ }
18226
+ return out;
18227
+ }
18228
+ function heightFieldMesh(spec) {
18229
+ const { nx, ny, heights, spacing = 1, x = 0, y = 0 } = spec;
18230
+ const flatShading = spec.flatShading ?? true;
18231
+ const vertices = [];
18232
+ for (let iy = 0; iy < ny; iy++) {
18233
+ for (let ix = 0; ix < nx; ix++) {
18234
+ vertices.push([
18235
+ x + (ix - (nx - 1) / 2) * spacing,
18236
+ y + (iy - (ny - 1) / 2) * spacing,
18237
+ heights[iy * nx + ix] ?? 0
18238
+ ]);
18239
+ }
18240
+ }
18241
+ const bands = [...spec.bands ?? []].sort((a, b) => (a.min ?? -Infinity) - (b.min ?? -Infinity));
18242
+ const facesByBand = /* @__PURE__ */ new Map();
18243
+ for (let iy = 0; iy < ny - 1; iy++) {
18244
+ for (let ix = 0; ix < nx - 1; ix++) {
18245
+ const v00 = iy * nx + ix;
18246
+ const v10 = iy * nx + ix + 1;
18247
+ const v01 = (iy + 1) * nx + ix;
18248
+ const v11 = (iy + 1) * nx + ix + 1;
18249
+ for (const face of [[v00, v10, v01], [v10, v11, v01]]) {
18250
+ const centroid = (vertices[face[0]][2] + vertices[face[1]][2] + vertices[face[2]][2]) / 3;
18251
+ let band = null;
18252
+ for (const b of bands) {
18253
+ if ((b.min ?? -Infinity) <= centroid) band = b;
18254
+ }
18255
+ const key = bands.length > 0 ? band : null;
18256
+ const list = facesByBand.get(key) ?? [];
18257
+ list.push(face);
18258
+ facesByBand.set(key, list);
18259
+ }
18260
+ }
18261
+ }
18262
+ const out = [];
18263
+ for (const [band, faces] of facesByBand) {
18264
+ if (faces.length === 0) continue;
18265
+ out.push({
18266
+ type: "draw-mesh",
18267
+ shape: "polyhedron",
18268
+ position: { x: 0, y: 0, z: 0 },
18269
+ vertices,
18270
+ faces,
18271
+ pivot: "center",
18272
+ material: { color: band?.color ?? spec.color ?? "#64748b", flatShading, side: "double" },
18273
+ ...spec.opacity !== void 0 ? { opacity: spec.opacity } : {}
18274
+ });
18275
+ }
18276
+ return out;
18277
+ }
18278
+ function arrowField(vectors, opts) {
18279
+ const scale = opts?.scale ?? 1;
18280
+ const out = [];
18281
+ for (const v of vectors) {
18282
+ const to = [
18283
+ v.from[0] + v.delta[0] * scale,
18284
+ v.from[1] + v.delta[1] * scale,
18285
+ v.from[2] + v.delta[2] * scale
18286
+ ];
18287
+ const arrow = arrowBetween(v.from, to, v.color ?? "#dc2626", v.width, v.id);
18288
+ if (arrow) out.push(arrow);
18289
+ if (v.label) out.push(billboardLabel(v.label, to[0], to[1], to[2], { color: opts?.labelColor }));
18290
+ }
18291
+ return out;
18292
+ }
18293
+ function helixDrawables(spec, opts) {
18294
+ const count = spec.count ?? spec.rungs?.length ?? 0;
18295
+ const rungs = Array.from({ length: count }, (_, i) => spec.rungs?.[i] ?? {});
18296
+ const radius = spec.radius ?? 1;
18297
+ const rise = spec.rise ?? 0.34;
18298
+ const twistRad = (spec.twistDeg ?? 36) * (Math.PI / 180);
18299
+ const strandAColor = spec.strandAColor ?? "#38bdf8";
18300
+ const strandBColor = spec.strandBColor ?? "#fb923c";
18301
+ const backboneRadius = spec.backboneRadius ?? 0.16;
18302
+ const rungRadius = spec.rungRadius ?? 0.12;
18303
+ const cx = spec.x ?? 0;
18304
+ const cy = spec.y ?? 0;
18305
+ const cz = spec.z ?? 0;
18306
+ const unwoundCount = spec.unwoundCount ?? 0;
18307
+ const unwindSpread = spec.unwindSpread ?? 1.8;
18308
+ const strandA = [];
18309
+ const strandB = [];
18310
+ for (let i = 0; i < count; i++) {
18311
+ const yi = cy + (i - (count - 1) / 2) * rise;
18312
+ const theta = i * twistRad;
18313
+ const s = i < unwoundCount ? unwindSpread : 1;
18314
+ strandA.push([cx + s * radius * Math.cos(theta), yi, cz + s * radius * Math.sin(theta)]);
18315
+ strandB.push([cx + s * radius * Math.cos(theta + Math.PI), yi, cz + s * radius * Math.sin(theta + Math.PI)]);
18316
+ }
18317
+ const out = [];
18318
+ for (let i = 0; i < count; i++) {
18319
+ out.push(meshSphere(`hx-a-${i}`, strandA[i][0], strandA[i][1], strandA[i][2], backboneRadius, strandAColor));
18320
+ out.push(meshSphere(`hx-b-${i}`, strandB[i][0], strandB[i][1], strandB[i][2], backboneRadius, strandBColor));
18321
+ if (i > 0) {
18322
+ const segA = cylinderBetween(strandA[i - 1], strandA[i], backboneRadius, strandAColor);
18323
+ if (segA) out.push(segA);
18324
+ const segB = cylinderBetween(strandB[i - 1], strandB[i], backboneRadius, strandBColor);
18325
+ if (segB) out.push(segB);
18326
+ }
18327
+ const rung = rungs[i];
18328
+ const rungColor = rung.color ?? "#94a3b8";
18329
+ const rod = cylinderBetween(strandA[i], strandB[i], rungRadius, rungColor);
18330
+ if (rod) out.push(rod);
18331
+ const mid = [
18332
+ (strandA[i][0] + strandB[i][0]) / 2,
18333
+ (strandA[i][1] + strandB[i][1]) / 2,
18334
+ (strandA[i][2] + strandB[i][2]) / 2
18335
+ ];
18336
+ const markerRadius = rung.radius ?? rungRadius;
18337
+ out.push(meshSphere(rung.id, mid[0], mid[1], mid[2], markerRadius, rungColor));
18338
+ if (rung.label) out.push(billboardLabel(rung.label, mid[0], mid[1], mid[2] + markerRadius, { color: opts?.labelColor }));
18339
+ }
18340
+ return out;
18341
+ }
18342
+ function latticeDrawables(spec, opts) {
18343
+ const nx = spec.nx ?? 2;
18344
+ const ny = spec.ny ?? 2;
18345
+ const nz = spec.nz ?? 2;
18346
+ const latticeConstant = spec.latticeConstant ?? 2;
18347
+ const bondRadius = spec.bondRadius ?? 0.06;
18348
+ const highlightCell = spec.highlightCell ?? false;
18349
+ const dimColor = spec.dimColor ?? "#475569";
18350
+ const showLabels = spec.showLabels ?? false;
18351
+ const selectedColor = spec.selectedColor ?? "#f59e0b";
18352
+ const posByKey = /* @__PURE__ */ new Map();
18353
+ const inCellByKey = /* @__PURE__ */ new Map();
18354
+ const out = [];
18355
+ for (const site of spec.basis) {
18356
+ const snx = site.xEdge ? nx + 1 : nx;
18357
+ const sny = site.yEdge ? ny + 1 : ny;
18358
+ const snz = site.zEdge ? nz + 1 : nz;
18359
+ for (let i = 0; i < snx; i++) {
18360
+ for (let j = 0; j < sny; j++) {
18361
+ for (let k = 0; k < snz; k++) {
18362
+ const key = `${site.key}-${i}-${j}-${k}`;
18363
+ const inCell = i + site.dx <= 1 && j + site.dy <= 1 && k + site.dz <= 1;
18364
+ const pos = [
18365
+ (i + site.dx) * latticeConstant - nx * latticeConstant / 2,
18366
+ (j + site.dy) * latticeConstant - ny * latticeConstant / 2,
18367
+ (k + site.dz) * latticeConstant - nz * latticeConstant / 2
18368
+ ];
18369
+ posByKey.set(key, pos);
18370
+ inCellByKey.set(key, inCell);
18371
+ const isSelected = spec.selectedId === `lat-${key}`;
18372
+ const color = isSelected ? selectedColor : highlightCell && !inCell ? dimColor : site.color ?? "#2563eb";
18373
+ const radius = (site.radius ?? 0.3) * (isSelected ? 1.4 : 1);
18374
+ out.push(meshSphere(`lat-${key}`, pos[0], pos[1], pos[2], radius, color));
18375
+ if (showLabels && site.element) {
18376
+ out.push(billboardLabel(site.element, pos[0], pos[1], pos[2] + radius, { color: opts?.labelColor }));
18377
+ }
18378
+ }
18379
+ }
18380
+ }
18381
+ }
18382
+ const basisByKey = new Map(spec.basis.map((s) => [s.key, s]));
18383
+ for (const bond of spec.bonds ?? []) {
18384
+ const fromSite = basisByKey.get(bond.from);
18385
+ const toSite = basisByKey.get(bond.to);
18386
+ if (!fromSite || !toSite) continue;
18387
+ const fnx = fromSite.xEdge ? nx + 1 : nx;
18388
+ const fny = fromSite.yEdge ? ny + 1 : ny;
18389
+ const fnz = fromSite.zEdge ? nz + 1 : nz;
18390
+ const tnx = toSite.xEdge ? nx + 1 : nx;
18391
+ const tny = toSite.yEdge ? ny + 1 : ny;
18392
+ const tnz = toSite.zEdge ? nz + 1 : nz;
18393
+ const bdx = bond.dx ?? 0;
18394
+ const bdy = bond.dy ?? 0;
18395
+ const bdz = bond.dz ?? 0;
18396
+ for (let i = 0; i < fnx; i++) {
18397
+ for (let j = 0; j < fny; j++) {
18398
+ for (let k = 0; k < fnz; k++) {
18399
+ const ti = i + bdx;
18400
+ const tj = j + bdy;
18401
+ const tk = k + bdz;
18402
+ if (ti < 0 || ti >= tnx || tj < 0 || tj >= tny || tk < 0 || tk >= tnz) continue;
18403
+ const fromKey = `${fromSite.key}-${i}-${j}-${k}`;
18404
+ const toKey = `${toSite.key}-${ti}-${tj}-${tk}`;
18405
+ const fromPos = posByKey.get(fromKey);
18406
+ const toPos = posByKey.get(toKey);
18407
+ if (!fromPos || !toPos) continue;
18408
+ const dimmed = highlightCell && !(inCellByKey.get(fromKey) && inCellByKey.get(toKey));
18409
+ const seg = cylinderBetween(fromPos, toPos, bondRadius, dimmed ? dimColor : bond.color ?? "#6b7280");
18410
+ if (seg) out.push(seg);
18411
+ }
18412
+ }
18413
+ }
18414
+ }
18415
+ return out;
18416
+ }
18417
+ var sceneLog, KNOWN_BLOOM_KEYS, Canvas3DHost2;
18418
+ var init_learningScene3D = __esm({
18419
+ "components/learning/molecules/learningScene3D.tsx"() {
18420
+ "use client";
18421
+ init_atoms();
18422
+ init_Stack();
18423
+ init_useEventBus();
18424
+ sceneLog = createLogger("almadar:ui:learning-scene-3d");
18425
+ KNOWN_BLOOM_KEYS = /* @__PURE__ */ new Set(["intensity", "threshold", "smoothing"]);
18426
+ Canvas3DHost2 = lazy(
18427
+ () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
18428
+ );
18429
+ LearningScene3D.displayName = "LearningScene3D";
18430
+ }
18431
+ });
18432
+ var biologyLog, BIO_BAND_COLORS, BIO_STAGE_FILL, BIO_STAGE_TEXT, BiologyCanvas;
17099
18433
  var init_BiologyCanvas = __esm({
17100
18434
  "components/learning/molecules/BiologyCanvas.tsx"() {
17101
18435
  "use client";
17102
18436
  init_atoms();
17103
18437
  init_Stack();
17104
18438
  init_LearningCanvas();
18439
+ init_learningScene3D();
18440
+ biologyLog = createLogger("almadar:ui:biology-canvas");
18441
+ BIO_BAND_COLORS = ["#dcfce7", "#fef9c3", "#fee2e2", "#e0e7ff"];
18442
+ BIO_STAGE_FILL = {
18443
+ pending: "#e2e8f0",
18444
+ active: "#3b82f6",
18445
+ done: "#94a3b8"
18446
+ };
18447
+ BIO_STAGE_TEXT = {
18448
+ pending: "#64748b",
18449
+ active: "#ffffff",
18450
+ done: "#ffffff"
18451
+ };
17105
18452
  BiologyCanvas = ({
17106
18453
  className,
17107
18454
  width = 600,
17108
18455
  height = 400,
17109
18456
  title,
17110
18457
  backgroundColor,
18458
+ mode = "2d",
18459
+ camera,
18460
+ lighting,
18461
+ post,
17111
18462
  nodes = [],
17112
18463
  edges = [],
18464
+ compartments = [],
18465
+ bands = [],
18466
+ stages = [],
18467
+ stageStyle = "timeline",
18468
+ helix,
18469
+ helix3d,
17113
18470
  shapes = [],
17114
- interactive = false,
18471
+ readouts,
18472
+ traces,
18473
+ showGrid,
18474
+ shadows,
18475
+ interactive,
17115
18476
  animate = false,
17116
18477
  onShapeClick,
17117
18478
  isLoading,
@@ -17123,19 +18484,148 @@ var init_BiologyCanvas = __esm({
17123
18484
  for (const n of nodes) {
17124
18485
  if (n.id) nodeById.set(n.id, n);
17125
18486
  }
18487
+ const bandCount = bands.length;
18488
+ for (let i = 0; i < bandCount; i++) {
18489
+ const band = bands[i];
18490
+ const bandColor = band.color ?? BIO_BAND_COLORS[i % BIO_BAND_COLORS.length];
18491
+ const bandY = i * height / bandCount;
18492
+ const bandH = height / bandCount;
18493
+ out.push({
18494
+ type: "rect",
18495
+ x: 0,
18496
+ y: bandY,
18497
+ width,
18498
+ height: bandH,
18499
+ color: bandColor,
18500
+ fill: bandColor,
18501
+ opacity: 0.45
18502
+ });
18503
+ if (band.label) {
18504
+ out.push({
18505
+ type: "text",
18506
+ x: 8,
18507
+ y: bandY + 14,
18508
+ text: band.label,
18509
+ color: "#6b7280",
18510
+ fontSize: 10
18511
+ });
18512
+ }
18513
+ }
18514
+ for (const c of compartments) {
18515
+ const color = c.color ?? "#16a34a";
18516
+ out.push({
18517
+ type: "ellipse",
18518
+ x: c.x,
18519
+ y: c.y,
18520
+ width: c.width,
18521
+ height: c.height,
18522
+ color,
18523
+ fill: c.fill ?? `${color}1A`,
18524
+ lineWidth: c.lineWidth ?? 2,
18525
+ ...c.dash ? { dash: c.dash } : {}
18526
+ });
18527
+ if (c.label) {
18528
+ out.push({
18529
+ type: "text",
18530
+ x: c.x,
18531
+ y: c.y - c.height / 2 + 14,
18532
+ text: c.label,
18533
+ color: "#111827",
18534
+ fontSize: 11,
18535
+ align: "center"
18536
+ });
18537
+ }
18538
+ }
18539
+ if (helix) {
18540
+ const hx = helix.x ?? 24;
18541
+ const hy = helix.y ?? height * 0.25;
18542
+ const hw = helix.width ?? width - 48;
18543
+ const hh = helix.height ?? height * 0.5;
18544
+ const rungs = helix.rungs;
18545
+ const n = rungs.length;
18546
+ const cy = hy + hh / 2;
18547
+ const colorA = helix.colorA ?? "#2563eb";
18548
+ const colorB = helix.colorB ?? "#dc2626";
18549
+ const rungColor = helix.rungColor ?? "#94a3b8";
18550
+ const fork = helix.fork ?? 0;
18551
+ const maxSep = Math.min(hh - 8, 96);
18552
+ const strandA = [];
18553
+ const strandB = [];
18554
+ const rungGeoms = [];
18555
+ for (let i = 0; i < n; i++) {
18556
+ const rx = hx + (i + 0.5) * hw / n;
18557
+ const t = (i + 0.5) / n;
18558
+ const paired = t >= fork;
18559
+ const sep = paired ? 28 : 28 + (maxSep - 28) * ((fork - t) / fork);
18560
+ strandA.push({ x: rx, y: cy - sep / 2 });
18561
+ strandB.push({ x: rx, y: cy + sep / 2 });
18562
+ rungGeoms.push({ rx, sep, rung: rungs[i], paired });
18563
+ }
18564
+ for (let i = 1; i < n; i++) {
18565
+ out.push({ type: "line", x1: strandA[i - 1].x, y1: strandA[i - 1].y, x2: strandA[i].x, y2: strandA[i].y, color: colorA, lineWidth: 3 });
18566
+ }
18567
+ for (let i = 1; i < n; i++) {
18568
+ out.push({ type: "line", x1: strandB[i - 1].x, y1: strandB[i - 1].y, x2: strandB[i].x, y2: strandB[i].y, color: colorB, lineWidth: 3 });
18569
+ }
18570
+ for (const g of rungGeoms) {
18571
+ const rColor = g.rung.color ?? (g.rung.state === "new" ? "#16a34a" : rungColor);
18572
+ const topY = cy - g.sep / 2;
18573
+ const bottomY = cy + g.sep / 2;
18574
+ if (g.paired) {
18575
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: bottomY, color: rColor });
18576
+ if (g.rung.a) {
18577
+ out.push({ type: "text", x: g.rx, y: cy - g.sep / 4, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
18578
+ }
18579
+ if (g.rung.b) {
18580
+ out.push({ type: "text", x: g.rx, y: cy + g.sep / 4, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
18581
+ }
18582
+ } else {
18583
+ const stubTopY = topY + 8;
18584
+ const stubBottomY = bottomY - 8;
18585
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: stubTopY, color: rColor });
18586
+ out.push({ type: "line", x1: g.rx, y1: bottomY, x2: g.rx, y2: stubBottomY, color: rColor });
18587
+ if (g.rung.a) {
18588
+ out.push({ type: "text", x: g.rx, y: stubTopY + 6, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
18589
+ }
18590
+ if (g.rung.b) {
18591
+ out.push({ type: "text", x: g.rx, y: stubBottomY - 6, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
18592
+ }
18593
+ }
18594
+ }
18595
+ }
17126
18596
  for (const e of edges) {
17127
18597
  const a = nodeById.get(e.from);
17128
18598
  const b = nodeById.get(e.to);
17129
18599
  if (!a || !b) continue;
17130
- out.push({
17131
- type: "line",
17132
- x1: a.x,
17133
- y1: a.y,
17134
- x2: b.x,
17135
- y2: b.y,
17136
- color: e.color ?? "#9ca3af",
17137
- lineWidth: 2
17138
- });
18600
+ const color = e.color ?? "#9ca3af";
18601
+ if (e.directed) {
18602
+ const rA = a.radius ?? 16;
18603
+ const rB = b.radius ?? 16;
18604
+ const dx = b.x - a.x;
18605
+ const dy = b.y - a.y;
18606
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
18607
+ const ux = dx / dist;
18608
+ const uy = dy / dist;
18609
+ out.push({
18610
+ type: "arrow",
18611
+ x1: a.x + ux * rA,
18612
+ y1: a.y + uy * rA,
18613
+ x2: b.x - ux * rB,
18614
+ y2: b.y - uy * rB,
18615
+ color,
18616
+ lineWidth: 2
18617
+ });
18618
+ } else {
18619
+ out.push({
18620
+ type: "line",
18621
+ x1: a.x,
18622
+ y1: a.y,
18623
+ x2: b.x,
18624
+ y2: b.y,
18625
+ color,
18626
+ lineWidth: 2
18627
+ });
18628
+ }
17139
18629
  if (e.label) {
17140
18630
  out.push({
17141
18631
  type: "text",
@@ -17148,6 +18638,8 @@ var init_BiologyCanvas = __esm({
17148
18638
  }
17149
18639
  }
17150
18640
  for (const n of nodes) {
18641
+ const state = n.state ?? "default";
18642
+ const muted = state === "muted";
17151
18643
  out.push({
17152
18644
  type: "circle",
17153
18645
  x: n.x,
@@ -17155,23 +18647,197 @@ var init_BiologyCanvas = __esm({
17155
18647
  radius: n.radius ?? 16,
17156
18648
  color: n.color ?? "#16a34a",
17157
18649
  fill: `${n.color ?? "#16a34a"}33`,
17158
- id: n.id
18650
+ id: n.id,
18651
+ ...muted ? { opacity: 0.35 } : {}
17159
18652
  });
18653
+ if (state === "highlight") {
18654
+ out.push({
18655
+ type: "circle",
18656
+ x: n.x,
18657
+ y: n.y,
18658
+ radius: (n.radius ?? 16) + 4,
18659
+ color: "#f59e0b",
18660
+ lineWidth: 2
18661
+ });
18662
+ }
17160
18663
  if (n.label) {
17161
18664
  out.push({
17162
18665
  type: "text",
17163
18666
  x: n.x,
17164
18667
  y: n.y + (n.radius ?? 16) + 14,
17165
18668
  text: n.label,
18669
+ ...muted ? { opacity: 0.35 } : {},
17166
18670
  color: "#111827",
17167
18671
  fontSize: 12,
17168
18672
  align: "center"
17169
18673
  });
17170
18674
  }
17171
18675
  }
18676
+ const stageCount = stages.length;
18677
+ if (stageCount > 0) {
18678
+ if (stageStyle === "ring") {
18679
+ const cx = width / 2;
18680
+ const cy = height / 2;
18681
+ const R = Math.min(width, height) / 2 - 48;
18682
+ const ringPoints = [];
18683
+ for (let i = 0; i < stageCount; i++) {
18684
+ const angleRad = (-90 + 360 * i / stageCount) * Math.PI / 180;
18685
+ ringPoints.push({ x: cx + R * Math.cos(angleRad), y: cy + R * Math.sin(angleRad) });
18686
+ }
18687
+ if (stageCount >= 2) {
18688
+ for (let i = 0; i < stageCount - 1; i++) {
18689
+ const p1 = ringPoints[i];
18690
+ const p2 = ringPoints[i + 1];
18691
+ const dx = p2.x - p1.x;
18692
+ const dy = p2.y - p1.y;
18693
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
18694
+ const ux = dx / dist;
18695
+ const uy = dy / dist;
18696
+ out.push({
18697
+ type: "arrow",
18698
+ x1: p1.x + ux * 46,
18699
+ y1: p1.y + uy * 46,
18700
+ x2: p2.x - ux * 46,
18701
+ y2: p2.y - uy * 46,
18702
+ color: "#94a3b8"
18703
+ });
18704
+ }
18705
+ }
18706
+ for (let i = 0; i < stageCount; i++) {
18707
+ const stage = stages[i];
18708
+ const state = stage.state ?? "pending";
18709
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
18710
+ const w = Math.max(26, Math.min(84, stage.label.length * 6 + 10));
18711
+ const h = 18;
18712
+ const p = ringPoints[i];
18713
+ out.push({ type: "rect", x: p.x - w / 2, y: p.y - h / 2, width: w, height: h, color: fill, fill });
18714
+ out.push({ type: "text", x: p.x, y: p.y, text: stage.label, color: BIO_STAGE_TEXT[state], fontSize: 10, align: "center" });
18715
+ }
18716
+ } else {
18717
+ const stripY = height - 32;
18718
+ const slotW = (width - 16) / stageCount;
18719
+ const chipGeoms = [];
18720
+ for (let i = 0; i < stageCount; i++) {
18721
+ chipGeoms.push({ x: 8 + i * slotW + 5, w: slotW - 10 });
18722
+ }
18723
+ for (let i = 0; i < stageCount - 1; i++) {
18724
+ const midY = stripY + 13;
18725
+ out.push({
18726
+ type: "arrow",
18727
+ x1: chipGeoms[i].x + chipGeoms[i].w,
18728
+ y1: midY,
18729
+ x2: chipGeoms[i + 1].x,
18730
+ y2: midY,
18731
+ color: "#94a3b8"
18732
+ });
18733
+ }
18734
+ for (let i = 0; i < stageCount; i++) {
18735
+ const stage = stages[i];
18736
+ const state = stage.state ?? "pending";
18737
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
18738
+ const g = chipGeoms[i];
18739
+ out.push({ type: "rect", x: g.x, y: stripY, width: g.w, height: 26, color: fill, fill });
18740
+ out.push({
18741
+ type: "text",
18742
+ x: g.x + g.w / 2,
18743
+ y: stripY + 13,
18744
+ text: stage.label,
18745
+ color: BIO_STAGE_TEXT[state],
18746
+ fontSize: 10,
18747
+ align: "center"
18748
+ });
18749
+ }
18750
+ }
18751
+ }
17172
18752
  out.push(...shapes);
17173
18753
  return out;
17174
- }, [nodes, edges, shapes]);
18754
+ }, [nodes, edges, compartments, bands, stages, stageStyle, helix, shapes, width, height]);
18755
+ const drawables3D = useMemo(() => {
18756
+ if (mode !== "3d") return [];
18757
+ if (shapes.length > 0) {
18758
+ biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
18759
+ }
18760
+ if (compartments.length > 0 || bands.length > 0 || stages.length > 0 || helix) {
18761
+ biologyLog.debug("2D-only families ignored in 3D mode (pixel-authored 2D vocabulary)", {
18762
+ compartments: compartments.length,
18763
+ bands: bands.length,
18764
+ stages: stages.length,
18765
+ helix: helix != null
18766
+ });
18767
+ }
18768
+ if (animate) {
18769
+ biologyLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
18770
+ }
18771
+ const out = [];
18772
+ const labelColor = labelColorForBackground(backgroundColor);
18773
+ const nodeById = /* @__PURE__ */ new Map();
18774
+ for (const n of nodes) {
18775
+ if (n.id) nodeById.set(n.id, n);
18776
+ }
18777
+ for (const e of edges) {
18778
+ const a = nodeById.get(e.from);
18779
+ const b = nodeById.get(e.to);
18780
+ if (!a || !b) continue;
18781
+ const edgeRadius = Math.max(0.04, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.12);
18782
+ const edge = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], edgeRadius, e.color ?? "#9ca3af");
18783
+ if (edge) out.push(edge);
18784
+ if (e.label) {
18785
+ out.push(
18786
+ billboardLabel(
18787
+ e.label,
18788
+ (a.x + b.x) / 2,
18789
+ (a.y + b.y) / 2,
18790
+ ((a.z ?? 0) + (b.z ?? 0)) / 2,
18791
+ { color: labelColor }
18792
+ )
18793
+ );
18794
+ }
18795
+ }
18796
+ for (const n of nodes) {
18797
+ const radius = n.radius ?? 0.5;
18798
+ const nz = n.z ?? 0;
18799
+ out.push(meshSphere(n.id, n.x, n.y, nz, radius, n.color ?? "#16a34a", { shape: n.shape, ...n.opacity !== void 0 ? { opacity: n.opacity } : {} }));
18800
+ if (n.label) {
18801
+ out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
18802
+ }
18803
+ }
18804
+ if (helix3d) {
18805
+ out.push(...helixDrawables(helix3d, { labelColor }));
18806
+ }
18807
+ return out;
18808
+ }, [mode, nodes, edges, shapes, compartments, bands, stages, helix, helix3d, animate, backgroundColor]);
18809
+ const nodeIndexById = useMemo(() => {
18810
+ const m = /* @__PURE__ */ new Map();
18811
+ (helix3d?.rungs ?? []).forEach((rung, i) => {
18812
+ if (rung.id) m.set(rung.id, i);
18813
+ });
18814
+ nodes.forEach((n, i) => {
18815
+ if (n.id) m.set(n.id, i);
18816
+ });
18817
+ return m;
18818
+ }, [nodes, helix3d]);
18819
+ if (mode === "3d") {
18820
+ return /* @__PURE__ */ jsx(
18821
+ LearningScene3D,
18822
+ {
18823
+ className,
18824
+ width,
18825
+ height,
18826
+ title,
18827
+ backgroundColor,
18828
+ drawables: drawables3D,
18829
+ camera,
18830
+ lighting,
18831
+ post,
18832
+ showGrid,
18833
+ shadows,
18834
+ interactive,
18835
+ isLoading,
18836
+ error,
18837
+ onItemClick: get3DClickPayload(onShapeClick, nodeIndexById)
18838
+ }
18839
+ );
18840
+ }
17175
18841
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
17176
18842
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
17177
18843
  /* @__PURE__ */ jsx(
@@ -17181,7 +18847,9 @@ var init_BiologyCanvas = __esm({
17181
18847
  height,
17182
18848
  backgroundColor,
17183
18849
  shapes: derivedShapes,
17184
- interactive,
18850
+ readouts,
18851
+ traces,
18852
+ interactive: interactive ?? false,
17185
18853
  animate,
17186
18854
  onShapeClick,
17187
18855
  isLoading,
@@ -24021,24 +25689,55 @@ var init_ChatBar = __esm({
24021
25689
  ChatBar.displayName = "ChatBar";
24022
25690
  }
24023
25691
  });
24024
- var ChemistryCanvas;
25692
+ function bondPerpendicular(a, b) {
25693
+ const dx = b[0] - a[0];
25694
+ const dy = b[1] - a[1];
25695
+ const px = dy;
25696
+ const py = -dx;
25697
+ const len = Math.sqrt(px * px + py * py);
25698
+ if (len < 1e-6) return [1, 0, 0];
25699
+ return [px / len, py / len, 0];
25700
+ }
25701
+ var chemistryLog, CHEM_BOND_STATE_COLOR, LONE_PAIR_ANGLES, ChemistryCanvas;
24025
25702
  var init_ChemistryCanvas = __esm({
24026
25703
  "components/learning/molecules/ChemistryCanvas.tsx"() {
24027
25704
  "use client";
24028
25705
  init_atoms();
24029
25706
  init_Stack();
24030
25707
  init_LearningCanvas();
25708
+ init_learningScene3D();
25709
+ chemistryLog = createLogger("almadar:ui:chemistry-canvas");
25710
+ CHEM_BOND_STATE_COLOR = {
25711
+ default: "#6b7280",
25712
+ forming: "#16a34a",
25713
+ breaking: "#dc2626",
25714
+ highlight: "#f59e0b"
25715
+ };
25716
+ LONE_PAIR_ANGLES = [-90, 0, 90, 180];
24031
25717
  ChemistryCanvas = ({
24032
25718
  className,
24033
25719
  width = 600,
24034
25720
  height = 400,
24035
25721
  title,
24036
25722
  backgroundColor,
25723
+ mode = "2d",
25724
+ camera,
25725
+ lighting,
25726
+ post,
24037
25727
  atoms = [],
24038
25728
  bonds = [],
24039
25729
  arrows = [],
25730
+ bondStyle = "thick",
25731
+ containers = [],
25732
+ equation,
25733
+ equationColor,
25734
+ lattice3d,
24040
25735
  shapes = [],
24041
- interactive = false,
25736
+ readouts,
25737
+ traces,
25738
+ showGrid,
25739
+ shadows,
25740
+ interactive,
24042
25741
  animate = false,
24043
25742
  onShapeClick,
24044
25743
  isLoading,
@@ -24050,21 +25749,118 @@ var init_ChemistryCanvas = __esm({
24050
25749
  for (const a of atoms) {
24051
25750
  if (a.id) atomById.set(a.id, a);
24052
25751
  }
25752
+ for (const c of containers) {
25753
+ const color = c.color ?? "#64748b";
25754
+ if (c.level != null) {
25755
+ const lv = c.level;
25756
+ out.push({
25757
+ type: "rect",
25758
+ x: c.x + 1,
25759
+ y: c.y + c.height * (1 - lv),
25760
+ width: c.width - 2,
25761
+ height: c.height * lv - 1,
25762
+ color: c.levelColor ?? "#60a5fa",
25763
+ fill: c.levelColor ?? "#60a5fa",
25764
+ opacity: 0.5
25765
+ });
25766
+ }
25767
+ out.push({
25768
+ type: "rect",
25769
+ x: c.x,
25770
+ y: c.y,
25771
+ width: c.width,
25772
+ height: c.height,
25773
+ color,
25774
+ fill: c.fill,
25775
+ lineWidth: c.lineWidth ?? 2
25776
+ });
25777
+ const divider = c.divider ?? "none";
25778
+ if (divider !== "none") {
25779
+ out.push({
25780
+ type: "line",
25781
+ x1: c.x + c.width / 2,
25782
+ y1: c.y,
25783
+ x2: c.x + c.width / 2,
25784
+ y2: c.y + c.height,
25785
+ color: c.dividerColor ?? color,
25786
+ ...divider === "dashed" || divider === "dotted" ? { dash: divider } : {}
25787
+ });
25788
+ }
25789
+ if (c.leftLabel) {
25790
+ out.push({
25791
+ type: "text",
25792
+ x: c.x + c.width * 0.25,
25793
+ y: c.y + 12,
25794
+ text: c.leftLabel,
25795
+ color: "#374151",
25796
+ fontSize: 11,
25797
+ align: "center"
25798
+ });
25799
+ }
25800
+ if (c.rightLabel) {
25801
+ out.push({
25802
+ type: "text",
25803
+ x: c.x + c.width * 0.75,
25804
+ y: c.y + 12,
25805
+ text: c.rightLabel,
25806
+ color: "#374151",
25807
+ fontSize: 11,
25808
+ align: "center"
25809
+ });
25810
+ }
25811
+ if (c.label) {
25812
+ out.push({
25813
+ type: "text",
25814
+ x: c.x + c.width / 2,
25815
+ y: c.y + c.height + 12,
25816
+ text: c.label,
25817
+ color: "#111827",
25818
+ fontSize: 12,
25819
+ align: "center"
25820
+ });
25821
+ }
25822
+ }
24053
25823
  for (const b of bonds) {
24054
25824
  const a = atomById.get(b.from);
24055
25825
  const c = atomById.get(b.to);
24056
25826
  if (!a || !c) continue;
24057
- const color = b.color ?? "#6b7280";
24058
- const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
24059
- out.push({
24060
- type: "line",
24061
- x1: a.x,
24062
- y1: a.y,
24063
- x2: c.x,
24064
- y2: c.y,
24065
- color,
24066
- lineWidth: strokeWidth
24067
- });
25827
+ const state = b.state ?? "default";
25828
+ const color = b.color ?? CHEM_BOND_STATE_COLOR[state];
25829
+ const dash = state === "forming" || state === "breaking" ? "dashed" : void 0;
25830
+ if (bondStyle === "parallel") {
25831
+ const dx = c.x - a.x;
25832
+ const dy = c.y - a.y;
25833
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
25834
+ const ux = dx / dist;
25835
+ const uy = dy / dist;
25836
+ const px = -uy;
25837
+ const py = ux;
25838
+ const offsets = b.type === "double" ? [-3, 3] : b.type === "triple" ? [-4, 0, 4] : [0];
25839
+ for (const off of offsets) {
25840
+ out.push({
25841
+ type: "line",
25842
+ x1: a.x + px * off,
25843
+ y1: a.y + py * off,
25844
+ x2: c.x + px * off,
25845
+ y2: c.y + py * off,
25846
+ color,
25847
+ lineWidth: 2,
25848
+ ...dash ? { dash } : {}
25849
+ });
25850
+ }
25851
+ } else {
25852
+ const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
25853
+ out.push({
25854
+ type: "line",
25855
+ x1: a.x,
25856
+ y1: a.y,
25857
+ x2: c.x,
25858
+ y2: c.y,
25859
+ color,
25860
+ lineWidth: strokeWidth,
25861
+ ...dash ? { dash } : {}
25862
+ });
25863
+ }
24068
25864
  }
24069
25865
  for (const a of arrows) {
24070
25866
  const angle = (a.angle ?? 0) * (Math.PI / 180);
@@ -24113,10 +25909,142 @@ var init_ChemistryCanvas = __esm({
24113
25909
  align: "center"
24114
25910
  });
24115
25911
  }
25912
+ const r2 = a.radius ?? 14;
25913
+ if (a.charge) {
25914
+ out.push({
25915
+ type: "text",
25916
+ x: a.x + r2 * 0.85,
25917
+ y: a.y - r2 * 0.85,
25918
+ text: a.charge,
25919
+ color: "#111827",
25920
+ fontSize: 9,
25921
+ align: "left"
25922
+ });
25923
+ }
25924
+ const lonePairs = Math.max(0, Math.min(4, a.lonePairs ?? 0));
25925
+ for (let k = 0; k < lonePairs; k++) {
25926
+ const angleRad = LONE_PAIR_ANGLES[k] * Math.PI / 180;
25927
+ const cx = a.x + (r2 + 6) * Math.cos(angleRad);
25928
+ const cy = a.y + (r2 + 6) * Math.sin(angleRad);
25929
+ const perpX = -Math.sin(angleRad);
25930
+ const perpY = Math.cos(angleRad);
25931
+ for (const sign of [1, -1]) {
25932
+ out.push({
25933
+ type: "circle",
25934
+ x: cx + perpX * 2.5 * sign,
25935
+ y: cy + perpY * 2.5 * sign,
25936
+ radius: 1.5,
25937
+ color: "#374151",
25938
+ fill: "#374151"
25939
+ });
25940
+ }
25941
+ }
25942
+ }
25943
+ if (equation) {
25944
+ out.push({
25945
+ type: "text",
25946
+ x: width / 2,
25947
+ y: 14,
25948
+ text: equation,
25949
+ color: equationColor ?? "#111827",
25950
+ fontSize: 13,
25951
+ align: "center"
25952
+ });
24116
25953
  }
24117
25954
  out.push(...shapes);
24118
25955
  return out;
24119
- }, [atoms, bonds, arrows, shapes]);
25956
+ }, [atoms, bonds, arrows, bondStyle, containers, equation, equationColor, shapes, width]);
25957
+ const drawables3D = useMemo(() => {
25958
+ if (mode !== "3d") return [];
25959
+ if (shapes.length > 0) {
25960
+ chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
25961
+ }
25962
+ if (containers.length > 0) {
25963
+ chemistryLog.debug("containers ignored in 3D mode (pixel-authored 2D vocabulary)", { count: containers.length });
25964
+ }
25965
+ if (animate) {
25966
+ chemistryLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
25967
+ }
25968
+ const out = [];
25969
+ const labelColor = labelColorForBackground(backgroundColor);
25970
+ const atomById = /* @__PURE__ */ new Map();
25971
+ for (const a of atoms) {
25972
+ if (a.id) atomById.set(a.id, a);
25973
+ }
25974
+ for (const b of bonds) {
25975
+ const a = atomById.get(b.from);
25976
+ const c = atomById.get(b.to);
25977
+ if (!a || !c) continue;
25978
+ const color = b.color ?? "#6b7280";
25979
+ const from = [a.x, a.y, a.z ?? 0];
25980
+ const to = [c.x, c.y, c.z ?? 0];
25981
+ const perp = bondPerpendicular(from, to);
25982
+ const bondRadius = Math.max(0.06, Math.min(a.radius ?? 0.45, c.radius ?? 0.45) * 0.22);
25983
+ const step = bondRadius * 2.2;
25984
+ const offsets = b.type === "double" ? [-step, step] : b.type === "triple" ? [-step, 0, step] : [0];
25985
+ for (const off of offsets) {
25986
+ const bond = cylinderBetween(
25987
+ [from[0] + perp[0] * off, from[1] + perp[1] * off, from[2] + perp[2] * off],
25988
+ [to[0] + perp[0] * off, to[1] + perp[1] * off, to[2] + perp[2] * off],
25989
+ bondRadius,
25990
+ color
25991
+ );
25992
+ if (bond) out.push(bond);
25993
+ }
25994
+ }
25995
+ for (const a of arrows) {
25996
+ const angle = (a.angle ?? 0) * (Math.PI / 180);
25997
+ const len = a.length ?? 60;
25998
+ const x2 = a.x + Math.cos(angle) * len;
25999
+ const y2 = a.y + Math.sin(angle) * len;
26000
+ const arrow = arrowBetween([a.x, a.y, 0], [x2, y2, 0], a.color ?? "#dc2626");
26001
+ if (arrow) out.push(arrow);
26002
+ if (a.label) {
26003
+ out.push(billboardLabel(a.label, (a.x + x2) / 2, (a.y + y2) / 2, 0, { color: labelColor }));
26004
+ }
26005
+ }
26006
+ for (const a of atoms) {
26007
+ const radius = a.radius ?? 0.45;
26008
+ const az = a.z ?? 0;
26009
+ out.push(meshSphere(a.id, a.x, a.y, az, radius, a.color ?? "#2563eb"));
26010
+ if (a.element) {
26011
+ out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
26012
+ }
26013
+ }
26014
+ if (lattice3d) {
26015
+ out.push(...latticeDrawables(lattice3d, { labelColor }));
26016
+ }
26017
+ return out;
26018
+ }, [mode, atoms, bonds, arrows, shapes, containers, lattice3d, animate, backgroundColor]);
26019
+ const atomIndexById = useMemo(() => {
26020
+ const m = /* @__PURE__ */ new Map();
26021
+ atoms.forEach((a, i) => {
26022
+ if (a.id) m.set(a.id, i);
26023
+ });
26024
+ return m;
26025
+ }, [atoms]);
26026
+ if (mode === "3d") {
26027
+ return /* @__PURE__ */ jsx(
26028
+ LearningScene3D,
26029
+ {
26030
+ className,
26031
+ width,
26032
+ height,
26033
+ title,
26034
+ backgroundColor,
26035
+ drawables: drawables3D,
26036
+ camera,
26037
+ lighting,
26038
+ post,
26039
+ showGrid,
26040
+ shadows,
26041
+ interactive,
26042
+ isLoading,
26043
+ error,
26044
+ onItemClick: get3DClickPayload(onShapeClick, atomIndexById)
26045
+ }
26046
+ );
26047
+ }
24120
26048
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
24121
26049
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
24122
26050
  /* @__PURE__ */ jsx(
@@ -24126,7 +26054,9 @@ var init_ChemistryCanvas = __esm({
24126
26054
  height,
24127
26055
  backgroundColor,
24128
26056
  shapes: derivedShapes,
24129
- interactive,
26057
+ readouts,
26058
+ traces,
26059
+ interactive: interactive ?? false,
24130
26060
  animate,
24131
26061
  onShapeClick,
24132
26062
  isLoading,
@@ -30315,6 +32245,10 @@ var init_ProgressDots = __esm({
30315
32245
  ProgressDots.displayName = "ProgressDots";
30316
32246
  }
30317
32247
  });
32248
+ function formatTick(v) {
32249
+ if (Number.isInteger(v)) return String(v);
32250
+ return v.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
32251
+ }
30318
32252
  var MathCanvas;
30319
32253
  var init_MathCanvas = __esm({
30320
32254
  "components/learning/molecules/MathCanvas.tsx"() {
@@ -30334,10 +32268,19 @@ var init_MathCanvas = __esm({
30334
32268
  showAxes = true,
30335
32269
  showGrid = true,
30336
32270
  gridStep = 1,
32271
+ showTickLabels = false,
32272
+ showCurveLabels = false,
30337
32273
  curves = [],
30338
32274
  points = [],
30339
32275
  vectors = [],
32276
+ regions = [],
32277
+ bars = [],
32278
+ guides = [],
32279
+ angles = [],
32280
+ hops = [],
30340
32281
  shapes = [],
32282
+ readouts,
32283
+ traces,
30341
32284
  interactive = false,
30342
32285
  animate = false,
30343
32286
  onShapeClick,
@@ -30351,6 +32294,8 @@ var init_MathCanvas = __esm({
30351
32294
  const plotH = height - margin * 2;
30352
32295
  const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
30353
32296
  const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
32297
+ const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
32298
+ const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
30354
32299
  if (showGrid) {
30355
32300
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep) {
30356
32301
  const px = mapX(x);
@@ -30361,14 +32306,99 @@ var init_MathCanvas = __esm({
30361
32306
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color: "#e5e7eb", lineWidth: 1 });
30362
32307
  }
30363
32308
  }
32309
+ if (showTickLabels) {
32310
+ const labelEveryX = Math.max(1, Math.ceil((xMax - xMin) / gridStep / Math.floor(plotW / 40)));
32311
+ let kx = 0;
32312
+ for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
32313
+ if (kx % labelEveryX === 0 && x !== 0) {
32314
+ out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: 10, align: "center" });
32315
+ }
32316
+ }
32317
+ const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
32318
+ let ky = 0;
32319
+ for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
32320
+ if (ky % labelEveryY === 0 && y !== 0) {
32321
+ out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: 10, align: "right" });
32322
+ }
32323
+ }
32324
+ if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
32325
+ out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: 10, align: "right" });
32326
+ }
32327
+ }
32328
+ for (const region of regions) {
32329
+ if (!region.samples || region.samples.length === 0) continue;
32330
+ const baseline = region.baseline ?? 0;
32331
+ const clampedPoint = (p) => ({
32332
+ x: mapX(Math.min(xMax, Math.max(xMin, p.x))),
32333
+ y: mapY(Math.min(yMax, Math.max(yMin, p.y)))
32334
+ });
32335
+ const upper = region.samples.map(clampedPoint);
32336
+ const first = region.samples[0];
32337
+ const last = region.samples[region.samples.length - 1];
32338
+ const closing = region.samples2 && region.samples2.length > 0 ? [...region.samples2].reverse().map(clampedPoint) : [clampedPoint({ x: last.x, y: baseline }), clampedPoint({ x: first.x, y: baseline })];
32339
+ const color = region.color ?? "#2563eb";
32340
+ out.push({
32341
+ type: "polygon",
32342
+ points: [...upper, ...closing],
32343
+ fill: color,
32344
+ color,
32345
+ opacity: region.opacity ?? 0.2,
32346
+ lineWidth: 1
32347
+ });
32348
+ if (region.label) {
32349
+ const mid = Math.floor(region.samples.length / 2);
32350
+ out.push({
32351
+ type: "text",
32352
+ x: mapX((first.x + last.x) / 2),
32353
+ y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
32354
+ text: region.label,
32355
+ color: "#111827",
32356
+ fontSize: 11
32357
+ });
32358
+ }
32359
+ }
32360
+ for (const bar of bars) {
32361
+ if (bar.x + bar.width < xMin || bar.x > xMax) continue;
32362
+ const y0 = bar.y0 ?? 0;
32363
+ const color = bar.color ?? "#93c5fd";
32364
+ out.push({
32365
+ type: "rect",
32366
+ x: mapX(bar.x),
32367
+ y: mapY(Math.max(y0, bar.y1)),
32368
+ width: mapX(bar.x + bar.width) - mapX(bar.x),
32369
+ height: Math.abs(mapY(bar.y1) - mapY(y0)),
32370
+ color,
32371
+ fill: color,
32372
+ opacity: bar.opacity ?? 0.5,
32373
+ lineWidth: 1
32374
+ });
32375
+ }
30364
32376
  if (showAxes) {
30365
- const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
30366
- const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
30367
32377
  out.push({ type: "line", x1: margin, y1: xAxisY, x2: width - margin, y2: xAxisY, color: "#374151", lineWidth: 2 });
30368
32378
  out.push({ type: "line", x1: yAxisX, y1: margin, x2: yAxisX, y2: height - margin, color: "#374151", lineWidth: 2 });
30369
32379
  }
32380
+ for (const guide of guides) {
32381
+ const color = guide.color ?? "#9ca3af";
32382
+ const dash = guide.dash ?? "dashed";
32383
+ if (guide.kind === "vline") {
32384
+ if (guide.at < xMin || guide.at > xMax) continue;
32385
+ const px = mapX(guide.at);
32386
+ out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
32387
+ if (guide.label) {
32388
+ out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color: "#111827", fontSize: 11 });
32389
+ }
32390
+ } else {
32391
+ if (guide.at < yMin || guide.at > yMax) continue;
32392
+ const py = mapY(guide.at);
32393
+ out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
32394
+ if (guide.label) {
32395
+ out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color: "#111827", fontSize: 11, align: "right" });
32396
+ }
32397
+ }
32398
+ }
30370
32399
  for (const curve of curves) {
30371
32400
  if (!curve.samples || curve.samples.length < 2) continue;
32401
+ let lastInRange;
30372
32402
  for (let i = 1; i < curve.samples.length; i++) {
30373
32403
  const a = curve.samples[i - 1];
30374
32404
  const b = curve.samples[i];
@@ -30380,19 +32410,97 @@ var init_MathCanvas = __esm({
30380
32410
  x2: mapX(b.x),
30381
32411
  y2: mapY(b.y),
30382
32412
  color: curve.color ?? "#2563eb",
30383
- lineWidth: 2
32413
+ lineWidth: 2,
32414
+ dash: curve.dash
32415
+ });
32416
+ lastInRange = b;
32417
+ }
32418
+ if (showCurveLabels && curve.label && lastInRange) {
32419
+ out.push({
32420
+ type: "text",
32421
+ x: mapX(lastInRange.x) + 6,
32422
+ y: mapY(lastInRange.y) - 6,
32423
+ text: curve.label,
32424
+ color: curve.color ?? "#2563eb",
32425
+ fontSize: 11
32426
+ });
32427
+ }
32428
+ }
32429
+ for (const hop of hops) {
32430
+ const x1 = mapX(hop.from);
32431
+ const x2 = mapX(hop.to);
32432
+ const peak = Math.min(36, plotH * 0.3);
32433
+ const color = hop.color ?? "#7c3aed";
32434
+ out.push({
32435
+ type: "ellipse",
32436
+ x: (x1 + x2) / 2,
32437
+ y: xAxisY,
32438
+ width: Math.abs(x2 - x1),
32439
+ height: 2 * peak,
32440
+ startAngle: 180,
32441
+ endAngle: 360,
32442
+ color
32443
+ });
32444
+ const s = Math.sign(hop.to - hop.from);
32445
+ out.push({
32446
+ type: "polygon",
32447
+ points: [
32448
+ { x: x2, y: xAxisY },
32449
+ { x: x2 - 4 * s, y: xAxisY - 7 },
32450
+ { x: x2 + 2 * s, y: xAxisY - 7 }
32451
+ ],
32452
+ fill: color,
32453
+ color
32454
+ });
32455
+ if (hop.label) {
32456
+ out.push({
32457
+ type: "text",
32458
+ x: (x1 + x2) / 2,
32459
+ y: xAxisY - peak - 8,
32460
+ text: hop.label,
32461
+ color: "#111827",
32462
+ fontSize: 10,
32463
+ align: "center"
32464
+ });
32465
+ }
32466
+ }
32467
+ for (const angle of angles) {
32468
+ const radius = angle.radius ?? 0.8;
32469
+ const color = angle.color ?? "#0ea5e9";
32470
+ out.push({
32471
+ type: "ellipse",
32472
+ x: mapX(angle.x),
32473
+ y: mapY(angle.y),
32474
+ width: 2 * radius * plotW / (xMax - xMin),
32475
+ height: 2 * radius * plotH / (yMax - yMin),
32476
+ startAngle: -angle.to,
32477
+ endAngle: -angle.from,
32478
+ color
32479
+ });
32480
+ if (angle.label) {
32481
+ const mid = (angle.from + angle.to) / 2;
32482
+ const rad = mid * Math.PI / 180;
32483
+ out.push({
32484
+ type: "text",
32485
+ x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
32486
+ y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
32487
+ text: angle.label,
32488
+ color: "#111827",
32489
+ fontSize: 11,
32490
+ align: "center"
30384
32491
  });
30385
32492
  }
30386
32493
  }
30387
32494
  for (const p of points) {
30388
32495
  if (p.x < xMin || p.x > xMax || p.y < yMin || p.y > yMax) continue;
32496
+ const isOpen = p.style === "open";
30389
32497
  out.push({
30390
32498
  type: "circle",
30391
32499
  x: mapX(p.x),
30392
32500
  y: mapY(p.y),
30393
32501
  radius: p.radius ?? 4,
30394
32502
  color: p.color ?? "#dc2626",
30395
- fill: p.color ?? "#dc2626"
32503
+ fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
30396
32504
  });
30397
32505
  if (p.label) {
30398
32506
  out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: "#111827", fontSize: 12 });
@@ -30411,7 +32519,28 @@ var init_MathCanvas = __esm({
30411
32519
  }
30412
32520
  out.push(...shapes);
30413
32521
  return out;
30414
- }, [width, height, xMin, xMax, yMin, yMax, showAxes, showGrid, gridStep, curves, points, vectors, shapes]);
32522
+ }, [
32523
+ width,
32524
+ height,
32525
+ xMin,
32526
+ xMax,
32527
+ yMin,
32528
+ yMax,
32529
+ showAxes,
32530
+ showGrid,
32531
+ gridStep,
32532
+ showTickLabels,
32533
+ showCurveLabels,
32534
+ curves,
32535
+ points,
32536
+ vectors,
32537
+ regions,
32538
+ bars,
32539
+ guides,
32540
+ angles,
32541
+ hops,
32542
+ shapes
32543
+ ]);
30415
32544
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
30416
32545
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
30417
32546
  /* @__PURE__ */ jsx(
@@ -30420,6 +32549,8 @@ var init_MathCanvas = __esm({
30420
32549
  width,
30421
32550
  height,
30422
32551
  shapes: derivedShapes,
32552
+ readouts,
32553
+ traces,
30423
32554
  interactive,
30424
32555
  animate,
30425
32556
  onShapeClick,
@@ -30431,27 +32562,355 @@ var init_MathCanvas = __esm({
30431
32562
  };
30432
32563
  }
30433
32564
  });
30434
- var PhysicsCanvas;
32565
+ function formatMeterValue(v) {
32566
+ return Number.isInteger(v) ? String(v) : String(Number(v.toFixed(2)));
32567
+ }
32568
+ function sceneObjectShapes(obj, canvasWidth, canvasHeight) {
32569
+ const out = [];
32570
+ const color = obj.color ?? "#334155";
32571
+ switch (obj.kind) {
32572
+ case "ground": {
32573
+ const xStart = obj.x1 ?? 0;
32574
+ const xEnd = obj.x2 ?? canvasWidth;
32575
+ const y = obj.y ?? 0;
32576
+ out.push({ type: "line", x1: xStart, y1: y, x2: xEnd, y2: y, color, lineWidth: 2 });
32577
+ for (let hx = xStart + 7; hx <= xEnd; hx += 14) {
32578
+ out.push({ type: "line", x1: hx, y1: y, x2: hx - 7, y2: y + 7, color, lineWidth: 1 });
32579
+ }
32580
+ if (obj.label) {
32581
+ out.push({
32582
+ type: "text",
32583
+ x: (xStart + xEnd) / 2,
32584
+ y: y - 10,
32585
+ text: obj.label,
32586
+ color: PHYSICS_LABEL_COLOR,
32587
+ fontSize: 11,
32588
+ align: "center"
32589
+ });
32590
+ }
32591
+ break;
32592
+ }
32593
+ case "wall": {
32594
+ const yStart = obj.y1 ?? 0;
32595
+ const yEnd = obj.y2 ?? canvasHeight;
32596
+ const x = obj.x ?? 0;
32597
+ out.push({ type: "line", x1: x, y1: yStart, x2: x, y2: yEnd, color, lineWidth: 2 });
32598
+ for (let hy = yStart + 7; hy <= yEnd; hy += 14) {
32599
+ out.push({ type: "line", x1: x, y1: hy, x2: x - 7, y2: hy + 7, color, lineWidth: 1 });
32600
+ }
32601
+ if (obj.label) {
32602
+ out.push({
32603
+ type: "text",
32604
+ x: x + 12,
32605
+ y: (yStart + yEnd) / 2,
32606
+ text: obj.label,
32607
+ color: PHYSICS_LABEL_COLOR,
32608
+ fontSize: 11,
32609
+ align: "left"
32610
+ });
32611
+ }
32612
+ break;
32613
+ }
32614
+ case "ramp": {
32615
+ const x1 = obj.x1 ?? 0;
32616
+ const y1 = obj.y1 ?? 0;
32617
+ const x2 = obj.x2 ?? canvasWidth;
32618
+ const y2 = obj.y2 ?? canvasHeight;
32619
+ out.push({
32620
+ type: "polygon",
32621
+ points: [
32622
+ { x: x1, y: y1 },
32623
+ { x: x2, y: y2 },
32624
+ { x: x1, y: y2 }
32625
+ ],
32626
+ color,
32627
+ fill: obj.fill ?? "#e2e8f0",
32628
+ lineWidth: 2
32629
+ });
32630
+ if (obj.label) {
32631
+ out.push({
32632
+ type: "text",
32633
+ x: (2 * x1 + x2) / 3,
32634
+ y: (y1 + 2 * y2) / 3,
32635
+ text: obj.label,
32636
+ color: PHYSICS_LABEL_COLOR,
32637
+ fontSize: 11,
32638
+ align: "center"
32639
+ });
32640
+ }
32641
+ break;
32642
+ }
32643
+ case "box": {
32644
+ const x = obj.x ?? 0;
32645
+ const y = obj.y ?? 0;
32646
+ const w = obj.width ?? 40;
32647
+ const h = obj.height ?? 40;
32648
+ out.push({ type: "rect", x, y, width: w, height: h, color, fill: obj.fill, lineWidth: 2 });
32649
+ if (obj.label) {
32650
+ out.push({
32651
+ type: "text",
32652
+ x: x + w / 2,
32653
+ y: y + h / 2,
32654
+ text: obj.label,
32655
+ color: PHYSICS_LABEL_COLOR,
32656
+ fontSize: 11,
32657
+ align: "center"
32658
+ });
32659
+ }
32660
+ break;
32661
+ }
32662
+ case "pivot": {
32663
+ const x = obj.x ?? 0;
32664
+ const y = obj.y ?? 0;
32665
+ out.push({ type: "circle", x, y, radius: 5, color, fill: color });
32666
+ out.push({ type: "line", x1: x - 14, y1: y - 8, x2: x + 14, y2: y - 8, color, lineWidth: 1 });
32667
+ for (let k = 0; k < 5; k++) {
32668
+ const hx = x - 14 + 7 * k;
32669
+ out.push({ type: "line", x1: hx, y1: y - 8, x2: hx - 6, y2: y - 14, color, lineWidth: 1 });
32670
+ }
32671
+ if (obj.label) {
32672
+ out.push({
32673
+ type: "text",
32674
+ x,
32675
+ y: y - 20,
32676
+ text: obj.label,
32677
+ color: PHYSICS_LABEL_COLOR,
32678
+ fontSize: 11,
32679
+ align: "center"
32680
+ });
32681
+ }
32682
+ break;
32683
+ }
32684
+ }
32685
+ return out;
32686
+ }
32687
+ function trailShapes(trail) {
32688
+ const n = trail.points.length;
32689
+ if (n < 2) return [];
32690
+ const color = trail.color ?? "#94a3b8";
32691
+ const lineWidth = trail.width ?? 2;
32692
+ const fade = trail.fade ?? true;
32693
+ const globalOpacity = trail.opacity ?? 1;
32694
+ const out = [];
32695
+ for (let i = 0; i < n - 1; i++) {
32696
+ const a = trail.points[i];
32697
+ const b = trail.points[i + 1];
32698
+ const segmentOpacity = fade ? 0.12 + 0.68 * i / (n - 1) : 0.6;
32699
+ out.push({
32700
+ type: "line",
32701
+ x1: a.x,
32702
+ y1: a.y,
32703
+ x2: b.x,
32704
+ y2: b.y,
32705
+ color,
32706
+ lineWidth,
32707
+ opacity: segmentOpacity * globalOpacity
32708
+ });
32709
+ }
32710
+ return out;
32711
+ }
32712
+ function constraintShapes(c, a, b) {
32713
+ const color = c.color ?? "#9ca3af";
32714
+ const kind = c.kind ?? "rod";
32715
+ if (kind === "rod") {
32716
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2 }];
32717
+ }
32718
+ if (kind === "string") {
32719
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2, dash: "dashed" }];
32720
+ }
32721
+ const COILS = 8;
32722
+ const AMP = 7;
32723
+ const LEAD = 10;
32724
+ const dx = b.x - a.x;
32725
+ const dy = b.y - a.y;
32726
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
32727
+ const ux = dx / dist;
32728
+ const uy = dy / dist;
32729
+ const perpX = -uy;
32730
+ const perpY = ux;
32731
+ const aPrime = { x: a.x + LEAD * ux, y: a.y + LEAD * uy };
32732
+ const bPrime = { x: b.x - LEAD * ux, y: b.y - LEAD * uy };
32733
+ const m = 2 * COILS;
32734
+ const polyline = [{ x: a.x, y: a.y }, aPrime];
32735
+ for (let j = 1; j <= m; j++) {
32736
+ const t = j / (m + 1);
32737
+ const baseX = aPrime.x + t * (bPrime.x - aPrime.x);
32738
+ const baseY = aPrime.y + t * (bPrime.y - aPrime.y);
32739
+ const sign = j % 2 === 0 ? 1 : -1;
32740
+ polyline.push({ x: baseX + sign * AMP * perpX, y: baseY + sign * AMP * perpY });
32741
+ }
32742
+ polyline.push(bPrime, { x: b.x, y: b.y });
32743
+ const out = [];
32744
+ for (let i = 1; i < polyline.length; i++) {
32745
+ out.push({
32746
+ type: "line",
32747
+ x1: polyline[i - 1].x,
32748
+ y1: polyline[i - 1].y,
32749
+ x2: polyline[i].x,
32750
+ y2: polyline[i].y,
32751
+ color,
32752
+ lineWidth: 2
32753
+ });
32754
+ }
32755
+ return out;
32756
+ }
32757
+ function vectorShapes(v, bodyById) {
32758
+ let ax;
32759
+ let ay;
32760
+ if (v.body) {
32761
+ const anchor = bodyById.get(v.body);
32762
+ if (!anchor) return [];
32763
+ ax = anchor.x;
32764
+ ay = anchor.y;
32765
+ } else {
32766
+ ax = v.x ?? 0;
32767
+ ay = v.y ?? 0;
32768
+ }
32769
+ const scale = v.scale ?? 1;
32770
+ const color = v.color ?? "#dc2626";
32771
+ const tx = ax + v.dx * scale;
32772
+ const ty = ay + v.dy * scale;
32773
+ const out = [{ type: "arrow", x1: ax, y1: ay, x2: tx, y2: ty, color, lineWidth: 2, dash: v.dash }];
32774
+ if (v.label) {
32775
+ const dist = Math.max(1e-6, Math.hypot(tx - ax, ty - ay));
32776
+ const ux = (tx - ax) / dist;
32777
+ const uy = (ty - ay) / dist;
32778
+ out.push({
32779
+ type: "text",
32780
+ x: tx + 8 * ux,
32781
+ y: ty + 8 * uy,
32782
+ text: v.label,
32783
+ color,
32784
+ fontSize: 11,
32785
+ align: "center"
32786
+ });
32787
+ }
32788
+ return out;
32789
+ }
32790
+ function angleMarkerShapes(a) {
32791
+ const radius = a.radius ?? 26;
32792
+ const color = a.color ?? "#0ea5e9";
32793
+ const out = [
32794
+ {
32795
+ type: "ellipse",
32796
+ x: a.x,
32797
+ y: a.y,
32798
+ width: radius * 2,
32799
+ height: radius * 2,
32800
+ startAngle: a.from,
32801
+ endAngle: a.to,
32802
+ color,
32803
+ lineWidth: 2
32804
+ }
32805
+ ];
32806
+ if (a.label) {
32807
+ const mid = (a.from + a.to) / 2 * (Math.PI / 180);
32808
+ out.push({
32809
+ type: "text",
32810
+ x: a.x + (radius + 13) * Math.cos(mid),
32811
+ y: a.y + (radius + 13) * Math.sin(mid),
32812
+ text: a.label,
32813
+ color,
32814
+ fontSize: 11,
32815
+ align: "center"
32816
+ });
32817
+ }
32818
+ return out;
32819
+ }
32820
+ function fieldShapes(field, canvasWidth, canvasHeight) {
32821
+ const spacing = field.spacing ?? 48;
32822
+ const size = field.size ?? 14;
32823
+ const color = field.color ?? "#94a3b8";
32824
+ const regionX = field.x ?? 0;
32825
+ const regionY = field.y ?? 0;
32826
+ const regionW = field.width ?? canvasWidth;
32827
+ const regionH = field.height ?? canvasHeight;
32828
+ const out = [];
32829
+ for (let gx = regionX + spacing / 2; gx < regionX + regionW; gx += spacing) {
32830
+ for (let gy = regionY + spacing / 2; gy < regionY + regionH; gy += spacing) {
32831
+ if (field.kind === "arrows") {
32832
+ const rad = (field.angle ?? 0) * Math.PI / 180;
32833
+ const hx = Math.cos(rad) * size / 2;
32834
+ const hy = Math.sin(rad) * size / 2;
32835
+ out.push({ type: "arrow", x1: gx - hx, y1: gy - hy, x2: gx + hx, y2: gy + hy, color, lineWidth: 2 });
32836
+ } else if (field.kind === "into") {
32837
+ const r2 = size / 3;
32838
+ const d = 0.6 * r2 * Math.SQRT1_2;
32839
+ out.push({ type: "circle", x: gx, y: gy, radius: r2, color });
32840
+ out.push({ type: "line", x1: gx - d, y1: gy - d, x2: gx + d, y2: gy + d, color, lineWidth: 1 });
32841
+ out.push({ type: "line", x1: gx - d, y1: gy + d, x2: gx + d, y2: gy - d, color, lineWidth: 1 });
32842
+ } else {
32843
+ const r2 = size / 3;
32844
+ out.push({ type: "circle", x: gx, y: gy, radius: r2, color });
32845
+ out.push({ type: "circle", x: gx, y: gy, radius: 1.5, color, fill: color });
32846
+ }
32847
+ }
32848
+ }
32849
+ return out;
32850
+ }
32851
+ function meterShapes(meters, canvasHeight) {
32852
+ const n = meters.length;
32853
+ const out = [];
32854
+ const sharedMax = Math.max(1e-6, ...meters.map((m) => m.value));
32855
+ meters.forEach((meter, i) => {
32856
+ const rowY = canvasHeight - 10 - 16 * (n - i);
32857
+ const color = meter.color ?? "#3b82f6";
32858
+ const M = meter.max ?? sharedMax;
32859
+ const w = Math.round(Math.min(1, Math.max(0, meter.value / M)) * 110);
32860
+ out.push({ type: "text", x: 8, y: rowY + 8, text: meter.label, color: PHYSICS_LABEL_COLOR, fontSize: 10 });
32861
+ out.push({ type: "rect", x: 52, y: rowY, width: w, height: 10, color, fill: color });
32862
+ out.push({
32863
+ type: "text",
32864
+ x: 166,
32865
+ y: rowY + 8,
32866
+ text: formatMeterValue(meter.value),
32867
+ color: "#6b7280",
32868
+ fontSize: 9
32869
+ });
32870
+ });
32871
+ return out;
32872
+ }
32873
+ var physicsLog2, PHYSICS_LABEL_COLOR, PhysicsCanvas;
30435
32874
  var init_PhysicsCanvas = __esm({
30436
32875
  "components/learning/molecules/PhysicsCanvas.tsx"() {
30437
32876
  "use client";
30438
32877
  init_atoms();
30439
32878
  init_Stack();
30440
32879
  init_LearningCanvas();
32880
+ init_learningScene3D();
32881
+ physicsLog2 = createLogger("almadar:ui:physics-canvas");
32882
+ PHYSICS_LABEL_COLOR = "#374151";
30441
32883
  PhysicsCanvas = ({
30442
32884
  className,
30443
32885
  width = 600,
30444
32886
  height = 400,
30445
32887
  title,
30446
32888
  backgroundColor,
32889
+ mode = "2d",
32890
+ camera,
32891
+ lighting,
32892
+ post,
30447
32893
  bodies = [],
30448
32894
  constraints = [],
30449
32895
  showVelocity = true,
30450
32896
  showForces = false,
30451
32897
  velocityScale = 20,
30452
32898
  forceScale = 20,
32899
+ sceneObjects = [],
32900
+ trails = [],
32901
+ vectors = [],
32902
+ surface3d,
32903
+ vectors3d = [],
32904
+ vectorScale = 1,
32905
+ angles = [],
32906
+ field,
32907
+ meters = [],
30453
32908
  shapes = [],
30454
- interactive = false,
32909
+ readouts,
32910
+ traces,
32911
+ showGrid,
32912
+ shadows,
32913
+ interactive,
30455
32914
  animate = false,
30456
32915
  onShapeClick,
30457
32916
  isLoading,
@@ -30463,19 +32922,14 @@ var init_PhysicsCanvas = __esm({
30463
32922
  for (const b of bodies) {
30464
32923
  if (b.id) bodyById.set(b.id, b);
30465
32924
  }
32925
+ if (field) out.push(...fieldShapes(field, width, height));
32926
+ for (const obj of sceneObjects) out.push(...sceneObjectShapes(obj, width, height));
32927
+ for (const trail of trails) out.push(...trailShapes(trail));
30466
32928
  for (const c of constraints) {
30467
32929
  const a = bodyById.get(c.from);
30468
32930
  const b = bodyById.get(c.to);
30469
32931
  if (!a || !b) continue;
30470
- out.push({
30471
- type: "line",
30472
- x1: a.x,
30473
- y1: a.y,
30474
- x2: b.x,
30475
- y2: b.y,
30476
- color: c.color ?? "#9ca3af",
30477
- lineWidth: 2
30478
- });
32932
+ out.push(...constraintShapes(c, a, b));
30479
32933
  }
30480
32934
  for (const b of bodies) {
30481
32935
  out.push({
@@ -30520,9 +32974,181 @@ var init_PhysicsCanvas = __esm({
30520
32974
  });
30521
32975
  }
30522
32976
  }
32977
+ for (const v of vectors) out.push(...vectorShapes(v, bodyById));
32978
+ for (const a of angles) out.push(...angleMarkerShapes(a));
32979
+ if (meters.length > 0) out.push(...meterShapes(meters, height));
30523
32980
  out.push(...shapes);
30524
32981
  return out;
30525
- }, [bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes]);
32982
+ }, [
32983
+ bodies,
32984
+ constraints,
32985
+ showVelocity,
32986
+ showForces,
32987
+ velocityScale,
32988
+ forceScale,
32989
+ sceneObjects,
32990
+ trails,
32991
+ vectors,
32992
+ angles,
32993
+ field,
32994
+ meters,
32995
+ shapes,
32996
+ width,
32997
+ height
32998
+ ]);
32999
+ const drawables3D = useMemo(() => {
33000
+ if (mode !== "3d") return [];
33001
+ if (shapes.length > 0) {
33002
+ physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
33003
+ }
33004
+ if (sceneObjects.length > 0) {
33005
+ physicsLog2.debug("sceneObjects ignored in 3D mode (pixel-authored 2D vocabulary)", { count: sceneObjects.length });
33006
+ }
33007
+ if (vectors.length > 0) {
33008
+ physicsLog2.debug("vectors ignored in 3D mode (pixel-authored 2D vocabulary)", { count: vectors.length });
33009
+ }
33010
+ if (angles.length > 0) {
33011
+ physicsLog2.debug("angles ignored in 3D mode (pixel-authored 2D vocabulary)", { count: angles.length });
33012
+ }
33013
+ if (field) {
33014
+ physicsLog2.debug("field ignored in 3D mode (pixel-authored 2D vocabulary)");
33015
+ }
33016
+ if (meters.length > 0) {
33017
+ physicsLog2.debug("meters ignored in 3D mode (pixel-authored 2D vocabulary)", { count: meters.length });
33018
+ }
33019
+ if (animate) {
33020
+ physicsLog2.debug("animate ignored in 3D mode (motion is entity-state driven)");
33021
+ }
33022
+ const out = [];
33023
+ const labelColor = labelColorForBackground(backgroundColor);
33024
+ const bodyById = /* @__PURE__ */ new Map();
33025
+ for (const b of bodies) {
33026
+ if (b.id) bodyById.set(b.id, b);
33027
+ }
33028
+ for (const c of constraints) {
33029
+ const a = bodyById.get(c.from);
33030
+ const b = bodyById.get(c.to);
33031
+ if (!a || !b) continue;
33032
+ const rodRadius = Math.max(0.05, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.15);
33033
+ const rod = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], rodRadius, c.color ?? "#9ca3af");
33034
+ if (rod) out.push(rod);
33035
+ }
33036
+ for (const b of bodies) {
33037
+ const radius = b.radius ?? 0.5;
33038
+ const bz = b.z ?? 0;
33039
+ out.push(meshSphere(b.id, b.x, b.y, bz, radius, b.color ?? "#2563eb"));
33040
+ if (b.label) {
33041
+ out.push(billboardLabel(b.label, b.x, b.y, bz + radius, { color: labelColor }));
33042
+ }
33043
+ const vx = b.vx ?? 0;
33044
+ const vy = b.vy ?? 0;
33045
+ const vz = b.vz ?? 0;
33046
+ const arrowRadius = Math.max(0.05, radius * 0.15);
33047
+ if (showVelocity && (vx !== 0 || vy !== 0 || vz !== 0)) {
33048
+ const arrow = arrowBetween(
33049
+ [b.x, b.y, bz],
33050
+ [b.x + vx * velocityScale, b.y + vy * velocityScale, bz + vz * velocityScale],
33051
+ "#16a34a",
33052
+ arrowRadius
33053
+ );
33054
+ if (arrow) out.push(arrow);
33055
+ }
33056
+ const fx = b.fx ?? 0;
33057
+ const fy = b.fy ?? 0;
33058
+ const fz = b.fz ?? 0;
33059
+ if (showForces && (fx !== 0 || fy !== 0 || fz !== 0)) {
33060
+ const arrow = arrowBetween(
33061
+ [b.x, b.y, bz],
33062
+ [b.x + fx * forceScale, b.y + fy * forceScale, bz + fz * forceScale],
33063
+ "#dc2626",
33064
+ arrowRadius
33065
+ );
33066
+ if (arrow) out.push(arrow);
33067
+ }
33068
+ }
33069
+ for (const trail of trails) {
33070
+ if (trail.fade !== void 0) {
33071
+ physicsLog2.debug("trail.fade ignored in 3D mode (2D-only fade curve \u2014 3D draws an opaque tube)", { id: trail.id });
33072
+ }
33073
+ const points = trail.points.map((p) => [p.x, p.y, p.z ?? 0]);
33074
+ out.push(
33075
+ ...polylineTube(points, trail.width ?? 0.05, trail.color ?? "#94a3b8", {
33076
+ ...trail.opacity !== void 0 ? { opacity: trail.opacity } : {}
33077
+ })
33078
+ );
33079
+ }
33080
+ if (surface3d) {
33081
+ out.push(...heightFieldMesh(surface3d));
33082
+ }
33083
+ if (vectors3d.length > 0) {
33084
+ out.push(
33085
+ ...arrowField(
33086
+ vectors3d.map((v) => ({
33087
+ id: v.id,
33088
+ from: [v.x, v.y, v.z ?? 0],
33089
+ delta: [v.dx, v.dy, v.dz ?? 0],
33090
+ color: v.color,
33091
+ label: v.label,
33092
+ width: v.width
33093
+ })),
33094
+ { scale: vectorScale, labelColor }
33095
+ )
33096
+ );
33097
+ }
33098
+ return out;
33099
+ }, [
33100
+ mode,
33101
+ bodies,
33102
+ constraints,
33103
+ showVelocity,
33104
+ showForces,
33105
+ velocityScale,
33106
+ forceScale,
33107
+ shapes,
33108
+ sceneObjects,
33109
+ trails,
33110
+ vectors,
33111
+ surface3d,
33112
+ vectors3d,
33113
+ vectorScale,
33114
+ angles,
33115
+ field,
33116
+ meters,
33117
+ animate,
33118
+ backgroundColor
33119
+ ]);
33120
+ const bodyIndexById = useMemo(() => {
33121
+ const m = /* @__PURE__ */ new Map();
33122
+ vectors3d.forEach((v, i) => {
33123
+ if (v.id) m.set(v.id, i);
33124
+ });
33125
+ bodies.forEach((b, i) => {
33126
+ if (b.id) m.set(b.id, i);
33127
+ });
33128
+ return m;
33129
+ }, [bodies, vectors3d]);
33130
+ if (mode === "3d") {
33131
+ return /* @__PURE__ */ jsx(
33132
+ LearningScene3D,
33133
+ {
33134
+ className,
33135
+ width,
33136
+ height,
33137
+ title,
33138
+ backgroundColor,
33139
+ drawables: drawables3D,
33140
+ camera,
33141
+ lighting,
33142
+ post,
33143
+ showGrid,
33144
+ shadows,
33145
+ interactive,
33146
+ isLoading,
33147
+ error,
33148
+ onItemClick: get3DClickPayload(onShapeClick, bodyIndexById)
33149
+ }
33150
+ );
33151
+ }
30526
33152
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
30527
33153
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
30528
33154
  /* @__PURE__ */ jsx(
@@ -30532,7 +33158,9 @@ var init_PhysicsCanvas = __esm({
30532
33158
  height,
30533
33159
  backgroundColor,
30534
33160
  shapes: derivedShapes,
30535
- interactive,
33161
+ readouts,
33162
+ traces,
33163
+ interactive: interactive ?? false,
30536
33164
  animate,
30537
33165
  onShapeClick,
30538
33166
  isLoading,
@@ -30682,7 +33310,7 @@ function layoutFlow(nodeIds, adjacency, roots, width, height, margin) {
30682
33310
  }
30683
33311
  return nodeIds.map((id) => positions.get(id));
30684
33312
  }
30685
- function layoutTree2(nodeIds, adjacency, roots, width, height, margin) {
33313
+ function layoutTree3(nodeIds, adjacency, roots, width, height, margin) {
30686
33314
  const effectiveRoots = roots.length > 0 ? roots : [nodeIds[0]];
30687
33315
  const layers = assignLayers(nodeIds, adjacency, effectiveRoots);
30688
33316
  const maxLayer = Math.max(...Array.from(layers.values()));
@@ -30738,7 +33366,7 @@ function computeStaticLayout(mode, input) {
30738
33366
  const adjacency = buildAdjacency(nodeIds, edges);
30739
33367
  const roots = findRoots(nodeIds, adjacency);
30740
33368
  if (mode === "flow") return layoutFlow(nodeIds, adjacency, roots, width, height, margin);
30741
- if (mode === "tree") return layoutTree2(nodeIds, adjacency, roots, width, height, margin);
33369
+ if (mode === "tree") return layoutTree3(nodeIds, adjacency, roots, width, height, margin);
30742
33370
  return layoutRadial(nodeIds, adjacency, roots, width, height, margin);
30743
33371
  }
30744
33372
  var init_graphViewLayouts = __esm({
@@ -31150,12 +33778,12 @@ var init_MapView = __esm({
31150
33778
  shadowSize: [41, 41]
31151
33779
  });
31152
33780
  L.Marker.prototype.options.icon = defaultIcon;
31153
- const { useEffect: useEffect64, useRef: useRef64, useCallback: useCallback96, useState: useState100 } = React94__default;
33781
+ const { useEffect: useEffect64, useRef: useRef65, useCallback: useCallback96, useState: useState100 } = React94__default;
31154
33782
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
31155
33783
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
31156
33784
  function MapUpdater({ centerLat, centerLng, zoom }) {
31157
33785
  const map = useMap();
31158
- const prevRef = useRef64({ centerLat, centerLng, zoom });
33786
+ const prevRef = useRef65({ centerLat, centerLng, zoom });
31159
33787
  useEffect64(() => {
31160
33788
  const prev = prevRef.current;
31161
33789
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
@@ -31981,26 +34609,6 @@ var init_Lightbox = __esm({
31981
34609
  Lightbox.displayName = "Lightbox";
31982
34610
  }
31983
34611
  });
31984
- function useMediaQuery(query) {
31985
- const subscribe = useCallback(
31986
- (onChange) => {
31987
- const mql = window.matchMedia(query);
31988
- mql.addEventListener("change", onChange);
31989
- return () => mql.removeEventListener("change", onChange);
31990
- },
31991
- [query]
31992
- );
31993
- return useSyncExternalStore(
31994
- subscribe,
31995
- () => window.matchMedia(query).matches,
31996
- () => false
31997
- );
31998
- }
31999
- var init_useMediaQuery = __esm({
32000
- "hooks/useMediaQuery.ts"() {
32001
- "use client";
32002
- }
32003
- });
32004
34612
  function renderIconInput3(icon, props) {
32005
34613
  return typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsx(Icon, { icon, ...props });
32006
34614
  }
@@ -32039,8 +34647,8 @@ function TableView({
32039
34647
  columns,
32040
34648
  fields,
32041
34649
  itemActions,
32042
- maxInlineActions,
32043
- itemClickEvent,
34650
+ maxInlineActions: _maxInlineActions,
34651
+ itemClickEvent = "",
32044
34652
  selectable = false,
32045
34653
  selectEvent,
32046
34654
  selectedIds,
@@ -32090,7 +34698,6 @@ function TableView({
32090
34698
  const hasMore = pageSize > 0 && visibleCount < ordered2.length;
32091
34699
  const hasRenderProp = typeof children === "function";
32092
34700
  const idField = dndItemIdField ?? "id";
32093
- const isCoarsePointer = useMediaQuery("(pointer: coarse)");
32094
34701
  React94__default.useEffect(() => {
32095
34702
  tableViewLog.debug("render", {
32096
34703
  rowCount: data.length,
@@ -32132,21 +34739,14 @@ function TableView({
32132
34739
  const dir = sortColumn === (col.field ?? col.key) && sortDirection === "asc" ? "desc" : "asc";
32133
34740
  eventBus.emit(`UI:${sortEvent}`, { column: col.field ?? col.key, direction: dir });
32134
34741
  };
32135
- const handleActionClick = (action, row) => (e) => {
32136
- e.stopPropagation();
32137
- const payload = {
32138
- id: row.id,
32139
- row
32140
- };
32141
- eventBus.emit(`UI:${action.event}`, payload);
32142
- };
34742
+ const rowClickEvent = itemClickEvent || actionDefs.find((a) => a.variant !== "danger")?.event;
32143
34743
  const handleRowClick = (row) => () => {
32144
- if (!itemClickEvent) return;
34744
+ if (!rowClickEvent) return;
32145
34745
  const payload = {
32146
34746
  id: row.id,
32147
34747
  row
32148
34748
  };
32149
- eventBus.emit(`UI:${itemClickEvent}`, payload);
34749
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
32150
34750
  };
32151
34751
  const colFloors = React94__default.useMemo(
32152
34752
  () => colDefs.map((col) => {
@@ -32162,10 +34762,7 @@ function TableView({
32162
34762
  const statusNode = isLoading ? /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) }) : error ? /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "error", children: error.message }) }) : data.length === 0 ? /* @__PURE__ */ jsx(Box, { className: "text-center py-12", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: emptyMessage || t("empty.noItems") }) }) : null;
32163
34763
  const lk = LOOKS[look];
32164
34764
  const hasActions = actionDefs.length > 0;
32165
- const effectiveMaxInline = isCoarsePointer ? 0 : maxInlineActions;
32166
- const inlineActionCount = hasActions ? effectiveMaxInline != null ? Math.min(actionDefs.length, effectiveMaxInline) : actionDefs.length : 0;
32167
- const hasOverflowActions = hasActions && effectiveMaxInline != null && actionDefs.length > effectiveMaxInline;
32168
- const actionsTrack = hasActions ? `${inlineActionCount * 6 + (hasOverflowActions ? 3 : 0)}rem` : null;
34765
+ const actionsTrack = hasActions ? "3rem" : null;
32169
34766
  const gridTemplateColumns = [
32170
34767
  selectable ? "auto" : null,
32171
34768
  ...colDefs.map((c, i) => c.width ?? `minmax(${colFloors[i]}ch, 1fr)`),
@@ -32212,7 +34809,7 @@ function TableView({
32212
34809
  col.key
32213
34810
  );
32214
34811
  }),
32215
- hasActions && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)]" })
34812
+ hasActions && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)] border-l border-[var(--color-border)] h-full" })
32216
34813
  ]
32217
34814
  }
32218
34815
  );
@@ -32224,12 +34821,12 @@ function TableView({
32224
34821
  role: "row",
32225
34822
  "data-entity-row": true,
32226
34823
  "data-entity-id": id,
32227
- onClick: itemClickEvent ? handleRowClick(row) : void 0,
34824
+ onClick: rowClickEvent ? handleRowClick(row) : void 0,
32228
34825
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
32229
34826
  className: cn(
32230
34827
  "group items-center gap-3 transition-colors duration-fast",
32231
34828
  hasRenderProp ? "flex" : "grid",
32232
- itemClickEvent && "cursor-pointer",
34829
+ rowClickEvent && "cursor-pointer",
32233
34830
  lk.rowPad,
32234
34831
  lk.divider && "border-b border-[var(--color-border)]",
32235
34832
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -32237,7 +34834,7 @@ function TableView({
32237
34834
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
32238
34835
  ),
32239
34836
  children: [
32240
- selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
34837
+ selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
32241
34838
  Checkbox,
32242
34839
  {
32243
34840
  checked: selected.has(id),
@@ -32258,53 +34855,37 @@ function TableView({
32258
34855
  }
32259
34856
  return /* @__PURE__ */ jsx(Box, { role: "cell", className: cellBase, children: /* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children: formatCell(raw, col.format) }) }, col.key);
32260
34857
  }),
32261
- hasActions && /* @__PURE__ */ jsxs(
34858
+ hasActions && /* @__PURE__ */ jsx(
32262
34859
  HStack,
32263
34860
  {
32264
34861
  gap: "xs",
32265
- onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
34862
+ onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0,
32266
34863
  className: cn(
32267
34864
  // Pinned: the fixed column tracks routinely overflow the caller's
32268
- // scroll container, which used to leave the actions off-screen.
32269
- // Opaque so scrolled cells pass underneath it.
34865
+ // scroll container, which would leave the kebab off-screen.
34866
+ // Opaque + hairline edge so it reads as a pinned column, not a
34867
+ // floating control, while scrolled cells pass underneath.
32270
34868
  "justify-end flex-shrink-0 sticky right-0 z-[1] transition-colors",
34869
+ "border-l border-[var(--color-border)]",
32271
34870
  lk.striped && index % 2 === 1 ? "bg-[var(--color-surface-subtle)]" : "bg-[var(--color-card)] group-hover:bg-[var(--color-surface-subtle)]"
32272
34871
  ),
32273
- children: [
32274
- (effectiveMaxInline != null ? actionDefs.slice(0, effectiveMaxInline) : actionDefs).map((action, i) => /* @__PURE__ */ jsxs(
32275
- Button,
32276
- {
32277
- variant: action.variant === "primary" ? "primary" : "ghost",
32278
- size: "sm",
32279
- onClick: handleActionClick(action, row),
32280
- "data-testid": `action-${action.event}`,
32281
- "data-row-id": String(row.id),
32282
- className: cn(action.variant === "danger" && "text-error hover:text-error hover:bg-error/10"),
32283
- children: [
32284
- action.icon && renderIconInput3(action.icon, { size: "xs", className: "mr-1" }),
32285
- action.label
32286
- ]
32287
- },
32288
- i
32289
- )),
32290
- effectiveMaxInline != null && actionDefs.length > effectiveMaxInline && /* @__PURE__ */ jsx(
32291
- Menu,
32292
- {
32293
- position: "bottom-end",
32294
- trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
32295
- items: actionDefs.slice(effectiveMaxInline).map((action) => ({
32296
- label: action.label,
32297
- icon: action.icon,
32298
- event: action.event,
32299
- variant: action.variant === "danger" ? "danger" : "default",
32300
- onClick: () => eventBus.emit(`UI:${action.event}`, {
32301
- id: row.id,
32302
- row
32303
- })
32304
- }))
32305
- }
32306
- )
32307
- ]
34872
+ children: /* @__PURE__ */ jsx(
34873
+ Menu,
34874
+ {
34875
+ position: "bottom-end",
34876
+ trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", "data-row-id": String(row.id), children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
34877
+ items: actionDefs.map((action) => ({
34878
+ label: action.label,
34879
+ icon: action.icon,
34880
+ event: action.event,
34881
+ variant: action.variant === "danger" ? "danger" : "default",
34882
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
34883
+ id: row.id,
34884
+ row
34885
+ })
34886
+ }))
34887
+ }
34888
+ )
32308
34889
  }
32309
34890
  )
32310
34891
  ]
@@ -32347,7 +34928,6 @@ var init_TableView = __esm({
32347
34928
  init_format();
32348
34929
  init_getNestedValue();
32349
34930
  init_useEventBus();
32350
- init_useMediaQuery();
32351
34931
  init_Box();
32352
34932
  init_Stack();
32353
34933
  init_Typography();
@@ -41569,7 +44149,7 @@ function getEnumOptions(field) {
41569
44149
  }));
41570
44150
  }
41571
44151
  const validation = field.validation;
41572
- if (validation?.enum && Array.isArray(validation.enum)) {
44152
+ if (validation?.enum && validation.enum.length > 0) {
41573
44153
  return validation.enum.map((v) => ({
41574
44154
  value: v,
41575
44155
  label: v.charAt(0).toUpperCase() + v.slice(1).replace(/_/g, " ")
@@ -47016,6 +49596,7 @@ var init_component_registry_generated = __esm({
47016
49596
  init_ActionTile();
47017
49597
  init_ActivationBlock();
47018
49598
  init_ComponentPatterns();
49599
+ init_AlgoGraphCanvas();
47019
49600
  init_AlgorithmCanvas();
47020
49601
  init_AnimatedCounter();
47021
49602
  init_AnimatedGraphic();
@@ -47279,6 +49860,7 @@ var init_component_registry_generated = __esm({
47279
49860
  "ActivationBlock": ActivationBlock,
47280
49861
  "Alert": AlertPattern,
47281
49862
  "AlertPattern": AlertPattern,
49863
+ "AlgoGraphCanvas": AlgoGraphCanvas,
47282
49864
  "AlgorithmCanvas": AlgorithmCanvas,
47283
49865
  "AnimatedCounter": AnimatedCounter,
47284
49866
  "AnimatedGraphic": AnimatedGraphic,
@@ -52151,9 +54733,9 @@ var log8 = createLogger("almadar:ui:effects:client-handlers");
52151
54733
  function createClientEffectHandlers(options) {
52152
54734
  const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
52153
54735
  return {
52154
- emit: (event, payload) => {
54736
+ emit: (event, payload, source) => {
52155
54737
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
52156
- eventBus.emit(prefixedEvent, payload);
54738
+ eventBus.emit(prefixedEvent, payload, source);
52157
54739
  },
52158
54740
  persist: async () => {
52159
54741
  log8.warn("persist is server-side only, ignored on client");
@@ -52318,7 +54900,7 @@ function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
52318
54900
  }
52319
54901
  };
52320
54902
  ctx.emit = (event, payload) => {
52321
- emit(event, payload);
54903
+ emit(event, payload, { trait: traitName, tick: tick.name });
52322
54904
  };
52323
54905
  if (tick.guard !== void 0 && !evaluateGuard(tick.guard, ctx)) {
52324
54906
  tickLog.debug("guard-blocked", { traitName, tick: tick.name, state: currentState });
@@ -52770,6 +55352,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
52770
55352
  }
52771
55353
  const effectContext = {
52772
55354
  traitName,
55355
+ orbitalName: orbitalsByTrait?.[traitName],
52773
55356
  state: previousState,
52774
55357
  transition: `${previousState}->${newState}`,
52775
55358
  linkedEntity,
@@ -52846,7 +55429,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
52846
55429
  });
52847
55430
  }
52848
55431
  return emittedDuringExec;
52849
- }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot]);
55432
+ }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot, orbitalsByTrait]);
52850
55433
  const runTickEffects = useCallback((tick, binding) => {
52851
55434
  const traitName = binding.trait.name;
52852
55435
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
@@ -52882,9 +55465,9 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
52882
55465
  log: tickLog
52883
55466
  });
52884
55467
  }, [executeTransitionEffects, sharedEntityStore]);
52885
- const emitFromSharedWriter = useCallback((event, payload) => {
55468
+ const emitFromSharedWriter = useCallback((event, payload, source) => {
52886
55469
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
52887
- eventBus.emit(prefixedEvent, payload);
55470
+ eventBus.emit(prefixedEvent, payload, source);
52888
55471
  }, [eventBus]);
52889
55472
  useEffect(() => {
52890
55473
  const scheduler = createTickScheduler();