@almadar/ui 5.166.0 → 6.1.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.
@@ -3,7 +3,7 @@ import React87__default, { createContext, useContext, useMemo, useRef, useEffect
3
3
  import { EventBusContext, useTraitScopeChain, RenderSlotProvider, useEntitySchemaOptional, useEntityBindingSnapshot, useTraitScope, TraitScopeProvider, useNavStack, useCurrentPagePath, useRenderSlot, useGameAudioContextOptional, matchPathAmong } from '@almadar/ui/providers';
4
4
  import { createLogger, isLogLevelEnabled } from '@almadar/logger';
5
5
  import { createContextFromBindings, interpolateValue } from '@almadar/runtime';
6
- import { ANONYMOUS_USER, isRenderBindingMarker, isFileValue, isInlineTrait, ANIMATION_NAMES } from '@almadar/core';
6
+ import { ANONYMOUS_USER, isRenderBindingMarker, isFileValue, isInlineTrait } from '@almadar/core';
7
7
  export { ANONYMOUS_USER } from '@almadar/core';
8
8
  import { clsx } from 'clsx';
9
9
  import { twMerge } from 'tailwind-merge';
@@ -5800,6 +5800,7 @@ function resolveFxView(item, presets) {
5800
5800
  vy: item.vy ?? row.vy,
5801
5801
  vz: item.vz ?? row.vz,
5802
5802
  particleCount: item.particleCount ?? row.particleCount,
5803
+ animation: item.animation ?? row.animation,
5803
5804
  shape: row.shape,
5804
5805
  count: row.count,
5805
5806
  glow: row.glow,
@@ -7332,9 +7333,6 @@ var init_isometric = __esm({
7332
7333
  };
7333
7334
  }
7334
7335
  });
7335
- function isAnimationName(name) {
7336
- return ANIMATION_NAMES.includes(name);
7337
- }
7338
7336
  function frameRect(frame, row, columns, frameWidth, frameHeight) {
7339
7337
  return {
7340
7338
  sx: frame % columns * frameWidth,
@@ -9268,13 +9266,17 @@ var init_DrawSprite = __esm({
9268
9266
  return;
9269
9267
  }
9270
9268
  if (isSpriteSheetAtlas(atlas)) {
9271
- const def = isAnimationName(node.animation) ? atlas.animations[node.animation] : void 0;
9269
+ const def = atlas.animations[node.animation];
9272
9270
  if (!def) {
9273
9271
  paintFallbackSquare(painter, node, dctx, "sprite-missing");
9274
9272
  return;
9275
9273
  }
9276
- const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, dctx.time);
9274
+ const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, node.clockMs ?? dctx.time);
9277
9275
  const r = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
9276
+ if (![r.sx, r.sy, r.sw, r.sh].every(Number.isFinite)) {
9277
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
9278
+ return;
9279
+ }
9278
9280
  src = { x: r.sx, y: r.sy, w: r.sw, h: r.sh };
9279
9281
  }
9280
9282
  }
@@ -9822,7 +9824,10 @@ function expandFxItem(view, node, epochNowMs, dim) {
9822
9824
  anchor: "center",
9823
9825
  width: spriteSize,
9824
9826
  height: spriteSize,
9825
- opacity: fade
9827
+ opacity: fade,
9828
+ animation: view.animation ?? sprite.animations?.[0],
9829
+ // One-shot fx rows (burst) play from the item's birth, not wall-time.
9830
+ clockMs: ageMs
9826
9831
  });
