@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
@@ -207,7 +207,7 @@ function useEventBus() {
207
207
  return {
208
208
  ...baseBus,
209
209
  emit: (type, payload, source) => {
210
- if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
210
+ if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".") && !source?.trait) {
211
211
  scopeLog.warn("emit:bare-key-no-scope", { type });
212
212
  }
213
213
  baseBus.emit(type, payload, source);
@@ -10375,9 +10375,10 @@ function Canvas({
10375
10375
  drawables: [...drawables ?? [], ...childDrawables],
10376
10376
  isLoading,
10377
10377
  cameraMode: to3DCameraMode(camera?.mode),
10378
- ...zoom !== void 0 ? { scale: zoom } : {},
10378
+ ...zoom !== void 0 ? { zoom } : {},
10379
10379
  ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
10380
10380
  ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
10381
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
10381
10382
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
10382
10383
  unitScale,
10383
10384
  backgroundColor,
@@ -11645,6 +11646,14 @@ function shapeBounds(shape) {
11645
11646
  w: shape.radius * 2 + 8,
11646
11647
  h: shape.radius * 2 + 8
11647
11648
  };
11649
+ case "ellipse":
11650
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11651
+ return {
11652
+ x: shape.x - shape.width / 2 - 4,
11653
+ y: shape.y - shape.height / 2 - 4,
11654
+ w: shape.width + 8,
11655
+ h: shape.height + 8
11656
+ };
11648
11657
  case "rect":
11649
11658
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11650
11659
  return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
@@ -11678,13 +11687,14 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
11678
11687
  ctx.closePath();
11679
11688
  ctx.fill();
11680
11689
  }
11681
- function drawShape(ctx, shape, width, height) {
11690
+ function drawShape(ctx, shape, width, height, allShapes) {
11682
11691
  ctx.save();
11683
11692
  const opacity = shape.opacity ?? 1;
11684
11693
  ctx.globalAlpha = opacity;
11685
11694
  const stroke = resolveColor2(shape.color, ctx, "#333333");
11686
11695
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
11687
11696
  ctx.lineWidth = shape.lineWidth ?? 2;
11697
+ if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
11688
11698
  switch (shape.type) {
11689
11699
  case "grid": {
11690
11700
  const step = shape.step ?? 40;
@@ -11750,6 +11760,20 @@ function drawShape(ctx, shape, width, height) {
11750
11760
  ctx.stroke();
11751
11761
  break;
11752
11762
  }
11763
+ case "ellipse": {
11764
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11765
+ const startAngle = (shape.startAngle ?? 0) * Math.PI / 180;
11766
+ const endAngle = (shape.endAngle ?? 360) * Math.PI / 180;
11767
+ ctx.beginPath();
11768
+ ctx.ellipse(shape.x, shape.y, shape.width / 2, shape.height / 2, 0, startAngle, endAngle);
11769
+ if (fill) {
11770
+ ctx.fillStyle = fill;
11771
+ ctx.fill();
11772
+ }
11773
+ ctx.strokeStyle = stroke;
11774
+ ctx.stroke();
11775
+ break;
11776
+ }
11753
11777
  case "rect": {
11754
11778
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11755
11779
  if (fill) {
@@ -11796,21 +11820,153 @@ function drawShape(ctx, shape, width, height) {
11796
11820
  ctx.fillText(shape.text, shape.x, shape.y);
11797
11821
  break;
11798
11822
  }
11823
+ case "venn-region": {
11824
+ const resolveCircles = (ids) => (ids ?? []).flatMap((id) => {
11825
+ const c = allShapes.find((s) => s.type === "circle" && s.id === id);
11826
+ return c && c.x != null && c.y != null && c.radius != null ? [{ x: c.x, y: c.y, radius: c.radius }] : [];
11827
+ });
11828
+ const inside = resolveCircles(shape.inside);
11829
+ if (inside.length === 0) break;
11830
+ const outside = resolveCircles(shape.outside);
11831
+ const off = document.createElement("canvas");
11832
+ off.width = ctx.canvas.width;
11833
+ off.height = ctx.canvas.height;
11834
+ const octx = off.getContext("2d");
11835
+ if (!octx) break;
11836
+ octx.setTransform(ctx.getTransform());
11837
+ for (const c of inside) {
11838
+ const p = new Path2D();
11839
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11840
+ octx.clip(p);
11841
+ }
11842
+ octx.fillStyle = fill ?? stroke;
11843
+ octx.fillRect(0, 0, width, height);
11844
+ octx.globalCompositeOperation = "destination-out";
11845
+ for (const c of outside) {
11846
+ const p = new Path2D();
11847
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11848
+ octx.fill(p);
11849
+ }
11850
+ ctx.save();
11851
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
11852
+ ctx.drawImage(off, 0, 0);
11853
+ ctx.restore();
11854
+ break;
11855
+ }
11799
11856
  }
11800
11857
  ctx.restore();
11801
11858
  }
11802
- var LearningCanvas;
11859
+ function readoutShapes(readouts, width) {
11860
+ const out = [];
11861
+ const chipH = 18;
11862
+ const gap = 6;
11863
+ let rightEdge = width - 6;
11864
+ let rowY = 6;
11865
+ for (const readout of readouts) {
11866
+ const text = `${readout.label}: ${String(readout.value)}`;
11867
+ const chipW = Math.min(170, Math.max(34, text.length * 6 + 12));
11868
+ let chipX = rightEdge - chipW;
11869
+ if (chipX < 4) {
11870
+ rowY += chipH + 4;
11871
+ rightEdge = width - 6;
11872
+ chipX = rightEdge - chipW;
11873
+ }
11874
+ const color = readout.color ?? "#334155";
11875
+ out.push({ type: "rect", x: chipX, y: rowY, width: chipW, height: chipH, color, fill: color });
11876
+ out.push({
11877
+ type: "text",
11878
+ x: chipX + chipW / 2,
11879
+ y: rowY + chipH / 2,
11880
+ text,
11881
+ color: "#ffffff",
11882
+ fontSize: 10,
11883
+ align: "center"
11884
+ });
11885
+ rightEdge = chipX - gap;
11886
+ }
11887
+ return out;
11888
+ }
11889
+ function traceShapes(panel, k, width, height) {
11890
+ const w = panel.width ?? Math.round(width * 0.32);
11891
+ const h = panel.height ?? Math.round(height * 0.28);
11892
+ const x = panel.x ?? width - w - 8;
11893
+ const y = panel.y ?? height - h - 8 - k * (h + 8);
11894
+ const allSamples = panel.series.flatMap((series) => series.samples);
11895
+ let xLo = Math.min(...allSamples.map((p) => p.x));
11896
+ let xHi = Math.max(...allSamples.map((p) => p.x));
11897
+ let yLo = Math.min(...allSamples.map((p) => p.y));
11898
+ let yHi = Math.max(...allSamples.map((p) => p.y));
11899
+ if (xLo === xHi) {
11900
+ xLo -= 1;
11901
+ xHi += 1;
11902
+ }
11903
+ if (yLo === yHi) {
11904
+ yLo -= 1;
11905
+ yHi += 1;
11906
+ }
11907
+ const backgroundColor = panel.backgroundColor ?? "#ffffff";
11908
+ const frameColor = panel.frameColor ?? "#94a3b8";
11909
+ const out = [];
11910
+ out.push({
11911
+ type: "rect",
11912
+ x,
11913
+ y,
11914
+ width: w,
11915
+ height: h,
11916
+ color: backgroundColor,
11917
+ fill: backgroundColor,
11918
+ opacity: panel.backgroundOpacity ?? 0.85
11919
+ });
11920
+ out.push({ type: "rect", x, y, width: w, height: h, color: frameColor, lineWidth: 1 });
11921
+ panel.series.forEach((series, j) => {
11922
+ const color = series.color ?? TRACE_SERIES_COLORS[j % TRACE_SERIES_COLORS.length];
11923
+ const mapped = series.samples.map((p) => ({
11924
+ x: x + 4 + (p.x - xLo) / (xHi - xLo) * (w - 8),
11925
+ y: y + h - 4 - (p.y - yLo) / (yHi - yLo) * (h - 8)
11926
+ }));
11927
+ for (let i = 1; i < mapped.length; i++) {
11928
+ out.push({
11929
+ type: "line",
11930
+ x1: mapped[i - 1].x,
11931
+ y1: mapped[i - 1].y,
11932
+ x2: mapped[i].x,
11933
+ y2: mapped[i].y,
11934
+ color,
11935
+ lineWidth: 1.5
11936
+ });
11937
+ }
11938
+ if (mapped.length > 0) {
11939
+ const last = mapped[mapped.length - 1];
11940
+ out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
11941
+ }
11942
+ if (series.label) {
11943
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
11944
+ }
11945
+ });
11946
+ if (panel.yLabel) {
11947
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
11948
+ }
11949
+ if (panel.xLabel) {
11950
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
11951
+ }
11952
+ return out;
11953
+ }
11954
+ var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
11803
11955
  var init_LearningCanvas = __esm({
11804
11956
  "components/learning/atoms/LearningCanvas.tsx"() {
11805
11957
  "use client";
11806
11958
  init_cn();
11807
11959
  init_useEventBus();
11960
+ DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
11961
+ TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
11808
11962
  LearningCanvas = ({
11809
11963
  className,
11810
11964
  width = 600,
11811
11965
  height = 400,
11812
11966
  backgroundColor,
11813
11967
  shapes = [],
11968
+ readouts,
11969
+ traces,
11814
11970
  interactive = false,
11815
11971
  animate = false,
11816
11972
  onShapeClick,
@@ -11836,6 +11992,12 @@ var init_LearningCanvas = __esm({
11836
11992
  }
11837
11993
  return -1;
11838
11994
  }, [shapes]);
11995
+ const derivedShapes = React87.useMemo(() => {
11996
+ if (!traces?.length && !readouts?.length) return shapes;
11997
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
11998
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
11999
+ return [...shapes, ...traceOut, ...readoutOut];
12000
+ }, [shapes, traces, readouts, width, height]);
11839
12001
  const draw = React87.useCallback(() => {
11840
12002
  const canvas = canvasRef.current;
11841
12003
  if (!canvas) return;
@@ -11852,13 +12014,13 @@ var init_LearningCanvas = __esm({
11852
12014
  ctx.fillStyle = backgroundColor;
11853
12015
  ctx.fillRect(0, 0, width, height);
11854
12016
  }
11855
- for (const shape of shapes) {
11856
- if (shape.type !== "text") drawShape(ctx, shape, width, height);
12017
+ for (const shape of derivedShapes) {
12018
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
11857
12019
  }
11858
- for (const shape of shapes) {
11859
- if (shape.type === "text") drawShape(ctx, shape, width, height);
12020
+ for (const shape of derivedShapes) {
12021
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
11860
12022
  }
11861
- }, [width, height, backgroundColor, shapes]);
12023
+ }, [width, height, backgroundColor, derivedShapes]);
11862
12024
  React87.useEffect(() => {
11863
12025
  draw();
11864
12026
  }, [draw]);
@@ -13791,7 +13953,363 @@ var init_ComponentPatterns = __esm({
13791
13953
  AlertPattern.displayName = "AlertPattern";
13792
13954
  }
13793
13955
  });
13794
- var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, AlgorithmCanvas;
13956
+ function layoutCircle(nodes, width, height) {
13957
+ const cx = width / 2;
13958
+ const cy = height / 2;
13959
+ const radius = Math.max(10, Math.min(cx, cy) - 40);
13960
+ const positions = /* @__PURE__ */ new Map();
13961
+ const n = nodes.length;
13962
+ nodes.forEach((node, i) => {
13963
+ const angle = 2 * Math.PI * i / Math.max(n, 1) - Math.PI / 2;
13964
+ positions.set(node.id, { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) });
13965
+ });
13966
+ return positions;
13967
+ }
13968
+ function layoutTree(nodes, edges, root, width, height) {
13969
+ const nodeIds = nodes.map((n) => n.id);
13970
+ const idSet = new Set(nodeIds);
13971
+ const childrenOf = /* @__PURE__ */ new Map();
13972
+ const hasIncoming = /* @__PURE__ */ new Set();
13973
+ for (const e of edges) {
13974
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
13975
+ const list = childrenOf.get(e.from) ?? [];
13976
+ list.push(e.to);
13977
+ childrenOf.set(e.from, list);
13978
+ hasIncoming.add(e.to);
13979
+ }
13980
+ const depth = /* @__PURE__ */ new Map();
13981
+ const treeChildren = /* @__PURE__ */ new Map();
13982
+ const visited = /* @__PURE__ */ new Set();
13983
+ const bfsFrom = (start) => {
13984
+ if (visited.has(start)) return;
13985
+ visited.add(start);
13986
+ depth.set(start, 0);
13987
+ const queue = [start];
13988
+ while (queue.length > 0) {
13989
+ const u = queue.shift();
13990
+ for (const v of childrenOf.get(u) ?? []) {
13991
+ if (visited.has(v)) continue;
13992
+ visited.add(v);
13993
+ depth.set(v, (depth.get(u) ?? 0) + 1);
13994
+ const list = treeChildren.get(u) ?? [];
13995
+ list.push(v);
13996
+ treeChildren.set(u, list);
13997
+ queue.push(v);
13998
+ }
13999
+ }
14000
+ };
14001
+ const primaryRoot = root && idSet.has(root) ? root : nodeIds.find((id) => !hasIncoming.has(id)) ?? nodeIds[0];
14002
+ const rootsOrder = [];
14003
+ if (primaryRoot !== void 0) {
14004
+ bfsFrom(primaryRoot);
14005
+ rootsOrder.push(primaryRoot);
14006
+ }
14007
+ for (const id of nodeIds) {
14008
+ if (!visited.has(id)) {
14009
+ bfsFrom(id);
14010
+ rootsOrder.push(id);
14011
+ }
14012
+ }
14013
+ let leafCounter = 0;
14014
+ const xSlot = /* @__PURE__ */ new Map();
14015
+ const assignXSlot = (u) => {
14016
+ const children = treeChildren.get(u) ?? [];
14017
+ if (children.length === 0) {
14018
+ const slot = leafCounter++;
14019
+ xSlot.set(u, slot);
14020
+ return slot;
14021
+ }
14022
+ const childSlots = children.map(assignXSlot);
14023
+ const avg = childSlots.reduce((a, b) => a + b, 0) / childSlots.length;
14024
+ xSlot.set(u, avg);
14025
+ return avg;
14026
+ };
14027
+ for (const r of rootsOrder) assignXSlot(r);
14028
+ let maxDepth = 0;
14029
+ for (const d of depth.values()) maxDepth = Math.max(maxDepth, d);
14030
+ const colWidth = width / Math.max(1, leafCounter);
14031
+ const rowHeight = height / (maxDepth + 1);
14032
+ const positions = /* @__PURE__ */ new Map();
14033
+ for (const id of nodeIds) {
14034
+ const slot = xSlot.get(id) ?? 0;
14035
+ const d = depth.get(id) ?? 0;
14036
+ positions.set(id, { x: slot * colWidth + colWidth / 2, y: d * rowHeight + rowHeight / 2 });
14037
+ }
14038
+ return positions;
14039
+ }
14040
+ function layoutLayered(nodes, edges, width, height) {
14041
+ const nodeIds = nodes.map((n) => n.id);
14042
+ const idSet = new Set(nodeIds);
14043
+ const adj = /* @__PURE__ */ new Map();
14044
+ const remainingIndegree = /* @__PURE__ */ new Map();
14045
+ for (const id of nodeIds) remainingIndegree.set(id, 0);
14046
+ for (const e of edges) {
14047
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
14048
+ const list = adj.get(e.from) ?? [];
14049
+ list.push(e.to);
14050
+ adj.set(e.from, list);
14051
+ remainingIndegree.set(e.to, (remainingIndegree.get(e.to) ?? 0) + 1);
14052
+ }
14053
+ const layer = /* @__PURE__ */ new Map();
14054
+ const dequeued = /* @__PURE__ */ new Set();
14055
+ const queue = [];
14056
+ for (const id of nodeIds) {
14057
+ if ((remainingIndegree.get(id) ?? 0) === 0) {
14058
+ layer.set(id, 0);
14059
+ queue.push(id);
14060
+ }
14061
+ }
14062
+ while (queue.length > 0) {
14063
+ const u = queue.shift();
14064
+ dequeued.add(u);
14065
+ for (const v of adj.get(u) ?? []) {
14066
+ const candidate = (layer.get(u) ?? 0) + 1;
14067
+ layer.set(v, Math.max(layer.get(v) ?? 0, candidate));
14068
+ remainingIndegree.set(v, (remainingIndegree.get(v) ?? 0) - 1);
14069
+ if ((remainingIndegree.get(v) ?? 0) === 0 && !dequeued.has(v)) {
14070
+ queue.push(v);
14071
+ }
14072
+ }
14073
+ }
14074
+ let baseMaxLayer = 0;
14075
+ for (const id of nodeIds) {
14076
+ if (dequeued.has(id)) baseMaxLayer = Math.max(baseMaxLayer, layer.get(id) ?? 0);
14077
+ }
14078
+ const cycleLayer = baseMaxLayer + 1;
14079
+ let maxLayer = baseMaxLayer;
14080
+ for (const id of nodeIds) {
14081
+ if (!dequeued.has(id)) {
14082
+ layer.set(id, cycleLayer);
14083
+ maxLayer = cycleLayer;
14084
+ }
14085
+ }
14086
+ const colWidth = width / Math.max(1, maxLayer + 1);
14087
+ const byLayer = /* @__PURE__ */ new Map();
14088
+ for (const id of nodeIds) {
14089
+ const l = layer.get(id) ?? 0;
14090
+ const list = byLayer.get(l) ?? [];
14091
+ list.push(id);
14092
+ byLayer.set(l, list);
14093
+ }
14094
+ const positions = /* @__PURE__ */ new Map();
14095
+ for (const [l, ids] of byLayer) {
14096
+ const rowHeight = height / ids.length;
14097
+ ids.forEach((id, i) => {
14098
+ positions.set(id, { x: l * colWidth + colWidth / 2, y: i * rowHeight + rowHeight / 2 });
14099
+ });
14100
+ }
14101
+ return positions;
14102
+ }
14103
+ function computePositions(nodes, edges, layout, root, width, height) {
14104
+ switch (layout) {
14105
+ case "circle":
14106
+ return layoutCircle(nodes, width, height);
14107
+ case "tree":
14108
+ return layoutTree(nodes, edges, root, width, height);
14109
+ case "layered":
14110
+ return layoutLayered(nodes, edges, width, height);
14111
+ case "manual":
14112
+ default: {
14113
+ const positions = /* @__PURE__ */ new Map();
14114
+ for (const n of nodes) positions.set(n.id, { x: n.x ?? 0, y: n.y ?? 0 });
14115
+ return positions;
14116
+ }
14117
+ }
14118
+ }
14119
+ var NODE_STATE_COLOR, EDGE_STATE_COLOR, DEFAULT_NODE_RADIUS, AlgoGraphCanvas;
14120
+ var init_AlgoGraphCanvas = __esm({
14121
+ "components/learning/molecules/AlgoGraphCanvas.tsx"() {
14122
+ "use client";
14123
+ init_atoms();
14124
+ init_Stack();
14125
+ init_LearningCanvas();
14126
+ NODE_STATE_COLOR = {
14127
+ unvisited: "#cbd5e1",
14128
+ frontier: "#f59e0b",
14129
+ current: "#ef4444",
14130
+ visited: "#22c55e",
14131
+ goal: "#8b5cf6",
14132
+ path: "#0ea5e9"
14133
+ };
14134
+ EDGE_STATE_COLOR = {
14135
+ default: "#9ca3af",
14136
+ tree: "#16a34a",
14137
+ relaxed: "#f59e0b",
14138
+ candidate: "#38bdf8",
14139
+ path: "#dc2626"
14140
+ };
14141
+ DEFAULT_NODE_RADIUS = 18;
14142
+ AlgoGraphCanvas = ({
14143
+ className,
14144
+ width = 600,
14145
+ height = 400,
14146
+ title,
14147
+ backgroundColor,
14148
+ nodes = [],
14149
+ edges = [],
14150
+ layout = "manual",
14151
+ root,
14152
+ shapes = [],
14153
+ interactive = false,
14154
+ animate = false,
14155
+ onShapeClick,
14156
+ onNodeClick,
14157
+ isLoading,
14158
+ error
14159
+ }) => {
14160
+ const nodeById = React87.useMemo(() => {
14161
+ const m = /* @__PURE__ */ new Map();
14162
+ for (const n of nodes) m.set(n.id, n);
14163
+ return m;
14164
+ }, [nodes]);
14165
+ const nodeIndexById = React87.useMemo(() => {
14166
+ const m = /* @__PURE__ */ new Map();
14167
+ nodes.forEach((n, i) => m.set(n.id, i));
14168
+ return m;
14169
+ }, [nodes]);
14170
+ const derivedShapes = React87.useMemo(() => {
14171
+ const out = [];
14172
+ const positions = computePositions(nodes, edges, layout, root, width, height);
14173
+ const edgeGeoms = [];
14174
+ for (const e of edges) {
14175
+ const a = nodeById.get(e.from);
14176
+ const b = nodeById.get(e.to);
14177
+ const posA = positions.get(e.from);
14178
+ const posB = positions.get(e.to);
14179
+ if (!a || !b || !posA || !posB) continue;
14180
+ const rA = a.radius ?? DEFAULT_NODE_RADIUS;
14181
+ const rB = b.radius ?? DEFAULT_NODE_RADIUS;
14182
+ const dx = posB.x - posA.x;
14183
+ const dy = posB.y - posA.y;
14184
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
14185
+ const ux = dx / dist;
14186
+ const uy = dy / dist;
14187
+ edgeGeoms.push({
14188
+ directed: e.directed ?? false,
14189
+ x1: posA.x + ux * rA,
14190
+ y1: posA.y + uy * rA,
14191
+ x2: posB.x - ux * rB,
14192
+ y2: posB.y - uy * rB,
14193
+ color: e.color ?? EDGE_STATE_COLOR[e.state ?? "default"],
14194
+ label: e.label ?? (e.weight != null ? String(e.weight) : void 0)
14195
+ });
14196
+ }
14197
+ for (const g of edgeGeoms) {
14198
+ out.push({
14199
+ type: g.directed ? "arrow" : "line",
14200
+ x1: g.x1,
14201
+ y1: g.y1,
14202
+ x2: g.x2,
14203
+ y2: g.y2,
14204
+ color: g.color,
14205
+ lineWidth: 2
14206
+ });
14207
+ }
14208
+ for (const g of edgeGeoms) {
14209
+ if (g.label === void 0) continue;
14210
+ const midX = (g.x1 + g.x2) / 2;
14211
+ const midY = (g.y1 + g.y2) / 2;
14212
+ const dx = g.x2 - g.x1;
14213
+ const dy = g.y2 - g.y1;
14214
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
14215
+ const perpX = -(dy / dist);
14216
+ const perpY = dx / dist;
14217
+ out.push({
14218
+ type: "text",
14219
+ x: midX + perpX * 10,
14220
+ y: midY + perpY * 10,
14221
+ text: g.label,
14222
+ fontSize: 11,
14223
+ align: "center",
14224
+ color: "#374151"
14225
+ });
14226
+ }
14227
+ const nodeGeoms = [];
14228
+ for (const n of nodes) {
14229
+ const pos = positions.get(n.id);
14230
+ if (!pos) continue;
14231
+ nodeGeoms.push({
14232
+ id: n.id,
14233
+ x: pos.x,
14234
+ y: pos.y,
14235
+ radius: n.radius ?? DEFAULT_NODE_RADIUS,
14236
+ color: n.color ?? NODE_STATE_COLOR[n.state ?? "unvisited"],
14237
+ label: n.label,
14238
+ badge: n.badge
14239
+ });
14240
+ }
14241
+ for (const g of nodeGeoms) {
14242
+ out.push({ type: "circle", id: g.id, x: g.x, y: g.y, radius: g.radius, color: g.color, fill: `${g.color}33` });
14243
+ }
14244
+ const badgeGeoms = [];
14245
+ for (const g of nodeGeoms) {
14246
+ if (!g.badge) continue;
14247
+ const w = Math.min(42, Math.max(18, g.badge.text.length * 6 + 10));
14248
+ badgeGeoms.push({
14249
+ cx: g.x + g.radius * 0.75,
14250
+ cy: g.y - g.radius * 0.75,
14251
+ w,
14252
+ h: 14,
14253
+ // Borderless pill: same color drives both stroke and fill.
14254
+ color: g.badge.color ?? "#1e293b",
14255
+ text: g.badge.text
14256
+ });
14257
+ }
14258
+ for (const b of badgeGeoms) {
14259
+ 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 });
14260
+ }
14261
+ for (const b of badgeGeoms) {
14262
+ out.push({ type: "text", x: b.cx, y: b.cy, text: b.text, fontSize: 9, align: "center", color: "#ffffff" });
14263
+ }
14264
+ for (const g of nodeGeoms) {
14265
+ if (g.label === void 0) continue;
14266
+ out.push({
14267
+ type: "text",
14268
+ x: g.x,
14269
+ y: g.y + g.radius + 14,
14270
+ text: g.label,
14271
+ fontSize: 12,
14272
+ align: "center",
14273
+ color: "#111827"
14274
+ });
14275
+ }
14276
+ out.push(...shapes);
14277
+ return out;
14278
+ }, [nodes, edges, layout, root, width, height, nodeById, shapes]);
14279
+ const handleShapeClick = React87.useCallback(
14280
+ (payload) => {
14281
+ if (payload.type === "circle" && payload.id) {
14282
+ const node = nodeById.get(payload.id);
14283
+ const idx = nodeIndexById.get(payload.id);
14284
+ if (node && idx !== void 0) {
14285
+ onNodeClick?.({ id: node.id, label: node.label, index: idx });
14286
+ }
14287
+ }
14288
+ onShapeClick?.(payload);
14289
+ },
14290
+ [nodeById, nodeIndexById, onNodeClick, onShapeClick]
14291
+ );
14292
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
14293
+ title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
14294
+ /* @__PURE__ */ jsxRuntime.jsx(
14295
+ LearningCanvas,
14296
+ {
14297
+ width,
14298
+ height,
14299
+ backgroundColor,
14300
+ shapes: derivedShapes,
14301
+ interactive,
14302
+ animate,
14303
+ onShapeClick: onShapeClick || onNodeClick ? handleShapeClick : void 0,
14304
+ isLoading,
14305
+ error
14306
+ }
14307
+ )
14308
+ ] }) });
14309
+ };
14310
+ }
14311
+ });
14312
+ 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;
13795
14313
  var init_AlgorithmCanvas = __esm({
13796
14314
  "components/learning/molecules/AlgorithmCanvas.tsx"() {
13797
14315
  "use client";
@@ -13803,6 +14321,43 @@ var init_AlgorithmCanvas = __esm({
13803
14321
  DEFAULT_POINTER_COLOR = "#dc2626";
13804
14322
  POINTER_BAND = 34;
13805
14323
  TOP_PAD = 26;
14324
+ PANEL_FAMILY_ORDER = ["bars", "slots", "cells", "buckets", "frames"];
14325
+ RANGE_COLOR_DEFAULT = "#3b82f6";
14326
+ RANGE_FILL_OPACITY = 0.15;
14327
+ BRACKET_TOP_OFFSET = 16;
14328
+ BRACKET_ROW_H = 14;
14329
+ BRACKET_TICK_H = 6;
14330
+ BRACKET_LABEL_OFFSET = 6;
14331
+ SLOT_EMPTY_FILL = "#f1f5f9";
14332
+ SLOT_EMPTY_STROKE = "#cbd5e1";
14333
+ SLOT_FILLED_STROKE = "#9ca3af";
14334
+ SLOT_HIGHLIGHT_DEFAULT = "#f59e0b";
14335
+ SLOT_VALUE_TEXT_COLOR = "#ffffff";
14336
+ FRAME_ACTIVE_COLOR = "#3b82f6";
14337
+ FRAME_RETURNING_COLOR = "#f59e0b";
14338
+ FRAME_DONE_COLOR = "#94a3b8";
14339
+ FRAME_LABEL_COLOR = "#ffffff";
14340
+ FRAME_DETAIL_COLOR = "#e2e8f0";
14341
+ FRAME_TWO_LINE_MIN_H = 22;
14342
+ BUCKET_INDEX_FILL = "#e2e8f0";
14343
+ BUCKET_INDEX_STROKE = "#9ca3af";
14344
+ BUCKET_INDEX_TEXT = "#374151";
14345
+ BUCKET_ENTRY_TEXT = "#ffffff";
14346
+ BUCKET_ENTRY_DEFAULT = "#3b82f6";
14347
+ BUCKET_ENTRY_HIGHLIGHT = "#f59e0b";
14348
+ BUCKET_ENTRY_PROBING = "#38bdf8";
14349
+ BUCKET_ENTRY_MIN_W = 24;
14350
+ BUCKET_ENTRY_MAX_W = 64;
14351
+ AXIS_LABEL_COLOR = "#6b7280";
14352
+ AXIS_LABEL_FONT_SIZE = 10;
14353
+ CORNER_TEXT_COLOR = "#111827";
14354
+ CORNER_FONT_SIZE = 7;
14355
+ CORNER_MIN_CELL = 28;
14356
+ CORNER_INSET_X = 3;
14357
+ CORNER_INSET_Y = 6;
14358
+ AUX_PRIMARY_RATIO = 0.6;
14359
+ AUX_LABEL_BAND = 18;
14360
+ AUX_BASELINE_PAD = 8;
13806
14361
  AlgorithmCanvas = ({
13807
14362
  className,
13808
14363
  width = 600,
@@ -13812,6 +14367,14 @@ var init_AlgorithmCanvas = __esm({
13812
14367
  bars = [],
13813
14368
  cells = [],
13814
14369
  pointers = [],
14370
+ ranges = [],
14371
+ slots = [],
14372
+ slotOrientation = "horizontal",
14373
+ frames = [],
14374
+ buckets = [],
14375
+ auxBars = [],
14376
+ rowLabels = [],
14377
+ colLabels = [],
13815
14378
  shapes = [],
13816
14379
  interactive = false,
13817
14380
  animate = false,
@@ -13821,12 +14384,35 @@ var init_AlgorithmCanvas = __esm({
13821
14384
  }) => {
13822
14385
  const derivedShapes = React87.useMemo(() => {
13823
14386
  const out = [];
14387
+ const presence = {
14388
+ bars: bars.length > 0,
14389
+ slots: slots.length > 0,
14390
+ cells: cells.length > 0,
14391
+ buckets: buckets.length > 0,
14392
+ frames: frames.length > 0
14393
+ };
14394
+ const panelCount = PANEL_FAMILY_ORDER.filter((f3) => presence[f3]).length;
14395
+ const panelHeight = height / Math.max(1, panelCount);
14396
+ const panelY = { bars: 0, slots: 0, cells: 0, buckets: 0, frames: 0 };
14397
+ let compactIndex = 0;
14398
+ PANEL_FAMILY_ORDER.forEach((f3) => {
14399
+ if (presence[f3]) {
14400
+ panelY[f3] = compactIndex * panelHeight;
14401
+ compactIndex += 1;
14402
+ }
14403
+ });
13824
14404
  if (bars.length > 0) {
14405
+ const panelYBars = panelY.bars;
13825
14406
  const slot = width / bars.length;
13826
14407
  const barW = slot * 0.8;
13827
14408
  const gap = slot * 0.1;
13828
- const baseline = height - POINTER_BAND;
13829
- const usableH = baseline - TOP_PAD;
14409
+ const bracketRanges = ranges.filter((r) => r.kind === "bracket");
14410
+ const bracketCount = bracketRanges.length;
14411
+ const bracketHeadroom = bracketCount > 0 ? BRACKET_TOP_OFFSET + bracketCount * BRACKET_ROW_H : 0;
14412
+ const hasAux = auxBars.length > 0;
14413
+ const primaryH = hasAux ? panelHeight * AUX_PRIMARY_RATIO : panelHeight;
14414
+ const baseline = panelYBars + primaryH - POINTER_BAND;
14415
+ const usableH = baseline - (panelYBars + TOP_PAD + bracketHeadroom);
13830
14416
  const maxV = Math.max(1, ...bars.map((b) => Number.isFinite(b.value) ? b.value : 0));
13831
14417
  bars.forEach((bar, i) => {
13832
14418
  const v = Number.isFinite(bar.value) ? bar.value : 0;
@@ -13856,6 +14442,89 @@ var init_AlgorithmCanvas = __esm({
13856
14442
  });
13857
14443
  }
13858
14444
  });
14445
+ ranges.forEach((r) => {
14446
+ const kind = r.kind ?? "fill";
14447
+ if (kind !== "fill") return;
14448
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14449
+ out.push({
14450
+ type: "rect",
14451
+ x: r.from * slot,
14452
+ y: panelYBars,
14453
+ width: (r.to - r.from + 1) * slot,
14454
+ height: primaryH,
14455
+ color,
14456
+ fill: color,
14457
+ opacity: RANGE_FILL_OPACITY
14458
+ });
14459
+ if (r.label) {
14460
+ out.push({
14461
+ type: "text",
14462
+ x: r.from * slot + 4,
14463
+ // Sits below the bracket block (if any) so fill and bracket labels never collide.
14464
+ y: panelYBars + 10 + bracketHeadroom,
14465
+ text: r.label,
14466
+ color,
14467
+ fontSize: 10,
14468
+ align: "left"
14469
+ });
14470
+ }
14471
+ });
14472
+ bracketRanges.forEach((r, i) => {
14473
+ const bracketY = panelYBars + BRACKET_TOP_OFFSET + i * BRACKET_ROW_H;
14474
+ const x1 = r.from * slot + slot * 0.1;
14475
+ const x2 = (r.to + 1) * slot - slot * 0.1;
14476
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14477
+ out.push({ type: "line", x1, y1: bracketY, x2, y2: bracketY, color, lineWidth: 2 });
14478
+ out.push({ type: "line", x1, y1: bracketY, x2: x1, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14479
+ out.push({ type: "line", x1: x2, y1: bracketY, x2, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14480
+ if (r.label) {
14481
+ out.push({
14482
+ type: "text",
14483
+ x: (x1 + x2) / 2,
14484
+ y: bracketY - BRACKET_LABEL_OFFSET,
14485
+ text: r.label,
14486
+ color,
14487
+ fontSize: 10,
14488
+ align: "center"
14489
+ });
14490
+ }
14491
+ });
14492
+ if (hasAux) {
14493
+ const auxH = panelHeight - primaryH;
14494
+ const slot2 = width / auxBars.length;
14495
+ const auxBaseline = panelYBars + primaryH + auxH - AUX_BASELINE_PAD;
14496
+ const auxUsableH = auxBaseline - (panelYBars + primaryH + AUX_LABEL_BAND);
14497
+ const maxAuxV = Math.max(1, ...auxBars.map((b) => Number.isFinite(b.value) ? b.value : 0));
14498
+ auxBars.forEach((bar, i) => {
14499
+ const v = Number.isFinite(bar.value) ? bar.value : 0;
14500
+ const bh = Math.max(0, v / maxAuxV * auxUsableH);
14501
+ const x = i * slot2 + slot2 * 0.1;
14502
+ const w = slot2 * 0.8;
14503
+ const color = bar.color ?? DEFAULT_BAR_COLOR;
14504
+ out.push({
14505
+ type: "rect",
14506
+ id: `auxbar-${i}`,
14507
+ x,
14508
+ y: auxBaseline - bh,
14509
+ width: w,
14510
+ height: bh,
14511
+ color,
14512
+ fill: color
14513
+ });
14514
+ const label = bar.label ?? (auxBars.length <= 24 ? String(v) : void 0);
14515
+ if (label) {
14516
+ out.push({
14517
+ type: "text",
14518
+ x: x + w / 2,
14519
+ y: auxBaseline - bh - 8,
14520
+ text: label,
14521
+ color: "#374151",
14522
+ fontSize: 11,
14523
+ align: "center"
14524
+ });
14525
+ }
14526
+ });
14527
+ }
13859
14528
  pointers.forEach((p) => {
13860
14529
  if (p.index < 0 || p.index >= bars.length) return;
13861
14530
  const cx = p.index * slot + slot / 2;
@@ -13863,7 +14532,7 @@ var init_AlgorithmCanvas = __esm({
13863
14532
  out.push({
13864
14533
  type: "arrow",
13865
14534
  x1: cx,
13866
- y1: height - 6,
14535
+ y1: panelYBars + primaryH - 18,
13867
14536
  x2: cx,
13868
14537
  y2: baseline + 4,
13869
14538
  color,
@@ -13873,7 +14542,7 @@ var init_AlgorithmCanvas = __esm({
13873
14542
  out.push({
13874
14543
  type: "text",
13875
14544
  x: cx,
13876
- y: height - 22,
14545
+ y: panelYBars + primaryH - 8,
13877
14546
  text: p.label,
13878
14547
  color,
13879
14548
  fontSize: 11,
@@ -13882,14 +14551,111 @@ var init_AlgorithmCanvas = __esm({
13882
14551
  }
13883
14552
  });
13884
14553
  }
14554
+ if (slots.length > 0) {
14555
+ const panelYSlots = panelY.slots;
14556
+ const n = slots.length;
14557
+ const vertical = slotOrientation === "vertical";
14558
+ const vBoxH = panelHeight / n;
14559
+ const vBoxW = Math.min(width * 0.5, 120);
14560
+ const vBoxX = (width - vBoxW) / 2;
14561
+ const hCellW = width / n;
14562
+ const hBoxW = hCellW * 0.82;
14563
+ const hBoxH = Math.min(panelHeight * 0.6, 48);
14564
+ const hBoxY = panelYSlots + (panelHeight - hBoxH) / 2;
14565
+ 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 };
14566
+ slots.forEach((s, i) => {
14567
+ const box = slotBox(i);
14568
+ const state = s.state ?? "filled";
14569
+ const fill = state === "empty" ? SLOT_EMPTY_FILL : state === "highlight" ? s.color ?? SLOT_HIGHLIGHT_DEFAULT : s.color ?? DEFAULT_BAR_COLOR;
14570
+ const stroke = state === "empty" ? SLOT_EMPTY_STROKE : SLOT_FILLED_STROKE;
14571
+ out.push({
14572
+ type: "rect",
14573
+ id: `slot-${i}`,
14574
+ x: box.x,
14575
+ y: box.y,
14576
+ width: box.width,
14577
+ height: box.height,
14578
+ color: stroke,
14579
+ fill
14580
+ });
14581
+ if (s.value != null && state !== "empty") {
14582
+ out.push({
14583
+ type: "text",
14584
+ x: box.x + box.width / 2,
14585
+ y: box.y + box.height / 2,
14586
+ text: String(s.value),
14587
+ color: SLOT_VALUE_TEXT_COLOR,
14588
+ fontSize: 12,
14589
+ align: "center"
14590
+ });
14591
+ }
14592
+ });
14593
+ if (bars.length === 0) {
14594
+ pointers.forEach((p) => {
14595
+ if (p.index < 0 || p.index >= slots.length) return;
14596
+ const box = slotBox(p.index);
14597
+ const color = p.color ?? DEFAULT_POINTER_COLOR;
14598
+ if (vertical) {
14599
+ const cy = box.y + box.height / 2;
14600
+ out.push({
14601
+ type: "arrow",
14602
+ x1: box.x + box.width + 34,
14603
+ y1: cy,
14604
+ x2: box.x + box.width + 4,
14605
+ y2: cy,
14606
+ color,
14607
+ lineWidth: 2
14608
+ });
14609
+ if (p.label) {
14610
+ out.push({
14611
+ type: "text",
14612
+ x: box.x + box.width + 38,
14613
+ y: cy,
14614
+ text: p.label,
14615
+ color,
14616
+ fontSize: 11,
14617
+ align: "left"
14618
+ });
14619
+ }
14620
+ } else {
14621
+ const cx = box.x + box.width / 2;
14622
+ out.push({
14623
+ type: "arrow",
14624
+ x1: cx,
14625
+ y1: panelYSlots + panelHeight - 18,
14626
+ x2: cx,
14627
+ y2: box.y + box.height + 4,
14628
+ color,
14629
+ lineWidth: 2
14630
+ });
14631
+ if (p.label) {
14632
+ out.push({
14633
+ type: "text",
14634
+ x: cx,
14635
+ y: panelYSlots + panelHeight - 8,
14636
+ text: p.label,
14637
+ color,
14638
+ fontSize: 11,
14639
+ align: "center"
14640
+ });
14641
+ }
14642
+ }
14643
+ });
14644
+ }
14645
+ }
13885
14646
  if (cells.length > 0) {
14647
+ const panelYCells = panelY.cells;
13886
14648
  const maxCol = Math.max(0, ...cells.map((c) => c.col)) + 1;
13887
14649
  const maxRow = Math.max(0, ...cells.map((c) => c.row)) + 1;
13888
- const cw = width / maxCol;
13889
- const ch = height / maxRow;
14650
+ const colLabelH = colLabels.length > 0 ? 16 : 0;
14651
+ const rowLabelW = rowLabels.length > 0 ? 20 : 0;
14652
+ const gridX0 = rowLabelW;
14653
+ const gridY0 = panelYCells + colLabelH;
14654
+ const cw = (width - rowLabelW) / maxCol;
14655
+ const ch = (panelHeight - colLabelH) / maxRow;
13890
14656
  cells.forEach((c, i) => {
13891
- const x = c.col * cw;
13892
- const y = c.row * ch;
14657
+ const x = gridX0 + c.col * cw;
14658
+ const y = gridY0 + c.row * ch;
13893
14659
  const color = c.color ?? DEFAULT_CELL_COLOR;
13894
14660
  out.push({
13895
14661
  type: "rect",
@@ -13913,11 +14679,207 @@ var init_AlgorithmCanvas = __esm({
13913
14679
  align: "center"
13914
14680
  });
13915
14681
  }
14682
+ if (c.corner && cw >= CORNER_MIN_CELL && ch >= CORNER_MIN_CELL) {
14683
+ const { tl, tr, bl, br } = c.corner;
14684
+ if (tl) {
14685
+ out.push({
14686
+ type: "text",
14687
+ x: x + CORNER_INSET_X,
14688
+ y: y + CORNER_INSET_Y,
14689
+ text: tl,
14690
+ color: CORNER_TEXT_COLOR,
14691
+ fontSize: CORNER_FONT_SIZE,
14692
+ align: "left"
14693
+ });
14694
+ }
14695
+ if (tr) {
14696
+ out.push({
14697
+ type: "text",
14698
+ x: x + cw - CORNER_INSET_X,
14699
+ y: y + CORNER_INSET_Y,
14700
+ text: tr,
14701
+ color: CORNER_TEXT_COLOR,
14702
+ fontSize: CORNER_FONT_SIZE,
14703
+ align: "right"
14704
+ });
14705
+ }
14706
+ if (bl) {
14707
+ out.push({
14708
+ type: "text",
14709
+ x: x + CORNER_INSET_X,
14710
+ y: y + ch - CORNER_INSET_Y,
14711
+ text: bl,
14712
+ color: CORNER_TEXT_COLOR,
14713
+ fontSize: CORNER_FONT_SIZE,
14714
+ align: "left"
14715
+ });
14716
+ }
14717
+ if (br) {
14718
+ out.push({
14719
+ type: "text",
14720
+ x: x + cw - CORNER_INSET_X,
14721
+ y: y + ch - CORNER_INSET_Y,
14722
+ text: br,
14723
+ color: CORNER_TEXT_COLOR,
14724
+ fontSize: CORNER_FONT_SIZE,
14725
+ align: "right"
14726
+ });
14727
+ }
14728
+ }
14729
+ });
14730
+ colLabels.forEach((l) => {
14731
+ out.push({
14732
+ type: "text",
14733
+ x: gridX0 + l.index * cw + cw / 2,
14734
+ y: panelYCells + colLabelH / 2,
14735
+ text: l.text,
14736
+ color: l.color ?? AXIS_LABEL_COLOR,
14737
+ fontSize: AXIS_LABEL_FONT_SIZE,
14738
+ align: "center"
14739
+ });
14740
+ });
14741
+ rowLabels.forEach((l) => {
14742
+ out.push({
14743
+ type: "text",
14744
+ x: rowLabelW - 6,
14745
+ y: gridY0 + l.index * ch + ch / 2,
14746
+ text: l.text,
14747
+ color: l.color ?? AXIS_LABEL_COLOR,
14748
+ fontSize: AXIS_LABEL_FONT_SIZE,
14749
+ align: "right"
14750
+ });
14751
+ });
14752
+ }
14753
+ if (buckets.length > 0) {
14754
+ const panelYBuckets = panelY.buckets;
14755
+ const bucketCount = Math.max(0, ...buckets.map((b) => b.index)) + 1;
14756
+ const rowH = panelHeight / bucketCount;
14757
+ const indexColW = Math.min(width * 0.12, 40);
14758
+ const maxChainLen = Math.max(1, ...buckets.map((b) => b.entries.length));
14759
+ const entryW = Math.min(BUCKET_ENTRY_MAX_W, Math.max(BUCKET_ENTRY_MIN_W, (width - indexColW - 8) / maxChainLen));
14760
+ const maxVisible = Math.floor((width - indexColW - 4) / entryW);
14761
+ buckets.forEach((b) => {
14762
+ const rowY = panelYBuckets + b.index * rowH;
14763
+ out.push({
14764
+ type: "rect",
14765
+ id: `bucket-index-${b.index}`,
14766
+ x: 2,
14767
+ y: rowY + 2,
14768
+ width: indexColW - 4,
14769
+ height: rowH - 4,
14770
+ color: BUCKET_INDEX_STROKE,
14771
+ fill: BUCKET_INDEX_FILL
14772
+ });
14773
+ out.push({
14774
+ type: "text",
14775
+ x: 2 + (indexColW - 4) / 2,
14776
+ y: rowY + rowH / 2,
14777
+ text: String(b.index),
14778
+ color: BUCKET_INDEX_TEXT,
14779
+ fontSize: 10,
14780
+ align: "center"
14781
+ });
14782
+ const overflow = b.entries.length > maxVisible;
14783
+ const visibleCount = overflow ? Math.max(0, maxVisible - 1) : b.entries.length;
14784
+ for (let j = 0; j < visibleCount; j++) {
14785
+ const entry = b.entries[j];
14786
+ const ex = indexColW + 4 + j * entryW;
14787
+ const state = entry.state ?? "default";
14788
+ const fill = state === "highlight" ? entry.color ?? BUCKET_ENTRY_HIGHLIGHT : state === "probing" ? entry.color ?? BUCKET_ENTRY_PROBING : entry.color ?? BUCKET_ENTRY_DEFAULT;
14789
+ out.push({
14790
+ type: "rect",
14791
+ id: `bucket-${b.index}-${j}`,
14792
+ x: ex,
14793
+ y: rowY + 2,
14794
+ width: entryW - 2,
14795
+ height: rowH - 4,
14796
+ color: fill,
14797
+ fill
14798
+ });
14799
+ if (entryW >= 20 && rowH >= 16) {
14800
+ out.push({
14801
+ type: "text",
14802
+ x: ex + (entryW - 2) / 2,
14803
+ y: rowY + rowH / 2,
14804
+ text: entry.label,
14805
+ color: BUCKET_ENTRY_TEXT,
14806
+ fontSize: 10,
14807
+ align: "center"
14808
+ });
14809
+ }
14810
+ }
14811
+ if (overflow) {
14812
+ const ex = indexColW + 4 + visibleCount * entryW;
14813
+ out.push({
14814
+ type: "rect",
14815
+ id: `bucket-${b.index}-overflow`,
14816
+ x: ex,
14817
+ y: rowY + 2,
14818
+ width: entryW - 2,
14819
+ height: rowH - 4,
14820
+ color: BUCKET_ENTRY_DEFAULT,
14821
+ fill: BUCKET_ENTRY_DEFAULT
14822
+ });
14823
+ out.push({
14824
+ type: "text",
14825
+ x: ex + (entryW - 2) / 2,
14826
+ y: rowY + rowH / 2,
14827
+ text: `+${b.entries.length - visibleCount}`,
14828
+ color: BUCKET_ENTRY_TEXT,
14829
+ fontSize: 10,
14830
+ align: "center"
14831
+ });
14832
+ }
14833
+ });
14834
+ }
14835
+ if (frames.length > 0) {
14836
+ const panelYFrames = panelY.frames;
14837
+ const n = frames.length;
14838
+ const frameH = panelHeight / n;
14839
+ const x = 8;
14840
+ const w = width - 16;
14841
+ frames.forEach((f3, i) => {
14842
+ const y = panelYFrames + panelHeight - (i + 1) * frameH;
14843
+ const state = f3.state ?? "active";
14844
+ const fill = state === "returning" ? f3.color ?? FRAME_RETURNING_COLOR : state === "done" ? f3.color ?? FRAME_DONE_COLOR : f3.color ?? FRAME_ACTIVE_COLOR;
14845
+ out.push({ type: "rect", id: `frame-${i}`, x, y, width: w, height: frameH, color: fill, fill });
14846
+ if (frameH >= FRAME_TWO_LINE_MIN_H) {
14847
+ out.push({
14848
+ type: "text",
14849
+ x: 16,
14850
+ y: y + frameH * 0.35,
14851
+ text: f3.label,
14852
+ color: FRAME_LABEL_COLOR,
14853
+ fontSize: 10,
14854
+ align: "left"
14855
+ });
14856
+ if (f3.detail) {
14857
+ out.push({
14858
+ type: "text",
14859
+ x: 16,
14860
+ y: y + frameH * 0.7,
14861
+ text: f3.detail,
14862
+ color: FRAME_DETAIL_COLOR,
14863
+ fontSize: 10,
14864
+ align: "left"
14865
+ });
14866
+ }
14867
+ } else {
14868
+ out.push({
14869
+ type: "text",
14870
+ x: 16,
14871
+ y: y + frameH / 2,
14872
+ text: f3.label,
14873
+ color: FRAME_LABEL_COLOR,
14874
+ fontSize: 10,
14875
+ align: "left"
14876
+ });
14877
+ }
13916
14878
  });
13917
14879
  }
13918
14880
  out.push(...shapes);
13919
14881
  return out;
13920
- }, [bars, cells, pointers, shapes, width, height]);
14882
+ }, [bars, cells, pointers, ranges, slots, slotOrientation, frames, buckets, auxBars, rowLabels, colLabels, shapes, width, height]);
13921
14883
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
13922
14884
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
13923
14885
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -14906,23 +15868,422 @@ var init_BehaviorView = __esm({
14906
15868
  BehaviorView.displayName = "BehaviorView";
14907
15869
  }
14908
15870
  });
14909
- var BiologyCanvas;
15871
+ function LearningScene3D({
15872
+ className,
15873
+ width = 600,
15874
+ height = 400,
15875
+ title,
15876
+ backgroundColor,
15877
+ drawables,
15878
+ camera,
15879
+ lighting,
15880
+ post,
15881
+ showGrid = false,
15882
+ shadows,
15883
+ interactive,
15884
+ isLoading,
15885
+ error,
15886
+ onItemClick
15887
+ }) {
15888
+ const instanceId = React87.useId().replace(/[^a-zA-Z0-9]/g, "");
15889
+ const clickEvent = onItemClick ? `LEARNING_SCENE_3D.ITEM_CLICK.${instanceId}` : void 0;
15890
+ const onItemClickRef = React87.useRef(onItemClick);
15891
+ onItemClickRef.current = onItemClick;
15892
+ useEventListener(`UI:${clickEvent ?? "LEARNING_SCENE_3D.ITEM_CLICK.__disabled"}`, (event) => {
15893
+ const unitId = event.payload?.unitId;
15894
+ if (typeof unitId === "string") onItemClickRef.current?.(unitId);
15895
+ });
15896
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && post?.bloom) {
15897
+ const unknownKeys = Object.keys(post.bloom).filter((k) => !KNOWN_BLOOM_KEYS.has(k));
15898
+ if (unknownKeys.length > 0) {
15899
+ sceneLog.debug("post.bloom has unrecognized keys \u2014 only intensity/threshold/smoothing are read", { unknownKeys });
15900
+ }
15901
+ }
15902
+ const props3d = {
15903
+ drawables,
15904
+ isLoading,
15905
+ error: error ? error.message : null,
15906
+ cameraMode: camera?.mode ?? "perspective",
15907
+ ...camera?.zoom !== void 0 ? { zoom: camera.zoom } : {},
15908
+ ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
15909
+ ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
15910
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
15911
+ ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
15912
+ backgroundColor,
15913
+ showGrid,
15914
+ ...shadows !== void 0 ? { shadows } : {},
15915
+ ...interactive !== void 0 ? { controlsEnabled: interactive } : {},
15916
+ lighting,
15917
+ post,
15918
+ ...clickEvent !== void 0 ? { unitClickEvent: clickEvent } : {}
15919
+ };
15920
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
15921
+ title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
15922
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width, height, display: "flex" }, children: /* @__PURE__ */ jsxRuntime.jsx(React87.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(Canvas3DHost2, { ...props3d }) }) })
15923
+ ] }) });
15924
+ }
15925
+ function meshSphere(id, x, y, z, radius, color, opts) {
15926
+ return {
15927
+ type: "draw-mesh",
15928
+ ...id !== void 0 ? { id } : {},
15929
+ shape: opts?.shape ?? "sphere",
15930
+ position: { x, y, z },
15931
+ radius,
15932
+ pivot: "center",
15933
+ segments: opts?.segments ?? 24,
15934
+ material: { color, ...opts?.material },
15935
+ ...opts?.opacity !== void 0 ? { opacity: opts.opacity } : {}
15936
+ };
15937
+ }
15938
+ function axisRotation(from, to) {
15939
+ const wx = to[0] - from[0];
15940
+ const wy = to[2] - from[2];
15941
+ const wz = to[1] - from[1];
15942
+ const len = Math.sqrt(wx * wx + wy * wy + wz * wz);
15943
+ const tilt = Math.acos(Math.min(1, Math.max(-1, len > 0 ? wy / len : 1)));
15944
+ return [0, Math.atan2(wz, -wx), tilt];
15945
+ }
15946
+ function segmentLength(from, to) {
15947
+ const dx = to[0] - from[0];
15948
+ const dy = to[1] - from[1];
15949
+ const dz = to[2] - from[2];
15950
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
15951
+ }
15952
+ function cylinderBetween(from, to, radius, color) {
15953
+ const len = segmentLength(from, to);
15954
+ if (len < 1e-6) return null;
15955
+ return {
15956
+ type: "draw-mesh",
15957
+ shape: "cylinder",
15958
+ position: { x: (from[0] + to[0]) / 2, y: (from[1] + to[1]) / 2, z: (from[2] + to[2]) / 2 },
15959
+ radius,
15960
+ height: len,
15961
+ rotation: axisRotation(from, to),
15962
+ pivot: "center",
15963
+ segments: 12,
15964
+ material: { color }
15965
+ };
15966
+ }
15967
+ function arrowBetween(from, to, color, shaftRadius = 0.08, id) {
15968
+ const len = segmentLength(from, to);
15969
+ if (len < 1e-6) return null;
15970
+ const tipLen = Math.min(shaftRadius * 8, len * 0.35);
15971
+ const k = (len - tipLen) / len;
15972
+ const delta = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
15973
+ const shaftEnd = [delta[0] * k, delta[1] * k, delta[2] * k];
15974
+ const tipStart = shaftEnd;
15975
+ const tipEnd = delta;
15976
+ const shaft = cylinderBetween([0, 0, 0], shaftEnd, shaftRadius, color);
15977
+ const tipLenActual = segmentLength(tipStart, tipEnd);
15978
+ const tip = {
15979
+ type: "draw-mesh",
15980
+ shape: "cone",
15981
+ position: {
15982
+ x: (tipStart[0] + tipEnd[0]) / 2,
15983
+ y: (tipStart[1] + tipEnd[1]) / 2,
15984
+ z: (tipStart[2] + tipEnd[2]) / 2
15985
+ },
15986
+ radius: shaftRadius * 3,
15987
+ height: tipLenActual,
15988
+ rotation: axisRotation(tipStart, tipEnd),
15989
+ pivot: "center",
15990
+ segments: 12,
15991
+ material: { color }
15992
+ };
15993
+ return {
15994
+ type: "draw-group",
15995
+ ...id !== void 0 ? { id } : {},
15996
+ position: { x: from[0], y: from[1], z: from[2] },
15997
+ items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
15998
+ };
15999
+ }
16000
+ function billboardLabel(text, x, y, z, opts) {
16001
+ return {
16002
+ type: "draw-text",
16003
+ text,
16004
+ position: { x, y, z },
16005
+ color: opts?.color ?? "#111827"
16006
+ };
16007
+ }
16008
+ function labelColorForBackground(backgroundColor) {
16009
+ const m = /^#(?:([0-9a-f]{3})|([0-9a-f]{6}))$/i.exec(backgroundColor ?? "");
16010
+ if (!m) return "#111827";
16011
+ const hex = m[1] !== void 0 ? m[1].split("").map((c) => c + c).join("") : m[2];
16012
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
16013
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
16014
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
16015
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
16016
+ return luminance < 0.5 ? "#e5e7eb" : "#111827";
16017
+ }
16018
+ function get3DClickPayload(onShapeClick, idToIndex) {
16019
+ if (!onShapeClick) return void 0;
16020
+ return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
16021
+ }
16022
+ function polylineTube(points, radius, color, opts) {
16023
+ const maxSegments = opts?.maxSegments ?? 128;
16024
+ let pts = points;
16025
+ if (pts.length - 1 > maxSegments) {
16026
+ const step = (pts.length - 1) / maxSegments;
16027
+ const kept = [pts[0]];
16028
+ for (let s = 1; s < maxSegments; s++) kept.push(pts[Math.round(s * step)]);
16029
+ kept.push(pts[pts.length - 1]);
16030
+ pts = kept;
16031
+ }
16032
+ const out = [];
16033
+ for (let i = 0; i < pts.length - 1; i++) {
16034
+ const seg = cylinderBetween(pts[i], pts[i + 1], radius, color);
16035
+ if (seg) out.push(opts?.opacity !== void 0 ? { ...seg, opacity: opts.opacity } : seg);
16036
+ }
16037
+ return out;
16038
+ }
16039
+ function heightFieldMesh(spec) {
16040
+ const { nx, ny, heights, spacing = 1, x = 0, y = 0 } = spec;
16041
+ const flatShading = spec.flatShading ?? true;
16042
+ const vertices = [];
16043
+ for (let iy = 0; iy < ny; iy++) {
16044
+ for (let ix = 0; ix < nx; ix++) {
16045
+ vertices.push([
16046
+ x + (ix - (nx - 1) / 2) * spacing,
16047
+ y + (iy - (ny - 1) / 2) * spacing,
16048
+ heights[iy * nx + ix] ?? 0
16049
+ ]);
16050
+ }
16051
+ }
16052
+ const bands = [...spec.bands ?? []].sort((a, b) => (a.min ?? -Infinity) - (b.min ?? -Infinity));
16053
+ const facesByBand = /* @__PURE__ */ new Map();
16054
+ for (let iy = 0; iy < ny - 1; iy++) {
16055
+ for (let ix = 0; ix < nx - 1; ix++) {
16056
+ const v00 = iy * nx + ix;
16057
+ const v10 = iy * nx + ix + 1;
16058
+ const v01 = (iy + 1) * nx + ix;
16059
+ const v11 = (iy + 1) * nx + ix + 1;
16060
+ for (const face of [[v00, v10, v01], [v10, v11, v01]]) {
16061
+ const centroid = (vertices[face[0]][2] + vertices[face[1]][2] + vertices[face[2]][2]) / 3;
16062
+ let band = null;
16063
+ for (const b of bands) {
16064
+ if ((b.min ?? -Infinity) <= centroid) band = b;
16065
+ }
16066
+ const key = bands.length > 0 ? band : null;
16067
+ const list = facesByBand.get(key) ?? [];
16068
+ list.push(face);
16069
+ facesByBand.set(key, list);
16070
+ }
16071
+ }
16072
+ }
16073
+ const out = [];
16074
+ for (const [band, faces] of facesByBand) {
16075
+ if (faces.length === 0) continue;
16076
+ out.push({
16077
+ type: "draw-mesh",
16078
+ shape: "polyhedron",
16079
+ position: { x: 0, y: 0, z: 0 },
16080
+ vertices,
16081
+ faces,
16082
+ pivot: "center",
16083
+ material: { color: band?.color ?? spec.color ?? "#64748b", flatShading, side: "double" },
16084
+ ...spec.opacity !== void 0 ? { opacity: spec.opacity } : {}
16085
+ });
16086
+ }
16087
+ return out;
16088
+ }
16089
+ function arrowField(vectors, opts) {
16090
+ const scale = opts?.scale ?? 1;
16091
+ const out = [];
16092
+ for (const v of vectors) {
16093
+ const to = [
16094
+ v.from[0] + v.delta[0] * scale,
16095
+ v.from[1] + v.delta[1] * scale,
16096
+ v.from[2] + v.delta[2] * scale
16097
+ ];
16098
+ const arrow = arrowBetween(v.from, to, v.color ?? "#dc2626", v.width, v.id);
16099
+ if (arrow) out.push(arrow);
16100
+ if (v.label) out.push(billboardLabel(v.label, to[0], to[1], to[2], { color: opts?.labelColor }));
16101
+ }
16102
+ return out;
16103
+ }
16104
+ function helixDrawables(spec, opts) {
16105
+ const count = spec.count ?? spec.rungs?.length ?? 0;
16106
+ const rungs = Array.from({ length: count }, (_, i) => spec.rungs?.[i] ?? {});
16107
+ const radius = spec.radius ?? 1;
16108
+ const rise = spec.rise ?? 0.34;
16109
+ const twistRad = (spec.twistDeg ?? 36) * (Math.PI / 180);
16110
+ const strandAColor = spec.strandAColor ?? "#38bdf8";
16111
+ const strandBColor = spec.strandBColor ?? "#fb923c";
16112
+ const backboneRadius = spec.backboneRadius ?? 0.16;
16113
+ const rungRadius = spec.rungRadius ?? 0.12;
16114
+ const cx = spec.x ?? 0;
16115
+ const cy = spec.y ?? 0;
16116
+ const cz = spec.z ?? 0;
16117
+ const unwoundCount = spec.unwoundCount ?? 0;
16118
+ const unwindSpread = spec.unwindSpread ?? 1.8;
16119
+ const strandA = [];
16120
+ const strandB = [];
16121
+ for (let i = 0; i < count; i++) {
16122
+ const yi = cy + (i - (count - 1) / 2) * rise;
16123
+ const theta = i * twistRad;
16124
+ const s = i < unwoundCount ? unwindSpread : 1;
16125
+ strandA.push([cx + s * radius * Math.cos(theta), yi, cz + s * radius * Math.sin(theta)]);
16126
+ strandB.push([cx + s * radius * Math.cos(theta + Math.PI), yi, cz + s * radius * Math.sin(theta + Math.PI)]);
16127
+ }
16128
+ const out = [];
16129
+ for (let i = 0; i < count; i++) {
16130
+ out.push(meshSphere(`hx-a-${i}`, strandA[i][0], strandA[i][1], strandA[i][2], backboneRadius, strandAColor));
16131
+ out.push(meshSphere(`hx-b-${i}`, strandB[i][0], strandB[i][1], strandB[i][2], backboneRadius, strandBColor));
16132
+ if (i > 0) {
16133
+ const segA = cylinderBetween(strandA[i - 1], strandA[i], backboneRadius, strandAColor);
16134
+ if (segA) out.push(segA);
16135
+ const segB = cylinderBetween(strandB[i - 1], strandB[i], backboneRadius, strandBColor);
16136
+ if (segB) out.push(segB);
16137
+ }
16138
+ const rung = rungs[i];
16139
+ const rungColor = rung.color ?? "#94a3b8";
16140
+ const rod = cylinderBetween(strandA[i], strandB[i], rungRadius, rungColor);
16141
+ if (rod) out.push(rod);
16142
+ const mid = [
16143
+ (strandA[i][0] + strandB[i][0]) / 2,
16144
+ (strandA[i][1] + strandB[i][1]) / 2,
16145
+ (strandA[i][2] + strandB[i][2]) / 2
16146
+ ];
16147
+ const markerRadius = rung.radius ?? rungRadius;
16148
+ out.push(meshSphere(rung.id, mid[0], mid[1], mid[2], markerRadius, rungColor));
16149
+ if (rung.label) out.push(billboardLabel(rung.label, mid[0], mid[1], mid[2] + markerRadius, { color: opts?.labelColor }));
16150
+ }
16151
+ return out;
16152
+ }
16153
+ function latticeDrawables(spec, opts) {
16154
+ const nx = spec.nx ?? 2;
16155
+ const ny = spec.ny ?? 2;
16156
+ const nz = spec.nz ?? 2;
16157
+ const latticeConstant = spec.latticeConstant ?? 2;
16158
+ const bondRadius = spec.bondRadius ?? 0.06;
16159
+ const highlightCell = spec.highlightCell ?? false;
16160
+ const dimColor = spec.dimColor ?? "#475569";
16161
+ const showLabels = spec.showLabels ?? false;
16162
+ const selectedColor = spec.selectedColor ?? "#f59e0b";
16163
+ const posByKey = /* @__PURE__ */ new Map();
16164
+ const inCellByKey = /* @__PURE__ */ new Map();
16165
+ const out = [];
16166
+ for (const site of spec.basis) {
16167
+ const snx = site.xEdge ? nx + 1 : nx;
16168
+ const sny = site.yEdge ? ny + 1 : ny;
16169
+ const snz = site.zEdge ? nz + 1 : nz;
16170
+ for (let i = 0; i < snx; i++) {
16171
+ for (let j = 0; j < sny; j++) {
16172
+ for (let k = 0; k < snz; k++) {
16173
+ const key = `${site.key}-${i}-${j}-${k}`;
16174
+ const inCell = i + site.dx <= 1 && j + site.dy <= 1 && k + site.dz <= 1;
16175
+ const pos = [
16176
+ (i + site.dx) * latticeConstant - nx * latticeConstant / 2,
16177
+ (j + site.dy) * latticeConstant - ny * latticeConstant / 2,
16178
+ (k + site.dz) * latticeConstant - nz * latticeConstant / 2
16179
+ ];
16180
+ posByKey.set(key, pos);
16181
+ inCellByKey.set(key, inCell);
16182
+ const isSelected = spec.selectedId === `lat-${key}`;
16183
+ const color = isSelected ? selectedColor : highlightCell && !inCell ? dimColor : site.color ?? "#2563eb";
16184
+ const radius = (site.radius ?? 0.3) * (isSelected ? 1.4 : 1);
16185
+ out.push(meshSphere(`lat-${key}`, pos[0], pos[1], pos[2], radius, color));
16186
+ if (showLabels && site.element) {
16187
+ out.push(billboardLabel(site.element, pos[0], pos[1], pos[2] + radius, { color: opts?.labelColor }));
16188
+ }
16189
+ }
16190
+ }
16191
+ }
16192
+ }
16193
+ const basisByKey = new Map(spec.basis.map((s) => [s.key, s]));
16194
+ for (const bond of spec.bonds ?? []) {
16195
+ const fromSite = basisByKey.get(bond.from);
16196
+ const toSite = basisByKey.get(bond.to);
16197
+ if (!fromSite || !toSite) continue;
16198
+ const fnx = fromSite.xEdge ? nx + 1 : nx;
16199
+ const fny = fromSite.yEdge ? ny + 1 : ny;
16200
+ const fnz = fromSite.zEdge ? nz + 1 : nz;
16201
+ const tnx = toSite.xEdge ? nx + 1 : nx;
16202
+ const tny = toSite.yEdge ? ny + 1 : ny;
16203
+ const tnz = toSite.zEdge ? nz + 1 : nz;
16204
+ const bdx = bond.dx ?? 0;
16205
+ const bdy = bond.dy ?? 0;
16206
+ const bdz = bond.dz ?? 0;
16207
+ for (let i = 0; i < fnx; i++) {
16208
+ for (let j = 0; j < fny; j++) {
16209
+ for (let k = 0; k < fnz; k++) {
16210
+ const ti = i + bdx;
16211
+ const tj = j + bdy;
16212
+ const tk = k + bdz;
16213
+ if (ti < 0 || ti >= tnx || tj < 0 || tj >= tny || tk < 0 || tk >= tnz) continue;
16214
+ const fromKey = `${fromSite.key}-${i}-${j}-${k}`;
16215
+ const toKey = `${toSite.key}-${ti}-${tj}-${tk}`;
16216
+ const fromPos = posByKey.get(fromKey);
16217
+ const toPos = posByKey.get(toKey);
16218
+ if (!fromPos || !toPos) continue;
16219
+ const dimmed = highlightCell && !(inCellByKey.get(fromKey) && inCellByKey.get(toKey));
16220
+ const seg = cylinderBetween(fromPos, toPos, bondRadius, dimmed ? dimColor : bond.color ?? "#6b7280");
16221
+ if (seg) out.push(seg);
16222
+ }
16223
+ }
16224
+ }
16225
+ }
16226
+ return out;
16227
+ }
16228
+ var sceneLog, KNOWN_BLOOM_KEYS, Canvas3DHost2;
16229
+ var init_learningScene3D = __esm({
16230
+ "components/learning/molecules/learningScene3D.tsx"() {
16231
+ "use client";
16232
+ init_atoms();
16233
+ init_Stack();
16234
+ init_useEventBus();
16235
+ sceneLog = logger.createLogger("almadar:ui:learning-scene-3d");
16236
+ KNOWN_BLOOM_KEYS = /* @__PURE__ */ new Set(["intensity", "threshold", "smoothing"]);
16237
+ Canvas3DHost2 = React87.lazy(
16238
+ () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
16239
+ );
16240
+ LearningScene3D.displayName = "LearningScene3D";
16241
+ }
16242
+ });
16243
+ var biologyLog, BIO_BAND_COLORS, BIO_STAGE_FILL, BIO_STAGE_TEXT, BiologyCanvas;
14910
16244
  var init_BiologyCanvas = __esm({
14911
16245
  "components/learning/molecules/BiologyCanvas.tsx"() {
14912
16246
  "use client";
14913
16247
  init_atoms();
14914
16248
  init_Stack();
14915
16249
  init_LearningCanvas();
16250
+ init_learningScene3D();
16251
+ biologyLog = logger.createLogger("almadar:ui:biology-canvas");
16252
+ BIO_BAND_COLORS = ["#dcfce7", "#fef9c3", "#fee2e2", "#e0e7ff"];
16253
+ BIO_STAGE_FILL = {
16254
+ pending: "#e2e8f0",
16255
+ active: "#3b82f6",
16256
+ done: "#94a3b8"
16257
+ };
16258
+ BIO_STAGE_TEXT = {
16259
+ pending: "#64748b",
16260
+ active: "#ffffff",
16261
+ done: "#ffffff"
16262
+ };
14916
16263
  BiologyCanvas = ({
14917
16264
  className,
14918
16265
  width = 600,
14919
16266
  height = 400,
14920
16267
  title,
14921
16268
  backgroundColor,
16269
+ mode = "2d",
16270
+ camera,
16271
+ lighting,
16272
+ post,
14922
16273
  nodes = [],
14923
16274
  edges = [],
16275
+ compartments = [],
16276
+ bands = [],
16277
+ stages = [],
16278
+ stageStyle = "timeline",
16279
+ helix,
16280
+ helix3d,
14924
16281
  shapes = [],
14925
- interactive = false,
16282
+ readouts,
16283
+ traces,
16284
+ showGrid,
16285
+ shadows,
16286
+ interactive,
14926
16287
  animate = false,
14927
16288
  onShapeClick,
14928
16289
  isLoading,
@@ -14934,19 +16295,148 @@ var init_BiologyCanvas = __esm({
14934
16295
  for (const n of nodes) {
14935
16296
  if (n.id) nodeById.set(n.id, n);
14936
16297
  }
16298
+ const bandCount = bands.length;
16299
+ for (let i = 0; i < bandCount; i++) {
16300
+ const band = bands[i];
16301
+ const bandColor = band.color ?? BIO_BAND_COLORS[i % BIO_BAND_COLORS.length];
16302
+ const bandY = i * height / bandCount;
16303
+ const bandH = height / bandCount;
16304
+ out.push({
16305
+ type: "rect",
16306
+ x: 0,
16307
+ y: bandY,
16308
+ width,
16309
+ height: bandH,
16310
+ color: bandColor,
16311
+ fill: bandColor,
16312
+ opacity: 0.45
16313
+ });
16314
+ if (band.label) {
16315
+ out.push({
16316
+ type: "text",
16317
+ x: 8,
16318
+ y: bandY + 14,
16319
+ text: band.label,
16320
+ color: "#6b7280",
16321
+ fontSize: 10
16322
+ });
16323
+ }
16324
+ }
16325
+ for (const c of compartments) {
16326
+ const color = c.color ?? "#16a34a";
16327
+ out.push({
16328
+ type: "ellipse",
16329
+ x: c.x,
16330
+ y: c.y,
16331
+ width: c.width,
16332
+ height: c.height,
16333
+ color,
16334
+ fill: c.fill ?? `${color}1A`,
16335
+ lineWidth: c.lineWidth ?? 2,
16336
+ ...c.dash ? { dash: c.dash } : {}
16337
+ });
16338
+ if (c.label) {
16339
+ out.push({
16340
+ type: "text",
16341
+ x: c.x,
16342
+ y: c.y - c.height / 2 + 14,
16343
+ text: c.label,
16344
+ color: "#111827",
16345
+ fontSize: 11,
16346
+ align: "center"
16347
+ });
16348
+ }
16349
+ }
16350
+ if (helix) {
16351
+ const hx = helix.x ?? 24;
16352
+ const hy = helix.y ?? height * 0.25;
16353
+ const hw = helix.width ?? width - 48;
16354
+ const hh = helix.height ?? height * 0.5;
16355
+ const rungs = helix.rungs;
16356
+ const n = rungs.length;
16357
+ const cy = hy + hh / 2;
16358
+ const colorA = helix.colorA ?? "#2563eb";
16359
+ const colorB = helix.colorB ?? "#dc2626";
16360
+ const rungColor = helix.rungColor ?? "#94a3b8";
16361
+ const fork = helix.fork ?? 0;
16362
+ const maxSep = Math.min(hh - 8, 96);
16363
+ const strandA = [];
16364
+ const strandB = [];
16365
+ const rungGeoms = [];
16366
+ for (let i = 0; i < n; i++) {
16367
+ const rx = hx + (i + 0.5) * hw / n;
16368
+ const t = (i + 0.5) / n;
16369
+ const paired = t >= fork;
16370
+ const sep = paired ? 28 : 28 + (maxSep - 28) * ((fork - t) / fork);
16371
+ strandA.push({ x: rx, y: cy - sep / 2 });
16372
+ strandB.push({ x: rx, y: cy + sep / 2 });
16373
+ rungGeoms.push({ rx, sep, rung: rungs[i], paired });
16374
+ }
16375
+ for (let i = 1; i < n; i++) {
16376
+ 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 });
16377
+ }
16378
+ for (let i = 1; i < n; i++) {
16379
+ 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 });
16380
+ }
16381
+ for (const g of rungGeoms) {
16382
+ const rColor = g.rung.color ?? (g.rung.state === "new" ? "#16a34a" : rungColor);
16383
+ const topY = cy - g.sep / 2;
16384
+ const bottomY = cy + g.sep / 2;
16385
+ if (g.paired) {
16386
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: bottomY, color: rColor });
16387
+ if (g.rung.a) {
16388
+ out.push({ type: "text", x: g.rx, y: cy - g.sep / 4, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
16389
+ }
16390
+ if (g.rung.b) {
16391
+ out.push({ type: "text", x: g.rx, y: cy + g.sep / 4, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16392
+ }
16393
+ } else {
16394
+ const stubTopY = topY + 8;
16395
+ const stubBottomY = bottomY - 8;
16396
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: stubTopY, color: rColor });
16397
+ out.push({ type: "line", x1: g.rx, y1: bottomY, x2: g.rx, y2: stubBottomY, color: rColor });
16398
+ if (g.rung.a) {
16399
+ out.push({ type: "text", x: g.rx, y: stubTopY + 6, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
16400
+ }
16401
+ if (g.rung.b) {
16402
+ out.push({ type: "text", x: g.rx, y: stubBottomY - 6, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16403
+ }
16404
+ }
16405
+ }
16406
+ }
14937
16407
  for (const e of edges) {
14938
16408
  const a = nodeById.get(e.from);
14939
16409
  const b = nodeById.get(e.to);
14940
16410
  if (!a || !b) continue;
14941
- out.push({
14942
- type: "line",
14943
- x1: a.x,
14944
- y1: a.y,
14945
- x2: b.x,
14946
- y2: b.y,
14947
- color: e.color ?? "#9ca3af",
14948
- lineWidth: 2
14949
- });
16411
+ const color = e.color ?? "#9ca3af";
16412
+ if (e.directed) {
16413
+ const rA = a.radius ?? 16;
16414
+ const rB = b.radius ?? 16;
16415
+ const dx = b.x - a.x;
16416
+ const dy = b.y - a.y;
16417
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16418
+ const ux = dx / dist;
16419
+ const uy = dy / dist;
16420
+ out.push({
16421
+ type: "arrow",
16422
+ x1: a.x + ux * rA,
16423
+ y1: a.y + uy * rA,
16424
+ x2: b.x - ux * rB,
16425
+ y2: b.y - uy * rB,
16426
+ color,
16427
+ lineWidth: 2
16428
+ });
16429
+ } else {
16430
+ out.push({
16431
+ type: "line",
16432
+ x1: a.x,
16433
+ y1: a.y,
16434
+ x2: b.x,
16435
+ y2: b.y,
16436
+ color,
16437
+ lineWidth: 2
16438
+ });
16439
+ }
14950
16440
  if (e.label) {
14951
16441
  out.push({
14952
16442
  type: "text",
@@ -14959,6 +16449,8 @@ var init_BiologyCanvas = __esm({
14959
16449
  }
14960
16450
  }
14961
16451
  for (const n of nodes) {
16452
+ const state = n.state ?? "default";
16453
+ const muted = state === "muted";
14962
16454
  out.push({
14963
16455
  type: "circle",
14964
16456
  x: n.x,
@@ -14966,23 +16458,197 @@ var init_BiologyCanvas = __esm({
14966
16458
  radius: n.radius ?? 16,
14967
16459
  color: n.color ?? "#16a34a",
14968
16460
  fill: `${n.color ?? "#16a34a"}33`,
14969
- id: n.id
16461
+ id: n.id,
16462
+ ...muted ? { opacity: 0.35 } : {}
14970
16463
  });
16464
+ if (state === "highlight") {
16465
+ out.push({
16466
+ type: "circle",
16467
+ x: n.x,
16468
+ y: n.y,
16469
+ radius: (n.radius ?? 16) + 4,
16470
+ color: "#f59e0b",
16471
+ lineWidth: 2
16472
+ });
16473
+ }
14971
16474
  if (n.label) {
14972
16475
  out.push({
14973
16476
  type: "text",
14974
16477
  x: n.x,
14975
16478
  y: n.y + (n.radius ?? 16) + 14,
14976
16479
  text: n.label,
16480
+ ...muted ? { opacity: 0.35 } : {},
14977
16481
  color: "#111827",
14978
16482
  fontSize: 12,
14979
16483
  align: "center"
14980
16484
  });
14981
16485
  }
14982
16486
  }
16487
+ const stageCount = stages.length;
16488
+ if (stageCount > 0) {
16489
+ if (stageStyle === "ring") {
16490
+ const cx = width / 2;
16491
+ const cy = height / 2;
16492
+ const R = Math.min(width, height) / 2 - 48;
16493
+ const ringPoints = [];
16494
+ for (let i = 0; i < stageCount; i++) {
16495
+ const angleRad = (-90 + 360 * i / stageCount) * Math.PI / 180;
16496
+ ringPoints.push({ x: cx + R * Math.cos(angleRad), y: cy + R * Math.sin(angleRad) });
16497
+ }
16498
+ if (stageCount >= 2) {
16499
+ for (let i = 0; i < stageCount - 1; i++) {
16500
+ const p1 = ringPoints[i];
16501
+ const p2 = ringPoints[i + 1];
16502
+ const dx = p2.x - p1.x;
16503
+ const dy = p2.y - p1.y;
16504
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16505
+ const ux = dx / dist;
16506
+ const uy = dy / dist;
16507
+ out.push({
16508
+ type: "arrow",
16509
+ x1: p1.x + ux * 46,
16510
+ y1: p1.y + uy * 46,
16511
+ x2: p2.x - ux * 46,
16512
+ y2: p2.y - uy * 46,
16513
+ color: "#94a3b8"
16514
+ });
16515
+ }
16516
+ }
16517
+ for (let i = 0; i < stageCount; i++) {
16518
+ const stage = stages[i];
16519
+ const state = stage.state ?? "pending";
16520
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16521
+ const w = Math.max(26, Math.min(84, stage.label.length * 6 + 10));
16522
+ const h = 18;
16523
+ const p = ringPoints[i];
16524
+ out.push({ type: "rect", x: p.x - w / 2, y: p.y - h / 2, width: w, height: h, color: fill, fill });
16525
+ out.push({ type: "text", x: p.x, y: p.y, text: stage.label, color: BIO_STAGE_TEXT[state], fontSize: 10, align: "center" });
16526
+ }
16527
+ } else {
16528
+ const stripY = height - 32;
16529
+ const slotW = (width - 16) / stageCount;
16530
+ const chipGeoms = [];
16531
+ for (let i = 0; i < stageCount; i++) {
16532
+ chipGeoms.push({ x: 8 + i * slotW + 5, w: slotW - 10 });
16533
+ }
16534
+ for (let i = 0; i < stageCount - 1; i++) {
16535
+ const midY = stripY + 13;
16536
+ out.push({
16537
+ type: "arrow",
16538
+ x1: chipGeoms[i].x + chipGeoms[i].w,
16539
+ y1: midY,
16540
+ x2: chipGeoms[i + 1].x,
16541
+ y2: midY,
16542
+ color: "#94a3b8"
16543
+ });
16544
+ }
16545
+ for (let i = 0; i < stageCount; i++) {
16546
+ const stage = stages[i];
16547
+ const state = stage.state ?? "pending";
16548
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16549
+ const g = chipGeoms[i];
16550
+ out.push({ type: "rect", x: g.x, y: stripY, width: g.w, height: 26, color: fill, fill });
16551
+ out.push({
16552
+ type: "text",
16553
+ x: g.x + g.w / 2,
16554
+ y: stripY + 13,
16555
+ text: stage.label,
16556
+ color: BIO_STAGE_TEXT[state],
16557
+ fontSize: 10,
16558
+ align: "center"
16559
+ });
16560
+ }
16561
+ }
16562
+ }
14983
16563
  out.push(...shapes);
14984
16564
  return out;
14985
- }, [nodes, edges, shapes]);
16565
+ }, [nodes, edges, compartments, bands, stages, stageStyle, helix, shapes, width, height]);
16566
+ const drawables3D = React87.useMemo(() => {
16567
+ if (mode !== "3d") return [];
16568
+ if (shapes.length > 0) {
16569
+ biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
16570
+ }
16571
+ if (compartments.length > 0 || bands.length > 0 || stages.length > 0 || helix) {
16572
+ biologyLog.debug("2D-only families ignored in 3D mode (pixel-authored 2D vocabulary)", {
16573
+ compartments: compartments.length,
16574
+ bands: bands.length,
16575
+ stages: stages.length,
16576
+ helix: helix != null
16577
+ });
16578
+ }
16579
+ if (animate) {
16580
+ biologyLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
16581
+ }
16582
+ const out = [];
16583
+ const labelColor = labelColorForBackground(backgroundColor);
16584
+ const nodeById = /* @__PURE__ */ new Map();
16585
+ for (const n of nodes) {
16586
+ if (n.id) nodeById.set(n.id, n);
16587
+ }
16588
+ for (const e of edges) {
16589
+ const a = nodeById.get(e.from);
16590
+ const b = nodeById.get(e.to);
16591
+ if (!a || !b) continue;
16592
+ const edgeRadius = Math.max(0.04, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.12);
16593
+ const edge = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], edgeRadius, e.color ?? "#9ca3af");
16594
+ if (edge) out.push(edge);
16595
+ if (e.label) {
16596
+ out.push(
16597
+ billboardLabel(
16598
+ e.label,
16599
+ (a.x + b.x) / 2,
16600
+ (a.y + b.y) / 2,
16601
+ ((a.z ?? 0) + (b.z ?? 0)) / 2,
16602
+ { color: labelColor }
16603
+ )
16604
+ );
16605
+ }
16606
+ }
16607
+ for (const n of nodes) {
16608
+ const radius = n.radius ?? 0.5;
16609
+ const nz = n.z ?? 0;
16610
+ out.push(meshSphere(n.id, n.x, n.y, nz, radius, n.color ?? "#16a34a", { shape: n.shape, ...n.opacity !== void 0 ? { opacity: n.opacity } : {} }));
16611
+ if (n.label) {
16612
+ out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
16613
+ }
16614
+ }
16615
+ if (helix3d) {
16616
+ out.push(...helixDrawables(helix3d, { labelColor }));
16617
+ }
16618
+ return out;
16619
+ }, [mode, nodes, edges, shapes, compartments, bands, stages, helix, helix3d, animate, backgroundColor]);
16620
+ const nodeIndexById = React87.useMemo(() => {
16621
+ const m = /* @__PURE__ */ new Map();
16622
+ (helix3d?.rungs ?? []).forEach((rung, i) => {
16623
+ if (rung.id) m.set(rung.id, i);
16624
+ });
16625
+ nodes.forEach((n, i) => {
16626
+ if (n.id) m.set(n.id, i);
16627
+ });
16628
+ return m;
16629
+ }, [nodes, helix3d]);
16630
+ if (mode === "3d") {
16631
+ return /* @__PURE__ */ jsxRuntime.jsx(
16632
+ LearningScene3D,
16633
+ {
16634
+ className,
16635
+ width,
16636
+ height,
16637
+ title,
16638
+ backgroundColor,
16639
+ drawables: drawables3D,
16640
+ camera,
16641
+ lighting,
16642
+ post,
16643
+ showGrid,
16644
+ shadows,
16645
+ interactive,
16646
+ isLoading,
16647
+ error,
16648
+ onItemClick: get3DClickPayload(onShapeClick, nodeIndexById)
16649
+ }
16650
+ );
16651
+ }
14986
16652
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
14987
16653
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
14988
16654
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -14992,7 +16658,9 @@ var init_BiologyCanvas = __esm({
14992
16658
  height,
14993
16659
  backgroundColor,
14994
16660
  shapes: derivedShapes,
14995
- interactive,
16661
+ readouts,
16662
+ traces,
16663
+ interactive: interactive ?? false,
14996
16664
  animate,
14997
16665
  onShapeClick,
14998
16666
  isLoading,
@@ -21832,24 +23500,55 @@ var init_ChatBar = __esm({
21832
23500
  ChatBar.displayName = "ChatBar";
21833
23501
  }
21834
23502
  });
21835
- var ChemistryCanvas;
23503
+ function bondPerpendicular(a, b) {
23504
+ const dx = b[0] - a[0];
23505
+ const dy = b[1] - a[1];
23506
+ const px = dy;
23507
+ const py = -dx;
23508
+ const len = Math.sqrt(px * px + py * py);
23509
+ if (len < 1e-6) return [1, 0, 0];
23510
+ return [px / len, py / len, 0];
23511
+ }
23512
+ var chemistryLog, CHEM_BOND_STATE_COLOR, LONE_PAIR_ANGLES, ChemistryCanvas;
21836
23513
  var init_ChemistryCanvas = __esm({
21837
23514
  "components/learning/molecules/ChemistryCanvas.tsx"() {
21838
23515
  "use client";
21839
23516
  init_atoms();
21840
23517
  init_Stack();
21841
23518
  init_LearningCanvas();
23519
+ init_learningScene3D();
23520
+ chemistryLog = logger.createLogger("almadar:ui:chemistry-canvas");
23521
+ CHEM_BOND_STATE_COLOR = {
23522
+ default: "#6b7280",
23523
+ forming: "#16a34a",
23524
+ breaking: "#dc2626",
23525
+ highlight: "#f59e0b"
23526
+ };
23527
+ LONE_PAIR_ANGLES = [-90, 0, 90, 180];
21842
23528
  ChemistryCanvas = ({
21843
23529
  className,
21844
23530
  width = 600,
21845
23531
  height = 400,
21846
23532
  title,
21847
23533
  backgroundColor,
23534
+ mode = "2d",
23535
+ camera,
23536
+ lighting,
23537
+ post,
21848
23538
  atoms = [],
21849
23539
  bonds = [],
21850
23540
  arrows = [],
23541
+ bondStyle = "thick",
23542
+ containers = [],
23543
+ equation,
23544
+ equationColor,
23545
+ lattice3d,
21851
23546
  shapes = [],
21852
- interactive = false,
23547
+ readouts,
23548
+ traces,
23549
+ showGrid,
23550
+ shadows,
23551
+ interactive,
21853
23552
  animate = false,
21854
23553
  onShapeClick,
21855
23554
  isLoading,
@@ -21861,21 +23560,118 @@ var init_ChemistryCanvas = __esm({
21861
23560
  for (const a of atoms) {
21862
23561
  if (a.id) atomById.set(a.id, a);
21863
23562
  }
23563
+ for (const c of containers) {
23564
+ const color = c.color ?? "#64748b";
23565
+ if (c.level != null) {
23566
+ const lv = c.level;
23567
+ out.push({
23568
+ type: "rect",
23569
+ x: c.x + 1,
23570
+ y: c.y + c.height * (1 - lv),
23571
+ width: c.width - 2,
23572
+ height: c.height * lv - 1,
23573
+ color: c.levelColor ?? "#60a5fa",
23574
+ fill: c.levelColor ?? "#60a5fa",
23575
+ opacity: 0.5
23576
+ });
23577
+ }
23578
+ out.push({
23579
+ type: "rect",
23580
+ x: c.x,
23581
+ y: c.y,
23582
+ width: c.width,
23583
+ height: c.height,
23584
+ color,
23585
+ fill: c.fill,
23586
+ lineWidth: c.lineWidth ?? 2
23587
+ });
23588
+ const divider = c.divider ?? "none";
23589
+ if (divider !== "none") {
23590
+ out.push({
23591
+ type: "line",
23592
+ x1: c.x + c.width / 2,
23593
+ y1: c.y,
23594
+ x2: c.x + c.width / 2,
23595
+ y2: c.y + c.height,
23596
+ color: c.dividerColor ?? color,
23597
+ ...divider === "dashed" || divider === "dotted" ? { dash: divider } : {}
23598
+ });
23599
+ }
23600
+ if (c.leftLabel) {
23601
+ out.push({
23602
+ type: "text",
23603
+ x: c.x + c.width * 0.25,
23604
+ y: c.y + 12,
23605
+ text: c.leftLabel,
23606
+ color: "#374151",
23607
+ fontSize: 11,
23608
+ align: "center"
23609
+ });
23610
+ }
23611
+ if (c.rightLabel) {
23612
+ out.push({
23613
+ type: "text",
23614
+ x: c.x + c.width * 0.75,
23615
+ y: c.y + 12,
23616
+ text: c.rightLabel,
23617
+ color: "#374151",
23618
+ fontSize: 11,
23619
+ align: "center"
23620
+ });
23621
+ }
23622
+ if (c.label) {
23623
+ out.push({
23624
+ type: "text",
23625
+ x: c.x + c.width / 2,
23626
+ y: c.y + c.height + 12,
23627
+ text: c.label,
23628
+ color: "#111827",
23629
+ fontSize: 12,
23630
+ align: "center"
23631
+ });
23632
+ }
23633
+ }
21864
23634
  for (const b of bonds) {
21865
23635
  const a = atomById.get(b.from);
21866
23636
  const c = atomById.get(b.to);
21867
23637
  if (!a || !c) continue;
21868
- const color = b.color ?? "#6b7280";
21869
- const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
21870
- out.push({
21871
- type: "line",
21872
- x1: a.x,
21873
- y1: a.y,
21874
- x2: c.x,
21875
- y2: c.y,
21876
- color,
21877
- lineWidth: strokeWidth
21878
- });
23638
+ const state = b.state ?? "default";
23639
+ const color = b.color ?? CHEM_BOND_STATE_COLOR[state];
23640
+ const dash = state === "forming" || state === "breaking" ? "dashed" : void 0;
23641
+ if (bondStyle === "parallel") {
23642
+ const dx = c.x - a.x;
23643
+ const dy = c.y - a.y;
23644
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
23645
+ const ux = dx / dist;
23646
+ const uy = dy / dist;
23647
+ const px = -uy;
23648
+ const py = ux;
23649
+ const offsets = b.type === "double" ? [-3, 3] : b.type === "triple" ? [-4, 0, 4] : [0];
23650
+ for (const off of offsets) {
23651
+ out.push({
23652
+ type: "line",
23653
+ x1: a.x + px * off,
23654
+ y1: a.y + py * off,
23655
+ x2: c.x + px * off,
23656
+ y2: c.y + py * off,
23657
+ color,
23658
+ lineWidth: 2,
23659
+ ...dash ? { dash } : {}
23660
+ });
23661
+ }
23662
+ } else {
23663
+ const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
23664
+ out.push({
23665
+ type: "line",
23666
+ x1: a.x,
23667
+ y1: a.y,
23668
+ x2: c.x,
23669
+ y2: c.y,
23670
+ color,
23671
+ lineWidth: strokeWidth,
23672
+ ...dash ? { dash } : {}
23673
+ });
23674
+ }
21879
23675
  }
21880
23676
  for (const a of arrows) {
21881
23677
  const angle = (a.angle ?? 0) * (Math.PI / 180);
@@ -21924,10 +23720,142 @@ var init_ChemistryCanvas = __esm({
21924
23720
  align: "center"
21925
23721
  });
21926
23722
  }
23723
+ const r = a.radius ?? 14;
23724
+ if (a.charge) {
23725
+ out.push({
23726
+ type: "text",
23727
+ x: a.x + r * 0.85,
23728
+ y: a.y - r * 0.85,
23729
+ text: a.charge,
23730
+ color: "#111827",
23731
+ fontSize: 9,
23732
+ align: "left"
23733
+ });
23734
+ }
23735
+ const lonePairs = Math.max(0, Math.min(4, a.lonePairs ?? 0));
23736
+ for (let k = 0; k < lonePairs; k++) {
23737
+ const angleRad = LONE_PAIR_ANGLES[k] * Math.PI / 180;
23738
+ const cx = a.x + (r + 6) * Math.cos(angleRad);
23739
+ const cy = a.y + (r + 6) * Math.sin(angleRad);
23740
+ const perpX = -Math.sin(angleRad);
23741
+ const perpY = Math.cos(angleRad);
23742
+ for (const sign of [1, -1]) {
23743
+ out.push({
23744
+ type: "circle",
23745
+ x: cx + perpX * 2.5 * sign,
23746
+ y: cy + perpY * 2.5 * sign,
23747
+ radius: 1.5,
23748
+ color: "#374151",
23749
+ fill: "#374151"
23750
+ });
23751
+ }
23752
+ }
23753
+ }
23754
+ if (equation) {
23755
+ out.push({
23756
+ type: "text",
23757
+ x: width / 2,
23758
+ y: 14,
23759
+ text: equation,
23760
+ color: equationColor ?? "#111827",
23761
+ fontSize: 13,
23762
+ align: "center"
23763
+ });
21927
23764
  }
21928
23765
  out.push(...shapes);
21929
23766
  return out;
21930
- }, [atoms, bonds, arrows, shapes]);
23767
+ }, [atoms, bonds, arrows, bondStyle, containers, equation, equationColor, shapes, width]);
23768
+ const drawables3D = React87.useMemo(() => {
23769
+ if (mode !== "3d") return [];
23770
+ if (shapes.length > 0) {
23771
+ chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
23772
+ }
23773
+ if (containers.length > 0) {
23774
+ chemistryLog.debug("containers ignored in 3D mode (pixel-authored 2D vocabulary)", { count: containers.length });
23775
+ }
23776
+ if (animate) {
23777
+ chemistryLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
23778
+ }
23779
+ const out = [];
23780
+ const labelColor = labelColorForBackground(backgroundColor);
23781
+ const atomById = /* @__PURE__ */ new Map();
23782
+ for (const a of atoms) {
23783
+ if (a.id) atomById.set(a.id, a);
23784
+ }
23785
+ for (const b of bonds) {
23786
+ const a = atomById.get(b.from);
23787
+ const c = atomById.get(b.to);
23788
+ if (!a || !c) continue;
23789
+ const color = b.color ?? "#6b7280";
23790
+ const from = [a.x, a.y, a.z ?? 0];
23791
+ const to = [c.x, c.y, c.z ?? 0];
23792
+ const perp = bondPerpendicular(from, to);
23793
+ const bondRadius = Math.max(0.06, Math.min(a.radius ?? 0.45, c.radius ?? 0.45) * 0.22);
23794
+ const step = bondRadius * 2.2;
23795
+ const offsets = b.type === "double" ? [-step, step] : b.type === "triple" ? [-step, 0, step] : [0];
23796
+ for (const off of offsets) {
23797
+ const bond = cylinderBetween(
23798
+ [from[0] + perp[0] * off, from[1] + perp[1] * off, from[2] + perp[2] * off],
23799
+ [to[0] + perp[0] * off, to[1] + perp[1] * off, to[2] + perp[2] * off],
23800
+ bondRadius,
23801
+ color
23802
+ );
23803
+ if (bond) out.push(bond);
23804
+ }
23805
+ }
23806
+ for (const a of arrows) {
23807
+ const angle = (a.angle ?? 0) * (Math.PI / 180);
23808
+ const len = a.length ?? 60;
23809
+ const x2 = a.x + Math.cos(angle) * len;
23810
+ const y2 = a.y + Math.sin(angle) * len;
23811
+ const arrow = arrowBetween([a.x, a.y, 0], [x2, y2, 0], a.color ?? "#dc2626");
23812
+ if (arrow) out.push(arrow);
23813
+ if (a.label) {
23814
+ out.push(billboardLabel(a.label, (a.x + x2) / 2, (a.y + y2) / 2, 0, { color: labelColor }));
23815
+ }
23816
+ }
23817
+ for (const a of atoms) {
23818
+ const radius = a.radius ?? 0.45;
23819
+ const az = a.z ?? 0;
23820
+ out.push(meshSphere(a.id, a.x, a.y, az, radius, a.color ?? "#2563eb"));
23821
+ if (a.element) {
23822
+ out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
23823
+ }
23824
+ }
23825
+ if (lattice3d) {
23826
+ out.push(...latticeDrawables(lattice3d, { labelColor }));
23827
+ }
23828
+ return out;
23829
+ }, [mode, atoms, bonds, arrows, shapes, containers, lattice3d, animate, backgroundColor]);
23830
+ const atomIndexById = React87.useMemo(() => {
23831
+ const m = /* @__PURE__ */ new Map();
23832
+ atoms.forEach((a, i) => {
23833
+ if (a.id) m.set(a.id, i);
23834
+ });
23835
+ return m;
23836
+ }, [atoms]);
23837
+ if (mode === "3d") {
23838
+ return /* @__PURE__ */ jsxRuntime.jsx(
23839
+ LearningScene3D,
23840
+ {
23841
+ className,
23842
+ width,
23843
+ height,
23844
+ title,
23845
+ backgroundColor,
23846
+ drawables: drawables3D,
23847
+ camera,
23848
+ lighting,
23849
+ post,
23850
+ showGrid,
23851
+ shadows,
23852
+ interactive,
23853
+ isLoading,
23854
+ error,
23855
+ onItemClick: get3DClickPayload(onShapeClick, atomIndexById)
23856
+ }
23857
+ );
23858
+ }
21931
23859
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
21932
23860
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
21933
23861
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -21937,7 +23865,9 @@ var init_ChemistryCanvas = __esm({
21937
23865
  height,
21938
23866
  backgroundColor,
21939
23867
  shapes: derivedShapes,
21940
- interactive,
23868
+ readouts,
23869
+ traces,
23870
+ interactive: interactive ?? false,
21941
23871
  animate,
21942
23872
  onShapeClick,
21943
23873
  isLoading,
@@ -28126,6 +30056,10 @@ var init_ProgressDots = __esm({
28126
30056
  ProgressDots.displayName = "ProgressDots";
28127
30057
  }
28128
30058
  });
30059
+ function formatTick(v) {
30060
+ if (Number.isInteger(v)) return String(v);
30061
+ return v.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
30062
+ }
28129
30063
  var MathCanvas;
28130
30064
  var init_MathCanvas = __esm({
28131
30065
  "components/learning/molecules/MathCanvas.tsx"() {
@@ -28145,10 +30079,19 @@ var init_MathCanvas = __esm({
28145
30079
  showAxes = true,
28146
30080
  showGrid = true,
28147
30081
  gridStep = 1,
30082
+ showTickLabels = false,
30083
+ showCurveLabels = false,
28148
30084
  curves = [],
28149
30085
  points = [],
28150
30086
  vectors = [],
30087
+ regions = [],
30088
+ bars = [],
30089
+ guides = [],
30090
+ angles = [],
30091
+ hops = [],
28151
30092
  shapes = [],
30093
+ readouts,
30094
+ traces,
28152
30095
  interactive = false,
28153
30096
  animate = false,
28154
30097
  onShapeClick,
@@ -28162,6 +30105,8 @@ var init_MathCanvas = __esm({
28162
30105
  const plotH = height - margin * 2;
28163
30106
  const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
28164
30107
  const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
30108
+ const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
30109
+ const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
28165
30110
  if (showGrid) {
28166
30111
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep) {
28167
30112
  const px = mapX(x);
@@ -28172,14 +30117,99 @@ var init_MathCanvas = __esm({
28172
30117
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color: "#e5e7eb", lineWidth: 1 });
28173
30118
  }
28174
30119
  }
30120
+ if (showTickLabels) {
30121
+ const labelEveryX = Math.max(1, Math.ceil((xMax - xMin) / gridStep / Math.floor(plotW / 40)));
30122
+ let kx = 0;
30123
+ for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
30124
+ if (kx % labelEveryX === 0 && x !== 0) {
30125
+ out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: 10, align: "center" });
30126
+ }
30127
+ }
30128
+ const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
30129
+ let ky = 0;
30130
+ for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
30131
+ if (ky % labelEveryY === 0 && y !== 0) {
30132
+ out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: 10, align: "right" });
30133
+ }
30134
+ }
30135
+ if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
30136
+ out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: 10, align: "right" });
30137
+ }
30138
+ }
30139
+ for (const region of regions) {
30140
+ if (!region.samples || region.samples.length === 0) continue;
30141
+ const baseline = region.baseline ?? 0;
30142
+ const clampedPoint = (p) => ({
30143
+ x: mapX(Math.min(xMax, Math.max(xMin, p.x))),
30144
+ y: mapY(Math.min(yMax, Math.max(yMin, p.y)))
30145
+ });
30146
+ const upper = region.samples.map(clampedPoint);
30147
+ const first = region.samples[0];
30148
+ const last = region.samples[region.samples.length - 1];
30149
+ 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 })];
30150
+ const color = region.color ?? "#2563eb";
30151
+ out.push({
30152
+ type: "polygon",
30153
+ points: [...upper, ...closing],
30154
+ fill: color,
30155
+ color,
30156
+ opacity: region.opacity ?? 0.2,
30157
+ lineWidth: 1
30158
+ });
30159
+ if (region.label) {
30160
+ const mid = Math.floor(region.samples.length / 2);
30161
+ out.push({
30162
+ type: "text",
30163
+ x: mapX((first.x + last.x) / 2),
30164
+ y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
30165
+ text: region.label,
30166
+ color: "#111827",
30167
+ fontSize: 11
30168
+ });
30169
+ }
30170
+ }
30171
+ for (const bar of bars) {
30172
+ if (bar.x + bar.width < xMin || bar.x > xMax) continue;
30173
+ const y0 = bar.y0 ?? 0;
30174
+ const color = bar.color ?? "#93c5fd";
30175
+ out.push({
30176
+ type: "rect",
30177
+ x: mapX(bar.x),
30178
+ y: mapY(Math.max(y0, bar.y1)),
30179
+ width: mapX(bar.x + bar.width) - mapX(bar.x),
30180
+ height: Math.abs(mapY(bar.y1) - mapY(y0)),
30181
+ color,
30182
+ fill: color,
30183
+ opacity: bar.opacity ?? 0.5,
30184
+ lineWidth: 1
30185
+ });
30186
+ }
28175
30187
  if (showAxes) {
28176
- const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
28177
- const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
28178
30188
  out.push({ type: "line", x1: margin, y1: xAxisY, x2: width - margin, y2: xAxisY, color: "#374151", lineWidth: 2 });
28179
30189
  out.push({ type: "line", x1: yAxisX, y1: margin, x2: yAxisX, y2: height - margin, color: "#374151", lineWidth: 2 });
28180
30190
  }
30191
+ for (const guide of guides) {
30192
+ const color = guide.color ?? "#9ca3af";
30193
+ const dash = guide.dash ?? "dashed";
30194
+ if (guide.kind === "vline") {
30195
+ if (guide.at < xMin || guide.at > xMax) continue;
30196
+ const px = mapX(guide.at);
30197
+ out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
30198
+ if (guide.label) {
30199
+ out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color: "#111827", fontSize: 11 });
30200
+ }
30201
+ } else {
30202
+ if (guide.at < yMin || guide.at > yMax) continue;
30203
+ const py = mapY(guide.at);
30204
+ out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
30205
+ if (guide.label) {
30206
+ out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color: "#111827", fontSize: 11, align: "right" });
30207
+ }
30208
+ }
30209
+ }
28181
30210
  for (const curve of curves) {
28182
30211
  if (!curve.samples || curve.samples.length < 2) continue;
30212
+ let lastInRange;
28183
30213
  for (let i = 1; i < curve.samples.length; i++) {
28184
30214
  const a = curve.samples[i - 1];
28185
30215
  const b = curve.samples[i];
@@ -28191,19 +30221,97 @@ var init_MathCanvas = __esm({
28191
30221
  x2: mapX(b.x),
28192
30222
  y2: mapY(b.y),
28193
30223
  color: curve.color ?? "#2563eb",
28194
- lineWidth: 2
30224
+ lineWidth: 2,
30225
+ dash: curve.dash
30226
+ });
30227
+ lastInRange = b;
30228
+ }
30229
+ if (showCurveLabels && curve.label && lastInRange) {
30230
+ out.push({
30231
+ type: "text",
30232
+ x: mapX(lastInRange.x) + 6,
30233
+ y: mapY(lastInRange.y) - 6,
30234
+ text: curve.label,
30235
+ color: curve.color ?? "#2563eb",
30236
+ fontSize: 11
30237
+ });
30238
+ }
30239
+ }
30240
+ for (const hop of hops) {
30241
+ const x1 = mapX(hop.from);
30242
+ const x2 = mapX(hop.to);
30243
+ const peak = Math.min(36, plotH * 0.3);
30244
+ const color = hop.color ?? "#7c3aed";
30245
+ out.push({
30246
+ type: "ellipse",
30247
+ x: (x1 + x2) / 2,
30248
+ y: xAxisY,
30249
+ width: Math.abs(x2 - x1),
30250
+ height: 2 * peak,
30251
+ startAngle: 180,
30252
+ endAngle: 360,
30253
+ color
30254
+ });
30255
+ const s = Math.sign(hop.to - hop.from);
30256
+ out.push({
30257
+ type: "polygon",
30258
+ points: [
30259
+ { x: x2, y: xAxisY },
30260
+ { x: x2 - 4 * s, y: xAxisY - 7 },
30261
+ { x: x2 + 2 * s, y: xAxisY - 7 }
30262
+ ],
30263
+ fill: color,
30264
+ color
30265
+ });
30266
+ if (hop.label) {
30267
+ out.push({
30268
+ type: "text",
30269
+ x: (x1 + x2) / 2,
30270
+ y: xAxisY - peak - 8,
30271
+ text: hop.label,
30272
+ color: "#111827",
30273
+ fontSize: 10,
30274
+ align: "center"
30275
+ });
30276
+ }
30277
+ }
30278
+ for (const angle of angles) {
30279
+ const radius = angle.radius ?? 0.8;
30280
+ const color = angle.color ?? "#0ea5e9";
30281
+ out.push({
30282
+ type: "ellipse",
30283
+ x: mapX(angle.x),
30284
+ y: mapY(angle.y),
30285
+ width: 2 * radius * plotW / (xMax - xMin),
30286
+ height: 2 * radius * plotH / (yMax - yMin),
30287
+ startAngle: -angle.to,
30288
+ endAngle: -angle.from,
30289
+ color
30290
+ });
30291
+ if (angle.label) {
30292
+ const mid = (angle.from + angle.to) / 2;
30293
+ const rad = mid * Math.PI / 180;
30294
+ out.push({
30295
+ type: "text",
30296
+ x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
30297
+ y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
30298
+ text: angle.label,
30299
+ color: "#111827",
30300
+ fontSize: 11,
30301
+ align: "center"
28195
30302
  });
28196
30303
  }
28197
30304
  }
28198
30305
  for (const p of points) {
28199
30306
  if (p.x < xMin || p.x > xMax || p.y < yMin || p.y > yMax) continue;
30307
+ const isOpen = p.style === "open";
28200
30308
  out.push({
28201
30309
  type: "circle",
28202
30310
  x: mapX(p.x),
28203
30311
  y: mapY(p.y),
28204
30312
  radius: p.radius ?? 4,
28205
30313
  color: p.color ?? "#dc2626",
28206
- fill: p.color ?? "#dc2626"
30314
+ fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
28207
30315
  });
28208
30316
  if (p.label) {
28209
30317
  out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: "#111827", fontSize: 12 });
@@ -28222,7 +30330,28 @@ var init_MathCanvas = __esm({
28222
30330
  }
28223
30331
  out.push(...shapes);
28224
30332
  return out;
28225
- }, [width, height, xMin, xMax, yMin, yMax, showAxes, showGrid, gridStep, curves, points, vectors, shapes]);
30333
+ }, [
30334
+ width,
30335
+ height,
30336
+ xMin,
30337
+ xMax,
30338
+ yMin,
30339
+ yMax,
30340
+ showAxes,
30341
+ showGrid,
30342
+ gridStep,
30343
+ showTickLabels,
30344
+ showCurveLabels,
30345
+ curves,
30346
+ points,
30347
+ vectors,
30348
+ regions,
30349
+ bars,
30350
+ guides,
30351
+ angles,
30352
+ hops,
30353
+ shapes
30354
+ ]);
28226
30355
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
28227
30356
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
28228
30357
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -28231,6 +30360,8 @@ var init_MathCanvas = __esm({
28231
30360
  width,
28232
30361
  height,
28233
30362
  shapes: derivedShapes,
30363
+ readouts,
30364
+ traces,
28234
30365
  interactive,
28235
30366
  animate,
28236
30367
  onShapeClick,
@@ -28242,27 +30373,355 @@ var init_MathCanvas = __esm({
28242
30373
  };
28243
30374
  }
28244
30375
  });
28245
- var PhysicsCanvas;
30376
+ function formatMeterValue(v) {
30377
+ return Number.isInteger(v) ? String(v) : String(Number(v.toFixed(2)));
30378
+ }
30379
+ function sceneObjectShapes(obj, canvasWidth, canvasHeight) {
30380
+ const out = [];
30381
+ const color = obj.color ?? "#334155";
30382
+ switch (obj.kind) {
30383
+ case "ground": {
30384
+ const xStart = obj.x1 ?? 0;
30385
+ const xEnd = obj.x2 ?? canvasWidth;
30386
+ const y = obj.y ?? 0;
30387
+ out.push({ type: "line", x1: xStart, y1: y, x2: xEnd, y2: y, color, lineWidth: 2 });
30388
+ for (let hx = xStart + 7; hx <= xEnd; hx += 14) {
30389
+ out.push({ type: "line", x1: hx, y1: y, x2: hx - 7, y2: y + 7, color, lineWidth: 1 });
30390
+ }
30391
+ if (obj.label) {
30392
+ out.push({
30393
+ type: "text",
30394
+ x: (xStart + xEnd) / 2,
30395
+ y: y - 10,
30396
+ text: obj.label,
30397
+ color: PHYSICS_LABEL_COLOR,
30398
+ fontSize: 11,
30399
+ align: "center"
30400
+ });
30401
+ }
30402
+ break;
30403
+ }
30404
+ case "wall": {
30405
+ const yStart = obj.y1 ?? 0;
30406
+ const yEnd = obj.y2 ?? canvasHeight;
30407
+ const x = obj.x ?? 0;
30408
+ out.push({ type: "line", x1: x, y1: yStart, x2: x, y2: yEnd, color, lineWidth: 2 });
30409
+ for (let hy = yStart + 7; hy <= yEnd; hy += 14) {
30410
+ out.push({ type: "line", x1: x, y1: hy, x2: x - 7, y2: hy + 7, color, lineWidth: 1 });
30411
+ }
30412
+ if (obj.label) {
30413
+ out.push({
30414
+ type: "text",
30415
+ x: x + 12,
30416
+ y: (yStart + yEnd) / 2,
30417
+ text: obj.label,
30418
+ color: PHYSICS_LABEL_COLOR,
30419
+ fontSize: 11,
30420
+ align: "left"
30421
+ });
30422
+ }
30423
+ break;
30424
+ }
30425
+ case "ramp": {
30426
+ const x1 = obj.x1 ?? 0;
30427
+ const y1 = obj.y1 ?? 0;
30428
+ const x2 = obj.x2 ?? canvasWidth;
30429
+ const y2 = obj.y2 ?? canvasHeight;
30430
+ out.push({
30431
+ type: "polygon",
30432
+ points: [
30433
+ { x: x1, y: y1 },
30434
+ { x: x2, y: y2 },
30435
+ { x: x1, y: y2 }
30436
+ ],
30437
+ color,
30438
+ fill: obj.fill ?? "#e2e8f0",
30439
+ lineWidth: 2
30440
+ });
30441
+ if (obj.label) {
30442
+ out.push({
30443
+ type: "text",
30444
+ x: (2 * x1 + x2) / 3,
30445
+ y: (y1 + 2 * y2) / 3,
30446
+ text: obj.label,
30447
+ color: PHYSICS_LABEL_COLOR,
30448
+ fontSize: 11,
30449
+ align: "center"
30450
+ });
30451
+ }
30452
+ break;
30453
+ }
30454
+ case "box": {
30455
+ const x = obj.x ?? 0;
30456
+ const y = obj.y ?? 0;
30457
+ const w = obj.width ?? 40;
30458
+ const h = obj.height ?? 40;
30459
+ out.push({ type: "rect", x, y, width: w, height: h, color, fill: obj.fill, lineWidth: 2 });
30460
+ if (obj.label) {
30461
+ out.push({
30462
+ type: "text",
30463
+ x: x + w / 2,
30464
+ y: y + h / 2,
30465
+ text: obj.label,
30466
+ color: PHYSICS_LABEL_COLOR,
30467
+ fontSize: 11,
30468
+ align: "center"
30469
+ });
30470
+ }
30471
+ break;
30472
+ }
30473
+ case "pivot": {
30474
+ const x = obj.x ?? 0;
30475
+ const y = obj.y ?? 0;
30476
+ out.push({ type: "circle", x, y, radius: 5, color, fill: color });
30477
+ out.push({ type: "line", x1: x - 14, y1: y - 8, x2: x + 14, y2: y - 8, color, lineWidth: 1 });
30478
+ for (let k = 0; k < 5; k++) {
30479
+ const hx = x - 14 + 7 * k;
30480
+ out.push({ type: "line", x1: hx, y1: y - 8, x2: hx - 6, y2: y - 14, color, lineWidth: 1 });
30481
+ }
30482
+ if (obj.label) {
30483
+ out.push({
30484
+ type: "text",
30485
+ x,
30486
+ y: y - 20,
30487
+ text: obj.label,
30488
+ color: PHYSICS_LABEL_COLOR,
30489
+ fontSize: 11,
30490
+ align: "center"
30491
+ });
30492
+ }
30493
+ break;
30494
+ }
30495
+ }
30496
+ return out;
30497
+ }
30498
+ function trailShapes(trail) {
30499
+ const n = trail.points.length;
30500
+ if (n < 2) return [];
30501
+ const color = trail.color ?? "#94a3b8";
30502
+ const lineWidth = trail.width ?? 2;
30503
+ const fade = trail.fade ?? true;
30504
+ const globalOpacity = trail.opacity ?? 1;
30505
+ const out = [];
30506
+ for (let i = 0; i < n - 1; i++) {
30507
+ const a = trail.points[i];
30508
+ const b = trail.points[i + 1];
30509
+ const segmentOpacity = fade ? 0.12 + 0.68 * i / (n - 1) : 0.6;
30510
+ out.push({
30511
+ type: "line",
30512
+ x1: a.x,
30513
+ y1: a.y,
30514
+ x2: b.x,
30515
+ y2: b.y,
30516
+ color,
30517
+ lineWidth,
30518
+ opacity: segmentOpacity * globalOpacity
30519
+ });
30520
+ }
30521
+ return out;
30522
+ }
30523
+ function constraintShapes(c, a, b) {
30524
+ const color = c.color ?? "#9ca3af";
30525
+ const kind = c.kind ?? "rod";
30526
+ if (kind === "rod") {
30527
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2 }];
30528
+ }
30529
+ if (kind === "string") {
30530
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2, dash: "dashed" }];
30531
+ }
30532
+ const COILS = 8;
30533
+ const AMP = 7;
30534
+ const LEAD = 10;
30535
+ const dx = b.x - a.x;
30536
+ const dy = b.y - a.y;
30537
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
30538
+ const ux = dx / dist;
30539
+ const uy = dy / dist;
30540
+ const perpX = -uy;
30541
+ const perpY = ux;
30542
+ const aPrime = { x: a.x + LEAD * ux, y: a.y + LEAD * uy };
30543
+ const bPrime = { x: b.x - LEAD * ux, y: b.y - LEAD * uy };
30544
+ const m = 2 * COILS;
30545
+ const polyline = [{ x: a.x, y: a.y }, aPrime];
30546
+ for (let j = 1; j <= m; j++) {
30547
+ const t = j / (m + 1);
30548
+ const baseX = aPrime.x + t * (bPrime.x - aPrime.x);
30549
+ const baseY = aPrime.y + t * (bPrime.y - aPrime.y);
30550
+ const sign = j % 2 === 0 ? 1 : -1;
30551
+ polyline.push({ x: baseX + sign * AMP * perpX, y: baseY + sign * AMP * perpY });
30552
+ }
30553
+ polyline.push(bPrime, { x: b.x, y: b.y });
30554
+ const out = [];
30555
+ for (let i = 1; i < polyline.length; i++) {
30556
+ out.push({
30557
+ type: "line",
30558
+ x1: polyline[i - 1].x,
30559
+ y1: polyline[i - 1].y,
30560
+ x2: polyline[i].x,
30561
+ y2: polyline[i].y,
30562
+ color,
30563
+ lineWidth: 2
30564
+ });
30565
+ }
30566
+ return out;
30567
+ }
30568
+ function vectorShapes(v, bodyById) {
30569
+ let ax;
30570
+ let ay;
30571
+ if (v.body) {
30572
+ const anchor = bodyById.get(v.body);
30573
+ if (!anchor) return [];
30574
+ ax = anchor.x;
30575
+ ay = anchor.y;
30576
+ } else {
30577
+ ax = v.x ?? 0;
30578
+ ay = v.y ?? 0;
30579
+ }
30580
+ const scale = v.scale ?? 1;
30581
+ const color = v.color ?? "#dc2626";
30582
+ const tx = ax + v.dx * scale;
30583
+ const ty = ay + v.dy * scale;
30584
+ const out = [{ type: "arrow", x1: ax, y1: ay, x2: tx, y2: ty, color, lineWidth: 2, dash: v.dash }];
30585
+ if (v.label) {
30586
+ const dist = Math.max(1e-6, Math.hypot(tx - ax, ty - ay));
30587
+ const ux = (tx - ax) / dist;
30588
+ const uy = (ty - ay) / dist;
30589
+ out.push({
30590
+ type: "text",
30591
+ x: tx + 8 * ux,
30592
+ y: ty + 8 * uy,
30593
+ text: v.label,
30594
+ color,
30595
+ fontSize: 11,
30596
+ align: "center"
30597
+ });
30598
+ }
30599
+ return out;
30600
+ }
30601
+ function angleMarkerShapes(a) {
30602
+ const radius = a.radius ?? 26;
30603
+ const color = a.color ?? "#0ea5e9";
30604
+ const out = [
30605
+ {
30606
+ type: "ellipse",
30607
+ x: a.x,
30608
+ y: a.y,
30609
+ width: radius * 2,
30610
+ height: radius * 2,
30611
+ startAngle: a.from,
30612
+ endAngle: a.to,
30613
+ color,
30614
+ lineWidth: 2
30615
+ }
30616
+ ];
30617
+ if (a.label) {
30618
+ const mid = (a.from + a.to) / 2 * (Math.PI / 180);
30619
+ out.push({
30620
+ type: "text",
30621
+ x: a.x + (radius + 13) * Math.cos(mid),
30622
+ y: a.y + (radius + 13) * Math.sin(mid),
30623
+ text: a.label,
30624
+ color,
30625
+ fontSize: 11,
30626
+ align: "center"
30627
+ });
30628
+ }
30629
+ return out;
30630
+ }
30631
+ function fieldShapes(field, canvasWidth, canvasHeight) {
30632
+ const spacing = field.spacing ?? 48;
30633
+ const size = field.size ?? 14;
30634
+ const color = field.color ?? "#94a3b8";
30635
+ const regionX = field.x ?? 0;
30636
+ const regionY = field.y ?? 0;
30637
+ const regionW = field.width ?? canvasWidth;
30638
+ const regionH = field.height ?? canvasHeight;
30639
+ const out = [];
30640
+ for (let gx = regionX + spacing / 2; gx < regionX + regionW; gx += spacing) {
30641
+ for (let gy = regionY + spacing / 2; gy < regionY + regionH; gy += spacing) {
30642
+ if (field.kind === "arrows") {
30643
+ const rad = (field.angle ?? 0) * Math.PI / 180;
30644
+ const hx = Math.cos(rad) * size / 2;
30645
+ const hy = Math.sin(rad) * size / 2;
30646
+ out.push({ type: "arrow", x1: gx - hx, y1: gy - hy, x2: gx + hx, y2: gy + hy, color, lineWidth: 2 });
30647
+ } else if (field.kind === "into") {
30648
+ const r = size / 3;
30649
+ const d = 0.6 * r * Math.SQRT1_2;
30650
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30651
+ out.push({ type: "line", x1: gx - d, y1: gy - d, x2: gx + d, y2: gy + d, color, lineWidth: 1 });
30652
+ out.push({ type: "line", x1: gx - d, y1: gy + d, x2: gx + d, y2: gy - d, color, lineWidth: 1 });
30653
+ } else {
30654
+ const r = size / 3;
30655
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30656
+ out.push({ type: "circle", x: gx, y: gy, radius: 1.5, color, fill: color });
30657
+ }
30658
+ }
30659
+ }
30660
+ return out;
30661
+ }
30662
+ function meterShapes(meters, canvasHeight) {
30663
+ const n = meters.length;
30664
+ const out = [];
30665
+ const sharedMax = Math.max(1e-6, ...meters.map((m) => m.value));
30666
+ meters.forEach((meter, i) => {
30667
+ const rowY = canvasHeight - 10 - 16 * (n - i);
30668
+ const color = meter.color ?? "#3b82f6";
30669
+ const M = meter.max ?? sharedMax;
30670
+ const w = Math.round(Math.min(1, Math.max(0, meter.value / M)) * 110);
30671
+ out.push({ type: "text", x: 8, y: rowY + 8, text: meter.label, color: PHYSICS_LABEL_COLOR, fontSize: 10 });
30672
+ out.push({ type: "rect", x: 52, y: rowY, width: w, height: 10, color, fill: color });
30673
+ out.push({
30674
+ type: "text",
30675
+ x: 166,
30676
+ y: rowY + 8,
30677
+ text: formatMeterValue(meter.value),
30678
+ color: "#6b7280",
30679
+ fontSize: 9
30680
+ });
30681
+ });
30682
+ return out;
30683
+ }
30684
+ var physicsLog2, PHYSICS_LABEL_COLOR, PhysicsCanvas;
28246
30685
  var init_PhysicsCanvas = __esm({
28247
30686
  "components/learning/molecules/PhysicsCanvas.tsx"() {
28248
30687
  "use client";
28249
30688
  init_atoms();
28250
30689
  init_Stack();
28251
30690
  init_LearningCanvas();
30691
+ init_learningScene3D();
30692
+ physicsLog2 = logger.createLogger("almadar:ui:physics-canvas");
30693
+ PHYSICS_LABEL_COLOR = "#374151";
28252
30694
  PhysicsCanvas = ({
28253
30695
  className,
28254
30696
  width = 600,
28255
30697
  height = 400,
28256
30698
  title,
28257
30699
  backgroundColor,
30700
+ mode = "2d",
30701
+ camera,
30702
+ lighting,
30703
+ post,
28258
30704
  bodies = [],
28259
30705
  constraints = [],
28260
30706
  showVelocity = true,
28261
30707
  showForces = false,
28262
30708
  velocityScale = 20,
28263
30709
  forceScale = 20,
30710
+ sceneObjects = [],
30711
+ trails = [],
30712
+ vectors = [],
30713
+ surface3d,
30714
+ vectors3d = [],
30715
+ vectorScale = 1,
30716
+ angles = [],
30717
+ field,
30718
+ meters = [],
28264
30719
  shapes = [],
28265
- interactive = false,
30720
+ readouts,
30721
+ traces,
30722
+ showGrid,
30723
+ shadows,
30724
+ interactive,
28266
30725
  animate = false,
28267
30726
  onShapeClick,
28268
30727
  isLoading,
@@ -28274,19 +30733,14 @@ var init_PhysicsCanvas = __esm({
28274
30733
  for (const b of bodies) {
28275
30734
  if (b.id) bodyById.set(b.id, b);
28276
30735
  }
30736
+ if (field) out.push(...fieldShapes(field, width, height));
30737
+ for (const obj of sceneObjects) out.push(...sceneObjectShapes(obj, width, height));
30738
+ for (const trail of trails) out.push(...trailShapes(trail));
28277
30739
  for (const c of constraints) {
28278
30740
  const a = bodyById.get(c.from);
28279
30741
  const b = bodyById.get(c.to);
28280
30742
  if (!a || !b) continue;
28281
- out.push({
28282
- type: "line",
28283
- x1: a.x,
28284
- y1: a.y,
28285
- x2: b.x,
28286
- y2: b.y,
28287
- color: c.color ?? "#9ca3af",
28288
- lineWidth: 2
28289
- });
30743
+ out.push(...constraintShapes(c, a, b));
28290
30744
  }
28291
30745
  for (const b of bodies) {
28292
30746
  out.push({
@@ -28331,9 +30785,181 @@ var init_PhysicsCanvas = __esm({
28331
30785
  });
28332
30786
  }
28333
30787
  }
30788
+ for (const v of vectors) out.push(...vectorShapes(v, bodyById));
30789
+ for (const a of angles) out.push(...angleMarkerShapes(a));
30790
+ if (meters.length > 0) out.push(...meterShapes(meters, height));
28334
30791
  out.push(...shapes);
28335
30792
  return out;
28336
- }, [bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes]);
30793
+ }, [
30794
+ bodies,
30795
+ constraints,
30796
+ showVelocity,
30797
+ showForces,
30798
+ velocityScale,
30799
+ forceScale,
30800
+ sceneObjects,
30801
+ trails,
30802
+ vectors,
30803
+ angles,
30804
+ field,
30805
+ meters,
30806
+ shapes,
30807
+ width,
30808
+ height
30809
+ ]);
30810
+ const drawables3D = React87.useMemo(() => {
30811
+ if (mode !== "3d") return [];
30812
+ if (shapes.length > 0) {
30813
+ physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
30814
+ }
30815
+ if (sceneObjects.length > 0) {
30816
+ physicsLog2.debug("sceneObjects ignored in 3D mode (pixel-authored 2D vocabulary)", { count: sceneObjects.length });
30817
+ }
30818
+ if (vectors.length > 0) {
30819
+ physicsLog2.debug("vectors ignored in 3D mode (pixel-authored 2D vocabulary)", { count: vectors.length });
30820
+ }
30821
+ if (angles.length > 0) {
30822
+ physicsLog2.debug("angles ignored in 3D mode (pixel-authored 2D vocabulary)", { count: angles.length });
30823
+ }
30824
+ if (field) {
30825
+ physicsLog2.debug("field ignored in 3D mode (pixel-authored 2D vocabulary)");
30826
+ }
30827
+ if (meters.length > 0) {
30828
+ physicsLog2.debug("meters ignored in 3D mode (pixel-authored 2D vocabulary)", { count: meters.length });
30829
+ }
30830
+ if (animate) {
30831
+ physicsLog2.debug("animate ignored in 3D mode (motion is entity-state driven)");
30832
+ }
30833
+ const out = [];
30834
+ const labelColor = labelColorForBackground(backgroundColor);
30835
+ const bodyById = /* @__PURE__ */ new Map();
30836
+ for (const b of bodies) {
30837
+ if (b.id) bodyById.set(b.id, b);
30838
+ }
30839
+ for (const c of constraints) {
30840
+ const a = bodyById.get(c.from);
30841
+ const b = bodyById.get(c.to);
30842
+ if (!a || !b) continue;
30843
+ const rodRadius = Math.max(0.05, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.15);
30844
+ const rod = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], rodRadius, c.color ?? "#9ca3af");
30845
+ if (rod) out.push(rod);
30846
+ }
30847
+ for (const b of bodies) {
30848
+ const radius = b.radius ?? 0.5;
30849
+ const bz = b.z ?? 0;
30850
+ out.push(meshSphere(b.id, b.x, b.y, bz, radius, b.color ?? "#2563eb"));
30851
+ if (b.label) {
30852
+ out.push(billboardLabel(b.label, b.x, b.y, bz + radius, { color: labelColor }));
30853
+ }
30854
+ const vx = b.vx ?? 0;
30855
+ const vy = b.vy ?? 0;
30856
+ const vz = b.vz ?? 0;
30857
+ const arrowRadius = Math.max(0.05, radius * 0.15);
30858
+ if (showVelocity && (vx !== 0 || vy !== 0 || vz !== 0)) {
30859
+ const arrow = arrowBetween(
30860
+ [b.x, b.y, bz],
30861
+ [b.x + vx * velocityScale, b.y + vy * velocityScale, bz + vz * velocityScale],
30862
+ "#16a34a",
30863
+ arrowRadius
30864
+ );
30865
+ if (arrow) out.push(arrow);
30866
+ }
30867
+ const fx = b.fx ?? 0;
30868
+ const fy = b.fy ?? 0;
30869
+ const fz = b.fz ?? 0;
30870
+ if (showForces && (fx !== 0 || fy !== 0 || fz !== 0)) {
30871
+ const arrow = arrowBetween(
30872
+ [b.x, b.y, bz],
30873
+ [b.x + fx * forceScale, b.y + fy * forceScale, bz + fz * forceScale],
30874
+ "#dc2626",
30875
+ arrowRadius
30876
+ );
30877
+ if (arrow) out.push(arrow);
30878
+ }
30879
+ }
30880
+ for (const trail of trails) {
30881
+ if (trail.fade !== void 0) {
30882
+ physicsLog2.debug("trail.fade ignored in 3D mode (2D-only fade curve \u2014 3D draws an opaque tube)", { id: trail.id });
30883
+ }
30884
+ const points = trail.points.map((p) => [p.x, p.y, p.z ?? 0]);
30885
+ out.push(
30886
+ ...polylineTube(points, trail.width ?? 0.05, trail.color ?? "#94a3b8", {
30887
+ ...trail.opacity !== void 0 ? { opacity: trail.opacity } : {}
30888
+ })
30889
+ );
30890
+ }
30891
+ if (surface3d) {
30892
+ out.push(...heightFieldMesh(surface3d));
30893
+ }
30894
+ if (vectors3d.length > 0) {
30895
+ out.push(
30896
+ ...arrowField(
30897
+ vectors3d.map((v) => ({
30898
+ id: v.id,
30899
+ from: [v.x, v.y, v.z ?? 0],
30900
+ delta: [v.dx, v.dy, v.dz ?? 0],
30901
+ color: v.color,
30902
+ label: v.label,
30903
+ width: v.width
30904
+ })),
30905
+ { scale: vectorScale, labelColor }
30906
+ )
30907
+ );
30908
+ }
30909
+ return out;
30910
+ }, [
30911
+ mode,
30912
+ bodies,
30913
+ constraints,
30914
+ showVelocity,
30915
+ showForces,
30916
+ velocityScale,
30917
+ forceScale,
30918
+ shapes,
30919
+ sceneObjects,
30920
+ trails,
30921
+ vectors,
30922
+ surface3d,
30923
+ vectors3d,
30924
+ vectorScale,
30925
+ angles,
30926
+ field,
30927
+ meters,
30928
+ animate,
30929
+ backgroundColor
30930
+ ]);
30931
+ const bodyIndexById = React87.useMemo(() => {
30932
+ const m = /* @__PURE__ */ new Map();
30933
+ vectors3d.forEach((v, i) => {
30934
+ if (v.id) m.set(v.id, i);
30935
+ });
30936
+ bodies.forEach((b, i) => {
30937
+ if (b.id) m.set(b.id, i);
30938
+ });
30939
+ return m;
30940
+ }, [bodies, vectors3d]);
30941
+ if (mode === "3d") {
30942
+ return /* @__PURE__ */ jsxRuntime.jsx(
30943
+ LearningScene3D,
30944
+ {
30945
+ className,
30946
+ width,
30947
+ height,
30948
+ title,
30949
+ backgroundColor,
30950
+ drawables: drawables3D,
30951
+ camera,
30952
+ lighting,
30953
+ post,
30954
+ showGrid,
30955
+ shadows,
30956
+ interactive,
30957
+ isLoading,
30958
+ error,
30959
+ onItemClick: get3DClickPayload(onShapeClick, bodyIndexById)
30960
+ }
30961
+ );
30962
+ }
28337
30963
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
28338
30964
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
28339
30965
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -28343,7 +30969,9 @@ var init_PhysicsCanvas = __esm({
28343
30969
  height,
28344
30970
  backgroundColor,
28345
30971
  shapes: derivedShapes,
28346
- interactive,
30972
+ readouts,
30973
+ traces,
30974
+ interactive: interactive ?? false,
28347
30975
  animate,
28348
30976
  onShapeClick,
28349
30977
  isLoading,
@@ -28493,7 +31121,7 @@ function layoutFlow(nodeIds, adjacency, roots, width, height, margin) {
28493
31121
  }
28494
31122
  return nodeIds.map((id) => positions.get(id));
28495
31123
  }
28496
- function layoutTree(nodeIds, adjacency, roots, width, height, margin) {
31124
+ function layoutTree2(nodeIds, adjacency, roots, width, height, margin) {
28497
31125
  const effectiveRoots = roots.length > 0 ? roots : [nodeIds[0]];
28498
31126
  const layers = assignLayers(nodeIds, adjacency, effectiveRoots);
28499
31127
  const maxLayer = Math.max(...Array.from(layers.values()));
@@ -28549,7 +31177,7 @@ function computeStaticLayout(mode, input) {
28549
31177
  const adjacency = buildAdjacency(nodeIds, edges);
28550
31178
  const roots = findRoots(nodeIds, adjacency);
28551
31179
  if (mode === "flow") return layoutFlow(nodeIds, adjacency, roots, width, height, margin);
28552
- if (mode === "tree") return layoutTree(nodeIds, adjacency, roots, width, height, margin);
31180
+ if (mode === "tree") return layoutTree2(nodeIds, adjacency, roots, width, height, margin);
28553
31181
  return layoutRadial(nodeIds, adjacency, roots, width, height, margin);
28554
31182
  }
28555
31183
  var init_graphViewLayouts = __esm({
@@ -28961,12 +31589,12 @@ var init_MapView = __esm({
28961
31589
  shadowSize: [41, 41]
28962
31590
  });
28963
31591
  L.Marker.prototype.options.icon = defaultIcon;
28964
- const { useEffect: useEffect63, useRef: useRef62, useCallback: useCallback96, useState: useState94 } = React87__namespace.default;
31592
+ const { useEffect: useEffect63, useRef: useRef63, useCallback: useCallback96, useState: useState94 } = React87__namespace.default;
28965
31593
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
28966
31594
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
28967
31595
  function MapUpdater({ centerLat, centerLng, zoom }) {
28968
31596
  const map = useMap();
28969
- const prevRef = useRef62({ centerLat, centerLng, zoom });
31597
+ const prevRef = useRef63({ centerLat, centerLng, zoom });
28970
31598
  useEffect63(() => {
28971
31599
  const prev = prevRef.current;
28972
31600
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
@@ -29792,26 +32420,6 @@ var init_Lightbox = __esm({
29792
32420
  Lightbox.displayName = "Lightbox";
29793
32421
  }
29794
32422
  });
29795
- function useMediaQuery(query) {
29796
- const subscribe = React87.useCallback(
29797
- (onChange) => {
29798
- const mql = window.matchMedia(query);
29799
- mql.addEventListener("change", onChange);
29800
- return () => mql.removeEventListener("change", onChange);
29801
- },
29802
- [query]
29803
- );
29804
- return React87.useSyncExternalStore(
29805
- subscribe,
29806
- () => window.matchMedia(query).matches,
29807
- () => false
29808
- );
29809
- }
29810
- var init_useMediaQuery = __esm({
29811
- "hooks/useMediaQuery.ts"() {
29812
- "use client";
29813
- }
29814
- });
29815
32423
  function renderIconInput3(icon, props) {
29816
32424
  return typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, ...props });
29817
32425
  }
@@ -29850,8 +32458,8 @@ function TableView({
29850
32458
  columns,
29851
32459
  fields,
29852
32460
  itemActions,
29853
- maxInlineActions,
29854
- itemClickEvent,
32461
+ maxInlineActions: _maxInlineActions,
32462
+ itemClickEvent = "",
29855
32463
  selectable = false,
29856
32464
  selectEvent,
29857
32465
  selectedIds,
@@ -29901,7 +32509,6 @@ function TableView({
29901
32509
  const hasMore = pageSize > 0 && visibleCount < ordered2.length;
29902
32510
  const hasRenderProp = typeof children === "function";
29903
32511
  const idField = dndItemIdField ?? "id";
29904
- const isCoarsePointer = useMediaQuery("(pointer: coarse)");
29905
32512
  React87__namespace.default.useEffect(() => {
29906
32513
  tableViewLog.debug("render", {
29907
32514
  rowCount: data.length,
@@ -29943,21 +32550,14 @@ function TableView({
29943
32550
  const dir = sortColumn === (col.field ?? col.key) && sortDirection === "asc" ? "desc" : "asc";
29944
32551
  eventBus.emit(`UI:${sortEvent}`, { column: col.field ?? col.key, direction: dir });
29945
32552
  };
29946
- const handleActionClick = (action, row) => (e) => {
29947
- e.stopPropagation();
29948
- const payload = {
29949
- id: row.id,
29950
- row
29951
- };
29952
- eventBus.emit(`UI:${action.event}`, payload);
29953
- };
32553
+ const rowClickEvent = itemClickEvent || actionDefs.find((a) => a.variant !== "danger")?.event;
29954
32554
  const handleRowClick = (row) => () => {
29955
- if (!itemClickEvent) return;
32555
+ if (!rowClickEvent) return;
29956
32556
  const payload = {
29957
32557
  id: row.id,
29958
32558
  row
29959
32559
  };
29960
- eventBus.emit(`UI:${itemClickEvent}`, payload);
32560
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
29961
32561
  };
29962
32562
  const colFloors = React87__namespace.default.useMemo(
29963
32563
  () => colDefs.map((col) => {
@@ -29973,10 +32573,7 @@ function TableView({
29973
32573
  const statusNode = isLoading ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) }) : error ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "error", children: error.message }) }) : data.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-12", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: emptyMessage || t("empty.noItems") }) }) : null;
29974
32574
  const lk = LOOKS[look];
29975
32575
  const hasActions = actionDefs.length > 0;
29976
- const effectiveMaxInline = isCoarsePointer ? 0 : maxInlineActions;
29977
- const inlineActionCount = hasActions ? effectiveMaxInline != null ? Math.min(actionDefs.length, effectiveMaxInline) : actionDefs.length : 0;
29978
- const hasOverflowActions = hasActions && effectiveMaxInline != null && actionDefs.length > effectiveMaxInline;
29979
- const actionsTrack = hasActions ? `${inlineActionCount * 6 + (hasOverflowActions ? 3 : 0)}rem` : null;
32576
+ const actionsTrack = hasActions ? "3rem" : null;
29980
32577
  const gridTemplateColumns = [
29981
32578
  selectable ? "auto" : null,
29982
32579
  ...colDefs.map((c, i) => c.width ?? `minmax(${colFloors[i]}ch, 1fr)`),
@@ -30023,7 +32620,7 @@ function TableView({
30023
32620
  col.key
30024
32621
  );
30025
32622
  }),
30026
- hasActions && /* @__PURE__ */ jsxRuntime.jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)]" })
32623
+ hasActions && /* @__PURE__ */ jsxRuntime.jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)] border-l border-[var(--color-border)] h-full" })
30027
32624
  ]
30028
32625
  }
30029
32626
  );
@@ -30035,12 +32632,12 @@ function TableView({
30035
32632
  role: "row",
30036
32633
  "data-entity-row": true,
30037
32634
  "data-entity-id": id,
30038
- onClick: itemClickEvent ? handleRowClick(row) : void 0,
32635
+ onClick: rowClickEvent ? handleRowClick(row) : void 0,
30039
32636
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
30040
32637
  className: cn(
30041
32638
  "group items-center gap-3 transition-colors duration-fast",
30042
32639
  hasRenderProp ? "flex" : "grid",
30043
- itemClickEvent && "cursor-pointer",
32640
+ rowClickEvent && "cursor-pointer",
30044
32641
  lk.rowPad,
30045
32642
  lk.divider && "border-b border-[var(--color-border)]",
30046
32643
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -30048,7 +32645,7 @@ function TableView({
30048
32645
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
30049
32646
  ),
30050
32647
  children: [
30051
- selectable && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex items-center", onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsxRuntime.jsx(
32648
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex items-center", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsxRuntime.jsx(
30052
32649
  Checkbox,
30053
32650
  {
30054
32651
  checked: selected.has(id),
@@ -30069,53 +32666,37 @@ function TableView({
30069
32666
  }
30070
32667
  return /* @__PURE__ */ jsxRuntime.jsx(Box, { role: "cell", className: cellBase, children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-foreground", children: formatCell(raw, col.format) }) }, col.key);
30071
32668
  }),
30072
- hasActions && /* @__PURE__ */ jsxRuntime.jsxs(
32669
+ hasActions && /* @__PURE__ */ jsxRuntime.jsx(
30073
32670
  HStack,
30074
32671
  {
30075
32672
  gap: "xs",
30076
- onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
32673
+ onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0,
30077
32674
  className: cn(
30078
32675
  // Pinned: the fixed column tracks routinely overflow the caller's
30079
- // scroll container, which used to leave the actions off-screen.
30080
- // Opaque so scrolled cells pass underneath it.
32676
+ // scroll container, which would leave the kebab off-screen.
32677
+ // Opaque + hairline edge so it reads as a pinned column, not a
32678
+ // floating control, while scrolled cells pass underneath.
30081
32679
  "justify-end flex-shrink-0 sticky right-0 z-[1] transition-colors",
32680
+ "border-l border-[var(--color-border)]",
30082
32681
  lk.striped && index % 2 === 1 ? "bg-[var(--color-surface-subtle)]" : "bg-[var(--color-card)] group-hover:bg-[var(--color-surface-subtle)]"
30083
32682
  ),
30084
- children: [
30085
- (effectiveMaxInline != null ? actionDefs.slice(0, effectiveMaxInline) : actionDefs).map((action, i) => /* @__PURE__ */ jsxRuntime.jsxs(
30086
- Button,
30087
- {
30088
- variant: action.variant === "primary" ? "primary" : "ghost",
30089
- size: "sm",
30090
- onClick: handleActionClick(action, row),
30091
- "data-testid": `action-${action.event}`,
30092
- "data-row-id": String(row.id),
30093
- className: cn(action.variant === "danger" && "text-error hover:text-error hover:bg-error/10"),
30094
- children: [
30095
- action.icon && renderIconInput3(action.icon, { size: "xs", className: "mr-1" }),
30096
- action.label
30097
- ]
30098
- },
30099
- i
30100
- )),
30101
- effectiveMaxInline != null && actionDefs.length > effectiveMaxInline && /* @__PURE__ */ jsxRuntime.jsx(
30102
- Menu,
30103
- {
30104
- position: "bottom-end",
30105
- trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
30106
- items: actionDefs.slice(effectiveMaxInline).map((action) => ({
30107
- label: action.label,
30108
- icon: action.icon,
30109
- event: action.event,
30110
- variant: action.variant === "danger" ? "danger" : "default",
30111
- onClick: () => eventBus.emit(`UI:${action.event}`, {
30112
- id: row.id,
30113
- row
30114
- })
30115
- }))
30116
- }
30117
- )
30118
- ]
32683
+ children: /* @__PURE__ */ jsxRuntime.jsx(
32684
+ Menu,
32685
+ {
32686
+ position: "bottom-end",
32687
+ trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", "data-row-id": String(row.id), children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
32688
+ items: actionDefs.map((action) => ({
32689
+ label: action.label,
32690
+ icon: action.icon,
32691
+ event: action.event,
32692
+ variant: action.variant === "danger" ? "danger" : "default",
32693
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
32694
+ id: row.id,
32695
+ row
32696
+ })
32697
+ }))
32698
+ }
32699
+ )
30119
32700
  }
30120
32701
  )
30121
32702
  ]
@@ -30158,7 +32739,6 @@ var init_TableView = __esm({
30158
32739
  init_format();
30159
32740
  init_getNestedValue();
30160
32741
  init_useEventBus();
30161
- init_useMediaQuery();
30162
32742
  init_Box();
30163
32743
  init_Stack();
30164
32744
  init_Typography();
@@ -39789,7 +42369,7 @@ function getEnumOptions(field) {
39789
42369
  }));
39790
42370
  }
39791
42371
  const validation = field.validation;
39792
- if (validation?.enum && Array.isArray(validation.enum)) {
42372
+ if (validation?.enum && validation.enum.length > 0) {
39793
42373
  return validation.enum.map((v) => ({
39794
42374
  value: v,
39795
42375
  label: v.charAt(0).toUpperCase() + v.slice(1).replace(/_/g, " ")
@@ -45217,6 +47797,7 @@ var init_component_registry_generated = __esm({
45217
47797
  init_ActionTile();
45218
47798
  init_ActivationBlock();
45219
47799
  init_ComponentPatterns();
47800
+ init_AlgoGraphCanvas();
45220
47801
  init_AlgorithmCanvas();
45221
47802
  init_AnimatedCounter();
45222
47803
  init_AnimatedGraphic();
@@ -45480,6 +48061,7 @@ var init_component_registry_generated = __esm({
45480
48061
  "ActivationBlock": ActivationBlock,
45481
48062
  "Alert": AlertPattern,
45482
48063
  "AlertPattern": AlertPattern,
48064
+ "AlgoGraphCanvas": AlgoGraphCanvas,
45483
48065
  "AlgorithmCanvas": AlgorithmCanvas,
45484
48066
  "AnimatedCounter": AnimatedCounter,
45485
48067
  "AnimatedGraphic": AnimatedGraphic,