@almadar/ui 6.0.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.
@@ -9617,6 +9617,7 @@ function resolveFxView(item, presets) {
9617
9617
  vy: item.vy ?? row.vy,
9618
9618
  vz: item.vz ?? row.vz,
9619
9619
  particleCount: item.particleCount ?? row.particleCount,
9620
+ animation: item.animation ?? row.animation,
9620
9621
  shape: row.shape,
9621
9622
  count: row.count,
9622
9623
  glow: row.glow,
@@ -11148,9 +11149,6 @@ var init_isometric = __esm({
11148
11149
  };
11149
11150
  }
11150
11151
  });
11151
- function isAnimationName(name) {
11152
- return core.ANIMATION_NAMES.includes(name);
11153
- }
11154
11152
  function frameRect(frame, row, columns, frameWidth, frameHeight) {
11155
11153
  return {
11156
11154
  sx: frame % columns * frameWidth,
@@ -13084,13 +13082,17 @@ var init_DrawSprite = __esm({
13084
13082
  return;
13085
13083
  }
13086
13084
  if (isSpriteSheetAtlas(atlas)) {
13087
- const def = isAnimationName(node.animation) ? atlas.animations[node.animation] : void 0;
13085
+ const def = atlas.animations[node.animation];
13088
13086
  if (!def) {
13089
13087
  paintFallbackSquare(painter, node, dctx, "sprite-missing");
13090
13088
  return;
13091
13089
  }
13092
- const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, dctx.time);
13090
+ const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, node.clockMs ?? dctx.time);
13093
13091
  const r2 = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
13092
+ if (![r2.sx, r2.sy, r2.sw, r2.sh].every(Number.isFinite)) {
13093
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
13094
+ return;
13095
+ }
13094
13096
  src = { x: r2.sx, y: r2.sy, w: r2.sw, h: r2.sh };
13095
13097
  }
13096
13098
  }
@@ -13638,7 +13640,10 @@ function expandFxItem(view, node, epochNowMs, dim) {
13638
13640
  anchor: "center",
13639
13641
  width: spriteSize,
13640
13642
  height: spriteSize,
13641
- opacity: fade
13643
+ opacity: fade,
13644
+ animation: view.animation ?? sprite.animations?.[0],
13645
+ // One-shot fx rows (burst) play from the item's birth, not wall-time.
13646
+ clockMs: ageMs
13642
13647
  });
