@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.
@@ -209,7 +209,7 @@ function useEventBus() {
209
209
  return {
210
210
  ...baseBus,
211
211
  emit: (type, payload, source) => {
212
- if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
212
+ if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".") && !source?.trait) {
213
213
  scopeLog.warn("emit:bare-key-no-scope", { type });
214
214
  }
215
215
  baseBus.emit(type, payload, source);
@@ -613,26 +613,6 @@ var init_useDragReorder = __esm({
613
613
  "use client";
614
614
  }
615
615
  });
616
- function useMediaQuery(query) {
617
- const subscribe = React85.useCallback(
618
- (onChange) => {
619
- const mql = window.matchMedia(query);
620
- mql.addEventListener("change", onChange);
621
- return () => mql.removeEventListener("change", onChange);
622
- },
623
- [query]
624
- );
625
- return React85.useSyncExternalStore(
626
- subscribe,
627
- () => window.matchMedia(query).matches,
628
- () => false
629
- );
630
- }
631
- var init_useMediaQuery = __esm({
632
- "hooks/useMediaQuery.ts"() {
633
- "use client";
634
- }
635
- });
636
616
  function useInfiniteScroll(onLoadMore, options = {}) {
637
617
  const { rootMargin = "200px", hasMore = true, isLoading = false } = options;
638
618
  const observerRef = React85.useRef(null);
@@ -11733,6 +11713,14 @@ function shapeBounds(shape) {
11733
11713
  w: shape.radius * 2 + 8,
11734
11714
  h: shape.radius * 2 + 8
11735
11715
  };
11716
+ case "ellipse":
11717
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11718
+ return {
11719
+ x: shape.x - shape.width / 2 - 4,
11720
+ y: shape.y - shape.height / 2 - 4,
11721
+ w: shape.width + 8,
11722
+ h: shape.height + 8
11723
+ };
11736
11724
  case "rect":
11737
11725
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11738
11726
  return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
@@ -11766,13 +11754,14 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
11766
11754
  ctx.closePath();
11767
11755
  ctx.fill();
11768
11756
  }
