@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
@@ -135,7 +135,7 @@ function useEventBus() {
135
135
  return {
136
136
  ...baseBus,
137
137
  emit: (type, payload, source) => {
138
- if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
138
+ if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".") && !source?.trait) {
139
139
  scopeLog.warn("emit:bare-key-no-scope", { type });
140
140
  }
141
141
  baseBus.emit(type, payload, source);
@@ -539,26 +539,6 @@ var init_useDragReorder = __esm({
539
539
  "use client";
540
540
  }
541
541
  });
542
- function useMediaQuery(query) {
543
- const subscribe = useCallback(
544
- (onChange) => {
545
- const mql = window.matchMedia(query);
546
- mql.addEventListener("change", onChange);
547
- return () => mql.removeEventListener("change", onChange);
548
- },
549
- [query]
550
- );
551
- return useSyncExternalStore(
552
- subscribe,
553
- () => window.matchMedia(query).matches,
554
- () => false
555
- );
556
- }
557
- var init_useMediaQuery = __esm({
558
- "hooks/useMediaQuery.ts"() {
559
- "use client";
560
- }
561
- });
562
542
  function useInfiniteScroll(onLoadMore, options = {}) {
563
543
  const { rootMargin = "200px", hasMore = true, isLoading = false } = options;
564
544
  const observerRef = useRef(null);
@@ -10655,9 +10635,10 @@ function Canvas({
10655
10635
  drawables: [...drawables ?? [], ...childDrawables],
10656
10636
  isLoading,
10657
10637
  cameraMode: to3DCameraMode(camera?.mode),
10658
- ...zoom !== void 0 ? { scale: zoom } : {},
10638
+ ...zoom !== void 0 ? { zoom } : {},
10659
10639
  ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
10660
10640
  ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
10641
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
10661
10642
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
10662
10643
  unitScale,
10663
10644
  backgroundColor,
@@ -11658,6 +11639,14 @@ function shapeBounds(shape) {
11658
11639
  w: shape.radius * 2 + 8,
11659
11640
  h: shape.radius * 2 + 8
11660
11641
  };
11642
+ case "ellipse":
11643
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11644
+ return {
11645
+ x: shape.x - shape.width / 2 - 4,
11646
+ y: shape.y - shape.height / 2 - 4,
11647
+ w: shape.width + 8,
11648
+ h: shape.height + 8
11649
+ };
11661
11650
  case "rect":
11662
11651
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11663
11652
  return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
@@ -11691,13 +11680,14 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
11691
11680
  ctx.closePath();
11692
11681
  ctx.fill();
11693
11682
  }
11694
- function drawShape(ctx, shape, width, height) {
11683
+ function drawShape(ctx, shape, width, height, allShapes) {
11695
11684
  ctx.save();
11696
11685
  const opacity = shape.opacity ?? 1;
11697
11686
  ctx.globalAlpha = opacity;
11698
11687
  const stroke = resolveColor2(shape.color, ctx, "#333333");
11699
11688
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
11700
11689
  ctx.lineWidth = shape.lineWidth ?? 2;
11690
+ if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
11701
11691
  switch (shape.type) {
11702
11692
  case "grid": {
11703
11693
  const step = shape.step ?? 40;
@@ -11763,6 +11753,20 @@ function drawShape(ctx, shape, width, height) {
11763
11753
  ctx.stroke();
11764
11754
  break;
11765
11755
  }
11756
+ case "ellipse": {
11757
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11758
+ const startAngle = (shape.startAngle ?? 0) * Math.PI / 180;
11759
+ const endAngle = (shape.endAngle ?? 360) * Math.PI / 180;
11760
+ ctx.beginPath();
11761
+ ctx.ellipse(shape.x, shape.y, shape.width / 2, shape.height / 2, 0, startAngle, endAngle);
11762
+ if (fill) {
11763
+ ctx.fillStyle = fill;
11764
+ ctx.fill();
11765
+ }
11766
+ ctx.strokeStyle = stroke;
11767
+ ctx.stroke();
11768
+ break;
11769
+ }
11766
11770
  case "rect": {
11767
11771
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11768
11772
  if (fill) {
@@ -11809,21 +11813,153 @@ function drawShape(ctx, shape, width, height) {
11809
11813
  ctx.fillText(shape.text, shape.x, shape.y);
11810
11814
  break;
11811
11815
  }
11816
+ case "venn-region": {
11817
+ const resolveCircles = (ids) => (ids ?? []).flatMap((id) => {
11818
+ const c = allShapes.find((s) => s.type === "circle" && s.id === id);
11819
+ return c && c.x != null && c.y != null && c.radius != null ? [{ x: c.x, y: c.y, radius: c.radius }] : [];
11820
+ });
11821
+ const inside = resolveCircles(shape.inside);
11822
+ if (inside.length === 0) break;
11823
+ const outside = resolveCircles(shape.outside);
11824
+ const off = document.createElement("canvas");
11825
+ off.width = ctx.canvas.width;
11826
+ off.height = ctx.canvas.height;
11827
+ const octx = off.getContext("2d");
11828
+ if (!octx) break;
11829
+ octx.setTransform(ctx.getTransform());
11830
+ for (const c of inside) {
11831
+ const p = new Path2D();
11832
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11833
+ octx.clip(p);
11834
+ }
11835
+ octx.fillStyle = fill ?? stroke;
11836
+ octx.fillRect(0, 0, width, height);
11837
+ octx.globalCompositeOperation = "destination-out";
11838
+ for (const c of outside) {
11839
+ const p = new Path2D();
11840
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11841
+ octx.fill(p);
11842
+ }
11843
+ ctx.save();
11844
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
11845
+ ctx.drawImage(off, 0, 0);
11846
+ ctx.restore();
11847
+ break;
11848
+ }
11812
11849
  }
11813
11850
  ctx.restore();
11814
11851
  }
11815
- var LearningCanvas;
11852
+ function readoutShapes(readouts, width) {
11853
+ const out = [];
11854
+ const chipH = 18;
11855
+ const gap = 6;
11856
+ let rightEdge = width - 6;
11857
+ let rowY = 6;
11858
+ for (const readout of readouts) {
11859
+ const text = `${readout.label}: ${String(readout.value)}`;
11860
+ const chipW = Math.min(170, Math.max(34, text.length * 6 + 12));
11861
+ let chipX = rightEdge - chipW;
11862
+ if (chipX < 4) {
11863
+ rowY += chipH + 4;
11864
+ rightEdge = width - 6;
11865
+ chipX = rightEdge - chipW;
11866
+ }
11867
+ const color = readout.color ?? "#334155";
11868
+ out.push({ type: "rect", x: chipX, y: rowY, width: chipW, height: chipH, color, fill: color });
11869
+ out.push({
11870
+ type: "text",
11871
+ x: chipX + chipW / 2,
11872
+ y: rowY + chipH / 2,
11873
+ text,
11874
+ color: "#ffffff",
11875
+ fontSize: 10,
11876
+ align: "center"
11877
+ });
11878
+ rightEdge = chipX - gap;
11879
+ }
11880
+ return out;
11881
+ }
11882
+ function traceShapes(panel, k, width, height) {
11883
+ const w = panel.width ?? Math.round(width * 0.32);
11884
+ const h = panel.height ?? Math.round(height * 0.28);
11885
+ const x = panel.x ?? width - w - 8;
11886
+ const y = panel.y ?? height - h - 8 - k * (h + 8);
11887
+ const allSamples = panel.series.flatMap((series) => series.samples);
11888
+ let xLo = Math.min(...allSamples.map((p) => p.x));
11889
+ let xHi = Math.max(...allSamples.map((p) => p.x));
11890
+ let yLo = Math.min(...allSamples.map((p) => p.y));
11891
+ let yHi = Math.max(...allSamples.map((p) => p.y));
11892
+ if (xLo === xHi) {
11893
+ xLo -= 1;
11894
+ xHi += 1;
11895
+ }
11896
+ if (yLo === yHi) {
11897
+ yLo -= 1;
11898
+ yHi += 1;
11899
+ }
11900
+ const backgroundColor = panel.backgroundColor ?? "#ffffff";
11901
+ const frameColor = panel.frameColor ?? "#94a3b8";
11902
+ const out = [];
11903
+ out.push({
11904
+ type: "rect",
11905
+ x,
11906
+ y,
11907
+ width: w,
11908
+ height: h,
11909
+ color: backgroundColor,
11910
+ fill: backgroundColor,
11911
+ opacity: panel.backgroundOpacity ?? 0.85
11912
+ });
11913
+ out.push({ type: "rect", x, y, width: w, height: h, color: frameColor, lineWidth: 1 });
11914
+ panel.series.forEach((series, j) => {
11915
+ const color = series.color ?? TRACE_SERIES_COLORS[j % TRACE_SERIES_COLORS.length];
11916
+ const mapped = series.samples.map((p) => ({
11917
+ x: x + 4 + (p.x - xLo) / (xHi - xLo) * (w - 8),
11918
+ y: y + h - 4 - (p.y - yLo) / (yHi - yLo) * (h - 8)
11919
+ }));
11920
+ for (let i = 1; i < mapped.length; i++) {
11921
+ out.push({
11922
+ type: "line",
11923
+ x1: mapped[i - 1].x,
11924
+ y1: mapped[i - 1].y,
11925
+ x2: mapped[i].x,
11926
+ y2: mapped[i].y,
11927
+ color,
11928
+ lineWidth: 1.5
11929
+ });
11930
+ }
11931
+ if (mapped.length > 0) {
11932
+ const last = mapped[mapped.length - 1];
11933
+ out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
11934
+ }
11935
+ if (series.label) {
11936
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
11937
+ }
11938
+ });
11939
+ if (panel.yLabel) {
11940
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
11941
+ }
11942
+ if (panel.xLabel) {
11943
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
11944
+ }
11945
+ return out;
11946
+ }
11947
+ var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
11816
11948
  var init_LearningCanvas = __esm({
11817
11949
  "components/learning/atoms/LearningCanvas.tsx"() {
11818
11950
  "use client";
11819
11951
  init_cn();
11820
11952
  init_useEventBus();
11953
+ DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
11954
+ TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
11821
11955
  LearningCanvas = ({
11822
11956
  className,
11823
11957
  width = 600,
11824
11958
  height = 400,
11825
11959
  backgroundColor,
11826
11960
  shapes = [],
11961
+ readouts,
11962
+ traces,
11827
11963
  interactive = false,
11828
11964
  animate = false,
11829
11965
  onShapeClick,
@@ -11849,6 +11985,12 @@ var init_LearningCanvas = __esm({
11849
11985
  }
11850
11986
  return -1;
11851
11987
  }, [shapes]);
11988
+ const derivedShapes = useMemo(() => {
11989
+ if (!traces?.length && !readouts?.length) return shapes;
11990
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
11991
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
11992
+ return [...shapes, ...traceOut, ...readoutOut];
11993
+ }, [shapes, traces, readouts, width, height]);
11852
11994
  const draw = useCallback(() => {
11853
11995
  const canvas = canvasRef.current;
11854
11996
  if (!canvas) return;
@@ -11865,13 +12007,13 @@ var init_LearningCanvas = __esm({
11865
12007
  ctx.fillStyle = backgroundColor;
11866
12008
  ctx.fillRect(0, 0, width, height);
11867
12009
  }
11868
- for (const shape of shapes) {
11869
- if (shape.type !== "text") drawShape(ctx, shape, width, height);
12010
+ for (const shape of derivedShapes) {
12011
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
11870
12012
  }
11871
- for (const shape of shapes) {
11872
- if (shape.type === "text") drawShape(ctx, shape, width, height);
12013
+ for (const shape of derivedShapes) {
12014
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
11873
12015
  }
11874
- }, [width, height, backgroundColor, shapes]);
12016
+ }, [width, height, backgroundColor, derivedShapes]);
11875
12017
  useEffect(() => {
11876
12018
  draw();
11877
12019
  }, [draw]);
@@ -13421,7 +13563,363 @@ var init_ComponentPatterns = __esm({
13421
13563
  AlertPattern.displayName = "AlertPattern";
13422
13564
  }
13423
13565
  });
13424
- var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, AlgorithmCanvas;
13566
+ function layoutCircle(nodes, width, height) {
13567
+ const cx = width / 2;
13568
+ const cy = height / 2;
13569
+ const radius = Math.max(10, Math.min(cx, cy) - 40);
13570
+ const positions = /* @__PURE__ */ new Map();
13571
+ const n = nodes.length;
13572
+ nodes.forEach((node, i) => {
13573
+ const angle = 2 * Math.PI * i / Math.max(n, 1) - Math.PI / 2;
13574
+ positions.set(node.id, { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) });
13575
+ });
13576
+ return positions;
13577
+ }
13578
+ function layoutTree(nodes, edges, root, width, height) {
13579
+ const nodeIds = nodes.map((n) => n.id);
13580
+ const idSet = new Set(nodeIds);
13581
+ const childrenOf = /* @__PURE__ */ new Map();
13582
+ const hasIncoming = /* @__PURE__ */ new Set();
13583
+ for (const e of edges) {
13584
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
13585
+ const list = childrenOf.get(e.from) ?? [];
13586
+ list.push(e.to);
13587
+ childrenOf.set(e.from, list);
13588
+ hasIncoming.add(e.to);
13589
+ }
13590
+ const depth = /* @__PURE__ */ new Map();
13591
+ const treeChildren = /* @__PURE__ */ new Map();
13592
+ const visited = /* @__PURE__ */ new Set();
13593
+ const bfsFrom = (start) => {
13594
+ if (visited.has(start)) return;
13595
+ visited.add(start);
13596
+ depth.set(start, 0);
13597
+ const queue = [start];
13598
+ while (queue.length > 0) {
13599
+ const u = queue.shift();
13600
+ for (const v of childrenOf.get(u) ?? []) {
13601
+ if (visited.has(v)) continue;
13602
+ visited.add(v);
13603
+ depth.set(v, (depth.get(u) ?? 0) + 1);
13604
+ const list = treeChildren.get(u) ?? [];
13605
+ list.push(v);
13606
+ treeChildren.set(u, list);
13607
+ queue.push(v);
13608
+ }
13609
+ }
13610
+ };
13611
+ const primaryRoot = root && idSet.has(root) ? root : nodeIds.find((id) => !hasIncoming.has(id)) ?? nodeIds[0];
13612
+ const rootsOrder = [];
13613
+ if (primaryRoot !== void 0) {
13614
+ bfsFrom(primaryRoot);
13615
+ rootsOrder.push(primaryRoot);
13616
+ }
13617
+ for (const id of nodeIds) {
13618
+ if (!visited.has(id)) {
13619
+ bfsFrom(id);
13620
+ rootsOrder.push(id);
13621
+ }
13622
+ }
13623
+ let leafCounter = 0;
13624
+ const xSlot = /* @__PURE__ */ new Map();
13625
+ const assignXSlot = (u) => {
13626
+ const children = treeChildren.get(u) ?? [];
13627
+ if (children.length === 0) {
13628
+ const slot = leafCounter++;
13629
+ xSlot.set(u, slot);
13630
+ return slot;
13631
+ }
13632
+ const childSlots = children.map(assignXSlot);
13633
+ const avg = childSlots.reduce((a, b) => a + b, 0) / childSlots.length;
13634
+ xSlot.set(u, avg);
13635
+ return avg;
13636
+ };
13637
+ for (const r of rootsOrder) assignXSlot(r);
13638
+ let maxDepth = 0;
13639
+ for (const d of depth.values()) maxDepth = Math.max(maxDepth, d);
13640
+ const colWidth = width / Math.max(1, leafCounter);
13641
+ const rowHeight = height / (maxDepth + 1);
13642
+ const positions = /* @__PURE__ */ new Map();
13643
+ for (const id of nodeIds) {
13644
+ const slot = xSlot.get(id) ?? 0;
13645
+ const d = depth.get(id) ?? 0;
13646
+ positions.set(id, { x: slot * colWidth + colWidth / 2, y: d * rowHeight + rowHeight / 2 });
13647
+ }
13648
+ return positions;
13649
+ }
13650
+ function layoutLayered(nodes, edges, width, height) {
13651
+ const nodeIds = nodes.map((n) => n.id);
13652
+ const idSet = new Set(nodeIds);
13653
+ const adj = /* @__PURE__ */ new Map();
13654
+ const remainingIndegree = /* @__PURE__ */ new Map();
13655
+ for (const id of nodeIds) remainingIndegree.set(id, 0);
13656
+ for (const e of edges) {
13657
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
13658
+ const list = adj.get(e.from) ?? [];
13659
+ list.push(e.to);
13660
+ adj.set(e.from, list);
13661
+ remainingIndegree.set(e.to, (remainingIndegree.get(e.to) ?? 0) + 1);
13662
+ }
13663
+ const layer = /* @__PURE__ */ new Map();
13664
+ const dequeued = /* @__PURE__ */ new Set();
13665
+ const queue = [];
13666
+ for (const id of nodeIds) {
13667
+ if ((remainingIndegree.get(id) ?? 0) === 0) {
13668
+ layer.set(id, 0);
13669
+ queue.push(id);
13670
+ }
13671
+ }
13672
+ while (queue.length > 0) {
13673
+ const u = queue.shift();
13674
+ dequeued.add(u);
13675
+ for (const v of adj.get(u) ?? []) {
13676
+ const candidate = (layer.get(u) ?? 0) + 1;
13677
+ layer.set(v, Math.max(layer.get(v) ?? 0, candidate));
13678
+ remainingIndegree.set(v, (remainingIndegree.get(v) ?? 0) - 1);
13679
+ if ((remainingIndegree.get(v) ?? 0) === 0 && !dequeued.has(v)) {
13680
+ queue.push(v);
13681
+ }
13682
+ }
13683
+ }
13684
+ let baseMaxLayer = 0;
13685
+ for (const id of nodeIds) {
13686
+ if (dequeued.has(id)) baseMaxLayer = Math.max(baseMaxLayer, layer.get(id) ?? 0);
13687
+ }
13688
+ const cycleLayer = baseMaxLayer + 1;
13689
+ let maxLayer = baseMaxLayer;
13690
+ for (const id of nodeIds) {
13691
+ if (!dequeued.has(id)) {
13692
+ layer.set(id, cycleLayer);
13693
+ maxLayer = cycleLayer;
13694
+ }
13695
+ }
13696
+ const colWidth = width / Math.max(1, maxLayer + 1);
13697
+ const byLayer = /* @__PURE__ */ new Map();
13698
+ for (const id of nodeIds) {
13699
+ const l = layer.get(id) ?? 0;
13700
+ const list = byLayer.get(l) ?? [];
13701
+ list.push(id);
13702
+ byLayer.set(l, list);
13703
+ }
13704
+ const positions = /* @__PURE__ */ new Map();
13705
+ for (const [l, ids] of byLayer) {
13706
+ const rowHeight = height / ids.length;
13707
+ ids.forEach((id, i) => {
13708
+ positions.set(id, { x: l * colWidth + colWidth / 2, y: i * rowHeight + rowHeight / 2 });
13709
+ });
13710
+ }
13711
+ return positions;
13712
+ }
13713
+ function computePositions(nodes, edges, layout, root, width, height) {
13714
+ switch (layout) {
13715
+ case "circle":
13716
+ return layoutCircle(nodes, width, height);
13717
+ case "tree":
13718
+ return layoutTree(nodes, edges, root, width, height);
13719
+ case "layered":
13720
+ return layoutLayered(nodes, edges, width, height);
13721
+ case "manual":
13722
+ default: {
13723
+ const positions = /* @__PURE__ */ new Map();
13724
+ for (const n of nodes) positions.set(n.id, { x: n.x ?? 0, y: n.y ?? 0 });
13725
+ return positions;
13726
+ }
13727
+ }
13728
+ }
13729
+ var NODE_STATE_COLOR, EDGE_STATE_COLOR, DEFAULT_NODE_RADIUS, AlgoGraphCanvas;
13730
+ var init_AlgoGraphCanvas = __esm({
13731
+ "components/learning/molecules/AlgoGraphCanvas.tsx"() {
13732
+ "use client";
13733
+ init_atoms();
13734
+ init_Stack();
13735
+ init_LearningCanvas();
13736
+ NODE_STATE_COLOR = {
13737
+ unvisited: "#cbd5e1",
13738
+ frontier: "#f59e0b",
13739
+ current: "#ef4444",
13740
+ visited: "#22c55e",
13741
+ goal: "#8b5cf6",
13742
+ path: "#0ea5e9"
13743
+ };
13744
+ EDGE_STATE_COLOR = {
13745
+ default: "#9ca3af",
13746
+ tree: "#16a34a",
13747
+ relaxed: "#f59e0b",
13748
+ candidate: "#38bdf8",
13749
+ path: "#dc2626"
13750
+ };
13751
+ DEFAULT_NODE_RADIUS = 18;
13752
+ AlgoGraphCanvas = ({
13753
+ className,
13754
+ width = 600,
13755
+ height = 400,
13756
+ title,
13757
+ backgroundColor,
13758
+ nodes = [],
13759
+ edges = [],
13760
+ layout = "manual",
13761
+ root,
13762
+ shapes = [],
13763
+ interactive = false,
13764
+ animate = false,
13765
+ onShapeClick,
13766
+ onNodeClick,
13767
+ isLoading,
13768
+ error
13769
+ }) => {
13770
+ const nodeById = useMemo(() => {
13771
+ const m = /* @__PURE__ */ new Map();
13772
+ for (const n of nodes) m.set(n.id, n);
13773
+ return m;
13774
+ }, [nodes]);
13775
+ const nodeIndexById = useMemo(() => {
13776
+ const m = /* @__PURE__ */ new Map();
13777
+ nodes.forEach((n, i) => m.set(n.id, i));
13778
+ return m;
13779
+ }, [nodes]);
13780
+ const derivedShapes = useMemo(() => {
13781
+ const out = [];
13782
+ const positions = computePositions(nodes, edges, layout, root, width, height);
13783
+ const edgeGeoms = [];
13784
+ for (const e of edges) {
13785
+ const a = nodeById.get(e.from);
13786
+ const b = nodeById.get(e.to);
13787
+ const posA = positions.get(e.from);
13788
+ const posB = positions.get(e.to);
13789
+ if (!a || !b || !posA || !posB) continue;
13790
+ const rA = a.radius ?? DEFAULT_NODE_RADIUS;
13791
+ const rB = b.radius ?? DEFAULT_NODE_RADIUS;
13792
+ const dx = posB.x - posA.x;
13793
+ const dy = posB.y - posA.y;
13794
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
13795
+ const ux = dx / dist;
13796
+ const uy = dy / dist;
13797
+ edgeGeoms.push({
13798
+ directed: e.directed ?? false,
13799
+ x1: posA.x + ux * rA,
13800
+ y1: posA.y + uy * rA,
13801
+ x2: posB.x - ux * rB,
13802
+ y2: posB.y - uy * rB,
13803
+ color: e.color ?? EDGE_STATE_COLOR[e.state ?? "default"],
13804
+ label: e.label ?? (e.weight != null ? String(e.weight) : void 0)
13805
+ });
13806
+ }
13807
+ for (const g of edgeGeoms) {
13808
+ out.push({
13809
+ type: g.directed ? "arrow" : "line",
13810
+ x1: g.x1,
13811
+ y1: g.y1,
13812
+ x2: g.x2,
13813
+ y2: g.y2,
13814
+ color: g.color,
13815
+ lineWidth: 2
13816
+ });
13817
+ }
13818
+ for (const g of edgeGeoms) {
13819
+ if (g.label === void 0) continue;
13820
+ const midX = (g.x1 + g.x2) / 2;
13821
+ const midY = (g.y1 + g.y2) / 2;
13822
+ const dx = g.x2 - g.x1;
13823
+ const dy = g.y2 - g.y1;
13824
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
13825
+ const perpX = -(dy / dist);
13826
+ const perpY = dx / dist;
13827
+ out.push({
13828
+ type: "text",
13829
+ x: midX + perpX * 10,
13830
+ y: midY + perpY * 10,
13831
+ text: g.label,
13832
+ fontSize: 11,
13833
+ align: "center",
13834
+ color: "#374151"
13835
+ });
13836
+ }
13837
+ const nodeGeoms = [];
13838
+ for (const n of nodes) {
13839
+ const pos = positions.get(n.id);
13840
+ if (!pos) continue;
13841
+ nodeGeoms.push({
13842
+ id: n.id,
13843
+ x: pos.x,
13844
+ y: pos.y,
13845
+ radius: n.radius ?? DEFAULT_NODE_RADIUS,
13846
+ color: n.color ?? NODE_STATE_COLOR[n.state ?? "unvisited"],
13847
+ label: n.label,
13848
+ badge: n.badge
13849
+ });
13850
+ }
13851
+ for (const g of nodeGeoms) {
13852
+ out.push({ type: "circle", id: g.id, x: g.x, y: g.y, radius: g.radius, color: g.color, fill: `${g.color}33` });
13853
+ }
13854
+ const badgeGeoms = [];
13855
+ for (const g of nodeGeoms) {
13856
+ if (!g.badge) continue;
13857
+ const w = Math.min(42, Math.max(18, g.badge.text.length * 6 + 10));
13858
+ badgeGeoms.push({
13859
+ cx: g.x + g.radius * 0.75,
13860
+ cy: g.y - g.radius * 0.75,
13861
+ w,
13862
+ h: 14,
13863
+ // Borderless pill: same color drives both stroke and fill.
13864
+ color: g.badge.color ?? "#1e293b",
13865
+ text: g.badge.text
13866
+ });
13867
+ }
13868
+ for (const b of badgeGeoms) {
13869
+ 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 });
13870
+ }
13871
+ for (const b of badgeGeoms) {
13872
+ out.push({ type: "text", x: b.cx, y: b.cy, text: b.text, fontSize: 9, align: "center", color: "#ffffff" });
13873
+ }
13874
+ for (const g of nodeGeoms) {
13875
+ if (g.label === void 0) continue;
13876
+ out.push({
13877
+ type: "text",
13878
+ x: g.x,
13879
+ y: g.y + g.radius + 14,
13880
+ text: g.label,
13881
+ fontSize: 12,
13882
+ align: "center",
13883
+ color: "#111827"
13884
+ });
13885
+ }
13886
+ out.push(...shapes);
13887
+ return out;
13888
+ }, [nodes, edges, layout, root, width, height, nodeById, shapes]);
13889
+ const handleShapeClick = useCallback(
13890
+ (payload) => {
13891
+ if (payload.type === "circle" && payload.id) {
13892
+ const node = nodeById.get(payload.id);
13893
+ const idx = nodeIndexById.get(payload.id);
13894
+ if (node && idx !== void 0) {
13895
+ onNodeClick?.({ id: node.id, label: node.label, index: idx });
13896
+ }
13897
+ }
13898
+ onShapeClick?.(payload);
13899
+ },
13900
+ [nodeById, nodeIndexById, onNodeClick, onShapeClick]
13901
+ );
13902
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
13903
+ title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
13904
+ /* @__PURE__ */ jsx(
13905
+ LearningCanvas,
13906
+ {
13907
+ width,
13908
+ height,
13909
+ backgroundColor,
13910
+ shapes: derivedShapes,
13911
+ interactive,
13912
+ animate,
13913
+ onShapeClick: onShapeClick || onNodeClick ? handleShapeClick : void 0,
13914
+ isLoading,
13915
+ error
13916
+ }
13917
+ )
13918
+ ] }) });
13919
+ };
13920
+ }
13921
+ });
13922
+ 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;
13425
13923
  var init_AlgorithmCanvas = __esm({
13426
13924
  "components/learning/molecules/AlgorithmCanvas.tsx"() {
13427
13925
  "use client";
@@ -13433,6 +13931,43 @@ var init_AlgorithmCanvas = __esm({
13433
13931
  DEFAULT_POINTER_COLOR = "#dc2626";
13434
13932
  POINTER_BAND = 34;
13435
13933
  TOP_PAD = 26;
13934
+ PANEL_FAMILY_ORDER = ["bars", "slots", "cells", "buckets", "frames"];
13935
+ RANGE_COLOR_DEFAULT = "#3b82f6";
13936
+ RANGE_FILL_OPACITY = 0.15;
13937
+ BRACKET_TOP_OFFSET = 16;
13938
+ BRACKET_ROW_H = 14;
13939
+ BRACKET_TICK_H = 6;
13940
+ BRACKET_LABEL_OFFSET = 6;
13941
+ SLOT_EMPTY_FILL = "#f1f5f9";
13942
+ SLOT_EMPTY_STROKE = "#cbd5e1";
13943
+ SLOT_FILLED_STROKE = "#9ca3af";
13944
+ SLOT_HIGHLIGHT_DEFAULT = "#f59e0b";
13945
+ SLOT_VALUE_TEXT_COLOR = "#ffffff";
13946
+ FRAME_ACTIVE_COLOR = "#3b82f6";
13947
+ FRAME_RETURNING_COLOR = "#f59e0b";
13948
+ FRAME_DONE_COLOR = "#94a3b8";
13949
+ FRAME_LABEL_COLOR = "#ffffff";
13950
+ FRAME_DETAIL_COLOR = "#e2e8f0";
13951
+ FRAME_TWO_LINE_MIN_H = 22;
13952
+ BUCKET_INDEX_FILL = "#e2e8f0";
13953
+ BUCKET_INDEX_STROKE = "#9ca3af";
13954
+ BUCKET_INDEX_TEXT = "#374151";
13955
+ BUCKET_ENTRY_TEXT = "#ffffff";
13956
+ BUCKET_ENTRY_DEFAULT = "#3b82f6";
13957
+ BUCKET_ENTRY_HIGHLIGHT = "#f59e0b";
13958
+ BUCKET_ENTRY_PROBING = "#38bdf8";
13959
+ BUCKET_ENTRY_MIN_W = 24;
13960
+ BUCKET_ENTRY_MAX_W = 64;
13961
+ AXIS_LABEL_COLOR = "#6b7280";
13962
+ AXIS_LABEL_FONT_SIZE = 10;
13963
+ CORNER_TEXT_COLOR = "#111827";
13964
+ CORNER_FONT_SIZE = 7;
13965
+ CORNER_MIN_CELL = 28;
13966
+ CORNER_INSET_X = 3;
13967
+ CORNER_INSET_Y = 6;
13968
+ AUX_PRIMARY_RATIO = 0.6;
13969
+ AUX_LABEL_BAND = 18;
13970
+ AUX_BASELINE_PAD = 8;
13436
13971
  AlgorithmCanvas = ({
13437
13972
  className,
13438
13973
  width = 600,
@@ -13442,6 +13977,14 @@ var init_AlgorithmCanvas = __esm({
13442
13977
  bars = [],
13443
13978
  cells = [],
13444
13979
  pointers = [],
13980
+ ranges = [],
13981
+ slots = [],
13982
+ slotOrientation = "horizontal",
13983
+ frames = [],
13984
+ buckets = [],
13985
+ auxBars = [],
13986
+ rowLabels = [],
13987
+ colLabels = [],
13445
13988
  shapes = [],
13446
13989
  interactive = false,
13447
13990
  animate = false,
@@ -13451,12 +13994,35 @@ var init_AlgorithmCanvas = __esm({
13451
13994
  }) => {
13452
13995
  const derivedShapes = useMemo(() => {
13453
13996
  const out = [];
13997
+ const presence = {
13998
+ bars: bars.length > 0,
13999
+ slots: slots.length > 0,
14000
+ cells: cells.length > 0,
14001
+ buckets: buckets.length > 0,
14002
+ frames: frames.length > 0
14003
+ };
14004
+ const panelCount = PANEL_FAMILY_ORDER.filter((f3) => presence[f3]).length;
14005
+ const panelHeight = height / Math.max(1, panelCount);
14006
+ const panelY = { bars: 0, slots: 0, cells: 0, buckets: 0, frames: 0 };
14007
+ let compactIndex = 0;
14008
+ PANEL_FAMILY_ORDER.forEach((f3) => {
14009
+ if (presence[f3]) {
14010
+ panelY[f3] = compactIndex * panelHeight;
14011
+ compactIndex += 1;
14012
+ }
14013
+ });
13454
14014
  if (bars.length > 0) {
14015
+ const panelYBars = panelY.bars;
13455
14016
  const slot = width / bars.length;
13456
14017
  const barW = slot * 0.8;
13457
14018
  const gap = slot * 0.1;
13458
- const baseline = height - POINTER_BAND;
13459
- const usableH = baseline - TOP_PAD;
14019
+ const bracketRanges = ranges.filter((r) => r.kind === "bracket");
14020
+ const bracketCount = bracketRanges.length;
14021
+ const bracketHeadroom = bracketCount > 0 ? BRACKET_TOP_OFFSET + bracketCount * BRACKET_ROW_H : 0;
14022
+ const hasAux = auxBars.length > 0;
14023
+ const primaryH = hasAux ? panelHeight * AUX_PRIMARY_RATIO : panelHeight;
14024
+ const baseline = panelYBars + primaryH - POINTER_BAND;
14025
+ const usableH = baseline - (panelYBars + TOP_PAD + bracketHeadroom);
13460
14026
  const maxV = Math.max(1, ...bars.map((b) => Number.isFinite(b.value) ? b.value : 0));
13461
14027
  bars.forEach((bar, i) => {
13462
14028
  const v = Number.isFinite(bar.value) ? bar.value : 0;
@@ -13486,6 +14052,89 @@ var init_AlgorithmCanvas = __esm({
13486
14052
  });
13487
14053
  }
13488
14054
  });
14055
+ ranges.forEach((r) => {
14056
+ const kind = r.kind ?? "fill";
14057
+ if (kind !== "fill") return;
14058
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14059
+ out.push({
14060
+ type: "rect",
14061
+ x: r.from * slot,
14062
+ y: panelYBars,
14063
+ width: (r.to - r.from + 1) * slot,
14064
+ height: primaryH,
14065
+ color,
14066
+ fill: color,
14067
+ opacity: RANGE_FILL_OPACITY
14068
+ });
14069
+ if (r.label) {
14070
+ out.push({
14071
+ type: "text",
14072
+ x: r.from * slot + 4,
14073
+ // Sits below the bracket block (if any) so fill and bracket labels never collide.
14074
+ y: panelYBars + 10 + bracketHeadroom,
14075
+ text: r.label,
14076
+ color,
14077
+ fontSize: 10,
14078
+ align: "left"
14079
+ });
14080
+ }
14081
+ });
14082
+ bracketRanges.forEach((r, i) => {
14083
+ const bracketY = panelYBars + BRACKET_TOP_OFFSET + i * BRACKET_ROW_H;
14084
+ const x1 = r.from * slot + slot * 0.1;
14085
+ const x2 = (r.to + 1) * slot - slot * 0.1;
14086
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14087
+ out.push({ type: "line", x1, y1: bracketY, x2, y2: bracketY, color, lineWidth: 2 });
14088
+ out.push({ type: "line", x1, y1: bracketY, x2: x1, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14089
+ out.push({ type: "line", x1: x2, y1: bracketY, x2, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14090
+ if (r.label) {
14091
+ out.push({
14092
+ type: "text",
14093
+ x: (x1 + x2) / 2,
14094
+ y: bracketY - BRACKET_LABEL_OFFSET,
14095
+ text: r.label,
14096
+ color,
14097
+ fontSize: 10,
14098
+ align: "center"
14099
+ });
14100
+ }
14101
+ });
14102
+ if (hasAux) {
14103
+ const auxH = panelHeight - primaryH;
14104
+ const slot2 = width / auxBars.length;
14105
+ const auxBaseline = panelYBars + primaryH + auxH - AUX_BASELINE_PAD;
14106
+ const auxUsableH = auxBaseline - (panelYBars + primaryH + AUX_LABEL_BAND);
14107
+ const maxAuxV = Math.max(1, ...auxBars.map((b) => Number.isFinite(b.value) ? b.value : 0));
14108
+ auxBars.forEach((bar, i) => {
14109
+ const v = Number.isFinite(bar.value) ? bar.value : 0;
14110
+ const bh = Math.max(0, v / maxAuxV * auxUsableH);
14111
+ const x = i * slot2 + slot2 * 0.1;
14112
+ const w = slot2 * 0.8;
14113
+ const color = bar.color ?? DEFAULT_BAR_COLOR;
14114
+ out.push({
14115
+ type: "rect",
14116
+ id: `auxbar-${i}`,
14117
+ x,
14118
+ y: auxBaseline - bh,
14119
+ width: w,
14120
+ height: bh,
14121
+ color,
14122
+ fill: color
14123
+ });
14124
+ const label = bar.label ?? (auxBars.length <= 24 ? String(v) : void 0);
14125
+ if (label) {
14126
+ out.push({
14127
+ type: "text",
14128
+ x: x + w / 2,
14129
+ y: auxBaseline - bh - 8,
14130
+ text: label,
14131
+ color: "#374151",
14132
+ fontSize: 11,
14133
+ align: "center"
14134
+ });
14135
+ }
14136
+ });
14137
+ }
13489
14138
  pointers.forEach((p) => {
13490
14139
  if (p.index < 0 || p.index >= bars.length) return;
13491
14140
  const cx = p.index * slot + slot / 2;
@@ -13493,7 +14142,7 @@ var init_AlgorithmCanvas = __esm({
13493
14142
  out.push({
13494
14143
  type: "arrow",
13495
14144
  x1: cx,
13496
- y1: height - 6,
14145
+ y1: panelYBars + primaryH - 18,
13497
14146
  x2: cx,
13498
14147
  y2: baseline + 4,
13499
14148
  color,
@@ -13503,7 +14152,7 @@ var init_AlgorithmCanvas = __esm({
13503
14152
  out.push({
13504
14153
  type: "text",
13505
14154
  x: cx,
13506
- y: height - 22,
14155
+ y: panelYBars + primaryH - 8,
13507
14156
  text: p.label,
13508
14157
  color,
13509
14158
  fontSize: 11,
@@ -13512,14 +14161,111 @@ var init_AlgorithmCanvas = __esm({
13512
14161
  }
13513
14162
  });
13514
14163
  }
14164
+ if (slots.length > 0) {
14165
+ const panelYSlots = panelY.slots;
14166
+ const n = slots.length;
14167
+ const vertical = slotOrientation === "vertical";
14168
+ const vBoxH = panelHeight / n;
14169
+ const vBoxW = Math.min(width * 0.5, 120);
14170
+ const vBoxX = (width - vBoxW) / 2;
14171
+ const hCellW = width / n;
14172
+ const hBoxW = hCellW * 0.82;
14173
+ const hBoxH = Math.min(panelHeight * 0.6, 48);
14174
+ const hBoxY = panelYSlots + (panelHeight - hBoxH) / 2;
14175
+ 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 };
14176
+ slots.forEach((s, i) => {
14177
+ const box = slotBox(i);
14178
+ const state = s.state ?? "filled";
14179
+ const fill = state === "empty" ? SLOT_EMPTY_FILL : state === "highlight" ? s.color ?? SLOT_HIGHLIGHT_DEFAULT : s.color ?? DEFAULT_BAR_COLOR;
14180
+ const stroke = state === "empty" ? SLOT_EMPTY_STROKE : SLOT_FILLED_STROKE;
14181
+ out.push({
14182
+ type: "rect",
14183
+ id: `slot-${i}`,
14184
+ x: box.x,
14185
+ y: box.y,
14186
+ width: box.width,
14187
+ height: box.height,
14188
+ color: stroke,
14189
+ fill
14190
+ });
14191
+ if (s.value != null && state !== "empty") {
14192
+ out.push({
14193
+ type: "text",
14194
+ x: box.x + box.width / 2,
14195
+ y: box.y + box.height / 2,
14196
+ text: String(s.value),
14197
+ color: SLOT_VALUE_TEXT_COLOR,
14198
+ fontSize: 12,
14199
+ align: "center"
14200
+ });
14201
+ }
14202
+ });
14203
+ if (bars.length === 0) {
14204
+ pointers.forEach((p) => {
14205
+ if (p.index < 0 || p.index >= slots.length) return;
14206
+ const box = slotBox(p.index);
14207
+ const color = p.color ?? DEFAULT_POINTER_COLOR;
14208
+ if (vertical) {
14209
+ const cy = box.y + box.height / 2;
14210
+ out.push({
14211
+ type: "arrow",
14212
+ x1: box.x + box.width + 34,
14213
+ y1: cy,
14214
+ x2: box.x + box.width + 4,
14215
+ y2: cy,
14216
+ color,
14217
+ lineWidth: 2
14218
+ });
14219
+ if (p.label) {
14220
+ out.push({
14221
+ type: "text",
14222
+ x: box.x + box.width + 38,
14223
+ y: cy,
14224
+ text: p.label,
14225
+ color,
14226
+ fontSize: 11,
14227
+ align: "left"
14228
+ });
14229
+ }
14230
+ } else {
14231
+ const cx = box.x + box.width / 2;
14232
+ out.push({
14233
+ type: "arrow",
14234
+ x1: cx,
14235
+ y1: panelYSlots + panelHeight - 18,
14236
+ x2: cx,
14237
+ y2: box.y + box.height + 4,
14238
+ color,
14239
+ lineWidth: 2
14240
+ });
14241
+ if (p.label) {
14242
+ out.push({
14243
+ type: "text",
14244
+ x: cx,
14245
+ y: panelYSlots + panelHeight - 8,
14246
+ text: p.label,
14247
+ color,
14248
+ fontSize: 11,
14249
+ align: "center"
14250
+ });
14251
+ }
14252
+ }
14253
+ });
14254
+ }
14255
+ }
13515
14256
  if (cells.length > 0) {
14257
+ const panelYCells = panelY.cells;
13516
14258
  const maxCol = Math.max(0, ...cells.map((c) => c.col)) + 1;
13517
14259
  const maxRow = Math.max(0, ...cells.map((c) => c.row)) + 1;
13518
- const cw = width / maxCol;
13519
- const ch = height / maxRow;
14260
+ const colLabelH = colLabels.length > 0 ? 16 : 0;
14261
+ const rowLabelW = rowLabels.length > 0 ? 20 : 0;
14262
+ const gridX0 = rowLabelW;
14263
+ const gridY0 = panelYCells + colLabelH;
14264
+ const cw = (width - rowLabelW) / maxCol;
14265
+ const ch = (panelHeight - colLabelH) / maxRow;
13520
14266
  cells.forEach((c, i) => {
13521
- const x = c.col * cw;
13522
- const y = c.row * ch;
14267
+ const x = gridX0 + c.col * cw;
14268
+ const y = gridY0 + c.row * ch;
13523
14269
  const color = c.color ?? DEFAULT_CELL_COLOR;
13524
14270
  out.push({
13525
14271
  type: "rect",
@@ -13543,11 +14289,207 @@ var init_AlgorithmCanvas = __esm({
13543
14289
  align: "center"
13544
14290
  });
13545
14291
  }
14292
+ if (c.corner && cw >= CORNER_MIN_CELL && ch >= CORNER_MIN_CELL) {
14293
+ const { tl, tr, bl, br } = c.corner;
14294
+ if (tl) {
14295
+ out.push({
14296
+ type: "text",
14297
+ x: x + CORNER_INSET_X,
14298
+ y: y + CORNER_INSET_Y,
14299
+ text: tl,
14300
+ color: CORNER_TEXT_COLOR,
14301
+ fontSize: CORNER_FONT_SIZE,
14302
+ align: "left"
14303
+ });
14304
+ }
14305
+ if (tr) {
14306
+ out.push({
14307
+ type: "text",
14308
+ x: x + cw - CORNER_INSET_X,
14309
+ y: y + CORNER_INSET_Y,
14310
+ text: tr,
14311
+ color: CORNER_TEXT_COLOR,
14312
+ fontSize: CORNER_FONT_SIZE,
14313
+ align: "right"
14314
+ });
14315
+ }
14316
+ if (bl) {
14317
+ out.push({
14318
+ type: "text",
14319
+ x: x + CORNER_INSET_X,
14320
+ y: y + ch - CORNER_INSET_Y,
14321
+ text: bl,
14322
+ color: CORNER_TEXT_COLOR,
14323
+ fontSize: CORNER_FONT_SIZE,
14324
+ align: "left"
14325
+ });
14326
+ }
14327
+ if (br) {
14328
+ out.push({
14329
+ type: "text",
14330
+ x: x + cw - CORNER_INSET_X,
14331
+ y: y + ch - CORNER_INSET_Y,
14332
+ text: br,
14333
+ color: CORNER_TEXT_COLOR,
14334
+ fontSize: CORNER_FONT_SIZE,
14335
+ align: "right"
14336
+ });
14337
+ }
14338
+ }
14339
+ });
14340
+ colLabels.forEach((l) => {
14341
+ out.push({
14342
+ type: "text",
14343
+ x: gridX0 + l.index * cw + cw / 2,
14344
+ y: panelYCells + colLabelH / 2,
14345
+ text: l.text,
14346
+ color: l.color ?? AXIS_LABEL_COLOR,
14347
+ fontSize: AXIS_LABEL_FONT_SIZE,
14348
+ align: "center"
14349
+ });
14350
+ });
14351
+ rowLabels.forEach((l) => {
14352
+ out.push({
14353
+ type: "text",
14354
+ x: rowLabelW - 6,
14355
+ y: gridY0 + l.index * ch + ch / 2,
14356
+ text: l.text,
14357
+ color: l.color ?? AXIS_LABEL_COLOR,
14358
+ fontSize: AXIS_LABEL_FONT_SIZE,
14359
+ align: "right"
14360
+ });
14361
+ });
14362
+ }
14363
+ if (buckets.length > 0) {
14364
+ const panelYBuckets = panelY.buckets;
14365
+ const bucketCount = Math.max(0, ...buckets.map((b) => b.index)) + 1;
14366
+ const rowH = panelHeight / bucketCount;
14367
+ const indexColW = Math.min(width * 0.12, 40);
14368
+ const maxChainLen = Math.max(1, ...buckets.map((b) => b.entries.length));
14369
+ const entryW = Math.min(BUCKET_ENTRY_MAX_W, Math.max(BUCKET_ENTRY_MIN_W, (width - indexColW - 8) / maxChainLen));
14370
+ const maxVisible = Math.floor((width - indexColW - 4) / entryW);
14371
+ buckets.forEach((b) => {
14372
+ const rowY = panelYBuckets + b.index * rowH;
14373
+ out.push({
14374
+ type: "rect",
14375
+ id: `bucket-index-${b.index}`,
14376
+ x: 2,
14377
+ y: rowY + 2,
14378
+ width: indexColW - 4,
14379
+ height: rowH - 4,
14380
+ color: BUCKET_INDEX_STROKE,
14381
+ fill: BUCKET_INDEX_FILL
14382
+ });
14383
+ out.push({
14384
+ type: "text",
14385
+ x: 2 + (indexColW - 4) / 2,
14386
+ y: rowY + rowH / 2,
14387
+ text: String(b.index),
14388
+ color: BUCKET_INDEX_TEXT,
14389
+ fontSize: 10,
14390
+ align: "center"
14391
+ });
14392
+ const overflow = b.entries.length > maxVisible;
14393
+ const visibleCount = overflow ? Math.max(0, maxVisible - 1) : b.entries.length;
14394
+ for (let j = 0; j < visibleCount; j++) {
14395
+ const entry = b.entries[j];
14396
+ const ex = indexColW + 4 + j * entryW;
14397
+ const state = entry.state ?? "default";
14398
+ const fill = state === "highlight" ? entry.color ?? BUCKET_ENTRY_HIGHLIGHT : state === "probing" ? entry.color ?? BUCKET_ENTRY_PROBING : entry.color ?? BUCKET_ENTRY_DEFAULT;
14399
+ out.push({
14400
+ type: "rect",
14401
+ id: `bucket-${b.index}-${j}`,
14402
+ x: ex,
14403
+ y: rowY + 2,
14404
+ width: entryW - 2,
14405
+ height: rowH - 4,
14406
+ color: fill,
14407
+ fill
14408
+ });
14409
+ if (entryW >= 20 && rowH >= 16) {
14410
+ out.push({
14411
+ type: "text",
14412
+ x: ex + (entryW - 2) / 2,
14413
+ y: rowY + rowH / 2,
14414
+ text: entry.label,
14415
+ color: BUCKET_ENTRY_TEXT,
14416
+ fontSize: 10,
14417
+ align: "center"
14418
+ });
14419
+ }
14420
+ }
14421
+ if (overflow) {
14422
+ const ex = indexColW + 4 + visibleCount * entryW;
14423
+ out.push({
14424
+ type: "rect",
14425
+ id: `bucket-${b.index}-overflow`,
14426
+ x: ex,
14427
+ y: rowY + 2,
14428
+ width: entryW - 2,
14429
+ height: rowH - 4,
14430
+ color: BUCKET_ENTRY_DEFAULT,
14431
+ fill: BUCKET_ENTRY_DEFAULT
14432
+ });
14433
+ out.push({
14434
+ type: "text",
14435
+ x: ex + (entryW - 2) / 2,
14436
+ y: rowY + rowH / 2,
14437
+ text: `+${b.entries.length - visibleCount}`,
14438
+ color: BUCKET_ENTRY_TEXT,
14439
+ fontSize: 10,
14440
+ align: "center"
14441
+ });
14442
+ }
14443
+ });
14444
+ }
14445
+ if (frames.length > 0) {
14446
+ const panelYFrames = panelY.frames;
14447
+ const n = frames.length;
14448
+ const frameH = panelHeight / n;
14449
+ const x = 8;
14450
+ const w = width - 16;
14451
+ frames.forEach((f3, i) => {
14452
+ const y = panelYFrames + panelHeight - (i + 1) * frameH;
14453
+ const state = f3.state ?? "active";
14454
+ const fill = state === "returning" ? f3.color ?? FRAME_RETURNING_COLOR : state === "done" ? f3.color ?? FRAME_DONE_COLOR : f3.color ?? FRAME_ACTIVE_COLOR;
14455
+ out.push({ type: "rect", id: `frame-${i}`, x, y, width: w, height: frameH, color: fill, fill });
14456
+ if (frameH >= FRAME_TWO_LINE_MIN_H) {
14457
+ out.push({
14458
+ type: "text",
14459
+ x: 16,
14460
+ y: y + frameH * 0.35,
14461
+ text: f3.label,
14462
+ color: FRAME_LABEL_COLOR,
14463
+ fontSize: 10,
14464
+ align: "left"
14465
+ });
14466
+ if (f3.detail) {
14467
+ out.push({
14468
+ type: "text",
14469
+ x: 16,
14470
+ y: y + frameH * 0.7,
14471
+ text: f3.detail,
14472
+ color: FRAME_DETAIL_COLOR,
14473
+ fontSize: 10,
14474
+ align: "left"
14475
+ });
14476
+ }
14477
+ } else {
14478
+ out.push({
14479
+ type: "text",
14480
+ x: 16,
14481
+ y: y + frameH / 2,
14482
+ text: f3.label,
14483
+ color: FRAME_LABEL_COLOR,
14484
+ fontSize: 10,
14485
+ align: "left"
14486
+ });
14487
+ }
13546
14488
  });
13547
14489
  }
13548
14490
  out.push(...shapes);
13549
14491
  return out;
13550
- }, [bars, cells, pointers, shapes, width, height]);
14492
+ }, [bars, cells, pointers, ranges, slots, slotOrientation, frames, buckets, auxBars, rowLabels, colLabels, shapes, width, height]);
13551
14493
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
13552
14494
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
13553
14495
  /* @__PURE__ */ jsx(
@@ -14536,23 +15478,422 @@ var init_BehaviorView = __esm({
14536
15478
  BehaviorView.displayName = "BehaviorView";
14537
15479
  }
14538
15480
  });
14539
- var BiologyCanvas;
15481
+ function LearningScene3D({
15482
+ className,
15483
+ width = 600,
15484
+ height = 400,
15485
+ title,
15486
+ backgroundColor,
15487
+ drawables,
15488
+ camera,
15489
+ lighting,
15490
+ post,
15491
+ showGrid = false,
15492
+ shadows,
15493
+ interactive,
15494
+ isLoading,
15495
+ error,
15496
+ onItemClick
15497
+ }) {
15498
+ const instanceId = useId().replace(/[^a-zA-Z0-9]/g, "");
15499
+ const clickEvent = onItemClick ? `LEARNING_SCENE_3D.ITEM_CLICK.${instanceId}` : void 0;
15500
+ const onItemClickRef = useRef(onItemClick);
15501
+ onItemClickRef.current = onItemClick;
15502
+ useEventListener(`UI:${clickEvent ?? "LEARNING_SCENE_3D.ITEM_CLICK.__disabled"}`, (event) => {
15503
+ const unitId = event.payload?.unitId;
15504
+ if (typeof unitId === "string") onItemClickRef.current?.(unitId);
15505
+ });
15506
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && post?.bloom) {
15507
+ const unknownKeys = Object.keys(post.bloom).filter((k) => !KNOWN_BLOOM_KEYS.has(k));
15508
+ if (unknownKeys.length > 0) {
15509
+ sceneLog.debug("post.bloom has unrecognized keys \u2014 only intensity/threshold/smoothing are read", { unknownKeys });
15510
+ }
15511
+ }
15512
+ const props3d = {
15513
+ drawables,
15514
+ isLoading,
15515
+ error: error ? error.message : null,
15516
+ cameraMode: camera?.mode ?? "perspective",
15517
+ ...camera?.zoom !== void 0 ? { zoom: camera.zoom } : {},
15518
+ ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
15519
+ ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
15520
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
15521
+ ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
15522
+ backgroundColor,
15523
+ showGrid,
15524
+ ...shadows !== void 0 ? { shadows } : {},
15525
+ ...interactive !== void 0 ? { controlsEnabled: interactive } : {},
15526
+ lighting,
15527
+ post,
15528
+ ...clickEvent !== void 0 ? { unitClickEvent: clickEvent } : {}
15529
+ };
15530
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
15531
+ title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
15532
+ /* @__PURE__ */ jsx("div", { style: { width, height, display: "flex" }, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(Canvas3DHost2, { ...props3d }) }) })
15533
+ ] }) });
15534
+ }
15535
+ function meshSphere(id, x, y, z, radius, color, opts) {
15536
+ return {
15537
+ type: "draw-mesh",
15538
+ ...id !== void 0 ? { id } : {},
15539
+ shape: opts?.shape ?? "sphere",
15540
+ position: { x, y, z },
15541
+ radius,
15542
+ pivot: "center",
15543
+ segments: opts?.segments ?? 24,
15544
+ material: { color, ...opts?.material },
15545
+ ...opts?.opacity !== void 0 ? { opacity: opts.opacity } : {}
15546
+ };
15547
+ }
15548
+ function axisRotation(from, to) {
15549
+ const wx = to[0] - from[0];
15550
+ const wy = to[2] - from[2];
15551
+ const wz = to[1] - from[1];
15552
+ const len = Math.sqrt(wx * wx + wy * wy + wz * wz);
15553
+ const tilt = Math.acos(Math.min(1, Math.max(-1, len > 0 ? wy / len : 1)));
15554
+ return [0, Math.atan2(wz, -wx), tilt];
15555
+ }
15556
+ function segmentLength(from, to) {
15557
+ const dx = to[0] - from[0];
15558
+ const dy = to[1] - from[1];
15559
+ const dz = to[2] - from[2];
15560
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
15561
+ }
15562
+ function cylinderBetween(from, to, radius, color) {
15563
+ const len = segmentLength(from, to);
15564
+ if (len < 1e-6) return null;
15565
+ return {
15566
+ type: "draw-mesh",
15567
+ shape: "cylinder",
15568
+ position: { x: (from[0] + to[0]) / 2, y: (from[1] + to[1]) / 2, z: (from[2] + to[2]) / 2 },
15569
+ radius,
15570
+ height: len,
15571
+ rotation: axisRotation(from, to),
15572
+ pivot: "center",
15573
+ segments: 12,
15574
+ material: { color }
15575
+ };
15576
+ }
15577
+ function arrowBetween(from, to, color, shaftRadius = 0.08, id) {
15578
+ const len = segmentLength(from, to);
15579
+ if (len < 1e-6) return null;
15580
+ const tipLen = Math.min(shaftRadius * 8, len * 0.35);
15581
+ const k = (len - tipLen) / len;
15582
+ const delta = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
15583
+ const shaftEnd = [delta[0] * k, delta[1] * k, delta[2] * k];
15584
+ const tipStart = shaftEnd;
15585
+ const tipEnd = delta;
15586
+ const shaft = cylinderBetween([0, 0, 0], shaftEnd, shaftRadius, color);
15587
+ const tipLenActual = segmentLength(tipStart, tipEnd);
15588
+ const tip = {
15589
+ type: "draw-mesh",
15590
+ shape: "cone",
15591
+ position: {
15592
+ x: (tipStart[0] + tipEnd[0]) / 2,
15593
+ y: (tipStart[1] + tipEnd[1]) / 2,
15594
+ z: (tipStart[2] + tipEnd[2]) / 2
15595
+ },
15596
+ radius: shaftRadius * 3,
15597
+ height: tipLenActual,
15598
+ rotation: axisRotation(tipStart, tipEnd),
15599
+ pivot: "center",
15600
+ segments: 12,
15601
+ material: { color }
15602
+ };
15603
+ return {
15604
+ type: "draw-group",
15605
+ ...id !== void 0 ? { id } : {},
15606
+ position: { x: from[0], y: from[1], z: from[2] },
15607
+ items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
15608
+ };
15609
+ }
15610
+ function billboardLabel(text, x, y, z, opts) {
15611
+ return {
15612
+ type: "draw-text",
15613
+ text,
15614
+ position: { x, y, z },
15615
+ color: opts?.color ?? "#111827"
15616
+ };
15617
+ }
15618
+ function labelColorForBackground(backgroundColor) {
15619
+ const m = /^#(?:([0-9a-f]{3})|([0-9a-f]{6}))$/i.exec(backgroundColor ?? "");
15620
+ if (!m) return "#111827";
15621
+ const hex = m[1] !== void 0 ? m[1].split("").map((c) => c + c).join("") : m[2];
15622
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
15623
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
15624
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
15625
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
15626
+ return luminance < 0.5 ? "#e5e7eb" : "#111827";
15627
+ }
15628
+ function get3DClickPayload(onShapeClick, idToIndex) {
15629
+ if (!onShapeClick) return void 0;
15630
+ return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
15631
+ }
15632
+ function polylineTube(points, radius, color, opts) {
15633
+ const maxSegments = opts?.maxSegments ?? 128;
15634
+ let pts = points;
15635
+ if (pts.length - 1 > maxSegments) {
15636
+ const step = (pts.length - 1) / maxSegments;
15637
+ const kept = [pts[0]];
15638
+ for (let s = 1; s < maxSegments; s++) kept.push(pts[Math.round(s * step)]);
15639
+ kept.push(pts[pts.length - 1]);
15640
+ pts = kept;
15641
+ }
15642
+ const out = [];
15643
+ for (let i = 0; i < pts.length - 1; i++) {
15644
+ const seg = cylinderBetween(pts[i], pts[i + 1], radius, color);
15645
+ if (seg) out.push(opts?.opacity !== void 0 ? { ...seg, opacity: opts.opacity } : seg);
15646
+ }
15647
+ return out;
15648
+ }
15649
+ function heightFieldMesh(spec) {
15650
+ const { nx, ny, heights, spacing = 1, x = 0, y = 0 } = spec;
15651
+ const flatShading = spec.flatShading ?? true;
15652
+ const vertices = [];
15653
+ for (let iy = 0; iy < ny; iy++) {
15654
+ for (let ix = 0; ix < nx; ix++) {
15655
+ vertices.push([
15656
+ x + (ix - (nx - 1) / 2) * spacing,
15657
+ y + (iy - (ny - 1) / 2) * spacing,
15658
+ heights[iy * nx + ix] ?? 0
15659
+ ]);
15660
+ }
15661
+ }
15662
+ const bands = [...spec.bands ?? []].sort((a, b) => (a.min ?? -Infinity) - (b.min ?? -Infinity));
15663
+ const facesByBand = /* @__PURE__ */ new Map();
15664
+ for (let iy = 0; iy < ny - 1; iy++) {
15665
+ for (let ix = 0; ix < nx - 1; ix++) {
15666
+ const v00 = iy * nx + ix;
15667
+ const v10 = iy * nx + ix + 1;
15668
+ const v01 = (iy + 1) * nx + ix;
15669
+ const v11 = (iy + 1) * nx + ix + 1;
15670
+ for (const face of [[v00, v10, v01], [v10, v11, v01]]) {
15671
+ const centroid = (vertices[face[0]][2] + vertices[face[1]][2] + vertices[face[2]][2]) / 3;
15672
+ let band = null;
15673
+ for (const b of bands) {
15674
+ if ((b.min ?? -Infinity) <= centroid) band = b;
15675
+ }
15676
+ const key = bands.length > 0 ? band : null;
15677
+ const list = facesByBand.get(key) ?? [];
15678
+ list.push(face);
15679
+ facesByBand.set(key, list);
15680
+ }
15681
+ }
15682
+ }
15683
+ const out = [];
15684
+ for (const [band, faces] of facesByBand) {
15685
+ if (faces.length === 0) continue;
15686
+ out.push({
15687
+ type: "draw-mesh",
15688
+ shape: "polyhedron",
15689
+ position: { x: 0, y: 0, z: 0 },
15690
+ vertices,
15691
+ faces,
15692
+ pivot: "center",
15693
+ material: { color: band?.color ?? spec.color ?? "#64748b", flatShading, side: "double" },
15694
+ ...spec.opacity !== void 0 ? { opacity: spec.opacity } : {}
15695
+ });
15696
+ }
15697
+ return out;
15698
+ }
15699
+ function arrowField(vectors, opts) {
15700
+ const scale = opts?.scale ?? 1;
15701
+ const out = [];
15702
+ for (const v of vectors) {
15703
+ const to = [
15704
+ v.from[0] + v.delta[0] * scale,
15705
+ v.from[1] + v.delta[1] * scale,
15706
+ v.from[2] + v.delta[2] * scale
15707
+ ];
15708
+ const arrow = arrowBetween(v.from, to, v.color ?? "#dc2626", v.width, v.id);
15709
+ if (arrow) out.push(arrow);
15710
+ if (v.label) out.push(billboardLabel(v.label, to[0], to[1], to[2], { color: opts?.labelColor }));
15711
+ }
15712
+ return out;
15713
+ }
15714
+ function helixDrawables(spec, opts) {
15715
+ const count = spec.count ?? spec.rungs?.length ?? 0;
15716
+ const rungs = Array.from({ length: count }, (_, i) => spec.rungs?.[i] ?? {});
15717
+ const radius = spec.radius ?? 1;
15718
+ const rise = spec.rise ?? 0.34;
15719
+ const twistRad = (spec.twistDeg ?? 36) * (Math.PI / 180);
15720
+ const strandAColor = spec.strandAColor ?? "#38bdf8";
15721
+ const strandBColor = spec.strandBColor ?? "#fb923c";
15722
+ const backboneRadius = spec.backboneRadius ?? 0.16;
15723
+ const rungRadius = spec.rungRadius ?? 0.12;
15724
+ const cx = spec.x ?? 0;
15725
+ const cy = spec.y ?? 0;
15726
+ const cz = spec.z ?? 0;
15727
+ const unwoundCount = spec.unwoundCount ?? 0;
15728
+ const unwindSpread = spec.unwindSpread ?? 1.8;
15729
+ const strandA = [];
15730
+ const strandB = [];
15731
+ for (let i = 0; i < count; i++) {
15732
+ const yi = cy + (i - (count - 1) / 2) * rise;
15733
+ const theta = i * twistRad;
15734
+ const s = i < unwoundCount ? unwindSpread : 1;
15735
+ strandA.push([cx + s * radius * Math.cos(theta), yi, cz + s * radius * Math.sin(theta)]);
15736
+ strandB.push([cx + s * radius * Math.cos(theta + Math.PI), yi, cz + s * radius * Math.sin(theta + Math.PI)]);
15737
+ }
15738
+ const out = [];
15739
+ for (let i = 0; i < count; i++) {
15740
+ out.push(meshSphere(`hx-a-${i}`, strandA[i][0], strandA[i][1], strandA[i][2], backboneRadius, strandAColor));
15741
+ out.push(meshSphere(`hx-b-${i}`, strandB[i][0], strandB[i][1], strandB[i][2], backboneRadius, strandBColor));
15742
+ if (i > 0) {
15743
+ const segA = cylinderBetween(strandA[i - 1], strandA[i], backboneRadius, strandAColor);
15744
+ if (segA) out.push(segA);
15745
+ const segB = cylinderBetween(strandB[i - 1], strandB[i], backboneRadius, strandBColor);
15746
+ if (segB) out.push(segB);
15747
+ }
15748
+ const rung = rungs[i];
15749
+ const rungColor = rung.color ?? "#94a3b8";
15750
+ const rod = cylinderBetween(strandA[i], strandB[i], rungRadius, rungColor);
15751
+ if (rod) out.push(rod);
15752
+ const mid = [
15753
+ (strandA[i][0] + strandB[i][0]) / 2,
15754
+ (strandA[i][1] + strandB[i][1]) / 2,
15755
+ (strandA[i][2] + strandB[i][2]) / 2
15756
+ ];
15757
+ const markerRadius = rung.radius ?? rungRadius;
15758
+ out.push(meshSphere(rung.id, mid[0], mid[1], mid[2], markerRadius, rungColor));
15759
+ if (rung.label) out.push(billboardLabel(rung.label, mid[0], mid[1], mid[2] + markerRadius, { color: opts?.labelColor }));
15760
+ }
15761
+ return out;
15762
+ }
15763
+ function latticeDrawables(spec, opts) {
15764
+ const nx = spec.nx ?? 2;
15765
+ const ny = spec.ny ?? 2;
15766
+ const nz = spec.nz ?? 2;
15767
+ const latticeConstant = spec.latticeConstant ?? 2;
15768
+ const bondRadius = spec.bondRadius ?? 0.06;
15769
+ const highlightCell = spec.highlightCell ?? false;
15770
+ const dimColor = spec.dimColor ?? "#475569";
15771
+ const showLabels = spec.showLabels ?? false;
15772
+ const selectedColor = spec.selectedColor ?? "#f59e0b";
15773
+ const posByKey = /* @__PURE__ */ new Map();
15774
+ const inCellByKey = /* @__PURE__ */ new Map();
15775
+ const out = [];
15776
+ for (const site of spec.basis) {
15777
+ const snx = site.xEdge ? nx + 1 : nx;
15778
+ const sny = site.yEdge ? ny + 1 : ny;
15779
+ const snz = site.zEdge ? nz + 1 : nz;
15780
+ for (let i = 0; i < snx; i++) {
15781
+ for (let j = 0; j < sny; j++) {
15782
+ for (let k = 0; k < snz; k++) {
15783
+ const key = `${site.key}-${i}-${j}-${k}`;
15784
+ const inCell = i + site.dx <= 1 && j + site.dy <= 1 && k + site.dz <= 1;
15785
+ const pos = [
15786
+ (i + site.dx) * latticeConstant - nx * latticeConstant / 2,
15787
+ (j + site.dy) * latticeConstant - ny * latticeConstant / 2,
15788
+ (k + site.dz) * latticeConstant - nz * latticeConstant / 2
15789
+ ];
15790
+ posByKey.set(key, pos);
15791
+ inCellByKey.set(key, inCell);
15792
+ const isSelected = spec.selectedId === `lat-${key}`;
15793
+ const color = isSelected ? selectedColor : highlightCell && !inCell ? dimColor : site.color ?? "#2563eb";
15794
+ const radius = (site.radius ?? 0.3) * (isSelected ? 1.4 : 1);
15795
+ out.push(meshSphere(`lat-${key}`, pos[0], pos[1], pos[2], radius, color));
15796
+ if (showLabels && site.element) {
15797
+ out.push(billboardLabel(site.element, pos[0], pos[1], pos[2] + radius, { color: opts?.labelColor }));
15798
+ }
15799
+ }
15800
+ }
15801
+ }
15802
+ }
15803
+ const basisByKey = new Map(spec.basis.map((s) => [s.key, s]));
15804
+ for (const bond of spec.bonds ?? []) {
15805
+ const fromSite = basisByKey.get(bond.from);
15806
+ const toSite = basisByKey.get(bond.to);
15807
+ if (!fromSite || !toSite) continue;
15808
+ const fnx = fromSite.xEdge ? nx + 1 : nx;
15809
+ const fny = fromSite.yEdge ? ny + 1 : ny;
15810
+ const fnz = fromSite.zEdge ? nz + 1 : nz;
15811
+ const tnx = toSite.xEdge ? nx + 1 : nx;
15812
+ const tny = toSite.yEdge ? ny + 1 : ny;
15813
+ const tnz = toSite.zEdge ? nz + 1 : nz;
15814
+ const bdx = bond.dx ?? 0;
15815
+ const bdy = bond.dy ?? 0;
15816
+ const bdz = bond.dz ?? 0;
15817
+ for (let i = 0; i < fnx; i++) {
15818
+ for (let j = 0; j < fny; j++) {
15819
+ for (let k = 0; k < fnz; k++) {
15820
+ const ti = i + bdx;
15821
+ const tj = j + bdy;
15822
+ const tk = k + bdz;
15823
+ if (ti < 0 || ti >= tnx || tj < 0 || tj >= tny || tk < 0 || tk >= tnz) continue;
15824
+ const fromKey = `${fromSite.key}-${i}-${j}-${k}`;
15825
+ const toKey = `${toSite.key}-${ti}-${tj}-${tk}`;
15826
+ const fromPos = posByKey.get(fromKey);
15827
+ const toPos = posByKey.get(toKey);
15828
+ if (!fromPos || !toPos) continue;
15829
+ const dimmed = highlightCell && !(inCellByKey.get(fromKey) && inCellByKey.get(toKey));
15830
+ const seg = cylinderBetween(fromPos, toPos, bondRadius, dimmed ? dimColor : bond.color ?? "#6b7280");
15831
+ if (seg) out.push(seg);
15832
+ }
15833
+ }
15834
+ }
15835
+ }
15836
+ return out;
15837
+ }
15838
+ var sceneLog, KNOWN_BLOOM_KEYS, Canvas3DHost2;
15839
+ var init_learningScene3D = __esm({
15840
+ "components/learning/molecules/learningScene3D.tsx"() {
15841
+ "use client";
15842
+ init_atoms();
15843
+ init_Stack();
15844
+ init_useEventBus();
15845
+ sceneLog = createLogger("almadar:ui:learning-scene-3d");
15846
+ KNOWN_BLOOM_KEYS = /* @__PURE__ */ new Set(["intensity", "threshold", "smoothing"]);
15847
+ Canvas3DHost2 = lazy(
15848
+ () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
15849
+ );
15850
+ LearningScene3D.displayName = "LearningScene3D";
15851
+ }
15852
+ });
15853
+ var biologyLog, BIO_BAND_COLORS, BIO_STAGE_FILL, BIO_STAGE_TEXT, BiologyCanvas;
14540
15854
  var init_BiologyCanvas = __esm({
14541
15855
  "components/learning/molecules/BiologyCanvas.tsx"() {
14542
15856
  "use client";
14543
15857
  init_atoms();
14544
15858
  init_Stack();
14545
15859
  init_LearningCanvas();
15860
+ init_learningScene3D();
15861
+ biologyLog = createLogger("almadar:ui:biology-canvas");
15862
+ BIO_BAND_COLORS = ["#dcfce7", "#fef9c3", "#fee2e2", "#e0e7ff"];
15863
+ BIO_STAGE_FILL = {
15864
+ pending: "#e2e8f0",
15865
+ active: "#3b82f6",
15866
+ done: "#94a3b8"
15867
+ };
15868
+ BIO_STAGE_TEXT = {
15869
+ pending: "#64748b",
15870
+ active: "#ffffff",
15871
+ done: "#ffffff"
15872
+ };
14546
15873
  BiologyCanvas = ({
14547
15874
  className,
14548
15875
  width = 600,
14549
15876
  height = 400,
14550
15877
  title,
14551
15878
  backgroundColor,
15879
+ mode = "2d",
15880
+ camera,
15881
+ lighting,
15882
+ post,
14552
15883
  nodes = [],
14553
15884
  edges = [],
15885
+ compartments = [],
15886
+ bands = [],
15887
+ stages = [],
15888
+ stageStyle = "timeline",
15889
+ helix,
15890
+ helix3d,
14554
15891
  shapes = [],
14555
- interactive = false,
15892
+ readouts,
15893
+ traces,
15894
+ showGrid,
15895
+ shadows,
15896
+ interactive,
14556
15897
  animate = false,
14557
15898
  onShapeClick,
14558
15899
  isLoading,
@@ -14564,19 +15905,148 @@ var init_BiologyCanvas = __esm({
14564
15905
  for (const n of nodes) {
14565
15906
  if (n.id) nodeById.set(n.id, n);
14566
15907
  }
15908
+ const bandCount = bands.length;
15909
+ for (let i = 0; i < bandCount; i++) {
15910
+ const band = bands[i];
15911
+ const bandColor = band.color ?? BIO_BAND_COLORS[i % BIO_BAND_COLORS.length];
15912
+ const bandY = i * height / bandCount;
15913
+ const bandH = height / bandCount;
15914
+ out.push({
15915
+ type: "rect",
15916
+ x: 0,
15917
+ y: bandY,
15918
+ width,
15919
+ height: bandH,
15920
+ color: bandColor,
15921
+ fill: bandColor,
15922
+ opacity: 0.45
15923
+ });
15924
+ if (band.label) {
15925
+ out.push({
15926
+ type: "text",
15927
+ x: 8,
15928
+ y: bandY + 14,
15929
+ text: band.label,
15930
+ color: "#6b7280",
15931
+ fontSize: 10
15932
+ });
15933
+ }
15934
+ }
15935
+ for (const c of compartments) {
15936
+ const color = c.color ?? "#16a34a";
15937
+ out.push({
15938
+ type: "ellipse",
15939
+ x: c.x,
15940
+ y: c.y,
15941
+ width: c.width,
15942
+ height: c.height,
15943
+ color,
15944
+ fill: c.fill ?? `${color}1A`,
15945
+ lineWidth: c.lineWidth ?? 2,
15946
+ ...c.dash ? { dash: c.dash } : {}
15947
+ });
15948
+ if (c.label) {
15949
+ out.push({
15950
+ type: "text",
15951
+ x: c.x,
15952
+ y: c.y - c.height / 2 + 14,
15953
+ text: c.label,
15954
+ color: "#111827",
15955
+ fontSize: 11,
15956
+ align: "center"
15957
+ });
15958
+ }
15959
+ }
15960
+ if (helix) {
15961
+ const hx = helix.x ?? 24;
15962
+ const hy = helix.y ?? height * 0.25;
15963
+ const hw = helix.width ?? width - 48;
15964
+ const hh = helix.height ?? height * 0.5;
15965
+ const rungs = helix.rungs;
15966
+ const n = rungs.length;
15967
+ const cy = hy + hh / 2;
15968
+ const colorA = helix.colorA ?? "#2563eb";
15969
+ const colorB = helix.colorB ?? "#dc2626";
15970
+ const rungColor = helix.rungColor ?? "#94a3b8";
15971
+ const fork = helix.fork ?? 0;
15972
+ const maxSep = Math.min(hh - 8, 96);
15973
+ const strandA = [];
15974
+ const strandB = [];
15975
+ const rungGeoms = [];
15976
+ for (let i = 0; i < n; i++) {
15977
+ const rx = hx + (i + 0.5) * hw / n;
15978
+ const t = (i + 0.5) / n;
15979
+ const paired = t >= fork;
15980
+ const sep = paired ? 28 : 28 + (maxSep - 28) * ((fork - t) / fork);
15981
+ strandA.push({ x: rx, y: cy - sep / 2 });
15982
+ strandB.push({ x: rx, y: cy + sep / 2 });
15983
+ rungGeoms.push({ rx, sep, rung: rungs[i], paired });
15984
+ }
15985
+ for (let i = 1; i < n; i++) {
15986
+ 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 });
15987
+ }
15988
+ for (let i = 1; i < n; i++) {
15989
+ 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 });
15990
+ }
15991
+ for (const g of rungGeoms) {
15992
+ const rColor = g.rung.color ?? (g.rung.state === "new" ? "#16a34a" : rungColor);
15993
+ const topY = cy - g.sep / 2;
15994
+ const bottomY = cy + g.sep / 2;
15995
+ if (g.paired) {
15996
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: bottomY, color: rColor });
15997
+ if (g.rung.a) {
15998
+ out.push({ type: "text", x: g.rx, y: cy - g.sep / 4, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
15999
+ }
16000
+ if (g.rung.b) {
16001
+ out.push({ type: "text", x: g.rx, y: cy + g.sep / 4, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16002
+ }
16003
+ } else {
16004
+ const stubTopY = topY + 8;
16005
+ const stubBottomY = bottomY - 8;
16006
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: stubTopY, color: rColor });
16007
+ out.push({ type: "line", x1: g.rx, y1: bottomY, x2: g.rx, y2: stubBottomY, color: rColor });
16008
+ if (g.rung.a) {
16009
+ out.push({ type: "text", x: g.rx, y: stubTopY + 6, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
16010
+ }
16011
+ if (g.rung.b) {
16012
+ out.push({ type: "text", x: g.rx, y: stubBottomY - 6, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16013
+ }
16014
+ }
16015
+ }
16016
+ }
14567
16017
  for (const e of edges) {
14568
16018
  const a = nodeById.get(e.from);
14569
16019
  const b = nodeById.get(e.to);
14570
16020
  if (!a || !b) continue;
14571
- out.push({
14572
- type: "line",
14573
- x1: a.x,
14574
- y1: a.y,
14575
- x2: b.x,
14576
- y2: b.y,
14577
- color: e.color ?? "#9ca3af",
14578
- lineWidth: 2
14579
- });
16021
+ const color = e.color ?? "#9ca3af";
16022
+ if (e.directed) {
16023
+ const rA = a.radius ?? 16;
16024
+ const rB = b.radius ?? 16;
16025
+ const dx = b.x - a.x;
16026
+ const dy = b.y - a.y;
16027
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16028
+ const ux = dx / dist;
16029
+ const uy = dy / dist;
16030
+ out.push({
16031
+ type: "arrow",
16032
+ x1: a.x + ux * rA,
16033
+ y1: a.y + uy * rA,
16034
+ x2: b.x - ux * rB,
16035
+ y2: b.y - uy * rB,
16036
+ color,
16037
+ lineWidth: 2
16038
+ });
16039
+ } else {
16040
+ out.push({
16041
+ type: "line",
16042
+ x1: a.x,
16043
+ y1: a.y,
16044
+ x2: b.x,
16045
+ y2: b.y,
16046
+ color,
16047
+ lineWidth: 2
16048
+ });
16049
+ }
14580
16050
  if (e.label) {
14581
16051
  out.push({
14582
16052
  type: "text",
@@ -14589,6 +16059,8 @@ var init_BiologyCanvas = __esm({
14589
16059
  }
14590
16060
  }
14591
16061
  for (const n of nodes) {
16062
+ const state = n.state ?? "default";
16063
+ const muted = state === "muted";
14592
16064
  out.push({
14593
16065
  type: "circle",
14594
16066
  x: n.x,
@@ -14596,23 +16068,197 @@ var init_BiologyCanvas = __esm({
14596
16068
  radius: n.radius ?? 16,
14597
16069
  color: n.color ?? "#16a34a",
14598
16070
  fill: `${n.color ?? "#16a34a"}33`,
14599
- id: n.id
16071
+ id: n.id,
16072
+ ...muted ? { opacity: 0.35 } : {}
14600
16073
  });
16074
+ if (state === "highlight") {
16075
+ out.push({
16076
+ type: "circle",
16077
+ x: n.x,
16078
+ y: n.y,
16079
+ radius: (n.radius ?? 16) + 4,
16080
+ color: "#f59e0b",
16081
+ lineWidth: 2
16082
+ });
16083
+ }
14601
16084
  if (n.label) {
14602
16085
  out.push({
14603
16086
  type: "text",
14604
16087
  x: n.x,
14605
16088
  y: n.y + (n.radius ?? 16) + 14,
14606
16089
  text: n.label,
16090
+ ...muted ? { opacity: 0.35 } : {},
14607
16091
  color: "#111827",
14608
16092
  fontSize: 12,
14609
16093
  align: "center"
14610
16094
  });
14611
16095
  }
14612
16096
  }
16097
+ const stageCount = stages.length;
16098
+ if (stageCount > 0) {
16099
+ if (stageStyle === "ring") {
16100
+ const cx = width / 2;
16101
+ const cy = height / 2;
16102
+ const R = Math.min(width, height) / 2 - 48;
16103
+ const ringPoints = [];
16104
+ for (let i = 0; i < stageCount; i++) {
16105
+ const angleRad = (-90 + 360 * i / stageCount) * Math.PI / 180;
16106
+ ringPoints.push({ x: cx + R * Math.cos(angleRad), y: cy + R * Math.sin(angleRad) });
16107
+ }
16108
+ if (stageCount >= 2) {
16109
+ for (let i = 0; i < stageCount - 1; i++) {
16110
+ const p1 = ringPoints[i];
16111
+ const p2 = ringPoints[i + 1];
16112
+ const dx = p2.x - p1.x;
16113
+ const dy = p2.y - p1.y;
16114
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16115
+ const ux = dx / dist;
16116
+ const uy = dy / dist;
16117
+ out.push({
16118
+ type: "arrow",
16119
+ x1: p1.x + ux * 46,
16120
+ y1: p1.y + uy * 46,
16121
+ x2: p2.x - ux * 46,
16122
+ y2: p2.y - uy * 46,
16123
+ color: "#94a3b8"
16124
+ });
16125
+ }
16126
+ }
16127
+ for (let i = 0; i < stageCount; i++) {
16128
+ const stage = stages[i];
16129
+ const state = stage.state ?? "pending";
16130
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16131
+ const w = Math.max(26, Math.min(84, stage.label.length * 6 + 10));
16132
+ const h = 18;
16133
+ const p = ringPoints[i];
16134
+ out.push({ type: "rect", x: p.x - w / 2, y: p.y - h / 2, width: w, height: h, color: fill, fill });
16135
+ out.push({ type: "text", x: p.x, y: p.y, text: stage.label, color: BIO_STAGE_TEXT[state], fontSize: 10, align: "center" });
16136
+ }
16137
+ } else {
16138
+ const stripY = height - 32;
16139
+ const slotW = (width - 16) / stageCount;
16140
+ const chipGeoms = [];
16141
+ for (let i = 0; i < stageCount; i++) {
16142
+ chipGeoms.push({ x: 8 + i * slotW + 5, w: slotW - 10 });
16143
+ }
16144
+ for (let i = 0; i < stageCount - 1; i++) {
16145
+ const midY = stripY + 13;
16146
+ out.push({
16147
+ type: "arrow",
16148
+ x1: chipGeoms[i].x + chipGeoms[i].w,
16149
+ y1: midY,
16150
+ x2: chipGeoms[i + 1].x,
16151
+ y2: midY,
16152
+ color: "#94a3b8"
16153
+ });
16154
+ }
16155
+ for (let i = 0; i < stageCount; i++) {
16156
+ const stage = stages[i];
16157
+ const state = stage.state ?? "pending";
16158
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16159
+ const g = chipGeoms[i];
16160
+ out.push({ type: "rect", x: g.x, y: stripY, width: g.w, height: 26, color: fill, fill });
16161
+ out.push({
16162
+ type: "text",
16163
+ x: g.x + g.w / 2,
16164
+ y: stripY + 13,
16165
+ text: stage.label,
16166
+ color: BIO_STAGE_TEXT[state],
16167
+ fontSize: 10,
16168
+ align: "center"
16169
+ });
16170
+ }
16171
+ }
16172
+ }
14613
16173
  out.push(...shapes);
14614
16174
  return out;
14615
- }, [nodes, edges, shapes]);
16175
+ }, [nodes, edges, compartments, bands, stages, stageStyle, helix, shapes, width, height]);
16176
+ const drawables3D = useMemo(() => {
16177
+ if (mode !== "3d") return [];
16178
+ if (shapes.length > 0) {
16179
+ biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
16180
+ }
16181
+ if (compartments.length > 0 || bands.length > 0 || stages.length > 0 || helix) {
16182
+ biologyLog.debug("2D-only families ignored in 3D mode (pixel-authored 2D vocabulary)", {
16183
+ compartments: compartments.length,
16184
+ bands: bands.length,
16185
+ stages: stages.length,
16186
+ helix: helix != null
16187
+ });
16188
+ }
16189
+ if (animate) {
16190
+ biologyLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
16191
+ }
16192
+ const out = [];
16193
+ const labelColor = labelColorForBackground(backgroundColor);
16194
+ const nodeById = /* @__PURE__ */ new Map();
16195
+ for (const n of nodes) {
16196
+ if (n.id) nodeById.set(n.id, n);
16197
+ }
16198
+ for (const e of edges) {
16199
+ const a = nodeById.get(e.from);
16200
+ const b = nodeById.get(e.to);
16201
+ if (!a || !b) continue;
16202
+ const edgeRadius = Math.max(0.04, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.12);
16203
+ const edge = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], edgeRadius, e.color ?? "#9ca3af");
16204
+ if (edge) out.push(edge);
16205
+ if (e.label) {
16206
+ out.push(
16207
+ billboardLabel(
16208
+ e.label,
16209
+ (a.x + b.x) / 2,
16210
+ (a.y + b.y) / 2,
16211
+ ((a.z ?? 0) + (b.z ?? 0)) / 2,
16212
+ { color: labelColor }
16213
+ )
16214
+ );
16215
+ }
16216
+ }
16217
+ for (const n of nodes) {
16218
+ const radius = n.radius ?? 0.5;
16219
+ const nz = n.z ?? 0;
16220
+ out.push(meshSphere(n.id, n.x, n.y, nz, radius, n.color ?? "#16a34a", { shape: n.shape, ...n.opacity !== void 0 ? { opacity: n.opacity } : {} }));
16221
+ if (n.label) {
16222
+ out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
16223
+ }
16224
+ }
16225
+ if (helix3d) {
16226
+ out.push(...helixDrawables(helix3d, { labelColor }));
16227
+ }
16228
+ return out;
16229
+ }, [mode, nodes, edges, shapes, compartments, bands, stages, helix, helix3d, animate, backgroundColor]);
16230
+ const nodeIndexById = useMemo(() => {
16231
+ const m = /* @__PURE__ */ new Map();
16232
+ (helix3d?.rungs ?? []).forEach((rung, i) => {
16233
+ if (rung.id) m.set(rung.id, i);
16234
+ });
16235
+ nodes.forEach((n, i) => {
16236
+ if (n.id) m.set(n.id, i);
16237
+ });
16238
+ return m;
16239
+ }, [nodes, helix3d]);
16240
+ if (mode === "3d") {
16241
+ return /* @__PURE__ */ jsx(
16242
+ LearningScene3D,
16243
+ {
16244
+ className,
16245
+ width,
16246
+ height,
16247
+ title,
16248
+ backgroundColor,
16249
+ drawables: drawables3D,
16250
+ camera,
16251
+ lighting,
16252
+ post,
16253
+ showGrid,
16254
+ shadows,
16255
+ interactive,
16256
+ isLoading,
16257
+ error,
16258
+ onItemClick: get3DClickPayload(onShapeClick, nodeIndexById)
16259
+ }
16260
+ );
16261
+ }
14616
16262
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
14617
16263
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
14618
16264
  /* @__PURE__ */ jsx(
@@ -14622,7 +16268,9 @@ var init_BiologyCanvas = __esm({
14622
16268
  height,
14623
16269
  backgroundColor,
14624
16270
  shapes: derivedShapes,
14625
- interactive,
16271
+ readouts,
16272
+ traces,
16273
+ interactive: interactive ?? false,
14626
16274
  animate,
14627
16275
  onShapeClick,
14628
16276
  isLoading,
@@ -21345,24 +22993,55 @@ var init_ChatBar = __esm({
21345
22993
  ChatBar.displayName = "ChatBar";
21346
22994
  }
21347
22995
  });
21348
- var ChemistryCanvas;
22996
+ function bondPerpendicular(a, b) {
22997
+ const dx = b[0] - a[0];
22998
+ const dy = b[1] - a[1];
22999
+ const px = dy;
23000
+ const py = -dx;
23001
+ const len = Math.sqrt(px * px + py * py);
23002
+ if (len < 1e-6) return [1, 0, 0];
23003
+ return [px / len, py / len, 0];
23004
+ }
23005
+ var chemistryLog, CHEM_BOND_STATE_COLOR, LONE_PAIR_ANGLES, ChemistryCanvas;
21349
23006
  var init_ChemistryCanvas = __esm({
21350
23007
  "components/learning/molecules/ChemistryCanvas.tsx"() {
21351
23008
  "use client";
21352
23009
  init_atoms();
21353
23010
  init_Stack();
21354
23011
  init_LearningCanvas();
23012
+ init_learningScene3D();
23013
+ chemistryLog = createLogger("almadar:ui:chemistry-canvas");
23014
+ CHEM_BOND_STATE_COLOR = {
23015
+ default: "#6b7280",
23016
+ forming: "#16a34a",
23017
+ breaking: "#dc2626",
23018
+ highlight: "#f59e0b"
23019
+ };
23020
+ LONE_PAIR_ANGLES = [-90, 0, 90, 180];
21355
23021
  ChemistryCanvas = ({
21356
23022
  className,
21357
23023
  width = 600,
21358
23024
  height = 400,
21359
23025
  title,
21360
23026
  backgroundColor,
23027
+ mode = "2d",
23028
+ camera,
23029
+ lighting,
23030
+ post,
21361
23031
  atoms = [],
21362
23032
  bonds = [],
21363
23033
  arrows = [],
23034
+ bondStyle = "thick",
23035
+ containers = [],
23036
+ equation,
23037
+ equationColor,
23038
+ lattice3d,
21364
23039
  shapes = [],
21365
- interactive = false,
23040
+ readouts,
23041
+ traces,
23042
+ showGrid,
23043
+ shadows,
23044
+ interactive,
21366
23045
  animate = false,
21367
23046
  onShapeClick,
21368
23047
  isLoading,
@@ -21374,21 +23053,118 @@ var init_ChemistryCanvas = __esm({
21374
23053
  for (const a of atoms) {
21375
23054
  if (a.id) atomById.set(a.id, a);
21376
23055
  }
23056
+ for (const c of containers) {
23057
+ const color = c.color ?? "#64748b";
23058
+ if (c.level != null) {
23059
+ const lv = c.level;
23060
+ out.push({
23061
+ type: "rect",
23062
+ x: c.x + 1,
23063
+ y: c.y + c.height * (1 - lv),
23064
+ width: c.width - 2,
23065
+ height: c.height * lv - 1,
23066
+ color: c.levelColor ?? "#60a5fa",
23067
+ fill: c.levelColor ?? "#60a5fa",
23068
+ opacity: 0.5
23069
+ });
23070
+ }
23071
+ out.push({
23072
+ type: "rect",
23073
+ x: c.x,
23074
+ y: c.y,
23075
+ width: c.width,
23076
+ height: c.height,
23077
+ color,
23078
+ fill: c.fill,
23079
+ lineWidth: c.lineWidth ?? 2
23080
+ });
23081
+ const divider = c.divider ?? "none";
23082
+ if (divider !== "none") {
23083
+ out.push({
23084
+ type: "line",
23085
+ x1: c.x + c.width / 2,
23086
+ y1: c.y,
23087
+ x2: c.x + c.width / 2,
23088
+ y2: c.y + c.height,
23089
+ color: c.dividerColor ?? color,
23090
+ ...divider === "dashed" || divider === "dotted" ? { dash: divider } : {}
23091
+ });
23092
+ }
23093
+ if (c.leftLabel) {
23094
+ out.push({
23095
+ type: "text",
23096
+ x: c.x + c.width * 0.25,
23097
+ y: c.y + 12,
23098
+ text: c.leftLabel,
23099
+ color: "#374151",
23100
+ fontSize: 11,
23101
+ align: "center"
23102
+ });
23103
+ }
23104
+ if (c.rightLabel) {
23105
+ out.push({
23106
+ type: "text",
23107
+ x: c.x + c.width * 0.75,
23108
+ y: c.y + 12,
23109
+ text: c.rightLabel,
23110
+ color: "#374151",
23111
+ fontSize: 11,
23112
+ align: "center"
23113
+ });
23114
+ }
23115
+ if (c.label) {
23116
+ out.push({
23117
+ type: "text",
23118
+ x: c.x + c.width / 2,
23119
+ y: c.y + c.height + 12,
23120
+ text: c.label,
23121
+ color: "#111827",
23122
+ fontSize: 12,
23123
+ align: "center"
23124
+ });
23125
+ }
23126
+ }
21377
23127
  for (const b of bonds) {
21378
23128
  const a = atomById.get(b.from);
21379
23129
  const c = atomById.get(b.to);
21380
23130
  if (!a || !c) continue;
21381
- const color = b.color ?? "#6b7280";
21382
- const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
21383
- out.push({
21384
- type: "line",
21385
- x1: a.x,
21386
- y1: a.y,
21387
- x2: c.x,
21388
- y2: c.y,
21389
- color,
21390
- lineWidth: strokeWidth
21391
- });
23131
+ const state = b.state ?? "default";
23132
+ const color = b.color ?? CHEM_BOND_STATE_COLOR[state];
23133
+ const dash = state === "forming" || state === "breaking" ? "dashed" : void 0;
23134
+ if (bondStyle === "parallel") {
23135
+ const dx = c.x - a.x;
23136
+ const dy = c.y - a.y;
23137
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
23138
+ const ux = dx / dist;
23139
+ const uy = dy / dist;
23140
+ const px = -uy;
23141
+ const py = ux;
23142
+ const offsets = b.type === "double" ? [-3, 3] : b.type === "triple" ? [-4, 0, 4] : [0];
23143
+ for (const off of offsets) {
23144
+ out.push({
23145
+ type: "line",
23146
+ x1: a.x + px * off,
23147
+ y1: a.y + py * off,
23148
+ x2: c.x + px * off,
23149
+ y2: c.y + py * off,
23150
+ color,
23151
+ lineWidth: 2,
23152
+ ...dash ? { dash } : {}
23153
+ });
23154
+ }
23155
+ } else {
23156
+ const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
23157
+ out.push({
23158
+ type: "line",
23159
+ x1: a.x,
23160
+ y1: a.y,
23161
+ x2: c.x,
23162
+ y2: c.y,
23163
+ color,
23164
+ lineWidth: strokeWidth,
23165
+ ...dash ? { dash } : {}
23166
+ });
23167
+ }
21392
23168
  }
21393
23169
  for (const a of arrows) {
21394
23170
  const angle = (a.angle ?? 0) * (Math.PI / 180);
@@ -21437,10 +23213,142 @@ var init_ChemistryCanvas = __esm({
21437
23213
  align: "center"
21438
23214
  });
21439
23215
  }
23216
+ const r = a.radius ?? 14;
23217
+ if (a.charge) {
23218
+ out.push({
23219
+ type: "text",
23220
+ x: a.x + r * 0.85,
23221
+ y: a.y - r * 0.85,
23222
+ text: a.charge,
23223
+ color: "#111827",
23224
+ fontSize: 9,
23225
+ align: "left"
23226
+ });
23227
+ }
23228
+ const lonePairs = Math.max(0, Math.min(4, a.lonePairs ?? 0));
23229
+ for (let k = 0; k < lonePairs; k++) {
23230
+ const angleRad = LONE_PAIR_ANGLES[k] * Math.PI / 180;
23231
+ const cx = a.x + (r + 6) * Math.cos(angleRad);
23232
+ const cy = a.y + (r + 6) * Math.sin(angleRad);
23233
+ const perpX = -Math.sin(angleRad);
23234
+ const perpY = Math.cos(angleRad);
23235
+ for (const sign of [1, -1]) {
23236
+ out.push({
23237
+ type: "circle",
23238
+ x: cx + perpX * 2.5 * sign,
23239
+ y: cy + perpY * 2.5 * sign,
23240
+ radius: 1.5,
23241
+ color: "#374151",
23242
+ fill: "#374151"
23243
+ });
23244
+ }
23245
+ }
23246
+ }
23247
+ if (equation) {
23248
+ out.push({
23249
+ type: "text",
23250
+ x: width / 2,
23251
+ y: 14,
23252
+ text: equation,
23253
+ color: equationColor ?? "#111827",
23254
+ fontSize: 13,
23255
+ align: "center"
23256
+ });
21440
23257
  }
21441
23258
  out.push(...shapes);
21442
23259
  return out;
21443
- }, [atoms, bonds, arrows, shapes]);
23260
+ }, [atoms, bonds, arrows, bondStyle, containers, equation, equationColor, shapes, width]);
23261
+ const drawables3D = useMemo(() => {
23262
+ if (mode !== "3d") return [];
23263
+ if (shapes.length > 0) {
23264
+ chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
23265
+ }
23266
+ if (containers.length > 0) {
23267
+ chemistryLog.debug("containers ignored in 3D mode (pixel-authored 2D vocabulary)", { count: containers.length });
23268
+ }
23269
+ if (animate) {
23270
+ chemistryLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
23271
+ }
23272
+ const out = [];
23273
+ const labelColor = labelColorForBackground(backgroundColor);
23274
+ const atomById = /* @__PURE__ */ new Map();
23275
+ for (const a of atoms) {
23276
+ if (a.id) atomById.set(a.id, a);
23277
+ }
23278
+ for (const b of bonds) {
23279
+ const a = atomById.get(b.from);
23280
+ const c = atomById.get(b.to);
23281
+ if (!a || !c) continue;
23282
+ const color = b.color ?? "#6b7280";
23283
+ const from = [a.x, a.y, a.z ?? 0];
23284
+ const to = [c.x, c.y, c.z ?? 0];
23285
+ const perp = bondPerpendicular(from, to);
23286
+ const bondRadius = Math.max(0.06, Math.min(a.radius ?? 0.45, c.radius ?? 0.45) * 0.22);
23287
+ const step = bondRadius * 2.2;
23288
+ const offsets = b.type === "double" ? [-step, step] : b.type === "triple" ? [-step, 0, step] : [0];
23289
+ for (const off of offsets) {
23290
+ const bond = cylinderBetween(
23291
+ [from[0] + perp[0] * off, from[1] + perp[1] * off, from[2] + perp[2] * off],
23292
+ [to[0] + perp[0] * off, to[1] + perp[1] * off, to[2] + perp[2] * off],
23293
+ bondRadius,
23294
+ color
23295
+ );
23296
+ if (bond) out.push(bond);
23297
+ }
23298
+ }
23299
+ for (const a of arrows) {
23300
+ const angle = (a.angle ?? 0) * (Math.PI / 180);
23301
+ const len = a.length ?? 60;
23302
+ const x2 = a.x + Math.cos(angle) * len;
23303
+ const y2 = a.y + Math.sin(angle) * len;
23304
+ const arrow = arrowBetween([a.x, a.y, 0], [x2, y2, 0], a.color ?? "#dc2626");
23305
+ if (arrow) out.push(arrow);
23306
+ if (a.label) {
23307
+ out.push(billboardLabel(a.label, (a.x + x2) / 2, (a.y + y2) / 2, 0, { color: labelColor }));
23308
+ }
23309
+ }
23310
+ for (const a of atoms) {
23311
+ const radius = a.radius ?? 0.45;
23312
+ const az = a.z ?? 0;
23313
+ out.push(meshSphere(a.id, a.x, a.y, az, radius, a.color ?? "#2563eb"));
23314
+ if (a.element) {
23315
+ out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
23316
+ }
23317
+ }
23318
+ if (lattice3d) {
23319
+ out.push(...latticeDrawables(lattice3d, { labelColor }));
23320
+ }
23321
+ return out;
23322
+ }, [mode, atoms, bonds, arrows, shapes, containers, lattice3d, animate, backgroundColor]);
23323
+ const atomIndexById = useMemo(() => {
23324
+ const m = /* @__PURE__ */ new Map();
23325
+ atoms.forEach((a, i) => {
23326
+ if (a.id) m.set(a.id, i);
23327
+ });
23328
+ return m;
23329
+ }, [atoms]);
23330
+ if (mode === "3d") {
23331
+ return /* @__PURE__ */ jsx(
23332
+ LearningScene3D,
23333
+ {
23334
+ className,
23335
+ width,
23336
+ height,
23337
+ title,
23338
+ backgroundColor,
23339
+ drawables: drawables3D,
23340
+ camera,
23341
+ lighting,
23342
+ post,
23343
+ showGrid,
23344
+ shadows,
23345
+ interactive,
23346
+ isLoading,
23347
+ error,
23348
+ onItemClick: get3DClickPayload(onShapeClick, atomIndexById)
23349
+ }
23350
+ );
23351
+ }
21444
23352
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
21445
23353
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
21446
23354
  /* @__PURE__ */ jsx(
@@ -21450,7 +23358,9 @@ var init_ChemistryCanvas = __esm({
21450
23358
  height,
21451
23359
  backgroundColor,
21452
23360
  shapes: derivedShapes,
21453
- interactive,
23361
+ readouts,
23362
+ traces,
23363
+ interactive: interactive ?? false,
21454
23364
  animate,
21455
23365
  onShapeClick,
21456
23366
  isLoading,
@@ -27561,6 +29471,10 @@ var init_ProgressDots = __esm({
27561
29471
  ProgressDots.displayName = "ProgressDots";
27562
29472
  }
27563
29473
  });
29474
+ function formatTick(v) {
29475
+ if (Number.isInteger(v)) return String(v);
29476
+ return v.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
29477
+ }
27564
29478
  var MathCanvas;
27565
29479
  var init_MathCanvas = __esm({
27566
29480
  "components/learning/molecules/MathCanvas.tsx"() {
@@ -27580,10 +29494,19 @@ var init_MathCanvas = __esm({
27580
29494
  showAxes = true,
27581
29495
  showGrid = true,
27582
29496
  gridStep = 1,
29497
+ showTickLabels = false,
29498
+ showCurveLabels = false,
27583
29499
  curves = [],
27584
29500
  points = [],
27585
29501
  vectors = [],
29502
+ regions = [],
29503
+ bars = [],
29504
+ guides = [],
29505
+ angles = [],
29506
+ hops = [],
27586
29507
  shapes = [],
29508
+ readouts,
29509
+ traces,
27587
29510
  interactive = false,
27588
29511
  animate = false,
27589
29512
  onShapeClick,
@@ -27597,6 +29520,8 @@ var init_MathCanvas = __esm({
27597
29520
  const plotH = height - margin * 2;
27598
29521
  const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
27599
29522
  const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
29523
+ const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
29524
+ const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
27600
29525
  if (showGrid) {
27601
29526
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep) {
27602
29527
  const px = mapX(x);
@@ -27607,14 +29532,99 @@ var init_MathCanvas = __esm({
27607
29532
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color: "#e5e7eb", lineWidth: 1 });
27608
29533
  }
27609
29534
  }
29535
+ if (showTickLabels) {
29536
+ const labelEveryX = Math.max(1, Math.ceil((xMax - xMin) / gridStep / Math.floor(plotW / 40)));
29537
+ let kx = 0;
29538
+ for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
29539
+ if (kx % labelEveryX === 0 && x !== 0) {
29540
+ out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: 10, align: "center" });
29541
+ }
29542
+ }
29543
+ const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
29544
+ let ky = 0;
29545
+ for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
29546
+ if (ky % labelEveryY === 0 && y !== 0) {
29547
+ out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: 10, align: "right" });
29548
+ }
29549
+ }
29550
+ if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
29551
+ out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: 10, align: "right" });
29552
+ }
29553
+ }
29554
+ for (const region of regions) {
29555
+ if (!region.samples || region.samples.length === 0) continue;
29556
+ const baseline = region.baseline ?? 0;
29557
+ const clampedPoint = (p) => ({
29558
+ x: mapX(Math.min(xMax, Math.max(xMin, p.x))),
29559
+ y: mapY(Math.min(yMax, Math.max(yMin, p.y)))
29560
+ });
29561
+ const upper = region.samples.map(clampedPoint);
29562
+ const first = region.samples[0];
29563
+ const last = region.samples[region.samples.length - 1];
29564
+ 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 })];
29565
+ const color = region.color ?? "#2563eb";
29566
+ out.push({
29567
+ type: "polygon",
29568
+ points: [...upper, ...closing],
29569
+ fill: color,
29570
+ color,
29571
+ opacity: region.opacity ?? 0.2,
29572
+ lineWidth: 1
29573
+ });
29574
+ if (region.label) {
29575
+ const mid = Math.floor(region.samples.length / 2);
29576
+ out.push({
29577
+ type: "text",
29578
+ x: mapX((first.x + last.x) / 2),
29579
+ y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
29580
+ text: region.label,
29581
+ color: "#111827",
29582
+ fontSize: 11
29583
+ });
29584
+ }
29585
+ }
29586
+ for (const bar of bars) {
29587
+ if (bar.x + bar.width < xMin || bar.x > xMax) continue;
29588
+ const y0 = bar.y0 ?? 0;
29589
+ const color = bar.color ?? "#93c5fd";
29590
+ out.push({
29591
+ type: "rect",
29592
+ x: mapX(bar.x),
29593
+ y: mapY(Math.max(y0, bar.y1)),
29594
+ width: mapX(bar.x + bar.width) - mapX(bar.x),
29595
+ height: Math.abs(mapY(bar.y1) - mapY(y0)),
29596
+ color,
29597
+ fill: color,
29598
+ opacity: bar.opacity ?? 0.5,
29599
+ lineWidth: 1
29600
+ });
29601
+ }
27610
29602
  if (showAxes) {
27611
- const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
27612
- const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
27613
29603
  out.push({ type: "line", x1: margin, y1: xAxisY, x2: width - margin, y2: xAxisY, color: "#374151", lineWidth: 2 });
27614
29604
  out.push({ type: "line", x1: yAxisX, y1: margin, x2: yAxisX, y2: height - margin, color: "#374151", lineWidth: 2 });
27615
29605
  }
29606
+ for (const guide of guides) {
29607
+ const color = guide.color ?? "#9ca3af";
29608
+ const dash = guide.dash ?? "dashed";
29609
+ if (guide.kind === "vline") {
29610
+ if (guide.at < xMin || guide.at > xMax) continue;
29611
+ const px = mapX(guide.at);
29612
+ out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
29613
+ if (guide.label) {
29614
+ out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color: "#111827", fontSize: 11 });
29615
+ }
29616
+ } else {
29617
+ if (guide.at < yMin || guide.at > yMax) continue;
29618
+ const py = mapY(guide.at);
29619
+ out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
29620
+ if (guide.label) {
29621
+ out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color: "#111827", fontSize: 11, align: "right" });
29622
+ }
29623
+ }
29624
+ }
27616
29625
  for (const curve of curves) {
27617
29626
  if (!curve.samples || curve.samples.length < 2) continue;
29627
+ let lastInRange;
27618
29628
  for (let i = 1; i < curve.samples.length; i++) {
27619
29629
  const a = curve.samples[i - 1];
27620
29630
  const b = curve.samples[i];
@@ -27626,19 +29636,97 @@ var init_MathCanvas = __esm({
27626
29636
  x2: mapX(b.x),
27627
29637
  y2: mapY(b.y),
27628
29638
  color: curve.color ?? "#2563eb",
27629
- lineWidth: 2
29639
+ lineWidth: 2,
29640
+ dash: curve.dash
29641
+ });
29642
+ lastInRange = b;
29643
+ }
29644
+ if (showCurveLabels && curve.label && lastInRange) {
29645
+ out.push({
29646
+ type: "text",
29647
+ x: mapX(lastInRange.x) + 6,
29648
+ y: mapY(lastInRange.y) - 6,
29649
+ text: curve.label,
29650
+ color: curve.color ?? "#2563eb",
29651
+ fontSize: 11
29652
+ });
29653
+ }
29654
+ }
29655
+ for (const hop of hops) {
29656
+ const x1 = mapX(hop.from);
29657
+ const x2 = mapX(hop.to);
29658
+ const peak = Math.min(36, plotH * 0.3);
29659
+ const color = hop.color ?? "#7c3aed";
29660
+ out.push({
29661
+ type: "ellipse",
29662
+ x: (x1 + x2) / 2,
29663
+ y: xAxisY,
29664
+ width: Math.abs(x2 - x1),
29665
+ height: 2 * peak,
29666
+ startAngle: 180,
29667
+ endAngle: 360,
29668
+ color
29669
+ });
29670
+ const s = Math.sign(hop.to - hop.from);
29671
+ out.push({
29672
+ type: "polygon",
29673
+ points: [
29674
+ { x: x2, y: xAxisY },
29675
+ { x: x2 - 4 * s, y: xAxisY - 7 },
29676
+ { x: x2 + 2 * s, y: xAxisY - 7 }
29677
+ ],
29678
+ fill: color,
29679
+ color
29680
+ });
29681
+ if (hop.label) {
29682
+ out.push({
29683
+ type: "text",
29684
+ x: (x1 + x2) / 2,
29685
+ y: xAxisY - peak - 8,
29686
+ text: hop.label,
29687
+ color: "#111827",
29688
+ fontSize: 10,
29689
+ align: "center"
29690
+ });
29691
+ }
29692
+ }
29693
+ for (const angle of angles) {
29694
+ const radius = angle.radius ?? 0.8;
29695
+ const color = angle.color ?? "#0ea5e9";
29696
+ out.push({
29697
+ type: "ellipse",
29698
+ x: mapX(angle.x),
29699
+ y: mapY(angle.y),
29700
+ width: 2 * radius * plotW / (xMax - xMin),
29701
+ height: 2 * radius * plotH / (yMax - yMin),
29702
+ startAngle: -angle.to,
29703
+ endAngle: -angle.from,
29704
+ color
29705
+ });
29706
+ if (angle.label) {
29707
+ const mid = (angle.from + angle.to) / 2;
29708
+ const rad = mid * Math.PI / 180;
29709
+ out.push({
29710
+ type: "text",
29711
+ x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
29712
+ y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
29713
+ text: angle.label,
29714
+ color: "#111827",
29715
+ fontSize: 11,
29716
+ align: "center"
27630
29717
  });
27631
29718
  }
27632
29719
  }
27633
29720
  for (const p of points) {
27634
29721
  if (p.x < xMin || p.x > xMax || p.y < yMin || p.y > yMax) continue;
29722
+ const isOpen = p.style === "open";
27635
29723
  out.push({
27636
29724
  type: "circle",
27637
29725
  x: mapX(p.x),
27638
29726
  y: mapY(p.y),
27639
29727
  radius: p.radius ?? 4,
27640
29728
  color: p.color ?? "#dc2626",
27641
- fill: p.color ?? "#dc2626"
29729
+ fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
27642
29730
  });
27643
29731
  if (p.label) {
27644
29732
  out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: "#111827", fontSize: 12 });
@@ -27657,7 +29745,28 @@ var init_MathCanvas = __esm({
27657
29745
  }
27658
29746
  out.push(...shapes);
27659
29747
  return out;
27660
- }, [width, height, xMin, xMax, yMin, yMax, showAxes, showGrid, gridStep, curves, points, vectors, shapes]);
29748
+ }, [
29749
+ width,
29750
+ height,
29751
+ xMin,
29752
+ xMax,
29753
+ yMin,
29754
+ yMax,
29755
+ showAxes,
29756
+ showGrid,
29757
+ gridStep,
29758
+ showTickLabels,
29759
+ showCurveLabels,
29760
+ curves,
29761
+ points,
29762
+ vectors,
29763
+ regions,
29764
+ bars,
29765
+ guides,
29766
+ angles,
29767
+ hops,
29768
+ shapes
29769
+ ]);
27661
29770
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
27662
29771
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
27663
29772
  /* @__PURE__ */ jsx(
@@ -27666,6 +29775,8 @@ var init_MathCanvas = __esm({
27666
29775
  width,
27667
29776
  height,
27668
29777
  shapes: derivedShapes,
29778
+ readouts,
29779
+ traces,
27669
29780
  interactive,
27670
29781
  animate,
27671
29782
  onShapeClick,
@@ -27677,27 +29788,355 @@ var init_MathCanvas = __esm({
27677
29788
  };
27678
29789
  }
27679
29790
  });
27680
- var PhysicsCanvas;
29791
+ function formatMeterValue(v) {
29792
+ return Number.isInteger(v) ? String(v) : String(Number(v.toFixed(2)));
29793
+ }
29794
+ function sceneObjectShapes(obj, canvasWidth, canvasHeight) {
29795
+ const out = [];
29796
+ const color = obj.color ?? "#334155";
29797
+ switch (obj.kind) {
29798
+ case "ground": {
29799
+ const xStart = obj.x1 ?? 0;
29800
+ const xEnd = obj.x2 ?? canvasWidth;
29801
+ const y = obj.y ?? 0;
29802
+ out.push({ type: "line", x1: xStart, y1: y, x2: xEnd, y2: y, color, lineWidth: 2 });
29803
+ for (let hx = xStart + 7; hx <= xEnd; hx += 14) {
29804
+ out.push({ type: "line", x1: hx, y1: y, x2: hx - 7, y2: y + 7, color, lineWidth: 1 });
29805
+ }
29806
+ if (obj.label) {
29807
+ out.push({
29808
+ type: "text",
29809
+ x: (xStart + xEnd) / 2,
29810
+ y: y - 10,
29811
+ text: obj.label,
29812
+ color: PHYSICS_LABEL_COLOR,
29813
+ fontSize: 11,
29814
+ align: "center"
29815
+ });
29816
+ }
29817
+ break;
29818
+ }
29819
+ case "wall": {
29820
+ const yStart = obj.y1 ?? 0;
29821
+ const yEnd = obj.y2 ?? canvasHeight;
29822
+ const x = obj.x ?? 0;
29823
+ out.push({ type: "line", x1: x, y1: yStart, x2: x, y2: yEnd, color, lineWidth: 2 });
29824
+ for (let hy = yStart + 7; hy <= yEnd; hy += 14) {
29825
+ out.push({ type: "line", x1: x, y1: hy, x2: x - 7, y2: hy + 7, color, lineWidth: 1 });
29826
+ }
29827
+ if (obj.label) {
29828
+ out.push({
29829
+ type: "text",
29830
+ x: x + 12,
29831
+ y: (yStart + yEnd) / 2,
29832
+ text: obj.label,
29833
+ color: PHYSICS_LABEL_COLOR,
29834
+ fontSize: 11,
29835
+ align: "left"
29836
+ });
29837
+ }
29838
+ break;
29839
+ }
29840
+ case "ramp": {
29841
+ const x1 = obj.x1 ?? 0;
29842
+ const y1 = obj.y1 ?? 0;
29843
+ const x2 = obj.x2 ?? canvasWidth;
29844
+ const y2 = obj.y2 ?? canvasHeight;
29845
+ out.push({
29846
+ type: "polygon",
29847
+ points: [
29848
+ { x: x1, y: y1 },
29849
+ { x: x2, y: y2 },
29850
+ { x: x1, y: y2 }
29851
+ ],
29852
+ color,
29853
+ fill: obj.fill ?? "#e2e8f0",
29854
+ lineWidth: 2
29855
+ });
29856
+ if (obj.label) {
29857
+ out.push({
29858
+ type: "text",
29859
+ x: (2 * x1 + x2) / 3,
29860
+ y: (y1 + 2 * y2) / 3,
29861
+ text: obj.label,
29862
+ color: PHYSICS_LABEL_COLOR,
29863
+ fontSize: 11,
29864
+ align: "center"
29865
+ });
29866
+ }
29867
+ break;
29868
+ }
29869
+ case "box": {
29870
+ const x = obj.x ?? 0;
29871
+ const y = obj.y ?? 0;
29872
+ const w = obj.width ?? 40;
29873
+ const h = obj.height ?? 40;
29874
+ out.push({ type: "rect", x, y, width: w, height: h, color, fill: obj.fill, lineWidth: 2 });
29875
+ if (obj.label) {
29876
+ out.push({
29877
+ type: "text",
29878
+ x: x + w / 2,
29879
+ y: y + h / 2,
29880
+ text: obj.label,
29881
+ color: PHYSICS_LABEL_COLOR,
29882
+ fontSize: 11,
29883
+ align: "center"
29884
+ });
29885
+ }
29886
+ break;
29887
+ }
29888
+ case "pivot": {
29889
+ const x = obj.x ?? 0;
29890
+ const y = obj.y ?? 0;
29891
+ out.push({ type: "circle", x, y, radius: 5, color, fill: color });
29892
+ out.push({ type: "line", x1: x - 14, y1: y - 8, x2: x + 14, y2: y - 8, color, lineWidth: 1 });
29893
+ for (let k = 0; k < 5; k++) {
29894
+ const hx = x - 14 + 7 * k;
29895
+ out.push({ type: "line", x1: hx, y1: y - 8, x2: hx - 6, y2: y - 14, color, lineWidth: 1 });
29896
+ }
29897
+ if (obj.label) {
29898
+ out.push({
29899
+ type: "text",
29900
+ x,
29901
+ y: y - 20,
29902
+ text: obj.label,
29903
+ color: PHYSICS_LABEL_COLOR,
29904
+ fontSize: 11,
29905
+ align: "center"
29906
+ });
29907
+ }
29908
+ break;
29909
+ }
29910
+ }
29911
+ return out;
29912
+ }
29913
+ function trailShapes(trail) {
29914
+ const n = trail.points.length;
29915
+ if (n < 2) return [];
29916
+ const color = trail.color ?? "#94a3b8";
29917
+ const lineWidth = trail.width ?? 2;
29918
+ const fade = trail.fade ?? true;
29919
+ const globalOpacity = trail.opacity ?? 1;
29920
+ const out = [];
29921
+ for (let i = 0; i < n - 1; i++) {
29922
+ const a = trail.points[i];
29923
+ const b = trail.points[i + 1];
29924
+ const segmentOpacity = fade ? 0.12 + 0.68 * i / (n - 1) : 0.6;
29925
+ out.push({
29926
+ type: "line",
29927
+ x1: a.x,
29928
+ y1: a.y,
29929
+ x2: b.x,
29930
+ y2: b.y,
29931
+ color,
29932
+ lineWidth,
29933
+ opacity: segmentOpacity * globalOpacity
29934
+ });
29935
+ }
29936
+ return out;
29937
+ }
29938
+ function constraintShapes(c, a, b) {
29939
+ const color = c.color ?? "#9ca3af";
29940
+ const kind = c.kind ?? "rod";
29941
+ if (kind === "rod") {
29942
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2 }];
29943
+ }
29944
+ if (kind === "string") {
29945
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2, dash: "dashed" }];
29946
+ }
29947
+ const COILS = 8;
29948
+ const AMP = 7;
29949
+ const LEAD = 10;
29950
+ const dx = b.x - a.x;
29951
+ const dy = b.y - a.y;
29952
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
29953
+ const ux = dx / dist;
29954
+ const uy = dy / dist;
29955
+ const perpX = -uy;
29956
+ const perpY = ux;
29957
+ const aPrime = { x: a.x + LEAD * ux, y: a.y + LEAD * uy };
29958
+ const bPrime = { x: b.x - LEAD * ux, y: b.y - LEAD * uy };
29959
+ const m = 2 * COILS;
29960
+ const polyline = [{ x: a.x, y: a.y }, aPrime];
29961
+ for (let j = 1; j <= m; j++) {
29962
+ const t = j / (m + 1);
29963
+ const baseX = aPrime.x + t * (bPrime.x - aPrime.x);
29964
+ const baseY = aPrime.y + t * (bPrime.y - aPrime.y);
29965
+ const sign = j % 2 === 0 ? 1 : -1;
29966
+ polyline.push({ x: baseX + sign * AMP * perpX, y: baseY + sign * AMP * perpY });
29967
+ }
29968
+ polyline.push(bPrime, { x: b.x, y: b.y });
29969
+ const out = [];
29970
+ for (let i = 1; i < polyline.length; i++) {
29971
+ out.push({
29972
+ type: "line",
29973
+ x1: polyline[i - 1].x,
29974
+ y1: polyline[i - 1].y,
29975
+ x2: polyline[i].x,
29976
+ y2: polyline[i].y,
29977
+ color,
29978
+ lineWidth: 2
29979
+ });
29980
+ }
29981
+ return out;
29982
+ }
29983
+ function vectorShapes(v, bodyById) {
29984
+ let ax;
29985
+ let ay;
29986
+ if (v.body) {
29987
+ const anchor = bodyById.get(v.body);
29988
+ if (!anchor) return [];
29989
+ ax = anchor.x;
29990
+ ay = anchor.y;
29991
+ } else {
29992
+ ax = v.x ?? 0;
29993
+ ay = v.y ?? 0;
29994
+ }
29995
+ const scale = v.scale ?? 1;
29996
+ const color = v.color ?? "#dc2626";
29997
+ const tx = ax + v.dx * scale;
29998
+ const ty = ay + v.dy * scale;
29999
+ const out = [{ type: "arrow", x1: ax, y1: ay, x2: tx, y2: ty, color, lineWidth: 2, dash: v.dash }];
30000
+ if (v.label) {
30001
+ const dist = Math.max(1e-6, Math.hypot(tx - ax, ty - ay));
30002
+ const ux = (tx - ax) / dist;
30003
+ const uy = (ty - ay) / dist;
30004
+ out.push({
30005
+ type: "text",
30006
+ x: tx + 8 * ux,
30007
+ y: ty + 8 * uy,
30008
+ text: v.label,
30009
+ color,
30010
+ fontSize: 11,
30011
+ align: "center"
30012
+ });
30013
+ }
30014
+ return out;
30015
+ }
30016
+ function angleMarkerShapes(a) {
30017
+ const radius = a.radius ?? 26;
30018
+ const color = a.color ?? "#0ea5e9";
30019
+ const out = [
30020
+ {
30021
+ type: "ellipse",
30022
+ x: a.x,
30023
+ y: a.y,
30024
+ width: radius * 2,
30025
+ height: radius * 2,
30026
+ startAngle: a.from,
30027
+ endAngle: a.to,
30028
+ color,
30029
+ lineWidth: 2
30030
+ }
30031
+ ];
30032
+ if (a.label) {
30033
+ const mid = (a.from + a.to) / 2 * (Math.PI / 180);
30034
+ out.push({
30035
+ type: "text",
30036
+ x: a.x + (radius + 13) * Math.cos(mid),
30037
+ y: a.y + (radius + 13) * Math.sin(mid),
30038
+ text: a.label,
30039
+ color,
30040
+ fontSize: 11,
30041
+ align: "center"
30042
+ });
30043
+ }
30044
+ return out;
30045
+ }
30046
+ function fieldShapes(field, canvasWidth, canvasHeight) {
30047
+ const spacing = field.spacing ?? 48;
30048
+ const size = field.size ?? 14;
30049
+ const color = field.color ?? "#94a3b8";
30050
+ const regionX = field.x ?? 0;
30051
+ const regionY = field.y ?? 0;
30052
+ const regionW = field.width ?? canvasWidth;
30053
+ const regionH = field.height ?? canvasHeight;
30054
+ const out = [];
30055
+ for (let gx = regionX + spacing / 2; gx < regionX + regionW; gx += spacing) {
30056
+ for (let gy = regionY + spacing / 2; gy < regionY + regionH; gy += spacing) {
30057
+ if (field.kind === "arrows") {
30058
+ const rad = (field.angle ?? 0) * Math.PI / 180;
30059
+ const hx = Math.cos(rad) * size / 2;
30060
+ const hy = Math.sin(rad) * size / 2;
30061
+ out.push({ type: "arrow", x1: gx - hx, y1: gy - hy, x2: gx + hx, y2: gy + hy, color, lineWidth: 2 });
30062
+ } else if (field.kind === "into") {
30063
+ const r = size / 3;
30064
+ const d = 0.6 * r * Math.SQRT1_2;
30065
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30066
+ out.push({ type: "line", x1: gx - d, y1: gy - d, x2: gx + d, y2: gy + d, color, lineWidth: 1 });
30067
+ out.push({ type: "line", x1: gx - d, y1: gy + d, x2: gx + d, y2: gy - d, color, lineWidth: 1 });
30068
+ } else {
30069
+ const r = size / 3;
30070
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30071
+ out.push({ type: "circle", x: gx, y: gy, radius: 1.5, color, fill: color });
30072
+ }
30073
+ }
30074
+ }
30075
+ return out;
30076
+ }
30077
+ function meterShapes(meters, canvasHeight) {
30078
+ const n = meters.length;
30079
+ const out = [];
30080
+ const sharedMax = Math.max(1e-6, ...meters.map((m) => m.value));
30081
+ meters.forEach((meter, i) => {
30082
+ const rowY = canvasHeight - 10 - 16 * (n - i);
30083
+ const color = meter.color ?? "#3b82f6";
30084
+ const M = meter.max ?? sharedMax;
30085
+ const w = Math.round(Math.min(1, Math.max(0, meter.value / M)) * 110);
30086
+ out.push({ type: "text", x: 8, y: rowY + 8, text: meter.label, color: PHYSICS_LABEL_COLOR, fontSize: 10 });
30087
+ out.push({ type: "rect", x: 52, y: rowY, width: w, height: 10, color, fill: color });
30088
+ out.push({
30089
+ type: "text",
30090
+ x: 166,
30091
+ y: rowY + 8,
30092
+ text: formatMeterValue(meter.value),
30093
+ color: "#6b7280",
30094
+ fontSize: 9
30095
+ });
30096
+ });
30097
+ return out;
30098
+ }
30099
+ var physicsLog2, PHYSICS_LABEL_COLOR, PhysicsCanvas;
27681
30100
  var init_PhysicsCanvas = __esm({
27682
30101
  "components/learning/molecules/PhysicsCanvas.tsx"() {
27683
30102
  "use client";
27684
30103
  init_atoms();
27685
30104
  init_Stack();
27686
30105
  init_LearningCanvas();
30106
+ init_learningScene3D();
30107
+ physicsLog2 = createLogger("almadar:ui:physics-canvas");
30108
+ PHYSICS_LABEL_COLOR = "#374151";
27687
30109
  PhysicsCanvas = ({
27688
30110
  className,
27689
30111
  width = 600,
27690
30112
  height = 400,
27691
30113
  title,
27692
30114
  backgroundColor,
30115
+ mode = "2d",
30116
+ camera,
30117
+ lighting,
30118
+ post,
27693
30119
  bodies = [],
27694
30120
  constraints = [],
27695
30121
  showVelocity = true,
27696
30122
  showForces = false,
27697
30123
  velocityScale = 20,
27698
30124
  forceScale = 20,
30125
+ sceneObjects = [],
30126
+ trails = [],
30127
+ vectors = [],
30128
+ surface3d,
30129
+ vectors3d = [],
30130
+ vectorScale = 1,
30131
+ angles = [],
30132
+ field,
30133
+ meters = [],
27699
30134
  shapes = [],
27700
- interactive = false,
30135
+ readouts,
30136
+ traces,
30137
+ showGrid,
30138
+ shadows,
30139
+ interactive,
27701
30140
  animate = false,
27702
30141
  onShapeClick,
27703
30142
  isLoading,
@@ -27709,19 +30148,14 @@ var init_PhysicsCanvas = __esm({
27709
30148
  for (const b of bodies) {
27710
30149
  if (b.id) bodyById.set(b.id, b);
27711
30150
  }
30151
+ if (field) out.push(...fieldShapes(field, width, height));
30152
+ for (const obj of sceneObjects) out.push(...sceneObjectShapes(obj, width, height));
30153
+ for (const trail of trails) out.push(...trailShapes(trail));
27712
30154
  for (const c of constraints) {
27713
30155
  const a = bodyById.get(c.from);
27714
30156
  const b = bodyById.get(c.to);
27715
30157
  if (!a || !b) continue;
27716
- out.push({
27717
- type: "line",
27718
- x1: a.x,
27719
- y1: a.y,
27720
- x2: b.x,
27721
- y2: b.y,
27722
- color: c.color ?? "#9ca3af",
27723
- lineWidth: 2
27724
- });
30158
+ out.push(...constraintShapes(c, a, b));
27725
30159
  }
27726
30160
  for (const b of bodies) {
27727
30161
  out.push({
@@ -27766,9 +30200,181 @@ var init_PhysicsCanvas = __esm({
27766
30200
  });
27767
30201
  }
27768
30202
  }
30203
+ for (const v of vectors) out.push(...vectorShapes(v, bodyById));
30204
+ for (const a of angles) out.push(...angleMarkerShapes(a));
30205
+ if (meters.length > 0) out.push(...meterShapes(meters, height));
27769
30206
  out.push(...shapes);
27770
30207
  return out;
27771
- }, [bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes]);
30208
+ }, [
30209
+ bodies,
30210
+ constraints,
30211
+ showVelocity,
30212
+ showForces,
30213
+ velocityScale,
30214
+ forceScale,
30215
+ sceneObjects,
30216
+ trails,
30217
+ vectors,
30218
+ angles,
30219
+ field,
30220
+ meters,
30221
+ shapes,
30222
+ width,
30223
+ height
30224
+ ]);
30225
+ const drawables3D = useMemo(() => {
30226
+ if (mode !== "3d") return [];
30227
+ if (shapes.length > 0) {
30228
+ physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
30229
+ }
30230
+ if (sceneObjects.length > 0) {
30231
+ physicsLog2.debug("sceneObjects ignored in 3D mode (pixel-authored 2D vocabulary)", { count: sceneObjects.length });
30232
+ }
30233
+ if (vectors.length > 0) {
30234
+ physicsLog2.debug("vectors ignored in 3D mode (pixel-authored 2D vocabulary)", { count: vectors.length });
30235
+ }
30236
+ if (angles.length > 0) {
30237
+ physicsLog2.debug("angles ignored in 3D mode (pixel-authored 2D vocabulary)", { count: angles.length });
30238
+ }
30239
+ if (field) {
30240
+ physicsLog2.debug("field ignored in 3D mode (pixel-authored 2D vocabulary)");
30241
+ }
30242
+ if (meters.length > 0) {
30243
+ physicsLog2.debug("meters ignored in 3D mode (pixel-authored 2D vocabulary)", { count: meters.length });
30244
+ }
30245
+ if (animate) {
30246
+ physicsLog2.debug("animate ignored in 3D mode (motion is entity-state driven)");
30247
+ }
30248
+ const out = [];
30249
+ const labelColor = labelColorForBackground(backgroundColor);
30250
+ const bodyById = /* @__PURE__ */ new Map();
30251
+ for (const b of bodies) {
30252
+ if (b.id) bodyById.set(b.id, b);
30253
+ }
30254
+ for (const c of constraints) {
30255
+ const a = bodyById.get(c.from);
30256
+ const b = bodyById.get(c.to);
30257
+ if (!a || !b) continue;
30258
+ const rodRadius = Math.max(0.05, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.15);
30259
+ const rod = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], rodRadius, c.color ?? "#9ca3af");
30260
+ if (rod) out.push(rod);
30261
+ }
30262
+ for (const b of bodies) {
30263
+ const radius = b.radius ?? 0.5;
30264
+ const bz = b.z ?? 0;
30265
+ out.push(meshSphere(b.id, b.x, b.y, bz, radius, b.color ?? "#2563eb"));
30266
+ if (b.label) {
30267
+ out.push(billboardLabel(b.label, b.x, b.y, bz + radius, { color: labelColor }));
30268
+ }
30269
+ const vx = b.vx ?? 0;
30270
+ const vy = b.vy ?? 0;
30271
+ const vz = b.vz ?? 0;
30272
+ const arrowRadius = Math.max(0.05, radius * 0.15);
30273
+ if (showVelocity && (vx !== 0 || vy !== 0 || vz !== 0)) {
30274
+ const arrow = arrowBetween(
30275
+ [b.x, b.y, bz],
30276
+ [b.x + vx * velocityScale, b.y + vy * velocityScale, bz + vz * velocityScale],
30277
+ "#16a34a",
30278
+ arrowRadius
30279
+ );
30280
+ if (arrow) out.push(arrow);
30281
+ }
30282
+ const fx = b.fx ?? 0;
30283
+ const fy = b.fy ?? 0;
30284
+ const fz = b.fz ?? 0;
30285
+ if (showForces && (fx !== 0 || fy !== 0 || fz !== 0)) {
30286
+ const arrow = arrowBetween(
30287
+ [b.x, b.y, bz],
30288
+ [b.x + fx * forceScale, b.y + fy * forceScale, bz + fz * forceScale],
30289
+ "#dc2626",
30290
+ arrowRadius
30291
+ );
30292
+ if (arrow) out.push(arrow);
30293
+ }
30294
+ }
30295
+ for (const trail of trails) {
30296
+ if (trail.fade !== void 0) {
30297
+ physicsLog2.debug("trail.fade ignored in 3D mode (2D-only fade curve \u2014 3D draws an opaque tube)", { id: trail.id });
30298
+ }
30299
+ const points = trail.points.map((p) => [p.x, p.y, p.z ?? 0]);
30300
+ out.push(
30301
+ ...polylineTube(points, trail.width ?? 0.05, trail.color ?? "#94a3b8", {
30302
+ ...trail.opacity !== void 0 ? { opacity: trail.opacity } : {}
30303
+ })
30304
+ );
30305
+ }
30306
+ if (surface3d) {
30307
+ out.push(...heightFieldMesh(surface3d));
30308
+ }
30309
+ if (vectors3d.length > 0) {
30310
+ out.push(
30311
+ ...arrowField(
30312
+ vectors3d.map((v) => ({
30313
+ id: v.id,
30314
+ from: [v.x, v.y, v.z ?? 0],
30315
+ delta: [v.dx, v.dy, v.dz ?? 0],
30316
+ color: v.color,
30317
+ label: v.label,
30318
+ width: v.width
30319
+ })),
30320
+ { scale: vectorScale, labelColor }
30321
+ )
30322
+ );
30323
+ }
30324
+ return out;
30325
+ }, [
30326
+ mode,
30327
+ bodies,
30328
+ constraints,
30329
+ showVelocity,
30330
+ showForces,
30331
+ velocityScale,
30332
+ forceScale,
30333
+ shapes,
30334
+ sceneObjects,
30335
+ trails,
30336
+ vectors,
30337
+ surface3d,
30338
+ vectors3d,
30339
+ vectorScale,
30340
+ angles,
30341
+ field,
30342
+ meters,
30343
+ animate,
30344
+ backgroundColor
30345
+ ]);
30346
+ const bodyIndexById = useMemo(() => {
30347
+ const m = /* @__PURE__ */ new Map();
30348
+ vectors3d.forEach((v, i) => {
30349
+ if (v.id) m.set(v.id, i);
30350
+ });
30351
+ bodies.forEach((b, i) => {
30352
+ if (b.id) m.set(b.id, i);
30353
+ });
30354
+ return m;
30355
+ }, [bodies, vectors3d]);
30356
+ if (mode === "3d") {
30357
+ return /* @__PURE__ */ jsx(
30358
+ LearningScene3D,
30359
+ {
30360
+ className,
30361
+ width,
30362
+ height,
30363
+ title,
30364
+ backgroundColor,
30365
+ drawables: drawables3D,
30366
+ camera,
30367
+ lighting,
30368
+ post,
30369
+ showGrid,
30370
+ shadows,
30371
+ interactive,
30372
+ isLoading,
30373
+ error,
30374
+ onItemClick: get3DClickPayload(onShapeClick, bodyIndexById)
30375
+ }
30376
+ );
30377
+ }
27772
30378
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
27773
30379
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
27774
30380
  /* @__PURE__ */ jsx(
@@ -27778,7 +30384,9 @@ var init_PhysicsCanvas = __esm({
27778
30384
  height,
27779
30385
  backgroundColor,
27780
30386
  shapes: derivedShapes,
27781
- interactive,
30387
+ readouts,
30388
+ traces,
30389
+ interactive: interactive ?? false,
27782
30390
  animate,
27783
30391
  onShapeClick,
27784
30392
  isLoading,
@@ -27928,7 +30536,7 @@ function layoutFlow(nodeIds, adjacency, roots, width, height, margin) {
27928
30536
  }
27929
30537
  return nodeIds.map((id) => positions.get(id));
27930
30538
  }
27931
- function layoutTree(nodeIds, adjacency, roots, width, height, margin) {
30539
+ function layoutTree2(nodeIds, adjacency, roots, width, height, margin) {
27932
30540
  const effectiveRoots = roots.length > 0 ? roots : [nodeIds[0]];
27933
30541
  const layers = assignLayers(nodeIds, adjacency, effectiveRoots);
27934
30542
  const maxLayer = Math.max(...Array.from(layers.values()));
@@ -27984,7 +30592,7 @@ function computeStaticLayout(mode, input) {
27984
30592
  const adjacency = buildAdjacency(nodeIds, edges);
27985
30593
  const roots = findRoots(nodeIds, adjacency);
27986
30594
  if (mode === "flow") return layoutFlow(nodeIds, adjacency, roots, width, height, margin);
27987
- if (mode === "tree") return layoutTree(nodeIds, adjacency, roots, width, height, margin);
30595
+ if (mode === "tree") return layoutTree2(nodeIds, adjacency, roots, width, height, margin);
27988
30596
  return layoutRadial(nodeIds, adjacency, roots, width, height, margin);
27989
30597
  }
27990
30598
  var init_graphViewLayouts = __esm({
@@ -28396,12 +31004,12 @@ var init_MapView = __esm({
28396
31004
  shadowSize: [41, 41]
28397
31005
  });
28398
31006
  L.Marker.prototype.options.icon = defaultIcon;
28399
- const { useEffect: useEffect60, useRef: useRef62, useCallback: useCallback90, useState: useState92 } = React85__default;
31007
+ const { useEffect: useEffect60, useRef: useRef63, useCallback: useCallback90, useState: useState92 } = React85__default;
28400
31008
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
28401
31009
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
28402
31010
  function MapUpdater({ centerLat, centerLng, zoom }) {
28403
31011
  const map = useMap();
28404
- const prevRef = useRef62({ centerLat, centerLng, zoom });
31012
+ const prevRef = useRef63({ centerLat, centerLng, zoom });
28405
31013
  useEffect60(() => {
28406
31014
  const prev = prevRef.current;
28407
31015
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
@@ -29265,8 +31873,8 @@ function TableView({
29265
31873
  columns,
29266
31874
  fields,
29267
31875
  itemActions,
29268
- maxInlineActions,
29269
- itemClickEvent,
31876
+ maxInlineActions: _maxInlineActions,
31877
+ itemClickEvent = "",
29270
31878
  selectable = false,
29271
31879
  selectEvent,
29272
31880
  selectedIds,
@@ -29316,7 +31924,6 @@ function TableView({
29316
31924
  const hasMore = pageSize > 0 && visibleCount < ordered2.length;
29317
31925
  const hasRenderProp = typeof children === "function";
29318
31926
  const idField = dndItemIdField ?? "id";
29319
- const isCoarsePointer = useMediaQuery("(pointer: coarse)");
29320
31927
  React85__default.useEffect(() => {
29321
31928
  tableViewLog.debug("render", {
29322
31929
  rowCount: data.length,
@@ -29358,21 +31965,14 @@ function TableView({
29358
31965
  const dir = sortColumn === (col.field ?? col.key) && sortDirection === "asc" ? "desc" : "asc";
29359
31966
  eventBus.emit(`UI:${sortEvent}`, { column: col.field ?? col.key, direction: dir });
29360
31967
  };
29361
- const handleActionClick = (action, row) => (e) => {
29362
- e.stopPropagation();
29363
- const payload = {
29364
- id: row.id,
29365
- row
29366
- };
29367
- eventBus.emit(`UI:${action.event}`, payload);
29368
- };
31968
+ const rowClickEvent = itemClickEvent || actionDefs.find((a) => a.variant !== "danger")?.event;
29369
31969
  const handleRowClick = (row) => () => {
29370
- if (!itemClickEvent) return;
31970
+ if (!rowClickEvent) return;
29371
31971
  const payload = {
29372
31972
  id: row.id,
29373
31973
  row
29374
31974
  };
29375
- eventBus.emit(`UI:${itemClickEvent}`, payload);
31975
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
29376
31976
  };
29377
31977
  const colFloors = React85__default.useMemo(
29378
31978
  () => colDefs.map((col) => {
@@ -29388,10 +31988,7 @@ function TableView({
29388
31988
  const statusNode = isLoading ? /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) }) : error ? /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "error", children: error.message }) }) : data.length === 0 ? /* @__PURE__ */ jsx(Box, { className: "text-center py-12", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: emptyMessage || t("empty.noItems") }) }) : null;
29389
31989
  const lk = LOOKS[look];
29390
31990
  const hasActions = actionDefs.length > 0;
29391
- const effectiveMaxInline = isCoarsePointer ? 0 : maxInlineActions;
29392
- const inlineActionCount = hasActions ? effectiveMaxInline != null ? Math.min(actionDefs.length, effectiveMaxInline) : actionDefs.length : 0;
29393
- const hasOverflowActions = hasActions && effectiveMaxInline != null && actionDefs.length > effectiveMaxInline;
29394
- const actionsTrack = hasActions ? `${inlineActionCount * 6 + (hasOverflowActions ? 3 : 0)}rem` : null;
31991
+ const actionsTrack = hasActions ? "3rem" : null;
29395
31992
  const gridTemplateColumns = [
29396
31993
  selectable ? "auto" : null,
29397
31994
  ...colDefs.map((c, i) => c.width ?? `minmax(${colFloors[i]}ch, 1fr)`),
@@ -29438,7 +32035,7 @@ function TableView({
29438
32035
  col.key
29439
32036
  );
29440
32037
  }),
29441
- hasActions && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)]" })
32038
+ hasActions && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)] border-l border-[var(--color-border)] h-full" })
29442
32039
  ]
29443
32040
  }
29444
32041
  );
@@ -29450,12 +32047,12 @@ function TableView({
29450
32047
  role: "row",
29451
32048
  "data-entity-row": true,
29452
32049
  "data-entity-id": id,
29453
- onClick: itemClickEvent ? handleRowClick(row) : void 0,
32050
+ onClick: rowClickEvent ? handleRowClick(row) : void 0,
29454
32051
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
29455
32052
  className: cn(
29456
32053
  "group items-center gap-3 transition-colors duration-fast",
29457
32054
  hasRenderProp ? "flex" : "grid",
29458
- itemClickEvent && "cursor-pointer",
32055
+ rowClickEvent && "cursor-pointer",
29459
32056
  lk.rowPad,
29460
32057
  lk.divider && "border-b border-[var(--color-border)]",
29461
32058
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -29463,7 +32060,7 @@ function TableView({
29463
32060
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
29464
32061
  ),
29465
32062
  children: [
29466
- selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
32063
+ selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
29467
32064
  Checkbox,
29468
32065
  {
29469
32066
  checked: selected.has(id),
@@ -29484,53 +32081,37 @@ function TableView({
29484
32081
  }
29485
32082
  return /* @__PURE__ */ jsx(Box, { role: "cell", className: cellBase, children: /* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children: formatCell(raw, col.format) }) }, col.key);
29486
32083
  }),
29487
- hasActions && /* @__PURE__ */ jsxs(
32084
+ hasActions && /* @__PURE__ */ jsx(
29488
32085
  HStack,
29489
32086
  {
29490
32087
  gap: "xs",
29491
- onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
32088
+ onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0,
29492
32089
  className: cn(
29493
32090
  // Pinned: the fixed column tracks routinely overflow the caller's
29494
- // scroll container, which used to leave the actions off-screen.
29495
- // Opaque so scrolled cells pass underneath it.
32091
+ // scroll container, which would leave the kebab off-screen.
32092
+ // Opaque + hairline edge so it reads as a pinned column, not a
32093
+ // floating control, while scrolled cells pass underneath.
29496
32094
  "justify-end flex-shrink-0 sticky right-0 z-[1] transition-colors",
32095
+ "border-l border-[var(--color-border)]",
29497
32096
  lk.striped && index % 2 === 1 ? "bg-[var(--color-surface-subtle)]" : "bg-[var(--color-card)] group-hover:bg-[var(--color-surface-subtle)]"
29498
32097
  ),
29499
- children: [
29500
- (effectiveMaxInline != null ? actionDefs.slice(0, effectiveMaxInline) : actionDefs).map((action, i) => /* @__PURE__ */ jsxs(
29501
- Button,
29502
- {
29503
- variant: action.variant === "primary" ? "primary" : "ghost",
29504
- size: "sm",
29505
- onClick: handleActionClick(action, row),
29506
- "data-testid": `action-${action.event}`,
29507
- "data-row-id": String(row.id),
29508
- className: cn(action.variant === "danger" && "text-error hover:text-error hover:bg-error/10"),
29509
- children: [
29510
- action.icon && renderIconInput3(action.icon, { size: "xs", className: "mr-1" }),
29511
- action.label
29512
- ]
29513
- },
29514
- i
29515
- )),
29516
- effectiveMaxInline != null && actionDefs.length > effectiveMaxInline && /* @__PURE__ */ jsx(
29517
- Menu,
29518
- {
29519
- position: "bottom-end",
29520
- trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
29521
- items: actionDefs.slice(effectiveMaxInline).map((action) => ({
29522
- label: action.label,
29523
- icon: action.icon,
29524
- event: action.event,
29525
- variant: action.variant === "danger" ? "danger" : "default",
29526
- onClick: () => eventBus.emit(`UI:${action.event}`, {
29527
- id: row.id,
29528
- row
29529
- })
29530
- }))
29531
- }
29532
- )
29533
- ]
32098
+ children: /* @__PURE__ */ jsx(
32099
+ Menu,
32100
+ {
32101
+ position: "bottom-end",
32102
+ trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", "data-row-id": String(row.id), children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
32103
+ items: actionDefs.map((action) => ({
32104
+ label: action.label,
32105
+ icon: action.icon,
32106
+ event: action.event,
32107
+ variant: action.variant === "danger" ? "danger" : "default",
32108
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
32109
+ id: row.id,
32110
+ row
32111
+ })
32112
+ }))
32113
+ }
32114
+ )
29534
32115
  }
29535
32116
  )
29536
32117
  ]
@@ -29573,7 +32154,6 @@ var init_TableView = __esm({
29573
32154
  init_format();
29574
32155
  init_getNestedValue();
29575
32156
  init_useEventBus();
29576
- init_useMediaQuery();
29577
32157
  init_Box();
29578
32158
  init_Stack();
29579
32159
  init_Typography();
@@ -39066,7 +41646,7 @@ function getEnumOptions(field) {
39066
41646
  }));
39067
41647
  }
39068
41648
  const validation = field.validation;
39069
- if (validation?.enum && Array.isArray(validation.enum)) {
41649
+ if (validation?.enum && validation.enum.length > 0) {
39070
41650
  return validation.enum.map((v) => ({
39071
41651
  value: v,
39072
41652
  label: v.charAt(0).toUpperCase() + v.slice(1).replace(/_/g, " ")
@@ -44513,6 +47093,7 @@ var init_component_registry_generated = __esm({
44513
47093
  init_ActionTile();
44514
47094
  init_ActivationBlock();
44515
47095
  init_ComponentPatterns();
47096
+ init_AlgoGraphCanvas();
44516
47097
  init_AlgorithmCanvas();
44517
47098
  init_AnimatedCounter();
44518
47099
  init_AnimatedGraphic();
@@ -44776,6 +47357,7 @@ var init_component_registry_generated = __esm({
44776
47357
  "ActivationBlock": ActivationBlock,
44777
47358
  "Alert": AlertPattern,
44778
47359
  "AlertPattern": AlertPattern,
47360
+ "AlgoGraphCanvas": AlgoGraphCanvas,
44779
47361
  "AlgorithmCanvas": AlgorithmCanvas,
44780
47362
  "AnimatedCounter": AnimatedCounter,
44781
47363
  "AnimatedGraphic": AnimatedGraphic,
@@ -46102,9 +48684,9 @@ var log3 = createLogger("almadar:ui:effects:client-handlers");
46102
48684
  function createClientEffectHandlers(options) {
46103
48685
  const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
46104
48686
  return {
46105
- emit: (event, payload) => {
48687
+ emit: (event, payload, source) => {
46106
48688
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
46107
- eventBus.emit(prefixedEvent, payload);
48689
+ eventBus.emit(prefixedEvent, payload, source);
46108
48690
  },
46109
48691
  persist: async () => {
46110
48692
  log3.warn("persist is server-side only, ignored on client");
@@ -46439,7 +49021,7 @@ function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
46439
49021
  }
46440
49022
  };
46441
49023
  ctx.emit = (event, payload) => {
46442
- emit(event, payload);
49024
+ emit(event, payload, { trait: traitName, tick: tick.name });
46443
49025
  };
46444
49026
  if (tick.guard !== void 0 && !evaluateGuard(tick.guard, ctx)) {
46445
49027
  tickLog.debug("guard-blocked", { traitName, tick: tick.name, state: currentState });
@@ -46891,6 +49473,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
46891
49473
  }
46892
49474
  const effectContext = {
46893
49475
  traitName,
49476
+ orbitalName: orbitalsByTrait?.[traitName],
46894
49477
  state: previousState,
46895
49478
  transition: `${previousState}->${newState}`,
46896
49479
  linkedEntity,
@@ -46967,7 +49550,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
46967
49550
  });
46968
49551
  }
46969
49552
  return emittedDuringExec;
46970
- }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot]);
49553
+ }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot, orbitalsByTrait]);
46971
49554
  const runTickEffects = useCallback((tick, binding) => {
46972
49555
  const traitName = binding.trait.name;
46973
49556
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
@@ -47003,9 +49586,9 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
47003
49586
  log: tickLog
47004
49587
  });
47005
49588
  }, [executeTransitionEffects, sharedEntityStore]);
47006
- const emitFromSharedWriter = useCallback((event, payload) => {
49589
+ const emitFromSharedWriter = useCallback((event, payload, source) => {
47007
49590
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
47008
- eventBus.emit(prefixedEvent, payload);
49591
+ eventBus.emit(prefixedEvent, payload, source);
47009
49592
  }, [eventBus]);
47010
49593
  useEffect(() => {
47011
49594
  const scheduler = createTickScheduler();