@almadar/ui 5.110.0 → 5.111.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.
@@ -1,7 +1,7 @@
1
- import React6, { forwardRef, useRef, useState, useEffect, useMemo, useImperativeHandle, useCallback, createContext, useContext, Component, useReducer } from 'react';
1
+ import React3, { forwardRef, useRef, useState, useEffect, useMemo, useImperativeHandle, useCallback, createContext, useContext, Component, useReducer } from 'react';
2
2
  import { EventBusContext, useTraitScope } from '@almadar/ui/providers';
3
3
  import { createLogger } from '@almadar/logger';
4
- import { Canvas, useThree, useFrame, useLoader } from '@react-three/fiber';
4
+ import { Canvas, useThree, useFrame } from '@react-three/fiber';
5
5
  import * as THREE9 from 'three';
6
6
  import { Vector3, QuadraticBezierCurve3, MathUtils, Quaternion } from 'three';
7
7
  import { Grid, OrbitControls, Billboard, Text, Stars, Sparkles, Html, RoundedBox } from '@react-three/drei';
@@ -505,6 +505,54 @@ function FollowCamera3D({
505
505
  });
506
506
  return null;
507
507
  }
508
+
509
+ // lib/atlasSlice.ts
510
+ var atlasCache = /* @__PURE__ */ new Map();
511
+ function isTilesheet(a) {
512
+ return typeof a.tileWidth === "number";
513
+ }
514
+ function getAtlas(url, onReady) {
515
+ if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
516
+ atlasCache.set(url, void 0);
517
+ fetch(url).then((r) => r.json()).then((json) => {
518
+ atlasCache.set(url, json);
519
+ onReady();
520
+ }).catch(() => {
521
+ atlasCache.set(url, null);
522
+ });
523
+ return void 0;
524
+ }
525
+ function subRectFor(atlas, sprite) {
526
+ if (isTilesheet(atlas)) {
527
+ let col;
528
+ let row;
529
+ if (sprite.includes(",")) {
530
+ const [c, r] = sprite.split(",").map((n) => Number(n.trim()));
531
+ col = c;
532
+ row = r;
533
+ } else {
534
+ const i = Number(sprite);
535
+ if (!Number.isFinite(i)) return null;
536
+ col = i % atlas.columns;
537
+ row = Math.floor(i / atlas.columns);
538
+ }
539
+ if (!Number.isFinite(col) || !Number.isFinite(row) || col < 0 || row < 0 || col >= atlas.columns || row >= atlas.rows) return null;
540
+ const margin = atlas.margin ?? 0;
541
+ const spacing = atlas.spacing ?? 0;
542
+ return {
543
+ sx: margin + col * (atlas.tileWidth + spacing),
544
+ sy: margin + row * (atlas.tileHeight + spacing),
545
+ sw: atlas.tileWidth,
546
+ sh: atlas.tileHeight
547
+ };
548
+ }
549
+ const st = atlas.subTextures[sprite];
550
+ if (!st) return null;
551
+ return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
552
+ }
553
+ function isAtlasAsset(asset) {
554
+ return !!asset && typeof asset.atlas === "string" && typeof asset.sprite === "string";
555
+ }
508
556
  var log3 = createLogger("almadar:ui:game:model-loader");
509
557
  var gltfCache = /* @__PURE__ */ new Map();
510
558
  function loadGltfCached(url, assetRoot) {
@@ -706,22 +754,126 @@ function ModelLoader({
706
754
  }
707
755
  );
708
756
  }