11769
- function drawShape(ctx, shape, width, height) {
11757
+ function drawShape(ctx, shape, width, height, allShapes) {
11770
11758
  ctx.save();
11771
11759
  const opacity = shape.opacity ?? 1;
11772
11760
  ctx.globalAlpha = opacity;
11773
11761
  const stroke = resolveColor2(shape.color, ctx, "#333333");
11774
11762
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
11775
11763
  ctx.lineWidth = shape.lineWidth ?? 2;
11764
+ if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
11776
11765
  switch (shape.type) {
11777
11766
  case "grid": {
11778
11767
  const step = shape.step ?? 40;
@@ -11838,6 +11827,20 @@ function drawShape(ctx, shape, width, height) {
11838
11827
  ctx.stroke();
11839
11828
  break;
11840
11829
  }
11830
+ case "ellipse": {
11831
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11832
+ const startAngle = (shape.startAngle ?? 0) * Math.PI / 180;
11833
+ const endAngle = (shape.endAngle ?? 360) * Math.PI / 180;
11834
+ ctx.beginPath();
11835
+ ctx.ellipse(shape.x, shape.y, shape.width / 2, shape.height / 2, 0, startAngle, endAngle);
11836
+ if (fill) {
11837
+ ctx.fillStyle = fill;
11838
+ ctx.fill();
11839
+ }
11840
+ ctx.strokeStyle = stroke;
11841
+ ctx.stroke();
11842
+ break;
11843
+ }
11841
11844
  case "rect": {
11842
11845
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11843
11846
  if (fill) {
@@ -11884,21 +11887,153 @@ function drawShape(ctx, shape, width, height) {
11884
11887
  ctx.fillText(shape.text, shape.x, shape.y);
11885
11888
  break;
11886
11889
  }
11890
+ case "venn-region": {
11891
+ const resolveCircles = (ids) => (ids ?? []).flatMap((id) => {
11892
+ const c = allShapes.find((s) => s.type === "circle" && s.id === id);
11893
+ return c && c.x != null && c.y != null && c.radius != null ? [{ x: c.x, y: c.y, radius: c.radius }] : [];
11894
+ });
11895
+ const inside = resolveCircles(shape.inside);
11896
+ if (inside.length === 0) break;
11897
+ const outside = resolveCircles(shape.outside);
11898
+ const off = document.createElement("canvas");
11899
+ off.width = ctx.canvas.width;
11900
+ off.height = ctx.canvas.height;
11901
+ const octx = off.getContext("2d");
11902
+ if (!octx) break;
11903
+ octx.setTransform(ctx.getTransform());
11904
+ for (const c of inside) {
11905
+ const p = new Path2D();
11906
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11907
+ octx.clip(p);
11908
+ }
11909
+ octx.fillStyle = fill ?? stroke;
11910
+ octx.fillRect(0, 0, width, height);
11911
+ octx.globalCompositeOperation = "destination-out";
11912
+ for (const c of outside) {
11913
+ const p = new Path2D();
11914
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11915
+ octx.fill(p);
11916
+ }
11917
+ ctx.save();
11918
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
11919
+ ctx.drawImage(off, 0, 0);
11920
+ ctx.restore();
11921
+ break;
11922
+ }
11887
11923
  }
11888
11924
  ctx.restore();
11889
11925
  }
11890
- var LearningCanvas;
11926
+ function readoutShapes(readouts, width) {
11927
+ const out = [];
11928
+ const chipH = 18;
11929
+ const gap = 6;
11930
+ let rightEdge = width - 6;
11931
+ let rowY = 6;
11932
+ for (const readout of readouts) {
11933
+ const text = `${readout.label}: ${String(readout.value)}`;
11934
+ const chipW = Math.min(170, Math.max(34, text.length * 6 + 12));
11935
+ let chipX = rightEdge - chipW;
11936
+ if (chipX < 4) {
11937
+ rowY += chipH + 4;
11938
+ rightEdge = width - 6;
11939
+ chipX = rightEdge - chipW;
11940
+ }
11941
+ const color = readout.color ?? "#334155";
11942
+ out.push({ type: "rect", x: chipX, y: rowY, width: chipW, height: chipH, color, fill: color });
11943
+ out.push({
11944
+ type: "text",
11945
+ x: chipX + chipW / 2,
11946
+ y: rowY + chipH / 2,
11947
+ text,
11948
+ color: "#ffffff",
11949
+ fontSize: 10,
11950
+ align: "center"
11951
+ });
11952
+ rightEdge = chipX - gap;
11953
+ }
11954
+ return out;
11955
+ }
11956
+ function traceShapes(panel, k, width, height) {
11957
+ const w = panel.width ?? Math.round(width * 0.32);
11958
+ const h = panel.height ?? Math.round(height * 0.28);
11959
+ const x = panel.x ?? width - w - 8;
11960
+ const y = panel.y ?? height - h - 8 - k * (h + 8);
11961
+ const allSamples = panel.series.flatMap((series) => series.samples);
11962
+ let xLo = Math.min(...allSamples.map((p) => p.x));
11963
+ let xHi = Math.max(...allSamples.map((p) => p.x));
11964
+ let yLo = Math.min(...allSamples.map((p) => p.y));
11965
+ let yHi = Math.max(...allSamples.map((p) => p.y));
11966
+ if (xLo === xHi) {
11967
+ xLo -= 1;
11968
+ xHi += 1;
11969
+ }
11970
+ if (yLo === yHi) {
11971
+ yLo -= 1;
11972
+ yHi += 1;
11973
+ }
11974
+ const backgroundColor = panel.backgroundColor ?? "#ffffff";
11975
+ const frameColor = panel.frameColor ?? "#94a3b8";
11976
+ const out = [];
11977
+ out.push({
11978
+ type: "rect",
11979
+ x,
11980
+ y,
11981
+ width: w,
11982
+ height: h,
11983
+ color: backgroundColor,
11984
+ fill: backgroundColor,
11985
+ opacity: panel.backgroundOpacity ?? 0.85
11986
+ });
11987
+ out.push({ type: "rect", x, y, width: w, height: h, color: frameColor, lineWidth: 1 });
11988
+ panel.series.forEach((series, j) => {
11989
+ const color = series.color ?? TRACE_SERIES_COLORS[j % TRACE_SERIES_COLORS.length];
11990
+ const mapped = series.samples.map((p) => ({
11991
+ x: x + 4 + (p.x - xLo) / (xHi - xLo) * (w - 8),
11992
+ y: y + h - 4 - (p.y - yLo) / (yHi - yLo) * (h - 8)
11993
+ }));
11994
+ for (let i = 1; i < mapped.length; i++) {
11995
+ out.push({
11996
+ type: "line",
11997
+ x1: mapped[i - 1].x,
11998
+ y1: mapped[i - 1].y,
11999
+ x2: mapped[i].x,
12000
+ y2: mapped[i].y,
12001
+ color,
12002
+ lineWidth: 1.5
12003
+ });
12004
+ }
12005
+ if (mapped.length > 0) {
12006
+ const last = mapped[mapped.length - 1];
12007
+ out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
12008
+ }
12009
+ if (series.label) {
12010
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
12011
+ }
12012
+ });
12013
+ if (panel.yLabel) {
12014
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
12015
+ }
12016
+ if (panel.xLabel) {
12017
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
12018
+ }
12019
+ return out;
12020
+ }
12021
+ var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
11891
12022
  var init_LearningCanvas = __esm({
11892
12023
  "components/learning/atoms/LearningCanvas.tsx"() {
11893
12024
  "use client";
11894
12025
  init_cn();
11895
12026
  init_useEventBus();
12027
+ DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
12028
+ TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
11896
12029
  LearningCanvas = ({
11897
12030
  className,
11898
12031
  width = 600,
11899
12032
  height = 400,
11900
12033
  backgroundColor,
11901
12034
  shapes = [],
12035
+ readouts,
12036
+ traces,
11902
12037
  interactive = false,
11903
12038
  animate = false,
11904
12039
  onShapeClick,
@@ -11924,6 +12059,12 @@ var init_LearningCanvas = __esm({
11924
12059
  }
11925
12060
  return -1;
11926
12061
  }, [shapes]);
12062
+ const derivedShapes = React85.useMemo(() => {
12063
+ if (!traces?.length && !readouts?.length) return shapes;
12064
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
12065
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
12066
+ return [...shapes, ...traceOut, ...readoutOut];
12067
+ }, [shapes, traces, readouts, width, height]);
11927
12068
  const draw = React85.useCallback(() => {
11928
12069
  const canvas = canvasRef.current;
11929
12070
  if (!canvas) return;
@@ -11940,13 +12081,13 @@ var init_LearningCanvas = __esm({
11940
12081
  ctx.fillStyle = backgroundColor;
11941
12082
  ctx.fillRect(0, 0, width, height);
11942
12083
  }
11943
- for (const shape of shapes) {
11944
- if (shape.type !== "text") drawShape(ctx, shape, width, height);
12084
+ for (const shape of derivedShapes) {
12085
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
11945
12086
  }
11946
- for (const shape of shapes) {
11947
- if (shape.type === "text") drawShape(ctx, shape, width, height);
12087
+ for (const shape of derivedShapes) {
12088
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
11948
12089
  }
11949
- }, [width, height, backgroundColor, shapes]);
12090
+ }, [width, height, backgroundColor, derivedShapes]);
11950
12091
  React85.useEffect(() => {
11951
12092
  draw();
11952
12093
  }, [draw]);
@@ -13496,7 +13637,363 @@ var init_ComponentPatterns = __esm({
13496
13637
  AlertPattern.displayName = "AlertPattern";
13497
13638
  }
13498
13639
  });
13499
- var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, AlgorithmCanvas;
13640
+ function layoutCircle(nodes, width, height) {
13641
+ const cx = width / 2;
13642
+ const cy = height / 2;
13643
+ const radius = Math.max(10, Math.min(cx, cy) - 40);
13644
+ const positions = /* @__PURE__ */ new Map();
13645
+ const n = nodes.length;
13646
+ nodes.forEach((node, i) => {
13647
+ const angle = 2 * Math.PI * i / Math.max(n, 1) - Math.PI / 2;
13648
+ positions.set(node.id, { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) });
13649
+ });
13650
+ return positions;
13651
+ }
13652
+ function layoutTree(nodes, edges, root, width, height) {
13653
+ const nodeIds = nodes.map((n) => n.id);
13654
+ const idSet = new Set(nodeIds);
13655
+ const childrenOf = /* @__PURE__ */ new Map();
13656
+ const hasIncoming = /* @__PURE__ */ new Set();
13657
+ for (const e of edges) {
13658
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
13659
+ const list = childrenOf.get(e.from) ?? [];
13660
+ list.push(e.to);
13661
+ childrenOf.set(e.from, list);
13662
+ hasIncoming.add(e.to);
13663
+ }
13664
+ const depth = /* @__PURE__ */ new Map();
13665
+ const treeChildren = /* @__PURE__ */ new Map();
13666
+ const visited = /* @__PURE__ */ new Set();
13667
+ const bfsFrom = (start) => {
13668
+ if (visited.has(start)) return;
13669
+ visited.add(start);
13670
+ depth.set(start, 0);
13671
+ const queue = [start];
13672
+ while (queue.length > 0) {
13673
+ const u = queue.shift();
13674
+ for (const v of childrenOf.get(u) ?? []) {
13675
+ if (visited.has(v)) continue;
13676
+ visited.add(v);
13677
+ depth.set(v, (depth.get(u) ?? 0) + 1);
13678
+ const list = treeChildren.get(u) ?? [];
13679
+ list.push(v);
13680
+ treeChildren.set(u, list);
13681
+ queue.push(v);
13682
+ }
13683
+ }
13684
+ };
13685
+ const primaryRoot = root && idSet.has(root) ? root : nodeIds.find((id) => !hasIncoming.has(id)) ?? nodeIds[0];
13686
+ const rootsOrder = [];
13687
+ if (primaryRoot !== void 0) {
13688
+ bfsFrom(primaryRoot);
13689
+ rootsOrder.push(primaryRoot);
13690
+ }
13691
+ for (const id of nodeIds) {
13692
+ if (!visited.has(id)) {
13693
+ bfsFrom(id);
13694
+ rootsOrder.push(id);
13695
+ }
13696
+ }
13697
+ let leafCounter = 0;
13698
+ const xSlot = /* @__PURE__ */ new Map();
13699
+ const assignXSlot = (u) => {
13700
+ const children = treeChildren.get(u) ?? [];
13701
+ if (children.length === 0) {
13702
+ const slot = leafCounter++;
13703
+ xSlot.set(u, slot);
13704
+ return slot;
13705
+ }
13706
+ const childSlots = children.map(assignXSlot);
13707
+ const avg = childSlots.reduce((a, b) => a + b, 0) / childSlots.length;
13708
+ xSlot.set(u, avg);
13709
+ return avg;
13710
+ };
13711
+ for (const r of rootsOrder) assignXSlot(r);
13712
+ let maxDepth = 0;
13713
+ for (const d of depth.values()) maxDepth = Math.max(maxDepth, d);
13714
+ const colWidth = width / Math.max(1, leafCounter);
13715
+ const rowHeight = height / (maxDepth + 1);
13716
+ const positions = /* @__PURE__ */ new Map();
13717
+ for (const id of nodeIds) {
13718
+ const slot = xSlot.get(id) ?? 0;
13719
+ const d = depth.get(id) ?? 0;
13720
+ positions.set(id, { x: slot * colWidth + colWidth / 2, y: d * rowHeight + rowHeight / 2 });
13721
+ }
13722
+ return positions;
13723
+ }
13724
+ function layoutLayered(nodes, edges, width, height) {
13725
+ const nodeIds = nodes.map((n) => n.id);
13726
+ const idSet = new Set(nodeIds);
13727
+ const adj = /* @__PURE__ */ new Map();
13728
+ const remainingIndegree = /* @__PURE__ */ new Map();
13729
+ for (const id of nodeIds) remainingIndegree.set(id, 0);
13730
+ for (const e of edges) {
13731
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
13732
+ const list = adj.get(e.from) ?? [];
13733
+ list.push(e.to);
13734
+ adj.set(e.from, list);
13735
+ remainingIndegree.set(e.to, (remainingIndegree.get(e.to) ?? 0) + 1);
13736
+ }
13737
+ const layer = /* @__PURE__ */ new Map();
13738
+ const dequeued = /* @__PURE__ */ new Set();
13739
+ const queue = [];
13740
+ for (const id of nodeIds) {
13741
+ if ((remainingIndegree.get(id) ?? 0) === 0) {
13742
+ layer.set(id, 0);
13743
+ queue.push(id);
13744
+ }
13745
+ }
13746
+ while (queue.length > 0) {
13747
+ const u = queue.shift();
13748
+ dequeued.add(u);
13749
+ for (const v of adj.get(u) ?? []) {
13750
+ const candidate = (layer.get(u) ?? 0) + 1;
13751
+ layer.set(v, Math.max(layer.get(v) ?? 0, candidate));
13752
+ remainingIndegree.set(v, (remainingIndegree.get(v) ?? 0) - 1);
13753
+ if ((remainingIndegree.get(v) ?? 0) === 0 && !dequeued.has(v)) {
13754
+ queue.push(v);
13755
+ }
13756
+ }
13757
+ }
13758
+ let baseMaxLayer = 0;
13759
+ for (const id of nodeIds) {
13760
+ if (dequeued.has(id)) baseMaxLayer = Math.max(baseMaxLayer, layer.get(id) ?? 0);
13761
+ }
13762
+ const cycleLayer = baseMaxLayer + 1;
13763
+ let maxLayer = baseMaxLayer;
13764
+ for (const id of nodeIds) {
13765
+ if (!dequeued.has(id)) {
13766
+ layer.set(id, cycleLayer);
13767
+ maxLayer = cycleLayer;
13768
+ }
13769
+ }
13770
+ const colWidth = width / Math.max(1, maxLayer + 1);
13771
+ const byLayer = /* @__PURE__ */ new Map();
13772
+ for (const id of nodeIds) {
13773
+ const l = layer.get(id) ?? 0;
13774
+ const list = byLayer.get(l) ?? [];
13775
+ list.push(id);
13776
+ byLayer.set(l, list);
13777
+ }
13778
+ const positions = /* @__PURE__ */ new Map();
13779
+ for (const [l, ids] of byLayer) {
13780
+ const rowHeight = height / ids.length;
13781
+ ids.forEach((id, i) => {
13782
+ positions.set(id, { x: l * colWidth + colWidth / 2, y: i * rowHeight + rowHeight / 2 });
13783
+ });
13784
+ }
13785
+ return positions;
13786
+ }
13787
+ function computePositions(nodes, edges, layout, root, width, height) {
13788
+ switch (layout) {
13789
+ case "circle":
13790
+ return layoutCircle(nodes, width, height);
13791
+ case "tree":
13792
+ return layoutTree(nodes, edges, root, width, height);
13793
+ case "layered":
13794
+ return layoutLayered(nodes, edges, width, height);
13795
+ case "manual":
13796
+ default: {
13797
+ const positions = /* @__PURE__ */ new Map();
13798
+ for (const n of nodes) positions.set(n.id, { x: n.x ?? 0, y: n.y ?? 0 });
13799
+ return positions;
13800
+ }
13801
+ }
13802
+ }
13803
+ var NODE_STATE_COLOR, EDGE_STATE_COLOR, DEFAULT_NODE_RADIUS, AlgoGraphCanvas;
13804
+ var init_AlgoGraphCanvas = __esm({
13805
+ "components/learning/molecules/AlgoGraphCanvas.tsx"() {
13806
+ "use client";
13807
+ init_atoms();
13808
+ init_Stack();
13809
+ init_LearningCanvas();
13810
+ NODE_STATE_COLOR = {
13811
+ unvisited: "#cbd5e1",
13812
+ frontier: "#f59e0b",
13813
+ current: "#ef4444",
13814
+ visited: "#22c55e",
13815
+ goal: "#8b5cf6",
13816
+ path: "#0ea5e9"
13817
+ };
13818
+ EDGE_STATE_COLOR = {
13819
+ default: "#9ca3af",
13820
+ tree: "#16a34a",
13821
+ relaxed: "#f59e0b",
13822
+ candidate: "#38bdf8",
13823
+ path: "#dc2626"
13824
+ };
13825
+ DEFAULT_NODE_RADIUS = 18;
13826
+ AlgoGraphCanvas = ({
13827
+ className,
13828
+ width = 600,
13829
+ height = 400,
13830
+ title,
13831
+ backgroundColor,
13832
+ nodes = [],
13833
+ edges = [],
13834
+ layout = "manual",
13835
+ root,
13836
+ shapes = [],
13837
+ interactive = false,
13838
+ animate = false,
13839
+ onShapeClick,
13840
+ onNodeClick,
13841
+ isLoading,
13842
+ error
13843
+ }) => {
13844
+ const nodeById = React85.useMemo(() => {
13845
+ const m = /* @__PURE__ */ new Map();
13846
+ for (const n of nodes) m.set(n.id, n);
13847
+ return m;
13848
+ }, [nodes]);
13849
+ const nodeIndexById = React85.useMemo(() => {
13850
+ const m = /* @__PURE__ */ new Map();
13851
+ nodes.forEach((n, i) => m.set(n.id, i));
13852
+ return m;
13853
+ }, [nodes]);
13854
+ const derivedShapes = React85.useMemo(() => {
13855
+ const out = [];
13856
+ const positions = computePositions(nodes, edges, layout, root, width, height);
13857
+ const edgeGeoms = [];
13858
+ for (const e of edges) {
13859
+ const a = nodeById.get(e.from);
13860
+ const b = nodeById.get(e.to);
13861
+ const posA = positions.get(e.from);
13862
+ const posB = positions.get(e.to);
13863
+ if (!a || !b || !posA || !posB) continue;
13864
+ const rA = a.radius ?? DEFAULT_NODE_RADIUS;
13865
+ const rB = b.radius ?? DEFAULT_NODE_RADIUS;
13866
+ const dx = posB.x - posA.x;
13867
+ const dy = posB.y - posA.y;
13868
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
13869
+ const ux = dx / dist;
13870
+ const uy = dy / dist;
13871
+ edgeGeoms.push({
13872
+ directed: e.directed ?? false,
13873
+ x1: posA.x + ux * rA,
13874
+ y1: posA.y + uy * rA,
13875
+ x2: posB.x - ux * rB,
13876
+ y2: posB.y - uy * rB,
13877
+ color: e.color ?? EDGE_STATE_COLOR[e.state ?? "default"],
13878
+ label: e.label ?? (e.weight != null ? String(e.weight) : void 0)
13879
+ });
13880
+ }
13881
+ for (const g of edgeGeoms) {
13882
+ out.push({
13883
+ type: g.directed ? "arrow" : "line",
13884
+ x1: g.x1,
13885
+ y1: g.y1,
13886
+ x2: g.x2,
13887
+ y2: g.y2,
13888
+ color: g.color,
13889
+ lineWidth: 2
13890
+ });
13891
+ }
13892
+ for (const g of edgeGeoms) {
13893
+ if (g.label === void 0) continue;
13894
+ const midX = (g.x1 + g.x2) / 2;
13895
+ const midY = (g.y1 + g.y2) / 2;
13896
+ const dx = g.x2 - g.x1;
13897
+ const dy = g.y2 - g.y1;
13898
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
13899
+ const perpX = -(dy / dist);
13900
+ const perpY = dx / dist;
13901
+ out.push({
13902
+ type: "text",
13903
+ x: midX + perpX * 10,
13904
+ y: midY + perpY * 10,
13905
+ text: g.label,
13906
+ fontSize: 11,
13907
+ align: "center",
13908
+ color: "#374151"
13909
+ });
13910
+ }
13911
+ const nodeGeoms = [];
13912
+ for (const n of nodes) {
13913
+ const pos = positions.get(n.id);
13914
+ if (!pos) continue;
13915
+ nodeGeoms.push({
13916
+ id: n.id,
13917
+ x: pos.x,
13918
+ y: pos.y,
13919
+ radius: n.radius ?? DEFAULT_NODE_RADIUS,
13920
+ color: n.color ?? NODE_STATE_COLOR[n.state ?? "unvisited"],
13921
+ label: n.label,
13922
+ badge: n.badge
13923
+ });
13924
+ }
13925
+ for (const g of nodeGeoms) {
13926
+ out.push({ type: "circle", id: g.id, x: g.x, y: g.y, radius: g.radius, color: g.color, fill: `${g.color}33` });
13927
+ }
13928
+ const badgeGeoms = [];
13929
+ for (const g of nodeGeoms) {
13930
+ if (!g.badge) continue;
13931
+ const w = Math.min(42, Math.max(18, g.badge.text.length * 6 + 10));
13932
+ badgeGeoms.push({
13933
+ cx: g.x + g.radius * 0.75,
13934
+ cy: g.y - g.radius * 0.75,
13935
+ w,
13936
+ h: 14,
13937
+ // Borderless pill: same color drives both stroke and fill.
13938
+ color: g.badge.color ?? "#1e293b",
13939
+ text: g.badge.text
13940
+ });
13941
+ }
13942
+ for (const b of badgeGeoms) {
13943
+ out.push({ type: "rect", x: b.cx - b.w / 2, y: b.cy - b.h / 2, width: b.w, height: b.h, color: b.color, fill: b.color });
13944
+ }
13945
+ for (const b of badgeGeoms) {
13946
+ out.push({ type: "text", x: b.cx, y: b.cy, text: b.text, fontSize: 9, align: "center", color: "#ffffff" });
13947
+ }
13948
+ for (const g of nodeGeoms) {
13949
+ if (g.label === void 0) continue;
13950
+ out.push({
13951
+ type: "text",
13952
+ x: g.x,
13953
+ y: g.y + g.radius + 14,
13954
+ text: g.label,
13955
+ fontSize: 12,
13956
+ align: "center",
13957
+ color: "#111827"
13958
+ });
13959
+ }
13960
+ out.push(...shapes);
13961
+ return out;
13962
+ }, [nodes, edges, layout, root, width, height, nodeById, shapes]);
13963
+ const handleShapeClick = React85.useCallback(
13964
+ (payload) => {
13965
+ if (payload.type === "circle" && payload.id) {
13966
+ const node = nodeById.get(payload.id);
13967
+ const idx = nodeIndexById.get(payload.id);
13968
+ if (node && idx !== void 0) {
13969
+ onNodeClick?.({ id: node.id, label: node.label, index: idx });
13970
+ }
13971
+ }
13972
+ onShapeClick?.(payload);
13973
+ },
13974
+ [nodeById, nodeIndexById, onNodeClick, onShapeClick]
13975
+ );
13976
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
13977
+ title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
13978
+ /* @__PURE__ */ jsxRuntime.jsx(
13979
+ LearningCanvas,
13980
+ {
13981
+ width,
13982
+ height,
13983
+ backgroundColor,
13984
+ shapes: derivedShapes,
13985
+ interactive,
13986
+ animate,
13987
+ onShapeClick: onShapeClick || onNodeClick ? handleShapeClick : void 0,
13988
+ isLoading,
13989
+ error
13990
+ }
13991
+ )
13992
+ ] }) });
13993
+ };
13994
+ }
13995
+ });
13996
+ var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, PANEL_FAMILY_ORDER, RANGE_COLOR_DEFAULT, RANGE_FILL_OPACITY, BRACKET_TOP_OFFSET, BRACKET_ROW_H, BRACKET_TICK_H, BRACKET_LABEL_OFFSET, SLOT_EMPTY_FILL, SLOT_EMPTY_STROKE, SLOT_FILLED_STROKE, SLOT_HIGHLIGHT_DEFAULT, SLOT_VALUE_TEXT_COLOR, FRAME_ACTIVE_COLOR, FRAME_RETURNING_COLOR, FRAME_DONE_COLOR, FRAME_LABEL_COLOR, FRAME_DETAIL_COLOR, FRAME_TWO_LINE_MIN_H, BUCKET_INDEX_FILL, BUCKET_INDEX_STROKE, BUCKET_INDEX_TEXT, BUCKET_ENTRY_TEXT, BUCKET_ENTRY_DEFAULT, BUCKET_ENTRY_HIGHLIGHT, BUCKET_ENTRY_PROBING, BUCKET_ENTRY_MIN_W, BUCKET_ENTRY_MAX_W, AXIS_LABEL_COLOR, AXIS_LABEL_FONT_SIZE, CORNER_TEXT_COLOR, CORNER_FONT_SIZE, CORNER_MIN_CELL, CORNER_INSET_X, CORNER_INSET_Y, AUX_PRIMARY_RATIO, AUX_LABEL_BAND, AUX_BASELINE_PAD, AlgorithmCanvas;
13500
13997
  var init_AlgorithmCanvas = __esm({
13501
13998
  "components/learning/molecules/AlgorithmCanvas.tsx"() {
13502
13999
  "use client";
@@ -13508,6 +14005,43 @@ var init_AlgorithmCanvas = __esm({
13508
14005
  DEFAULT_POINTER_COLOR = "#dc2626";
13509
14006
  POINTER_BAND = 34;
13510
14007
  TOP_PAD = 26;
14008
+ PANEL_FAMILY_ORDER = ["bars", "slots", "cells", "buckets", "frames"];
14009
+ RANGE_COLOR_DEFAULT = "#3b82f6";
14010
+ RANGE_FILL_OPACITY = 0.15;
14011
+ BRACKET_TOP_OFFSET = 16;
14012
+ BRACKET_ROW_H = 14;
14013
+ BRACKET_TICK_H = 6;
14014
+ BRACKET_LABEL_OFFSET = 6;
14015
+ SLOT_EMPTY_FILL = "#f1f5f9";
14016
+ SLOT_EMPTY_STROKE = "#cbd5e1";
14017
+ SLOT_FILLED_STROKE = "#9ca3af";
14018
+ SLOT_HIGHLIGHT_DEFAULT = "#f59e0b";
14019
+ SLOT_VALUE_TEXT_COLOR = "#ffffff";
14020
+ FRAME_ACTIVE_COLOR = "#3b82f6";
14021
+ FRAME_RETURNING_COLOR = "#f59e0b";
14022
+ FRAME_DONE_COLOR = "#94a3b8";
14023
+ FRAME_LABEL_COLOR = "#ffffff";
14024
+ FRAME_DETAIL_COLOR = "#e2e8f0";
14025
+ FRAME_TWO_LINE_MIN_H = 22;
14026
+ BUCKET_INDEX_FILL = "#e2e8f0";
14027
+ BUCKET_INDEX_STROKE = "#9ca3af";
14028
+ BUCKET_INDEX_TEXT = "#374151";
14029
+ BUCKET_ENTRY_TEXT = "#ffffff";
14030
+ BUCKET_ENTRY_DEFAULT = "#3b82f6";
14031
+ BUCKET_ENTRY_HIGHLIGHT = "#f59e0b";
14032
+ BUCKET_ENTRY_PROBING = "#38bdf8";
14033
+ BUCKET_ENTRY_MIN_W = 24;
14034
+ BUCKET_ENTRY_MAX_W = 64;
14035
+ AXIS_LABEL_COLOR = "#6b7280";
14036
+ AXIS_LABEL_FONT_SIZE = 10;
14037
+ CORNER_TEXT_COLOR = "#111827";
14038
+ CORNER_FONT_SIZE = 7;
14039
+ CORNER_MIN_CELL = 28;
14040
+ CORNER_INSET_X = 3;
14041
+ CORNER_INSET_Y = 6;
14042
+ AUX_PRIMARY_RATIO = 0.6;
14043
+ AUX_LABEL_BAND = 18;
14044
+ AUX_BASELINE_PAD = 8;
13511
14045
  AlgorithmCanvas = ({
13512
14046
  className,
13513
14047
  width = 600,
@@ -13517,6 +14051,14 @@ var init_AlgorithmCanvas = __esm({
13517
14051
  bars = [],
13518
14052
  cells = [],
13519
14053
  pointers = [],
14054
+ ranges = [],
14055
+ slots = [],
14056
+ slotOrientation = "horizontal",
14057
+ frames = [],
14058
+ buckets = [],
14059
+ auxBars = [],
14060
+ rowLabels = [],
14061
+ colLabels = [],
13520
14062
  shapes = [],
13521
14063
  interactive = false,
13522
14064
  animate = false,
@@ -13526,12 +14068,35 @@ var init_AlgorithmCanvas = __esm({
13526
14068
  }) => {
13527
14069
  const derivedShapes = React85.useMemo(() => {
13528
14070
  const out = [];
14071
+ const presence = {
14072
+ bars: bars.length > 0,
14073
+ slots: slots.length > 0,
14074
+ cells: cells.length > 0,
14075
+ buckets: buckets.length > 0,
14076
+ frames: frames.length > 0
14077
+ };
14078
+ const panelCount = PANEL_FAMILY_ORDER.filter((f3) => presence[f3]).length;
14079
+ const panelHeight = height / Math.max(1, panelCount);
14080
+ const panelY = { bars: 0, slots: 0, cells: 0, buckets: 0, frames: 0 };
14081
+ let compactIndex = 0;
14082
+ PANEL_FAMILY_ORDER.forEach((f3) => {
14083
+ if (presence[f3]) {
14084
+ panelY[f3] = compactIndex * panelHeight;
14085
+ compactIndex += 1;
14086
+ }
14087
+ });
13529
14088
  if (bars.length > 0) {
14089
+ const panelYBars = panelY.bars;
13530
14090
  const slot = width / bars.length;
13531
14091
  const barW = slot * 0.8;
13532
14092
  const gap = slot * 0.1;
13533
- const baseline = height - POINTER_BAND;
13534
- const usableH = baseline - TOP_PAD;
14093
+ const bracketRanges = ranges.filter((r) => r.kind === "bracket");
14094
+ const bracketCount = bracketRanges.length;
14095
+ const bracketHeadroom = bracketCount > 0 ? BRACKET_TOP_OFFSET + bracketCount * BRACKET_ROW_H : 0;
14096
+ const hasAux = auxBars.length > 0;
14097
+ const primaryH = hasAux ? panelHeight * AUX_PRIMARY_RATIO : panelHeight;
14098
+ const baseline = panelYBars + primaryH - POINTER_BAND;
14099
+ const usableH = baseline - (panelYBars + TOP_PAD + bracketHeadroom);
13535
14100
  const maxV = Math.max(1, ...bars.map((b) => Number.isFinite(b.value) ? b.value : 0));
13536
14101
  bars.forEach((bar, i) => {
13537
14102
  const v = Number.isFinite(bar.value) ? bar.value : 0;
@@ -13561,6 +14126,89 @@ var init_AlgorithmCanvas = __esm({
13561
14126
  });
13562
14127
  }
13563
14128
  });
14129
+ ranges.forEach((r) => {
14130
+ const kind = r.kind ?? "fill";
14131
+ if (kind !== "fill") return;
14132
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14133
+ out.push({
14134
+ type: "rect",
14135
+ x: r.from * slot,
14136
+ y: panelYBars,
14137
+ width: (r.to - r.from + 1) * slot,
14138
+ height: primaryH,
14139
+ color,
14140
+ fill: color,
14141
+ opacity: RANGE_FILL_OPACITY
14142
+ });
14143
+ if (r.label) {
14144
+ out.push({
14145
+ type: "text",
14146
+ x: r.from * slot + 4,
14147
+ // Sits below the bracket block (if any) so fill and bracket labels never collide.
14148
+ y: panelYBars + 10 + bracketHeadroom,
14149
+ text: r.label,
14150
+ color,
14151
+ fontSize: 10,
14152
+ align: "left"
14153
+ });
14154
+ }
14155
+ });
14156
+ bracketRanges.forEach((r, i) => {
14157
+ const bracketY = panelYBars + BRACKET_TOP_OFFSET + i * BRACKET_ROW_H;
14158
+ const x1 = r.from * slot + slot * 0.1;
14159
+ const x2 = (r.to + 1) * slot - slot * 0.1;
14160
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14161
+ out.push({ type: "line", x1, y1: bracketY, x2, y2: bracketY, color, lineWidth: 2 });
14162
+ out.push({ type: "line", x1, y1: bracketY, x2: x1, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14163
+ out.push({ type: "line", x1: x2, y1: bracketY, x2, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14164
+ if (r.label) {
14165
+ out.push({
14166
+ type: "text",
14167
+ x: (x1 + x2) / 2,
14168
+ y: bracketY - BRACKET_LABEL_OFFSET,
14169
+ text: r.label,
14170
+ color,
14171
+ fontSize: 10,
14172
+ align: "center"
14173
+ });
14174
+ }
14175
+ });
14176
+ if (hasAux) {
14177
+ const auxH = panelHeight - primaryH;
14178
+ const slot2 = width / auxBars.length;
14179
+ const auxBaseline = panelYBars + primaryH + auxH - AUX_BASELINE_PAD;
14180
+ const auxUsableH = auxBaseline - (panelYBars + primaryH + AUX_LABEL_BAND);
14181
+ const maxAuxV = Math.max(1, ...auxBars.map((b) => Number.isFinite(b.value) ? b.value : 0));
14182
+ auxBars.forEach((bar, i) => {
14183
+ const v = Number.isFinite(bar.value) ? bar.value : 0;
14184
+ const bh = Math.max(0, v / maxAuxV * auxUsableH);
14185
+ const x = i * slot2 + slot2 * 0.1;
14186
+ const w = slot2 * 0.8;
14187
+ const color = bar.color ?? DEFAULT_BAR_COLOR;
14188
+ out.push({
14189
+ type: "rect",
14190
+ id: `auxbar-${i}`,
14191
+ x,
14192
+ y: auxBaseline - bh,
14193
+ width: w,
14194
+ height: bh,
14195
+ color,
14196
+ fill: color
14197
+ });
14198
+ const label = bar.label ?? (auxBars.length <= 24 ? String(v) : void 0);
14199
+ if (label) {
14200
+ out.push({
14201
+ type: "text",
14202
+ x: x + w / 2,
14203
+ y: auxBaseline - bh - 8,
14204
+ text: label,
14205
+ color: "#374151",
14206
+ fontSize: 11,
14207
+ align: "center"
14208
+ });
14209
+ }
14210
+ });
14211
+ }
13564
14212
  pointers.forEach((p) => {
13565
14213
  if (p.index < 0 || p.index >= bars.length) return;
13566
14214
  const cx = p.index * slot + slot / 2;
@@ -13568,7 +14216,7 @@ var init_AlgorithmCanvas = __esm({
13568
14216
  out.push({
13569
14217
  type: "arrow",
13570
14218
  x1: cx,
13571
- y1: height - 6,
14219
+ y1: panelYBars + primaryH - 18,
13572
14220
  x2: cx,
13573
14221
  y2: baseline + 4,
13574
14222
  color,
@@ -13578,7 +14226,7 @@ var init_AlgorithmCanvas = __esm({
13578
14226
  out.push({
13579
14227
  type: "text",
13580
14228
  x: cx,
13581
- y: height - 22,
14229
+ y: panelYBars + primaryH - 8,
13582
14230
  text: p.label,
13583
14231
  color,
13584
14232
  fontSize: 11,
@@ -13587,14 +14235,111 @@ var init_AlgorithmCanvas = __esm({
13587
14235
  }
13588
14236
  });
13589
14237
  }
14238
+ if (slots.length > 0) {
14239
+ const panelYSlots = panelY.slots;
14240
+ const n = slots.length;
14241
+ const vertical = slotOrientation === "vertical";
14242
+ const vBoxH = panelHeight / n;
14243
+ const vBoxW = Math.min(width * 0.5, 120);
14244
+ const vBoxX = (width - vBoxW) / 2;
14245
+ const hCellW = width / n;
14246
+ const hBoxW = hCellW * 0.82;
14247
+ const hBoxH = Math.min(panelHeight * 0.6, 48);
14248
+ const hBoxY = panelYSlots + (panelHeight - hBoxH) / 2;
14249
+ const slotBox = (i) => vertical ? { x: vBoxX, y: panelYSlots + panelHeight - (i + 1) * vBoxH, width: vBoxW, height: vBoxH } : { x: i * hCellW + (hCellW - hBoxW) / 2, y: hBoxY, width: hBoxW, height: hBoxH };
14250
+ slots.forEach((s, i) => {
14251
+ const box = slotBox(i);
14252
+ const state = s.state ?? "filled";
14253
+ const fill = state === "empty" ? SLOT_EMPTY_FILL : state === "highlight" ? s.color ?? SLOT_HIGHLIGHT_DEFAULT : s.color ?? DEFAULT_BAR_COLOR;
14254
+ const stroke = state === "empty" ? SLOT_EMPTY_STROKE : SLOT_FILLED_STROKE;
14255
+ out.push({
14256
+ type: "rect",
14257
+ id: `slot-${i}`,
14258
+ x: box.x,
14259
+ y: box.y,
14260
+ width: box.width,
14261
+ height: box.height,
14262
+ color: stroke,
14263
+ fill
14264
+ });
14265
+ if (s.value != null && state !== "empty") {
14266
+ out.push({
14267
+ type: "text",
14268
+ x: box.x + box.width / 2,
14269
+ y: box.y + box.height / 2,
14270
+ text: String(s.value),
14271
+ color: SLOT_VALUE_TEXT_COLOR,
14272
+ fontSize: 12,
14273
+ align: "center"
14274
+ });
14275
+ }
14276
+ });
14277
+ if (bars.length === 0) {
14278
+ pointers.forEach((p) => {
14279
+ if (p.index < 0 || p.index >= slots.length) return;
14280
+ const box = slotBox(p.index);
14281
+ const color = p.color ?? DEFAULT_POINTER_COLOR;
14282
+ if (vertical) {
14283
+ const cy = box.y + box.height / 2;
14284
+ out.push({
14285
+ type: "arrow",
14286
+ x1: box.x + box.width + 34,
14287
+ y1: cy,
14288
+ x2: box.x + box.width + 4,
14289
+ y2: cy,
14290
+ color,
14291
+ lineWidth: 2
14292
+ });
14293
+ if (p.label) {
14294
+ out.push({
14295
+ type: "text",
14296
+ x: box.x + box.width + 38,
14297
+ y: cy,
14298
+ text: p.label,
14299
+ color,
14300
+ fontSize: 11,
14301
+ align: "left"
14302
+ });
14303
+ }
14304
+ } else {
14305
+ const cx = box.x + box.width / 2;
14306
+ out.push({
14307
+ type: "arrow",
14308
+ x1: cx,
14309
+ y1: panelYSlots + panelHeight - 18,
14310
+ x2: cx,
14311
+ y2: box.y + box.height + 4,
14312
+ color,
14313
+ lineWidth: 2
14314
+ });
14315
+ if (p.label) {
14316
+ out.push({
14317
+ type: "text",
14318
+ x: cx,
14319
+ y: panelYSlots + panelHeight - 8,
14320
+ text: p.label,
14321
+ color,
14322
+ fontSize: 11,
14323
+ align: "center"
14324
+ });
14325
+ }
14326
+ }
14327
+ });
14328
+ }
14329
+ }
13590
14330
  if (cells.length > 0) {
14331
+ const panelYCells = panelY.cells;
13591
14332
  const maxCol = Math.max(0, ...cells.map((c) => c.col)) + 1;
13592
14333
  const maxRow = Math.max(0, ...cells.map((c) => c.row)) + 1;
13593
- const cw = width / maxCol;
13594
- const ch = height / maxRow;
14334
+ const colLabelH = colLabels.length > 0 ? 16 : 0;
14335
+ const rowLabelW = rowLabels.length > 0 ? 20 : 0;
14336
+ const gridX0 = rowLabelW;
14337
+ const gridY0 = panelYCells + colLabelH;
14338
+ const cw = (width - rowLabelW) / maxCol;
14339
+ const ch = (panelHeight - colLabelH) / maxRow;
13595
14340
  cells.forEach((c, i) => {
13596
- const x = c.col * cw;
13597
- const y = c.row * ch;
14341
+ const x = gridX0 + c.col * cw;
14342
+ const y = gridY0 + c.row * ch;
13598
14343
  const color = c.color ?? DEFAULT_CELL_COLOR;
13599
14344
  out.push({
13600
14345
  type: "rect",
@@ -13618,11 +14363,207 @@ var init_AlgorithmCanvas = __esm({
13618
14363
  align: "center"
13619
14364
  });
13620
14365
  }
14366
+ if (c.corner && cw >= CORNER_MIN_CELL && ch >= CORNER_MIN_CELL) {
14367
+ const { tl, tr, bl, br } = c.corner;
14368
+ if (tl) {
14369
+ out.push({
14370
+ type: "text",
14371
+ x: x + CORNER_INSET_X,
14372
+ y: y + CORNER_INSET_Y,
14373
+ text: tl,
14374
+ color: CORNER_TEXT_COLOR,
14375
+ fontSize: CORNER_FONT_SIZE,
14376
+ align: "left"
14377
+ });
14378
+ }
14379
+ if (tr) {
14380
+ out.push({
14381
+ type: "text",
14382
+ x: x + cw - CORNER_INSET_X,
14383
+ y: y + CORNER_INSET_Y,
14384
+ text: tr,
14385
+ color: CORNER_TEXT_COLOR,
14386
+ fontSize: CORNER_FONT_SIZE,
14387
+ align: "right"
14388
+ });
14389
+ }
14390
+ if (bl) {
14391
+ out.push({
14392
+ type: "text",
14393
+ x: x + CORNER_INSET_X,
14394
+ y: y + ch - CORNER_INSET_Y,
14395
+ text: bl,
14396
+ color: CORNER_TEXT_COLOR,
14397
+ fontSize: CORNER_FONT_SIZE,
14398
+ align: "left"
14399
+ });
14400
+ }
14401
+ if (br) {
14402
+ out.push({
14403
+ type: "text",
14404
+ x: x + cw - CORNER_INSET_X,
14405
+ y: y + ch - CORNER_INSET_Y,
14406
+ text: br,
14407
+ color: CORNER_TEXT_COLOR,
14408
+ fontSize: CORNER_FONT_SIZE,
14409
+ align: "right"
14410
+ });
14411
+ }
14412
+ }
14413
+ });
14414
+ colLabels.forEach((l) => {
14415
+ out.push({
14416
+ type: "text",
14417
+ x: gridX0 + l.index * cw + cw / 2,
14418
+ y: panelYCells + colLabelH / 2,
14419
+ text: l.text,
14420
+ color: l.color ?? AXIS_LABEL_COLOR,
14421
+ fontSize: AXIS_LABEL_FONT_SIZE,
14422
+ align: "center"
14423
+ });
14424
+ });
14425
+ rowLabels.forEach((l) => {
14426
+ out.push({
14427
+ type: "text",
14428
+ x: rowLabelW - 6,
14429
+ y: gridY0 + l.index * ch + ch / 2,
14430
+ text: l.text,
14431
+ color: l.color ?? AXIS_LABEL_COLOR,
14432
+ fontSize: AXIS_LABEL_FONT_SIZE,
14433
+ align: "right"
14434
+ });
14435
+ });
14436
+ }
14437
+ if (buckets.length > 0) {
14438
+ const panelYBuckets = panelY.buckets;
14439
+ const bucketCount = Math.max(0, ...buckets.map((b) => b.index)) + 1;
14440
+ const rowH = panelHeight / bucketCount;
14441
+ const indexColW = Math.min(width * 0.12, 40);
14442
+ const maxChainLen = Math.max(1, ...buckets.map((b) => b.entries.length));
14443
+ const entryW = Math.min(BUCKET_ENTRY_MAX_W, Math.max(BUCKET_ENTRY_MIN_W, (width - indexColW - 8) / maxChainLen));
14444
+ const maxVisible = Math.floor((width - indexColW - 4) / entryW);
14445
+ buckets.forEach((b) => {
14446
+ const rowY = panelYBuckets + b.index * rowH;
14447
+ out.push({
14448
+ type: "rect",
14449
+ id: `bucket-index-${b.index}`,
14450
+ x: 2,
14451
+ y: rowY + 2,
14452
+ width: indexColW - 4,
14453
+ height: rowH - 4,
14454
+ color: BUCKET_INDEX_STROKE,
14455
+ fill: BUCKET_INDEX_FILL
14456
+ });
14457
+ out.push({
14458
+ type: "text",
14459
+ x: 2 + (indexColW - 4) / 2,
14460
+ y: rowY + rowH / 2,
14461
+ text: String(b.index),
14462
+ color: BUCKET_INDEX_TEXT,
14463
+ fontSize: 10,
14464
+ align: "center"
14465
+ });
14466
+ const overflow = b.entries.length > maxVisible;
14467
+ const visibleCount = overflow ? Math.max(0, maxVisible - 1) : b.entries.length;
14468
+ for (let j = 0; j < visibleCount; j++) {
14469
+ const entry = b.entries[j];
14470
+ const ex = indexColW + 4 + j * entryW;
14471
+ const state = entry.state ?? "default";
14472
+ const fill = state === "highlight" ? entry.color ?? BUCKET_ENTRY_HIGHLIGHT : state === "probing" ? entry.color ?? BUCKET_ENTRY_PROBING : entry.color ?? BUCKET_ENTRY_DEFAULT;
14473
+ out.push({
14474
+ type: "rect",
14475
+ id: `bucket-${b.index}-${j}`,
14476
+ x: ex,
14477
+ y: rowY + 2,
14478
+ width: entryW - 2,
14479
+ height: rowH - 4,
14480
+ color: fill,
14481
+ fill
14482
+ });
14483
+ if (entryW >= 20 && rowH >= 16) {
14484
+ out.push({
14485
+ type: "text",
14486
+ x: ex + (entryW - 2) / 2,
14487
+ y: rowY + rowH / 2,
14488
+ text: entry.label,
14489
+ color: BUCKET_ENTRY_TEXT,
14490
+ fontSize: 10,
14491
+ align: "center"
14492
+ });
14493
+ }
14494
+ }
14495
+ if (overflow) {
14496
+ const ex = indexColW + 4 + visibleCount * entryW;
14497
+ out.push({
14498
+ type: "rect",
14499
+ id: `bucket-${b.index}-overflow`,
14500
+ x: ex,
14501
+ y: rowY + 2,
14502
+ width: entryW - 2,
14503
+ height: rowH - 4,
14504
+ color: BUCKET_ENTRY_DEFAULT,
14505
+ fill: BUCKET_ENTRY_DEFAULT
14506
+ });
14507
+ out.push({
14508
+ type: "text",
14509
+ x: ex + (entryW - 2) / 2,
14510
+ y: rowY + rowH / 2,
14511
+ text: `+${b.entries.length - visibleCount}`,
14512
+ color: BUCKET_ENTRY_TEXT,
14513
+ fontSize: 10,
14514
+ align: "center"
14515
+ });
14516
+ }
14517
+ });
14518
+ }
14519
+ if (frames.length > 0) {
14520
+ const panelYFrames = panelY.frames;
14521
+ const n = frames.length;
14522
+ const frameH = panelHeight / n;
14523
+ const x = 8;
14524
+ const w = width - 16;
14525
+ frames.forEach((f3, i) => {
14526
+ const y = panelYFrames + panelHeight - (i + 1) * frameH;
14527
+ const state = f3.state ?? "active";
14528
+ const fill = state === "returning" ? f3.color ?? FRAME_RETURNING_COLOR : state === "done" ? f3.color ?? FRAME_DONE_COLOR : f3.color ?? FRAME_ACTIVE_COLOR;
14529
+ out.push({ type: "rect", id: `frame-${i}`, x, y, width: w, height: frameH, color: fill, fill });
14530
+ if (frameH >= FRAME_TWO_LINE_MIN_H) {
14531
+ out.push({
14532
+ type: "text",
14533
+ x: 16,
14534
+ y: y + frameH * 0.35,
14535
+ text: f3.label,
14536
+ color: FRAME_LABEL_COLOR,
14537
+ fontSize: 10,
14538
+ align: "left"
14539
+ });
14540
+ if (f3.detail) {
14541
+ out.push({
14542
+ type: "text",
14543
+ x: 16,
14544
+ y: y + frameH * 0.7,
14545
+ text: f3.detail,
14546
+ color: FRAME_DETAIL_COLOR,
14547
+ fontSize: 10,
14548
+ align: "left"
14549
+ });
14550
+ }
14551
+ } else {
14552
+ out.push({
14553
+ type: "text",
14554
+ x: 16,
14555
+ y: y + frameH / 2,
14556
+ text: f3.label,
14557
+ color: FRAME_LABEL_COLOR,
14558
+ fontSize: 10,
14559
+ align: "left"
14560
+ });
14561
+ }
13621
14562
  });
13622
14563
  }
13623
14564
  out.push(...shapes);
13624
14565
  return out;
13625
- }, [bars, cells, pointers, shapes, width, height]);
14566
+ }, [bars, cells, pointers, ranges, slots, slotOrientation, frames, buckets, auxBars, rowLabels, colLabels, shapes, width, height]);
13626
14567
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
13627
14568
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
13628
14569
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -14636,6 +15577,12 @@ function LearningScene3D({
14636
15577
  const unitId = event.payload?.unitId;
14637
15578
  if (typeof unitId === "string") onItemClickRef.current?.(unitId);
14638
15579
  });
15580
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && post?.bloom) {
15581
+ const unknownKeys = Object.keys(post.bloom).filter((k) => !KNOWN_BLOOM_KEYS.has(k));
15582
+ if (unknownKeys.length > 0) {
15583
+ sceneLog.debug("post.bloom has unrecognized keys \u2014 only intensity/threshold/smoothing are read", { unknownKeys });
15584
+ }
15585
+ }
14639
15586
  const props3d = {
14640
15587
  drawables,
14641
15588
  isLoading,
@@ -14701,7 +15648,7 @@ function cylinderBetween(from, to, radius, color) {
14701
15648
  material: { color }
14702
15649
  };
14703
15650
  }
14704
- function arrowBetween(from, to, color, shaftRadius = 0.08) {
15651
+ function arrowBetween(from, to, color, shaftRadius = 0.08, id) {
14705
15652
  const len = segmentLength(from, to);
14706
15653
  if (len < 1e-6) return null;
14707
15654
  const tipLen = Math.min(shaftRadius * 8, len * 0.35);
@@ -14729,6 +15676,7 @@ function arrowBetween(from, to, color, shaftRadius = 0.08) {
14729
15676
  };
14730
15677
  return {
14731
15678
  type: "draw-group",
15679
+ ...id !== void 0 ? { id } : {},
14732
15680
  position: { x: from[0], y: from[1], z: from[2] },
14733
15681
  items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
14734
15682
  };
@@ -14755,20 +15703,228 @@ function get3DClickPayload(onShapeClick, idToIndex) {
14755
15703
  if (!onShapeClick) return void 0;
14756
15704
  return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
14757
15705
  }
14758
- var Canvas3DHost2;
15706
+ function polylineTube(points, radius, color, opts) {
15707
+ const maxSegments = opts?.maxSegments ?? 128;
15708
+ let pts = points;
15709
+ if (pts.length - 1 > maxSegments) {
15710
+ const step = (pts.length - 1) / maxSegments;
15711
+ const kept = [pts[0]];
15712
+ for (let s = 1; s < maxSegments; s++) kept.push(pts[Math.round(s * step)]);
15713
+ kept.push(pts[pts.length - 1]);
15714
+ pts = kept;
15715
+ }
15716
+ const out = [];
15717
+ for (let i = 0; i < pts.length - 1; i++) {
15718
+ const seg = cylinderBetween(pts[i], pts[i + 1], radius, color);
15719
+ if (seg) out.push(opts?.opacity !== void 0 ? { ...seg, opacity: opts.opacity } : seg);
15720
+ }
15721
+ return out;
15722
+ }
15723
+ function heightFieldMesh(spec) {
15724
+ const { nx, ny, heights, spacing = 1, x = 0, y = 0 } = spec;
15725
+ const flatShading = spec.flatShading ?? true;
15726
+ const vertices = [];
15727
+ for (let iy = 0; iy < ny; iy++) {
15728
+ for (let ix = 0; ix < nx; ix++) {
15729
+ vertices.push([
15730
+ x + (ix - (nx - 1) / 2) * spacing,
15731
+ y + (iy - (ny - 1) / 2) * spacing,
15732
+ heights[iy * nx + ix] ?? 0
15733
+ ]);
15734
+ }
15735
+ }
15736
+ const bands = [...spec.bands ?? []].sort((a, b) => (a.min ?? -Infinity) - (b.min ?? -Infinity));
15737
+ const facesByBand = /* @__PURE__ */ new Map();
15738
+ for (let iy = 0; iy < ny - 1; iy++) {
15739
+ for (let ix = 0; ix < nx - 1; ix++) {
15740
+ const v00 = iy * nx + ix;
15741
+ const v10 = iy * nx + ix + 1;
15742
+ const v01 = (iy + 1) * nx + ix;
15743
+ const v11 = (iy + 1) * nx + ix + 1;
15744
+ for (const face of [[v00, v10, v01], [v10, v11, v01]]) {
15745
+ const centroid = (vertices[face[0]][2] + vertices[face[1]][2] + vertices[face[2]][2]) / 3;
15746
+ let band = null;
15747
+ for (const b of bands) {
15748
+ if ((b.min ?? -Infinity) <= centroid) band = b;
15749
+ }
15750
+ const key = bands.length > 0 ? band : null;
15751
+ const list = facesByBand.get(key) ?? [];
15752
+ list.push(face);
15753
+ facesByBand.set(key, list);
15754
+ }
15755
+ }
15756
+ }
15757
+ const out = [];
15758
+ for (const [band, faces] of facesByBand) {
15759
+ if (faces.length === 0) continue;
15760
+ out.push({
15761
+ type: "draw-mesh",
15762
+ shape: "polyhedron",
15763
+ position: { x: 0, y: 0, z: 0 },
15764
+ vertices,
15765
+ faces,
15766
+ pivot: "center",
15767
+ material: { color: band?.color ?? spec.color ?? "#64748b", flatShading, side: "double" },
15768
+ ...spec.opacity !== void 0 ? { opacity: spec.opacity } : {}
15769
+ });
15770
+ }
15771
+ return out;
15772
+ }
15773
+ function arrowField(vectors, opts) {
15774
+ const scale = opts?.scale ?? 1;
15775
+ const out = [];
15776
+ for (const v of vectors) {
15777
+ const to = [
15778
+ v.from[0] + v.delta[0] * scale,
15779
+ v.from[1] + v.delta[1] * scale,
15780
+ v.from[2] + v.delta[2] * scale
15781
+ ];
15782
+ const arrow = arrowBetween(v.from, to, v.color ?? "#dc2626", v.width, v.id);
15783
+ if (arrow) out.push(arrow);
15784
+ if (v.label) out.push(billboardLabel(v.label, to[0], to[1], to[2], { color: opts?.labelColor }));
15785
+ }
15786
+ return out;
15787
+ }
15788
+ function helixDrawables(spec, opts) {
15789
+ const count = spec.count ?? spec.rungs?.length ?? 0;
15790
+ const rungs = Array.from({ length: count }, (_, i) => spec.rungs?.[i] ?? {});
15791
+ const radius = spec.radius ?? 1;
15792
+ const rise = spec.rise ?? 0.34;
15793
+ const twistRad = (spec.twistDeg ?? 36) * (Math.PI / 180);
15794
+ const strandAColor = spec.strandAColor ?? "#38bdf8";
15795
+ const strandBColor = spec.strandBColor ?? "#fb923c";
15796
+ const backboneRadius = spec.backboneRadius ?? 0.16;
15797
+ const rungRadius = spec.rungRadius ?? 0.12;
15798
+ const cx = spec.x ?? 0;
15799
+ const cy = spec.y ?? 0;
15800
+ const cz = spec.z ?? 0;
15801
+ const unwoundCount = spec.unwoundCount ?? 0;
15802
+ const unwindSpread = spec.unwindSpread ?? 1.8;
15803
+ const strandA = [];
15804
+ const strandB = [];
15805
+ for (let i = 0; i < count; i++) {
15806
+ const yi = cy + (i - (count - 1) / 2) * rise;
15807
+ const theta = i * twistRad;
15808
+ const s = i < unwoundCount ? unwindSpread : 1;
15809
+ strandA.push([cx + s * radius * Math.cos(theta), yi, cz + s * radius * Math.sin(theta)]);
15810
+ strandB.push([cx + s * radius * Math.cos(theta + Math.PI), yi, cz + s * radius * Math.sin(theta + Math.PI)]);
15811
+ }
15812
+ const out = [];
15813
+ for (let i = 0; i < count; i++) {
15814
+ out.push(meshSphere(`hx-a-${i}`, strandA[i][0], strandA[i][1], strandA[i][2], backboneRadius, strandAColor));
15815
+ out.push(meshSphere(`hx-b-${i}`, strandB[i][0], strandB[i][1], strandB[i][2], backboneRadius, strandBColor));
15816
+ if (i > 0) {
15817
+ const segA = cylinderBetween(strandA[i - 1], strandA[i], backboneRadius, strandAColor);
15818
+ if (segA) out.push(segA);
15819
+ const segB = cylinderBetween(strandB[i - 1], strandB[i], backboneRadius, strandBColor);
15820
+ if (segB) out.push(segB);
15821
+ }
15822
+ const rung = rungs[i];
15823
+ const rungColor = rung.color ?? "#94a3b8";
15824
+ const rod = cylinderBetween(strandA[i], strandB[i], rungRadius, rungColor);
15825
+ if (rod) out.push(rod);
15826
+ const mid = [
15827
+ (strandA[i][0] + strandB[i][0]) / 2,
15828
+ (strandA[i][1] + strandB[i][1]) / 2,
15829
+ (strandA[i][2] + strandB[i][2]) / 2
15830
+ ];
15831
+ const markerRadius = rung.radius ?? rungRadius;
15832
+ out.push(meshSphere(rung.id, mid[0], mid[1], mid[2], markerRadius, rungColor));
15833
+ if (rung.label) out.push(billboardLabel(rung.label, mid[0], mid[1], mid[2] + markerRadius, { color: opts?.labelColor }));
15834
+ }
15835
+ return out;
15836
+ }
15837
+ function latticeDrawables(spec, opts) {
15838
+ const nx = spec.nx ?? 2;
15839
+ const ny = spec.ny ?? 2;
15840
+ const nz = spec.nz ?? 2;
15841
+ const latticeConstant = spec.latticeConstant ?? 2;
15842
+ const bondRadius = spec.bondRadius ?? 0.06;
15843
+ const highlightCell = spec.highlightCell ?? false;
15844
+ const dimColor = spec.dimColor ?? "#475569";
15845
+ const showLabels = spec.showLabels ?? false;
15846
+ const selectedColor = spec.selectedColor ?? "#f59e0b";
15847
+ const posByKey = /* @__PURE__ */ new Map();
15848
+ const inCellByKey = /* @__PURE__ */ new Map();
15849
+ const out = [];
15850
+ for (const site of spec.basis) {
15851
+ const snx = site.xEdge ? nx + 1 : nx;
15852
+ const sny = site.yEdge ? ny + 1 : ny;
15853
+ const snz = site.zEdge ? nz + 1 : nz;
15854
+ for (let i = 0; i < snx; i++) {
15855
+ for (let j = 0; j < sny; j++) {
15856
+ for (let k = 0; k < snz; k++) {
15857
+ const key = `${site.key}-${i}-${j}-${k}`;
15858
+ const inCell = i + site.dx <= 1 && j + site.dy <= 1 && k + site.dz <= 1;
15859
+ const pos = [
15860
+ (i + site.dx) * latticeConstant - nx * latticeConstant / 2,
15861
+ (j + site.dy) * latticeConstant - ny * latticeConstant / 2,
15862
+ (k + site.dz) * latticeConstant - nz * latticeConstant / 2
15863
+ ];
15864
+ posByKey.set(key, pos);
15865
+ inCellByKey.set(key, inCell);
15866
+ const isSelected = spec.selectedId === `lat-${key}`;
15867
+ const color = isSelected ? selectedColor : highlightCell && !inCell ? dimColor : site.color ?? "#2563eb";
15868
+ const radius = (site.radius ?? 0.3) * (isSelected ? 1.4 : 1);
15869
+ out.push(meshSphere(`lat-${key}`, pos[0], pos[1], pos[2], radius, color));
15870
+ if (showLabels && site.element) {
15871
+ out.push(billboardLabel(site.element, pos[0], pos[1], pos[2] + radius, { color: opts?.labelColor }));
15872
+ }
15873
+ }
15874
+ }
15875
+ }
15876
+ }
15877
+ const basisByKey = new Map(spec.basis.map((s) => [s.key, s]));
15878
+ for (const bond of spec.bonds ?? []) {
15879
+ const fromSite = basisByKey.get(bond.from);
15880
+ const toSite = basisByKey.get(bond.to);
15881
+ if (!fromSite || !toSite) continue;
15882
+ const fnx = fromSite.xEdge ? nx + 1 : nx;
15883
+ const fny = fromSite.yEdge ? ny + 1 : ny;
15884
+ const fnz = fromSite.zEdge ? nz + 1 : nz;
15885
+ const tnx = toSite.xEdge ? nx + 1 : nx;
15886
+ const tny = toSite.yEdge ? ny + 1 : ny;
15887
+ const tnz = toSite.zEdge ? nz + 1 : nz;
15888
+ const bdx = bond.dx ?? 0;
15889
+ const bdy = bond.dy ?? 0;
15890
+ const bdz = bond.dz ?? 0;
15891
+ for (let i = 0; i < fnx; i++) {
15892
+ for (let j = 0; j < fny; j++) {
15893
+ for (let k = 0; k < fnz; k++) {
15894
+ const ti = i + bdx;
15895
+ const tj = j + bdy;
15896
+ const tk = k + bdz;
15897
+ if (ti < 0 || ti >= tnx || tj < 0 || tj >= tny || tk < 0 || tk >= tnz) continue;
15898
+ const fromKey = `${fromSite.key}-${i}-${j}-${k}`;
15899
+ const toKey = `${toSite.key}-${ti}-${tj}-${tk}`;
15900
+ const fromPos = posByKey.get(fromKey);
15901
+ const toPos = posByKey.get(toKey);
15902
+ if (!fromPos || !toPos) continue;
15903
+ const dimmed = highlightCell && !(inCellByKey.get(fromKey) && inCellByKey.get(toKey));
15904
+ const seg = cylinderBetween(fromPos, toPos, bondRadius, dimmed ? dimColor : bond.color ?? "#6b7280");
15905
+ if (seg) out.push(seg);
15906
+ }
15907
+ }
15908
+ }
15909
+ }
15910
+ return out;
15911
+ }
15912
+ var sceneLog, KNOWN_BLOOM_KEYS, Canvas3DHost2;
14759
15913
  var init_learningScene3D = __esm({
14760
15914
  "components/learning/molecules/learningScene3D.tsx"() {
14761
15915
  "use client";
14762
15916
  init_atoms();
14763
15917
  init_Stack();
14764
15918
  init_useEventBus();
15919
+ sceneLog = logger.createLogger("almadar:ui:learning-scene-3d");
15920
+ KNOWN_BLOOM_KEYS = /* @__PURE__ */ new Set(["intensity", "threshold", "smoothing"]);
14765
15921
  Canvas3DHost2 = React85.lazy(
14766
15922
  () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
14767
15923
  );
14768
15924
  LearningScene3D.displayName = "LearningScene3D";
14769
15925
  }
14770
15926
  });
14771
- var biologyLog, BiologyCanvas;
15927
+ var biologyLog, BIO_BAND_COLORS, BIO_STAGE_FILL, BIO_STAGE_TEXT, BiologyCanvas;
14772
15928
  var init_BiologyCanvas = __esm({
14773
15929
  "components/learning/molecules/BiologyCanvas.tsx"() {
14774
15930
  "use client";
@@ -14777,6 +15933,17 @@ var init_BiologyCanvas = __esm({
14777
15933
  init_LearningCanvas();
14778
15934
  init_learningScene3D();
14779
15935
  biologyLog = logger.createLogger("almadar:ui:biology-canvas");
15936
+ BIO_BAND_COLORS = ["#dcfce7", "#fef9c3", "#fee2e2", "#e0e7ff"];
15937
+ BIO_STAGE_FILL = {
15938
+ pending: "#e2e8f0",
15939
+ active: "#3b82f6",
15940
+ done: "#94a3b8"
15941
+ };
15942
+ BIO_STAGE_TEXT = {
15943
+ pending: "#64748b",
15944
+ active: "#ffffff",
15945
+ done: "#ffffff"
15946
+ };
14780
15947
  BiologyCanvas = ({
14781
15948
  className,
14782
15949
  width = 600,
@@ -14789,7 +15956,15 @@ var init_BiologyCanvas = __esm({
14789
15956
  post,
14790
15957
  nodes = [],
14791
15958
  edges = [],
15959
+ compartments = [],
15960
+ bands = [],
15961
+ stages = [],
15962
+ stageStyle = "timeline",
15963
+ helix,
15964
+ helix3d,
14792
15965
  shapes = [],
15966
+ readouts,
15967
+ traces,
14793
15968
  showGrid,
14794
15969
  shadows,
14795
15970
  interactive,
@@ -14804,19 +15979,148 @@ var init_BiologyCanvas = __esm({
14804
15979
  for (const n of nodes) {
14805
15980
  if (n.id) nodeById.set(n.id, n);
14806
15981
  }
15982
+ const bandCount = bands.length;
15983
+ for (let i = 0; i < bandCount; i++) {
15984
+ const band = bands[i];
15985
+ const bandColor = band.color ?? BIO_BAND_COLORS[i % BIO_BAND_COLORS.length];
15986
+ const bandY = i * height / bandCount;
15987
+ const bandH = height / bandCount;
15988
+ out.push({
15989
+ type: "rect",
15990
+ x: 0,
15991
+ y: bandY,
15992
+ width,
15993
+ height: bandH,
15994
+ color: bandColor,
15995
+ fill: bandColor,
15996
+ opacity: 0.45
15997
+ });
15998
+ if (band.label) {
15999
+ out.push({
16000
+ type: "text",
16001
+ x: 8,
16002
+ y: bandY + 14,
16003
+ text: band.label,
16004
+ color: "#6b7280",
16005
+ fontSize: 10
16006
+ });
16007
+ }
16008
+ }
16009
+ for (const c of compartments) {
16010
+ const color = c.color ?? "#16a34a";
16011
+ out.push({
16012
+ type: "ellipse",
16013
+ x: c.x,
16014
+ y: c.y,
16015
+ width: c.width,
16016
+ height: c.height,
16017
+ color,
16018
+ fill: c.fill ?? `${color}1A`,
16019
+ lineWidth: c.lineWidth ?? 2,
16020
+ ...c.dash ? { dash: c.dash } : {}
16021
+ });
16022
+ if (c.label) {
16023
+ out.push({
16024
+ type: "text",
16025
+ x: c.x,
16026
+ y: c.y - c.height / 2 + 14,
16027
+ text: c.label,
16028
+ color: "#111827",
16029
+ fontSize: 11,
16030
+ align: "center"
16031
+ });
16032
+ }
16033
+ }
16034
+ if (helix) {
16035
+ const hx = helix.x ?? 24;
16036
+ const hy = helix.y ?? height * 0.25;
16037
+ const hw = helix.width ?? width - 48;
16038
+ const hh = helix.height ?? height * 0.5;
16039
+ const rungs = helix.rungs;
16040
+ const n = rungs.length;
16041
+ const cy = hy + hh / 2;
16042
+ const colorA = helix.colorA ?? "#2563eb";
16043
+ const colorB = helix.colorB ?? "#dc2626";
16044
+ const rungColor = helix.rungColor ?? "#94a3b8";
16045
+ const fork = helix.fork ?? 0;
16046
+ const maxSep = Math.min(hh - 8, 96);
16047
+ const strandA = [];
16048
+ const strandB = [];
16049
+ const rungGeoms = [];
16050
+ for (let i = 0; i < n; i++) {
16051
+ const rx = hx + (i + 0.5) * hw / n;
16052
+ const t = (i + 0.5) / n;
16053
+ const paired = t >= fork;
16054
+ const sep = paired ? 28 : 28 + (maxSep - 28) * ((fork - t) / fork);
16055
+ strandA.push({ x: rx, y: cy - sep / 2 });
16056
+ strandB.push({ x: rx, y: cy + sep / 2 });
16057
+ rungGeoms.push({ rx, sep, rung: rungs[i], paired });
16058
+ }
16059
+ for (let i = 1; i < n; i++) {
16060
+ out.push({ type: "line", x1: strandA[i - 1].x, y1: strandA[i - 1].y, x2: strandA[i].x, y2: strandA[i].y, color: colorA, lineWidth: 3 });
16061
+ }
16062
+ for (let i = 1; i < n; i++) {
16063
+ out.push({ type: "line", x1: strandB[i - 1].x, y1: strandB[i - 1].y, x2: strandB[i].x, y2: strandB[i].y, color: colorB, lineWidth: 3 });
16064
+ }
16065
+ for (const g of rungGeoms) {
16066
+ const rColor = g.rung.color ?? (g.rung.state === "new" ? "#16a34a" : rungColor);
16067
+ const topY = cy - g.sep / 2;
16068
+ const bottomY = cy + g.sep / 2;
16069
+ if (g.paired) {
16070
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: bottomY, color: rColor });
16071
+ if (g.rung.a) {
16072
+ out.push({ type: "text", x: g.rx, y: cy - g.sep / 4, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
16073
+ }
16074
+ if (g.rung.b) {
16075
+ out.push({ type: "text", x: g.rx, y: cy + g.sep / 4, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16076
+ }
16077
+ } else {
16078
+ const stubTopY = topY + 8;
16079
+ const stubBottomY = bottomY - 8;
16080
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: stubTopY, color: rColor });
16081
+ out.push({ type: "line", x1: g.rx, y1: bottomY, x2: g.rx, y2: stubBottomY, color: rColor });
16082
+ if (g.rung.a) {
16083
+ out.push({ type: "text", x: g.rx, y: stubTopY + 6, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
16084
+ }
16085
+ if (g.rung.b) {
16086
+ out.push({ type: "text", x: g.rx, y: stubBottomY - 6, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16087
+ }
16088
+ }
16089
+ }
16090
+ }
14807
16091
  for (const e of edges) {
14808
16092
  const a = nodeById.get(e.from);
14809
16093
  const b = nodeById.get(e.to);
14810
16094
  if (!a || !b) continue;
14811
- out.push({
14812
- type: "line",
14813
- x1: a.x,
14814
- y1: a.y,
14815
- x2: b.x,
14816
- y2: b.y,
14817
- color: e.color ?? "#9ca3af",
14818
- lineWidth: 2
14819
- });
16095
+ const color = e.color ?? "#9ca3af";
16096
+ if (e.directed) {
16097
+ const rA = a.radius ?? 16;
16098
+ const rB = b.radius ?? 16;
16099
+ const dx = b.x - a.x;
16100
+ const dy = b.y - a.y;
16101
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16102
+ const ux = dx / dist;
16103
+ const uy = dy / dist;
16104
+ out.push({
16105
+ type: "arrow",
16106
+ x1: a.x + ux * rA,
16107
+ y1: a.y + uy * rA,
16108
+ x2: b.x - ux * rB,
16109
+ y2: b.y - uy * rB,
16110
+ color,
16111
+ lineWidth: 2
16112
+ });
16113
+ } else {
16114
+ out.push({
16115
+ type: "line",
16116
+ x1: a.x,
16117
+ y1: a.y,
16118
+ x2: b.x,
16119
+ y2: b.y,
16120
+ color,
16121
+ lineWidth: 2
16122
+ });
16123
+ }
14820
16124
  if (e.label) {
14821
16125
  out.push({
14822
16126
  type: "text",
@@ -14829,6 +16133,8 @@ var init_BiologyCanvas = __esm({
14829
16133
  }
14830
16134
  }
14831
16135
  for (const n of nodes) {
16136
+ const state = n.state ?? "default";
16137
+ const muted = state === "muted";
14832
16138
  out.push({
14833
16139
  type: "circle",
14834
16140
  x: n.x,
@@ -14836,28 +16142,127 @@ var init_BiologyCanvas = __esm({
14836
16142
  radius: n.radius ?? 16,
14837
16143
  color: n.color ?? "#16a34a",
14838
16144
  fill: `${n.color ?? "#16a34a"}33`,
14839
- id: n.id
16145
+ id: n.id,
16146
+ ...muted ? { opacity: 0.35 } : {}
14840
16147
  });
16148
+ if (state === "highlight") {
16149
+ out.push({
16150
+ type: "circle",
16151
+ x: n.x,
16152
+ y: n.y,
16153
+ radius: (n.radius ?? 16) + 4,
16154
+ color: "#f59e0b",
16155
+ lineWidth: 2
16156
+ });
16157
+ }
14841
16158
  if (n.label) {
14842
16159
  out.push({
14843
16160
  type: "text",
14844
16161
  x: n.x,
14845
16162
  y: n.y + (n.radius ?? 16) + 14,
14846
16163
  text: n.label,
16164
+ ...muted ? { opacity: 0.35 } : {},
14847
16165
  color: "#111827",
14848
16166
  fontSize: 12,
14849
16167
  align: "center"
14850
16168
  });
14851
16169
  }
14852
16170
  }
16171
+ const stageCount = stages.length;
16172
+ if (stageCount > 0) {
16173
+ if (stageStyle === "ring") {
16174
+ const cx = width / 2;
16175
+ const cy = height / 2;
16176
+ const R = Math.min(width, height) / 2 - 48;
16177
+ const ringPoints = [];
16178
+ for (let i = 0; i < stageCount; i++) {
16179
+ const angleRad = (-90 + 360 * i / stageCount) * Math.PI / 180;
16180
+ ringPoints.push({ x: cx + R * Math.cos(angleRad), y: cy + R * Math.sin(angleRad) });
16181
+ }
16182
+ if (stageCount >= 2) {
16183
+ for (let i = 0; i < stageCount - 1; i++) {
16184
+ const p1 = ringPoints[i];
16185
+ const p2 = ringPoints[i + 1];
16186
+ const dx = p2.x - p1.x;
16187
+ const dy = p2.y - p1.y;
16188
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16189
+ const ux = dx / dist;
16190
+ const uy = dy / dist;
16191
+ out.push({
16192
+ type: "arrow",
16193
+ x1: p1.x + ux * 46,
16194
+ y1: p1.y + uy * 46,
16195
+ x2: p2.x - ux * 46,
16196
+ y2: p2.y - uy * 46,
16197
+ color: "#94a3b8"
16198
+ });
16199
+ }
16200
+ }
16201
+ for (let i = 0; i < stageCount; i++) {
16202
+ const stage = stages[i];
16203
+ const state = stage.state ?? "pending";
16204
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16205
+ const w = Math.max(26, Math.min(84, stage.label.length * 6 + 10));
16206
+ const h = 18;
16207
+ const p = ringPoints[i];
16208
+ out.push({ type: "rect", x: p.x - w / 2, y: p.y - h / 2, width: w, height: h, color: fill, fill });
16209
+ out.push({ type: "text", x: p.x, y: p.y, text: stage.label, color: BIO_STAGE_TEXT[state], fontSize: 10, align: "center" });
16210
+ }
16211
+ } else {
16212
+ const stripY = height - 32;
16213
+ const slotW = (width - 16) / stageCount;
16214
+ const chipGeoms = [];
16215
+ for (let i = 0; i < stageCount; i++) {
16216
+ chipGeoms.push({ x: 8 + i * slotW + 5, w: slotW - 10 });
16217
+ }
16218
+ for (let i = 0; i < stageCount - 1; i++) {
16219
+ const midY = stripY + 13;
16220
+ out.push({
16221
+ type: "arrow",
16222
+ x1: chipGeoms[i].x + chipGeoms[i].w,
16223
+ y1: midY,
16224
+ x2: chipGeoms[i + 1].x,
16225
+ y2: midY,
16226
+ color: "#94a3b8"
16227
+ });
16228
+ }
16229
+ for (let i = 0; i < stageCount; i++) {
16230
+ const stage = stages[i];
16231
+ const state = stage.state ?? "pending";
16232
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16233
+ const g = chipGeoms[i];
16234
+ out.push({ type: "rect", x: g.x, y: stripY, width: g.w, height: 26, color: fill, fill });
16235
+ out.push({
16236
+ type: "text",
16237
+ x: g.x + g.w / 2,
16238
+ y: stripY + 13,
16239
+ text: stage.label,
16240
+ color: BIO_STAGE_TEXT[state],
16241
+ fontSize: 10,
16242
+ align: "center"
16243
+ });
16244
+ }
16245
+ }
16246
+ }
14853
16247
  out.push(...shapes);
14854
16248
  return out;
14855
- }, [nodes, edges, shapes]);
16249
+ }, [nodes, edges, compartments, bands, stages, stageStyle, helix, shapes, width, height]);
14856
16250
  const drawables3D = React85.useMemo(() => {
14857
16251
  if (mode !== "3d") return [];
14858
16252
  if (shapes.length > 0) {
14859
16253
  biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
14860
16254
  }
16255
+ if (compartments.length > 0 || bands.length > 0 || stages.length > 0 || helix) {
16256
+ biologyLog.debug("2D-only families ignored in 3D mode (pixel-authored 2D vocabulary)", {
16257
+ compartments: compartments.length,
16258
+ bands: bands.length,
16259
+ stages: stages.length,
16260
+ helix: helix != null
16261
+ });
16262
+ }
16263
+ if (animate) {
16264
+ biologyLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
16265
+ }
14861
16266
  const out = [];
14862
16267
  const labelColor = labelColorForBackground(backgroundColor);
14863
16268
  const nodeById = /* @__PURE__ */ new Map();
@@ -14891,15 +16296,21 @@ var init_BiologyCanvas = __esm({
14891
16296
  out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
14892
16297
  }
14893
16298
  }
16299
+ if (helix3d) {
16300
+ out.push(...helixDrawables(helix3d, { labelColor }));
16301
+ }
14894
16302
  return out;
14895
- }, [mode, nodes, edges, shapes, backgroundColor]);
16303
+ }, [mode, nodes, edges, shapes, compartments, bands, stages, helix, helix3d, animate, backgroundColor]);
14896
16304
  const nodeIndexById = React85.useMemo(() => {
14897
16305
  const m = /* @__PURE__ */ new Map();
16306
+ (helix3d?.rungs ?? []).forEach((rung, i) => {
16307
+ if (rung.id) m.set(rung.id, i);
16308
+ });
14898
16309
  nodes.forEach((n, i) => {
14899
16310
  if (n.id) m.set(n.id, i);
14900
16311
  });
14901
16312
  return m;
14902
- }, [nodes]);
16313
+ }, [nodes, helix3d]);
14903
16314
  if (mode === "3d") {
14904
16315
  return /* @__PURE__ */ jsxRuntime.jsx(
14905
16316
  LearningScene3D,
@@ -14931,6 +16342,8 @@ var init_BiologyCanvas = __esm({
14931
16342
  height,
14932
16343
  backgroundColor,
14933
16344
  shapes: derivedShapes,
16345
+ readouts,
16346
+ traces,
14934
16347
  interactive: interactive ?? false,
14935
16348
  animate,
14936
16349
  onShapeClick,
@@ -21663,7 +23076,7 @@ function bondPerpendicular(a, b) {
21663
23076
  if (len < 1e-6) return [1, 0, 0];
21664
23077
  return [px / len, py / len, 0];
21665
23078
  }
21666
- var chemistryLog, ChemistryCanvas;
23079
+ var chemistryLog, CHEM_BOND_STATE_COLOR, LONE_PAIR_ANGLES, ChemistryCanvas;
21667
23080
  var init_ChemistryCanvas = __esm({
21668
23081
  "components/learning/molecules/ChemistryCanvas.tsx"() {
21669
23082
  "use client";
@@ -21672,6 +23085,13 @@ var init_ChemistryCanvas = __esm({
21672
23085
  init_LearningCanvas();
21673
23086
  init_learningScene3D();
21674
23087
  chemistryLog = logger.createLogger("almadar:ui:chemistry-canvas");
23088
+ CHEM_BOND_STATE_COLOR = {
23089
+ default: "#6b7280",
23090
+ forming: "#16a34a",
23091
+ breaking: "#dc2626",
23092
+ highlight: "#f59e0b"
23093
+ };
23094
+ LONE_PAIR_ANGLES = [-90, 0, 90, 180];
21675
23095
  ChemistryCanvas = ({
21676
23096
  className,
21677
23097
  width = 600,
@@ -21685,7 +23105,14 @@ var init_ChemistryCanvas = __esm({
21685
23105
  atoms = [],
21686
23106
  bonds = [],
21687
23107
  arrows = [],
23108
+ bondStyle = "thick",
23109
+ containers = [],
23110
+ equation,
23111
+ equationColor,
23112
+ lattice3d,
21688
23113
  shapes = [],
23114
+ readouts,
23115
+ traces,
21689
23116
  showGrid,
21690
23117
  shadows,
21691
23118
  interactive,
@@ -21700,21 +23127,118 @@ var init_ChemistryCanvas = __esm({
21700
23127
  for (const a of atoms) {
21701
23128
  if (a.id) atomById.set(a.id, a);
21702
23129
  }
23130
+ for (const c of containers) {
23131
+ const color = c.color ?? "#64748b";
23132
+ if (c.level != null) {
23133
+ const lv = c.level;
23134
+ out.push({
23135
+ type: "rect",
23136
+ x: c.x + 1,
23137
+ y: c.y + c.height * (1 - lv),
23138
+ width: c.width - 2,
23139
+ height: c.height * lv - 1,
23140
+ color: c.levelColor ?? "#60a5fa",
23141
+ fill: c.levelColor ?? "#60a5fa",
23142
+ opacity: 0.5
23143
+ });
23144
+ }
23145
+ out.push({
23146
+ type: "rect",
23147
+ x: c.x,
23148
+ y: c.y,
23149
+ width: c.width,
23150
+ height: c.height,
23151
+ color,
23152
+ fill: c.fill,
23153
+ lineWidth: c.lineWidth ?? 2
23154
+ });
23155
+ const divider = c.divider ?? "none";
23156
+ if (divider !== "none") {
23157
+ out.push({
23158
+ type: "line",
23159
+ x1: c.x + c.width / 2,
23160
+ y1: c.y,
23161
+ x2: c.x + c.width / 2,
23162
+ y2: c.y + c.height,
23163
+ color: c.dividerColor ?? color,
23164
+ ...divider === "dashed" || divider === "dotted" ? { dash: divider } : {}
23165
+ });
23166
+ }
23167
+ if (c.leftLabel) {
23168
+ out.push({
23169
+ type: "text",
23170
+ x: c.x + c.width * 0.25,
23171
+ y: c.y + 12,
23172
+ text: c.leftLabel,
23173
+ color: "#374151",
23174
+ fontSize: 11,
23175
+ align: "center"
23176
+ });
23177
+ }
23178
+ if (c.rightLabel) {
23179
+ out.push({
23180
+ type: "text",
23181
+ x: c.x + c.width * 0.75,
23182
+ y: c.y + 12,
23183
+ text: c.rightLabel,
23184
+ color: "#374151",
23185
+ fontSize: 11,
23186
+ align: "center"
23187
+ });
23188
+ }
23189
+ if (c.label) {
23190
+ out.push({
23191
+ type: "text",
23192
+ x: c.x + c.width / 2,
23193
+ y: c.y + c.height + 12,
23194
+ text: c.label,
23195
+ color: "#111827",
23196
+ fontSize: 12,
23197
+ align: "center"
23198
+ });
23199
+ }
23200
+ }
21703
23201
  for (const b of bonds) {
21704
23202
  const a = atomById.get(b.from);
21705
23203
  const c = atomById.get(b.to);
21706
23204
  if (!a || !c) continue;
21707
- const color = b.color ?? "#6b7280";
21708
- const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
21709
- out.push({
21710
- type: "line",
21711
- x1: a.x,
21712
- y1: a.y,
21713
- x2: c.x,
21714
- y2: c.y,
21715
- color,
21716
- lineWidth: strokeWidth
21717
- });
23205
+ const state = b.state ?? "default";
23206
+ const color = b.color ?? CHEM_BOND_STATE_COLOR[state];
23207
+ const dash = state === "forming" || state === "breaking" ? "dashed" : void 0;
23208
+ if (bondStyle === "parallel") {
23209
+ const dx = c.x - a.x;
23210
+ const dy = c.y - a.y;
23211
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
23212
+ const ux = dx / dist;
23213
+ const uy = dy / dist;
23214
+ const px = -uy;
23215
+ const py = ux;
23216
+ const offsets = b.type === "double" ? [-3, 3] : b.type === "triple" ? [-4, 0, 4] : [0];
23217
+ for (const off of offsets) {
23218
+ out.push({
23219
+ type: "line",
23220
+ x1: a.x + px * off,
23221
+ y1: a.y + py * off,
23222
+ x2: c.x + px * off,
23223
+ y2: c.y + py * off,
23224
+ color,
23225
+ lineWidth: 2,
23226
+ ...dash ? { dash } : {}
23227
+ });
23228
+ }
23229
+ } else {
23230
+ const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
23231
+ out.push({
23232
+ type: "line",
23233
+ x1: a.x,
23234
+ y1: a.y,
23235
+ x2: c.x,
23236
+ y2: c.y,
23237
+ color,
23238
+ lineWidth: strokeWidth,
23239
+ ...dash ? { dash } : {}
23240
+ });
23241
+ }
21718
23242
  }
21719
23243
  for (const a of arrows) {
21720
23244
  const angle = (a.angle ?? 0) * (Math.PI / 180);
@@ -21763,15 +23287,62 @@ var init_ChemistryCanvas = __esm({
21763
23287
  align: "center"
21764
23288
  });
21765
23289
  }
23290
+ const r = a.radius ?? 14;
23291
+ if (a.charge) {
23292
+ out.push({
23293
+ type: "text",
23294
+ x: a.x + r * 0.85,
23295
+ y: a.y - r * 0.85,
23296
+ text: a.charge,
23297
+ color: "#111827",
23298
+ fontSize: 9,
23299
+ align: "left"
23300
+ });
23301
+ }
23302
+ const lonePairs = Math.max(0, Math.min(4, a.lonePairs ?? 0));
23303
+ for (let k = 0; k < lonePairs; k++) {
23304
+ const angleRad = LONE_PAIR_ANGLES[k] * Math.PI / 180;
23305
+ const cx = a.x + (r + 6) * Math.cos(angleRad);
23306
+ const cy = a.y + (r + 6) * Math.sin(angleRad);
23307
+ const perpX = -Math.sin(angleRad);
23308
+ const perpY = Math.cos(angleRad);
23309
+ for (const sign of [1, -1]) {
23310
+ out.push({
23311
+ type: "circle",
23312
+ x: cx + perpX * 2.5 * sign,
23313
+ y: cy + perpY * 2.5 * sign,
23314
+ radius: 1.5,
23315
+ color: "#374151",
23316
+ fill: "#374151"
23317
+ });
23318
+ }
23319
+ }
23320
+ }
23321
+ if (equation) {
23322
+ out.push({
23323
+ type: "text",
23324
+ x: width / 2,
23325
+ y: 14,
23326
+ text: equation,
23327
+ color: equationColor ?? "#111827",
23328
+ fontSize: 13,
23329
+ align: "center"
23330
+ });
21766
23331
  }
21767
23332
  out.push(...shapes);
21768
23333
  return out;
21769
- }, [atoms, bonds, arrows, shapes]);
23334
+ }, [atoms, bonds, arrows, bondStyle, containers, equation, equationColor, shapes, width]);
21770
23335
  const drawables3D = React85.useMemo(() => {
21771
23336
  if (mode !== "3d") return [];
21772
23337
  if (shapes.length > 0) {
21773
23338
  chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
21774
23339
  }
23340
+ if (containers.length > 0) {
23341
+ chemistryLog.debug("containers ignored in 3D mode (pixel-authored 2D vocabulary)", { count: containers.length });
23342
+ }
23343
+ if (animate) {
23344
+ chemistryLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
23345
+ }
21775
23346
  const out = [];
21776
23347
  const labelColor = labelColorForBackground(backgroundColor);
21777
23348
  const atomById = /* @__PURE__ */ new Map();
@@ -21818,8 +23389,11 @@ var init_ChemistryCanvas = __esm({
21818
23389
  out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
21819
23390
  }
21820
23391
  }
23392
+ if (lattice3d) {
23393
+ out.push(...latticeDrawables(lattice3d, { labelColor }));
23394
+ }
21821
23395
  return out;
21822
- }, [mode, atoms, bonds, arrows, shapes, backgroundColor]);
23396
+ }, [mode, atoms, bonds, arrows, shapes, containers, lattice3d, animate, backgroundColor]);
21823
23397
  const atomIndexById = React85.useMemo(() => {
21824
23398
  const m = /* @__PURE__ */ new Map();
21825
23399
  atoms.forEach((a, i) => {
@@ -21858,6 +23432,8 @@ var init_ChemistryCanvas = __esm({
21858
23432
  height,
21859
23433
  backgroundColor,
21860
23434
  shapes: derivedShapes,
23435
+ readouts,
23436
+ traces,
21861
23437
  interactive: interactive ?? false,
21862
23438
  animate,
21863
23439
  onShapeClick,
@@ -27969,6 +29545,10 @@ var init_ProgressDots = __esm({
27969
29545
  ProgressDots.displayName = "ProgressDots";
27970
29546
  }
27971
29547
  });
29548
+ function formatTick(v) {
29549
+ if (Number.isInteger(v)) return String(v);
29550
+ return v.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
29551
+ }
27972
29552
  var MathCanvas;
27973
29553
  var init_MathCanvas = __esm({
27974
29554
  "components/learning/molecules/MathCanvas.tsx"() {
@@ -27988,10 +29568,19 @@ var init_MathCanvas = __esm({
27988
29568
  showAxes = true,
27989
29569
  showGrid = true,
27990
29570
  gridStep = 1,
29571
+ showTickLabels = false,
29572
+ showCurveLabels = false,
27991
29573
  curves = [],
27992
29574
  points = [],
27993
29575
  vectors = [],
29576
+ regions = [],
29577
+ bars = [],
29578
+ guides = [],
29579
+ angles = [],
29580
+ hops = [],
27994
29581
  shapes = [],
29582
+ readouts,
29583
+ traces,
27995
29584
  interactive = false,
27996
29585
  animate = false,
27997
29586
  onShapeClick,
@@ -28005,6 +29594,8 @@ var init_MathCanvas = __esm({
28005
29594
  const plotH = height - margin * 2;
28006
29595
  const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
28007
29596
  const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
29597
+ const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
29598
+ const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
28008
29599
  if (showGrid) {
28009
29600
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep) {
28010
29601
  const px = mapX(x);
@@ -28015,14 +29606,99 @@ var init_MathCanvas = __esm({
28015
29606
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color: "#e5e7eb", lineWidth: 1 });
28016
29607
  }
28017
29608
  }
29609
+ if (showTickLabels) {
29610
+ const labelEveryX = Math.max(1, Math.ceil((xMax - xMin) / gridStep / Math.floor(plotW / 40)));
29611
+ let kx = 0;
29612
+ for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
29613
+ if (kx % labelEveryX === 0 && x !== 0) {
29614
+ out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: 10, align: "center" });
29615
+ }
29616
+ }
29617
+ const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
29618
+ let ky = 0;
29619
+ for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
29620
+ if (ky % labelEveryY === 0 && y !== 0) {
29621
+ out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: 10, align: "right" });
29622
+ }
29623
+ }
29624
+ if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
29625
+ out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: 10, align: "right" });
29626
+ }
29627
+ }
29628
+ for (const region of regions) {
29629
+ if (!region.samples || region.samples.length === 0) continue;
29630
+ const baseline = region.baseline ?? 0;
29631
+ const clampedPoint = (p) => ({
29632
+ x: mapX(Math.min(xMax, Math.max(xMin, p.x))),
29633
+ y: mapY(Math.min(yMax, Math.max(yMin, p.y)))
29634
+ });
29635
+ const upper = region.samples.map(clampedPoint);
29636
+ const first = region.samples[0];
29637
+ const last = region.samples[region.samples.length - 1];
29638
+ const closing = region.samples2 && region.samples2.length > 0 ? [...region.samples2].reverse().map(clampedPoint) : [clampedPoint({ x: last.x, y: baseline }), clampedPoint({ x: first.x, y: baseline })];
29639
+ const color = region.color ?? "#2563eb";
29640
+ out.push({
29641
+ type: "polygon",
29642
+ points: [...upper, ...closing],
29643
+ fill: color,
29644
+ color,
29645
+ opacity: region.opacity ?? 0.2,
29646
+ lineWidth: 1
29647
+ });
29648
+ if (region.label) {
29649
+ const mid = Math.floor(region.samples.length / 2);
29650
+ out.push({
29651
+ type: "text",
29652
+ x: mapX((first.x + last.x) / 2),
29653
+ y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
29654
+ text: region.label,
29655
+ color: "#111827",
29656
+ fontSize: 11
29657
+ });
29658
+ }
29659
+ }
29660
+ for (const bar of bars) {
29661
+ if (bar.x + bar.width < xMin || bar.x > xMax) continue;
29662
+ const y0 = bar.y0 ?? 0;
29663
+ const color = bar.color ?? "#93c5fd";
29664
+ out.push({
29665
+ type: "rect",
29666
+ x: mapX(bar.x),
29667
+ y: mapY(Math.max(y0, bar.y1)),
29668
+ width: mapX(bar.x + bar.width) - mapX(bar.x),
29669
+ height: Math.abs(mapY(bar.y1) - mapY(y0)),
29670
+ color,
29671
+ fill: color,
29672
+ opacity: bar.opacity ?? 0.5,
29673
+ lineWidth: 1
29674
+ });
29675
+ }
28018
29676
  if (showAxes) {
28019
- const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
28020
- const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
28021
29677
  out.push({ type: "line", x1: margin, y1: xAxisY, x2: width - margin, y2: xAxisY, color: "#374151", lineWidth: 2 });
28022
29678
  out.push({ type: "line", x1: yAxisX, y1: margin, x2: yAxisX, y2: height - margin, color: "#374151", lineWidth: 2 });
28023
29679
  }
29680
+ for (const guide of guides) {
29681
+ const color = guide.color ?? "#9ca3af";
29682
+ const dash = guide.dash ?? "dashed";
29683
+ if (guide.kind === "vline") {
29684
+ if (guide.at < xMin || guide.at > xMax) continue;
29685
+ const px = mapX(guide.at);
29686
+ out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
29687
+ if (guide.label) {
29688
+ out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color: "#111827", fontSize: 11 });
29689
+ }
29690
+ } else {
29691
+ if (guide.at < yMin || guide.at > yMax) continue;
29692
+ const py = mapY(guide.at);
29693
+ out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
29694
+ if (guide.label) {
29695
+ out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color: "#111827", fontSize: 11, align: "right" });
29696
+ }
29697
+ }
29698
+ }
28024
29699
  for (const curve of curves) {
28025
29700
  if (!curve.samples || curve.samples.length < 2) continue;
29701
+ let lastInRange;
28026
29702
  for (let i = 1; i < curve.samples.length; i++) {
28027
29703
  const a = curve.samples[i - 1];
28028
29704
  const b = curve.samples[i];
@@ -28034,19 +29710,97 @@ var init_MathCanvas = __esm({
28034
29710
  x2: mapX(b.x),
28035
29711
  y2: mapY(b.y),
28036
29712
  color: curve.color ?? "#2563eb",
28037
- lineWidth: 2
29713
+ lineWidth: 2,
29714
+ dash: curve.dash
29715
+ });
29716
+ lastInRange = b;
29717
+ }
29718
+ if (showCurveLabels && curve.label && lastInRange) {
29719
+ out.push({
29720
+ type: "text",
29721
+ x: mapX(lastInRange.x) + 6,
29722
+ y: mapY(lastInRange.y) - 6,
29723
+ text: curve.label,
29724
+ color: curve.color ?? "#2563eb",
29725
+ fontSize: 11
29726
+ });
29727
+ }
29728
+ }
29729
+ for (const hop of hops) {
29730
+ const x1 = mapX(hop.from);
29731
+ const x2 = mapX(hop.to);
29732
+ const peak = Math.min(36, plotH * 0.3);
29733
+ const color = hop.color ?? "#7c3aed";
29734
+ out.push({
29735
+ type: "ellipse",
29736
+ x: (x1 + x2) / 2,
29737
+ y: xAxisY,
29738
+ width: Math.abs(x2 - x1),
29739
+ height: 2 * peak,
29740
+ startAngle: 180,
29741
+ endAngle: 360,
29742
+ color
29743
+ });
29744
+ const s = Math.sign(hop.to - hop.from);
29745
+ out.push({
29746
+ type: "polygon",
29747
+ points: [
29748
+ { x: x2, y: xAxisY },
29749
+ { x: x2 - 4 * s, y: xAxisY - 7 },
29750
+ { x: x2 + 2 * s, y: xAxisY - 7 }
29751
+ ],
29752
+ fill: color,
29753
+ color
29754
+ });
29755
+ if (hop.label) {
29756
+ out.push({
29757
+ type: "text",
29758
+ x: (x1 + x2) / 2,
29759
+ y: xAxisY - peak - 8,
29760
+ text: hop.label,
29761
+ color: "#111827",
29762
+ fontSize: 10,
29763
+ align: "center"
29764
+ });
29765
+ }
29766
+ }
29767
+ for (const angle of angles) {
29768
+ const radius = angle.radius ?? 0.8;
29769
+ const color = angle.color ?? "#0ea5e9";
29770
+ out.push({
29771
+ type: "ellipse",
29772
+ x: mapX(angle.x),
29773
+ y: mapY(angle.y),
29774
+ width: 2 * radius * plotW / (xMax - xMin),
29775
+ height: 2 * radius * plotH / (yMax - yMin),
29776
+ startAngle: -angle.to,
29777
+ endAngle: -angle.from,
29778
+ color
29779
+ });
29780
+ if (angle.label) {
29781
+ const mid = (angle.from + angle.to) / 2;
29782
+ const rad = mid * Math.PI / 180;
29783
+ out.push({
29784
+ type: "text",
29785
+ x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
29786
+ y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
29787
+ text: angle.label,
29788
+ color: "#111827",
29789
+ fontSize: 11,
29790
+ align: "center"
28038
29791
  });
28039
29792
  }
28040
29793
  }
28041
29794
  for (const p of points) {
28042
29795
  if (p.x < xMin || p.x > xMax || p.y < yMin || p.y > yMax) continue;
29796
+ const isOpen = p.style === "open";
28043
29797
  out.push({
28044
29798
  type: "circle",
28045
29799
  x: mapX(p.x),
28046
29800
  y: mapY(p.y),
28047
29801
  radius: p.radius ?? 4,
28048
29802
  color: p.color ?? "#dc2626",
28049
- fill: p.color ?? "#dc2626"
29803
+ fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
28050
29804
  });
28051
29805
  if (p.label) {
28052
29806
  out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: "#111827", fontSize: 12 });
@@ -28065,7 +29819,28 @@ var init_MathCanvas = __esm({
28065
29819
  }
28066
29820
  out.push(...shapes);
28067
29821
  return out;
28068
- }, [width, height, xMin, xMax, yMin, yMax, showAxes, showGrid, gridStep, curves, points, vectors, shapes]);
29822
+ }, [
29823
+ width,
29824
+ height,
29825
+ xMin,
29826
+ xMax,
29827
+ yMin,
29828
+ yMax,
29829
+ showAxes,
29830
+ showGrid,
29831
+ gridStep,
29832
+ showTickLabels,
29833
+ showCurveLabels,
29834
+ curves,
29835
+ points,
29836
+ vectors,
29837
+ regions,
29838
+ bars,
29839
+ guides,
29840
+ angles,
29841
+ hops,
29842
+ shapes
29843
+ ]);
28069
29844
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
28070
29845
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
28071
29846
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -28074,6 +29849,8 @@ var init_MathCanvas = __esm({
28074
29849
  width,
28075
29850
  height,
28076
29851
  shapes: derivedShapes,
29852
+ readouts,
29853
+ traces,
28077
29854
  interactive,
28078
29855
  animate,
28079
29856
  onShapeClick,
@@ -28085,7 +29862,315 @@ var init_MathCanvas = __esm({
28085
29862
  };
28086
29863
  }
28087
29864
  });
28088
- var physicsLog2, PhysicsCanvas;
29865
+ function formatMeterValue(v) {
29866
+ return Number.isInteger(v) ? String(v) : String(Number(v.toFixed(2)));
29867
+ }
29868
+ function sceneObjectShapes(obj, canvasWidth, canvasHeight) {
29869
+ const out = [];
29870
+ const color = obj.color ?? "#334155";
29871
+ switch (obj.kind) {
29872
+ case "ground": {
29873
+ const xStart = obj.x1 ?? 0;
29874
+ const xEnd = obj.x2 ?? canvasWidth;
29875
+ const y = obj.y ?? 0;
29876
+ out.push({ type: "line", x1: xStart, y1: y, x2: xEnd, y2: y, color, lineWidth: 2 });
29877
+ for (let hx = xStart + 7; hx <= xEnd; hx += 14) {
29878
+ out.push({ type: "line", x1: hx, y1: y, x2: hx - 7, y2: y + 7, color, lineWidth: 1 });
29879
+ }
29880
+ if (obj.label) {
29881
+ out.push({
29882
+ type: "text",
29883
+ x: (xStart + xEnd) / 2,
29884
+ y: y - 10,
29885
+ text: obj.label,
29886
+ color: PHYSICS_LABEL_COLOR,
29887
+ fontSize: 11,
29888
+ align: "center"
29889
+ });
29890
+ }
29891
+ break;
29892
+ }
29893
+ case "wall": {
29894
+ const yStart = obj.y1 ?? 0;
29895
+ const yEnd = obj.y2 ?? canvasHeight;
29896
+ const x = obj.x ?? 0;
29897
+ out.push({ type: "line", x1: x, y1: yStart, x2: x, y2: yEnd, color, lineWidth: 2 });
29898
+ for (let hy = yStart + 7; hy <= yEnd; hy += 14) {
29899
+ out.push({ type: "line", x1: x, y1: hy, x2: x - 7, y2: hy + 7, color, lineWidth: 1 });
29900
+ }
29901
+ if (obj.label) {
29902
+ out.push({
29903
+ type: "text",
29904
+ x: x + 12,
29905
+ y: (yStart + yEnd) / 2,
29906
+ text: obj.label,
29907
+ color: PHYSICS_LABEL_COLOR,
29908
+ fontSize: 11,
29909
+ align: "left"
29910
+ });
29911
+ }
29912
+ break;
29913
+ }
29914
+ case "ramp": {
29915
+ const x1 = obj.x1 ?? 0;
29916
+ const y1 = obj.y1 ?? 0;
29917
+ const x2 = obj.x2 ?? canvasWidth;
29918
+ const y2 = obj.y2 ?? canvasHeight;
29919
+ out.push({
29920
+ type: "polygon",
29921
+ points: [
29922
+ { x: x1, y: y1 },
29923
+ { x: x2, y: y2 },
29924
+ { x: x1, y: y2 }
29925
+ ],
29926
+ color,
29927
+ fill: obj.fill ?? "#e2e8f0",
29928
+ lineWidth: 2
29929
+ });
29930
+ if (obj.label) {
29931
+ out.push({
29932
+ type: "text",
29933
+ x: (2 * x1 + x2) / 3,
29934
+ y: (y1 + 2 * y2) / 3,
29935
+ text: obj.label,
29936
+ color: PHYSICS_LABEL_COLOR,
29937
+ fontSize: 11,
29938
+ align: "center"
29939
+ });
29940
+ }
29941
+ break;
29942
+ }
29943
+ case "box": {
29944
+ const x = obj.x ?? 0;
29945
+ const y = obj.y ?? 0;
29946
+ const w = obj.width ?? 40;
29947
+ const h = obj.height ?? 40;
29948
+ out.push({ type: "rect", x, y, width: w, height: h, color, fill: obj.fill, lineWidth: 2 });
29949
+ if (obj.label) {
29950
+ out.push({
29951
+ type: "text",
29952
+ x: x + w / 2,
29953
+ y: y + h / 2,
29954
+ text: obj.label,
29955
+ color: PHYSICS_LABEL_COLOR,
29956
+ fontSize: 11,
29957
+ align: "center"
29958
+ });
29959
+ }
29960
+ break;
29961
+ }
29962
+ case "pivot": {
29963
+ const x = obj.x ?? 0;
29964
+ const y = obj.y ?? 0;
29965
+ out.push({ type: "circle", x, y, radius: 5, color, fill: color });
29966
+ out.push({ type: "line", x1: x - 14, y1: y - 8, x2: x + 14, y2: y - 8, color, lineWidth: 1 });
29967
+ for (let k = 0; k < 5; k++) {
29968
+ const hx = x - 14 + 7 * k;
29969
+ out.push({ type: "line", x1: hx, y1: y - 8, x2: hx - 6, y2: y - 14, color, lineWidth: 1 });
29970
+ }
29971
+ if (obj.label) {
29972
+ out.push({
29973
+ type: "text",
29974
+ x,
29975
+ y: y - 20,
29976
+ text: obj.label,
29977
+ color: PHYSICS_LABEL_COLOR,
29978
+ fontSize: 11,
29979
+ align: "center"
29980
+ });
29981
+ }
29982
+ break;
29983
+ }
29984
+ }
29985
+ return out;
29986
+ }
29987
+ function trailShapes(trail) {
29988
+ const n = trail.points.length;
29989
+ if (n < 2) return [];
29990
+ const color = trail.color ?? "#94a3b8";
29991
+ const lineWidth = trail.width ?? 2;
29992
+ const fade = trail.fade ?? true;
29993
+ const globalOpacity = trail.opacity ?? 1;
29994
+ const out = [];
29995
+ for (let i = 0; i < n - 1; i++) {
29996
+ const a = trail.points[i];
29997
+ const b = trail.points[i + 1];
29998
+ const segmentOpacity = fade ? 0.12 + 0.68 * i / (n - 1) : 0.6;
29999
+ out.push({
30000
+ type: "line",
30001
+ x1: a.x,
30002
+ y1: a.y,
30003
+ x2: b.x,
30004
+ y2: b.y,
30005
+ color,
30006
+ lineWidth,
30007
+ opacity: segmentOpacity * globalOpacity
30008
+ });
30009
+ }
30010
+ return out;
30011
+ }
30012
+ function constraintShapes(c, a, b) {
30013
+ const color = c.color ?? "#9ca3af";
30014
+ const kind = c.kind ?? "rod";
30015
+ if (kind === "rod") {
30016
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2 }];
30017
+ }
30018
+ if (kind === "string") {
30019
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2, dash: "dashed" }];
30020
+ }
30021
+ const COILS = 8;
30022
+ const AMP = 7;
30023
+ const LEAD = 10;
30024
+ const dx = b.x - a.x;
30025
+ const dy = b.y - a.y;
30026
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
30027
+ const ux = dx / dist;
30028
+ const uy = dy / dist;
30029
+ const perpX = -uy;
30030
+ const perpY = ux;
30031
+ const aPrime = { x: a.x + LEAD * ux, y: a.y + LEAD * uy };
30032
+ const bPrime = { x: b.x - LEAD * ux, y: b.y - LEAD * uy };
30033
+ const m = 2 * COILS;
30034
+ const polyline = [{ x: a.x, y: a.y }, aPrime];
30035
+ for (let j = 1; j <= m; j++) {
30036
+ const t = j / (m + 1);
30037
+ const baseX = aPrime.x + t * (bPrime.x - aPrime.x);
30038
+ const baseY = aPrime.y + t * (bPrime.y - aPrime.y);
30039
+ const sign = j % 2 === 0 ? 1 : -1;
30040
+ polyline.push({ x: baseX + sign * AMP * perpX, y: baseY + sign * AMP * perpY });
30041
+ }
30042
+ polyline.push(bPrime, { x: b.x, y: b.y });
30043
+ const out = [];
30044
+ for (let i = 1; i < polyline.length; i++) {
30045
+ out.push({
30046
+ type: "line",
30047
+ x1: polyline[i - 1].x,
30048
+ y1: polyline[i - 1].y,
30049
+ x2: polyline[i].x,
30050
+ y2: polyline[i].y,
30051
+ color,
30052
+ lineWidth: 2
30053
+ });
30054
+ }
30055
+ return out;
30056
+ }
30057
+ function vectorShapes(v, bodyById) {
30058
+ let ax;
30059
+ let ay;
30060
+ if (v.body) {
30061
+ const anchor = bodyById.get(v.body);
30062
+ if (!anchor) return [];
30063
+ ax = anchor.x;
30064
+ ay = anchor.y;
30065
+ } else {
30066
+ ax = v.x ?? 0;
30067
+ ay = v.y ?? 0;
30068
+ }
30069
+ const scale = v.scale ?? 1;
30070
+ const color = v.color ?? "#dc2626";
30071
+ const tx = ax + v.dx * scale;
30072
+ const ty = ay + v.dy * scale;
30073
+ const out = [{ type: "arrow", x1: ax, y1: ay, x2: tx, y2: ty, color, lineWidth: 2, dash: v.dash }];
30074
+ if (v.label) {
30075
+ const dist = Math.max(1e-6, Math.hypot(tx - ax, ty - ay));
30076
+ const ux = (tx - ax) / dist;
30077
+ const uy = (ty - ay) / dist;
30078
+ out.push({
30079
+ type: "text",
30080
+ x: tx + 8 * ux,
30081
+ y: ty + 8 * uy,
30082
+ text: v.label,
30083
+ color,
30084
+ fontSize: 11,
30085
+ align: "center"
30086
+ });
30087
+ }
30088
+ return out;
30089
+ }
30090
+ function angleMarkerShapes(a) {
30091
+ const radius = a.radius ?? 26;
30092
+ const color = a.color ?? "#0ea5e9";
30093
+ const out = [
30094
+ {
30095
+ type: "ellipse",
30096
+ x: a.x,
30097
+ y: a.y,
30098
+ width: radius * 2,
30099
+ height: radius * 2,
30100
+ startAngle: a.from,
30101
+ endAngle: a.to,
30102
+ color,
30103
+ lineWidth: 2
30104
+ }
30105
+ ];
30106
+ if (a.label) {
30107
+ const mid = (a.from + a.to) / 2 * (Math.PI / 180);
30108
+ out.push({
30109
+ type: "text",
30110
+ x: a.x + (radius + 13) * Math.cos(mid),
30111
+ y: a.y + (radius + 13) * Math.sin(mid),
30112
+ text: a.label,
30113
+ color,
30114
+ fontSize: 11,
30115
+ align: "center"
30116
+ });
30117
+ }
30118
+ return out;
30119
+ }
30120
+ function fieldShapes(field, canvasWidth, canvasHeight) {
30121
+ const spacing = field.spacing ?? 48;
30122
+ const size = field.size ?? 14;
30123
+ const color = field.color ?? "#94a3b8";
30124
+ const regionX = field.x ?? 0;
30125
+ const regionY = field.y ?? 0;
30126
+ const regionW = field.width ?? canvasWidth;
30127
+ const regionH = field.height ?? canvasHeight;
30128
+ const out = [];
30129
+ for (let gx = regionX + spacing / 2; gx < regionX + regionW; gx += spacing) {
30130
+ for (let gy = regionY + spacing / 2; gy < regionY + regionH; gy += spacing) {
30131
+ if (field.kind === "arrows") {
30132
+ const rad = (field.angle ?? 0) * Math.PI / 180;
30133
+ const hx = Math.cos(rad) * size / 2;
30134
+ const hy = Math.sin(rad) * size / 2;
30135
+ out.push({ type: "arrow", x1: gx - hx, y1: gy - hy, x2: gx + hx, y2: gy + hy, color, lineWidth: 2 });
30136
+ } else if (field.kind === "into") {
30137
+ const r = size / 3;
30138
+ const d = 0.6 * r * Math.SQRT1_2;
30139
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30140
+ out.push({ type: "line", x1: gx - d, y1: gy - d, x2: gx + d, y2: gy + d, color, lineWidth: 1 });
30141
+ out.push({ type: "line", x1: gx - d, y1: gy + d, x2: gx + d, y2: gy - d, color, lineWidth: 1 });
30142
+ } else {
30143
+ const r = size / 3;
30144
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30145
+ out.push({ type: "circle", x: gx, y: gy, radius: 1.5, color, fill: color });
30146
+ }
30147
+ }
30148
+ }
30149
+ return out;
30150
+ }
30151
+ function meterShapes(meters, canvasHeight) {
30152
+ const n = meters.length;
30153
+ const out = [];
30154
+ const sharedMax = Math.max(1e-6, ...meters.map((m) => m.value));
30155
+ meters.forEach((meter, i) => {
30156
+ const rowY = canvasHeight - 10 - 16 * (n - i);
30157
+ const color = meter.color ?? "#3b82f6";
30158
+ const M = meter.max ?? sharedMax;
30159
+ const w = Math.round(Math.min(1, Math.max(0, meter.value / M)) * 110);
30160
+ out.push({ type: "text", x: 8, y: rowY + 8, text: meter.label, color: PHYSICS_LABEL_COLOR, fontSize: 10 });
30161
+ out.push({ type: "rect", x: 52, y: rowY, width: w, height: 10, color, fill: color });
30162
+ out.push({
30163
+ type: "text",
30164
+ x: 166,
30165
+ y: rowY + 8,
30166
+ text: formatMeterValue(meter.value),
30167
+ color: "#6b7280",
30168
+ fontSize: 9
30169
+ });
30170
+ });
30171
+ return out;
30172
+ }
30173
+ var physicsLog2, PHYSICS_LABEL_COLOR, PhysicsCanvas;
28089
30174
  var init_PhysicsCanvas = __esm({
28090
30175
  "components/learning/molecules/PhysicsCanvas.tsx"() {
28091
30176
  "use client";
@@ -28094,6 +30179,7 @@ var init_PhysicsCanvas = __esm({
28094
30179
  init_LearningCanvas();
28095
30180
  init_learningScene3D();
28096
30181
  physicsLog2 = logger.createLogger("almadar:ui:physics-canvas");
30182
+ PHYSICS_LABEL_COLOR = "#374151";
28097
30183
  PhysicsCanvas = ({
28098
30184
  className,
28099
30185
  width = 600,
@@ -28110,7 +30196,18 @@ var init_PhysicsCanvas = __esm({
28110
30196
  showForces = false,
28111
30197
  velocityScale = 20,
28112
30198
  forceScale = 20,
30199
+ sceneObjects = [],
30200
+ trails = [],
30201
+ vectors = [],
30202
+ surface3d,
30203
+ vectors3d = [],
30204
+ vectorScale = 1,
30205
+ angles = [],
30206
+ field,
30207
+ meters = [],
28113
30208
  shapes = [],
30209
+ readouts,
30210
+ traces,
28114
30211
  showGrid,
28115
30212
  shadows,
28116
30213
  interactive,
@@ -28125,19 +30222,14 @@ var init_PhysicsCanvas = __esm({
28125
30222
  for (const b of bodies) {
28126
30223
  if (b.id) bodyById.set(b.id, b);
28127
30224
  }
30225
+ if (field) out.push(...fieldShapes(field, width, height));
30226
+ for (const obj of sceneObjects) out.push(...sceneObjectShapes(obj, width, height));
30227
+ for (const trail of trails) out.push(...trailShapes(trail));
28128
30228
  for (const c of constraints) {
28129
30229
  const a = bodyById.get(c.from);
28130
30230
  const b = bodyById.get(c.to);
28131
30231
  if (!a || !b) continue;
28132
- out.push({
28133
- type: "line",
28134
- x1: a.x,
28135
- y1: a.y,
28136
- x2: b.x,
28137
- y2: b.y,
28138
- color: c.color ?? "#9ca3af",
28139
- lineWidth: 2
28140
- });
30232
+ out.push(...constraintShapes(c, a, b));
28141
30233
  }
28142
30234
  for (const b of bodies) {
28143
30235
  out.push({
@@ -28182,14 +30274,51 @@ var init_PhysicsCanvas = __esm({
28182
30274
  });
28183
30275
  }
28184
30276
  }
30277
+ for (const v of vectors) out.push(...vectorShapes(v, bodyById));
30278
+ for (const a of angles) out.push(...angleMarkerShapes(a));
30279
+ if (meters.length > 0) out.push(...meterShapes(meters, height));
28185
30280
  out.push(...shapes);
28186
30281
  return out;
28187
- }, [bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes]);
30282
+ }, [
30283
+ bodies,
30284
+ constraints,
30285
+ showVelocity,
30286
+ showForces,
30287
+ velocityScale,
30288
+ forceScale,
30289
+ sceneObjects,
30290
+ trails,
30291
+ vectors,
30292
+ angles,
30293
+ field,
30294
+ meters,
30295
+ shapes,
30296
+ width,
30297
+ height
30298
+ ]);
28188
30299
  const drawables3D = React85.useMemo(() => {
28189
30300
  if (mode !== "3d") return [];
28190
30301
  if (shapes.length > 0) {
28191
30302
  physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
28192
30303
  }
30304
+ if (sceneObjects.length > 0) {
30305
+ physicsLog2.debug("sceneObjects ignored in 3D mode (pixel-authored 2D vocabulary)", { count: sceneObjects.length });
30306
+ }
30307
+ if (vectors.length > 0) {
30308
+ physicsLog2.debug("vectors ignored in 3D mode (pixel-authored 2D vocabulary)", { count: vectors.length });
30309
+ }
30310
+ if (angles.length > 0) {
30311
+ physicsLog2.debug("angles ignored in 3D mode (pixel-authored 2D vocabulary)", { count: angles.length });
30312
+ }
30313
+ if (field) {
30314
+ physicsLog2.debug("field ignored in 3D mode (pixel-authored 2D vocabulary)");
30315
+ }
30316
+ if (meters.length > 0) {
30317
+ physicsLog2.debug("meters ignored in 3D mode (pixel-authored 2D vocabulary)", { count: meters.length });
30318
+ }
30319
+ if (animate) {
30320
+ physicsLog2.debug("animate ignored in 3D mode (motion is entity-state driven)");
30321
+ }
28193
30322
  const out = [];
28194
30323
  const labelColor = labelColorForBackground(backgroundColor);
28195
30324
  const bodyById = /* @__PURE__ */ new Map();
@@ -28237,15 +30366,67 @@ var init_PhysicsCanvas = __esm({
28237
30366
  if (arrow) out.push(arrow);
28238
30367
  }
28239
30368
  }
30369
+ for (const trail of trails) {
30370
+ if (trail.fade !== void 0) {
30371
+ physicsLog2.debug("trail.fade ignored in 3D mode (2D-only fade curve \u2014 3D draws an opaque tube)", { id: trail.id });
30372
+ }
30373
+ const points = trail.points.map((p) => [p.x, p.y, p.z ?? 0]);
30374
+ out.push(
30375
+ ...polylineTube(points, trail.width ?? 0.05, trail.color ?? "#94a3b8", {
30376
+ ...trail.opacity !== void 0 ? { opacity: trail.opacity } : {}
30377
+ })
30378
+ );
30379
+ }
30380
+ if (surface3d) {
30381
+ out.push(...heightFieldMesh(surface3d));
30382
+ }
30383
+ if (vectors3d.length > 0) {
30384
+ out.push(
30385
+ ...arrowField(
30386
+ vectors3d.map((v) => ({
30387
+ id: v.id,
30388
+ from: [v.x, v.y, v.z ?? 0],
30389
+ delta: [v.dx, v.dy, v.dz ?? 0],
30390
+ color: v.color,
30391
+ label: v.label,
30392
+ width: v.width
30393
+ })),
30394
+ { scale: vectorScale, labelColor }
30395
+ )
30396
+ );
30397
+ }
28240
30398
  return out;
28241
- }, [mode, bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes, backgroundColor]);
30399
+ }, [
30400
+ mode,
30401
+ bodies,
30402
+ constraints,
30403
+ showVelocity,
30404
+ showForces,
30405
+ velocityScale,
30406
+ forceScale,
30407
+ shapes,
30408
+ sceneObjects,
30409
+ trails,
30410
+ vectors,
30411
+ surface3d,
30412
+ vectors3d,
30413
+ vectorScale,
30414
+ angles,
30415
+ field,
30416
+ meters,
30417
+ animate,
30418
+ backgroundColor
30419
+ ]);
28242
30420
  const bodyIndexById = React85.useMemo(() => {
28243
30421
  const m = /* @__PURE__ */ new Map();
30422
+ vectors3d.forEach((v, i) => {
30423
+ if (v.id) m.set(v.id, i);
30424
+ });
28244
30425
  bodies.forEach((b, i) => {
28245
30426
  if (b.id) m.set(b.id, i);
28246
30427
  });
28247
30428
  return m;
28248
- }, [bodies]);
30429
+ }, [bodies, vectors3d]);
28249
30430
  if (mode === "3d") {
28250
30431
  return /* @__PURE__ */ jsxRuntime.jsx(
28251
30432
  LearningScene3D,
@@ -28277,6 +30458,8 @@ var init_PhysicsCanvas = __esm({
28277
30458
  height,
28278
30459
  backgroundColor,
28279
30460
  shapes: derivedShapes,
30461
+ readouts,
30462
+ traces,
28280
30463
  interactive: interactive ?? false,
28281
30464
  animate,
28282
30465
  onShapeClick,
@@ -28427,7 +30610,7 @@ function layoutFlow(nodeIds, adjacency, roots, width, height, margin) {
28427
30610
  }
28428
30611
  return nodeIds.map((id) => positions.get(id));
28429
30612
  }
28430
- function layoutTree(nodeIds, adjacency, roots, width, height, margin) {
30613
+ function layoutTree2(nodeIds, adjacency, roots, width, height, margin) {
28431
30614
  const effectiveRoots = roots.length > 0 ? roots : [nodeIds[0]];
28432
30615
  const layers = assignLayers(nodeIds, adjacency, effectiveRoots);
28433
30616
  const maxLayer = Math.max(...Array.from(layers.values()));
@@ -28483,7 +30666,7 @@ function computeStaticLayout(mode, input) {
28483
30666
  const adjacency = buildAdjacency(nodeIds, edges);
28484
30667
  const roots = findRoots(nodeIds, adjacency);
28485
30668
  if (mode === "flow") return layoutFlow(nodeIds, adjacency, roots, width, height, margin);
28486
- if (mode === "tree") return layoutTree(nodeIds, adjacency, roots, width, height, margin);
30669
+ if (mode === "tree") return layoutTree2(nodeIds, adjacency, roots, width, height, margin);
28487
30670
  return layoutRadial(nodeIds, adjacency, roots, width, height, margin);
28488
30671
  }
28489
30672
  var init_graphViewLayouts = __esm({
@@ -29764,8 +31947,8 @@ function TableView({
29764
31947
  columns,
29765
31948
  fields,
29766
31949
  itemActions,
29767
- maxInlineActions,
29768
- itemClickEvent,
31950
+ maxInlineActions: _maxInlineActions,
31951
+ itemClickEvent = "",
29769
31952
  selectable = false,
29770
31953
  selectEvent,
29771
31954
  selectedIds,
@@ -29815,7 +31998,6 @@ function TableView({
29815
31998
  const hasMore = pageSize > 0 && visibleCount < ordered2.length;
29816
31999
  const hasRenderProp = typeof children === "function";
29817
32000
  const idField = dndItemIdField ?? "id";
29818
- const isCoarsePointer = useMediaQuery("(pointer: coarse)");
29819
32001
  React85__namespace.default.useEffect(() => {
29820
32002
  tableViewLog.debug("render", {
29821
32003
  rowCount: data.length,
@@ -29857,21 +32039,14 @@ function TableView({
29857
32039
  const dir = sortColumn === (col.field ?? col.key) && sortDirection === "asc" ? "desc" : "asc";
29858
32040
  eventBus.emit(`UI:${sortEvent}`, { column: col.field ?? col.key, direction: dir });
29859
32041
  };
29860
- const handleActionClick = (action, row) => (e) => {
29861
- e.stopPropagation();
29862
- const payload = {
29863
- id: row.id,
29864
- row
29865
- };
29866
- eventBus.emit(`UI:${action.event}`, payload);
29867
- };
32042
+ const rowClickEvent = itemClickEvent || actionDefs.find((a) => a.variant !== "danger")?.event;
29868
32043
  const handleRowClick = (row) => () => {
29869
- if (!itemClickEvent) return;
32044
+ if (!rowClickEvent) return;
29870
32045
  const payload = {
29871
32046
  id: row.id,
29872
32047
  row
29873
32048
  };
29874
- eventBus.emit(`UI:${itemClickEvent}`, payload);
32049
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
29875
32050
  };
29876
32051
  const colFloors = React85__namespace.default.useMemo(
29877
32052
  () => colDefs.map((col) => {
@@ -29887,10 +32062,7 @@ function TableView({
29887
32062
  const statusNode = isLoading ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) }) : error ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "error", children: error.message }) }) : data.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "text-center py-12", children: /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "body", color: "secondary", children: emptyMessage || t("empty.noItems") }) }) : null;
29888
32063
  const lk = LOOKS[look];
29889
32064
  const hasActions = actionDefs.length > 0;
29890
- const effectiveMaxInline = isCoarsePointer ? 0 : maxInlineActions;
29891
- const inlineActionCount = hasActions ? effectiveMaxInline != null ? Math.min(actionDefs.length, effectiveMaxInline) : actionDefs.length : 0;
29892
- const hasOverflowActions = hasActions && effectiveMaxInline != null && actionDefs.length > effectiveMaxInline;
29893
- const actionsTrack = hasActions ? `${inlineActionCount * 6 + (hasOverflowActions ? 3 : 0)}rem` : null;
32065
+ const actionsTrack = hasActions ? "3rem" : null;
29894
32066
  const gridTemplateColumns = [
29895
32067
  selectable ? "auto" : null,
29896
32068
  ...colDefs.map((c, i) => c.width ?? `minmax(${colFloors[i]}ch, 1fr)`),
@@ -29937,7 +32109,7 @@ function TableView({
29937
32109
  col.key
29938
32110
  );
29939
32111
  }),
29940
- hasActions && /* @__PURE__ */ jsxRuntime.jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)]" })
32112
+ hasActions && /* @__PURE__ */ jsxRuntime.jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)] border-l border-[var(--color-border)] h-full" })
29941
32113
  ]
29942
32114
  }
29943
32115
  );
@@ -29949,12 +32121,12 @@ function TableView({
29949
32121
  role: "row",
29950
32122
  "data-entity-row": true,
29951
32123
  "data-entity-id": id,
29952
- onClick: itemClickEvent ? handleRowClick(row) : void 0,
32124
+ onClick: rowClickEvent ? handleRowClick(row) : void 0,
29953
32125
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
29954
32126
  className: cn(
29955
32127
  "group items-center gap-3 transition-colors duration-fast",
29956
32128
  hasRenderProp ? "flex" : "grid",
29957
- itemClickEvent && "cursor-pointer",
32129
+ rowClickEvent && "cursor-pointer",
29958
32130
  lk.rowPad,
29959
32131
  lk.divider && "border-b border-[var(--color-border)]",
29960
32132
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -29962,7 +32134,7 @@ function TableView({
29962
32134
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
29963
32135
  ),
29964
32136
  children: [
29965
- selectable && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex items-center", onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsxRuntime.jsx(
32137
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "flex items-center", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsxRuntime.jsx(
29966
32138
  Checkbox,
29967
32139
  {
29968
32140
  checked: selected.has(id),
@@ -29983,53 +32155,37 @@ function TableView({
29983
32155
  }
29984
32156
  return /* @__PURE__ */ jsxRuntime.jsx(Box, { role: "cell", className: cellBase, children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-foreground", children: formatCell(raw, col.format) }) }, col.key);
29985
32157
  }),
29986
- hasActions && /* @__PURE__ */ jsxRuntime.jsxs(
32158
+ hasActions && /* @__PURE__ */ jsxRuntime.jsx(
29987
32159
  HStack,
29988
32160
  {
29989
32161
  gap: "xs",
29990
- onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
32162
+ onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0,
29991
32163
  className: cn(
29992
32164
  // Pinned: the fixed column tracks routinely overflow the caller's
29993
- // scroll container, which used to leave the actions off-screen.
29994
- // Opaque so scrolled cells pass underneath it.
32165
+ // scroll container, which would leave the kebab off-screen.
32166
+ // Opaque + hairline edge so it reads as a pinned column, not a
32167
+ // floating control, while scrolled cells pass underneath.
29995
32168
  "justify-end flex-shrink-0 sticky right-0 z-[1] transition-colors",
32169
+ "border-l border-[var(--color-border)]",
29996
32170
  lk.striped && index % 2 === 1 ? "bg-[var(--color-surface-subtle)]" : "bg-[var(--color-card)] group-hover:bg-[var(--color-surface-subtle)]"
29997
32171
  ),
29998
- children: [
29999
- (effectiveMaxInline != null ? actionDefs.slice(0, effectiveMaxInline) : actionDefs).map((action, i) => /* @__PURE__ */ jsxRuntime.jsxs(
30000
- Button,
30001
- {
30002
- variant: action.variant === "primary" ? "primary" : "ghost",
30003
- size: "sm",
30004
- onClick: handleActionClick(action, row),
30005
- "data-testid": `action-${action.event}`,
30006
- "data-row-id": String(row.id),
30007
- className: cn(action.variant === "danger" && "text-error hover:text-error hover:bg-error/10"),
30008
- children: [
30009
- action.icon && renderIconInput3(action.icon, { size: "xs", className: "mr-1" }),
30010
- action.label
30011
- ]
30012
- },
30013
- i
30014
- )),
30015
- effectiveMaxInline != null && actionDefs.length > effectiveMaxInline && /* @__PURE__ */ jsxRuntime.jsx(
30016
- Menu,
30017
- {
30018
- position: "bottom-end",
30019
- 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" }) }),
30020
- items: actionDefs.slice(effectiveMaxInline).map((action) => ({
30021
- label: action.label,
30022
- icon: action.icon,
30023
- event: action.event,
30024
- variant: action.variant === "danger" ? "danger" : "default",
30025
- onClick: () => eventBus.emit(`UI:${action.event}`, {
30026
- id: row.id,
30027
- row
30028
- })
30029
- }))
30030
- }
30031
- )
30032
- ]
32172
+ children: /* @__PURE__ */ jsxRuntime.jsx(
32173
+ Menu,
32174
+ {
32175
+ position: "bottom-end",
32176
+ trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", "data-row-id": String(row.id), children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
32177
+ items: actionDefs.map((action) => ({
32178
+ label: action.label,
32179
+ icon: action.icon,
32180
+ event: action.event,
32181
+ variant: action.variant === "danger" ? "danger" : "default",
32182
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
32183
+ id: row.id,
32184
+ row
32185
+ })
32186
+ }))
32187
+ }
32188
+ )
30033
32189
  }
30034
32190
  )
30035
32191
  ]
@@ -30072,7 +32228,6 @@ var init_TableView = __esm({
30072
32228
  init_format();
30073
32229
  init_getNestedValue();
30074
32230
  init_useEventBus();
30075
- init_useMediaQuery();
30076
32231
  init_Box();
30077
32232
  init_Stack();
30078
32233
  init_Typography();
@@ -45012,6 +47167,7 @@ var init_component_registry_generated = __esm({
45012
47167
  init_ActionTile();
45013
47168
  init_ActivationBlock();
45014
47169
  init_ComponentPatterns();
47170
+ init_AlgoGraphCanvas();
45015
47171
  init_AlgorithmCanvas();
45016
47172
  init_AnimatedCounter();
45017
47173
  init_AnimatedGraphic();
@@ -45275,6 +47431,7 @@ var init_component_registry_generated = __esm({
45275
47431
  "ActivationBlock": ActivationBlock,
45276
47432
  "Alert": AlertPattern,
45277
47433
  "AlertPattern": AlertPattern,
47434
+ "AlgoGraphCanvas": AlgoGraphCanvas,
45278
47435
  "AlgorithmCanvas": AlgorithmCanvas,
45279
47436
  "AnimatedCounter": AnimatedCounter,
45280
47437
  "AnimatedGraphic": AnimatedGraphic,
@@ -46601,9 +48758,9 @@ var log3 = logger.createLogger("almadar:ui:effects:client-handlers");
46601
48758
  function createClientEffectHandlers(options) {
46602
48759
  const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
46603
48760
  return {
46604
- emit: (event, payload) => {
48761
+ emit: (event, payload, source) => {
46605
48762
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
46606
- eventBus.emit(prefixedEvent, payload);
48763
+ eventBus.emit(prefixedEvent, payload, source);
46607
48764
  },
46608
48765
  persist: async () => {
46609
48766
  log3.warn("persist is server-side only, ignored on client");
@@ -46938,7 +49095,7 @@ function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
46938
49095
  }
46939
49096
  };
46940
49097
  ctx.emit = (event, payload) => {
46941
- emit(event, payload);
49098
+ emit(event, payload, { trait: traitName, tick: tick.name });
46942
49099
  };
46943
49100
  if (tick.guard !== void 0 && !evaluator.evaluateGuard(tick.guard, ctx)) {
46944
49101
  tickLog.debug("guard-blocked", { traitName, tick: tick.name, state: currentState });
@@ -47390,6 +49547,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
47390
49547
  }
47391
49548
  const effectContext = {
47392
49549
  traitName,
49550
+ orbitalName: orbitalsByTrait?.[traitName],
47393
49551
  state: previousState,
47394
49552
  transition: `${previousState}->${newState}`,
47395
49553
  linkedEntity,
@@ -47466,7 +49624,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
47466
49624
  });
47467
49625
  }
47468
49626
  return emittedDuringExec;
47469
- }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot]);
49627
+ }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot, orbitalsByTrait]);
47470
49628
  const runTickEffects = React85.useCallback((tick, binding) => {
47471
49629
  const traitName = binding.trait.name;
47472
49630
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
@@ -47502,9 +49660,9 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
47502
49660
  log: tickLog
47503
49661
  });
47504
49662
  }, [executeTransitionEffects, sharedEntityStore]);
47505
- const emitFromSharedWriter = React85.useCallback((event, payload) => {
49663
+ const emitFromSharedWriter = React85.useCallback((event, payload, source) => {
47506
49664
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
47507
- eventBus.emit(prefixedEvent, payload);
49665
+ eventBus.emit(prefixedEvent, payload, source);
47508
49666
  }, [eventBus]);
47509
49667
  React85.useEffect(() => {
47510
49668
  const scheduler = runtime.createTickScheduler();
@@ -48358,11 +50516,11 @@ function SchemaRunner({ schema, serverUrl, transport, mockData, pageName, routeP
48358
50516
  for (const orb of schema.orbitals) {
48359
50517
  for (const pageRef of orb.pages ?? []) {
48360
50518
  const name = typeof pageRef === "object" && pageRef !== null ? pageRef.name : void 0;
48361
- if (name === pageName) return orb.theme;
50519
+ if (name === pageName) return orb.theme ?? schema.theme;
48362
50520
  }
48363
50521
  }
48364
50522
  }
48365
- return schema.orbitals[0]?.theme;
50523
+ return schema.orbitals[0]?.theme ?? schema.theme;
48366
50524
  }, [schema, pageName]);
48367
50525
  const inner = /* @__PURE__ */ jsxRuntime.jsx(providers.VerificationProvider, { enabled: true, children: /* @__PURE__ */ jsxRuntime.jsx(
48368
50526
  providers.EntitySchemaProvider,