@almadar/ui 5.149.0 → 5.151.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.
@@ -133,7 +133,7 @@ function useEventBus() {
133
133
  return {
134
134
  ...baseBus,
135
135
  emit: (type, payload, source) => {
136
- if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
136
+ if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".") && !source?.trait) {
137
137
  scopeLog.warn("emit:bare-key-no-scope", { type });
138
138
  }
139
139
  baseBus.emit(type, payload, source);
@@ -11572,6 +11572,14 @@ function shapeBounds(shape) {
11572
11572
  w: shape.radius * 2 + 8,
11573
11573
  h: shape.radius * 2 + 8
11574
11574
  };
11575
+ case "ellipse":
11576
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11577
+ return {
11578
+ x: shape.x - shape.width / 2 - 4,
11579
+ y: shape.y - shape.height / 2 - 4,
11580
+ w: shape.width + 8,
11581
+ h: shape.height + 8
11582
+ };
11575
11583
  case "rect":
11576
11584
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11577
11585
  return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
@@ -11605,13 +11613,14 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
11605
11613
  ctx.closePath();
11606
11614
  ctx.fill();
11607
11615
  }
11608
- function drawShape(ctx, shape, width, height) {
11616
+ function drawShape(ctx, shape, width, height, allShapes) {
11609
11617
  ctx.save();
11610
11618
  const opacity = shape.opacity ?? 1;
11611
11619
  ctx.globalAlpha = opacity;
11612
11620
  const stroke = resolveColor2(shape.color, ctx, "#333333");
11613
11621
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
11614
11622
  ctx.lineWidth = shape.lineWidth ?? 2;
11623
+ if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
11615
11624
  switch (shape.type) {
11616
11625
  case "grid": {
11617
11626
  const step = shape.step ?? 40;
@@ -11677,6 +11686,20 @@ function drawShape(ctx, shape, width, height) {
11677
11686
  ctx.stroke();
11678
11687
  break;
11679
11688
  }
11689
+ case "ellipse": {
11690
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11691
+ const startAngle = (shape.startAngle ?? 0) * Math.PI / 180;
11692
+ const endAngle = (shape.endAngle ?? 360) * Math.PI / 180;
11693
+ ctx.beginPath();
11694
+ ctx.ellipse(shape.x, shape.y, shape.width / 2, shape.height / 2, 0, startAngle, endAngle);
11695
+ if (fill) {
11696
+ ctx.fillStyle = fill;
11697
+ ctx.fill();
11698
+ }
11699
+ ctx.strokeStyle = stroke;
11700
+ ctx.stroke();
11701
+ break;
11702
+ }
11680
11703
  case "rect": {
11681
11704
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11682
11705
  if (fill) {
@@ -11723,21 +11746,153 @@ function drawShape(ctx, shape, width, height) {
11723
11746
  ctx.fillText(shape.text, shape.x, shape.y);
11724
11747
  break;
11725
11748
  }
11749
+ case "venn-region": {
11750
+ const resolveCircles = (ids) => (ids ?? []).flatMap((id) => {
11751
+ const c = allShapes.find((s) => s.type === "circle" && s.id === id);
11752
+ return c && c.x != null && c.y != null && c.radius != null ? [{ x: c.x, y: c.y, radius: c.radius }] : [];
11753
+ });
11754
+ const inside = resolveCircles(shape.inside);
11755
+ if (inside.length === 0) break;
11756
+ const outside = resolveCircles(shape.outside);
11757
+ const off = document.createElement("canvas");
11758
+ off.width = ctx.canvas.width;
11759
+ off.height = ctx.canvas.height;
11760
+ const octx = off.getContext("2d");
11761
+ if (!octx) break;
11762
+ octx.setTransform(ctx.getTransform());
11763
+ for (const c of inside) {
11764
+ const p = new Path2D();
11765
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11766
+ octx.clip(p);
11767
+ }
11768
+ octx.fillStyle = fill ?? stroke;
11769
+ octx.fillRect(0, 0, width, height);
11770
+ octx.globalCompositeOperation = "destination-out";
11771
+ for (const c of outside) {
11772
+ const p = new Path2D();
11773
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11774
+ octx.fill(p);
11775
+ }
11776
+ ctx.save();
11777
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
11778
+ ctx.drawImage(off, 0, 0);
11779
+ ctx.restore();
11780
+ break;
11781
+ }
11726
11782
  }
11727
11783
  ctx.restore();
11728
11784
  }
11729
- var LearningCanvas;
11785
+ function readoutShapes(readouts, width) {
11786
+ const out = [];
11787
+ const chipH = 18;
11788
+ const gap = 6;
11789
+ let rightEdge = width - 6;
11790
+ let rowY = 6;
11791
+ for (const readout of readouts) {
11792
+ const text = `${readout.label}: ${String(readout.value)}`;
11793
+ const chipW = Math.min(170, Math.max(34, text.length * 6 + 12));
11794
+ let chipX = rightEdge - chipW;
11795
+ if (chipX < 4) {
11796
+ rowY += chipH + 4;
11797
+ rightEdge = width - 6;
11798
+ chipX = rightEdge - chipW;
11799
+ }
11800
+ const color = readout.color ?? "#334155";
11801
+ out.push({ type: "rect", x: chipX, y: rowY, width: chipW, height: chipH, color, fill: color });
11802
+ out.push({
11803
+ type: "text",
11804
+ x: chipX + chipW / 2,
11805
+ y: rowY + chipH / 2,
11806
+ text,
11807
+ color: "#ffffff",
11808
+ fontSize: 10,
11809
+ align: "center"
11810
+ });
11811
+ rightEdge = chipX - gap;
11812
+ }
11813
+ return out;
11814
+ }
11815
+ function traceShapes(panel, k, width, height) {
11816
+ const w = panel.width ?? Math.round(width * 0.32);
11817
+ const h = panel.height ?? Math.round(height * 0.28);
11818
+ const x = panel.x ?? width - w - 8;
11819
+ const y = panel.y ?? height - h - 8 - k * (h + 8);
11820
+ const allSamples = panel.series.flatMap((series) => series.samples);
11821
+ let xLo = Math.min(...allSamples.map((p) => p.x));
11822
+ let xHi = Math.max(...allSamples.map((p) => p.x));
11823
+ let yLo = Math.min(...allSamples.map((p) => p.y));
11824
+ let yHi = Math.max(...allSamples.map((p) => p.y));
11825
+ if (xLo === xHi) {
11826
+ xLo -= 1;
11827
+ xHi += 1;
11828
+ }
11829
+ if (yLo === yHi) {
11830
+ yLo -= 1;
11831
+ yHi += 1;
11832
+ }
11833
+ const backgroundColor = panel.backgroundColor ?? "#ffffff";
11834
+ const frameColor = panel.frameColor ?? "#94a3b8";
11835
+ const out = [];
11836
+ out.push({
11837
+ type: "rect",
11838
+ x,
11839
+ y,
11840
+ width: w,
11841
+ height: h,
11842
+ color: backgroundColor,
11843
+ fill: backgroundColor,
11844
+ opacity: panel.backgroundOpacity ?? 0.85
11845
+ });
11846
+ out.push({ type: "rect", x, y, width: w, height: h, color: frameColor, lineWidth: 1 });
11847
+ panel.series.forEach((series, j) => {
11848
+ const color = series.color ?? TRACE_SERIES_COLORS[j % TRACE_SERIES_COLORS.length];
11849
+ const mapped = series.samples.map((p) => ({
11850
+ x: x + 4 + (p.x - xLo) / (xHi - xLo) * (w - 8),
11851
+ y: y + h - 4 - (p.y - yLo) / (yHi - yLo) * (h - 8)
11852
+ }));
11853
+ for (let i = 1; i < mapped.length; i++) {
11854
+ out.push({
11855
+ type: "line",
11856
+ x1: mapped[i - 1].x,
11857
+ y1: mapped[i - 1].y,
11858
+ x2: mapped[i].x,
11859
+ y2: mapped[i].y,
11860
+ color,
11861
+ lineWidth: 1.5
11862
+ });
11863
+ }
11864
+ if (mapped.length > 0) {
11865
+ const last = mapped[mapped.length - 1];
11866
+ out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
11867
+ }
11868
+ if (series.label) {
11869
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
11870
+ }
11871
+ });
11872
+ if (panel.yLabel) {
11873
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
11874
+ }
11875
+ if (panel.xLabel) {
11876
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
11877
+ }
11878
+ return out;
11879
+ }
11880
+ var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
11730
11881
  var init_LearningCanvas = __esm({
11731
11882
  "components/learning/atoms/LearningCanvas.tsx"() {
11732
11883
  "use client";
11733
11884
  init_cn();
11734
11885
  init_useEventBus();
11886
+ DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
11887
+ TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
11735
11888
  LearningCanvas = ({
11736
11889
  className,
11737
11890
  width = 600,
11738
11891
  height = 400,
11739
11892
  backgroundColor,
11740
11893
  shapes = [],
11894
+ readouts,
11895
+ traces,
11741
11896
  interactive = false,
11742
11897
  animate = false,
11743
11898
  onShapeClick,
@@ -11763,6 +11918,12 @@ var init_LearningCanvas = __esm({
11763
11918
  }
11764
11919
  return -1;
11765
11920
  }, [shapes]);
11921
+ const derivedShapes = useMemo(() => {
11922
+ if (!traces?.length && !readouts?.length) return shapes;
11923
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
11924
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
11925
+ return [...shapes, ...traceOut, ...readoutOut];
11926
+ }, [shapes, traces, readouts, width, height]);
11766
11927
  const draw = useCallback(() => {
11767
11928
  const canvas = canvasRef.current;
11768
11929
  if (!canvas) return;
@@ -11779,13 +11940,13 @@ var init_LearningCanvas = __esm({
11779
11940
  ctx.fillStyle = backgroundColor;
11780
11941
  ctx.fillRect(0, 0, width, height);
11781
11942
  }
11782
- for (const shape of shapes) {
11783
- if (shape.type !== "text") drawShape(ctx, shape, width, height);
11943
+ for (const shape of derivedShapes) {
11944
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
11784
11945
  }
11785
- for (const shape of shapes) {
11786
- if (shape.type === "text") drawShape(ctx, shape, width, height);
11946
+ for (const shape of derivedShapes) {
11947
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
11787
11948
  }
11788
- }, [width, height, backgroundColor, shapes]);
11949
+ }, [width, height, backgroundColor, derivedShapes]);
11789
11950
  useEffect(() => {
11790
11951
  draw();
11791
11952
  }, [draw]);
@@ -13718,7 +13879,363 @@ var init_ComponentPatterns = __esm({
13718
13879
  AlertPattern.displayName = "AlertPattern";
13719
13880
  }
13720
13881
  });
13721
- var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, AlgorithmCanvas;
13882
+ function layoutCircle(nodes, width, height) {
13883
+ const cx = width / 2;
13884
+ const cy = height / 2;
13885
+ const radius = Math.max(10, Math.min(cx, cy) - 40);
13886
+ const positions = /* @__PURE__ */ new Map();
13887
+ const n = nodes.length;
13888
+ nodes.forEach((node, i) => {
13889
+ const angle = 2 * Math.PI * i / Math.max(n, 1) - Math.PI / 2;
13890
+ positions.set(node.id, { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) });
13891
+ });
13892
+ return positions;
13893
+ }
13894
+ function layoutTree(nodes, edges, root, width, height) {
13895
+ const nodeIds = nodes.map((n) => n.id);
13896
+ const idSet = new Set(nodeIds);
13897
+ const childrenOf = /* @__PURE__ */ new Map();
13898
+ const hasIncoming = /* @__PURE__ */ new Set();
13899
+ for (const e of edges) {
13900
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
13901
+ const list = childrenOf.get(e.from) ?? [];
13902
+ list.push(e.to);
13903
+ childrenOf.set(e.from, list);
13904
+ hasIncoming.add(e.to);
13905
+ }
13906
+ const depth = /* @__PURE__ */ new Map();
13907
+ const treeChildren = /* @__PURE__ */ new Map();
13908
+ const visited = /* @__PURE__ */ new Set();
13909
+ const bfsFrom = (start) => {
13910
+ if (visited.has(start)) return;
13911
+ visited.add(start);
13912
+ depth.set(start, 0);
13913
+ const queue = [start];
13914
+ while (queue.length > 0) {
13915
+ const u = queue.shift();
13916
+ for (const v of childrenOf.get(u) ?? []) {
13917
+ if (visited.has(v)) continue;
13918
+ visited.add(v);
13919
+ depth.set(v, (depth.get(u) ?? 0) + 1);
13920
+ const list = treeChildren.get(u) ?? [];
13921
+ list.push(v);
13922
+ treeChildren.set(u, list);
13923
+ queue.push(v);
13924
+ }
13925
+ }
13926
+ };
13927
+ const primaryRoot = root && idSet.has(root) ? root : nodeIds.find((id) => !hasIncoming.has(id)) ?? nodeIds[0];
13928
+ const rootsOrder = [];
13929
+ if (primaryRoot !== void 0) {
13930
+ bfsFrom(primaryRoot);
13931
+ rootsOrder.push(primaryRoot);
13932
+ }
13933
+ for (const id of nodeIds) {
13934
+ if (!visited.has(id)) {
13935
+ bfsFrom(id);
13936
+ rootsOrder.push(id);
13937
+ }
13938
+ }
13939
+ let leafCounter = 0;
13940
+ const xSlot = /* @__PURE__ */ new Map();
13941
+ const assignXSlot = (u) => {
13942
+ const children = treeChildren.get(u) ?? [];
13943
+ if (children.length === 0) {
13944
+ const slot = leafCounter++;
13945
+ xSlot.set(u, slot);
13946
+ return slot;
13947
+ }
13948
+ const childSlots = children.map(assignXSlot);
13949
+ const avg = childSlots.reduce((a, b) => a + b, 0) / childSlots.length;
13950
+ xSlot.set(u, avg);
13951
+ return avg;
13952
+ };
13953
+ for (const r of rootsOrder) assignXSlot(r);
13954
+ let maxDepth = 0;
13955
+ for (const d of depth.values()) maxDepth = Math.max(maxDepth, d);
13956
+ const colWidth = width / Math.max(1, leafCounter);
13957
+ const rowHeight = height / (maxDepth + 1);
13958
+ const positions = /* @__PURE__ */ new Map();
13959
+ for (const id of nodeIds) {
13960
+ const slot = xSlot.get(id) ?? 0;
13961
+ const d = depth.get(id) ?? 0;
13962
+ positions.set(id, { x: slot * colWidth + colWidth / 2, y: d * rowHeight + rowHeight / 2 });
13963
+ }
13964
+ return positions;
13965
+ }
13966
+ function layoutLayered(nodes, edges, width, height) {
13967
+ const nodeIds = nodes.map((n) => n.id);
13968
+ const idSet = new Set(nodeIds);
13969
+ const adj = /* @__PURE__ */ new Map();
13970
+ const remainingIndegree = /* @__PURE__ */ new Map();
13971
+ for (const id of nodeIds) remainingIndegree.set(id, 0);
13972
+ for (const e of edges) {
13973
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
13974
+ const list = adj.get(e.from) ?? [];
13975
+ list.push(e.to);
13976
+ adj.set(e.from, list);
13977
+ remainingIndegree.set(e.to, (remainingIndegree.get(e.to) ?? 0) + 1);
13978
+ }
13979
+ const layer = /* @__PURE__ */ new Map();
13980
+ const dequeued = /* @__PURE__ */ new Set();
13981
+ const queue = [];
13982
+ for (const id of nodeIds) {
13983
+ if ((remainingIndegree.get(id) ?? 0) === 0) {
13984
+ layer.set(id, 0);
13985
+ queue.push(id);
13986
+ }
13987
+ }
13988
+ while (queue.length > 0) {
13989
+ const u = queue.shift();
13990
+ dequeued.add(u);
13991
+ for (const v of adj.get(u) ?? []) {
13992
+ const candidate = (layer.get(u) ?? 0) + 1;
13993
+ layer.set(v, Math.max(layer.get(v) ?? 0, candidate));
13994
+ remainingIndegree.set(v, (remainingIndegree.get(v) ?? 0) - 1);
13995
+ if ((remainingIndegree.get(v) ?? 0) === 0 && !dequeued.has(v)) {
13996
+ queue.push(v);
13997
+ }
13998
+ }
13999
+ }
14000
+ let baseMaxLayer = 0;
14001
+ for (const id of nodeIds) {
14002
+ if (dequeued.has(id)) baseMaxLayer = Math.max(baseMaxLayer, layer.get(id) ?? 0);
14003
+ }
14004
+ const cycleLayer = baseMaxLayer + 1;
14005
+ let maxLayer = baseMaxLayer;
14006
+ for (const id of nodeIds) {
14007
+ if (!dequeued.has(id)) {
14008
+ layer.set(id, cycleLayer);
14009
+ maxLayer = cycleLayer;
14010
+ }
14011
+ }
14012
+ const colWidth = width / Math.max(1, maxLayer + 1);
14013
+ const byLayer = /* @__PURE__ */ new Map();
14014
+ for (const id of nodeIds) {
14015
+ const l = layer.get(id) ?? 0;
14016
+ const list = byLayer.get(l) ?? [];
14017
+ list.push(id);
14018
+ byLayer.set(l, list);
14019
+ }
14020
+ const positions = /* @__PURE__ */ new Map();
14021
+ for (const [l, ids] of byLayer) {
14022
+ const rowHeight = height / ids.length;
14023
+ ids.forEach((id, i) => {
14024
+ positions.set(id, { x: l * colWidth + colWidth / 2, y: i * rowHeight + rowHeight / 2 });
14025
+ });
14026
+ }
14027
+ return positions;
14028
+ }
14029
+ function computePositions(nodes, edges, layout, root, width, height) {
14030
+ switch (layout) {
14031
+ case "circle":
14032
+ return layoutCircle(nodes, width, height);
14033
+ case "tree":
14034
+ return layoutTree(nodes, edges, root, width, height);
14035
+ case "layered":
14036
+ return layoutLayered(nodes, edges, width, height);
14037
+ case "manual":
14038
+ default: {
14039
+ const positions = /* @__PURE__ */ new Map();
14040
+ for (const n of nodes) positions.set(n.id, { x: n.x ?? 0, y: n.y ?? 0 });
14041
+ return positions;
14042
+ }
14043
+ }
14044
+ }
14045
+ var NODE_STATE_COLOR, EDGE_STATE_COLOR, DEFAULT_NODE_RADIUS, AlgoGraphCanvas;
14046
+ var init_AlgoGraphCanvas = __esm({
14047
+ "components/learning/molecules/AlgoGraphCanvas.tsx"() {
14048
+ "use client";
14049
+ init_atoms();
14050
+ init_Stack();
14051
+ init_LearningCanvas();
14052
+ NODE_STATE_COLOR = {
14053
+ unvisited: "#cbd5e1",
14054
+ frontier: "#f59e0b",
14055
+ current: "#ef4444",
14056
+ visited: "#22c55e",
14057
+ goal: "#8b5cf6",
14058
+ path: "#0ea5e9"
14059
+ };
14060
+ EDGE_STATE_COLOR = {
14061
+ default: "#9ca3af",
14062
+ tree: "#16a34a",
14063
+ relaxed: "#f59e0b",
14064
+ candidate: "#38bdf8",
14065
+ path: "#dc2626"
14066
+ };
14067
+ DEFAULT_NODE_RADIUS = 18;
14068
+ AlgoGraphCanvas = ({
14069
+ className,
14070
+ width = 600,
14071
+ height = 400,
14072
+ title,
14073
+ backgroundColor,
14074
+ nodes = [],
14075
+ edges = [],
14076
+ layout = "manual",
14077
+ root,
14078
+ shapes = [],
14079
+ interactive = false,
14080
+ animate = false,
14081
+ onShapeClick,
14082
+ onNodeClick,
14083
+ isLoading,
14084
+ error
14085
+ }) => {
14086
+ const nodeById = useMemo(() => {
14087
+ const m = /* @__PURE__ */ new Map();
14088
+ for (const n of nodes) m.set(n.id, n);
14089
+ return m;
14090
+ }, [nodes]);
14091
+ const nodeIndexById = useMemo(() => {
14092
+ const m = /* @__PURE__ */ new Map();
14093
+ nodes.forEach((n, i) => m.set(n.id, i));
14094
+ return m;
14095
+ }, [nodes]);
14096
+ const derivedShapes = useMemo(() => {
14097
+ const out = [];
14098
+ const positions = computePositions(nodes, edges, layout, root, width, height);
14099
+ const edgeGeoms = [];
14100
+ for (const e of edges) {
14101
+ const a = nodeById.get(e.from);
14102
+ const b = nodeById.get(e.to);
14103
+ const posA = positions.get(e.from);
14104
+ const posB = positions.get(e.to);
14105
+ if (!a || !b || !posA || !posB) continue;
14106
+ const rA = a.radius ?? DEFAULT_NODE_RADIUS;
14107
+ const rB = b.radius ?? DEFAULT_NODE_RADIUS;
14108
+ const dx = posB.x - posA.x;
14109
+ const dy = posB.y - posA.y;
14110
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
14111
+ const ux = dx / dist;
14112
+ const uy = dy / dist;
14113
+ edgeGeoms.push({
14114
+ directed: e.directed ?? false,
14115
+ x1: posA.x + ux * rA,
14116
+ y1: posA.y + uy * rA,
14117
+ x2: posB.x - ux * rB,
14118
+ y2: posB.y - uy * rB,
14119
+ color: e.color ?? EDGE_STATE_COLOR[e.state ?? "default"],
14120
+ label: e.label ?? (e.weight != null ? String(e.weight) : void 0)
14121
+ });
14122
+ }
14123
+ for (const g of edgeGeoms) {
14124
+ out.push({
14125
+ type: g.directed ? "arrow" : "line",
14126
+ x1: g.x1,
14127
+ y1: g.y1,
14128
+ x2: g.x2,
14129
+ y2: g.y2,
14130
+ color: g.color,
14131
+ lineWidth: 2
14132
+ });
14133
+ }
14134
+ for (const g of edgeGeoms) {
14135
+ if (g.label === void 0) continue;
14136
+ const midX = (g.x1 + g.x2) / 2;
14137
+ const midY = (g.y1 + g.y2) / 2;
14138
+ const dx = g.x2 - g.x1;
14139
+ const dy = g.y2 - g.y1;
14140
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
14141
+ const perpX = -(dy / dist);
14142
+ const perpY = dx / dist;
14143
+ out.push({
14144
+ type: "text",
14145
+ x: midX + perpX * 10,
14146
+ y: midY + perpY * 10,
14147
+ text: g.label,
14148
+ fontSize: 11,
14149
+ align: "center",
14150
+ color: "#374151"
14151
+ });
14152
+ }
14153
+ const nodeGeoms = [];
14154
+ for (const n of nodes) {
14155
+ const pos = positions.get(n.id);
14156
+ if (!pos) continue;
14157
+ nodeGeoms.push({
14158
+ id: n.id,
14159
+ x: pos.x,
14160
+ y: pos.y,
14161
+ radius: n.radius ?? DEFAULT_NODE_RADIUS,
14162
+ color: n.color ?? NODE_STATE_COLOR[n.state ?? "unvisited"],
14163
+ label: n.label,
14164
+ badge: n.badge
14165
+ });
14166
+ }
14167
+ for (const g of nodeGeoms) {
14168
+ out.push({ type: "circle", id: g.id, x: g.x, y: g.y, radius: g.radius, color: g.color, fill: `${g.color}33` });
14169
+ }
14170
+ const badgeGeoms = [];
14171
+ for (const g of nodeGeoms) {
14172
+ if (!g.badge) continue;
14173
+ const w = Math.min(42, Math.max(18, g.badge.text.length * 6 + 10));
14174
+ badgeGeoms.push({
14175
+ cx: g.x + g.radius * 0.75,
14176
+ cy: g.y - g.radius * 0.75,
14177
+ w,
14178
+ h: 14,
14179
+ // Borderless pill: same color drives both stroke and fill.
14180
+ color: g.badge.color ?? "#1e293b",
14181
+ text: g.badge.text
14182
+ });
14183
+ }
14184
+ for (const b of badgeGeoms) {
14185
+ out.push({ type: "rect", x: b.cx - b.w / 2, y: b.cy - b.h / 2, width: b.w, height: b.h, color: b.color, fill: b.color });
14186
+ }
14187
+ for (const b of badgeGeoms) {
14188
+ out.push({ type: "text", x: b.cx, y: b.cy, text: b.text, fontSize: 9, align: "center", color: "#ffffff" });
14189
+ }
14190
+ for (const g of nodeGeoms) {
14191
+ if (g.label === void 0) continue;
14192
+ out.push({
14193
+ type: "text",
14194
+ x: g.x,
14195
+ y: g.y + g.radius + 14,
14196
+ text: g.label,
14197
+ fontSize: 12,
14198
+ align: "center",
14199
+ color: "#111827"
14200
+ });
14201
+ }
14202
+ out.push(...shapes);
14203
+ return out;
14204
+ }, [nodes, edges, layout, root, width, height, nodeById, shapes]);
14205
+ const handleShapeClick = useCallback(
14206
+ (payload) => {
14207
+ if (payload.type === "circle" && payload.id) {
14208
+ const node = nodeById.get(payload.id);
14209
+ const idx = nodeIndexById.get(payload.id);
14210
+ if (node && idx !== void 0) {
14211
+ onNodeClick?.({ id: node.id, label: node.label, index: idx });
14212
+ }
14213
+ }
14214
+ onShapeClick?.(payload);
14215
+ },
14216
+ [nodeById, nodeIndexById, onNodeClick, onShapeClick]
14217
+ );
14218
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
14219
+ title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
14220
+ /* @__PURE__ */ jsx(
14221
+ LearningCanvas,
14222
+ {
14223
+ width,
14224
+ height,
14225
+ backgroundColor,
14226
+ shapes: derivedShapes,
14227
+ interactive,
14228
+ animate,
14229
+ onShapeClick: onShapeClick || onNodeClick ? handleShapeClick : void 0,
14230
+ isLoading,
14231
+ error
14232
+ }
14233
+ )
14234
+ ] }) });
14235
+ };
14236
+ }
14237
+ });
14238
+ var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, PANEL_FAMILY_ORDER, RANGE_COLOR_DEFAULT, RANGE_FILL_OPACITY, BRACKET_TOP_OFFSET, BRACKET_ROW_H, BRACKET_TICK_H, BRACKET_LABEL_OFFSET, SLOT_EMPTY_FILL, SLOT_EMPTY_STROKE, SLOT_FILLED_STROKE, SLOT_HIGHLIGHT_DEFAULT, SLOT_VALUE_TEXT_COLOR, FRAME_ACTIVE_COLOR, FRAME_RETURNING_COLOR, FRAME_DONE_COLOR, FRAME_LABEL_COLOR, FRAME_DETAIL_COLOR, FRAME_TWO_LINE_MIN_H, BUCKET_INDEX_FILL, BUCKET_INDEX_STROKE, BUCKET_INDEX_TEXT, BUCKET_ENTRY_TEXT, BUCKET_ENTRY_DEFAULT, BUCKET_ENTRY_HIGHLIGHT, BUCKET_ENTRY_PROBING, BUCKET_ENTRY_MIN_W, BUCKET_ENTRY_MAX_W, AXIS_LABEL_COLOR, AXIS_LABEL_FONT_SIZE, CORNER_TEXT_COLOR, CORNER_FONT_SIZE, CORNER_MIN_CELL, CORNER_INSET_X, CORNER_INSET_Y, AUX_PRIMARY_RATIO, AUX_LABEL_BAND, AUX_BASELINE_PAD, AlgorithmCanvas;
13722
14239
  var init_AlgorithmCanvas = __esm({
13723
14240
  "components/learning/molecules/AlgorithmCanvas.tsx"() {
13724
14241
  "use client";
@@ -13730,6 +14247,43 @@ var init_AlgorithmCanvas = __esm({
13730
14247
  DEFAULT_POINTER_COLOR = "#dc2626";
13731
14248
  POINTER_BAND = 34;
13732
14249
  TOP_PAD = 26;
14250
+ PANEL_FAMILY_ORDER = ["bars", "slots", "cells", "buckets", "frames"];
14251
+ RANGE_COLOR_DEFAULT = "#3b82f6";
14252
+ RANGE_FILL_OPACITY = 0.15;
14253
+ BRACKET_TOP_OFFSET = 16;
14254
+ BRACKET_ROW_H = 14;
14255
+ BRACKET_TICK_H = 6;
14256
+ BRACKET_LABEL_OFFSET = 6;
14257
+ SLOT_EMPTY_FILL = "#f1f5f9";
14258
+ SLOT_EMPTY_STROKE = "#cbd5e1";
14259
+ SLOT_FILLED_STROKE = "#9ca3af";
14260
+ SLOT_HIGHLIGHT_DEFAULT = "#f59e0b";
14261
+ SLOT_VALUE_TEXT_COLOR = "#ffffff";
14262
+ FRAME_ACTIVE_COLOR = "#3b82f6";
14263
+ FRAME_RETURNING_COLOR = "#f59e0b";
14264
+ FRAME_DONE_COLOR = "#94a3b8";
14265
+ FRAME_LABEL_COLOR = "#ffffff";
14266
+ FRAME_DETAIL_COLOR = "#e2e8f0";
14267
+ FRAME_TWO_LINE_MIN_H = 22;
14268
+ BUCKET_INDEX_FILL = "#e2e8f0";
14269
+ BUCKET_INDEX_STROKE = "#9ca3af";
14270
+ BUCKET_INDEX_TEXT = "#374151";
14271
+ BUCKET_ENTRY_TEXT = "#ffffff";
14272
+ BUCKET_ENTRY_DEFAULT = "#3b82f6";
14273
+ BUCKET_ENTRY_HIGHLIGHT = "#f59e0b";
14274
+ BUCKET_ENTRY_PROBING = "#38bdf8";
14275
+ BUCKET_ENTRY_MIN_W = 24;
14276
+ BUCKET_ENTRY_MAX_W = 64;
14277
+ AXIS_LABEL_COLOR = "#6b7280";
14278
+ AXIS_LABEL_FONT_SIZE = 10;
14279
+ CORNER_TEXT_COLOR = "#111827";
14280
+ CORNER_FONT_SIZE = 7;
14281
+ CORNER_MIN_CELL = 28;
14282
+ CORNER_INSET_X = 3;
14283
+ CORNER_INSET_Y = 6;
14284
+ AUX_PRIMARY_RATIO = 0.6;
14285
+ AUX_LABEL_BAND = 18;
14286
+ AUX_BASELINE_PAD = 8;
13733
14287
  AlgorithmCanvas = ({
13734
14288
  className,
13735
14289
  width = 600,
@@ -13739,6 +14293,14 @@ var init_AlgorithmCanvas = __esm({
13739
14293
  bars = [],
13740
14294
  cells = [],
13741
14295
  pointers = [],
14296
+ ranges = [],
14297
+ slots = [],
14298
+ slotOrientation = "horizontal",
14299
+ frames = [],
14300
+ buckets = [],
14301
+ auxBars = [],
14302
+ rowLabels = [],
14303
+ colLabels = [],
13742
14304
  shapes = [],
13743
14305
  interactive = false,
13744
14306
  animate = false,
@@ -13748,12 +14310,35 @@ var init_AlgorithmCanvas = __esm({
13748
14310
  }) => {
13749
14311
  const derivedShapes = useMemo(() => {
13750
14312
  const out = [];
14313
+ const presence = {
14314
+ bars: bars.length > 0,
14315
+ slots: slots.length > 0,
14316
+ cells: cells.length > 0,
14317
+ buckets: buckets.length > 0,
14318
+ frames: frames.length > 0
14319
+ };
14320
+ const panelCount = PANEL_FAMILY_ORDER.filter((f3) => presence[f3]).length;
14321
+ const panelHeight = height / Math.max(1, panelCount);
14322
+ const panelY = { bars: 0, slots: 0, cells: 0, buckets: 0, frames: 0 };
14323
+ let compactIndex = 0;
14324
+ PANEL_FAMILY_ORDER.forEach((f3) => {
14325
+ if (presence[f3]) {
14326
+ panelY[f3] = compactIndex * panelHeight;
14327
+ compactIndex += 1;
14328
+ }
14329
+ });
13751
14330
  if (bars.length > 0) {
14331
+ const panelYBars = panelY.bars;
13752
14332
  const slot = width / bars.length;
13753
14333
  const barW = slot * 0.8;
13754
14334
  const gap = slot * 0.1;
13755
- const baseline = height - POINTER_BAND;
13756
- const usableH = baseline - TOP_PAD;
14335
+ const bracketRanges = ranges.filter((r) => r.kind === "bracket");
14336
+ const bracketCount = bracketRanges.length;
14337
+ const bracketHeadroom = bracketCount > 0 ? BRACKET_TOP_OFFSET + bracketCount * BRACKET_ROW_H : 0;
14338
+ const hasAux = auxBars.length > 0;
14339
+ const primaryH = hasAux ? panelHeight * AUX_PRIMARY_RATIO : panelHeight;
14340
+ const baseline = panelYBars + primaryH - POINTER_BAND;
14341
+ const usableH = baseline - (panelYBars + TOP_PAD + bracketHeadroom);
13757
14342
  const maxV = Math.max(1, ...bars.map((b) => Number.isFinite(b.value) ? b.value : 0));
13758
14343
  bars.forEach((bar, i) => {
13759
14344
  const v = Number.isFinite(bar.value) ? bar.value : 0;
@@ -13783,6 +14368,89 @@ var init_AlgorithmCanvas = __esm({
13783
14368
  });
13784
14369
  }
13785
14370
  });
14371
+ ranges.forEach((r) => {
14372
+ const kind = r.kind ?? "fill";
14373
+ if (kind !== "fill") return;
14374
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14375
+ out.push({
14376
+ type: "rect",
14377
+ x: r.from * slot,
14378
+ y: panelYBars,
14379
+ width: (r.to - r.from + 1) * slot,
14380
+ height: primaryH,
14381
+ color,
14382
+ fill: color,
14383
+ opacity: RANGE_FILL_OPACITY
14384
+ });
14385
+ if (r.label) {
14386
+ out.push({
14387
+ type: "text",
14388
+ x: r.from * slot + 4,
14389
+ // Sits below the bracket block (if any) so fill and bracket labels never collide.
14390
+ y: panelYBars + 10 + bracketHeadroom,
14391
+ text: r.label,
14392
+ color,
14393
+ fontSize: 10,
14394
+ align: "left"
14395
+ });
14396
+ }
14397
+ });
14398
+ bracketRanges.forEach((r, i) => {
14399
+ const bracketY = panelYBars + BRACKET_TOP_OFFSET + i * BRACKET_ROW_H;
14400
+ const x1 = r.from * slot + slot * 0.1;
14401
+ const x2 = (r.to + 1) * slot - slot * 0.1;
14402
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14403
+ out.push({ type: "line", x1, y1: bracketY, x2, y2: bracketY, color, lineWidth: 2 });
14404
+ out.push({ type: "line", x1, y1: bracketY, x2: x1, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14405
+ out.push({ type: "line", x1: x2, y1: bracketY, x2, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14406
+ if (r.label) {
14407
+ out.push({
14408
+ type: "text",
14409
+ x: (x1 + x2) / 2,
14410
+ y: bracketY - BRACKET_LABEL_OFFSET,
14411
+ text: r.label,
14412
+ color,
14413
+ fontSize: 10,
14414
+ align: "center"
14415
+ });
14416
+ }
14417
+ });
14418
+ if (hasAux) {
14419
+ const auxH = panelHeight - primaryH;
14420
+ const slot2 = width / auxBars.length;
14421
+ const auxBaseline = panelYBars + primaryH + auxH - AUX_BASELINE_PAD;
14422
+ const auxUsableH = auxBaseline - (panelYBars + primaryH + AUX_LABEL_BAND);
14423
+ const maxAuxV = Math.max(1, ...auxBars.map((b) => Number.isFinite(b.value) ? b.value : 0));
14424
+ auxBars.forEach((bar, i) => {
14425
+ const v = Number.isFinite(bar.value) ? bar.value : 0;
14426
+ const bh = Math.max(0, v / maxAuxV * auxUsableH);
14427
+ const x = i * slot2 + slot2 * 0.1;
14428
+ const w = slot2 * 0.8;
14429
+ const color = bar.color ?? DEFAULT_BAR_COLOR;
14430
+ out.push({
14431
+ type: "rect",
14432
+ id: `auxbar-${i}`,
14433
+ x,
14434
+ y: auxBaseline - bh,
14435
+ width: w,
14436
+ height: bh,
14437
+ color,
14438
+ fill: color
14439
+ });
14440
+ const label = bar.label ?? (auxBars.length <= 24 ? String(v) : void 0);
14441
+ if (label) {
14442
+ out.push({
14443
+ type: "text",
14444
+ x: x + w / 2,
14445
+ y: auxBaseline - bh - 8,
14446
+ text: label,
14447
+ color: "#374151",
14448
+ fontSize: 11,
14449
+ align: "center"
14450
+ });
14451
+ }
14452
+ });
14453
+ }
13786
14454
  pointers.forEach((p) => {
13787
14455
  if (p.index < 0 || p.index >= bars.length) return;
13788
14456
  const cx = p.index * slot + slot / 2;
@@ -13790,7 +14458,7 @@ var init_AlgorithmCanvas = __esm({
13790
14458
  out.push({
13791
14459
  type: "arrow",
13792
14460
  x1: cx,
13793
- y1: height - 6,
14461
+ y1: panelYBars + primaryH - 18,
13794
14462
  x2: cx,
13795
14463
  y2: baseline + 4,
13796
14464
  color,
@@ -13800,7 +14468,7 @@ var init_AlgorithmCanvas = __esm({
13800
14468
  out.push({
13801
14469
  type: "text",
13802
14470
  x: cx,
13803
- y: height - 22,
14471
+ y: panelYBars + primaryH - 8,
13804
14472
  text: p.label,
13805
14473
  color,
13806
14474
  fontSize: 11,
@@ -13809,14 +14477,111 @@ var init_AlgorithmCanvas = __esm({
13809
14477
  }
13810
14478
  });
13811
14479
  }
14480
+ if (slots.length > 0) {
14481
+ const panelYSlots = panelY.slots;
14482
+ const n = slots.length;
14483
+ const vertical = slotOrientation === "vertical";
14484
+ const vBoxH = panelHeight / n;
14485
+ const vBoxW = Math.min(width * 0.5, 120);
14486
+ const vBoxX = (width - vBoxW) / 2;
14487
+ const hCellW = width / n;
14488
+ const hBoxW = hCellW * 0.82;
14489
+ const hBoxH = Math.min(panelHeight * 0.6, 48);
14490
+ const hBoxY = panelYSlots + (panelHeight - hBoxH) / 2;
14491
+ const slotBox = (i) => vertical ? { x: vBoxX, y: panelYSlots + panelHeight - (i + 1) * vBoxH, width: vBoxW, height: vBoxH } : { x: i * hCellW + (hCellW - hBoxW) / 2, y: hBoxY, width: hBoxW, height: hBoxH };
14492
+ slots.forEach((s, i) => {
14493
+ const box = slotBox(i);
14494
+ const state = s.state ?? "filled";
14495
+ const fill = state === "empty" ? SLOT_EMPTY_FILL : state === "highlight" ? s.color ?? SLOT_HIGHLIGHT_DEFAULT : s.color ?? DEFAULT_BAR_COLOR;
14496
+ const stroke = state === "empty" ? SLOT_EMPTY_STROKE : SLOT_FILLED_STROKE;
14497
+ out.push({
14498
+ type: "rect",
14499
+ id: `slot-${i}`,
14500
+ x: box.x,
14501
+ y: box.y,
14502
+ width: box.width,
14503
+ height: box.height,
14504
+ color: stroke,
14505
+ fill
14506
+ });
14507
+ if (s.value != null && state !== "empty") {
14508
+ out.push({
14509
+ type: "text",
14510
+ x: box.x + box.width / 2,
14511
+ y: box.y + box.height / 2,
14512
+ text: String(s.value),
14513
+ color: SLOT_VALUE_TEXT_COLOR,
14514
+ fontSize: 12,
14515
+ align: "center"
14516
+ });
14517
+ }
14518
+ });
14519
+ if (bars.length === 0) {
14520
+ pointers.forEach((p) => {
14521
+ if (p.index < 0 || p.index >= slots.length) return;
14522
+ const box = slotBox(p.index);
14523
+ const color = p.color ?? DEFAULT_POINTER_COLOR;
14524
+ if (vertical) {
14525
+ const cy = box.y + box.height / 2;
14526
+ out.push({
14527
+ type: "arrow",
14528
+ x1: box.x + box.width + 34,
14529
+ y1: cy,
14530
+ x2: box.x + box.width + 4,
14531
+ y2: cy,
14532
+ color,
14533
+ lineWidth: 2
14534
+ });
14535
+ if (p.label) {
14536
+ out.push({
14537
+ type: "text",
14538
+ x: box.x + box.width + 38,
14539
+ y: cy,
14540
+ text: p.label,
14541
+ color,
14542
+ fontSize: 11,
14543
+ align: "left"
14544
+ });
14545
+ }
14546
+ } else {
14547
+ const cx = box.x + box.width / 2;
14548
+ out.push({
14549
+ type: "arrow",
14550
+ x1: cx,
14551
+ y1: panelYSlots + panelHeight - 18,
14552
+ x2: cx,
14553
+ y2: box.y + box.height + 4,
14554
+ color,
14555
+ lineWidth: 2
14556
+ });
14557
+ if (p.label) {
14558
+ out.push({
14559
+ type: "text",
14560
+ x: cx,
14561
+ y: panelYSlots + panelHeight - 8,
14562
+ text: p.label,
14563
+ color,
14564
+ fontSize: 11,
14565
+ align: "center"
14566
+ });
14567
+ }
14568
+ }
14569
+ });
14570
+ }
14571
+ }
13812
14572
  if (cells.length > 0) {
14573
+ const panelYCells = panelY.cells;
13813
14574
  const maxCol = Math.max(0, ...cells.map((c) => c.col)) + 1;
13814
14575
  const maxRow = Math.max(0, ...cells.map((c) => c.row)) + 1;
13815
- const cw = width / maxCol;
13816
- const ch = height / maxRow;
14576
+ const colLabelH = colLabels.length > 0 ? 16 : 0;
14577
+ const rowLabelW = rowLabels.length > 0 ? 20 : 0;
14578
+ const gridX0 = rowLabelW;
14579
+ const gridY0 = panelYCells + colLabelH;
14580
+ const cw = (width - rowLabelW) / maxCol;
14581
+ const ch = (panelHeight - colLabelH) / maxRow;
13817
14582
  cells.forEach((c, i) => {
13818
- const x = c.col * cw;
13819
- const y = c.row * ch;
14583
+ const x = gridX0 + c.col * cw;
14584
+ const y = gridY0 + c.row * ch;
13820
14585
  const color = c.color ?? DEFAULT_CELL_COLOR;
13821
14586
  out.push({
13822
14587
  type: "rect",
@@ -13840,11 +14605,207 @@ var init_AlgorithmCanvas = __esm({
13840
14605
  align: "center"
13841
14606
  });
13842
14607
  }
14608
+ if (c.corner && cw >= CORNER_MIN_CELL && ch >= CORNER_MIN_CELL) {
14609
+ const { tl, tr, bl, br } = c.corner;
14610
+ if (tl) {
14611
+ out.push({
14612
+ type: "text",
14613
+ x: x + CORNER_INSET_X,
14614
+ y: y + CORNER_INSET_Y,
14615
+ text: tl,
14616
+ color: CORNER_TEXT_COLOR,
14617
+ fontSize: CORNER_FONT_SIZE,
14618
+ align: "left"
14619
+ });
14620
+ }
14621
+ if (tr) {
14622
+ out.push({
14623
+ type: "text",
14624
+ x: x + cw - CORNER_INSET_X,
14625
+ y: y + CORNER_INSET_Y,
14626
+ text: tr,
14627
+ color: CORNER_TEXT_COLOR,
14628
+ fontSize: CORNER_FONT_SIZE,
14629
+ align: "right"
14630
+ });
14631
+ }
14632
+ if (bl) {
14633
+ out.push({
14634
+ type: "text",
14635
+ x: x + CORNER_INSET_X,
14636
+ y: y + ch - CORNER_INSET_Y,
14637
+ text: bl,
14638
+ color: CORNER_TEXT_COLOR,
14639
+ fontSize: CORNER_FONT_SIZE,
14640
+ align: "left"
14641
+ });
14642
+ }
14643
+ if (br) {
14644
+ out.push({
14645
+ type: "text",
14646
+ x: x + cw - CORNER_INSET_X,
14647
+ y: y + ch - CORNER_INSET_Y,
14648
+ text: br,
14649
+ color: CORNER_TEXT_COLOR,
14650
+ fontSize: CORNER_FONT_SIZE,
14651
+ align: "right"
14652
+ });
14653
+ }
14654
+ }
14655
+ });
14656
+ colLabels.forEach((l) => {
14657
+ out.push({
14658
+ type: "text",
14659
+ x: gridX0 + l.index * cw + cw / 2,
14660
+ y: panelYCells + colLabelH / 2,
14661
+ text: l.text,
14662
+ color: l.color ?? AXIS_LABEL_COLOR,
14663
+ fontSize: AXIS_LABEL_FONT_SIZE,
14664
+ align: "center"
14665
+ });
14666
+ });
14667
+ rowLabels.forEach((l) => {
14668
+ out.push({
14669
+ type: "text",
14670
+ x: rowLabelW - 6,
14671
+ y: gridY0 + l.index * ch + ch / 2,
14672
+ text: l.text,
14673
+ color: l.color ?? AXIS_LABEL_COLOR,
14674
+ fontSize: AXIS_LABEL_FONT_SIZE,
14675
+ align: "right"
14676
+ });
14677
+ });
14678
+ }
14679
+ if (buckets.length > 0) {
14680
+ const panelYBuckets = panelY.buckets;
14681
+ const bucketCount = Math.max(0, ...buckets.map((b) => b.index)) + 1;
14682
+ const rowH = panelHeight / bucketCount;
14683
+ const indexColW = Math.min(width * 0.12, 40);
14684
+ const maxChainLen = Math.max(1, ...buckets.map((b) => b.entries.length));
14685
+ const entryW = Math.min(BUCKET_ENTRY_MAX_W, Math.max(BUCKET_ENTRY_MIN_W, (width - indexColW - 8) / maxChainLen));
14686
+ const maxVisible = Math.floor((width - indexColW - 4) / entryW);
14687
+ buckets.forEach((b) => {
14688
+ const rowY = panelYBuckets + b.index * rowH;
14689
+ out.push({
14690
+ type: "rect",
14691
+ id: `bucket-index-${b.index}`,
14692
+ x: 2,
14693
+ y: rowY + 2,
14694
+ width: indexColW - 4,
14695
+ height: rowH - 4,
14696
+ color: BUCKET_INDEX_STROKE,
14697
+ fill: BUCKET_INDEX_FILL
14698
+ });
14699
+ out.push({
14700
+ type: "text",
14701
+ x: 2 + (indexColW - 4) / 2,
14702
+ y: rowY + rowH / 2,
14703
+ text: String(b.index),
14704
+ color: BUCKET_INDEX_TEXT,
14705
+ fontSize: 10,
14706
+ align: "center"
14707
+ });
14708
+ const overflow = b.entries.length > maxVisible;
14709
+ const visibleCount = overflow ? Math.max(0, maxVisible - 1) : b.entries.length;
14710
+ for (let j = 0; j < visibleCount; j++) {
14711
+ const entry = b.entries[j];
14712
+ const ex = indexColW + 4 + j * entryW;
14713
+ const state = entry.state ?? "default";
14714
+ const fill = state === "highlight" ? entry.color ?? BUCKET_ENTRY_HIGHLIGHT : state === "probing" ? entry.color ?? BUCKET_ENTRY_PROBING : entry.color ?? BUCKET_ENTRY_DEFAULT;
14715
+ out.push({
14716
+ type: "rect",
14717
+ id: `bucket-${b.index}-${j}`,
14718
+ x: ex,
14719
+ y: rowY + 2,
14720
+ width: entryW - 2,
14721
+ height: rowH - 4,
14722
+ color: fill,
14723
+ fill
14724
+ });
14725
+ if (entryW >= 20 && rowH >= 16) {
14726
+ out.push({
14727
+ type: "text",
14728
+ x: ex + (entryW - 2) / 2,
14729
+ y: rowY + rowH / 2,
14730
+ text: entry.label,
14731
+ color: BUCKET_ENTRY_TEXT,
14732
+ fontSize: 10,
14733
+ align: "center"
14734
+ });
14735
+ }
14736
+ }
14737
+ if (overflow) {
14738
+ const ex = indexColW + 4 + visibleCount * entryW;
14739
+ out.push({
14740
+ type: "rect",
14741
+ id: `bucket-${b.index}-overflow`,
14742
+ x: ex,
14743
+ y: rowY + 2,
14744
+ width: entryW - 2,
14745
+ height: rowH - 4,
14746
+ color: BUCKET_ENTRY_DEFAULT,
14747
+ fill: BUCKET_ENTRY_DEFAULT
14748
+ });
14749
+ out.push({
14750
+ type: "text",
14751
+ x: ex + (entryW - 2) / 2,
14752
+ y: rowY + rowH / 2,
14753
+ text: `+${b.entries.length - visibleCount}`,
14754
+ color: BUCKET_ENTRY_TEXT,
14755
+ fontSize: 10,
14756
+ align: "center"
14757
+ });
14758
+ }
14759
+ });
14760
+ }
14761
+ if (frames.length > 0) {
14762
+ const panelYFrames = panelY.frames;
14763
+ const n = frames.length;
14764
+ const frameH = panelHeight / n;
14765
+ const x = 8;
14766
+ const w = width - 16;
14767
+ frames.forEach((f3, i) => {
14768
+ const y = panelYFrames + panelHeight - (i + 1) * frameH;
14769
+ const state = f3.state ?? "active";
14770
+ const fill = state === "returning" ? f3.color ?? FRAME_RETURNING_COLOR : state === "done" ? f3.color ?? FRAME_DONE_COLOR : f3.color ?? FRAME_ACTIVE_COLOR;
14771
+ out.push({ type: "rect", id: `frame-${i}`, x, y, width: w, height: frameH, color: fill, fill });
14772
+ if (frameH >= FRAME_TWO_LINE_MIN_H) {
14773
+ out.push({
14774
+ type: "text",
14775
+ x: 16,
14776
+ y: y + frameH * 0.35,
14777
+ text: f3.label,
14778
+ color: FRAME_LABEL_COLOR,
14779
+ fontSize: 10,
14780
+ align: "left"
14781
+ });
14782
+ if (f3.detail) {
14783
+ out.push({
14784
+ type: "text",
14785
+ x: 16,
14786
+ y: y + frameH * 0.7,
14787
+ text: f3.detail,
14788
+ color: FRAME_DETAIL_COLOR,
14789
+ fontSize: 10,
14790
+ align: "left"
14791
+ });
14792
+ }
14793
+ } else {
14794
+ out.push({
14795
+ type: "text",
14796
+ x: 16,
14797
+ y: y + frameH / 2,
14798
+ text: f3.label,
14799
+ color: FRAME_LABEL_COLOR,
14800
+ fontSize: 10,
14801
+ align: "left"
14802
+ });
14803
+ }
13843
14804
  });
13844
14805
  }
13845
14806
  out.push(...shapes);
13846
14807
  return out;
13847
- }, [bars, cells, pointers, shapes, width, height]);
14808
+ }, [bars, cells, pointers, ranges, slots, slotOrientation, frames, buckets, auxBars, rowLabels, colLabels, shapes, width, height]);
13848
14809
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
13849
14810
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
13850
14811
  /* @__PURE__ */ jsx(
@@ -14858,6 +15819,12 @@ function LearningScene3D({
14858
15819
  const unitId = event.payload?.unitId;
14859
15820
  if (typeof unitId === "string") onItemClickRef.current?.(unitId);
14860
15821
  });
15822
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && post?.bloom) {
15823
+ const unknownKeys = Object.keys(post.bloom).filter((k) => !KNOWN_BLOOM_KEYS.has(k));
15824
+ if (unknownKeys.length > 0) {
15825
+ sceneLog.debug("post.bloom has unrecognized keys \u2014 only intensity/threshold/smoothing are read", { unknownKeys });
15826
+ }
15827
+ }
14861
15828
  const props3d = {
14862
15829
  drawables,
14863
15830
  isLoading,
@@ -14923,7 +15890,7 @@ function cylinderBetween(from, to, radius, color) {
14923
15890
  material: { color }
14924
15891
  };
14925
15892
  }
14926
- function arrowBetween(from, to, color, shaftRadius = 0.08) {
15893
+ function arrowBetween(from, to, color, shaftRadius = 0.08, id) {
14927
15894
  const len = segmentLength(from, to);
14928
15895
  if (len < 1e-6) return null;
14929
15896
  const tipLen = Math.min(shaftRadius * 8, len * 0.35);
@@ -14951,6 +15918,7 @@ function arrowBetween(from, to, color, shaftRadius = 0.08) {
14951
15918
  };
14952
15919
  return {
14953
15920
  type: "draw-group",
15921
+ ...id !== void 0 ? { id } : {},
14954
15922
  position: { x: from[0], y: from[1], z: from[2] },
14955
15923
  items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
14956
15924
  };
@@ -14977,20 +15945,228 @@ function get3DClickPayload(onShapeClick, idToIndex) {
14977
15945
  if (!onShapeClick) return void 0;
14978
15946
  return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
14979
15947
  }
14980
- var Canvas3DHost2;
15948
+ function polylineTube(points, radius, color, opts) {
15949
+ const maxSegments = opts?.maxSegments ?? 128;
15950
+ let pts = points;
15951
+ if (pts.length - 1 > maxSegments) {
15952
+ const step = (pts.length - 1) / maxSegments;
15953
+ const kept = [pts[0]];
15954
+ for (let s = 1; s < maxSegments; s++) kept.push(pts[Math.round(s * step)]);
15955
+ kept.push(pts[pts.length - 1]);
15956
+ pts = kept;
15957
+ }
15958
+ const out = [];
15959
+ for (let i = 0; i < pts.length - 1; i++) {
15960
+ const seg = cylinderBetween(pts[i], pts[i + 1], radius, color);
15961
+ if (seg) out.push(opts?.opacity !== void 0 ? { ...seg, opacity: opts.opacity } : seg);
15962
+ }
15963
+ return out;
15964
+ }
15965
+ function heightFieldMesh(spec) {
15966
+ const { nx, ny, heights, spacing = 1, x = 0, y = 0 } = spec;
15967
+ const flatShading = spec.flatShading ?? true;
15968
+ const vertices = [];
15969
+ for (let iy = 0; iy < ny; iy++) {
15970
+ for (let ix = 0; ix < nx; ix++) {
15971
+ vertices.push([
15972
+ x + (ix - (nx - 1) / 2) * spacing,
15973
+ y + (iy - (ny - 1) / 2) * spacing,
15974
+ heights[iy * nx + ix] ?? 0
15975
+ ]);
15976
+ }
15977
+ }
15978
+ const bands = [...spec.bands ?? []].sort((a, b) => (a.min ?? -Infinity) - (b.min ?? -Infinity));
15979
+ const facesByBand = /* @__PURE__ */ new Map();
15980
+ for (let iy = 0; iy < ny - 1; iy++) {
15981
+ for (let ix = 0; ix < nx - 1; ix++) {
15982
+ const v00 = iy * nx + ix;
15983
+ const v10 = iy * nx + ix + 1;
15984
+ const v01 = (iy + 1) * nx + ix;
15985
+ const v11 = (iy + 1) * nx + ix + 1;
15986
+ for (const face of [[v00, v10, v01], [v10, v11, v01]]) {
15987
+ const centroid = (vertices[face[0]][2] + vertices[face[1]][2] + vertices[face[2]][2]) / 3;
15988
+ let band = null;
15989
+ for (const b of bands) {
15990
+ if ((b.min ?? -Infinity) <= centroid) band = b;
15991
+ }
15992
+ const key = bands.length > 0 ? band : null;
15993
+ const list = facesByBand.get(key) ?? [];
15994
+ list.push(face);
15995
+ facesByBand.set(key, list);
15996
+ }
15997
+ }
15998
+ }
15999
+ const out = [];
16000
+ for (const [band, faces] of facesByBand) {
16001
+ if (faces.length === 0) continue;
16002
+ out.push({
16003
+ type: "draw-mesh",
16004
+ shape: "polyhedron",
16005
+ position: { x: 0, y: 0, z: 0 },
16006
+ vertices,
16007
+ faces,
16008
+ pivot: "center",
16009
+ material: { color: band?.color ?? spec.color ?? "#64748b", flatShading, side: "double" },
16010
+ ...spec.opacity !== void 0 ? { opacity: spec.opacity } : {}
16011
+ });
16012
+ }
16013
+ return out;
16014
+ }
16015
+ function arrowField(vectors, opts) {
16016
+ const scale = opts?.scale ?? 1;
16017
+ const out = [];
16018
+ for (const v of vectors) {
16019
+ const to = [
16020
+ v.from[0] + v.delta[0] * scale,
16021
+ v.from[1] + v.delta[1] * scale,
16022
+ v.from[2] + v.delta[2] * scale
16023
+ ];
16024
+ const arrow = arrowBetween(v.from, to, v.color ?? "#dc2626", v.width, v.id);
16025
+ if (arrow) out.push(arrow);
16026
+ if (v.label) out.push(billboardLabel(v.label, to[0], to[1], to[2], { color: opts?.labelColor }));
16027
+ }
16028
+ return out;
16029
+ }
16030
+ function helixDrawables(spec, opts) {
16031
+ const count = spec.count ?? spec.rungs?.length ?? 0;
16032
+ const rungs = Array.from({ length: count }, (_, i) => spec.rungs?.[i] ?? {});
16033
+ const radius = spec.radius ?? 1;
16034
+ const rise = spec.rise ?? 0.34;
16035
+ const twistRad = (spec.twistDeg ?? 36) * (Math.PI / 180);
16036
+ const strandAColor = spec.strandAColor ?? "#38bdf8";
16037
+ const strandBColor = spec.strandBColor ?? "#fb923c";
16038
+ const backboneRadius = spec.backboneRadius ?? 0.16;
16039
+ const rungRadius = spec.rungRadius ?? 0.12;
16040
+ const cx = spec.x ?? 0;
16041
+ const cy = spec.y ?? 0;
16042
+ const cz = spec.z ?? 0;
16043
+ const unwoundCount = spec.unwoundCount ?? 0;
16044
+ const unwindSpread = spec.unwindSpread ?? 1.8;
16045
+ const strandA = [];
16046
+ const strandB = [];
16047
+ for (let i = 0; i < count; i++) {
16048
+ const yi = cy + (i - (count - 1) / 2) * rise;
16049
+ const theta = i * twistRad;
16050
+ const s = i < unwoundCount ? unwindSpread : 1;
16051
+ strandA.push([cx + s * radius * Math.cos(theta), yi, cz + s * radius * Math.sin(theta)]);
16052
+ strandB.push([cx + s * radius * Math.cos(theta + Math.PI), yi, cz + s * radius * Math.sin(theta + Math.PI)]);
16053
+ }
16054
+ const out = [];
16055
+ for (let i = 0; i < count; i++) {
16056
+ out.push(meshSphere(`hx-a-${i}`, strandA[i][0], strandA[i][1], strandA[i][2], backboneRadius, strandAColor));
16057
+ out.push(meshSphere(`hx-b-${i}`, strandB[i][0], strandB[i][1], strandB[i][2], backboneRadius, strandBColor));
16058
+ if (i > 0) {
16059
+ const segA = cylinderBetween(strandA[i - 1], strandA[i], backboneRadius, strandAColor);
16060
+ if (segA) out.push(segA);
16061
+ const segB = cylinderBetween(strandB[i - 1], strandB[i], backboneRadius, strandBColor);
16062
+ if (segB) out.push(segB);
16063
+ }
16064
+ const rung = rungs[i];
16065
+ const rungColor = rung.color ?? "#94a3b8";
16066
+ const rod = cylinderBetween(strandA[i], strandB[i], rungRadius, rungColor);
16067
+ if (rod) out.push(rod);
16068
+ const mid = [
16069
+ (strandA[i][0] + strandB[i][0]) / 2,
16070
+ (strandA[i][1] + strandB[i][1]) / 2,
16071
+ (strandA[i][2] + strandB[i][2]) / 2
16072
+ ];
16073
+ const markerRadius = rung.radius ?? rungRadius;
16074
+ out.push(meshSphere(rung.id, mid[0], mid[1], mid[2], markerRadius, rungColor));
16075
+ if (rung.label) out.push(billboardLabel(rung.label, mid[0], mid[1], mid[2] + markerRadius, { color: opts?.labelColor }));
16076
+ }
16077
+ return out;
16078
+ }
16079
+ function latticeDrawables(spec, opts) {
16080
+ const nx = spec.nx ?? 2;
16081
+ const ny = spec.ny ?? 2;
16082
+ const nz = spec.nz ?? 2;
16083
+ const latticeConstant = spec.latticeConstant ?? 2;
16084
+ const bondRadius = spec.bondRadius ?? 0.06;
16085
+ const highlightCell = spec.highlightCell ?? false;
16086
+ const dimColor = spec.dimColor ?? "#475569";
16087
+ const showLabels = spec.showLabels ?? false;
16088
+ const selectedColor = spec.selectedColor ?? "#f59e0b";
16089
+ const posByKey = /* @__PURE__ */ new Map();
16090
+ const inCellByKey = /* @__PURE__ */ new Map();
16091
+ const out = [];
16092
+ for (const site of spec.basis) {
16093
+ const snx = site.xEdge ? nx + 1 : nx;
16094
+ const sny = site.yEdge ? ny + 1 : ny;
16095
+ const snz = site.zEdge ? nz + 1 : nz;
16096
+ for (let i = 0; i < snx; i++) {
16097
+ for (let j = 0; j < sny; j++) {
16098
+ for (let k = 0; k < snz; k++) {
16099
+ const key = `${site.key}-${i}-${j}-${k}`;
16100
+ const inCell = i + site.dx <= 1 && j + site.dy <= 1 && k + site.dz <= 1;
16101
+ const pos = [
16102
+ (i + site.dx) * latticeConstant - nx * latticeConstant / 2,
16103
+ (j + site.dy) * latticeConstant - ny * latticeConstant / 2,
16104
+ (k + site.dz) * latticeConstant - nz * latticeConstant / 2
16105
+ ];
16106
+ posByKey.set(key, pos);
16107
+ inCellByKey.set(key, inCell);
16108
+ const isSelected = spec.selectedId === `lat-${key}`;
16109
+ const color = isSelected ? selectedColor : highlightCell && !inCell ? dimColor : site.color ?? "#2563eb";
16110
+ const radius = (site.radius ?? 0.3) * (isSelected ? 1.4 : 1);
16111
+ out.push(meshSphere(`lat-${key}`, pos[0], pos[1], pos[2], radius, color));
16112
+ if (showLabels && site.element) {
16113
+ out.push(billboardLabel(site.element, pos[0], pos[1], pos[2] + radius, { color: opts?.labelColor }));
16114
+ }
16115
+ }
16116
+ }
16117
+ }
16118
+ }
16119
+ const basisByKey = new Map(spec.basis.map((s) => [s.key, s]));
16120
+ for (const bond of spec.bonds ?? []) {
16121
+ const fromSite = basisByKey.get(bond.from);
16122
+ const toSite = basisByKey.get(bond.to);
16123
+ if (!fromSite || !toSite) continue;
16124
+ const fnx = fromSite.xEdge ? nx + 1 : nx;
16125
+ const fny = fromSite.yEdge ? ny + 1 : ny;
16126
+ const fnz = fromSite.zEdge ? nz + 1 : nz;
16127
+ const tnx = toSite.xEdge ? nx + 1 : nx;
16128
+ const tny = toSite.yEdge ? ny + 1 : ny;
16129
+ const tnz = toSite.zEdge ? nz + 1 : nz;
16130
+ const bdx = bond.dx ?? 0;
16131
+ const bdy = bond.dy ?? 0;
16132
+ const bdz = bond.dz ?? 0;
16133
+ for (let i = 0; i < fnx; i++) {
16134
+ for (let j = 0; j < fny; j++) {
16135
+ for (let k = 0; k < fnz; k++) {
16136
+ const ti = i + bdx;
16137
+ const tj = j + bdy;
16138
+ const tk = k + bdz;
16139
+ if (ti < 0 || ti >= tnx || tj < 0 || tj >= tny || tk < 0 || tk >= tnz) continue;
16140
+ const fromKey = `${fromSite.key}-${i}-${j}-${k}`;
16141
+ const toKey = `${toSite.key}-${ti}-${tj}-${tk}`;
16142
+ const fromPos = posByKey.get(fromKey);
16143
+ const toPos = posByKey.get(toKey);
16144
+ if (!fromPos || !toPos) continue;
16145
+ const dimmed = highlightCell && !(inCellByKey.get(fromKey) && inCellByKey.get(toKey));
16146
+ const seg = cylinderBetween(fromPos, toPos, bondRadius, dimmed ? dimColor : bond.color ?? "#6b7280");
16147
+ if (seg) out.push(seg);
16148
+ }
16149
+ }
16150
+ }
16151
+ }
16152
+ return out;
16153
+ }
16154
+ var sceneLog, KNOWN_BLOOM_KEYS, Canvas3DHost2;
14981
16155
  var init_learningScene3D = __esm({
14982
16156
  "components/learning/molecules/learningScene3D.tsx"() {
14983
16157
  "use client";
14984
16158
  init_atoms();
14985
16159
  init_Stack();
14986
16160
  init_useEventBus();
16161
+ sceneLog = createLogger("almadar:ui:learning-scene-3d");
16162
+ KNOWN_BLOOM_KEYS = /* @__PURE__ */ new Set(["intensity", "threshold", "smoothing"]);
14987
16163
  Canvas3DHost2 = lazy(
14988
16164
  () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
14989
16165
  );
14990
16166
  LearningScene3D.displayName = "LearningScene3D";
14991
16167
  }
14992
16168
  });
14993
- var biologyLog, BiologyCanvas;
16169
+ var biologyLog, BIO_BAND_COLORS, BIO_STAGE_FILL, BIO_STAGE_TEXT, BiologyCanvas;
14994
16170
  var init_BiologyCanvas = __esm({
14995
16171
  "components/learning/molecules/BiologyCanvas.tsx"() {
14996
16172
  "use client";
@@ -14999,6 +16175,17 @@ var init_BiologyCanvas = __esm({
14999
16175
  init_LearningCanvas();
15000
16176
  init_learningScene3D();
15001
16177
  biologyLog = createLogger("almadar:ui:biology-canvas");
16178
+ BIO_BAND_COLORS = ["#dcfce7", "#fef9c3", "#fee2e2", "#e0e7ff"];
16179
+ BIO_STAGE_FILL = {
16180
+ pending: "#e2e8f0",
16181
+ active: "#3b82f6",
16182
+ done: "#94a3b8"
16183
+ };
16184
+ BIO_STAGE_TEXT = {
16185
+ pending: "#64748b",
16186
+ active: "#ffffff",
16187
+ done: "#ffffff"
16188
+ };
15002
16189
  BiologyCanvas = ({
15003
16190
  className,
15004
16191
  width = 600,
@@ -15011,7 +16198,15 @@ var init_BiologyCanvas = __esm({
15011
16198
  post,
15012
16199
  nodes = [],
15013
16200
  edges = [],
16201
+ compartments = [],
16202
+ bands = [],
16203
+ stages = [],
16204
+ stageStyle = "timeline",
16205
+ helix,
16206
+ helix3d,
15014
16207
  shapes = [],
16208
+ readouts,
16209
+ traces,
15015
16210
  showGrid,
15016
16211
  shadows,
15017
16212
  interactive,
@@ -15026,19 +16221,148 @@ var init_BiologyCanvas = __esm({
15026
16221
  for (const n of nodes) {
15027
16222
  if (n.id) nodeById.set(n.id, n);
15028
16223
  }
16224
+ const bandCount = bands.length;
16225
+ for (let i = 0; i < bandCount; i++) {
16226
+ const band = bands[i];
16227
+ const bandColor = band.color ?? BIO_BAND_COLORS[i % BIO_BAND_COLORS.length];
16228
+ const bandY = i * height / bandCount;
16229
+ const bandH = height / bandCount;
16230
+ out.push({
16231
+ type: "rect",
16232
+ x: 0,
16233
+ y: bandY,
16234
+ width,
16235
+ height: bandH,
16236
+ color: bandColor,
16237
+ fill: bandColor,
16238
+ opacity: 0.45
16239
+ });
16240
+ if (band.label) {
16241
+ out.push({
16242
+ type: "text",
16243
+ x: 8,
16244
+ y: bandY + 14,
16245
+ text: band.label,
16246
+ color: "#6b7280",
16247
+ fontSize: 10
16248
+ });
16249
+ }
16250
+ }
16251
+ for (const c of compartments) {
16252
+ const color = c.color ?? "#16a34a";
16253
+ out.push({
16254
+ type: "ellipse",
16255
+ x: c.x,
16256
+ y: c.y,
16257
+ width: c.width,
16258
+ height: c.height,
16259
+ color,
16260
+ fill: c.fill ?? `${color}1A`,
16261
+ lineWidth: c.lineWidth ?? 2,
16262
+ ...c.dash ? { dash: c.dash } : {}
16263
+ });
16264
+ if (c.label) {
16265
+ out.push({
16266
+ type: "text",
16267
+ x: c.x,
16268
+ y: c.y - c.height / 2 + 14,
16269
+ text: c.label,
16270
+ color: "#111827",
16271
+ fontSize: 11,
16272
+ align: "center"
16273
+ });
16274
+ }
16275
+ }
16276
+ if (helix) {
16277
+ const hx = helix.x ?? 24;
16278
+ const hy = helix.y ?? height * 0.25;
16279
+ const hw = helix.width ?? width - 48;
16280
+ const hh = helix.height ?? height * 0.5;
16281
+ const rungs = helix.rungs;
16282
+ const n = rungs.length;
16283
+ const cy = hy + hh / 2;
16284
+ const colorA = helix.colorA ?? "#2563eb";
16285
+ const colorB = helix.colorB ?? "#dc2626";
16286
+ const rungColor = helix.rungColor ?? "#94a3b8";
16287
+ const fork = helix.fork ?? 0;
16288
+ const maxSep = Math.min(hh - 8, 96);
16289
+ const strandA = [];
16290
+ const strandB = [];
16291
+ const rungGeoms = [];
16292
+ for (let i = 0; i < n; i++) {
16293
+ const rx = hx + (i + 0.5) * hw / n;
16294
+ const t = (i + 0.5) / n;
16295
+ const paired = t >= fork;
16296
+ const sep = paired ? 28 : 28 + (maxSep - 28) * ((fork - t) / fork);
16297
+ strandA.push({ x: rx, y: cy - sep / 2 });
16298
+ strandB.push({ x: rx, y: cy + sep / 2 });
16299
+ rungGeoms.push({ rx, sep, rung: rungs[i], paired });
16300
+ }
16301
+ for (let i = 1; i < n; i++) {
16302
+ out.push({ type: "line", x1: strandA[i - 1].x, y1: strandA[i - 1].y, x2: strandA[i].x, y2: strandA[i].y, color: colorA, lineWidth: 3 });
16303
+ }
16304
+ for (let i = 1; i < n; i++) {
16305
+ out.push({ type: "line", x1: strandB[i - 1].x, y1: strandB[i - 1].y, x2: strandB[i].x, y2: strandB[i].y, color: colorB, lineWidth: 3 });
16306
+ }
16307
+ for (const g of rungGeoms) {
16308
+ const rColor = g.rung.color ?? (g.rung.state === "new" ? "#16a34a" : rungColor);
16309
+ const topY = cy - g.sep / 2;
16310
+ const bottomY = cy + g.sep / 2;
16311
+ if (g.paired) {
16312
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: bottomY, color: rColor });
16313
+ if (g.rung.a) {
16314
+ out.push({ type: "text", x: g.rx, y: cy - g.sep / 4, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
16315
+ }
16316
+ if (g.rung.b) {
16317
+ out.push({ type: "text", x: g.rx, y: cy + g.sep / 4, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16318
+ }
16319
+ } else {
16320
+ const stubTopY = topY + 8;
16321
+ const stubBottomY = bottomY - 8;
16322
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: stubTopY, color: rColor });
16323
+ out.push({ type: "line", x1: g.rx, y1: bottomY, x2: g.rx, y2: stubBottomY, color: rColor });
16324
+ if (g.rung.a) {
16325
+ out.push({ type: "text", x: g.rx, y: stubTopY + 6, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
16326
+ }
16327
+ if (g.rung.b) {
16328
+ out.push({ type: "text", x: g.rx, y: stubBottomY - 6, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16329
+ }
16330
+ }
16331
+ }
16332
+ }
15029
16333
  for (const e of edges) {
15030
16334
  const a = nodeById.get(e.from);
15031
16335
  const b = nodeById.get(e.to);
15032
16336
  if (!a || !b) continue;
15033
- out.push({
15034
- type: "line",
15035
- x1: a.x,
15036
- y1: a.y,
15037
- x2: b.x,
15038
- y2: b.y,
15039
- color: e.color ?? "#9ca3af",
15040
- lineWidth: 2
15041
- });
16337
+ const color = e.color ?? "#9ca3af";
16338
+ if (e.directed) {
16339
+ const rA = a.radius ?? 16;
16340
+ const rB = b.radius ?? 16;
16341
+ const dx = b.x - a.x;
16342
+ const dy = b.y - a.y;
16343
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16344
+ const ux = dx / dist;
16345
+ const uy = dy / dist;
16346
+ out.push({
16347
+ type: "arrow",
16348
+ x1: a.x + ux * rA,
16349
+ y1: a.y + uy * rA,
16350
+ x2: b.x - ux * rB,
16351
+ y2: b.y - uy * rB,
16352
+ color,
16353
+ lineWidth: 2
16354
+ });
16355
+ } else {
16356
+ out.push({
16357
+ type: "line",
16358
+ x1: a.x,
16359
+ y1: a.y,
16360
+ x2: b.x,
16361
+ y2: b.y,
16362
+ color,
16363
+ lineWidth: 2
16364
+ });
16365
+ }
15042
16366
  if (e.label) {
15043
16367
  out.push({
15044
16368
  type: "text",
@@ -15051,6 +16375,8 @@ var init_BiologyCanvas = __esm({
15051
16375
  }
15052
16376
  }
15053
16377
  for (const n of nodes) {
16378
+ const state = n.state ?? "default";
16379
+ const muted = state === "muted";
15054
16380
  out.push({
15055
16381
  type: "circle",
15056
16382
  x: n.x,
@@ -15058,28 +16384,127 @@ var init_BiologyCanvas = __esm({
15058
16384
  radius: n.radius ?? 16,
15059
16385
  color: n.color ?? "#16a34a",
15060
16386
  fill: `${n.color ?? "#16a34a"}33`,
15061
- id: n.id
16387
+ id: n.id,
16388
+ ...muted ? { opacity: 0.35 } : {}
15062
16389
  });
16390
+ if (state === "highlight") {
16391
+ out.push({
16392
+ type: "circle",
16393
+ x: n.x,
16394
+ y: n.y,
16395
+ radius: (n.radius ?? 16) + 4,
16396
+ color: "#f59e0b",
16397
+ lineWidth: 2
16398
+ });
16399
+ }
15063
16400
  if (n.label) {
15064
16401
  out.push({
15065
16402
  type: "text",
15066
16403
  x: n.x,
15067
16404
  y: n.y + (n.radius ?? 16) + 14,
15068
16405
  text: n.label,
16406
+ ...muted ? { opacity: 0.35 } : {},
15069
16407
  color: "#111827",
15070
16408
  fontSize: 12,
15071
16409
  align: "center"
15072
16410
  });
15073
16411
  }
15074
16412
  }
16413
+ const stageCount = stages.length;
16414
+ if (stageCount > 0) {
16415
+ if (stageStyle === "ring") {
16416
+ const cx = width / 2;
16417
+ const cy = height / 2;
16418
+ const R = Math.min(width, height) / 2 - 48;
16419
+ const ringPoints = [];
16420
+ for (let i = 0; i < stageCount; i++) {
16421
+ const angleRad = (-90 + 360 * i / stageCount) * Math.PI / 180;
16422
+ ringPoints.push({ x: cx + R * Math.cos(angleRad), y: cy + R * Math.sin(angleRad) });
16423
+ }
16424
+ if (stageCount >= 2) {
16425
+ for (let i = 0; i < stageCount - 1; i++) {
16426
+ const p1 = ringPoints[i];
16427
+ const p2 = ringPoints[i + 1];
16428
+ const dx = p2.x - p1.x;
16429
+ const dy = p2.y - p1.y;
16430
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16431
+ const ux = dx / dist;
16432
+ const uy = dy / dist;
16433
+ out.push({
16434
+ type: "arrow",
16435
+ x1: p1.x + ux * 46,
16436
+ y1: p1.y + uy * 46,
16437
+ x2: p2.x - ux * 46,
16438
+ y2: p2.y - uy * 46,
16439
+ color: "#94a3b8"
16440
+ });
16441
+ }
16442
+ }
16443
+ for (let i = 0; i < stageCount; i++) {
16444
+ const stage = stages[i];
16445
+ const state = stage.state ?? "pending";
16446
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16447
+ const w = Math.max(26, Math.min(84, stage.label.length * 6 + 10));
16448
+ const h = 18;
16449
+ const p = ringPoints[i];
16450
+ out.push({ type: "rect", x: p.x - w / 2, y: p.y - h / 2, width: w, height: h, color: fill, fill });
16451
+ out.push({ type: "text", x: p.x, y: p.y, text: stage.label, color: BIO_STAGE_TEXT[state], fontSize: 10, align: "center" });
16452
+ }
16453
+ } else {
16454
+ const stripY = height - 32;
16455
+ const slotW = (width - 16) / stageCount;
16456
+ const chipGeoms = [];
16457
+ for (let i = 0; i < stageCount; i++) {
16458
+ chipGeoms.push({ x: 8 + i * slotW + 5, w: slotW - 10 });
16459
+ }
16460
+ for (let i = 0; i < stageCount - 1; i++) {
16461
+ const midY = stripY + 13;
16462
+ out.push({
16463
+ type: "arrow",
16464
+ x1: chipGeoms[i].x + chipGeoms[i].w,
16465
+ y1: midY,
16466
+ x2: chipGeoms[i + 1].x,
16467
+ y2: midY,
16468
+ color: "#94a3b8"
16469
+ });
16470
+ }
16471
+ for (let i = 0; i < stageCount; i++) {
16472
+ const stage = stages[i];
16473
+ const state = stage.state ?? "pending";
16474
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16475
+ const g = chipGeoms[i];
16476
+ out.push({ type: "rect", x: g.x, y: stripY, width: g.w, height: 26, color: fill, fill });
16477
+ out.push({
16478
+ type: "text",
16479
+ x: g.x + g.w / 2,
16480
+ y: stripY + 13,
16481
+ text: stage.label,
16482
+ color: BIO_STAGE_TEXT[state],
16483
+ fontSize: 10,
16484
+ align: "center"
16485
+ });
16486
+ }
16487
+ }
16488
+ }
15075
16489
  out.push(...shapes);
15076
16490
  return out;
15077
- }, [nodes, edges, shapes]);
16491
+ }, [nodes, edges, compartments, bands, stages, stageStyle, helix, shapes, width, height]);
15078
16492
  const drawables3D = useMemo(() => {
15079
16493
  if (mode !== "3d") return [];
15080
16494
  if (shapes.length > 0) {
15081
16495
  biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
15082
16496
  }
16497
+ if (compartments.length > 0 || bands.length > 0 || stages.length > 0 || helix) {
16498
+ biologyLog.debug("2D-only families ignored in 3D mode (pixel-authored 2D vocabulary)", {
16499
+ compartments: compartments.length,
16500
+ bands: bands.length,
16501
+ stages: stages.length,
16502
+ helix: helix != null
16503
+ });
16504
+ }
16505
+ if (animate) {
16506
+ biologyLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
16507
+ }
15083
16508
  const out = [];
15084
16509
  const labelColor = labelColorForBackground(backgroundColor);
15085
16510
  const nodeById = /* @__PURE__ */ new Map();
@@ -15113,15 +16538,21 @@ var init_BiologyCanvas = __esm({
15113
16538
  out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
15114
16539
  }
15115
16540
  }
16541
+ if (helix3d) {
16542
+ out.push(...helixDrawables(helix3d, { labelColor }));
16543
+ }
15116
16544
  return out;
15117
- }, [mode, nodes, edges, shapes, backgroundColor]);
16545
+ }, [mode, nodes, edges, shapes, compartments, bands, stages, helix, helix3d, animate, backgroundColor]);
15118
16546
  const nodeIndexById = useMemo(() => {
15119
16547
  const m = /* @__PURE__ */ new Map();
16548
+ (helix3d?.rungs ?? []).forEach((rung, i) => {
16549
+ if (rung.id) m.set(rung.id, i);
16550
+ });
15120
16551
  nodes.forEach((n, i) => {
15121
16552
  if (n.id) m.set(n.id, i);
15122
16553
  });
15123
16554
  return m;
15124
- }, [nodes]);
16555
+ }, [nodes, helix3d]);
15125
16556
  if (mode === "3d") {
15126
16557
  return /* @__PURE__ */ jsx(
15127
16558
  LearningScene3D,
@@ -15153,6 +16584,8 @@ var init_BiologyCanvas = __esm({
15153
16584
  height,
15154
16585
  backgroundColor,
15155
16586
  shapes: derivedShapes,
16587
+ readouts,
16588
+ traces,
15156
16589
  interactive: interactive ?? false,
15157
16590
  animate,
15158
16591
  onShapeClick,
@@ -22002,7 +23435,7 @@ function bondPerpendicular(a, b) {
22002
23435
  if (len < 1e-6) return [1, 0, 0];
22003
23436
  return [px / len, py / len, 0];
22004
23437
  }
22005
- var chemistryLog, ChemistryCanvas;
23438
+ var chemistryLog, CHEM_BOND_STATE_COLOR, LONE_PAIR_ANGLES, ChemistryCanvas;
22006
23439
  var init_ChemistryCanvas = __esm({
22007
23440
  "components/learning/molecules/ChemistryCanvas.tsx"() {
22008
23441
  "use client";
@@ -22011,6 +23444,13 @@ var init_ChemistryCanvas = __esm({
22011
23444
  init_LearningCanvas();
22012
23445
  init_learningScene3D();
22013
23446
  chemistryLog = createLogger("almadar:ui:chemistry-canvas");
23447
+ CHEM_BOND_STATE_COLOR = {
23448
+ default: "#6b7280",
23449
+ forming: "#16a34a",
23450
+ breaking: "#dc2626",
23451
+ highlight: "#f59e0b"
23452
+ };
23453
+ LONE_PAIR_ANGLES = [-90, 0, 90, 180];
22014
23454
  ChemistryCanvas = ({
22015
23455
  className,
22016
23456
  width = 600,
@@ -22024,7 +23464,14 @@ var init_ChemistryCanvas = __esm({
22024
23464
  atoms = [],
22025
23465
  bonds = [],
22026
23466
  arrows = [],
23467
+ bondStyle = "thick",
23468
+ containers = [],
23469
+ equation,
23470
+ equationColor,
23471
+ lattice3d,
22027
23472
  shapes = [],
23473
+ readouts,
23474
+ traces,
22028
23475
  showGrid,
22029
23476
  shadows,
22030
23477
  interactive,
@@ -22039,21 +23486,118 @@ var init_ChemistryCanvas = __esm({
22039
23486
  for (const a of atoms) {
22040
23487
  if (a.id) atomById.set(a.id, a);
22041
23488
  }
23489
+ for (const c of containers) {
23490
+ const color = c.color ?? "#64748b";
23491
+ if (c.level != null) {
23492
+ const lv = c.level;
23493
+ out.push({
23494
+ type: "rect",
23495
+ x: c.x + 1,
23496
+ y: c.y + c.height * (1 - lv),
23497
+ width: c.width - 2,
23498
+ height: c.height * lv - 1,
23499
+ color: c.levelColor ?? "#60a5fa",
23500
+ fill: c.levelColor ?? "#60a5fa",
23501
+ opacity: 0.5
23502
+ });
23503
+ }
23504
+ out.push({
23505
+ type: "rect",
23506
+ x: c.x,
23507
+ y: c.y,
23508
+ width: c.width,
23509
+ height: c.height,
23510
+ color,
23511
+ fill: c.fill,
23512
+ lineWidth: c.lineWidth ?? 2
23513
+ });
23514
+ const divider = c.divider ?? "none";
23515
+ if (divider !== "none") {
23516
+ out.push({
23517
+ type: "line",
23518
+ x1: c.x + c.width / 2,
23519
+ y1: c.y,
23520
+ x2: c.x + c.width / 2,
23521
+ y2: c.y + c.height,
23522
+ color: c.dividerColor ?? color,
23523
+ ...divider === "dashed" || divider === "dotted" ? { dash: divider } : {}
23524
+ });
23525
+ }
23526
+ if (c.leftLabel) {
23527
+ out.push({
23528
+ type: "text",
23529
+ x: c.x + c.width * 0.25,
23530
+ y: c.y + 12,
23531
+ text: c.leftLabel,
23532
+ color: "#374151",
23533
+ fontSize: 11,
23534
+ align: "center"
23535
+ });
23536
+ }
23537
+ if (c.rightLabel) {
23538
+ out.push({
23539
+ type: "text",
23540
+ x: c.x + c.width * 0.75,
23541
+ y: c.y + 12,
23542
+ text: c.rightLabel,
23543
+ color: "#374151",
23544
+ fontSize: 11,
23545
+ align: "center"
23546
+ });
23547
+ }
23548
+ if (c.label) {
23549
+ out.push({
23550
+ type: "text",
23551
+ x: c.x + c.width / 2,
23552
+ y: c.y + c.height + 12,
23553
+ text: c.label,
23554
+ color: "#111827",
23555
+ fontSize: 12,
23556
+ align: "center"
23557
+ });
23558
+ }
23559
+ }
22042
23560
  for (const b of bonds) {
22043
23561
  const a = atomById.get(b.from);
22044
23562
  const c = atomById.get(b.to);
22045
23563
  if (!a || !c) continue;
22046
- const color = b.color ?? "#6b7280";
22047
- const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
22048
- out.push({
22049
- type: "line",
22050
- x1: a.x,
22051
- y1: a.y,
22052
- x2: c.x,
22053
- y2: c.y,
22054
- color,
22055
- lineWidth: strokeWidth
22056
- });
23564
+ const state = b.state ?? "default";
23565
+ const color = b.color ?? CHEM_BOND_STATE_COLOR[state];
23566
+ const dash = state === "forming" || state === "breaking" ? "dashed" : void 0;
23567
+ if (bondStyle === "parallel") {
23568
+ const dx = c.x - a.x;
23569
+ const dy = c.y - a.y;
23570
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
23571
+ const ux = dx / dist;
23572
+ const uy = dy / dist;
23573
+ const px = -uy;
23574
+ const py = ux;
23575
+ const offsets = b.type === "double" ? [-3, 3] : b.type === "triple" ? [-4, 0, 4] : [0];
23576
+ for (const off of offsets) {
23577
+ out.push({
23578
+ type: "line",
23579
+ x1: a.x + px * off,
23580
+ y1: a.y + py * off,
23581
+ x2: c.x + px * off,
23582
+ y2: c.y + py * off,
23583
+ color,
23584
+ lineWidth: 2,
23585
+ ...dash ? { dash } : {}
23586
+ });
23587
+ }
23588
+ } else {
23589
+ const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
23590
+ out.push({
23591
+ type: "line",
23592
+ x1: a.x,
23593
+ y1: a.y,
23594
+ x2: c.x,
23595
+ y2: c.y,
23596
+ color,
23597
+ lineWidth: strokeWidth,
23598
+ ...dash ? { dash } : {}
23599
+ });
23600
+ }
22057
23601
  }
22058
23602
  for (const a of arrows) {
22059
23603
  const angle = (a.angle ?? 0) * (Math.PI / 180);
@@ -22102,15 +23646,62 @@ var init_ChemistryCanvas = __esm({
22102
23646
  align: "center"
22103
23647
  });
22104
23648
  }
23649
+ const r = a.radius ?? 14;
23650
+ if (a.charge) {
23651
+ out.push({
23652
+ type: "text",
23653
+ x: a.x + r * 0.85,
23654
+ y: a.y - r * 0.85,
23655
+ text: a.charge,
23656
+ color: "#111827",
23657
+ fontSize: 9,
23658
+ align: "left"
23659
+ });
23660
+ }
23661
+ const lonePairs = Math.max(0, Math.min(4, a.lonePairs ?? 0));
23662
+ for (let k = 0; k < lonePairs; k++) {
23663
+ const angleRad = LONE_PAIR_ANGLES[k] * Math.PI / 180;
23664
+ const cx = a.x + (r + 6) * Math.cos(angleRad);
23665
+ const cy = a.y + (r + 6) * Math.sin(angleRad);
23666
+ const perpX = -Math.sin(angleRad);
23667
+ const perpY = Math.cos(angleRad);
23668
+ for (const sign of [1, -1]) {
23669
+ out.push({
23670
+ type: "circle",
23671
+ x: cx + perpX * 2.5 * sign,
23672
+ y: cy + perpY * 2.5 * sign,
23673
+ radius: 1.5,
23674
+ color: "#374151",
23675
+ fill: "#374151"
23676
+ });
23677
+ }
23678
+ }
23679
+ }
23680
+ if (equation) {
23681
+ out.push({
23682
+ type: "text",
23683
+ x: width / 2,
23684
+ y: 14,
23685
+ text: equation,
23686
+ color: equationColor ?? "#111827",
23687
+ fontSize: 13,
23688
+ align: "center"
23689
+ });
22105
23690
  }
22106
23691
  out.push(...shapes);
22107
23692
  return out;
22108
- }, [atoms, bonds, arrows, shapes]);
23693
+ }, [atoms, bonds, arrows, bondStyle, containers, equation, equationColor, shapes, width]);
22109
23694
  const drawables3D = useMemo(() => {
22110
23695
  if (mode !== "3d") return [];
22111
23696
  if (shapes.length > 0) {
22112
23697
  chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
22113
23698
  }
23699
+ if (containers.length > 0) {
23700
+ chemistryLog.debug("containers ignored in 3D mode (pixel-authored 2D vocabulary)", { count: containers.length });
23701
+ }
23702
+ if (animate) {
23703
+ chemistryLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
23704
+ }
22114
23705
  const out = [];
22115
23706
  const labelColor = labelColorForBackground(backgroundColor);
22116
23707
  const atomById = /* @__PURE__ */ new Map();
@@ -22157,8 +23748,11 @@ var init_ChemistryCanvas = __esm({
22157
23748
  out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
22158
23749
  }
22159
23750
  }
23751
+ if (lattice3d) {
23752
+ out.push(...latticeDrawables(lattice3d, { labelColor }));
23753
+ }
22160
23754
  return out;
22161
- }, [mode, atoms, bonds, arrows, shapes, backgroundColor]);
23755
+ }, [mode, atoms, bonds, arrows, shapes, containers, lattice3d, animate, backgroundColor]);
22162
23756
  const atomIndexById = useMemo(() => {
22163
23757
  const m = /* @__PURE__ */ new Map();
22164
23758
  atoms.forEach((a, i) => {
@@ -22197,6 +23791,8 @@ var init_ChemistryCanvas = __esm({
22197
23791
  height,
22198
23792
  backgroundColor,
22199
23793
  shapes: derivedShapes,
23794
+ readouts,
23795
+ traces,
22200
23796
  interactive: interactive ?? false,
22201
23797
  animate,
22202
23798
  onShapeClick,
@@ -28386,6 +29982,10 @@ var init_ProgressDots = __esm({
28386
29982
  ProgressDots.displayName = "ProgressDots";
28387
29983
  }
28388
29984
  });
29985
+ function formatTick(v) {
29986
+ if (Number.isInteger(v)) return String(v);
29987
+ return v.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
29988
+ }
28389
29989
  var MathCanvas;
28390
29990
  var init_MathCanvas = __esm({
28391
29991
  "components/learning/molecules/MathCanvas.tsx"() {
@@ -28405,10 +30005,19 @@ var init_MathCanvas = __esm({
28405
30005
  showAxes = true,
28406
30006
  showGrid = true,
28407
30007
  gridStep = 1,
30008
+ showTickLabels = false,
30009
+ showCurveLabels = false,
28408
30010
  curves = [],
28409
30011
  points = [],
28410
30012
  vectors = [],
30013
+ regions = [],
30014
+ bars = [],
30015
+ guides = [],
30016
+ angles = [],
30017
+ hops = [],
28411
30018
  shapes = [],
30019
+ readouts,
30020
+ traces,
28412
30021
  interactive = false,
28413
30022
  animate = false,
28414
30023
  onShapeClick,
@@ -28422,6 +30031,8 @@ var init_MathCanvas = __esm({
28422
30031
  const plotH = height - margin * 2;
28423
30032
  const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
28424
30033
  const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
30034
+ const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
30035
+ const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
28425
30036
  if (showGrid) {
28426
30037
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep) {
28427
30038
  const px = mapX(x);
@@ -28432,14 +30043,99 @@ var init_MathCanvas = __esm({
28432
30043
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color: "#e5e7eb", lineWidth: 1 });
28433
30044
  }
28434
30045
  }
30046
+ if (showTickLabels) {
30047
+ const labelEveryX = Math.max(1, Math.ceil((xMax - xMin) / gridStep / Math.floor(plotW / 40)));
30048
+ let kx = 0;
30049
+ for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
30050
+ if (kx % labelEveryX === 0 && x !== 0) {
30051
+ out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: 10, align: "center" });
30052
+ }
30053
+ }
30054
+ const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
30055
+ let ky = 0;
30056
+ for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
30057
+ if (ky % labelEveryY === 0 && y !== 0) {
30058
+ out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: 10, align: "right" });
30059
+ }
30060
+ }
30061
+ if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
30062
+ out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: 10, align: "right" });
30063
+ }
30064
+ }
30065
+ for (const region of regions) {
30066
+ if (!region.samples || region.samples.length === 0) continue;
30067
+ const baseline = region.baseline ?? 0;
30068
+ const clampedPoint = (p) => ({
30069
+ x: mapX(Math.min(xMax, Math.max(xMin, p.x))),
30070
+ y: mapY(Math.min(yMax, Math.max(yMin, p.y)))
30071
+ });
30072
+ const upper = region.samples.map(clampedPoint);
30073
+ const first = region.samples[0];
30074
+ const last = region.samples[region.samples.length - 1];
30075
+ const closing = region.samples2 && region.samples2.length > 0 ? [...region.samples2].reverse().map(clampedPoint) : [clampedPoint({ x: last.x, y: baseline }), clampedPoint({ x: first.x, y: baseline })];
30076
+ const color = region.color ?? "#2563eb";
30077
+ out.push({
30078
+ type: "polygon",
30079
+ points: [...upper, ...closing],
30080
+ fill: color,
30081
+ color,
30082
+ opacity: region.opacity ?? 0.2,
30083
+ lineWidth: 1
30084
+ });
30085
+ if (region.label) {
30086
+ const mid = Math.floor(region.samples.length / 2);
30087
+ out.push({
30088
+ type: "text",
30089
+ x: mapX((first.x + last.x) / 2),
30090
+ y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
30091
+ text: region.label,
30092
+ color: "#111827",
30093
+ fontSize: 11
30094
+ });
30095
+ }
30096
+ }
30097
+ for (const bar of bars) {
30098
+ if (bar.x + bar.width < xMin || bar.x > xMax) continue;
30099
+ const y0 = bar.y0 ?? 0;
30100
+ const color = bar.color ?? "#93c5fd";
30101
+ out.push({
30102
+ type: "rect",
30103
+ x: mapX(bar.x),
30104
+ y: mapY(Math.max(y0, bar.y1)),
30105
+ width: mapX(bar.x + bar.width) - mapX(bar.x),
30106
+ height: Math.abs(mapY(bar.y1) - mapY(y0)),
30107
+ color,
30108
+ fill: color,
30109
+ opacity: bar.opacity ?? 0.5,
30110
+ lineWidth: 1
30111
+ });
30112
+ }
28435
30113
  if (showAxes) {
28436
- const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
28437
- const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
28438
30114
  out.push({ type: "line", x1: margin, y1: xAxisY, x2: width - margin, y2: xAxisY, color: "#374151", lineWidth: 2 });
28439
30115
  out.push({ type: "line", x1: yAxisX, y1: margin, x2: yAxisX, y2: height - margin, color: "#374151", lineWidth: 2 });
28440
30116
  }
30117
+ for (const guide of guides) {
30118
+ const color = guide.color ?? "#9ca3af";
30119
+ const dash = guide.dash ?? "dashed";
30120
+ if (guide.kind === "vline") {
30121
+ if (guide.at < xMin || guide.at > xMax) continue;
30122
+ const px = mapX(guide.at);
30123
+ out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
30124
+ if (guide.label) {
30125
+ out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color: "#111827", fontSize: 11 });
30126
+ }
30127
+ } else {
30128
+ if (guide.at < yMin || guide.at > yMax) continue;
30129
+ const py = mapY(guide.at);
30130
+ out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
30131
+ if (guide.label) {
30132
+ out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color: "#111827", fontSize: 11, align: "right" });
30133
+ }
30134
+ }
30135
+ }
28441
30136
  for (const curve of curves) {
28442
30137
  if (!curve.samples || curve.samples.length < 2) continue;
30138
+ let lastInRange;
28443
30139
  for (let i = 1; i < curve.samples.length; i++) {
28444
30140
  const a = curve.samples[i - 1];
28445
30141
  const b = curve.samples[i];
@@ -28451,19 +30147,97 @@ var init_MathCanvas = __esm({
28451
30147
  x2: mapX(b.x),
28452
30148
  y2: mapY(b.y),
28453
30149
  color: curve.color ?? "#2563eb",
28454
- lineWidth: 2
30150
+ lineWidth: 2,
30151
+ dash: curve.dash
30152
+ });
30153
+ lastInRange = b;
30154
+ }
30155
+ if (showCurveLabels && curve.label && lastInRange) {
30156
+ out.push({
30157
+ type: "text",
30158
+ x: mapX(lastInRange.x) + 6,
30159
+ y: mapY(lastInRange.y) - 6,
30160
+ text: curve.label,
30161
+ color: curve.color ?? "#2563eb",
30162
+ fontSize: 11
30163
+ });
30164
+ }
30165
+ }
30166
+ for (const hop of hops) {
30167
+ const x1 = mapX(hop.from);
30168
+ const x2 = mapX(hop.to);
30169
+ const peak = Math.min(36, plotH * 0.3);
30170
+ const color = hop.color ?? "#7c3aed";
30171
+ out.push({
30172
+ type: "ellipse",
30173
+ x: (x1 + x2) / 2,
30174
+ y: xAxisY,
30175
+ width: Math.abs(x2 - x1),
30176
+ height: 2 * peak,
30177
+ startAngle: 180,
30178
+ endAngle: 360,
30179
+ color
30180
+ });
30181
+ const s = Math.sign(hop.to - hop.from);
30182
+ out.push({
30183
+ type: "polygon",
30184
+ points: [
30185
+ { x: x2, y: xAxisY },
30186
+ { x: x2 - 4 * s, y: xAxisY - 7 },
30187
+ { x: x2 + 2 * s, y: xAxisY - 7 }
30188
+ ],
30189
+ fill: color,
30190
+ color
30191
+ });
30192
+ if (hop.label) {
30193
+ out.push({
30194
+ type: "text",
30195
+ x: (x1 + x2) / 2,
30196
+ y: xAxisY - peak - 8,
30197
+ text: hop.label,
30198
+ color: "#111827",
30199
+ fontSize: 10,
30200
+ align: "center"
30201
+ });
30202
+ }
30203
+ }
30204
+ for (const angle of angles) {
30205
+ const radius = angle.radius ?? 0.8;
30206
+ const color = angle.color ?? "#0ea5e9";
30207
+ out.push({
30208
+ type: "ellipse",
30209
+ x: mapX(angle.x),
30210
+ y: mapY(angle.y),
30211
+ width: 2 * radius * plotW / (xMax - xMin),
30212
+ height: 2 * radius * plotH / (yMax - yMin),
30213
+ startAngle: -angle.to,
30214
+ endAngle: -angle.from,
30215
+ color
30216
+ });
30217
+ if (angle.label) {
30218
+ const mid = (angle.from + angle.to) / 2;
30219
+ const rad = mid * Math.PI / 180;
30220
+ out.push({
30221
+ type: "text",
30222
+ x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
30223
+ y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
30224
+ text: angle.label,
30225
+ color: "#111827",
30226
+ fontSize: 11,
30227
+ align: "center"
28455
30228
  });
28456
30229
  }
28457
30230
  }
28458
30231
  for (const p of points) {
28459
30232
  if (p.x < xMin || p.x > xMax || p.y < yMin || p.y > yMax) continue;
30233
+ const isOpen = p.style === "open";
28460
30234
  out.push({
28461
30235
  type: "circle",
28462
30236
  x: mapX(p.x),
28463
30237
  y: mapY(p.y),
28464
30238
  radius: p.radius ?? 4,
28465
30239
  color: p.color ?? "#dc2626",
28466
- fill: p.color ?? "#dc2626"
30240
+ fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
28467
30241
  });
28468
30242
  if (p.label) {
28469
30243
  out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: "#111827", fontSize: 12 });
@@ -28482,7 +30256,28 @@ var init_MathCanvas = __esm({
28482
30256
  }
28483
30257
  out.push(...shapes);
28484
30258
  return out;
28485
- }, [width, height, xMin, xMax, yMin, yMax, showAxes, showGrid, gridStep, curves, points, vectors, shapes]);
30259
+ }, [
30260
+ width,
30261
+ height,
30262
+ xMin,
30263
+ xMax,
30264
+ yMin,
30265
+ yMax,
30266
+ showAxes,
30267
+ showGrid,
30268
+ gridStep,
30269
+ showTickLabels,
30270
+ showCurveLabels,
30271
+ curves,
30272
+ points,
30273
+ vectors,
30274
+ regions,
30275
+ bars,
30276
+ guides,
30277
+ angles,
30278
+ hops,
30279
+ shapes
30280
+ ]);
28486
30281
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
28487
30282
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
28488
30283
  /* @__PURE__ */ jsx(
@@ -28491,6 +30286,8 @@ var init_MathCanvas = __esm({
28491
30286
  width,
28492
30287
  height,
28493
30288
  shapes: derivedShapes,
30289
+ readouts,
30290
+ traces,
28494
30291
  interactive,
28495
30292
  animate,
28496
30293
  onShapeClick,
@@ -28502,7 +30299,315 @@ var init_MathCanvas = __esm({
28502
30299
  };
28503
30300
  }
28504
30301
  });
28505
- var physicsLog2, PhysicsCanvas;
30302
+ function formatMeterValue(v) {
30303
+ return Number.isInteger(v) ? String(v) : String(Number(v.toFixed(2)));
30304
+ }
30305
+ function sceneObjectShapes(obj, canvasWidth, canvasHeight) {
30306
+ const out = [];
30307
+ const color = obj.color ?? "#334155";
30308
+ switch (obj.kind) {
30309
+ case "ground": {
30310
+ const xStart = obj.x1 ?? 0;
30311
+ const xEnd = obj.x2 ?? canvasWidth;
30312
+ const y = obj.y ?? 0;
30313
+ out.push({ type: "line", x1: xStart, y1: y, x2: xEnd, y2: y, color, lineWidth: 2 });
30314
+ for (let hx = xStart + 7; hx <= xEnd; hx += 14) {
30315
+ out.push({ type: "line", x1: hx, y1: y, x2: hx - 7, y2: y + 7, color, lineWidth: 1 });
30316
+ }
30317
+ if (obj.label) {
30318
+ out.push({
30319
+ type: "text",
30320
+ x: (xStart + xEnd) / 2,
30321
+ y: y - 10,
30322
+ text: obj.label,
30323
+ color: PHYSICS_LABEL_COLOR,
30324
+ fontSize: 11,
30325
+ align: "center"
30326
+ });
30327
+ }
30328
+ break;
30329
+ }
30330
+ case "wall": {
30331
+ const yStart = obj.y1 ?? 0;
30332
+ const yEnd = obj.y2 ?? canvasHeight;
30333
+ const x = obj.x ?? 0;
30334
+ out.push({ type: "line", x1: x, y1: yStart, x2: x, y2: yEnd, color, lineWidth: 2 });
30335
+ for (let hy = yStart + 7; hy <= yEnd; hy += 14) {
30336
+ out.push({ type: "line", x1: x, y1: hy, x2: x - 7, y2: hy + 7, color, lineWidth: 1 });
30337
+ }
30338
+ if (obj.label) {
30339
+ out.push({
30340
+ type: "text",
30341
+ x: x + 12,
30342
+ y: (yStart + yEnd) / 2,
30343
+ text: obj.label,
30344
+ color: PHYSICS_LABEL_COLOR,
30345
+ fontSize: 11,
30346
+ align: "left"
30347
+ });
30348
+ }
30349
+ break;
30350
+ }
30351
+ case "ramp": {
30352
+ const x1 = obj.x1 ?? 0;
30353
+ const y1 = obj.y1 ?? 0;
30354
+ const x2 = obj.x2 ?? canvasWidth;
30355
+ const y2 = obj.y2 ?? canvasHeight;
30356
+ out.push({
30357
+ type: "polygon",
30358
+ points: [
30359
+ { x: x1, y: y1 },
30360
+ { x: x2, y: y2 },
30361
+ { x: x1, y: y2 }
30362
+ ],
30363
+ color,
30364
+ fill: obj.fill ?? "#e2e8f0",
30365
+ lineWidth: 2
30366
+ });
30367
+ if (obj.label) {
30368
+ out.push({
30369
+ type: "text",
30370
+ x: (2 * x1 + x2) / 3,
30371
+ y: (y1 + 2 * y2) / 3,
30372
+ text: obj.label,
30373
+ color: PHYSICS_LABEL_COLOR,
30374
+ fontSize: 11,
30375
+ align: "center"
30376
+ });
30377
+ }
30378
+ break;
30379
+ }
30380
+ case "box": {
30381
+ const x = obj.x ?? 0;
30382
+ const y = obj.y ?? 0;
30383
+ const w = obj.width ?? 40;
30384
+ const h = obj.height ?? 40;
30385
+ out.push({ type: "rect", x, y, width: w, height: h, color, fill: obj.fill, lineWidth: 2 });
30386
+ if (obj.label) {
30387
+ out.push({
30388
+ type: "text",
30389
+ x: x + w / 2,
30390
+ y: y + h / 2,
30391
+ text: obj.label,
30392
+ color: PHYSICS_LABEL_COLOR,
30393
+ fontSize: 11,
30394
+ align: "center"
30395
+ });
30396
+ }
30397
+ break;
30398
+ }
30399
+ case "pivot": {
30400
+ const x = obj.x ?? 0;
30401
+ const y = obj.y ?? 0;
30402
+ out.push({ type: "circle", x, y, radius: 5, color, fill: color });
30403
+ out.push({ type: "line", x1: x - 14, y1: y - 8, x2: x + 14, y2: y - 8, color, lineWidth: 1 });
30404
+ for (let k = 0; k < 5; k++) {
30405
+ const hx = x - 14 + 7 * k;
30406
+ out.push({ type: "line", x1: hx, y1: y - 8, x2: hx - 6, y2: y - 14, color, lineWidth: 1 });
30407
+ }
30408
+ if (obj.label) {
30409
+ out.push({
30410
+ type: "text",
30411
+ x,
30412
+ y: y - 20,
30413
+ text: obj.label,
30414
+ color: PHYSICS_LABEL_COLOR,
30415
+ fontSize: 11,
30416
+ align: "center"
30417
+ });
30418
+ }
30419
+ break;
30420
+ }
30421
+ }
30422
+ return out;
30423
+ }
30424
+ function trailShapes(trail) {
30425
+ const n = trail.points.length;
30426
+ if (n < 2) return [];
30427
+ const color = trail.color ?? "#94a3b8";
30428
+ const lineWidth = trail.width ?? 2;
30429
+ const fade = trail.fade ?? true;
30430
+ const globalOpacity = trail.opacity ?? 1;
30431
+ const out = [];
30432
+ for (let i = 0; i < n - 1; i++) {
30433
+ const a = trail.points[i];
30434
+ const b = trail.points[i + 1];
30435
+ const segmentOpacity = fade ? 0.12 + 0.68 * i / (n - 1) : 0.6;
30436
+ out.push({
30437
+ type: "line",
30438
+ x1: a.x,
30439
+ y1: a.y,
30440
+ x2: b.x,
30441
+ y2: b.y,
30442
+ color,
30443
+ lineWidth,
30444
+ opacity: segmentOpacity * globalOpacity
30445
+ });
30446
+ }
30447
+ return out;
30448
+ }
30449
+ function constraintShapes(c, a, b) {
30450
+ const color = c.color ?? "#9ca3af";
30451
+ const kind = c.kind ?? "rod";
30452
+ if (kind === "rod") {
30453
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2 }];
30454
+ }
30455
+ if (kind === "string") {
30456
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2, dash: "dashed" }];
30457
+ }
30458
+ const COILS = 8;
30459
+ const AMP = 7;
30460
+ const LEAD = 10;
30461
+ const dx = b.x - a.x;
30462
+ const dy = b.y - a.y;
30463
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
30464
+ const ux = dx / dist;
30465
+ const uy = dy / dist;
30466
+ const perpX = -uy;
30467
+ const perpY = ux;
30468
+ const aPrime = { x: a.x + LEAD * ux, y: a.y + LEAD * uy };
30469
+ const bPrime = { x: b.x - LEAD * ux, y: b.y - LEAD * uy };
30470
+ const m = 2 * COILS;
30471
+ const polyline = [{ x: a.x, y: a.y }, aPrime];
30472
+ for (let j = 1; j <= m; j++) {
30473
+ const t = j / (m + 1);
30474
+ const baseX = aPrime.x + t * (bPrime.x - aPrime.x);
30475
+ const baseY = aPrime.y + t * (bPrime.y - aPrime.y);
30476
+ const sign = j % 2 === 0 ? 1 : -1;
30477
+ polyline.push({ x: baseX + sign * AMP * perpX, y: baseY + sign * AMP * perpY });
30478
+ }
30479
+ polyline.push(bPrime, { x: b.x, y: b.y });
30480
+ const out = [];
30481
+ for (let i = 1; i < polyline.length; i++) {
30482
+ out.push({
30483
+ type: "line",
30484
+ x1: polyline[i - 1].x,
30485
+ y1: polyline[i - 1].y,
30486
+ x2: polyline[i].x,
30487
+ y2: polyline[i].y,
30488
+ color,
30489
+ lineWidth: 2
30490
+ });
30491
+ }
30492
+ return out;
30493
+ }
30494
+ function vectorShapes(v, bodyById) {
30495
+ let ax;
30496
+ let ay;
30497
+ if (v.body) {
30498
+ const anchor = bodyById.get(v.body);
30499
+ if (!anchor) return [];
30500
+ ax = anchor.x;
30501
+ ay = anchor.y;
30502
+ } else {
30503
+ ax = v.x ?? 0;
30504
+ ay = v.y ?? 0;
30505
+ }
30506
+ const scale = v.scale ?? 1;
30507
+ const color = v.color ?? "#dc2626";
30508
+ const tx = ax + v.dx * scale;
30509
+ const ty = ay + v.dy * scale;
30510
+ const out = [{ type: "arrow", x1: ax, y1: ay, x2: tx, y2: ty, color, lineWidth: 2, dash: v.dash }];
30511
+ if (v.label) {
30512
+ const dist = Math.max(1e-6, Math.hypot(tx - ax, ty - ay));
30513
+ const ux = (tx - ax) / dist;
30514
+ const uy = (ty - ay) / dist;
30515
+ out.push({
30516
+ type: "text",
30517
+ x: tx + 8 * ux,
30518
+ y: ty + 8 * uy,
30519
+ text: v.label,
30520
+ color,
30521
+ fontSize: 11,
30522
+ align: "center"
30523
+ });
30524
+ }
30525
+ return out;
30526
+ }
30527
+ function angleMarkerShapes(a) {
30528
+ const radius = a.radius ?? 26;
30529
+ const color = a.color ?? "#0ea5e9";
30530
+ const out = [
30531
+ {
30532
+ type: "ellipse",
30533
+ x: a.x,
30534
+ y: a.y,
30535
+ width: radius * 2,
30536
+ height: radius * 2,
30537
+ startAngle: a.from,
30538
+ endAngle: a.to,
30539
+ color,
30540
+ lineWidth: 2
30541
+ }
30542
+ ];
30543
+ if (a.label) {
30544
+ const mid = (a.from + a.to) / 2 * (Math.PI / 180);
30545
+ out.push({
30546
+ type: "text",
30547
+ x: a.x + (radius + 13) * Math.cos(mid),
30548
+ y: a.y + (radius + 13) * Math.sin(mid),
30549
+ text: a.label,
30550
+ color,
30551
+ fontSize: 11,
30552
+ align: "center"
30553
+ });
30554
+ }
30555
+ return out;
30556
+ }
30557
+ function fieldShapes(field, canvasWidth, canvasHeight) {
30558
+ const spacing = field.spacing ?? 48;
30559
+ const size = field.size ?? 14;
30560
+ const color = field.color ?? "#94a3b8";
30561
+ const regionX = field.x ?? 0;
30562
+ const regionY = field.y ?? 0;
30563
+ const regionW = field.width ?? canvasWidth;
30564
+ const regionH = field.height ?? canvasHeight;
30565
+ const out = [];
30566
+ for (let gx = regionX + spacing / 2; gx < regionX + regionW; gx += spacing) {
30567
+ for (let gy = regionY + spacing / 2; gy < regionY + regionH; gy += spacing) {
30568
+ if (field.kind === "arrows") {
30569
+ const rad = (field.angle ?? 0) * Math.PI / 180;
30570
+ const hx = Math.cos(rad) * size / 2;
30571
+ const hy = Math.sin(rad) * size / 2;
30572
+ out.push({ type: "arrow", x1: gx - hx, y1: gy - hy, x2: gx + hx, y2: gy + hy, color, lineWidth: 2 });
30573
+ } else if (field.kind === "into") {
30574
+ const r = size / 3;
30575
+ const d = 0.6 * r * Math.SQRT1_2;
30576
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30577
+ out.push({ type: "line", x1: gx - d, y1: gy - d, x2: gx + d, y2: gy + d, color, lineWidth: 1 });
30578
+ out.push({ type: "line", x1: gx - d, y1: gy + d, x2: gx + d, y2: gy - d, color, lineWidth: 1 });
30579
+ } else {
30580
+ const r = size / 3;
30581
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30582
+ out.push({ type: "circle", x: gx, y: gy, radius: 1.5, color, fill: color });
30583
+ }
30584
+ }
30585
+ }
30586
+ return out;
30587
+ }
30588
+ function meterShapes(meters, canvasHeight) {
30589
+ const n = meters.length;
30590
+ const out = [];
30591
+ const sharedMax = Math.max(1e-6, ...meters.map((m) => m.value));
30592
+ meters.forEach((meter, i) => {
30593
+ const rowY = canvasHeight - 10 - 16 * (n - i);
30594
+ const color = meter.color ?? "#3b82f6";
30595
+ const M = meter.max ?? sharedMax;
30596
+ const w = Math.round(Math.min(1, Math.max(0, meter.value / M)) * 110);
30597
+ out.push({ type: "text", x: 8, y: rowY + 8, text: meter.label, color: PHYSICS_LABEL_COLOR, fontSize: 10 });
30598
+ out.push({ type: "rect", x: 52, y: rowY, width: w, height: 10, color, fill: color });
30599
+ out.push({
30600
+ type: "text",
30601
+ x: 166,
30602
+ y: rowY + 8,
30603
+ text: formatMeterValue(meter.value),
30604
+ color: "#6b7280",
30605
+ fontSize: 9
30606
+ });
30607
+ });
30608
+ return out;
30609
+ }
30610
+ var physicsLog2, PHYSICS_LABEL_COLOR, PhysicsCanvas;
28506
30611
  var init_PhysicsCanvas = __esm({
28507
30612
  "components/learning/molecules/PhysicsCanvas.tsx"() {
28508
30613
  "use client";
@@ -28511,6 +30616,7 @@ var init_PhysicsCanvas = __esm({
28511
30616
  init_LearningCanvas();
28512
30617
  init_learningScene3D();
28513
30618
  physicsLog2 = createLogger("almadar:ui:physics-canvas");
30619
+ PHYSICS_LABEL_COLOR = "#374151";
28514
30620
  PhysicsCanvas = ({
28515
30621
  className,
28516
30622
  width = 600,
@@ -28527,7 +30633,18 @@ var init_PhysicsCanvas = __esm({
28527
30633
  showForces = false,
28528
30634
  velocityScale = 20,
28529
30635
  forceScale = 20,
30636
+ sceneObjects = [],
30637
+ trails = [],
30638
+ vectors = [],
30639
+ surface3d,
30640
+ vectors3d = [],
30641
+ vectorScale = 1,
30642
+ angles = [],
30643
+ field,
30644
+ meters = [],
28530
30645
  shapes = [],
30646
+ readouts,
30647
+ traces,
28531
30648
  showGrid,
28532
30649
  shadows,
28533
30650
  interactive,
@@ -28542,19 +30659,14 @@ var init_PhysicsCanvas = __esm({
28542
30659
  for (const b of bodies) {
28543
30660
  if (b.id) bodyById.set(b.id, b);
28544
30661
  }
30662
+ if (field) out.push(...fieldShapes(field, width, height));
30663
+ for (const obj of sceneObjects) out.push(...sceneObjectShapes(obj, width, height));
30664
+ for (const trail of trails) out.push(...trailShapes(trail));
28545
30665
  for (const c of constraints) {
28546
30666
  const a = bodyById.get(c.from);
28547
30667
  const b = bodyById.get(c.to);
28548
30668
  if (!a || !b) continue;
28549
- out.push({
28550
- type: "line",
28551
- x1: a.x,
28552
- y1: a.y,
28553
- x2: b.x,
28554
- y2: b.y,
28555
- color: c.color ?? "#9ca3af",
28556
- lineWidth: 2
28557
- });
30669
+ out.push(...constraintShapes(c, a, b));
28558
30670
  }
28559
30671
  for (const b of bodies) {
28560
30672
  out.push({
@@ -28599,14 +30711,51 @@ var init_PhysicsCanvas = __esm({
28599
30711
  });
28600
30712
  }
28601
30713
  }
30714
+ for (const v of vectors) out.push(...vectorShapes(v, bodyById));
30715
+ for (const a of angles) out.push(...angleMarkerShapes(a));
30716
+ if (meters.length > 0) out.push(...meterShapes(meters, height));
28602
30717
  out.push(...shapes);
28603
30718
  return out;
28604
- }, [bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes]);
30719
+ }, [
30720
+ bodies,
30721
+ constraints,
30722
+ showVelocity,
30723
+ showForces,
30724
+ velocityScale,
30725
+ forceScale,
30726
+ sceneObjects,
30727
+ trails,
30728
+ vectors,
30729
+ angles,
30730
+ field,
30731
+ meters,
30732
+ shapes,
30733
+ width,
30734
+ height
30735
+ ]);
28605
30736
  const drawables3D = useMemo(() => {
28606
30737
  if (mode !== "3d") return [];
28607
30738
  if (shapes.length > 0) {
28608
30739
  physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
28609
30740
  }
30741
+ if (sceneObjects.length > 0) {
30742
+ physicsLog2.debug("sceneObjects ignored in 3D mode (pixel-authored 2D vocabulary)", { count: sceneObjects.length });
30743
+ }
30744
+ if (vectors.length > 0) {
30745
+ physicsLog2.debug("vectors ignored in 3D mode (pixel-authored 2D vocabulary)", { count: vectors.length });
30746
+ }
30747
+ if (angles.length > 0) {
30748
+ physicsLog2.debug("angles ignored in 3D mode (pixel-authored 2D vocabulary)", { count: angles.length });
30749
+ }
30750
+ if (field) {
30751
+ physicsLog2.debug("field ignored in 3D mode (pixel-authored 2D vocabulary)");
30752
+ }
30753
+ if (meters.length > 0) {
30754
+ physicsLog2.debug("meters ignored in 3D mode (pixel-authored 2D vocabulary)", { count: meters.length });
30755
+ }
30756
+ if (animate) {
30757
+ physicsLog2.debug("animate ignored in 3D mode (motion is entity-state driven)");
30758
+ }
28610
30759
  const out = [];
28611
30760
  const labelColor = labelColorForBackground(backgroundColor);
28612
30761
  const bodyById = /* @__PURE__ */ new Map();
@@ -28654,15 +30803,67 @@ var init_PhysicsCanvas = __esm({
28654
30803
  if (arrow) out.push(arrow);
28655
30804
  }
28656
30805
  }
30806
+ for (const trail of trails) {
30807
+ if (trail.fade !== void 0) {
30808
+ physicsLog2.debug("trail.fade ignored in 3D mode (2D-only fade curve \u2014 3D draws an opaque tube)", { id: trail.id });
30809
+ }
30810
+ const points = trail.points.map((p) => [p.x, p.y, p.z ?? 0]);
30811
+ out.push(
30812
+ ...polylineTube(points, trail.width ?? 0.05, trail.color ?? "#94a3b8", {
30813
+ ...trail.opacity !== void 0 ? { opacity: trail.opacity } : {}
30814
+ })
30815
+ );
30816
+ }
30817
+ if (surface3d) {
30818
+ out.push(...heightFieldMesh(surface3d));
30819
+ }
30820
+ if (vectors3d.length > 0) {
30821
+ out.push(
30822
+ ...arrowField(
30823
+ vectors3d.map((v) => ({
30824
+ id: v.id,
30825
+ from: [v.x, v.y, v.z ?? 0],
30826
+ delta: [v.dx, v.dy, v.dz ?? 0],
30827
+ color: v.color,
30828
+ label: v.label,
30829
+ width: v.width
30830
+ })),
30831
+ { scale: vectorScale, labelColor }
30832
+ )
30833
+ );
30834
+ }
28657
30835
  return out;
28658
- }, [mode, bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes, backgroundColor]);
30836
+ }, [
30837
+ mode,
30838
+ bodies,
30839
+ constraints,
30840
+ showVelocity,
30841
+ showForces,
30842
+ velocityScale,
30843
+ forceScale,
30844
+ shapes,
30845
+ sceneObjects,
30846
+ trails,
30847
+ vectors,
30848
+ surface3d,
30849
+ vectors3d,
30850
+ vectorScale,
30851
+ angles,
30852
+ field,
30853
+ meters,
30854
+ animate,
30855
+ backgroundColor
30856
+ ]);
28659
30857
  const bodyIndexById = useMemo(() => {
28660
30858
  const m = /* @__PURE__ */ new Map();
30859
+ vectors3d.forEach((v, i) => {
30860
+ if (v.id) m.set(v.id, i);
30861
+ });
28661
30862
  bodies.forEach((b, i) => {
28662
30863
  if (b.id) m.set(b.id, i);
28663
30864
  });
28664
30865
  return m;
28665
- }, [bodies]);
30866
+ }, [bodies, vectors3d]);
28666
30867
  if (mode === "3d") {
28667
30868
  return /* @__PURE__ */ jsx(
28668
30869
  LearningScene3D,
@@ -28694,6 +30895,8 @@ var init_PhysicsCanvas = __esm({
28694
30895
  height,
28695
30896
  backgroundColor,
28696
30897
  shapes: derivedShapes,
30898
+ readouts,
30899
+ traces,
28697
30900
  interactive: interactive ?? false,
28698
30901
  animate,
28699
30902
  onShapeClick,
@@ -28844,7 +31047,7 @@ function layoutFlow(nodeIds, adjacency, roots, width, height, margin) {
28844
31047
  }
28845
31048
  return nodeIds.map((id) => positions.get(id));
28846
31049
  }
28847
- function layoutTree(nodeIds, adjacency, roots, width, height, margin) {
31050
+ function layoutTree2(nodeIds, adjacency, roots, width, height, margin) {
28848
31051
  const effectiveRoots = roots.length > 0 ? roots : [nodeIds[0]];
28849
31052
  const layers = assignLayers(nodeIds, adjacency, effectiveRoots);
28850
31053
  const maxLayer = Math.max(...Array.from(layers.values()));
@@ -28900,7 +31103,7 @@ function computeStaticLayout(mode, input) {
28900
31103
  const adjacency = buildAdjacency(nodeIds, edges);
28901
31104
  const roots = findRoots(nodeIds, adjacency);
28902
31105
  if (mode === "flow") return layoutFlow(nodeIds, adjacency, roots, width, height, margin);
28903
- if (mode === "tree") return layoutTree(nodeIds, adjacency, roots, width, height, margin);
31106
+ if (mode === "tree") return layoutTree2(nodeIds, adjacency, roots, width, height, margin);
28904
31107
  return layoutRadial(nodeIds, adjacency, roots, width, height, margin);
28905
31108
  }
28906
31109
  var init_graphViewLayouts = __esm({
@@ -30143,26 +32346,6 @@ var init_Lightbox = __esm({
30143
32346
  Lightbox.displayName = "Lightbox";
30144
32347
  }
30145
32348
  });
30146
- function useMediaQuery(query) {
30147
- const subscribe = useCallback(
30148
- (onChange) => {
30149
- const mql = window.matchMedia(query);
30150
- mql.addEventListener("change", onChange);
30151
- return () => mql.removeEventListener("change", onChange);
30152
- },
30153
- [query]
30154
- );
30155
- return useSyncExternalStore(
30156
- subscribe,
30157
- () => window.matchMedia(query).matches,
30158
- () => false
30159
- );
30160
- }
30161
- var init_useMediaQuery = __esm({
30162
- "hooks/useMediaQuery.ts"() {
30163
- "use client";
30164
- }
30165
- });
30166
32349
  function renderIconInput3(icon, props) {
30167
32350
  return typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsx(Icon, { icon, ...props });
30168
32351
  }
@@ -30201,8 +32384,8 @@ function TableView({
30201
32384
  columns,
30202
32385
  fields,
30203
32386
  itemActions,
30204
- maxInlineActions,
30205
- itemClickEvent,
32387
+ maxInlineActions: _maxInlineActions,
32388
+ itemClickEvent = "",
30206
32389
  selectable = false,
30207
32390
  selectEvent,
30208
32391
  selectedIds,
@@ -30252,7 +32435,6 @@ function TableView({
30252
32435
  const hasMore = pageSize > 0 && visibleCount < ordered2.length;
30253
32436
  const hasRenderProp = typeof children === "function";
30254
32437
  const idField = dndItemIdField ?? "id";
30255
- const isCoarsePointer = useMediaQuery("(pointer: coarse)");
30256
32438
  React87__default.useEffect(() => {
30257
32439
  tableViewLog.debug("render", {
30258
32440
  rowCount: data.length,
@@ -30294,21 +32476,14 @@ function TableView({
30294
32476
  const dir = sortColumn === (col.field ?? col.key) && sortDirection === "asc" ? "desc" : "asc";
30295
32477
  eventBus.emit(`UI:${sortEvent}`, { column: col.field ?? col.key, direction: dir });
30296
32478
  };
30297
- const handleActionClick = (action, row) => (e) => {
30298
- e.stopPropagation();
30299
- const payload = {
30300
- id: row.id,
30301
- row
30302
- };
30303
- eventBus.emit(`UI:${action.event}`, payload);
30304
- };
32479
+ const rowClickEvent = itemClickEvent || actionDefs.find((a) => a.variant !== "danger")?.event;
30305
32480
  const handleRowClick = (row) => () => {
30306
- if (!itemClickEvent) return;
32481
+ if (!rowClickEvent) return;
30307
32482
  const payload = {
30308
32483
  id: row.id,
30309
32484
  row
30310
32485
  };
30311
- eventBus.emit(`UI:${itemClickEvent}`, payload);
32486
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
30312
32487
  };
30313
32488
  const colFloors = React87__default.useMemo(
30314
32489
  () => colDefs.map((col) => {
@@ -30324,10 +32499,7 @@ function TableView({
30324
32499
  const statusNode = isLoading ? /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) }) : error ? /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "error", children: error.message }) }) : data.length === 0 ? /* @__PURE__ */ jsx(Box, { className: "text-center py-12", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: emptyMessage || t("empty.noItems") }) }) : null;
30325
32500
  const lk = LOOKS[look];
30326
32501
  const hasActions = actionDefs.length > 0;
30327
- const effectiveMaxInline = isCoarsePointer ? 0 : maxInlineActions;
30328
- const inlineActionCount = hasActions ? effectiveMaxInline != null ? Math.min(actionDefs.length, effectiveMaxInline) : actionDefs.length : 0;
30329
- const hasOverflowActions = hasActions && effectiveMaxInline != null && actionDefs.length > effectiveMaxInline;
30330
- const actionsTrack = hasActions ? `${inlineActionCount * 6 + (hasOverflowActions ? 3 : 0)}rem` : null;
32502
+ const actionsTrack = hasActions ? "3rem" : null;
30331
32503
  const gridTemplateColumns = [
30332
32504
  selectable ? "auto" : null,
30333
32505
  ...colDefs.map((c, i) => c.width ?? `minmax(${colFloors[i]}ch, 1fr)`),
@@ -30374,7 +32546,7 @@ function TableView({
30374
32546
  col.key
30375
32547
  );
30376
32548
  }),
30377
- hasActions && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)]" })
32549
+ hasActions && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)] border-l border-[var(--color-border)] h-full" })
30378
32550
  ]
30379
32551
  }
30380
32552
  );
@@ -30386,12 +32558,12 @@ function TableView({
30386
32558
  role: "row",
30387
32559
  "data-entity-row": true,
30388
32560
  "data-entity-id": id,
30389
- onClick: itemClickEvent ? handleRowClick(row) : void 0,
32561
+ onClick: rowClickEvent ? handleRowClick(row) : void 0,
30390
32562
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
30391
32563
  className: cn(
30392
32564
  "group items-center gap-3 transition-colors duration-fast",
30393
32565
  hasRenderProp ? "flex" : "grid",
30394
- itemClickEvent && "cursor-pointer",
32566
+ rowClickEvent && "cursor-pointer",
30395
32567
  lk.rowPad,
30396
32568
  lk.divider && "border-b border-[var(--color-border)]",
30397
32569
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -30399,7 +32571,7 @@ function TableView({
30399
32571
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
30400
32572
  ),
30401
32573
  children: [
30402
- selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
32574
+ selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
30403
32575
  Checkbox,
30404
32576
  {
30405
32577
  checked: selected.has(id),
@@ -30420,53 +32592,37 @@ function TableView({
30420
32592
  }
30421
32593
  return /* @__PURE__ */ jsx(Box, { role: "cell", className: cellBase, children: /* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children: formatCell(raw, col.format) }) }, col.key);
30422
32594
  }),
30423
- hasActions && /* @__PURE__ */ jsxs(
32595
+ hasActions && /* @__PURE__ */ jsx(
30424
32596
  HStack,
30425
32597
  {
30426
32598
  gap: "xs",
30427
- onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
32599
+ onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0,
30428
32600
  className: cn(
30429
32601
  // Pinned: the fixed column tracks routinely overflow the caller's
30430
- // scroll container, which used to leave the actions off-screen.
30431
- // Opaque so scrolled cells pass underneath it.
32602
+ // scroll container, which would leave the kebab off-screen.
32603
+ // Opaque + hairline edge so it reads as a pinned column, not a
32604
+ // floating control, while scrolled cells pass underneath.
30432
32605
  "justify-end flex-shrink-0 sticky right-0 z-[1] transition-colors",
32606
+ "border-l border-[var(--color-border)]",
30433
32607
  lk.striped && index % 2 === 1 ? "bg-[var(--color-surface-subtle)]" : "bg-[var(--color-card)] group-hover:bg-[var(--color-surface-subtle)]"
30434
32608
  ),
30435
- children: [
30436
- (effectiveMaxInline != null ? actionDefs.slice(0, effectiveMaxInline) : actionDefs).map((action, i) => /* @__PURE__ */ jsxs(
30437
- Button,
30438
- {
30439
- variant: action.variant === "primary" ? "primary" : "ghost",
30440
- size: "sm",
30441
- onClick: handleActionClick(action, row),
30442
- "data-testid": `action-${action.event}`,
30443
- "data-row-id": String(row.id),
30444
- className: cn(action.variant === "danger" && "text-error hover:text-error hover:bg-error/10"),
30445
- children: [
30446
- action.icon && renderIconInput3(action.icon, { size: "xs", className: "mr-1" }),
30447
- action.label
30448
- ]
30449
- },
30450
- i
30451
- )),
30452
- effectiveMaxInline != null && actionDefs.length > effectiveMaxInline && /* @__PURE__ */ jsx(
30453
- Menu,
30454
- {
30455
- position: "bottom-end",
30456
- 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" }) }),
30457
- items: actionDefs.slice(effectiveMaxInline).map((action) => ({
30458
- label: action.label,
30459
- icon: action.icon,
30460
- event: action.event,
30461
- variant: action.variant === "danger" ? "danger" : "default",
30462
- onClick: () => eventBus.emit(`UI:${action.event}`, {
30463
- id: row.id,
30464
- row
30465
- })
30466
- }))
30467
- }
30468
- )
30469
- ]
32609
+ children: /* @__PURE__ */ jsx(
32610
+ Menu,
32611
+ {
32612
+ position: "bottom-end",
32613
+ trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", "data-row-id": String(row.id), children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
32614
+ items: actionDefs.map((action) => ({
32615
+ label: action.label,
32616
+ icon: action.icon,
32617
+ event: action.event,
32618
+ variant: action.variant === "danger" ? "danger" : "default",
32619
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
32620
+ id: row.id,
32621
+ row
32622
+ })
32623
+ }))
32624
+ }
32625
+ )
30470
32626
  }
30471
32627
  )
30472
32628
  ]
@@ -30509,7 +32665,6 @@ var init_TableView = __esm({
30509
32665
  init_format();
30510
32666
  init_getNestedValue();
30511
32667
  init_useEventBus();
30512
- init_useMediaQuery();
30513
32668
  init_Box();
30514
32669
  init_Stack();
30515
32670
  init_Typography();
@@ -45568,6 +47723,7 @@ var init_component_registry_generated = __esm({
45568
47723
  init_ActionTile();
45569
47724
  init_ActivationBlock();
45570
47725
  init_ComponentPatterns();
47726
+ init_AlgoGraphCanvas();
45571
47727
  init_AlgorithmCanvas();
45572
47728
  init_AnimatedCounter();
45573
47729
  init_AnimatedGraphic();
@@ -45831,6 +47987,7 @@ var init_component_registry_generated = __esm({
45831
47987
  "ActivationBlock": ActivationBlock,
45832
47988
  "Alert": AlertPattern,
45833
47989
  "AlertPattern": AlertPattern,
47990
+ "AlgoGraphCanvas": AlgoGraphCanvas,
45834
47991
  "AlgorithmCanvas": AlgorithmCanvas,
45835
47992
  "AnimatedCounter": AnimatedCounter,
45836
47993
  "AnimatedGraphic": AnimatedGraphic,