@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.
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import * as React77 from 'react';
3
- import React77__default, { createContext, useContext, useMemo, useRef, useEffect, useCallback, useState, useLayoutEffect, useId, Suspense, useSyncExternalStore, lazy } from 'react';
3
+ import React77__default, { createContext, useContext, useMemo, useRef, useEffect, useCallback, useState, useLayoutEffect, useId, Suspense, lazy, useSyncExternalStore } from 'react';
4
4
  import { clsx } from 'clsx';
5
5
  import { twMerge } from 'tailwind-merge';
6
6
  import { EventBusContext, useTraitScopeChain, useCurrentPagePath, useGameAudioContextOptional, useEntitySchemaOptional, useEntityBindingSnapshot, useTraitScope, TraitScopeProvider } from '@almadar/ui/providers';
@@ -1081,7 +1081,7 @@ function useEventBus() {
1081
1081
  return {
1082
1082
  ...baseBus,
1083
1083
  emit: (type, payload, source) => {
1084
- if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
1084
+ if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".") && !source?.trait) {
1085
1085
  scopeLog.warn("emit:bare-key-no-scope", { type });
1086
1086
  }
1087
1087
  baseBus.emit(type, payload, source);
@@ -8407,6 +8407,14 @@ function shapeBounds(shape) {
8407
8407
  w: shape.radius * 2 + 8,
8408
8408
  h: shape.radius * 2 + 8
8409
8409
  };
8410
+ case "ellipse":
8411
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
8412
+ return {
8413
+ x: shape.x - shape.width / 2 - 4,
8414
+ y: shape.y - shape.height / 2 - 4,
8415
+ w: shape.width + 8,
8416
+ h: shape.height + 8
8417
+ };
8410
8418
  case "rect":
8411
8419
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
8412
8420
  return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
@@ -8440,13 +8448,14 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
8440
8448
  ctx.closePath();
8441
8449
  ctx.fill();
8442
8450
  }
