@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.
@@ -207,7 +207,7 @@ function useEventBus() {
207
207
  return {
208
208
  ...baseBus,
209
209
  emit: (type, payload, source) => {
210
- if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
210
+ if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".") && !source?.trait) {
211
211
  scopeLog.warn("emit:bare-key-no-scope", { type });
212
212
  }
213
213
  baseBus.emit(type, payload, source);
@@ -11646,6 +11646,14 @@ function shapeBounds(shape) {
11646
11646
  w: shape.radius * 2 + 8,
11647
11647
  h: shape.radius * 2 + 8
11648
11648
  };
11649
+ case "ellipse":
11650
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11651
+ return {
11652
+ x: shape.x - shape.width / 2 - 4,
11653
+ y: shape.y - shape.height / 2 - 4,
11654
+ w: shape.width + 8,
11655
+ h: shape.height + 8
11656
+ };
11649
11657
  case "rect":
11650
11658
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11651
11659
  return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
@@ -11679,13 +11687,14 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
11679
11687
  ctx.closePath();
11680
11688
  ctx.fill();
11681
11689
  }
11682
- function drawShape(ctx, shape, width, height) {
11690
+ function drawShape(ctx, shape, width, height, allShapes) {
11683
11691
  ctx.save();
11684
11692
  const opacity = shape.opacity ?? 1;
11685
11693
  ctx.globalAlpha = opacity;
11686
11694
  const stroke = resolveColor2(shape.color, ctx, "#333333");
11687
11695
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
11688
11696
  ctx.lineWidth = shape.lineWidth ?? 2;
11697
+ if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
11689
11698
  switch (shape.type) {
11690
11699
  case "grid": {
11691
11700
  const step = shape.step ?? 40;
@@ -11751,6 +11760,20 @@ function drawShape(ctx, shape, width, height) {
11751
11760
  ctx.stroke();
11752
11761
  break;
11753
11762
  }
11763
+ case "ellipse": {
11764
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11765
+ const startAngle = (shape.startAngle ?? 0) * Math.PI / 180;
11766
+ const endAngle = (shape.endAngle ?? 360) * Math.PI / 180;
11767
+ ctx.beginPath();
11768
+ ctx.ellipse(shape.x, shape.y, shape.width / 2, shape.height / 2, 0, startAngle, endAngle);
11769
+ if (fill) {
11770
+ ctx.fillStyle = fill;
11771
+ ctx.fill();
11772
+ }
11773
+ ctx.strokeStyle = stroke;
11774
+ ctx.stroke();
11775
+ break;
11776
+ }
11754
11777
  case "rect": {
11755
11778
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11756
11779
  if (fill) {
@@ -11797,21 +11820,153 @@ function drawShape(ctx, shape, width, height) {
11797
11820
  ctx.fillText(shape.text, shape.x, shape.y);
11798
11821
  break;
11799
11822
  }
11823
+ case "venn-region": {
11824
+ const resolveCircles = (ids) => (ids ?? []).flatMap((id) => {
11825
+ const c = allShapes.find((s) => s.type === "circle" && s.id === id);
11826
+ return c && c.x != null && c.y != null && c.radius != null ? [{ x: c.x, y: c.y, radius: c.radius }] : [];
11827
+ });
11828
+ const inside = resolveCircles(shape.inside);
11829
+ if (inside.length === 0) break;
11830
+ const outside = resolveCircles(shape.outside);
11831
+ const off = document.createElement("canvas");
11832
+ off.width = ctx.canvas.width;
11833
+ off.height = ctx.canvas.height;
11834
+ const octx = off.getContext("2d");
11835
+ if (!octx) break;
11836
+ octx.setTransform(ctx.getTransform());
11837
+ for (const c of inside) {
11838
+ const p = new Path2D();
11839
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11840
+ octx.clip(p);
11841
+ }
11842
+ octx.fillStyle = fill ?? stroke;
11843
+ octx.fillRect(0, 0, width, height);
11844
+ octx.globalCompositeOperation = "destination-out";
11845
+ for (const c of outside) {
11846
+ const p = new Path2D();
11847
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11848
+ octx.fill(p);
11849
+ }
11850
+ ctx.save();
11851
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
11852
+ ctx.drawImage(off, 0, 0);
11853
+ ctx.restore();
11854
+ break;
11855
+ }
11800
11856
  }
11801
11857
  ctx.restore();
11802
11858
  }
11803
- var LearningCanvas;
11859
+ function readoutShapes(readouts, width) {
11860
+ const out = [];
11861
+ const chipH = 18;
11862
+ const gap = 6;
11863
+ let rightEdge = width - 6;
11864
+ let rowY = 6;
11865
+ for (const readout of readouts) {
11866
+ const text = `${readout.label}: ${String(readout.value)}`;
11867
+ const chipW = Math.min(170, Math.max(34, text.length * 6 + 12));
11868
+ let chipX = rightEdge - chipW;
11869
+ if (chipX < 4) {
11870
+ rowY += chipH + 4;
11871
+ rightEdge = width - 6;
11872
+ chipX = rightEdge - chipW;
11873
+ }
11874
+ const color = readout.color ?? "#334155";
11875
+ out.push({ type: "rect", x: chipX, y: rowY, width: chipW, height: chipH, color, fill: color });
11876
+ out.push({
11877
+ type: "text",
11878
+ x: chipX + chipW / 2,
11879
+ y: rowY + chipH / 2,
11880
+ text,
11881
+ color: "#ffffff",
11882
+ fontSize: 10,
11883
+ align: "center"
11884
+ });
11885
+ rightEdge = chipX - gap;
11886
+ }
11887
+ return out;
11888
+ }
11889
+ function traceShapes(panel, k, width, height) {
11890
+ const w = panel.width ?? Math.round(width * 0.32);
11891
+ const h = panel.height ?? Math.round(height * 0.28);
11892
+ const x = panel.x ?? width - w - 8;
11893
+ const y = panel.y ?? height - h - 8 - k * (h + 8);
11894
+ const allSamples = panel.series.flatMap((series) => series.samples);
11895
+ let xLo = Math.min(...allSamples.map((p) => p.x));
11896
+ let xHi = Math.max(...allSamples.map((p) => p.x));
11897
+ let yLo = Math.min(...allSamples.map((p) => p.y));
11898
+ let yHi = Math.max(...allSamples.map((p) => p.y));
11899
+ if (xLo === xHi) {
11900
+ xLo -= 1;
11901
+ xHi += 1;
11902
+ }
11903
+ if (yLo === yHi) {
11904
+ yLo -= 1;
11905
+ yHi += 1;
11906
+ }
11907
+ const backgroundColor = panel.backgroundColor ?? "#ffffff";
11908
+ const frameColor = panel.frameColor ?? "#94a3b8";
11909
+ const out = [];
11910
+ out.push({
11911
+ type: "rect",
11912
+ x,
11913
+ y,
11914
+ width: w,
11915
+ height: h,
11916
+ color: backgroundColor,
11917
+ fill: backgroundColor,
11918
+ opacity: panel.backgroundOpacity ?? 0.85
11919
+ });
11920
+ out.push({ type: "rect", x, y, width: w, height: h, color: frameColor, lineWidth: 1 });
11921
+ panel.series.forEach((series, j) => {
11922
+ const color = series.color ?? TRACE_SERIES_COLORS[j % TRACE_SERIES_COLORS.length];
11923
+ const mapped = series.samples.map((p) => ({
11924
+ x: x + 4 + (p.x - xLo) / (xHi - xLo) * (w - 8),
11925
+ y: y + h - 4 - (p.y - yLo) / (yHi - yLo) * (h - 8)
11926
+ }));
11927
+ for (let i = 1; i < mapped.length; i++) {
11928
+ out.push({
11929
+ type: "line",
11930
+ x1: mapped[i - 1].x,
11931
+ y1: mapped[i - 1].y,
11932
+ x2: mapped[i].x,
11933
+ y2: mapped[i].y,
11934
+ color,
11935
+ lineWidth: 1.5
11936
+ });
11937
+ }
11938
+ if (mapped.length > 0) {
11939
+ const last = mapped[mapped.length - 1];
11940
+ out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
11941
+ }
11942
+ if (series.label) {
11943
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
11944
+ }
11945
+ });
11946
+ if (panel.yLabel) {
11947
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
11948
+ }
11949
+ if (panel.xLabel) {
11950
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
11951
+ }
11952
+ return out;
11953
+ }
11954
+ var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
11804
11955
  var init_LearningCanvas = __esm({
11805
11956
  "components/learning/atoms/LearningCanvas.tsx"() {
11806
11957
  "use client";
11807
11958
  init_cn();
11808
11959
  init_useEventBus();
11960
+ DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
11961
+ TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
11809
11962
  LearningCanvas = ({
11810
11963
  className,
11811
11964
  width = 600,
11812
11965
  height = 400,
11813
11966
  backgroundColor,
11814
11967
  shapes = [],
11968
+ readouts,
11969
+ traces,
11815
11970
  interactive = false,
11816
11971
  animate = false,
11817
11972
  onShapeClick,
@@ -11837,6 +11992,12 @@ var init_LearningCanvas = __esm({
11837
11992
  }
11838
11993
  return -1;
11839
11994
  }, [shapes]);
11995
+ const derivedShapes = React87.useMemo(() => {
11996
+ if (!traces?.length && !readouts?.length) return shapes;
11997
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
11998
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
11999
+ return [...shapes, ...traceOut, ...readoutOut];
12000
+ }, [shapes, traces, readouts, width, height]);
11840
12001
  const draw = React87.useCallback(() => {
11841
12002
  const canvas = canvasRef.current;
11842
12003
  if (!canvas) return;
@@ -11853,13 +12014,13 @@ var init_LearningCanvas = __esm({
11853
12014
  ctx.fillStyle = backgroundColor;
11854
12015
  ctx.fillRect(0, 0, width, height);
11855
12016
  }
11856
- for (const shape of shapes) {
11857
- if (shape.type !== "text") drawShape(ctx, shape, width, height);
12017
+ for (const shape of derivedShapes) {
12018
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
11858
12019
  }
11859
- for (const shape of shapes) {
11860
- if (shape.type === "text") drawShape(ctx, shape, width, height);
12020
+ for (const shape of derivedShapes) {
12021
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
11861
12022
  }
11862
- }, [width, height, backgroundColor, shapes]);
12023
+ }, [width, height, backgroundColor, derivedShapes]);
11863
12024
  React87.useEffect(() => {
11864
12025
  draw();
11865
12026
  }, [draw]);
@@ -13792,7 +13953,363 @@ var init_ComponentPatterns = __esm({
13792
13953
  AlertPattern.displayName = "AlertPattern";
13793
13954
  }
13794
13955
  });
13795
- var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, AlgorithmCanvas;
13956
+ function layoutCircle(nodes, width, height) {
13957
+ const cx = width / 2;
13958
+ const cy = height / 2;
13959
+ const radius = Math.max(10, Math.min(cx, cy) - 40);
13960
+ const positions = /* @__PURE__ */ new Map();
13961
+ const n = nodes.length;
13962
+ nodes.forEach((node, i) => {
13963
+ const angle = 2 * Math.PI * i / Math.max(n, 1) - Math.PI / 2;
13964
+ positions.set(node.id, { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) });
13965
+ });
13966
+ return positions;
13967
+ }
13968
+ function layoutTree(nodes, edges, root, width, height) {
13969
+ const nodeIds = nodes.map((n) => n.id);
13970
+ const idSet = new Set(nodeIds);
13971
+ const childrenOf = /* @__PURE__ */ new Map();
13972
+ const hasIncoming = /* @__PURE__ */ new Set();
13973
+ for (const e of edges) {
13974
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
13975
+ const list = childrenOf.get(e.from) ?? [];
13976
+ list.push(e.to);
13977
+ childrenOf.set(e.from, list);
13978
+ hasIncoming.add(e.to);
13979
+ }
13980
+ const depth = /* @__PURE__ */ new Map();
13981
+ const treeChildren = /* @__PURE__ */ new Map();
13982
+ const visited = /* @__PURE__ */ new Set();
13983
+ const bfsFrom = (start) => {
13984
+ if (visited.has(start)) return;
13985
+ visited.add(start);
13986
+ depth.set(start, 0);
13987
+ const queue = [start];
13988
+ while (queue.length > 0) {
13989
+ const u = queue.shift();
13990
+ for (const v of childrenOf.get(u) ?? []) {
13991
+ if (visited.has(v)) continue;
13992
+ visited.add(v);
13993
+ depth.set(v, (depth.get(u) ?? 0) + 1);
13994
+ const list = treeChildren.get(u) ?? [];
13995
+ list.push(v);
13996
+ treeChildren.set(u, list);
13997
+ queue.push(v);
13998
+ }
13999
+ }
14000
+ };
14001
+ const primaryRoot = root && idSet.has(root) ? root : nodeIds.find((id) => !hasIncoming.has(id)) ?? nodeIds[0];
14002
+ const rootsOrder = [];
14003
+ if (primaryRoot !== void 0) {
14004
+ bfsFrom(primaryRoot);
14005
+ rootsOrder.push(primaryRoot);
14006
+ }
14007
+ for (const id of nodeIds) {
14008
+ if (!visited.has(id)) {
14009
+ bfsFrom(id);
14010
+ rootsOrder.push(id);
14011
+ }
14012
+ }
14013
+ let leafCounter = 0;
14014
+ const xSlot = /* @__PURE__ */ new Map();
14015
+ const assignXSlot = (u) => {
14016
+ const children = treeChildren.get(u) ?? [];
14017
+ if (children.length === 0) {
14018
+ const slot = leafCounter++;
14019
+ xSlot.set(u, slot);
14020
+ return slot;
14021
+ }
14022
+ const childSlots = children.map(assignXSlot);
14023
+ const avg = childSlots.reduce((a, b) => a + b, 0) / childSlots.length;
14024
+ xSlot.set(u, avg);
14025
+ return avg;
14026
+ };
14027
+ for (const r of rootsOrder) assignXSlot(r);
14028
+ let maxDepth = 0;
14029
+ for (const d of depth.values()) maxDepth = Math.max(maxDepth, d);
14030
+ const colWidth = width / Math.max(1, leafCounter);
14031
+ const rowHeight = height / (maxDepth + 1);
14032
+ const positions = /* @__PURE__ */ new Map();
14033
+ for (const id of nodeIds) {
14034
+ const slot = xSlot.get(id) ?? 0;
14035
+ const d = depth.get(id) ?? 0;
14036
+ positions.set(id, { x: slot * colWidth + colWidth / 2, y: d * rowHeight + rowHeight / 2 });
14037
+ }
14038
+ return positions;
14039
+ }
14040
+ function layoutLayered(nodes, edges, width, height) {
14041
+ const nodeIds = nodes.map((n) => n.id);
14042
+ const idSet = new Set(nodeIds);
14043
+ const adj = /* @__PURE__ */ new Map();
14044
+ const remainingIndegree = /* @__PURE__ */ new Map();
14045
+ for (const id of nodeIds) remainingIndegree.set(id, 0);
14046
+ for (const e of edges) {
14047
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
14048
+ const list = adj.get(e.from) ?? [];
14049
+ list.push(e.to);
14050
+ adj.set(e.from, list);
14051
+ remainingIndegree.set(e.to, (remainingIndegree.get(e.to) ?? 0) + 1);
14052
+ }
14053
+ const layer = /* @__PURE__ */ new Map();
14054
+ const dequeued = /* @__PURE__ */ new Set();
14055
+ const queue = [];
14056
+ for (const id of nodeIds) {
14057
+ if ((remainingIndegree.get(id) ?? 0) === 0) {
14058
+ layer.set(id, 0);
14059
+ queue.push(id);
14060
+ }
14061
+ }
14062
+ while (queue.length > 0) {
14063
+ const u = queue.shift();
14064
+ dequeued.add(u);
14065
+ for (const v of adj.get(u) ?? []) {
14066
+ const candidate = (layer.get(u) ?? 0) + 1;
14067
+ layer.set(v, Math.max(layer.get(v) ?? 0, candidate));
14068
+ remainingIndegree.set(v, (remainingIndegree.get(v) ?? 0) - 1);
14069
+ if ((remainingIndegree.get(v) ?? 0) === 0 && !dequeued.has(v)) {
14070
+ queue.push(v);
14071
+ }
14072
+ }
14073
+ }
14074
+ let baseMaxLayer = 0;
14075
+ for (const id of nodeIds) {
14076
+ if (dequeued.has(id)) baseMaxLayer = Math.max(baseMaxLayer, layer.get(id) ?? 0);
14077
+ }
14078
+ const cycleLayer = baseMaxLayer + 1;
14079
+ let maxLayer = baseMaxLayer;
14080
+ for (const id of nodeIds) {
14081
+ if (!dequeued.has(id)) {
14082
+ layer.set(id, cycleLayer);
14083
+ maxLayer = cycleLayer;
14084
+ }
14085
+ }
14086
+ const colWidth = width / Math.max(1, maxLayer + 1);
14087
+ const byLayer = /* @__PURE__ */ new Map();
14088
+ for (const id of nodeIds) {
14089
+ const l = layer.get(id) ?? 0;
14090
+ const list = byLayer.get(l) ?? [];
14091
+ list.push(id);
14092
+ byLayer.set(l, list);
14093
+ }
14094
+ const positions = /* @__PURE__ */ new Map();
14095
+ for (const [l, ids] of byLayer) {
14096
+ const rowHeight = height / ids.length;
14097
+ ids.forEach((id, i) => {
14098
+ positions.set(id, { x: l * colWidth + colWidth / 2, y: i * rowHeight + rowHeight / 2 });
14099
+ });
14100
+ }
14101
+ return positions;
14102
+ }
14103
+ function computePositions(nodes, edges, layout, root, width, height) {
14104
+ switch (layout) {
14105
+ case "circle":
14106
+ return layoutCircle(nodes, width, height);
14107
+ case "tree":
14108
+ return layoutTree(nodes, edges, root, width, height);
14109
+ case "layered":
14110
+ return layoutLayered(nodes, edges, width, height);
14111
+ case "manual":
14112
+ default: {
14113
+ const positions = /* @__PURE__ */ new Map();
14114
+ for (const n of nodes) positions.set(n.id, { x: n.x ?? 0, y: n.y ?? 0 });
14115
+ return positions;
14116
+ }
14117
+ }
14118
+ }
14119
+ var NODE_STATE_COLOR, EDGE_STATE_COLOR, DEFAULT_NODE_RADIUS, AlgoGraphCanvas;
14120
+ var init_AlgoGraphCanvas = __esm({
14121
+ "components/learning/molecules/AlgoGraphCanvas.tsx"() {
14122
+ "use client";
14123
+ init_atoms();
14124
+ init_Stack();
14125
+ init_LearningCanvas();
14126
+ NODE_STATE_COLOR = {
14127
+ unvisited: "#cbd5e1",
14128
+ frontier: "#f59e0b",
14129
+ current: "#ef4444",
14130
+ visited: "#22c55e",
14131
+ goal: "#8b5cf6",
14132
+ path: "#0ea5e9"
14133
+ };
14134
+ EDGE_STATE_COLOR = {
14135
+ default: "#9ca3af",
14136
+ tree: "#16a34a",
14137
+ relaxed: "#f59e0b",
14138
+ candidate: "#38bdf8",
14139
+ path: "#dc2626"
14140
+ };
14141
+ DEFAULT_NODE_RADIUS = 18;
14142
+ AlgoGraphCanvas = ({
14143
+ className,
14144
+ width = 600,
14145
+ height = 400,
14146
+ title,
14147
+ backgroundColor,
14148
+ nodes = [],
14149
+ edges = [],
14150
+ layout = "manual",
14151
+ root,
14152
+ shapes = [],
14153
+ interactive = false,
14154
+ animate = false,
14155
+ onShapeClick,
14156
+ onNodeClick,
14157
+ isLoading,
14158
+ error
14159
+ }) => {
14160
+ const nodeById = React87.useMemo(() => {
14161
+ const m = /* @__PURE__ */ new Map();
14162
+ for (const n of nodes) m.set(n.id, n);
14163
+ return m;
14164
+ }, [nodes]);
14165
+ const nodeIndexById = React87.useMemo(() => {
14166
+ const m = /* @__PURE__ */ new Map();
14167
+ nodes.forEach((n, i) => m.set(n.id, i));
14168
+ return m;
14169
+ }, [nodes]);
14170
+ const derivedShapes = React87.useMemo(() => {
14171
+ const out = [];
14172
+ const positions = computePositions(nodes, edges, layout, root, width, height);
14173
+ const edgeGeoms = [];
14174
+ for (const e of edges) {
14175
+ const a = nodeById.get(e.from);
14176
+ const b = nodeById.get(e.to);
14177
+ const posA = positions.get(e.from);
14178
+ const posB = positions.get(e.to);
14179
+ if (!a || !b || !posA || !posB) continue;
14180
+ const rA = a.radius ?? DEFAULT_NODE_RADIUS;
14181
+ const rB = b.radius ?? DEFAULT_NODE_RADIUS;
14182
+ const dx = posB.x - posA.x;
14183
+ const dy = posB.y - posA.y;
14184
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
14185
+ const ux = dx / dist;
14186
+ const uy = dy / dist;
14187
+ edgeGeoms.push({
14188
+ directed: e.directed ?? false,
14189
+ x1: posA.x + ux * rA,
14190
+ y1: posA.y + uy * rA,
14191
+ x2: posB.x - ux * rB,
14192
+ y2: posB.y - uy * rB,
14193
+ color: e.color ?? EDGE_STATE_COLOR[e.state ?? "default"],
14194
+ label: e.label ?? (e.weight != null ? String(e.weight) : void 0)
14195
+ });
14196
+ }
14197
+ for (const g of edgeGeoms) {
14198
+ out.push({
14199
+ type: g.directed ? "arrow" : "line",
14200
+ x1: g.x1,
14201
+ y1: g.y1,
14202
+ x2: g.x2,
14203
+ y2: g.y2,
14204
+ color: g.color,
14205
+ lineWidth: 2
14206
+ });
14207
+ }
14208
+ for (const g of edgeGeoms) {
14209
+ if (g.label === void 0) continue;
14210
+ const midX = (g.x1 + g.x2) / 2;
14211
+ const midY = (g.y1 + g.y2) / 2;
14212
+ const dx = g.x2 - g.x1;
14213
+ const dy = g.y2 - g.y1;
14214
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
14215
+ const perpX = -(dy / dist);
14216
+ const perpY = dx / dist;
14217
+ out.push({
14218
+ type: "text",
14219
+ x: midX + perpX * 10,
14220
+ y: midY + perpY * 10,
14221
+ text: g.label,
14222
+ fontSize: 11,
14223
+ align: "center",
14224
+ color: "#374151"
14225
+ });
14226
+ }
14227
+ const nodeGeoms = [];
14228
+ for (const n of nodes) {
14229
+ const pos = positions.get(n.id);
14230
+ if (!pos) continue;
14231
+ nodeGeoms.push({
14232
+ id: n.id,
14233
+ x: pos.x,
14234
+ y: pos.y,
14235
+ radius: n.radius ?? DEFAULT_NODE_RADIUS,
14236
+ color: n.color ?? NODE_STATE_COLOR[n.state ?? "unvisited"],
14237
+ label: n.label,
14238
+ badge: n.badge
14239
+ });
14240
+ }
14241
+ for (const g of nodeGeoms) {
14242
+ out.push({ type: "circle", id: g.id, x: g.x, y: g.y, radius: g.radius, color: g.color, fill: `${g.color}33` });
14243
+ }
14244
+ const badgeGeoms = [];
14245
+ for (const g of nodeGeoms) {
14246
+ if (!g.badge) continue;
14247
+ const w = Math.min(42, Math.max(18, g.badge.text.length * 6 + 10));
14248
+ badgeGeoms.push({
14249
+ cx: g.x + g.radius * 0.75,
14250
+ cy: g.y - g.radius * 0.75,
14251
+ w,
14252
+ h: 14,
14253
+ // Borderless pill: same color drives both stroke and fill.
14254
+ color: g.badge.color ?? "#1e293b",
14255
+ text: g.badge.text
14256
+ });
14257
+ }
14258
+ for (const b of badgeGeoms) {
14259
+ out.push({ type: "rect", x: b.cx - b.w / 2, y: b.cy - b.h / 2, width: b.w, height: b.h, color: b.color, fill: b.color });
14260
+ }
14261
+ for (const b of badgeGeoms) {
14262
+ out.push({ type: "text", x: b.cx, y: b.cy, text: b.text, fontSize: 9, align: "center", color: "#ffffff" });
14263
+ }
14264
+ for (const g of nodeGeoms) {
14265
+ if (g.label === void 0) continue;
14266
+ out.push({
14267
+ type: "text",
14268
+ x: g.x,
14269
+ y: g.y + g.radius + 14,
14270
+ text: g.label,
14271
+ fontSize: 12,
14272
+ align: "center",
14273
+ color: "#111827"
14274
+ });
14275
+ }
14276
+ out.push(...shapes);
14277
+ return out;
14278
+ }, [nodes, edges, layout, root, width, height, nodeById, shapes]);
14279
+ const handleShapeClick = React87.useCallback(
14280
+ (payload) => {
14281
+ if (payload.type === "circle" && payload.id) {
14282
+ const node = nodeById.get(payload.id);
14283
+ const idx = nodeIndexById.get(payload.id);
14284
+ if (node && idx !== void 0) {
14285
+ onNodeClick?.({ id: node.id, label: node.label, index: idx });
14286
+ }
14287
+ }
14288
+ onShapeClick?.(payload);
14289
+ },
14290
+ [nodeById, nodeIndexById, onNodeClick, onShapeClick]
14291
+ );
14292
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
14293
+ title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
14294
+ /* @__PURE__ */ jsxRuntime.jsx(
14295
+ LearningCanvas,
14296
+ {
14297
+ width,
14298
+ height,
14299
+ backgroundColor,
14300
+ shapes: derivedShapes,
14301
+ interactive,
14302
+ animate,
14303
+ onShapeClick: onShapeClick || onNodeClick ? handleShapeClick : void 0,
14304
+ isLoading,
14305
+ error
14306
+ }
14307
+ )
14308
+ ] }) });
14309
+ };
14310
+ }
14311
+ });
14312
+ var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, PANEL_FAMILY_ORDER, RANGE_COLOR_DEFAULT, RANGE_FILL_OPACITY, BRACKET_TOP_OFFSET, BRACKET_ROW_H, BRACKET_TICK_H, BRACKET_LABEL_OFFSET, SLOT_EMPTY_FILL, SLOT_EMPTY_STROKE, SLOT_FILLED_STROKE, SLOT_HIGHLIGHT_DEFAULT, SLOT_VALUE_TEXT_COLOR, FRAME_ACTIVE_COLOR, FRAME_RETURNING_COLOR, FRAME_DONE_COLOR, FRAME_LABEL_COLOR, FRAME_DETAIL_COLOR, FRAME_TWO_LINE_MIN_H, BUCKET_INDEX_FILL, BUCKET_INDEX_STROKE, BUCKET_INDEX_TEXT, BUCKET_ENTRY_TEXT, BUCKET_ENTRY_DEFAULT, BUCKET_ENTRY_HIGHLIGHT, BUCKET_ENTRY_PROBING, BUCKET_ENTRY_MIN_W, BUCKET_ENTRY_MAX_W, AXIS_LABEL_COLOR, AXIS_LABEL_FONT_SIZE, CORNER_TEXT_COLOR, CORNER_FONT_SIZE, CORNER_MIN_CELL, CORNER_INSET_X, CORNER_INSET_Y, AUX_PRIMARY_RATIO, AUX_LABEL_BAND, AUX_BASELINE_PAD, AlgorithmCanvas;
13796
14313
  var init_AlgorithmCanvas = __esm({
13797
14314
  "components/learning/molecules/AlgorithmCanvas.tsx"() {
13798
14315
  "use client";
@@ -13804,6 +14321,43 @@ var init_AlgorithmCanvas = __esm({
13804
14321
  DEFAULT_POINTER_COLOR = "#dc2626";
13805
14322
  POINTER_BAND = 34;
13806
14323
  TOP_PAD = 26;
14324
+ PANEL_FAMILY_ORDER = ["bars", "slots", "cells", "buckets", "frames"];
14325
+ RANGE_COLOR_DEFAULT = "#3b82f6";
14326
+ RANGE_FILL_OPACITY = 0.15;
14327
+ BRACKET_TOP_OFFSET = 16;
14328
+ BRACKET_ROW_H = 14;
14329
+ BRACKET_TICK_H = 6;
14330
+ BRACKET_LABEL_OFFSET = 6;
14331
+ SLOT_EMPTY_FILL = "#f1f5f9";
14332
+ SLOT_EMPTY_STROKE = "#cbd5e1";
14333
+ SLOT_FILLED_STROKE = "#9ca3af";
14334
+ SLOT_HIGHLIGHT_DEFAULT = "#f59e0b";
14335
+ SLOT_VALUE_TEXT_COLOR = "#ffffff";
14336
+ FRAME_ACTIVE_COLOR = "#3b82f6";
14337
+ FRAME_RETURNING_COLOR = "#f59e0b";
14338
+ FRAME_DONE_COLOR = "#94a3b8";
14339
+ FRAME_LABEL_COLOR = "#ffffff";
14340
+ FRAME_DETAIL_COLOR = "#e2e8f0";
14341
+ FRAME_TWO_LINE_MIN_H = 22;
14342
+ BUCKET_INDEX_FILL = "#e2e8f0";
14343
+ BUCKET_INDEX_STROKE = "#9ca3af";
14344
+ BUCKET_INDEX_TEXT = "#374151";
14345
+ BUCKET_ENTRY_TEXT = "#ffffff";
14346
+ BUCKET_ENTRY_DEFAULT = "#3b82f6";
14347
+ BUCKET_ENTRY_HIGHLIGHT = "#f59e0b";
14348
+ BUCKET_ENTRY_PROBING = "#38bdf8";
14349
+ BUCKET_ENTRY_MIN_W = 24;
14350
+ BUCKET_ENTRY_MAX_W = 64;
14351
+ AXIS_LABEL_COLOR = "#6b7280";
14352
+ AXIS_LABEL_FONT_SIZE = 10;
14353
+ CORNER_TEXT_COLOR = "#111827";
14354
+ CORNER_FONT_SIZE = 7;
14355
+ CORNER_MIN_CELL = 28;
14356
+ CORNER_INSET_X = 3;
14357
+ CORNER_INSET_Y = 6;
14358
+ AUX_PRIMARY_RATIO = 0.6;
14359
+ AUX_LABEL_BAND = 18;
14360
+ AUX_BASELINE_PAD = 8;
13807
14361
  AlgorithmCanvas = ({
13808
14362
  className,
13809
14363
  width = 600,
@@ -13813,6 +14367,14 @@ var init_AlgorithmCanvas = __esm({
13813
14367
  bars = [],
13814
14368
  cells = [],
13815
14369
  pointers = [],
14370
+ ranges = [],
14371
+ slots = [],
14372
+ slotOrientation = "horizontal",
14373
+ frames = [],
14374
+ buckets = [],
14375
+ auxBars = [],
14376
+ rowLabels = [],
14377
+ colLabels = [],
13816
14378
  shapes = [],
13817
14379
  interactive = false,
13818
14380
  animate = false,
@@ -13822,12 +14384,35 @@ var init_AlgorithmCanvas = __esm({
13822
14384
  }) => {
13823
14385
  const derivedShapes = React87.useMemo(() => {
13824
14386
  const out = [];
14387
+ const presence = {
14388
+ bars: bars.length > 0,
14389
+ slots: slots.length > 0,
14390
+ cells: cells.length > 0,
14391
+ buckets: buckets.length > 0,
14392
+ frames: frames.length > 0
14393
+ };
14394
+ const panelCount = PANEL_FAMILY_ORDER.filter((f3) => presence[f3]).length;
14395
+ const panelHeight = height / Math.max(1, panelCount);
14396
+ const panelY = { bars: 0, slots: 0, cells: 0, buckets: 0, frames: 0 };
14397
+ let compactIndex = 0;
14398
+ PANEL_FAMILY_ORDER.forEach((f3) => {
14399
+ if (presence[f3]) {
14400
+ panelY[f3] = compactIndex * panelHeight;
14401
+ compactIndex += 1;
14402
+ }
14403
+ });
13825
14404
  if (bars.length > 0) {
14405
+ const panelYBars = panelY.bars;
13826
14406
  const slot = width / bars.length;
13827
14407
  const barW = slot * 0.8;
13828
14408
  const gap = slot * 0.1;
13829
- const baseline = height - POINTER_BAND;
13830
- const usableH = baseline - TOP_PAD;
14409
+ const bracketRanges = ranges.filter((r) => r.kind === "bracket");
14410
+ const bracketCount = bracketRanges.length;
14411
+ const bracketHeadroom = bracketCount > 0 ? BRACKET_TOP_OFFSET + bracketCount * BRACKET_ROW_H : 0;
14412
+ const hasAux = auxBars.length > 0;
14413
+ const primaryH = hasAux ? panelHeight * AUX_PRIMARY_RATIO : panelHeight;
14414
+ const baseline = panelYBars + primaryH - POINTER_BAND;
14415
+ const usableH = baseline - (panelYBars + TOP_PAD + bracketHeadroom);
13831
14416
  const maxV = Math.max(1, ...bars.map((b) => Number.isFinite(b.value) ? b.value : 0));
13832
14417
  bars.forEach((bar, i) => {
13833
14418
  const v = Number.isFinite(bar.value) ? bar.value : 0;
@@ -13857,6 +14442,89 @@ var init_AlgorithmCanvas = __esm({
13857
14442
  });
13858
14443
  }
13859
14444
  });
14445
+ ranges.forEach((r) => {
14446
+ const kind = r.kind ?? "fill";
14447
+ if (kind !== "fill") return;
14448
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14449
+ out.push({
14450
+ type: "rect",
14451
+ x: r.from * slot,
14452
+ y: panelYBars,
14453
+ width: (r.to - r.from + 1) * slot,
14454
+ height: primaryH,
14455
+ color,
14456
+ fill: color,
14457
+ opacity: RANGE_FILL_OPACITY
14458
+ });
14459
+ if (r.label) {
14460
+ out.push({
14461
+ type: "text",
14462
+ x: r.from * slot + 4,
14463
+ // Sits below the bracket block (if any) so fill and bracket labels never collide.
14464
+ y: panelYBars + 10 + bracketHeadroom,
14465
+ text: r.label,
14466
+ color,
14467
+ fontSize: 10,
14468
+ align: "left"
14469
+ });
14470
+ }
14471
+ });
14472
+ bracketRanges.forEach((r, i) => {
14473
+ const bracketY = panelYBars + BRACKET_TOP_OFFSET + i * BRACKET_ROW_H;
14474
+ const x1 = r.from * slot + slot * 0.1;
14475
+ const x2 = (r.to + 1) * slot - slot * 0.1;
14476
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14477
+ out.push({ type: "line", x1, y1: bracketY, x2, y2: bracketY, color, lineWidth: 2 });
14478
+ out.push({ type: "line", x1, y1: bracketY, x2: x1, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14479
+ out.push({ type: "line", x1: x2, y1: bracketY, x2, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14480
+ if (r.label) {
14481
+ out.push({
14482
+ type: "text",
14483
+ x: (x1 + x2) / 2,
14484
+ y: bracketY - BRACKET_LABEL_OFFSET,
14485
+ text: r.label,
14486
+ color,
14487
+ fontSize: 10,
14488
+ align: "center"
14489
+ });
14490
+ }
14491
+ });
14492
+ if (hasAux) {
14493
+ const auxH = panelHeight - primaryH;
14494
+ const slot2 = width / auxBars.length;
14495
+ const auxBaseline = panelYBars + primaryH + auxH - AUX_BASELINE_PAD;
14496
+ const auxUsableH = auxBaseline - (panelYBars + primaryH + AUX_LABEL_BAND);
14497
+ const maxAuxV = Math.max(1, ...auxBars.map((b) => Number.isFinite(b.value) ? b.value : 0));
14498
+ auxBars.forEach((bar, i) => {
14499
+ const v = Number.isFinite(bar.value) ? bar.value : 0;
14500
+ const bh = Math.max(0, v / maxAuxV * auxUsableH);
14501
+ const x = i * slot2 + slot2 * 0.1;
14502
+ const w = slot2 * 0.8;
14503
+ const color = bar.color ?? DEFAULT_BAR_COLOR;
14504
+ out.push({
14505
+ type: "rect",
14506
+ id: `auxbar-${i}`,
14507
+ x,
14508
+ y: auxBaseline - bh,
14509
+ width: w,
14510
+ height: bh,
14511
+ color,
14512
+ fill: color
14513
+ });
14514
+ const label = bar.label ?? (auxBars.length <= 24 ? String(v) : void 0);
14515
+ if (label) {
14516
+ out.push({
14517
+ type: "text",
14518
+ x: x + w / 2,
14519
+ y: auxBaseline - bh - 8,
14520
+ text: label,
14521
+ color: "#374151",
14522
+ fontSize: 11,
14523
+ align: "center"
14524
+ });
14525
+ }
14526
+ });
14527
+ }
13860
14528
  pointers.forEach((p) => {
13861
14529
  if (p.index < 0 || p.index >= bars.length) return;
13862
14530
  const cx = p.index * slot + slot / 2;
@@ -13864,7 +14532,7 @@ var init_AlgorithmCanvas = __esm({
13864
14532
  out.push({
13865
14533
  type: "arrow",
13866
14534
  x1: cx,
13867
- y1: height - 6,
14535
+ y1: panelYBars + primaryH - 18,
13868
14536
  x2: cx,
13869
14537
  y2: baseline + 4,
13870
14538
  color,
@@ -13874,7 +14542,7 @@ var init_AlgorithmCanvas = __esm({
13874
14542
  out.push({
13875
14543
  type: "text",
13876
14544
  x: cx,
13877
- y: height - 22,
14545
+ y: panelYBars + primaryH - 8,
13878
14546
  text: p.label,
13879
14547
  color,
13880
14548
  fontSize: 11,
@@ -13883,14 +14551,111 @@ var init_AlgorithmCanvas = __esm({
13883
14551
  }
13884
14552
  });
13885
14553
  }
14554
+ if (slots.length > 0) {
14555
+ const panelYSlots = panelY.slots;
14556
+ const n = slots.length;
14557
+ const vertical = slotOrientation === "vertical";
14558
+ const vBoxH = panelHeight / n;
14559
+ const vBoxW = Math.min(width * 0.5, 120);
14560
+ const vBoxX = (width - vBoxW) / 2;
14561
+ const hCellW = width / n;
14562
+ const hBoxW = hCellW * 0.82;
14563
+ const hBoxH = Math.min(panelHeight * 0.6, 48);
14564
+ const hBoxY = panelYSlots + (panelHeight - hBoxH) / 2;
14565
+ const slotBox = (i) => vertical ? { x: vBoxX, y: panelYSlots + panelHeight - (i + 1) * vBoxH, width: vBoxW, height: vBoxH } : { x: i * hCellW + (hCellW - hBoxW) / 2, y: hBoxY, width: hBoxW, height: hBoxH };
14566
+ slots.forEach((s, i) => {
14567
+ const box = slotBox(i);
14568
+ const state = s.state ?? "filled";
14569
+ const fill = state === "empty" ? SLOT_EMPTY_FILL : state === "highlight" ? s.color ?? SLOT_HIGHLIGHT_DEFAULT : s.color ?? DEFAULT_BAR_COLOR;
14570
+ const stroke = state === "empty" ? SLOT_EMPTY_STROKE : SLOT_FILLED_STROKE;
14571
+ out.push({
14572
+ type: "rect",
14573
+ id: `slot-${i}`,
14574
+ x: box.x,
14575
+ y: box.y,
14576
+ width: box.width,
14577
+ height: box.height,
14578
+ color: stroke,
14579
+ fill
14580
+ });
14581
+ if (s.value != null && state !== "empty") {
14582
+ out.push({
14583
+ type: "text",
14584
+ x: box.x + box.width / 2,
14585
+ y: box.y + box.height / 2,
14586
+ text: String(s.value),
14587
+ color: SLOT_VALUE_TEXT_COLOR,
14588
+ fontSize: 12,
14589
+ align: "center"
14590
+ });
14591
+ }
14592
+ });
14593
+ if (bars.length === 0) {
14594
+ pointers.forEach((p) => {
14595
+ if (p.index < 0 || p.index >= slots.length) return;
14596
+ const box = slotBox(p.index);
14597
+ const color = p.color ?? DEFAULT_POINTER_COLOR;
14598
+ if (vertical) {
14599
+ const cy = box.y + box.height / 2;
14600
+ out.push({
14601
+ type: "arrow",
14602
+ x1: box.x + box.width + 34,
14603
+ y1: cy,
14604
+ x2: box.x + box.width + 4,
14605
+ y2: cy,
14606
+ color,
14607
+ lineWidth: 2
14608
+ });
14609
+ if (p.label) {
14610
+ out.push({
14611
+ type: "text",
14612
+ x: box.x + box.width + 38,
14613
+ y: cy,
14614
+ text: p.label,
14615
+ color,
14616
+ fontSize: 11,
14617
+ align: "left"
14618
+ });
14619
+ }
14620
+ } else {
14621
+ const cx = box.x + box.width / 2;
14622
+ out.push({
14623
+ type: "arrow",
14624
+ x1: cx,
14625
+ y1: panelYSlots + panelHeight - 18,
14626
+ x2: cx,
14627
+ y2: box.y + box.height + 4,
14628
+ color,
14629
+ lineWidth: 2
14630
+ });
14631
+ if (p.label) {
14632
+ out.push({
14633
+ type: "text",
14634
+ x: cx,
14635
+ y: panelYSlots + panelHeight - 8,
14636
+ text: p.label,
14637
+ color,
14638
+ fontSize: 11,
14639
+ align: "center"
14640
+ });
14641
+ }
14642
+ }
14643
+ });
14644
+ }
14645
+ }
13886
14646
  if (cells.length > 0) {
14647
+ const panelYCells = panelY.cells;
13887
14648
  const maxCol = Math.max(0, ...cells.map((c) => c.col)) + 1;
13888
14649
  const maxRow = Math.max(0, ...cells.map((c) => c.row)) + 1;
13889
- const cw = width / maxCol;
13890
- const ch = height / maxRow;
14650
+ const colLabelH = colLabels.length > 0 ? 16 : 0;
14651
+ const rowLabelW = rowLabels.length > 0 ? 20 : 0;
14652
+ const gridX0 = rowLabelW;
14653
+ const gridY0 = panelYCells + colLabelH;
14654
+ const cw = (width - rowLabelW) / maxCol;
14655
+ const ch = (panelHeight - colLabelH) / maxRow;
13891
14656
  cells.forEach((c, i) => {
13892
- const x = c.col * cw;
13893
- const y = c.row * ch;
14657
+ const x = gridX0 + c.col * cw;
14658
+ const y = gridY0 + c.row * ch;
13894
14659
  const color = c.color ?? DEFAULT_CELL_COLOR;
13895
14660
  out.push({
13896
14661
  type: "rect",
@@ -13914,11 +14679,207 @@ var init_AlgorithmCanvas = __esm({
13914
14679
  align: "center"
13915
14680
  });
13916
14681
  }
14682
+ if (c.corner && cw >= CORNER_MIN_CELL && ch >= CORNER_MIN_CELL) {
14683
+ const { tl, tr, bl, br } = c.corner;
14684
+ if (tl) {
14685
+ out.push({
14686
+ type: "text",
14687
+ x: x + CORNER_INSET_X,
14688
+ y: y + CORNER_INSET_Y,
14689
+ text: tl,
14690
+ color: CORNER_TEXT_COLOR,
14691
+ fontSize: CORNER_FONT_SIZE,
14692
+ align: "left"
14693
+ });
14694
+ }
14695
+ if (tr) {
14696
+ out.push({
14697
+ type: "text",
14698
+ x: x + cw - CORNER_INSET_X,
14699
+ y: y + CORNER_INSET_Y,
14700
+ text: tr,
14701
+ color: CORNER_TEXT_COLOR,
14702
+ fontSize: CORNER_FONT_SIZE,
14703
+ align: "right"
14704
+ });
14705
+ }
14706
+ if (bl) {
14707
+ out.push({
14708
+ type: "text",
14709
+ x: x + CORNER_INSET_X,
14710
+ y: y + ch - CORNER_INSET_Y,
14711
+ text: bl,
14712
+ color: CORNER_TEXT_COLOR,
14713
+ fontSize: CORNER_FONT_SIZE,
14714
+ align: "left"
14715
+ });
14716
+ }
14717
+ if (br) {
14718
+ out.push({
14719
+ type: "text",
14720
+ x: x + cw - CORNER_INSET_X,
14721
+ y: y + ch - CORNER_INSET_Y,
14722
+ text: br,
14723
+ color: CORNER_TEXT_COLOR,
14724
+ fontSize: CORNER_FONT_SIZE,
14725
+ align: "right"
14726
+ });
14727
+ }
14728
+ }
14729
+ });
14730
+ colLabels.forEach((l) => {
14731
+ out.push({
14732
+ type: "text",
14733
+ x: gridX0 + l.index * cw + cw / 2,
14734
+ y: panelYCells + colLabelH / 2,
14735
+ text: l.text,
14736
+ color: l.color ?? AXIS_LABEL_COLOR,
14737
+ fontSize: AXIS_LABEL_FONT_SIZE,
14738
+ align: "center"
14739
+ });
14740
+ });
14741
+ rowLabels.forEach((l) => {
14742
+ out.push({
14743
+ type: "text",
14744
+ x: rowLabelW - 6,
14745
+ y: gridY0 + l.index * ch + ch / 2,
14746
+ text: l.text,
14747
+ color: l.color ?? AXIS_LABEL_COLOR,
14748
+ fontSize: AXIS_LABEL_FONT_SIZE,
14749
+ align: "right"
14750
+ });
14751
+ });
14752
+ }
14753
+ if (buckets.length > 0) {
14754
+ const panelYBuckets = panelY.buckets;
14755
+ const bucketCount = Math.max(0, ...buckets.map((b) => b.index)) + 1;
14756
+ const rowH = panelHeight / bucketCount;
14757
+ const indexColW = Math.min(width * 0.12, 40);
14758
+ const maxChainLen = Math.max(1, ...buckets.map((b) => b.entries.length));
14759
+ const entryW = Math.min(BUCKET_ENTRY_MAX_W, Math.max(BUCKET_ENTRY_MIN_W, (width - indexColW - 8) / maxChainLen));
14760
+ const maxVisible = Math.floor((width - indexColW - 4) / entryW);
14761
+ buckets.forEach((b) => {
14762
+ const rowY = panelYBuckets + b.index * rowH;
14763
+ out.push({
14764
+ type: "rect",
14765
+ id: `bucket-index-${b.index}`,
14766
+ x: 2,
14767
+ y: rowY + 2,
14768
+ width: indexColW - 4,
14769
+ height: rowH - 4,
14770
+ color: BUCKET_INDEX_STROKE,
14771
+ fill: BUCKET_INDEX_FILL
14772
+ });
14773
+ out.push({
14774
+ type: "text",
14775
+ x: 2 + (indexColW - 4) / 2,
14776
+ y: rowY + rowH / 2,
14777
+ text: String(b.index),
14778
+ color: BUCKET_INDEX_TEXT,
14779
+ fontSize: 10,
14780
+ align: "center"
14781
+ });
14782
+ const overflow = b.entries.length > maxVisible;
14783
+ const visibleCount = overflow ? Math.max(0, maxVisible - 1) : b.entries.length;
14784
+ for (let j = 0; j < visibleCount; j++) {
14785
+ const entry = b.entries[j];
14786
+ const ex = indexColW + 4 + j * entryW;
14787
+ const state = entry.state ?? "default";
14788
+ const fill = state === "highlight" ? entry.color ?? BUCKET_ENTRY_HIGHLIGHT : state === "probing" ? entry.color ?? BUCKET_ENTRY_PROBING : entry.color ?? BUCKET_ENTRY_DEFAULT;
14789
+ out.push({
14790
+ type: "rect",
14791
+ id: `bucket-${b.index}-${j}`,
14792
+ x: ex,
14793
+ y: rowY + 2,
14794
+ width: entryW - 2,
14795
+ height: rowH - 4,
14796
+ color: fill,
14797
+ fill
14798
+ });
14799
+ if (entryW >= 20 && rowH >= 16) {
14800
+ out.push({
14801
+ type: "text",
14802
+ x: ex + (entryW - 2) / 2,
14803
+ y: rowY + rowH / 2,
14804
+ text: entry.label,
14805
+ color: BUCKET_ENTRY_TEXT,
14806
+ fontSize: 10,
14807
+ align: "center"
14808
+ });
14809
+ }
14810
+ }
14811
+ if (overflow) {
14812
+ const ex = indexColW + 4 + visibleCount * entryW;
14813
+ out.push({
14814
+ type: "rect",
14815
+ id: `bucket-${b.index}-overflow`,
14816
+ x: ex,
14817
+ y: rowY + 2,
14818
+ width: entryW - 2,
14819
+ height: rowH - 4,
14820
+ color: BUCKET_ENTRY_DEFAULT,
14821
+ fill: BUCKET_ENTRY_DEFAULT
14822
+ });
14823
+ out.push({
14824
+ type: "text",
14825
+ x: ex + (entryW - 2) / 2,
14826
+ y: rowY + rowH / 2,
14827
+ text: `+${b.entries.length - visibleCount}`,
14828
+ color: BUCKET_ENTRY_TEXT,
14829
+ fontSize: 10,
14830
+ align: "center"
14831
+ });
14832
+ }
14833
+ });
14834
+ }
14835
+ if (frames.length > 0) {
14836
+ const panelYFrames = panelY.frames;
14837
+ const n = frames.length;
14838
+ const frameH = panelHeight / n;
14839
+ const x = 8;
14840
+ const w = width - 16;
14841
+ frames.forEach((f3, i) => {
14842
+ const y = panelYFrames + panelHeight - (i + 1) * frameH;
14843
+ const state = f3.state ?? "active";
14844
+ const fill = state === "returning" ? f3.color ?? FRAME_RETURNING_COLOR : state === "done" ? f3.color ?? FRAME_DONE_COLOR : f3.color ?? FRAME_ACTIVE_COLOR;
14845
+ out.push({ type: "rect", id: `frame-${i}`, x, y, width: w, height: frameH, color: fill, fill });
14846
+ if (frameH >= FRAME_TWO_LINE_MIN_H) {
14847
+ out.push({
14848
+ type: "text",
14849
+ x: 16,
14850
+ y: y + frameH * 0.35,
14851
+ text: f3.label,
14852
+ color: FRAME_LABEL_COLOR,
14853
+ fontSize: 10,
14854
+ align: "left"
14855
+ });
14856
+ if (f3.detail) {
14857
+ out.push({
14858
+ type: "text",
14859
+ x: 16,
14860
+ y: y + frameH * 0.7,
14861
+ text: f3.detail,
14862
+ color: FRAME_DETAIL_COLOR,
14863
+ fontSize: 10,
14864
+ align: "left"
14865
+ });
14866
+ }
14867
+ } else {
14868
+ out.push({
14869
+ type: "text",
14870
+ x: 16,
14871
+ y: y + frameH / 2,
14872
+ text: f3.label,
14873
+ color: FRAME_LABEL_COLOR,
14874
+ fontSize: 10,
14875
+ align: "left"
14876
+ });
14877
+ }
13917
14878
  });
13918
14879
  }
13919
14880
  out.push(...shapes);
13920
14881
  return out;
13921
- }, [bars, cells, pointers, shapes, width, height]);
14882
+ }, [bars, cells, pointers, ranges, slots, slotOrientation, frames, buckets, auxBars, rowLabels, colLabels, shapes, width, height]);
13922
14883
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
13923
14884
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
13924
14885
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -14932,6 +15893,12 @@ function LearningScene3D({
14932
15893
  const unitId = event.payload?.unitId;
14933
15894
  if (typeof unitId === "string") onItemClickRef.current?.(unitId);
14934
15895
  });
15896
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && post?.bloom) {
15897
+ const unknownKeys = Object.keys(post.bloom).filter((k) => !KNOWN_BLOOM_KEYS.has(k));
15898
+ if (unknownKeys.length > 0) {
15899
+ sceneLog.debug("post.bloom has unrecognized keys \u2014 only intensity/threshold/smoothing are read", { unknownKeys });
15900
+ }
15901
+ }
14935
15902
  const props3d = {
14936
15903
  drawables,
14937
15904
  isLoading,
@@ -14997,7 +15964,7 @@ function cylinderBetween(from, to, radius, color) {
14997
15964
  material: { color }
14998
15965
  };
14999
15966
  }
15000
- function arrowBetween(from, to, color, shaftRadius = 0.08) {
15967
+ function arrowBetween(from, to, color, shaftRadius = 0.08, id) {
15001
15968
  const len = segmentLength(from, to);
15002
15969
  if (len < 1e-6) return null;
15003
15970
  const tipLen = Math.min(shaftRadius * 8, len * 0.35);
@@ -15025,6 +15992,7 @@ function arrowBetween(from, to, color, shaftRadius = 0.08) {
15025
15992
  };
15026
15993
  return {
15027
15994
  type: "draw-group",
15995
+ ...id !== void 0 ? { id } : {},
15028
15996
  position: { x: from[0], y: from[1], z: from[2] },
15029
15997
  items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
15030
15998
  };
@@ -15051,20 +16019,228 @@ function get3DClickPayload(onShapeClick, idToIndex) {
15051
16019
  if (!onShapeClick) return void 0;
15052
16020
  return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
15053
16021
  }
15054
- var Canvas3DHost2;
16022
+ function polylineTube(points, radius, color, opts) {
16023
+ const maxSegments = opts?.maxSegments ?? 128;
16024
+ let pts = points;
16025
+ if (pts.length - 1 > maxSegments) {
16026
+ const step = (pts.length - 1) / maxSegments;
16027
+ const kept = [pts[0]];
16028
+ for (let s = 1; s < maxSegments; s++) kept.push(pts[Math.round(s * step)]);
16029
+ kept.push(pts[pts.length - 1]);
16030
+ pts = kept;
16031
+ }
16032
+ const out = [];
16033
+ for (let i = 0; i < pts.length - 1; i++) {
16034
+ const seg = cylinderBetween(pts[i], pts[i + 1], radius, color);
16035
+ if (seg) out.push(opts?.opacity !== void 0 ? { ...seg, opacity: opts.opacity } : seg);
16036
+ }
16037
+ return out;
16038
+ }
16039
+ function heightFieldMesh(spec) {
16040
+ const { nx, ny, heights, spacing = 1, x = 0, y = 0 } = spec;
16041
+ const flatShading = spec.flatShading ?? true;
16042
+ const vertices = [];
16043
+ for (let iy = 0; iy < ny; iy++) {
16044
+ for (let ix = 0; ix < nx; ix++) {
16045
+ vertices.push([
16046
+ x + (ix - (nx - 1) / 2) * spacing,
16047
+ y + (iy - (ny - 1) / 2) * spacing,
16048
+ heights[iy * nx + ix] ?? 0
16049
+ ]);
16050
+ }
16051
+ }
16052
+ const bands = [...spec.bands ?? []].sort((a, b) => (a.min ?? -Infinity) - (b.min ?? -Infinity));
16053
+ const facesByBand = /* @__PURE__ */ new Map();
16054
+ for (let iy = 0; iy < ny - 1; iy++) {
16055
+ for (let ix = 0; ix < nx - 1; ix++) {
16056
+ const v00 = iy * nx + ix;
16057
+ const v10 = iy * nx + ix + 1;
16058
+ const v01 = (iy + 1) * nx + ix;
16059
+ const v11 = (iy + 1) * nx + ix + 1;
16060
+ for (const face of [[v00, v10, v01], [v10, v11, v01]]) {
16061
+ const centroid = (vertices[face[0]][2] + vertices[face[1]][2] + vertices[face[2]][2]) / 3;
16062
+ let band = null;
16063
+ for (const b of bands) {
16064
+ if ((b.min ?? -Infinity) <= centroid) band = b;
16065
+ }
16066
+ const key = bands.length > 0 ? band : null;
16067
+ const list = facesByBand.get(key) ?? [];
16068
+ list.push(face);
16069
+ facesByBand.set(key, list);
16070
+ }
16071
+ }
16072
+ }
16073
+ const out = [];
16074
+ for (const [band, faces] of facesByBand) {
16075
+ if (faces.length === 0) continue;
16076
+ out.push({
16077
+ type: "draw-mesh",
16078
+ shape: "polyhedron",
16079
+ position: { x: 0, y: 0, z: 0 },
16080
+ vertices,
16081
+ faces,
16082
+ pivot: "center",
16083
+ material: { color: band?.color ?? spec.color ?? "#64748b", flatShading, side: "double" },
16084
+ ...spec.opacity !== void 0 ? { opacity: spec.opacity } : {}
16085
+ });
16086
+ }
16087
+ return out;
16088
+ }
16089
+ function arrowField(vectors, opts) {
16090
+ const scale = opts?.scale ?? 1;
16091
+ const out = [];
16092
+ for (const v of vectors) {
16093
+ const to = [
16094
+ v.from[0] + v.delta[0] * scale,
16095
+ v.from[1] + v.delta[1] * scale,
16096
+ v.from[2] + v.delta[2] * scale
16097
+ ];
16098
+ const arrow = arrowBetween(v.from, to, v.color ?? "#dc2626", v.width, v.id);
16099
+ if (arrow) out.push(arrow);
16100
+ if (v.label) out.push(billboardLabel(v.label, to[0], to[1], to[2], { color: opts?.labelColor }));
16101
+ }
16102
+ return out;
16103
+ }
16104
+ function helixDrawables(spec, opts) {
16105
+ const count = spec.count ?? spec.rungs?.length ?? 0;
16106
+ const rungs = Array.from({ length: count }, (_, i) => spec.rungs?.[i] ?? {});
16107
+ const radius = spec.radius ?? 1;
16108
+ const rise = spec.rise ?? 0.34;
16109
+ const twistRad = (spec.twistDeg ?? 36) * (Math.PI / 180);
16110
+ const strandAColor = spec.strandAColor ?? "#38bdf8";
16111
+ const strandBColor = spec.strandBColor ?? "#fb923c";
16112
+ const backboneRadius = spec.backboneRadius ?? 0.16;
16113
+ const rungRadius = spec.rungRadius ?? 0.12;
16114
+ const cx = spec.x ?? 0;
16115
+ const cy = spec.y ?? 0;
16116
+ const cz = spec.z ?? 0;
16117
+ const unwoundCount = spec.unwoundCount ?? 0;
16118
+ const unwindSpread = spec.unwindSpread ?? 1.8;
16119
+ const strandA = [];
16120
+ const strandB = [];
16121
+ for (let i = 0; i < count; i++) {
16122
+ const yi = cy + (i - (count - 1) / 2) * rise;
16123
+ const theta = i * twistRad;
16124
+ const s = i < unwoundCount ? unwindSpread : 1;
16125
+ strandA.push([cx + s * radius * Math.cos(theta), yi, cz + s * radius * Math.sin(theta)]);
16126
+ strandB.push([cx + s * radius * Math.cos(theta + Math.PI), yi, cz + s * radius * Math.sin(theta + Math.PI)]);
16127
+ }
16128
+ const out = [];
16129
+ for (let i = 0; i < count; i++) {
16130
+ out.push(meshSphere(`hx-a-${i}`, strandA[i][0], strandA[i][1], strandA[i][2], backboneRadius, strandAColor));
16131
+ out.push(meshSphere(`hx-b-${i}`, strandB[i][0], strandB[i][1], strandB[i][2], backboneRadius, strandBColor));
16132
+ if (i > 0) {
16133
+ const segA = cylinderBetween(strandA[i - 1], strandA[i], backboneRadius, strandAColor);
16134
+ if (segA) out.push(segA);
16135
+ const segB = cylinderBetween(strandB[i - 1], strandB[i], backboneRadius, strandBColor);
16136
+ if (segB) out.push(segB);
16137
+ }
16138
+ const rung = rungs[i];
16139
+ const rungColor = rung.color ?? "#94a3b8";
16140
+ const rod = cylinderBetween(strandA[i], strandB[i], rungRadius, rungColor);
16141
+ if (rod) out.push(rod);
16142
+ const mid = [
16143
+ (strandA[i][0] + strandB[i][0]) / 2,
16144
+ (strandA[i][1] + strandB[i][1]) / 2,
16145
+ (strandA[i][2] + strandB[i][2]) / 2
16146
+ ];
16147
+ const markerRadius = rung.radius ?? rungRadius;
16148
+ out.push(meshSphere(rung.id, mid[0], mid[1], mid[2], markerRadius, rungColor));
16149
+ if (rung.label) out.push(billboardLabel(rung.label, mid[0], mid[1], mid[2] + markerRadius, { color: opts?.labelColor }));
16150
+ }
16151
+ return out;
16152
+ }
16153
+ function latticeDrawables(spec, opts) {
16154
+ const nx = spec.nx ?? 2;
16155
+ const ny = spec.ny ?? 2;
16156
+ const nz = spec.nz ?? 2;
16157
+ const latticeConstant = spec.latticeConstant ?? 2;
16158
+ const bondRadius = spec.bondRadius ?? 0.06;
16159
+ const highlightCell = spec.highlightCell ?? false;
16160
+ const dimColor = spec.dimColor ?? "#475569";
16161
+ const showLabels = spec.showLabels ?? false;
16162
+ const selectedColor = spec.selectedColor ?? "#f59e0b";
16163
+ const posByKey = /* @__PURE__ */ new Map();
16164
+ const inCellByKey = /* @__PURE__ */ new Map();
16165
+ const out = [];
16166
+ for (const site of spec.basis) {
16167
+ const snx = site.xEdge ? nx + 1 : nx;
16168
+ const sny = site.yEdge ? ny + 1 : ny;
16169
+ const snz = site.zEdge ? nz + 1 : nz;
16170
+ for (let i = 0; i < snx; i++) {
16171
+ for (let j = 0; j < sny; j++) {
16172
+ for (let k = 0; k < snz; k++) {
16173
+ const key = `${site.key}-${i}-${j}-${k}`;
16174
+ const inCell = i + site.dx <= 1 && j + site.dy <= 1 && k + site.dz <= 1;
16175
+ const pos = [
16176
+ (i + site.dx) * latticeConstant - nx * latticeConstant / 2,
16177
+ (j + site.dy) * latticeConstant - ny * latticeConstant / 2,
16178
+ (k + site.dz) * latticeConstant - nz * latticeConstant / 2
16179
+ ];
16180
+ posByKey.set(key, pos);
16181
+ inCellByKey.set(key, inCell);
16182
+ const isSelected = spec.selectedId === `lat-${key}`;
16183
+ const color = isSelected ? selectedColor : highlightCell && !inCell ? dimColor : site.color ?? "#2563eb";
16184
+ const radius = (site.radius ?? 0.3) * (isSelected ? 1.4 : 1);
16185
+ out.push(meshSphere(`lat-${key}`, pos[0], pos[1], pos[2], radius, color));
16186
+ if (showLabels && site.element) {
16187
+ out.push(billboardLabel(site.element, pos[0], pos[1], pos[2] + radius, { color: opts?.labelColor }));
16188
+ }
16189
+ }
16190
+ }
16191
+ }
16192
+ }
16193
+ const basisByKey = new Map(spec.basis.map((s) => [s.key, s]));
16194
+ for (const bond of spec.bonds ?? []) {
16195
+ const fromSite = basisByKey.get(bond.from);
16196
+ const toSite = basisByKey.get(bond.to);
16197
+ if (!fromSite || !toSite) continue;
16198
+ const fnx = fromSite.xEdge ? nx + 1 : nx;
16199
+ const fny = fromSite.yEdge ? ny + 1 : ny;
16200
+ const fnz = fromSite.zEdge ? nz + 1 : nz;
16201
+ const tnx = toSite.xEdge ? nx + 1 : nx;
16202
+ const tny = toSite.yEdge ? ny + 1 : ny;
16203
+ const tnz = toSite.zEdge ? nz + 1 : nz;
16204
+ const bdx = bond.dx ?? 0;
16205
+ const bdy = bond.dy ?? 0;
16206
+ const bdz = bond.dz ?? 0;
16207
+ for (let i = 0; i < fnx; i++) {
16208
+ for (let j = 0; j < fny; j++) {
16209
+ for (let k = 0; k < fnz; k++) {
16210
+ const ti = i + bdx;
16211
+ const tj = j + bdy;
16212
+ const tk = k + bdz;
16213
+ if (ti < 0 || ti >= tnx || tj < 0 || tj >= tny || tk < 0 || tk >= tnz) continue;
16214
+ const fromKey = `${fromSite.key}-${i}-${j}-${k}`;
16215
+ const toKey = `${toSite.key}-${ti}-${tj}-${tk}`;
16216
+ const fromPos = posByKey.get(fromKey);
16217
+ const toPos = posByKey.get(toKey);
16218
+ if (!fromPos || !toPos) continue;
16219
+ const dimmed = highlightCell && !(inCellByKey.get(fromKey) && inCellByKey.get(toKey));
16220
+ const seg = cylinderBetween(fromPos, toPos, bondRadius, dimmed ? dimColor : bond.color ?? "#6b7280");
16221
+ if (seg) out.push(seg);
16222
+ }
16223
+ }
16224
+ }
16225
+ }
16226
+ return out;
16227
+ }
16228
+ var sceneLog, KNOWN_BLOOM_KEYS, Canvas3DHost2;
15055
16229
  var init_learningScene3D = __esm({
15056
16230
  "components/learning/molecules/learningScene3D.tsx"() {
15057
16231
  "use client";
15058
16232
  init_atoms();
15059
16233
  init_Stack();
15060
16234
  init_useEventBus();
16235
+ sceneLog = logger.createLogger("almadar:ui:learning-scene-3d");
16236
+ KNOWN_BLOOM_KEYS = /* @__PURE__ */ new Set(["intensity", "threshold", "smoothing"]);
15061
16237
  Canvas3DHost2 = React87.lazy(
15062
16238
  () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
15063
16239
  );
15064
16240
  LearningScene3D.displayName = "LearningScene3D";
15065
16241
  }
15066
16242
  });
15067
- var biologyLog, BiologyCanvas;
16243
+ var biologyLog, BIO_BAND_COLORS, BIO_STAGE_FILL, BIO_STAGE_TEXT, BiologyCanvas;
15068
16244
  var init_BiologyCanvas = __esm({
15069
16245
  "components/learning/molecules/BiologyCanvas.tsx"() {
15070
16246
  "use client";
@@ -15073,6 +16249,17 @@ var init_BiologyCanvas = __esm({
15073
16249
  init_LearningCanvas();
15074
16250
  init_learningScene3D();
15075
16251
  biologyLog = logger.createLogger("almadar:ui:biology-canvas");
16252
+ BIO_BAND_COLORS = ["#dcfce7", "#fef9c3", "#fee2e2", "#e0e7ff"];
16253
+ BIO_STAGE_FILL = {
16254
+ pending: "#e2e8f0",
16255
+ active: "#3b82f6",
16256
+ done: "#94a3b8"
16257
+ };
16258
+ BIO_STAGE_TEXT = {
16259
+ pending: "#64748b",
16260
+ active: "#ffffff",
16261
+ done: "#ffffff"
16262
+ };
15076
16263
  BiologyCanvas = ({
15077
16264
  className,
15078
16265
  width = 600,
@@ -15085,7 +16272,15 @@ var init_BiologyCanvas = __esm({
15085
16272
  post,
15086
16273
  nodes = [],
15087
16274
  edges = [],
16275
+ compartments = [],
16276
+ bands = [],
16277
+ stages = [],
16278
+ stageStyle = "timeline",
16279
+ helix,
16280
+ helix3d,
15088
16281
  shapes = [],
16282
+ readouts,
16283
+ traces,
15089
16284
  showGrid,
15090
16285
  shadows,
15091
16286
  interactive,
@@ -15100,19 +16295,148 @@ var init_BiologyCanvas = __esm({
15100
16295
  for (const n of nodes) {
15101
16296
  if (n.id) nodeById.set(n.id, n);
15102
16297
  }
16298
+ const bandCount = bands.length;
16299
+ for (let i = 0; i < bandCount; i++) {
16300
+ const band = bands[i];
16301
+ const bandColor = band.color ?? BIO_BAND_COLORS[i % BIO_BAND_COLORS.length];
16302
+ const bandY = i * height / bandCount;
16303
+ const bandH = height / bandCount;
16304
+ out.push({
16305
+ type: "rect",
16306
+ x: 0,
16307
+ y: bandY,
16308
+ width,
16309
+ height: bandH,
16310
+ color: bandColor,
16311
+ fill: bandColor,
16312
+ opacity: 0.45
16313
+ });
16314
+ if (band.label) {
16315
+ out.push({
16316
+ type: "text",
16317
+ x: 8,
16318
+ y: bandY + 14,
16319
+ text: band.label,
16320
+ color: "#6b7280",
16321
+ fontSize: 10
16322
+ });
16323
+ }
16324
+ }
16325
+ for (const c of compartments) {
16326
+ const color = c.color ?? "#16a34a";
16327
+ out.push({
16328
+ type: "ellipse",
16329
+ x: c.x,
16330
+ y: c.y,
16331
+ width: c.width,
16332
+ height: c.height,
16333
+ color,
16334
+ fill: c.fill ?? `${color}1A`,
16335
+ lineWidth: c.lineWidth ?? 2,
16336
+ ...c.dash ? { dash: c.dash } : {}
16337
+ });
16338
+ if (c.label) {
16339
+ out.push({
16340
+ type: "text",
16341
+ x: c.x,
16342
+ y: c.y - c.height / 2 + 14,
16343
+ text: c.label,
16344
+ color: "#111827",
16345
+ fontSize: 11,
16346
+ align: "center"
16347
+ });
16348
+ }
16349
+ }
16350
+ if (helix) {
16351
+ const hx = helix.x ?? 24;
16352
+ const hy = helix.y ?? height * 0.25;
16353
+ const hw = helix.width ?? width - 48;
16354
+ const hh = helix.height ?? height * 0.5;
16355
+ const rungs = helix.rungs;
16356
+ const n = rungs.length;
16357
+ const cy = hy + hh / 2;
16358
+ const colorA = helix.colorA ?? "#2563eb";
16359
+ const colorB = helix.colorB ?? "#dc2626";
16360
+ const rungColor = helix.rungColor ?? "#94a3b8";
16361
+ const fork = helix.fork ?? 0;
16362
+ const maxSep = Math.min(hh - 8, 96);
16363
+ const strandA = [];
16364
+ const strandB = [];
16365
+ const rungGeoms = [];
16366
+ for (let i = 0; i < n; i++) {
16367
+ const rx = hx + (i + 0.5) * hw / n;
16368
+ const t = (i + 0.5) / n;
16369
+ const paired = t >= fork;
16370
+ const sep = paired ? 28 : 28 + (maxSep - 28) * ((fork - t) / fork);
16371
+ strandA.push({ x: rx, y: cy - sep / 2 });
16372
+ strandB.push({ x: rx, y: cy + sep / 2 });
16373
+ rungGeoms.push({ rx, sep, rung: rungs[i], paired });
16374
+ }
16375
+ for (let i = 1; i < n; i++) {
16376
+ out.push({ type: "line", x1: strandA[i - 1].x, y1: strandA[i - 1].y, x2: strandA[i].x, y2: strandA[i].y, color: colorA, lineWidth: 3 });
16377
+ }
16378
+ for (let i = 1; i < n; i++) {
16379
+ out.push({ type: "line", x1: strandB[i - 1].x, y1: strandB[i - 1].y, x2: strandB[i].x, y2: strandB[i].y, color: colorB, lineWidth: 3 });
16380
+ }
16381
+ for (const g of rungGeoms) {
16382
+ const rColor = g.rung.color ?? (g.rung.state === "new" ? "#16a34a" : rungColor);
16383
+ const topY = cy - g.sep / 2;
16384
+ const bottomY = cy + g.sep / 2;
16385
+ if (g.paired) {
16386
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: bottomY, color: rColor });
16387
+ if (g.rung.a) {
16388
+ out.push({ type: "text", x: g.rx, y: cy - g.sep / 4, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
16389
+ }
16390
+ if (g.rung.b) {
16391
+ out.push({ type: "text", x: g.rx, y: cy + g.sep / 4, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16392
+ }
16393
+ } else {
16394
+ const stubTopY = topY + 8;
16395
+ const stubBottomY = bottomY - 8;
16396
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: stubTopY, color: rColor });
16397
+ out.push({ type: "line", x1: g.rx, y1: bottomY, x2: g.rx, y2: stubBottomY, color: rColor });
16398
+ if (g.rung.a) {
16399
+ out.push({ type: "text", x: g.rx, y: stubTopY + 6, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
16400
+ }
16401
+ if (g.rung.b) {
16402
+ out.push({ type: "text", x: g.rx, y: stubBottomY - 6, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16403
+ }
16404
+ }
16405
+ }
16406
+ }
15103
16407
  for (const e of edges) {
15104
16408
  const a = nodeById.get(e.from);
15105
16409
  const b = nodeById.get(e.to);
15106
16410
  if (!a || !b) continue;
15107
- out.push({
15108
- type: "line",
15109
- x1: a.x,
15110
- y1: a.y,
15111
- x2: b.x,
15112
- y2: b.y,
15113
- color: e.color ?? "#9ca3af",
15114
- lineWidth: 2
15115
- });
16411
+ const color = e.color ?? "#9ca3af";
16412
+ if (e.directed) {
16413
+ const rA = a.radius ?? 16;
16414
+ const rB = b.radius ?? 16;
16415
+ const dx = b.x - a.x;
16416
+ const dy = b.y - a.y;
16417
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16418
+ const ux = dx / dist;
16419
+ const uy = dy / dist;
16420
+ out.push({
16421
+ type: "arrow",
16422
+ x1: a.x + ux * rA,
16423
+ y1: a.y + uy * rA,
16424
+ x2: b.x - ux * rB,
16425
+ y2: b.y - uy * rB,
16426
+ color,
16427
+ lineWidth: 2
16428
+ });
16429
+ } else {
16430
+ out.push({
16431
+ type: "line",
16432
+ x1: a.x,
16433
+ y1: a.y,
16434
+ x2: b.x,
16435
+ y2: b.y,
16436
+ color,
16437
+ lineWidth: 2
16438
+ });
16439
+ }
15116
16440
  if (e.label) {
15117
16441
  out.push({
15118
16442
  type: "text",
@@ -15125,6 +16449,8 @@ var init_BiologyCanvas = __esm({
15125
16449
  }
15126
16450
  }
15127
16451
  for (const n of nodes) {
16452
+ const state = n.state ?? "default";
16453
+ const muted = state === "muted";
15128
16454
  out.push({
15129
16455
  type: "circle",
15130
16456
  x: n.x,
@@ -15132,28 +16458,127 @@ var init_BiologyCanvas = __esm({
15132
16458
  radius: n.radius ?? 16,
15133
16459
  color: n.color ?? "#16a34a",
15134
16460
  fill: `${n.color ?? "#16a34a"}33`,
15135
- id: n.id
16461
+ id: n.id,
16462
+ ...muted ? { opacity: 0.35 } : {}
15136
16463
  });
16464
+ if (state === "highlight") {
16465
+ out.push({
16466
+ type: "circle",
16467
+ x: n.x,
16468
+ y: n.y,
16469
+ radius: (n.radius ?? 16) + 4,
16470
+ color: "#f59e0b",
16471
+ lineWidth: 2
16472
+ });
16473
+ }
15137
16474
  if (n.label) {
15138
16475
  out.push({
15139
16476
  type: "text",
15140
16477
  x: n.x,
15141
16478
  y: n.y + (n.radius ?? 16) + 14,
15142
16479
  text: n.label,
16480
+ ...muted ? { opacity: 0.35 } : {},
15143
16481
  color: "#111827",
15144
16482
  fontSize: 12,
15145
16483
  align: "center"
15146
16484
  });
15147
16485
  }
15148
16486
  }
16487
+ const stageCount = stages.length;
16488
+ if (stageCount > 0) {
16489
+ if (stageStyle === "ring") {
16490
+ const cx = width / 2;
16491
+ const cy = height / 2;
16492
+ const R = Math.min(width, height) / 2 - 48;
16493
+ const ringPoints = [];
16494
+ for (let i = 0; i < stageCount; i++) {
16495
+ const angleRad = (-90 + 360 * i / stageCount) * Math.PI / 180;
16496
+ ringPoints.push({ x: cx + R * Math.cos(angleRad), y: cy + R * Math.sin(angleRad) });
16497
+ }
16498
+ if (stageCount >= 2) {
16499
+ for (let i = 0; i < stageCount - 1; i++) {
16500
+ const p1 = ringPoints[i];
16501
+ const p2 = ringPoints[i + 1];
16502
+ const dx = p2.x - p1.x;
16503
+ const dy = p2.y - p1.y;
16504
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16505
+ const ux = dx / dist;
16506
+ const uy = dy / dist;
16507
+ out.push({
16508
+ type: "arrow",
16509
+ x1: p1.x + ux * 46,
16510
+ y1: p1.y + uy * 46,
16511
+ x2: p2.x - ux * 46,
16512
+ y2: p2.y - uy * 46,
16513
+ color: "#94a3b8"
16514
+ });
16515
+ }
16516
+ }
16517
+ for (let i = 0; i < stageCount; i++) {
16518
+ const stage = stages[i];
16519
+ const state = stage.state ?? "pending";
16520
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16521
+ const w = Math.max(26, Math.min(84, stage.label.length * 6 + 10));
16522
+ const h = 18;
16523
+ const p = ringPoints[i];
16524
+ out.push({ type: "rect", x: p.x - w / 2, y: p.y - h / 2, width: w, height: h, color: fill, fill });
16525
+ out.push({ type: "text", x: p.x, y: p.y, text: stage.label, color: BIO_STAGE_TEXT[state], fontSize: 10, align: "center" });
16526
+ }
16527
+ } else {
16528
+ const stripY = height - 32;
16529
+ const slotW = (width - 16) / stageCount;
16530
+ const chipGeoms = [];
16531
+ for (let i = 0; i < stageCount; i++) {
16532
+ chipGeoms.push({ x: 8 + i * slotW + 5, w: slotW - 10 });
16533
+ }
16534
+ for (let i = 0; i < stageCount - 1; i++) {
16535
+ const midY = stripY + 13;
16536
+ out.push({
16537
+ type: "arrow",
16538
+ x1: chipGeoms[i].x + chipGeoms[i].w,
16539
+ y1: midY,
16540
+ x2: chipGeoms[i + 1].x,
16541
+ y2: midY,
16542
+ color: "#94a3b8"
16543
+ });
16544
+ }
16545
+ for (let i = 0; i < stageCount; i++) {
16546
+ const stage = stages[i];
16547
+ const state = stage.state ?? "pending";
16548
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16549
+ const g = chipGeoms[i];
16550
+ out.push({ type: "rect", x: g.x, y: stripY, width: g.w, height: 26, color: fill, fill });
16551
+ out.push({
16552
+ type: "text",
16553
+ x: g.x + g.w / 2,
16554
+ y: stripY + 13,
16555
+ text: stage.label,
16556
+ color: BIO_STAGE_TEXT[state],
16557
+ fontSize: 10,
16558
+ align: "center"
16559
+ });
16560
+ }
16561
+ }
16562
+ }
15149
16563
  out.push(...shapes);
15150
16564
  return out;
15151
- }, [nodes, edges, shapes]);
16565
+ }, [nodes, edges, compartments, bands, stages, stageStyle, helix, shapes, width, height]);
15152
16566
  const drawables3D = React87.useMemo(() => {
15153
16567
  if (mode !== "3d") return [];
15154
16568
  if (shapes.length > 0) {
15155
16569
  biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
15156
16570
  }
16571
+ if (compartments.length > 0 || bands.length > 0 || stages.length > 0 || helix) {
16572
+ biologyLog.debug("2D-only families ignored in 3D mode (pixel-authored 2D vocabulary)", {
16573
+ compartments: compartments.length,
16574
+ bands: bands.length,
16575
+ stages: stages.length,
16576
+ helix: helix != null
16577
+ });
16578
+ }
16579
+ if (animate) {
16580
+ biologyLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
16581
+ }
15157
16582
  const out = [];
15158
16583
  const labelColor = labelColorForBackground(backgroundColor);
15159
16584
  const nodeById = /* @__PURE__ */ new Map();
@@ -15187,15 +16612,21 @@ var init_BiologyCanvas = __esm({
15187
16612
  out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
15188
16613
  }
15189
16614
  }
16615
+ if (helix3d) {
16616
+ out.push(...helixDrawables(helix3d, { labelColor }));
16617
+ }
15190
16618
  return out;
15191
- }, [mode, nodes, edges, shapes, backgroundColor]);
16619
+ }, [mode, nodes, edges, shapes, compartments, bands, stages, helix, helix3d, animate, backgroundColor]);
15192
16620
  const nodeIndexById = React87.useMemo(() => {
15193
16621
  const m = /* @__PURE__ */ new Map();
16622
+ (helix3d?.rungs ?? []).forEach((rung, i) => {
16623
+ if (rung.id) m.set(rung.id, i);
16624
+ });
15194
16625
  nodes.forEach((n, i) => {
15195
16626
  if (n.id) m.set(n.id, i);
15196
16627
  });
15197
16628
  return m;
15198
- }, [nodes]);
16629
+ }, [nodes, helix3d]);
15199
16630
  if (mode === "3d") {
15200
16631
  return /* @__PURE__ */ jsxRuntime.jsx(
15201
16632
  LearningScene3D,
@@ -15227,6 +16658,8 @@ var init_BiologyCanvas = __esm({
15227
16658
  height,
15228
16659
  backgroundColor,
15229
16660
  shapes: derivedShapes,
16661
+ readouts,
16662
+ traces,
15230
16663
  interactive: interactive ?? false,
15231
16664
  animate,
15232
16665
  onShapeClick,
@@ -22076,7 +23509,7 @@ function bondPerpendicular(a, b) {
22076
23509
  if (len < 1e-6) return [1, 0, 0];
22077
23510
  return [px / len, py / len, 0];
22078
23511
  }
22079
- var chemistryLog, ChemistryCanvas;
23512
+ var chemistryLog, CHEM_BOND_STATE_COLOR, LONE_PAIR_ANGLES, ChemistryCanvas;
22080
23513
  var init_ChemistryCanvas = __esm({
22081
23514
  "components/learning/molecules/ChemistryCanvas.tsx"() {
22082
23515
  "use client";
@@ -22085,6 +23518,13 @@ var init_ChemistryCanvas = __esm({
22085
23518
  init_LearningCanvas();
22086
23519
  init_learningScene3D();
22087
23520
  chemistryLog = logger.createLogger("almadar:ui:chemistry-canvas");
23521
+ CHEM_BOND_STATE_COLOR = {
23522
+ default: "#6b7280",
23523
+ forming: "#16a34a",
23524
+ breaking: "#dc2626",
23525
+ highlight: "#f59e0b"
23526
+ };
23527
+ LONE_PAIR_ANGLES = [-90, 0, 90, 180];
22088
23528
  ChemistryCanvas = ({
22089
23529
  className,
22090
23530
  width = 600,
@@ -22098,7 +23538,14 @@ var init_ChemistryCanvas = __esm({
22098
23538
  atoms = [],
22099
23539
  bonds = [],
22100
23540
  arrows = [],
23541
+ bondStyle = "thick",
23542
+ containers = [],
23543
+ equation,
23544
+ equationColor,
23545
+ lattice3d,
22101
23546
  shapes = [],
23547
+ readouts,
23548
+ traces,
22102
23549
  showGrid,
22103
23550
  shadows,
22104
23551
  interactive,
@@ -22113,21 +23560,118 @@ var init_ChemistryCanvas = __esm({
22113
23560
  for (const a of atoms) {
22114
23561
  if (a.id) atomById.set(a.id, a);
22115
23562
  }
23563
+ for (const c of containers) {
23564
+ const color = c.color ?? "#64748b";
23565
+ if (c.level != null) {
23566
+ const lv = c.level;
23567
+ out.push({
23568
+ type: "rect",
23569
+ x: c.x + 1,
23570
+ y: c.y + c.height * (1 - lv),
23571
+ width: c.width - 2,
23572
+ height: c.height * lv - 1,
23573
+ color: c.levelColor ?? "#60a5fa",
23574
+ fill: c.levelColor ?? "#60a5fa",
23575
+ opacity: 0.5
23576
+ });
23577
+ }
23578
+ out.push({
23579
+ type: "rect",
23580
+ x: c.x,
23581
+ y: c.y,
23582
+ width: c.width,
23583
+ height: c.height,
23584
+ color,
23585
+ fill: c.fill,
23586
+ lineWidth: c.lineWidth ?? 2
23587
+ });
23588
+ const divider = c.divider ?? "none";
23589
+ if (divider !== "none") {
23590
+ out.push({
23591
+ type: "line",
23592
+ x1: c.x + c.width / 2,
23593
+ y1: c.y,
23594
+ x2: c.x + c.width / 2,
23595
+ y2: c.y + c.height,
23596
+ color: c.dividerColor ?? color,
23597
+ ...divider === "dashed" || divider === "dotted" ? { dash: divider } : {}
23598
+ });
23599
+ }
23600
+ if (c.leftLabel) {
23601
+ out.push({
23602
+ type: "text",
23603
+ x: c.x + c.width * 0.25,
23604
+ y: c.y + 12,
23605
+ text: c.leftLabel,
23606
+ color: "#374151",
23607
+ fontSize: 11,
23608
+ align: "center"
23609
+ });
23610
+ }
23611
+ if (c.rightLabel) {
23612
+ out.push({
23613
+ type: "text",
23614
+ x: c.x + c.width * 0.75,
23615
+ y: c.y + 12,
23616
+ text: c.rightLabel,
23617
+ color: "#374151",
23618
+ fontSize: 11,
23619
+ align: "center"
23620
+ });
23621
+ }
23622
+ if (c.label) {
23623
+ out.push({
23624
+ type: "text",
23625
+ x: c.x + c.width / 2,
23626
+ y: c.y + c.height + 12,
23627
+ text: c.label,
23628
+ color: "#111827",
23629
+ fontSize: 12,
23630
+ align: "center"
23631
+ });
23632
+ }
23633
+ }
22116
23634
  for (const b of bonds) {
22117
23635
  const a = atomById.get(b.from);
22118
23636
  const c = atomById.get(b.to);
22119
23637
  if (!a || !c) continue;
22120
- const color = b.color ?? "#6b7280";
22121
- const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
22122
- out.push({
22123
- type: "line",
22124
- x1: a.x,
22125
- y1: a.y,
22126
- x2: c.x,
22127
- y2: c.y,
22128
- color,
22129
- lineWidth: strokeWidth
22130
- });
23638
+ const state = b.state ?? "default";
23639
+ const color = b.color ?? CHEM_BOND_STATE_COLOR[state];
23640
+ const dash = state === "forming" || state === "breaking" ? "dashed" : void 0;
23641
+ if (bondStyle === "parallel") {
23642
+ const dx = c.x - a.x;
23643
+ const dy = c.y - a.y;
23644
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
23645
+ const ux = dx / dist;
23646
+ const uy = dy / dist;
23647
+ const px = -uy;
23648
+ const py = ux;
23649
+ const offsets = b.type === "double" ? [-3, 3] : b.type === "triple" ? [-4, 0, 4] : [0];
23650
+ for (const off of offsets) {
23651
+ out.push({
23652
+ type: "line",
23653
+ x1: a.x + px * off,
23654
+ y1: a.y + py * off,
23655
+ x2: c.x + px * off,
23656
+ y2: c.y + py * off,
23657
+ color,
23658
+ lineWidth: 2,
23659
+ ...dash ? { dash } : {}
23660
+ });
23661
+ }
23662
+ } else {
23663
+ const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
23664
+ out.push({
23665
+ type: "line",
23666
+ x1: a.x,
23667
+ y1: a.y,
23668
+ x2: c.x,
23669
+ y2: c.y,
23670
+ color,
23671
+ lineWidth: strokeWidth,
23672
+ ...dash ? { dash } : {}
23673
+ });
23674
+ }
22131
23675
  }
22132
23676
  for (const a of arrows) {
22133
23677
  const angle = (a.angle ?? 0) * (Math.PI / 180);
@@ -22176,15 +23720,62 @@ var init_ChemistryCanvas = __esm({
22176
23720
  align: "center"
22177
23721
  });
22178
23722
  }
23723
+ const r = a.radius ?? 14;
23724
+ if (a.charge) {
23725
+ out.push({
23726
+ type: "text",
23727
+ x: a.x + r * 0.85,
23728
+ y: a.y - r * 0.85,
23729
+ text: a.charge,
23730
+ color: "#111827",
23731
+ fontSize: 9,
23732
+ align: "left"
23733
+ });
23734
+ }
23735
+ const lonePairs = Math.max(0, Math.min(4, a.lonePairs ?? 0));
23736
+ for (let k = 0; k < lonePairs; k++) {
23737
+ const angleRad = LONE_PAIR_ANGLES[k] * Math.PI / 180;
23738
+ const cx = a.x + (r + 6) * Math.cos(angleRad);
23739
+ const cy = a.y + (r + 6) * Math.sin(angleRad);
23740
+ const perpX = -Math.sin(angleRad);
23741
+ const perpY = Math.cos(angleRad);
23742
+ for (const sign of [1, -1]) {
23743
+ out.push({
23744
+ type: "circle",
23745
+ x: cx + perpX * 2.5 * sign,
23746
+ y: cy + perpY * 2.5 * sign,
23747
+ radius: 1.5,
23748
+ color: "#374151",
23749
+ fill: "#374151"
23750
+ });
23751
+ }
23752
+ }
23753
+ }
23754
+ if (equation) {
23755
+ out.push({
23756
+ type: "text",
23757
+ x: width / 2,
23758
+ y: 14,
23759
+ text: equation,
23760
+ color: equationColor ?? "#111827",
23761
+ fontSize: 13,
23762
+ align: "center"
23763
+ });
22179
23764
  }
22180
23765
  out.push(...shapes);
22181
23766
  return out;
22182
- }, [atoms, bonds, arrows, shapes]);
23767
+ }, [atoms, bonds, arrows, bondStyle, containers, equation, equationColor, shapes, width]);
22183
23768
  const drawables3D = React87.useMemo(() => {
22184
23769
  if (mode !== "3d") return [];
22185
23770
  if (shapes.length > 0) {
22186
23771
  chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
22187
23772
  }
23773
+ if (containers.length > 0) {
23774
+ chemistryLog.debug("containers ignored in 3D mode (pixel-authored 2D vocabulary)", { count: containers.length });
23775
+ }
23776
+ if (animate) {
23777
+ chemistryLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
23778
+ }
22188
23779
  const out = [];
22189
23780
  const labelColor = labelColorForBackground(backgroundColor);
22190
23781
  const atomById = /* @__PURE__ */ new Map();
@@ -22231,8 +23822,11 @@ var init_ChemistryCanvas = __esm({
22231
23822
  out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
22232
23823
  }
22233
23824
  }
23825
+ if (lattice3d) {
23826
+ out.push(...latticeDrawables(lattice3d, { labelColor }));
23827
+ }
22234
23828
  return out;
22235
- }, [mode, atoms, bonds, arrows, shapes, backgroundColor]);
23829
+ }, [mode, atoms, bonds, arrows, shapes, containers, lattice3d, animate, backgroundColor]);
22236
23830
  const atomIndexById = React87.useMemo(() => {
22237
23831
  const m = /* @__PURE__ */ new Map();
22238
23832
  atoms.forEach((a, i) => {
@@ -22271,6 +23865,8 @@ var init_ChemistryCanvas = __esm({
22271
23865
  height,
22272
23866
  backgroundColor,
22273
23867
  shapes: derivedShapes,
23868
+ readouts,
23869
+ traces,
22274
23870
  interactive: interactive ?? false,
22275
23871
  animate,
22276
23872
  onShapeClick,
@@ -28460,6 +30056,10 @@ var init_ProgressDots = __esm({
28460
30056
  ProgressDots.displayName = "ProgressDots";
28461
30057
  }
28462
30058
  });
30059
+ function formatTick(v) {
30060
+ if (Number.isInteger(v)) return String(v);
30061
+ return v.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
30062
+ }
28463
30063
  var MathCanvas;
28464
30064
  var init_MathCanvas = __esm({
28465
30065
  "components/learning/molecules/MathCanvas.tsx"() {
@@ -28479,10 +30079,19 @@ var init_MathCanvas = __esm({
28479
30079
  showAxes = true,
28480
30080
  showGrid = true,
28481
30081
  gridStep = 1,
30082
+ showTickLabels = false,
30083
+ showCurveLabels = false,
28482
30084
  curves = [],
28483
30085
  points = [],
28484
30086
  vectors = [],
30087
+ regions = [],
30088
+ bars = [],
30089
+ guides = [],
30090
+ angles = [],
30091
+ hops = [],
28485
30092
  shapes = [],
30093
+ readouts,
30094
+ traces,
28486
30095
  interactive = false,
28487
30096
  animate = false,
28488
30097
  onShapeClick,
@@ -28496,6 +30105,8 @@ var init_MathCanvas = __esm({
28496
30105
  const plotH = height - margin * 2;
28497
30106
  const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
28498
30107
  const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
30108
+ const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
30109
+ const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
28499
30110
  if (showGrid) {
28500
30111
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep) {
28501
30112
  const px = mapX(x);
@@ -28506,14 +30117,99 @@ var init_MathCanvas = __esm({
28506
30117
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color: "#e5e7eb", lineWidth: 1 });
28507
30118
  }
28508
30119
  }
30120
+ if (showTickLabels) {
30121
+ const labelEveryX = Math.max(1, Math.ceil((xMax - xMin) / gridStep / Math.floor(plotW / 40)));
30122
+ let kx = 0;
30123
+ for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
30124
+ if (kx % labelEveryX === 0 && x !== 0) {
30125
+ out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: 10, align: "center" });
30126
+ }
30127
+ }
30128
+ const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
30129
+ let ky = 0;
30130
+ for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
30131
+ if (ky % labelEveryY === 0 && y !== 0) {
30132
+ out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: 10, align: "right" });
30133
+ }
30134
+ }
30135
+ if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
30136
+ out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: 10, align: "right" });
30137
+ }
30138
+ }
30139
+ for (const region of regions) {
30140
+ if (!region.samples || region.samples.length === 0) continue;
30141
+ const baseline = region.baseline ?? 0;
30142
+ const clampedPoint = (p) => ({
30143
+ x: mapX(Math.min(xMax, Math.max(xMin, p.x))),
30144
+ y: mapY(Math.min(yMax, Math.max(yMin, p.y)))
30145
+ });
30146
+ const upper = region.samples.map(clampedPoint);
30147
+ const first = region.samples[0];
30148
+ const last = region.samples[region.samples.length - 1];
30149
+ const closing = region.samples2 && region.samples2.length > 0 ? [...region.samples2].reverse().map(clampedPoint) : [clampedPoint({ x: last.x, y: baseline }), clampedPoint({ x: first.x, y: baseline })];
30150
+ const color = region.color ?? "#2563eb";
30151
+ out.push({
30152
+ type: "polygon",
30153
+ points: [...upper, ...closing],
30154
+ fill: color,
30155
+ color,
30156
+ opacity: region.opacity ?? 0.2,
30157
+ lineWidth: 1
30158
+ });
30159
+ if (region.label) {
30160
+ const mid = Math.floor(region.samples.length / 2);
30161
+ out.push({
30162
+ type: "text",
30163
+ x: mapX((first.x + last.x) / 2),
30164
+ y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
30165
+ text: region.label,
30166
+ color: "#111827",
30167
+ fontSize: 11
30168
+ });
30169
+ }
30170
+ }
30171
+ for (const bar of bars) {
30172
+ if (bar.x + bar.width < xMin || bar.x > xMax) continue;
30173
+ const y0 = bar.y0 ?? 0;
30174
+ const color = bar.color ?? "#93c5fd";
30175
+ out.push({
30176
+ type: "rect",
30177
+ x: mapX(bar.x),
30178
+ y: mapY(Math.max(y0, bar.y1)),
30179
+ width: mapX(bar.x + bar.width) - mapX(bar.x),
30180
+ height: Math.abs(mapY(bar.y1) - mapY(y0)),
30181
+ color,
30182
+ fill: color,
30183
+ opacity: bar.opacity ?? 0.5,
30184
+ lineWidth: 1
30185
+ });
30186
+ }
28509
30187
  if (showAxes) {
28510
- const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
28511
- const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
28512
30188
  out.push({ type: "line", x1: margin, y1: xAxisY, x2: width - margin, y2: xAxisY, color: "#374151", lineWidth: 2 });
28513
30189
  out.push({ type: "line", x1: yAxisX, y1: margin, x2: yAxisX, y2: height - margin, color: "#374151", lineWidth: 2 });
28514
30190
  }
30191
+ for (const guide of guides) {
30192
+ const color = guide.color ?? "#9ca3af";
30193
+ const dash = guide.dash ?? "dashed";
30194
+ if (guide.kind === "vline") {
30195
+ if (guide.at < xMin || guide.at > xMax) continue;
30196
+ const px = mapX(guide.at);
30197
+ out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
30198
+ if (guide.label) {
30199
+ out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color: "#111827", fontSize: 11 });
30200
+ }
30201
+ } else {
30202
+ if (guide.at < yMin || guide.at > yMax) continue;
30203
+ const py = mapY(guide.at);
30204
+ out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
30205
+ if (guide.label) {
30206
+ out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color: "#111827", fontSize: 11, align: "right" });
30207
+ }
30208
+ }
30209
+ }
28515
30210
  for (const curve of curves) {
28516
30211
  if (!curve.samples || curve.samples.length < 2) continue;
30212
+ let lastInRange;
28517
30213
  for (let i = 1; i < curve.samples.length; i++) {
28518
30214
  const a = curve.samples[i - 1];
28519
30215
  const b = curve.samples[i];
@@ -28525,19 +30221,97 @@ var init_MathCanvas = __esm({
28525
30221
  x2: mapX(b.x),
28526
30222
  y2: mapY(b.y),
28527
30223
  color: curve.color ?? "#2563eb",
28528
- lineWidth: 2
30224
+ lineWidth: 2,
30225
+ dash: curve.dash
30226
+ });
30227
+ lastInRange = b;
30228
+ }
30229
+ if (showCurveLabels && curve.label && lastInRange) {
30230
+ out.push({
30231
+ type: "text",
30232
+ x: mapX(lastInRange.x) + 6,
30233
+ y: mapY(lastInRange.y) - 6,
30234
+ text: curve.label,
30235
+ color: curve.color ?? "#2563eb",
30236
+ fontSize: 11
30237
+ });
30238
+ }
30239
+ }
30240
+ for (const hop of hops) {
30241
+ const x1 = mapX(hop.from);
30242
+ const x2 = mapX(hop.to);
30243
+ const peak = Math.min(36, plotH * 0.3);
30244
+ const color = hop.color ?? "#7c3aed";
30245
+ out.push({
30246
+ type: "ellipse",
30247
+ x: (x1 + x2) / 2,
30248
+ y: xAxisY,
30249
+ width: Math.abs(x2 - x1),
30250
+ height: 2 * peak,
30251
+ startAngle: 180,
30252
+ endAngle: 360,
30253
+ color
30254
+ });
30255
+ const s = Math.sign(hop.to - hop.from);
30256
+ out.push({
30257
+ type: "polygon",
30258
+ points: [
30259
+ { x: x2, y: xAxisY },
30260
+ { x: x2 - 4 * s, y: xAxisY - 7 },
30261
+ { x: x2 + 2 * s, y: xAxisY - 7 }
30262
+ ],
30263
+ fill: color,
30264
+ color
30265
+ });
30266
+ if (hop.label) {
30267
+ out.push({
30268
+ type: "text",
30269
+ x: (x1 + x2) / 2,
30270
+ y: xAxisY - peak - 8,
30271
+ text: hop.label,
30272
+ color: "#111827",
30273
+ fontSize: 10,
30274
+ align: "center"
30275
+ });
30276
+ }
30277
+ }
30278
+ for (const angle of angles) {
30279
+ const radius = angle.radius ?? 0.8;
30280
+ const color = angle.color ?? "#0ea5e9";
30281
+ out.push({
30282
+ type: "ellipse",
30283
+ x: mapX(angle.x),
30284
+ y: mapY(angle.y),
30285
+ width: 2 * radius * plotW / (xMax - xMin),
30286
+ height: 2 * radius * plotH / (yMax - yMin),
30287
+ startAngle: -angle.to,
30288
+ endAngle: -angle.from,
30289
+ color
30290
+ });
30291
+ if (angle.label) {
30292
+ const mid = (angle.from + angle.to) / 2;
30293
+ const rad = mid * Math.PI / 180;
30294
+ out.push({
30295
+ type: "text",
30296
+ x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
30297
+ y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
30298
+ text: angle.label,
30299
+ color: "#111827",
30300
+ fontSize: 11,
30301
+ align: "center"
28529
30302
  });
28530
30303
  }
28531
30304
  }
28532
30305
  for (const p of points) {
28533
30306
  if (p.x < xMin || p.x > xMax || p.y < yMin || p.y > yMax) continue;
30307
+ const isOpen = p.style === "open";
28534
30308
  out.push({
28535
30309
  type: "circle",
28536
30310
  x: mapX(p.x),
28537
30311
  y: mapY(p.y),
28538
30312
  radius: p.radius ?? 4,
28539
30313
  color: p.color ?? "#dc2626",
28540
- fill: p.color ?? "#dc2626"
30314
+ fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
28541
30315
  });
28542
30316
  if (p.label) {
28543
30317
  out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: "#111827", fontSize: 12 });
@@ -28556,7 +30330,28 @@ var init_MathCanvas = __esm({
28556
30330
  }
28557
30331
  out.push(...shapes);
28558
30332
  return out;
28559
- }, [width, height, xMin, xMax, yMin, yMax, showAxes, showGrid, gridStep, curves, points, vectors, shapes]);
30333
+ }, [
30334
+ width,
30335
+ height,
30336
+ xMin,
30337
+ xMax,
30338
+ yMin,
30339
+ yMax,
30340
+ showAxes,
30341
+ showGrid,
30342
+ gridStep,
30343
+ showTickLabels,
30344
+ showCurveLabels,
30345
+ curves,
30346
+ points,
30347
+ vectors,
30348
+ regions,
30349
+ bars,
30350
+ guides,
30351
+ angles,
30352
+ hops,
30353
+ shapes
30354
+ ]);
28560
30355
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
28561
30356
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
28562
30357
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -28565,6 +30360,8 @@ var init_MathCanvas = __esm({
28565
30360
  width,
28566
30361
  height,
28567
30362
  shapes: derivedShapes,
30363
+ readouts,
30364
+ traces,
28568
30365
  interactive,
28569
30366
  animate,
28570
30367
  onShapeClick,
@@ -28576,7 +30373,315 @@ var init_MathCanvas = __esm({
28576
30373
  };
28577
30374
  }
28578
30375
  });
28579
- var physicsLog2, PhysicsCanvas;
30376
+ function formatMeterValue(v) {
30377
+ return Number.isInteger(v) ? String(v) : String(Number(v.toFixed(2)));
30378
+ }
30379
+ function sceneObjectShapes(obj, canvasWidth, canvasHeight) {
30380
+ const out = [];
30381
+ const color = obj.color ?? "#334155";
30382
+ switch (obj.kind) {
30383
+ case "ground": {
30384
+ const xStart = obj.x1 ?? 0;
30385
+ const xEnd = obj.x2 ?? canvasWidth;
30386
+ const y = obj.y ?? 0;
30387
+ out.push({ type: "line", x1: xStart, y1: y, x2: xEnd, y2: y, color, lineWidth: 2 });
30388
+ for (let hx = xStart + 7; hx <= xEnd; hx += 14) {
30389
+ out.push({ type: "line", x1: hx, y1: y, x2: hx - 7, y2: y + 7, color, lineWidth: 1 });
30390
+ }
30391
+ if (obj.label) {
30392
+ out.push({
30393
+ type: "text",
30394
+ x: (xStart + xEnd) / 2,
30395
+ y: y - 10,
30396
+ text: obj.label,
30397
+ color: PHYSICS_LABEL_COLOR,
30398
+ fontSize: 11,
30399
+ align: "center"
30400
+ });
30401
+ }
30402
+ break;
30403
+ }
30404
+ case "wall": {
30405
+ const yStart = obj.y1 ?? 0;
30406
+ const yEnd = obj.y2 ?? canvasHeight;
30407
+ const x = obj.x ?? 0;
30408
+ out.push({ type: "line", x1: x, y1: yStart, x2: x, y2: yEnd, color, lineWidth: 2 });
30409
+ for (let hy = yStart + 7; hy <= yEnd; hy += 14) {
30410
+ out.push({ type: "line", x1: x, y1: hy, x2: x - 7, y2: hy + 7, color, lineWidth: 1 });
30411
+ }
30412
+ if (obj.label) {
30413
+ out.push({
30414
+ type: "text",
30415
+ x: x + 12,
30416
+ y: (yStart + yEnd) / 2,
30417
+ text: obj.label,
30418
+ color: PHYSICS_LABEL_COLOR,
30419
+ fontSize: 11,
30420
+ align: "left"
30421
+ });
30422
+ }
30423
+ break;
30424
+ }
30425
+ case "ramp": {
30426
+ const x1 = obj.x1 ?? 0;
30427
+ const y1 = obj.y1 ?? 0;
30428
+ const x2 = obj.x2 ?? canvasWidth;
30429
+ const y2 = obj.y2 ?? canvasHeight;
30430
+ out.push({
30431
+ type: "polygon",
30432
+ points: [
30433
+ { x: x1, y: y1 },
30434
+ { x: x2, y: y2 },
30435
+ { x: x1, y: y2 }
30436
+ ],
30437
+ color,
30438
+ fill: obj.fill ?? "#e2e8f0",
30439
+ lineWidth: 2
30440
+ });
30441
+ if (obj.label) {
30442
+ out.push({
30443
+ type: "text",
30444
+ x: (2 * x1 + x2) / 3,
30445
+ y: (y1 + 2 * y2) / 3,
30446
+ text: obj.label,
30447
+ color: PHYSICS_LABEL_COLOR,
30448
+ fontSize: 11,
30449
+ align: "center"
30450
+ });
30451
+ }
30452
+ break;
30453
+ }
30454
+ case "box": {
30455
+ const x = obj.x ?? 0;
30456
+ const y = obj.y ?? 0;
30457
+ const w = obj.width ?? 40;
30458
+ const h = obj.height ?? 40;
30459
+ out.push({ type: "rect", x, y, width: w, height: h, color, fill: obj.fill, lineWidth: 2 });
30460
+ if (obj.label) {
30461
+ out.push({
30462
+ type: "text",
30463
+ x: x + w / 2,
30464
+ y: y + h / 2,
30465
+ text: obj.label,
30466
+ color: PHYSICS_LABEL_COLOR,
30467
+ fontSize: 11,
30468
+ align: "center"
30469
+ });
30470
+ }
30471
+ break;
30472
+ }
30473
+ case "pivot": {
30474
+ const x = obj.x ?? 0;
30475
+ const y = obj.y ?? 0;
30476
+ out.push({ type: "circle", x, y, radius: 5, color, fill: color });
30477
+ out.push({ type: "line", x1: x - 14, y1: y - 8, x2: x + 14, y2: y - 8, color, lineWidth: 1 });
30478
+ for (let k = 0; k < 5; k++) {
30479
+ const hx = x - 14 + 7 * k;
30480
+ out.push({ type: "line", x1: hx, y1: y - 8, x2: hx - 6, y2: y - 14, color, lineWidth: 1 });
30481
+ }
30482
+ if (obj.label) {
30483
+ out.push({
30484
+ type: "text",
30485
+ x,
30486
+ y: y - 20,
30487
+ text: obj.label,
30488
+ color: PHYSICS_LABEL_COLOR,
30489
+ fontSize: 11,
30490
+ align: "center"
30491
+ });
30492
+ }
30493
+ break;
30494
+ }
30495
+ }
30496
+ return out;
30497
+ }
30498
+ function trailShapes(trail) {
30499
+ const n = trail.points.length;
30500
+ if (n < 2) return [];
30501
+ const color = trail.color ?? "#94a3b8";
30502
+ const lineWidth = trail.width ?? 2;
30503
+ const fade = trail.fade ?? true;
30504
+ const globalOpacity = trail.opacity ?? 1;
30505
+ const out = [];
30506
+ for (let i = 0; i < n - 1; i++) {
30507
+ const a = trail.points[i];
30508
+ const b = trail.points[i + 1];
30509
+ const segmentOpacity = fade ? 0.12 + 0.68 * i / (n - 1) : 0.6;
30510
+ out.push({
30511
+ type: "line",
30512
+ x1: a.x,
30513
+ y1: a.y,
30514
+ x2: b.x,
30515
+ y2: b.y,
30516
+ color,
30517
+ lineWidth,
30518
+ opacity: segmentOpacity * globalOpacity
30519
+ });
30520
+ }
30521
+ return out;
30522
+ }
30523
+ function constraintShapes(c, a, b) {
30524
+ const color = c.color ?? "#9ca3af";
30525
+ const kind = c.kind ?? "rod";
30526
+ if (kind === "rod") {
30527
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2 }];
30528
+ }
30529
+ if (kind === "string") {
30530
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2, dash: "dashed" }];
30531
+ }
30532
+ const COILS = 8;
30533
+ const AMP = 7;
30534
+ const LEAD = 10;
30535
+ const dx = b.x - a.x;
30536
+ const dy = b.y - a.y;
30537
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
30538
+ const ux = dx / dist;
30539
+ const uy = dy / dist;
30540
+ const perpX = -uy;
30541
+ const perpY = ux;
30542
+ const aPrime = { x: a.x + LEAD * ux, y: a.y + LEAD * uy };
30543
+ const bPrime = { x: b.x - LEAD * ux, y: b.y - LEAD * uy };
30544
+ const m = 2 * COILS;
30545
+ const polyline = [{ x: a.x, y: a.y }, aPrime];
30546
+ for (let j = 1; j <= m; j++) {
30547
+ const t = j / (m + 1);
30548
+ const baseX = aPrime.x + t * (bPrime.x - aPrime.x);
30549
+ const baseY = aPrime.y + t * (bPrime.y - aPrime.y);
30550
+ const sign = j % 2 === 0 ? 1 : -1;
30551
+ polyline.push({ x: baseX + sign * AMP * perpX, y: baseY + sign * AMP * perpY });
30552
+ }
30553
+ polyline.push(bPrime, { x: b.x, y: b.y });
30554
+ const out = [];
30555
+ for (let i = 1; i < polyline.length; i++) {
30556
+ out.push({
30557
+ type: "line",
30558
+ x1: polyline[i - 1].x,
30559
+ y1: polyline[i - 1].y,
30560
+ x2: polyline[i].x,
30561
+ y2: polyline[i].y,
30562
+ color,
30563
+ lineWidth: 2
30564
+ });
30565
+ }
30566
+ return out;
30567
+ }
30568
+ function vectorShapes(v, bodyById) {
30569
+ let ax;
30570
+ let ay;
30571
+ if (v.body) {
30572
+ const anchor = bodyById.get(v.body);
30573
+ if (!anchor) return [];
30574
+ ax = anchor.x;
30575
+ ay = anchor.y;
30576
+ } else {
30577
+ ax = v.x ?? 0;
30578
+ ay = v.y ?? 0;
30579
+ }
30580
+ const scale = v.scale ?? 1;
30581
+ const color = v.color ?? "#dc2626";
30582
+ const tx = ax + v.dx * scale;
30583
+ const ty = ay + v.dy * scale;
30584
+ const out = [{ type: "arrow", x1: ax, y1: ay, x2: tx, y2: ty, color, lineWidth: 2, dash: v.dash }];
30585
+ if (v.label) {
30586
+ const dist = Math.max(1e-6, Math.hypot(tx - ax, ty - ay));
30587
+ const ux = (tx - ax) / dist;
30588
+ const uy = (ty - ay) / dist;
30589
+ out.push({
30590
+ type: "text",
30591
+ x: tx + 8 * ux,
30592
+ y: ty + 8 * uy,
30593
+ text: v.label,
30594
+ color,
30595
+ fontSize: 11,
30596
+ align: "center"
30597
+ });
30598
+ }
30599
+ return out;
30600
+ }
30601
+ function angleMarkerShapes(a) {
30602
+ const radius = a.radius ?? 26;
30603
+ const color = a.color ?? "#0ea5e9";
30604
+ const out = [
30605
+ {
30606
+ type: "ellipse",
30607
+ x: a.x,
30608
+ y: a.y,
30609
+ width: radius * 2,
30610
+ height: radius * 2,
30611
+ startAngle: a.from,
30612
+ endAngle: a.to,
30613
+ color,
30614
+ lineWidth: 2
30615
+ }
30616
+ ];
30617
+ if (a.label) {
30618
+ const mid = (a.from + a.to) / 2 * (Math.PI / 180);
30619
+ out.push({
30620
+ type: "text",
30621
+ x: a.x + (radius + 13) * Math.cos(mid),
30622
+ y: a.y + (radius + 13) * Math.sin(mid),
30623
+ text: a.label,
30624
+ color,
30625
+ fontSize: 11,
30626
+ align: "center"
30627
+ });
30628
+ }
30629
+ return out;
30630
+ }
30631
+ function fieldShapes(field, canvasWidth, canvasHeight) {
30632
+ const spacing = field.spacing ?? 48;
30633
+ const size = field.size ?? 14;
30634
+ const color = field.color ?? "#94a3b8";
30635
+ const regionX = field.x ?? 0;
30636
+ const regionY = field.y ?? 0;
30637
+ const regionW = field.width ?? canvasWidth;
30638
+ const regionH = field.height ?? canvasHeight;
30639
+ const out = [];
30640
+ for (let gx = regionX + spacing / 2; gx < regionX + regionW; gx += spacing) {
30641
+ for (let gy = regionY + spacing / 2; gy < regionY + regionH; gy += spacing) {
30642
+ if (field.kind === "arrows") {
30643
+ const rad = (field.angle ?? 0) * Math.PI / 180;
30644
+ const hx = Math.cos(rad) * size / 2;
30645
+ const hy = Math.sin(rad) * size / 2;
30646
+ out.push({ type: "arrow", x1: gx - hx, y1: gy - hy, x2: gx + hx, y2: gy + hy, color, lineWidth: 2 });
30647
+ } else if (field.kind === "into") {
30648
+ const r = size / 3;
30649
+ const d = 0.6 * r * Math.SQRT1_2;
30650
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30651
+ out.push({ type: "line", x1: gx - d, y1: gy - d, x2: gx + d, y2: gy + d, color, lineWidth: 1 });
30652
+ out.push({ type: "line", x1: gx - d, y1: gy + d, x2: gx + d, y2: gy - d, color, lineWidth: 1 });
30653
+ } else {
30654
+ const r = size / 3;
30655
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30656
+ out.push({ type: "circle", x: gx, y: gy, radius: 1.5, color, fill: color });
30657
+ }
30658
+ }
30659
+ }
30660
+ return out;
30661
+ }
30662
+ function meterShapes(meters, canvasHeight) {
30663
+ const n = meters.length;
30664
+ const out = [];
30665
+ const sharedMax = Math.max(1e-6, ...meters.map((m) => m.value));
30666
+ meters.forEach((meter, i) => {
30667
+ const rowY = canvasHeight - 10 - 16 * (n - i);
30668
+ const color = meter.color ?? "#3b82f6";
30669
+ const M = meter.max ?? sharedMax;
30670
+ const w = Math.round(Math.min(1, Math.max(0, meter.value / M)) * 110);
30671
+ out.push({ type: "text", x: 8, y: rowY + 8, text: meter.label, color: PHYSICS_LABEL_COLOR, fontSize: 10 });
30672
+ out.push({ type: "rect", x: 52, y: rowY, width: w, height: 10, color, fill: color });
30673
+ out.push({
30674
+ type: "text",
30675
+ x: 166,
30676
+ y: rowY + 8,
30677
+ text: formatMeterValue(meter.value),
30678
+ color: "#6b7280",
30679
+ fontSize: 9
30680
+ });
30681
+ });
30682
+ return out;
30683
+ }
30684
+ var physicsLog2, PHYSICS_LABEL_COLOR, PhysicsCanvas;
28580
30685
  var init_PhysicsCanvas = __esm({
28581
30686
  "components/learning/molecules/PhysicsCanvas.tsx"() {
28582
30687
  "use client";
@@ -28585,6 +30690,7 @@ var init_PhysicsCanvas = __esm({
28585
30690
  init_LearningCanvas();
28586
30691
  init_learningScene3D();
28587
30692
  physicsLog2 = logger.createLogger("almadar:ui:physics-canvas");
30693
+ PHYSICS_LABEL_COLOR = "#374151";
28588
30694
  PhysicsCanvas = ({
28589
30695
  className,
28590
30696
  width = 600,
@@ -28601,7 +30707,18 @@ var init_PhysicsCanvas = __esm({
28601
30707
  showForces = false,
28602
30708
  velocityScale = 20,
28603
30709
  forceScale = 20,
30710
+ sceneObjects = [],
30711
+ trails = [],
30712
+ vectors = [],
30713
+ surface3d,
30714
+ vectors3d = [],
30715
+ vectorScale = 1,
30716
+ angles = [],
30717
+ field,
30718
+ meters = [],
28604
30719
  shapes = [],
30720
+ readouts,
30721
+ traces,
28605
30722
  showGrid,
28606
30723
  shadows,
28607
30724
  interactive,
@@ -28616,19 +30733,14 @@ var init_PhysicsCanvas = __esm({
28616
30733
  for (const b of bodies) {
28617
30734
  if (b.id) bodyById.set(b.id, b);
28618
30735
  }
30736
+ if (field) out.push(...fieldShapes(field, width, height));
30737
+ for (const obj of sceneObjects) out.push(...sceneObjectShapes(obj, width, height));
30738
+ for (const trail of trails) out.push(...trailShapes(trail));
28619
30739
  for (const c of constraints) {
28620
30740
  const a = bodyById.get(c.from);
28621
30741
  const b = bodyById.get(c.to);
28622
30742
  if (!a || !b) continue;
28623
- out.push({
28624
- type: "line",
28625
- x1: a.x,
28626
- y1: a.y,
28627
- x2: b.x,
28628
- y2: b.y,
28629
- color: c.color ?? "#9ca3af",
28630
- lineWidth: 2
28631
- });
30743
+ out.push(...constraintShapes(c, a, b));
28632
30744
  }
28633
30745
  for (const b of bodies) {
28634
30746
  out.push({
@@ -28673,14 +30785,51 @@ var init_PhysicsCanvas = __esm({
28673
30785
  });
28674
30786
  }
28675
30787
  }
30788
+ for (const v of vectors) out.push(...vectorShapes(v, bodyById));
30789
+ for (const a of angles) out.push(...angleMarkerShapes(a));
30790
+ if (meters.length > 0) out.push(...meterShapes(meters, height));
28676
30791
  out.push(...shapes);
28677
30792
  return out;
28678
- }, [bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes]);
30793
+ }, [
30794
+ bodies,
30795
+ constraints,
30796
+ showVelocity,
30797
+ showForces,
30798
+ velocityScale,
30799
+ forceScale,
30800
+ sceneObjects,
30801
+ trails,
30802
+ vectors,
30803
+ angles,
30804
+ field,
30805
+ meters,
30806
+ shapes,
30807
+ width,
30808
+ height
30809
+ ]);
28679
30810
  const drawables3D = React87.useMemo(() => {
28680
30811
  if (mode !== "3d") return [];
28681
30812
  if (shapes.length > 0) {
28682
30813
  physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
28683
30814
  }
30815
+ if (sceneObjects.length > 0) {
30816
+ physicsLog2.debug("sceneObjects ignored in 3D mode (pixel-authored 2D vocabulary)", { count: sceneObjects.length });
30817
+ }
30818
+ if (vectors.length > 0) {
30819
+ physicsLog2.debug("vectors ignored in 3D mode (pixel-authored 2D vocabulary)", { count: vectors.length });
30820
+ }
30821
+ if (angles.length > 0) {
30822
+ physicsLog2.debug("angles ignored in 3D mode (pixel-authored 2D vocabulary)", { count: angles.length });
30823
+ }
30824
+ if (field) {
30825
+ physicsLog2.debug("field ignored in 3D mode (pixel-authored 2D vocabulary)");
30826
+ }
30827
+ if (meters.length > 0) {
30828
+ physicsLog2.debug("meters ignored in 3D mode (pixel-authored 2D vocabulary)", { count: meters.length });
30829
+ }
30830
+ if (animate) {
30831
+ physicsLog2.debug("animate ignored in 3D mode (motion is entity-state driven)");
30832
+ }
28684
30833
  const out = [];
28685
30834
  const labelColor = labelColorForBackground(backgroundColor);
28686
30835
  const bodyById = /* @__PURE__ */ new Map();
@@ -28728,15 +30877,67 @@ var init_PhysicsCanvas = __esm({
28728
30877
  if (arrow) out.push(arrow);
28729
30878
  }
28730
30879
  }
30880
+ for (const trail of trails) {
30881
+ if (trail.fade !== void 0) {
30882
+ physicsLog2.debug("trail.fade ignored in 3D mode (2D-only fade curve \u2014 3D draws an opaque tube)", { id: trail.id });
30883
+ }
30884
+ const points = trail.points.map((p) => [p.x, p.y, p.z ?? 0]);
30885
+ out.push(
30886
+ ...polylineTube(points, trail.width ?? 0.05, trail.color ?? "#94a3b8", {
30887
+ ...trail.opacity !== void 0 ? { opacity: trail.opacity } : {}
30888
+ })
30889
+ );
30890
+ }
30891
+ if (surface3d) {
30892
+ out.push(...heightFieldMesh(surface3d));
30893
+ }
30894
+ if (vectors3d.length > 0) {
30895
+ out.push(
30896
+ ...arrowField(
30897
+ vectors3d.map((v) => ({
30898
+ id: v.id,
30899
+ from: [v.x, v.y, v.z ?? 0],
30900
+ delta: [v.dx, v.dy, v.dz ?? 0],
30901
+ color: v.color,
30902
+ label: v.label,
30903
+ width: v.width
30904
+ })),
30905
+ { scale: vectorScale, labelColor }
30906
+ )
30907
+ );
30908
+ }
28731
30909
  return out;
28732
- }, [mode, bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes, backgroundColor]);
30910
+ }, [
30911
+ mode,
30912
+ bodies,
30913
+ constraints,
30914
+ showVelocity,
30915
+ showForces,
30916
+ velocityScale,
30917
+ forceScale,
30918
+ shapes,
30919
+ sceneObjects,
30920
+ trails,
30921
+ vectors,
30922
+ surface3d,
30923
+ vectors3d,
30924
+ vectorScale,
30925
+ angles,
30926
+ field,
30927
+ meters,
30928
+ animate,
30929
+ backgroundColor
30930
+ ]);
28733
30931
  const bodyIndexById = React87.useMemo(() => {
28734
30932
  const m = /* @__PURE__ */ new Map();
30933
+ vectors3d.forEach((v, i) => {
30934
+ if (v.id) m.set(v.id, i);
30935
+ });
28735
30936
  bodies.forEach((b, i) => {
28736
30937
  if (b.id) m.set(b.id, i);
28737
30938
  });
28738
30939
  return m;
28739
- }, [bodies]);
30940
+ }, [bodies, vectors3d]);
28740
30941
  if (mode === "3d") {
28741
30942
  return /* @__PURE__ */ jsxRuntime.jsx(
28742
30943
  LearningScene3D,
@@ -28768,6 +30969,8 @@ var init_PhysicsCanvas = __esm({
28768
30969
  height,
28769
30970
  backgroundColor,
28770
30971
  shapes: derivedShapes,
30972
+ readouts,
30973
+ traces,
28771
30974
  interactive: interactive ?? false,
28772
30975
  animate,
28773
30976
  onShapeClick,
@@ -28918,7 +31121,7 @@ function layoutFlow(nodeIds, adjacency, roots, width, height, margin) {
28918
31121
  }
28919
31122
  return nodeIds.map((id) => positions.get(id));
28920
31123
  }
28921
- function layoutTree(nodeIds, adjacency, roots, width, height, margin) {
31124
+ function layoutTree2(nodeIds, adjacency, roots, width, height, margin) {
28922
31125
  const effectiveRoots = roots.length > 0 ? roots : [nodeIds[0]];
28923
31126
  const layers = assignLayers(nodeIds, adjacency, effectiveRoots);
28924
31127
  const maxLayer = Math.max(...Array.from(layers.values()));
@@ -28974,7 +31177,7 @@ function computeStaticLayout(mode, input) {
28974
31177
  const adjacency = buildAdjacency(nodeIds, edges);
28975
31178
  const roots = findRoots(nodeIds, adjacency);
28976
31179
  if (mode === "flow") return layoutFlow(nodeIds, adjacency, roots, width, height, margin);
28977
- if (mode === "tree") return layoutTree(nodeIds, adjacency, roots, width, height, margin);
31180
+ if (mode === "tree") return layoutTree2(nodeIds, adjacency, roots, width, height, margin);
28978
31181
  return layoutRadial(nodeIds, adjacency, roots, width, height, margin);
28979
31182
  }
28980
31183
  var init_graphViewLayouts = __esm({
@@ -30217,26 +32420,6 @@ var init_Lightbox = __esm({
30217
32420
  Lightbox.displayName = "Lightbox";
30218
32421
  }
30219
32422
  });
30220
- function useMediaQuery(query) {
30221
- const subscribe = React87.useCallback(
30222
- (onChange) => {
30223
- const mql = window.matchMedia(query);
30224
- mql.addEventListener("change", onChange);
30225
- return () => mql.removeEventListener("change", onChange);
30226
- },
30227
- [query]
30228
- );
30229
- return React87.useSyncExternalStore(
30230
- subscribe,
30231
- () => window.matchMedia(query).matches,
30232
- () => false
30233
- );
30234
- }
30235
- var init_useMediaQuery = __esm({
30236
- "hooks/useMediaQuery.ts"() {
30237
- "use client";
30238
- }
30239
- });
30240
32423
  function renderIconInput3(icon, props) {
30241
32424
  return typeof icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon, ...props });
30242
32425
  }
@@ -30275,8 +32458,8 @@ function TableView({
30275
32458
  columns,
30276
32459
  fields,
30277
32460
  itemActions,
30278
- maxInlineActions,
30279
- itemClickEvent,
32461
+ maxInlineActions: _maxInlineActions,
32462
+ itemClickEvent = "",
30280
32463
  selectable = false,
30281
32464
  selectEvent,
30282
32465
  selectedIds,
@@ -30326,7 +32509,6 @@ function TableView({
30326
32509
  const hasMore = pageSize > 0 && visibleCount < ordered2.length;
30327
32510
  const hasRenderProp = typeof children === "function";
30328
32511
  const idField = dndItemIdField ?? "id";
30329
- const isCoarsePointer = useMediaQuery("(pointer: coarse)");
30330
32512
  React87__namespace.default.useEffect(() => {
30331
32513
  tableViewLog.debug("render", {
30332
32514
  rowCount: data.length,
@@ -30368,21 +32550,14 @@ function TableView({
30368
32550
  const dir = sortColumn === (col.field ?? col.key) && sortDirection === "asc" ? "desc" : "asc";
30369
32551
  eventBus.emit(`UI:${sortEvent}`, { column: col.field ?? col.key, direction: dir });
30370
32552
  };
30371
- const handleActionClick = (action, row) => (e) => {
30372
- e.stopPropagation();
30373
- const payload = {
30374
- id: row.id,
30375
- row
30376
- };
30377
- eventBus.emit(`UI:${action.event}`, payload);
30378
- };
32553
+ const rowClickEvent = itemClickEvent || actionDefs.find((a) => a.variant !== "danger")?.event;
30379
32554
  const handleRowClick = (row) => () => {
30380
- if (!itemClickEvent) return;
32555
+ if (!rowClickEvent) return;
30381
32556
  const payload = {
30382
32557
  id: row.id,
30383
32558
  row
30384
32559
  };
30385
- eventBus.emit(`UI:${itemClickEvent}`, payload);
32560
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
30386
32561
  };
30387
32562
  const colFloors = React87__namespace.default.useMemo(
30388
32563
  () => colDefs.map((col) => {
@@ -30398,10 +32573,7 @@ function TableView({
30398
32573
  const statusNode = isLoading ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) }) : error ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "error", children: error.message }) }) : data.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-12", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: emptyMessage || t("empty.noItems") }) }) : null;
30399
32574
  const lk = LOOKS[look];
30400
32575
  const hasActions = actionDefs.length > 0;
30401
- const effectiveMaxInline = isCoarsePointer ? 0 : maxInlineActions;
30402
- const inlineActionCount = hasActions ? effectiveMaxInline != null ? Math.min(actionDefs.length, effectiveMaxInline) : actionDefs.length : 0;
30403
- const hasOverflowActions = hasActions && effectiveMaxInline != null && actionDefs.length > effectiveMaxInline;
30404
- const actionsTrack = hasActions ? `${inlineActionCount * 6 + (hasOverflowActions ? 3 : 0)}rem` : null;
32576
+ const actionsTrack = hasActions ? "3rem" : null;
30405
32577
  const gridTemplateColumns = [
30406
32578
  selectable ? "auto" : null,
30407
32579
  ...colDefs.map((c, i) => c.width ?? `minmax(${colFloors[i]}ch, 1fr)`),
@@ -30448,7 +32620,7 @@ function TableView({
30448
32620
  col.key
30449
32621
  );
30450
32622
  }),
30451
- hasActions && /* @__PURE__ */ jsxRuntime.jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)]" })
32623
+ hasActions && /* @__PURE__ */ jsxRuntime.jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)] border-l border-[var(--color-border)] h-full" })
30452
32624
  ]
30453
32625
  }
30454
32626
  );
@@ -30460,12 +32632,12 @@ function TableView({
30460
32632
  role: "row",
30461
32633
  "data-entity-row": true,
30462
32634
  "data-entity-id": id,
30463
- onClick: itemClickEvent ? handleRowClick(row) : void 0,
32635
+ onClick: rowClickEvent ? handleRowClick(row) : void 0,
30464
32636
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
30465
32637
  className: cn(
30466
32638
  "group items-center gap-3 transition-colors duration-fast",
30467
32639
  hasRenderProp ? "flex" : "grid",
30468
- itemClickEvent && "cursor-pointer",
32640
+ rowClickEvent && "cursor-pointer",
30469
32641
  lk.rowPad,
30470
32642
  lk.divider && "border-b border-[var(--color-border)]",
30471
32643
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -30473,7 +32645,7 @@ function TableView({
30473
32645
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
30474
32646
  ),
30475
32647
  children: [
30476
- selectable && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex items-center", onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsxRuntime.jsx(
32648
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex items-center", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsxRuntime.jsx(
30477
32649
  Checkbox,
30478
32650
  {
30479
32651
  checked: selected.has(id),
@@ -30494,53 +32666,37 @@ function TableView({
30494
32666
  }
30495
32667
  return /* @__PURE__ */ jsxRuntime.jsx(Box, { role: "cell", className: cellBase, children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-foreground", children: formatCell(raw, col.format) }) }, col.key);
30496
32668
  }),
30497
- hasActions && /* @__PURE__ */ jsxRuntime.jsxs(
32669
+ hasActions && /* @__PURE__ */ jsxRuntime.jsx(
30498
32670
  HStack,
30499
32671
  {
30500
32672
  gap: "xs",
30501
- onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
32673
+ onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0,
30502
32674
  className: cn(
30503
32675
  // Pinned: the fixed column tracks routinely overflow the caller's
30504
- // scroll container, which used to leave the actions off-screen.
30505
- // Opaque so scrolled cells pass underneath it.
32676
+ // scroll container, which would leave the kebab off-screen.
32677
+ // Opaque + hairline edge so it reads as a pinned column, not a
32678
+ // floating control, while scrolled cells pass underneath.
30506
32679
  "justify-end flex-shrink-0 sticky right-0 z-[1] transition-colors",
32680
+ "border-l border-[var(--color-border)]",
30507
32681
  lk.striped && index % 2 === 1 ? "bg-[var(--color-surface-subtle)]" : "bg-[var(--color-card)] group-hover:bg-[var(--color-surface-subtle)]"
30508
32682
  ),
30509
- children: [
30510
- (effectiveMaxInline != null ? actionDefs.slice(0, effectiveMaxInline) : actionDefs).map((action, i) => /* @__PURE__ */ jsxRuntime.jsxs(
30511
- Button,
30512
- {
30513
- variant: action.variant === "primary" ? "primary" : "ghost",
30514
- size: "sm",
30515
- onClick: handleActionClick(action, row),
30516
- "data-testid": `action-${action.event}`,
30517
- "data-row-id": String(row.id),
30518
- className: cn(action.variant === "danger" && "text-error hover:text-error hover:bg-error/10"),
30519
- children: [
30520
- action.icon && renderIconInput3(action.icon, { size: "xs", className: "mr-1" }),
30521
- action.label
30522
- ]
30523
- },
30524
- i
30525
- )),
30526
- effectiveMaxInline != null && actionDefs.length > effectiveMaxInline && /* @__PURE__ */ jsxRuntime.jsx(
30527
- Menu,
30528
- {
30529
- position: "bottom-end",
30530
- trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
30531
- items: actionDefs.slice(effectiveMaxInline).map((action) => ({
30532
- label: action.label,
30533
- icon: action.icon,
30534
- event: action.event,
30535
- variant: action.variant === "danger" ? "danger" : "default",
30536
- onClick: () => eventBus.emit(`UI:${action.event}`, {
30537
- id: row.id,
30538
- row
30539
- })
30540
- }))
30541
- }
30542
- )
30543
- ]
32683
+ children: /* @__PURE__ */ jsxRuntime.jsx(
32684
+ Menu,
32685
+ {
32686
+ position: "bottom-end",
32687
+ trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", "data-row-id": String(row.id), children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
32688
+ items: actionDefs.map((action) => ({
32689
+ label: action.label,
32690
+ icon: action.icon,
32691
+ event: action.event,
32692
+ variant: action.variant === "danger" ? "danger" : "default",
32693
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
32694
+ id: row.id,
32695
+ row
32696
+ })
32697
+ }))
32698
+ }
32699
+ )
30544
32700
  }
30545
32701
  )
30546
32702
  ]
@@ -30583,7 +32739,6 @@ var init_TableView = __esm({
30583
32739
  init_format();
30584
32740
  init_getNestedValue();
30585
32741
  init_useEventBus();
30586
- init_useMediaQuery();
30587
32742
  init_Box();
30588
32743
  init_Stack();
30589
32744
  init_Typography();
@@ -45642,6 +47797,7 @@ var init_component_registry_generated = __esm({
45642
47797
  init_ActionTile();
45643
47798
  init_ActivationBlock();
45644
47799
  init_ComponentPatterns();
47800
+ init_AlgoGraphCanvas();
45645
47801
  init_AlgorithmCanvas();
45646
47802
  init_AnimatedCounter();
45647
47803
  init_AnimatedGraphic();
@@ -45905,6 +48061,7 @@ var init_component_registry_generated = __esm({
45905
48061
  "ActivationBlock": ActivationBlock,
45906
48062
  "Alert": AlertPattern,
45907
48063
  "AlertPattern": AlertPattern,
48064
+ "AlgoGraphCanvas": AlgoGraphCanvas,
45908
48065
  "AlgorithmCanvas": AlgorithmCanvas,
45909
48066
  "AnimatedCounter": AnimatedCounter,
45910
48067
  "AnimatedGraphic": AnimatedGraphic,