757
+ var CrossOriginTextureLoader = class extends THREE9.TextureLoader {
758
+ constructor() {
759
+ super();
760
+ this.crossOrigin = "anonymous";
761
+ }
762
+ };
763
+ function useBillboardTexture(url) {
764
+ const [state, setState] = React3.useState({
765
+ texture: null,
766
+ error: false
767
+ });
768
+ React3.useEffect(() => {
769
+ let active = true;
770
+ const loader = new CrossOriginTextureLoader();
771
+ loader.load(
772
+ url,
773
+ (texture) => {
774
+ if (!active) return;
775
+ console.log("[SpriteBillboard] texture loaded", url, texture.image?.width, texture.image?.height);
776
+ texture.colorSpace = THREE9.SRGBColorSpace;
777
+ setState({ texture, error: false });
778
+ },
779
+ void 0,
780
+ (err) => {
781
+ if (!active) return;
782
+ console.error("[SpriteBillboard] texture load error", url, err);
783
+ setState({ texture: null, error: true });
784
+ }
785
+ );
786
+ return () => {
787
+ active = false;
788
+ };
789
+ }, [url]);
790
+ return state;
791
+ }
792
+ function useAtlasFrame(asset) {
793
+ const [tick, setTick] = React3.useState(0);
794
+ React3.useEffect(() => {
795
+ if (!isAtlasAsset(asset)) return;
796
+ getAtlas(asset.atlas, () => setTick((t) => t + 1));
797
+ }, [asset, tick]);
798
+ return React3.useMemo(() => {
799
+ if (!isAtlasAsset(asset)) return { frame: null, ready: true };
800
+ const atlas = getAtlas(asset.atlas, () => setTick((t) => t + 1));
801
+ if (!atlas) {
802
+ console.log("[SpriteBillboard] atlas not ready", asset.atlas, asset.sprite);
803
+ return { frame: null, ready: false };
804
+ }
805
+ const r = subRectFor(atlas, asset.sprite);
806
+ console.log("[SpriteBillboard] atlas frame", asset.atlas, asset.sprite, r);
807
+ if (!r) return { frame: null, ready: true };
808
+ return { frame: { x: r.sx, y: r.sy, w: r.sw, h: r.sh }, ready: true };
809
+ }, [asset, tick]);
810
+ }
709
811
  function SpriteBillboard({ node, world }) {
710
- const texture = useLoader(THREE9.TextureLoader, node.asset.url);
711
- const img = texture.image;
712
- const imgW = img?.width || 1;
713
- const imgH = img?.height || 1;
714
- if (node.frame) {
715
- texture.repeat.set(node.frame.w / imgW, node.frame.h / imgH);
716
- texture.offset.set(node.frame.x / imgW, 1 - (node.frame.y + node.frame.h) / imgH);
717
- texture.magFilter = texture.minFilter = THREE9.NearestFilter;
718
- texture.needsUpdate = true;
812
+ const { texture, error: textureError } = useBillboardTexture(node.asset.url);
813
+ const { frame: atlasFrame, ready: atlasReady } = useAtlasFrame(node.asset);
814
+ const frame = node.frame ?? atlasFrame;
815
+ const anchor = node.anchor ?? "top-left";
816
+ console.log("[SpriteBillboard] render", node.asset.url, anchor, frame, "texture?", !!texture, "atlasReady?", atlasReady);
817
+ const size = React3.useMemo(() => {
818
+ const img = texture?.image;
819
+ const imgW = img?.width || 1;
820
+ const imgH = img?.height || 1;
821
+ const aspect = frame ? frame.w / frame.h : imgW / imgH;
822
+ if (anchor === "top-left") {
823
+ const width2 = node.width ?? 1;
824
+ const height2 = node.height ?? 1;
825
+ return { width: width2, height: height2, imgW, imgH };
826
+ }
827
+ const height = node.height ?? 1;
828
+ const width = node.width ?? height * aspect;
829
+ return { width, height, imgW, imgH };
830
+ }, [texture, frame, node.height, node.width, anchor]);
831
+ if (!texture || !atlasReady) {
832
+ if (anchor === "top-left") {
833
+ return /* @__PURE__ */ jsx("group", { position: [world[0] + size.width / 2, 0.02, world[2] + size.height / 2], children: /* @__PURE__ */ jsxs("mesh", { rotation: [-Math.PI / 2, 0, 0], children: [
834
+ /* @__PURE__ */ jsx("planeGeometry", { args: [size.width, size.height] }),
835
+ /* @__PURE__ */ jsx("meshBasicMaterial", { color: textureError ? 16729156 : 8947848, transparent: true, opacity: 0.6, side: THREE9.DoubleSide })
836
+ ] }) });
837
+ }
838
+ return /* @__PURE__ */ jsx("group", { position: [world[0], world[1] + size.height / 2, world[2]], children: /* @__PURE__ */ jsxs("mesh", { children: [
839
+ /* @__PURE__ */ jsx("planeGeometry", { args: [size.width, size.height] }),
840
+ /* @__PURE__ */ jsx("meshBasicMaterial", { color: textureError ? 16729156 : 8947848, transparent: true, opacity: 0.6, side: THREE9.DoubleSide })
841
+ ] }) });
842
+ }
843
+ const groundGeometry = React3.useMemo(() => {
844
+ if (anchor !== "top-left" || !texture || !atlasReady) return null;
845
+ const g = new THREE9.PlaneGeometry(size.width, size.height);
846
+ g.rotateX(-Math.PI / 2);
847
+ if (frame) {
848
+ const u0 = frame.x / size.imgW;
849
+ const v0 = 1 - (frame.y + frame.h) / size.imgH;
850
+ const u1 = u0 + frame.w / size.imgW;
851
+ const v1 = v0 + frame.h / size.imgH;
852
+ const uvs = new Float32Array([u0, v1, u1, v1, u0, v0, u1, v0]);
853
+ g.setAttribute("uv", new THREE9.BufferAttribute(uvs, 2));
854
+ }
855
+ return g;
856
+ }, [anchor, texture, atlasReady, size.width, size.height, size.imgW, size.imgH, frame]);
857
+ texture.magFilter = texture.minFilter = THREE9.NearestFilter;
858
+ texture.needsUpdate = true;
859
+ if (anchor === "top-left") {
860
+ return /* @__PURE__ */ jsx("group", { position: [world[0] + size.width / 2, 0.01, world[2] + size.height / 2], children: /* @__PURE__ */ jsx("mesh", { geometry: groundGeometry ?? void 0, children: /* @__PURE__ */ jsx(
861
+ "meshBasicMaterial",
862
+ {
863
+ map: texture,
864
+ transparent: true,
865
+ alphaTest: 0.1,
866
+ side: THREE9.DoubleSide,
867
+ opacity: node.opacity ?? 1
868
+ }
869
+ ) }) });
870
+ }
871
+ if (frame) {
872
+ texture.repeat.set(frame.w / size.imgW, frame.h / size.imgH);
873
+ texture.offset.set(frame.x / size.imgW, 1 - (frame.y + frame.h) / size.imgH);
719
874
  }
720
- const aspect = node.frame ? node.frame.w / node.frame.h : imgW / imgH;
721
- const height = node.height ?? 1;
722
- const width = node.width ?? height * aspect;
723
- return /* @__PURE__ */ jsx(Billboard, { position: [world[0], world[1] + height / 2, world[2]], children: /* @__PURE__ */ jsxs("mesh", { children: [
724
- /* @__PURE__ */ jsx("planeGeometry", { args: [width, height] }),
875
+ return /* @__PURE__ */ jsx(Billboard, { position: [world[0], world[1] + size.height / 2, world[2]], children: /* @__PURE__ */ jsxs("mesh", { children: [
876
+ /* @__PURE__ */ jsx("planeGeometry", { args: [size.width, size.height] }),
725
877
  /* @__PURE__ */ jsx(
726
878
  "meshBasicMaterial",
727
879
  {
@@ -2508,7 +2660,7 @@ var positionStyles = {
2508
2660
  fixed: "fixed",
2509
2661
  sticky: "sticky"
2510
2662
  };
2511
- var Box = React6.forwardRef(
2663
+ var Box = React3.forwardRef(
2512
2664
  ({
2513
2665
  padding,
2514
2666
  paddingX,
@@ -2573,7 +2725,7 @@ var Box = React6.forwardRef(
2573
2725
  onPointerDown?.(e);
2574
2726
  }, [hoverEvent, tapReveal, triggerProps, onPointerDown]);
2575
2727
  const isClickable = action || onClick;
2576
- return React6.createElement(
2728
+ return React3.createElement(
2577
2729
  Component2,
2578
2730
  {
2579
2731
  ref,
@@ -2694,7 +2846,7 @@ var Typography = ({
2694
2846
  }) => {
2695
2847
  const variant = variantProp ?? (level ? `h${level}` : "body1");
2696
2848
  const Component2 = as || defaultElements[variant];
2697
- return React6.createElement(
2849
+ return React3.createElement(
2698
2850
  Component2,
2699
2851
  {
2700
2852
  id,
@@ -2767,7 +2919,7 @@ var Stack = ({
2767
2919
  };
2768
2920
  const isHorizontal = direction === "horizontal";
2769
2921
  const directionClass = responsive && isHorizontal ? reverse ? "flex-col-reverse md:flex-row-reverse" : "flex-col md:flex-row" : isHorizontal ? reverse ? "flex-row-reverse" : "flex-row" : reverse ? "flex-col-reverse" : "flex-col";
2770
- return React6.createElement(
2922
+ return React3.createElement(
2771
2923
  Component2,
2772
2924
  {
2773
2925
  className: cn(
@@ -5062,7 +5214,7 @@ var Avl3DViewer = ({
5062
5214
  const handleTraitClick = useCallback((name) => {
5063
5215
  dispatch({ type: "ZOOM_INTO_TRAIT", trait: name, targetPosition: { x: 0, y: 0 } });
5064
5216
  }, []);
5065
- const [highlightedTrait, setHighlightedTrait] = React6.useState(null);
5217
+ const [highlightedTrait, setHighlightedTrait] = React3.useState(null);
5066
5218
  const handleTransitionClick = useCallback((index) => {
5067
5219
  dispatch({ type: "ZOOM_INTO_TRANSITION", transitionIndex: index, targetPosition: { x: 0, y: 0 } });
5068
5220
  }, []);
@@ -5149,7 +5301,7 @@ var Avl3DViewer = ({
5149
5301
  gap: "xs",
5150
5302
  align: "center",
5151
5303
  className: "absolute top-2 left-2 z-10 bg-surface/80 backdrop-blur rounded-md px-3 py-1.5",
5152
- children: breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxs(React6.Fragment, { children: [
5304
+ children: breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxs(React3.Fragment, { children: [
5153
5305
  i > 0 && /* @__PURE__ */ jsx(Typography, { variant: "small", color: "muted", className: "mx-1", children: "/" }),
5154
5306
  i < breadcrumbs.length - 1 ? /* @__PURE__ */ jsx(
5155
5307
  Box,
@@ -737,6 +737,13 @@ function bindCanvasCapture(captureFn) {
737
737
  window.__orbitalVerification.captureFrame = captureFn;
738
738
  }
739
739
  }
740
+ function bindLastDrawables(getDrawables) {
741
+ if (typeof window === "undefined") return;
742
+ exposeOnWindow();
743
+ if (window.__orbitalVerification) {
744
+ window.__orbitalVerification.getLastDrawables = getDrawables;
745
+ }
746
+ }
740
747
  function updateAssetStatus(url, status) {
741
748
  if (typeof window === "undefined") return;
742
749
  exposeOnWindow();
@@ -1818,6 +1825,7 @@ exports.apiClient = apiClient;
1818
1825
  exports.arrowheadPath = arrowheadPath;
1819
1826
  exports.bindCanvasCapture = bindCanvasCapture;
1820
1827
  exports.bindEventBus = bindEventBus;
1828
+ exports.bindLastDrawables = bindLastDrawables;
1821
1829
  exports.bindTraitStateGetter = bindTraitStateGetter;
1822
1830
  exports.brainIconPath = brainIconPath;
1823
1831
  exports.clearDebugEvents = clearDebugEvents;
package/dist/lib/index.js CHANGED
@@ -735,6 +735,13 @@ function bindCanvasCapture(captureFn) {
735
735
  window.__orbitalVerification.captureFrame = captureFn;
736
736
  }
737
737
  }
738
+ function bindLastDrawables(getDrawables) {
739
+ if (typeof window === "undefined") return;
740
+ exposeOnWindow();
741
+ if (window.__orbitalVerification) {
742
+ window.__orbitalVerification.getLastDrawables = getDrawables;
743
+ }
744
+ }
738
745
  function updateAssetStatus(url, status) {
739
746
  if (typeof window === "undefined") return;
740
747
  exposeOnWindow();
@@ -1809,4 +1816,4 @@ var JAZARI_COLORS = {
1809
1816
  darkBg: "#1a1a2e"
1810
1817
  };
1811
1818
 
1812
- export { ApiError, DEFAULT_CONFIG, JAZARI_COLORS, apiClient, arrowheadPath, bindCanvasCapture, bindEventBus, bindTraitStateGetter, brainIconPath, clearDebugEvents, clearEntityProvider, clearGuardHistory, clearTicks, clearTraits, clearVerification, cn, computeJazariLayout, debug, debugCollision, debugError, debugGameState, debugGroup, debugGroupEnd, debugInput, debugPhysics, debugTable, debugTime, debugTimeEnd, debugWarn, eightPointedStarPath, extractOutputsFromTransitions, extractStateMachine, formatGuard, formatNestedFieldLabel, gearTeethPath, getAllChecks, getAllTicks, getAllTraits, getBridgeHealth, getDebugEvents, getEffectSummary, getEntitiesByType, getEntityById, getEntitySnapshot, getEventsBySource, getEventsByType, getGuardEvaluationsForTrait, getGuardHistory, getNestedValue, getRecentEvents, getRecentGuardEvaluations, getSnapshot, getSummary, getTick, getTrait, getTraitSnapshots, getTransitions, getTransitionsForTrait, initDebugShortcut, isDebugEnabled, lockIconPath, logDebugEvent, logEffectExecuted, logError, logEventFired, logInfo, logStateChange, logWarning, onDebugToggle, parseContentSegments, parseMarkdownWithCodeBlocks, pipeIconPath, recordGuardEvaluation, recordServerResponse, recordTransition, registerCheck, registerTick, registerTrait, registerTraitSnapshot, renderStateMachineToDomData, renderStateMachineToSvg, setDebugEnabled, setEntityProvider, setTickActive, subscribeToDebugEvents, subscribeToGuardChanges, subscribeToTickChanges, subscribeToTraitChanges, subscribeToVerification, toggleDebug, unregisterTick, unregisterTrait, updateAssetStatus, updateBridgeHealth, updateCheck, updateGuardResult, updateTickExecution, updateTraitState, waitForTransition };
1819
+ export { ApiError, DEFAULT_CONFIG, JAZARI_COLORS, apiClient, arrowheadPath, bindCanvasCapture, bindEventBus, bindLastDrawables, bindTraitStateGetter, brainIconPath, clearDebugEvents, clearEntityProvider, clearGuardHistory, clearTicks, clearTraits, clearVerification, cn, computeJazariLayout, debug, debugCollision, debugError, debugGameState, debugGroup, debugGroupEnd, debugInput, debugPhysics, debugTable, debugTime, debugTimeEnd, debugWarn, eightPointedStarPath, extractOutputsFromTransitions, extractStateMachine, formatGuard, formatNestedFieldLabel, gearTeethPath, getAllChecks, getAllTicks, getAllTraits, getBridgeHealth, getDebugEvents, getEffectSummary, getEntitiesByType, getEntityById, getEntitySnapshot, getEventsBySource, getEventsByType, getGuardEvaluationsForTrait, getGuardHistory, getNestedValue, getRecentEvents, getRecentGuardEvaluations, getSnapshot, getSummary, getTick, getTrait, getTraitSnapshots, getTransitions, getTransitionsForTrait, initDebugShortcut, isDebugEnabled, lockIconPath, logDebugEvent, logEffectExecuted, logError, logEventFired, logInfo, logStateChange, logWarning, onDebugToggle, parseContentSegments, parseMarkdownWithCodeBlocks, pipeIconPath, recordGuardEvaluation, recordServerResponse, recordTransition, registerCheck, registerTick, registerTrait, registerTraitSnapshot, renderStateMachineToDomData, renderStateMachineToSvg, setDebugEnabled, setEntityProvider, setTickActive, subscribeToDebugEvents, subscribeToGuardChanges, subscribeToTickChanges, subscribeToTraitChanges, subscribeToVerification, toggleDebug, unregisterTick, unregisterTrait, updateAssetStatus, updateBridgeHealth, updateCheck, updateGuardResult, updateTickExecution, updateTraitState, waitForTransition };
@@ -83,6 +83,12 @@ export declare function registerTraitSnapshot(traitName: string, getter: TraitSn
83
83
  * so the verifier can snapshot canvas content at checkpoints.
84
84
  */
85
85
  export declare function bindCanvasCapture(captureFn: () => string | null): void;
86
+ /**
87
+ * Bind a reader for the last drawables list received by a canvas host.
88
+ * Lets verification scripts inspect exactly which neutral descriptors reached
89
+ * the painter without re-deriving them from state.
90
+ */
91
+ export declare function bindLastDrawables(getDrawables: () => unknown[] | null): void;
86
92
  /**
87
93
  * Update asset load status for canvas verification.
88
94
  * Game organisms call this when images load or fail.
@@ -7407,6 +7407,13 @@ function bindCanvasCapture(captureFn) {
7407
7407
  window.__orbitalVerification.captureFrame = captureFn;
7408
7408
  }
7409
7409
  }
7410
+ function bindLastDrawables(getDrawables) {
7411
+ if (typeof window === "undefined") return;
7412
+ exposeOnWindow();
7413
+ if (window.__orbitalVerification) {
7414
+ window.__orbitalVerification.getLastDrawables = getDrawables;
7415
+ }
7416
+ }
7410
7417
  function updateAssetStatus(url, status) {
7411
7418
  if (typeof window === "undefined") return;
7412
7419
  exposeOnWindow();
@@ -9251,8 +9258,9 @@ var init_DrawSprite = __esm({
9251
9258
  if (!r) return;
9252
9259
  src = { x: r.sx, y: r.sy, w: r.sw, h: r.sh };
9253
9260
  }
9254
- const w = node.width ?? (src ? src.w : tex.width);
9255
- const h = node.height ?? (src ? src.h : tex.height);
9261
+ const tw = dctx.projector.tileWidth;
9262
+ const w = node.width !== void 0 ? node.width * tw : src ? src.w : tex.width;
9263
+ const h = node.height !== void 0 ? node.height * tw : src ? src.h : tex.height;
9256
9264
  const anchor = node.anchor ?? "top-left";
9257
9265
  const p = dctx.projector.anchorPoint(node.position, anchor);
9258
9266
  const dx = anchor === "top-left" ? p.x : p.x - w / 2;
@@ -9299,27 +9307,30 @@ var init_DrawShape = __esm({
9299
9307
  }
9300
9308
  case "rect": {
9301
9309
  const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "top-left");
9302
- const x = p.x + (node.offsetX ?? 0);
9303
- const y = p.y + (node.offsetY ?? 0);
9304
- const w = node.width ?? 0;
9305
- const h = node.height ?? 0;
9310
+ const tw = dctx.projector.tileWidth;
9311
+ const x = p.x + (node.offsetX ?? 0) * tw;
9312
+ const y = p.y + (node.offsetY ?? 0) * tw;
9313
+ const w = (node.width ?? 0) * tw;
9314
+ const h = (node.height ?? 0) * tw;
9306
9315
  if (node.fill) painter.fillRect(x, y, w, h, node.fill);
9307
9316
  if (node.stroke) painter.strokeRect(x, y, w, h, node.stroke, node.strokeWidth ?? 1);
9308
9317
  break;
9309
9318
  }
9310
9319
  case "ellipse": {
9311
9320
  const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "ground");
9312
- const cx = p.x + (node.offsetX ?? 0);
9313
- const cy = p.y + (node.offsetY ?? 0);
9314
- const rx = node.radiusX ?? 0;
9315
- const ry = node.radiusY ?? rx;
9321
+ const tw = dctx.projector.tileWidth;
9322
+ const cx = p.x + (node.offsetX ?? 0) * tw;
9323
+ const cy = p.y + (node.offsetY ?? 0) * tw;
9324
+ const rx = (node.radiusX ?? 0) * tw;
9325
+ const ry = (node.radiusY ?? rx) * tw;
9316
9326
  if (node.fill) painter.fillEllipse(cx, cy, rx, ry, node.fill);
9317
9327
  if (node.stroke) painter.strokeEllipse(cx, cy, rx, ry, node.stroke, node.strokeWidth ?? 1);
9318
9328
  break;
9319
9329
  }
9320
9330
  case "poly": {
9321
9331
  const base = dctx.projector.project(node.position);
9322
- const pts = (node.points ?? []).map((pt) => ({ x: base.x + pt.x, y: base.y + pt.y }));
9332
+ const tw = dctx.projector.tileWidth;
9333
+ const pts = (node.points ?? []).map((pt) => ({ x: base.x + pt.x * tw, y: base.y + pt.y * tw }));
9323
9334
  if (node.fill) painter.fillPoly(pts, node.fill);
9324
9335
  if (node.stroke) painter.strokePoly(pts, node.stroke, node.strokeWidth ?? 1, true);
9325
9336
  break;
@@ -9550,10 +9561,12 @@ function Canvas2D({
9550
9561
  const canvas = canvasRef.current;
9551
9562
  if (!canvas) return;
9552
9563
  bindCanvasCapture(() => canvas.toDataURL("image/png"));
9564
+ bindLastDrawables(() => drawables ?? null);
9553
9565
  return () => {
9554
9566
  bindCanvasCapture(() => null);
9567
+ bindLastDrawables(() => null);
9555
9568
  };
9556
- }, []);
9569
+ }, [drawables]);
9557
9570
  const enableCamera = camera === "pan-zoom";
9558
9571
  const {
9559
9572
  cameraRef,
@@ -9568,7 +9581,7 @@ function Canvas2D({
9568
9581
  screenToWorld,
9569
9582
  lerpToTarget
9570
9583
  } = useCamera();
9571
- const [, setAtlasVersion] = React83.useState(0);
9584
+ const [atlasVersion, setAtlasVersion] = React83.useState(0);
9572
9585
  const bumpAtlas = React83.useCallback(() => setAtlasVersion((v) => v + 1), []);
9573
9586
  const miniMapTiles = React83.useMemo(() => {
9574
9587
  if (!showMinimap) return [];
@@ -9636,6 +9649,9 @@ function Canvas2D({
9636
9649
  React83.useEffect(() => {
9637
9650
  draw();
9638
9651
  }, [_imagePendingCount, draw]);
9652
+ React83.useEffect(() => {
9653
+ draw();
9654
+ }, [atlasVersion, draw]);
9639
9655
  React83.useEffect(() => {
9640
9656
  if (camera !== "follow" || !followTarget) return;
9641
9657
  let running = true;
@@ -7362,6 +7362,13 @@ function bindCanvasCapture(captureFn) {
7362
7362
  window.__orbitalVerification.captureFrame = captureFn;
7363
7363
  }
7364
7364
  }
7365
+ function bindLastDrawables(getDrawables) {
7366
+ if (typeof window === "undefined") return;
7367
+ exposeOnWindow();
7368
+ if (window.__orbitalVerification) {
7369
+ window.__orbitalVerification.getLastDrawables = getDrawables;
7370
+ }
7371
+ }
7365
7372
  function updateAssetStatus(url, status) {
7366
7373
  if (typeof window === "undefined") return;
7367
7374
  exposeOnWindow();
@@ -9206,8 +9213,9 @@ var init_DrawSprite = __esm({
9206
9213
  if (!r) return;
9207
9214
  src = { x: r.sx, y: r.sy, w: r.sw, h: r.sh };
9208
9215
  }
9209
- const w = node.width ?? (src ? src.w : tex.width);
9210
- const h = node.height ?? (src ? src.h : tex.height);
9216
+ const tw = dctx.projector.tileWidth;
9217
+ const w = node.width !== void 0 ? node.width * tw : src ? src.w : tex.width;
9218
+ const h = node.height !== void 0 ? node.height * tw : src ? src.h : tex.height;
9211
9219
  const anchor = node.anchor ?? "top-left";
9212
9220
  const p = dctx.projector.anchorPoint(node.position, anchor);
9213
9221
  const dx = anchor === "top-left" ? p.x : p.x - w / 2;
@@ -9254,27 +9262,30 @@ var init_DrawShape = __esm({
9254
9262
  }
9255
9263
  case "rect": {
9256
9264
  const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "top-left");
9257
- const x = p.x + (node.offsetX ?? 0);
9258
- const y = p.y + (node.offsetY ?? 0);
9259
- const w = node.width ?? 0;
9260
- const h = node.height ?? 0;
9265
+ const tw = dctx.projector.tileWidth;
9266
+ const x = p.x + (node.offsetX ?? 0) * tw;
9267
+ const y = p.y + (node.offsetY ?? 0) * tw;
9268
+ const w = (node.width ?? 0) * tw;
9269
+ const h = (node.height ?? 0) * tw;
9261
9270
  if (node.fill) painter.fillRect(x, y, w, h, node.fill);
9262
9271
  if (node.stroke) painter.strokeRect(x, y, w, h, node.stroke, node.strokeWidth ?? 1);
9263
9272
  break;
9264
9273
  }
9265
9274
  case "ellipse": {
9266
9275
  const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "ground");
9267
- const cx = p.x + (node.offsetX ?? 0);
9268
- const cy = p.y + (node.offsetY ?? 0);
9269
- const rx = node.radiusX ?? 0;
9270
- const ry = node.radiusY ?? rx;
9276
+ const tw = dctx.projector.tileWidth;
9277
+ const cx = p.x + (node.offsetX ?? 0) * tw;
9278
+ const cy = p.y + (node.offsetY ?? 0) * tw;
9279
+ const rx = (node.radiusX ?? 0) * tw;
9280
+ const ry = (node.radiusY ?? rx) * tw;
9271
9281
  if (node.fill) painter.fillEllipse(cx, cy, rx, ry, node.fill);
9272
9282
  if (node.stroke) painter.strokeEllipse(cx, cy, rx, ry, node.stroke, node.strokeWidth ?? 1);
9273
9283
  break;
9274
9284
  }
9275
9285
  case "poly": {
9276
9286
  const base = dctx.projector.project(node.position);
9277
- const pts = (node.points ?? []).map((pt) => ({ x: base.x + pt.x, y: base.y + pt.y }));
9287
+ const tw = dctx.projector.tileWidth;
9288
+ const pts = (node.points ?? []).map((pt) => ({ x: base.x + pt.x * tw, y: base.y + pt.y * tw }));
9278
9289
  if (node.fill) painter.fillPoly(pts, node.fill);
9279
9290
  if (node.stroke) painter.strokePoly(pts, node.stroke, node.strokeWidth ?? 1, true);
9280
9291
  break;
@@ -9505,10 +9516,12 @@ function Canvas2D({
9505
9516
  const canvas = canvasRef.current;
9506
9517
  if (!canvas) return;
9507
9518
  bindCanvasCapture(() => canvas.toDataURL("image/png"));
9519
+ bindLastDrawables(() => drawables ?? null);
9508
9520
  return () => {
9509
9521
  bindCanvasCapture(() => null);
9522
+ bindLastDrawables(() => null);
9510
9523
  };
9511
- }, []);
9524
+ }, [drawables]);
9512
9525
  const enableCamera = camera === "pan-zoom";
9513
9526
  const {
9514
9527
  cameraRef,
@@ -9523,7 +9536,7 @@ function Canvas2D({
9523
9536
  screenToWorld,
9524
9537
  lerpToTarget
9525
9538
  } = useCamera();
9526
- const [, setAtlasVersion] = useState(0);
9539
+ const [atlasVersion, setAtlasVersion] = useState(0);
9527
9540
  const bumpAtlas = useCallback(() => setAtlasVersion((v) => v + 1), []);
9528
9541
  const miniMapTiles = useMemo(() => {
9529
9542
  if (!showMinimap) return [];
@@ -9591,6 +9604,9 @@ function Canvas2D({
9591
9604
  useEffect(() => {
9592
9605
  draw();
9593
9606
  }, [_imagePendingCount, draw]);
9607
+ useEffect(() => {
9608
+ draw();
9609
+ }, [atlasVersion, draw]);
9594
9610
  useEffect(() => {
9595
9611
  if (camera !== "follow" || !followTarget) return;
9596
9612
  let running = true;
@@ -7861,6 +7861,13 @@ function bindCanvasCapture(captureFn) {
7861
7861
  window.__orbitalVerification.captureFrame = captureFn;
7862
7862
  }
7863
7863
  }
7864
+ function bindLastDrawables(getDrawables) {
7865
+ if (typeof window === "undefined") return;
7866
+ exposeOnWindow();
7867
+ if (window.__orbitalVerification) {
7868
+ window.__orbitalVerification.getLastDrawables = getDrawables;
7869
+ }
7870
+ }
7864
7871
  function updateAssetStatus(url, status) {
7865
7872
  if (typeof window === "undefined") return;
7866
7873
  exposeOnWindow();
@@ -9598,8 +9605,9 @@ var init_DrawSprite = __esm({
9598
9605
  if (!r) return;
9599
9606
  src = { x: r.sx, y: r.sy, w: r.sw, h: r.sh };
9600
9607
  }
9601
- const w = node.width ?? (src ? src.w : tex.width);
9602
- const h = node.height ?? (src ? src.h : tex.height);
9608
+ const tw = dctx.projector.tileWidth;
9609
+ const w = node.width !== void 0 ? node.width * tw : src ? src.w : tex.width;
9610
+ const h = node.height !== void 0 ? node.height * tw : src ? src.h : tex.height;
9603
9611
  const anchor = node.anchor ?? "top-left";
9604
9612
  const p = dctx.projector.anchorPoint(node.position, anchor);
9605
9613
  const dx = anchor === "top-left" ? p.x : p.x - w / 2;
@@ -9646,27 +9654,30 @@ var init_DrawShape = __esm({
9646
9654
  }
9647
9655
  case "rect": {
9648
9656
  const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "top-left");
9649
- const x = p.x + (node.offsetX ?? 0);
9650
- const y = p.y + (node.offsetY ?? 0);
9651
- const w = node.width ?? 0;
9652
- const h = node.height ?? 0;
9657
+ const tw = dctx.projector.tileWidth;
9658
+ const x = p.x + (node.offsetX ?? 0) * tw;
9659
+ const y = p.y + (node.offsetY ?? 0) * tw;
9660
+ const w = (node.width ?? 0) * tw;
9661
+ const h = (node.height ?? 0) * tw;
9653
9662
  if (node.fill) painter.fillRect(x, y, w, h, node.fill);
9654
9663
  if (node.stroke) painter.strokeRect(x, y, w, h, node.stroke, node.strokeWidth ?? 1);
9655
9664
  break;
9656
9665
  }
9657
9666
  case "ellipse": {
9658
9667
  const p = dctx.projector.anchorPoint(node.position, node.anchor ?? "ground");
9659
- const cx = p.x + (node.offsetX ?? 0);
9660
- const cy = p.y + (node.offsetY ?? 0);
9661
- const rx = node.radiusX ?? 0;
9662
- const ry = node.radiusY ?? rx;
9668
+ const tw = dctx.projector.tileWidth;
9669
+ const cx = p.x + (node.offsetX ?? 0) * tw;
9670
+ const cy = p.y + (node.offsetY ?? 0) * tw;
9671
+ const rx = (node.radiusX ?? 0) * tw;
9672
+ const ry = (node.radiusY ?? rx) * tw;
9663
9673
  if (node.fill) painter.fillEllipse(cx, cy, rx, ry, node.fill);
9664
9674
  if (node.stroke) painter.strokeEllipse(cx, cy, rx, ry, node.stroke, node.strokeWidth ?? 1);
9665
9675
  break;
9666
9676
  }
9667
9677
  case "poly": {
9668
9678
  const base = dctx.projector.project(node.position);
9669
- const pts = (node.points ?? []).map((pt) => ({ x: base.x + pt.x, y: base.y + pt.y }));
9679
+ const tw = dctx.projector.tileWidth;
9680
+ const pts = (node.points ?? []).map((pt) => ({ x: base.x + pt.x * tw, y: base.y + pt.y * tw }));
9670
9681
  if (node.fill) painter.fillPoly(pts, node.fill);
9671
9682
  if (node.stroke) painter.strokePoly(pts, node.stroke, node.strokeWidth ?? 1, true);
9672
9683
  break;
@@ -9897,10 +9908,12 @@ function Canvas2D({
9897
9908
  const canvas = canvasRef.current;
9898
9909
  if (!canvas) return;
9899
9910
  bindCanvasCapture(() => canvas.toDataURL("image/png"));
9911
+ bindLastDrawables(() => drawables ?? null);
9900
9912
  return () => {
9901
9913
  bindCanvasCapture(() => null);
9914
+ bindLastDrawables(() => null);
9902
9915
  };
9903
- }, []);
9916
+ }, [drawables]);
9904
9917
  const enableCamera = camera === "pan-zoom";
9905
9918
  const {
9906
9919
  cameraRef,
@@ -9915,7 +9928,7 @@ function Canvas2D({
9915
9928
  screenToWorld,
9916
9929
  lerpToTarget
9917
9930
  } = useCamera();
9918
- const [, setAtlasVersion] = React81.useState(0);
9931
+ const [atlasVersion, setAtlasVersion] = React81.useState(0);
9919
9932
  const bumpAtlas = React81.useCallback(() => setAtlasVersion((v) => v + 1), []);
9920
9933
  const miniMapTiles = React81.useMemo(() => {
9921
9934
  if (!showMinimap) return [];
@@ -9983,6 +9996,9 @@ function Canvas2D({
9983
9996
  React81.useEffect(() => {
9984
9997
  draw();
9985
9998
  }, [_imagePendingCount, draw]);
9999
+ React81.useEffect(() => {
10000
+ draw();
10001
+ }, [atlasVersion, draw]);
9986
10002
  React81.useEffect(() => {
9987
10003
  if (camera !== "follow" || !followTarget) return;
9988
10004
  let running = true;