@almadar/ui 5.136.0 → 5.138.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.
@@ -12578,19 +12578,12 @@ var init_useCanvasGestures = __esm({
12578
12578
  });
12579
12579
 
12580
12580
  // lib/imageCache.ts
12581
- function getOrLoadImage(url, onReady) {
12582
- if (!url || typeof window === "undefined") return null;
12583
- const existing = cache.get(url);
12584
- if (existing) {
12585
- if (existing.status === "loaded") return existing.img;
12586
- if (existing.status === "pending" && onReady) existing.onReady = onReady;
12587
- return null;
12588
- }
12581
+ function startLoad(url, onReady, existing) {
12589
12582
  const img = new Image();
12590
12583
  img.crossOrigin = "anonymous";
12591
- const entry = { img, status: "pending", onReady };
12592
- cache.set(url, entry);
12593
- updateAssetStatus(url, "pending");
12584
+ const entry = existing ?? { img, status: "pending" };
12585
+ entry.img = img;
12586
+ if (onReady) entry.onReady = onReady;
12594
12587
  img.onload = () => {
12595
12588
  entry.status = "loaded";
12596
12589
  updateAssetStatus(url, "loaded");
@@ -12598,20 +12591,39 @@ function getOrLoadImage(url, onReady) {
12598
12591
  };
12599
12592
  img.onerror = () => {
12600
12593
  entry.status = "failed";
12594
+ entry.failedAt = Date.now();
12601
12595
  updateAssetStatus(url, "failed");
12602
12596
  entry.onReady?.();
12603
12597
  };
12604
12598
  img.src = url;
12599
+ return entry;
12600
+ }
12601
+ function getOrLoadImage(url, onReady) {
12602
+ if (!url || typeof window === "undefined") return null;
12603
+ const existing = cache.get(url);
12604
+ if (existing) {
12605
+ if (existing.status === "loaded") return existing.img;
12606
+ if (existing.status === "pending" && onReady) existing.onReady = onReady;
12607
+ if (existing.status === "failed" && Date.now() - (existing.failedAt ?? 0) >= RETRY_AFTER_MS) {
12608
+ existing.failedAt = Date.now();
12609
+ startLoad(url, onReady, existing);
12610
+ }
12611
+ return null;
12612
+ }
12613
+ const entry = startLoad(url, onReady);
12614
+ cache.set(url, entry);
12615
+ updateAssetStatus(url, "pending");
12605
12616
  return null;
12606
12617
  }
12607
12618
  function getImageStatus(url) {
12608
12619
  return cache.get(url)?.status;
12609
12620
  }
12610
- var cache;
12621
+ var RETRY_AFTER_MS, cache;
12611
12622
  var init_imageCache = __esm({
12612
12623
  "lib/imageCache.ts"() {
12613
12624
  "use client";
12614
12625
  init_verificationRegistry();
12626
+ RETRY_AFTER_MS = 5e3;
12615
12627
  cache = /* @__PURE__ */ new Map();
12616
12628
  }
12617
12629
  });
@@ -13207,6 +13219,11 @@ function Canvas2D({
13207
13219
  }
13208
13220
  return { width: maxX + 1, height: maxY + 1 };
13209
13221
  }, [scenePositions]);
13222
+ const defaultGridFocus = React92.useMemo(() => {
13223
+ if (isFree || projection === "side") return void 0;
13224
+ if (gridExtent.width < 2 || gridExtent.height < 2) return void 0;
13225
+ return { x: (gridExtent.width - 1) / 2, y: (gridExtent.height - 1) / 2 };
13226
+ }, [isFree, projection, gridExtent]);
13210
13227
  const baseOffsetX = React92.useMemo(() => {
13211
13228
  if (isFree || projection === "flat" || projection === "side") return 0;
13212
13229
  return (gridExtent.height - 1) * (scaledTileWidth / 2);
@@ -13311,10 +13328,13 @@ function Canvas2D({
13311
13328
  }
13312
13329
  if (!drawables || drawables.length === 0) return;
13313
13330
  const cam = cameraRef.current;
13314
- if (cameraPos && dragDistance() === 0) {
13315
- const p = projector.anchorPoint(cameraPos, "center");
13316
- cam.x = p.x - viewportSize.width / 2;
13317
- cam.y = p.y - viewportSize.height / 2;
13331
+ if (camera !== "follow" && dragDistance() === 0) {
13332
+ const focus = cameraPos ?? defaultGridFocus;
13333
+ if (focus) {
13334
+ const p = projector.anchorPoint(focus, "center");
13335
+ cam.x = p.x - viewportSize.width / 2;
13336
+ cam.y = p.y - viewportSize.height / 2;
13337
+ }
13318
13338
  }
13319
13339
  const containerRect = containerRef.current?.getBoundingClientRect();
13320
13340
  const canvasRect = canvas.getBoundingClientRect();
@@ -13327,7 +13347,7 @@ function Canvas2D({
13327
13347
  const dctx = { projector, time: 0, invalidate: bumpAtlas };
13328
13348
  for (const node of drawables) paintDrawable(painter, node, dctx);
13329
13349
  painter.restore();
13330
- }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage]);
13350
+ }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
13331
13351
  React92.useEffect(() => {
13332
13352
  if (camera !== "follow" || !followTarget) return;
13333
13353
  const p = projector.anchorPoint(followTarget, "center");
package/dist/avl/index.js CHANGED
@@ -12502,19 +12502,12 @@ var init_useCanvasGestures = __esm({
12502
12502
  });
12503
12503
 
12504
12504
  // lib/imageCache.ts
12505
- function getOrLoadImage(url, onReady) {
12506
- if (!url || typeof window === "undefined") return null;
12507
- const existing = cache.get(url);
12508
- if (existing) {
12509
- if (existing.status === "loaded") return existing.img;
12510
- if (existing.status === "pending" && onReady) existing.onReady = onReady;
12511
- return null;
12512
- }
12505
+ function startLoad(url, onReady, existing) {
12513
12506
  const img = new Image();
12514
12507
  img.crossOrigin = "anonymous";
12515
- const entry = { img, status: "pending", onReady };
12516
- cache.set(url, entry);
12517
- updateAssetStatus(url, "pending");
12508
+ const entry = existing ?? { img, status: "pending" };
12509
+ entry.img = img;
12510
+ if (onReady) entry.onReady = onReady;
12518
12511
  img.onload = () => {
12519
12512
  entry.status = "loaded";
12520
12513
  updateAssetStatus(url, "loaded");
@@ -12522,20 +12515,39 @@ function getOrLoadImage(url, onReady) {
12522
12515
  };
12523
12516
  img.onerror = () => {
12524
12517
  entry.status = "failed";
12518
+ entry.failedAt = Date.now();
12525
12519
  updateAssetStatus(url, "failed");
12526
12520
  entry.onReady?.();
12527
12521
  };
12528
12522
  img.src = url;
12523
+ return entry;
12524
+ }
12525
+ function getOrLoadImage(url, onReady) {
12526
+ if (!url || typeof window === "undefined") return null;
12527
+ const existing = cache.get(url);
12528
+ if (existing) {
12529
+ if (existing.status === "loaded") return existing.img;
12530
+ if (existing.status === "pending" && onReady) existing.onReady = onReady;
12531
+ if (existing.status === "failed" && Date.now() - (existing.failedAt ?? 0) >= RETRY_AFTER_MS) {
12532
+ existing.failedAt = Date.now();
12533
+ startLoad(url, onReady, existing);
12534
+ }
12535
+ return null;
12536
+ }
12537
+ const entry = startLoad(url, onReady);
12538
+ cache.set(url, entry);
12539
+ updateAssetStatus(url, "pending");
12529
12540
  return null;
12530
12541
  }
12531
12542
  function getImageStatus(url) {
12532
12543
  return cache.get(url)?.status;
12533
12544
  }
12534
- var cache;
12545
+ var RETRY_AFTER_MS, cache;
12535
12546
  var init_imageCache = __esm({
12536
12547
  "lib/imageCache.ts"() {
12537
12548
  "use client";
12538
12549
  init_verificationRegistry();
12550
+ RETRY_AFTER_MS = 5e3;
12539
12551
  cache = /* @__PURE__ */ new Map();
12540
12552
  }
12541
12553
  });
@@ -13131,6 +13143,11 @@ function Canvas2D({
13131
13143
  }
13132
13144
  return { width: maxX + 1, height: maxY + 1 };
13133
13145
  }, [scenePositions]);
13146
+ const defaultGridFocus = useMemo(() => {
13147
+ if (isFree || projection === "side") return void 0;
13148
+ if (gridExtent.width < 2 || gridExtent.height < 2) return void 0;
13149
+ return { x: (gridExtent.width - 1) / 2, y: (gridExtent.height - 1) / 2 };
13150
+ }, [isFree, projection, gridExtent]);
13134
13151
  const baseOffsetX = useMemo(() => {
13135
13152
  if (isFree || projection === "flat" || projection === "side") return 0;
13136
13153
  return (gridExtent.height - 1) * (scaledTileWidth / 2);
@@ -13235,10 +13252,13 @@ function Canvas2D({
13235
13252
  }
13236
13253
  if (!drawables || drawables.length === 0) return;
13237
13254
  const cam = cameraRef.current;
13238
- if (cameraPos && dragDistance() === 0) {
13239
- const p = projector.anchorPoint(cameraPos, "center");
13240
- cam.x = p.x - viewportSize.width / 2;
13241
- cam.y = p.y - viewportSize.height / 2;
13255
+ if (camera !== "follow" && dragDistance() === 0) {
13256
+ const focus = cameraPos ?? defaultGridFocus;
13257
+ if (focus) {
13258
+ const p = projector.anchorPoint(focus, "center");
13259
+ cam.x = p.x - viewportSize.width / 2;
13260
+ cam.y = p.y - viewportSize.height / 2;
13261
+ }
13242
13262
  }
13243
13263
  const containerRect = containerRef.current?.getBoundingClientRect();
13244
13264
  const canvasRect = canvas.getBoundingClientRect();
@@ -13251,7 +13271,7 @@ function Canvas2D({
13251
13271
  const dctx = { projector, time: 0, invalidate: bumpAtlas };
13252
13272
  for (const node of drawables) paintDrawable(painter, node, dctx);
13253
13273
  painter.restore();
13254
- }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage]);
13274
+ }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
13255
13275
  useEffect(() => {
13256
13276
  if (camera !== "follow" || !followTarget) return;
13257
13277
  const p = projector.anchorPoint(followTarget, "center");
@@ -15646,19 +15646,12 @@ var init_useCanvasGestures = __esm({
15646
15646
  });
15647
15647
 
15648
15648
  // lib/imageCache.ts
15649
- function getOrLoadImage(url, onReady) {
15650
- if (!url || typeof window === "undefined") return null;
15651
- const existing = cache.get(url);
15652
- if (existing) {
15653
- if (existing.status === "loaded") return existing.img;
15654
- if (existing.status === "pending" && onReady) existing.onReady = onReady;
15655
- return null;
15656
- }
15649
+ function startLoad(url, onReady, existing) {
15657
15650
  const img = new Image();
15658
15651
  img.crossOrigin = "anonymous";
15659
- const entry = { img, status: "pending", onReady };
15660
- cache.set(url, entry);
15661
- updateAssetStatus(url, "pending");
15652
+ const entry = existing ?? { img, status: "pending" };
15653
+ entry.img = img;
15654
+ if (onReady) entry.onReady = onReady;
15662
15655
  img.onload = () => {
15663
15656
  entry.status = "loaded";
15664
15657
  updateAssetStatus(url, "loaded");
@@ -15666,20 +15659,39 @@ function getOrLoadImage(url, onReady) {
15666
15659
  };
15667
15660
  img.onerror = () => {
15668
15661
  entry.status = "failed";
15662
+ entry.failedAt = Date.now();
15669
15663
  updateAssetStatus(url, "failed");
15670
15664
  entry.onReady?.();
15671
15665
  };
15672
15666
  img.src = url;
15667
+ return entry;
15668
+ }
15669
+ function getOrLoadImage(url, onReady) {
15670
+ if (!url || typeof window === "undefined") return null;
15671
+ const existing = cache.get(url);
15672
+ if (existing) {
15673
+ if (existing.status === "loaded") return existing.img;
15674
+ if (existing.status === "pending" && onReady) existing.onReady = onReady;
15675
+ if (existing.status === "failed" && Date.now() - (existing.failedAt ?? 0) >= RETRY_AFTER_MS) {
15676
+ existing.failedAt = Date.now();
15677
+ startLoad(url, onReady, existing);
15678
+ }
15679
+ return null;
15680
+ }
15681
+ const entry = startLoad(url, onReady);
15682
+ cache.set(url, entry);
15683
+ updateAssetStatus(url, "pending");
15673
15684
  return null;
15674
15685
  }
15675
15686
  function getImageStatus(url) {
15676
15687
  return cache.get(url)?.status;
15677
15688
  }
15678
- var cache;
15689
+ var RETRY_AFTER_MS, cache;
15679
15690
  var init_imageCache = __esm({
15680
15691
  "lib/imageCache.ts"() {
15681
15692
  "use client";
15682
15693
  init_verificationRegistry();
15694
+ RETRY_AFTER_MS = 5e3;
15683
15695
  cache = /* @__PURE__ */ new Map();
15684
15696
  }
15685
15697
  });
@@ -16337,6 +16349,11 @@ function Canvas2D({
16337
16349
  }
16338
16350
  return { width: maxX + 1, height: maxY + 1 };
16339
16351
  }, [scenePositions]);
16352
+ const defaultGridFocus = React75.useMemo(() => {
16353
+ if (isFree || projection === "side") return void 0;
16354
+ if (gridExtent.width < 2 || gridExtent.height < 2) return void 0;
16355
+ return { x: (gridExtent.width - 1) / 2, y: (gridExtent.height - 1) / 2 };
16356
+ }, [isFree, projection, gridExtent]);
16340
16357
  const baseOffsetX = React75.useMemo(() => {
16341
16358
  if (isFree || projection === "flat" || projection === "side") return 0;
16342
16359
  return (gridExtent.height - 1) * (scaledTileWidth / 2);
@@ -16441,10 +16458,13 @@ function Canvas2D({
16441
16458
  }
16442
16459
  if (!drawables || drawables.length === 0) return;
16443
16460
  const cam = cameraRef.current;
16444
- if (cameraPos && dragDistance() === 0) {
16445
- const p = projector.anchorPoint(cameraPos, "center");
16446
- cam.x = p.x - viewportSize.width / 2;
16447
- cam.y = p.y - viewportSize.height / 2;
16461
+ if (camera !== "follow" && dragDistance() === 0) {
16462
+ const focus = cameraPos ?? defaultGridFocus;
16463
+ if (focus) {
16464
+ const p = projector.anchorPoint(focus, "center");
16465
+ cam.x = p.x - viewportSize.width / 2;
16466
+ cam.y = p.y - viewportSize.height / 2;
16467
+ }
16448
16468
  }
16449
16469
  const containerRect = containerRef.current?.getBoundingClientRect();
16450
16470
  const canvasRect = canvas.getBoundingClientRect();
@@ -16457,7 +16477,7 @@ function Canvas2D({
16457
16477
  const dctx = { projector, time: 0, invalidate: bumpAtlas };
16458
16478
  for (const node of drawables) paintDrawable(painter, node, dctx);
16459
16479
  painter.restore();
16460
- }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage]);
16480
+ }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
16461
16481
  React75.useEffect(() => {
16462
16482
  if (camera !== "follow" || !followTarget) return;
16463
16483
  const p = projector.anchorPoint(followTarget, "center");
@@ -15571,19 +15571,12 @@ var init_useCanvasGestures = __esm({
15571
15571
  });
15572
15572
 
15573
15573
  // lib/imageCache.ts
15574
- function getOrLoadImage(url, onReady) {
15575
- if (!url || typeof window === "undefined") return null;
15576
- const existing = cache.get(url);
15577
- if (existing) {
15578
- if (existing.status === "loaded") return existing.img;
15579
- if (existing.status === "pending" && onReady) existing.onReady = onReady;
15580
- return null;
15581
- }
15574
+ function startLoad(url, onReady, existing) {
15582
15575
  const img = new Image();
15583
15576
  img.crossOrigin = "anonymous";
15584
- const entry = { img, status: "pending", onReady };
15585
- cache.set(url, entry);
15586
- updateAssetStatus(url, "pending");
15577
+ const entry = existing ?? { img, status: "pending" };
15578
+ entry.img = img;
15579
+ if (onReady) entry.onReady = onReady;
15587
15580
  img.onload = () => {
15588
15581
  entry.status = "loaded";
15589
15582
  updateAssetStatus(url, "loaded");
@@ -15591,20 +15584,39 @@ function getOrLoadImage(url, onReady) {
15591
15584
  };
15592
15585
  img.onerror = () => {
15593
15586
  entry.status = "failed";
15587
+ entry.failedAt = Date.now();
15594
15588
  updateAssetStatus(url, "failed");
15595
15589
  entry.onReady?.();
15596
15590
  };
15597
15591
  img.src = url;
15592
+ return entry;
15593
+ }
15594
+ function getOrLoadImage(url, onReady) {
15595
+ if (!url || typeof window === "undefined") return null;
15596
+ const existing = cache.get(url);
15597
+ if (existing) {
15598
+ if (existing.status === "loaded") return existing.img;
15599
+ if (existing.status === "pending" && onReady) existing.onReady = onReady;
15600
+ if (existing.status === "failed" && Date.now() - (existing.failedAt ?? 0) >= RETRY_AFTER_MS) {
15601
+ existing.failedAt = Date.now();
15602
+ startLoad(url, onReady, existing);
15603
+ }
15604
+ return null;
15605
+ }
15606
+ const entry = startLoad(url, onReady);
15607
+ cache.set(url, entry);
15608
+ updateAssetStatus(url, "pending");
15598
15609
  return null;
15599
15610
  }
15600
15611
  function getImageStatus(url) {
15601
15612
  return cache.get(url)?.status;
15602
15613
  }
15603
- var cache;
15614
+ var RETRY_AFTER_MS, cache;
15604
15615
  var init_imageCache = __esm({
15605
15616
  "lib/imageCache.ts"() {
15606
15617
  "use client";
15607
15618
  init_verificationRegistry();
15619
+ RETRY_AFTER_MS = 5e3;
15608
15620
  cache = /* @__PURE__ */ new Map();
15609
15621
  }
15610
15622
  });
@@ -16262,6 +16274,11 @@ function Canvas2D({
16262
16274
  }
16263
16275
  return { width: maxX + 1, height: maxY + 1 };
16264
16276
  }, [scenePositions]);
16277
+ const defaultGridFocus = useMemo(() => {
16278
+ if (isFree || projection === "side") return void 0;
16279
+ if (gridExtent.width < 2 || gridExtent.height < 2) return void 0;
16280
+ return { x: (gridExtent.width - 1) / 2, y: (gridExtent.height - 1) / 2 };
16281
+ }, [isFree, projection, gridExtent]);
16265
16282
  const baseOffsetX = useMemo(() => {
16266
16283
  if (isFree || projection === "flat" || projection === "side") return 0;
16267
16284
  return (gridExtent.height - 1) * (scaledTileWidth / 2);
@@ -16366,10 +16383,13 @@ function Canvas2D({
16366
16383
  }
16367
16384
  if (!drawables || drawables.length === 0) return;
16368
16385
  const cam = cameraRef.current;
16369
- if (cameraPos && dragDistance() === 0) {
16370
- const p = projector.anchorPoint(cameraPos, "center");
16371
- cam.x = p.x - viewportSize.width / 2;
16372
- cam.y = p.y - viewportSize.height / 2;
16386
+ if (camera !== "follow" && dragDistance() === 0) {
16387
+ const focus = cameraPos ?? defaultGridFocus;
16388
+ if (focus) {
16389
+ const p = projector.anchorPoint(focus, "center");
16390
+ cam.x = p.x - viewportSize.width / 2;
16391
+ cam.y = p.y - viewportSize.height / 2;
16392
+ }
16373
16393
  }
16374
16394
  const containerRect = containerRef.current?.getBoundingClientRect();
16375
16395
  const canvasRect = canvas.getBoundingClientRect();
@@ -16382,7 +16402,7 @@ function Canvas2D({
16382
16402
  const dctx = { projector, time: 0, invalidate: bumpAtlas };
16383
16403
  for (const node of drawables) paintDrawable(painter, node, dctx);
16384
16404
  painter.restore();
16385
- }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage]);
16405
+ }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
16386
16406
  useEffect(() => {
16387
16407
  if (camera !== "follow" || !followTarget) return;
16388
16408
  const p = projector.anchorPoint(followTarget, "center");
@@ -8836,19 +8836,12 @@ var init_useCanvasGestures = __esm({
8836
8836
  });
8837
8837
 
8838
8838
  // lib/imageCache.ts
8839
- function getOrLoadImage(url, onReady) {
8840
- if (!url || typeof window === "undefined") return null;
8841
- const existing = cache.get(url);
8842
- if (existing) {
8843
- if (existing.status === "loaded") return existing.img;
8844
- if (existing.status === "pending" && onReady) existing.onReady = onReady;
8845
- return null;
8846
- }
8839
+ function startLoad(url, onReady, existing) {
8847
8840
  const img = new Image();
8848
8841
  img.crossOrigin = "anonymous";
8849
- const entry = { img, status: "pending", onReady };
8850
- cache.set(url, entry);
8851
- updateAssetStatus(url, "pending");
8842
+ const entry = existing ?? { img, status: "pending" };
8843
+ entry.img = img;
8844
+ if (onReady) entry.onReady = onReady;
8852
8845
  img.onload = () => {
8853
8846
  entry.status = "loaded";
8854
8847
  updateAssetStatus(url, "loaded");
@@ -8856,20 +8849,39 @@ function getOrLoadImage(url, onReady) {
8856
8849
  };
8857
8850
  img.onerror = () => {
8858
8851
  entry.status = "failed";
8852
+ entry.failedAt = Date.now();
8859
8853
  updateAssetStatus(url, "failed");
8860
8854
  entry.onReady?.();
8861
8855
  };
8862
8856
  img.src = url;
8857
+ return entry;
8858
+ }
8859
+ function getOrLoadImage(url, onReady) {
8860
+ if (!url || typeof window === "undefined") return null;
8861
+ const existing = cache.get(url);
8862
+ if (existing) {
8863
+ if (existing.status === "loaded") return existing.img;
8864
+ if (existing.status === "pending" && onReady) existing.onReady = onReady;
8865
+ if (existing.status === "failed" && Date.now() - (existing.failedAt ?? 0) >= RETRY_AFTER_MS) {
8866
+ existing.failedAt = Date.now();
8867
+ startLoad(url, onReady, existing);
8868
+ }
8869
+ return null;
8870
+ }
8871
+ const entry = startLoad(url, onReady);
8872
+ cache.set(url, entry);
8873
+ updateAssetStatus(url, "pending");
8863
8874
  return null;
8864
8875
  }
8865
8876
  function getImageStatus(url) {
8866
8877
  return cache.get(url)?.status;
8867
8878
  }
8868
- var cache;
8879
+ var RETRY_AFTER_MS, cache;
8869
8880
  var init_imageCache = __esm({
8870
8881
  "lib/imageCache.ts"() {
8871
8882
  "use client";
8872
8883
  init_verificationRegistry();
8884
+ RETRY_AFTER_MS = 5e3;
8873
8885
  cache = /* @__PURE__ */ new Map();
8874
8886
  }
8875
8887
  });
@@ -9465,6 +9477,11 @@ function Canvas2D({
9465
9477
  }
9466
9478
  return { width: maxX + 1, height: maxY + 1 };
9467
9479
  }, [scenePositions]);
9480
+ const defaultGridFocus = React85.useMemo(() => {
9481
+ if (isFree || projection === "side") return void 0;
9482
+ if (gridExtent.width < 2 || gridExtent.height < 2) return void 0;
9483
+ return { x: (gridExtent.width - 1) / 2, y: (gridExtent.height - 1) / 2 };
9484
+ }, [isFree, projection, gridExtent]);
9468
9485
  const baseOffsetX = React85.useMemo(() => {
9469
9486
  if (isFree || projection === "flat" || projection === "side") return 0;
9470
9487
  return (gridExtent.height - 1) * (scaledTileWidth / 2);
@@ -9569,10 +9586,13 @@ function Canvas2D({
9569
9586
  }
9570
9587
  if (!drawables || drawables.length === 0) return;
9571
9588
  const cam = cameraRef.current;
9572
- if (cameraPos && dragDistance() === 0) {
9573
- const p = projector.anchorPoint(cameraPos, "center");
9574
- cam.x = p.x - viewportSize.width / 2;
9575
- cam.y = p.y - viewportSize.height / 2;
9589
+ if (camera !== "follow" && dragDistance() === 0) {
9590
+ const focus = cameraPos ?? defaultGridFocus;
9591
+ if (focus) {
9592
+ const p = projector.anchorPoint(focus, "center");
9593
+ cam.x = p.x - viewportSize.width / 2;
9594
+ cam.y = p.y - viewportSize.height / 2;
9595
+ }
9576
9596
  }
9577
9597
  const containerRect = containerRef.current?.getBoundingClientRect();
9578
9598
  const canvasRect = canvas.getBoundingClientRect();
@@ -9585,7 +9605,7 @@ function Canvas2D({
9585
9605
  const dctx = { projector, time: 0, invalidate: bumpAtlas };
9586
9606
  for (const node of drawables) paintDrawable(painter, node, dctx);
9587
9607
  painter.restore();
9588
- }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage]);
9608
+ }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
9589
9609
  React85.useEffect(() => {
9590
9610
  if (camera !== "follow" || !followTarget) return;
9591
9611
  const p = projector.anchorPoint(followTarget, "center");
@@ -8762,19 +8762,12 @@ var init_useCanvasGestures = __esm({
8762
8762
  });
8763
8763
 
8764
8764
  // lib/imageCache.ts
8765
- function getOrLoadImage(url, onReady) {
8766
- if (!url || typeof window === "undefined") return null;
8767
- const existing = cache.get(url);
8768
- if (existing) {
8769
- if (existing.status === "loaded") return existing.img;
8770
- if (existing.status === "pending" && onReady) existing.onReady = onReady;
8771
- return null;
8772
- }
8765
+ function startLoad(url, onReady, existing) {
8773
8766
  const img = new Image();
8774
8767
  img.crossOrigin = "anonymous";
8775
- const entry = { img, status: "pending", onReady };
8776
- cache.set(url, entry);
8777
- updateAssetStatus(url, "pending");
8768
+ const entry = existing ?? { img, status: "pending" };
8769
+ entry.img = img;
8770
+ if (onReady) entry.onReady = onReady;
8778
8771
  img.onload = () => {
8779
8772
  entry.status = "loaded";
8780
8773
  updateAssetStatus(url, "loaded");
@@ -8782,20 +8775,39 @@ function getOrLoadImage(url, onReady) {
8782
8775
  };
8783
8776
  img.onerror = () => {
8784
8777
  entry.status = "failed";
8778
+ entry.failedAt = Date.now();
8785
8779
  updateAssetStatus(url, "failed");
8786
8780
  entry.onReady?.();
8787
8781
  };
8788
8782
  img.src = url;
8783
+ return entry;
8784
+ }
8785
+ function getOrLoadImage(url, onReady) {
8786
+ if (!url || typeof window === "undefined") return null;
8787
+ const existing = cache.get(url);
8788
+ if (existing) {
8789
+ if (existing.status === "loaded") return existing.img;
8790
+ if (existing.status === "pending" && onReady) existing.onReady = onReady;
8791
+ if (existing.status === "failed" && Date.now() - (existing.failedAt ?? 0) >= RETRY_AFTER_MS) {
8792
+ existing.failedAt = Date.now();
8793
+ startLoad(url, onReady, existing);
8794
+ }
8795
+ return null;
8796
+ }
8797
+ const entry = startLoad(url, onReady);
8798
+ cache.set(url, entry);
8799
+ updateAssetStatus(url, "pending");
8789
8800
  return null;
8790
8801
  }
8791
8802
  function getImageStatus(url) {
8792
8803
  return cache.get(url)?.status;
8793
8804
  }
8794
- var cache;
8805
+ var RETRY_AFTER_MS, cache;
8795
8806
  var init_imageCache = __esm({
8796
8807
  "lib/imageCache.ts"() {
8797
8808
  "use client";
8798
8809
  init_verificationRegistry();
8810
+ RETRY_AFTER_MS = 5e3;
8799
8811
  cache = /* @__PURE__ */ new Map();
8800
8812
  }
8801
8813
  });
@@ -9391,6 +9403,11 @@ function Canvas2D({
9391
9403
  }
9392
9404
  return { width: maxX + 1, height: maxY + 1 };
9393
9405
  }, [scenePositions]);
9406
+ const defaultGridFocus = useMemo(() => {
9407
+ if (isFree || projection === "side") return void 0;
9408
+ if (gridExtent.width < 2 || gridExtent.height < 2) return void 0;
9409
+ return { x: (gridExtent.width - 1) / 2, y: (gridExtent.height - 1) / 2 };
9410
+ }, [isFree, projection, gridExtent]);
9394
9411
  const baseOffsetX = useMemo(() => {
9395
9412
  if (isFree || projection === "flat" || projection === "side") return 0;
9396
9413
  return (gridExtent.height - 1) * (scaledTileWidth / 2);
@@ -9495,10 +9512,13 @@ function Canvas2D({
9495
9512
  }
9496
9513
  if (!drawables || drawables.length === 0) return;
9497
9514
  const cam = cameraRef.current;
9498
- if (cameraPos && dragDistance() === 0) {
9499
- const p = projector.anchorPoint(cameraPos, "center");
9500
- cam.x = p.x - viewportSize.width / 2;
9501
- cam.y = p.y - viewportSize.height / 2;
9515
+ if (camera !== "follow" && dragDistance() === 0) {
9516
+ const focus = cameraPos ?? defaultGridFocus;
9517
+ if (focus) {
9518
+ const p = projector.anchorPoint(focus, "center");
9519
+ cam.x = p.x - viewportSize.width / 2;
9520
+ cam.y = p.y - viewportSize.height / 2;
9521
+ }
9502
9522
  }
9503
9523
  const containerRect = containerRef.current?.getBoundingClientRect();
9504
9524
  const canvasRect = canvas.getBoundingClientRect();
@@ -9511,7 +9531,7 @@ function Canvas2D({
9511
9531
  const dctx = { projector, time: 0, invalidate: bumpAtlas };
9512
9532
  for (const node of drawables) paintDrawable(painter, node, dctx);
9513
9533
  painter.restore();
9514
- }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage]);
9534
+ }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
9515
9535
  useEffect(() => {
9516
9536
  if (camera !== "follow" || !followTarget) return;
9517
9537
  const p = projector.anchorPoint(followTarget, "center");
@@ -9190,19 +9190,12 @@ var init_useCamera = __esm({
9190
9190
  });
9191
9191
 
9192
9192
  // lib/imageCache.ts
9193
- function getOrLoadImage(url, onReady) {
9194
- if (!url || typeof window === "undefined") return null;
9195
- const existing = cache.get(url);
9196
- if (existing) {
9197
- if (existing.status === "loaded") return existing.img;
9198
- if (existing.status === "pending" && onReady) existing.onReady = onReady;
9199
- return null;
9200
- }
9193
+ function startLoad(url, onReady, existing) {
9201
9194
  const img = new Image();
9202
9195
  img.crossOrigin = "anonymous";
9203
- const entry = { img, status: "pending", onReady };
9204
- cache.set(url, entry);
9205
- updateAssetStatus(url, "pending");
9196
+ const entry = existing ?? { img, status: "pending" };
9197
+ entry.img = img;
9198
+ if (onReady) entry.onReady = onReady;
9206
9199
  img.onload = () => {
9207
9200
  entry.status = "loaded";
9208
9201
  updateAssetStatus(url, "loaded");
@@ -9210,20 +9203,39 @@ function getOrLoadImage(url, onReady) {
9210
9203
  };
9211
9204
  img.onerror = () => {
9212
9205
  entry.status = "failed";
9206
+ entry.failedAt = Date.now();
9213
9207
  updateAssetStatus(url, "failed");
9214
9208
  entry.onReady?.();
9215
9209
  };
9216
9210
  img.src = url;
9211
+ return entry;
9212
+ }
9213
+ function getOrLoadImage(url, onReady) {
9214
+ if (!url || typeof window === "undefined") return null;
9215
+ const existing = cache.get(url);
9216
+ if (existing) {
9217
+ if (existing.status === "loaded") return existing.img;
9218
+ if (existing.status === "pending" && onReady) existing.onReady = onReady;
9219
+ if (existing.status === "failed" && Date.now() - (existing.failedAt ?? 0) >= RETRY_AFTER_MS) {
9220
+ existing.failedAt = Date.now();
9221
+ startLoad(url, onReady, existing);
9222
+ }
9223
+ return null;
9224
+ }
9225
+ const entry = startLoad(url, onReady);
9226
+ cache.set(url, entry);
9227
+ updateAssetStatus(url, "pending");
9217
9228
  return null;
9218
9229
  }
9219
9230
  function getImageStatus(url) {
9220
9231
  return cache.get(url)?.status;
9221
9232
  }
9222
- var cache;
9233
+ var RETRY_AFTER_MS, cache;
9223
9234
  var init_imageCache = __esm({
9224
9235
  "lib/imageCache.ts"() {
9225
9236
  "use client";
9226
9237
  init_verificationRegistry();
9238
+ RETRY_AFTER_MS = 5e3;
9227
9239
  cache = /* @__PURE__ */ new Map();
9228
9240
  }
9229
9241
  });
@@ -9819,6 +9831,11 @@ function Canvas2D({
9819
9831
  }
9820
9832
  return { width: maxX + 1, height: maxY + 1 };
9821
9833
  }, [scenePositions]);
9834
+ const defaultGridFocus = React83.useMemo(() => {
9835
+ if (isFree || projection === "side") return void 0;
9836
+ if (gridExtent.width < 2 || gridExtent.height < 2) return void 0;
9837
+ return { x: (gridExtent.width - 1) / 2, y: (gridExtent.height - 1) / 2 };
9838
+ }, [isFree, projection, gridExtent]);
9822
9839
  const baseOffsetX = React83.useMemo(() => {
9823
9840
  if (isFree || projection === "flat" || projection === "side") return 0;
9824
9841
  return (gridExtent.height - 1) * (scaledTileWidth / 2);
@@ -9923,10 +9940,13 @@ function Canvas2D({
9923
9940
  }
9924
9941
  if (!drawables || drawables.length === 0) return;
9925
9942
  const cam = cameraRef.current;
9926
- if (cameraPos && dragDistance() === 0) {
9927
- const p = projector.anchorPoint(cameraPos, "center");
9928
- cam.x = p.x - viewportSize.width / 2;
9929
- cam.y = p.y - viewportSize.height / 2;
9943
+ if (camera !== "follow" && dragDistance() === 0) {
9944
+ const focus = cameraPos ?? defaultGridFocus;
9945
+ if (focus) {
9946
+ const p = projector.anchorPoint(focus, "center");
9947
+ cam.x = p.x - viewportSize.width / 2;
9948
+ cam.y = p.y - viewportSize.height / 2;
9949
+ }
9930
9950
  }
9931
9951
  const containerRect = containerRef.current?.getBoundingClientRect();
9932
9952
  const canvasRect = canvas.getBoundingClientRect();
@@ -9939,7 +9959,7 @@ function Canvas2D({
9939
9959
  const dctx = { projector, time: 0, invalidate: bumpAtlas };
9940
9960
  for (const node of drawables) paintDrawable(painter, node, dctx);
9941
9961
  painter.restore();
9942
- }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage]);
9962
+ }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
9943
9963
  React83.useEffect(() => {
9944
9964
  if (camera !== "follow" || !followTarget) return;
9945
9965
  const p = projector.anchorPoint(followTarget, "center");
@@ -9116,19 +9116,12 @@ var init_useCamera = __esm({
9116
9116
  });
9117
9117
 
9118
9118
  // lib/imageCache.ts
9119
- function getOrLoadImage(url, onReady) {
9120
- if (!url || typeof window === "undefined") return null;
9121
- const existing = cache.get(url);
9122
- if (existing) {
9123
- if (existing.status === "loaded") return existing.img;
9124
- if (existing.status === "pending" && onReady) existing.onReady = onReady;
9125
- return null;
9126
- }
9119
+ function startLoad(url, onReady, existing) {
9127
9120
  const img = new Image();
9128
9121
  img.crossOrigin = "anonymous";
9129
- const entry = { img, status: "pending", onReady };
9130
- cache.set(url, entry);
9131
- updateAssetStatus(url, "pending");
9122
+ const entry = existing ?? { img, status: "pending" };
9123
+ entry.img = img;
9124
+ if (onReady) entry.onReady = onReady;
9132
9125
  img.onload = () => {
9133
9126
  entry.status = "loaded";
9134
9127
  updateAssetStatus(url, "loaded");
@@ -9136,20 +9129,39 @@ function getOrLoadImage(url, onReady) {
9136
9129
  };
9137
9130
  img.onerror = () => {
9138
9131
  entry.status = "failed";
9132
+ entry.failedAt = Date.now();
9139
9133
  updateAssetStatus(url, "failed");
9140
9134
  entry.onReady?.();
9141
9135
  };
9142
9136
  img.src = url;
9137
+ return entry;
9138
+ }
9139
+ function getOrLoadImage(url, onReady) {
9140
+ if (!url || typeof window === "undefined") return null;
9141
+ const existing = cache.get(url);
9142
+ if (existing) {
9143
+ if (existing.status === "loaded") return existing.img;
9144
+ if (existing.status === "pending" && onReady) existing.onReady = onReady;
9145
+ if (existing.status === "failed" && Date.now() - (existing.failedAt ?? 0) >= RETRY_AFTER_MS) {
9146
+ existing.failedAt = Date.now();
9147
+ startLoad(url, onReady, existing);
9148
+ }
9149
+ return null;
9150
+ }
9151
+ const entry = startLoad(url, onReady);
9152
+ cache.set(url, entry);
9153
+ updateAssetStatus(url, "pending");
9143
9154
  return null;
9144
9155
  }
9145
9156
  function getImageStatus(url) {
9146
9157
  return cache.get(url)?.status;
9147
9158
  }
9148
- var cache;
9159
+ var RETRY_AFTER_MS, cache;
9149
9160
  var init_imageCache = __esm({
9150
9161
  "lib/imageCache.ts"() {
9151
9162
  "use client";
9152
9163
  init_verificationRegistry();
9164
+ RETRY_AFTER_MS = 5e3;
9153
9165
  cache = /* @__PURE__ */ new Map();
9154
9166
  }
9155
9167
  });
@@ -9745,6 +9757,11 @@ function Canvas2D({
9745
9757
  }
9746
9758
  return { width: maxX + 1, height: maxY + 1 };
9747
9759
  }, [scenePositions]);
9760
+ const defaultGridFocus = useMemo(() => {
9761
+ if (isFree || projection === "side") return void 0;
9762
+ if (gridExtent.width < 2 || gridExtent.height < 2) return void 0;
9763
+ return { x: (gridExtent.width - 1) / 2, y: (gridExtent.height - 1) / 2 };
9764
+ }, [isFree, projection, gridExtent]);
9748
9765
  const baseOffsetX = useMemo(() => {
9749
9766
  if (isFree || projection === "flat" || projection === "side") return 0;
9750
9767
  return (gridExtent.height - 1) * (scaledTileWidth / 2);
@@ -9849,10 +9866,13 @@ function Canvas2D({
9849
9866
  }
9850
9867
  if (!drawables || drawables.length === 0) return;
9851
9868
  const cam = cameraRef.current;
9852
- if (cameraPos && dragDistance() === 0) {
9853
- const p = projector.anchorPoint(cameraPos, "center");
9854
- cam.x = p.x - viewportSize.width / 2;
9855
- cam.y = p.y - viewportSize.height / 2;
9869
+ if (camera !== "follow" && dragDistance() === 0) {
9870
+ const focus = cameraPos ?? defaultGridFocus;
9871
+ if (focus) {
9872
+ const p = projector.anchorPoint(focus, "center");
9873
+ cam.x = p.x - viewportSize.width / 2;
9874
+ cam.y = p.y - viewportSize.height / 2;
9875
+ }
9856
9876
  }
9857
9877
  const containerRect = containerRef.current?.getBoundingClientRect();
9858
9878
  const canvasRect = canvas.getBoundingClientRect();
@@ -9865,7 +9885,7 @@ function Canvas2D({
9865
9885
  const dctx = { projector, time: 0, invalidate: bumpAtlas };
9866
9886
  for (const node of drawables) paintDrawable(painter, node, dctx);
9867
9887
  painter.restore();
9868
- }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage]);
9888
+ }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
9869
9889
  useEffect(() => {
9870
9890
  if (camera !== "follow" || !followTarget) return;
9871
9891
  const p = projector.anchorPoint(followTarget, "center");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/ui",
3
- "version": "5.136.0",
3
+ "version": "5.138.0",
4
4
  "description": "React UI components, hooks, and providers for Almadar",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -118,11 +118,11 @@
118
118
  "access": "public"
119
119
  },
120
120
  "dependencies": {
121
- "@almadar/core": "^10.43.0",
121
+ "@almadar/core": "^10.44.0",
122
122
  "@almadar/evaluator": "^2.38.0",
123
123
  "@almadar/logger": "^1.10.0",
124
- "@almadar/runtime": "^6.45.0",
125
- "@almadar/std": "^16.153.0",
124
+ "@almadar/runtime": "^6.46.0",
125
+ "@almadar/std": "^16.154.0",
126
126
  "@almadar/syntax": "^1.13.0",
127
127
  "@dnd-kit/core": "^6.3.1",
128
128
  "@dnd-kit/sortable": "^10.0.0",