@almadar/ui 5.147.0 → 5.149.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/{GameAudioProvider-CPGwD49P.d.cts → GameAudioProvider-B48iXwz3.d.ts} +2 -50
  2. package/dist/{GameAudioProvider-CPGwD49P.d.ts → GameAudioProvider-CQAdPreB.d.cts} +2 -50
  3. package/dist/avl/index.cjs +535 -15
  4. package/dist/avl/index.js +535 -15
  5. package/dist/{avl-schema-parser-DVmrgdwc.d.cts → avl-schema-parser-CVzkNzg7.d.cts} +18 -9
  6. package/dist/{avl-schema-parser-BRJ77Yze.d.ts → avl-schema-parser-Cx_4SVg9.d.ts} +18 -9
  7. package/dist/{cn-BAn68sNO.d.cts → cn-BnAJZcNb.d.cts} +2 -8
  8. package/dist/{cn-CWxxLkri.d.ts → cn-DJTUjk1M.d.ts} +2 -8
  9. package/dist/components/index.cjs +545 -18
  10. package/dist/components/index.d.cts +278 -98
  11. package/dist/components/index.d.ts +278 -98
  12. package/dist/components/index.js +541 -20
  13. package/dist/lib/drawable/three/index.cjs +56 -29
  14. package/dist/lib/drawable/three/index.d.cts +4 -3
  15. package/dist/lib/drawable/three/index.d.ts +4 -3
  16. package/dist/lib/drawable/three/index.js +56 -29
  17. package/dist/lib/index.d.cts +3 -2
  18. package/dist/lib/index.d.ts +3 -2
  19. package/dist/{paintDispatch-Cb_hQj4Y.d.ts → paintDispatch-BjZjUbcb.d.cts} +63 -69
  20. package/dist/{paintDispatch-Cb_hQj4Y.d.cts → paintDispatch-CxdLjHQq.d.ts} +63 -69
  21. package/dist/providers/index.cjs +535 -15
  22. package/dist/providers/index.d.cts +2 -1
  23. package/dist/providers/index.d.ts +2 -1
  24. package/dist/providers/index.js +535 -15
  25. package/dist/runtime/index.cjs +535 -15
  26. package/dist/runtime/index.js +535 -15
  27. package/dist/types-B3nHgnMG.d.cts +51 -0
  28. package/dist/types-B3nHgnMG.d.ts +51 -0
  29. package/package.json +4 -4
