@almadar/ui 5.142.0 → 5.144.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.
@@ -1408,6 +1408,10 @@ var init_Icon = __esm({
1408
1408
  function isTilesheet(a) {
1409
1409
  return typeof a.tileWidth === "number";
1410
1410
  }
1411
+ function isSpriteSheetAtlas(a) {
1412
+ const s = a;
1413
+ return typeof s.frameWidth === "number" && typeof s.frameHeight === "number" && typeof s.animations === "object";
1414
+ }
1411
1415
  function getAtlas(url, onReady) {
1412
1416
  if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
1413
1417
  atlasCache.set(url, void 0);
@@ -1447,6 +1451,7 @@ function subRectFor(atlas, sprite) {
1447
1451
  sh: atlas.tileHeight
1448
1452
  };
1449
1453
  }
1454
+ if (isSpriteSheetAtlas(atlas)) return null;
1450
1455
  const st = atlas.subTextures[sprite];
1451
1456
  if (!st) return null;
1452
1457
  return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
@@ -1756,7 +1761,7 @@ var init_Button = __esm({
1756
1761
  "data-testid": dataTestId ?? (action ? `action-${action}` : void 0),
1757
1762
  children: [
1758
1763
  isLoading ? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons2.Loader2, { className: "h-icon-default w-icon-default animate-spin" }) : resolvedLeftIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0", children: resolvedLeftIcon }),
1759
- children || label,
1764
+ (Array.isArray(children) ? children.length > 0 : Boolean(children)) ? children : label,
1760
1765
  resolvedRightIcon && !isLoading && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0", children: resolvedRightIcon })
1761
1766
  ]
1762
1767
  }
@@ -7761,6 +7766,34 @@ var init_isometric = __esm({
7761
7766
  };
7762
7767
  }
7763
7768
  });
7769
+ function isAnimationName(name) {
7770
+ return core.ANIMATION_NAMES.includes(name);
7771
+ }
7772
+ function frameRect(frame, row, columns, frameWidth, frameHeight) {
7773
+ return {
7774
+ sx: frame % columns * frameWidth,
7775
+ sy: row * frameHeight,
7776
+ sw: frameWidth,
7777
+ sh: frameHeight
7778
+ };
7779
+ }
7780
+ function getCurrentFrameFromDef(def, elapsed) {
7781
+ const frameDuration = 1e3 / def.frameRate;
7782
+ const totalDuration = def.frames * frameDuration;
7783
+ if (def.loop) {
7784
+ const frame2 = Math.floor(elapsed % totalDuration / frameDuration);
7785
+ return { frame: frame2, finished: false };
7786
+ }
7787
+ if (elapsed >= totalDuration) {
7788
+ return { frame: def.frames - 1, finished: true };
7789
+ }
7790
+ const frame = Math.floor(elapsed / frameDuration);
7791
+ return { frame, finished: false };
7792
+ }
7793
+ var init_spriteAnimation = __esm({
7794
+ "lib/spriteAnimation.ts"() {
7795
+ }
7796
+ });
7764
7797
 
7765
7798
  // lib/gameShared.ts
