@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
@@ -209,7 +209,7 @@ function useEventBus() {
209
209
  return {
210
210
  ...baseBus,
211
211
  emit: (type, payload, source) => {
212
- if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
212
+ if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".") && !source?.trait) {
213
213
  scopeLog.warn("emit:bare-key-no-scope", { type });
214
214
  }
215
215
  baseBus.emit(type, payload, source);
@@ -613,26 +613,6 @@ var init_useDragReorder = __esm({
613
613
  "use client";
614
614
  }
615
615
  });
616
- function useMediaQuery(query) {
617
- const subscribe = React85.useCallback(
618
- (onChange) => {
619
- const mql = window.matchMedia(query);
620
- mql.addEventListener("change", onChange);
621
- return () => mql.removeEventListener("change", onChange);
622
- },
623
- [query]
624
- );
625
- return React85.useSyncExternalStore(
626
- subscribe,
627
- () => window.matchMedia(query).matches,
628
- () => false
629
- );
630
- }
631
- var init_useMediaQuery = __esm({
632
- "hooks/useMediaQuery.ts"() {
633
- "use client";
634
- }
635
- });
636
616
  function useInfiniteScroll(onLoadMore, options = {}) {
637
617
  const { rootMargin = "200px", hasMore = true, isLoading = false } = options;
638
618
  const observerRef = React85.useRef(null);
@@ -10729,9 +10709,10 @@ function Canvas({
10729
10709
  drawables: [...drawables ?? [], ...childDrawables],
10730
10710
  isLoading,
10731
10711
  cameraMode: to3DCameraMode(camera?.mode),
10732
- ...zoom !== void 0 ? { scale: zoom } : {},
10712
+ ...zoom !== void 0 ? { zoom } : {},
10733
10713
  ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
10734
10714
  ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
10715
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
10735
10716
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
10736
10717
  unitScale,
10737
10718
  backgroundColor,
@@ -11732,6 +11713,14 @@ function shapeBounds(shape) {
11732
11713
  w: shape.radius * 2 + 8,
11733
11714
  h: shape.radius * 2 + 8
11734
11715
  };
11716
+ case "ellipse":
11717
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11718
+ return {
11719
+ x: shape.x - shape.width / 2 - 4,
11720
+ y: shape.y - shape.height / 2 - 4,
11721
+ w: shape.width + 8,
11722
+ h: shape.height + 8
11723
+ };
11735
11724
  case "rect":
11736
11725
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11737
11726
  return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
@@ -11765,13 +11754,14 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
11765
11754
  ctx.closePath();
11766
11755
  ctx.fill();
11767
11756
  }
11768
- function drawShape(ctx, shape, width, height) {
11757
+ function drawShape(ctx, shape, width, height, allShapes) {
11769
11758
  ctx.save();
11770
11759
  const opacity = shape.opacity ?? 1;
11771
11760
  ctx.globalAlpha = opacity;
11772
11761
  const stroke = resolveColor2(shape.color, ctx, "#333333");
11773
11762
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
11774
11763
  ctx.lineWidth = shape.lineWidth ?? 2;
11764
+ if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
11775
11765
  switch (shape.type) {
11776
11766
  case "grid": {
11777
11767
  const step = shape.step ?? 40;
@@ -11837,6 +11827,20 @@ function drawShape(ctx, shape, width, height) {
11837
11827
  ctx.stroke();
11838
11828
  break;
11839
11829
  }
11830
+ case "ellipse": {
11831
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11832
+ const startAngle = (shape.startAngle ?? 0) * Math.PI / 180;
11833
+ const endAngle = (shape.endAngle ?? 360) * Math.PI / 180;
11834
+ ctx.beginPath();
11835
+ ctx.ellipse(shape.x, shape.y, shape.width / 2, shape.height / 2, 0, startAngle, endAngle);
11836
+ if (fill) {
11837
+ ctx.fillStyle = fill;
11838
+ ctx.fill();
11839
+ }
11840
+ ctx.strokeStyle = stroke;
11841
+ ctx.stroke();
11842
+ break;
11843
+ }
11840
11844
  case "rect": {
11841
11845
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11842
11846
  if (fill) {
@@ -11883,21 +11887,153 @@ function drawShape(ctx, shape, width, height) {
11883
11887
  ctx.fillText(shape.text, shape.x, shape.y);
11884
11888
  break;
11885
11889
  }
11890
+ case "venn-region": {
11891
+ const resolveCircles = (ids) => (ids ?? []).flatMap((id) => {
11892
+ const c = allShapes.find((s) => s.type === "circle" && s.id === id);
11893
+ return c && c.x != null && c.y != null && c.radius != null ? [{ x: c.x, y: c.y, radius: c.radius }] : [];
11894
+ });
11895
+ const inside = resolveCircles(shape.inside);
11896
+ if (inside.length === 0) break;
11897
+ const outside = resolveCircles(shape.outside);
11898
+ const off = document.createElement("canvas");
11899
+ off.width = ctx.canvas.width;
11900
+ off.height = ctx.canvas.height;
11901
+ const octx = off.getContext("2d");
11902
+ if (!octx) break;
11903
+ octx.setTransform(ctx.getTransform());
11904
+ for (const c of inside) {
11905
+ const p = new Path2D();
11906
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11907
+ octx.clip(p);
11908
+ }
11909
+ octx.fillStyle = fill ?? stroke;
11910
+ octx.fillRect(0, 0, width, height);
11911
+ octx.globalCompositeOperation = "destination-out";
11912
+ for (const c of outside) {
11913
+ const p = new Path2D();
11914
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11915
+ octx.fill(p);
11916
+ }
11917
+ ctx.save();
11918
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
11919
+ ctx.drawImage(off, 0, 0);
11920
+ ctx.restore();
11921
+ break;
11922
+ }
11886
11923
  }
11887
11924
  ctx.restore();
11888
11925
  }
11889
- var LearningCanvas;
11926
+ function readoutShapes(readouts, width) {
11927
+ const out = [];
11928
+ const chipH = 18;
11929
+ const gap = 6;
11930
+ let rightEdge = width - 6;
11931
+ let rowY = 6;
11932
+ for (const readout of readouts) {
11933
+ const text = `${readout.label}: ${String(readout.value)}`;
11934
+ const chipW = Math.min(170, Math.max(34, text.length * 6 + 12));
11935
+ let chipX = rightEdge - chipW;
11936
+ if (chipX < 4) {
11937
+ rowY += chipH + 4;
11938
+ rightEdge = width - 6;
11939
+ chipX = rightEdge - chipW;
11940
+ }
11941
+ const color = readout.color ?? "#334155";
11942
+ out.push({ type: "rect", x: chipX, y: rowY, width: chipW, height: chipH, color, fill: color });
11943
+ out.push({
11944
+ type: "text",
11945
+ x: chipX + chipW / 2,
11946
+ y: rowY + chipH / 2,
11947
+ text,
11948
+ color: "#ffffff",
11949
+ fontSize: 10,
11950
+ align: "center"
11951
+ });
11952
+ rightEdge = chipX - gap;
11953
+ }
11954
+ return out;
11955
+ }
11956
+ function traceShapes(panel, k, width, height) {
11957
+ const w = panel.width ?? Math.round(width * 0.32);
11958
+ const h = panel.height ?? Math.round(height * 0.28);
11959
+ const x = panel.x ?? width - w - 8;
11960
+ const y = panel.y ?? height - h - 8 - k * (h + 8);
11961
+ const allSamples = panel.series.flatMap((series) => series.samples);
11962
+ let xLo = Math.min(...allSamples.map((p) => p.x));
11963
+ let xHi = Math.max(...allSamples.map((p) => p.x));
11964
+ let yLo = Math.min(...allSamples.map((p) => p.y));
11965
+ let yHi = Math.max(...allSamples.map((p) => p.y));
11966
+ if (xLo === xHi) {
11967
+ xLo -= 1;
11968
+ xHi += 1;
11969
+ }
11970
+ if (yLo === yHi) {
11971
+ yLo -= 1;
11972
+ yHi += 1;
11973
+ }
11974
+ const backgroundColor = panel.backgroundColor ?? "#ffffff";
11975
+ const frameColor = panel.frameColor ?? "#94a3b8";
11976
+ const out = [];
11977
+ out.push({
11978
+ type: "rect",
11979
+ x,
11980
+ y,
11981
+ width: w,
11982
+ height: h,
11983
+ color: backgroundColor,
11984
+ fill: backgroundColor,
11985
+ opacity: panel.backgroundOpacity ?? 0.85
11986
+ });
11987
+ out.push({ type: "rect", x, y, width: w, height: h, color: frameColor, lineWidth: 1 });
11988
+ panel.series.forEach((series, j) => {
11989
+ const color = series.color ?? TRACE_SERIES_COLORS[j % TRACE_SERIES_COLORS.length];
11990
+ const mapped = series.samples.map((p) => ({
11991
+ x: x + 4 + (p.x - xLo) / (xHi - xLo) * (w - 8),
11992
+ y: y + h - 4 - (p.y - yLo) / (yHi - yLo) * (h - 8)
11993
+ }));
11994
+ for (let i = 1; i < mapped.length; i++) {
11995
+ out.push({
11996
+ type: "line",
11997
+ x1: mapped[i - 1].x,
11998
+ y1: mapped[i - 1].y,
11999
+ x2: mapped[i].x,
12000
+ y2: mapped[i].y,
12001
+ color,
12002
+ lineWidth: 1.5
12003
+ });
12004
+ }
12005
+ if (mapped.length > 0) {
12006
+ const last = mapped[mapped.length - 1];
12007
+ out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
12008
+ }
12009
+ if (series.label) {
12010
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
12011
+ }
12012
+ });
12013
+ if (panel.yLabel) {
12014
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
12015
+ }
12016
+ if (panel.xLabel) {
12017
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
12018
+ }
12019
+ return out;
12020
+ }
12021
+ var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
11890
12022
  var init_LearningCanvas = __esm({
11891
12023
  "components/learning/atoms/LearningCanvas.tsx"() {
11892
12024
  "use client";
11893
12025
  init_cn();
11894
12026
  init_useEventBus();
12027
+ DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
12028
+ TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
11895
12029
  LearningCanvas = ({
11896
12030
  className,
11897
12031
  width = 600,
11898
12032
  height = 400,
11899
12033
  backgroundColor,
11900
12034
  shapes = [],
12035
+ readouts,
12036
+ traces,
11901
12037
  interactive = false,
11902
12038
  animate = false,
11903
12039
  onShapeClick,
@@ -11923,6 +12059,12 @@ var init_LearningCanvas = __esm({
11923
12059
  }
11924
12060
  return -1;
11925
12061
  }, [shapes]);
12062
+ const derivedShapes = React85.useMemo(() => {
12063
+ if (!traces?.length && !readouts?.length) return shapes;
12064
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
12065
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
12066
+ return [...shapes, ...traceOut, ...readoutOut];
12067
+ }, [shapes, traces, readouts, width, height]);
11926
12068
  const draw = React85.useCallback(() => {
11927
12069
  const canvas = canvasRef.current;
11928
12070
  if (!canvas) return;
@@ -11939,13 +12081,13 @@ var init_LearningCanvas = __esm({
11939
12081
  ctx.fillStyle = backgroundColor;
11940
12082
  ctx.fillRect(0, 0, width, height);
11941
12083
  }
11942
- for (const shape of shapes) {
11943
- if (shape.type !== "text") drawShape(ctx, shape, width, height);
12084
+ for (const shape of derivedShapes) {
12085
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
11944
12086
  }
11945
- for (const shape of shapes) {
11946
- if (shape.type === "text") drawShape(ctx, shape, width, height);
12087
+ for (const shape of derivedShapes) {
12088
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
11947
12089
  }
11948
- }, [width, height, backgroundColor, shapes]);
12090
+ }, [width, height, backgroundColor, derivedShapes]);
11949
12091
  React85.useEffect(() => {
11950
12092
  draw();
11951
12093
  }, [draw]);
@@ -13495,7 +13637,363 @@ var init_ComponentPatterns = __esm({
13495
13637
  AlertPattern.displayName = "AlertPattern";
13496
13638
  }
13497
13639
  });
13498
- var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, AlgorithmCanvas;
13640
+ function layoutCircle(nodes, width, height) {
13641
+ const cx = width / 2;
13642
+ const cy = height / 2;
13643
+ const radius = Math.max(10, Math.min(cx, cy) - 40);
13644
+ const positions = /* @__PURE__ */ new Map();
13645
+ const n = nodes.length;
13646
+ nodes.forEach((node, i) => {
13647
+ const angle = 2 * Math.PI * i / Math.max(n, 1) - Math.PI / 2;
13648
+ positions.set(node.id, { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) });
13649
+ });
13650
+ return positions;
13651
+ }
13652
+ function layoutTree(nodes, edges, root, width, height) {
13653
+ const nodeIds = nodes.map((n) => n.id);
13654
+ const idSet = new Set(nodeIds);
13655
+ const childrenOf = /* @__PURE__ */ new Map();
13656
+ const hasIncoming = /* @__PURE__ */ new Set();
13657
+ for (const e of edges) {
13658
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
13659
+ const list = childrenOf.get(e.from) ?? [];
13660
+ list.push(e.to);
13661
+ childrenOf.set(e.from, list);
13662
+ hasIncoming.add(e.to);
13663
+ }
13664
+ const depth = /* @__PURE__ */ new Map();
13665
+ const treeChildren = /* @__PURE__ */ new Map();
13666
+ const visited = /* @__PURE__ */ new Set();
13667
+ const bfsFrom = (start) => {
13668
+ if (visited.has(start)) return;
13669
+ visited.add(start);
13670
+ depth.set(start, 0);
13671
+ const queue = [start];
13672
+ while (queue.length > 0) {
13673
+ const u = queue.shift();
13674
+ for (const v of childrenOf.get(u) ?? []) {
13675
+ if (visited.has(v)) continue;
13676
+ visited.add(v);
13677
+ depth.set(v, (depth.get(u) ?? 0) + 1);
13678
+ const list = treeChildren.get(u) ?? [];
13679
+ list.push(v);
13680
+ treeChildren.set(u, list);
13681
+ queue.push(v);
13682
+ }
13683
+ }
13684
+ };
13685
+ const primaryRoot = root && idSet.has(root) ? root : nodeIds.find((id) => !hasIncoming.has(id)) ?? nodeIds[0];
13686
+ const rootsOrder = [];
13687
+ if (primaryRoot !== void 0) {
13688
+ bfsFrom(primaryRoot);
13689
+ rootsOrder.push(primaryRoot);
13690
+ }
13691
+ for (const id of nodeIds) {
13692
+ if (!visited.has(id)) {
13693
+ bfsFrom(id);
13694
+ rootsOrder.push(id);
13695
+ }
13696
+ }
13697
+ let leafCounter = 0;
13698
+ const xSlot = /* @__PURE__ */ new Map();
13699
+ const assignXSlot = (u) => {
13700
+ const children = treeChildren.get(u) ?? [];
13701
+ if (children.length === 0) {
13702
+ const slot = leafCounter++;
13703
+ xSlot.set(u, slot);
13704
+ return slot;
13705
+ }
13706
+ const childSlots = children.map(assignXSlot);
13707
+ const avg = childSlots.reduce((a, b) => a + b, 0) / childSlots.length;
13708
+ xSlot.set(u, avg);
13709
+ return avg;
13710
+ };
13711
+ for (const r of rootsOrder) assignXSlot(r);
13712
+ let maxDepth = 0;
13713
+ for (const d of depth.values()) maxDepth = Math.max(maxDepth, d);
13714
+ const colWidth = width / Math.max(1, leafCounter);
13715
+ const rowHeight = height / (maxDepth + 1);
13716
+ const positions = /* @__PURE__ */ new Map();
13717
+ for (const id of nodeIds) {
13718
+ const slot = xSlot.get(id) ?? 0;
13719
+ const d = depth.get(id) ?? 0;
13720
+ positions.set(id, { x: slot * colWidth + colWidth / 2, y: d * rowHeight + rowHeight / 2 });
13721
+ }
13722
+ return positions;
13723
+ }
13724
+ function layoutLayered(nodes, edges, width, height) {
13725
+ const nodeIds = nodes.map((n) => n.id);
13726
+ const idSet = new Set(nodeIds);
13727
+ const adj = /* @__PURE__ */ new Map();
13728
+ const remainingIndegree = /* @__PURE__ */ new Map();
13729
+ for (const id of nodeIds) remainingIndegree.set(id, 0);
13730
+ for (const e of edges) {
13731
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
13732
+ const list = adj.get(e.from) ?? [];
13733
+ list.push(e.to);
13734
+ adj.set(e.from, list);
13735
+ remainingIndegree.set(e.to, (remainingIndegree.get(e.to) ?? 0) + 1);
13736
+ }
13737
+ const layer = /* @__PURE__ */ new Map();
13738
+ const dequeued = /* @__PURE__ */ new Set();
13739
+ const queue = [];
13740
+ for (const id of nodeIds) {
13741
+ if ((remainingIndegree.get(id) ?? 0) === 0) {
13742
+ layer.set(id, 0);
13743
+ queue.push(id);
13744
+ }
13745
+ }
13746
+ while (queue.length > 0) {
13747
+ const u = queue.shift();
13748
+ dequeued.add(u);
13749
+ for (const v of adj.get(u) ?? []) {
13750
+ const candidate = (layer.get(u) ?? 0) + 1;
13751
+ layer.set(v, Math.max(layer.get(v) ?? 0, candidate));
13752
+ remainingIndegree.set(v, (remainingIndegree.get(v) ?? 0) - 1);
13753
+ if ((remainingIndegree.get(v) ?? 0) === 0 && !dequeued.has(v)) {
13754
+ queue.push(v);
13755
+ }
13756
+ }
13757
+ }
13758
+ let baseMaxLayer = 0;
13759
+ for (const id of nodeIds) {
13760
+ if (dequeued.has(id)) baseMaxLayer = Math.max(baseMaxLayer, layer.get(id) ?? 0);
13761
+ }
13762
+ const cycleLayer = baseMaxLayer + 1;
13763
+ let maxLayer = baseMaxLayer;
13764
+ for (const id of nodeIds) {
13765
+ if (!dequeued.has(id)) {
13766
+ layer.set(id, cycleLayer);
13767
+ maxLayer = cycleLayer;
13768
+ }
13769
+ }
13770
+ const colWidth = width / Math.max(1, maxLayer + 1);
13771
+ const byLayer = /* @__PURE__ */ new Map();
13772
+ for (const id of nodeIds) {
13773
+ const l = layer.get(id) ?? 0;
13774
+ const list = byLayer.get(l) ?? [];
13775
+ list.push(id);
13776
+ byLayer.set(l, list);
13777
+ }
13778
+ const positions = /* @__PURE__ */ new Map();
13779
+ for (const [l, ids] of byLayer) {
13780
+ const rowHeight = height / ids.length;
13781
+ ids.forEach((id, i) => {
13782
+ positions.set(id, { x: l * colWidth + colWidth / 2, y: i * rowHeight + rowHeight / 2 });
13783
+ });
13784
+ }
13785
+ return positions;
13786
+ }
13787
+ function computePositions(nodes, edges, layout, root, width, height) {
13788
+ switch (layout) {
13789
+ case "circle":
13790
+ return layoutCircle(nodes, width, height);
13791
+ case "tree":
13792
+ return layoutTree(nodes, edges, root, width, height);
13793
+ case "layered":
13794
+ return layoutLayered(nodes, edges, width, height);
13795
+ case "manual":
13796
+ default: {
13797
+ const positions = /* @__PURE__ */ new Map();
13798
+ for (const n of nodes) positions.set(n.id, { x: n.x ?? 0, y: n.y ?? 0 });
13799
+ return positions;
13800
+ }
13801
+ }
13802
+ }
13803
+ var NODE_STATE_COLOR, EDGE_STATE_COLOR, DEFAULT_NODE_RADIUS, AlgoGraphCanvas;
13804
+ var init_AlgoGraphCanvas = __esm({
13805
+ "components/learning/molecules/AlgoGraphCanvas.tsx"() {
13806
+ "use client";
13807
+ init_atoms();
13808
+ init_Stack();
13809
+ init_LearningCanvas();
13810
+ NODE_STATE_COLOR = {
13811
+ unvisited: "#cbd5e1",
13812
+ frontier: "#f59e0b",
13813
+ current: "#ef4444",
13814
+ visited: "#22c55e",
13815
+ goal: "#8b5cf6",
13816
+ path: "#0ea5e9"
13817
+ };
13818
+ EDGE_STATE_COLOR = {
13819
+ default: "#9ca3af",
13820
+ tree: "#16a34a",
13821
+ relaxed: "#f59e0b",
13822
+ candidate: "#38bdf8",
13823
+ path: "#dc2626"
13824
+ };
13825
+ DEFAULT_NODE_RADIUS = 18;
13826
+ AlgoGraphCanvas = ({
13827
+ className,
13828
+ width = 600,
13829
+ height = 400,
13830
+ title,
13831
+ backgroundColor,
13832
+ nodes = [],
13833
+ edges = [],
13834
+ layout = "manual",
13835
+ root,
13836
+ shapes = [],
13837
+ interactive = false,
13838
+ animate = false,
13839
+ onShapeClick,
13840
+ onNodeClick,
13841
+ isLoading,
13842
+ error
13843
+ }) => {
13844
+ const nodeById = React85.useMemo(() => {
13845
+ const m = /* @__PURE__ */ new Map();
13846
+ for (const n of nodes) m.set(n.id, n);
13847
+ return m;
13848
+ }, [nodes]);
13849
+ const nodeIndexById = React85.useMemo(() => {
13850
+ const m = /* @__PURE__ */ new Map();
13851
+ nodes.forEach((n, i) => m.set(n.id, i));
13852
+ return m;
13853
+ }, [nodes]);
13854
+ const derivedShapes = React85.useMemo(() => {
13855
+ const out = [];
13856
+ const positions = computePositions(nodes, edges, layout, root, width, height);
13857
+ const edgeGeoms = [];
13858
+ for (const e of edges) {
13859
+ const a = nodeById.get(e.from);
13860
+ const b = nodeById.get(e.to);
13861
+ const posA = positions.get(e.from);
13862
+ const posB = positions.get(e.to);
13863
+ if (!a || !b || !posA || !posB) continue;
13864
+ const rA = a.radius ?? DEFAULT_NODE_RADIUS;
13865
+ const rB = b.radius ?? DEFAULT_NODE_RADIUS;
13866
+ const dx = posB.x - posA.x;
13867
+ const dy = posB.y - posA.y;
13868
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
13869
+ const ux = dx / dist;
13870
+ const uy = dy / dist;
13871
+ edgeGeoms.push({
13872
+ directed: e.directed ?? false,
13873
+ x1: posA.x + ux * rA,
13874
+ y1: posA.y + uy * rA,
13875
+ x2: posB.x - ux * rB,
13876
+ y2: posB.y - uy * rB,
13877
+ color: e.color ?? EDGE_STATE_COLOR[e.state ?? "default"],
13878
+ label: e.label ?? (e.weight != null ? String(e.weight) : void 0)
13879
+ });
13880
+ }
13881
+ for (const g of edgeGeoms) {
13882
+ out.push({
13883
+ type: g.directed ? "arrow" : "line",
13884
+ x1: g.x1,
13885
+ y1: g.y1,
13886
+ x2: g.x2,
13887
+ y2: g.y2,
13888
+ color: g.color,
13889
+ lineWidth: 2
13890
+ });
13891
+ }
13892
+ for (const g of edgeGeoms) {
13893
+ if (g.label === void 0) continue;
13894
+ const midX = (g.x1 + g.x2) / 2;
13895
+ const midY = (g.y1 + g.y2) / 2;
13896
+ const dx = g.x2 - g.x1;
13897
+ const dy = g.y2 - g.y1;
13898
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
13899
+ const perpX = -(dy / dist);
13900
+ const perpY = dx / dist;
13901
+ out.push({
13902
+ type: "text",
13903
+ x: midX + perpX * 10,
13904
+ y: midY + perpY * 10,
13905
+ text: g.label,
13906
+ fontSize: 11,
13907
+ align: "center",
13908
+ color: "#374151"
13909
+ });
13910
+ }
13911
+ const nodeGeoms = [];
13912
+ for (const n of nodes) {
13913
+ const pos = positions.get(n.id);
13914
+ if (!pos) continue;
13915
+ nodeGeoms.push({
13916
+ id: n.id,
13917
+ x: pos.x,
13918
+ y: pos.y,
13919
+ radius: n.radius ?? DEFAULT_NODE_RADIUS,
13920
+ color: n.color ?? NODE_STATE_COLOR[n.state ?? "unvisited"],
13921
+ label: n.label,
13922
+ badge: n.badge
13923
+ });
13924
+ }
13925
+ for (const g of nodeGeoms) {
13926
+ out.push({ type: "circle", id: g.id, x: g.x, y: g.y, radius: g.radius, color: g.color, fill: `${g.color}33` });
13927
+ }
13928
+ const badgeGeoms = [];
13929
+ for (const g of nodeGeoms) {
13930
+ if (!g.badge) continue;
13931
+ const w = Math.min(42, Math.max(18, g.badge.text.length * 6 + 10));
13932
+ badgeGeoms.push({
13933
+ cx: g.x + g.radius * 0.75,
13934
+ cy: g.y - g.radius * 0.75,
13935
+ w,
13936
+ h: 14,
13937
+ // Borderless pill: same color drives both stroke and fill.
13938
+ color: g.badge.color ?? "#1e293b",
13939
+ text: g.badge.text
13940
+ });
13941
+ }
13942
+ for (const b of badgeGeoms) {
13943
+ 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 });
13944
+ }
13945
+ for (const b of badgeGeoms) {
13946
+ out.push({ type: "text", x: b.cx, y: b.cy, text: b.text, fontSize: 9, align: "center", color: "#ffffff" });
13947
+ }
13948
+ for (const g of nodeGeoms) {
13949
+ if (g.label === void 0) continue;
13950
+ out.push({
13951
+ type: "text",
13952
+ x: g.x,
13953
+ y: g.y + g.radius + 14,
13954
+ text: g.label,
13955
+ fontSize: 12,
13956
+ align: "center",
13957
+ color: "#111827"
13958
+ });
13959
+ }
13960
+ out.push(...shapes);
13961
+ return out;
13962
+ }, [nodes, edges, layout, root, width, height, nodeById, shapes]);
13963
+ const handleShapeClick = React85.useCallback(
13964
+ (payload) => {
13965
+ if (payload.type === "circle" && payload.id) {
13966
+ const node = nodeById.get(payload.id);
13967
+ const idx = nodeIndexById.get(payload.id);
13968
+ if (node && idx !== void 0) {
13969
+ onNodeClick?.({ id: node.id, label: node.label, index: idx });
13970
+ }
13971
+ }
13972
+ onShapeClick?.(payload);
13973
+ },
13974
+ [nodeById, nodeIndexById, onNodeClick, onShapeClick]
13975
+ );
13976
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
13977
+ title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
13978
+ /* @__PURE__ */ jsxRuntime.jsx(
13979
+ LearningCanvas,
13980
+ {
13981
+ width,
13982
+ height,
13983
+ backgroundColor,
13984
+ shapes: derivedShapes,
13985
+ interactive,
13986
+ animate,
13987
+ onShapeClick: onShapeClick || onNodeClick ? handleShapeClick : void 0,
13988
+ isLoading,
13989
+ error
13990
+ }
13991
+ )
13992
+ ] }) });
13993
+ };
13994
+ }
13995
+ });
13996
+ 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;
13499
13997
  var init_AlgorithmCanvas = __esm({
13500
13998
  "components/learning/molecules/AlgorithmCanvas.tsx"() {
13501
13999
  "use client";
@@ -13507,6 +14005,43 @@ var init_AlgorithmCanvas = __esm({
13507
14005
  DEFAULT_POINTER_COLOR = "#dc2626";
13508
14006
  POINTER_BAND = 34;
13509
14007
  TOP_PAD = 26;
14008
+ PANEL_FAMILY_ORDER = ["bars", "slots", "cells", "buckets", "frames"];
14009
+ RANGE_COLOR_DEFAULT = "#3b82f6";
14010
+ RANGE_FILL_OPACITY = 0.15;
14011
+ BRACKET_TOP_OFFSET = 16;
14012
+ BRACKET_ROW_H = 14;
14013
+ BRACKET_TICK_H = 6;
14014
+ BRACKET_LABEL_OFFSET = 6;
14015
+ SLOT_EMPTY_FILL = "#f1f5f9";
14016
+ SLOT_EMPTY_STROKE = "#cbd5e1";
14017
+ SLOT_FILLED_STROKE = "#9ca3af";
14018
+ SLOT_HIGHLIGHT_DEFAULT = "#f59e0b";
14019
+ SLOT_VALUE_TEXT_COLOR = "#ffffff";
14020
+ FRAME_ACTIVE_COLOR = "#3b82f6";
14021
+ FRAME_RETURNING_COLOR = "#f59e0b";
14022
+ FRAME_DONE_COLOR = "#94a3b8";
14023
+ FRAME_LABEL_COLOR = "#ffffff";
14024
+ FRAME_DETAIL_COLOR = "#e2e8f0";
14025
+ FRAME_TWO_LINE_MIN_H = 22;
14026
+ BUCKET_INDEX_FILL = "#e2e8f0";
14027
+ BUCKET_INDEX_STROKE = "#9ca3af";
14028
+ BUCKET_INDEX_TEXT = "#374151";
14029
+ BUCKET_ENTRY_TEXT = "#ffffff";
14030
+ BUCKET_ENTRY_DEFAULT = "#3b82f6";
14031
+ BUCKET_ENTRY_HIGHLIGHT = "#f59e0b";
14032
+ BUCKET_ENTRY_PROBING = "#38bdf8";
14033
+ BUCKET_ENTRY_MIN_W = 24;
14034
+ BUCKET_ENTRY_MAX_W = 64;
14035
+ AXIS_LABEL_COLOR = "#6b7280";
14036
+ AXIS_LABEL_FONT_SIZE = 10;
14037
+ CORNER_TEXT_COLOR = "#111827";
14038
+ CORNER_FONT_SIZE = 7;
14039
+ CORNER_MIN_CELL = 28;
14040
+ CORNER_INSET_X = 3;
14041
+ CORNER_INSET_Y = 6;
14042
+ AUX_PRIMARY_RATIO = 0.6;
14043
+ AUX_LABEL_BAND = 18;
14044
+ AUX_BASELINE_PAD = 8;
13510
14045
  AlgorithmCanvas = ({
13511
14046
  className,
13512
14047
  width = 600,
@@ -13516,6 +14051,14 @@ var init_AlgorithmCanvas = __esm({
13516
14051
  bars = [],
13517
14052
  cells = [],
13518
14053
  pointers = [],
14054
+ ranges = [],
14055
+ slots = [],
14056
+ slotOrientation = "horizontal",
14057
+ frames = [],
14058
+ buckets = [],
14059
+ auxBars = [],
14060
+ rowLabels = [],
14061
+ colLabels = [],
13519
14062
  shapes = [],
13520
14063
  interactive = false,
13521
14064
  animate = false,
@@ -13525,12 +14068,35 @@ var init_AlgorithmCanvas = __esm({
13525
14068
  }) => {
13526
14069
  const derivedShapes = React85.useMemo(() => {
13527
14070
  const out = [];
14071
+ const presence = {
14072
+ bars: bars.length > 0,
14073
+ slots: slots.length > 0,
14074
+ cells: cells.length > 0,
14075
+ buckets: buckets.length > 0,
14076
+ frames: frames.length > 0
14077
+ };
14078
+ const panelCount = PANEL_FAMILY_ORDER.filter((f3) => presence[f3]).length;
14079
+ const panelHeight = height / Math.max(1, panelCount);
14080
+ const panelY = { bars: 0, slots: 0, cells: 0, buckets: 0, frames: 0 };
14081
+ let compactIndex = 0;
14082
+ PANEL_FAMILY_ORDER.forEach((f3) => {
14083
+ if (presence[f3]) {
14084
+ panelY[f3] = compactIndex * panelHeight;
14085
+ compactIndex += 1;
14086
+ }
14087
+ });
13528
14088
  if (bars.length > 0) {
14089
+ const panelYBars = panelY.bars;
13529
14090
  const slot = width / bars.length;
13530
14091
  const barW = slot * 0.8;
13531
14092
  const gap = slot * 0.1;
13532
- const baseline = height - POINTER_BAND;
13533
- const usableH = baseline - TOP_PAD;
14093
+ const bracketRanges = ranges.filter((r) => r.kind === "bracket");
14094
+ const bracketCount = bracketRanges.length;
14095
+ const bracketHeadroom = bracketCount > 0 ? BRACKET_TOP_OFFSET + bracketCount * BRACKET_ROW_H : 0;
14096
+ const hasAux = auxBars.length > 0;
14097
+ const primaryH = hasAux ? panelHeight * AUX_PRIMARY_RATIO : panelHeight;
14098
+ const baseline = panelYBars + primaryH - POINTER_BAND;
14099
+ const usableH = baseline - (panelYBars + TOP_PAD + bracketHeadroom);
13534
14100
  const maxV = Math.max(1, ...bars.map((b) => Number.isFinite(b.value) ? b.value : 0));
13535
14101
  bars.forEach((bar, i) => {
13536
14102
  const v = Number.isFinite(bar.value) ? bar.value : 0;
@@ -13560,6 +14126,89 @@ var init_AlgorithmCanvas = __esm({
13560
14126
  });
13561
14127
  }
13562
14128
  });
14129
+ ranges.forEach((r) => {
14130
+ const kind = r.kind ?? "fill";
14131
+ if (kind !== "fill") return;
14132
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14133
+ out.push({
14134
+ type: "rect",
14135
+ x: r.from * slot,
14136
+ y: panelYBars,
14137
+ width: (r.to - r.from + 1) * slot,
14138
+ height: primaryH,
14139
+ color,
14140
+ fill: color,
14141
+ opacity: RANGE_FILL_OPACITY
14142
+ });
14143
+ if (r.label) {
14144
+ out.push({
14145
+ type: "text",
14146
+ x: r.from * slot + 4,
14147
+ // Sits below the bracket block (if any) so fill and bracket labels never collide.
14148
+ y: panelYBars + 10 + bracketHeadroom,
14149
+ text: r.label,
14150
+ color,
14151
+ fontSize: 10,
14152
+ align: "left"
14153
+ });
14154
+ }
14155
+ });
14156
+ bracketRanges.forEach((r, i) => {
14157
+ const bracketY = panelYBars + BRACKET_TOP_OFFSET + i * BRACKET_ROW_H;
14158
+ const x1 = r.from * slot + slot * 0.1;
14159
+ const x2 = (r.to + 1) * slot - slot * 0.1;
14160
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14161
+ out.push({ type: "line", x1, y1: bracketY, x2, y2: bracketY, color, lineWidth: 2 });
14162
+ out.push({ type: "line", x1, y1: bracketY, x2: x1, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14163
+ out.push({ type: "line", x1: x2, y1: bracketY, x2, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14164
+ if (r.label) {
14165
+ out.push({
14166
+ type: "text",
14167
+ x: (x1 + x2) / 2,
14168
+ y: bracketY - BRACKET_LABEL_OFFSET,
14169
+ text: r.label,
14170
+ color,
14171
+ fontSize: 10,
14172
+ align: "center"
14173
+ });
14174
+ }
14175
+ });
14176
+ if (hasAux) {
14177
+ const auxH = panelHeight - primaryH;
14178
+ const slot2 = width / auxBars.length;
14179
+ const auxBaseline = panelYBars + primaryH + auxH - AUX_BASELINE_PAD;
14180
+ const auxUsableH = auxBaseline - (panelYBars + primaryH + AUX_LABEL_BAND);
14181
+ const maxAuxV = Math.max(1, ...auxBars.map((b) => Number.isFinite(b.value) ? b.value : 0));
14182
+ auxBars.forEach((bar, i) => {
14183
+ const v = Number.isFinite(bar.value) ? bar.value : 0;
14184
+ const bh = Math.max(0, v / maxAuxV * auxUsableH);
14185
+ const x = i * slot2 + slot2 * 0.1;
14186
+ const w = slot2 * 0.8;
14187
+ const color = bar.color ?? DEFAULT_BAR_COLOR;
14188
+ out.push({
14189
+ type: "rect",
14190
+ id: `auxbar-${i}`,
14191
+ x,
14192
+ y: auxBaseline - bh,
14193
+ width: w,
14194
+ height: bh,
14195
+ color,
14196
+ fill: color
14197
+ });
14198
+ const label = bar.label ?? (auxBars.length <= 24 ? String(v) : void 0);
14199
+ if (label) {
14200
+ out.push({
14201
+ type: "text",
14202
+ x: x + w / 2,
14203
+ y: auxBaseline - bh - 8,
14204
+ text: label,
14205
+ color: "#374151",
14206
+ fontSize: 11,
14207
+ align: "center"
14208
+ });
14209
+ }
14210
+ });
14211
+ }
13563
14212
  pointers.forEach((p) => {
13564
14213
  if (p.index < 0 || p.index >= bars.length) return;
13565
14214
  const cx = p.index * slot + slot / 2;
@@ -13567,7 +14216,7 @@ var init_AlgorithmCanvas = __esm({
13567
14216
  out.push({
13568
14217
  type: "arrow",
13569
14218
  x1: cx,
13570
- y1: height - 6,
14219
+ y1: panelYBars + primaryH - 18,
13571
14220
  x2: cx,
13572
14221
  y2: baseline + 4,
13573
14222
  color,
@@ -13577,7 +14226,7 @@ var init_AlgorithmCanvas = __esm({
13577
14226
  out.push({
13578
14227
  type: "text",
13579
14228
  x: cx,
13580
- y: height - 22,
14229
+ y: panelYBars + primaryH - 8,
13581
14230
  text: p.label,
13582
14231
  color,
13583
14232
  fontSize: 11,
@@ -13586,14 +14235,111 @@ var init_AlgorithmCanvas = __esm({
13586
14235
  }
13587
14236
  });
13588
14237
  }
14238
+ if (slots.length > 0) {
14239
+ const panelYSlots = panelY.slots;
14240
+ const n = slots.length;
14241
+ const vertical = slotOrientation === "vertical";
14242
+ const vBoxH = panelHeight / n;
14243
+ const vBoxW = Math.min(width * 0.5, 120);
14244
+ const vBoxX = (width - vBoxW) / 2;
14245
+ const hCellW = width / n;
14246
+ const hBoxW = hCellW * 0.82;
14247
+ const hBoxH = Math.min(panelHeight * 0.6, 48);
14248
+ const hBoxY = panelYSlots + (panelHeight - hBoxH) / 2;
14249
+ 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 };
14250
+ slots.forEach((s, i) => {
14251
+ const box = slotBox(i);
14252
+ const state = s.state ?? "filled";
14253
+ const fill = state === "empty" ? SLOT_EMPTY_FILL : state === "highlight" ? s.color ?? SLOT_HIGHLIGHT_DEFAULT : s.color ?? DEFAULT_BAR_COLOR;
14254
+ const stroke = state === "empty" ? SLOT_EMPTY_STROKE : SLOT_FILLED_STROKE;
14255
+ out.push({
14256
+ type: "rect",
14257
+ id: `slot-${i}`,
14258
+ x: box.x,
14259
+ y: box.y,
14260
+ width: box.width,
14261
+ height: box.height,
14262
+ color: stroke,
14263
+ fill
14264
+ });
14265
+ if (s.value != null && state !== "empty") {
14266
+ out.push({
14267
+ type: "text",
14268
+ x: box.x + box.width / 2,
14269
+ y: box.y + box.height / 2,
14270
+ text: String(s.value),
14271
+ color: SLOT_VALUE_TEXT_COLOR,
14272
+ fontSize: 12,
14273
+ align: "center"
14274
+ });
14275
+ }
14276
+ });
14277
+ if (bars.length === 0) {
14278
+ pointers.forEach((p) => {
14279
+ if (p.index < 0 || p.index >= slots.length) return;
14280
+ const box = slotBox(p.index);
14281
+ const color = p.color ?? DEFAULT_POINTER_COLOR;
14282
+ if (vertical) {
14283
+ const cy = box.y + box.height / 2;
14284
+ out.push({
14285
+ type: "arrow",
14286
+ x1: box.x + box.width + 34,
14287
+ y1: cy,
14288
+ x2: box.x + box.width + 4,
14289
+ y2: cy,
14290
+ color,
14291
+ lineWidth: 2
14292
+ });
14293
+ if (p.label) {
14294
+ out.push({
14295
+ type: "text",
14296
+ x: box.x + box.width + 38,
14297
+ y: cy,
14298
+ text: p.label,
14299
+ color,
14300
+ fontSize: 11,
14301
+ align: "left"
14302
+ });
14303
+ }
14304
+ } else {
14305
+ const cx = box.x + box.width / 2;
14306
+ out.push({
14307
+ type: "arrow",
14308
+ x1: cx,
14309
+ y1: panelYSlots + panelHeight - 18,
14310
+ x2: cx,
14311
+ y2: box.y + box.height + 4,
14312
+ color,
14313
+ lineWidth: 2
14314
+ });
14315
+ if (p.label) {
14316
+ out.push({
14317
+ type: "text",
14318
+ x: cx,
14319
+ y: panelYSlots + panelHeight - 8,
14320
+ text: p.label,
14321
+ color,
14322
+ fontSize: 11,
14323
+ align: "center"
14324
+ });
14325
+ }
14326
+ }
14327
+ });
14328
+ }
14329
+ }
13589
14330
  if (cells.length > 0) {
14331
+ const panelYCells = panelY.cells;
13590
14332
  const maxCol = Math.max(0, ...cells.map((c) => c.col)) + 1;
13591
14333
  const maxRow = Math.max(0, ...cells.map((c) => c.row)) + 1;
13592
- const cw = width / maxCol;
13593
- const ch = height / maxRow;
14334
+ const colLabelH = colLabels.length > 0 ? 16 : 0;
14335
+ const rowLabelW = rowLabels.length > 0 ? 20 : 0;
14336
+ const gridX0 = rowLabelW;
14337
+ const gridY0 = panelYCells + colLabelH;
14338
+ const cw = (width - rowLabelW) / maxCol;
14339
+ const ch = (panelHeight - colLabelH) / maxRow;
13594
14340
  cells.forEach((c, i) => {
13595
- const x = c.col * cw;
13596
- const y = c.row * ch;
14341
+ const x = gridX0 + c.col * cw;
14342
+ const y = gridY0 + c.row * ch;
13597
14343
  const color = c.color ?? DEFAULT_CELL_COLOR;
13598
14344
  out.push({
13599
14345
  type: "rect",
@@ -13617,11 +14363,207 @@ var init_AlgorithmCanvas = __esm({
13617
14363
  align: "center"
13618
14364
  });
13619
14365
  }
14366
+ if (c.corner && cw >= CORNER_MIN_CELL && ch >= CORNER_MIN_CELL) {
14367
+ const { tl, tr, bl, br } = c.corner;
14368
+ if (tl) {
14369
+ out.push({
14370
+ type: "text",
14371
+ x: x + CORNER_INSET_X,
14372
+ y: y + CORNER_INSET_Y,
14373
+ text: tl,
14374
+ color: CORNER_TEXT_COLOR,
14375
+ fontSize: CORNER_FONT_SIZE,
14376
+ align: "left"
14377
+ });
14378
+ }
14379
+ if (tr) {
14380
+ out.push({
14381
+ type: "text",
14382
+ x: x + cw - CORNER_INSET_X,
14383
+ y: y + CORNER_INSET_Y,
14384
+ text: tr,
14385
+ color: CORNER_TEXT_COLOR,
14386
+ fontSize: CORNER_FONT_SIZE,
14387
+ align: "right"
14388
+ });
14389
+ }
14390
+ if (bl) {
14391
+ out.push({
14392
+ type: "text",
14393
+ x: x + CORNER_INSET_X,
14394
+ y: y + ch - CORNER_INSET_Y,
14395
+ text: bl,
14396
+ color: CORNER_TEXT_COLOR,
14397
+ fontSize: CORNER_FONT_SIZE,
14398
+ align: "left"
14399
+ });
14400
+ }
14401
+ if (br) {
14402
+ out.push({
14403
+ type: "text",
14404
+ x: x + cw - CORNER_INSET_X,
14405
+ y: y + ch - CORNER_INSET_Y,
14406
+ text: br,
14407
+ color: CORNER_TEXT_COLOR,
14408
+ fontSize: CORNER_FONT_SIZE,
14409
+ align: "right"
14410
+ });
14411
+ }
14412
+ }
14413
+ });
14414
+ colLabels.forEach((l) => {
14415
+ out.push({
14416
+ type: "text",
14417
+ x: gridX0 + l.index * cw + cw / 2,
14418
+ y: panelYCells + colLabelH / 2,
14419
+ text: l.text,
14420
+ color: l.color ?? AXIS_LABEL_COLOR,
14421
+ fontSize: AXIS_LABEL_FONT_SIZE,
14422
+ align: "center"
14423
+ });
14424
+ });
14425
+ rowLabels.forEach((l) => {
14426
+ out.push({
14427
+ type: "text",
14428
+ x: rowLabelW - 6,
14429
+ y: gridY0 + l.index * ch + ch / 2,
14430
+ text: l.text,
14431
+ color: l.color ?? AXIS_LABEL_COLOR,
14432
+ fontSize: AXIS_LABEL_FONT_SIZE,
14433
+ align: "right"
14434
+ });
14435
+ });
14436
+ }
14437
+ if (buckets.length > 0) {
14438
+ const panelYBuckets = panelY.buckets;
14439
+ const bucketCount = Math.max(0, ...buckets.map((b) => b.index)) + 1;
14440
+ const rowH = panelHeight / bucketCount;
14441
+ const indexColW = Math.min(width * 0.12, 40);
14442
+ const maxChainLen = Math.max(1, ...buckets.map((b) => b.entries.length));
14443
+ const entryW = Math.min(BUCKET_ENTRY_MAX_W, Math.max(BUCKET_ENTRY_MIN_W, (width - indexColW - 8) / maxChainLen));
14444
+ const maxVisible = Math.floor((width - indexColW - 4) / entryW);
14445
+ buckets.forEach((b) => {
14446
+ const rowY = panelYBuckets + b.index * rowH;
14447
+ out.push({
14448
+ type: "rect",
14449
+ id: `bucket-index-${b.index}`,
14450
+ x: 2,
14451
+ y: rowY + 2,
14452
+ width: indexColW - 4,
14453
+ height: rowH - 4,
14454
+ color: BUCKET_INDEX_STROKE,
14455
+ fill: BUCKET_INDEX_FILL
14456
+ });
14457
+ out.push({
14458
+ type: "text",
14459
+ x: 2 + (indexColW - 4) / 2,
14460
+ y: rowY + rowH / 2,
14461
+ text: String(b.index),
14462
+ color: BUCKET_INDEX_TEXT,
14463
+ fontSize: 10,
14464
+ align: "center"
14465
+ });
14466
+ const overflow = b.entries.length > maxVisible;
14467
+ const visibleCount = overflow ? Math.max(0, maxVisible - 1) : b.entries.length;
14468
+ for (let j = 0; j < visibleCount; j++) {
14469
+ const entry = b.entries[j];
14470
+ const ex = indexColW + 4 + j * entryW;
14471
+ const state = entry.state ?? "default";
14472
+ const fill = state === "highlight" ? entry.color ?? BUCKET_ENTRY_HIGHLIGHT : state === "probing" ? entry.color ?? BUCKET_ENTRY_PROBING : entry.color ?? BUCKET_ENTRY_DEFAULT;
14473
+ out.push({
14474
+ type: "rect",
14475
+ id: `bucket-${b.index}-${j}`,
14476
+ x: ex,
14477
+ y: rowY + 2,
14478
+ width: entryW - 2,
14479
+ height: rowH - 4,
14480
+ color: fill,
14481
+ fill
14482
+ });
14483
+ if (entryW >= 20 && rowH >= 16) {
14484
+ out.push({
14485
+ type: "text",
14486
+ x: ex + (entryW - 2) / 2,
14487
+ y: rowY + rowH / 2,
14488
+ text: entry.label,
14489
+ color: BUCKET_ENTRY_TEXT,
14490
+ fontSize: 10,
14491
+ align: "center"
14492
+ });
14493
+ }
14494
+ }
14495
+ if (overflow) {
14496
+ const ex = indexColW + 4 + visibleCount * entryW;
14497
+ out.push({
14498
+ type: "rect",
14499
+ id: `bucket-${b.index}-overflow`,
14500
+ x: ex,
14501
+ y: rowY + 2,
14502
+ width: entryW - 2,
14503
+ height: rowH - 4,
14504
+ color: BUCKET_ENTRY_DEFAULT,
14505
+ fill: BUCKET_ENTRY_DEFAULT
14506
+ });
14507
+ out.push({
14508
+ type: "text",
14509
+ x: ex + (entryW - 2) / 2,
14510
+ y: rowY + rowH / 2,
14511
+ text: `+${b.entries.length - visibleCount}`,
14512
+ color: BUCKET_ENTRY_TEXT,
14513
+ fontSize: 10,
14514
+ align: "center"
14515
+ });
14516
+ }
14517
+ });
14518
+ }
14519
+ if (frames.length > 0) {
14520
+ const panelYFrames = panelY.frames;
14521
+ const n = frames.length;
14522
+ const frameH = panelHeight / n;
14523
+ const x = 8;
14524
+ const w = width - 16;
14525
+ frames.forEach((f3, i) => {
14526
+ const y = panelYFrames + panelHeight - (i + 1) * frameH;
14527
+ const state = f3.state ?? "active";
14528
+ const fill = state === "returning" ? f3.color ?? FRAME_RETURNING_COLOR : state === "done" ? f3.color ?? FRAME_DONE_COLOR : f3.color ?? FRAME_ACTIVE_COLOR;
14529
+ out.push({ type: "rect", id: `frame-${i}`, x, y, width: w, height: frameH, color: fill, fill });
14530
+ if (frameH >= FRAME_TWO_LINE_MIN_H) {
14531
+ out.push({
14532
+ type: "text",
14533
+ x: 16,
14534
+ y: y + frameH * 0.35,
14535
+ text: f3.label,
14536
+ color: FRAME_LABEL_COLOR,
14537
+ fontSize: 10,
14538
+ align: "left"
14539
+ });
14540
+ if (f3.detail) {
14541
+ out.push({
14542
+ type: "text",
14543
+ x: 16,
14544
+ y: y + frameH * 0.7,
14545
+ text: f3.detail,
14546
+ color: FRAME_DETAIL_COLOR,
14547
+ fontSize: 10,
14548
+ align: "left"
14549
+ });
14550
+ }
14551
+ } else {
14552
+ out.push({
14553
+ type: "text",
14554
+ x: 16,
14555
+ y: y + frameH / 2,
14556
+ text: f3.label,
14557
+ color: FRAME_LABEL_COLOR,
14558
+ fontSize: 10,
14559
+ align: "left"
14560
+ });
14561
+ }
13620
14562
  });
13621
14563
  }
13622
14564
  out.push(...shapes);
13623
14565
  return out;
13624
- }, [bars, cells, pointers, shapes, width, height]);
14566
+ }, [bars, cells, pointers, ranges, slots, slotOrientation, frames, buckets, auxBars, rowLabels, colLabels, shapes, width, height]);
13625
14567
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
13626
14568
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
13627
14569
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -14610,23 +15552,422 @@ var init_BehaviorView = __esm({
14610
15552
  BehaviorView.displayName = "BehaviorView";
14611
15553
  }
14612
15554
  });
14613
- var BiologyCanvas;
15555
+ function LearningScene3D({
15556
+ className,
15557
+ width = 600,
15558
+ height = 400,
15559
+ title,
15560
+ backgroundColor,
15561
+ drawables,
15562
+ camera,
15563
+ lighting,
15564
+ post,
15565
+ showGrid = false,
15566
+ shadows,
15567
+ interactive,
15568
+ isLoading,
15569
+ error,
15570
+ onItemClick
15571
+ }) {
15572
+ const instanceId = React85.useId().replace(/[^a-zA-Z0-9]/g, "");
15573
+ const clickEvent = onItemClick ? `LEARNING_SCENE_3D.ITEM_CLICK.${instanceId}` : void 0;
15574
+ const onItemClickRef = React85.useRef(onItemClick);
15575
+ onItemClickRef.current = onItemClick;
15576
+ useEventListener(`UI:${clickEvent ?? "LEARNING_SCENE_3D.ITEM_CLICK.__disabled"}`, (event) => {
15577
+ const unitId = event.payload?.unitId;
15578
+ if (typeof unitId === "string") onItemClickRef.current?.(unitId);
15579
+ });
15580
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && post?.bloom) {
15581
+ const unknownKeys = Object.keys(post.bloom).filter((k) => !KNOWN_BLOOM_KEYS.has(k));
15582
+ if (unknownKeys.length > 0) {
15583
+ sceneLog.debug("post.bloom has unrecognized keys \u2014 only intensity/threshold/smoothing are read", { unknownKeys });
15584
+ }
15585
+ }
15586
+ const props3d = {
15587
+ drawables,
15588
+ isLoading,
15589
+ error: error ? error.message : null,
15590
+ cameraMode: camera?.mode ?? "perspective",
15591
+ ...camera?.zoom !== void 0 ? { zoom: camera.zoom } : {},
15592
+ ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
15593
+ ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
15594
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
15595
+ ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
15596
+ backgroundColor,
15597
+ showGrid,
15598
+ ...shadows !== void 0 ? { shadows } : {},
15599
+ ...interactive !== void 0 ? { controlsEnabled: interactive } : {},
15600
+ lighting,
15601
+ post,
15602
+ ...clickEvent !== void 0 ? { unitClickEvent: clickEvent } : {}
15603
+ };
15604
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
15605
+ title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
15606
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width, height, display: "flex" }, children: /* @__PURE__ */ jsxRuntime.jsx(React85.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(Canvas3DHost2, { ...props3d }) }) })
15607
+ ] }) });
15608
+ }
15609
+ function meshSphere(id, x, y, z, radius, color, opts) {
15610
+ return {
15611
+ type: "draw-mesh",
15612
+ ...id !== void 0 ? { id } : {},
15613
+ shape: opts?.shape ?? "sphere",
15614
+ position: { x, y, z },
15615
+ radius,
15616
+ pivot: "center",
15617
+ segments: opts?.segments ?? 24,
15618
+ material: { color, ...opts?.material },
15619
+ ...opts?.opacity !== void 0 ? { opacity: opts.opacity } : {}
15620
+ };
15621
+ }
15622
+ function axisRotation(from, to) {
15623
+ const wx = to[0] - from[0];
15624
+ const wy = to[2] - from[2];
15625
+ const wz = to[1] - from[1];
15626
+ const len = Math.sqrt(wx * wx + wy * wy + wz * wz);
15627
+ const tilt = Math.acos(Math.min(1, Math.max(-1, len > 0 ? wy / len : 1)));
15628
+ return [0, Math.atan2(wz, -wx), tilt];
15629
+ }
15630
+ function segmentLength(from, to) {
15631
+ const dx = to[0] - from[0];
15632
+ const dy = to[1] - from[1];
15633
+ const dz = to[2] - from[2];
15634
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
15635
+ }
15636
+ function cylinderBetween(from, to, radius, color) {
15637
+ const len = segmentLength(from, to);
15638
+ if (len < 1e-6) return null;
15639
+ return {
15640
+ type: "draw-mesh",
15641
+ shape: "cylinder",
15642
+ position: { x: (from[0] + to[0]) / 2, y: (from[1] + to[1]) / 2, z: (from[2] + to[2]) / 2 },
15643
+ radius,
15644
+ height: len,
15645
+ rotation: axisRotation(from, to),
15646
+ pivot: "center",
15647
+ segments: 12,
15648
+ material: { color }
15649
+ };
15650
+ }
15651
+ function arrowBetween(from, to, color, shaftRadius = 0.08, id) {
15652
+ const len = segmentLength(from, to);
15653
+ if (len < 1e-6) return null;
15654
+ const tipLen = Math.min(shaftRadius * 8, len * 0.35);
15655
+ const k = (len - tipLen) / len;
15656
+ const delta = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
15657
+ const shaftEnd = [delta[0] * k, delta[1] * k, delta[2] * k];
15658
+ const tipStart = shaftEnd;
15659
+ const tipEnd = delta;
15660
+ const shaft = cylinderBetween([0, 0, 0], shaftEnd, shaftRadius, color);
15661
+ const tipLenActual = segmentLength(tipStart, tipEnd);
15662
+ const tip = {
15663
+ type: "draw-mesh",
15664
+ shape: "cone",
15665
+ position: {
15666
+ x: (tipStart[0] + tipEnd[0]) / 2,
15667
+ y: (tipStart[1] + tipEnd[1]) / 2,
15668
+ z: (tipStart[2] + tipEnd[2]) / 2
15669
+ },
15670
+ radius: shaftRadius * 3,
15671
+ height: tipLenActual,
15672
+ rotation: axisRotation(tipStart, tipEnd),
15673
+ pivot: "center",
15674
+ segments: 12,
15675
+ material: { color }
15676
+ };
15677
+ return {
15678
+ type: "draw-group",
15679
+ ...id !== void 0 ? { id } : {},
15680
+ position: { x: from[0], y: from[1], z: from[2] },
15681
+ items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
15682
+ };
15683
+ }
15684
+ function billboardLabel(text, x, y, z, opts) {
15685
+ return {
15686
+ type: "draw-text",
15687
+ text,
15688
+ position: { x, y, z },
15689
+ color: opts?.color ?? "#111827"
15690
+ };
15691
+ }
15692
+ function labelColorForBackground(backgroundColor) {
15693
+ const m = /^#(?:([0-9a-f]{3})|([0-9a-f]{6}))$/i.exec(backgroundColor ?? "");
15694
+ if (!m) return "#111827";
15695
+ const hex = m[1] !== void 0 ? m[1].split("").map((c) => c + c).join("") : m[2];
15696
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
15697
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
15698
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
15699
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
15700
+ return luminance < 0.5 ? "#e5e7eb" : "#111827";
15701
+ }
15702
+ function get3DClickPayload(onShapeClick, idToIndex) {
15703
+ if (!onShapeClick) return void 0;
15704
+ return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
15705
+ }
15706
+ function polylineTube(points, radius, color, opts) {
15707
+ const maxSegments = opts?.maxSegments ?? 128;
15708
+ let pts = points;
15709
+ if (pts.length - 1 > maxSegments) {
15710
+ const step = (pts.length - 1) / maxSegments;
15711
+ const kept = [pts[0]];
15712
+ for (let s = 1; s < maxSegments; s++) kept.push(pts[Math.round(s * step)]);
15713
+ kept.push(pts[pts.length - 1]);
15714
+ pts = kept;
15715
+ }
15716
+ const out = [];
15717
+ for (let i = 0; i < pts.length - 1; i++) {
15718
+ const seg = cylinderBetween(pts[i], pts[i + 1], radius, color);
15719
+ if (seg) out.push(opts?.opacity !== void 0 ? { ...seg, opacity: opts.opacity } : seg);
15720
+ }
15721
+ return out;
15722
+ }
15723
+ function heightFieldMesh(spec) {
15724
+ const { nx, ny, heights, spacing = 1, x = 0, y = 0 } = spec;
15725
+ const flatShading = spec.flatShading ?? true;
15726
+ const vertices = [];
15727
+ for (let iy = 0; iy < ny; iy++) {
15728
+ for (let ix = 0; ix < nx; ix++) {
15729
+ vertices.push([
15730
+ x + (ix - (nx - 1) / 2) * spacing,
15731
+ y + (iy - (ny - 1) / 2) * spacing,
15732
+ heights[iy * nx + ix] ?? 0
15733
+ ]);
15734
+ }
15735
+ }
15736
+ const bands = [...spec.bands ?? []].sort((a, b) => (a.min ?? -Infinity) - (b.min ?? -Infinity));
15737
+ const facesByBand = /* @__PURE__ */ new Map();
15738
+ for (let iy = 0; iy < ny - 1; iy++) {
15739
+ for (let ix = 0; ix < nx - 1; ix++) {
15740
+ const v00 = iy * nx + ix;
15741
+ const v10 = iy * nx + ix + 1;
15742
+ const v01 = (iy + 1) * nx + ix;
15743
+ const v11 = (iy + 1) * nx + ix + 1;
15744
+ for (const face of [[v00, v10, v01], [v10, v11, v01]]) {
15745
+ const centroid = (vertices[face[0]][2] + vertices[face[1]][2] + vertices[face[2]][2]) / 3;
15746
+ let band = null;
15747
+ for (const b of bands) {
15748
+ if ((b.min ?? -Infinity) <= centroid) band = b;
15749
+ }
15750
+ const key = bands.length > 0 ? band : null;
15751
+ const list = facesByBand.get(key) ?? [];
15752
+ list.push(face);
15753
+ facesByBand.set(key, list);
15754
+ }
15755
+ }
15756
+ }
15757
+ const out = [];
15758
+ for (const [band, faces] of facesByBand) {
15759
+ if (faces.length === 0) continue;
15760
+ out.push({
15761
+ type: "draw-mesh",
15762
+ shape: "polyhedron",
15763
+ position: { x: 0, y: 0, z: 0 },
15764
+ vertices,
15765
+ faces,
15766
+ pivot: "center",
15767
+ material: { color: band?.color ?? spec.color ?? "#64748b", flatShading, side: "double" },
15768
+ ...spec.opacity !== void 0 ? { opacity: spec.opacity } : {}
15769
+ });
15770
+ }
15771
+ return out;
15772
+ }
15773
+ function arrowField(vectors, opts) {
15774
+ const scale = opts?.scale ?? 1;
15775
+ const out = [];
15776
+ for (const v of vectors) {
15777
+ const to = [
15778
+ v.from[0] + v.delta[0] * scale,
15779
+ v.from[1] + v.delta[1] * scale,
15780
+ v.from[2] + v.delta[2] * scale
15781
+ ];
15782
+ const arrow = arrowBetween(v.from, to, v.color ?? "#dc2626", v.width, v.id);
15783
+ if (arrow) out.push(arrow);
15784
+ if (v.label) out.push(billboardLabel(v.label, to[0], to[1], to[2], { color: opts?.labelColor }));
15785
+ }
15786
+ return out;
15787
+ }
15788
+ function helixDrawables(spec, opts) {
15789
+ const count = spec.count ?? spec.rungs?.length ?? 0;
15790
+ const rungs = Array.from({ length: count }, (_, i) => spec.rungs?.[i] ?? {});
15791
+ const radius = spec.radius ?? 1;
15792
+ const rise = spec.rise ?? 0.34;
15793
+ const twistRad = (spec.twistDeg ?? 36) * (Math.PI / 180);
15794
+ const strandAColor = spec.strandAColor ?? "#38bdf8";
15795
+ const strandBColor = spec.strandBColor ?? "#fb923c";
15796
+ const backboneRadius = spec.backboneRadius ?? 0.16;
15797
+ const rungRadius = spec.rungRadius ?? 0.12;
15798
+ const cx = spec.x ?? 0;
15799
+ const cy = spec.y ?? 0;
15800
+ const cz = spec.z ?? 0;
15801
+ const unwoundCount = spec.unwoundCount ?? 0;
15802
+ const unwindSpread = spec.unwindSpread ?? 1.8;
15803
+ const strandA = [];
15804
+ const strandB = [];
15805
+ for (let i = 0; i < count; i++) {
15806
+ const yi = cy + (i - (count - 1) / 2) * rise;
15807
+ const theta = i * twistRad;
15808
+ const s = i < unwoundCount ? unwindSpread : 1;
15809
+ strandA.push([cx + s * radius * Math.cos(theta), yi, cz + s * radius * Math.sin(theta)]);
15810
+ strandB.push([cx + s * radius * Math.cos(theta + Math.PI), yi, cz + s * radius * Math.sin(theta + Math.PI)]);
15811
+ }
15812
+ const out = [];
15813
+ for (let i = 0; i < count; i++) {
15814
+ out.push(meshSphere(`hx-a-${i}`, strandA[i][0], strandA[i][1], strandA[i][2], backboneRadius, strandAColor));
15815
+ out.push(meshSphere(`hx-b-${i}`, strandB[i][0], strandB[i][1], strandB[i][2], backboneRadius, strandBColor));
15816
+ if (i > 0) {
15817
+ const segA = cylinderBetween(strandA[i - 1], strandA[i], backboneRadius, strandAColor);
15818
+ if (segA) out.push(segA);
15819
+ const segB = cylinderBetween(strandB[i - 1], strandB[i], backboneRadius, strandBColor);
15820
+ if (segB) out.push(segB);
15821
+ }
15822
+ const rung = rungs[i];
15823
+ const rungColor = rung.color ?? "#94a3b8";
15824
+ const rod = cylinderBetween(strandA[i], strandB[i], rungRadius, rungColor);
15825
+ if (rod) out.push(rod);
15826
+ const mid = [
15827
+ (strandA[i][0] + strandB[i][0]) / 2,
15828
+ (strandA[i][1] + strandB[i][1]) / 2,
15829
+ (strandA[i][2] + strandB[i][2]) / 2
15830
+ ];
15831
+ const markerRadius = rung.radius ?? rungRadius;
15832
+ out.push(meshSphere(rung.id, mid[0], mid[1], mid[2], markerRadius, rungColor));
15833
+ if (rung.label) out.push(billboardLabel(rung.label, mid[0], mid[1], mid[2] + markerRadius, { color: opts?.labelColor }));
15834
+ }
15835
+ return out;
15836
+ }
15837
+ function latticeDrawables(spec, opts) {
15838
+ const nx = spec.nx ?? 2;
15839
+ const ny = spec.ny ?? 2;
15840
+ const nz = spec.nz ?? 2;
15841
+ const latticeConstant = spec.latticeConstant ?? 2;
15842
+ const bondRadius = spec.bondRadius ?? 0.06;
15843
+ const highlightCell = spec.highlightCell ?? false;
15844
+ const dimColor = spec.dimColor ?? "#475569";
15845
+ const showLabels = spec.showLabels ?? false;
15846
+ const selectedColor = spec.selectedColor ?? "#f59e0b";
15847
+ const posByKey = /* @__PURE__ */ new Map();
15848
+ const inCellByKey = /* @__PURE__ */ new Map();
15849
+ const out = [];
15850
+ for (const site of spec.basis) {
15851
+ const snx = site.xEdge ? nx + 1 : nx;
15852
+ const sny = site.yEdge ? ny + 1 : ny;
15853
+ const snz = site.zEdge ? nz + 1 : nz;
15854
+ for (let i = 0; i < snx; i++) {
15855
+ for (let j = 0; j < sny; j++) {
15856
+ for (let k = 0; k < snz; k++) {
15857
+ const key = `${site.key}-${i}-${j}-${k}`;
15858
+ const inCell = i + site.dx <= 1 && j + site.dy <= 1 && k + site.dz <= 1;
15859
+ const pos = [
15860
+ (i + site.dx) * latticeConstant - nx * latticeConstant / 2,
15861
+ (j + site.dy) * latticeConstant - ny * latticeConstant / 2,
15862
+ (k + site.dz) * latticeConstant - nz * latticeConstant / 2
15863
+ ];
15864
+ posByKey.set(key, pos);
15865
+ inCellByKey.set(key, inCell);
15866
+ const isSelected = spec.selectedId === `lat-${key}`;
15867
+ const color = isSelected ? selectedColor : highlightCell && !inCell ? dimColor : site.color ?? "#2563eb";
15868
+ const radius = (site.radius ?? 0.3) * (isSelected ? 1.4 : 1);
15869
+ out.push(meshSphere(`lat-${key}`, pos[0], pos[1], pos[2], radius, color));
15870
+ if (showLabels && site.element) {
15871
+ out.push(billboardLabel(site.element, pos[0], pos[1], pos[2] + radius, { color: opts?.labelColor }));
15872
+ }
15873
+ }
15874
+ }
15875
+ }
15876
+ }
15877
+ const basisByKey = new Map(spec.basis.map((s) => [s.key, s]));
15878
+ for (const bond of spec.bonds ?? []) {
15879
+ const fromSite = basisByKey.get(bond.from);
15880
+ const toSite = basisByKey.get(bond.to);
15881
+ if (!fromSite || !toSite) continue;
15882
+ const fnx = fromSite.xEdge ? nx + 1 : nx;
15883
+ const fny = fromSite.yEdge ? ny + 1 : ny;
15884
+ const fnz = fromSite.zEdge ? nz + 1 : nz;
15885
+ const tnx = toSite.xEdge ? nx + 1 : nx;
15886
+ const tny = toSite.yEdge ? ny + 1 : ny;
15887
+ const tnz = toSite.zEdge ? nz + 1 : nz;
15888
+ const bdx = bond.dx ?? 0;
15889
+ const bdy = bond.dy ?? 0;
15890
+ const bdz = bond.dz ?? 0;
15891
+ for (let i = 0; i < fnx; i++) {
15892
+ for (let j = 0; j < fny; j++) {
15893
+ for (let k = 0; k < fnz; k++) {
15894
+ const ti = i + bdx;
15895
+ const tj = j + bdy;
15896
+ const tk = k + bdz;
15897
+ if (ti < 0 || ti >= tnx || tj < 0 || tj >= tny || tk < 0 || tk >= tnz) continue;
15898
+ const fromKey = `${fromSite.key}-${i}-${j}-${k}`;
15899
+ const toKey = `${toSite.key}-${ti}-${tj}-${tk}`;
15900
+ const fromPos = posByKey.get(fromKey);
15901
+ const toPos = posByKey.get(toKey);
15902
+ if (!fromPos || !toPos) continue;
15903
+ const dimmed = highlightCell && !(inCellByKey.get(fromKey) && inCellByKey.get(toKey));
15904
+ const seg = cylinderBetween(fromPos, toPos, bondRadius, dimmed ? dimColor : bond.color ?? "#6b7280");
15905
+ if (seg) out.push(seg);
15906
+ }
15907
+ }
15908
+ }
15909
+ }
15910
+ return out;
15911
+ }
15912
+ var sceneLog, KNOWN_BLOOM_KEYS, Canvas3DHost2;
15913
+ var init_learningScene3D = __esm({
15914
+ "components/learning/molecules/learningScene3D.tsx"() {
15915
+ "use client";
15916
+ init_atoms();
15917
+ init_Stack();
15918
+ init_useEventBus();
15919
+ sceneLog = logger.createLogger("almadar:ui:learning-scene-3d");
15920
+ KNOWN_BLOOM_KEYS = /* @__PURE__ */ new Set(["intensity", "threshold", "smoothing"]);
15921
+ Canvas3DHost2 = React85.lazy(
15922
+ () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
15923
+ );
15924
+ LearningScene3D.displayName = "LearningScene3D";
15925
+ }
15926
+ });
15927
+ var biologyLog, BIO_BAND_COLORS, BIO_STAGE_FILL, BIO_STAGE_TEXT, BiologyCanvas;
14614
15928
  var init_BiologyCanvas = __esm({
14615
15929
  "components/learning/molecules/BiologyCanvas.tsx"() {
14616
15930
  "use client";
14617
15931
  init_atoms();
14618
15932
  init_Stack();
14619
15933
  init_LearningCanvas();
15934
+ init_learningScene3D();
15935
+ biologyLog = logger.createLogger("almadar:ui:biology-canvas");
15936
+ BIO_BAND_COLORS = ["#dcfce7", "#fef9c3", "#fee2e2", "#e0e7ff"];
15937
+ BIO_STAGE_FILL = {
15938
+ pending: "#e2e8f0",
15939
+ active: "#3b82f6",
15940
+ done: "#94a3b8"
15941
+ };
15942
+ BIO_STAGE_TEXT = {
15943
+ pending: "#64748b",
15944
+ active: "#ffffff",
15945
+ done: "#ffffff"
15946
+ };
14620
15947
  BiologyCanvas = ({
14621
15948
  className,
14622
15949
  width = 600,
14623
15950
  height = 400,
14624
15951
  title,
14625
15952
  backgroundColor,
15953
+ mode = "2d",
15954
+ camera,
15955
+ lighting,
15956
+ post,
14626
15957
  nodes = [],
14627
15958
  edges = [],
15959
+ compartments = [],
15960
+ bands = [],
15961
+ stages = [],
15962
+ stageStyle = "timeline",
15963
+ helix,
15964
+ helix3d,
14628
15965
  shapes = [],
14629
- interactive = false,
15966
+ readouts,
15967
+ traces,
15968
+ showGrid,
15969
+ shadows,
15970
+ interactive,
14630
15971
  animate = false,
14631
15972
  onShapeClick,
14632
15973
  isLoading,
@@ -14638,19 +15979,148 @@ var init_BiologyCanvas = __esm({
14638
15979
  for (const n of nodes) {
14639
15980
  if (n.id) nodeById.set(n.id, n);
14640
15981
  }
15982
+ const bandCount = bands.length;
15983
+ for (let i = 0; i < bandCount; i++) {
15984
+ const band = bands[i];
15985
+ const bandColor = band.color ?? BIO_BAND_COLORS[i % BIO_BAND_COLORS.length];
15986
+ const bandY = i * height / bandCount;
15987
+ const bandH = height / bandCount;
15988
+ out.push({
15989
+ type: "rect",
15990
+ x: 0,
15991
+ y: bandY,
15992
+ width,
15993
+ height: bandH,
15994
+ color: bandColor,
15995
+ fill: bandColor,
15996
+ opacity: 0.45
15997
+ });
15998
+ if (band.label) {
15999
+ out.push({
16000
+ type: "text",
16001
+ x: 8,
16002
+ y: bandY + 14,
16003
+ text: band.label,
16004
+ color: "#6b7280",
16005
+ fontSize: 10
16006
+ });
16007
+ }
16008
+ }
16009
+ for (const c of compartments) {
16010
+ const color = c.color ?? "#16a34a";
16011
+ out.push({
16012
+ type: "ellipse",
16013
+ x: c.x,
16014
+ y: c.y,
16015
+ width: c.width,
16016
+ height: c.height,
16017
+ color,
16018
+ fill: c.fill ?? `${color}1A`,
16019
+ lineWidth: c.lineWidth ?? 2,
16020
+ ...c.dash ? { dash: c.dash } : {}
16021
+ });
16022
+ if (c.label) {
16023
+ out.push({
16024
+ type: "text",
16025
+ x: c.x,
16026
+ y: c.y - c.height / 2 + 14,
16027
+ text: c.label,
16028
+ color: "#111827",
16029
+ fontSize: 11,
16030
+ align: "center"
16031
+ });
16032
+ }
16033
+ }
16034
+ if (helix) {
16035
+ const hx = helix.x ?? 24;
16036
+ const hy = helix.y ?? height * 0.25;
16037
+ const hw = helix.width ?? width - 48;
16038
+ const hh = helix.height ?? height * 0.5;
16039
+ const rungs = helix.rungs;
16040
+ const n = rungs.length;
16041
+ const cy = hy + hh / 2;
16042
+ const colorA = helix.colorA ?? "#2563eb";
16043
+ const colorB = helix.colorB ?? "#dc2626";
16044
+ const rungColor = helix.rungColor ?? "#94a3b8";
16045
+ const fork = helix.fork ?? 0;
16046
+ const maxSep = Math.min(hh - 8, 96);
16047
+ const strandA = [];
16048
+ const strandB = [];
16049
+ const rungGeoms = [];
16050
+ for (let i = 0; i < n; i++) {
16051
+ const rx = hx + (i + 0.5) * hw / n;
16052
+ const t = (i + 0.5) / n;
16053
+ const paired = t >= fork;
16054
+ const sep = paired ? 28 : 28 + (maxSep - 28) * ((fork - t) / fork);
16055
+ strandA.push({ x: rx, y: cy - sep / 2 });
16056
+ strandB.push({ x: rx, y: cy + sep / 2 });
16057
+ rungGeoms.push({ rx, sep, rung: rungs[i], paired });
16058
+ }
16059
+ for (let i = 1; i < n; i++) {
16060
+ 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 });
16061
+ }
16062
+ for (let i = 1; i < n; i++) {
16063
+ 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 });
16064
+ }
16065
+ for (const g of rungGeoms) {
16066
+ const rColor = g.rung.color ?? (g.rung.state === "new" ? "#16a34a" : rungColor);
16067
+ const topY = cy - g.sep / 2;
16068
+ const bottomY = cy + g.sep / 2;
16069
+ if (g.paired) {
16070
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: bottomY, color: rColor });
16071
+ if (g.rung.a) {
16072
+ out.push({ type: "text", x: g.rx, y: cy - g.sep / 4, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
16073
+ }
16074
+ if (g.rung.b) {
16075
+ out.push({ type: "text", x: g.rx, y: cy + g.sep / 4, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16076
+ }
16077
+ } else {
16078
+ const stubTopY = topY + 8;
16079
+ const stubBottomY = bottomY - 8;
16080
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: stubTopY, color: rColor });
16081
+ out.push({ type: "line", x1: g.rx, y1: bottomY, x2: g.rx, y2: stubBottomY, color: rColor });
16082
+ if (g.rung.a) {
16083
+ out.push({ type: "text", x: g.rx, y: stubTopY + 6, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
16084
+ }
16085
+ if (g.rung.b) {
16086
+ out.push({ type: "text", x: g.rx, y: stubBottomY - 6, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16087
+ }
16088
+ }
16089
+ }
16090
+ }
14641
16091
  for (const e of edges) {
14642
16092
  const a = nodeById.get(e.from);
14643
16093
  const b = nodeById.get(e.to);
14644
16094
  if (!a || !b) continue;
14645
- out.push({
14646
- type: "line",
14647
- x1: a.x,
14648
- y1: a.y,
14649
- x2: b.x,
14650
- y2: b.y,
14651
- color: e.color ?? "#9ca3af",
14652
- lineWidth: 2
14653
- });
16095
+ const color = e.color ?? "#9ca3af";
16096
+ if (e.directed) {
16097
+ const rA = a.radius ?? 16;
16098
+ const rB = b.radius ?? 16;
16099
+ const dx = b.x - a.x;
16100
+ const dy = b.y - a.y;
16101
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16102
+ const ux = dx / dist;
16103
+ const uy = dy / dist;
16104
+ out.push({
16105
+ type: "arrow",
16106
+ x1: a.x + ux * rA,
16107
+ y1: a.y + uy * rA,
16108
+ x2: b.x - ux * rB,
16109
+ y2: b.y - uy * rB,
16110
+ color,
16111
+ lineWidth: 2
16112
+ });
16113
+ } else {
16114
+ out.push({
16115
+ type: "line",
16116
+ x1: a.x,
16117
+ y1: a.y,
16118
+ x2: b.x,
16119
+ y2: b.y,
16120
+ color,
16121
+ lineWidth: 2
16122
+ });
16123
+ }
14654
16124
  if (e.label) {
14655
16125
  out.push({
14656
16126
  type: "text",
@@ -14663,6 +16133,8 @@ var init_BiologyCanvas = __esm({
14663
16133
  }
14664
16134
  }
14665
16135
  for (const n of nodes) {
16136
+ const state = n.state ?? "default";
16137
+ const muted = state === "muted";
14666
16138
  out.push({
14667
16139
  type: "circle",
14668
16140
  x: n.x,
@@ -14670,23 +16142,197 @@ var init_BiologyCanvas = __esm({
14670
16142
  radius: n.radius ?? 16,
14671
16143
  color: n.color ?? "#16a34a",
14672
16144
  fill: `${n.color ?? "#16a34a"}33`,
14673
- id: n.id
16145
+ id: n.id,
16146
+ ...muted ? { opacity: 0.35 } : {}
14674
16147
  });
16148
+ if (state === "highlight") {
16149
+ out.push({
16150
+ type: "circle",
16151
+ x: n.x,
16152
+ y: n.y,
16153
+ radius: (n.radius ?? 16) + 4,
16154
+ color: "#f59e0b",
16155
+ lineWidth: 2
16156
+ });
16157
+ }
14675
16158
  if (n.label) {
14676
16159
  out.push({
14677
16160
  type: "text",
14678
16161
  x: n.x,
14679
16162
  y: n.y + (n.radius ?? 16) + 14,
14680
16163
  text: n.label,
16164
+ ...muted ? { opacity: 0.35 } : {},
14681
16165
  color: "#111827",
14682
16166
  fontSize: 12,
14683
16167
  align: "center"
14684
16168
  });
14685
16169
  }
14686
16170
  }
16171
+ const stageCount = stages.length;
16172
+ if (stageCount > 0) {
16173
+ if (stageStyle === "ring") {
16174
+ const cx = width / 2;
16175
+ const cy = height / 2;
16176
+ const R = Math.min(width, height) / 2 - 48;
16177
+ const ringPoints = [];
16178
+ for (let i = 0; i < stageCount; i++) {
16179
+ const angleRad = (-90 + 360 * i / stageCount) * Math.PI / 180;
16180
+ ringPoints.push({ x: cx + R * Math.cos(angleRad), y: cy + R * Math.sin(angleRad) });
16181
+ }
16182
+ if (stageCount >= 2) {
16183
+ for (let i = 0; i < stageCount - 1; i++) {
16184
+ const p1 = ringPoints[i];
16185
+ const p2 = ringPoints[i + 1];
16186
+ const dx = p2.x - p1.x;
16187
+ const dy = p2.y - p1.y;
16188
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16189
+ const ux = dx / dist;
16190
+ const uy = dy / dist;
16191
+ out.push({
16192
+ type: "arrow",
16193
+ x1: p1.x + ux * 46,
16194
+ y1: p1.y + uy * 46,
16195
+ x2: p2.x - ux * 46,
16196
+ y2: p2.y - uy * 46,
16197
+ color: "#94a3b8"
16198
+ });
16199
+ }
16200
+ }
16201
+ for (let i = 0; i < stageCount; i++) {
16202
+ const stage = stages[i];
16203
+ const state = stage.state ?? "pending";
16204
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16205
+ const w = Math.max(26, Math.min(84, stage.label.length * 6 + 10));
16206
+ const h = 18;
16207
+ const p = ringPoints[i];
16208
+ out.push({ type: "rect", x: p.x - w / 2, y: p.y - h / 2, width: w, height: h, color: fill, fill });
16209
+ out.push({ type: "text", x: p.x, y: p.y, text: stage.label, color: BIO_STAGE_TEXT[state], fontSize: 10, align: "center" });
16210
+ }
16211
+ } else {
16212
+ const stripY = height - 32;
16213
+ const slotW = (width - 16) / stageCount;
16214
+ const chipGeoms = [];
16215
+ for (let i = 0; i < stageCount; i++) {
16216
+ chipGeoms.push({ x: 8 + i * slotW + 5, w: slotW - 10 });
16217
+ }
16218
+ for (let i = 0; i < stageCount - 1; i++) {
16219
+ const midY = stripY + 13;
16220
+ out.push({
16221
+ type: "arrow",
16222
+ x1: chipGeoms[i].x + chipGeoms[i].w,
16223
+ y1: midY,
16224
+ x2: chipGeoms[i + 1].x,
16225
+ y2: midY,
16226
+ color: "#94a3b8"
16227
+ });
16228
+ }
16229
+ for (let i = 0; i < stageCount; i++) {
16230
+ const stage = stages[i];
16231
+ const state = stage.state ?? "pending";
16232
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16233
+ const g = chipGeoms[i];
16234
+ out.push({ type: "rect", x: g.x, y: stripY, width: g.w, height: 26, color: fill, fill });
16235
+ out.push({
16236
+ type: "text",
16237
+ x: g.x + g.w / 2,
16238
+ y: stripY + 13,
16239
+ text: stage.label,
16240
+ color: BIO_STAGE_TEXT[state],
16241
+ fontSize: 10,
16242
+ align: "center"
16243
+ });
16244
+ }
16245
+ }
16246
+ }
14687
16247
  out.push(...shapes);
14688
16248
  return out;
14689
- }, [nodes, edges, shapes]);
16249
+ }, [nodes, edges, compartments, bands, stages, stageStyle, helix, shapes, width, height]);
16250
+ const drawables3D = React85.useMemo(() => {
16251
+ if (mode !== "3d") return [];
16252
+ if (shapes.length > 0) {
16253
+ biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
16254
+ }
16255
+ if (compartments.length > 0 || bands.length > 0 || stages.length > 0 || helix) {
16256
+ biologyLog.debug("2D-only families ignored in 3D mode (pixel-authored 2D vocabulary)", {
16257
+ compartments: compartments.length,
16258
+ bands: bands.length,
16259
+ stages: stages.length,
16260
+ helix: helix != null
16261
+ });
16262
+ }
16263
+ if (animate) {
16264
+ biologyLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
16265
+ }
16266
+ const out = [];
16267
+ const labelColor = labelColorForBackground(backgroundColor);
16268
+ const nodeById = /* @__PURE__ */ new Map();
16269
+ for (const n of nodes) {
16270
+ if (n.id) nodeById.set(n.id, n);
16271
+ }
16272
+ for (const e of edges) {
16273
+ const a = nodeById.get(e.from);
16274
+ const b = nodeById.get(e.to);
16275
+ if (!a || !b) continue;
16276
+ const edgeRadius = Math.max(0.04, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.12);
16277
+ const edge = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], edgeRadius, e.color ?? "#9ca3af");
16278
+ if (edge) out.push(edge);
16279
+ if (e.label) {
16280
+ out.push(
16281
+ billboardLabel(
16282
+ e.label,
16283
+ (a.x + b.x) / 2,
16284
+ (a.y + b.y) / 2,
16285
+ ((a.z ?? 0) + (b.z ?? 0)) / 2,
16286
+ { color: labelColor }
16287
+ )
16288
+ );
16289
+ }
16290
+ }
16291
+ for (const n of nodes) {
16292
+ const radius = n.radius ?? 0.5;
16293
+ const nz = n.z ?? 0;
16294
+ out.push(meshSphere(n.id, n.x, n.y, nz, radius, n.color ?? "#16a34a", { shape: n.shape, ...n.opacity !== void 0 ? { opacity: n.opacity } : {} }));
16295
+ if (n.label) {
16296
+ out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
16297
+ }
16298
+ }
16299
+ if (helix3d) {
16300
+ out.push(...helixDrawables(helix3d, { labelColor }));
16301
+ }
16302
+ return out;
16303
+ }, [mode, nodes, edges, shapes, compartments, bands, stages, helix, helix3d, animate, backgroundColor]);
16304
+ const nodeIndexById = React85.useMemo(() => {
16305
+ const m = /* @__PURE__ */ new Map();
16306
+ (helix3d?.rungs ?? []).forEach((rung, i) => {
16307
+ if (rung.id) m.set(rung.id, i);
16308
+ });
16309
+ nodes.forEach((n, i) => {
16310
+ if (n.id) m.set(n.id, i);
16311
+ });
16312
+ return m;
16313
+ }, [nodes, helix3d]);
16314
+ if (mode === "3d") {
16315
+ return /* @__PURE__ */ jsxRuntime.jsx(
16316
+ LearningScene3D,
16317
+ {
16318
+ className,
16319
+ width,
16320
+ height,
16321
+ title,
16322
+ backgroundColor,
16323
+ drawables: drawables3D,
16324
+ camera,
16325
+ lighting,
16326
+ post,
16327
+ showGrid,
16328
+ shadows,
16329
+ interactive,
16330
+ isLoading,
16331
+ error,
16332
+ onItemClick: get3DClickPayload(onShapeClick, nodeIndexById)
16333
+ }
16334
+ );
16335
+ }
14690
16336
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
14691
16337
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
14692
16338
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -14696,7 +16342,9 @@ var init_BiologyCanvas = __esm({
14696
16342
  height,
14697
16343
  backgroundColor,
14698
16344
  shapes: derivedShapes,
14699
- interactive,
16345
+ readouts,
16346
+ traces,
16347
+ interactive: interactive ?? false,
14700
16348
  animate,
14701
16349
  onShapeClick,
14702
16350
  isLoading,
@@ -21419,24 +23067,55 @@ var init_ChatBar = __esm({
21419
23067
  ChatBar.displayName = "ChatBar";
21420
23068
  }
21421
23069
  });
21422
- var ChemistryCanvas;
23070
+ function bondPerpendicular(a, b) {
23071
+ const dx = b[0] - a[0];
23072
+ const dy = b[1] - a[1];
23073
+ const px = dy;
23074
+ const py = -dx;
23075
+ const len = Math.sqrt(px * px + py * py);
23076
+ if (len < 1e-6) return [1, 0, 0];
23077
+ return [px / len, py / len, 0];
23078
+ }
23079
+ var chemistryLog, CHEM_BOND_STATE_COLOR, LONE_PAIR_ANGLES, ChemistryCanvas;
21423
23080
  var init_ChemistryCanvas = __esm({
21424
23081
  "components/learning/molecules/ChemistryCanvas.tsx"() {
21425
23082
  "use client";
21426
23083
  init_atoms();
21427
23084
  init_Stack();
21428
23085
  init_LearningCanvas();
23086
+ init_learningScene3D();
23087
+ chemistryLog = logger.createLogger("almadar:ui:chemistry-canvas");
23088
+ CHEM_BOND_STATE_COLOR = {
23089
+ default: "#6b7280",
23090
+ forming: "#16a34a",
23091
+ breaking: "#dc2626",
23092
+ highlight: "#f59e0b"
23093
+ };
23094
+ LONE_PAIR_ANGLES = [-90, 0, 90, 180];
21429
23095
  ChemistryCanvas = ({
21430
23096
  className,
21431
23097
  width = 600,
21432
23098
  height = 400,
21433
23099
  title,
21434
23100
  backgroundColor,
23101
+ mode = "2d",
23102
+ camera,
23103
+ lighting,
23104
+ post,
21435
23105
  atoms = [],
21436
23106
  bonds = [],
21437
23107
  arrows = [],
23108
+ bondStyle = "thick",
23109
+ containers = [],
23110
+ equation,
23111
+ equationColor,
23112
+ lattice3d,
21438
23113
  shapes = [],
21439
- interactive = false,
23114
+ readouts,
23115
+ traces,
23116
+ showGrid,
23117
+ shadows,
23118
+ interactive,
21440
23119
  animate = false,
21441
23120
  onShapeClick,
21442
23121
  isLoading,
@@ -21448,21 +23127,118 @@ var init_ChemistryCanvas = __esm({
21448
23127
  for (const a of atoms) {
21449
23128
  if (a.id) atomById.set(a.id, a);
21450
23129
  }
23130
+ for (const c of containers) {
23131
+ const color = c.color ?? "#64748b";
23132
+ if (c.level != null) {
23133
+ const lv = c.level;
23134
+ out.push({
23135
+ type: "rect",
23136
+ x: c.x + 1,
23137
+ y: c.y + c.height * (1 - lv),
23138
+ width: c.width - 2,
23139
+ height: c.height * lv - 1,
23140
+ color: c.levelColor ?? "#60a5fa",
23141
+ fill: c.levelColor ?? "#60a5fa",
23142
+ opacity: 0.5
23143
+ });
23144
+ }
23145
+ out.push({
23146
+ type: "rect",
23147
+ x: c.x,
23148
+ y: c.y,
23149
+ width: c.width,
23150
+ height: c.height,
23151
+ color,
23152
+ fill: c.fill,
23153
+ lineWidth: c.lineWidth ?? 2
23154
+ });
23155
+ const divider = c.divider ?? "none";
23156
+ if (divider !== "none") {
23157
+ out.push({
23158
+ type: "line",
23159
+ x1: c.x + c.width / 2,
23160
+ y1: c.y,
23161
+ x2: c.x + c.width / 2,
23162
+ y2: c.y + c.height,
23163
+ color: c.dividerColor ?? color,
23164
+ ...divider === "dashed" || divider === "dotted" ? { dash: divider } : {}
23165
+ });
23166
+ }
23167
+ if (c.leftLabel) {
23168
+ out.push({
23169
+ type: "text",
23170
+ x: c.x + c.width * 0.25,
23171
+ y: c.y + 12,
23172
+ text: c.leftLabel,
23173
+ color: "#374151",
23174
+ fontSize: 11,
23175
+ align: "center"
23176
+ });
23177
+ }
23178
+ if (c.rightLabel) {
23179
+ out.push({
23180
+ type: "text",
23181
+ x: c.x + c.width * 0.75,
23182
+ y: c.y + 12,
23183
+ text: c.rightLabel,
23184
+ color: "#374151",
23185
+ fontSize: 11,
23186
+ align: "center"
23187
+ });
23188
+ }
23189
+ if (c.label) {
23190
+ out.push({
23191
+ type: "text",
23192
+ x: c.x + c.width / 2,
23193
+ y: c.y + c.height + 12,
23194
+ text: c.label,
23195
+ color: "#111827",
23196
+ fontSize: 12,
23197
+ align: "center"
23198
+ });
23199
+ }
23200
+ }
21451
23201
  for (const b of bonds) {
21452
23202
  const a = atomById.get(b.from);
21453
23203
  const c = atomById.get(b.to);
21454
23204
  if (!a || !c) continue;
21455
- const color = b.color ?? "#6b7280";
21456
- const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
21457
- out.push({
21458
- type: "line",
21459
- x1: a.x,
21460
- y1: a.y,
21461
- x2: c.x,
21462
- y2: c.y,
21463
- color,
21464
- lineWidth: strokeWidth
21465
- });
23205
+ const state = b.state ?? "default";
23206
+ const color = b.color ?? CHEM_BOND_STATE_COLOR[state];
23207
+ const dash = state === "forming" || state === "breaking" ? "dashed" : void 0;
23208
+ if (bondStyle === "parallel") {
23209
+ const dx = c.x - a.x;
23210
+ const dy = c.y - a.y;
23211
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
23212
+ const ux = dx / dist;
23213
+ const uy = dy / dist;
23214
+ const px = -uy;
23215
+ const py = ux;
23216
+ const offsets = b.type === "double" ? [-3, 3] : b.type === "triple" ? [-4, 0, 4] : [0];
23217
+ for (const off of offsets) {
23218
+ out.push({
23219
+ type: "line",
23220
+ x1: a.x + px * off,
23221
+ y1: a.y + py * off,
23222
+ x2: c.x + px * off,
23223
+ y2: c.y + py * off,
23224
+ color,
23225
+ lineWidth: 2,
23226
+ ...dash ? { dash } : {}
23227
+ });
23228
+ }
23229
+ } else {
23230
+ const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
23231
+ out.push({
23232
+ type: "line",
23233
+ x1: a.x,
23234
+ y1: a.y,
23235
+ x2: c.x,
23236
+ y2: c.y,
23237
+ color,
23238
+ lineWidth: strokeWidth,
23239
+ ...dash ? { dash } : {}
23240
+ });
23241
+ }
21466
23242
  }
21467
23243
  for (const a of arrows) {
21468
23244
  const angle = (a.angle ?? 0) * (Math.PI / 180);
@@ -21511,10 +23287,142 @@ var init_ChemistryCanvas = __esm({
21511
23287
  align: "center"
21512
23288
  });
21513
23289
  }
23290
+ const r = a.radius ?? 14;
23291
+ if (a.charge) {
23292
+ out.push({
23293
+ type: "text",
23294
+ x: a.x + r * 0.85,
23295
+ y: a.y - r * 0.85,
23296
+ text: a.charge,
23297
+ color: "#111827",
23298
+ fontSize: 9,
23299
+ align: "left"
23300
+ });
23301
+ }
23302
+ const lonePairs = Math.max(0, Math.min(4, a.lonePairs ?? 0));
23303
+ for (let k = 0; k < lonePairs; k++) {
23304
+ const angleRad = LONE_PAIR_ANGLES[k] * Math.PI / 180;
23305
+ const cx = a.x + (r + 6) * Math.cos(angleRad);
23306
+ const cy = a.y + (r + 6) * Math.sin(angleRad);
23307
+ const perpX = -Math.sin(angleRad);
23308
+ const perpY = Math.cos(angleRad);
23309
+ for (const sign of [1, -1]) {
23310
+ out.push({
23311
+ type: "circle",
23312
+ x: cx + perpX * 2.5 * sign,
23313
+ y: cy + perpY * 2.5 * sign,
23314
+ radius: 1.5,
23315
+ color: "#374151",
23316
+ fill: "#374151"
23317
+ });
23318
+ }
23319
+ }
23320
+ }
23321
+ if (equation) {
23322
+ out.push({
23323
+ type: "text",
23324
+ x: width / 2,
23325
+ y: 14,
23326
+ text: equation,
23327
+ color: equationColor ?? "#111827",
23328
+ fontSize: 13,
23329
+ align: "center"
23330
+ });
21514
23331
  }
21515
23332
  out.push(...shapes);
21516
23333
  return out;
21517
- }, [atoms, bonds, arrows, shapes]);
23334
+ }, [atoms, bonds, arrows, bondStyle, containers, equation, equationColor, shapes, width]);
23335
+ const drawables3D = React85.useMemo(() => {
23336
+ if (mode !== "3d") return [];
23337
+ if (shapes.length > 0) {
23338
+ chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
23339
+ }
23340
+ if (containers.length > 0) {
23341
+ chemistryLog.debug("containers ignored in 3D mode (pixel-authored 2D vocabulary)", { count: containers.length });
23342
+ }
23343
+ if (animate) {
23344
+ chemistryLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
23345
+ }
23346
+ const out = [];
23347
+ const labelColor = labelColorForBackground(backgroundColor);
23348
+ const atomById = /* @__PURE__ */ new Map();
23349
+ for (const a of atoms) {
23350
+ if (a.id) atomById.set(a.id, a);
23351
+ }
23352
+ for (const b of bonds) {
23353
+ const a = atomById.get(b.from);
23354
+ const c = atomById.get(b.to);
23355
+ if (!a || !c) continue;
23356
+ const color = b.color ?? "#6b7280";
23357
+ const from = [a.x, a.y, a.z ?? 0];
23358
+ const to = [c.x, c.y, c.z ?? 0];
23359
+ const perp = bondPerpendicular(from, to);
23360
+ const bondRadius = Math.max(0.06, Math.min(a.radius ?? 0.45, c.radius ?? 0.45) * 0.22);
23361
+ const step = bondRadius * 2.2;
23362
+ const offsets = b.type === "double" ? [-step, step] : b.type === "triple" ? [-step, 0, step] : [0];
23363
+ for (const off of offsets) {
23364
+ const bond = cylinderBetween(
23365
+ [from[0] + perp[0] * off, from[1] + perp[1] * off, from[2] + perp[2] * off],
23366
+ [to[0] + perp[0] * off, to[1] + perp[1] * off, to[2] + perp[2] * off],
23367
+ bondRadius,
23368
+ color
23369
+ );
23370
+ if (bond) out.push(bond);
23371
+ }
23372
+ }
23373
+ for (const a of arrows) {
23374
+ const angle = (a.angle ?? 0) * (Math.PI / 180);
23375
+ const len = a.length ?? 60;
23376
+ const x2 = a.x + Math.cos(angle) * len;
23377
+ const y2 = a.y + Math.sin(angle) * len;
23378
+ const arrow = arrowBetween([a.x, a.y, 0], [x2, y2, 0], a.color ?? "#dc2626");
23379
+ if (arrow) out.push(arrow);
23380
+ if (a.label) {
23381
+ out.push(billboardLabel(a.label, (a.x + x2) / 2, (a.y + y2) / 2, 0, { color: labelColor }));
23382
+ }
23383
+ }
23384
+ for (const a of atoms) {
23385
+ const radius = a.radius ?? 0.45;
23386
+ const az = a.z ?? 0;
23387
+ out.push(meshSphere(a.id, a.x, a.y, az, radius, a.color ?? "#2563eb"));
23388
+ if (a.element) {
23389
+ out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
23390
+ }
23391
+ }
23392
+ if (lattice3d) {
23393
+ out.push(...latticeDrawables(lattice3d, { labelColor }));
23394
+ }
23395
+ return out;
23396
+ }, [mode, atoms, bonds, arrows, shapes, containers, lattice3d, animate, backgroundColor]);
23397
+ const atomIndexById = React85.useMemo(() => {
23398
+ const m = /* @__PURE__ */ new Map();
23399
+ atoms.forEach((a, i) => {
23400
+ if (a.id) m.set(a.id, i);
23401
+ });
23402
+ return m;
23403
+ }, [atoms]);
23404
+ if (mode === "3d") {
23405
+ return /* @__PURE__ */ jsxRuntime.jsx(
23406
+ LearningScene3D,
23407
+ {
23408
+ className,
23409
+ width,
23410
+ height,
23411
+ title,
23412
+ backgroundColor,
23413
+ drawables: drawables3D,
23414
+ camera,
23415
+ lighting,
23416
+ post,
23417
+ showGrid,
23418
+ shadows,
23419
+ interactive,
23420
+ isLoading,
23421
+ error,
23422
+ onItemClick: get3DClickPayload(onShapeClick, atomIndexById)
23423
+ }
23424
+ );
23425
+ }
21518
23426
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
21519
23427
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
21520
23428
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -21524,7 +23432,9 @@ var init_ChemistryCanvas = __esm({
21524
23432
  height,
21525
23433
  backgroundColor,
21526
23434
  shapes: derivedShapes,
21527
- interactive,
23435
+ readouts,
23436
+ traces,
23437
+ interactive: interactive ?? false,
21528
23438
  animate,
21529
23439
  onShapeClick,
21530
23440
  isLoading,
@@ -27635,6 +29545,10 @@ var init_ProgressDots = __esm({
27635
29545
  ProgressDots.displayName = "ProgressDots";
27636
29546
  }
27637
29547
  });
29548
+ function formatTick(v) {
29549
+ if (Number.isInteger(v)) return String(v);
29550
+ return v.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
29551
+ }
27638
29552
  var MathCanvas;
27639
29553
  var init_MathCanvas = __esm({
27640
29554
  "components/learning/molecules/MathCanvas.tsx"() {
@@ -27654,10 +29568,19 @@ var init_MathCanvas = __esm({
27654
29568
  showAxes = true,
27655
29569
  showGrid = true,
27656
29570
  gridStep = 1,
29571
+ showTickLabels = false,
29572
+ showCurveLabels = false,
27657
29573
  curves = [],
27658
29574
  points = [],
27659
29575
  vectors = [],
29576
+ regions = [],
29577
+ bars = [],
29578
+ guides = [],
29579
+ angles = [],
29580
+ hops = [],
27660
29581
  shapes = [],
29582
+ readouts,
29583
+ traces,
27661
29584
  interactive = false,
27662
29585
  animate = false,
27663
29586
  onShapeClick,
@@ -27671,6 +29594,8 @@ var init_MathCanvas = __esm({
27671
29594
  const plotH = height - margin * 2;
27672
29595
  const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
27673
29596
  const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
29597
+ const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
29598
+ const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
27674
29599
  if (showGrid) {
27675
29600
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep) {
27676
29601
  const px = mapX(x);
@@ -27681,14 +29606,99 @@ var init_MathCanvas = __esm({
27681
29606
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color: "#e5e7eb", lineWidth: 1 });
27682
29607
  }
27683
29608
  }
29609
+ if (showTickLabels) {
29610
+ const labelEveryX = Math.max(1, Math.ceil((xMax - xMin) / gridStep / Math.floor(plotW / 40)));
29611
+ let kx = 0;
29612
+ for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
29613
+ if (kx % labelEveryX === 0 && x !== 0) {
29614
+ out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: 10, align: "center" });
29615
+ }
29616
+ }
29617
+ const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
29618
+ let ky = 0;
29619
+ for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
29620
+ if (ky % labelEveryY === 0 && y !== 0) {
29621
+ out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: 10, align: "right" });
29622
+ }
29623
+ }
29624
+ if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
29625
+ out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: 10, align: "right" });
29626
+ }
29627
+ }
29628
+ for (const region of regions) {
29629
+ if (!region.samples || region.samples.length === 0) continue;
29630
+ const baseline = region.baseline ?? 0;
29631
+ const clampedPoint = (p) => ({
29632
+ x: mapX(Math.min(xMax, Math.max(xMin, p.x))),
29633
+ y: mapY(Math.min(yMax, Math.max(yMin, p.y)))
29634
+ });
29635
+ const upper = region.samples.map(clampedPoint);
29636
+ const first = region.samples[0];
29637
+ const last = region.samples[region.samples.length - 1];
29638
+ 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 })];
29639
+ const color = region.color ?? "#2563eb";
29640
+ out.push({
29641
+ type: "polygon",
29642
+ points: [...upper, ...closing],
29643
+ fill: color,
29644
+ color,
29645
+ opacity: region.opacity ?? 0.2,
29646
+ lineWidth: 1
29647
+ });
29648
+ if (region.label) {
29649
+ const mid = Math.floor(region.samples.length / 2);
29650
+ out.push({
29651
+ type: "text",
29652
+ x: mapX((first.x + last.x) / 2),
29653
+ y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
29654
+ text: region.label,
29655
+ color: "#111827",
29656
+ fontSize: 11
29657
+ });
29658
+ }
29659
+ }
29660
+ for (const bar of bars) {
29661
+ if (bar.x + bar.width < xMin || bar.x > xMax) continue;
29662
+ const y0 = bar.y0 ?? 0;
29663
+ const color = bar.color ?? "#93c5fd";
29664
+ out.push({
29665
+ type: "rect",
29666
+ x: mapX(bar.x),
29667
+ y: mapY(Math.max(y0, bar.y1)),
29668
+ width: mapX(bar.x + bar.width) - mapX(bar.x),
29669
+ height: Math.abs(mapY(bar.y1) - mapY(y0)),
29670
+ color,
29671
+ fill: color,
29672
+ opacity: bar.opacity ?? 0.5,
29673
+ lineWidth: 1
29674
+ });
29675
+ }
27684
29676
  if (showAxes) {
27685
- const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
27686
- const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
27687
29677
  out.push({ type: "line", x1: margin, y1: xAxisY, x2: width - margin, y2: xAxisY, color: "#374151", lineWidth: 2 });
27688
29678
  out.push({ type: "line", x1: yAxisX, y1: margin, x2: yAxisX, y2: height - margin, color: "#374151", lineWidth: 2 });
27689
29679
  }
29680
+ for (const guide of guides) {
29681
+ const color = guide.color ?? "#9ca3af";
29682
+ const dash = guide.dash ?? "dashed";
29683
+ if (guide.kind === "vline") {
29684
+ if (guide.at < xMin || guide.at > xMax) continue;
29685
+ const px = mapX(guide.at);
29686
+ out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
29687
+ if (guide.label) {
29688
+ out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color: "#111827", fontSize: 11 });
29689
+ }
29690
+ } else {
29691
+ if (guide.at < yMin || guide.at > yMax) continue;
29692
+ const py = mapY(guide.at);
29693
+ out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
29694
+ if (guide.label) {
29695
+ out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color: "#111827", fontSize: 11, align: "right" });
29696
+ }
29697
+ }
29698
+ }
27690
29699
  for (const curve of curves) {
27691
29700
  if (!curve.samples || curve.samples.length < 2) continue;
29701
+ let lastInRange;
27692
29702
  for (let i = 1; i < curve.samples.length; i++) {
27693
29703
  const a = curve.samples[i - 1];
27694
29704
  const b = curve.samples[i];
@@ -27700,19 +29710,97 @@ var init_MathCanvas = __esm({
27700
29710
  x2: mapX(b.x),
27701
29711
  y2: mapY(b.y),
27702
29712
  color: curve.color ?? "#2563eb",
27703
- lineWidth: 2
29713
+ lineWidth: 2,
29714
+ dash: curve.dash
29715
+ });
29716
+ lastInRange = b;
29717
+ }
29718
+ if (showCurveLabels && curve.label && lastInRange) {
29719
+ out.push({
29720
+ type: "text",
29721
+ x: mapX(lastInRange.x) + 6,
29722
+ y: mapY(lastInRange.y) - 6,
29723
+ text: curve.label,
29724
+ color: curve.color ?? "#2563eb",
29725
+ fontSize: 11
29726
+ });
29727
+ }
29728
+ }
29729
+ for (const hop of hops) {
29730
+ const x1 = mapX(hop.from);
29731
+ const x2 = mapX(hop.to);
29732
+ const peak = Math.min(36, plotH * 0.3);
29733
+ const color = hop.color ?? "#7c3aed";
29734
+ out.push({
29735
+ type: "ellipse",
29736
+ x: (x1 + x2) / 2,
29737
+ y: xAxisY,
29738
+ width: Math.abs(x2 - x1),
29739
+ height: 2 * peak,
29740
+ startAngle: 180,
29741
+ endAngle: 360,
29742
+ color
29743
+ });
29744
+ const s = Math.sign(hop.to - hop.from);
29745
+ out.push({
29746
+ type: "polygon",
29747
+ points: [
29748
+ { x: x2, y: xAxisY },
29749
+ { x: x2 - 4 * s, y: xAxisY - 7 },
29750
+ { x: x2 + 2 * s, y: xAxisY - 7 }
29751
+ ],
29752
+ fill: color,
29753
+ color
29754
+ });
29755
+ if (hop.label) {
29756
+ out.push({
29757
+ type: "text",
29758
+ x: (x1 + x2) / 2,
29759
+ y: xAxisY - peak - 8,
29760
+ text: hop.label,
29761
+ color: "#111827",
29762
+ fontSize: 10,
29763
+ align: "center"
29764
+ });
29765
+ }
29766
+ }
29767
+ for (const angle of angles) {
29768
+ const radius = angle.radius ?? 0.8;
29769
+ const color = angle.color ?? "#0ea5e9";
29770
+ out.push({
29771
+ type: "ellipse",
29772
+ x: mapX(angle.x),
29773
+ y: mapY(angle.y),
29774
+ width: 2 * radius * plotW / (xMax - xMin),
29775
+ height: 2 * radius * plotH / (yMax - yMin),
29776
+ startAngle: -angle.to,
29777
+ endAngle: -angle.from,
29778
+ color
29779
+ });
29780
+ if (angle.label) {
29781
+ const mid = (angle.from + angle.to) / 2;
29782
+ const rad = mid * Math.PI / 180;
29783
+ out.push({
29784
+ type: "text",
29785
+ x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
29786
+ y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
29787
+ text: angle.label,
29788
+ color: "#111827",
29789
+ fontSize: 11,
29790
+ align: "center"
27704
29791
  });
27705
29792
  }
27706
29793
  }
27707
29794
  for (const p of points) {
27708
29795
  if (p.x < xMin || p.x > xMax || p.y < yMin || p.y > yMax) continue;
29796
+ const isOpen = p.style === "open";
27709
29797
  out.push({
27710
29798
  type: "circle",
27711
29799
  x: mapX(p.x),
27712
29800
  y: mapY(p.y),
27713
29801
  radius: p.radius ?? 4,
27714
29802
  color: p.color ?? "#dc2626",
27715
- fill: p.color ?? "#dc2626"
29803
+ fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
27716
29804
  });
27717
29805
  if (p.label) {
27718
29806
  out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: "#111827", fontSize: 12 });
@@ -27731,7 +29819,28 @@ var init_MathCanvas = __esm({
27731
29819
  }
27732
29820
  out.push(...shapes);
27733
29821
  return out;
27734
- }, [width, height, xMin, xMax, yMin, yMax, showAxes, showGrid, gridStep, curves, points, vectors, shapes]);
29822
+ }, [
29823
+ width,
29824
+ height,
29825
+ xMin,
29826
+ xMax,
29827
+ yMin,
29828
+ yMax,
29829
+ showAxes,
29830
+ showGrid,
29831
+ gridStep,
29832
+ showTickLabels,
29833
+ showCurveLabels,
29834
+ curves,
29835
+ points,
29836
+ vectors,
29837
+ regions,
29838
+ bars,
29839
+ guides,
29840
+ angles,
29841
+ hops,
29842
+ shapes
29843
+ ]);
27735
29844
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
27736
29845
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
27737
29846
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -27740,6 +29849,8 @@ var init_MathCanvas = __esm({
27740
29849
  width,
27741
29850
  height,
27742
29851
  shapes: derivedShapes,
29852
+ readouts,
29853
+ traces,
27743
29854
  interactive,
27744
29855
  animate,
27745
29856
  onShapeClick,
@@ -27751,27 +29862,355 @@ var init_MathCanvas = __esm({
27751
29862
  };
27752
29863
  }
27753
29864
  });
27754
- var PhysicsCanvas;
29865
+ function formatMeterValue(v) {
29866
+ return Number.isInteger(v) ? String(v) : String(Number(v.toFixed(2)));
29867
+ }
29868
+ function sceneObjectShapes(obj, canvasWidth, canvasHeight) {
29869
+ const out = [];
29870
+ const color = obj.color ?? "#334155";
29871
+ switch (obj.kind) {
29872
+ case "ground": {
29873
+ const xStart = obj.x1 ?? 0;
29874
+ const xEnd = obj.x2 ?? canvasWidth;
29875
+ const y = obj.y ?? 0;
29876
+ out.push({ type: "line", x1: xStart, y1: y, x2: xEnd, y2: y, color, lineWidth: 2 });
29877
+ for (let hx = xStart + 7; hx <= xEnd; hx += 14) {
29878
+ out.push({ type: "line", x1: hx, y1: y, x2: hx - 7, y2: y + 7, color, lineWidth: 1 });
29879
+ }
29880
+ if (obj.label) {
29881
+ out.push({
29882
+ type: "text",
29883
+ x: (xStart + xEnd) / 2,
29884
+ y: y - 10,
29885
+ text: obj.label,
29886
+ color: PHYSICS_LABEL_COLOR,
29887
+ fontSize: 11,
29888
+ align: "center"
29889
+ });
29890
+ }
29891
+ break;
29892
+ }
29893
+ case "wall": {
29894
+ const yStart = obj.y1 ?? 0;
29895
+ const yEnd = obj.y2 ?? canvasHeight;
29896
+ const x = obj.x ?? 0;
29897
+ out.push({ type: "line", x1: x, y1: yStart, x2: x, y2: yEnd, color, lineWidth: 2 });
29898
+ for (let hy = yStart + 7; hy <= yEnd; hy += 14) {
29899
+ out.push({ type: "line", x1: x, y1: hy, x2: x - 7, y2: hy + 7, color, lineWidth: 1 });
29900
+ }
29901
+ if (obj.label) {
29902
+ out.push({
29903
+ type: "text",
29904
+ x: x + 12,
29905
+ y: (yStart + yEnd) / 2,
29906
+ text: obj.label,
29907
+ color: PHYSICS_LABEL_COLOR,
29908
+ fontSize: 11,
29909
+ align: "left"
29910
+ });
29911
+ }
29912
+ break;
29913
+ }
29914
+ case "ramp": {
29915
+ const x1 = obj.x1 ?? 0;
29916
+ const y1 = obj.y1 ?? 0;
29917
+ const x2 = obj.x2 ?? canvasWidth;
29918
+ const y2 = obj.y2 ?? canvasHeight;
29919
+ out.push({
29920
+ type: "polygon",
29921
+ points: [
29922
+ { x: x1, y: y1 },
29923
+ { x: x2, y: y2 },
29924
+ { x: x1, y: y2 }
29925
+ ],
29926
+ color,
29927
+ fill: obj.fill ?? "#e2e8f0",
29928
+ lineWidth: 2
29929
+ });
29930
+ if (obj.label) {
29931
+ out.push({
29932
+ type: "text",
29933
+ x: (2 * x1 + x2) / 3,
29934
+ y: (y1 + 2 * y2) / 3,
29935
+ text: obj.label,
29936
+ color: PHYSICS_LABEL_COLOR,
29937
+ fontSize: 11,
29938
+ align: "center"
29939
+ });
29940
+ }
29941
+ break;
29942
+ }
29943
+ case "box": {
29944
+ const x = obj.x ?? 0;
29945
+ const y = obj.y ?? 0;
29946
+ const w = obj.width ?? 40;
29947
+ const h = obj.height ?? 40;
29948
+ out.push({ type: "rect", x, y, width: w, height: h, color, fill: obj.fill, lineWidth: 2 });
29949
+ if (obj.label) {
29950
+ out.push({
29951
+ type: "text",
29952
+ x: x + w / 2,
29953
+ y: y + h / 2,
29954
+ text: obj.label,
29955
+ color: PHYSICS_LABEL_COLOR,
29956
+ fontSize: 11,
29957
+ align: "center"
29958
+ });
29959
+ }
29960
+ break;
29961
+ }
29962
+ case "pivot": {
29963
+ const x = obj.x ?? 0;
29964
+ const y = obj.y ?? 0;
29965
+ out.push({ type: "circle", x, y, radius: 5, color, fill: color });
29966
+ out.push({ type: "line", x1: x - 14, y1: y - 8, x2: x + 14, y2: y - 8, color, lineWidth: 1 });
29967
+ for (let k = 0; k < 5; k++) {
29968
+ const hx = x - 14 + 7 * k;
29969
+ out.push({ type: "line", x1: hx, y1: y - 8, x2: hx - 6, y2: y - 14, color, lineWidth: 1 });
29970
+ }
29971
+ if (obj.label) {
29972
+ out.push({
29973
+ type: "text",
29974
+ x,
29975
+ y: y - 20,
29976
+ text: obj.label,
29977
+ color: PHYSICS_LABEL_COLOR,
29978
+ fontSize: 11,
29979
+ align: "center"
29980
+ });
29981
+ }
29982
+ break;
29983
+ }
29984
+ }
29985
+ return out;
29986
+ }
29987
+ function trailShapes(trail) {
29988
+ const n = trail.points.length;
29989
+ if (n < 2) return [];
29990
+ const color = trail.color ?? "#94a3b8";
29991
+ const lineWidth = trail.width ?? 2;
29992
+ const fade = trail.fade ?? true;
29993
+ const globalOpacity = trail.opacity ?? 1;
29994
+ const out = [];
29995
+ for (let i = 0; i < n - 1; i++) {
29996
+ const a = trail.points[i];
29997
+ const b = trail.points[i + 1];
29998
+ const segmentOpacity = fade ? 0.12 + 0.68 * i / (n - 1) : 0.6;
29999
+ out.push({
30000
+ type: "line",
30001
+ x1: a.x,
30002
+ y1: a.y,
30003
+ x2: b.x,
30004
+ y2: b.y,
30005
+ color,
30006
+ lineWidth,
30007
+ opacity: segmentOpacity * globalOpacity
30008
+ });
30009
+ }
30010
+ return out;
30011
+ }
30012
+ function constraintShapes(c, a, b) {
30013
+ const color = c.color ?? "#9ca3af";
30014
+ const kind = c.kind ?? "rod";
30015
+ if (kind === "rod") {
30016
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2 }];
30017
+ }
30018
+ if (kind === "string") {
30019
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2, dash: "dashed" }];
30020
+ }
30021
+ const COILS = 8;
30022
+ const AMP = 7;
30023
+ const LEAD = 10;
30024
+ const dx = b.x - a.x;
30025
+ const dy = b.y - a.y;
30026
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
30027
+ const ux = dx / dist;
30028
+ const uy = dy / dist;
30029
+ const perpX = -uy;
30030
+ const perpY = ux;
30031
+ const aPrime = { x: a.x + LEAD * ux, y: a.y + LEAD * uy };
30032
+ const bPrime = { x: b.x - LEAD * ux, y: b.y - LEAD * uy };
30033
+ const m = 2 * COILS;
30034
+ const polyline = [{ x: a.x, y: a.y }, aPrime];
30035
+ for (let j = 1; j <= m; j++) {
30036
+ const t = j / (m + 1);
30037
+ const baseX = aPrime.x + t * (bPrime.x - aPrime.x);
30038
+ const baseY = aPrime.y + t * (bPrime.y - aPrime.y);
30039
+ const sign = j % 2 === 0 ? 1 : -1;
30040
+ polyline.push({ x: baseX + sign * AMP * perpX, y: baseY + sign * AMP * perpY });
30041
+ }
30042
+ polyline.push(bPrime, { x: b.x, y: b.y });
30043
+ const out = [];
30044
+ for (let i = 1; i < polyline.length; i++) {
30045
+ out.push({
30046
+ type: "line",
30047
+ x1: polyline[i - 1].x,
30048
+ y1: polyline[i - 1].y,
30049
+ x2: polyline[i].x,
30050
+ y2: polyline[i].y,
30051
+ color,
30052
+ lineWidth: 2
30053
+ });
30054
+ }
30055
+ return out;
30056
+ }
30057
+ function vectorShapes(v, bodyById) {
30058
+ let ax;
30059
+ let ay;
30060
+ if (v.body) {
30061
+ const anchor = bodyById.get(v.body);
30062
+ if (!anchor) return [];
30063
+ ax = anchor.x;
30064
+ ay = anchor.y;
30065
+ } else {
30066
+ ax = v.x ?? 0;
30067
+ ay = v.y ?? 0;
30068
+ }
30069
+ const scale = v.scale ?? 1;
30070
+ const color = v.color ?? "#dc2626";
30071
+ const tx = ax + v.dx * scale;
30072
+ const ty = ay + v.dy * scale;
30073
+ const out = [{ type: "arrow", x1: ax, y1: ay, x2: tx, y2: ty, color, lineWidth: 2, dash: v.dash }];
30074
+ if (v.label) {
30075
+ const dist = Math.max(1e-6, Math.hypot(tx - ax, ty - ay));
30076
+ const ux = (tx - ax) / dist;
30077
+ const uy = (ty - ay) / dist;
30078
+ out.push({
30079
+ type: "text",
30080
+ x: tx + 8 * ux,
30081
+ y: ty + 8 * uy,
30082
+ text: v.label,
30083
+ color,
30084
+ fontSize: 11,
30085
+ align: "center"
30086
+ });
30087
+ }
30088
+ return out;
30089
+ }
30090
+ function angleMarkerShapes(a) {
30091
+ const radius = a.radius ?? 26;
30092
+ const color = a.color ?? "#0ea5e9";
30093
+ const out = [
30094
+ {
30095
+ type: "ellipse",
30096
+ x: a.x,
30097
+ y: a.y,
30098
+ width: radius * 2,
30099
+ height: radius * 2,
30100
+ startAngle: a.from,
30101
+ endAngle: a.to,
30102
+ color,
30103
+ lineWidth: 2
30104
+ }
30105
+ ];
30106
+ if (a.label) {
30107
+ const mid = (a.from + a.to) / 2 * (Math.PI / 180);
30108
+ out.push({
30109
+ type: "text",
30110
+ x: a.x + (radius + 13) * Math.cos(mid),
30111
+ y: a.y + (radius + 13) * Math.sin(mid),
30112
+ text: a.label,
30113
+ color,
30114
+ fontSize: 11,
30115
+ align: "center"
30116
+ });
30117
+ }
30118
+ return out;
30119
+ }
30120
+ function fieldShapes(field, canvasWidth, canvasHeight) {
30121
+ const spacing = field.spacing ?? 48;
30122
+ const size = field.size ?? 14;
30123
+ const color = field.color ?? "#94a3b8";
30124
+ const regionX = field.x ?? 0;
30125
+ const regionY = field.y ?? 0;
30126
+ const regionW = field.width ?? canvasWidth;
30127
+ const regionH = field.height ?? canvasHeight;
30128
+ const out = [];
30129
+ for (let gx = regionX + spacing / 2; gx < regionX + regionW; gx += spacing) {
30130
+ for (let gy = regionY + spacing / 2; gy < regionY + regionH; gy += spacing) {
30131
+ if (field.kind === "arrows") {
30132
+ const rad = (field.angle ?? 0) * Math.PI / 180;
30133
+ const hx = Math.cos(rad) * size / 2;
30134
+ const hy = Math.sin(rad) * size / 2;
30135
+ out.push({ type: "arrow", x1: gx - hx, y1: gy - hy, x2: gx + hx, y2: gy + hy, color, lineWidth: 2 });
30136
+ } else if (field.kind === "into") {
30137
+ const r = size / 3;
30138
+ const d = 0.6 * r * Math.SQRT1_2;
30139
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30140
+ out.push({ type: "line", x1: gx - d, y1: gy - d, x2: gx + d, y2: gy + d, color, lineWidth: 1 });
30141
+ out.push({ type: "line", x1: gx - d, y1: gy + d, x2: gx + d, y2: gy - d, color, lineWidth: 1 });
30142
+ } else {
30143
+ const r = size / 3;
30144
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30145
+ out.push({ type: "circle", x: gx, y: gy, radius: 1.5, color, fill: color });
30146
+ }
30147
+ }
30148
+ }
30149
+ return out;
30150
+ }
30151
+ function meterShapes(meters, canvasHeight) {
30152
+ const n = meters.length;
30153
+ const out = [];
30154
+ const sharedMax = Math.max(1e-6, ...meters.map((m) => m.value));
30155
+ meters.forEach((meter, i) => {
30156
+ const rowY = canvasHeight - 10 - 16 * (n - i);
30157
+ const color = meter.color ?? "#3b82f6";
30158
+ const M = meter.max ?? sharedMax;
30159
+ const w = Math.round(Math.min(1, Math.max(0, meter.value / M)) * 110);
30160
+ out.push({ type: "text", x: 8, y: rowY + 8, text: meter.label, color: PHYSICS_LABEL_COLOR, fontSize: 10 });
30161
+ out.push({ type: "rect", x: 52, y: rowY, width: w, height: 10, color, fill: color });
30162
+ out.push({
30163
+ type: "text",
30164
+ x: 166,
30165
+ y: rowY + 8,
30166
+ text: formatMeterValue(meter.value),
30167
+ color: "#6b7280",
30168
+ fontSize: 9
30169
+ });
30170
+ });
30171
+ return out;
30172
+ }
30173
+ var physicsLog2, PHYSICS_LABEL_COLOR, PhysicsCanvas;
27755
30174
  var init_PhysicsCanvas = __esm({
27756
30175
  "components/learning/molecules/PhysicsCanvas.tsx"() {
27757
30176
  "use client";
27758
30177
  init_atoms();
27759
30178
  init_Stack();
27760
30179
  init_LearningCanvas();
30180
+ init_learningScene3D();
30181
+ physicsLog2 = logger.createLogger("almadar:ui:physics-canvas");
30182
+ PHYSICS_LABEL_COLOR = "#374151";
27761
30183
  PhysicsCanvas = ({
27762
30184
  className,
27763
30185
  width = 600,
27764
30186
  height = 400,
27765
30187
  title,
27766
30188
  backgroundColor,
30189
+ mode = "2d",
30190
+ camera,
30191
+ lighting,
30192
+ post,
27767
30193
  bodies = [],
27768
30194
  constraints = [],
27769
30195
  showVelocity = true,
27770
30196
  showForces = false,
27771
30197
  velocityScale = 20,
27772
30198
  forceScale = 20,
30199
+ sceneObjects = [],
30200
+ trails = [],
30201
+ vectors = [],
30202
+ surface3d,
30203
+ vectors3d = [],
30204
+ vectorScale = 1,
30205
+ angles = [],
30206
+ field,
30207
+ meters = [],
27773
30208
  shapes = [],
27774
- interactive = false,
30209
+ readouts,
30210
+ traces,
30211
+ showGrid,
30212
+ shadows,
30213
+ interactive,
27775
30214
  animate = false,
27776
30215
  onShapeClick,
27777
30216
  isLoading,
@@ -27783,19 +30222,14 @@ var init_PhysicsCanvas = __esm({
27783
30222
  for (const b of bodies) {
27784
30223
  if (b.id) bodyById.set(b.id, b);
27785
30224
  }
30225
+ if (field) out.push(...fieldShapes(field, width, height));
30226
+ for (const obj of sceneObjects) out.push(...sceneObjectShapes(obj, width, height));
30227
+ for (const trail of trails) out.push(...trailShapes(trail));
27786
30228
  for (const c of constraints) {
27787
30229
  const a = bodyById.get(c.from);
27788
30230
  const b = bodyById.get(c.to);
27789
30231
  if (!a || !b) continue;
27790
- out.push({
27791
- type: "line",
27792
- x1: a.x,
27793
- y1: a.y,
27794
- x2: b.x,
27795
- y2: b.y,
27796
- color: c.color ?? "#9ca3af",
27797
- lineWidth: 2
27798
- });
30232
+ out.push(...constraintShapes(c, a, b));
27799
30233
  }
27800
30234
  for (const b of bodies) {
27801
30235
  out.push({
@@ -27840,9 +30274,181 @@ var init_PhysicsCanvas = __esm({
27840
30274
  });
27841
30275
  }
27842
30276
  }
30277
+ for (const v of vectors) out.push(...vectorShapes(v, bodyById));
30278
+ for (const a of angles) out.push(...angleMarkerShapes(a));
30279
+ if (meters.length > 0) out.push(...meterShapes(meters, height));
27843
30280
  out.push(...shapes);
27844
30281
  return out;
27845
- }, [bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes]);
30282
+ }, [
30283
+ bodies,
30284
+ constraints,
30285
+ showVelocity,
30286
+ showForces,
30287
+ velocityScale,
30288
+ forceScale,
30289
+ sceneObjects,
30290
+ trails,
30291
+ vectors,
30292
+ angles,
30293
+ field,
30294
+ meters,
30295
+ shapes,
30296
+ width,
30297
+ height
30298
+ ]);
30299
+ const drawables3D = React85.useMemo(() => {
30300
+ if (mode !== "3d") return [];
30301
+ if (shapes.length > 0) {
30302
+ physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
30303
+ }
30304
+ if (sceneObjects.length > 0) {
30305
+ physicsLog2.debug("sceneObjects ignored in 3D mode (pixel-authored 2D vocabulary)", { count: sceneObjects.length });
30306
+ }
30307
+ if (vectors.length > 0) {
30308
+ physicsLog2.debug("vectors ignored in 3D mode (pixel-authored 2D vocabulary)", { count: vectors.length });
30309
+ }
30310
+ if (angles.length > 0) {
30311
+ physicsLog2.debug("angles ignored in 3D mode (pixel-authored 2D vocabulary)", { count: angles.length });
30312
+ }
30313
+ if (field) {
30314
+ physicsLog2.debug("field ignored in 3D mode (pixel-authored 2D vocabulary)");
30315
+ }
30316
+ if (meters.length > 0) {
30317
+ physicsLog2.debug("meters ignored in 3D mode (pixel-authored 2D vocabulary)", { count: meters.length });
30318
+ }
30319
+ if (animate) {
30320
+ physicsLog2.debug("animate ignored in 3D mode (motion is entity-state driven)");
30321
+ }
30322
+ const out = [];
30323
+ const labelColor = labelColorForBackground(backgroundColor);
30324
+ const bodyById = /* @__PURE__ */ new Map();
30325
+ for (const b of bodies) {
30326
+ if (b.id) bodyById.set(b.id, b);
30327
+ }
30328
+ for (const c of constraints) {
30329
+ const a = bodyById.get(c.from);
30330
+ const b = bodyById.get(c.to);
30331
+ if (!a || !b) continue;
30332
+ const rodRadius = Math.max(0.05, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.15);
30333
+ const rod = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], rodRadius, c.color ?? "#9ca3af");
30334
+ if (rod) out.push(rod);
30335
+ }
30336
+ for (const b of bodies) {
30337
+ const radius = b.radius ?? 0.5;
30338
+ const bz = b.z ?? 0;
30339
+ out.push(meshSphere(b.id, b.x, b.y, bz, radius, b.color ?? "#2563eb"));
30340
+ if (b.label) {
30341
+ out.push(billboardLabel(b.label, b.x, b.y, bz + radius, { color: labelColor }));
30342
+ }
30343
+ const vx = b.vx ?? 0;
30344
+ const vy = b.vy ?? 0;
30345
+ const vz = b.vz ?? 0;
30346
+ const arrowRadius = Math.max(0.05, radius * 0.15);
30347
+ if (showVelocity && (vx !== 0 || vy !== 0 || vz !== 0)) {
30348
+ const arrow = arrowBetween(
30349
+ [b.x, b.y, bz],
30350
+ [b.x + vx * velocityScale, b.y + vy * velocityScale, bz + vz * velocityScale],
30351
+ "#16a34a",
30352
+ arrowRadius
30353
+ );
30354
+ if (arrow) out.push(arrow);
30355
+ }
30356
+ const fx = b.fx ?? 0;
30357
+ const fy = b.fy ?? 0;
30358
+ const fz = b.fz ?? 0;
30359
+ if (showForces && (fx !== 0 || fy !== 0 || fz !== 0)) {
30360
+ const arrow = arrowBetween(
30361
+ [b.x, b.y, bz],
30362
+ [b.x + fx * forceScale, b.y + fy * forceScale, bz + fz * forceScale],
30363
+ "#dc2626",
30364
+ arrowRadius
30365
+ );
30366
+ if (arrow) out.push(arrow);
30367
+ }
30368
+ }
30369
+ for (const trail of trails) {
30370
+ if (trail.fade !== void 0) {
30371
+ physicsLog2.debug("trail.fade ignored in 3D mode (2D-only fade curve \u2014 3D draws an opaque tube)", { id: trail.id });
30372
+ }
30373
+ const points = trail.points.map((p) => [p.x, p.y, p.z ?? 0]);
30374
+ out.push(
30375
+ ...polylineTube(points, trail.width ?? 0.05, trail.color ?? "#94a3b8", {
30376
+ ...trail.opacity !== void 0 ? { opacity: trail.opacity } : {}
30377
+ })
30378
+ );
30379
+ }
30380
+ if (surface3d) {
30381
+ out.push(...heightFieldMesh(surface3d));
30382
+ }
30383
+ if (vectors3d.length > 0) {
30384
+ out.push(
30385
+ ...arrowField(
30386
+ vectors3d.map((v) => ({
30387
+ id: v.id,
30388
+ from: [v.x, v.y, v.z ?? 0],
30389
+ delta: [v.dx, v.dy, v.dz ?? 0],
30390
+ color: v.color,
30391
+ label: v.label,
30392
+ width: v.width
30393
+ })),
30394
+ { scale: vectorScale, labelColor }
30395
+ )
30396
+ );
30397
+ }
30398
+ return out;
30399
+ }, [
30400
+ mode,
30401
+ bodies,
30402
+ constraints,
30403
+ showVelocity,
30404
+ showForces,
30405
+ velocityScale,
30406
+ forceScale,
30407
+ shapes,
30408
+ sceneObjects,
30409
+ trails,
30410
+ vectors,
30411
+ surface3d,
30412
+ vectors3d,
30413
+ vectorScale,
30414
+ angles,
30415
+ field,
30416
+ meters,
30417
+ animate,
30418
+ backgroundColor
30419
+ ]);
30420
+ const bodyIndexById = React85.useMemo(() => {
30421
+ const m = /* @__PURE__ */ new Map();
30422
+ vectors3d.forEach((v, i) => {
30423
+ if (v.id) m.set(v.id, i);
30424
+ });
30425
+ bodies.forEach((b, i) => {
30426
+ if (b.id) m.set(b.id, i);
30427
+ });
30428
+ return m;
30429
+ }, [bodies, vectors3d]);
30430
+ if (mode === "3d") {
30431
+ return /* @__PURE__ */ jsxRuntime.jsx(
30432
+ LearningScene3D,
30433
+ {
30434
+ className,
30435
+ width,
30436
+ height,
30437
+ title,
30438
+ backgroundColor,
30439
+ drawables: drawables3D,
30440
+ camera,
30441
+ lighting,
30442
+ post,
30443
+ showGrid,
30444
+ shadows,
30445
+ interactive,
30446
+ isLoading,
30447
+ error,
30448
+ onItemClick: get3DClickPayload(onShapeClick, bodyIndexById)
30449
+ }
30450
+ );
30451
+ }
27846
30452
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
27847
30453
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
27848
30454
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -27852,7 +30458,9 @@ var init_PhysicsCanvas = __esm({
27852
30458
  height,
27853
30459
  backgroundColor,
27854
30460
  shapes: derivedShapes,
27855
- interactive,
30461
+ readouts,
30462
+ traces,
30463
+ interactive: interactive ?? false,
27856
30464
  animate,
27857
30465
  onShapeClick,
27858
30466
  isLoading,
@@ -28002,7 +30610,7 @@ function layoutFlow(nodeIds, adjacency, roots, width, height, margin) {
28002
30610
  }
28003
30611
  return nodeIds.map((id) => positions.get(id));
28004
30612
  }
28005
- function layoutTree(nodeIds, adjacency, roots, width, height, margin) {
30613
+ function layoutTree2(nodeIds, adjacency, roots, width, height, margin) {
28006
30614
  const effectiveRoots = roots.length > 0 ? roots : [nodeIds[0]];
28007
30615
  const layers = assignLayers(nodeIds, adjacency, effectiveRoots);
28008
30616
  const maxLayer = Math.max(...Array.from(layers.values()));
@@ -28058,7 +30666,7 @@ function computeStaticLayout(mode, input) {
28058
30666
  const adjacency = buildAdjacency(nodeIds, edges);
28059
30667
  const roots = findRoots(nodeIds, adjacency);
28060
30668
  if (mode === "flow") return layoutFlow(nodeIds, adjacency, roots, width, height, margin);
28061
- if (mode === "tree") return layoutTree(nodeIds, adjacency, roots, width, height, margin);
30669
+ if (mode === "tree") return layoutTree2(nodeIds, adjacency, roots, width, height, margin);
28062
30670
  return layoutRadial(nodeIds, adjacency, roots, width, height, margin);
28063
30671
  }
28064
30672
  var init_graphViewLayouts = __esm({
@@ -28470,12 +31078,12 @@ var init_MapView = __esm({
28470
31078
  shadowSize: [41, 41]
28471
31079
  });
28472
31080
  L.Marker.prototype.options.icon = defaultIcon;
28473
- const { useEffect: useEffect60, useRef: useRef62, useCallback: useCallback90, useState: useState92 } = React85__namespace.default;
31081
+ const { useEffect: useEffect60, useRef: useRef63, useCallback: useCallback90, useState: useState92 } = React85__namespace.default;
28474
31082
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
28475
31083
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
28476
31084
  function MapUpdater({ centerLat, centerLng, zoom }) {
28477
31085
  const map = useMap();
28478
- const prevRef = useRef62({ centerLat, centerLng, zoom });
31086
+ const prevRef = useRef63({ centerLat, centerLng, zoom });
28479
31087
  useEffect60(() => {
28480
31088
  const prev = prevRef.current;
28481
31089
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
@@ -29339,8 +31947,8 @@ function TableView({
29339
31947
  columns,
29340
31948
  fields,
29341
31949
  itemActions,
29342
- maxInlineActions,
29343
- itemClickEvent,
31950
+ maxInlineActions: _maxInlineActions,
31951
+ itemClickEvent = "",
29344
31952
  selectable = false,
29345
31953
  selectEvent,
29346
31954
  selectedIds,
@@ -29390,7 +31998,6 @@ function TableView({
29390
31998
  const hasMore = pageSize > 0 && visibleCount < ordered2.length;
29391
31999
  const hasRenderProp = typeof children === "function";
29392
32000
  const idField = dndItemIdField ?? "id";
29393
- const isCoarsePointer = useMediaQuery("(pointer: coarse)");
29394
32001
  React85__namespace.default.useEffect(() => {
29395
32002
  tableViewLog.debug("render", {
29396
32003
  rowCount: data.length,
@@ -29432,21 +32039,14 @@ function TableView({
29432
32039
  const dir = sortColumn === (col.field ?? col.key) && sortDirection === "asc" ? "desc" : "asc";
29433
32040
  eventBus.emit(`UI:${sortEvent}`, { column: col.field ?? col.key, direction: dir });
29434
32041
  };
29435
- const handleActionClick = (action, row) => (e) => {
29436
- e.stopPropagation();
29437
- const payload = {
29438
- id: row.id,
29439
- row
29440
- };
29441
- eventBus.emit(`UI:${action.event}`, payload);
29442
- };
32042
+ const rowClickEvent = itemClickEvent || actionDefs.find((a) => a.variant !== "danger")?.event;
29443
32043
  const handleRowClick = (row) => () => {
29444
- if (!itemClickEvent) return;
32044
+ if (!rowClickEvent) return;
29445
32045
  const payload = {
29446
32046
  id: row.id,
29447
32047
  row
29448
32048
  };
29449
- eventBus.emit(`UI:${itemClickEvent}`, payload);
32049
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
29450
32050
  };
29451
32051
  const colFloors = React85__namespace.default.useMemo(
29452
32052
  () => colDefs.map((col) => {
@@ -29462,10 +32062,7 @@ function TableView({
29462
32062
  const statusNode = isLoading ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) }) : error ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "error", children: error.message }) }) : data.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-12", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: emptyMessage || t("empty.noItems") }) }) : null;
29463
32063
  const lk = LOOKS[look];
29464
32064
  const hasActions = actionDefs.length > 0;
29465
- const effectiveMaxInline = isCoarsePointer ? 0 : maxInlineActions;
29466
- const inlineActionCount = hasActions ? effectiveMaxInline != null ? Math.min(actionDefs.length, effectiveMaxInline) : actionDefs.length : 0;
29467
- const hasOverflowActions = hasActions && effectiveMaxInline != null && actionDefs.length > effectiveMaxInline;
29468
- const actionsTrack = hasActions ? `${inlineActionCount * 6 + (hasOverflowActions ? 3 : 0)}rem` : null;
32065
+ const actionsTrack = hasActions ? "3rem" : null;
29469
32066
  const gridTemplateColumns = [
29470
32067
  selectable ? "auto" : null,
29471
32068
  ...colDefs.map((c, i) => c.width ?? `minmax(${colFloors[i]}ch, 1fr)`),
@@ -29512,7 +32109,7 @@ function TableView({
29512
32109
  col.key
29513
32110
  );
29514
32111
  }),
29515
- hasActions && /* @__PURE__ */ jsxRuntime.jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)]" })
32112
+ hasActions && /* @__PURE__ */ jsxRuntime.jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)] border-l border-[var(--color-border)] h-full" })
29516
32113
  ]
29517
32114
  }
29518
32115
  );
@@ -29524,12 +32121,12 @@ function TableView({
29524
32121
  role: "row",
29525
32122
  "data-entity-row": true,
29526
32123
  "data-entity-id": id,
29527
- onClick: itemClickEvent ? handleRowClick(row) : void 0,
32124
+ onClick: rowClickEvent ? handleRowClick(row) : void 0,
29528
32125
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
29529
32126
  className: cn(
29530
32127
  "group items-center gap-3 transition-colors duration-fast",
29531
32128
  hasRenderProp ? "flex" : "grid",
29532
- itemClickEvent && "cursor-pointer",
32129
+ rowClickEvent && "cursor-pointer",
29533
32130
  lk.rowPad,
29534
32131
  lk.divider && "border-b border-[var(--color-border)]",
29535
32132
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -29537,7 +32134,7 @@ function TableView({
29537
32134
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
29538
32135
  ),
29539
32136
  children: [
29540
- selectable && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex items-center", onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsxRuntime.jsx(
32137
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex items-center", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsxRuntime.jsx(
29541
32138
  Checkbox,
29542
32139
  {
29543
32140
  checked: selected.has(id),
@@ -29558,53 +32155,37 @@ function TableView({
29558
32155
  }
29559
32156
  return /* @__PURE__ */ jsxRuntime.jsx(Box, { role: "cell", className: cellBase, children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-foreground", children: formatCell(raw, col.format) }) }, col.key);
29560
32157
  }),
29561
- hasActions && /* @__PURE__ */ jsxRuntime.jsxs(
32158
+ hasActions && /* @__PURE__ */ jsxRuntime.jsx(
29562
32159
  HStack,
29563
32160
  {
29564
32161
  gap: "xs",
29565
- onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
32162
+ onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0,
29566
32163
  className: cn(
29567
32164
  // Pinned: the fixed column tracks routinely overflow the caller's
29568
- // scroll container, which used to leave the actions off-screen.
29569
- // Opaque so scrolled cells pass underneath it.
32165
+ // scroll container, which would leave the kebab off-screen.
32166
+ // Opaque + hairline edge so it reads as a pinned column, not a
32167
+ // floating control, while scrolled cells pass underneath.
29570
32168
  "justify-end flex-shrink-0 sticky right-0 z-[1] transition-colors",
32169
+ "border-l border-[var(--color-border)]",
29571
32170
  lk.striped && index % 2 === 1 ? "bg-[var(--color-surface-subtle)]" : "bg-[var(--color-card)] group-hover:bg-[var(--color-surface-subtle)]"
29572
32171
  ),
29573
- children: [
29574
- (effectiveMaxInline != null ? actionDefs.slice(0, effectiveMaxInline) : actionDefs).map((action, i) => /* @__PURE__ */ jsxRuntime.jsxs(
29575
- Button,
29576
- {
29577
- variant: action.variant === "primary" ? "primary" : "ghost",
29578
- size: "sm",
29579
- onClick: handleActionClick(action, row),
29580
- "data-testid": `action-${action.event}`,
29581
- "data-row-id": String(row.id),
29582
- className: cn(action.variant === "danger" && "text-error hover:text-error hover:bg-error/10"),
29583
- children: [
29584
- action.icon && renderIconInput3(action.icon, { size: "xs", className: "mr-1" }),
29585
- action.label
29586
- ]
29587
- },
29588
- i
29589
- )),
29590
- effectiveMaxInline != null && actionDefs.length > effectiveMaxInline && /* @__PURE__ */ jsxRuntime.jsx(
29591
- Menu,
29592
- {
29593
- position: "bottom-end",
29594
- trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
29595
- items: actionDefs.slice(effectiveMaxInline).map((action) => ({
29596
- label: action.label,
29597
- icon: action.icon,
29598
- event: action.event,
29599
- variant: action.variant === "danger" ? "danger" : "default",
29600
- onClick: () => eventBus.emit(`UI:${action.event}`, {
29601
- id: row.id,
29602
- row
29603
- })
29604
- }))
29605
- }
29606
- )
29607
- ]
32172
+ children: /* @__PURE__ */ jsxRuntime.jsx(
32173
+ Menu,
32174
+ {
32175
+ position: "bottom-end",
32176
+ trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", "data-row-id": String(row.id), children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
32177
+ items: actionDefs.map((action) => ({
32178
+ label: action.label,
32179
+ icon: action.icon,
32180
+ event: action.event,
32181
+ variant: action.variant === "danger" ? "danger" : "default",
32182
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
32183
+ id: row.id,
32184
+ row
32185
+ })
32186
+ }))
32187
+ }
32188
+ )
29608
32189
  }
29609
32190
  )
29610
32191
  ]
@@ -29647,7 +32228,6 @@ var init_TableView = __esm({
29647
32228
  init_format();
29648
32229
  init_getNestedValue();
29649
32230
  init_useEventBus();
29650
- init_useMediaQuery();
29651
32231
  init_Box();
29652
32232
  init_Stack();
29653
32233
  init_Typography();
@@ -39140,7 +41720,7 @@ function getEnumOptions(field) {
39140
41720
  }));
39141
41721
  }
39142
41722
  const validation = field.validation;
39143
- if (validation?.enum && Array.isArray(validation.enum)) {
41723
+ if (validation?.enum && validation.enum.length > 0) {
39144
41724
  return validation.enum.map((v) => ({
39145
41725
  value: v,
39146
41726
  label: v.charAt(0).toUpperCase() + v.slice(1).replace(/_/g, " ")
@@ -44587,6 +47167,7 @@ var init_component_registry_generated = __esm({
44587
47167
  init_ActionTile();
44588
47168
  init_ActivationBlock();
44589
47169
  init_ComponentPatterns();
47170
+ init_AlgoGraphCanvas();
44590
47171
  init_AlgorithmCanvas();
44591
47172
  init_AnimatedCounter();
44592
47173
  init_AnimatedGraphic();
@@ -44850,6 +47431,7 @@ var init_component_registry_generated = __esm({
44850
47431
  "ActivationBlock": ActivationBlock,
44851
47432
  "Alert": AlertPattern,
44852
47433
  "AlertPattern": AlertPattern,
47434
+ "AlgoGraphCanvas": AlgoGraphCanvas,
44853
47435
  "AlgorithmCanvas": AlgorithmCanvas,
44854
47436
  "AnimatedCounter": AnimatedCounter,
44855
47437
  "AnimatedGraphic": AnimatedGraphic,
@@ -46176,9 +48758,9 @@ var log3 = logger.createLogger("almadar:ui:effects:client-handlers");
46176
48758
  function createClientEffectHandlers(options) {
46177
48759
  const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
46178
48760
  return {
46179
- emit: (event, payload) => {
48761
+ emit: (event, payload, source) => {
46180
48762
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
46181
- eventBus.emit(prefixedEvent, payload);
48763
+ eventBus.emit(prefixedEvent, payload, source);
46182
48764
  },
46183
48765
  persist: async () => {
46184
48766
  log3.warn("persist is server-side only, ignored on client");
@@ -46513,7 +49095,7 @@ function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
46513
49095
  }
46514
49096
  };
46515
49097
  ctx.emit = (event, payload) => {
46516
- emit(event, payload);
49098
+ emit(event, payload, { trait: traitName, tick: tick.name });
46517
49099
  };
46518
49100
  if (tick.guard !== void 0 && !evaluator.evaluateGuard(tick.guard, ctx)) {
46519
49101
  tickLog.debug("guard-blocked", { traitName, tick: tick.name, state: currentState });
@@ -46965,6 +49547,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
46965
49547
  }
46966
49548
  const effectContext = {
46967
49549
  traitName,
49550
+ orbitalName: orbitalsByTrait?.[traitName],
46968
49551
  state: previousState,
46969
49552
  transition: `${previousState}->${newState}`,
46970
49553
  linkedEntity,
@@ -47041,7 +49624,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
47041
49624
  });
47042
49625
  }
47043
49626
  return emittedDuringExec;
47044
- }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot]);
49627
+ }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot, orbitalsByTrait]);
47045
49628
  const runTickEffects = React85.useCallback((tick, binding) => {
47046
49629
  const traitName = binding.trait.name;
47047
49630
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
@@ -47077,9 +49660,9 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
47077
49660
  log: tickLog
47078
49661
  });
47079
49662
  }, [executeTransitionEffects, sharedEntityStore]);
47080
- const emitFromSharedWriter = React85.useCallback((event, payload) => {
49663
+ const emitFromSharedWriter = React85.useCallback((event, payload, source) => {
47081
49664
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
47082
- eventBus.emit(prefixedEvent, payload);
49665
+ eventBus.emit(prefixedEvent, payload, source);
47083
49666
  }, [eventBus]);
47084
49667
  React85.useEffect(() => {
47085
49668
  const scheduler = runtime.createTickScheduler();