@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
@@ -133,7 +133,7 @@ function useEventBus() {
133
133
  return {
134
134
  ...baseBus,
135
135
  emit: (type, payload, source) => {
136
- if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
136
+ if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".") && !source?.trait) {
137
137
  scopeLog.warn("emit:bare-key-no-scope", { type });
138
138
  }
139
139
  baseBus.emit(type, payload, source);
@@ -10301,9 +10301,10 @@ function Canvas({
10301
10301
  drawables: [...drawables ?? [], ...childDrawables],
10302
10302
  isLoading,
10303
10303
  cameraMode: to3DCameraMode(camera?.mode),
10304
- ...zoom !== void 0 ? { scale: zoom } : {},
10304
+ ...zoom !== void 0 ? { zoom } : {},
10305
10305
  ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
10306
10306
  ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
10307
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
10307
10308
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
10308
10309
  unitScale,
10309
10310
  backgroundColor,
@@ -11571,6 +11572,14 @@ function shapeBounds(shape) {
11571
11572
  w: shape.radius * 2 + 8,
11572
11573
  h: shape.radius * 2 + 8
11573
11574
  };
11575
+ case "ellipse":
11576
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11577
+ return {
11578
+ x: shape.x - shape.width / 2 - 4,
11579
+ y: shape.y - shape.height / 2 - 4,
11580
+ w: shape.width + 8,
11581
+ h: shape.height + 8
11582
+ };
11574
11583
  case "rect":
11575
11584
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11576
11585
  return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
@@ -11604,13 +11613,14 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
11604
11613
  ctx.closePath();
11605
11614
  ctx.fill();
11606
11615
  }
11607
- function drawShape(ctx, shape, width, height) {
11616
+ function drawShape(ctx, shape, width, height, allShapes) {
11608
11617
  ctx.save();
11609
11618
  const opacity = shape.opacity ?? 1;
11610
11619
  ctx.globalAlpha = opacity;
11611
11620
  const stroke = resolveColor2(shape.color, ctx, "#333333");
11612
11621
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
11613
11622
  ctx.lineWidth = shape.lineWidth ?? 2;
11623
+ if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
11614
11624
  switch (shape.type) {
11615
11625
  case "grid": {
11616
11626
  const step = shape.step ?? 40;
@@ -11676,6 +11686,20 @@ function drawShape(ctx, shape, width, height) {
11676
11686
  ctx.stroke();
11677
11687
  break;
11678
11688
  }
11689
+ case "ellipse": {
11690
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11691
+ const startAngle = (shape.startAngle ?? 0) * Math.PI / 180;
11692
+ const endAngle = (shape.endAngle ?? 360) * Math.PI / 180;
11693
+ ctx.beginPath();
11694
+ ctx.ellipse(shape.x, shape.y, shape.width / 2, shape.height / 2, 0, startAngle, endAngle);
11695
+ if (fill) {
11696
+ ctx.fillStyle = fill;
11697
+ ctx.fill();
11698
+ }
11699
+ ctx.strokeStyle = stroke;
11700
+ ctx.stroke();
11701
+ break;
11702
+ }
11679
11703
  case "rect": {
11680
11704
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11681
11705
  if (fill) {
@@ -11722,21 +11746,153 @@ function drawShape(ctx, shape, width, height) {
11722
11746
  ctx.fillText(shape.text, shape.x, shape.y);
11723
11747
  break;
11724
11748
  }
11749
+ case "venn-region": {
11750
+ const resolveCircles = (ids) => (ids ?? []).flatMap((id) => {
11751
+ const c = allShapes.find((s) => s.type === "circle" && s.id === id);
11752
+ return c && c.x != null && c.y != null && c.radius != null ? [{ x: c.x, y: c.y, radius: c.radius }] : [];
11753
+ });
11754
+ const inside = resolveCircles(shape.inside);
11755
+ if (inside.length === 0) break;
11756
+ const outside = resolveCircles(shape.outside);
11757
+ const off = document.createElement("canvas");
11758
+ off.width = ctx.canvas.width;
11759
+ off.height = ctx.canvas.height;
11760
+ const octx = off.getContext("2d");
11761
+ if (!octx) break;
11762
+ octx.setTransform(ctx.getTransform());
11763
+ for (const c of inside) {
11764
+ const p = new Path2D();
11765
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11766
+ octx.clip(p);
11767
+ }
11768
+ octx.fillStyle = fill ?? stroke;
11769
+ octx.fillRect(0, 0, width, height);
11770
+ octx.globalCompositeOperation = "destination-out";
11771
+ for (const c of outside) {
11772
+ const p = new Path2D();
11773
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11774
+ octx.fill(p);
11775
+ }
11776
+ ctx.save();
11777
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
11778
+ ctx.drawImage(off, 0, 0);
11779
+ ctx.restore();
11780
+ break;
11781
+ }
11725
11782
  }
11726
11783
  ctx.restore();
11727
11784
  }
11728
- var LearningCanvas;
11785
+ function readoutShapes(readouts, width) {
11786
+ const out = [];
11787
+ const chipH = 18;
11788
+ const gap = 6;
11789
+ let rightEdge = width - 6;
11790
+ let rowY = 6;
11791
+ for (const readout of readouts) {
11792
+ const text = `${readout.label}: ${String(readout.value)}`;
11793
+ const chipW = Math.min(170, Math.max(34, text.length * 6 + 12));
11794
+ let chipX = rightEdge - chipW;
11795
+ if (chipX < 4) {
11796
+ rowY += chipH + 4;
11797
+ rightEdge = width - 6;
11798
+ chipX = rightEdge - chipW;
11799
+ }
11800
+ const color = readout.color ?? "#334155";
11801
+ out.push({ type: "rect", x: chipX, y: rowY, width: chipW, height: chipH, color, fill: color });
11802
+ out.push({
11803
+ type: "text",
11804
+ x: chipX + chipW / 2,
11805
+ y: rowY + chipH / 2,
11806
+ text,
11807
+ color: "#ffffff",
11808
+ fontSize: 10,
11809
+ align: "center"
11810
+ });
11811
+ rightEdge = chipX - gap;
11812
+ }
11813
+ return out;
11814
+ }
11815
+ function traceShapes(panel, k, width, height) {
11816
+ const w = panel.width ?? Math.round(width * 0.32);
11817
+ const h = panel.height ?? Math.round(height * 0.28);
11818
+ const x = panel.x ?? width - w - 8;
11819
+ const y = panel.y ?? height - h - 8 - k * (h + 8);
11820
+ const allSamples = panel.series.flatMap((series) => series.samples);
11821
+ let xLo = Math.min(...allSamples.map((p) => p.x));
11822
+ let xHi = Math.max(...allSamples.map((p) => p.x));
11823
+ let yLo = Math.min(...allSamples.map((p) => p.y));
11824
+ let yHi = Math.max(...allSamples.map((p) => p.y));
11825
+ if (xLo === xHi) {
11826
+ xLo -= 1;
11827
+ xHi += 1;
11828
+ }
11829
+ if (yLo === yHi) {
11830
+ yLo -= 1;
11831
+ yHi += 1;
11832
+ }
11833
+ const backgroundColor = panel.backgroundColor ?? "#ffffff";
11834
+ const frameColor = panel.frameColor ?? "#94a3b8";
11835
+ const out = [];
11836
+ out.push({
11837
+ type: "rect",
11838
+ x,
11839
+ y,
11840
+ width: w,
11841
+ height: h,
11842
+ color: backgroundColor,
11843
+ fill: backgroundColor,
11844
+ opacity: panel.backgroundOpacity ?? 0.85
11845
+ });
11846
+ out.push({ type: "rect", x, y, width: w, height: h, color: frameColor, lineWidth: 1 });
11847
+ panel.series.forEach((series, j) => {
11848
+ const color = series.color ?? TRACE_SERIES_COLORS[j % TRACE_SERIES_COLORS.length];
11849
+ const mapped = series.samples.map((p) => ({
11850
+ x: x + 4 + (p.x - xLo) / (xHi - xLo) * (w - 8),
11851
+ y: y + h - 4 - (p.y - yLo) / (yHi - yLo) * (h - 8)
11852
+ }));
11853
+ for (let i = 1; i < mapped.length; i++) {
11854
+ out.push({
11855
+ type: "line",
11856
+ x1: mapped[i - 1].x,
11857
+ y1: mapped[i - 1].y,
11858
+ x2: mapped[i].x,
11859
+ y2: mapped[i].y,
11860
+ color,
11861
+ lineWidth: 1.5
11862
+ });
11863
+ }
11864
+ if (mapped.length > 0) {
11865
+ const last = mapped[mapped.length - 1];
11866
+ out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
11867
+ }
11868
+ if (series.label) {
11869
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
11870
+ }
11871
+ });
11872
+ if (panel.yLabel) {
11873
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
11874
+ }
11875
+ if (panel.xLabel) {
11876
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
11877
+ }
11878
+ return out;
11879
+ }
11880
+ var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
11729
11881
  var init_LearningCanvas = __esm({
11730
11882
  "components/learning/atoms/LearningCanvas.tsx"() {
11731
11883
  "use client";
11732
11884
  init_cn();
11733
11885
  init_useEventBus();
11886
+ DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
11887
+ TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
11734
11888
  LearningCanvas = ({
11735
11889
  className,
11736
11890
  width = 600,
11737
11891
  height = 400,
11738
11892
  backgroundColor,
11739
11893
  shapes = [],
11894
+ readouts,
11895
+ traces,
11740
11896
  interactive = false,
11741
11897
  animate = false,
11742
11898
  onShapeClick,
@@ -11762,6 +11918,12 @@ var init_LearningCanvas = __esm({
11762
11918
  }
11763
11919
  return -1;
11764
11920
  }, [shapes]);
11921
+ const derivedShapes = useMemo(() => {
11922
+ if (!traces?.length && !readouts?.length) return shapes;
11923
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
11924
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
11925
+ return [...shapes, ...traceOut, ...readoutOut];
11926
+ }, [shapes, traces, readouts, width, height]);
11765
11927
  const draw = useCallback(() => {
11766
11928
  const canvas = canvasRef.current;
11767
11929
  if (!canvas) return;
@@ -11778,13 +11940,13 @@ var init_LearningCanvas = __esm({
11778
11940
  ctx.fillStyle = backgroundColor;
11779
11941
  ctx.fillRect(0, 0, width, height);
11780
11942
  }
11781
- for (const shape of shapes) {
11782
- if (shape.type !== "text") drawShape(ctx, shape, width, height);
11943
+ for (const shape of derivedShapes) {
11944
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
11783
11945
  }
11784
- for (const shape of shapes) {
11785
- if (shape.type === "text") drawShape(ctx, shape, width, height);
11946
+ for (const shape of derivedShapes) {
11947
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
11786
11948
  }
11787
- }, [width, height, backgroundColor, shapes]);
11949
+ }, [width, height, backgroundColor, derivedShapes]);
11788
11950
  useEffect(() => {
11789
11951
  draw();
11790
11952
  }, [draw]);
@@ -13717,7 +13879,363 @@ var init_ComponentPatterns = __esm({
13717
13879
  AlertPattern.displayName = "AlertPattern";
13718
13880
  }
13719
13881
  });
13720
- var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, AlgorithmCanvas;
13882
+ function layoutCircle(nodes, width, height) {
13883
+ const cx = width / 2;
13884
+ const cy = height / 2;
13885
+ const radius = Math.max(10, Math.min(cx, cy) - 40);
13886
+ const positions = /* @__PURE__ */ new Map();
13887
+ const n = nodes.length;
13888
+ nodes.forEach((node, i) => {
13889
+ const angle = 2 * Math.PI * i / Math.max(n, 1) - Math.PI / 2;
13890
+ positions.set(node.id, { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) });
13891
+ });
13892
+ return positions;
13893
+ }
13894
+ function layoutTree(nodes, edges, root, width, height) {
13895
+ const nodeIds = nodes.map((n) => n.id);
13896
+ const idSet = new Set(nodeIds);
13897
+ const childrenOf = /* @__PURE__ */ new Map();
13898
+ const hasIncoming = /* @__PURE__ */ new Set();
13899
+ for (const e of edges) {
13900
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
13901
+ const list = childrenOf.get(e.from) ?? [];
13902
+ list.push(e.to);
13903
+ childrenOf.set(e.from, list);
13904
+ hasIncoming.add(e.to);
13905
+ }
13906
+ const depth = /* @__PURE__ */ new Map();
13907
+ const treeChildren = /* @__PURE__ */ new Map();
13908
+ const visited = /* @__PURE__ */ new Set();
13909
+ const bfsFrom = (start) => {
13910
+ if (visited.has(start)) return;
13911
+ visited.add(start);
13912
+ depth.set(start, 0);
13913
+ const queue = [start];
13914
+ while (queue.length > 0) {
13915
+ const u = queue.shift();
13916
+ for (const v of childrenOf.get(u) ?? []) {
13917
+ if (visited.has(v)) continue;
13918
+ visited.add(v);
13919
+ depth.set(v, (depth.get(u) ?? 0) + 1);
13920
+ const list = treeChildren.get(u) ?? [];
13921
+ list.push(v);
13922
+ treeChildren.set(u, list);
13923
+ queue.push(v);
13924
+ }
13925
+ }
13926
+ };
13927
+ const primaryRoot = root && idSet.has(root) ? root : nodeIds.find((id) => !hasIncoming.has(id)) ?? nodeIds[0];
13928
+ const rootsOrder = [];
13929
+ if (primaryRoot !== void 0) {
13930
+ bfsFrom(primaryRoot);
13931
+ rootsOrder.push(primaryRoot);
13932
+ }
13933
+ for (const id of nodeIds) {
13934
+ if (!visited.has(id)) {
13935
+ bfsFrom(id);
13936
+ rootsOrder.push(id);
13937
+ }
13938
+ }
13939
+ let leafCounter = 0;
13940
+ const xSlot = /* @__PURE__ */ new Map();
13941
+ const assignXSlot = (u) => {
13942
+ const children = treeChildren.get(u) ?? [];
13943
+ if (children.length === 0) {
13944
+ const slot = leafCounter++;
13945
+ xSlot.set(u, slot);
13946
+ return slot;
13947
+ }
13948
+ const childSlots = children.map(assignXSlot);
13949
+ const avg = childSlots.reduce((a, b) => a + b, 0) / childSlots.length;
13950
+ xSlot.set(u, avg);
13951
+ return avg;
13952
+ };
13953
+ for (const r of rootsOrder) assignXSlot(r);
13954
+ let maxDepth = 0;
13955
+ for (const d of depth.values()) maxDepth = Math.max(maxDepth, d);
13956
+ const colWidth = width / Math.max(1, leafCounter);
13957
+ const rowHeight = height / (maxDepth + 1);
13958
+ const positions = /* @__PURE__ */ new Map();
13959
+ for (const id of nodeIds) {
13960
+ const slot = xSlot.get(id) ?? 0;
13961
+ const d = depth.get(id) ?? 0;
13962
+ positions.set(id, { x: slot * colWidth + colWidth / 2, y: d * rowHeight + rowHeight / 2 });
13963
+ }
13964
+ return positions;
13965
+ }
13966
+ function layoutLayered(nodes, edges, width, height) {
13967
+ const nodeIds = nodes.map((n) => n.id);
13968
+ const idSet = new Set(nodeIds);
13969
+ const adj = /* @__PURE__ */ new Map();
13970
+ const remainingIndegree = /* @__PURE__ */ new Map();
13971
+ for (const id of nodeIds) remainingIndegree.set(id, 0);
13972
+ for (const e of edges) {
13973
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
13974
+ const list = adj.get(e.from) ?? [];
13975
+ list.push(e.to);
13976
+ adj.set(e.from, list);
13977
+ remainingIndegree.set(e.to, (remainingIndegree.get(e.to) ?? 0) + 1);
13978
+ }
13979
+ const layer = /* @__PURE__ */ new Map();
13980
+ const dequeued = /* @__PURE__ */ new Set();
13981
+ const queue = [];
13982
+ for (const id of nodeIds) {
13983
+ if ((remainingIndegree.get(id) ?? 0) === 0) {
13984
+ layer.set(id, 0);
13985
+ queue.push(id);
13986
+ }
13987
+ }
13988
+ while (queue.length > 0) {
13989
+ const u = queue.shift();
13990
+ dequeued.add(u);
13991
+ for (const v of adj.get(u) ?? []) {
13992
+ const candidate = (layer.get(u) ?? 0) + 1;
13993
+ layer.set(v, Math.max(layer.get(v) ?? 0, candidate));
13994
+ remainingIndegree.set(v, (remainingIndegree.get(v) ?? 0) - 1);
13995
+ if ((remainingIndegree.get(v) ?? 0) === 0 && !dequeued.has(v)) {
13996
+ queue.push(v);
13997
+ }
13998
+ }
13999
+ }
14000
+ let baseMaxLayer = 0;
14001
+ for (const id of nodeIds) {
14002
+ if (dequeued.has(id)) baseMaxLayer = Math.max(baseMaxLayer, layer.get(id) ?? 0);
14003
+ }
14004
+ const cycleLayer = baseMaxLayer + 1;
14005
+ let maxLayer = baseMaxLayer;
14006
+ for (const id of nodeIds) {
14007
+ if (!dequeued.has(id)) {
14008
+ layer.set(id, cycleLayer);
14009
+ maxLayer = cycleLayer;
14010
+ }
14011
+ }
14012
+ const colWidth = width / Math.max(1, maxLayer + 1);
14013
+ const byLayer = /* @__PURE__ */ new Map();
14014
+ for (const id of nodeIds) {
14015
+ const l = layer.get(id) ?? 0;
14016
+ const list = byLayer.get(l) ?? [];
14017
+ list.push(id);
14018
+ byLayer.set(l, list);
14019
+ }
14020
+ const positions = /* @__PURE__ */ new Map();
14021
+ for (const [l, ids] of byLayer) {
14022
+ const rowHeight = height / ids.length;
14023
+ ids.forEach((id, i) => {
14024
+ positions.set(id, { x: l * colWidth + colWidth / 2, y: i * rowHeight + rowHeight / 2 });
14025
+ });
14026
+ }
14027
+ return positions;
14028
+ }
14029
+ function computePositions(nodes, edges, layout, root, width, height) {
14030
+ switch (layout) {
14031
+ case "circle":
14032
+ return layoutCircle(nodes, width, height);
14033
+ case "tree":
14034
+ return layoutTree(nodes, edges, root, width, height);
14035
+ case "layered":
14036
+ return layoutLayered(nodes, edges, width, height);
14037
+ case "manual":
14038
+ default: {
14039
+ const positions = /* @__PURE__ */ new Map();
14040
+ for (const n of nodes) positions.set(n.id, { x: n.x ?? 0, y: n.y ?? 0 });
14041
+ return positions;
14042
+ }
14043
+ }
14044
+ }
14045
+ var NODE_STATE_COLOR, EDGE_STATE_COLOR, DEFAULT_NODE_RADIUS, AlgoGraphCanvas;
14046
+ var init_AlgoGraphCanvas = __esm({
14047
+ "components/learning/molecules/AlgoGraphCanvas.tsx"() {
14048
+ "use client";
14049
+ init_atoms();
14050
+ init_Stack();
14051
+ init_LearningCanvas();
14052
+ NODE_STATE_COLOR = {
14053
+ unvisited: "#cbd5e1",
14054
+ frontier: "#f59e0b",
14055
+ current: "#ef4444",
14056
+ visited: "#22c55e",
14057
+ goal: "#8b5cf6",
14058
+ path: "#0ea5e9"
14059
+ };
14060
+ EDGE_STATE_COLOR = {
14061
+ default: "#9ca3af",
14062
+ tree: "#16a34a",
14063
+ relaxed: "#f59e0b",
14064
+ candidate: "#38bdf8",
14065
+ path: "#dc2626"
14066
+ };
14067
+ DEFAULT_NODE_RADIUS = 18;
14068
+ AlgoGraphCanvas = ({
14069
+ className,
14070
+ width = 600,
14071
+ height = 400,
14072
+ title,
14073
+ backgroundColor,
14074
+ nodes = [],
14075
+ edges = [],
14076
+ layout = "manual",
14077
+ root,
14078
+ shapes = [],
14079
+ interactive = false,
14080
+ animate = false,
14081
+ onShapeClick,
14082
+ onNodeClick,
14083
+ isLoading,
14084
+ error
14085
+ }) => {
14086
+ const nodeById = useMemo(() => {
14087
+ const m = /* @__PURE__ */ new Map();
14088
+ for (const n of nodes) m.set(n.id, n);
14089
+ return m;
14090
+ }, [nodes]);
14091
+ const nodeIndexById = useMemo(() => {
14092
+ const m = /* @__PURE__ */ new Map();
14093
+ nodes.forEach((n, i) => m.set(n.id, i));
14094
+ return m;
14095
+ }, [nodes]);
14096
+ const derivedShapes = useMemo(() => {
14097
+ const out = [];
14098
+ const positions = computePositions(nodes, edges, layout, root, width, height);
14099
+ const edgeGeoms = [];
14100
+ for (const e of edges) {
14101
+ const a = nodeById.get(e.from);
14102
+ const b = nodeById.get(e.to);
14103
+ const posA = positions.get(e.from);
14104
+ const posB = positions.get(e.to);
14105
+ if (!a || !b || !posA || !posB) continue;
14106
+ const rA = a.radius ?? DEFAULT_NODE_RADIUS;
14107
+ const rB = b.radius ?? DEFAULT_NODE_RADIUS;
14108
+ const dx = posB.x - posA.x;
14109
+ const dy = posB.y - posA.y;
14110
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
14111
+ const ux = dx / dist;
14112
+ const uy = dy / dist;
14113
+ edgeGeoms.push({
14114
+ directed: e.directed ?? false,
14115
+ x1: posA.x + ux * rA,
14116
+ y1: posA.y + uy * rA,
14117
+ x2: posB.x - ux * rB,
14118
+ y2: posB.y - uy * rB,
14119
+ color: e.color ?? EDGE_STATE_COLOR[e.state ?? "default"],
14120
+ label: e.label ?? (e.weight != null ? String(e.weight) : void 0)
14121
+ });
14122
+ }
14123
+ for (const g of edgeGeoms) {
14124
+ out.push({
14125
+ type: g.directed ? "arrow" : "line",
14126
+ x1: g.x1,
14127
+ y1: g.y1,
14128
+ x2: g.x2,
14129
+ y2: g.y2,
14130
+ color: g.color,
14131
+ lineWidth: 2
14132
+ });
14133
+ }
14134
+ for (const g of edgeGeoms) {
14135
+ if (g.label === void 0) continue;
14136
+ const midX = (g.x1 + g.x2) / 2;
14137
+ const midY = (g.y1 + g.y2) / 2;
14138
+ const dx = g.x2 - g.x1;
14139
+ const dy = g.y2 - g.y1;
14140
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
14141
+ const perpX = -(dy / dist);
14142
+ const perpY = dx / dist;
14143
+ out.push({
14144
+ type: "text",
14145
+ x: midX + perpX * 10,
14146
+ y: midY + perpY * 10,
14147
+ text: g.label,
14148
+ fontSize: 11,
14149
+ align: "center",
14150
+ color: "#374151"
14151
+ });
14152
+ }
14153
+ const nodeGeoms = [];
14154
+ for (const n of nodes) {
14155
+ const pos = positions.get(n.id);
14156
+ if (!pos) continue;
14157
+ nodeGeoms.push({
14158
+ id: n.id,
14159
+ x: pos.x,
14160
+ y: pos.y,
14161
+ radius: n.radius ?? DEFAULT_NODE_RADIUS,
14162
+ color: n.color ?? NODE_STATE_COLOR[n.state ?? "unvisited"],
14163
+ label: n.label,
14164
+ badge: n.badge
14165
+ });
14166
+ }
14167
+ for (const g of nodeGeoms) {
14168
+ out.push({ type: "circle", id: g.id, x: g.x, y: g.y, radius: g.radius, color: g.color, fill: `${g.color}33` });
14169
+ }
14170
+ const badgeGeoms = [];
14171
+ for (const g of nodeGeoms) {
14172
+ if (!g.badge) continue;
14173
+ const w = Math.min(42, Math.max(18, g.badge.text.length * 6 + 10));
14174
+ badgeGeoms.push({
14175
+ cx: g.x + g.radius * 0.75,
14176
+ cy: g.y - g.radius * 0.75,
14177
+ w,
14178
+ h: 14,
14179
+ // Borderless pill: same color drives both stroke and fill.
14180
+ color: g.badge.color ?? "#1e293b",
14181
+ text: g.badge.text
14182
+ });
14183
+ }
14184
+ for (const b of badgeGeoms) {
14185
+ 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 });
14186
+ }
14187
+ for (const b of badgeGeoms) {
14188
+ out.push({ type: "text", x: b.cx, y: b.cy, text: b.text, fontSize: 9, align: "center", color: "#ffffff" });
14189
+ }
14190
+ for (const g of nodeGeoms) {
14191
+ if (g.label === void 0) continue;
14192
+ out.push({
14193
+ type: "text",
14194
+ x: g.x,
14195
+ y: g.y + g.radius + 14,
14196
+ text: g.label,
14197
+ fontSize: 12,
14198
+ align: "center",
14199
+ color: "#111827"
14200
+ });
14201
+ }
14202
+ out.push(...shapes);
14203
+ return out;
14204
+ }, [nodes, edges, layout, root, width, height, nodeById, shapes]);
14205
+ const handleShapeClick = useCallback(
14206
+ (payload) => {
14207
+ if (payload.type === "circle" && payload.id) {
14208
+ const node = nodeById.get(payload.id);
14209
+ const idx = nodeIndexById.get(payload.id);
14210
+ if (node && idx !== void 0) {
14211
+ onNodeClick?.({ id: node.id, label: node.label, index: idx });
14212
+ }
14213
+ }
14214
+ onShapeClick?.(payload);
14215
+ },
14216
+ [nodeById, nodeIndexById, onNodeClick, onShapeClick]
14217
+ );
14218
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
14219
+ title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
14220
+ /* @__PURE__ */ jsx(
14221
+ LearningCanvas,
14222
+ {
14223
+ width,
14224
+ height,
14225
+ backgroundColor,
14226
+ shapes: derivedShapes,
14227
+ interactive,
14228
+ animate,
14229
+ onShapeClick: onShapeClick || onNodeClick ? handleShapeClick : void 0,
14230
+ isLoading,
14231
+ error
14232
+ }
14233
+ )
14234
+ ] }) });
14235
+ };
14236
+ }
14237
+ });
14238
+ 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;
13721
14239
  var init_AlgorithmCanvas = __esm({
13722
14240
  "components/learning/molecules/AlgorithmCanvas.tsx"() {
13723
14241
  "use client";
@@ -13729,6 +14247,43 @@ var init_AlgorithmCanvas = __esm({
13729
14247
  DEFAULT_POINTER_COLOR = "#dc2626";
13730
14248
  POINTER_BAND = 34;
13731
14249
  TOP_PAD = 26;
14250
+ PANEL_FAMILY_ORDER = ["bars", "slots", "cells", "buckets", "frames"];
14251
+ RANGE_COLOR_DEFAULT = "#3b82f6";
14252
+ RANGE_FILL_OPACITY = 0.15;
14253
+ BRACKET_TOP_OFFSET = 16;
14254
+ BRACKET_ROW_H = 14;
14255
+ BRACKET_TICK_H = 6;
14256
+ BRACKET_LABEL_OFFSET = 6;
14257
+ SLOT_EMPTY_FILL = "#f1f5f9";
14258
+ SLOT_EMPTY_STROKE = "#cbd5e1";
14259
+ SLOT_FILLED_STROKE = "#9ca3af";
14260
+ SLOT_HIGHLIGHT_DEFAULT = "#f59e0b";
14261
+ SLOT_VALUE_TEXT_COLOR = "#ffffff";
14262
+ FRAME_ACTIVE_COLOR = "#3b82f6";
14263
+ FRAME_RETURNING_COLOR = "#f59e0b";
14264
+ FRAME_DONE_COLOR = "#94a3b8";
14265
+ FRAME_LABEL_COLOR = "#ffffff";
14266
+ FRAME_DETAIL_COLOR = "#e2e8f0";
14267
+ FRAME_TWO_LINE_MIN_H = 22;
14268
+ BUCKET_INDEX_FILL = "#e2e8f0";
14269
+ BUCKET_INDEX_STROKE = "#9ca3af";
14270
+ BUCKET_INDEX_TEXT = "#374151";
14271
+ BUCKET_ENTRY_TEXT = "#ffffff";
14272
+ BUCKET_ENTRY_DEFAULT = "#3b82f6";
14273
+ BUCKET_ENTRY_HIGHLIGHT = "#f59e0b";
14274
+ BUCKET_ENTRY_PROBING = "#38bdf8";
14275
+ BUCKET_ENTRY_MIN_W = 24;
14276
+ BUCKET_ENTRY_MAX_W = 64;
14277
+ AXIS_LABEL_COLOR = "#6b7280";
14278
+ AXIS_LABEL_FONT_SIZE = 10;
14279
+ CORNER_TEXT_COLOR = "#111827";
14280
+ CORNER_FONT_SIZE = 7;
14281
+ CORNER_MIN_CELL = 28;
14282
+ CORNER_INSET_X = 3;
14283
+ CORNER_INSET_Y = 6;
14284
+ AUX_PRIMARY_RATIO = 0.6;
14285
+ AUX_LABEL_BAND = 18;
14286
+ AUX_BASELINE_PAD = 8;
13732
14287
  AlgorithmCanvas = ({
13733
14288
  className,
13734
14289
  width = 600,
@@ -13738,6 +14293,14 @@ var init_AlgorithmCanvas = __esm({
13738
14293
  bars = [],
13739
14294
  cells = [],
13740
14295
  pointers = [],
14296
+ ranges = [],
14297
+ slots = [],
14298
+ slotOrientation = "horizontal",
14299
+ frames = [],
14300
+ buckets = [],
14301
+ auxBars = [],
14302
+ rowLabels = [],
14303
+ colLabels = [],
13741
14304
  shapes = [],
13742
14305
  interactive = false,
13743
14306
  animate = false,
@@ -13747,12 +14310,35 @@ var init_AlgorithmCanvas = __esm({
13747
14310
  }) => {
13748
14311
  const derivedShapes = useMemo(() => {
13749
14312
  const out = [];
14313
+ const presence = {
14314
+ bars: bars.length > 0,
14315
+ slots: slots.length > 0,
14316
+ cells: cells.length > 0,
14317
+ buckets: buckets.length > 0,
14318
+ frames: frames.length > 0
14319
+ };
14320
+ const panelCount = PANEL_FAMILY_ORDER.filter((f3) => presence[f3]).length;
14321
+ const panelHeight = height / Math.max(1, panelCount);
14322
+ const panelY = { bars: 0, slots: 0, cells: 0, buckets: 0, frames: 0 };
14323
+ let compactIndex = 0;
14324
+ PANEL_FAMILY_ORDER.forEach((f3) => {
14325
+ if (presence[f3]) {
14326
+ panelY[f3] = compactIndex * panelHeight;
14327
+ compactIndex += 1;
14328
+ }
14329
+ });
13750
14330
  if (bars.length > 0) {
14331
+ const panelYBars = panelY.bars;
13751
14332
  const slot = width / bars.length;
13752
14333
  const barW = slot * 0.8;
13753
14334
  const gap = slot * 0.1;
13754
- const baseline = height - POINTER_BAND;
13755
- const usableH = baseline - TOP_PAD;
14335
+ const bracketRanges = ranges.filter((r) => r.kind === "bracket");
14336
+ const bracketCount = bracketRanges.length;
14337
+ const bracketHeadroom = bracketCount > 0 ? BRACKET_TOP_OFFSET + bracketCount * BRACKET_ROW_H : 0;
14338
+ const hasAux = auxBars.length > 0;
14339
+ const primaryH = hasAux ? panelHeight * AUX_PRIMARY_RATIO : panelHeight;
14340
+ const baseline = panelYBars + primaryH - POINTER_BAND;
14341
+ const usableH = baseline - (panelYBars + TOP_PAD + bracketHeadroom);
13756
14342
  const maxV = Math.max(1, ...bars.map((b) => Number.isFinite(b.value) ? b.value : 0));
13757
14343
  bars.forEach((bar, i) => {
13758
14344
  const v = Number.isFinite(bar.value) ? bar.value : 0;
@@ -13782,6 +14368,89 @@ var init_AlgorithmCanvas = __esm({
13782
14368
  });
13783
14369
  }
13784
14370
  });
14371
+ ranges.forEach((r) => {
14372
+ const kind = r.kind ?? "fill";
14373
+ if (kind !== "fill") return;
14374
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14375
+ out.push({
14376
+ type: "rect",
14377
+ x: r.from * slot,
14378
+ y: panelYBars,
14379
+ width: (r.to - r.from + 1) * slot,
14380
+ height: primaryH,
14381
+ color,
14382
+ fill: color,
14383
+ opacity: RANGE_FILL_OPACITY
14384
+ });
14385
+ if (r.label) {
14386
+ out.push({
14387
+ type: "text",
14388
+ x: r.from * slot + 4,
14389
+ // Sits below the bracket block (if any) so fill and bracket labels never collide.
14390
+ y: panelYBars + 10 + bracketHeadroom,
14391
+ text: r.label,
14392
+ color,
14393
+ fontSize: 10,
14394
+ align: "left"
14395
+ });
14396
+ }
14397
+ });
14398
+ bracketRanges.forEach((r, i) => {
14399
+ const bracketY = panelYBars + BRACKET_TOP_OFFSET + i * BRACKET_ROW_H;
14400
+ const x1 = r.from * slot + slot * 0.1;
14401
+ const x2 = (r.to + 1) * slot - slot * 0.1;
14402
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14403
+ out.push({ type: "line", x1, y1: bracketY, x2, y2: bracketY, color, lineWidth: 2 });
14404
+ out.push({ type: "line", x1, y1: bracketY, x2: x1, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14405
+ out.push({ type: "line", x1: x2, y1: bracketY, x2, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14406
+ if (r.label) {
14407
+ out.push({
14408
+ type: "text",
14409
+ x: (x1 + x2) / 2,
14410
+ y: bracketY - BRACKET_LABEL_OFFSET,
14411
+ text: r.label,
14412
+ color,
14413
+ fontSize: 10,
14414
+ align: "center"
14415
+ });
14416
+ }
14417
+ });
14418
+ if (hasAux) {
14419
+ const auxH = panelHeight - primaryH;
14420
+ const slot2 = width / auxBars.length;
14421
+ const auxBaseline = panelYBars + primaryH + auxH - AUX_BASELINE_PAD;
14422
+ const auxUsableH = auxBaseline - (panelYBars + primaryH + AUX_LABEL_BAND);
14423
+ const maxAuxV = Math.max(1, ...auxBars.map((b) => Number.isFinite(b.value) ? b.value : 0));
14424
+ auxBars.forEach((bar, i) => {
14425
+ const v = Number.isFinite(bar.value) ? bar.value : 0;
14426
+ const bh = Math.max(0, v / maxAuxV * auxUsableH);
14427
+ const x = i * slot2 + slot2 * 0.1;
14428
+ const w = slot2 * 0.8;
14429
+ const color = bar.color ?? DEFAULT_BAR_COLOR;
14430
+ out.push({
14431
+ type: "rect",
14432
+ id: `auxbar-${i}`,
14433
+ x,
14434
+ y: auxBaseline - bh,
14435
+ width: w,
14436
+ height: bh,
14437
+ color,
14438
+ fill: color
14439
+ });
14440
+ const label = bar.label ?? (auxBars.length <= 24 ? String(v) : void 0);
14441
+ if (label) {
14442
+ out.push({
14443
+ type: "text",
14444
+ x: x + w / 2,
14445
+ y: auxBaseline - bh - 8,
14446
+ text: label,
14447
+ color: "#374151",
14448
+ fontSize: 11,
14449
+ align: "center"
14450
+ });
14451
+ }
14452
+ });
14453
+ }
13785
14454
  pointers.forEach((p) => {
13786
14455
  if (p.index < 0 || p.index >= bars.length) return;
13787
14456
  const cx = p.index * slot + slot / 2;
@@ -13789,7 +14458,7 @@ var init_AlgorithmCanvas = __esm({
13789
14458
  out.push({
13790
14459
  type: "arrow",
13791
14460
  x1: cx,
13792
- y1: height - 6,
14461
+ y1: panelYBars + primaryH - 18,
13793
14462
  x2: cx,
13794
14463
  y2: baseline + 4,
13795
14464
  color,
@@ -13799,7 +14468,7 @@ var init_AlgorithmCanvas = __esm({
13799
14468
  out.push({
13800
14469
  type: "text",
13801
14470
  x: cx,
13802
- y: height - 22,
14471
+ y: panelYBars + primaryH - 8,
13803
14472
  text: p.label,
13804
14473
  color,
13805
14474
  fontSize: 11,
@@ -13808,14 +14477,111 @@ var init_AlgorithmCanvas = __esm({
13808
14477
  }
13809
14478
  });
13810
14479
  }
14480
+ if (slots.length > 0) {
14481
+ const panelYSlots = panelY.slots;
14482
+ const n = slots.length;
14483
+ const vertical = slotOrientation === "vertical";
14484
+ const vBoxH = panelHeight / n;
14485
+ const vBoxW = Math.min(width * 0.5, 120);
14486
+ const vBoxX = (width - vBoxW) / 2;
14487
+ const hCellW = width / n;
14488
+ const hBoxW = hCellW * 0.82;
14489
+ const hBoxH = Math.min(panelHeight * 0.6, 48);
14490
+ const hBoxY = panelYSlots + (panelHeight - hBoxH) / 2;
14491
+ 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 };
14492
+ slots.forEach((s, i) => {
14493
+ const box = slotBox(i);
14494
+ const state = s.state ?? "filled";
14495
+ const fill = state === "empty" ? SLOT_EMPTY_FILL : state === "highlight" ? s.color ?? SLOT_HIGHLIGHT_DEFAULT : s.color ?? DEFAULT_BAR_COLOR;
14496
+ const stroke = state === "empty" ? SLOT_EMPTY_STROKE : SLOT_FILLED_STROKE;
14497
+ out.push({
14498
+ type: "rect",
14499
+ id: `slot-${i}`,
14500
+ x: box.x,
14501
+ y: box.y,
14502
+ width: box.width,
14503
+ height: box.height,
14504
+ color: stroke,
14505
+ fill
14506
+ });
14507
+ if (s.value != null && state !== "empty") {
14508
+ out.push({
14509
+ type: "text",
14510
+ x: box.x + box.width / 2,
14511
+ y: box.y + box.height / 2,
14512
+ text: String(s.value),
14513
+ color: SLOT_VALUE_TEXT_COLOR,
14514
+ fontSize: 12,
14515
+ align: "center"
14516
+ });
14517
+ }
14518
+ });
14519
+ if (bars.length === 0) {
14520
+ pointers.forEach((p) => {
14521
+ if (p.index < 0 || p.index >= slots.length) return;
14522
+ const box = slotBox(p.index);
14523
+ const color = p.color ?? DEFAULT_POINTER_COLOR;
14524
+ if (vertical) {
14525
+ const cy = box.y + box.height / 2;
14526
+ out.push({
14527
+ type: "arrow",
14528
+ x1: box.x + box.width + 34,
14529
+ y1: cy,
14530
+ x2: box.x + box.width + 4,
14531
+ y2: cy,
14532
+ color,
14533
+ lineWidth: 2
14534
+ });
14535
+ if (p.label) {
14536
+ out.push({
14537
+ type: "text",
14538
+ x: box.x + box.width + 38,
14539
+ y: cy,
14540
+ text: p.label,
14541
+ color,
14542
+ fontSize: 11,
14543
+ align: "left"
14544
+ });
14545
+ }
14546
+ } else {
14547
+ const cx = box.x + box.width / 2;
14548
+ out.push({
14549
+ type: "arrow",
14550
+ x1: cx,
14551
+ y1: panelYSlots + panelHeight - 18,
14552
+ x2: cx,
14553
+ y2: box.y + box.height + 4,
14554
+ color,
14555
+ lineWidth: 2
14556
+ });
14557
+ if (p.label) {
14558
+ out.push({
14559
+ type: "text",
14560
+ x: cx,
14561
+ y: panelYSlots + panelHeight - 8,
14562
+ text: p.label,
14563
+ color,
14564
+ fontSize: 11,
14565
+ align: "center"
14566
+ });
14567
+ }
14568
+ }
14569
+ });
14570
+ }
14571
+ }
13811
14572
  if (cells.length > 0) {
14573
+ const panelYCells = panelY.cells;
13812
14574
  const maxCol = Math.max(0, ...cells.map((c) => c.col)) + 1;
13813
14575
  const maxRow = Math.max(0, ...cells.map((c) => c.row)) + 1;
13814
- const cw = width / maxCol;
13815
- const ch = height / maxRow;
14576
+ const colLabelH = colLabels.length > 0 ? 16 : 0;
14577
+ const rowLabelW = rowLabels.length > 0 ? 20 : 0;
14578
+ const gridX0 = rowLabelW;
14579
+ const gridY0 = panelYCells + colLabelH;
14580
+ const cw = (width - rowLabelW) / maxCol;
14581
+ const ch = (panelHeight - colLabelH) / maxRow;
13816
14582
  cells.forEach((c, i) => {
13817
- const x = c.col * cw;
13818
- const y = c.row * ch;
14583
+ const x = gridX0 + c.col * cw;
14584
+ const y = gridY0 + c.row * ch;
13819
14585
  const color = c.color ?? DEFAULT_CELL_COLOR;
13820
14586
  out.push({
13821
14587
  type: "rect",
@@ -13839,11 +14605,207 @@ var init_AlgorithmCanvas = __esm({
13839
14605
  align: "center"
13840
14606
  });
13841
14607
  }
14608
+ if (c.corner && cw >= CORNER_MIN_CELL && ch >= CORNER_MIN_CELL) {
14609
+ const { tl, tr, bl, br } = c.corner;
14610
+ if (tl) {
14611
+ out.push({
14612
+ type: "text",
14613
+ x: x + CORNER_INSET_X,
14614
+ y: y + CORNER_INSET_Y,
14615
+ text: tl,
14616
+ color: CORNER_TEXT_COLOR,
14617
+ fontSize: CORNER_FONT_SIZE,
14618
+ align: "left"
14619
+ });
14620
+ }
14621
+ if (tr) {
14622
+ out.push({
14623
+ type: "text",
14624
+ x: x + cw - CORNER_INSET_X,
14625
+ y: y + CORNER_INSET_Y,
14626
+ text: tr,
14627
+ color: CORNER_TEXT_COLOR,
14628
+ fontSize: CORNER_FONT_SIZE,
14629
+ align: "right"
14630
+ });
14631
+ }
14632
+ if (bl) {
14633
+ out.push({
14634
+ type: "text",
14635
+ x: x + CORNER_INSET_X,
14636
+ y: y + ch - CORNER_INSET_Y,
14637
+ text: bl,
14638
+ color: CORNER_TEXT_COLOR,
14639
+ fontSize: CORNER_FONT_SIZE,
14640
+ align: "left"
14641
+ });
14642
+ }
14643
+ if (br) {
14644
+ out.push({
14645
+ type: "text",
14646
+ x: x + cw - CORNER_INSET_X,
14647
+ y: y + ch - CORNER_INSET_Y,
14648
+ text: br,
14649
+ color: CORNER_TEXT_COLOR,
14650
+ fontSize: CORNER_FONT_SIZE,
14651
+ align: "right"
14652
+ });
14653
+ }
14654
+ }
14655
+ });
14656
+ colLabels.forEach((l) => {
14657
+ out.push({
14658
+ type: "text",
14659
+ x: gridX0 + l.index * cw + cw / 2,
14660
+ y: panelYCells + colLabelH / 2,
14661
+ text: l.text,
14662
+ color: l.color ?? AXIS_LABEL_COLOR,
14663
+ fontSize: AXIS_LABEL_FONT_SIZE,
14664
+ align: "center"
14665
+ });
14666
+ });
14667
+ rowLabels.forEach((l) => {
14668
+ out.push({
14669
+ type: "text",
14670
+ x: rowLabelW - 6,
14671
+ y: gridY0 + l.index * ch + ch / 2,
14672
+ text: l.text,
14673
+ color: l.color ?? AXIS_LABEL_COLOR,
14674
+ fontSize: AXIS_LABEL_FONT_SIZE,
14675
+ align: "right"
14676
+ });
14677
+ });
14678
+ }
14679
+ if (buckets.length > 0) {
14680
+ const panelYBuckets = panelY.buckets;
14681
+ const bucketCount = Math.max(0, ...buckets.map((b) => b.index)) + 1;
14682
+ const rowH = panelHeight / bucketCount;
14683
+ const indexColW = Math.min(width * 0.12, 40);
14684
+ const maxChainLen = Math.max(1, ...buckets.map((b) => b.entries.length));
14685
+ const entryW = Math.min(BUCKET_ENTRY_MAX_W, Math.max(BUCKET_ENTRY_MIN_W, (width - indexColW - 8) / maxChainLen));
14686
+ const maxVisible = Math.floor((width - indexColW - 4) / entryW);
14687
+ buckets.forEach((b) => {
14688
+ const rowY = panelYBuckets + b.index * rowH;
14689
+ out.push({
14690
+ type: "rect",
14691
+ id: `bucket-index-${b.index}`,
14692
+ x: 2,
14693
+ y: rowY + 2,
14694
+ width: indexColW - 4,
14695
+ height: rowH - 4,
14696
+ color: BUCKET_INDEX_STROKE,
14697
+ fill: BUCKET_INDEX_FILL
14698
+ });
14699
+ out.push({
14700
+ type: "text",
14701
+ x: 2 + (indexColW - 4) / 2,
14702
+ y: rowY + rowH / 2,
14703
+ text: String(b.index),
14704
+ color: BUCKET_INDEX_TEXT,
14705
+ fontSize: 10,
14706
+ align: "center"
14707
+ });
14708
+ const overflow = b.entries.length > maxVisible;
14709
+ const visibleCount = overflow ? Math.max(0, maxVisible - 1) : b.entries.length;
14710
+ for (let j = 0; j < visibleCount; j++) {
14711
+ const entry = b.entries[j];
14712
+ const ex = indexColW + 4 + j * entryW;
14713
+ const state = entry.state ?? "default";
14714
+ const fill = state === "highlight" ? entry.color ?? BUCKET_ENTRY_HIGHLIGHT : state === "probing" ? entry.color ?? BUCKET_ENTRY_PROBING : entry.color ?? BUCKET_ENTRY_DEFAULT;
14715
+ out.push({
14716
+ type: "rect",
14717
+ id: `bucket-${b.index}-${j}`,
14718
+ x: ex,
14719
+ y: rowY + 2,
14720
+ width: entryW - 2,
14721
+ height: rowH - 4,
14722
+ color: fill,
14723
+ fill
14724
+ });
14725
+ if (entryW >= 20 && rowH >= 16) {
14726
+ out.push({
14727
+ type: "text",
14728
+ x: ex + (entryW - 2) / 2,
14729
+ y: rowY + rowH / 2,
14730
+ text: entry.label,
14731
+ color: BUCKET_ENTRY_TEXT,
14732
+ fontSize: 10,
14733
+ align: "center"
14734
+ });
14735
+ }
14736
+ }
14737
+ if (overflow) {
14738
+ const ex = indexColW + 4 + visibleCount * entryW;
14739
+ out.push({
14740
+ type: "rect",
14741
+ id: `bucket-${b.index}-overflow`,
14742
+ x: ex,
14743
+ y: rowY + 2,
14744
+ width: entryW - 2,
14745
+ height: rowH - 4,
14746
+ color: BUCKET_ENTRY_DEFAULT,
14747
+ fill: BUCKET_ENTRY_DEFAULT
14748
+ });
14749
+ out.push({
14750
+ type: "text",
14751
+ x: ex + (entryW - 2) / 2,
14752
+ y: rowY + rowH / 2,
14753
+ text: `+${b.entries.length - visibleCount}`,
14754
+ color: BUCKET_ENTRY_TEXT,
14755
+ fontSize: 10,
14756
+ align: "center"
14757
+ });
14758
+ }
14759
+ });
14760
+ }
14761
+ if (frames.length > 0) {
14762
+ const panelYFrames = panelY.frames;
14763
+ const n = frames.length;
14764
+ const frameH = panelHeight / n;
14765
+ const x = 8;
14766
+ const w = width - 16;
14767
+ frames.forEach((f3, i) => {
14768
+ const y = panelYFrames + panelHeight - (i + 1) * frameH;
14769
+ const state = f3.state ?? "active";
14770
+ const fill = state === "returning" ? f3.color ?? FRAME_RETURNING_COLOR : state === "done" ? f3.color ?? FRAME_DONE_COLOR : f3.color ?? FRAME_ACTIVE_COLOR;
14771
+ out.push({ type: "rect", id: `frame-${i}`, x, y, width: w, height: frameH, color: fill, fill });
14772
+ if (frameH >= FRAME_TWO_LINE_MIN_H) {
14773
+ out.push({
14774
+ type: "text",
14775
+ x: 16,
14776
+ y: y + frameH * 0.35,
14777
+ text: f3.label,
14778
+ color: FRAME_LABEL_COLOR,
14779
+ fontSize: 10,
14780
+ align: "left"
14781
+ });
14782
+ if (f3.detail) {
14783
+ out.push({
14784
+ type: "text",
14785
+ x: 16,
14786
+ y: y + frameH * 0.7,
14787
+ text: f3.detail,
14788
+ color: FRAME_DETAIL_COLOR,
14789
+ fontSize: 10,
14790
+ align: "left"
14791
+ });
14792
+ }
14793
+ } else {
14794
+ out.push({
14795
+ type: "text",
14796
+ x: 16,
14797
+ y: y + frameH / 2,
14798
+ text: f3.label,
14799
+ color: FRAME_LABEL_COLOR,
14800
+ fontSize: 10,
14801
+ align: "left"
14802
+ });
14803
+ }
13842
14804
  });
13843
14805
  }
13844
14806
  out.push(...shapes);
13845
14807
  return out;
13846
- }, [bars, cells, pointers, shapes, width, height]);
14808
+ }, [bars, cells, pointers, ranges, slots, slotOrientation, frames, buckets, auxBars, rowLabels, colLabels, shapes, width, height]);
13847
14809
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
13848
14810
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
13849
14811
  /* @__PURE__ */ jsx(
@@ -14832,23 +15794,422 @@ var init_BehaviorView = __esm({
14832
15794
  BehaviorView.displayName = "BehaviorView";
14833
15795
  }
14834
15796
  });
14835
- var BiologyCanvas;
15797
+ function LearningScene3D({
15798
+ className,
15799
+ width = 600,
15800
+ height = 400,
15801
+ title,
15802
+ backgroundColor,
15803
+ drawables,
15804
+ camera,
15805
+ lighting,
15806
+ post,
15807
+ showGrid = false,
15808
+ shadows,
15809
+ interactive,
15810
+ isLoading,
15811
+ error,
15812
+ onItemClick
15813
+ }) {
15814
+ const instanceId = useId().replace(/[^a-zA-Z0-9]/g, "");
15815
+ const clickEvent = onItemClick ? `LEARNING_SCENE_3D.ITEM_CLICK.${instanceId}` : void 0;
15816
+ const onItemClickRef = useRef(onItemClick);
15817
+ onItemClickRef.current = onItemClick;
15818
+ useEventListener(`UI:${clickEvent ?? "LEARNING_SCENE_3D.ITEM_CLICK.__disabled"}`, (event) => {
15819
+ const unitId = event.payload?.unitId;
15820
+ if (typeof unitId === "string") onItemClickRef.current?.(unitId);
15821
+ });
15822
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && post?.bloom) {
15823
+ const unknownKeys = Object.keys(post.bloom).filter((k) => !KNOWN_BLOOM_KEYS.has(k));
15824
+ if (unknownKeys.length > 0) {
15825
+ sceneLog.debug("post.bloom has unrecognized keys \u2014 only intensity/threshold/smoothing are read", { unknownKeys });
15826
+ }
15827
+ }
15828
+ const props3d = {
15829
+ drawables,
15830
+ isLoading,
15831
+ error: error ? error.message : null,
15832
+ cameraMode: camera?.mode ?? "perspective",
15833
+ ...camera?.zoom !== void 0 ? { zoom: camera.zoom } : {},
15834
+ ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
15835
+ ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
15836
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
15837
+ ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
15838
+ backgroundColor,
15839
+ showGrid,
15840
+ ...shadows !== void 0 ? { shadows } : {},
15841
+ ...interactive !== void 0 ? { controlsEnabled: interactive } : {},
15842
+ lighting,
15843
+ post,
15844
+ ...clickEvent !== void 0 ? { unitClickEvent: clickEvent } : {}
15845
+ };
15846
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
15847
+ title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
15848
+ /* @__PURE__ */ jsx("div", { style: { width, height, display: "flex" }, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(Canvas3DHost2, { ...props3d }) }) })
15849
+ ] }) });
15850
+ }
15851
+ function meshSphere(id, x, y, z, radius, color, opts) {
15852
+ return {
15853
+ type: "draw-mesh",
15854
+ ...id !== void 0 ? { id } : {},
15855
+ shape: opts?.shape ?? "sphere",
15856
+ position: { x, y, z },
15857
+ radius,
15858
+ pivot: "center",
15859
+ segments: opts?.segments ?? 24,
15860
+ material: { color, ...opts?.material },
15861
+ ...opts?.opacity !== void 0 ? { opacity: opts.opacity } : {}
15862
+ };
15863
+ }
15864
+ function axisRotation(from, to) {
15865
+ const wx = to[0] - from[0];
15866
+ const wy = to[2] - from[2];
15867
+ const wz = to[1] - from[1];
15868
+ const len = Math.sqrt(wx * wx + wy * wy + wz * wz);
15869
+ const tilt = Math.acos(Math.min(1, Math.max(-1, len > 0 ? wy / len : 1)));
15870
+ return [0, Math.atan2(wz, -wx), tilt];
15871
+ }
15872
+ function segmentLength(from, to) {
15873
+ const dx = to[0] - from[0];
15874
+ const dy = to[1] - from[1];
15875
+ const dz = to[2] - from[2];
15876
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
15877
+ }
15878
+ function cylinderBetween(from, to, radius, color) {
15879
+ const len = segmentLength(from, to);
15880
+ if (len < 1e-6) return null;
15881
+ return {
15882
+ type: "draw-mesh",
15883
+ shape: "cylinder",
15884
+ position: { x: (from[0] + to[0]) / 2, y: (from[1] + to[1]) / 2, z: (from[2] + to[2]) / 2 },
15885
+ radius,
15886
+ height: len,
15887
+ rotation: axisRotation(from, to),
15888
+ pivot: "center",
15889
+ segments: 12,
15890
+ material: { color }
15891
+ };
15892
+ }
15893
+ function arrowBetween(from, to, color, shaftRadius = 0.08, id) {
15894
+ const len = segmentLength(from, to);
15895
+ if (len < 1e-6) return null;
15896
+ const tipLen = Math.min(shaftRadius * 8, len * 0.35);
15897
+ const k = (len - tipLen) / len;
15898
+ const delta = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
15899
+ const shaftEnd = [delta[0] * k, delta[1] * k, delta[2] * k];
15900
+ const tipStart = shaftEnd;
15901
+ const tipEnd = delta;
15902
+ const shaft = cylinderBetween([0, 0, 0], shaftEnd, shaftRadius, color);
15903
+ const tipLenActual = segmentLength(tipStart, tipEnd);
15904
+ const tip = {
15905
+ type: "draw-mesh",
15906
+ shape: "cone",
15907
+ position: {
15908
+ x: (tipStart[0] + tipEnd[0]) / 2,
15909
+ y: (tipStart[1] + tipEnd[1]) / 2,
15910
+ z: (tipStart[2] + tipEnd[2]) / 2
15911
+ },
15912
+ radius: shaftRadius * 3,
15913
+ height: tipLenActual,
15914
+ rotation: axisRotation(tipStart, tipEnd),
15915
+ pivot: "center",
15916
+ segments: 12,
15917
+ material: { color }
15918
+ };
15919
+ return {
15920
+ type: "draw-group",
15921
+ ...id !== void 0 ? { id } : {},
15922
+ position: { x: from[0], y: from[1], z: from[2] },
15923
+ items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
15924
+ };
15925
+ }
15926
+ function billboardLabel(text, x, y, z, opts) {
15927
+ return {
15928
+ type: "draw-text",
15929
+ text,
15930
+ position: { x, y, z },
15931
+ color: opts?.color ?? "#111827"
15932
+ };
15933
+ }
15934
+ function labelColorForBackground(backgroundColor) {
15935
+ const m = /^#(?:([0-9a-f]{3})|([0-9a-f]{6}))$/i.exec(backgroundColor ?? "");
15936
+ if (!m) return "#111827";
15937
+ const hex = m[1] !== void 0 ? m[1].split("").map((c) => c + c).join("") : m[2];
15938
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
15939
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
15940
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
15941
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
15942
+ return luminance < 0.5 ? "#e5e7eb" : "#111827";
15943
+ }
15944
+ function get3DClickPayload(onShapeClick, idToIndex) {
15945
+ if (!onShapeClick) return void 0;
15946
+ return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
15947
+ }
15948
+ function polylineTube(points, radius, color, opts) {
15949
+ const maxSegments = opts?.maxSegments ?? 128;
15950
+ let pts = points;
15951
+ if (pts.length - 1 > maxSegments) {
15952
+ const step = (pts.length - 1) / maxSegments;
15953
+ const kept = [pts[0]];
15954
+ for (let s = 1; s < maxSegments; s++) kept.push(pts[Math.round(s * step)]);
15955
+ kept.push(pts[pts.length - 1]);
15956
+ pts = kept;
15957
+ }
15958
+ const out = [];
15959
+ for (let i = 0; i < pts.length - 1; i++) {
15960
+ const seg = cylinderBetween(pts[i], pts[i + 1], radius, color);
15961
+ if (seg) out.push(opts?.opacity !== void 0 ? { ...seg, opacity: opts.opacity } : seg);
15962
+ }
15963
+ return out;
15964
+ }
15965
+ function heightFieldMesh(spec) {
15966
+ const { nx, ny, heights, spacing = 1, x = 0, y = 0 } = spec;
15967
+ const flatShading = spec.flatShading ?? true;
15968
+ const vertices = [];
15969
+ for (let iy = 0; iy < ny; iy++) {
15970
+ for (let ix = 0; ix < nx; ix++) {
15971
+ vertices.push([
15972
+ x + (ix - (nx - 1) / 2) * spacing,
15973
+ y + (iy - (ny - 1) / 2) * spacing,
15974
+ heights[iy * nx + ix] ?? 0
15975
+ ]);
15976
+ }
15977
+ }
15978
+ const bands = [...spec.bands ?? []].sort((a, b) => (a.min ?? -Infinity) - (b.min ?? -Infinity));
15979
+ const facesByBand = /* @__PURE__ */ new Map();
15980
+ for (let iy = 0; iy < ny - 1; iy++) {
15981
+ for (let ix = 0; ix < nx - 1; ix++) {
15982
+ const v00 = iy * nx + ix;
15983
+ const v10 = iy * nx + ix + 1;
15984
+ const v01 = (iy + 1) * nx + ix;
15985
+ const v11 = (iy + 1) * nx + ix + 1;
15986
+ for (const face of [[v00, v10, v01], [v10, v11, v01]]) {
15987
+ const centroid = (vertices[face[0]][2] + vertices[face[1]][2] + vertices[face[2]][2]) / 3;
15988
+ let band = null;
15989
+ for (const b of bands) {
15990
+ if ((b.min ?? -Infinity) <= centroid) band = b;
15991
+ }
15992
+ const key = bands.length > 0 ? band : null;
15993
+ const list = facesByBand.get(key) ?? [];
15994
+ list.push(face);
15995
+ facesByBand.set(key, list);
15996
+ }
15997
+ }
15998
+ }
15999
+ const out = [];
16000
+ for (const [band, faces] of facesByBand) {
16001
+ if (faces.length === 0) continue;
16002
+ out.push({
16003
+ type: "draw-mesh",
16004
+ shape: "polyhedron",
16005
+ position: { x: 0, y: 0, z: 0 },
16006
+ vertices,
16007
+ faces,
16008
+ pivot: "center",
16009
+ material: { color: band?.color ?? spec.color ?? "#64748b", flatShading, side: "double" },
16010
+ ...spec.opacity !== void 0 ? { opacity: spec.opacity } : {}
16011
+ });
16012
+ }
16013
+ return out;
16014
+ }
16015
+ function arrowField(vectors, opts) {
16016
+ const scale = opts?.scale ?? 1;
16017
+ const out = [];
16018
+ for (const v of vectors) {
16019
+ const to = [
16020
+ v.from[0] + v.delta[0] * scale,
16021
+ v.from[1] + v.delta[1] * scale,
16022
+ v.from[2] + v.delta[2] * scale
16023
+ ];
16024
+ const arrow = arrowBetween(v.from, to, v.color ?? "#dc2626", v.width, v.id);
16025
+ if (arrow) out.push(arrow);
16026
+ if (v.label) out.push(billboardLabel(v.label, to[0], to[1], to[2], { color: opts?.labelColor }));
16027
+ }
16028
+ return out;
16029
+ }
16030
+ function helixDrawables(spec, opts) {
16031
+ const count = spec.count ?? spec.rungs?.length ?? 0;
16032
+ const rungs = Array.from({ length: count }, (_, i) => spec.rungs?.[i] ?? {});
16033
+ const radius = spec.radius ?? 1;
16034
+ const rise = spec.rise ?? 0.34;
16035
+ const twistRad = (spec.twistDeg ?? 36) * (Math.PI / 180);
16036
+ const strandAColor = spec.strandAColor ?? "#38bdf8";
16037
+ const strandBColor = spec.strandBColor ?? "#fb923c";
16038
+ const backboneRadius = spec.backboneRadius ?? 0.16;
16039
+ const rungRadius = spec.rungRadius ?? 0.12;
16040
+ const cx = spec.x ?? 0;
16041
+ const cy = spec.y ?? 0;
16042
+ const cz = spec.z ?? 0;
16043
+ const unwoundCount = spec.unwoundCount ?? 0;
16044
+ const unwindSpread = spec.unwindSpread ?? 1.8;
16045
+ const strandA = [];
16046
+ const strandB = [];
16047
+ for (let i = 0; i < count; i++) {
16048
+ const yi = cy + (i - (count - 1) / 2) * rise;
16049
+ const theta = i * twistRad;
16050
+ const s = i < unwoundCount ? unwindSpread : 1;
16051
+ strandA.push([cx + s * radius * Math.cos(theta), yi, cz + s * radius * Math.sin(theta)]);
16052
+ strandB.push([cx + s * radius * Math.cos(theta + Math.PI), yi, cz + s * radius * Math.sin(theta + Math.PI)]);
16053
+ }
16054
+ const out = [];
16055
+ for (let i = 0; i < count; i++) {
16056
+ out.push(meshSphere(`hx-a-${i}`, strandA[i][0], strandA[i][1], strandA[i][2], backboneRadius, strandAColor));
16057
+ out.push(meshSphere(`hx-b-${i}`, strandB[i][0], strandB[i][1], strandB[i][2], backboneRadius, strandBColor));
16058
+ if (i > 0) {
16059
+ const segA = cylinderBetween(strandA[i - 1], strandA[i], backboneRadius, strandAColor);
16060
+ if (segA) out.push(segA);
16061
+ const segB = cylinderBetween(strandB[i - 1], strandB[i], backboneRadius, strandBColor);
16062
+ if (segB) out.push(segB);
16063
+ }
16064
+ const rung = rungs[i];
16065
+ const rungColor = rung.color ?? "#94a3b8";
16066
+ const rod = cylinderBetween(strandA[i], strandB[i], rungRadius, rungColor);
16067
+ if (rod) out.push(rod);
16068
+ const mid = [
16069
+ (strandA[i][0] + strandB[i][0]) / 2,
16070
+ (strandA[i][1] + strandB[i][1]) / 2,
16071
+ (strandA[i][2] + strandB[i][2]) / 2
16072
+ ];
16073
+ const markerRadius = rung.radius ?? rungRadius;
16074
+ out.push(meshSphere(rung.id, mid[0], mid[1], mid[2], markerRadius, rungColor));
16075
+ if (rung.label) out.push(billboardLabel(rung.label, mid[0], mid[1], mid[2] + markerRadius, { color: opts?.labelColor }));
16076
+ }
16077
+ return out;
16078
+ }
16079
+ function latticeDrawables(spec, opts) {
16080
+ const nx = spec.nx ?? 2;
16081
+ const ny = spec.ny ?? 2;
16082
+ const nz = spec.nz ?? 2;
16083
+ const latticeConstant = spec.latticeConstant ?? 2;
16084
+ const bondRadius = spec.bondRadius ?? 0.06;
16085
+ const highlightCell = spec.highlightCell ?? false;
16086
+ const dimColor = spec.dimColor ?? "#475569";
16087
+ const showLabels = spec.showLabels ?? false;
16088
+ const selectedColor = spec.selectedColor ?? "#f59e0b";
16089
+ const posByKey = /* @__PURE__ */ new Map();
16090
+ const inCellByKey = /* @__PURE__ */ new Map();
16091
+ const out = [];
16092
+ for (const site of spec.basis) {
16093
+ const snx = site.xEdge ? nx + 1 : nx;
16094
+ const sny = site.yEdge ? ny + 1 : ny;
16095
+ const snz = site.zEdge ? nz + 1 : nz;
16096
+ for (let i = 0; i < snx; i++) {
16097
+ for (let j = 0; j < sny; j++) {
16098
+ for (let k = 0; k < snz; k++) {
16099
+ const key = `${site.key}-${i}-${j}-${k}`;
16100
+ const inCell = i + site.dx <= 1 && j + site.dy <= 1 && k + site.dz <= 1;
16101
+ const pos = [
16102
+ (i + site.dx) * latticeConstant - nx * latticeConstant / 2,
16103
+ (j + site.dy) * latticeConstant - ny * latticeConstant / 2,
16104
+ (k + site.dz) * latticeConstant - nz * latticeConstant / 2
16105
+ ];
16106
+ posByKey.set(key, pos);
16107
+ inCellByKey.set(key, inCell);
16108
+ const isSelected = spec.selectedId === `lat-${key}`;
16109
+ const color = isSelected ? selectedColor : highlightCell && !inCell ? dimColor : site.color ?? "#2563eb";
16110
+ const radius = (site.radius ?? 0.3) * (isSelected ? 1.4 : 1);
16111
+ out.push(meshSphere(`lat-${key}`, pos[0], pos[1], pos[2], radius, color));
16112
+ if (showLabels && site.element) {
16113
+ out.push(billboardLabel(site.element, pos[0], pos[1], pos[2] + radius, { color: opts?.labelColor }));
16114
+ }
16115
+ }
16116
+ }
16117
+ }
16118
+ }
16119
+ const basisByKey = new Map(spec.basis.map((s) => [s.key, s]));
16120
+ for (const bond of spec.bonds ?? []) {
16121
+ const fromSite = basisByKey.get(bond.from);
16122
+ const toSite = basisByKey.get(bond.to);
16123
+ if (!fromSite || !toSite) continue;
16124
+ const fnx = fromSite.xEdge ? nx + 1 : nx;
16125
+ const fny = fromSite.yEdge ? ny + 1 : ny;
16126
+ const fnz = fromSite.zEdge ? nz + 1 : nz;
16127
+ const tnx = toSite.xEdge ? nx + 1 : nx;
16128
+ const tny = toSite.yEdge ? ny + 1 : ny;
16129
+ const tnz = toSite.zEdge ? nz + 1 : nz;
16130
+ const bdx = bond.dx ?? 0;
16131
+ const bdy = bond.dy ?? 0;
16132
+ const bdz = bond.dz ?? 0;
16133
+ for (let i = 0; i < fnx; i++) {
16134
+ for (let j = 0; j < fny; j++) {
16135
+ for (let k = 0; k < fnz; k++) {
16136
+ const ti = i + bdx;
16137
+ const tj = j + bdy;
16138
+ const tk = k + bdz;
16139
+ if (ti < 0 || ti >= tnx || tj < 0 || tj >= tny || tk < 0 || tk >= tnz) continue;
16140
+ const fromKey = `${fromSite.key}-${i}-${j}-${k}`;
16141
+ const toKey = `${toSite.key}-${ti}-${tj}-${tk}`;
16142
+ const fromPos = posByKey.get(fromKey);
16143
+ const toPos = posByKey.get(toKey);
16144
+ if (!fromPos || !toPos) continue;
16145
+ const dimmed = highlightCell && !(inCellByKey.get(fromKey) && inCellByKey.get(toKey));
16146
+ const seg = cylinderBetween(fromPos, toPos, bondRadius, dimmed ? dimColor : bond.color ?? "#6b7280");
16147
+ if (seg) out.push(seg);
16148
+ }
16149
+ }
16150
+ }
16151
+ }
16152
+ return out;
16153
+ }
16154
+ var sceneLog, KNOWN_BLOOM_KEYS, Canvas3DHost2;
16155
+ var init_learningScene3D = __esm({
16156
+ "components/learning/molecules/learningScene3D.tsx"() {
16157
+ "use client";
16158
+ init_atoms();
16159
+ init_Stack();
16160
+ init_useEventBus();
16161
+ sceneLog = createLogger("almadar:ui:learning-scene-3d");
16162
+ KNOWN_BLOOM_KEYS = /* @__PURE__ */ new Set(["intensity", "threshold", "smoothing"]);
16163
+ Canvas3DHost2 = lazy(
16164
+ () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
16165
+ );
16166
+ LearningScene3D.displayName = "LearningScene3D";
16167
+ }
16168
+ });
16169
+ var biologyLog, BIO_BAND_COLORS, BIO_STAGE_FILL, BIO_STAGE_TEXT, BiologyCanvas;
14836
16170
  var init_BiologyCanvas = __esm({
14837
16171
  "components/learning/molecules/BiologyCanvas.tsx"() {
14838
16172
  "use client";
14839
16173
  init_atoms();
14840
16174
  init_Stack();
14841
16175
  init_LearningCanvas();
16176
+ init_learningScene3D();
16177
+ biologyLog = createLogger("almadar:ui:biology-canvas");
16178
+ BIO_BAND_COLORS = ["#dcfce7", "#fef9c3", "#fee2e2", "#e0e7ff"];
16179
+ BIO_STAGE_FILL = {
16180
+ pending: "#e2e8f0",
16181
+ active: "#3b82f6",
16182
+ done: "#94a3b8"
16183
+ };
16184
+ BIO_STAGE_TEXT = {
16185
+ pending: "#64748b",
16186
+ active: "#ffffff",
16187
+ done: "#ffffff"
16188
+ };
14842
16189
  BiologyCanvas = ({
14843
16190
  className,
14844
16191
  width = 600,
14845
16192
  height = 400,
14846
16193
  title,
14847
16194
  backgroundColor,
16195
+ mode = "2d",
16196
+ camera,
16197
+ lighting,
16198
+ post,
14848
16199
  nodes = [],
14849
16200
  edges = [],
16201
+ compartments = [],
16202
+ bands = [],
16203
+ stages = [],
16204
+ stageStyle = "timeline",
16205
+ helix,
16206
+ helix3d,
14850
16207
  shapes = [],
14851
- interactive = false,
16208
+ readouts,
16209
+ traces,
16210
+ showGrid,
16211
+ shadows,
16212
+ interactive,
14852
16213
  animate = false,
14853
16214
  onShapeClick,
14854
16215
  isLoading,
@@ -14860,19 +16221,148 @@ var init_BiologyCanvas = __esm({
14860
16221
  for (const n of nodes) {
14861
16222
  if (n.id) nodeById.set(n.id, n);
14862
16223
  }
16224
+ const bandCount = bands.length;
16225
+ for (let i = 0; i < bandCount; i++) {
16226
+ const band = bands[i];
16227
+ const bandColor = band.color ?? BIO_BAND_COLORS[i % BIO_BAND_COLORS.length];
16228
+ const bandY = i * height / bandCount;
16229
+ const bandH = height / bandCount;
16230
+ out.push({
16231
+ type: "rect",
16232
+ x: 0,
16233
+ y: bandY,
16234
+ width,
16235
+ height: bandH,
16236
+ color: bandColor,
16237
+ fill: bandColor,
16238
+ opacity: 0.45
16239
+ });
16240
+ if (band.label) {
16241
+ out.push({
16242
+ type: "text",
16243
+ x: 8,
16244
+ y: bandY + 14,
16245
+ text: band.label,
16246
+ color: "#6b7280",
16247
+ fontSize: 10
16248
+ });
16249
+ }
16250
+ }
16251
+ for (const c of compartments) {
16252
+ const color = c.color ?? "#16a34a";
16253
+ out.push({
16254
+ type: "ellipse",
16255
+ x: c.x,
16256
+ y: c.y,
16257
+ width: c.width,
16258
+ height: c.height,
16259
+ color,
16260
+ fill: c.fill ?? `${color}1A`,
16261
+ lineWidth: c.lineWidth ?? 2,
16262
+ ...c.dash ? { dash: c.dash } : {}
16263
+ });
16264
+ if (c.label) {
16265
+ out.push({
16266
+ type: "text",
16267
+ x: c.x,
16268
+ y: c.y - c.height / 2 + 14,
16269
+ text: c.label,
16270
+ color: "#111827",
16271
+ fontSize: 11,
16272
+ align: "center"
16273
+ });
16274
+ }
16275
+ }
16276
+ if (helix) {
16277
+ const hx = helix.x ?? 24;
16278
+ const hy = helix.y ?? height * 0.25;
16279
+ const hw = helix.width ?? width - 48;
16280
+ const hh = helix.height ?? height * 0.5;
16281
+ const rungs = helix.rungs;
16282
+ const n = rungs.length;
16283
+ const cy = hy + hh / 2;
16284
+ const colorA = helix.colorA ?? "#2563eb";
16285
+ const colorB = helix.colorB ?? "#dc2626";
16286
+ const rungColor = helix.rungColor ?? "#94a3b8";
16287
+ const fork = helix.fork ?? 0;
16288
+ const maxSep = Math.min(hh - 8, 96);
16289
+ const strandA = [];
16290
+ const strandB = [];
16291
+ const rungGeoms = [];
16292
+ for (let i = 0; i < n; i++) {
16293
+ const rx = hx + (i + 0.5) * hw / n;
16294
+ const t = (i + 0.5) / n;
16295
+ const paired = t >= fork;
16296
+ const sep = paired ? 28 : 28 + (maxSep - 28) * ((fork - t) / fork);
16297
+ strandA.push({ x: rx, y: cy - sep / 2 });
16298
+ strandB.push({ x: rx, y: cy + sep / 2 });
16299
+ rungGeoms.push({ rx, sep, rung: rungs[i], paired });
16300
+ }
16301
+ for (let i = 1; i < n; i++) {
16302
+ 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 });
16303
+ }
16304
+ for (let i = 1; i < n; i++) {
16305
+ 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 });
16306
+ }
16307
+ for (const g of rungGeoms) {
16308
+ const rColor = g.rung.color ?? (g.rung.state === "new" ? "#16a34a" : rungColor);
16309
+ const topY = cy - g.sep / 2;
16310
+ const bottomY = cy + g.sep / 2;
16311
+ if (g.paired) {
16312
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: bottomY, color: rColor });
16313
+ if (g.rung.a) {
16314
+ out.push({ type: "text", x: g.rx, y: cy - g.sep / 4, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
16315
+ }
16316
+ if (g.rung.b) {
16317
+ out.push({ type: "text", x: g.rx, y: cy + g.sep / 4, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16318
+ }
16319
+ } else {
16320
+ const stubTopY = topY + 8;
16321
+ const stubBottomY = bottomY - 8;
16322
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: stubTopY, color: rColor });
16323
+ out.push({ type: "line", x1: g.rx, y1: bottomY, x2: g.rx, y2: stubBottomY, color: rColor });
16324
+ if (g.rung.a) {
16325
+ out.push({ type: "text", x: g.rx, y: stubTopY + 6, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
16326
+ }
16327
+ if (g.rung.b) {
16328
+ out.push({ type: "text", x: g.rx, y: stubBottomY - 6, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16329
+ }
16330
+ }
16331
+ }
16332
+ }
14863
16333
  for (const e of edges) {
14864
16334
  const a = nodeById.get(e.from);
14865
16335
  const b = nodeById.get(e.to);
14866
16336
  if (!a || !b) continue;
14867
- out.push({
14868
- type: "line",
14869
- x1: a.x,
14870
- y1: a.y,
14871
- x2: b.x,
14872
- y2: b.y,
14873
- color: e.color ?? "#9ca3af",
14874
- lineWidth: 2
14875
- });
16337
+ const color = e.color ?? "#9ca3af";
16338
+ if (e.directed) {
16339
+ const rA = a.radius ?? 16;
16340
+ const rB = b.radius ?? 16;
16341
+ const dx = b.x - a.x;
16342
+ const dy = b.y - a.y;
16343
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16344
+ const ux = dx / dist;
16345
+ const uy = dy / dist;
16346
+ out.push({
16347
+ type: "arrow",
16348
+ x1: a.x + ux * rA,
16349
+ y1: a.y + uy * rA,
16350
+ x2: b.x - ux * rB,
16351
+ y2: b.y - uy * rB,
16352
+ color,
16353
+ lineWidth: 2
16354
+ });
16355
+ } else {
16356
+ out.push({
16357
+ type: "line",
16358
+ x1: a.x,
16359
+ y1: a.y,
16360
+ x2: b.x,
16361
+ y2: b.y,
16362
+ color,
16363
+ lineWidth: 2
16364
+ });
16365
+ }
14876
16366
  if (e.label) {
14877
16367
  out.push({
14878
16368
  type: "text",
@@ -14885,6 +16375,8 @@ var init_BiologyCanvas = __esm({
14885
16375
  }
14886
16376
  }
14887
16377
  for (const n of nodes) {
16378
+ const state = n.state ?? "default";
16379
+ const muted = state === "muted";
14888
16380
  out.push({
14889
16381
  type: "circle",
14890
16382
  x: n.x,
@@ -14892,23 +16384,197 @@ var init_BiologyCanvas = __esm({
14892
16384
  radius: n.radius ?? 16,
14893
16385
  color: n.color ?? "#16a34a",
14894
16386
  fill: `${n.color ?? "#16a34a"}33`,
14895
- id: n.id
16387
+ id: n.id,
16388
+ ...muted ? { opacity: 0.35 } : {}
14896
16389
  });
16390
+ if (state === "highlight") {
16391
+ out.push({
16392
+ type: "circle",
16393
+ x: n.x,
16394
+ y: n.y,
16395
+ radius: (n.radius ?? 16) + 4,
16396
+ color: "#f59e0b",
16397
+ lineWidth: 2
16398
+ });
16399
+ }
14897
16400
  if (n.label) {
14898
16401
  out.push({
14899
16402
  type: "text",
14900
16403
  x: n.x,
14901
16404
  y: n.y + (n.radius ?? 16) + 14,
14902
16405
  text: n.label,
16406
+ ...muted ? { opacity: 0.35 } : {},
14903
16407
  color: "#111827",
14904
16408
  fontSize: 12,
14905
16409
  align: "center"
14906
16410
  });
14907
16411
  }
14908
16412
  }
16413
+ const stageCount = stages.length;
16414
+ if (stageCount > 0) {
16415
+ if (stageStyle === "ring") {
16416
+ const cx = width / 2;
16417
+ const cy = height / 2;
16418
+ const R = Math.min(width, height) / 2 - 48;
16419
+ const ringPoints = [];
16420
+ for (let i = 0; i < stageCount; i++) {
16421
+ const angleRad = (-90 + 360 * i / stageCount) * Math.PI / 180;
16422
+ ringPoints.push({ x: cx + R * Math.cos(angleRad), y: cy + R * Math.sin(angleRad) });
16423
+ }
16424
+ if (stageCount >= 2) {
16425
+ for (let i = 0; i < stageCount - 1; i++) {
16426
+ const p1 = ringPoints[i];
16427
+ const p2 = ringPoints[i + 1];
16428
+ const dx = p2.x - p1.x;
16429
+ const dy = p2.y - p1.y;
16430
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16431
+ const ux = dx / dist;
16432
+ const uy = dy / dist;
16433
+ out.push({
16434
+ type: "arrow",
16435
+ x1: p1.x + ux * 46,
16436
+ y1: p1.y + uy * 46,
16437
+ x2: p2.x - ux * 46,
16438
+ y2: p2.y - uy * 46,
16439
+ color: "#94a3b8"
16440
+ });
16441
+ }
16442
+ }
16443
+ for (let i = 0; i < stageCount; i++) {
16444
+ const stage = stages[i];
16445
+ const state = stage.state ?? "pending";
16446
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16447
+ const w = Math.max(26, Math.min(84, stage.label.length * 6 + 10));
16448
+ const h = 18;
16449
+ const p = ringPoints[i];
16450
+ out.push({ type: "rect", x: p.x - w / 2, y: p.y - h / 2, width: w, height: h, color: fill, fill });
16451
+ out.push({ type: "text", x: p.x, y: p.y, text: stage.label, color: BIO_STAGE_TEXT[state], fontSize: 10, align: "center" });
16452
+ }
16453
+ } else {
16454
+ const stripY = height - 32;
16455
+ const slotW = (width - 16) / stageCount;
16456
+ const chipGeoms = [];
16457
+ for (let i = 0; i < stageCount; i++) {
16458
+ chipGeoms.push({ x: 8 + i * slotW + 5, w: slotW - 10 });
16459
+ }
16460
+ for (let i = 0; i < stageCount - 1; i++) {
16461
+ const midY = stripY + 13;
16462
+ out.push({
16463
+ type: "arrow",
16464
+ x1: chipGeoms[i].x + chipGeoms[i].w,
16465
+ y1: midY,
16466
+ x2: chipGeoms[i + 1].x,
16467
+ y2: midY,
16468
+ color: "#94a3b8"
16469
+ });
16470
+ }
16471
+ for (let i = 0; i < stageCount; i++) {
16472
+ const stage = stages[i];
16473
+ const state = stage.state ?? "pending";
16474
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16475
+ const g = chipGeoms[i];
16476
+ out.push({ type: "rect", x: g.x, y: stripY, width: g.w, height: 26, color: fill, fill });
16477
+ out.push({
16478
+ type: "text",
16479
+ x: g.x + g.w / 2,
16480
+ y: stripY + 13,
16481
+ text: stage.label,
16482
+ color: BIO_STAGE_TEXT[state],
16483
+ fontSize: 10,
16484
+ align: "center"
16485
+ });
16486
+ }
16487
+ }
16488
+ }
14909
16489
  out.push(...shapes);
14910
16490
  return out;
14911
- }, [nodes, edges, shapes]);
16491
+ }, [nodes, edges, compartments, bands, stages, stageStyle, helix, shapes, width, height]);
16492
+ const drawables3D = useMemo(() => {
16493
+ if (mode !== "3d") return [];
16494
+ if (shapes.length > 0) {
16495
+ biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
16496
+ }
16497
+ if (compartments.length > 0 || bands.length > 0 || stages.length > 0 || helix) {
16498
+ biologyLog.debug("2D-only families ignored in 3D mode (pixel-authored 2D vocabulary)", {
16499
+ compartments: compartments.length,
16500
+ bands: bands.length,
16501
+ stages: stages.length,
16502
+ helix: helix != null
16503
+ });
16504
+ }
16505
+ if (animate) {
16506
+ biologyLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
16507
+ }
16508
+ const out = [];
16509
+ const labelColor = labelColorForBackground(backgroundColor);
16510
+ const nodeById = /* @__PURE__ */ new Map();
16511
+ for (const n of nodes) {
16512
+ if (n.id) nodeById.set(n.id, n);
16513
+ }
16514
+ for (const e of edges) {
16515
+ const a = nodeById.get(e.from);
16516
+ const b = nodeById.get(e.to);
16517
+ if (!a || !b) continue;
16518
+ const edgeRadius = Math.max(0.04, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.12);
16519
+ const edge = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], edgeRadius, e.color ?? "#9ca3af");
16520
+ if (edge) out.push(edge);
16521
+ if (e.label) {
16522
+ out.push(
16523
+ billboardLabel(
16524
+ e.label,
16525
+ (a.x + b.x) / 2,
16526
+ (a.y + b.y) / 2,
16527
+ ((a.z ?? 0) + (b.z ?? 0)) / 2,
16528
+ { color: labelColor }
16529
+ )
16530
+ );
16531
+ }
16532
+ }
16533
+ for (const n of nodes) {
16534
+ const radius = n.radius ?? 0.5;
16535
+ const nz = n.z ?? 0;
16536
+ out.push(meshSphere(n.id, n.x, n.y, nz, radius, n.color ?? "#16a34a", { shape: n.shape, ...n.opacity !== void 0 ? { opacity: n.opacity } : {} }));
16537
+ if (n.label) {
16538
+ out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
16539
+ }
16540
+ }
16541
+ if (helix3d) {
16542
+ out.push(...helixDrawables(helix3d, { labelColor }));
16543
+ }
16544
+ return out;
16545
+ }, [mode, nodes, edges, shapes, compartments, bands, stages, helix, helix3d, animate, backgroundColor]);
16546
+ const nodeIndexById = useMemo(() => {
16547
+ const m = /* @__PURE__ */ new Map();
16548
+ (helix3d?.rungs ?? []).forEach((rung, i) => {
16549
+ if (rung.id) m.set(rung.id, i);
16550
+ });
16551
+ nodes.forEach((n, i) => {
16552
+ if (n.id) m.set(n.id, i);
16553
+ });
16554
+ return m;
16555
+ }, [nodes, helix3d]);
16556
+ if (mode === "3d") {
16557
+ return /* @__PURE__ */ jsx(
16558
+ LearningScene3D,
16559
+ {
16560
+ className,
16561
+ width,
16562
+ height,
16563
+ title,
16564
+ backgroundColor,
16565
+ drawables: drawables3D,
16566
+ camera,
16567
+ lighting,
16568
+ post,
16569
+ showGrid,
16570
+ shadows,
16571
+ interactive,
16572
+ isLoading,
16573
+ error,
16574
+ onItemClick: get3DClickPayload(onShapeClick, nodeIndexById)
16575
+ }
16576
+ );
16577
+ }
14912
16578
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
14913
16579
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
14914
16580
  /* @__PURE__ */ jsx(
@@ -14918,7 +16584,9 @@ var init_BiologyCanvas = __esm({
14918
16584
  height,
14919
16585
  backgroundColor,
14920
16586
  shapes: derivedShapes,
14921
- interactive,
16587
+ readouts,
16588
+ traces,
16589
+ interactive: interactive ?? false,
14922
16590
  animate,
14923
16591
  onShapeClick,
14924
16592
  isLoading,
@@ -21758,24 +23426,55 @@ var init_ChatBar = __esm({
21758
23426
  ChatBar.displayName = "ChatBar";
21759
23427
  }
21760
23428
  });
21761
- var ChemistryCanvas;
23429
+ function bondPerpendicular(a, b) {
23430
+ const dx = b[0] - a[0];
23431
+ const dy = b[1] - a[1];
23432
+ const px = dy;
23433
+ const py = -dx;
23434
+ const len = Math.sqrt(px * px + py * py);
23435
+ if (len < 1e-6) return [1, 0, 0];
23436
+ return [px / len, py / len, 0];
23437
+ }
23438
+ var chemistryLog, CHEM_BOND_STATE_COLOR, LONE_PAIR_ANGLES, ChemistryCanvas;
21762
23439
  var init_ChemistryCanvas = __esm({
21763
23440
  "components/learning/molecules/ChemistryCanvas.tsx"() {
21764
23441
  "use client";
21765
23442
  init_atoms();
21766
23443
  init_Stack();
21767
23444
  init_LearningCanvas();
23445
+ init_learningScene3D();
23446
+ chemistryLog = createLogger("almadar:ui:chemistry-canvas");
23447
+ CHEM_BOND_STATE_COLOR = {
23448
+ default: "#6b7280",
23449
+ forming: "#16a34a",
23450
+ breaking: "#dc2626",
23451
+ highlight: "#f59e0b"
23452
+ };
23453
+ LONE_PAIR_ANGLES = [-90, 0, 90, 180];
21768
23454
  ChemistryCanvas = ({
21769
23455
  className,
21770
23456
  width = 600,
21771
23457
  height = 400,
21772
23458
  title,
21773
23459
  backgroundColor,
23460
+ mode = "2d",
23461
+ camera,
23462
+ lighting,
23463
+ post,
21774
23464
  atoms = [],
21775
23465
  bonds = [],
21776
23466
  arrows = [],
23467
+ bondStyle = "thick",
23468
+ containers = [],
23469
+ equation,
23470
+ equationColor,
23471
+ lattice3d,
21777
23472
  shapes = [],
21778
- interactive = false,
23473
+ readouts,
23474
+ traces,
23475
+ showGrid,
23476
+ shadows,
23477
+ interactive,
21779
23478
  animate = false,
21780
23479
  onShapeClick,
21781
23480
  isLoading,
@@ -21787,21 +23486,118 @@ var init_ChemistryCanvas = __esm({
21787
23486
  for (const a of atoms) {
21788
23487
  if (a.id) atomById.set(a.id, a);
21789
23488
  }
23489
+ for (const c of containers) {
23490
+ const color = c.color ?? "#64748b";
23491
+ if (c.level != null) {
23492
+ const lv = c.level;
23493
+ out.push({
23494
+ type: "rect",
23495
+ x: c.x + 1,
23496
+ y: c.y + c.height * (1 - lv),
23497
+ width: c.width - 2,
23498
+ height: c.height * lv - 1,
23499
+ color: c.levelColor ?? "#60a5fa",
23500
+ fill: c.levelColor ?? "#60a5fa",
23501
+ opacity: 0.5
23502
+ });
23503
+ }
23504
+ out.push({
23505
+ type: "rect",
23506
+ x: c.x,
23507
+ y: c.y,
23508
+ width: c.width,
23509
+ height: c.height,
23510
+ color,
23511
+ fill: c.fill,
23512
+ lineWidth: c.lineWidth ?? 2
23513
+ });
23514
+ const divider = c.divider ?? "none";
23515
+ if (divider !== "none") {
23516
+ out.push({
23517
+ type: "line",
23518
+ x1: c.x + c.width / 2,
23519
+ y1: c.y,
23520
+ x2: c.x + c.width / 2,
23521
+ y2: c.y + c.height,
23522
+ color: c.dividerColor ?? color,
23523
+ ...divider === "dashed" || divider === "dotted" ? { dash: divider } : {}
23524
+ });
23525
+ }
23526
+ if (c.leftLabel) {
23527
+ out.push({
23528
+ type: "text",
23529
+ x: c.x + c.width * 0.25,
23530
+ y: c.y + 12,
23531
+ text: c.leftLabel,
23532
+ color: "#374151",
23533
+ fontSize: 11,
23534
+ align: "center"
23535
+ });
23536
+ }
23537
+ if (c.rightLabel) {
23538
+ out.push({
23539
+ type: "text",
23540
+ x: c.x + c.width * 0.75,
23541
+ y: c.y + 12,
23542
+ text: c.rightLabel,
23543
+ color: "#374151",
23544
+ fontSize: 11,
23545
+ align: "center"
23546
+ });
23547
+ }
23548
+ if (c.label) {
23549
+ out.push({
23550
+ type: "text",
23551
+ x: c.x + c.width / 2,
23552
+ y: c.y + c.height + 12,
23553
+ text: c.label,
23554
+ color: "#111827",
23555
+ fontSize: 12,
23556
+ align: "center"
23557
+ });
23558
+ }
23559
+ }
21790
23560
  for (const b of bonds) {
21791
23561
  const a = atomById.get(b.from);
21792
23562
  const c = atomById.get(b.to);
21793
23563
  if (!a || !c) continue;
21794
- const color = b.color ?? "#6b7280";
21795
- const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
21796
- out.push({
21797
- type: "line",
21798
- x1: a.x,
21799
- y1: a.y,
21800
- x2: c.x,
21801
- y2: c.y,
21802
- color,
21803
- lineWidth: strokeWidth
21804
- });
23564
+ const state = b.state ?? "default";
23565
+ const color = b.color ?? CHEM_BOND_STATE_COLOR[state];
23566
+ const dash = state === "forming" || state === "breaking" ? "dashed" : void 0;
23567
+ if (bondStyle === "parallel") {
23568
+ const dx = c.x - a.x;
23569
+ const dy = c.y - a.y;
23570
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
23571
+ const ux = dx / dist;
23572
+ const uy = dy / dist;
23573
+ const px = -uy;
23574
+ const py = ux;
23575
+ const offsets = b.type === "double" ? [-3, 3] : b.type === "triple" ? [-4, 0, 4] : [0];
23576
+ for (const off of offsets) {
23577
+ out.push({
23578
+ type: "line",
23579
+ x1: a.x + px * off,
23580
+ y1: a.y + py * off,
23581
+ x2: c.x + px * off,
23582
+ y2: c.y + py * off,
23583
+ color,
23584
+ lineWidth: 2,
23585
+ ...dash ? { dash } : {}
23586
+ });
23587
+ }
23588
+ } else {
23589
+ const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
23590
+ out.push({
23591
+ type: "line",
23592
+ x1: a.x,
23593
+ y1: a.y,
23594
+ x2: c.x,
23595
+ y2: c.y,
23596
+ color,
23597
+ lineWidth: strokeWidth,
23598
+ ...dash ? { dash } : {}
23599
+ });
23600
+ }
21805
23601
  }
21806
23602
  for (const a of arrows) {
21807
23603
  const angle = (a.angle ?? 0) * (Math.PI / 180);
@@ -21850,10 +23646,142 @@ var init_ChemistryCanvas = __esm({
21850
23646
  align: "center"
21851
23647
  });
21852
23648
  }
23649
+ const r = a.radius ?? 14;
23650
+ if (a.charge) {
23651
+ out.push({
23652
+ type: "text",
23653
+ x: a.x + r * 0.85,
23654
+ y: a.y - r * 0.85,
23655
+ text: a.charge,
23656
+ color: "#111827",
23657
+ fontSize: 9,
23658
+ align: "left"
23659
+ });
23660
+ }
23661
+ const lonePairs = Math.max(0, Math.min(4, a.lonePairs ?? 0));
23662
+ for (let k = 0; k < lonePairs; k++) {
23663
+ const angleRad = LONE_PAIR_ANGLES[k] * Math.PI / 180;
23664
+ const cx = a.x + (r + 6) * Math.cos(angleRad);
23665
+ const cy = a.y + (r + 6) * Math.sin(angleRad);
23666
+ const perpX = -Math.sin(angleRad);
23667
+ const perpY = Math.cos(angleRad);
23668
+ for (const sign of [1, -1]) {
23669
+ out.push({
23670
+ type: "circle",
23671
+ x: cx + perpX * 2.5 * sign,
23672
+ y: cy + perpY * 2.5 * sign,
23673
+ radius: 1.5,
23674
+ color: "#374151",
23675
+ fill: "#374151"
23676
+ });
23677
+ }
23678
+ }
23679
+ }
23680
+ if (equation) {
23681
+ out.push({
23682
+ type: "text",
23683
+ x: width / 2,
23684
+ y: 14,
23685
+ text: equation,
23686
+ color: equationColor ?? "#111827",
23687
+ fontSize: 13,
23688
+ align: "center"
23689
+ });
21853
23690
  }
21854
23691
  out.push(...shapes);
21855
23692
  return out;
21856
- }, [atoms, bonds, arrows, shapes]);
23693
+ }, [atoms, bonds, arrows, bondStyle, containers, equation, equationColor, shapes, width]);
23694
+ const drawables3D = useMemo(() => {
23695
+ if (mode !== "3d") return [];
23696
+ if (shapes.length > 0) {
23697
+ chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
23698
+ }
23699
+ if (containers.length > 0) {
23700
+ chemistryLog.debug("containers ignored in 3D mode (pixel-authored 2D vocabulary)", { count: containers.length });
23701
+ }
23702
+ if (animate) {
23703
+ chemistryLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
23704
+ }
23705
+ const out = [];
23706
+ const labelColor = labelColorForBackground(backgroundColor);
23707
+ const atomById = /* @__PURE__ */ new Map();
23708
+ for (const a of atoms) {
23709
+ if (a.id) atomById.set(a.id, a);
23710
+ }
23711
+ for (const b of bonds) {
23712
+ const a = atomById.get(b.from);
23713
+ const c = atomById.get(b.to);
23714
+ if (!a || !c) continue;
23715
+ const color = b.color ?? "#6b7280";
23716
+ const from = [a.x, a.y, a.z ?? 0];
23717
+ const to = [c.x, c.y, c.z ?? 0];
23718
+ const perp = bondPerpendicular(from, to);
23719
+ const bondRadius = Math.max(0.06, Math.min(a.radius ?? 0.45, c.radius ?? 0.45) * 0.22);
23720
+ const step = bondRadius * 2.2;
23721
+ const offsets = b.type === "double" ? [-step, step] : b.type === "triple" ? [-step, 0, step] : [0];
23722
+ for (const off of offsets) {
23723
+ const bond = cylinderBetween(
23724
+ [from[0] + perp[0] * off, from[1] + perp[1] * off, from[2] + perp[2] * off],
23725
+ [to[0] + perp[0] * off, to[1] + perp[1] * off, to[2] + perp[2] * off],
23726
+ bondRadius,
23727
+ color
23728
+ );
23729
+ if (bond) out.push(bond);
23730
+ }
23731
+ }
23732
+ for (const a of arrows) {
23733
+ const angle = (a.angle ?? 0) * (Math.PI / 180);
23734
+ const len = a.length ?? 60;
23735
+ const x2 = a.x + Math.cos(angle) * len;
23736
+ const y2 = a.y + Math.sin(angle) * len;
23737
+ const arrow = arrowBetween([a.x, a.y, 0], [x2, y2, 0], a.color ?? "#dc2626");
23738
+ if (arrow) out.push(arrow);
23739
+ if (a.label) {
23740
+ out.push(billboardLabel(a.label, (a.x + x2) / 2, (a.y + y2) / 2, 0, { color: labelColor }));
23741
+ }
23742
+ }
23743
+ for (const a of atoms) {
23744
+ const radius = a.radius ?? 0.45;
23745
+ const az = a.z ?? 0;
23746
+ out.push(meshSphere(a.id, a.x, a.y, az, radius, a.color ?? "#2563eb"));
23747
+ if (a.element) {
23748
+ out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
23749
+ }
23750
+ }
23751
+ if (lattice3d) {
23752
+ out.push(...latticeDrawables(lattice3d, { labelColor }));
23753
+ }
23754
+ return out;
23755
+ }, [mode, atoms, bonds, arrows, shapes, containers, lattice3d, animate, backgroundColor]);
23756
+ const atomIndexById = useMemo(() => {
23757
+ const m = /* @__PURE__ */ new Map();
23758
+ atoms.forEach((a, i) => {
23759
+ if (a.id) m.set(a.id, i);
23760
+ });
23761
+ return m;
23762
+ }, [atoms]);
23763
+ if (mode === "3d") {
23764
+ return /* @__PURE__ */ jsx(
23765
+ LearningScene3D,
23766
+ {
23767
+ className,
23768
+ width,
23769
+ height,
23770
+ title,
23771
+ backgroundColor,
23772
+ drawables: drawables3D,
23773
+ camera,
23774
+ lighting,
23775
+ post,
23776
+ showGrid,
23777
+ shadows,
23778
+ interactive,
23779
+ isLoading,
23780
+ error,
23781
+ onItemClick: get3DClickPayload(onShapeClick, atomIndexById)
23782
+ }
23783
+ );
23784
+ }
21857
23785
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
21858
23786
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
21859
23787
  /* @__PURE__ */ jsx(
@@ -21863,7 +23791,9 @@ var init_ChemistryCanvas = __esm({
21863
23791
  height,
21864
23792
  backgroundColor,
21865
23793
  shapes: derivedShapes,
21866
- interactive,
23794
+ readouts,
23795
+ traces,
23796
+ interactive: interactive ?? false,
21867
23797
  animate,
21868
23798
  onShapeClick,
21869
23799
  isLoading,
@@ -28052,6 +29982,10 @@ var init_ProgressDots = __esm({
28052
29982
  ProgressDots.displayName = "ProgressDots";
28053
29983
  }
28054
29984
  });
29985
+ function formatTick(v) {
29986
+ if (Number.isInteger(v)) return String(v);
29987
+ return v.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
29988
+ }
28055
29989
  var MathCanvas;
28056
29990
  var init_MathCanvas = __esm({
28057
29991
  "components/learning/molecules/MathCanvas.tsx"() {
@@ -28071,10 +30005,19 @@ var init_MathCanvas = __esm({
28071
30005
  showAxes = true,
28072
30006
  showGrid = true,
28073
30007
  gridStep = 1,
30008
+ showTickLabels = false,
30009
+ showCurveLabels = false,
28074
30010
  curves = [],
28075
30011
  points = [],
28076
30012
  vectors = [],
30013
+ regions = [],
30014
+ bars = [],
30015
+ guides = [],
30016
+ angles = [],
30017
+ hops = [],
28077
30018
  shapes = [],
30019
+ readouts,
30020
+ traces,
28078
30021
  interactive = false,
28079
30022
  animate = false,
28080
30023
  onShapeClick,
@@ -28088,6 +30031,8 @@ var init_MathCanvas = __esm({
28088
30031
  const plotH = height - margin * 2;
28089
30032
  const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
28090
30033
  const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
30034
+ const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
30035
+ const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
28091
30036
  if (showGrid) {
28092
30037
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep) {
28093
30038
  const px = mapX(x);
@@ -28098,14 +30043,99 @@ var init_MathCanvas = __esm({
28098
30043
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color: "#e5e7eb", lineWidth: 1 });
28099
30044
  }
28100
30045
  }
30046
+ if (showTickLabels) {
30047
+ const labelEveryX = Math.max(1, Math.ceil((xMax - xMin) / gridStep / Math.floor(plotW / 40)));
30048
+ let kx = 0;
30049
+ for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
30050
+ if (kx % labelEveryX === 0 && x !== 0) {
30051
+ out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: 10, align: "center" });
30052
+ }
30053
+ }
30054
+ const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
30055
+ let ky = 0;
30056
+ for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
30057
+ if (ky % labelEveryY === 0 && y !== 0) {
30058
+ out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: 10, align: "right" });
30059
+ }
30060
+ }
30061
+ if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
30062
+ out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: 10, align: "right" });
30063
+ }
30064
+ }
30065
+ for (const region of regions) {
30066
+ if (!region.samples || region.samples.length === 0) continue;
30067
+ const baseline = region.baseline ?? 0;
30068
+ const clampedPoint = (p) => ({
30069
+ x: mapX(Math.min(xMax, Math.max(xMin, p.x))),
30070
+ y: mapY(Math.min(yMax, Math.max(yMin, p.y)))
30071
+ });
30072
+ const upper = region.samples.map(clampedPoint);
30073
+ const first = region.samples[0];
30074
+ const last = region.samples[region.samples.length - 1];
30075
+ 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 })];
30076
+ const color = region.color ?? "#2563eb";
30077
+ out.push({
30078
+ type: "polygon",
30079
+ points: [...upper, ...closing],
30080
+ fill: color,
30081
+ color,
30082
+ opacity: region.opacity ?? 0.2,
30083
+ lineWidth: 1
30084
+ });
30085
+ if (region.label) {
30086
+ const mid = Math.floor(region.samples.length / 2);
30087
+ out.push({
30088
+ type: "text",
30089
+ x: mapX((first.x + last.x) / 2),
30090
+ y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
30091
+ text: region.label,
30092
+ color: "#111827",
30093
+ fontSize: 11
30094
+ });
30095
+ }
30096
+ }
30097
+ for (const bar of bars) {
30098
+ if (bar.x + bar.width < xMin || bar.x > xMax) continue;
30099
+ const y0 = bar.y0 ?? 0;
30100
+ const color = bar.color ?? "#93c5fd";
30101
+ out.push({
30102
+ type: "rect",
30103
+ x: mapX(bar.x),
30104
+ y: mapY(Math.max(y0, bar.y1)),
30105
+ width: mapX(bar.x + bar.width) - mapX(bar.x),
30106
+ height: Math.abs(mapY(bar.y1) - mapY(y0)),
30107
+ color,
30108
+ fill: color,
30109
+ opacity: bar.opacity ?? 0.5,
30110
+ lineWidth: 1
30111
+ });
30112
+ }
28101
30113
  if (showAxes) {
28102
- const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
28103
- const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
28104
30114
  out.push({ type: "line", x1: margin, y1: xAxisY, x2: width - margin, y2: xAxisY, color: "#374151", lineWidth: 2 });
28105
30115
  out.push({ type: "line", x1: yAxisX, y1: margin, x2: yAxisX, y2: height - margin, color: "#374151", lineWidth: 2 });
28106
30116
  }
30117
+ for (const guide of guides) {
30118
+ const color = guide.color ?? "#9ca3af";
30119
+ const dash = guide.dash ?? "dashed";
30120
+ if (guide.kind === "vline") {
30121
+ if (guide.at < xMin || guide.at > xMax) continue;
30122
+ const px = mapX(guide.at);
30123
+ out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
30124
+ if (guide.label) {
30125
+ out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color: "#111827", fontSize: 11 });
30126
+ }
30127
+ } else {
30128
+ if (guide.at < yMin || guide.at > yMax) continue;
30129
+ const py = mapY(guide.at);
30130
+ out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
30131
+ if (guide.label) {
30132
+ out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color: "#111827", fontSize: 11, align: "right" });
30133
+ }
30134
+ }
30135
+ }
28107
30136
  for (const curve of curves) {
28108
30137
  if (!curve.samples || curve.samples.length < 2) continue;
30138
+ let lastInRange;
28109
30139
  for (let i = 1; i < curve.samples.length; i++) {
28110
30140
  const a = curve.samples[i - 1];
28111
30141
  const b = curve.samples[i];
@@ -28117,19 +30147,97 @@ var init_MathCanvas = __esm({
28117
30147
  x2: mapX(b.x),
28118
30148
  y2: mapY(b.y),
28119
30149
  color: curve.color ?? "#2563eb",
28120
- lineWidth: 2
30150
+ lineWidth: 2,
30151
+ dash: curve.dash
30152
+ });
30153
+ lastInRange = b;
30154
+ }
30155
+ if (showCurveLabels && curve.label && lastInRange) {
30156
+ out.push({
30157
+ type: "text",
30158
+ x: mapX(lastInRange.x) + 6,
30159
+ y: mapY(lastInRange.y) - 6,
30160
+ text: curve.label,
30161
+ color: curve.color ?? "#2563eb",
30162
+ fontSize: 11
30163
+ });
30164
+ }
30165
+ }
30166
+ for (const hop of hops) {
30167
+ const x1 = mapX(hop.from);
30168
+ const x2 = mapX(hop.to);
30169
+ const peak = Math.min(36, plotH * 0.3);
30170
+ const color = hop.color ?? "#7c3aed";
30171
+ out.push({
30172
+ type: "ellipse",
30173
+ x: (x1 + x2) / 2,
30174
+ y: xAxisY,
30175
+ width: Math.abs(x2 - x1),
30176
+ height: 2 * peak,
30177
+ startAngle: 180,
30178
+ endAngle: 360,
30179
+ color
30180
+ });
30181
+ const s = Math.sign(hop.to - hop.from);
30182
+ out.push({
30183
+ type: "polygon",
30184
+ points: [
30185
+ { x: x2, y: xAxisY },
30186
+ { x: x2 - 4 * s, y: xAxisY - 7 },
30187
+ { x: x2 + 2 * s, y: xAxisY - 7 }
30188
+ ],
30189
+ fill: color,
30190
+ color
30191
+ });
30192
+ if (hop.label) {
30193
+ out.push({
30194
+ type: "text",
30195
+ x: (x1 + x2) / 2,
30196
+ y: xAxisY - peak - 8,
30197
+ text: hop.label,
30198
+ color: "#111827",
30199
+ fontSize: 10,
30200
+ align: "center"
30201
+ });
30202
+ }
30203
+ }
30204
+ for (const angle of angles) {
30205
+ const radius = angle.radius ?? 0.8;
30206
+ const color = angle.color ?? "#0ea5e9";
30207
+ out.push({
30208
+ type: "ellipse",
30209
+ x: mapX(angle.x),
30210
+ y: mapY(angle.y),
30211
+ width: 2 * radius * plotW / (xMax - xMin),
30212
+ height: 2 * radius * plotH / (yMax - yMin),
30213
+ startAngle: -angle.to,
30214
+ endAngle: -angle.from,
30215
+ color
30216
+ });
30217
+ if (angle.label) {
30218
+ const mid = (angle.from + angle.to) / 2;
30219
+ const rad = mid * Math.PI / 180;
30220
+ out.push({
30221
+ type: "text",
30222
+ x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
30223
+ y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
30224
+ text: angle.label,
30225
+ color: "#111827",
30226
+ fontSize: 11,
30227
+ align: "center"
28121
30228
  });
28122
30229
  }
28123
30230
  }
28124
30231
  for (const p of points) {
28125
30232
  if (p.x < xMin || p.x > xMax || p.y < yMin || p.y > yMax) continue;
30233
+ const isOpen = p.style === "open";
28126
30234
  out.push({
28127
30235
  type: "circle",
28128
30236
  x: mapX(p.x),
28129
30237
  y: mapY(p.y),
28130
30238
  radius: p.radius ?? 4,
28131
30239
  color: p.color ?? "#dc2626",
28132
- fill: p.color ?? "#dc2626"
30240
+ fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
28133
30241
  });
28134
30242
  if (p.label) {
28135
30243
  out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: "#111827", fontSize: 12 });
@@ -28148,7 +30256,28 @@ var init_MathCanvas = __esm({
28148
30256
  }
28149
30257
  out.push(...shapes);
28150
30258
  return out;
28151
- }, [width, height, xMin, xMax, yMin, yMax, showAxes, showGrid, gridStep, curves, points, vectors, shapes]);
30259
+ }, [
30260
+ width,
30261
+ height,
30262
+ xMin,
30263
+ xMax,
30264
+ yMin,
30265
+ yMax,
30266
+ showAxes,
30267
+ showGrid,
30268
+ gridStep,
30269
+ showTickLabels,
30270
+ showCurveLabels,
30271
+ curves,
30272
+ points,
30273
+ vectors,
30274
+ regions,
30275
+ bars,
30276
+ guides,
30277
+ angles,
30278
+ hops,
30279
+ shapes
30280
+ ]);
28152
30281
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
28153
30282
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
28154
30283
  /* @__PURE__ */ jsx(
@@ -28157,6 +30286,8 @@ var init_MathCanvas = __esm({
28157
30286
  width,
28158
30287
  height,
28159
30288
  shapes: derivedShapes,
30289
+ readouts,
30290
+ traces,
28160
30291
  interactive,
28161
30292
  animate,
28162
30293
  onShapeClick,
@@ -28168,27 +30299,355 @@ var init_MathCanvas = __esm({
28168
30299
  };
28169
30300
  }
28170
30301
  });
28171
- var PhysicsCanvas;
30302
+ function formatMeterValue(v) {
30303
+ return Number.isInteger(v) ? String(v) : String(Number(v.toFixed(2)));
30304
+ }
30305
+ function sceneObjectShapes(obj, canvasWidth, canvasHeight) {
30306
+ const out = [];
30307
+ const color = obj.color ?? "#334155";
30308
+ switch (obj.kind) {
30309
+ case "ground": {
30310
+ const xStart = obj.x1 ?? 0;
30311
+ const xEnd = obj.x2 ?? canvasWidth;
30312
+ const y = obj.y ?? 0;
30313
+ out.push({ type: "line", x1: xStart, y1: y, x2: xEnd, y2: y, color, lineWidth: 2 });
30314
+ for (let hx = xStart + 7; hx <= xEnd; hx += 14) {
30315
+ out.push({ type: "line", x1: hx, y1: y, x2: hx - 7, y2: y + 7, color, lineWidth: 1 });
30316
+ }
30317
+ if (obj.label) {
30318
+ out.push({
30319
+ type: "text",
30320
+ x: (xStart + xEnd) / 2,
30321
+ y: y - 10,
30322
+ text: obj.label,
30323
+ color: PHYSICS_LABEL_COLOR,
30324
+ fontSize: 11,
30325
+ align: "center"
30326
+ });
30327
+ }
30328
+ break;
30329
+ }
30330
+ case "wall": {
30331
+ const yStart = obj.y1 ?? 0;
30332
+ const yEnd = obj.y2 ?? canvasHeight;
30333
+ const x = obj.x ?? 0;
30334
+ out.push({ type: "line", x1: x, y1: yStart, x2: x, y2: yEnd, color, lineWidth: 2 });
30335
+ for (let hy = yStart + 7; hy <= yEnd; hy += 14) {
30336
+ out.push({ type: "line", x1: x, y1: hy, x2: x - 7, y2: hy + 7, color, lineWidth: 1 });
30337
+ }
30338
+ if (obj.label) {
30339
+ out.push({
30340
+ type: "text",
30341
+ x: x + 12,
30342
+ y: (yStart + yEnd) / 2,
30343
+ text: obj.label,
30344
+ color: PHYSICS_LABEL_COLOR,
30345
+ fontSize: 11,
30346
+ align: "left"
30347
+ });
30348
+ }
30349
+ break;
30350
+ }
30351
+ case "ramp": {
30352
+ const x1 = obj.x1 ?? 0;
30353
+ const y1 = obj.y1 ?? 0;
30354
+ const x2 = obj.x2 ?? canvasWidth;
30355
+ const y2 = obj.y2 ?? canvasHeight;
30356
+ out.push({
30357
+ type: "polygon",
30358
+ points: [
30359
+ { x: x1, y: y1 },
30360
+ { x: x2, y: y2 },
30361
+ { x: x1, y: y2 }
30362
+ ],
30363
+ color,
30364
+ fill: obj.fill ?? "#e2e8f0",
30365
+ lineWidth: 2
30366
+ });
30367
+ if (obj.label) {
30368
+ out.push({
30369
+ type: "text",
30370
+ x: (2 * x1 + x2) / 3,
30371
+ y: (y1 + 2 * y2) / 3,
30372
+ text: obj.label,
30373
+ color: PHYSICS_LABEL_COLOR,
30374
+ fontSize: 11,
30375
+ align: "center"
30376
+ });
30377
+ }
30378
+ break;
30379
+ }
30380
+ case "box": {
30381
+ const x = obj.x ?? 0;
30382
+ const y = obj.y ?? 0;
30383
+ const w = obj.width ?? 40;
30384
+ const h = obj.height ?? 40;
30385
+ out.push({ type: "rect", x, y, width: w, height: h, color, fill: obj.fill, lineWidth: 2 });
30386
+ if (obj.label) {
30387
+ out.push({
30388
+ type: "text",
30389
+ x: x + w / 2,
30390
+ y: y + h / 2,
30391
+ text: obj.label,
30392
+ color: PHYSICS_LABEL_COLOR,
30393
+ fontSize: 11,
30394
+ align: "center"
30395
+ });
30396
+ }
30397
+ break;
30398
+ }
30399
+ case "pivot": {
30400
+ const x = obj.x ?? 0;
30401
+ const y = obj.y ?? 0;
30402
+ out.push({ type: "circle", x, y, radius: 5, color, fill: color });
30403
+ out.push({ type: "line", x1: x - 14, y1: y - 8, x2: x + 14, y2: y - 8, color, lineWidth: 1 });
30404
+ for (let k = 0; k < 5; k++) {
30405
+ const hx = x - 14 + 7 * k;
30406
+ out.push({ type: "line", x1: hx, y1: y - 8, x2: hx - 6, y2: y - 14, color, lineWidth: 1 });
30407
+ }
30408
+ if (obj.label) {
30409
+ out.push({
30410
+ type: "text",
30411
+ x,
30412
+ y: y - 20,
30413
+ text: obj.label,
30414
+ color: PHYSICS_LABEL_COLOR,
30415
+ fontSize: 11,
30416
+ align: "center"
30417
+ });
30418
+ }
30419
+ break;
30420
+ }
30421
+ }
30422
+ return out;
30423
+ }
30424
+ function trailShapes(trail) {
30425
+ const n = trail.points.length;
30426
+ if (n < 2) return [];
30427
+ const color = trail.color ?? "#94a3b8";
30428
+ const lineWidth = trail.width ?? 2;
30429
+ const fade = trail.fade ?? true;
30430
+ const globalOpacity = trail.opacity ?? 1;
30431
+ const out = [];
30432
+ for (let i = 0; i < n - 1; i++) {
30433
+ const a = trail.points[i];
30434
+ const b = trail.points[i + 1];
30435
+ const segmentOpacity = fade ? 0.12 + 0.68 * i / (n - 1) : 0.6;
30436
+ out.push({
30437
+ type: "line",
30438
+ x1: a.x,
30439
+ y1: a.y,
30440
+ x2: b.x,
30441
+ y2: b.y,
30442
+ color,
30443
+ lineWidth,
30444
+ opacity: segmentOpacity * globalOpacity
30445
+ });
30446
+ }
30447
+ return out;
30448
+ }
30449
+ function constraintShapes(c, a, b) {
30450
+ const color = c.color ?? "#9ca3af";
30451
+ const kind = c.kind ?? "rod";
30452
+ if (kind === "rod") {
30453
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2 }];
30454
+ }
30455
+ if (kind === "string") {
30456
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2, dash: "dashed" }];
30457
+ }
30458
+ const COILS = 8;
30459
+ const AMP = 7;
30460
+ const LEAD = 10;
30461
+ const dx = b.x - a.x;
30462
+ const dy = b.y - a.y;
30463
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
30464
+ const ux = dx / dist;
30465
+ const uy = dy / dist;
30466
+ const perpX = -uy;
30467
+ const perpY = ux;
30468
+ const aPrime = { x: a.x + LEAD * ux, y: a.y + LEAD * uy };
30469
+ const bPrime = { x: b.x - LEAD * ux, y: b.y - LEAD * uy };
30470
+ const m = 2 * COILS;
30471
+ const polyline = [{ x: a.x, y: a.y }, aPrime];
30472
+ for (let j = 1; j <= m; j++) {
30473
+ const t = j / (m + 1);
30474
+ const baseX = aPrime.x + t * (bPrime.x - aPrime.x);
30475
+ const baseY = aPrime.y + t * (bPrime.y - aPrime.y);
30476
+ const sign = j % 2 === 0 ? 1 : -1;
30477
+ polyline.push({ x: baseX + sign * AMP * perpX, y: baseY + sign * AMP * perpY });
30478
+ }
30479
+ polyline.push(bPrime, { x: b.x, y: b.y });
30480
+ const out = [];
30481
+ for (let i = 1; i < polyline.length; i++) {
30482
+ out.push({
30483
+ type: "line",
30484
+ x1: polyline[i - 1].x,
30485
+ y1: polyline[i - 1].y,
30486
+ x2: polyline[i].x,
30487
+ y2: polyline[i].y,
30488
+ color,
30489
+ lineWidth: 2
30490
+ });
30491
+ }
30492
+ return out;
30493
+ }
30494
+ function vectorShapes(v, bodyById) {
30495
+ let ax;
30496
+ let ay;
30497
+ if (v.body) {
30498
+ const anchor = bodyById.get(v.body);
30499
+ if (!anchor) return [];
30500
+ ax = anchor.x;
30501
+ ay = anchor.y;
30502
+ } else {
30503
+ ax = v.x ?? 0;
30504
+ ay = v.y ?? 0;
30505
+ }
30506
+ const scale = v.scale ?? 1;
30507
+ const color = v.color ?? "#dc2626";
30508
+ const tx = ax + v.dx * scale;
30509
+ const ty = ay + v.dy * scale;
30510
+ const out = [{ type: "arrow", x1: ax, y1: ay, x2: tx, y2: ty, color, lineWidth: 2, dash: v.dash }];
30511
+ if (v.label) {
30512
+ const dist = Math.max(1e-6, Math.hypot(tx - ax, ty - ay));
30513
+ const ux = (tx - ax) / dist;
30514
+ const uy = (ty - ay) / dist;
30515
+ out.push({
30516
+ type: "text",
30517
+ x: tx + 8 * ux,
30518
+ y: ty + 8 * uy,
30519
+ text: v.label,
30520
+ color,
30521
+ fontSize: 11,
30522
+ align: "center"
30523
+ });
30524
+ }
30525
+ return out;
30526
+ }
30527
+ function angleMarkerShapes(a) {
30528
+ const radius = a.radius ?? 26;
30529
+ const color = a.color ?? "#0ea5e9";
30530
+ const out = [
30531
+ {
30532
+ type: "ellipse",
30533
+ x: a.x,
30534
+ y: a.y,
30535
+ width: radius * 2,
30536
+ height: radius * 2,
30537
+ startAngle: a.from,
30538
+ endAngle: a.to,
30539
+ color,
30540
+ lineWidth: 2
30541
+ }
30542
+ ];
30543
+ if (a.label) {
30544
+ const mid = (a.from + a.to) / 2 * (Math.PI / 180);
30545
+ out.push({
30546
+ type: "text",
30547
+ x: a.x + (radius + 13) * Math.cos(mid),
30548
+ y: a.y + (radius + 13) * Math.sin(mid),
30549
+ text: a.label,
30550
+ color,
30551
+ fontSize: 11,
30552
+ align: "center"
30553
+ });
30554
+ }
30555
+ return out;
30556
+ }
30557
+ function fieldShapes(field, canvasWidth, canvasHeight) {
30558
+ const spacing = field.spacing ?? 48;
30559
+ const size = field.size ?? 14;
30560
+ const color = field.color ?? "#94a3b8";
30561
+ const regionX = field.x ?? 0;
30562
+ const regionY = field.y ?? 0;
30563
+ const regionW = field.width ?? canvasWidth;
30564
+ const regionH = field.height ?? canvasHeight;
30565
+ const out = [];
30566
+ for (let gx = regionX + spacing / 2; gx < regionX + regionW; gx += spacing) {
30567
+ for (let gy = regionY + spacing / 2; gy < regionY + regionH; gy += spacing) {
30568
+ if (field.kind === "arrows") {
30569
+ const rad = (field.angle ?? 0) * Math.PI / 180;
30570
+ const hx = Math.cos(rad) * size / 2;
30571
+ const hy = Math.sin(rad) * size / 2;
30572
+ out.push({ type: "arrow", x1: gx - hx, y1: gy - hy, x2: gx + hx, y2: gy + hy, color, lineWidth: 2 });
30573
+ } else if (field.kind === "into") {
30574
+ const r = size / 3;
30575
+ const d = 0.6 * r * Math.SQRT1_2;
30576
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30577
+ out.push({ type: "line", x1: gx - d, y1: gy - d, x2: gx + d, y2: gy + d, color, lineWidth: 1 });
30578
+ out.push({ type: "line", x1: gx - d, y1: gy + d, x2: gx + d, y2: gy - d, color, lineWidth: 1 });
30579
+ } else {
30580
+ const r = size / 3;
30581
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30582
+ out.push({ type: "circle", x: gx, y: gy, radius: 1.5, color, fill: color });
30583
+ }
30584
+ }
30585
+ }
30586
+ return out;
30587
+ }
30588
+ function meterShapes(meters, canvasHeight) {
30589
+ const n = meters.length;
30590
+ const out = [];
30591
+ const sharedMax = Math.max(1e-6, ...meters.map((m) => m.value));
30592
+ meters.forEach((meter, i) => {
30593
+ const rowY = canvasHeight - 10 - 16 * (n - i);
30594
+ const color = meter.color ?? "#3b82f6";
30595
+ const M = meter.max ?? sharedMax;
30596
+ const w = Math.round(Math.min(1, Math.max(0, meter.value / M)) * 110);
30597
+ out.push({ type: "text", x: 8, y: rowY + 8, text: meter.label, color: PHYSICS_LABEL_COLOR, fontSize: 10 });
30598
+ out.push({ type: "rect", x: 52, y: rowY, width: w, height: 10, color, fill: color });
30599
+ out.push({
30600
+ type: "text",
30601
+ x: 166,
30602
+ y: rowY + 8,
30603
+ text: formatMeterValue(meter.value),
30604
+ color: "#6b7280",
30605
+ fontSize: 9
30606
+ });
30607
+ });
30608
+ return out;
30609
+ }
30610
+ var physicsLog2, PHYSICS_LABEL_COLOR, PhysicsCanvas;
28172
30611
  var init_PhysicsCanvas = __esm({
28173
30612
  "components/learning/molecules/PhysicsCanvas.tsx"() {
28174
30613
  "use client";
28175
30614
  init_atoms();
28176
30615
  init_Stack();
28177
30616
  init_LearningCanvas();
30617
+ init_learningScene3D();
30618
+ physicsLog2 = createLogger("almadar:ui:physics-canvas");
30619
+ PHYSICS_LABEL_COLOR = "#374151";
28178
30620
  PhysicsCanvas = ({
28179
30621
  className,
28180
30622
  width = 600,
28181
30623
  height = 400,
28182
30624
  title,
28183
30625
  backgroundColor,
30626
+ mode = "2d",
30627
+ camera,
30628
+ lighting,
30629
+ post,
28184
30630
  bodies = [],
28185
30631
  constraints = [],
28186
30632
  showVelocity = true,
28187
30633
  showForces = false,
28188
30634
  velocityScale = 20,
28189
30635
  forceScale = 20,
30636
+ sceneObjects = [],
30637
+ trails = [],
30638
+ vectors = [],
30639
+ surface3d,
30640
+ vectors3d = [],
30641
+ vectorScale = 1,
30642
+ angles = [],
30643
+ field,
30644
+ meters = [],
28190
30645
  shapes = [],
28191
- interactive = false,
30646
+ readouts,
30647
+ traces,
30648
+ showGrid,
30649
+ shadows,
30650
+ interactive,
28192
30651
  animate = false,
28193
30652
  onShapeClick,
28194
30653
  isLoading,
@@ -28200,19 +30659,14 @@ var init_PhysicsCanvas = __esm({
28200
30659
  for (const b of bodies) {
28201
30660
  if (b.id) bodyById.set(b.id, b);
28202
30661
  }
30662
+ if (field) out.push(...fieldShapes(field, width, height));
30663
+ for (const obj of sceneObjects) out.push(...sceneObjectShapes(obj, width, height));
30664
+ for (const trail of trails) out.push(...trailShapes(trail));
28203
30665
  for (const c of constraints) {
28204
30666
  const a = bodyById.get(c.from);
28205
30667
  const b = bodyById.get(c.to);
28206
30668
  if (!a || !b) continue;
28207
- out.push({
28208
- type: "line",
28209
- x1: a.x,
28210
- y1: a.y,
28211
- x2: b.x,
28212
- y2: b.y,
28213
- color: c.color ?? "#9ca3af",
28214
- lineWidth: 2
28215
- });
30669
+ out.push(...constraintShapes(c, a, b));
28216
30670
  }
28217
30671
  for (const b of bodies) {
28218
30672
  out.push({
@@ -28257,9 +30711,181 @@ var init_PhysicsCanvas = __esm({
28257
30711
  });
28258
30712
  }
28259
30713
  }
30714
+ for (const v of vectors) out.push(...vectorShapes(v, bodyById));
30715
+ for (const a of angles) out.push(...angleMarkerShapes(a));
30716
+ if (meters.length > 0) out.push(...meterShapes(meters, height));
28260
30717
  out.push(...shapes);
28261
30718
  return out;
28262
- }, [bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes]);
30719
+ }, [
30720
+ bodies,
30721
+ constraints,
30722
+ showVelocity,
30723
+ showForces,
30724
+ velocityScale,
30725
+ forceScale,
30726
+ sceneObjects,
30727
+ trails,
30728
+ vectors,
30729
+ angles,
30730
+ field,
30731
+ meters,
30732
+ shapes,
30733
+ width,
30734
+ height
30735
+ ]);
30736
+ const drawables3D = useMemo(() => {
30737
+ if (mode !== "3d") return [];
30738
+ if (shapes.length > 0) {
30739
+ physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
30740
+ }
30741
+ if (sceneObjects.length > 0) {
30742
+ physicsLog2.debug("sceneObjects ignored in 3D mode (pixel-authored 2D vocabulary)", { count: sceneObjects.length });
30743
+ }
30744
+ if (vectors.length > 0) {
30745
+ physicsLog2.debug("vectors ignored in 3D mode (pixel-authored 2D vocabulary)", { count: vectors.length });
30746
+ }
30747
+ if (angles.length > 0) {
30748
+ physicsLog2.debug("angles ignored in 3D mode (pixel-authored 2D vocabulary)", { count: angles.length });
30749
+ }
30750
+ if (field) {
30751
+ physicsLog2.debug("field ignored in 3D mode (pixel-authored 2D vocabulary)");
30752
+ }
30753
+ if (meters.length > 0) {
30754
+ physicsLog2.debug("meters ignored in 3D mode (pixel-authored 2D vocabulary)", { count: meters.length });
30755
+ }
30756
+ if (animate) {
30757
+ physicsLog2.debug("animate ignored in 3D mode (motion is entity-state driven)");
30758
+ }
30759
+ const out = [];
30760
+ const labelColor = labelColorForBackground(backgroundColor);
30761
+ const bodyById = /* @__PURE__ */ new Map();
30762
+ for (const b of bodies) {
30763
+ if (b.id) bodyById.set(b.id, b);
30764
+ }
30765
+ for (const c of constraints) {
30766
+ const a = bodyById.get(c.from);
30767
+ const b = bodyById.get(c.to);
30768
+ if (!a || !b) continue;
30769
+ const rodRadius = Math.max(0.05, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.15);
30770
+ const rod = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], rodRadius, c.color ?? "#9ca3af");
30771
+ if (rod) out.push(rod);
30772
+ }
30773
+ for (const b of bodies) {
30774
+ const radius = b.radius ?? 0.5;
30775
+ const bz = b.z ?? 0;
30776
+ out.push(meshSphere(b.id, b.x, b.y, bz, radius, b.color ?? "#2563eb"));
30777
+ if (b.label) {
30778
+ out.push(billboardLabel(b.label, b.x, b.y, bz + radius, { color: labelColor }));
30779
+ }
30780
+ const vx = b.vx ?? 0;
30781
+ const vy = b.vy ?? 0;
30782
+ const vz = b.vz ?? 0;
30783
+ const arrowRadius = Math.max(0.05, radius * 0.15);
30784
+ if (showVelocity && (vx !== 0 || vy !== 0 || vz !== 0)) {
30785
+ const arrow = arrowBetween(
30786
+ [b.x, b.y, bz],
30787
+ [b.x + vx * velocityScale, b.y + vy * velocityScale, bz + vz * velocityScale],
30788
+ "#16a34a",
30789
+ arrowRadius
30790
+ );
30791
+ if (arrow) out.push(arrow);
30792
+ }
30793
+ const fx = b.fx ?? 0;
30794
+ const fy = b.fy ?? 0;
30795
+ const fz = b.fz ?? 0;
30796
+ if (showForces && (fx !== 0 || fy !== 0 || fz !== 0)) {
30797
+ const arrow = arrowBetween(
30798
+ [b.x, b.y, bz],
30799
+ [b.x + fx * forceScale, b.y + fy * forceScale, bz + fz * forceScale],
30800
+ "#dc2626",
30801
+ arrowRadius
30802
+ );
30803
+ if (arrow) out.push(arrow);
30804
+ }
30805
+ }
30806
+ for (const trail of trails) {
30807
+ if (trail.fade !== void 0) {
30808
+ physicsLog2.debug("trail.fade ignored in 3D mode (2D-only fade curve \u2014 3D draws an opaque tube)", { id: trail.id });
30809
+ }
30810
+ const points = trail.points.map((p) => [p.x, p.y, p.z ?? 0]);
30811
+ out.push(
30812
+ ...polylineTube(points, trail.width ?? 0.05, trail.color ?? "#94a3b8", {
30813
+ ...trail.opacity !== void 0 ? { opacity: trail.opacity } : {}
30814
+ })
30815
+ );
30816
+ }
30817
+ if (surface3d) {
30818
+ out.push(...heightFieldMesh(surface3d));
30819
+ }
30820
+ if (vectors3d.length > 0) {
30821
+ out.push(
30822
+ ...arrowField(
30823
+ vectors3d.map((v) => ({
30824
+ id: v.id,
30825
+ from: [v.x, v.y, v.z ?? 0],
30826
+ delta: [v.dx, v.dy, v.dz ?? 0],
30827
+ color: v.color,
30828
+ label: v.label,
30829
+ width: v.width
30830
+ })),
30831
+ { scale: vectorScale, labelColor }
30832
+ )
30833
+ );
30834
+ }
30835
+ return out;
30836
+ }, [
30837
+ mode,
30838
+ bodies,
30839
+ constraints,
30840
+ showVelocity,
30841
+ showForces,
30842
+ velocityScale,
30843
+ forceScale,
30844
+ shapes,
30845
+ sceneObjects,
30846
+ trails,
30847
+ vectors,
30848
+ surface3d,
30849
+ vectors3d,
30850
+ vectorScale,
30851
+ angles,
30852
+ field,
30853
+ meters,
30854
+ animate,
30855
+ backgroundColor
30856
+ ]);
30857
+ const bodyIndexById = useMemo(() => {
30858
+ const m = /* @__PURE__ */ new Map();
30859
+ vectors3d.forEach((v, i) => {
30860
+ if (v.id) m.set(v.id, i);
30861
+ });
30862
+ bodies.forEach((b, i) => {
30863
+ if (b.id) m.set(b.id, i);
30864
+ });
30865
+ return m;
30866
+ }, [bodies, vectors3d]);
30867
+ if (mode === "3d") {
30868
+ return /* @__PURE__ */ jsx(
30869
+ LearningScene3D,
30870
+ {
30871
+ className,
30872
+ width,
30873
+ height,
30874
+ title,
30875
+ backgroundColor,
30876
+ drawables: drawables3D,
30877
+ camera,
30878
+ lighting,
30879
+ post,
30880
+ showGrid,
30881
+ shadows,
30882
+ interactive,
30883
+ isLoading,
30884
+ error,
30885
+ onItemClick: get3DClickPayload(onShapeClick, bodyIndexById)
30886
+ }
30887
+ );
30888
+ }
28263
30889
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
28264
30890
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
28265
30891
  /* @__PURE__ */ jsx(
@@ -28269,7 +30895,9 @@ var init_PhysicsCanvas = __esm({
28269
30895
  height,
28270
30896
  backgroundColor,
28271
30897
  shapes: derivedShapes,
28272
- interactive,
30898
+ readouts,
30899
+ traces,
30900
+ interactive: interactive ?? false,
28273
30901
  animate,
28274
30902
  onShapeClick,
28275
30903
  isLoading,
@@ -28419,7 +31047,7 @@ function layoutFlow(nodeIds, adjacency, roots, width, height, margin) {
28419
31047
  }
28420
31048
  return nodeIds.map((id) => positions.get(id));
28421
31049
  }
28422
- function layoutTree(nodeIds, adjacency, roots, width, height, margin) {
31050
+ function layoutTree2(nodeIds, adjacency, roots, width, height, margin) {
28423
31051
  const effectiveRoots = roots.length > 0 ? roots : [nodeIds[0]];
28424
31052
  const layers = assignLayers(nodeIds, adjacency, effectiveRoots);
28425
31053
  const maxLayer = Math.max(...Array.from(layers.values()));
@@ -28475,7 +31103,7 @@ function computeStaticLayout(mode, input) {
28475
31103
  const adjacency = buildAdjacency(nodeIds, edges);
28476
31104
  const roots = findRoots(nodeIds, adjacency);
28477
31105
  if (mode === "flow") return layoutFlow(nodeIds, adjacency, roots, width, height, margin);
28478
- if (mode === "tree") return layoutTree(nodeIds, adjacency, roots, width, height, margin);
31106
+ if (mode === "tree") return layoutTree2(nodeIds, adjacency, roots, width, height, margin);
28479
31107
  return layoutRadial(nodeIds, adjacency, roots, width, height, margin);
28480
31108
  }
28481
31109
  var init_graphViewLayouts = __esm({
@@ -28887,12 +31515,12 @@ var init_MapView = __esm({
28887
31515
  shadowSize: [41, 41]
28888
31516
  });
28889
31517
  L.Marker.prototype.options.icon = defaultIcon;
28890
- const { useEffect: useEffect63, useRef: useRef62, useCallback: useCallback96, useState: useState94 } = React87__default;
31518
+ const { useEffect: useEffect63, useRef: useRef63, useCallback: useCallback96, useState: useState94 } = React87__default;
28891
31519
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
28892
31520
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
28893
31521
  function MapUpdater({ centerLat, centerLng, zoom }) {
28894
31522
  const map = useMap();
28895
- const prevRef = useRef62({ centerLat, centerLng, zoom });
31523
+ const prevRef = useRef63({ centerLat, centerLng, zoom });
28896
31524
  useEffect63(() => {
28897
31525
  const prev = prevRef.current;
28898
31526
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
@@ -29718,26 +32346,6 @@ var init_Lightbox = __esm({
29718
32346
  Lightbox.displayName = "Lightbox";
29719
32347
  }
29720
32348
  });
29721
- function useMediaQuery(query) {
29722
- const subscribe = useCallback(
29723
- (onChange) => {
29724
- const mql = window.matchMedia(query);
29725
- mql.addEventListener("change", onChange);
29726
- return () => mql.removeEventListener("change", onChange);
29727
- },
29728
- [query]
29729
- );
29730
- return useSyncExternalStore(
29731
- subscribe,
29732
- () => window.matchMedia(query).matches,
29733
- () => false
29734
- );
29735
- }
29736
- var init_useMediaQuery = __esm({
29737
- "hooks/useMediaQuery.ts"() {
29738
- "use client";
29739
- }
29740
- });
29741
32349
  function renderIconInput3(icon, props) {
29742
32350
  return typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsx(Icon, { icon, ...props });
29743
32351
  }
@@ -29776,8 +32384,8 @@ function TableView({
29776
32384
  columns,
29777
32385
  fields,
29778
32386
  itemActions,
29779
- maxInlineActions,
29780
- itemClickEvent,
32387
+ maxInlineActions: _maxInlineActions,
32388
+ itemClickEvent = "",
29781
32389
  selectable = false,
29782
32390
  selectEvent,
29783
32391
  selectedIds,
@@ -29827,7 +32435,6 @@ function TableView({
29827
32435
  const hasMore = pageSize > 0 && visibleCount < ordered2.length;
29828
32436
  const hasRenderProp = typeof children === "function";
29829
32437
  const idField = dndItemIdField ?? "id";
29830
- const isCoarsePointer = useMediaQuery("(pointer: coarse)");
29831
32438
  React87__default.useEffect(() => {
29832
32439
  tableViewLog.debug("render", {
29833
32440
  rowCount: data.length,
@@ -29869,21 +32476,14 @@ function TableView({
29869
32476
  const dir = sortColumn === (col.field ?? col.key) && sortDirection === "asc" ? "desc" : "asc";
29870
32477
  eventBus.emit(`UI:${sortEvent}`, { column: col.field ?? col.key, direction: dir });
29871
32478
  };
29872
- const handleActionClick = (action, row) => (e) => {
29873
- e.stopPropagation();
29874
- const payload = {
29875
- id: row.id,
29876
- row
29877
- };
29878
- eventBus.emit(`UI:${action.event}`, payload);
29879
- };
32479
+ const rowClickEvent = itemClickEvent || actionDefs.find((a) => a.variant !== "danger")?.event;
29880
32480
  const handleRowClick = (row) => () => {
29881
- if (!itemClickEvent) return;
32481
+ if (!rowClickEvent) return;
29882
32482
  const payload = {
29883
32483
  id: row.id,
29884
32484
  row
29885
32485
  };
29886
- eventBus.emit(`UI:${itemClickEvent}`, payload);
32486
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
29887
32487
  };
29888
32488
  const colFloors = React87__default.useMemo(
29889
32489
  () => colDefs.map((col) => {
@@ -29899,10 +32499,7 @@ function TableView({
29899
32499
  const statusNode = isLoading ? /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) }) : error ? /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "error", children: error.message }) }) : data.length === 0 ? /* @__PURE__ */ jsx(Box, { className: "text-center py-12", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: emptyMessage || t("empty.noItems") }) }) : null;
29900
32500
  const lk = LOOKS[look];
29901
32501
  const hasActions = actionDefs.length > 0;
29902
- const effectiveMaxInline = isCoarsePointer ? 0 : maxInlineActions;
29903
- const inlineActionCount = hasActions ? effectiveMaxInline != null ? Math.min(actionDefs.length, effectiveMaxInline) : actionDefs.length : 0;
29904
- const hasOverflowActions = hasActions && effectiveMaxInline != null && actionDefs.length > effectiveMaxInline;
29905
- const actionsTrack = hasActions ? `${inlineActionCount * 6 + (hasOverflowActions ? 3 : 0)}rem` : null;
32502
+ const actionsTrack = hasActions ? "3rem" : null;
29906
32503
  const gridTemplateColumns = [
29907
32504
  selectable ? "auto" : null,
29908
32505
  ...colDefs.map((c, i) => c.width ?? `minmax(${colFloors[i]}ch, 1fr)`),
@@ -29949,7 +32546,7 @@ function TableView({
29949
32546
  col.key
29950
32547
  );
29951
32548
  }),
29952
- hasActions && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)]" })
32549
+ hasActions && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)] border-l border-[var(--color-border)] h-full" })
29953
32550
  ]
29954
32551
  }
29955
32552
  );
@@ -29961,12 +32558,12 @@ function TableView({
29961
32558
  role: "row",
29962
32559
  "data-entity-row": true,
29963
32560
  "data-entity-id": id,
29964
- onClick: itemClickEvent ? handleRowClick(row) : void 0,
32561
+ onClick: rowClickEvent ? handleRowClick(row) : void 0,
29965
32562
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
29966
32563
  className: cn(
29967
32564
  "group items-center gap-3 transition-colors duration-fast",
29968
32565
  hasRenderProp ? "flex" : "grid",
29969
- itemClickEvent && "cursor-pointer",
32566
+ rowClickEvent && "cursor-pointer",
29970
32567
  lk.rowPad,
29971
32568
  lk.divider && "border-b border-[var(--color-border)]",
29972
32569
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -29974,7 +32571,7 @@ function TableView({
29974
32571
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
29975
32572
  ),
29976
32573
  children: [
29977
- selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
32574
+ selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
29978
32575
  Checkbox,
29979
32576
  {
29980
32577
  checked: selected.has(id),
@@ -29995,53 +32592,37 @@ function TableView({
29995
32592
  }
29996
32593
  return /* @__PURE__ */ jsx(Box, { role: "cell", className: cellBase, children: /* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children: formatCell(raw, col.format) }) }, col.key);
29997
32594
  }),
29998
- hasActions && /* @__PURE__ */ jsxs(
32595
+ hasActions && /* @__PURE__ */ jsx(
29999
32596
  HStack,
30000
32597
  {
30001
32598
  gap: "xs",
30002
- onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
32599
+ onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0,
30003
32600
  className: cn(
30004
32601
  // Pinned: the fixed column tracks routinely overflow the caller's
30005
- // scroll container, which used to leave the actions off-screen.
30006
- // Opaque so scrolled cells pass underneath it.
32602
+ // scroll container, which would leave the kebab off-screen.
32603
+ // Opaque + hairline edge so it reads as a pinned column, not a
32604
+ // floating control, while scrolled cells pass underneath.
30007
32605
  "justify-end flex-shrink-0 sticky right-0 z-[1] transition-colors",
32606
+ "border-l border-[var(--color-border)]",
30008
32607
  lk.striped && index % 2 === 1 ? "bg-[var(--color-surface-subtle)]" : "bg-[var(--color-card)] group-hover:bg-[var(--color-surface-subtle)]"
30009
32608
  ),
30010
- children: [
30011
- (effectiveMaxInline != null ? actionDefs.slice(0, effectiveMaxInline) : actionDefs).map((action, i) => /* @__PURE__ */ jsxs(
30012
- Button,
30013
- {
30014
- variant: action.variant === "primary" ? "primary" : "ghost",
30015
- size: "sm",
30016
- onClick: handleActionClick(action, row),
30017
- "data-testid": `action-${action.event}`,
30018
- "data-row-id": String(row.id),
30019
- className: cn(action.variant === "danger" && "text-error hover:text-error hover:bg-error/10"),
30020
- children: [
30021
- action.icon && renderIconInput3(action.icon, { size: "xs", className: "mr-1" }),
30022
- action.label
30023
- ]
30024
- },
30025
- i
30026
- )),
30027
- effectiveMaxInline != null && actionDefs.length > effectiveMaxInline && /* @__PURE__ */ jsx(
30028
- Menu,
30029
- {
30030
- position: "bottom-end",
30031
- trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
30032
- items: actionDefs.slice(effectiveMaxInline).map((action) => ({
30033
- label: action.label,
30034
- icon: action.icon,
30035
- event: action.event,
30036
- variant: action.variant === "danger" ? "danger" : "default",
30037
- onClick: () => eventBus.emit(`UI:${action.event}`, {
30038
- id: row.id,
30039
- row
30040
- })
30041
- }))
30042
- }
30043
- )
30044
- ]
32609
+ children: /* @__PURE__ */ jsx(
32610
+ Menu,
32611
+ {
32612
+ position: "bottom-end",
32613
+ trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", "data-row-id": String(row.id), children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
32614
+ items: actionDefs.map((action) => ({
32615
+ label: action.label,
32616
+ icon: action.icon,
32617
+ event: action.event,
32618
+ variant: action.variant === "danger" ? "danger" : "default",
32619
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
32620
+ id: row.id,
32621
+ row
32622
+ })
32623
+ }))
32624
+ }
32625
+ )
30045
32626
  }
30046
32627
  )
30047
32628
  ]
@@ -30084,7 +32665,6 @@ var init_TableView = __esm({
30084
32665
  init_format();
30085
32666
  init_getNestedValue();
30086
32667
  init_useEventBus();
30087
- init_useMediaQuery();
30088
32668
  init_Box();
30089
32669
  init_Stack();
30090
32670
  init_Typography();
@@ -39715,7 +42295,7 @@ function getEnumOptions(field) {
39715
42295
  }));
39716
42296
  }
39717
42297
  const validation = field.validation;
39718
- if (validation?.enum && Array.isArray(validation.enum)) {
42298
+ if (validation?.enum && validation.enum.length > 0) {
39719
42299
  return validation.enum.map((v) => ({
39720
42300
  value: v,
39721
42301
  label: v.charAt(0).toUpperCase() + v.slice(1).replace(/_/g, " ")
@@ -45143,6 +47723,7 @@ var init_component_registry_generated = __esm({
45143
47723
  init_ActionTile();
45144
47724
  init_ActivationBlock();
45145
47725
  init_ComponentPatterns();
47726
+ init_AlgoGraphCanvas();
45146
47727
  init_AlgorithmCanvas();
45147
47728
  init_AnimatedCounter();
45148
47729
  init_AnimatedGraphic();
@@ -45406,6 +47987,7 @@ var init_component_registry_generated = __esm({
45406
47987
  "ActivationBlock": ActivationBlock,
45407
47988
  "Alert": AlertPattern,
45408
47989
  "AlertPattern": AlertPattern,
47990
+ "AlgoGraphCanvas": AlgoGraphCanvas,
45409
47991
  "AlgorithmCanvas": AlgorithmCanvas,
45410
47992
  "AnimatedCounter": AnimatedCounter,
45411
47993
  "AnimatedGraphic": AnimatedGraphic,