9827
9832
  } else {
9828
9833
  out.push(...proceduralShapes(view, pos, fade, progress));
@@ -11669,6 +11674,7 @@ var init_GameShell = __esm({
11669
11674
  init_Typography();
11670
11675
  init_AtlasImage();
11671
11676
  GAME_FONTS = {
11677
+ fredoka: "Fredoka",
11672
11678
  future: "Kenney Future",
11673
11679
  "future-narrow": "Kenney Future Narrow",
11674
11680
  pixel: "Kenney Pixel",
@@ -11685,10 +11691,10 @@ var init_GameShell = __esm({
11685
11691
  showTopBar = true,
11686
11692
  children,
11687
11693
  backgroundAsset,
11688
- fontFamily = "future",
11694
+ fontFamily,
11689
11695
  "data-theme": dataTheme
11690
11696
  }) => {
11691
- const font = GAME_FONTS[fontFamily] ?? fontFamily;
11697
+ const font = fontFamily ? GAME_FONTS[fontFamily] ?? fontFamily : void 0;
11692
11698
  return /* @__PURE__ */ jsxs(
11693
11699
  Box,
11694
11700
  {
@@ -11703,8 +11709,11 @@ var init_GameShell = __esm({
11703
11709
  color: "var(--color-foreground, #e0e0e0)",
11704
11710
  // The fontFamily knob is a scoped override of the theme contract's
11705
11711
  // display slot: titles/numerics take the game face, body text keeps
11706
- // the active theme's --font-family-body.
11707
- "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif`
11712
+ // the active theme's --font-family-body. Set ONLY when explicitly
11713
+ // passed an always-on inline stamp would shadow the orbital's
11714
+ // inline-theme font-family-display token (inline style beats the
11715
+ // theme provider's vars for the whole shell subtree).
11716
+ ...font ? { "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif` } : {}
11708
11717
  },
11709
11718
  children: [
11710
11719
  backgroundAsset && /* @__PURE__ */ jsx(
@@ -12172,6 +12181,16 @@ function traceShapes(panel, k, width, height) {
12172
12181
  }
12173
12182
  return out;
12174
12183
  }
12184
+ function drawableNeedsAnimation(nodes) {
12185
+ if (!nodes) return false;
12186
+ return nodes.some((node) => {
12187
+ if (node.type === "draw-sprite") return node.animation !== void 0;
12188
+ if (node.type === "draw-fx-layer") return Array.isArray(node.items) && node.items.length > 0;
12189
+ if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some((it) => it.animation !== void 0);
12190
+ if (node.type === "draw-group") return Array.isArray(node.items) && drawableNeedsAnimation(node.items);
12191
+ return false;
12192
+ });
12193
+ }
12175
12194
  var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
12176
12195
  var init_LearningCanvas = __esm({
12177
12196
  "components/learning/atoms/LearningCanvas.tsx"() {
@@ -12179,6 +12198,8 @@ var init_LearningCanvas = __esm({
12179
12198
  init_cn();
12180
12199
  init_perf();
12181
12200
  init_useEventBus();
12201
+ init_webPainter2d();
12202
+ init_paintDispatch();
12182
12203
  DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
12183
12204
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
12184
12205
  LearningCanvas = ({
@@ -12187,6 +12208,8 @@ var init_LearningCanvas = __esm({
12187
12208
  height = 400,
12188
12209
  backgroundColor,
12189
12210
  shapes = [],
12211
+ drawables,
12212
+ projector,
12190
12213
  readouts,
12191
12214
  traces,
12192
12215
  interactive = false,
@@ -12200,6 +12223,9 @@ var init_LearningCanvas = __esm({
12200
12223
  const eventBus = useEventBus();
12201
12224
  const animRef = useRef(0);
12202
12225
  const hoverIndexRef = useRef(-1);
12226
+ const [drawVersion, setDrawVersion] = useState(0);
12227
+ const invalidateRef = useRef(() => setDrawVersion((v) => v + 1));
12228
+ const needsAnim = useMemo(() => drawableNeedsAnimation(drawables), [drawables]);
12203
12229
  const findShapeAt = useCallback((clientX, clientY) => {
12204
12230
  const canvas = canvasRef.current;
12205
12231
  if (!canvas) return -1;
@@ -12243,20 +12269,29 @@ var init_LearningCanvas = __esm({
12243
12269
  for (const shape of derivedShapes) {
12244
12270
  if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
12245
12271
  }
12272
+ if (drawables?.length && projector) {
12273
+ const painter = createWebPainter(ctx, invalidateRef.current);
12274
+ const timeMs = needsAnim && typeof performance !== "undefined" ? performance.now() : 0;
12275
+ const dctx = { projector, time: timeMs, invalidate: invalidateRef.current };
12276
+ for (const node of drawables) {
12277
+ paintDrawable(painter, node, dctx);
12278
+ }
12279
+ }
12246
12280
  perfEnd("learningcanvas:paint", _perfT);
12247
- }, [width, height, backgroundColor, derivedShapes]);
12281
+ }, [width, height, backgroundColor, derivedShapes, drawables, projector, needsAnim]);
12248
12282
  useEffect(() => {
12249
12283
  draw();
12250
- }, [draw]);
12284
+ }, [draw, drawVersion]);
12251
12285
  useEffect(() => {
12252
- if (!animate) return;
12286
+ const shouldAnimate = animate || needsAnim;
12287
+ if (!shouldAnimate) return;
12253
12288
  const loop = () => {
12254
12289
  draw();
12255
12290
  animRef.current = requestAnimationFrame(loop);
12256
12291
  };
12257
12292
  animRef.current = requestAnimationFrame(loop);
12258
12293
  return () => cancelAnimationFrame(animRef.current);
12259
- }, [animate, draw]);
12294
+ }, [animate, needsAnim, draw]);
12260
12295
  const handlePointerMove = useCallback(
12261
12296
  (e) => {
12262
12297
  if (!interactive) return;
@@ -24898,8 +24933,9 @@ var init_DashboardLayout = __esm({
24898
24933
  if (notificationClickEvent) eventBus.emit(`UI:${notificationClickEvent}`, {});
24899
24934
  if (onNotificationClick) onNotificationClick();
24900
24935
  };
24901
- const handleTopBarActionClick = (event) => {
24902
- eventBus.emit(`UI:${event}`, {});
24936
+ const handleTopBarActionClick = (action) => {
24937
+ if (action.event) eventBus.emit(`UI:${action.event}`, {});
24938
+ if (action.navigatesTo) eventBus.emit("UI:NAVIGATE", { url: action.navigatesTo });
24903
24939
  };
24904
24940
  const [sidebarOpen, setSidebarOpen] = useState(false);
24905
24941
  const [userMenuOpen, setUserMenuOpen] = useState(false);
@@ -25114,7 +25150,7 @@ var init_DashboardLayout = __esm({
25114
25150
  {
25115
25151
  variant: "ghost",
25116
25152
  className: "relative p-2 rounded-full hover:bg-muted dark:hover:bg-muted",
25117
- onClick: () => handleTopBarActionClick(action.event),
25153
+ onClick: () => handleTopBarActionClick(action),
25118
25154
  "aria-label": action.label ?? (typeof action.icon === "string" ? action.icon : void 0),
25119
25155
  children: [
25120
25156
  /* @__PURE__ */ jsx(Icon, { icon: action.icon, className: "h-5 w-5 text-muted-foreground dark:text-muted-foreground" }),
@@ -25131,7 +25167,7 @@ var init_DashboardLayout = __esm({
25131
25167
  )
25132
25168
  ]
25133
25169
  },
25134
- `${action.event}-${idx}`
25170
+ `${action.event ?? action.navigatesTo ?? "action"}-${idx}`
25135
25171
  )),
25136
25172
  notificationsEnabled && /* @__PURE__ */ jsxs(
25137
25173
  Button,
@@ -30793,6 +30829,7 @@ var init_MathCanvas = __esm({
30793
30829
  angles = [],
30794
30830
  hops = [],
30795
30831
  shapes = [],
30832
+ drawables,
30796
30833
  readouts,
30797
30834
  traces,
30798
30835
  interactive = false,
@@ -31099,6 +31136,42 @@ var init_MathCanvas = __esm({
31099
31136
  hops,
31100
31137
  shapes
31101
31138
  ]);
31139
+ const projector = useMemo(() => {
31140
+ if (!drawables?.length) return void 0;
31141
+ const margin = 24;
31142
+ const plotW = width - margin * 2;
31143
+ const plotH = height - margin * 2;
31144
+ const xScale = plotW / (xMax - xMin);
31145
+ const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
31146
+ const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
31147
+ const project = (pos) => ({ x: mapX(pos.x), y: mapY(pos.y) });
31148
+ const anchorPoint = (pos, anchor) => {
31149
+ const base = project(pos);
31150
+ if (anchor === "top-left") return base;
31151
+ const cx = base.x + xScale / 2;
31152
+ if (anchor === "ground") return { x: cx, y: base.y + xScale * 0.92 };
31153
+ return { x: cx, y: base.y + xScale / 2 };
31154
+ };
31155
+ const cellPath = (pos) => {
31156
+ const base = project(pos);
31157
+ return [
31158
+ { x: base.x, y: base.y },
31159
+ { x: base.x + xScale, y: base.y },
31160
+ { x: base.x + xScale, y: base.y + xScale },
31161
+ { x: base.x, y: base.y + xScale }
31162
+ ];
31163
+ };
31164
+ return {
31165
+ project,
31166
+ anchorPoint,
31167
+ cellPath,
31168
+ tileWidth: xScale,
31169
+ floorHeight: xScale / 2,
31170
+ diamondTopY: 0,
31171
+ squareGrid: true,
31172
+ worldPixelDirect: false
31173
+ };
31174
+ }, [drawables?.length, width, height, xMin, xMax, yMin, yMax]);
31102
31175
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
31103
31176
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
31104
31177
  /* @__PURE__ */ jsx(
@@ -31108,6 +31181,8 @@ var init_MathCanvas = __esm({
31108
31181
  height,
31109
31182
  backgroundColor,
31110
31183
  shapes: derivedShapes,
31184
+ drawables,
31185
+ projector,
31111
31186
  readouts,
31112
31187
  traces,
31113
31188
  interactive,
@@ -32337,7 +32412,7 @@ var init_MapView = __esm({
32337
32412
  shadowSize: [41, 41]
32338
32413
  });
32339
32414
  L.Marker.prototype.options.icon = defaultIcon;
32340
- const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback97, useState: useState97 } = React87__default;
32415
+ const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback97, useState: useState98 } = React87__default;
32341
32416
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
32342
32417
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
32343
32418
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -32382,7 +32457,7 @@ var init_MapView = __esm({
32382
32457
  showAttribution = true
32383
32458
  }) {
32384
32459
  const eventBus = useEventBus2();
32385
- const [clickedPosition, setClickedPosition] = useState97(null);
32460
+ const [clickedPosition, setClickedPosition] = useState98(null);
32386
32461
  const handleMapClick = useCallback97((lat, lng) => {
32387
32462
  if (showClickedPin) {
32388
32463
  setClickedPosition({ lat, lng });
@@ -6332,6 +6332,7 @@ function resolveFxView(item, presets) {
6332
6332
  vy: item.vy ?? row.vy,
6333
6333
  vz: item.vz ?? row.vz,
6334
6334
  particleCount: item.particleCount ?? row.particleCount,
6335
+ animation: item.animation ?? row.animation,
6335
6336
  shape: row.shape,
6336
6337
  count: row.count,
6337
6338
  glow: row.glow,
@@ -7863,9 +7864,6 @@ var init_isometric = __esm({
7863
7864
  };
7864
7865
  }
7865
7866
  });
7866
- function isAnimationName(name) {
7867
- return core.ANIMATION_NAMES.includes(name);
7868
- }
7869
7867
  function frameRect(frame, row, columns, frameWidth, frameHeight) {
7870
7868
  return {
7871
7869
  sx: frame % columns * frameWidth,
@@ -9692,13 +9690,17 @@ var init_DrawSprite = __esm({
9692
9690
  return;
9693
9691
  }
9694
9692
  if (isSpriteSheetAtlas(atlas)) {
9695
- const def = isAnimationName(node.animation) ? atlas.animations[node.animation] : void 0;
9693
+ const def = atlas.animations[node.animation];
9696
9694
  if (!def) {
9697
9695
  paintFallbackSquare(painter, node, dctx, "sprite-missing");
9698
9696
  return;
9699
9697
  }
9700
- const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, dctx.time);
9698
+ const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, node.clockMs ?? dctx.time);
9701
9699
  const r = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
9700
+ if (![r.sx, r.sy, r.sw, r.sh].every(Number.isFinite)) {
9701
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
9702
+ return;
9703
+ }
9702
9704
  src = { x: r.sx, y: r.sy, w: r.sw, h: r.sh };
9703
9705
  }
9704
9706
  }
@@ -10246,7 +10248,10 @@ function expandFxItem(view, node, epochNowMs, dim) {
10246
10248
  anchor: "center",
10247
10249
  width: spriteSize,
10248
10250
  height: spriteSize,
10249
- opacity: fade
10251
+ opacity: fade,
10252
+ animation: view.animation ?? sprite.animations?.[0],
10253
+ // One-shot fx rows (burst) play from the item's birth, not wall-time.
10254
+ clockMs: ageMs
10250
10255
  });
10251
10256
  } else {
10252
10257
  out.push(...proceduralShapes(view, pos, fade, progress));
@@ -11826,6 +11831,7 @@ var init_GameShell = __esm({
11826
11831
  init_Typography();
11827
11832
  init_AtlasImage();
11828
11833
  GAME_FONTS = {
11834
+ fredoka: "Fredoka",
11829
11835
  future: "Kenney Future",
11830
11836
  "future-narrow": "Kenney Future Narrow",
11831
11837
  pixel: "Kenney Pixel",
@@ -11842,10 +11848,10 @@ var init_GameShell = __esm({
11842
11848
  showTopBar = true,
11843
11849
  children,
11844
11850
  backgroundAsset,
11845
- fontFamily = "future",
11851
+ fontFamily,
11846
11852
  "data-theme": dataTheme
11847
11853
  }) => {
11848
- const font = GAME_FONTS[fontFamily] ?? fontFamily;
11854
+ const font = fontFamily ? GAME_FONTS[fontFamily] ?? fontFamily : void 0;
11849
11855
  return /* @__PURE__ */ jsxRuntime.jsxs(
11850
11856
  Box,
11851
11857
  {
@@ -11860,8 +11866,11 @@ var init_GameShell = __esm({
11860
11866
  color: "var(--color-foreground, #e0e0e0)",
11861
11867
  // The fontFamily knob is a scoped override of the theme contract's
11862
11868
  // display slot: titles/numerics take the game face, body text keeps
11863
- // the active theme's --font-family-body.
11864
- "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif`
11869
+ // the active theme's --font-family-body. Set ONLY when explicitly
11870
+ // passed an always-on inline stamp would shadow the orbital's
11871
+ // inline-theme font-family-display token (inline style beats the
11872
+ // theme provider's vars for the whole shell subtree).
11873
+ ...font ? { "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif` } : {}
11865
11874
  },
11866
11875
  children: [
11867
11876
  backgroundAsset && /* @__PURE__ */ jsxRuntime.jsx(
@@ -12325,6 +12334,16 @@ function traceShapes(panel, k, width, height) {
12325
12334
  }
12326
12335
  return out;
12327
12336
  }
12337
+ function drawableNeedsAnimation(nodes) {
12338
+ if (!nodes) return false;
12339
+ return nodes.some((node) => {
12340
+ if (node.type === "draw-sprite") return node.animation !== void 0;
12341
+ if (node.type === "draw-fx-layer") return Array.isArray(node.items) && node.items.length > 0;
12342
+ if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some((it) => it.animation !== void 0);
12343
+ if (node.type === "draw-group") return Array.isArray(node.items) && drawableNeedsAnimation(node.items);
12344
+ return false;
12345
+ });
12346
+ }
12328
12347
  var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
12329
12348
  var init_LearningCanvas = __esm({
12330
12349
  "components/learning/atoms/LearningCanvas.tsx"() {
@@ -12332,6 +12351,8 @@ var init_LearningCanvas = __esm({
12332
12351
  init_cn();
12333
12352
  init_perf();
12334
12353
  init_useEventBus();
12354
+ init_webPainter2d();
12355
+ init_paintDispatch();
12335
12356
  DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
12336
12357
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
12337
12358
  LearningCanvas = ({
@@ -12340,6 +12361,8 @@ var init_LearningCanvas = __esm({
12340
12361
  height = 400,
12341
12362
  backgroundColor,
12342
12363
  shapes = [],
12364
+ drawables,
12365
+ projector,
12343
12366
  readouts,
12344
12367
  traces,
12345
12368
  interactive = false,
@@ -12353,6 +12376,9 @@ var init_LearningCanvas = __esm({
12353
12376
  const eventBus = useEventBus();
12354
12377
  const animRef = React85.useRef(0);
12355
12378
  const hoverIndexRef = React85.useRef(-1);
12379
+ const [drawVersion, setDrawVersion] = React85.useState(0);
12380
+ const invalidateRef = React85.useRef(() => setDrawVersion((v) => v + 1));
12381
+ const needsAnim = React85.useMemo(() => drawableNeedsAnimation(drawables), [drawables]);
12356
12382
  const findShapeAt = React85.useCallback((clientX, clientY) => {
12357
12383
  const canvas = canvasRef.current;
12358
12384
  if (!canvas) return -1;
@@ -12396,20 +12422,29 @@ var init_LearningCanvas = __esm({
12396
12422
  for (const shape of derivedShapes) {
12397
12423
  if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
12398
12424
  }
12425
+ if (drawables?.length && projector) {
12426
+ const painter = createWebPainter(ctx, invalidateRef.current);
12427
+ const timeMs = needsAnim && typeof performance !== "undefined" ? performance.now() : 0;
12428
+ const dctx = { projector, time: timeMs, invalidate: invalidateRef.current };
12429
+ for (const node of drawables) {
12430
+ paintDrawable(painter, node, dctx);
12431
+ }
12432
+ }
12399
12433
  ui.perfEnd("learningcanvas:paint", _perfT);
12400
- }, [width, height, backgroundColor, derivedShapes]);
12434
+ }, [width, height, backgroundColor, derivedShapes, drawables, projector, needsAnim]);
12401
12435
  React85.useEffect(() => {
12402
12436
  draw();
12403
- }, [draw]);
12437
+ }, [draw, drawVersion]);
12404
12438
  React85.useEffect(() => {
12405
- if (!animate) return;
12439
+ const shouldAnimate = animate || needsAnim;
12440
+ if (!shouldAnimate) return;
12406
12441
  const loop = () => {
12407
12442
  draw();
12408
12443
  animRef.current = requestAnimationFrame(loop);
12409
12444
  };
12410
12445
  animRef.current = requestAnimationFrame(loop);
12411
12446
  return () => cancelAnimationFrame(animRef.current);
12412
- }, [animate, draw]);
12447
+ }, [animate, needsAnim, draw]);
12413
12448
  const handlePointerMove = React85.useCallback(
12414
12449
  (e) => {
12415
12450
  if (!interactive) return;
@@ -24537,8 +24572,9 @@ var init_DashboardLayout = __esm({
24537
24572
  if (notificationClickEvent) eventBus.emit(`UI:${notificationClickEvent}`, {});
24538
24573
  if (onNotificationClick) onNotificationClick();
24539
24574
  };
24540
- const handleTopBarActionClick = (event) => {
24541
- eventBus.emit(`UI:${event}`, {});
24575
+ const handleTopBarActionClick = (action) => {
24576
+ if (action.event) eventBus.emit(`UI:${action.event}`, {});
24577
+ if (action.navigatesTo) eventBus.emit("UI:NAVIGATE", { url: action.navigatesTo });
24542
24578
  };
24543
24579
  const [sidebarOpen, setSidebarOpen] = React85.useState(false);
24544
24580
  const [userMenuOpen, setUserMenuOpen] = React85.useState(false);
@@ -24753,7 +24789,7 @@ var init_DashboardLayout = __esm({
24753
24789
  {
24754
24790
  variant: "ghost",
24755
24791
  className: "relative p-2 rounded-full hover:bg-muted dark:hover:bg-muted",
24756
- onClick: () => handleTopBarActionClick(action.event),
24792
+ onClick: () => handleTopBarActionClick(action),
24757
24793
  "aria-label": action.label ?? (typeof action.icon === "string" ? action.icon : void 0),
24758
24794
  children: [
24759
24795
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon: action.icon, className: "h-5 w-5 text-muted-foreground dark:text-muted-foreground" }),
@@ -24770,7 +24806,7 @@ var init_DashboardLayout = __esm({
24770
24806
  )
24771
24807
  ]
24772
24808
  },
24773
- `${action.event}-${idx}`
24809
+ `${action.event ?? action.navigatesTo ?? "action"}-${idx}`
24774
24810
  )),
24775
24811
  notificationsEnabled && /* @__PURE__ */ jsxRuntime.jsxs(
24776
24812
  Button,
@@ -30368,6 +30404,7 @@ var init_MathCanvas = __esm({
30368
30404
  angles = [],
30369
30405
  hops = [],
30370
30406
  shapes = [],
30407
+ drawables,
30371
30408
  readouts,
30372
30409
  traces,
30373
30410
  interactive = false,
@@ -30674,6 +30711,42 @@ var init_MathCanvas = __esm({
30674
30711
  hops,
30675
30712
  shapes
30676
30713
  ]);
30714
+ const projector = React85.useMemo(() => {
30715
+ if (!drawables?.length) return void 0;
30716
+ const margin = 24;
30717
+ const plotW = width - margin * 2;
30718
+ const plotH = height - margin * 2;
30719
+ const xScale = plotW / (xMax - xMin);
30720
+ const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
30721
+ const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
30722
+ const project = (pos) => ({ x: mapX(pos.x), y: mapY(pos.y) });
30723
+ const anchorPoint = (pos, anchor) => {
30724
+ const base = project(pos);
30725
+ if (anchor === "top-left") return base;
30726
+ const cx = base.x + xScale / 2;
30727
+ if (anchor === "ground") return { x: cx, y: base.y + xScale * 0.92 };
30728
+ return { x: cx, y: base.y + xScale / 2 };
30729
+ };
30730
+ const cellPath = (pos) => {
30731
+ const base = project(pos);
30732
+ return [
30733
+ { x: base.x, y: base.y },
30734
+ { x: base.x + xScale, y: base.y },
30735
+ { x: base.x + xScale, y: base.y + xScale },
30736
+ { x: base.x, y: base.y + xScale }
30737
+ ];
30738
+ };
30739
+ return {
30740
+ project,
30741
+ anchorPoint,
30742
+ cellPath,
30743
+ tileWidth: xScale,
30744
+ floorHeight: xScale / 2,
30745
+ diamondTopY: 0,
30746
+ squareGrid: true,
30747
+ worldPixelDirect: false
30748
+ };
30749
+ }, [drawables?.length, width, height, xMin, xMax, yMin, yMax]);
30677
30750
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
30678
30751
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
30679
30752
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -30683,6 +30756,8 @@ var init_MathCanvas = __esm({
30683
30756
  height,
30684
30757
  backgroundColor,
30685
30758
  shapes: derivedShapes,
30759
+ drawables,
30760
+ projector,
30686
30761
  readouts,
30687
30762
  traces,
30688
30763
  interactive,
@@ -31912,7 +31987,7 @@ var init_MapView = __esm({
31912
31987
  shadowSize: [41, 41]
31913
31988
  });
31914
31989
  L.Marker.prototype.options.icon = defaultIcon;
31915
- const { useEffect: useEffect63, useRef: useRef63, useCallback: useCallback90, useState: useState94 } = React85__namespace.default;
31990
+ const { useEffect: useEffect63, useRef: useRef63, useCallback: useCallback90, useState: useState95 } = React85__namespace.default;
31916
31991
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
31917
31992
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
31918
31993
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -31957,7 +32032,7 @@ var init_MapView = __esm({
31957
32032
  showAttribution = true
31958
32033
  }) {
31959
32034
  const eventBus = useEventBus2();
31960
- const [clickedPosition, setClickedPosition] = useState94(null);
32035
+ const [clickedPosition, setClickedPosition] = useState95(null);
31961
32036
  const handleMapClick = useCallback90((lat, lng) => {
31962
32037
  if (showClickedPin) {
31963
32038
  setClickedPosition({ lat, lng });