7766
7799
  var init_gameShared = __esm({
@@ -9514,6 +9547,9 @@ function paintFallbackSquare(painter, node, dctx, reason) {
9514
9547
  painter.strokeRect(rect.x, rect.y, rect.w, rect.h, "#5e564b", Math.max(1, tw / 32));
9515
9548
  painter.restore();
9516
9549
  }
9550
+ function isAnimatedSprite(node) {
9551
+ return node.animation !== void 0 && typeof node.asset?.atlas === "string";
9552
+ }
9517
9553
  function DrawSprite(_props) {
9518
9554
  return null;
9519
9555
  }
@@ -9522,6 +9558,7 @@ var init_DrawSprite = __esm({
9522
9558
  "components/game/atoms/DrawSprite.tsx"() {
9523
9559
  "use client";
9524
9560
  init_atlasSlice();
9561
+ init_spriteAnimation();
9525
9562
  init_imageCache();
9526
9563
  init_contract();
9527
9564
  spriteLog = logger.createLogger("almadar:ui:draw-sprite");
@@ -9535,6 +9572,23 @@ var init_DrawSprite = __esm({
9535
9572
  return;
9536
9573
  }
9537
9574
  let src = typeof node.frame === "object" ? node.frame : void 0;
9575
+ if (!src && node.animation !== void 0 && typeof node.asset.atlas === "string") {
9576
+ const atlas = getAtlas(node.asset.atlas, dctx.invalidate);
9577
+ if (!atlas) {
9578
+ if (atlasFailed(node.asset.atlas)) paintFallbackSquare(painter, node, dctx, "atlas-failed");
9579
+ return;
9580
+ }
9581
+ if (isSpriteSheetAtlas(atlas)) {
9582
+ const def = isAnimationName(node.animation) ? atlas.animations[node.animation] : void 0;
9583
+ if (!def) {
9584
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
9585
+ return;
9586
+ }
9587
+ const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, dctx.time);
9588
+ const r = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
9589
+ src = { x: r.sx, y: r.sy, w: r.sw, h: r.sh };
9590
+ }
9591
+ }
9538
9592
  if (!src && isAtlasAsset(node.asset)) {
9539
9593
  const atlas = getAtlas(node.asset.atlas, dctx.invalidate);
9540
9594
  if (!atlas) {
@@ -9826,6 +9880,83 @@ var init_DrawText = __esm({
9826
9880
  };
9827
9881
  }
9828
9882
  });
9883
+ function isAnimatedGroup(node) {
9884
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
9885
+ }
9886
+ function DrawGroup(props) {
9887
+ const register = React85.useContext(DrawableRegistryContext);
9888
+ if (register) register({ ...props, type: "draw-group" });
9889
+ return null;
9890
+ }
9891
+ var init_DrawGroup = __esm({
9892
+ "components/game/atoms/DrawGroup.tsx"() {
9893
+ "use client";
9894
+ init_registry();
9895
+ }
9896
+ });
9897
+ function applyMeshAnimation(node, timeMs) {
9898
+ const anim = node.animation;
9899
+ if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return null;
9900
+ const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
9901
+ const cycle = timeMs / anim.durationMs;
9902
+ const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
9903
+ const trackValue = (key) => {
9904
+ const defined = frames.filter((f3) => f3[key] !== void 0);
9905
+ if (defined.length === 0) return void 0;
9906
+ let prev;
9907
+ let next;
9908
+ for (const f3 of defined) {
9909
+ if (f3.at <= t) prev = f3;
9910
+ else if (!next) next = f3;
9911
+ }
9912
+ if (!prev) return defined[0][key];
9913
+ if (!next) return prev[key];
9914
+ const span = next.at - prev.at;
9915
+ const k = span > 0 ? (t - prev.at) / span : 1;
9916
+ const a = prev[key];
9917
+ const b = next[key];
9918
+ if (typeof a === "number" && typeof b === "number") return lerp2(a, b, k);
9919
+ return a;
9920
+ };
9921
+ const num = {};
9922
+ for (const key of NUMERIC_TRACKS2) {
9923
+ const v = trackValue(key);
9924
+ if (v !== void 0) num[key] = v;
9925
+ }
9926
+ return {
9927
+ offset: [num.offsetX ?? 0, num.offsetY ?? 0, num.offsetZ ?? 0],
9928
+ rotate: [num.rotateX ?? 0, num.rotateY ?? 0, num.rotateZ ?? 0],
9929
+ scale: num.scale ?? 1,
9930
+ opacity: num.opacity,
9931
+ emissiveIntensity: num.emissiveIntensity,
9932
+ color: trackValue("color"),
9933
+ emissive: trackValue("emissive")
9934
+ };
9935
+ }
9936
+ function DrawMesh(props) {
9937
+ const register = React85.useContext(DrawableRegistryContext);
9938
+ if (register) register({ ...props, type: "draw-mesh" });
9939
+ return null;
9940
+ }
9941
+ var lerp2, NUMERIC_TRACKS2;
9942
+ var init_DrawMesh = __esm({
9943
+ "components/game/atoms/DrawMesh.tsx"() {
9944
+ "use client";
9945
+ init_registry();
9946
+ lerp2 = (a, b, k) => a + (b - a) * k;
9947
+ NUMERIC_TRACKS2 = [
9948
+ "offsetX",
9949
+ "offsetY",
9950
+ "offsetZ",
9951
+ "rotateX",
9952
+ "rotateY",
9953
+ "rotateZ",
9954
+ "scale",
9955
+ "opacity",
9956
+ "emissiveIntensity"
9957
+ ];
9958
+ }
9959
+ });
9829
9960
 
9830
9961
  // components/game/molecules/DrawSpriteLayer.tsx
9831
9962
  function DrawSpriteLayer(_props) {
@@ -9892,18 +10023,22 @@ function paintDrawable(painter, node, dctx) {
9892
10023
  if (!isValidScenePos(node.position)) break;
9893
10024
  if (!Array.isArray(node.items)) break;
9894
10025
  const p = dctx.projector.project(node.position);
10026
+ const anim = dctx.time > 0 && isAnimatedGroup(node) ? applyMeshAnimation(node, dctx.time) : null;
10027
+ const tw = dctx.projector.tileWidth;
9895
10028
  painter.save();
9896
- painter.translate(p.x, p.y);
9897
- if (node.scale !== void 0) painter.scale(node.scale, node.scale);
9898
- if (node.rotate !== void 0) painter.rotate(node.rotate);
9899
- if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
10029
+ painter.translate(p.x + (anim ? anim.offset[0] * tw : 0), p.y + (anim ? anim.offset[1] * tw : 0));
10030
+ const scale = (node.scale ?? 1) * (anim?.scale ?? 1);
10031
+ if (scale !== 1) painter.scale(scale, scale);
10032
+ const rotate = (node.rotate ?? 0) + (node.rotation?.[2] ?? 0) + (anim?.rotate[2] ?? 0);
10033
+ if (rotate !== 0) painter.rotate(rotate);
10034
+ const opacity = (node.opacity ?? 1) * (anim?.opacity ?? 1);
10035
+ if (opacity !== 1) painter.setAlpha(opacity);
9900
10036
  if (node.clip) {
9901
- const tw = dctx.projector.tileWidth;
9902
10037
  painter.scale(tw, tw);
9903
10038
  painter.clipPath(node.clip);
9904
10039
  painter.scale(1 / tw, 1 / tw);
9905
10040
  }
9906
- const childCtx = node.scale !== void 0 && node.scale !== 1 ? { ...dctx, groupScale: (dctx.groupScale ?? 1) * node.scale } : dctx;
10041
+ const childCtx = scale !== 1 ? { ...dctx, groupScale: (dctx.groupScale ?? 1) * scale } : dctx;
9907
10042
  for (const item of node.items) paintDrawable(painter, item, childCtx);
9908
10043
  painter.restore();
9909
10044
  break;
@@ -9929,6 +10064,8 @@ var init_paintDispatch = __esm({
9929
10064
  init_DrawSprite();
9930
10065
  init_DrawShape();
9931
10066
  init_DrawText();
10067
+ init_DrawGroup();
10068
+ init_DrawMesh();
9932
10069
  init_DrawSpriteLayer();
9933
10070
  init_DrawShapeLayer();
9934
10071
  init_DrawTextLayer();
@@ -10175,8 +10312,11 @@ function Canvas2D({
10175
10312
  const miniMapHeight = gridExtent.height || 10;
10176
10313
  const drawableIsAnimated = (node) => {
10177
10314
  if (node.type === "draw-shape") return isAnimatedShape(node);
10178
- if (node.type === "draw-group") return Array.isArray(node.items) && node.items.some(drawableIsAnimated);
10315
+ if (node.type === "draw-sprite") return isAnimatedSprite(node);
10316
+ if (node.type === "draw-group")
10317
+ return isAnimatedGroup(node) || Array.isArray(node.items) && node.items.some(drawableIsAnimated);
10179
10318
  if (node.type === "draw-shape-layer") return Array.isArray(node.items) && node.items.some(isAnimatedShape);
10319
+ if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some(isAnimatedSprite);
10180
10320
  return false;
10181
10321
  };
10182
10322
  const animRafRef = React85.useRef(0);
@@ -10517,6 +10657,8 @@ var init_Canvas2D = __esm({
10517
10657
  init_projector();
10518
10658
  init_paintDispatch();
10519
10659
  init_DrawShape();
10660
+ init_DrawSprite();
10661
+ init_DrawGroup();
10520
10662
  init_registry();
10521
10663
  init_hitTest();
10522
10664
  init_isometric();
@@ -10589,6 +10731,7 @@ function Canvas({
10589
10731
  cameraMode: to3DCameraMode(camera?.mode),
10590
10732
  ...zoom !== void 0 ? { scale: zoom } : {},
10591
10733
  ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
10734
+ ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
10592
10735
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
10593
10736
  unitScale,
10594
10737
  backgroundColor,
@@ -22554,6 +22697,14 @@ var init_DashboardLayout = __esm({
22554
22697
  NavLinkBottom.displayName = "NavLinkBottom";
22555
22698
  }
22556
22699
  });
22700
+ function downloadItemUrl(url, label) {
22701
+ const a = document.createElement("a");
22702
+ a.href = url;
22703
+ a.download = url.startsWith("data:") ? label : url.split("/").pop() ?? "download";
22704
+ document.body.appendChild(a);
22705
+ a.click();
22706
+ a.remove();
22707
+ }
22557
22708
  function computeMenuStyle(position, triggerRect) {
22558
22709
  const isTop = position.startsWith("top");
22559
22710
  const isRight = position.endsWith("right") || position.endsWith("end");
@@ -22606,6 +22757,7 @@ function SubMenu({
22606
22757
  onClick: () => {
22607
22758
  if (item.disabled) return;
22608
22759
  if (item.event) eventBus.emit(`UI:${item.event}`, { itemId, label: item.label });
22760
+ if (item.url) downloadItemUrl(item.url, item.label);
22609
22761
  item.onClick?.();
22610
22762
  },
22611
22763
  "aria-disabled": item.disabled || void 0,
@@ -22759,6 +22911,7 @@ var init_Menu = __esm({
22759
22911
  setActiveSubMenu(itemId);
22760
22912
  } else {
22761
22913
  if (item.event) eventBus.emit(`UI:${item.event}`, { itemId, label: item.label });
22914
+ if (item.url) downloadItemUrl(item.url, item.label);
22762
22915
  item.onClick?.();
22763
22916
  setIsOpen(false);
22764
22917
  }
@@ -22794,12 +22947,19 @@ var init_Menu = __esm({
22794
22947
  "bottom-end": "bottom-start"
22795
22948
  };
22796
22949
  const effectivePosition = direction === "rtl" ? rtlMirror[position] ?? position : position;
22797
- const triggerChild = React85__namespace.default.isValidElement(trigger) ? trigger : /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", as: "span", children: trigger });
22798
- const triggerElement = React85__namespace.default.cloneElement(
22799
- triggerChild,
22950
+ const triggerElement = React85__namespace.default.isValidElement(trigger) ? React85__namespace.default.cloneElement(trigger, {
22951
+ ref: triggerRef,
22952
+ onClick: handleToggle
22953
+ }) : /* @__PURE__ */ jsxRuntime.jsx(
22954
+ Box,
22800
22955
  {
22801
- ref: triggerRef,
22802
- onClick: handleToggle
22956
+ as: "span",
22957
+ ref: (el) => {
22958
+ triggerRef.current = el;
22959
+ },
22960
+ onClick: handleToggle,
22961
+ className: "inline-flex",
22962
+ children: typeof trigger === "string" || typeof trigger === "number" ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", as: "span", children: trigger }) : trigger
22803
22963
  }
22804
22964
  );
22805
22965
  const renderMenuItems = (menuItems) => menuItems.map((item, index) => {
@@ -38419,28 +38579,6 @@ var init_DetailPanel = __esm({
38419
38579
  DetailPanel.displayName = "DetailPanel";
38420
38580
  }
38421
38581
  });
38422
- function DrawGroup(props) {
38423
- const register = React85.useContext(DrawableRegistryContext);
38424
- if (register) register({ ...props, type: "draw-group" });
38425
- return null;
38426
- }
38427
- var init_DrawGroup = __esm({
38428
- "components/game/atoms/DrawGroup.tsx"() {
38429
- "use client";
38430
- init_registry();
38431
- }
38432
- });
38433
- function DrawMesh(props) {
38434
- const register = React85.useContext(DrawableRegistryContext);
38435
- if (register) register({ ...props, type: "draw-mesh" });
38436
- return null;
38437
- }
38438
- var init_DrawMesh = __esm({
38439
- "components/game/atoms/DrawMesh.tsx"() {
38440
- "use client";
38441
- init_registry();
38442
- }
38443
- });
38444
38582
  function extractTitle(children) {
38445
38583
  if (!React85__namespace.default.isValidElement(children)) return void 0;
38446
38584
  const props = children.props;
@@ -4,7 +4,7 @@ import { EventBusContext, useTraitScopeChain, useEntitySchemaOptional, useEntity
4
4
  export { EntitySchemaProvider, ServerBridgeProvider, TraitContext, TraitProvider, useEntitySchema, useEntitySchemaOptional, useServerBridge, useTrait, useTraitContext } from '@almadar/ui/providers';
5
5
  import { createLogger, setNamespaceLevel, isLogLevelEnabled } from '@almadar/logger';
6
6
  import { StateMachineManager, collectDeclaredConfigDefaults, resolveCallSitePayloadCaptures, createServerEffectHandlers, EffectExecutor, createContextFromBindings, createTickScheduler, isValidCronExpression, parseDurationString, InMemoryPersistence, normalizeCallSiteConfigToValues, interpolateValue } from '@almadar/runtime';
7
- import { mergeEntityFrame, isCircuitEvent, applyListenPayloadMapping, schemaToIR, getPage, clearSchemaCache as clearSchemaCache$1, walkSExpr, buildResolvedTraitConfigs, isRenderBindingMarker, isInlineTrait, containsEntityBinding, isSExpr, isEventPayloadValue, RENDER_BINDING_MARKER } from '@almadar/core';
7
+ import { mergeEntityFrame, isCircuitEvent, applyListenPayloadMapping, schemaToIR, getPage, clearSchemaCache as clearSchemaCache$1, walkSExpr, buildResolvedTraitConfigs, isRenderBindingMarker, isInlineTrait, containsEntityBinding, isSExpr, isEventPayloadValue, RENDER_BINDING_MARKER, ANIMATION_NAMES } from '@almadar/core';
8
8
  import { clsx } from 'clsx';
9
9
  import { twMerge } from 'tailwind-merge';
10
10
  import * as LucideIcons2 from 'lucide-react';
@@ -1334,6 +1334,10 @@ var init_Icon = __esm({
1334
1334
  function isTilesheet(a) {
1335
1335
  return typeof a.tileWidth === "number";
1336
1336
  }
1337
+ function isSpriteSheetAtlas(a) {
1338
+ const s = a;
1339
+ return typeof s.frameWidth === "number" && typeof s.frameHeight === "number" && typeof s.animations === "object";
1340
+ }
1337
1341
  function getAtlas(url, onReady) {
1338
1342
  if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
1339
1343
  atlasCache.set(url, void 0);
@@ -1373,6 +1377,7 @@ function subRectFor(atlas, sprite) {
1373
1377
  sh: atlas.tileHeight
1374
1378
  };
1375
1379
  }
1380
+ if (isSpriteSheetAtlas(atlas)) return null;
1376
1381
  const st = atlas.subTextures[sprite];
1377
1382
  if (!st) return null;
1378
1383
  return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
@@ -1682,7 +1687,7 @@ var init_Button = __esm({
1682
1687
  "data-testid": dataTestId ?? (action ? `action-${action}` : void 0),
1683
1688
  children: [
1684
1689
  isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "h-icon-default w-icon-default animate-spin" }) : resolvedLeftIcon && /* @__PURE__ */ jsx("span", { className: "flex-shrink-0", children: resolvedLeftIcon }),
1685
- children || label,
1690
+ (Array.isArray(children) ? children.length > 0 : Boolean(children)) ? children : label,
1686
1691
  resolvedRightIcon && !isLoading && /* @__PURE__ */ jsx("span", { className: "flex-shrink-0", children: resolvedRightIcon })
1687
1692
  ]
1688
1693
  }
@@ -7687,6 +7692,34 @@ var init_isometric = __esm({
7687
7692
  };
7688
7693
  }
7689
7694
  });
7695
+ function isAnimationName(name) {
7696
+ return ANIMATION_NAMES.includes(name);
7697
+ }
7698
+ function frameRect(frame, row, columns, frameWidth, frameHeight) {
7699
+ return {
7700
+ sx: frame % columns * frameWidth,
7701
+ sy: row * frameHeight,
7702
+ sw: frameWidth,
7703
+ sh: frameHeight
7704
+ };
7705
+ }
7706
+ function getCurrentFrameFromDef(def, elapsed) {
7707
+ const frameDuration = 1e3 / def.frameRate;
7708
+ const totalDuration = def.frames * frameDuration;
7709
+ if (def.loop) {
7710
+ const frame2 = Math.floor(elapsed % totalDuration / frameDuration);
7711
+ return { frame: frame2, finished: false };
7712
+ }
7713
+ if (elapsed >= totalDuration) {
7714
+ return { frame: def.frames - 1, finished: true };
7715
+ }
7716
+ const frame = Math.floor(elapsed / frameDuration);
7717
+ return { frame, finished: false };
7718
+ }
7719
+ var init_spriteAnimation = __esm({
7720
+ "lib/spriteAnimation.ts"() {
7721
+ }
7722
+ });
7690
7723
 
7691
7724
  // lib/gameShared.ts
7692
7725
  var init_gameShared = __esm({
@@ -9440,6 +9473,9 @@ function paintFallbackSquare(painter, node, dctx, reason) {
9440
9473
  painter.strokeRect(rect.x, rect.y, rect.w, rect.h, "#5e564b", Math.max(1, tw / 32));
9441
9474
  painter.restore();
9442
9475
  }
9476
+ function isAnimatedSprite(node) {
9477
+ return node.animation !== void 0 && typeof node.asset?.atlas === "string";
9478
+ }
9443
9479
  function DrawSprite(_props) {
9444
9480
  return null;
9445
9481
  }
@@ -9448,6 +9484,7 @@ var init_DrawSprite = __esm({
9448
9484
  "components/game/atoms/DrawSprite.tsx"() {
9449
9485
  "use client";
9450
9486
  init_atlasSlice();
9487
+ init_spriteAnimation();
9451
9488
  init_imageCache();
9452
9489
  init_contract();
9453
9490
  spriteLog = createLogger("almadar:ui:draw-sprite");
@@ -9461,6 +9498,23 @@ var init_DrawSprite = __esm({
9461
9498
  return;
9462
9499
  }
9463
9500
  let src = typeof node.frame === "object" ? node.frame : void 0;
9501
+ if (!src && node.animation !== void 0 && typeof node.asset.atlas === "string") {
9502
+ const atlas = getAtlas(node.asset.atlas, dctx.invalidate);
9503
+ if (!atlas) {
9504
+ if (atlasFailed(node.asset.atlas)) paintFallbackSquare(painter, node, dctx, "atlas-failed");
9505
+ return;
9506
+ }
9507
+ if (isSpriteSheetAtlas(atlas)) {
9508
+ const def = isAnimationName(node.animation) ? atlas.animations[node.animation] : void 0;
9509
+ if (!def) {
9510
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
9511
+ return;
9512
+ }
9513
+ const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, dctx.time);
9514
+ const r = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
9515
+ src = { x: r.sx, y: r.sy, w: r.sw, h: r.sh };
9516
+ }
9517
+ }
9464
9518
  if (!src && isAtlasAsset(node.asset)) {
9465
9519
  const atlas = getAtlas(node.asset.atlas, dctx.invalidate);
9466
9520
  if (!atlas) {
@@ -9752,6 +9806,83 @@ var init_DrawText = __esm({
9752
9806
  };
9753
9807
  }
9754
9808
  });
9809
+ function isAnimatedGroup(node) {
9810
+ return Boolean(node.animation && node.animation.durationMs > 0 && node.animation.keyframes.length > 0);
9811
+ }
9812
+ function DrawGroup(props) {
9813
+ const register = useContext(DrawableRegistryContext);
9814
+ if (register) register({ ...props, type: "draw-group" });
9815
+ return null;
9816
+ }
9817
+ var init_DrawGroup = __esm({
9818
+ "components/game/atoms/DrawGroup.tsx"() {
9819
+ "use client";
9820
+ init_registry();
9821
+ }
9822
+ });
9823
+ function applyMeshAnimation(node, timeMs) {
9824
+ const anim = node.animation;
9825
+ if (!anim || !(anim.durationMs > 0) || anim.keyframes.length === 0) return null;
9826
+ const frames = [...anim.keyframes].sort((a, b) => a.at - b.at);
9827
+ const cycle = timeMs / anim.durationMs;
9828
+ const t = anim.loop === false ? Math.min(cycle, 1) : cycle - Math.floor(cycle);
9829
+ const trackValue = (key) => {
9830
+ const defined = frames.filter((f3) => f3[key] !== void 0);
9831
+ if (defined.length === 0) return void 0;
9832
+ let prev;
9833
+ let next;
9834
+ for (const f3 of defined) {
9835
+ if (f3.at <= t) prev = f3;
9836
+ else if (!next) next = f3;
9837
+ }
9838
+ if (!prev) return defined[0][key];
9839
+ if (!next) return prev[key];
9840
+ const span = next.at - prev.at;
9841
+ const k = span > 0 ? (t - prev.at) / span : 1;
9842
+ const a = prev[key];
9843
+ const b = next[key];
9844
+ if (typeof a === "number" && typeof b === "number") return lerp2(a, b, k);
9845
+ return a;
9846
+ };
9847
+ const num = {};
9848
+ for (const key of NUMERIC_TRACKS2) {
9849
+ const v = trackValue(key);
9850
+ if (v !== void 0) num[key] = v;
9851
+ }
9852
+ return {
9853
+ offset: [num.offsetX ?? 0, num.offsetY ?? 0, num.offsetZ ?? 0],
9854
+ rotate: [num.rotateX ?? 0, num.rotateY ?? 0, num.rotateZ ?? 0],
9855
+ scale: num.scale ?? 1,
9856
+ opacity: num.opacity,
9857
+ emissiveIntensity: num.emissiveIntensity,
9858
+ color: trackValue("color"),
9859
+ emissive: trackValue("emissive")
9860
+ };
9861
+ }
9862
+ function DrawMesh(props) {
9863
+ const register = useContext(DrawableRegistryContext);
9864
+ if (register) register({ ...props, type: "draw-mesh" });
9865
+ return null;
9866
+ }
9867
+ var lerp2, NUMERIC_TRACKS2;
9868
+ var init_DrawMesh = __esm({
9869
+ "components/game/atoms/DrawMesh.tsx"() {
9870
+ "use client";
9871
+ init_registry();
9872
+ lerp2 = (a, b, k) => a + (b - a) * k;
9873
+ NUMERIC_TRACKS2 = [
9874
+ "offsetX",
9875
+ "offsetY",
9876
+ "offsetZ",
9877
+ "rotateX",
9878
+ "rotateY",
9879
+ "rotateZ",
9880
+ "scale",
9881
+ "opacity",
9882
+ "emissiveIntensity"
9883
+ ];
9884
+ }
9885
+ });
9755
9886
 
9756
9887
  // components/game/molecules/DrawSpriteLayer.tsx
9757
9888
  function DrawSpriteLayer(_props) {
@@ -9818,18 +9949,22 @@ function paintDrawable(painter, node, dctx) {
9818
9949
  if (!isValidScenePos(node.position)) break;
9819
9950
  if (!Array.isArray(node.items)) break;
9820
9951
  const p = dctx.projector.project(node.position);
9952
+ const anim = dctx.time > 0 && isAnimatedGroup(node) ? applyMeshAnimation(node, dctx.time) : null;
9953
+ const tw = dctx.projector.tileWidth;
9821
9954
  painter.save();
9822
- painter.translate(p.x, p.y);
9823
- if (node.scale !== void 0) painter.scale(node.scale, node.scale);
9824
- if (node.rotate !== void 0) painter.rotate(node.rotate);
9825
- if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
9955
+ painter.translate(p.x + (anim ? anim.offset[0] * tw : 0), p.y + (anim ? anim.offset[1] * tw : 0));
9956
+ const scale = (node.scale ?? 1) * (anim?.scale ?? 1);
9957
+ if (scale !== 1) painter.scale(scale, scale);
9958
+ const rotate = (node.rotate ?? 0) + (node.rotation?.[2] ?? 0) + (anim?.rotate[2] ?? 0);
9959
+ if (rotate !== 0) painter.rotate(rotate);
9960
+ const opacity = (node.opacity ?? 1) * (anim?.opacity ?? 1);
9961
+ if (opacity !== 1) painter.setAlpha(opacity);
9826
9962
  if (node.clip) {
9827
- const tw = dctx.projector.tileWidth;
9828
9963
  painter.scale(tw, tw);
9829
9964
  painter.clipPath(node.clip);
9830
9965
  painter.scale(1 / tw, 1 / tw);
9831
9966
  }
9832
- const childCtx = node.scale !== void 0 && node.scale !== 1 ? { ...dctx, groupScale: (dctx.groupScale ?? 1) * node.scale } : dctx;
9967
+ const childCtx = scale !== 1 ? { ...dctx, groupScale: (dctx.groupScale ?? 1) * scale } : dctx;
9833
9968
  for (const item of node.items) paintDrawable(painter, item, childCtx);
9834
9969
  painter.restore();
9835
9970
  break;
@@ -9855,6 +9990,8 @@ var init_paintDispatch = __esm({
9855
9990
  init_DrawSprite();
9856
9991
  init_DrawShape();
9857
9992
  init_DrawText();
9993
+ init_DrawGroup();
9994
+ init_DrawMesh();
9858
9995
  init_DrawSpriteLayer();
9859
9996
  init_DrawShapeLayer();
9860
9997
  init_DrawTextLayer();
@@ -10101,8 +10238,11 @@ function Canvas2D({
10101
10238
  const miniMapHeight = gridExtent.height || 10;
10102
10239
  const drawableIsAnimated = (node) => {
10103
10240
  if (node.type === "draw-shape") return isAnimatedShape(node);
10104
- if (node.type === "draw-group") return Array.isArray(node.items) && node.items.some(drawableIsAnimated);
10241
+ if (node.type === "draw-sprite") return isAnimatedSprite(node);
10242
+ if (node.type === "draw-group")
10243
+ return isAnimatedGroup(node) || Array.isArray(node.items) && node.items.some(drawableIsAnimated);
10105
10244
  if (node.type === "draw-shape-layer") return Array.isArray(node.items) && node.items.some(isAnimatedShape);
10245
+ if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some(isAnimatedSprite);
10106
10246
  return false;
10107
10247
  };
10108
10248
  const animRafRef = useRef(0);
@@ -10443,6 +10583,8 @@ var init_Canvas2D = __esm({
10443
10583
  init_projector();
10444
10584
  init_paintDispatch();
10445
10585
  init_DrawShape();
10586
+ init_DrawSprite();
10587
+ init_DrawGroup();
10446
10588
  init_registry();
10447
10589
  init_hitTest();
10448
10590
  init_isometric();
@@ -10515,6 +10657,7 @@ function Canvas({
10515
10657
  cameraMode: to3DCameraMode(camera?.mode),
10516
10658
  ...zoom !== void 0 ? { scale: zoom } : {},
10517
10659
  ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
10660
+ ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
10518
10661
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
10519
10662
  unitScale,
10520
10663
  backgroundColor,
@@ -22480,6 +22623,14 @@ var init_DashboardLayout = __esm({
22480
22623
  NavLinkBottom.displayName = "NavLinkBottom";
22481
22624
  }
22482
22625
  });
22626
+ function downloadItemUrl(url, label) {
22627
+ const a = document.createElement("a");
22628
+ a.href = url;
22629
+ a.download = url.startsWith("data:") ? label : url.split("/").pop() ?? "download";
22630
+ document.body.appendChild(a);
22631
+ a.click();
22632
+ a.remove();
22633
+ }
22483
22634
  function computeMenuStyle(position, triggerRect) {
22484
22635
  const isTop = position.startsWith("top");
22485
22636
  const isRight = position.endsWith("right") || position.endsWith("end");
@@ -22532,6 +22683,7 @@ function SubMenu({
22532
22683
  onClick: () => {
22533
22684
  if (item.disabled) return;
22534
22685
  if (item.event) eventBus.emit(`UI:${item.event}`, { itemId, label: item.label });
22686
+ if (item.url) downloadItemUrl(item.url, item.label);
22535
22687
  item.onClick?.();
22536
22688
  },
22537
22689
  "aria-disabled": item.disabled || void 0,
@@ -22685,6 +22837,7 @@ var init_Menu = __esm({
22685
22837
  setActiveSubMenu(itemId);
22686
22838
  } else {
22687
22839
  if (item.event) eventBus.emit(`UI:${item.event}`, { itemId, label: item.label });
22840
+ if (item.url) downloadItemUrl(item.url, item.label);
22688
22841
  item.onClick?.();
22689
22842
  setIsOpen(false);
22690
22843
  }
@@ -22720,12 +22873,19 @@ var init_Menu = __esm({
22720
22873
  "bottom-end": "bottom-start"
22721
22874
  };
22722
22875
  const effectivePosition = direction === "rtl" ? rtlMirror[position] ?? position : position;
22723
- const triggerChild = React85__default.isValidElement(trigger) ? trigger : /* @__PURE__ */ jsx(Typography, { variant: "small", as: "span", children: trigger });
22724
- const triggerElement = React85__default.cloneElement(
22725
- triggerChild,
22876
+ const triggerElement = React85__default.isValidElement(trigger) ? React85__default.cloneElement(trigger, {
22877
+ ref: triggerRef,
22878
+ onClick: handleToggle
22879
+ }) : /* @__PURE__ */ jsx(
22880
+ Box,
22726
22881
  {
22727
- ref: triggerRef,
22728
- onClick: handleToggle
22882
+ as: "span",
22883
+ ref: (el) => {
22884
+ triggerRef.current = el;
22885
+ },
22886
+ onClick: handleToggle,
22887
+ className: "inline-flex",
22888
+ children: typeof trigger === "string" || typeof trigger === "number" ? /* @__PURE__ */ jsx(Typography, { variant: "small", as: "span", children: trigger }) : trigger
22729
22889
  }
22730
22890
  );
22731
22891
  const renderMenuItems = (menuItems) => menuItems.map((item, index) => {
@@ -38345,28 +38505,6 @@ var init_DetailPanel = __esm({
38345
38505
  DetailPanel.displayName = "DetailPanel";
38346
38506
  }
38347
38507
  });
38348
- function DrawGroup(props) {
38349
- const register = useContext(DrawableRegistryContext);
38350
- if (register) register({ ...props, type: "draw-group" });
38351
- return null;
38352
- }
38353
- var init_DrawGroup = __esm({
38354
- "components/game/atoms/DrawGroup.tsx"() {
38355
- "use client";
38356
- init_registry();
38357
- }
38358
- });
38359
- function DrawMesh(props) {
38360
- const register = useContext(DrawableRegistryContext);
38361
- if (register) register({ ...props, type: "draw-mesh" });
38362
- return null;
38363
- }
38364
- var init_DrawMesh = __esm({
38365
- "components/game/atoms/DrawMesh.tsx"() {
38366
- "use client";
38367
- init_registry();
38368
- }
38369
- });
38370
38508
  function extractTitle(children) {
38371
38509
  if (!React85__default.isValidElement(children)) return void 0;
38372
38510
  const props = children.props;