@@ -10729,9 +10729,10 @@ function Canvas({
10729
10729
  drawables: [...drawables ?? [], ...childDrawables],
10730
10730
  isLoading,
10731
10731
  cameraMode: to3DCameraMode(camera?.mode),
10732
- ...zoom !== void 0 ? { scale: zoom } : {},
10732
+ ...zoom !== void 0 ? { zoom } : {},
10733
10733
  ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
10734
10734
  ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
10735
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
10735
10736
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
10736
10737
  unitScale,
10737
10738
  backgroundColor,
@@ -11940,7 +11941,10 @@ var init_LearningCanvas = __esm({
11940
11941
  ctx.fillRect(0, 0, width, height);
11941
11942
  }
11942
11943
  for (const shape of shapes) {
11943
- drawShape(ctx, shape, width, height);
11944
+ if (shape.type !== "text") drawShape(ctx, shape, width, height);
11945
+ }
11946
+ for (const shape of shapes) {
11947
+ if (shape.type === "text") drawShape(ctx, shape, width, height);
11944
11948
  }
11945
11949
  }, [width, height, backgroundColor, shapes]);
11946
11950
  React85.useEffect(() => {
@@ -13503,7 +13507,7 @@ var init_AlgorithmCanvas = __esm({
13503
13507
  DEFAULT_CELL_COLOR = "#e5e7eb";
13504
13508
  DEFAULT_POINTER_COLOR = "#dc2626";
13505
13509
  POINTER_BAND = 34;
13506
- TOP_PAD = 12;
13510
+ TOP_PAD = 26;
13507
13511
  AlgorithmCanvas = ({
13508
13512
  className,
13509
13513
  width = 600,
@@ -14607,23 +14611,188 @@ var init_BehaviorView = __esm({
14607
14611
  BehaviorView.displayName = "BehaviorView";
14608
14612
  }
14609
14613
  });
14610
- var BiologyCanvas;
14614
+ function LearningScene3D({
14615
+ className,
14616
+ width = 600,
14617
+ height = 400,
14618
+ title,
14619
+ backgroundColor,
14620
+ drawables,
14621
+ camera,
14622
+ lighting,
14623
+ post,
14624
+ showGrid = false,
14625
+ shadows,
14626
+ interactive,
14627
+ isLoading,
14628
+ error,
14629
+ onItemClick
14630
+ }) {
14631
+ const instanceId = React85.useId().replace(/[^a-zA-Z0-9]/g, "");
14632
+ const clickEvent = onItemClick ? `LEARNING_SCENE_3D.ITEM_CLICK.${instanceId}` : void 0;
14633
+ const onItemClickRef = React85.useRef(onItemClick);
14634
+ onItemClickRef.current = onItemClick;
14635
+ useEventListener(`UI:${clickEvent ?? "LEARNING_SCENE_3D.ITEM_CLICK.__disabled"}`, (event) => {
14636
+ const unitId = event.payload?.unitId;
14637
+ if (typeof unitId === "string") onItemClickRef.current?.(unitId);
14638
+ });
14639
+ const props3d = {
14640
+ drawables,
14641
+ isLoading,
14642
+ error: error ? error.message : null,
14643
+ cameraMode: camera?.mode ?? "perspective",
14644
+ ...camera?.zoom !== void 0 ? { zoom: camera.zoom } : {},
14645
+ ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
14646
+ ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
14647
+ ...camera?.elevation !== void 0 ? { elevation: camera.elevation } : {},
14648
+ ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
14649
+ backgroundColor,
14650
+ showGrid,
14651
+ ...shadows !== void 0 ? { shadows } : {},
14652
+ ...interactive !== void 0 ? { controlsEnabled: interactive } : {},
14653
+ lighting,
14654
+ post,
14655
+ ...clickEvent !== void 0 ? { unitClickEvent: clickEvent } : {}
14656
+ };
14657
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
14658
+ title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
14659
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width, height, display: "flex" }, children: /* @__PURE__ */ jsxRuntime.jsx(React85.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(Canvas3DHost2, { ...props3d }) }) })
14660
+ ] }) });
14661
+ }
14662
+ function meshSphere(id, x, y, z, radius, color, opts) {
14663
+ return {
14664
+ type: "draw-mesh",
14665
+ ...id !== void 0 ? { id } : {},
14666
+ shape: opts?.shape ?? "sphere",
14667
+ position: { x, y, z },
14668
+ radius,
14669
+ pivot: "center",
14670
+ segments: opts?.segments ?? 24,
14671
+ material: { color, ...opts?.material },
14672
+ ...opts?.opacity !== void 0 ? { opacity: opts.opacity } : {}
14673
+ };
14674
+ }
14675
+ function axisRotation(from, to) {
14676
+ const wx = to[0] - from[0];
14677
+ const wy = to[2] - from[2];
14678
+ const wz = to[1] - from[1];
14679
+ const len = Math.sqrt(wx * wx + wy * wy + wz * wz);
14680
+ const tilt = Math.acos(Math.min(1, Math.max(-1, len > 0 ? wy / len : 1)));
14681
+ return [0, Math.atan2(wz, -wx), tilt];
14682
+ }
14683
+ function segmentLength(from, to) {
14684
+ const dx = to[0] - from[0];
14685
+ const dy = to[1] - from[1];
14686
+ const dz = to[2] - from[2];
14687
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
14688
+ }
14689
+ function cylinderBetween(from, to, radius, color) {
14690
+ const len = segmentLength(from, to);
14691
+ if (len < 1e-6) return null;
14692
+ return {
14693
+ type: "draw-mesh",
14694
+ shape: "cylinder",
14695
+ position: { x: (from[0] + to[0]) / 2, y: (from[1] + to[1]) / 2, z: (from[2] + to[2]) / 2 },
14696
+ radius,
14697
+ height: len,
14698
+ rotation: axisRotation(from, to),
14699
+ pivot: "center",
14700
+ segments: 12,
14701
+ material: { color }
14702
+ };
14703
+ }
14704
+ function arrowBetween(from, to, color, shaftRadius = 0.08) {
14705
+ const len = segmentLength(from, to);
14706
+ if (len < 1e-6) return null;
14707
+ const tipLen = Math.min(shaftRadius * 8, len * 0.35);
14708
+ const k = (len - tipLen) / len;
14709
+ const delta = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
14710
+ const shaftEnd = [delta[0] * k, delta[1] * k, delta[2] * k];
14711
+ const tipStart = shaftEnd;
14712
+ const tipEnd = delta;
14713
+ const shaft = cylinderBetween([0, 0, 0], shaftEnd, shaftRadius, color);
14714
+ const tipLenActual = segmentLength(tipStart, tipEnd);
14715
+ const tip = {
14716
+ type: "draw-mesh",
14717
+ shape: "cone",
14718
+ position: {
14719
+ x: (tipStart[0] + tipEnd[0]) / 2,
14720
+ y: (tipStart[1] + tipEnd[1]) / 2,
14721
+ z: (tipStart[2] + tipEnd[2]) / 2
14722
+ },
14723
+ radius: shaftRadius * 3,
14724
+ height: tipLenActual,
14725
+ rotation: axisRotation(tipStart, tipEnd),
14726
+ pivot: "center",
14727
+ segments: 12,
14728
+ material: { color }
14729
+ };
14730
+ return {
14731
+ type: "draw-group",
14732
+ position: { x: from[0], y: from[1], z: from[2] },
14733
+ items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
14734
+ };
14735
+ }
14736
+ function billboardLabel(text, x, y, z, opts) {
14737
+ return {
14738
+ type: "draw-text",
14739
+ text,
14740
+ position: { x, y, z },
14741
+ color: opts?.color ?? "#111827"
14742
+ };
14743
+ }
14744
+ function labelColorForBackground(backgroundColor) {
14745
+ const m = /^#(?:([0-9a-f]{3})|([0-9a-f]{6}))$/i.exec(backgroundColor ?? "");
14746
+ if (!m) return "#111827";
14747
+ const hex = m[1] !== void 0 ? m[1].split("").map((c) => c + c).join("") : m[2];
14748
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
14749
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
14750
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
14751
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
14752
+ return luminance < 0.5 ? "#e5e7eb" : "#111827";
14753
+ }
14754
+ function get3DClickPayload(onShapeClick, idToIndex) {
14755
+ if (!onShapeClick) return void 0;
14756
+ return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
14757
+ }
14758
+ var Canvas3DHost2;
14759
+ var init_learningScene3D = __esm({
14760
+ "components/learning/molecules/learningScene3D.tsx"() {
14761
+ "use client";
14762
+ init_atoms();
14763
+ init_Stack();
14764
+ init_useEventBus();
14765
+ Canvas3DHost2 = React85.lazy(
14766
+ () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
14767
+ );
14768
+ LearningScene3D.displayName = "LearningScene3D";
14769
+ }
14770
+ });
14771
+ var biologyLog, BiologyCanvas;
14611
14772
  var init_BiologyCanvas = __esm({
14612
14773
  "components/learning/molecules/BiologyCanvas.tsx"() {
14613
14774
  "use client";
14614
14775
  init_atoms();
14615
14776
  init_Stack();
14616
14777
  init_LearningCanvas();
14778
+ init_learningScene3D();
14779
+ biologyLog = logger.createLogger("almadar:ui:biology-canvas");
14617
14780
  BiologyCanvas = ({
14618
14781
  className,
14619
14782
  width = 600,
14620
14783
  height = 400,
14621
14784
  title,
14622
14785
  backgroundColor,
14786
+ mode = "2d",
14787
+ camera,
14788
+ lighting,
14789
+ post,
14623
14790
  nodes = [],
14624
14791
  edges = [],
14625
14792
  shapes = [],
14626
- interactive = false,
14793
+ showGrid,
14794
+ shadows,
14795
+ interactive,
14627
14796
  animate = false,
14628
14797
  onShapeClick,
14629
14798
  isLoading,
@@ -14684,6 +14853,75 @@ var init_BiologyCanvas = __esm({
14684
14853
  out.push(...shapes);
14685
14854
  return out;
14686
14855
  }, [nodes, edges, shapes]);
14856
+ const drawables3D = React85.useMemo(() => {
14857
+ if (mode !== "3d") return [];
14858
+ if (shapes.length > 0) {
14859
+ biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
14860
+ }
14861
+ const out = [];
14862
+ const labelColor = labelColorForBackground(backgroundColor);
14863
+ const nodeById = /* @__PURE__ */ new Map();
14864
+ for (const n of nodes) {
14865
+ if (n.id) nodeById.set(n.id, n);
14866
+ }
14867
+ for (const e of edges) {
14868
+ const a = nodeById.get(e.from);
14869
+ const b = nodeById.get(e.to);
14870
+ if (!a || !b) continue;
14871
+ const edgeRadius = Math.max(0.04, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.12);
14872
+ const edge = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], edgeRadius, e.color ?? "#9ca3af");
14873
+ if (edge) out.push(edge);
14874
+ if (e.label) {
14875
+ out.push(
14876
+ billboardLabel(
14877
+ e.label,
14878
+ (a.x + b.x) / 2,
14879
+ (a.y + b.y) / 2,
14880
+ ((a.z ?? 0) + (b.z ?? 0)) / 2,
14881
+ { color: labelColor }
14882
+ )
14883
+ );
14884
+ }
14885
+ }
14886
+ for (const n of nodes) {
14887
+ const radius = n.radius ?? 0.5;
14888
+ const nz = n.z ?? 0;
14889
+ out.push(meshSphere(n.id, n.x, n.y, nz, radius, n.color ?? "#16a34a", { shape: n.shape, ...n.opacity !== void 0 ? { opacity: n.opacity } : {} }));
14890
+ if (n.label) {
14891
+ out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
14892
+ }
14893
+ }
14894
+ return out;
14895
+ }, [mode, nodes, edges, shapes, backgroundColor]);
14896
+ const nodeIndexById = React85.useMemo(() => {
14897
+ const m = /* @__PURE__ */ new Map();
14898
+ nodes.forEach((n, i) => {
14899
+ if (n.id) m.set(n.id, i);
14900
+ });
14901
+ return m;
14902
+ }, [nodes]);
14903
+ if (mode === "3d") {
14904
+ return /* @__PURE__ */ jsxRuntime.jsx(
14905
+ LearningScene3D,
14906
+ {
14907
+ className,
14908
+ width,
14909
+ height,
14910
+ title,
14911
+ backgroundColor,
14912
+ drawables: drawables3D,
14913
+ camera,
14914
+ lighting,
14915
+ post,
14916
+ showGrid,
14917
+ shadows,
14918
+ interactive,
14919
+ isLoading,
14920
+ error,
14921
+ onItemClick: get3DClickPayload(onShapeClick, nodeIndexById)
14922
+ }
14923
+ );
14924
+ }
14687
14925
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
14688
14926
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
14689
14927
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -14693,7 +14931,7 @@ var init_BiologyCanvas = __esm({
14693
14931
  height,
14694
14932
  backgroundColor,
14695
14933
  shapes: derivedShapes,
14696
- interactive,
14934
+ interactive: interactive ?? false,
14697
14935
  animate,
14698
14936
  onShapeClick,
14699
14937
  isLoading,
@@ -21416,24 +21654,41 @@ var init_ChatBar = __esm({
21416
21654
  ChatBar.displayName = "ChatBar";
21417
21655
  }
21418
21656
  });
21419
- var ChemistryCanvas;
21657
+ function bondPerpendicular(a, b) {
21658
+ const dx = b[0] - a[0];
21659
+ const dy = b[1] - a[1];
21660
+ const px = dy;
21661
+ const py = -dx;
21662
+ const len = Math.sqrt(px * px + py * py);
21663
+ if (len < 1e-6) return [1, 0, 0];
21664
+ return [px / len, py / len, 0];
21665
+ }
21666
+ var chemistryLog, ChemistryCanvas;
21420
21667
  var init_ChemistryCanvas = __esm({
21421
21668
  "components/learning/molecules/ChemistryCanvas.tsx"() {
21422
21669
  "use client";
21423
21670
  init_atoms();
21424
21671
  init_Stack();
21425
21672
  init_LearningCanvas();
21673
+ init_learningScene3D();
21674
+ chemistryLog = logger.createLogger("almadar:ui:chemistry-canvas");
21426
21675
  ChemistryCanvas = ({
21427
21676
  className,
21428
21677
  width = 600,
21429
21678
  height = 400,
21430
21679
  title,
21431
21680
  backgroundColor,
21681
+ mode = "2d",
21682
+ camera,
21683
+ lighting,
21684
+ post,
21432
21685
  atoms = [],
21433
21686
  bonds = [],
21434
21687
  arrows = [],
21435
21688
  shapes = [],
21436
- interactive = false,
21689
+ showGrid,
21690
+ shadows,
21691
+ interactive,
21437
21692
  animate = false,
21438
21693
  onShapeClick,
21439
21694
  isLoading,
@@ -21512,6 +21767,88 @@ var init_ChemistryCanvas = __esm({
21512
21767
  out.push(...shapes);
21513
21768
  return out;
21514
21769
  }, [atoms, bonds, arrows, shapes]);
21770
+ const drawables3D = React85.useMemo(() => {
21771
+ if (mode !== "3d") return [];
21772
+ if (shapes.length > 0) {
21773
+ chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
21774
+ }
21775
+ const out = [];
21776
+ const labelColor = labelColorForBackground(backgroundColor);
21777
+ const atomById = /* @__PURE__ */ new Map();
21778
+ for (const a of atoms) {
21779
+ if (a.id) atomById.set(a.id, a);
21780
+ }
21781
+ for (const b of bonds) {
21782
+ const a = atomById.get(b.from);
21783
+ const c = atomById.get(b.to);
21784
+ if (!a || !c) continue;
21785
+ const color = b.color ?? "#6b7280";
21786
+ const from = [a.x, a.y, a.z ?? 0];
21787
+ const to = [c.x, c.y, c.z ?? 0];
21788
+ const perp = bondPerpendicular(from, to);
21789
+ const bondRadius = Math.max(0.06, Math.min(a.radius ?? 0.45, c.radius ?? 0.45) * 0.22);
21790
+ const step = bondRadius * 2.2;
21791
+ const offsets = b.type === "double" ? [-step, step] : b.type === "triple" ? [-step, 0, step] : [0];
21792
+ for (const off of offsets) {
21793
+ const bond = cylinderBetween(
21794
+ [from[0] + perp[0] * off, from[1] + perp[1] * off, from[2] + perp[2] * off],
21795
+ [to[0] + perp[0] * off, to[1] + perp[1] * off, to[2] + perp[2] * off],
21796
+ bondRadius,
21797
+ color
21798
+ );
21799
+ if (bond) out.push(bond);
21800
+ }
21801
+ }
21802
+ for (const a of arrows) {
21803
+ const angle = (a.angle ?? 0) * (Math.PI / 180);
21804
+ const len = a.length ?? 60;
21805
+ const x2 = a.x + Math.cos(angle) * len;
21806
+ const y2 = a.y + Math.sin(angle) * len;
21807
+ const arrow = arrowBetween([a.x, a.y, 0], [x2, y2, 0], a.color ?? "#dc2626");
21808
+ if (arrow) out.push(arrow);
21809
+ if (a.label) {
21810
+ out.push(billboardLabel(a.label, (a.x + x2) / 2, (a.y + y2) / 2, 0, { color: labelColor }));
21811
+ }
21812
+ }
21813
+ for (const a of atoms) {
21814
+ const radius = a.radius ?? 0.45;
21815
+ const az = a.z ?? 0;
21816
+ out.push(meshSphere(a.id, a.x, a.y, az, radius, a.color ?? "#2563eb"));
21817
+ if (a.element) {
21818
+ out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
21819
+ }
21820
+ }
21821
+ return out;
21822
+ }, [mode, atoms, bonds, arrows, shapes, backgroundColor]);
21823
+ const atomIndexById = React85.useMemo(() => {
21824
+ const m = /* @__PURE__ */ new Map();
21825
+ atoms.forEach((a, i) => {
21826
+ if (a.id) m.set(a.id, i);
21827
+ });
21828
+ return m;
21829
+ }, [atoms]);
21830
+ if (mode === "3d") {
21831
+ return /* @__PURE__ */ jsxRuntime.jsx(
21832
+ LearningScene3D,
21833
+ {
21834
+ className,
21835
+ width,
21836
+ height,
21837
+ title,
21838
+ backgroundColor,
21839
+ drawables: drawables3D,
21840
+ camera,
21841
+ lighting,
21842
+ post,
21843
+ showGrid,
21844
+ shadows,
21845
+ interactive,
21846
+ isLoading,
21847
+ error,
21848
+ onItemClick: get3DClickPayload(onShapeClick, atomIndexById)
21849
+ }
21850
+ );
21851
+ }
21515
21852
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
21516
21853
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
21517
21854
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -21521,7 +21858,7 @@ var init_ChemistryCanvas = __esm({
21521
21858
  height,
21522
21859
  backgroundColor,
21523
21860
  shapes: derivedShapes,
21524
- interactive,
21861
+ interactive: interactive ?? false,
21525
21862
  animate,
21526
21863
  onShapeClick,
21527
21864
  isLoading,
@@ -24914,6 +25251,7 @@ var init_FilterGroup = __esm({
24914
25251
  init_Badge();
24915
25252
  init_Stack();
24916
25253
  init_Icon();
25254
+ init_RangeSlider();
24917
25255
  init_useEventBus();
24918
25256
  init_useQuerySingleton();
24919
25257
  resolveFilterType = (filter) => filter.filterType ?? filter.type;
@@ -25108,6 +25446,35 @@ var init_FilterGroup = __esm({
25108
25446
  onClear: () => handleFilterSelect(`${filter.field}_to`, null)
25109
25447
  }
25110
25448
  )
25449
+ ] }) : resolveFilterType(filter) === "numberrange" || resolveFilterType(filter) === "number-range" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [
25450
+ /* @__PURE__ */ jsxRuntime.jsx(
25451
+ RangeSlider,
25452
+ {
25453
+ min: filter.min ?? 0,
25454
+ max: filter.max ?? 100,
25455
+ step: filter.step ?? 1,
25456
+ value: Number(
25457
+ selectedValues[`${filter.field}_min`] ?? filter.min ?? 0
25458
+ ),
25459
+ onChange: (v) => handleFilterSelect(`${filter.field}_min`, String(v)),
25460
+ showTooltip: true,
25461
+ "aria-label": t("filterGroup.from")
25462
+ }
25463
+ ),
25464
+ /* @__PURE__ */ jsxRuntime.jsx(
25465
+ RangeSlider,
25466
+ {
25467
+ min: filter.min ?? 0,
25468
+ max: filter.max ?? 100,
25469
+ step: filter.step ?? 1,
25470
+ value: Number(
25471
+ selectedValues[`${filter.field}_max`] ?? filter.max ?? 100
25472
+ ),
25473
+ onChange: (v) => handleFilterSelect(`${filter.field}_max`, String(v)),
25474
+ showTooltip: true,
25475
+ "aria-label": t("filterGroup.to")
25476
+ }
25477
+ )
25111
25478
  ] }) : resolveFilterType(filter) === "text" ? /* @__PURE__ */ jsxRuntime.jsx(
25112
25479
  Input,
25113
25480
  {
@@ -25190,6 +25557,36 @@ var init_FilterGroup = __esm({
25190
25557
  className: "text-sm min-w-[100px]"
25191
25558
  }
25192
25559
  )
25560
+ ] }) : resolveFilterType(filter) === "numberrange" || resolveFilterType(filter) === "number-range" ? /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", align: "center", children: [
25561
+ /* @__PURE__ */ jsxRuntime.jsx(
25562
+ RangeSlider,
25563
+ {
25564
+ min: filter.min ?? 0,
25565
+ max: filter.max ?? 100,
25566
+ step: filter.step ?? 1,
25567
+ value: Number(
25568
+ selectedValues[`${filter.field}_min`] ?? filter.min ?? 0
25569
+ ),
25570
+ onChange: (v) => handleFilterSelect(`${filter.field}_min`, String(v)),
25571
+ className: "min-w-[100px]",
25572
+ "aria-label": t("filterGroup.from")
25573
+ }
25574
+ ),
25575
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "-" }),
25576
+ /* @__PURE__ */ jsxRuntime.jsx(
25577
+ RangeSlider,
25578
+ {
25579
+ min: filter.min ?? 0,
25580
+ max: filter.max ?? 100,
25581
+ step: filter.step ?? 1,
25582
+ value: Number(
25583
+ selectedValues[`${filter.field}_max`] ?? filter.max ?? 100
25584
+ ),
25585
+ onChange: (v) => handleFilterSelect(`${filter.field}_max`, String(v)),
25586
+ className: "min-w-[100px]",
25587
+ "aria-label": t("filterGroup.to")
25588
+ }
25589
+ )
25193
25590
  ] }) : resolveFilterType(filter) === "text" ? /* @__PURE__ */ jsxRuntime.jsx(
25194
25591
  Input,
25195
25592
  {
@@ -25308,6 +25705,38 @@ var init_FilterGroup = __esm({
25308
25705
  className: "min-w-[130px]"
25309
25706
  }
25310
25707
  )
25708
+ ] }) : resolveFilterType(filter) === "numberrange" || resolveFilterType(filter) === "number-range" ? /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "xs", align: "center", children: [
25709
+ /* @__PURE__ */ jsxRuntime.jsx(
25710
+ RangeSlider,
25711
+ {
25712
+ min: filter.min ?? 0,
25713
+ max: filter.max ?? 100,
25714
+ step: filter.step ?? 1,
25715
+ value: Number(
25716
+ selectedValues[`${filter.field}_min`] ?? filter.min ?? 0
25717
+ ),
25718
+ onChange: (v) => handleFilterSelect(`${filter.field}_min`, String(v)),
25719
+ showTooltip: true,
25720
+ className: "min-w-[130px]",
25721
+ "aria-label": t("filterGroup.from")
25722
+ }
25723
+ ),
25724
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "-" }),
25725
+ /* @__PURE__ */ jsxRuntime.jsx(
25726
+ RangeSlider,
25727
+ {
25728
+ min: filter.min ?? 0,
25729
+ max: filter.max ?? 100,
25730
+ step: filter.step ?? 1,
25731
+ value: Number(
25732
+ selectedValues[`${filter.field}_max`] ?? filter.max ?? 100
25733
+ ),
25734
+ onChange: (v) => handleFilterSelect(`${filter.field}_max`, String(v)),
25735
+ showTooltip: true,
25736
+ className: "min-w-[130px]",
25737
+ "aria-label": t("filterGroup.to")
25738
+ }
25739
+ )
25311
25740
  ] }) : resolveFilterType(filter) === "text" ? /* @__PURE__ */ jsxRuntime.jsx(
25312
25741
  Input,
25313
25742
  {
@@ -27656,19 +28085,25 @@ var init_MathCanvas = __esm({
27656
28085
  };
27657
28086
  }
27658
28087
  });