8443
- function drawShape(ctx, shape, width, height) {
8451
+ function drawShape(ctx, shape, width, height, allShapes) {
8444
8452
  ctx.save();
8445
8453
  const opacity = shape.opacity ?? 1;
8446
8454
  ctx.globalAlpha = opacity;
8447
8455
  const stroke = resolveColor2(shape.color, ctx, "#333333");
8448
8456
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
8449
8457
  ctx.lineWidth = shape.lineWidth ?? 2;
8458
+ if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
8450
8459
  switch (shape.type) {
8451
8460
  case "grid": {
8452
8461
  const step = shape.step ?? 40;
@@ -8512,6 +8521,20 @@ function drawShape(ctx, shape, width, height) {
8512
8521
  ctx.stroke();
8513
8522
  break;
8514
8523
  }
8524
+ case "ellipse": {
8525
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
8526
+ const startAngle = (shape.startAngle ?? 0) * Math.PI / 180;
8527
+ const endAngle = (shape.endAngle ?? 360) * Math.PI / 180;
8528
+ ctx.beginPath();
8529
+ ctx.ellipse(shape.x, shape.y, shape.width / 2, shape.height / 2, 0, startAngle, endAngle);
8530
+ if (fill) {
8531
+ ctx.fillStyle = fill;
8532
+ ctx.fill();
8533
+ }
8534
+ ctx.strokeStyle = stroke;
8535
+ ctx.stroke();
8536
+ break;
8537
+ }
8515
8538
  case "rect": {
8516
8539
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
8517
8540
  if (fill) {
@@ -8558,21 +8581,153 @@ function drawShape(ctx, shape, width, height) {
8558
8581
  ctx.fillText(shape.text, shape.x, shape.y);
8559
8582
  break;
8560
8583
  }
8584
+ case "venn-region": {
8585
+ const resolveCircles = (ids) => (ids ?? []).flatMap((id) => {
8586
+ const c = allShapes.find((s) => s.type === "circle" && s.id === id);
8587
+ return c && c.x != null && c.y != null && c.radius != null ? [{ x: c.x, y: c.y, radius: c.radius }] : [];
8588
+ });
8589
+ const inside = resolveCircles(shape.inside);
8590
+ if (inside.length === 0) break;
8591
+ const outside = resolveCircles(shape.outside);
8592
+ const off = document.createElement("canvas");
8593
+ off.width = ctx.canvas.width;
8594
+ off.height = ctx.canvas.height;
8595
+ const octx = off.getContext("2d");
8596
+ if (!octx) break;
8597
+ octx.setTransform(ctx.getTransform());
8598
+ for (const c of inside) {
8599
+ const p = new Path2D();
8600
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
8601
+ octx.clip(p);
8602
+ }
8603
+ octx.fillStyle = fill ?? stroke;
8604
+ octx.fillRect(0, 0, width, height);
8605
+ octx.globalCompositeOperation = "destination-out";
8606
+ for (const c of outside) {
8607
+ const p = new Path2D();
8608
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
8609
+ octx.fill(p);
8610
+ }
8611
+ ctx.save();
8612
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
8613
+ ctx.drawImage(off, 0, 0);
8614
+ ctx.restore();
8615
+ break;
8616
+ }
8561
8617
  }
8562
8618
  ctx.restore();
8563
8619
  }
8564
- var LearningCanvas;
8620
+ function readoutShapes(readouts, width) {
8621
+ const out = [];
8622
+ const chipH = 18;
8623
+ const gap = 6;
8624
+ let rightEdge = width - 6;
8625
+ let rowY = 6;
8626
+ for (const readout of readouts) {
8627
+ const text = `${readout.label}: ${String(readout.value)}`;
8628
+ const chipW = Math.min(170, Math.max(34, text.length * 6 + 12));
8629
+ let chipX = rightEdge - chipW;
8630
+ if (chipX < 4) {
8631
+ rowY += chipH + 4;
8632
+ rightEdge = width - 6;
8633
+ chipX = rightEdge - chipW;
8634
+ }
8635
+ const color = readout.color ?? "#334155";
8636
+ out.push({ type: "rect", x: chipX, y: rowY, width: chipW, height: chipH, color, fill: color });
8637
+ out.push({
8638
+ type: "text",
8639
+ x: chipX + chipW / 2,
8640
+ y: rowY + chipH / 2,
8641
+ text,
8642
+ color: "#ffffff",
8643
+ fontSize: 10,
8644
+ align: "center"
8645
+ });
8646
+ rightEdge = chipX - gap;
8647
+ }
8648
+ return out;
8649
+ }
8650
+ function traceShapes(panel, k, width, height) {
8651
+ const w = panel.width ?? Math.round(width * 0.32);
8652
+ const h = panel.height ?? Math.round(height * 0.28);
8653
+ const x = panel.x ?? width - w - 8;
8654
+ const y = panel.y ?? height - h - 8 - k * (h + 8);
8655
+ const allSamples = panel.series.flatMap((series) => series.samples);
8656
+ let xLo = Math.min(...allSamples.map((p) => p.x));
8657
+ let xHi = Math.max(...allSamples.map((p) => p.x));
8658
+ let yLo = Math.min(...allSamples.map((p) => p.y));
8659
+ let yHi = Math.max(...allSamples.map((p) => p.y));
8660
+ if (xLo === xHi) {
8661
+ xLo -= 1;
8662
+ xHi += 1;
8663
+ }
8664
+ if (yLo === yHi) {
8665
+ yLo -= 1;
8666
+ yHi += 1;
8667
+ }
8668
+ const backgroundColor = panel.backgroundColor ?? "#ffffff";
8669
+ const frameColor = panel.frameColor ?? "#94a3b8";
8670
+ const out = [];
8671
+ out.push({
8672
+ type: "rect",
8673
+ x,
8674
+ y,
8675
+ width: w,
8676
+ height: h,
8677
+ color: backgroundColor,
8678
+ fill: backgroundColor,
8679
+ opacity: panel.backgroundOpacity ?? 0.85
8680
+ });
8681
+ out.push({ type: "rect", x, y, width: w, height: h, color: frameColor, lineWidth: 1 });
8682
+ panel.series.forEach((series, j) => {
8683
+ const color = series.color ?? TRACE_SERIES_COLORS[j % TRACE_SERIES_COLORS.length];
8684
+ const mapped = series.samples.map((p) => ({
8685
+ x: x + 4 + (p.x - xLo) / (xHi - xLo) * (w - 8),
8686
+ y: y + h - 4 - (p.y - yLo) / (yHi - yLo) * (h - 8)
8687
+ }));
8688
+ for (let i = 1; i < mapped.length; i++) {
8689
+ out.push({
8690
+ type: "line",
8691
+ x1: mapped[i - 1].x,
8692
+ y1: mapped[i - 1].y,
8693
+ x2: mapped[i].x,
8694
+ y2: mapped[i].y,
8695
+ color,
8696
+ lineWidth: 1.5
8697
+ });
8698
+ }
8699
+ if (mapped.length > 0) {
8700
+ const last = mapped[mapped.length - 1];
8701
+ out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
8702
+ }
8703
+ if (series.label) {
8704
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
8705
+ }
8706
+ });
8707
+ if (panel.yLabel) {
8708
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
8709
+ }
8710
+ if (panel.xLabel) {
8711
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
8712
+ }
8713
+ return out;
8714
+ }
8715
+ var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
8565
8716
  var init_LearningCanvas = __esm({
8566
8717
  "components/learning/atoms/LearningCanvas.tsx"() {
8567
8718
  "use client";
8568
8719
  init_cn();
8569
8720
  init_useEventBus();
8721
+ DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
8722
+ TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
8570
8723
  LearningCanvas = ({
8571
8724
  className,
8572
8725
  width = 600,
8573
8726
  height = 400,
8574
8727
  backgroundColor,
8575
8728
  shapes = [],
8729
+ readouts,
8730
+ traces,
8576
8731
  interactive = false,
8577
8732
  animate = false,
8578
8733
  onShapeClick,
@@ -8598,6 +8753,12 @@ var init_LearningCanvas = __esm({
8598
8753
  }
8599
8754
  return -1;
8600
8755
  }, [shapes]);
8756
+ const derivedShapes = useMemo(() => {
8757
+ if (!traces?.length && !readouts?.length) return shapes;
8758
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
8759
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
8760
+ return [...shapes, ...traceOut, ...readoutOut];
8761
+ }, [shapes, traces, readouts, width, height]);
8601
8762
  const draw = useCallback(() => {
8602
8763
  const canvas = canvasRef.current;
8603
8764
  if (!canvas) return;
@@ -8614,13 +8775,13 @@ var init_LearningCanvas = __esm({
8614
8775
  ctx.fillStyle = backgroundColor;
8615
8776
  ctx.fillRect(0, 0, width, height);
8616
8777
  }
8617
- for (const shape of shapes) {
8618
- if (shape.type !== "text") drawShape(ctx, shape, width, height);
8778
+ for (const shape of derivedShapes) {
8779
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
8619
8780
  }
8620
- for (const shape of shapes) {
8621
- if (shape.type === "text") drawShape(ctx, shape, width, height);
8781
+ for (const shape of derivedShapes) {
8782
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
8622
8783
  }
8623
- }, [width, height, backgroundColor, shapes]);
8784
+ }, [width, height, backgroundColor, derivedShapes]);
8624
8785
  useEffect(() => {
8625
8786
  draw();
8626
8787
  }, [draw]);
@@ -8688,7 +8849,363 @@ var init_LearningCanvas = __esm({
8688
8849
  };
8689
8850
  }
8690
8851
  });
8691
- var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, AlgorithmCanvas;
8852
+ function layoutCircle(nodes, width, height) {
8853
+ const cx = width / 2;
8854
+ const cy = height / 2;
8855
+ const radius = Math.max(10, Math.min(cx, cy) - 40);
8856
+ const positions = /* @__PURE__ */ new Map();
8857
+ const n = nodes.length;
8858
+ nodes.forEach((node, i) => {
8859
+ const angle = 2 * Math.PI * i / Math.max(n, 1) - Math.PI / 2;
8860
+ positions.set(node.id, { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) });
8861
+ });
8862
+ return positions;
8863
+ }
8864
+ function layoutTree(nodes, edges, root, width, height) {
8865
+ const nodeIds = nodes.map((n) => n.id);
8866
+ const idSet = new Set(nodeIds);
8867
+ const childrenOf = /* @__PURE__ */ new Map();
8868
+ const hasIncoming = /* @__PURE__ */ new Set();
8869
+ for (const e of edges) {
8870
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
8871
+ const list = childrenOf.get(e.from) ?? [];
8872
+ list.push(e.to);
8873
+ childrenOf.set(e.from, list);
8874
+ hasIncoming.add(e.to);
8875
+ }
8876
+ const depth = /* @__PURE__ */ new Map();
8877
+ const treeChildren = /* @__PURE__ */ new Map();
8878
+ const visited = /* @__PURE__ */ new Set();
8879
+ const bfsFrom = (start) => {
8880
+ if (visited.has(start)) return;
8881
+ visited.add(start);
8882
+ depth.set(start, 0);
8883
+ const queue = [start];
8884
+ while (queue.length > 0) {
8885
+ const u = queue.shift();
8886
+ for (const v of childrenOf.get(u) ?? []) {
8887
+ if (visited.has(v)) continue;
8888
+ visited.add(v);
8889
+ depth.set(v, (depth.get(u) ?? 0) + 1);
8890
+ const list = treeChildren.get(u) ?? [];
8891
+ list.push(v);
8892
+ treeChildren.set(u, list);
8893
+ queue.push(v);
8894
+ }
8895
+ }
8896
+ };
8897
+ const primaryRoot = root && idSet.has(root) ? root : nodeIds.find((id) => !hasIncoming.has(id)) ?? nodeIds[0];
8898
+ const rootsOrder = [];
8899
+ if (primaryRoot !== void 0) {
8900
+ bfsFrom(primaryRoot);
8901
+ rootsOrder.push(primaryRoot);
8902
+ }
8903
+ for (const id of nodeIds) {
8904
+ if (!visited.has(id)) {
8905
+ bfsFrom(id);
8906
+ rootsOrder.push(id);
8907
+ }
8908
+ }
8909
+ let leafCounter = 0;
8910
+ const xSlot = /* @__PURE__ */ new Map();
8911
+ const assignXSlot = (u) => {
8912
+ const children = treeChildren.get(u) ?? [];
8913
+ if (children.length === 0) {
8914
+ const slot = leafCounter++;
8915
+ xSlot.set(u, slot);
8916
+ return slot;
8917
+ }
8918
+ const childSlots = children.map(assignXSlot);
8919
+ const avg = childSlots.reduce((a, b) => a + b, 0) / childSlots.length;
8920
+ xSlot.set(u, avg);
8921
+ return avg;
8922
+ };
8923
+ for (const r of rootsOrder) assignXSlot(r);
8924
+ let maxDepth = 0;
8925
+ for (const d of depth.values()) maxDepth = Math.max(maxDepth, d);
8926
+ const colWidth = width / Math.max(1, leafCounter);
8927
+ const rowHeight = height / (maxDepth + 1);
8928
+ const positions = /* @__PURE__ */ new Map();
8929
+ for (const id of nodeIds) {
8930
+ const slot = xSlot.get(id) ?? 0;
8931
+ const d = depth.get(id) ?? 0;
8932
+ positions.set(id, { x: slot * colWidth + colWidth / 2, y: d * rowHeight + rowHeight / 2 });
8933
+ }
8934
+ return positions;
8935
+ }
8936
+ function layoutLayered(nodes, edges, width, height) {
8937
+ const nodeIds = nodes.map((n) => n.id);
8938
+ const idSet = new Set(nodeIds);
8939
+ const adj = /* @__PURE__ */ new Map();
8940
+ const remainingIndegree = /* @__PURE__ */ new Map();
8941
+ for (const id of nodeIds) remainingIndegree.set(id, 0);
8942
+ for (const e of edges) {
8943
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
8944
+ const list = adj.get(e.from) ?? [];
8945
+ list.push(e.to);
8946
+ adj.set(e.from, list);
8947
+ remainingIndegree.set(e.to, (remainingIndegree.get(e.to) ?? 0) + 1);
8948
+ }
8949
+ const layer = /* @__PURE__ */ new Map();
8950
+ const dequeued = /* @__PURE__ */ new Set();
8951
+ const queue = [];
8952
+ for (const id of nodeIds) {
8953
+ if ((remainingIndegree.get(id) ?? 0) === 0) {
8954
+ layer.set(id, 0);
8955
+ queue.push(id);
8956
+ }
8957
+ }
8958
+ while (queue.length > 0) {
8959
+ const u = queue.shift();
8960
+ dequeued.add(u);
8961
+ for (const v of adj.get(u) ?? []) {
8962
+ const candidate = (layer.get(u) ?? 0) + 1;
8963
+ layer.set(v, Math.max(layer.get(v) ?? 0, candidate));
8964
+ remainingIndegree.set(v, (remainingIndegree.get(v) ?? 0) - 1);
8965
+ if ((remainingIndegree.get(v) ?? 0) === 0 && !dequeued.has(v)) {
8966
+ queue.push(v);
8967
+ }
8968
+ }
8969
+ }
8970
+ let baseMaxLayer = 0;
8971
+ for (const id of nodeIds) {
8972
+ if (dequeued.has(id)) baseMaxLayer = Math.max(baseMaxLayer, layer.get(id) ?? 0);
8973
+ }
8974
+ const cycleLayer = baseMaxLayer + 1;
8975
+ let maxLayer = baseMaxLayer;
8976
+ for (const id of nodeIds) {
8977
+ if (!dequeued.has(id)) {
8978
+ layer.set(id, cycleLayer);
8979
+ maxLayer = cycleLayer;
8980
+ }
8981
+ }
8982
+ const colWidth = width / Math.max(1, maxLayer + 1);
8983
+ const byLayer = /* @__PURE__ */ new Map();
8984
+ for (const id of nodeIds) {
8985
+ const l = layer.get(id) ?? 0;
8986
+ const list = byLayer.get(l) ?? [];
8987
+ list.push(id);
8988
+ byLayer.set(l, list);
8989
+ }
8990
+ const positions = /* @__PURE__ */ new Map();
8991
+ for (const [l, ids] of byLayer) {
8992
+ const rowHeight = height / ids.length;
8993
+ ids.forEach((id, i) => {
8994
+ positions.set(id, { x: l * colWidth + colWidth / 2, y: i * rowHeight + rowHeight / 2 });
8995
+ });
8996
+ }
8997
+ return positions;
8998
+ }
8999
+ function computePositions(nodes, edges, layout, root, width, height) {
9000
+ switch (layout) {
9001
+ case "circle":
9002
+ return layoutCircle(nodes, width, height);
9003
+ case "tree":
9004
+ return layoutTree(nodes, edges, root, width, height);
9005
+ case "layered":
9006
+ return layoutLayered(nodes, edges, width, height);
9007
+ case "manual":
9008
+ default: {
9009
+ const positions = /* @__PURE__ */ new Map();
9010
+ for (const n of nodes) positions.set(n.id, { x: n.x ?? 0, y: n.y ?? 0 });
9011
+ return positions;
9012
+ }
9013
+ }
9014
+ }
9015
+ var NODE_STATE_COLOR, EDGE_STATE_COLOR, DEFAULT_NODE_RADIUS, AlgoGraphCanvas;
9016
+ var init_AlgoGraphCanvas = __esm({
9017
+ "components/learning/molecules/AlgoGraphCanvas.tsx"() {
9018
+ "use client";
9019
+ init_atoms();
9020
+ init_Stack();
9021
+ init_LearningCanvas();
9022
+ NODE_STATE_COLOR = {
9023
+ unvisited: "#cbd5e1",
9024
+ frontier: "#f59e0b",
9025
+ current: "#ef4444",
9026
+ visited: "#22c55e",
9027
+ goal: "#8b5cf6",
9028
+ path: "#0ea5e9"
9029
+ };
9030
+ EDGE_STATE_COLOR = {
9031
+ default: "#9ca3af",
9032
+ tree: "#16a34a",
9033
+ relaxed: "#f59e0b",
9034
+ candidate: "#38bdf8",
9035
+ path: "#dc2626"
9036
+ };
9037
+ DEFAULT_NODE_RADIUS = 18;
9038
+ AlgoGraphCanvas = ({
9039
+ className,
9040
+ width = 600,
9041
+ height = 400,
9042
+ title,
9043
+ backgroundColor,
9044
+ nodes = [],
9045
+ edges = [],
9046
+ layout = "manual",
9047
+ root,
9048
+ shapes = [],
9049
+ interactive = false,
9050
+ animate = false,
9051
+ onShapeClick,
9052
+ onNodeClick,
9053
+ isLoading,
9054
+ error
9055
+ }) => {
9056
+ const nodeById = useMemo(() => {
9057
+ const m = /* @__PURE__ */ new Map();
9058
+ for (const n of nodes) m.set(n.id, n);
9059
+ return m;
9060
+ }, [nodes]);
9061
+ const nodeIndexById = useMemo(() => {
9062
+ const m = /* @__PURE__ */ new Map();
9063
+ nodes.forEach((n, i) => m.set(n.id, i));
9064
+ return m;
9065
+ }, [nodes]);
9066
+ const derivedShapes = useMemo(() => {
9067
+ const out = [];
9068
+ const positions = computePositions(nodes, edges, layout, root, width, height);
9069
+ const edgeGeoms = [];
9070
+ for (const e of edges) {
9071
+ const a = nodeById.get(e.from);
9072
+ const b = nodeById.get(e.to);
9073
+ const posA = positions.get(e.from);
9074
+ const posB = positions.get(e.to);
9075
+ if (!a || !b || !posA || !posB) continue;
9076
+ const rA = a.radius ?? DEFAULT_NODE_RADIUS;
9077
+ const rB = b.radius ?? DEFAULT_NODE_RADIUS;
9078
+ const dx = posB.x - posA.x;
9079
+ const dy = posB.y - posA.y;
9080
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
9081
+ const ux = dx / dist;
9082
+ const uy = dy / dist;
9083
+ edgeGeoms.push({
9084
+ directed: e.directed ?? false,
9085
+ x1: posA.x + ux * rA,
9086
+ y1: posA.y + uy * rA,
9087
+ x2: posB.x - ux * rB,
9088
+ y2: posB.y - uy * rB,
9089
+ color: e.color ?? EDGE_STATE_COLOR[e.state ?? "default"],
9090
+ label: e.label ?? (e.weight != null ? String(e.weight) : void 0)
9091
+ });
9092
+ }
9093
+ for (const g of edgeGeoms) {
9094
+ out.push({
9095
+ type: g.directed ? "arrow" : "line",
9096
+ x1: g.x1,
9097
+ y1: g.y1,
9098
+ x2: g.x2,
9099
+ y2: g.y2,
9100
+ color: g.color,
9101
+ lineWidth: 2
9102
+ });
9103
+ }
9104
+ for (const g of edgeGeoms) {
9105
+ if (g.label === void 0) continue;
9106
+ const midX = (g.x1 + g.x2) / 2;
9107
+ const midY = (g.y1 + g.y2) / 2;
9108
+ const dx = g.x2 - g.x1;
9109
+ const dy = g.y2 - g.y1;
9110
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
9111
+ const perpX = -(dy / dist);
9112
+ const perpY = dx / dist;
9113
+ out.push({
9114
+ type: "text",
9115
+ x: midX + perpX * 10,
9116
+ y: midY + perpY * 10,
9117
+ text: g.label,
9118
+ fontSize: 11,
9119
+ align: "center",
9120
+ color: "#374151"
9121
+ });
9122
+ }
9123
+ const nodeGeoms = [];
9124
+ for (const n of nodes) {
9125
+ const pos = positions.get(n.id);
9126
+ if (!pos) continue;
9127
+ nodeGeoms.push({
9128
+ id: n.id,
9129
+ x: pos.x,
9130
+ y: pos.y,
9131
+ radius: n.radius ?? DEFAULT_NODE_RADIUS,
9132
+ color: n.color ?? NODE_STATE_COLOR[n.state ?? "unvisited"],
9133
+ label: n.label,
9134
+ badge: n.badge
9135
+ });
9136
+ }
9137
+ for (const g of nodeGeoms) {
9138
+ out.push({ type: "circle", id: g.id, x: g.x, y: g.y, radius: g.radius, color: g.color, fill: `${g.color}33` });
9139
+ }
9140
+ const badgeGeoms = [];
9141
+ for (const g of nodeGeoms) {
9142
+ if (!g.badge) continue;
9143
+ const w = Math.min(42, Math.max(18, g.badge.text.length * 6 + 10));
9144
+ badgeGeoms.push({
9145
+ cx: g.x + g.radius * 0.75,
9146
+ cy: g.y - g.radius * 0.75,
9147
+ w,
9148
+ h: 14,
9149
+ // Borderless pill: same color drives both stroke and fill.
9150
+ color: g.badge.color ?? "#1e293b",
9151
+ text: g.badge.text
9152
+ });
9153
+ }
9154
+ for (const b of badgeGeoms) {
9155
+ 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 });
9156
+ }
9157
+ for (const b of badgeGeoms) {
9158
+ out.push({ type: "text", x: b.cx, y: b.cy, text: b.text, fontSize: 9, align: "center", color: "#ffffff" });
9159
+ }
9160
+ for (const g of nodeGeoms) {
9161
+ if (g.label === void 0) continue;
9162
+ out.push({
9163
+ type: "text",
9164
+ x: g.x,
9165
+ y: g.y + g.radius + 14,
9166
+ text: g.label,
9167
+ fontSize: 12,
9168
+ align: "center",
9169
+ color: "#111827"
9170
+ });
9171
+ }
9172
+ out.push(...shapes);
9173
+ return out;
9174
+ }, [nodes, edges, layout, root, width, height, nodeById, shapes]);
9175
+ const handleShapeClick = useCallback(
9176
+ (payload) => {
9177
+ if (payload.type === "circle" && payload.id) {
9178
+ const node = nodeById.get(payload.id);
9179
+ const idx = nodeIndexById.get(payload.id);
9180
+ if (node && idx !== void 0) {
9181
+ onNodeClick?.({ id: node.id, label: node.label, index: idx });
9182
+ }
9183
+ }
9184
+ onShapeClick?.(payload);
9185
+ },
9186
+ [nodeById, nodeIndexById, onNodeClick, onShapeClick]
9187
+ );
9188
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
9189
+ title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
9190
+ /* @__PURE__ */ jsx(
9191
+ LearningCanvas,
9192
+ {
9193
+ width,
9194
+ height,
9195
+ backgroundColor,
9196
+ shapes: derivedShapes,
9197
+ interactive,
9198
+ animate,
9199
+ onShapeClick: onShapeClick || onNodeClick ? handleShapeClick : void 0,
9200
+ isLoading,
9201
+ error
9202
+ }
9203
+ )
9204
+ ] }) });
9205
+ };
9206
+ }
9207
+ });
9208
+ 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;
8692
9209
  var init_AlgorithmCanvas = __esm({
8693
9210
  "components/learning/molecules/AlgorithmCanvas.tsx"() {
8694
9211
  "use client";
@@ -8700,6 +9217,43 @@ var init_AlgorithmCanvas = __esm({
8700
9217
  DEFAULT_POINTER_COLOR = "#dc2626";
8701
9218
  POINTER_BAND = 34;
8702
9219
  TOP_PAD = 26;
9220
+ PANEL_FAMILY_ORDER = ["bars", "slots", "cells", "buckets", "frames"];
9221
+ RANGE_COLOR_DEFAULT = "#3b82f6";
9222
+ RANGE_FILL_OPACITY = 0.15;
9223
+ BRACKET_TOP_OFFSET = 16;
9224
+ BRACKET_ROW_H = 14;
9225
+ BRACKET_TICK_H = 6;
9226
+ BRACKET_LABEL_OFFSET = 6;
9227
+ SLOT_EMPTY_FILL = "#f1f5f9";
9228
+ SLOT_EMPTY_STROKE = "#cbd5e1";
9229
+ SLOT_FILLED_STROKE = "#9ca3af";
9230
+ SLOT_HIGHLIGHT_DEFAULT = "#f59e0b";
9231
+ SLOT_VALUE_TEXT_COLOR = "#ffffff";
9232
+ FRAME_ACTIVE_COLOR = "#3b82f6";
9233
+ FRAME_RETURNING_COLOR = "#f59e0b";
9234
+ FRAME_DONE_COLOR = "#94a3b8";
9235
+ FRAME_LABEL_COLOR = "#ffffff";
9236
+ FRAME_DETAIL_COLOR = "#e2e8f0";
9237
+ FRAME_TWO_LINE_MIN_H = 22;
9238
+ BUCKET_INDEX_FILL = "#e2e8f0";
9239
+ BUCKET_INDEX_STROKE = "#9ca3af";
9240
+ BUCKET_INDEX_TEXT = "#374151";
9241
+ BUCKET_ENTRY_TEXT = "#ffffff";
9242
+ BUCKET_ENTRY_DEFAULT = "#3b82f6";
9243
+ BUCKET_ENTRY_HIGHLIGHT = "#f59e0b";
9244
+ BUCKET_ENTRY_PROBING = "#38bdf8";
9245
+ BUCKET_ENTRY_MIN_W = 24;
9246
+ BUCKET_ENTRY_MAX_W = 64;
9247
+ AXIS_LABEL_COLOR = "#6b7280";
9248
+ AXIS_LABEL_FONT_SIZE = 10;
9249
+ CORNER_TEXT_COLOR = "#111827";
9250
+ CORNER_FONT_SIZE = 7;
9251
+ CORNER_MIN_CELL = 28;
9252
+ CORNER_INSET_X = 3;
9253
+ CORNER_INSET_Y = 6;
9254
+ AUX_PRIMARY_RATIO = 0.6;
9255
+ AUX_LABEL_BAND = 18;
9256
+ AUX_BASELINE_PAD = 8;
8703
9257
  AlgorithmCanvas = ({
8704
9258
  className,
8705
9259
  width = 600,
@@ -8709,6 +9263,14 @@ var init_AlgorithmCanvas = __esm({
8709
9263
  bars = [],
8710
9264
  cells = [],
8711
9265
  pointers = [],
9266
+ ranges = [],
9267
+ slots = [],
9268
+ slotOrientation = "horizontal",
9269
+ frames = [],
9270
+ buckets = [],
9271
+ auxBars = [],
9272
+ rowLabels = [],
9273
+ colLabels = [],
8712
9274
  shapes = [],
8713
9275
  interactive = false,
8714
9276
  animate = false,
@@ -8718,12 +9280,35 @@ var init_AlgorithmCanvas = __esm({
8718
9280
  }) => {
8719
9281
  const derivedShapes = useMemo(() => {
8720
9282
  const out = [];
9283
+ const presence = {
9284
+ bars: bars.length > 0,
9285
+ slots: slots.length > 0,
9286
+ cells: cells.length > 0,
9287
+ buckets: buckets.length > 0,
9288
+ frames: frames.length > 0
9289
+ };
9290
+ const panelCount = PANEL_FAMILY_ORDER.filter((f3) => presence[f3]).length;
9291
+ const panelHeight = height / Math.max(1, panelCount);
9292
+ const panelY = { bars: 0, slots: 0, cells: 0, buckets: 0, frames: 0 };
9293
+ let compactIndex = 0;
9294
+ PANEL_FAMILY_ORDER.forEach((f3) => {
9295
+ if (presence[f3]) {
9296
+ panelY[f3] = compactIndex * panelHeight;
9297
+ compactIndex += 1;
9298
+ }
9299
+ });
8721
9300
  if (bars.length > 0) {
9301
+ const panelYBars = panelY.bars;
8722
9302
  const slot = width / bars.length;
8723
9303
  const barW = slot * 0.8;
8724
9304
  const gap = slot * 0.1;
8725
- const baseline = height - POINTER_BAND;
8726
- const usableH = baseline - TOP_PAD;
9305
+ const bracketRanges = ranges.filter((r) => r.kind === "bracket");
9306
+ const bracketCount = bracketRanges.length;
9307
+ const bracketHeadroom = bracketCount > 0 ? BRACKET_TOP_OFFSET + bracketCount * BRACKET_ROW_H : 0;
9308
+ const hasAux = auxBars.length > 0;
9309
+ const primaryH = hasAux ? panelHeight * AUX_PRIMARY_RATIO : panelHeight;
9310
+ const baseline = panelYBars + primaryH - POINTER_BAND;
9311
+ const usableH = baseline - (panelYBars + TOP_PAD + bracketHeadroom);
8727
9312
  const maxV = Math.max(1, ...bars.map((b) => Number.isFinite(b.value) ? b.value : 0));
8728
9313
  bars.forEach((bar, i) => {
8729
9314
  const v = Number.isFinite(bar.value) ? bar.value : 0;
@@ -8753,6 +9338,89 @@ var init_AlgorithmCanvas = __esm({
8753
9338
  });
8754
9339
  }
8755
9340
  });
9341
+ ranges.forEach((r) => {
9342
+ const kind = r.kind ?? "fill";
9343
+ if (kind !== "fill") return;
9344
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
9345
+ out.push({
9346
+ type: "rect",
9347
+ x: r.from * slot,
9348
+ y: panelYBars,
9349
+ width: (r.to - r.from + 1) * slot,
9350
+ height: primaryH,
9351
+ color,
9352
+ fill: color,
9353
+ opacity: RANGE_FILL_OPACITY
9354
+ });
9355
+ if (r.label) {
9356
+ out.push({
9357
+ type: "text",
9358
+ x: r.from * slot + 4,
9359
+ // Sits below the bracket block (if any) so fill and bracket labels never collide.
9360
+ y: panelYBars + 10 + bracketHeadroom,
9361
+ text: r.label,
9362
+ color,
9363
+ fontSize: 10,
9364
+ align: "left"
9365
+ });
9366
+ }
9367
+ });
9368
+ bracketRanges.forEach((r, i) => {
9369
+ const bracketY = panelYBars + BRACKET_TOP_OFFSET + i * BRACKET_ROW_H;
9370
+ const x1 = r.from * slot + slot * 0.1;
9371
+ const x2 = (r.to + 1) * slot - slot * 0.1;
9372
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
9373
+ out.push({ type: "line", x1, y1: bracketY, x2, y2: bracketY, color, lineWidth: 2 });
9374
+ out.push({ type: "line", x1, y1: bracketY, x2: x1, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
9375
+ out.push({ type: "line", x1: x2, y1: bracketY, x2, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
9376
+ if (r.label) {
9377
+ out.push({
9378
+ type: "text",
9379
+ x: (x1 + x2) / 2,
9380
+ y: bracketY - BRACKET_LABEL_OFFSET,
9381
+ text: r.label,
9382
+ color,
9383
+ fontSize: 10,
9384
+ align: "center"
9385
+ });
9386
+ }
9387
+ });
9388
+ if (hasAux) {
9389
+ const auxH = panelHeight - primaryH;
9390
+ const slot2 = width / auxBars.length;
9391
+ const auxBaseline = panelYBars + primaryH + auxH - AUX_BASELINE_PAD;
9392
+ const auxUsableH = auxBaseline - (panelYBars + primaryH + AUX_LABEL_BAND);
9393
+ const maxAuxV = Math.max(1, ...auxBars.map((b) => Number.isFinite(b.value) ? b.value : 0));
9394
+ auxBars.forEach((bar, i) => {
9395
+ const v = Number.isFinite(bar.value) ? bar.value : 0;
9396
+ const bh = Math.max(0, v / maxAuxV * auxUsableH);
9397
+ const x = i * slot2 + slot2 * 0.1;
9398
+ const w = slot2 * 0.8;
9399
+ const color = bar.color ?? DEFAULT_BAR_COLOR;
9400
+ out.push({
9401
+ type: "rect",
9402
+ id: `auxbar-${i}`,
9403
+ x,
9404
+ y: auxBaseline - bh,
9405
+ width: w,
9406
+ height: bh,
9407
+ color,
9408
+ fill: color
9409
+ });
9410
+ const label = bar.label ?? (auxBars.length <= 24 ? String(v) : void 0);
9411
+ if (label) {
9412
+ out.push({
9413
+ type: "text",
9414
+ x: x + w / 2,
9415
+ y: auxBaseline - bh - 8,
9416
+ text: label,
9417
+ color: "#374151",
9418
+ fontSize: 11,
9419
+ align: "center"
9420
+ });
9421
+ }
9422
+ });
9423
+ }
8756
9424
  pointers.forEach((p) => {
8757
9425
  if (p.index < 0 || p.index >= bars.length) return;
8758
9426
  const cx = p.index * slot + slot / 2;
@@ -8760,7 +9428,7 @@ var init_AlgorithmCanvas = __esm({
8760
9428
  out.push({
8761
9429
  type: "arrow",
8762
9430
  x1: cx,
8763
- y1: height - 6,
9431
+ y1: panelYBars + primaryH - 18,
8764
9432
  x2: cx,
8765
9433
  y2: baseline + 4,
8766
9434
  color,
@@ -8770,7 +9438,7 @@ var init_AlgorithmCanvas = __esm({
8770
9438
  out.push({
8771
9439
  type: "text",
8772
9440
  x: cx,
8773
- y: height - 22,
9441
+ y: panelYBars + primaryH - 8,
8774
9442
  text: p.label,
8775
9443
  color,
8776
9444
  fontSize: 11,
@@ -8779,14 +9447,111 @@ var init_AlgorithmCanvas = __esm({
8779
9447
  }
8780
9448
  });
8781
9449
  }
9450
+ if (slots.length > 0) {
9451
+ const panelYSlots = panelY.slots;
9452
+ const n = slots.length;
9453
+ const vertical = slotOrientation === "vertical";
9454
+ const vBoxH = panelHeight / n;
9455
+ const vBoxW = Math.min(width * 0.5, 120);
9456
+ const vBoxX = (width - vBoxW) / 2;
9457
+ const hCellW = width / n;
9458
+ const hBoxW = hCellW * 0.82;
9459
+ const hBoxH = Math.min(panelHeight * 0.6, 48);
9460
+ const hBoxY = panelYSlots + (panelHeight - hBoxH) / 2;
9461
+ 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 };
9462
+ slots.forEach((s, i) => {
9463
+ const box = slotBox(i);
9464
+ const state = s.state ?? "filled";
9465
+ const fill = state === "empty" ? SLOT_EMPTY_FILL : state === "highlight" ? s.color ?? SLOT_HIGHLIGHT_DEFAULT : s.color ?? DEFAULT_BAR_COLOR;
9466
+ const stroke = state === "empty" ? SLOT_EMPTY_STROKE : SLOT_FILLED_STROKE;
9467
+ out.push({
9468
+ type: "rect",
9469
+ id: `slot-${i}`,
9470
+ x: box.x,
9471
+ y: box.y,
9472
+ width: box.width,
9473
+ height: box.height,
9474
+ color: stroke,
9475
+ fill
9476
+ });
9477
+ if (s.value != null && state !== "empty") {
9478
+ out.push({
9479
+ type: "text",
9480
+ x: box.x + box.width / 2,
9481
+ y: box.y + box.height / 2,
9482
+ text: String(s.value),
9483
+ color: SLOT_VALUE_TEXT_COLOR,
9484
+ fontSize: 12,
9485
+ align: "center"
9486
+ });
9487
+ }
9488
+ });
9489
+ if (bars.length === 0) {
9490
+ pointers.forEach((p) => {
9491
+ if (p.index < 0 || p.index >= slots.length) return;
9492
+ const box = slotBox(p.index);
9493
+ const color = p.color ?? DEFAULT_POINTER_COLOR;
9494
+ if (vertical) {
9495
+ const cy = box.y + box.height / 2;
9496
+ out.push({
9497
+ type: "arrow",
9498
+ x1: box.x + box.width + 34,
9499
+ y1: cy,
9500
+ x2: box.x + box.width + 4,
9501
+ y2: cy,
9502
+ color,
9503
+ lineWidth: 2
9504
+ });
9505
+ if (p.label) {
9506
+ out.push({
9507
+ type: "text",
9508
+ x: box.x + box.width + 38,
9509
+ y: cy,
9510
+ text: p.label,
9511
+ color,
9512
+ fontSize: 11,
9513
+ align: "left"
9514
+ });
9515
+ }
9516
+ } else {
9517
+ const cx = box.x + box.width / 2;
9518
+ out.push({
9519
+ type: "arrow",
9520
+ x1: cx,
9521
+ y1: panelYSlots + panelHeight - 18,
9522
+ x2: cx,
9523
+ y2: box.y + box.height + 4,
9524
+ color,
9525
+ lineWidth: 2
9526
+ });
9527
+ if (p.label) {
9528
+ out.push({
9529
+ type: "text",
9530
+ x: cx,
9531
+ y: panelYSlots + panelHeight - 8,
9532
+ text: p.label,
9533
+ color,
9534
+ fontSize: 11,
9535
+ align: "center"
9536
+ });
9537
+ }
9538
+ }
9539
+ });
9540
+ }
9541
+ }
8782
9542
  if (cells.length > 0) {
9543
+ const panelYCells = panelY.cells;
8783
9544
  const maxCol = Math.max(0, ...cells.map((c) => c.col)) + 1;
8784
9545
  const maxRow = Math.max(0, ...cells.map((c) => c.row)) + 1;
8785
- const cw = width / maxCol;
8786
- const ch = height / maxRow;
9546
+ const colLabelH = colLabels.length > 0 ? 16 : 0;
9547
+ const rowLabelW = rowLabels.length > 0 ? 20 : 0;
9548
+ const gridX0 = rowLabelW;
9549
+ const gridY0 = panelYCells + colLabelH;
9550
+ const cw = (width - rowLabelW) / maxCol;
9551
+ const ch = (panelHeight - colLabelH) / maxRow;
8787
9552
  cells.forEach((c, i) => {
8788
- const x = c.col * cw;
8789
- const y = c.row * ch;
9553
+ const x = gridX0 + c.col * cw;
9554
+ const y = gridY0 + c.row * ch;
8790
9555
  const color = c.color ?? DEFAULT_CELL_COLOR;
8791
9556
  out.push({
8792
9557
  type: "rect",
@@ -8810,11 +9575,207 @@ var init_AlgorithmCanvas = __esm({
8810
9575
  align: "center"
8811
9576
  });
8812
9577
  }
9578
+ if (c.corner && cw >= CORNER_MIN_CELL && ch >= CORNER_MIN_CELL) {
9579
+ const { tl, tr, bl, br } = c.corner;
9580
+ if (tl) {
9581
+ out.push({
9582
+ type: "text",
9583
+ x: x + CORNER_INSET_X,
9584
+ y: y + CORNER_INSET_Y,
9585
+ text: tl,
9586
+ color: CORNER_TEXT_COLOR,
9587
+ fontSize: CORNER_FONT_SIZE,
9588
+ align: "left"
9589
+ });
9590
+ }
9591
+ if (tr) {
9592
+ out.push({
9593
+ type: "text",
9594
+ x: x + cw - CORNER_INSET_X,
9595
+ y: y + CORNER_INSET_Y,
9596
+ text: tr,
9597
+ color: CORNER_TEXT_COLOR,
9598
+ fontSize: CORNER_FONT_SIZE,
9599
+ align: "right"
9600
+ });
9601
+ }
9602
+ if (bl) {
9603
+ out.push({
9604
+ type: "text",
9605
+ x: x + CORNER_INSET_X,
9606
+ y: y + ch - CORNER_INSET_Y,
9607
+ text: bl,
9608
+ color: CORNER_TEXT_COLOR,
9609
+ fontSize: CORNER_FONT_SIZE,
9610
+ align: "left"
9611
+ });
9612
+ }
9613
+ if (br) {
9614
+ out.push({
9615
+ type: "text",
9616
+ x: x + cw - CORNER_INSET_X,
9617
+ y: y + ch - CORNER_INSET_Y,
9618
+ text: br,
9619
+ color: CORNER_TEXT_COLOR,
9620
+ fontSize: CORNER_FONT_SIZE,
9621
+ align: "right"
9622
+ });
9623
+ }
9624
+ }
9625
+ });
9626
+ colLabels.forEach((l) => {
9627
+ out.push({
9628
+ type: "text",
9629
+ x: gridX0 + l.index * cw + cw / 2,
9630
+ y: panelYCells + colLabelH / 2,
9631
+ text: l.text,
9632
+ color: l.color ?? AXIS_LABEL_COLOR,
9633
+ fontSize: AXIS_LABEL_FONT_SIZE,
9634
+ align: "center"
9635
+ });
9636
+ });
9637
+ rowLabels.forEach((l) => {
9638
+ out.push({
9639
+ type: "text",
9640
+ x: rowLabelW - 6,
9641
+ y: gridY0 + l.index * ch + ch / 2,
9642
+ text: l.text,
9643
+ color: l.color ?? AXIS_LABEL_COLOR,
9644
+ fontSize: AXIS_LABEL_FONT_SIZE,
9645
+ align: "right"
9646
+ });
9647
+ });
9648
+ }
9649
+ if (buckets.length > 0) {
9650
+ const panelYBuckets = panelY.buckets;
9651
+ const bucketCount = Math.max(0, ...buckets.map((b) => b.index)) + 1;
9652
+ const rowH = panelHeight / bucketCount;
9653
+ const indexColW = Math.min(width * 0.12, 40);
9654
+ const maxChainLen = Math.max(1, ...buckets.map((b) => b.entries.length));
9655
+ const entryW = Math.min(BUCKET_ENTRY_MAX_W, Math.max(BUCKET_ENTRY_MIN_W, (width - indexColW - 8) / maxChainLen));
9656
+ const maxVisible = Math.floor((width - indexColW - 4) / entryW);
9657
+ buckets.forEach((b) => {
9658
+ const rowY = panelYBuckets + b.index * rowH;
9659
+ out.push({
9660
+ type: "rect",
9661
+ id: `bucket-index-${b.index}`,
9662
+ x: 2,
9663
+ y: rowY + 2,
9664
+ width: indexColW - 4,
9665
+ height: rowH - 4,
9666
+ color: BUCKET_INDEX_STROKE,
9667
+ fill: BUCKET_INDEX_FILL
9668
+ });
9669
+ out.push({
9670
+ type: "text",
9671
+ x: 2 + (indexColW - 4) / 2,
9672
+ y: rowY + rowH / 2,
9673
+ text: String(b.index),
9674
+ color: BUCKET_INDEX_TEXT,
9675
+ fontSize: 10,
9676
+ align: "center"
9677
+ });
9678
+ const overflow = b.entries.length > maxVisible;
9679
+ const visibleCount = overflow ? Math.max(0, maxVisible - 1) : b.entries.length;
9680
+ for (let j = 0; j < visibleCount; j++) {
9681
+ const entry = b.entries[j];
9682
+ const ex = indexColW + 4 + j * entryW;
9683
+ const state = entry.state ?? "default";
9684
+ const fill = state === "highlight" ? entry.color ?? BUCKET_ENTRY_HIGHLIGHT : state === "probing" ? entry.color ?? BUCKET_ENTRY_PROBING : entry.color ?? BUCKET_ENTRY_DEFAULT;
9685
+ out.push({
9686
+ type: "rect",
9687
+ id: `bucket-${b.index}-${j}`,
9688
+ x: ex,
9689
+ y: rowY + 2,
9690
+ width: entryW - 2,
9691
+ height: rowH - 4,
9692
+ color: fill,
9693
+ fill
9694
+ });
9695
+ if (entryW >= 20 && rowH >= 16) {
9696
+ out.push({
9697
+ type: "text",
9698
+ x: ex + (entryW - 2) / 2,
9699
+ y: rowY + rowH / 2,
9700
+ text: entry.label,
9701
+ color: BUCKET_ENTRY_TEXT,
9702
+ fontSize: 10,
9703
+ align: "center"
9704
+ });
9705
+ }
9706
+ }
9707
+ if (overflow) {
9708
+ const ex = indexColW + 4 + visibleCount * entryW;
9709
+ out.push({
9710
+ type: "rect",
9711
+ id: `bucket-${b.index}-overflow`,
9712
+ x: ex,
9713
+ y: rowY + 2,
9714
+ width: entryW - 2,
9715
+ height: rowH - 4,
9716
+ color: BUCKET_ENTRY_DEFAULT,
9717
+ fill: BUCKET_ENTRY_DEFAULT
9718
+ });
9719
+ out.push({
9720
+ type: "text",
9721
+ x: ex + (entryW - 2) / 2,
9722
+ y: rowY + rowH / 2,
9723
+ text: `+${b.entries.length - visibleCount}`,
9724
+ color: BUCKET_ENTRY_TEXT,
9725
+ fontSize: 10,
9726
+ align: "center"
9727
+ });
9728
+ }
9729
+ });
9730
+ }
9731
+ if (frames.length > 0) {
9732
+ const panelYFrames = panelY.frames;
9733
+ const n = frames.length;
9734
+ const frameH = panelHeight / n;
9735
+ const x = 8;
9736
+ const w = width - 16;
9737
+ frames.forEach((f3, i) => {
9738
+ const y = panelYFrames + panelHeight - (i + 1) * frameH;
9739
+ const state = f3.state ?? "active";
9740
+ const fill = state === "returning" ? f3.color ?? FRAME_RETURNING_COLOR : state === "done" ? f3.color ?? FRAME_DONE_COLOR : f3.color ?? FRAME_ACTIVE_COLOR;
9741
+ out.push({ type: "rect", id: `frame-${i}`, x, y, width: w, height: frameH, color: fill, fill });
9742
+ if (frameH >= FRAME_TWO_LINE_MIN_H) {
9743
+ out.push({
9744
+ type: "text",
9745
+ x: 16,
9746
+ y: y + frameH * 0.35,
9747
+ text: f3.label,
9748
+ color: FRAME_LABEL_COLOR,
9749
+ fontSize: 10,
9750
+ align: "left"
9751
+ });
9752
+ if (f3.detail) {
9753
+ out.push({
9754
+ type: "text",
9755
+ x: 16,
9756
+ y: y + frameH * 0.7,
9757
+ text: f3.detail,
9758
+ color: FRAME_DETAIL_COLOR,
9759
+ fontSize: 10,
9760
+ align: "left"
9761
+ });
9762
+ }
9763
+ } else {
9764
+ out.push({
9765
+ type: "text",
9766
+ x: 16,
9767
+ y: y + frameH / 2,
9768
+ text: f3.label,
9769
+ color: FRAME_LABEL_COLOR,
9770
+ fontSize: 10,
9771
+ align: "left"
9772
+ });
9773
+ }
8813
9774
  });
8814
9775
  }
8815
9776
  out.push(...shapes);
8816
9777
  return out;
8817
- }, [bars, cells, pointers, shapes, width, height]);
9778
+ }, [bars, cells, pointers, ranges, slots, slotOrientation, frames, buckets, auxBars, rowLabels, colLabels, shapes, width, height]);
8818
9779
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
8819
9780
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
8820
9781
  /* @__PURE__ */ jsx(
@@ -9828,6 +10789,12 @@ function LearningScene3D({
9828
10789
  const unitId = event.payload?.unitId;
9829
10790
  if (typeof unitId === "string") onItemClickRef.current?.(unitId);
9830
10791
  });
10792
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && post?.bloom) {
10793
+ const unknownKeys = Object.keys(post.bloom).filter((k) => !KNOWN_BLOOM_KEYS.has(k));
10794
+ if (unknownKeys.length > 0) {
10795
+ sceneLog.debug("post.bloom has unrecognized keys \u2014 only intensity/threshold/smoothing are read", { unknownKeys });
10796
+ }
10797
+ }
9831
10798
  const props3d = {
9832
10799
  drawables,
9833
10800
  isLoading,
@@ -9893,7 +10860,7 @@ function cylinderBetween(from, to, radius, color) {
9893
10860
  material: { color }
9894
10861
  };
9895
10862
  }
9896
- function arrowBetween(from, to, color, shaftRadius = 0.08) {
10863
+ function arrowBetween(from, to, color, shaftRadius = 0.08, id) {
9897
10864
  const len = segmentLength(from, to);
9898
10865
  if (len < 1e-6) return null;
9899
10866
  const tipLen = Math.min(shaftRadius * 8, len * 0.35);
@@ -9921,6 +10888,7 @@ function arrowBetween(from, to, color, shaftRadius = 0.08) {
9921
10888
  };
9922
10889
  return {
9923
10890
  type: "draw-group",
10891
+ ...id !== void 0 ? { id } : {},
9924
10892
  position: { x: from[0], y: from[1], z: from[2] },
9925
10893
  items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
9926
10894
  };
@@ -9947,20 +10915,228 @@ function get3DClickPayload(onShapeClick, idToIndex) {
9947
10915
  if (!onShapeClick) return void 0;
9948
10916
  return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
9949
10917
  }
9950
- var Canvas3DHost;
10918
+ function polylineTube(points, radius, color, opts) {
10919
+ const maxSegments = opts?.maxSegments ?? 128;
10920
+ let pts = points;
10921
+ if (pts.length - 1 > maxSegments) {
10922
+ const step = (pts.length - 1) / maxSegments;
10923
+ const kept = [pts[0]];
10924
+ for (let s = 1; s < maxSegments; s++) kept.push(pts[Math.round(s * step)]);
10925
+ kept.push(pts[pts.length - 1]);
10926
+ pts = kept;
10927
+ }
10928
+ const out = [];
10929
+ for (let i = 0; i < pts.length - 1; i++) {
10930
+ const seg = cylinderBetween(pts[i], pts[i + 1], radius, color);
10931
+ if (seg) out.push(opts?.opacity !== void 0 ? { ...seg, opacity: opts.opacity } : seg);
10932
+ }
10933
+ return out;
10934
+ }
10935
+ function heightFieldMesh(spec) {
10936
+ const { nx, ny, heights, spacing = 1, x = 0, y = 0 } = spec;
10937
+ const flatShading = spec.flatShading ?? true;
10938
+ const vertices = [];
10939
+ for (let iy = 0; iy < ny; iy++) {
10940
+ for (let ix = 0; ix < nx; ix++) {
10941
+ vertices.push([
10942
+ x + (ix - (nx - 1) / 2) * spacing,
10943
+ y + (iy - (ny - 1) / 2) * spacing,
10944
+ heights[iy * nx + ix] ?? 0
10945
+ ]);
10946
+ }
10947
+ }
10948
+ const bands = [...spec.bands ?? []].sort((a, b) => (a.min ?? -Infinity) - (b.min ?? -Infinity));
10949
+ const facesByBand = /* @__PURE__ */ new Map();
10950
+ for (let iy = 0; iy < ny - 1; iy++) {
10951
+ for (let ix = 0; ix < nx - 1; ix++) {
10952
+ const v00 = iy * nx + ix;
10953
+ const v10 = iy * nx + ix + 1;
10954
+ const v01 = (iy + 1) * nx + ix;
10955
+ const v11 = (iy + 1) * nx + ix + 1;
10956
+ for (const face of [[v00, v10, v01], [v10, v11, v01]]) {
10957
+ const centroid = (vertices[face[0]][2] + vertices[face[1]][2] + vertices[face[2]][2]) / 3;
10958
+ let band = null;
10959
+ for (const b of bands) {
10960
+ if ((b.min ?? -Infinity) <= centroid) band = b;
10961
+ }
10962
+ const key = bands.length > 0 ? band : null;
10963
+ const list = facesByBand.get(key) ?? [];
10964
+ list.push(face);
10965
+ facesByBand.set(key, list);
10966
+ }
10967
+ }
10968
+ }
10969
+ const out = [];
10970
+ for (const [band, faces] of facesByBand) {
10971
+ if (faces.length === 0) continue;
10972
+ out.push({
10973
+ type: "draw-mesh",
10974
+ shape: "polyhedron",
10975
+ position: { x: 0, y: 0, z: 0 },
10976
+ vertices,
10977
+ faces,
10978
+ pivot: "center",
10979
+ material: { color: band?.color ?? spec.color ?? "#64748b", flatShading, side: "double" },
10980
+ ...spec.opacity !== void 0 ? { opacity: spec.opacity } : {}
10981
+ });
10982
+ }
10983
+ return out;
10984
+ }
10985
+ function arrowField(vectors, opts) {
10986
+ const scale = opts?.scale ?? 1;
10987
+ const out = [];
10988
+ for (const v of vectors) {
10989
+ const to = [
10990
+ v.from[0] + v.delta[0] * scale,
10991
+ v.from[1] + v.delta[1] * scale,
10992
+ v.from[2] + v.delta[2] * scale
10993
+ ];
10994
+ const arrow = arrowBetween(v.from, to, v.color ?? "#dc2626", v.width, v.id);
10995
+ if (arrow) out.push(arrow);
10996
+ if (v.label) out.push(billboardLabel(v.label, to[0], to[1], to[2], { color: opts?.labelColor }));
10997
+ }
10998
+ return out;
10999
+ }
11000
+ function helixDrawables(spec, opts) {
11001
+ const count = spec.count ?? spec.rungs?.length ?? 0;
11002
+ const rungs = Array.from({ length: count }, (_, i) => spec.rungs?.[i] ?? {});
11003
+ const radius = spec.radius ?? 1;
11004
+ const rise = spec.rise ?? 0.34;
11005
+ const twistRad = (spec.twistDeg ?? 36) * (Math.PI / 180);
11006
+ const strandAColor = spec.strandAColor ?? "#38bdf8";
11007
+ const strandBColor = spec.strandBColor ?? "#fb923c";
11008
+ const backboneRadius = spec.backboneRadius ?? 0.16;
11009
+ const rungRadius = spec.rungRadius ?? 0.12;
11010
+ const cx = spec.x ?? 0;
11011
+ const cy = spec.y ?? 0;
11012
+ const cz = spec.z ?? 0;
11013
+ const unwoundCount = spec.unwoundCount ?? 0;
11014
+ const unwindSpread = spec.unwindSpread ?? 1.8;
11015
+ const strandA = [];
11016
+ const strandB = [];
11017
+ for (let i = 0; i < count; i++) {
11018
+ const yi = cy + (i - (count - 1) / 2) * rise;
11019
+ const theta = i * twistRad;
11020
+ const s = i < unwoundCount ? unwindSpread : 1;
11021
+ strandA.push([cx + s * radius * Math.cos(theta), yi, cz + s * radius * Math.sin(theta)]);
11022
+ strandB.push([cx + s * radius * Math.cos(theta + Math.PI), yi, cz + s * radius * Math.sin(theta + Math.PI)]);
11023
+ }
11024
+ const out = [];
11025
+ for (let i = 0; i < count; i++) {
11026
+ out.push(meshSphere(`hx-a-${i}`, strandA[i][0], strandA[i][1], strandA[i][2], backboneRadius, strandAColor));
11027
+ out.push(meshSphere(`hx-b-${i}`, strandB[i][0], strandB[i][1], strandB[i][2], backboneRadius, strandBColor));
11028
+ if (i > 0) {
11029
+ const segA = cylinderBetween(strandA[i - 1], strandA[i], backboneRadius, strandAColor);
11030
+ if (segA) out.push(segA);
11031
+ const segB = cylinderBetween(strandB[i - 1], strandB[i], backboneRadius, strandBColor);
11032
+ if (segB) out.push(segB);
11033
+ }
11034
+ const rung = rungs[i];
11035
+ const rungColor = rung.color ?? "#94a3b8";
11036
+ const rod = cylinderBetween(strandA[i], strandB[i], rungRadius, rungColor);
11037
+ if (rod) out.push(rod);
11038
+ const mid = [
11039
+ (strandA[i][0] + strandB[i][0]) / 2,
11040
+ (strandA[i][1] + strandB[i][1]) / 2,
11041
+ (strandA[i][2] + strandB[i][2]) / 2
11042
+ ];
11043
+ const markerRadius = rung.radius ?? rungRadius;
11044
+ out.push(meshSphere(rung.id, mid[0], mid[1], mid[2], markerRadius, rungColor));
11045
+ if (rung.label) out.push(billboardLabel(rung.label, mid[0], mid[1], mid[2] + markerRadius, { color: opts?.labelColor }));
11046
+ }
11047
+ return out;
11048
+ }
11049
+ function latticeDrawables(spec, opts) {
11050
+ const nx = spec.nx ?? 2;
11051
+ const ny = spec.ny ?? 2;
11052
+ const nz = spec.nz ?? 2;
11053
+ const latticeConstant = spec.latticeConstant ?? 2;
11054
+ const bondRadius = spec.bondRadius ?? 0.06;
11055
+ const highlightCell = spec.highlightCell ?? false;
11056
+ const dimColor = spec.dimColor ?? "#475569";
11057
+ const showLabels = spec.showLabels ?? false;
11058
+ const selectedColor = spec.selectedColor ?? "#f59e0b";
11059
+ const posByKey = /* @__PURE__ */ new Map();
11060
+ const inCellByKey = /* @__PURE__ */ new Map();
11061
+ const out = [];
11062
+ for (const site of spec.basis) {
11063
+ const snx = site.xEdge ? nx + 1 : nx;
11064
+ const sny = site.yEdge ? ny + 1 : ny;
11065
+ const snz = site.zEdge ? nz + 1 : nz;
11066
+ for (let i = 0; i < snx; i++) {
11067
+ for (let j = 0; j < sny; j++) {
11068
+ for (let k = 0; k < snz; k++) {
11069
+ const key = `${site.key}-${i}-${j}-${k}`;
11070
+ const inCell = i + site.dx <= 1 && j + site.dy <= 1 && k + site.dz <= 1;
11071
+ const pos = [
11072
+ (i + site.dx) * latticeConstant - nx * latticeConstant / 2,
11073
+ (j + site.dy) * latticeConstant - ny * latticeConstant / 2,
11074
+ (k + site.dz) * latticeConstant - nz * latticeConstant / 2
11075
+ ];
11076
+ posByKey.set(key, pos);
11077
+ inCellByKey.set(key, inCell);
11078
+ const isSelected = spec.selectedId === `lat-${key}`;
11079
+ const color = isSelected ? selectedColor : highlightCell && !inCell ? dimColor : site.color ?? "#2563eb";
11080
+ const radius = (site.radius ?? 0.3) * (isSelected ? 1.4 : 1);
11081
+ out.push(meshSphere(`lat-${key}`, pos[0], pos[1], pos[2], radius, color));
11082
+ if (showLabels && site.element) {
11083
+ out.push(billboardLabel(site.element, pos[0], pos[1], pos[2] + radius, { color: opts?.labelColor }));
11084
+ }
11085
+ }
11086
+ }
11087
+ }
11088
+ }
11089
+ const basisByKey = new Map(spec.basis.map((s) => [s.key, s]));
11090
+ for (const bond of spec.bonds ?? []) {
11091
+ const fromSite = basisByKey.get(bond.from);
11092
+ const toSite = basisByKey.get(bond.to);
11093
+ if (!fromSite || !toSite) continue;
11094
+ const fnx = fromSite.xEdge ? nx + 1 : nx;
11095
+ const fny = fromSite.yEdge ? ny + 1 : ny;
11096
+ const fnz = fromSite.zEdge ? nz + 1 : nz;
11097
+ const tnx = toSite.xEdge ? nx + 1 : nx;
11098
+ const tny = toSite.yEdge ? ny + 1 : ny;
11099
+ const tnz = toSite.zEdge ? nz + 1 : nz;
11100
+ const bdx = bond.dx ?? 0;
11101
+ const bdy = bond.dy ?? 0;
11102
+ const bdz = bond.dz ?? 0;
11103
+ for (let i = 0; i < fnx; i++) {
11104
+ for (let j = 0; j < fny; j++) {
11105
+ for (let k = 0; k < fnz; k++) {
11106
+ const ti = i + bdx;
11107
+ const tj = j + bdy;
11108
+ const tk = k + bdz;
11109
+ if (ti < 0 || ti >= tnx || tj < 0 || tj >= tny || tk < 0 || tk >= tnz) continue;
11110
+ const fromKey = `${fromSite.key}-${i}-${j}-${k}`;
11111
+ const toKey = `${toSite.key}-${ti}-${tj}-${tk}`;
11112
+ const fromPos = posByKey.get(fromKey);
11113
+ const toPos = posByKey.get(toKey);
11114
+ if (!fromPos || !toPos) continue;
11115
+ const dimmed = highlightCell && !(inCellByKey.get(fromKey) && inCellByKey.get(toKey));
11116
+ const seg = cylinderBetween(fromPos, toPos, bondRadius, dimmed ? dimColor : bond.color ?? "#6b7280");
11117
+ if (seg) out.push(seg);
11118
+ }
11119
+ }
11120
+ }
11121
+ }
11122
+ return out;
11123
+ }
11124
+ var sceneLog, KNOWN_BLOOM_KEYS, Canvas3DHost;
9951
11125
  var init_learningScene3D = __esm({
9952
11126
  "components/learning/molecules/learningScene3D.tsx"() {
9953
11127
  "use client";
9954
11128
  init_atoms();
9955
11129
  init_Stack();
9956
11130
  init_useEventBus();
11131
+ sceneLog = createLogger("almadar:ui:learning-scene-3d");
11132
+ KNOWN_BLOOM_KEYS = /* @__PURE__ */ new Set(["intensity", "threshold", "smoothing"]);
9957
11133
  Canvas3DHost = lazy(
9958
11134
  () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
9959
11135
  );
9960
11136
  LearningScene3D.displayName = "LearningScene3D";
9961
11137
  }
9962
11138
  });
9963
- var biologyLog, BiologyCanvas;
11139
+ var biologyLog, BIO_BAND_COLORS, BIO_STAGE_FILL, BIO_STAGE_TEXT, BiologyCanvas;
9964
11140
  var init_BiologyCanvas = __esm({
9965
11141
  "components/learning/molecules/BiologyCanvas.tsx"() {
9966
11142
  "use client";
@@ -9969,6 +11145,17 @@ var init_BiologyCanvas = __esm({
9969
11145
  init_LearningCanvas();
9970
11146
  init_learningScene3D();
9971
11147
  biologyLog = createLogger("almadar:ui:biology-canvas");
11148
+ BIO_BAND_COLORS = ["#dcfce7", "#fef9c3", "#fee2e2", "#e0e7ff"];
11149
+ BIO_STAGE_FILL = {
11150
+ pending: "#e2e8f0",
11151
+ active: "#3b82f6",
11152
+ done: "#94a3b8"
11153
+ };
11154
+ BIO_STAGE_TEXT = {
11155
+ pending: "#64748b",
11156
+ active: "#ffffff",
11157
+ done: "#ffffff"
11158
+ };
9972
11159
  BiologyCanvas = ({
9973
11160
  className,
9974
11161
  width = 600,
@@ -9981,7 +11168,15 @@ var init_BiologyCanvas = __esm({
9981
11168
  post,
9982
11169
  nodes = [],
9983
11170
  edges = [],
11171
+ compartments = [],
11172
+ bands = [],
11173
+ stages = [],
11174
+ stageStyle = "timeline",
11175
+ helix,
11176
+ helix3d,
9984
11177
  shapes = [],
11178
+ readouts,
11179
+ traces,
9985
11180
  showGrid,
9986
11181
  shadows,
9987
11182
  interactive,
@@ -9996,19 +11191,148 @@ var init_BiologyCanvas = __esm({
9996
11191
  for (const n of nodes) {
9997
11192
  if (n.id) nodeById.set(n.id, n);
9998
11193
  }
11194
+ const bandCount = bands.length;
11195
+ for (let i = 0; i < bandCount; i++) {
11196
+ const band = bands[i];
11197
+ const bandColor = band.color ?? BIO_BAND_COLORS[i % BIO_BAND_COLORS.length];
11198
+ const bandY = i * height / bandCount;
11199
+ const bandH = height / bandCount;
11200
+ out.push({
11201
+ type: "rect",
11202
+ x: 0,
11203
+ y: bandY,
11204
+ width,
11205
+ height: bandH,
11206
+ color: bandColor,
11207
+ fill: bandColor,
11208
+ opacity: 0.45
11209
+ });
11210
+ if (band.label) {
11211
+ out.push({
11212
+ type: "text",
11213
+ x: 8,
11214
+ y: bandY + 14,
11215
+ text: band.label,
11216
+ color: "#6b7280",
11217
+ fontSize: 10
11218
+ });
11219
+ }
11220
+ }
11221
+ for (const c of compartments) {
11222
+ const color = c.color ?? "#16a34a";
11223
+ out.push({
11224
+ type: "ellipse",
11225
+ x: c.x,
11226
+ y: c.y,
11227
+ width: c.width,
11228
+ height: c.height,
11229
+ color,
11230
+ fill: c.fill ?? `${color}1A`,
11231
+ lineWidth: c.lineWidth ?? 2,
11232
+ ...c.dash ? { dash: c.dash } : {}
11233
+ });
11234
+ if (c.label) {
11235
+ out.push({
11236
+ type: "text",
11237
+ x: c.x,
11238
+ y: c.y - c.height / 2 + 14,
11239
+ text: c.label,
11240
+ color: "#111827",
11241
+ fontSize: 11,
11242
+ align: "center"
11243
+ });
11244
+ }
11245
+ }
11246
+ if (helix) {
11247
+ const hx = helix.x ?? 24;
11248
+ const hy = helix.y ?? height * 0.25;
11249
+ const hw = helix.width ?? width - 48;
11250
+ const hh = helix.height ?? height * 0.5;
11251
+ const rungs = helix.rungs;
11252
+ const n = rungs.length;
11253
+ const cy = hy + hh / 2;
11254
+ const colorA = helix.colorA ?? "#2563eb";
11255
+ const colorB = helix.colorB ?? "#dc2626";
11256
+ const rungColor = helix.rungColor ?? "#94a3b8";
11257
+ const fork = helix.fork ?? 0;
11258
+ const maxSep = Math.min(hh - 8, 96);
11259
+ const strandA = [];
11260
+ const strandB = [];
11261
+ const rungGeoms = [];
11262
+ for (let i = 0; i < n; i++) {
11263
+ const rx = hx + (i + 0.5) * hw / n;
11264
+ const t = (i + 0.5) / n;
11265
+ const paired = t >= fork;
11266
+ const sep = paired ? 28 : 28 + (maxSep - 28) * ((fork - t) / fork);
11267
+ strandA.push({ x: rx, y: cy - sep / 2 });
11268
+ strandB.push({ x: rx, y: cy + sep / 2 });
11269
+ rungGeoms.push({ rx, sep, rung: rungs[i], paired });
11270
+ }
11271
+ for (let i = 1; i < n; i++) {
11272
+ 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 });
11273
+ }
11274
+ for (let i = 1; i < n; i++) {
11275
+ 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 });
11276
+ }
11277
+ for (const g of rungGeoms) {
11278
+ const rColor = g.rung.color ?? (g.rung.state === "new" ? "#16a34a" : rungColor);
11279
+ const topY = cy - g.sep / 2;
11280
+ const bottomY = cy + g.sep / 2;
11281
+ if (g.paired) {
11282
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: bottomY, color: rColor });
11283
+ if (g.rung.a) {
11284
+ out.push({ type: "text", x: g.rx, y: cy - g.sep / 4, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
11285
+ }
11286
+ if (g.rung.b) {
11287
+ out.push({ type: "text", x: g.rx, y: cy + g.sep / 4, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
11288
+ }
11289
+ } else {
11290
+ const stubTopY = topY + 8;
11291
+ const stubBottomY = bottomY - 8;
11292
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: stubTopY, color: rColor });
11293
+ out.push({ type: "line", x1: g.rx, y1: bottomY, x2: g.rx, y2: stubBottomY, color: rColor });
11294
+ if (g.rung.a) {
11295
+ out.push({ type: "text", x: g.rx, y: stubTopY + 6, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
11296
+ }
11297
+ if (g.rung.b) {
11298
+ out.push({ type: "text", x: g.rx, y: stubBottomY - 6, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
11299
+ }
11300
+ }
11301
+ }
11302
+ }
9999
11303
  for (const e of edges) {
10000
11304
  const a = nodeById.get(e.from);
10001
11305
  const b = nodeById.get(e.to);
10002
11306
  if (!a || !b) continue;
10003
- out.push({
10004
- type: "line",
10005
- x1: a.x,
10006
- y1: a.y,
10007
- x2: b.x,
10008
- y2: b.y,
10009
- color: e.color ?? "#9ca3af",
10010
- lineWidth: 2
10011
- });
11307
+ const color = e.color ?? "#9ca3af";
11308
+ if (e.directed) {
11309
+ const rA = a.radius ?? 16;
11310
+ const rB = b.radius ?? 16;
11311
+ const dx = b.x - a.x;
11312
+ const dy = b.y - a.y;
11313
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
11314
+ const ux = dx / dist;
11315
+ const uy = dy / dist;
11316
+ out.push({
11317
+ type: "arrow",
11318
+ x1: a.x + ux * rA,
11319
+ y1: a.y + uy * rA,
11320
+ x2: b.x - ux * rB,
11321
+ y2: b.y - uy * rB,
11322
+ color,
11323
+ lineWidth: 2
11324
+ });
11325
+ } else {
11326
+ out.push({
11327
+ type: "line",
11328
+ x1: a.x,
11329
+ y1: a.y,
11330
+ x2: b.x,
11331
+ y2: b.y,
11332
+ color,
11333
+ lineWidth: 2
11334
+ });
11335
+ }
10012
11336
  if (e.label) {
10013
11337
  out.push({
10014
11338
  type: "text",
@@ -10021,6 +11345,8 @@ var init_BiologyCanvas = __esm({
10021
11345
  }
10022
11346
  }
10023
11347
  for (const n of nodes) {
11348
+ const state = n.state ?? "default";
11349
+ const muted = state === "muted";
10024
11350
  out.push({
10025
11351
  type: "circle",
10026
11352
  x: n.x,
@@ -10028,28 +11354,127 @@ var init_BiologyCanvas = __esm({
10028
11354
  radius: n.radius ?? 16,
10029
11355
  color: n.color ?? "#16a34a",
10030
11356
  fill: `${n.color ?? "#16a34a"}33`,
10031
- id: n.id
11357
+ id: n.id,
11358
+ ...muted ? { opacity: 0.35 } : {}
10032
11359
  });
11360
+ if (state === "highlight") {
11361
+ out.push({
11362
+ type: "circle",
11363
+ x: n.x,
11364
+ y: n.y,
11365
+ radius: (n.radius ?? 16) + 4,
11366
+ color: "#f59e0b",
11367
+ lineWidth: 2
11368
+ });
11369
+ }
10033
11370
  if (n.label) {
10034
11371
  out.push({
10035
11372
  type: "text",
10036
11373
  x: n.x,
10037
11374
  y: n.y + (n.radius ?? 16) + 14,
10038
11375
  text: n.label,
11376
+ ...muted ? { opacity: 0.35 } : {},
10039
11377
  color: "#111827",
10040
11378
  fontSize: 12,
10041
11379
  align: "center"
10042
11380
  });
10043
11381
  }
10044
11382
  }
11383
+ const stageCount = stages.length;
11384
+ if (stageCount > 0) {
11385
+ if (stageStyle === "ring") {
11386
+ const cx = width / 2;
11387
+ const cy = height / 2;
11388
+ const R = Math.min(width, height) / 2 - 48;
11389
+ const ringPoints = [];
11390
+ for (let i = 0; i < stageCount; i++) {
11391
+ const angleRad = (-90 + 360 * i / stageCount) * Math.PI / 180;
11392
+ ringPoints.push({ x: cx + R * Math.cos(angleRad), y: cy + R * Math.sin(angleRad) });
11393
+ }
11394
+ if (stageCount >= 2) {
11395
+ for (let i = 0; i < stageCount - 1; i++) {
11396
+ const p1 = ringPoints[i];
11397
+ const p2 = ringPoints[i + 1];
11398
+ const dx = p2.x - p1.x;
11399
+ const dy = p2.y - p1.y;
11400
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
11401
+ const ux = dx / dist;
11402
+ const uy = dy / dist;
11403
+ out.push({
11404
+ type: "arrow",
11405
+ x1: p1.x + ux * 46,
11406
+ y1: p1.y + uy * 46,
11407
+ x2: p2.x - ux * 46,
11408
+ y2: p2.y - uy * 46,
11409
+ color: "#94a3b8"
11410
+ });
11411
+ }
11412
+ }
11413
+ for (let i = 0; i < stageCount; i++) {
11414
+ const stage = stages[i];
11415
+ const state = stage.state ?? "pending";
11416
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
11417
+ const w = Math.max(26, Math.min(84, stage.label.length * 6 + 10));
11418
+ const h = 18;
11419
+ const p = ringPoints[i];
11420
+ out.push({ type: "rect", x: p.x - w / 2, y: p.y - h / 2, width: w, height: h, color: fill, fill });
11421
+ out.push({ type: "text", x: p.x, y: p.y, text: stage.label, color: BIO_STAGE_TEXT[state], fontSize: 10, align: "center" });
11422
+ }
11423
+ } else {
11424
+ const stripY = height - 32;
11425
+ const slotW = (width - 16) / stageCount;
11426
+ const chipGeoms = [];
11427
+ for (let i = 0; i < stageCount; i++) {
11428
+ chipGeoms.push({ x: 8 + i * slotW + 5, w: slotW - 10 });
11429
+ }
11430
+ for (let i = 0; i < stageCount - 1; i++) {
11431
+ const midY = stripY + 13;
11432
+ out.push({
11433
+ type: "arrow",
11434
+ x1: chipGeoms[i].x + chipGeoms[i].w,
11435
+ y1: midY,
11436
+ x2: chipGeoms[i + 1].x,
11437
+ y2: midY,
11438
+ color: "#94a3b8"
11439
+ });
11440
+ }
11441
+ for (let i = 0; i < stageCount; i++) {
11442
+ const stage = stages[i];
11443
+ const state = stage.state ?? "pending";
11444
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
11445
+ const g = chipGeoms[i];
11446
+ out.push({ type: "rect", x: g.x, y: stripY, width: g.w, height: 26, color: fill, fill });
11447
+ out.push({
11448
+ type: "text",
11449
+ x: g.x + g.w / 2,
11450
+ y: stripY + 13,
11451
+ text: stage.label,
11452
+ color: BIO_STAGE_TEXT[state],
11453
+ fontSize: 10,
11454
+ align: "center"
11455
+ });
11456
+ }
11457
+ }
11458
+ }
10045
11459
  out.push(...shapes);
10046
11460
  return out;
10047
- }, [nodes, edges, shapes]);
11461
+ }, [nodes, edges, compartments, bands, stages, stageStyle, helix, shapes, width, height]);
10048
11462
  const drawables3D = useMemo(() => {
10049
11463
  if (mode !== "3d") return [];
10050
11464
  if (shapes.length > 0) {
10051
11465
  biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
10052
11466
  }
11467
+ if (compartments.length > 0 || bands.length > 0 || stages.length > 0 || helix) {
11468
+ biologyLog.debug("2D-only families ignored in 3D mode (pixel-authored 2D vocabulary)", {
11469
+ compartments: compartments.length,
11470
+ bands: bands.length,
11471
+ stages: stages.length,
11472
+ helix: helix != null
11473
+ });
11474
+ }
11475
+ if (animate) {
11476
+ biologyLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
11477
+ }
10053
11478
  const out = [];
10054
11479
  const labelColor = labelColorForBackground(backgroundColor);
10055
11480
  const nodeById = /* @__PURE__ */ new Map();
@@ -10083,15 +11508,21 @@ var init_BiologyCanvas = __esm({
10083
11508
  out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
10084
11509
  }
10085
11510
  }
11511
+ if (helix3d) {
11512
+ out.push(...helixDrawables(helix3d, { labelColor }));
11513
+ }
10086
11514
  return out;
10087
- }, [mode, nodes, edges, shapes, backgroundColor]);
11515
+ }, [mode, nodes, edges, shapes, compartments, bands, stages, helix, helix3d, animate, backgroundColor]);
10088
11516
  const nodeIndexById = useMemo(() => {
10089
11517
  const m = /* @__PURE__ */ new Map();
11518
+ (helix3d?.rungs ?? []).forEach((rung, i) => {
11519
+ if (rung.id) m.set(rung.id, i);
11520
+ });
10090
11521
  nodes.forEach((n, i) => {
10091
11522
  if (n.id) m.set(n.id, i);
10092
11523
  });
10093
11524
  return m;
10094
- }, [nodes]);
11525
+ }, [nodes, helix3d]);
10095
11526
  if (mode === "3d") {
10096
11527
  return /* @__PURE__ */ jsx(
10097
11528
  LearningScene3D,
@@ -10123,6 +11554,8 @@ var init_BiologyCanvas = __esm({
10123
11554
  height,
10124
11555
  backgroundColor,
10125
11556
  shapes: derivedShapes,
11557
+ readouts,
11558
+ traces,
10126
11559
  interactive: interactive ?? false,
10127
11560
  animate,
10128
11561
  onShapeClick,
@@ -19313,7 +20746,7 @@ function bondPerpendicular(a, b) {
19313
20746
  if (len < 1e-6) return [1, 0, 0];
19314
20747
  return [px / len, py / len, 0];
19315
20748
  }
19316
- var chemistryLog, ChemistryCanvas;
20749
+ var chemistryLog, CHEM_BOND_STATE_COLOR, LONE_PAIR_ANGLES, ChemistryCanvas;
19317
20750
  var init_ChemistryCanvas = __esm({
19318
20751
  "components/learning/molecules/ChemistryCanvas.tsx"() {
19319
20752
  "use client";
@@ -19322,6 +20755,13 @@ var init_ChemistryCanvas = __esm({
19322
20755
  init_LearningCanvas();
19323
20756
  init_learningScene3D();
19324
20757
  chemistryLog = createLogger("almadar:ui:chemistry-canvas");
20758
+ CHEM_BOND_STATE_COLOR = {
20759
+ default: "#6b7280",
20760
+ forming: "#16a34a",
20761
+ breaking: "#dc2626",
20762
+ highlight: "#f59e0b"
20763
+ };
20764
+ LONE_PAIR_ANGLES = [-90, 0, 90, 180];
19325
20765
  ChemistryCanvas = ({
19326
20766
  className,
19327
20767
  width = 600,
@@ -19335,7 +20775,14 @@ var init_ChemistryCanvas = __esm({
19335
20775
  atoms = [],
19336
20776
  bonds = [],
19337
20777
  arrows = [],
20778
+ bondStyle = "thick",
20779
+ containers = [],
20780
+ equation,
20781
+ equationColor,
20782
+ lattice3d,
19338
20783
  shapes = [],
20784
+ readouts,
20785
+ traces,
19339
20786
  showGrid,
19340
20787
  shadows,
19341
20788
  interactive,
@@ -19350,21 +20797,118 @@ var init_ChemistryCanvas = __esm({
19350
20797
  for (const a of atoms) {
19351
20798
  if (a.id) atomById.set(a.id, a);
19352
20799
  }
20800
+ for (const c of containers) {
20801
+ const color = c.color ?? "#64748b";
20802
+ if (c.level != null) {
20803
+ const lv = c.level;
20804
+ out.push({
20805
+ type: "rect",
20806
+ x: c.x + 1,
20807
+ y: c.y + c.height * (1 - lv),
20808
+ width: c.width - 2,
20809
+ height: c.height * lv - 1,
20810
+ color: c.levelColor ?? "#60a5fa",
20811
+ fill: c.levelColor ?? "#60a5fa",
20812
+ opacity: 0.5
20813
+ });
20814
+ }
20815
+ out.push({
20816
+ type: "rect",
20817
+ x: c.x,
20818
+ y: c.y,
20819
+ width: c.width,
20820
+ height: c.height,
20821
+ color,
20822
+ fill: c.fill,
20823
+ lineWidth: c.lineWidth ?? 2
20824
+ });
20825
+ const divider = c.divider ?? "none";
20826
+ if (divider !== "none") {
20827
+ out.push({
20828
+ type: "line",
20829
+ x1: c.x + c.width / 2,
20830
+ y1: c.y,
20831
+ x2: c.x + c.width / 2,
20832
+ y2: c.y + c.height,
20833
+ color: c.dividerColor ?? color,
20834
+ ...divider === "dashed" || divider === "dotted" ? { dash: divider } : {}
20835
+ });
20836
+ }
20837
+ if (c.leftLabel) {
20838
+ out.push({
20839
+ type: "text",
20840
+ x: c.x + c.width * 0.25,
20841
+ y: c.y + 12,
20842
+ text: c.leftLabel,
20843
+ color: "#374151",
20844
+ fontSize: 11,
20845
+ align: "center"
20846
+ });
20847
+ }
20848
+ if (c.rightLabel) {
20849
+ out.push({
20850
+ type: "text",
20851
+ x: c.x + c.width * 0.75,
20852
+ y: c.y + 12,
20853
+ text: c.rightLabel,
20854
+ color: "#374151",
20855
+ fontSize: 11,
20856
+ align: "center"
20857
+ });
20858
+ }
20859
+ if (c.label) {
20860
+ out.push({
20861
+ type: "text",
20862
+ x: c.x + c.width / 2,
20863
+ y: c.y + c.height + 12,
20864
+ text: c.label,
20865
+ color: "#111827",
20866
+ fontSize: 12,
20867
+ align: "center"
20868
+ });
20869
+ }
20870
+ }
19353
20871
  for (const b of bonds) {
19354
20872
  const a = atomById.get(b.from);
19355
20873
  const c = atomById.get(b.to);
19356
20874
  if (!a || !c) continue;
19357
- const color = b.color ?? "#6b7280";
19358
- const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
19359
- out.push({
19360
- type: "line",
19361
- x1: a.x,
19362
- y1: a.y,
19363
- x2: c.x,
19364
- y2: c.y,
19365
- color,
19366
- lineWidth: strokeWidth
19367
- });
20875
+ const state = b.state ?? "default";
20876
+ const color = b.color ?? CHEM_BOND_STATE_COLOR[state];
20877
+ const dash = state === "forming" || state === "breaking" ? "dashed" : void 0;
20878
+ if (bondStyle === "parallel") {
20879
+ const dx = c.x - a.x;
20880
+ const dy = c.y - a.y;
20881
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
20882
+ const ux = dx / dist;
20883
+ const uy = dy / dist;
20884
+ const px = -uy;
20885
+ const py = ux;
20886
+ const offsets = b.type === "double" ? [-3, 3] : b.type === "triple" ? [-4, 0, 4] : [0];
20887
+ for (const off of offsets) {
20888
+ out.push({
20889
+ type: "line",
20890
+ x1: a.x + px * off,
20891
+ y1: a.y + py * off,
20892
+ x2: c.x + px * off,
20893
+ y2: c.y + py * off,
20894
+ color,
20895
+ lineWidth: 2,
20896
+ ...dash ? { dash } : {}
20897
+ });
20898
+ }
20899
+ } else {
20900
+ const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
20901
+ out.push({
20902
+ type: "line",
20903
+ x1: a.x,
20904
+ y1: a.y,
20905
+ x2: c.x,
20906
+ y2: c.y,
20907
+ color,
20908
+ lineWidth: strokeWidth,
20909
+ ...dash ? { dash } : {}
20910
+ });
20911
+ }
19368
20912
  }
19369
20913
  for (const a of arrows) {
19370
20914
  const angle = (a.angle ?? 0) * (Math.PI / 180);
@@ -19413,15 +20957,62 @@ var init_ChemistryCanvas = __esm({
19413
20957
  align: "center"
19414
20958
  });
19415
20959
  }
20960
+ const r = a.radius ?? 14;
20961
+ if (a.charge) {
20962
+ out.push({
20963
+ type: "text",
20964
+ x: a.x + r * 0.85,
20965
+ y: a.y - r * 0.85,
20966
+ text: a.charge,
20967
+ color: "#111827",
20968
+ fontSize: 9,
20969
+ align: "left"
20970
+ });
20971
+ }
20972
+ const lonePairs = Math.max(0, Math.min(4, a.lonePairs ?? 0));
20973
+ for (let k = 0; k < lonePairs; k++) {
20974
+ const angleRad = LONE_PAIR_ANGLES[k] * Math.PI / 180;
20975
+ const cx = a.x + (r + 6) * Math.cos(angleRad);
20976
+ const cy = a.y + (r + 6) * Math.sin(angleRad);
20977
+ const perpX = -Math.sin(angleRad);
20978
+ const perpY = Math.cos(angleRad);
20979
+ for (const sign of [1, -1]) {
20980
+ out.push({
20981
+ type: "circle",
20982
+ x: cx + perpX * 2.5 * sign,
20983
+ y: cy + perpY * 2.5 * sign,
20984
+ radius: 1.5,
20985
+ color: "#374151",
20986
+ fill: "#374151"
20987
+ });
20988
+ }
20989
+ }
20990
+ }
20991
+ if (equation) {
20992
+ out.push({
20993
+ type: "text",
20994
+ x: width / 2,
20995
+ y: 14,
20996
+ text: equation,
20997
+ color: equationColor ?? "#111827",
20998
+ fontSize: 13,
20999
+ align: "center"
21000
+ });
19416
21001
  }
19417
21002
  out.push(...shapes);
19418
21003
  return out;
19419
- }, [atoms, bonds, arrows, shapes]);
21004
+ }, [atoms, bonds, arrows, bondStyle, containers, equation, equationColor, shapes, width]);
19420
21005
  const drawables3D = useMemo(() => {
19421
21006
  if (mode !== "3d") return [];
19422
21007
  if (shapes.length > 0) {
19423
21008
  chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
19424
21009
  }
21010
+ if (containers.length > 0) {
21011
+ chemistryLog.debug("containers ignored in 3D mode (pixel-authored 2D vocabulary)", { count: containers.length });
21012
+ }
21013
+ if (animate) {
21014
+ chemistryLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
21015
+ }
19425
21016
  const out = [];
19426
21017
  const labelColor = labelColorForBackground(backgroundColor);
19427
21018
  const atomById = /* @__PURE__ */ new Map();
@@ -19468,8 +21059,11 @@ var init_ChemistryCanvas = __esm({
19468
21059
  out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
19469
21060
  }
19470
21061
  }
21062
+ if (lattice3d) {
21063
+ out.push(...latticeDrawables(lattice3d, { labelColor }));
21064
+ }
19471
21065
  return out;
19472
- }, [mode, atoms, bonds, arrows, shapes, backgroundColor]);
21066
+ }, [mode, atoms, bonds, arrows, shapes, containers, lattice3d, animate, backgroundColor]);
19473
21067
  const atomIndexById = useMemo(() => {
19474
21068
  const m = /* @__PURE__ */ new Map();
19475
21069
  atoms.forEach((a, i) => {
@@ -19508,6 +21102,8 @@ var init_ChemistryCanvas = __esm({
19508
21102
  height,
19509
21103
  backgroundColor,
19510
21104
  shapes: derivedShapes,
21105
+ readouts,
21106
+ traces,
19511
21107
  interactive: interactive ?? false,
19512
21108
  animate,
19513
21109
  onShapeClick,
@@ -29420,6 +31016,10 @@ var init_molecules = __esm({
29420
31016
  init_GameShell();
29421
31017
  }
29422
31018
  });
31019
+ function formatTick(v) {
31020
+ if (Number.isInteger(v)) return String(v);
31021
+ return v.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
31022
+ }
29423
31023
  var MathCanvas;
29424
31024
  var init_MathCanvas = __esm({
29425
31025
  "components/learning/molecules/MathCanvas.tsx"() {
@@ -29439,10 +31039,19 @@ var init_MathCanvas = __esm({
29439
31039
  showAxes = true,
29440
31040
  showGrid = true,
29441
31041
  gridStep = 1,
31042
+ showTickLabels = false,
31043
+ showCurveLabels = false,
29442
31044
  curves = [],
29443
31045
  points = [],
29444
31046
  vectors = [],
31047
+ regions = [],
31048
+ bars = [],
31049
+ guides = [],
31050
+ angles = [],
31051
+ hops = [],
29445
31052
  shapes = [],
31053
+ readouts,
31054
+ traces,
29446
31055
  interactive = false,
29447
31056
  animate = false,
29448
31057
  onShapeClick,
@@ -29456,6 +31065,8 @@ var init_MathCanvas = __esm({
29456
31065
  const plotH = height - margin * 2;
29457
31066
  const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
29458
31067
  const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
31068
+ const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
31069
+ const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
29459
31070
  if (showGrid) {
29460
31071
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep) {
29461
31072
  const px = mapX(x);
@@ -29466,14 +31077,99 @@ var init_MathCanvas = __esm({
29466
31077
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color: "#e5e7eb", lineWidth: 1 });
29467
31078
  }
29468
31079
  }
31080
+ if (showTickLabels) {
31081
+ const labelEveryX = Math.max(1, Math.ceil((xMax - xMin) / gridStep / Math.floor(plotW / 40)));
31082
+ let kx = 0;
31083
+ for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
31084
+ if (kx % labelEveryX === 0 && x !== 0) {
31085
+ out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: 10, align: "center" });
31086
+ }
31087
+ }
31088
+ const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
31089
+ let ky = 0;
31090
+ for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
31091
+ if (ky % labelEveryY === 0 && y !== 0) {
31092
+ out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: 10, align: "right" });
31093
+ }
31094
+ }
31095
+ if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
31096
+ out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: 10, align: "right" });
31097
+ }
31098
+ }
31099
+ for (const region of regions) {
31100
+ if (!region.samples || region.samples.length === 0) continue;
31101
+ const baseline = region.baseline ?? 0;
31102
+ const clampedPoint = (p) => ({
31103
+ x: mapX(Math.min(xMax, Math.max(xMin, p.x))),
31104
+ y: mapY(Math.min(yMax, Math.max(yMin, p.y)))
31105
+ });
31106
+ const upper = region.samples.map(clampedPoint);
31107
+ const first = region.samples[0];
31108
+ const last = region.samples[region.samples.length - 1];
31109
+ 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 })];
31110
+ const color = region.color ?? "#2563eb";
31111
+ out.push({
31112
+ type: "polygon",
31113
+ points: [...upper, ...closing],
31114
+ fill: color,
31115
+ color,
31116
+ opacity: region.opacity ?? 0.2,
31117
+ lineWidth: 1
31118
+ });
31119
+ if (region.label) {
31120
+ const mid = Math.floor(region.samples.length / 2);
31121
+ out.push({
31122
+ type: "text",
31123
+ x: mapX((first.x + last.x) / 2),
31124
+ y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
31125
+ text: region.label,
31126
+ color: "#111827",
31127
+ fontSize: 11
31128
+ });
31129
+ }
31130
+ }
31131
+ for (const bar of bars) {
31132
+ if (bar.x + bar.width < xMin || bar.x > xMax) continue;
31133
+ const y0 = bar.y0 ?? 0;
31134
+ const color = bar.color ?? "#93c5fd";
31135
+ out.push({
31136
+ type: "rect",
31137
+ x: mapX(bar.x),
31138
+ y: mapY(Math.max(y0, bar.y1)),
31139
+ width: mapX(bar.x + bar.width) - mapX(bar.x),
31140
+ height: Math.abs(mapY(bar.y1) - mapY(y0)),
31141
+ color,
31142
+ fill: color,
31143
+ opacity: bar.opacity ?? 0.5,
31144
+ lineWidth: 1
31145
+ });
31146
+ }
29469
31147
  if (showAxes) {
29470
- const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
29471
- const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
29472
31148
  out.push({ type: "line", x1: margin, y1: xAxisY, x2: width - margin, y2: xAxisY, color: "#374151", lineWidth: 2 });
29473
31149
  out.push({ type: "line", x1: yAxisX, y1: margin, x2: yAxisX, y2: height - margin, color: "#374151", lineWidth: 2 });
29474
31150
  }
31151
+ for (const guide of guides) {
31152
+ const color = guide.color ?? "#9ca3af";
31153
+ const dash = guide.dash ?? "dashed";
31154
+ if (guide.kind === "vline") {
31155
+ if (guide.at < xMin || guide.at > xMax) continue;
31156
+ const px = mapX(guide.at);
31157
+ out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
31158
+ if (guide.label) {
31159
+ out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color: "#111827", fontSize: 11 });
31160
+ }
31161
+ } else {
31162
+ if (guide.at < yMin || guide.at > yMax) continue;
31163
+ const py = mapY(guide.at);
31164
+ out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
31165
+ if (guide.label) {
31166
+ out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color: "#111827", fontSize: 11, align: "right" });
31167
+ }
31168
+ }
31169
+ }
29475
31170
  for (const curve of curves) {
29476
31171
  if (!curve.samples || curve.samples.length < 2) continue;
31172
+ let lastInRange;
29477
31173
  for (let i = 1; i < curve.samples.length; i++) {
29478
31174
  const a = curve.samples[i - 1];
29479
31175
  const b = curve.samples[i];
@@ -29485,19 +31181,97 @@ var init_MathCanvas = __esm({
29485
31181
  x2: mapX(b.x),
29486
31182
  y2: mapY(b.y),
29487
31183
  color: curve.color ?? "#2563eb",
29488
- lineWidth: 2
31184
+ lineWidth: 2,
31185
+ dash: curve.dash
31186
+ });
31187
+ lastInRange = b;
31188
+ }
31189
+ if (showCurveLabels && curve.label && lastInRange) {
31190
+ out.push({
31191
+ type: "text",
31192
+ x: mapX(lastInRange.x) + 6,
31193
+ y: mapY(lastInRange.y) - 6,
31194
+ text: curve.label,
31195
+ color: curve.color ?? "#2563eb",
31196
+ fontSize: 11
31197
+ });
31198
+ }
31199
+ }
31200
+ for (const hop of hops) {
31201
+ const x1 = mapX(hop.from);
31202
+ const x2 = mapX(hop.to);
31203
+ const peak = Math.min(36, plotH * 0.3);
31204
+ const color = hop.color ?? "#7c3aed";
31205
+ out.push({
31206
+ type: "ellipse",
31207
+ x: (x1 + x2) / 2,
31208
+ y: xAxisY,
31209
+ width: Math.abs(x2 - x1),
31210
+ height: 2 * peak,
31211
+ startAngle: 180,
31212
+ endAngle: 360,
31213
+ color
31214
+ });
31215
+ const s = Math.sign(hop.to - hop.from);
31216
+ out.push({
31217
+ type: "polygon",
31218
+ points: [
31219
+ { x: x2, y: xAxisY },
31220
+ { x: x2 - 4 * s, y: xAxisY - 7 },
31221
+ { x: x2 + 2 * s, y: xAxisY - 7 }
31222
+ ],
31223
+ fill: color,
31224
+ color
31225
+ });
31226
+ if (hop.label) {
31227
+ out.push({
31228
+ type: "text",
31229
+ x: (x1 + x2) / 2,
31230
+ y: xAxisY - peak - 8,
31231
+ text: hop.label,
31232
+ color: "#111827",
31233
+ fontSize: 10,
31234
+ align: "center"
31235
+ });
31236
+ }
31237
+ }
31238
+ for (const angle of angles) {
31239
+ const radius = angle.radius ?? 0.8;
31240
+ const color = angle.color ?? "#0ea5e9";
31241
+ out.push({
31242
+ type: "ellipse",
31243
+ x: mapX(angle.x),
31244
+ y: mapY(angle.y),
31245
+ width: 2 * radius * plotW / (xMax - xMin),
31246
+ height: 2 * radius * plotH / (yMax - yMin),
31247
+ startAngle: -angle.to,
31248
+ endAngle: -angle.from,
31249
+ color
31250
+ });
31251
+ if (angle.label) {
31252
+ const mid = (angle.from + angle.to) / 2;
31253
+ const rad = mid * Math.PI / 180;
31254
+ out.push({
31255
+ type: "text",
31256
+ x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
31257
+ y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
31258
+ text: angle.label,
31259
+ color: "#111827",
31260
+ fontSize: 11,
31261
+ align: "center"
29489
31262
  });
29490
31263
  }
29491
31264
  }
29492
31265
  for (const p of points) {
29493
31266
  if (p.x < xMin || p.x > xMax || p.y < yMin || p.y > yMax) continue;
31267
+ const isOpen = p.style === "open";
29494
31268
  out.push({
29495
31269
  type: "circle",
29496
31270
  x: mapX(p.x),
29497
31271
  y: mapY(p.y),
29498
31272
  radius: p.radius ?? 4,
29499
31273
  color: p.color ?? "#dc2626",
29500
- fill: p.color ?? "#dc2626"
31274
+ fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
29501
31275
  });
29502
31276
  if (p.label) {
29503
31277
  out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: "#111827", fontSize: 12 });
@@ -29516,7 +31290,28 @@ var init_MathCanvas = __esm({
29516
31290
  }
29517
31291
  out.push(...shapes);
29518
31292
  return out;
29519
- }, [width, height, xMin, xMax, yMin, yMax, showAxes, showGrid, gridStep, curves, points, vectors, shapes]);
31293
+ }, [
31294
+ width,
31295
+ height,
31296
+ xMin,
31297
+ xMax,
31298
+ yMin,
31299
+ yMax,
31300
+ showAxes,
31301
+ showGrid,
31302
+ gridStep,
31303
+ showTickLabels,
31304
+ showCurveLabels,
31305
+ curves,
31306
+ points,
31307
+ vectors,
31308
+ regions,
31309
+ bars,
31310
+ guides,
31311
+ angles,
31312
+ hops,
31313
+ shapes
31314
+ ]);
29520
31315
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
29521
31316
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
29522
31317
  /* @__PURE__ */ jsx(
@@ -29525,6 +31320,8 @@ var init_MathCanvas = __esm({
29525
31320
  width,
29526
31321
  height,
29527
31322
  shapes: derivedShapes,
31323
+ readouts,
31324
+ traces,
29528
31325
  interactive,
29529
31326
  animate,
29530
31327
  onShapeClick,
@@ -29536,7 +31333,315 @@ var init_MathCanvas = __esm({
29536
31333
  };
29537
31334
  }
29538
31335
  });
29539
- var physicsLog2, PhysicsCanvas;
31336
+ function formatMeterValue(v) {
31337
+ return Number.isInteger(v) ? String(v) : String(Number(v.toFixed(2)));
31338
+ }
31339
+ function sceneObjectShapes(obj, canvasWidth, canvasHeight) {
31340
+ const out = [];
31341
+ const color = obj.color ?? "#334155";
31342
+ switch (obj.kind) {
31343
+ case "ground": {
31344
+ const xStart = obj.x1 ?? 0;
31345
+ const xEnd = obj.x2 ?? canvasWidth;
31346
+ const y = obj.y ?? 0;
31347
+ out.push({ type: "line", x1: xStart, y1: y, x2: xEnd, y2: y, color, lineWidth: 2 });
31348
+ for (let hx = xStart + 7; hx <= xEnd; hx += 14) {
31349
+ out.push({ type: "line", x1: hx, y1: y, x2: hx - 7, y2: y + 7, color, lineWidth: 1 });
31350
+ }
31351
+ if (obj.label) {
31352
+ out.push({
31353
+ type: "text",
31354
+ x: (xStart + xEnd) / 2,
31355
+ y: y - 10,
31356
+ text: obj.label,
31357
+ color: PHYSICS_LABEL_COLOR,
31358
+ fontSize: 11,
31359
+ align: "center"
31360
+ });
31361
+ }
31362
+ break;
31363
+ }
31364
+ case "wall": {
31365
+ const yStart = obj.y1 ?? 0;
31366
+ const yEnd = obj.y2 ?? canvasHeight;
31367
+ const x = obj.x ?? 0;
31368
+ out.push({ type: "line", x1: x, y1: yStart, x2: x, y2: yEnd, color, lineWidth: 2 });
31369
+ for (let hy = yStart + 7; hy <= yEnd; hy += 14) {
31370
+ out.push({ type: "line", x1: x, y1: hy, x2: x - 7, y2: hy + 7, color, lineWidth: 1 });
31371
+ }
31372
+ if (obj.label) {
31373
+ out.push({
31374
+ type: "text",
31375
+ x: x + 12,
31376
+ y: (yStart + yEnd) / 2,
31377
+ text: obj.label,
31378
+ color: PHYSICS_LABEL_COLOR,
31379
+ fontSize: 11,
31380
+ align: "left"
31381
+ });
31382
+ }
31383
+ break;
31384
+ }
31385
+ case "ramp": {
31386
+ const x1 = obj.x1 ?? 0;
31387
+ const y1 = obj.y1 ?? 0;
31388
+ const x2 = obj.x2 ?? canvasWidth;
31389
+ const y2 = obj.y2 ?? canvasHeight;
31390
+ out.push({
31391
+ type: "polygon",
31392
+ points: [
31393
+ { x: x1, y: y1 },
31394
+ { x: x2, y: y2 },
31395
+ { x: x1, y: y2 }
31396
+ ],
31397
+ color,
31398
+ fill: obj.fill ?? "#e2e8f0",
31399
+ lineWidth: 2
31400
+ });
31401
+ if (obj.label) {
31402
+ out.push({
31403
+ type: "text",
31404
+ x: (2 * x1 + x2) / 3,
31405
+ y: (y1 + 2 * y2) / 3,
31406
+ text: obj.label,
31407
+ color: PHYSICS_LABEL_COLOR,
31408
+ fontSize: 11,
31409
+ align: "center"
31410
+ });
31411
+ }
31412
+ break;
31413
+ }
31414
+ case "box": {
31415
+ const x = obj.x ?? 0;
31416
+ const y = obj.y ?? 0;
31417
+ const w = obj.width ?? 40;
31418
+ const h = obj.height ?? 40;
31419
+ out.push({ type: "rect", x, y, width: w, height: h, color, fill: obj.fill, lineWidth: 2 });
31420
+ if (obj.label) {
31421
+ out.push({
31422
+ type: "text",
31423
+ x: x + w / 2,
31424
+ y: y + h / 2,
31425
+ text: obj.label,
31426
+ color: PHYSICS_LABEL_COLOR,
31427
+ fontSize: 11,
31428
+ align: "center"
31429
+ });
31430
+ }
31431
+ break;
31432
+ }
31433
+ case "pivot": {
31434
+ const x = obj.x ?? 0;
31435
+ const y = obj.y ?? 0;
31436
+ out.push({ type: "circle", x, y, radius: 5, color, fill: color });
31437
+ out.push({ type: "line", x1: x - 14, y1: y - 8, x2: x + 14, y2: y - 8, color, lineWidth: 1 });
31438
+ for (let k = 0; k < 5; k++) {
31439
+ const hx = x - 14 + 7 * k;
31440
+ out.push({ type: "line", x1: hx, y1: y - 8, x2: hx - 6, y2: y - 14, color, lineWidth: 1 });
31441
+ }
31442
+ if (obj.label) {
31443
+ out.push({
31444
+ type: "text",
31445
+ x,
31446
+ y: y - 20,
31447
+ text: obj.label,
31448
+ color: PHYSICS_LABEL_COLOR,
31449
+ fontSize: 11,
31450
+ align: "center"
31451
+ });
31452
+ }
31453
+ break;
31454
+ }
31455
+ }
31456
+ return out;
31457
+ }
31458
+ function trailShapes(trail) {
31459
+ const n = trail.points.length;
31460
+ if (n < 2) return [];
31461
+ const color = trail.color ?? "#94a3b8";
31462
+ const lineWidth = trail.width ?? 2;
31463
+ const fade = trail.fade ?? true;
31464
+ const globalOpacity = trail.opacity ?? 1;
31465
+ const out = [];
31466
+ for (let i = 0; i < n - 1; i++) {
31467
+ const a = trail.points[i];
31468
+ const b = trail.points[i + 1];
31469
+ const segmentOpacity = fade ? 0.12 + 0.68 * i / (n - 1) : 0.6;
31470
+ out.push({
31471
+ type: "line",
31472
+ x1: a.x,
31473
+ y1: a.y,
31474
+ x2: b.x,
31475
+ y2: b.y,
31476
+ color,
31477
+ lineWidth,
31478
+ opacity: segmentOpacity * globalOpacity
31479
+ });
31480
+ }
31481
+ return out;
31482
+ }
31483
+ function constraintShapes(c, a, b) {
31484
+ const color = c.color ?? "#9ca3af";
31485
+ const kind = c.kind ?? "rod";
31486
+ if (kind === "rod") {
31487
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2 }];
31488
+ }
31489
+ if (kind === "string") {
31490
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2, dash: "dashed" }];
31491
+ }
31492
+ const COILS = 8;
31493
+ const AMP = 7;
31494
+ const LEAD = 10;
31495
+ const dx = b.x - a.x;
31496
+ const dy = b.y - a.y;
31497
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
31498
+ const ux = dx / dist;
31499
+ const uy = dy / dist;
31500
+ const perpX = -uy;
31501
+ const perpY = ux;
31502
+ const aPrime = { x: a.x + LEAD * ux, y: a.y + LEAD * uy };
31503
+ const bPrime = { x: b.x - LEAD * ux, y: b.y - LEAD * uy };
31504
+ const m = 2 * COILS;
31505
+ const polyline = [{ x: a.x, y: a.y }, aPrime];
31506
+ for (let j = 1; j <= m; j++) {
31507
+ const t = j / (m + 1);
31508
+ const baseX = aPrime.x + t * (bPrime.x - aPrime.x);
31509
+ const baseY = aPrime.y + t * (bPrime.y - aPrime.y);
31510
+ const sign = j % 2 === 0 ? 1 : -1;
31511
+ polyline.push({ x: baseX + sign * AMP * perpX, y: baseY + sign * AMP * perpY });
31512
+ }
31513
+ polyline.push(bPrime, { x: b.x, y: b.y });
31514
+ const out = [];
31515
+ for (let i = 1; i < polyline.length; i++) {
31516
+ out.push({
31517
+ type: "line",
31518
+ x1: polyline[i - 1].x,
31519
+ y1: polyline[i - 1].y,
31520
+ x2: polyline[i].x,
31521
+ y2: polyline[i].y,
31522
+ color,
31523
+ lineWidth: 2
31524
+ });
31525
+ }
31526
+ return out;
31527
+ }
31528
+ function vectorShapes(v, bodyById) {
31529
+ let ax;
31530
+ let ay;
31531
+ if (v.body) {
31532
+ const anchor = bodyById.get(v.body);
31533
+ if (!anchor) return [];
31534
+ ax = anchor.x;
31535
+ ay = anchor.y;
31536
+ } else {
31537
+ ax = v.x ?? 0;
31538
+ ay = v.y ?? 0;
31539
+ }
31540
+ const scale = v.scale ?? 1;
31541
+ const color = v.color ?? "#dc2626";
31542
+ const tx = ax + v.dx * scale;
31543
+ const ty = ay + v.dy * scale;
31544
+ const out = [{ type: "arrow", x1: ax, y1: ay, x2: tx, y2: ty, color, lineWidth: 2, dash: v.dash }];
31545
+ if (v.label) {
31546
+ const dist = Math.max(1e-6, Math.hypot(tx - ax, ty - ay));
31547
+ const ux = (tx - ax) / dist;
31548
+ const uy = (ty - ay) / dist;
31549
+ out.push({
31550
+ type: "text",
31551
+ x: tx + 8 * ux,
31552
+ y: ty + 8 * uy,
31553
+ text: v.label,
31554
+ color,
31555
+ fontSize: 11,
31556
+ align: "center"
31557
+ });
31558
+ }
31559
+ return out;
31560
+ }
31561
+ function angleMarkerShapes(a) {
31562
+ const radius = a.radius ?? 26;
31563
+ const color = a.color ?? "#0ea5e9";
31564
+ const out = [
31565
+ {
31566
+ type: "ellipse",
31567
+ x: a.x,
31568
+ y: a.y,
31569
+ width: radius * 2,
31570
+ height: radius * 2,
31571
+ startAngle: a.from,
31572
+ endAngle: a.to,
31573
+ color,
31574
+ lineWidth: 2
31575
+ }
31576
+ ];
31577
+ if (a.label) {
31578
+ const mid = (a.from + a.to) / 2 * (Math.PI / 180);
31579
+ out.push({
31580
+ type: "text",
31581
+ x: a.x + (radius + 13) * Math.cos(mid),
31582
+ y: a.y + (radius + 13) * Math.sin(mid),
31583
+ text: a.label,
31584
+ color,
31585
+ fontSize: 11,
31586
+ align: "center"
31587
+ });
31588
+ }
31589
+ return out;
31590
+ }
31591
+ function fieldShapes(field, canvasWidth, canvasHeight) {
31592
+ const spacing = field.spacing ?? 48;
31593
+ const size = field.size ?? 14;
31594
+ const color = field.color ?? "#94a3b8";
31595
+ const regionX = field.x ?? 0;
31596
+ const regionY = field.y ?? 0;
31597
+ const regionW = field.width ?? canvasWidth;
31598
+ const regionH = field.height ?? canvasHeight;
31599
+ const out = [];
31600
+ for (let gx = regionX + spacing / 2; gx < regionX + regionW; gx += spacing) {
31601
+ for (let gy = regionY + spacing / 2; gy < regionY + regionH; gy += spacing) {
31602
+ if (field.kind === "arrows") {
31603
+ const rad = (field.angle ?? 0) * Math.PI / 180;
31604
+ const hx = Math.cos(rad) * size / 2;
31605
+ const hy = Math.sin(rad) * size / 2;
31606
+ out.push({ type: "arrow", x1: gx - hx, y1: gy - hy, x2: gx + hx, y2: gy + hy, color, lineWidth: 2 });
31607
+ } else if (field.kind === "into") {
31608
+ const r = size / 3;
31609
+ const d = 0.6 * r * Math.SQRT1_2;
31610
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
31611
+ out.push({ type: "line", x1: gx - d, y1: gy - d, x2: gx + d, y2: gy + d, color, lineWidth: 1 });
31612
+ out.push({ type: "line", x1: gx - d, y1: gy + d, x2: gx + d, y2: gy - d, color, lineWidth: 1 });
31613
+ } else {
31614
+ const r = size / 3;
31615
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
31616
+ out.push({ type: "circle", x: gx, y: gy, radius: 1.5, color, fill: color });
31617
+ }
31618
+ }
31619
+ }
31620
+ return out;
31621
+ }
31622
+ function meterShapes(meters, canvasHeight) {
31623
+ const n = meters.length;
31624
+ const out = [];
31625
+ const sharedMax = Math.max(1e-6, ...meters.map((m) => m.value));
31626
+ meters.forEach((meter, i) => {
31627
+ const rowY = canvasHeight - 10 - 16 * (n - i);
31628
+ const color = meter.color ?? "#3b82f6";
31629
+ const M = meter.max ?? sharedMax;
31630
+ const w = Math.round(Math.min(1, Math.max(0, meter.value / M)) * 110);
31631
+ out.push({ type: "text", x: 8, y: rowY + 8, text: meter.label, color: PHYSICS_LABEL_COLOR, fontSize: 10 });
31632
+ out.push({ type: "rect", x: 52, y: rowY, width: w, height: 10, color, fill: color });
31633
+ out.push({
31634
+ type: "text",
31635
+ x: 166,
31636
+ y: rowY + 8,
31637
+ text: formatMeterValue(meter.value),
31638
+ color: "#6b7280",
31639
+ fontSize: 9
31640
+ });
31641
+ });
31642
+ return out;
31643
+ }
31644
+ var physicsLog2, PHYSICS_LABEL_COLOR, PhysicsCanvas;
29540
31645
  var init_PhysicsCanvas = __esm({
29541
31646
  "components/learning/molecules/PhysicsCanvas.tsx"() {
29542
31647
  "use client";
@@ -29545,6 +31650,7 @@ var init_PhysicsCanvas = __esm({
29545
31650
  init_LearningCanvas();
29546
31651
  init_learningScene3D();
29547
31652
  physicsLog2 = createLogger("almadar:ui:physics-canvas");
31653
+ PHYSICS_LABEL_COLOR = "#374151";
29548
31654
  PhysicsCanvas = ({
29549
31655
  className,
29550
31656
  width = 600,
@@ -29561,7 +31667,18 @@ var init_PhysicsCanvas = __esm({
29561
31667
  showForces = false,
29562
31668
  velocityScale = 20,
29563
31669
  forceScale = 20,
31670
+ sceneObjects = [],
31671
+ trails = [],
31672
+ vectors = [],
31673
+ surface3d,
31674
+ vectors3d = [],
31675
+ vectorScale = 1,
31676
+ angles = [],
31677
+ field,
31678
+ meters = [],
29564
31679
  shapes = [],
31680
+ readouts,
31681
+ traces,
29565
31682
  showGrid,
29566
31683
  shadows,
29567
31684
  interactive,
@@ -29576,19 +31693,14 @@ var init_PhysicsCanvas = __esm({
29576
31693
  for (const b of bodies) {
29577
31694
  if (b.id) bodyById.set(b.id, b);
29578
31695
  }
31696
+ if (field) out.push(...fieldShapes(field, width, height));
31697
+ for (const obj of sceneObjects) out.push(...sceneObjectShapes(obj, width, height));
31698
+ for (const trail of trails) out.push(...trailShapes(trail));
29579
31699
  for (const c of constraints) {
29580
31700
  const a = bodyById.get(c.from);
29581
31701
  const b = bodyById.get(c.to);
29582
31702
  if (!a || !b) continue;
29583
- out.push({
29584
- type: "line",
29585
- x1: a.x,
29586
- y1: a.y,
29587
- x2: b.x,
29588
- y2: b.y,
29589
- color: c.color ?? "#9ca3af",
29590
- lineWidth: 2
29591
- });
31703
+ out.push(...constraintShapes(c, a, b));
29592
31704
  }
29593
31705
  for (const b of bodies) {
29594
31706
  out.push({
@@ -29633,14 +31745,51 @@ var init_PhysicsCanvas = __esm({
29633
31745
  });
29634
31746
  }
29635
31747
  }
31748
+ for (const v of vectors) out.push(...vectorShapes(v, bodyById));
31749
+ for (const a of angles) out.push(...angleMarkerShapes(a));
31750
+ if (meters.length > 0) out.push(...meterShapes(meters, height));
29636
31751
  out.push(...shapes);
29637
31752
  return out;
29638
- }, [bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes]);
31753
+ }, [
31754
+ bodies,
31755
+ constraints,
31756
+ showVelocity,
31757
+ showForces,
31758
+ velocityScale,
31759
+ forceScale,
31760
+ sceneObjects,
31761
+ trails,
31762
+ vectors,
31763
+ angles,
31764
+ field,
31765
+ meters,
31766
+ shapes,
31767
+ width,
31768
+ height
31769
+ ]);
29639
31770
  const drawables3D = useMemo(() => {
29640
31771
  if (mode !== "3d") return [];
29641
31772
  if (shapes.length > 0) {
29642
31773
  physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
29643
31774
  }
31775
+ if (sceneObjects.length > 0) {
31776
+ physicsLog2.debug("sceneObjects ignored in 3D mode (pixel-authored 2D vocabulary)", { count: sceneObjects.length });
31777
+ }
31778
+ if (vectors.length > 0) {
31779
+ physicsLog2.debug("vectors ignored in 3D mode (pixel-authored 2D vocabulary)", { count: vectors.length });
31780
+ }
31781
+ if (angles.length > 0) {
31782
+ physicsLog2.debug("angles ignored in 3D mode (pixel-authored 2D vocabulary)", { count: angles.length });
31783
+ }
31784
+ if (field) {
31785
+ physicsLog2.debug("field ignored in 3D mode (pixel-authored 2D vocabulary)");
31786
+ }
31787
+ if (meters.length > 0) {
31788
+ physicsLog2.debug("meters ignored in 3D mode (pixel-authored 2D vocabulary)", { count: meters.length });
31789
+ }
31790
+ if (animate) {
31791
+ physicsLog2.debug("animate ignored in 3D mode (motion is entity-state driven)");
31792
+ }
29644
31793
  const out = [];
29645
31794
  const labelColor = labelColorForBackground(backgroundColor);
29646
31795
  const bodyById = /* @__PURE__ */ new Map();
@@ -29688,15 +31837,67 @@ var init_PhysicsCanvas = __esm({
29688
31837
  if (arrow) out.push(arrow);
29689
31838
  }
29690
31839
  }
31840
+ for (const trail of trails) {
31841
+ if (trail.fade !== void 0) {
31842
+ physicsLog2.debug("trail.fade ignored in 3D mode (2D-only fade curve \u2014 3D draws an opaque tube)", { id: trail.id });
31843
+ }
31844
+ const points = trail.points.map((p) => [p.x, p.y, p.z ?? 0]);
31845
+ out.push(
31846
+ ...polylineTube(points, trail.width ?? 0.05, trail.color ?? "#94a3b8", {
31847
+ ...trail.opacity !== void 0 ? { opacity: trail.opacity } : {}
31848
+ })
31849
+ );
31850
+ }
31851
+ if (surface3d) {
31852
+ out.push(...heightFieldMesh(surface3d));
31853
+ }
31854
+ if (vectors3d.length > 0) {
31855
+ out.push(
31856
+ ...arrowField(
31857
+ vectors3d.map((v) => ({
31858
+ id: v.id,
31859
+ from: [v.x, v.y, v.z ?? 0],
31860
+ delta: [v.dx, v.dy, v.dz ?? 0],
31861
+ color: v.color,
31862
+ label: v.label,
31863
+ width: v.width
31864
+ })),
31865
+ { scale: vectorScale, labelColor }
31866
+ )
31867
+ );
31868
+ }
29691
31869
  return out;
29692
- }, [mode, bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes, backgroundColor]);
31870
+ }, [
31871
+ mode,
31872
+ bodies,
31873
+ constraints,
31874
+ showVelocity,
31875
+ showForces,
31876
+ velocityScale,
31877
+ forceScale,
31878
+ shapes,
31879
+ sceneObjects,
31880
+ trails,
31881
+ vectors,
31882
+ surface3d,
31883
+ vectors3d,
31884
+ vectorScale,
31885
+ angles,
31886
+ field,
31887
+ meters,
31888
+ animate,
31889
+ backgroundColor
31890
+ ]);
29693
31891
  const bodyIndexById = useMemo(() => {
29694
31892
  const m = /* @__PURE__ */ new Map();
31893
+ vectors3d.forEach((v, i) => {
31894
+ if (v.id) m.set(v.id, i);
31895
+ });
29695
31896
  bodies.forEach((b, i) => {
29696
31897
  if (b.id) m.set(b.id, i);
29697
31898
  });
29698
31899
  return m;
29699
- }, [bodies]);
31900
+ }, [bodies, vectors3d]);
29700
31901
  if (mode === "3d") {
29701
31902
  return /* @__PURE__ */ jsx(
29702
31903
  LearningScene3D,
@@ -29728,6 +31929,8 @@ var init_PhysicsCanvas = __esm({
29728
31929
  height,
29729
31930
  backgroundColor,
29730
31931
  shapes: derivedShapes,
31932
+ readouts,
31933
+ traces,
29731
31934
  interactive: interactive ?? false,
29732
31935
  animate,
29733
31936
  onShapeClick,
@@ -29878,7 +32081,7 @@ function layoutFlow(nodeIds, adjacency, roots, width, height, margin) {
29878
32081
  }
29879
32082
  return nodeIds.map((id) => positions.get(id));
29880
32083
  }
29881
- function layoutTree(nodeIds, adjacency, roots, width, height, margin) {
32084
+ function layoutTree2(nodeIds, adjacency, roots, width, height, margin) {
29882
32085
  const effectiveRoots = roots.length > 0 ? roots : [nodeIds[0]];
29883
32086
  const layers = assignLayers(nodeIds, adjacency, effectiveRoots);
29884
32087
  const maxLayer = Math.max(...Array.from(layers.values()));
@@ -29934,7 +32137,7 @@ function computeStaticLayout(mode, input) {
29934
32137
  const adjacency = buildAdjacency(nodeIds, edges);
29935
32138
  const roots = findRoots(nodeIds, adjacency);
29936
32139
  if (mode === "flow") return layoutFlow(nodeIds, adjacency, roots, width, height, margin);
29937
- if (mode === "tree") return layoutTree(nodeIds, adjacency, roots, width, height, margin);
32140
+ if (mode === "tree") return layoutTree2(nodeIds, adjacency, roots, width, height, margin);
29938
32141
  return layoutRadial(nodeIds, adjacency, roots, width, height, margin);
29939
32142
  }
29940
32143
  var init_graphViewLayouts = __esm({
@@ -30346,7 +32549,7 @@ var init_MapView = __esm({
30346
32549
  shadowSize: [41, 41]
30347
32550
  });
30348
32551
  L.Marker.prototype.options.icon = defaultIcon;
30349
- const { useEffect: useEffect66, useRef: useRef65, useCallback: useCallback107, useState: useState104 } = React77__default;
32552
+ const { useEffect: useEffect66, useRef: useRef65, useCallback: useCallback108, useState: useState104 } = React77__default;
30350
32553
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
30351
32554
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
30352
32555
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -30392,7 +32595,7 @@ var init_MapView = __esm({
30392
32595
  }) {
30393
32596
  const eventBus = useEventBus2();
30394
32597
  const [clickedPosition, setClickedPosition] = useState104(null);
30395
- const handleMapClick = useCallback107((lat, lng) => {
32598
+ const handleMapClick = useCallback108((lat, lng) => {
30396
32599
  if (showClickedPin) {
30397
32600
  setClickedPosition({ lat, lng });
30398
32601
  }
@@ -30401,7 +32604,7 @@ var init_MapView = __esm({
30401
32604
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
30402
32605
  }
30403
32606
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
30404
- const handleMarkerClick = useCallback107((marker) => {
32607
+ const handleMarkerClick = useCallback108((marker) => {
30405
32608
  onMarkerClick?.(marker);
30406
32609
  if (markerClickEvent) {
30407
32610
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -31177,26 +33380,6 @@ var init_Lightbox = __esm({
31177
33380
  Lightbox.displayName = "Lightbox";
31178
33381
  }
31179
33382
  });
31180
- function useMediaQuery(query) {
31181
- const subscribe = useCallback(
31182
- (onChange) => {
31183
- const mql = window.matchMedia(query);
31184
- mql.addEventListener("change", onChange);
31185
- return () => mql.removeEventListener("change", onChange);
31186
- },
31187
- [query]
31188
- );
31189
- return useSyncExternalStore(
31190
- subscribe,
31191
- () => window.matchMedia(query).matches,
31192
- () => false
31193
- );
31194
- }
31195
- var init_useMediaQuery = __esm({
31196
- "hooks/useMediaQuery.ts"() {
31197
- "use client";
31198
- }
31199
- });
31200
33383
  function renderIconInput3(icon, props) {
31201
33384
  return typeof icon === "string" ? /* @__PURE__ */ jsx(Icon, { name: icon, ...props }) : /* @__PURE__ */ jsx(Icon, { icon, ...props });
31202
33385
  }
@@ -31235,8 +33418,8 @@ function TableView({
31235
33418
  columns,
31236
33419
  fields,
31237
33420
  itemActions,
31238
- maxInlineActions,
31239
- itemClickEvent,
33421
+ maxInlineActions: _maxInlineActions,
33422
+ itemClickEvent = "",
31240
33423
  selectable = false,
31241
33424
  selectEvent,
31242
33425
  selectedIds,
@@ -31286,7 +33469,6 @@ function TableView({
31286
33469
  const hasMore = pageSize > 0 && visibleCount < ordered2.length;
31287
33470
  const hasRenderProp = typeof children === "function";
31288
33471
  const idField = dndItemIdField ?? "id";
31289
- const isCoarsePointer = useMediaQuery("(pointer: coarse)");
31290
33472
  React77__default.useEffect(() => {
31291
33473
  tableViewLog.debug("render", {
31292
33474
  rowCount: data.length,
@@ -31328,21 +33510,14 @@ function TableView({
31328
33510
  const dir = sortColumn === (col.field ?? col.key) && sortDirection === "asc" ? "desc" : "asc";
31329
33511
  eventBus.emit(`UI:${sortEvent}`, { column: col.field ?? col.key, direction: dir });
31330
33512
  };
31331
- const handleActionClick = (action, row) => (e) => {
31332
- e.stopPropagation();
31333
- const payload = {
31334
- id: row.id,
31335
- row
31336
- };
31337
- eventBus.emit(`UI:${action.event}`, payload);
31338
- };
33513
+ const rowClickEvent = itemClickEvent || actionDefs.find((a) => a.variant !== "danger")?.event;
31339
33514
  const handleRowClick = (row) => () => {
31340
- if (!itemClickEvent) return;
33515
+ if (!rowClickEvent) return;
31341
33516
  const payload = {
31342
33517
  id: row.id,
31343
33518
  row
31344
33519
  };
31345
- eventBus.emit(`UI:${itemClickEvent}`, payload);
33520
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
31346
33521
  };
31347
33522
  const colFloors = React77__default.useMemo(
31348
33523
  () => colDefs.map((col) => {
@@ -31358,10 +33533,7 @@ function TableView({
31358
33533
  const statusNode = isLoading ? /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) }) : error ? /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "error", children: error.message }) }) : data.length === 0 ? /* @__PURE__ */ jsx(Box, { className: "text-center py-12", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: emptyMessage || t("empty.noItems") }) }) : null;
31359
33534
  const lk = LOOKS[look];
31360
33535
  const hasActions = actionDefs.length > 0;
31361
- const effectiveMaxInline = isCoarsePointer ? 0 : maxInlineActions;
31362
- const inlineActionCount = hasActions ? effectiveMaxInline != null ? Math.min(actionDefs.length, effectiveMaxInline) : actionDefs.length : 0;
31363
- const hasOverflowActions = hasActions && effectiveMaxInline != null && actionDefs.length > effectiveMaxInline;
31364
- const actionsTrack = hasActions ? `${inlineActionCount * 6 + (hasOverflowActions ? 3 : 0)}rem` : null;
33536
+ const actionsTrack = hasActions ? "3rem" : null;
31365
33537
  const gridTemplateColumns = [
31366
33538
  selectable ? "auto" : null,
31367
33539
  ...colDefs.map((c, i) => c.width ?? `minmax(${colFloors[i]}ch, 1fr)`),
@@ -31408,7 +33580,7 @@ function TableView({
31408
33580
  col.key
31409
33581
  );
31410
33582
  }),
31411
- hasActions && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)]" })
33583
+ hasActions && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)] border-l border-[var(--color-border)] h-full" })
31412
33584
  ]
31413
33585
  }
31414
33586
  );
@@ -31420,12 +33592,12 @@ function TableView({
31420
33592
  role: "row",
31421
33593
  "data-entity-row": true,
31422
33594
  "data-entity-id": id,
31423
- onClick: itemClickEvent ? handleRowClick(row) : void 0,
33595
+ onClick: rowClickEvent ? handleRowClick(row) : void 0,
31424
33596
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
31425
33597
  className: cn(
31426
33598
  "group items-center gap-3 transition-colors duration-fast",
31427
33599
  hasRenderProp ? "flex" : "grid",
31428
- itemClickEvent && "cursor-pointer",
33600
+ rowClickEvent && "cursor-pointer",
31429
33601
  lk.rowPad,
31430
33602
  lk.divider && "border-b border-[var(--color-border)]",
31431
33603
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -31433,7 +33605,7 @@ function TableView({
31433
33605
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
31434
33606
  ),
31435
33607
  children: [
31436
- selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
33608
+ selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
31437
33609
  Checkbox,
31438
33610
  {
31439
33611
  checked: selected.has(id),
@@ -31454,53 +33626,37 @@ function TableView({
31454
33626
  }
31455
33627
  return /* @__PURE__ */ jsx(Box, { role: "cell", className: cellBase, children: /* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children: formatCell(raw, col.format) }) }, col.key);
31456
33628
  }),
31457
- hasActions && /* @__PURE__ */ jsxs(
33629
+ hasActions && /* @__PURE__ */ jsx(
31458
33630
  HStack,
31459
33631
  {
31460
33632
  gap: "xs",
31461
- onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
33633
+ onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0,
31462
33634
  className: cn(
31463
33635
  // Pinned: the fixed column tracks routinely overflow the caller's
31464
- // scroll container, which used to leave the actions off-screen.
31465
- // Opaque so scrolled cells pass underneath it.
33636
+ // scroll container, which would leave the kebab off-screen.
33637
+ // Opaque + hairline edge so it reads as a pinned column, not a
33638
+ // floating control, while scrolled cells pass underneath.
31466
33639
  "justify-end flex-shrink-0 sticky right-0 z-[1] transition-colors",
33640
+ "border-l border-[var(--color-border)]",
31467
33641
  lk.striped && index % 2 === 1 ? "bg-[var(--color-surface-subtle)]" : "bg-[var(--color-card)] group-hover:bg-[var(--color-surface-subtle)]"
31468
33642
  ),
31469
- children: [
31470
- (effectiveMaxInline != null ? actionDefs.slice(0, effectiveMaxInline) : actionDefs).map((action, i) => /* @__PURE__ */ jsxs(
31471
- Button,
31472
- {
31473
- variant: action.variant === "primary" ? "primary" : "ghost",
31474
- size: "sm",
31475
- onClick: handleActionClick(action, row),
31476
- "data-testid": `action-${action.event}`,
31477
- "data-row-id": String(row.id),
31478
- className: cn(action.variant === "danger" && "text-error hover:text-error hover:bg-error/10"),
31479
- children: [
31480
- action.icon && renderIconInput3(action.icon, { size: "xs", className: "mr-1" }),
31481
- action.label
31482
- ]
31483
- },
31484
- i
31485
- )),
31486
- effectiveMaxInline != null && actionDefs.length > effectiveMaxInline && /* @__PURE__ */ jsx(
31487
- Menu,
31488
- {
31489
- position: "bottom-end",
31490
- trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
31491
- items: actionDefs.slice(effectiveMaxInline).map((action) => ({
31492
- label: action.label,
31493
- icon: action.icon,
31494
- event: action.event,
31495
- variant: action.variant === "danger" ? "danger" : "default",
31496
- onClick: () => eventBus.emit(`UI:${action.event}`, {
31497
- id: row.id,
31498
- row
31499
- })
31500
- }))
31501
- }
31502
- )
31503
- ]
33643
+ children: /* @__PURE__ */ jsx(
33644
+ Menu,
33645
+ {
33646
+ position: "bottom-end",
33647
+ trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", "data-row-id": String(row.id), children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
33648
+ items: actionDefs.map((action) => ({
33649
+ label: action.label,
33650
+ icon: action.icon,
33651
+ event: action.event,
33652
+ variant: action.variant === "danger" ? "danger" : "default",
33653
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
33654
+ id: row.id,
33655
+ row
33656
+ })
33657
+ }))
33658
+ }
33659
+ )
31504
33660
  }
31505
33661
  )
31506
33662
  ]
@@ -31543,7 +33699,6 @@ var init_TableView = __esm({
31543
33699
  init_format();
31544
33700
  init_getNestedValue();
31545
33701
  init_useEventBus();
31546
- init_useMediaQuery();
31547
33702
  init_Box();
31548
33703
  init_Stack();
31549
33704
  init_Typography();
@@ -40143,6 +42298,7 @@ var init_molecules2 = __esm({
40143
42298
  init_BiologyCanvas();
40144
42299
  init_ChemistryCanvas();
40145
42300
  init_AlgorithmCanvas();
42301
+ init_AlgoGraphCanvas();
40146
42302
  init_learningScene3D();
40147
42303
  init_GraphView();
40148
42304
  init_MapView();
@@ -46825,6 +48981,7 @@ var init_component_registry_generated = __esm({
46825
48981
  init_ActionTile();
46826
48982
  init_ActivationBlock();
46827
48983
  init_ComponentPatterns();
48984
+ init_AlgoGraphCanvas();
46828
48985
  init_AlgorithmCanvas();
46829
48986
  init_AnimatedCounter();
46830
48987
  init_AnimatedGraphic();
@@ -47088,6 +49245,7 @@ var init_component_registry_generated = __esm({
47088
49245
  "ActivationBlock": ActivationBlock,
47089
49246
  "Alert": AlertPattern,
47090
49247
  "AlertPattern": AlertPattern,
49248
+ "AlgoGraphCanvas": AlgoGraphCanvas,
47091
49249
  "AlgorithmCanvas": AlgorithmCanvas,
47092
49250
  "AnimatedCounter": AnimatedCounter,
47093
49251
  "AnimatedGraphic": AnimatedGraphic,
@@ -50560,7 +52718,23 @@ init_useAuthContext();
50560
52718
  init_useSwipeGesture();
50561
52719
  init_useLongPress();
50562
52720
  init_useDragReorder();
50563
- init_useMediaQuery();
52721
+ function useMediaQuery(query) {
52722
+ const subscribe = useCallback(
52723
+ (onChange) => {
52724
+ const mql = window.matchMedia(query);
52725
+ mql.addEventListener("change", onChange);
52726
+ return () => mql.removeEventListener("change", onChange);
52727
+ },
52728
+ [query]
52729
+ );
52730
+ return useSyncExternalStore(
52731
+ subscribe,
52732
+ () => window.matchMedia(query).matches,
52733
+ () => false
52734
+ );
52735
+ }
52736
+
52737
+ // hooks/index.ts
50564
52738
  init_useInfiniteScroll();
50565
52739
  init_usePullToRefresh();
50566
52740
  init_useCanvasGestures();
@@ -50806,4 +52980,4 @@ function useGitHubBranches(owner, repo, enabled = true) {
50806
52980
  });
50807
52981
  }
50808
52982
 
50809
- export { ALL_PRESETS, ALMADAR_DND_MIME, AR_BOOK_FIELDS, AboutPageTemplate, Accordion, Card2 as ActionCard, ActionPalette, ActionTile, ActivationBlock, Alert, AlgorithmCanvas, AnimatedCounter, AnimatedGraphic, AnimatedReveal, ArticleSection, Aside, AssetPicker, AtlasImage, AtlasPanel, AuthLayout, Avatar, Badge, BehaviorView, BiologyCanvas, BloomQuizBlock, BookChapterView, BookCoverPage, BookNavBar, BookTableOfContents, BookViewer, Box, BranchingLogicBuilder, Breadcrumb, Button, ButtonGroup, CTABanner, CalendarGrid, Canvas, Canvas2D, Card, CardBody, CardContent, CardFooter, CardGrid, CardHeader, CardTitle, Carousel, CaseStudyCard, CaseStudyOrganism, Center, Chart, ChartLegend, ChatBar, Checkbox, ChemistryCanvas, ChoiceButton, Coachmark, CodeBlock, CodeRunnerPanel, CollapsibleSection, CommunityLinks, ConditionalWrapper, ConfettiEffect, ConfirmDialog, ConnectionBlock, Container, ContentRenderer, ContentSection, ControlButton, ControlGrid, CounterTemplate, DEFAULT_LIKERT_OPTIONS, DEFAULT_MATRIX_COLUMNS, DEFAULT_SLOTS, DIAMOND_TOP_Y, DashboardGrid, DashboardLayout, DataGrid, DataList, DataTable, DateRangePicker, DateRangeSelector, DayCell, DetailPanel, Dialog, DialogueBubble, Divider, DocBreadcrumb, DocPagination, DocSearch, DocSidebar, DocTOC, DocumentViewer, StateMachineView as DomStateMachineVisualizer, Drawer, DrawerSlot, ELEMENT_SELECTED_EVENT, EdgeDecoration, EditorCheckbox, EditorSelect, EditorSlider, EditorTextInput, EditorToolbar, EmojiPicker, EmptyState, EntityDisplayEvents, ErrorBoundary, ErrorState, FEATURE_COLORS, FEATURE_TYPES, FLOOR_HEIGHT, FeatureCard, FeatureDetailPageTemplate, FeatureGrid, FeatureGridOrganism, FileTree, FilterGroup, FilterPill, Flex, FlipCard, FlipContainer, FloatingActionButton, Form, FormActions, FormField, FormLayout, FormSection, FormSectionHeader, GameAudioToggle, GameHud, GameIcon, GameMenu, GameShell, GenericAppTemplate, GeometricPattern, GradientDivider, GraphCanvas, GraphView, Grid, GridPicker, HStack, Header, HealthBar, HeroOrganism, HeroSection, I18nProvider, IDENTITY_BOOK_FIELDS, Icon, IconPicker, ImportPreviewTree, ImportProgress, ImportSourcePicker, InfiniteScrollSentinel, Input, InputGroup, InstallBox, JazariStateMachine, JsonTreeEditor, Label, LandingPageTemplate, LawReferenceTooltip, LearningCanvas, LearningScene3D, Lightbox, LikertScale, LineChart2 as LineChart, List3 as List, LoadingState, MapView, MarkdownContent, MarketingFooter, MarketingStatCard, MasterDetail, MasterDetailLayout, MathCanvas, MatrixQuestion, MediaGallery, Menu, Meter, Modal, ModalSlot, ModuleCard, Navigation, NodeSlotEditor, NotifyListener, NumberStepper, OnboardingSpotlight, OptionConstraintGroup, StateMachineView as OrbitalStateMachineView, OrbitalVisualization, Overlay, PageHeader, PageTransition, Pagination, PatternTile, PhysicsCanvas, Popover, PositionedCanvas, Presence, PricingCard, PricingGrid, PricingOrganism, PricingPageTemplate, ProgressBar, ProgressDots, PropertyInspector, PullQuote, PullToRefresh, QrScanner, QuizBlock, Radio, RangeSlider, ReflectionBlock, RelationSelect, RepeatableFormSection, ReplyTree, RichBlockEditor, RuntimeDebugger, SHEET_COLUMNS, SPRITE_SHEET_LAYOUT, ScaledDiagram, ScoreDisplay, SearchInput, Section, SectionHeader, SegmentRenderer, Select, SequenceBar, ServiceCatalog, SharedEntityStoreContext, ShowcaseCard, ShowcaseOrganism, SidePanel, Sidebar, SignaturePad, SimpleGrid, Skeleton, SlotContentRenderer, SocialProof, SortableList, Spacer, Sparkline, Spinner, Split, SplitPane, SplitSection, Stack, StarRating, StatBadge, StatCard, StatDisplay, StateGraph, StateJsonView, StateMachineView, StateNode2 as StateNode, StatsGrid, StatsOrganism, StatusBar, StatusDot, StepFlow, StepFlowOrganism, SubagentTracePanel, SvgBranch, SvgConnection, SvgFlow, SvgGrid, SvgLobe, SvgMesh, SvgMorph, SvgNode, SvgPulse, SvgRing, SvgShield, SvgStack, SwipeableRow, Switch, TERRAIN_COLORS, TILE_HEIGHT, TILE_WIDTH, TabbedContainer, TableView, Tabs, TagCloud, TagInput, TeamCard, TeamOrganism, TerrainPalette, TextHighlight, Textarea, ThemeToggle, TimeSlotCell, Timeline, TimerDisplay, Toast, ToastSlot, Tooltip, TraitFrame, TraitSlot, TraitStateViewer, TransitionArrow, TrendIndicator, TypewriterText, Typography, UISlotComponent, UISlotRenderer, UploadDropZone, VStack, VersionDiff, ViolationAlert, VoteStack, WizardContainer, WizardNavigation, WizardProgress, arrowBetween, billboardLabel, boardEntity, bool, calculateAttackTargets, calculateValidMoves, cn, createInitialGameState, createSharedEntityStore, createTranslate, createUnitAnimationState, cylinderBetween, get3DClickPayload, getCurrentFrame, getTileDimensions, inferDirection, isoToScreen, makeAsset, makeAssetMap, mapBookData, meshSphere, num, objAvailableActions, objAvailableEvents, objCurrentState, objIcon, objId, objMaxRules, objName, objRules, objStates, parseEditFocus, parseLessonSegments, parseMarkdownWithCodeBlocks, parseQueryBinding, pendulum, projectileMotion, registerCodeLanguageLoader, resolveFieldMap, resolveFrame, resolveSheetDirection, rows, runTickFrame, screenToIso, springOscillator, str, tickAnimationState, toCodeLanguage, transitionAnimation, unitHealth, unitPosition, unitTeam, useAgentChat, useAnchorRect, useAtlasSliceDataUrl, useAuthContext, useCamera, useCanvasGestures, useCompile, useConnectGitHub, useDeepAgentGeneration, useDisconnectGitHub, useDragReorder, useDraggable, useDropZone, useEmitEvent, useEventBus, useEventListener, useExtensions, useFileEditor, useFileSystem, useGameAudio, useGitHubBranches, useGitHubRepo, useGitHubRepos, useGitHubStatus, useImageCache, useInfiniteScroll, useLongPress, useMediaQuery, useOrbitalHistory, usePresence, usePreview, usePullToRefresh, useQuerySingleton, useRenderInterpolation, useSharedEntitySnapshot, useSharedEntityStore, useSharedEntityStoreContext, useSwipeGesture, useTapReveal, useTraitListens, useTranslate114 as useTranslate, useUIEvents, useUISlotManager, useUnitSpriteAtlas, useValidation, vec2 };
52983
+ export { ALL_PRESETS, ALMADAR_DND_MIME, AR_BOOK_FIELDS, AboutPageTemplate, Accordion, Card2 as ActionCard, ActionPalette, ActionTile, ActivationBlock, Alert, AlgoGraphCanvas, AlgorithmCanvas, AnimatedCounter, AnimatedGraphic, AnimatedReveal, ArticleSection, Aside, AssetPicker, AtlasImage, AtlasPanel, AuthLayout, Avatar, Badge, BehaviorView, BiologyCanvas, BloomQuizBlock, BookChapterView, BookCoverPage, BookNavBar, BookTableOfContents, BookViewer, Box, BranchingLogicBuilder, Breadcrumb, Button, ButtonGroup, CTABanner, CalendarGrid, Canvas, Canvas2D, Card, CardBody, CardContent, CardFooter, CardGrid, CardHeader, CardTitle, Carousel, CaseStudyCard, CaseStudyOrganism, Center, Chart, ChartLegend, ChatBar, Checkbox, ChemistryCanvas, ChoiceButton, Coachmark, CodeBlock, CodeRunnerPanel, CollapsibleSection, CommunityLinks, ConditionalWrapper, ConfettiEffect, ConfirmDialog, ConnectionBlock, Container, ContentRenderer, ContentSection, ControlButton, ControlGrid, CounterTemplate, DEFAULT_LIKERT_OPTIONS, DEFAULT_MATRIX_COLUMNS, DEFAULT_SLOTS, DIAMOND_TOP_Y, DashboardGrid, DashboardLayout, DataGrid, DataList, DataTable, DateRangePicker, DateRangeSelector, DayCell, DetailPanel, Dialog, DialogueBubble, Divider, DocBreadcrumb, DocPagination, DocSearch, DocSidebar, DocTOC, DocumentViewer, StateMachineView as DomStateMachineVisualizer, Drawer, DrawerSlot, ELEMENT_SELECTED_EVENT, EdgeDecoration, EditorCheckbox, EditorSelect, EditorSlider, EditorTextInput, EditorToolbar, EmojiPicker, EmptyState, EntityDisplayEvents, ErrorBoundary, ErrorState, FEATURE_COLORS, FEATURE_TYPES, FLOOR_HEIGHT, FeatureCard, FeatureDetailPageTemplate, FeatureGrid, FeatureGridOrganism, FileTree, FilterGroup, FilterPill, Flex, FlipCard, FlipContainer, FloatingActionButton, Form, FormActions, FormField, FormLayout, FormSection, FormSectionHeader, GameAudioToggle, GameHud, GameIcon, GameMenu, GameShell, GenericAppTemplate, GeometricPattern, GradientDivider, GraphCanvas, GraphView, Grid, GridPicker, HStack, Header, HealthBar, HeroOrganism, HeroSection, I18nProvider, IDENTITY_BOOK_FIELDS, Icon, IconPicker, ImportPreviewTree, ImportProgress, ImportSourcePicker, InfiniteScrollSentinel, Input, InputGroup, InstallBox, JazariStateMachine, JsonTreeEditor, Label, LandingPageTemplate, LawReferenceTooltip, LearningCanvas, LearningScene3D, Lightbox, LikertScale, LineChart2 as LineChart, List3 as List, LoadingState, MapView, MarkdownContent, MarketingFooter, MarketingStatCard, MasterDetail, MasterDetailLayout, MathCanvas, MatrixQuestion, MediaGallery, Menu, Meter, Modal, ModalSlot, ModuleCard, Navigation, NodeSlotEditor, NotifyListener, NumberStepper, OnboardingSpotlight, OptionConstraintGroup, StateMachineView as OrbitalStateMachineView, OrbitalVisualization, Overlay, PageHeader, PageTransition, Pagination, PatternTile, PhysicsCanvas, Popover, PositionedCanvas, Presence, PricingCard, PricingGrid, PricingOrganism, PricingPageTemplate, ProgressBar, ProgressDots, PropertyInspector, PullQuote, PullToRefresh, QrScanner, QuizBlock, Radio, RangeSlider, ReflectionBlock, RelationSelect, RepeatableFormSection, ReplyTree, RichBlockEditor, RuntimeDebugger, SHEET_COLUMNS, SPRITE_SHEET_LAYOUT, ScaledDiagram, ScoreDisplay, SearchInput, Section, SectionHeader, SegmentRenderer, Select, SequenceBar, ServiceCatalog, SharedEntityStoreContext, ShowcaseCard, ShowcaseOrganism, SidePanel, Sidebar, SignaturePad, SimpleGrid, Skeleton, SlotContentRenderer, SocialProof, SortableList, Spacer, Sparkline, Spinner, Split, SplitPane, SplitSection, Stack, StarRating, StatBadge, StatCard, StatDisplay, StateGraph, StateJsonView, StateMachineView, StateNode2 as StateNode, StatsGrid, StatsOrganism, StatusBar, StatusDot, StepFlow, StepFlowOrganism, SubagentTracePanel, SvgBranch, SvgConnection, SvgFlow, SvgGrid, SvgLobe, SvgMesh, SvgMorph, SvgNode, SvgPulse, SvgRing, SvgShield, SvgStack, SwipeableRow, Switch, TERRAIN_COLORS, TILE_HEIGHT, TILE_WIDTH, TabbedContainer, TableView, Tabs, TagCloud, TagInput, TeamCard, TeamOrganism, TerrainPalette, TextHighlight, Textarea, ThemeToggle, TimeSlotCell, Timeline, TimerDisplay, Toast, ToastSlot, Tooltip, TraitFrame, TraitSlot, TraitStateViewer, TransitionArrow, TrendIndicator, TypewriterText, Typography, UISlotComponent, UISlotRenderer, UploadDropZone, VStack, VersionDiff, ViolationAlert, VoteStack, WizardContainer, WizardNavigation, WizardProgress, arrowBetween, billboardLabel, boardEntity, bool, calculateAttackTargets, calculateValidMoves, cn, createInitialGameState, createSharedEntityStore, createTranslate, createUnitAnimationState, cylinderBetween, get3DClickPayload, getCurrentFrame, getTileDimensions, inferDirection, isoToScreen, makeAsset, makeAssetMap, mapBookData, meshSphere, num, objAvailableActions, objAvailableEvents, objCurrentState, objIcon, objId, objMaxRules, objName, objRules, objStates, parseEditFocus, parseLessonSegments, parseMarkdownWithCodeBlocks, parseQueryBinding, pendulum, projectileMotion, registerCodeLanguageLoader, resolveFieldMap, resolveFrame, resolveSheetDirection, rows, runTickFrame, screenToIso, springOscillator, str, tickAnimationState, toCodeLanguage, transitionAnimation, unitHealth, unitPosition, unitTeam, useAgentChat, useAnchorRect, useAtlasSliceDataUrl, useAuthContext, useCamera, useCanvasGestures, useCompile, useConnectGitHub, useDeepAgentGeneration, useDisconnectGitHub, useDragReorder, useDraggable, useDropZone, useEmitEvent, useEventBus, useEventListener, useExtensions, useFileEditor, useFileSystem, useGameAudio, useGitHubBranches, useGitHubRepo, useGitHubRepos, useGitHubStatus, useImageCache, useInfiniteScroll, useLongPress, useMediaQuery, useOrbitalHistory, usePresence, usePreview, usePullToRefresh, useQuerySingleton, useRenderInterpolation, useSharedEntitySnapshot, useSharedEntityStore, useSharedEntityStoreContext, useSwipeGesture, useTapReveal, useTraitListens, useTranslate114 as useTranslate, useUIEvents, useUISlotManager, useUnitSpriteAtlas, useValidation, vec2 };