13643
13648
  } else {
13644
13649
  out.push(...proceduralShapes(view, pos, fade, progress));
@@ -15218,6 +15223,7 @@ var init_GameShell = __esm({
15218
15223
  init_Typography();
15219
15224
  init_AtlasImage();
15220
15225
  GAME_FONTS = {
15226
+ fredoka: "Fredoka",
15221
15227
  future: "Kenney Future",
15222
15228
  "future-narrow": "Kenney Future Narrow",
15223
15229
  pixel: "Kenney Pixel",
@@ -15234,10 +15240,10 @@ var init_GameShell = __esm({
15234
15240
  showTopBar = true,
15235
15241
  children,
15236
15242
  backgroundAsset,
15237
- fontFamily = "future",
15243
+ fontFamily,
15238
15244
  "data-theme": dataTheme
15239
15245
  }) => {
15240
- const font = GAME_FONTS[fontFamily] ?? fontFamily;
15246
+ const font = fontFamily ? GAME_FONTS[fontFamily] ?? fontFamily : void 0;
15241
15247
  return /* @__PURE__ */ jsxRuntime.jsxs(
15242
15248
  Box,
15243
15249
  {
@@ -15252,8 +15258,11 @@ var init_GameShell = __esm({
15252
15258
  color: "var(--color-foreground, #e0e0e0)",
15253
15259
  // The fontFamily knob is a scoped override of the theme contract's
15254
15260
  // display slot: titles/numerics take the game face, body text keeps
15255
- // the active theme's --font-family-body.
15256
- "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif`
15261
+ // the active theme's --font-family-body. Set ONLY when explicitly
15262
+ // passed an always-on inline stamp would shadow the orbital's
15263
+ // inline-theme font-family-display token (inline style beats the
15264
+ // theme provider's vars for the whole shell subtree).
15265
+ ...font ? { "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif` } : {}
15257
15266
  },
15258
15267
  children: [
15259
15268
  backgroundAsset && /* @__PURE__ */ jsxRuntime.jsx(
@@ -15730,6 +15739,16 @@ function traceShapes(panel, k, width, height) {
15730
15739
  }
15731
15740
  return out;
15732
15741
  }
15742
+ function drawableNeedsAnimation(nodes) {
15743
+ if (!nodes) return false;
15744
+ return nodes.some((node) => {
15745
+ if (node.type === "draw-sprite") return node.animation !== void 0;
15746
+ if (node.type === "draw-fx-layer") return Array.isArray(node.items) && node.items.length > 0;
15747
+ if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some((it) => it.animation !== void 0);
15748
+ if (node.type === "draw-group") return Array.isArray(node.items) && drawableNeedsAnimation(node.items);
15749
+ return false;
15750
+ });
15751
+ }
15733
15752
  var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
15734
15753
  var init_LearningCanvas = __esm({
15735
15754
  "components/learning/atoms/LearningCanvas.tsx"() {
@@ -15737,6 +15756,8 @@ var init_LearningCanvas = __esm({
15737
15756
  init_cn();
15738
15757
  init_perf();
15739
15758
  init_useEventBus();
15759
+ init_webPainter2d();
15760
+ init_paintDispatch();
15740
15761
  DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
15741
15762
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
15742
15763
  LearningCanvas = ({
@@ -15745,6 +15766,8 @@ var init_LearningCanvas = __esm({
15745
15766
  height = 400,
15746
15767
  backgroundColor,
15747
15768
  shapes = [],
15769
+ drawables,
15770
+ projector,
15748
15771
  readouts,
15749
15772
  traces,
15750
15773
  interactive = false,
@@ -15758,6 +15781,9 @@ var init_LearningCanvas = __esm({
15758
15781
  const eventBus = useEventBus();
15759
15782
  const animRef = React94.useRef(0);
15760
15783
  const hoverIndexRef = React94.useRef(-1);
15784
+ const [drawVersion, setDrawVersion] = React94.useState(0);
15785
+ const invalidateRef = React94.useRef(() => setDrawVersion((v) => v + 1));
15786
+ const needsAnim = React94.useMemo(() => drawableNeedsAnimation(drawables), [drawables]);
15761
15787
  const findShapeAt = React94.useCallback((clientX, clientY) => {
15762
15788
  const canvas = canvasRef.current;
15763
15789
  if (!canvas) return -1;
@@ -15801,20 +15827,29 @@ var init_LearningCanvas = __esm({
15801
15827
  for (const shape of derivedShapes) {
15802
15828
  if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
15803
15829
  }
15830
+ if (drawables?.length && projector) {
15831
+ const painter = createWebPainter(ctx, invalidateRef.current);
15832
+ const timeMs = needsAnim && typeof performance !== "undefined" ? performance.now() : 0;
15833
+ const dctx = { projector, time: timeMs, invalidate: invalidateRef.current };
15834
+ for (const node of drawables) {
15835
+ paintDrawable(painter, node, dctx);
15836
+ }
15837
+ }
15804
15838
  ui.perfEnd("learningcanvas:paint", _perfT);
15805
- }, [width, height, backgroundColor, derivedShapes]);
15839
+ }, [width, height, backgroundColor, derivedShapes, drawables, projector, needsAnim]);
15806
15840
  React94.useEffect(() => {
15807
15841
  draw();
15808
- }, [draw]);
15842
+ }, [draw, drawVersion]);
15809
15843
  React94.useEffect(() => {
15810
- if (!animate) return;
15844
+ const shouldAnimate = animate || needsAnim;
15845
+ if (!shouldAnimate) return;
15811
15846
  const loop = () => {
15812
15847
  draw();
15813
15848
  animRef.current = requestAnimationFrame(loop);
15814
15849
  };
15815
15850
  animRef.current = requestAnimationFrame(loop);
15816
15851
  return () => cancelAnimationFrame(animRef.current);
15817
- }, [animate, draw]);
15852
+ }, [animate, needsAnim, draw]);
15818
15853
  const handlePointerMove = React94.useCallback(
15819
15854
  (e) => {
15820
15855
  if (!interactive) return;
@@ -27246,8 +27281,9 @@ var init_DashboardLayout = __esm({
27246
27281
  if (notificationClickEvent) eventBus.emit(`UI:${notificationClickEvent}`, {});
27247
27282
  if (onNotificationClick) onNotificationClick();
27248
27283
  };
27249
- const handleTopBarActionClick = (event) => {
27250
- eventBus.emit(`UI:${event}`, {});
27284
+ const handleTopBarActionClick = (action) => {
27285
+ if (action.event) eventBus.emit(`UI:${action.event}`, {});
27286
+ if (action.navigatesTo) eventBus.emit("UI:NAVIGATE", { url: action.navigatesTo });
27251
27287
  };
27252
27288
  const [sidebarOpen, setSidebarOpen] = React94.useState(false);
27253
27289
  const [userMenuOpen, setUserMenuOpen] = React94.useState(false);
@@ -27462,7 +27498,7 @@ var init_DashboardLayout = __esm({
27462
27498
  {
27463
27499
  variant: "ghost",
27464
27500
  className: "relative p-2 rounded-full hover:bg-muted dark:hover:bg-muted",
27465
- onClick: () => handleTopBarActionClick(action.event),
27501
+ onClick: () => handleTopBarActionClick(action),
27466
27502
  "aria-label": action.label ?? (typeof action.icon === "string" ? action.icon : void 0),
27467
27503
  children: [
27468
27504
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon: action.icon, className: "h-5 w-5 text-muted-foreground dark:text-muted-foreground" }),
@@ -27479,7 +27515,7 @@ var init_DashboardLayout = __esm({
27479
27515
  )
27480
27516
  ]
27481
27517
  },
27482
- `${action.event}-${idx}`
27518
+ `${action.event ?? action.navigatesTo ?? "action"}-${idx}`
27483
27519
  )),
27484
27520
  notificationsEnabled && /* @__PURE__ */ jsxRuntime.jsxs(
27485
27521
  Button,
@@ -33141,6 +33177,7 @@ var init_MathCanvas = __esm({
33141
33177
  angles = [],
33142
33178
  hops = [],
33143
33179
  shapes = [],
33180
+ drawables,
33144
33181
  readouts,
33145
33182
  traces,
33146
33183
  interactive = false,
@@ -33447,6 +33484,42 @@ var init_MathCanvas = __esm({
33447
33484
  hops,
33448
33485
  shapes
33449
33486
  ]);
33487
+ const projector = React94.useMemo(() => {
33488
+ if (!drawables?.length) return void 0;
33489
+ const margin = 24;
33490
+ const plotW = width - margin * 2;
33491
+ const plotH = height - margin * 2;
33492
+ const xScale = plotW / (xMax - xMin);
33493
+ const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
33494
+ const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
33495
+ const project = (pos) => ({ x: mapX(pos.x), y: mapY(pos.y) });
33496
+ const anchorPoint = (pos, anchor) => {
33497
+ const base = project(pos);
33498
+ if (anchor === "top-left") return base;
33499
+ const cx = base.x + xScale / 2;
33500
+ if (anchor === "ground") return { x: cx, y: base.y + xScale * 0.92 };
33501
+ return { x: cx, y: base.y + xScale / 2 };
33502
+ };
33503
+ const cellPath = (pos) => {
33504
+ const base = project(pos);
33505
+ return [
33506
+ { x: base.x, y: base.y },
33507
+ { x: base.x + xScale, y: base.y },
33508
+ { x: base.x + xScale, y: base.y + xScale },
33509
+ { x: base.x, y: base.y + xScale }
33510
+ ];
33511
+ };
33512
+ return {
33513
+ project,
33514
+ anchorPoint,
33515
+ cellPath,
33516
+ tileWidth: xScale,
33517
+ floorHeight: xScale / 2,
33518
+ diamondTopY: 0,
33519
+ squareGrid: true,
33520
+ worldPixelDirect: false
33521
+ };
33522
+ }, [drawables?.length, width, height, xMin, xMax, yMin, yMax]);
33450
33523
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
33451
33524
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
33452
33525
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -33456,6 +33529,8 @@ var init_MathCanvas = __esm({
33456
33529
  height,
33457
33530
  backgroundColor,
33458
33531
  shapes: derivedShapes,
33532
+ drawables,
33533
+ projector,
33459
33534
  readouts,
33460
33535
  traces,
33461
33536
  interactive,
@@ -34685,7 +34760,7 @@ var init_MapView = __esm({
34685
34760
  shadowSize: [41, 41]
34686
34761
  });
34687
34762
  L.Marker.prototype.options.icon = defaultIcon;
34688
- const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback96, useState: useState102 } = React94__namespace.default;
34763
+ const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback96, useState: useState103 } = React94__namespace.default;
34689
34764
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
34690
34765
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
34691
34766
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -34730,7 +34805,7 @@ var init_MapView = __esm({
34730
34805
  showAttribution = true
34731
34806
  }) {
34732
34807
  const eventBus = useEventBus2();
34733
- const [clickedPosition, setClickedPosition] = useState102(null);
34808
+ const [clickedPosition, setClickedPosition] = useState103(null);
34734
34809
  const handleMapClick = useCallback96((lat, lng) => {
34735
34810
  if (showClickedPin) {
34736
34811
  setClickedPosition({ lat, lng });
package/dist/avl/index.js CHANGED
@@ -7,7 +7,7 @@ import ELK from 'elkjs/lib/elk.bundled.js';
7
7
  import { MarkerType, useReactFlow, Handle, Position, getBezierPath, EdgeLabelRenderer, useNodeId, ReactFlowProvider, BaseEdge, useNodesState, useEdgesState, ReactFlow, Controls, Background, BackgroundVariant } from '@xyflow/react';
8
8
  import { useTranslate } from '@almadar/ui/hooks';
9
9
  import { InMemoryPersistence, StateMachineManager, collectDeclaredConfigDefaults, resolveCallSitePayloadCaptures, createServerEffectHandlers, EffectExecutor, createContextFromBindings, createTickScheduler, isValidCronExpression, parseDurationString, normalizeCallSiteConfigToValues, interpolateValue } from '@almadar/runtime';
10
- import { FieldTypeSchema, isInlineTrait, isPageReference, buildResolvedTraitConfigs, schemaToIR, getPage, mergeEntityFrame, isCircuitEvent, applyListenPayloadMapping, walkSExpr, isRenderBindingMarker, containsEntityBinding, isSExpr, isEventPayloadValue, RENDER_BINDING_MARKER, isFileValue, ANIMATION_NAMES } from '@almadar/core';
10
+ import { FieldTypeSchema, isInlineTrait, isPageReference, buildResolvedTraitConfigs, schemaToIR, getPage, mergeEntityFrame, isCircuitEvent, applyListenPayloadMapping, walkSExpr, isRenderBindingMarker, containsEntityBinding, isSExpr, isEventPayloadValue, RENDER_BINDING_MARKER, isFileValue } from '@almadar/core';
11
11
  import * as LucideIcons2 from 'lucide-react';
12
12
  import { Loader2, X, Code, FileText, WrapText, Check, Copy, Lightbulb, CheckCircle, List, Printer, ChevronRight, ChevronLeft, GitBranch, Pencil, Eye, Plus, ArrowRight, Trash, RotateCcw, Play, Terminal, XCircle, AlertTriangle, Trash2, Link2, ArrowLeft, ZoomOut, ZoomIn, Download, DollarSign, ChevronDown, Menu as Menu$1, Package, Calendar, MoreHorizontal, Image as Image$1, Upload, HelpCircle, PauseCircle, Search, Bold, Italic, Underline, Strikethrough, Heading1, Heading2, Heading3, ListOrdered, Quote, Link as Link$1, Minus, Eraser, TrendingUp, TrendingDown, AlertCircle, Circle, Clock, CheckCircle2, ChevronUp } from 'lucide-react';
13
13
  import { createPortal } from 'react-dom';
@@ -9541,6 +9541,7 @@ function resolveFxView(item, presets) {
9541
9541
  vy: item.vy ?? row.vy,
9542
9542
  vz: item.vz ?? row.vz,
9543
9543
  particleCount: item.particleCount ?? row.particleCount,
9544
+ animation: item.animation ?? row.animation,
9544
9545
  shape: row.shape,
9545
9546
  count: row.count,
9546
9547
  glow: row.glow,
@@ -11072,9 +11073,6 @@ var init_isometric = __esm({
11072
11073
  };
11073
11074
  }
11074
11075
  });
11075
- function isAnimationName(name) {
11076
- return ANIMATION_NAMES.includes(name);
11077
- }
11078
11076
  function frameRect(frame, row, columns, frameWidth, frameHeight) {
11079
11077
  return {
11080
11078
  sx: frame % columns * frameWidth,
@@ -13008,13 +13006,17 @@ var init_DrawSprite = __esm({
13008
13006
  return;
13009
13007
  }
13010
13008
  if (isSpriteSheetAtlas(atlas)) {
13011
- const def = isAnimationName(node.animation) ? atlas.animations[node.animation] : void 0;
13009
+ const def = atlas.animations[node.animation];
13012
13010
  if (!def) {
13013
13011
  paintFallbackSquare(painter, node, dctx, "sprite-missing");
13014
13012
  return;
13015
13013
  }
13016
- const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, dctx.time);
13014
+ const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, node.clockMs ?? dctx.time);
13017
13015
  const r2 = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
13016
+ if (![r2.sx, r2.sy, r2.sw, r2.sh].every(Number.isFinite)) {
13017
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
13018
+ return;
13019
+ }
13018
13020
  src = { x: r2.sx, y: r2.sy, w: r2.sw, h: r2.sh };
13019
13021
  }
13020
13022
  }
@@ -13562,7 +13564,10 @@ function expandFxItem(view, node, epochNowMs, dim) {
13562
13564
  anchor: "center",
13563
13565
  width: spriteSize,
13564
13566
  height: spriteSize,
13565
- opacity: fade
13567
+ opacity: fade,
13568
+ animation: view.animation ?? sprite.animations?.[0],
13569
+ // One-shot fx rows (burst) play from the item's birth, not wall-time.
13570
+ clockMs: ageMs
13566
13571
  });
13567
13572
  } else {
13568
13573
  out.push(...proceduralShapes(view, pos, fade, progress));
@@ -15142,6 +15147,7 @@ var init_GameShell = __esm({
15142
15147
  init_Typography();
15143
15148
  init_AtlasImage();
15144
15149
  GAME_FONTS = {
15150
+ fredoka: "Fredoka",
15145
15151
  future: "Kenney Future",
15146
15152
  "future-narrow": "Kenney Future Narrow",
15147
15153
  pixel: "Kenney Pixel",
@@ -15158,10 +15164,10 @@ var init_GameShell = __esm({
15158
15164
  showTopBar = true,
15159
15165
  children,
15160
15166
  backgroundAsset,
15161
- fontFamily = "future",
15167
+ fontFamily,
15162
15168
  "data-theme": dataTheme
15163
15169
  }) => {
15164
- const font = GAME_FONTS[fontFamily] ?? fontFamily;
15170
+ const font = fontFamily ? GAME_FONTS[fontFamily] ?? fontFamily : void 0;
15165
15171
  return /* @__PURE__ */ jsxs(
15166
15172
  Box,
15167
15173
  {
@@ -15176,8 +15182,11 @@ var init_GameShell = __esm({
15176
15182
  color: "var(--color-foreground, #e0e0e0)",
15177
15183
  // The fontFamily knob is a scoped override of the theme contract's
15178
15184
  // display slot: titles/numerics take the game face, body text keeps
15179
- // the active theme's --font-family-body.
15180
- "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif`
15185
+ // the active theme's --font-family-body. Set ONLY when explicitly
15186
+ // passed an always-on inline stamp would shadow the orbital's
15187
+ // inline-theme font-family-display token (inline style beats the
15188
+ // theme provider's vars for the whole shell subtree).
15189
+ ...font ? { "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif` } : {}
15181
15190
  },
15182
15191
  children: [
15183
15192
  backgroundAsset && /* @__PURE__ */ jsx(
@@ -15654,6 +15663,16 @@ function traceShapes(panel, k, width, height) {
15654
15663
  }
15655
15664
  return out;
15656
15665
  }
15666
+ function drawableNeedsAnimation(nodes) {
15667
+ if (!nodes) return false;
15668
+ return nodes.some((node) => {
15669
+ if (node.type === "draw-sprite") return node.animation !== void 0;
15670
+ if (node.type === "draw-fx-layer") return Array.isArray(node.items) && node.items.length > 0;
15671
+ if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some((it) => it.animation !== void 0);
15672
+ if (node.type === "draw-group") return Array.isArray(node.items) && drawableNeedsAnimation(node.items);
15673
+ return false;
15674
+ });
15675
+ }
15657
15676
  var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
15658
15677
  var init_LearningCanvas = __esm({
15659
15678
  "components/learning/atoms/LearningCanvas.tsx"() {
@@ -15661,6 +15680,8 @@ var init_LearningCanvas = __esm({
15661
15680
  init_cn();
15662
15681
  init_perf();
15663
15682
  init_useEventBus();
15683
+ init_webPainter2d();
15684
+ init_paintDispatch();
15664
15685
  DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
15665
15686
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
15666
15687
  LearningCanvas = ({
@@ -15669,6 +15690,8 @@ var init_LearningCanvas = __esm({
15669
15690
  height = 400,
15670
15691
  backgroundColor,
15671
15692
  shapes = [],
15693
+ drawables,
15694
+ projector,
15672
15695
  readouts,
15673
15696
  traces,
15674
15697
  interactive = false,
@@ -15682,6 +15705,9 @@ var init_LearningCanvas = __esm({
15682
15705
  const eventBus = useEventBus();
15683
15706
  const animRef = useRef(0);
15684
15707
  const hoverIndexRef = useRef(-1);
15708
+ const [drawVersion, setDrawVersion] = useState(0);
15709
+ const invalidateRef = useRef(() => setDrawVersion((v) => v + 1));
15710
+ const needsAnim = useMemo(() => drawableNeedsAnimation(drawables), [drawables]);
15685
15711
  const findShapeAt = useCallback((clientX, clientY) => {
15686
15712
  const canvas = canvasRef.current;
15687
15713
  if (!canvas) return -1;
@@ -15725,20 +15751,29 @@ var init_LearningCanvas = __esm({
15725
15751
  for (const shape of derivedShapes) {
15726
15752
  if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
15727
15753
  }
15754
+ if (drawables?.length && projector) {
15755
+ const painter = createWebPainter(ctx, invalidateRef.current);
15756
+ const timeMs = needsAnim && typeof performance !== "undefined" ? performance.now() : 0;
15757
+ const dctx = { projector, time: timeMs, invalidate: invalidateRef.current };
15758
+ for (const node of drawables) {
15759
+ paintDrawable(painter, node, dctx);
15760
+ }
15761
+ }
15728
15762
  perfEnd("learningcanvas:paint", _perfT);
15729
- }, [width, height, backgroundColor, derivedShapes]);
15763
+ }, [width, height, backgroundColor, derivedShapes, drawables, projector, needsAnim]);
15730
15764
  useEffect(() => {
15731
15765
  draw();
15732
- }, [draw]);
15766
+ }, [draw, drawVersion]);
15733
15767
  useEffect(() => {
15734
- if (!animate) return;
15768
+ const shouldAnimate = animate || needsAnim;
15769
+ if (!shouldAnimate) return;
15735
15770
  const loop = () => {
15736
15771
  draw();
15737
15772
  animRef.current = requestAnimationFrame(loop);
15738
15773
  };
15739
15774
  animRef.current = requestAnimationFrame(loop);
15740
15775
  return () => cancelAnimationFrame(animRef.current);
15741
- }, [animate, draw]);
15776
+ }, [animate, needsAnim, draw]);
15742
15777
  const handlePointerMove = useCallback(
15743
15778
  (e) => {
15744
15779
  if (!interactive) return;
@@ -27170,8 +27205,9 @@ var init_DashboardLayout = __esm({
27170
27205
  if (notificationClickEvent) eventBus.emit(`UI:${notificationClickEvent}`, {});
27171
27206
  if (onNotificationClick) onNotificationClick();
27172
27207
  };
27173
- const handleTopBarActionClick = (event) => {
27174
- eventBus.emit(`UI:${event}`, {});
27208
+ const handleTopBarActionClick = (action) => {
27209
+ if (action.event) eventBus.emit(`UI:${action.event}`, {});
27210
+ if (action.navigatesTo) eventBus.emit("UI:NAVIGATE", { url: action.navigatesTo });
27175
27211
  };
27176
27212
  const [sidebarOpen, setSidebarOpen] = useState(false);
27177
27213
  const [userMenuOpen, setUserMenuOpen] = useState(false);
@@ -27386,7 +27422,7 @@ var init_DashboardLayout = __esm({
27386
27422
  {
27387
27423
  variant: "ghost",
27388
27424
  className: "relative p-2 rounded-full hover:bg-muted dark:hover:bg-muted",
27389
- onClick: () => handleTopBarActionClick(action.event),
27425
+ onClick: () => handleTopBarActionClick(action),
27390
27426
  "aria-label": action.label ?? (typeof action.icon === "string" ? action.icon : void 0),
27391
27427
  children: [
27392
27428
  /* @__PURE__ */ jsx(Icon, { icon: action.icon, className: "h-5 w-5 text-muted-foreground dark:text-muted-foreground" }),
@@ -27403,7 +27439,7 @@ var init_DashboardLayout = __esm({
27403
27439
  )
27404
27440
  ]
27405
27441
  },
27406
- `${action.event}-${idx}`
27442
+ `${action.event ?? action.navigatesTo ?? "action"}-${idx}`
27407
27443
  )),
27408
27444
  notificationsEnabled && /* @__PURE__ */ jsxs(
27409
27445
  Button,
@@ -33065,6 +33101,7 @@ var init_MathCanvas = __esm({
33065
33101
  angles = [],
33066
33102
  hops = [],
33067
33103
  shapes = [],
33104
+ drawables,
33068
33105
  readouts,
33069
33106
  traces,
33070
33107
  interactive = false,
@@ -33371,6 +33408,42 @@ var init_MathCanvas = __esm({
33371
33408
  hops,
33372
33409
  shapes
33373
33410
  ]);
33411
+ const projector = useMemo(() => {
33412
+ if (!drawables?.length) return void 0;
33413
+ const margin = 24;
33414
+ const plotW = width - margin * 2;
33415
+ const plotH = height - margin * 2;
33416
+ const xScale = plotW / (xMax - xMin);
33417
+ const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
33418
+ const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
33419
+ const project = (pos) => ({ x: mapX(pos.x), y: mapY(pos.y) });
33420
+ const anchorPoint = (pos, anchor) => {
33421
+ const base = project(pos);
33422
+ if (anchor === "top-left") return base;
33423
+ const cx = base.x + xScale / 2;
33424
+ if (anchor === "ground") return { x: cx, y: base.y + xScale * 0.92 };
33425
+ return { x: cx, y: base.y + xScale / 2 };
33426
+ };
33427
+ const cellPath = (pos) => {
33428
+ const base = project(pos);
33429
+ return [
33430
+ { x: base.x, y: base.y },
33431
+ { x: base.x + xScale, y: base.y },
33432
+ { x: base.x + xScale, y: base.y + xScale },
33433
+ { x: base.x, y: base.y + xScale }
33434
+ ];
33435
+ };
33436
+ return {
33437
+ project,
33438
+ anchorPoint,
33439
+ cellPath,
33440
+ tileWidth: xScale,
33441
+ floorHeight: xScale / 2,
33442
+ diamondTopY: 0,
33443
+ squareGrid: true,
33444
+ worldPixelDirect: false
33445
+ };
33446
+ }, [drawables?.length, width, height, xMin, xMax, yMin, yMax]);
33374
33447
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
33375
33448
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
33376
33449
  /* @__PURE__ */ jsx(
@@ -33380,6 +33453,8 @@ var init_MathCanvas = __esm({
33380
33453
  height,
33381
33454
  backgroundColor,
33382
33455
  shapes: derivedShapes,
33456
+ drawables,
33457
+ projector,
33383
33458
  readouts,
33384
33459
  traces,
33385
33460
  interactive,
@@ -34609,7 +34684,7 @@ var init_MapView = __esm({
34609
34684
  shadowSize: [41, 41]
34610
34685
  });
34611
34686
  L.Marker.prototype.options.icon = defaultIcon;
34612
- const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback96, useState: useState102 } = React94__default;
34687
+ const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback96, useState: useState103 } = React94__default;
34613
34688
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
34614
34689
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
34615
34690
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -34654,7 +34729,7 @@ var init_MapView = __esm({
34654
34729
  showAttribution = true
34655
34730
  }) {
34656
34731
  const eventBus = useEventBus2();
34657
- const [clickedPosition, setClickedPosition] = useState102(null);
34732
+ const [clickedPosition, setClickedPosition] = useState103(null);
34658
34733
  const handleMapClick = useCallback96((lat, lng) => {
34659
34734
  if (showClickedPin) {
34660
34735
  setClickedPosition({ lat, lng });
@@ -1,7 +1,7 @@
1
1
  import { AnimationName, Asset, ScenePos, EventEmit, JsonObject, JsonValue } from '@almadar/core';
2
2
  import React__default from 'react';
3
3
  import * as THREE from 'three';
4
- import { D as DrawableNode } from './paintDispatch-B5n2DTB-.js';
4
+ import { D as DrawableNode } from './paintDispatch-B5ObLnUv.js';
5
5
 
6
6
  /**
7
7
  * Sprite Sheet Animation Types
@@ -1,7 +1,7 @@
1
1
  import { AnimationName, Asset, ScenePos, EventEmit, JsonObject, JsonValue } from '@almadar/core';
2
2
  import React__default from 'react';
3
3
  import * as THREE from 'three';
4
- import { D as DrawableNode } from './paintDispatch-DgBctxIq.cjs';
4
+ import { D as DrawableNode } from './paintDispatch-D3R8NNmV.cjs';
5
5
 
6
6
  /**
7
7
  * Sprite Sheet Animation Types
@@ -1,6 +1,6 @@
1
1
  import { OrbitalSchema, SExpr, Effect, OrbitalVerificationAPI, TraitStateSnapshot, EventPayload, BusEvent, VerificationCheck, BridgeHealth, VerificationSnapshot, VerificationSummary, TransitionTrace, ServerResponseTrace, CheckStatus, AssetLoadStatus } from '@almadar/core';
2
2
  import React__default from 'react';
3
- import { D as DrawableNode } from './paintDispatch-B5n2DTB-.js';
3
+ import { D as DrawableNode } from './paintDispatch-B5ObLnUv.js';
4
4
  import { ClassValue } from 'clsx';
5
5
 
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import { OrbitalSchema, SExpr, Effect, OrbitalVerificationAPI, TraitStateSnapshot, EventPayload, BusEvent, VerificationCheck, BridgeHealth, VerificationSnapshot, VerificationSummary, TransitionTrace, ServerResponseTrace, CheckStatus, AssetLoadStatus } from '@almadar/core';
2
2
  import React__default from 'react';
3
- import { D as DrawableNode } from './paintDispatch-DgBctxIq.cjs';
3
+ import { D as DrawableNode } from './paintDispatch-D3R8NNmV.cjs';
4
4
  import { ClassValue } from 'clsx';
5
5
 
6
6
  /**