27659
- var PhysicsCanvas;
28088
+ var physicsLog2, PhysicsCanvas;
27660
28089
  var init_PhysicsCanvas = __esm({
27661
28090
  "components/learning/molecules/PhysicsCanvas.tsx"() {
27662
28091
  "use client";
27663
28092
  init_atoms();
27664
28093
  init_Stack();
27665
28094
  init_LearningCanvas();
28095
+ init_learningScene3D();
28096
+ physicsLog2 = logger.createLogger("almadar:ui:physics-canvas");
27666
28097
  PhysicsCanvas = ({
27667
28098
  className,
27668
28099
  width = 600,
27669
28100
  height = 400,
27670
28101
  title,
27671
28102
  backgroundColor,
28103
+ mode = "2d",
28104
+ camera,
28105
+ lighting,
28106
+ post,
27672
28107
  bodies = [],
27673
28108
  constraints = [],
27674
28109
  showVelocity = true,
@@ -27676,7 +28111,9 @@ var init_PhysicsCanvas = __esm({
27676
28111
  velocityScale = 20,
27677
28112
  forceScale = 20,
27678
28113
  shapes = [],
27679
- interactive = false,
28114
+ showGrid,
28115
+ shadows,
28116
+ interactive,
27680
28117
  animate = false,
27681
28118
  onShapeClick,
27682
28119
  isLoading,
@@ -27748,6 +28185,89 @@ var init_PhysicsCanvas = __esm({
27748
28185
  out.push(...shapes);
27749
28186
  return out;
27750
28187
  }, [bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes]);
28188
+ const drawables3D = React85.useMemo(() => {
28189
+ if (mode !== "3d") return [];
28190
+ if (shapes.length > 0) {
28191
+ physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
28192
+ }
28193
+ const out = [];
28194
+ const labelColor = labelColorForBackground(backgroundColor);
28195
+ const bodyById = /* @__PURE__ */ new Map();
28196
+ for (const b of bodies) {
28197
+ if (b.id) bodyById.set(b.id, b);
28198
+ }
28199
+ for (const c of constraints) {
28200
+ const a = bodyById.get(c.from);
28201
+ const b = bodyById.get(c.to);
28202
+ if (!a || !b) continue;
28203
+ const rodRadius = Math.max(0.05, Math.min(a.radius ?? 0.5, b.radius ?? 0.5) * 0.15);
28204
+ const rod = cylinderBetween([a.x, a.y, a.z ?? 0], [b.x, b.y, b.z ?? 0], rodRadius, c.color ?? "#9ca3af");
28205
+ if (rod) out.push(rod);
28206
+ }
28207
+ for (const b of bodies) {
28208
+ const radius = b.radius ?? 0.5;
28209
+ const bz = b.z ?? 0;
28210
+ out.push(meshSphere(b.id, b.x, b.y, bz, radius, b.color ?? "#2563eb"));
28211
+ if (b.label) {
28212
+ out.push(billboardLabel(b.label, b.x, b.y, bz + radius, { color: labelColor }));
28213
+ }
28214
+ const vx = b.vx ?? 0;
28215
+ const vy = b.vy ?? 0;
28216
+ const vz = b.vz ?? 0;
28217
+ const arrowRadius = Math.max(0.05, radius * 0.15);
28218
+ if (showVelocity && (vx !== 0 || vy !== 0 || vz !== 0)) {
28219
+ const arrow = arrowBetween(
28220
+ [b.x, b.y, bz],
28221
+ [b.x + vx * velocityScale, b.y + vy * velocityScale, bz + vz * velocityScale],
28222
+ "#16a34a",
28223
+ arrowRadius
28224
+ );
28225
+ if (arrow) out.push(arrow);
28226
+ }
28227
+ const fx = b.fx ?? 0;
28228
+ const fy = b.fy ?? 0;
28229
+ const fz = b.fz ?? 0;
28230
+ if (showForces && (fx !== 0 || fy !== 0 || fz !== 0)) {
28231
+ const arrow = arrowBetween(
28232
+ [b.x, b.y, bz],
28233
+ [b.x + fx * forceScale, b.y + fy * forceScale, bz + fz * forceScale],
28234
+ "#dc2626",
28235
+ arrowRadius
28236
+ );
28237
+ if (arrow) out.push(arrow);
28238
+ }
28239
+ }
28240
+ return out;
28241
+ }, [mode, bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes, backgroundColor]);
28242
+ const bodyIndexById = React85.useMemo(() => {
28243
+ const m = /* @__PURE__ */ new Map();
28244
+ bodies.forEach((b, i) => {
28245
+ if (b.id) m.set(b.id, i);
28246
+ });
28247
+ return m;
28248
+ }, [bodies]);
28249
+ if (mode === "3d") {
28250
+ return /* @__PURE__ */ jsxRuntime.jsx(
28251
+ LearningScene3D,
28252
+ {
28253
+ className,
28254
+ width,
28255
+ height,
28256
+ title,
28257
+ backgroundColor,
28258
+ drawables: drawables3D,
28259
+ camera,
28260
+ lighting,
28261
+ post,
28262
+ showGrid,
28263
+ shadows,
28264
+ interactive,
28265
+ isLoading,
28266
+ error,
28267
+ onItemClick: get3DClickPayload(onShapeClick, bodyIndexById)
28268
+ }
28269
+ );
28270
+ }
27751
28271
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
27752
28272
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
27753
28273
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -27757,7 +28277,7 @@ var init_PhysicsCanvas = __esm({
27757
28277
  height,
27758
28278
  backgroundColor,
27759
28279
  shapes: derivedShapes,
27760
- interactive,
28280
+ interactive: interactive ?? false,
27761
28281
  animate,
27762
28282
  onShapeClick,
27763
28283
  isLoading,
@@ -28375,12 +28895,12 @@ var init_MapView = __esm({
28375
28895
  shadowSize: [41, 41]
28376
28896
  });
28377
28897
  L.Marker.prototype.options.icon = defaultIcon;
28378
- const { useEffect: useEffect60, useRef: useRef62, useCallback: useCallback90, useState: useState92 } = React85__namespace.default;
28898
+ const { useEffect: useEffect60, useRef: useRef63, useCallback: useCallback90, useState: useState92 } = React85__namespace.default;
28379
28899
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
28380
28900
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
28381
28901
  function MapUpdater({ centerLat, centerLng, zoom }) {
28382
28902
  const map = useMap();
28383
- const prevRef = useRef62({ centerLat, centerLng, zoom });
28903
+ const prevRef = useRef63({ centerLat, centerLng, zoom });
28384
28904
  useEffect60(() => {
28385
28905
  const prev = prevRef.current;
28386
28906
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
@@ -39045,7 +39565,7 @@ function getEnumOptions(field) {
39045
39565
  }));
39046
39566
  }
39047
39567
  const validation = field.validation;
39048
- if (validation?.enum && Array.isArray(validation.enum)) {
39568
+ if (validation?.enum && validation.enum.length > 0) {
39049
39569
  return validation.enum.map((v) => ({
39050
39570
  value: v,
39051
39571
  label: v.charAt(0).toUpperCase() + v.slice(1).replace(/_/g, " ")