@almadar/ui 5.113.0 → 5.115.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.
@@ -164,6 +164,11 @@ function useEmitEvent() {
164
164
  );
165
165
  }
166
166
 
167
+ // lib/drawable/contract.ts
168
+ function isValidScenePos(pos) {
169
+ return Number.isFinite(pos?.x) && Number.isFinite(pos?.y);
170
+ }
171
+
167
172
  // lib/drawable/hitTest.ts
168
173
  function collectDrawnItems(nodes) {
169
174
  const out = [];
@@ -172,12 +177,14 @@ function collectDrawnItems(nodes) {
172
177
  case "draw-sprite":
173
178
  case "draw-shape":
174
179
  case "draw-text":
175
- out.push({ pos: n.position, id: n.id });
180
+ if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id });
176
181
  break;
177
182
  case "draw-sprite-layer":
178
183
  case "draw-shape-layer":
179
184
  case "draw-text-layer":
180
- for (const it of n.items) out.push({ pos: it.position, id: it.id });
185
+ for (const it of n.items) {
186
+ if (isValidScenePos(it.position)) out.push({ pos: it.position, id: it.id });
187
+ }
181
188
  break;
182
189
  }
183
190
  }
@@ -699,6 +706,7 @@ function ModelLoader({
699
706
  box.getSize(size);
700
707
  const maxDim = Math.max(size.x, size.y, size.z);
701
708
  if (!Number.isFinite(maxDim) || maxDim < 0.05) return 1;
709
+ log3.warn(`TMP-GG1 bounds ${url.split("/").pop() ?? url} size=${JSON.stringify([size.x, size.y, size.z])} scaleProp=${JSON.stringify(scale)}`);
702
710
  return 1 / maxDim;
703
711
  }, [model]);
704
712
  const scaleArray = React3.useMemo(() => {
@@ -828,7 +836,7 @@ function useAtlasFrame(asset) {
828
836
  return { frame: { x: r.sx, y: r.sy, w: r.sw, h: r.sh }, ready: true };
829
837
  }, [asset, tick]);
830
838
  }
831
- function SpriteBillboard({ node, world }) {
839
+ function SpriteBillboard({ node, world, cellSize = 1 }) {
832
840
  const { texture, error: textureError } = useBillboardTexture(node.asset.url);
833
841
  const { frame: atlasFrame, ready: atlasReady } = useAtlasFrame(node.asset);
834
842
  const frame = node.frame ?? atlasFrame;
@@ -839,14 +847,14 @@ function SpriteBillboard({ node, world }) {
839
847
  const imgH = img?.height || 1;
840
848
  const aspect = frame ? frame.w / frame.h : imgW / imgH;
841
849
  if (anchor === "top-left") {
842
- const width2 = node.width ?? 1;
843
- const height2 = node.height ?? 1;
850
+ const width2 = (node.width ?? 1) * cellSize;
851
+ const height2 = (node.height ?? 1) * cellSize;
844
852
  return { width: width2, height: height2, imgW, imgH };
845
853
  }
846
854
  const height = node.height ?? 1;
847
855
  const width = node.width ?? height * aspect;
848
- return { width, height, imgW, imgH };
849
- }, [texture, frame, node.height, node.width, anchor]);
856
+ return { width: width * cellSize, height: height * cellSize, imgW, imgH };
857
+ }, [texture, frame, node.height, node.width, anchor, cellSize]);
850
858
  const groundGeometry = React3__default.default.useMemo(() => {
851
859
  if (anchor !== "top-left" || !texture || !atlasReady) return null;
852
860
  const g = new THREE9__namespace.PlaneGeometry(size.width, size.height);
@@ -898,31 +906,43 @@ function SpriteBillboard({ node, world }) {
898
906
  ] }) });
899
907
  }
900
908
  function Sprite3D({ node, projector }) {
901
- if (node.asset.dimension === "3d" && node.asset.url) {
902
- return /* @__PURE__ */ jsxRuntime.jsx("group", { position: projector.toWorld(node.position), children: /* @__PURE__ */ jsxRuntime.jsx(
909
+ const asset = node.asset;
910
+ if (!asset?.url || !isValidScenePos(node.position)) return null;
911
+ if (asset.dimension === "3d") {
912
+ const scale = node.width === void 0 ? 1 : node.height === void 0 ? node.width * projector.cellSize : [
913
+ node.width * projector.cellSize,
914
+ node.height * projector.cellSize,
915
+ node.width * projector.cellSize
916
+ ];
917
+ const world = projector.toWorld(node.position);
918
+ if (node.position.z !== void 0) {
919
+ console.warn(`TMP-GG1 sprite3d ${asset.url.split("/").pop() ?? asset.url} posZ=${node.position.z} world=${JSON.stringify(world)} scale=${JSON.stringify(scale)}`);
920
+ }
921
+ return /* @__PURE__ */ jsxRuntime.jsx("group", { position: world, children: /* @__PURE__ */ jsxRuntime.jsx(
903
922
  ModelLoader,
904
923
  {
905
- url: node.asset.url,
906
- scale: node.width ?? projector.cellSize,
924
+ url: asset.url,
925
+ scale,
907
926
  rotation: [0, node.rotation ?? 0, 0],
927
+ animation: node.animation,
908
928
  fallbackGeometry: "box",
909
929
  castShadow: true,
910
930
  receiveShadow: true
911
931
  }
912
932
  ) });
913
933
  }
914
- if (!node.asset.url) return null;
915
- return /* @__PURE__ */ jsxRuntime.jsx(SpriteBillboard, { node, world: projector.toWorld(node.position) });
934
+ return /* @__PURE__ */ jsxRuntime.jsx(SpriteBillboard, { node, world: projector.toWorld(node.position), cellSize: projector.cellSize });
916
935
  }
917
936
  function Shape3D({ node, projector }) {
937
+ if (!isValidScenePos(node.position)) return null;
918
938
  const world = projector.toWorld(node.position);
919
939
  const color = node.fill ?? node.stroke ?? "#ffffff";
920
940
  let geometry;
921
941
  switch (node.shape) {
922
942
  case "cell":
923
943
  case "rect": {
924
- const w = node.shape === "cell" ? projector.cellSize * 0.95 : node.width ?? projector.cellSize;
925
- const h = node.shape === "cell" ? projector.cellSize * 0.95 : node.height ?? projector.cellSize;
944
+ const w = node.shape === "cell" ? projector.cellSize * 0.95 : (node.width ?? 1) * projector.cellSize;
945
+ const h = node.shape === "cell" ? projector.cellSize * 0.95 : (node.height ?? 1) * projector.cellSize;
926
946
  geometry = /* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [w, h] });
927
947
  break;
928
948
  }
@@ -951,6 +971,7 @@ function Shape3D({ node, projector }) {
951
971
  ] });
952
972
  }
953
973
  function Text3D({ node, projector }) {
974
+ if (!isValidScenePos(node.position)) return null;
954
975
  const world = projector.toWorld(node.position);
955
976
  return /* @__PURE__ */ jsxRuntime.jsx(drei.Billboard, { position: [world[0], world[1] + 1.2, world[2]], children: /* @__PURE__ */ jsxRuntime.jsx(
956
977
  drei.Text,
@@ -1003,10 +1024,7 @@ function cn(...inputs) {
1003
1024
  return tailwindMerge.twMerge(clsx.clsx(inputs));
1004
1025
  }
1005
1026
  var DEFAULT_GRID_CONFIG = {
1006
- cellSize: 1,
1007
- offsetX: 0,
1008
- offsetZ: 0
1009
- };
1027
+ cellSize: 1};
1010
1028
  var Canvas3DHost = React3.forwardRef(
1011
1029
  ({
1012
1030
  cameraMode = "isometric",
@@ -1030,6 +1048,7 @@ var Canvas3DHost = React3.forwardRef(
1030
1048
  loadingMessage = "Loading 3D Scene...",
1031
1049
  keyMap,
1032
1050
  keyUpMap,
1051
+ pixelsPerUnit,
1033
1052
  children,
1034
1053
  drawables
1035
1054
  }, ref) => {
@@ -1090,21 +1109,22 @@ var Canvas3DHost = React3.forwardRef(
1090
1109
  }
1091
1110
  return { minX, maxX, minZ, maxZ };
1092
1111
  }, [scenePositions]);
1112
+ const cellSize = pixelsPerUnit !== void 0 && pixelsPerUnit > 0 ? 1 / pixelsPerUnit : DEFAULT_GRID_CONFIG.cellSize;
1093
1113
  const cameraTarget = React3.useMemo(
1094
1114
  () => [
1095
- (gridBounds.minX + gridBounds.maxX) / 2,
1115
+ (gridBounds.maxX - gridBounds.minX) / 2 * cellSize,
1096
1116
  0,
1097
- (gridBounds.minZ + gridBounds.maxZ) / 2
1117
+ (gridBounds.maxZ - gridBounds.minZ) / 2 * cellSize
1098
1118
  ],
1099
- [gridBounds]
1119
+ [gridBounds, cellSize]
1100
1120
  );
1101
1121
  const gridConfig = React3.useMemo(
1102
1122
  () => ({
1103
- ...DEFAULT_GRID_CONFIG,
1123
+ cellSize,
1104
1124
  offsetX: -(gridBounds.maxX - gridBounds.minX) / 2,
1105
1125
  offsetZ: -(gridBounds.maxZ - gridBounds.minZ) / 2
1106
1126
  }),
1107
- [gridBounds]
1127
+ [gridBounds, cellSize]
1108
1128
  );
1109
1129
  const drawableProjector = React3.useMemo(
1110
1130
  () => create3DProjector({
@@ -1149,13 +1169,13 @@ var Canvas3DHost = React3.forwardRef(
1149
1169
  }));
1150
1170
  const cameraConfig = React3.useMemo(() => {
1151
1171
  const size = Math.max(
1152
- gridBounds.maxX - gridBounds.minX,
1153
- gridBounds.maxZ - gridBounds.minZ,
1172
+ (gridBounds.maxX - gridBounds.minX) * cellSize,
1173
+ (gridBounds.maxZ - gridBounds.minZ) * cellSize,
1154
1174
  4
1155
1175
  // minimum framing distance so a tiny board isn't zoomed in
1156
1176
  );
1157
- const cx = (gridBounds.minX + gridBounds.maxX) / 2;
1158
- const cz = (gridBounds.minZ + gridBounds.maxZ) / 2;
1177
+ const cx = cameraTarget[0];
1178
+ const cz = cameraTarget[2];
1159
1179
  const d = size * 1;
1160
1180
  switch (cameraMode) {
1161
1181
  case "isometric":
@@ -1168,13 +1188,13 @@ var Canvas3DHost = React3.forwardRef(
1168
1188
  default:
1169
1189
  return { position: [cx + d, d, cz + d], fov: 45 };
1170
1190
  }
1171
- }, [cameraMode, gridBounds]);
1191
+ }, [cameraMode, gridBounds, cellSize, cameraTarget]);
1172
1192
  const followWorld = React3.useMemo(() => {
1173
1193
  if (followTarget) return drawableProjector.toWorld(followTarget);
1174
1194
  return cameraTarget;
1175
1195
  }, [followTarget, drawableProjector, cameraTarget]);
1176
1196
  const followOffset = React3.useMemo(
1177
- () => cameraMode === "chase" ? [0, 4, -7] : [5, 7, 5],
1197
+ () => cameraMode === "chase" ? [0, 4, -7] : [0, 12, 14],
1178
1198
  [cameraMode]
1179
1199
  );
1180
1200
  const handleGroundClick = React3.useCallback((e) => {
@@ -1258,13 +1278,13 @@ var Canvas3DHost = React3.forwardRef(
1258
1278
  drei.Grid,
1259
1279
  {
1260
1280
  args: [
1261
- Math.max(gridBounds.maxX - gridBounds.minX + 2, 10),
1262
- Math.max(gridBounds.maxZ - gridBounds.minZ + 2, 10)
1281
+ Math.max((gridBounds.maxX - gridBounds.minX + 2) * cellSize, 10),
1282
+ Math.max((gridBounds.maxZ - gridBounds.minZ + 2) * cellSize, 10)
1263
1283
  ],
1264
1284
  position: [
1265
- (gridBounds.maxX - gridBounds.minX) / 2 - 0.5,
1285
+ (gridBounds.maxX - gridBounds.minX) / 2 * cellSize - cellSize / 2,
1266
1286
  0,
1267
- (gridBounds.maxZ - gridBounds.minZ) / 2 - 0.5
1287
+ (gridBounds.maxZ - gridBounds.minZ) / 2 * cellSize - cellSize / 2
1268
1288
  ],
1269
1289
  cellSize: 1,
1270
1290
  cellThickness: 1,
@@ -1282,9 +1302,9 @@ var Canvas3DHost = React3.forwardRef(
1282
1302
  {
1283
1303
  rotation: [-Math.PI / 2, 0, 0],
1284
1304
  position: [
1285
- (gridBounds.maxX - gridBounds.minX) / 2 * gridConfig.cellSize,
1305
+ (gridBounds.maxX - gridBounds.minX) / 2 * cellSize,
1286
1306
  0,
1287
- (gridBounds.maxZ - gridBounds.minZ) / 2 * gridConfig.cellSize
1307
+ (gridBounds.maxZ - gridBounds.minZ) / 2 * cellSize
1288
1308
  ],
1289
1309
  onClick: handleGroundClick,
1290
1310
  children: [
@@ -1292,8 +1312,8 @@ var Canvas3DHost = React3.forwardRef(
1292
1312
  "planeGeometry",
1293
1313
  {
1294
1314
  args: [
1295
- (gridBounds.maxX - gridBounds.minX + 4) * gridConfig.cellSize,
1296
- (gridBounds.maxZ - gridBounds.minZ + 4) * gridConfig.cellSize
1315
+ (gridBounds.maxX - gridBounds.minX + 4) * cellSize,
1316
+ (gridBounds.maxZ - gridBounds.minZ + 4) * cellSize
1297
1317
  ]
1298
1318
  }
1299
1319
  ),
@@ -3,7 +3,7 @@ import React__default, { Component, ReactNode, ErrorInfo } from 'react';
3
3
  import { ScenePos, EventEmit, JsonObject, OrbitalSchema } from '@almadar/core';
4
4
  import * as THREE from 'three';
5
5
  import { QuadraticBezierCurve3 } from 'three';
6
- import { D as DrawableNode } from '../../../paintDispatch-DR942knx.cjs';
6
+ import { D as DrawableNode } from '../../../paintDispatch-BXJgISot.cjs';
7
7
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
8
8
  import { I as IsometricTile, a as IsometricUnit, b as IsometricFeature, A as ApplicationLevelData, O as OrbitalLevelData, T as TraitLevelData, c as TransitionLevelData } from '../../../avl-schema-parser-B8Onmfsu.cjs';
9
9
  export { U as UnitAnimationState } from '../../../avl-schema-parser-B8Onmfsu.cjs';
@@ -151,7 +151,8 @@ interface Canvas3DHostProps {
151
151
  worldWidth?: number;
152
152
  /** Side-view world size in pixels (accepted for API parity). */
153
153
  worldHeight?: number;
154
- /** Pixel→world-unit divisor for side view (accepted for API parity). */
154
+ /** Pixel→world-unit divisor for pixel-authored (side-view) scenes: world size =
155
+ * scene size ÷ `pixelsPerUnit`. Omitted → 1 world unit per scene unit (grid boards). */
155
156
  pixelsPerUnit?: number;
156
157
  }
157
158
  /** Imperative handle for GameCanvas3D */
@@ -3,7 +3,7 @@ import React__default, { Component, ReactNode, ErrorInfo } from 'react';
3
3
  import { ScenePos, EventEmit, JsonObject, OrbitalSchema } from '@almadar/core';
4
4
  import * as THREE from 'three';
5
5
  import { QuadraticBezierCurve3 } from 'three';
6
- import { D as DrawableNode } from '../../../paintDispatch-DR942knx.js';
6
+ import { D as DrawableNode } from '../../../paintDispatch-BXJgISot.js';
7
7
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
8
8
  import { I as IsometricTile, a as IsometricUnit, b as IsometricFeature, A as ApplicationLevelData, O as OrbitalLevelData, T as TraitLevelData, c as TransitionLevelData } from '../../../avl-schema-parser-B8Onmfsu.js';
9
9
  export { U as UnitAnimationState } from '../../../avl-schema-parser-B8Onmfsu.js';
@@ -151,7 +151,8 @@ interface Canvas3DHostProps {
151
151
  worldWidth?: number;
152
152
  /** Side-view world size in pixels (accepted for API parity). */
153
153
  worldHeight?: number;
154
- /** Pixel→world-unit divisor for side view (accepted for API parity). */
154
+ /** Pixel→world-unit divisor for pixel-authored (side-view) scenes: world size =
155
+ * scene size ÷ `pixelsPerUnit`. Omitted → 1 world unit per scene unit (grid boards). */
155
156
  pixelsPerUnit?: number;
156
157
  }
157
158
  /** Imperative handle for GameCanvas3D */
@@ -140,6 +140,11 @@ function useEmitEvent() {
140
140
  );
141
141
  }
142
142
 
143
+ // lib/drawable/contract.ts
144
+ function isValidScenePos(pos) {
145
+ return Number.isFinite(pos?.x) && Number.isFinite(pos?.y);
146
+ }
147
+
143
148
  // lib/drawable/hitTest.ts
144
149
  function collectDrawnItems(nodes) {
145
150
  const out = [];
@@ -148,12 +153,14 @@ function collectDrawnItems(nodes) {
148
153
  case "draw-sprite":
149
154
  case "draw-shape":
150
155
  case "draw-text":
151
- out.push({ pos: n.position, id: n.id });
156
+ if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id });
152
157
  break;
153
158
  case "draw-sprite-layer":
154
159
  case "draw-shape-layer":
155
160
  case "draw-text-layer":
156
- for (const it of n.items) out.push({ pos: it.position, id: it.id });
161
+ for (const it of n.items) {
162
+ if (isValidScenePos(it.position)) out.push({ pos: it.position, id: it.id });
163
+ }
157
164
  break;
158
165
  }
159
166
  }
@@ -675,6 +682,7 @@ function ModelLoader({
675
682
  box.getSize(size);
676
683
  const maxDim = Math.max(size.x, size.y, size.z);
677
684
  if (!Number.isFinite(maxDim) || maxDim < 0.05) return 1;
685
+ log3.warn(`TMP-GG1 bounds ${url.split("/").pop() ?? url} size=${JSON.stringify([size.x, size.y, size.z])} scaleProp=${JSON.stringify(scale)}`);
678
686
  return 1 / maxDim;
679
687
  }, [model]);
680
688
  const scaleArray = useMemo(() => {
@@ -804,7 +812,7 @@ function useAtlasFrame(asset) {
804
812
  return { frame: { x: r.sx, y: r.sy, w: r.sw, h: r.sh }, ready: true };
805
813
  }, [asset, tick]);
806
814
  }
807
- function SpriteBillboard({ node, world }) {
815
+ function SpriteBillboard({ node, world, cellSize = 1 }) {
808
816
  const { texture, error: textureError } = useBillboardTexture(node.asset.url);
809
817
  const { frame: atlasFrame, ready: atlasReady } = useAtlasFrame(node.asset);
810
818
  const frame = node.frame ?? atlasFrame;
@@ -815,14 +823,14 @@ function SpriteBillboard({ node, world }) {
815
823
  const imgH = img?.height || 1;
816
824
  const aspect = frame ? frame.w / frame.h : imgW / imgH;
817
825
  if (anchor === "top-left") {
818
- const width2 = node.width ?? 1;
819
- const height2 = node.height ?? 1;
826
+ const width2 = (node.width ?? 1) * cellSize;
827
+ const height2 = (node.height ?? 1) * cellSize;
820
828
  return { width: width2, height: height2, imgW, imgH };
821
829
  }
822
830
  const height = node.height ?? 1;
823
831
  const width = node.width ?? height * aspect;
824
- return { width, height, imgW, imgH };
825
- }, [texture, frame, node.height, node.width, anchor]);
832
+ return { width: width * cellSize, height: height * cellSize, imgW, imgH };
833
+ }, [texture, frame, node.height, node.width, anchor, cellSize]);
826
834
  const groundGeometry = React3.useMemo(() => {
827
835
  if (anchor !== "top-left" || !texture || !atlasReady) return null;
828
836
  const g = new THREE9.PlaneGeometry(size.width, size.height);
@@ -874,31 +882,43 @@ function SpriteBillboard({ node, world }) {
874
882
  ] }) });
875
883
  }
876
884
  function Sprite3D({ node, projector }) {
877
- if (node.asset.dimension === "3d" && node.asset.url) {
878
- return /* @__PURE__ */ jsx("group", { position: projector.toWorld(node.position), children: /* @__PURE__ */ jsx(
885
+ const asset = node.asset;
886
+ if (!asset?.url || !isValidScenePos(node.position)) return null;
887
+ if (asset.dimension === "3d") {
888
+ const scale = node.width === void 0 ? 1 : node.height === void 0 ? node.width * projector.cellSize : [
889
+ node.width * projector.cellSize,
890
+ node.height * projector.cellSize,
891
+ node.width * projector.cellSize
892
+ ];
893
+ const world = projector.toWorld(node.position);
894
+ if (node.position.z !== void 0) {
895
+ console.warn(`TMP-GG1 sprite3d ${asset.url.split("/").pop() ?? asset.url} posZ=${node.position.z} world=${JSON.stringify(world)} scale=${JSON.stringify(scale)}`);
896
+ }
897
+ return /* @__PURE__ */ jsx("group", { position: world, children: /* @__PURE__ */ jsx(
879
898
  ModelLoader,
880
899
  {
881
- url: node.asset.url,
882
- scale: node.width ?? projector.cellSize,
900
+ url: asset.url,
901
+ scale,
883
902
  rotation: [0, node.rotation ?? 0, 0],
903
+ animation: node.animation,
884
904
  fallbackGeometry: "box",
885
905
  castShadow: true,
886
906
  receiveShadow: true
887
907
  }
888
908
  ) });
889
909
  }
890
- if (!node.asset.url) return null;
891
- return /* @__PURE__ */ jsx(SpriteBillboard, { node, world: projector.toWorld(node.position) });
910
+ return /* @__PURE__ */ jsx(SpriteBillboard, { node, world: projector.toWorld(node.position), cellSize: projector.cellSize });
892
911
  }
893
912
  function Shape3D({ node, projector }) {
913
+ if (!isValidScenePos(node.position)) return null;
894
914
  const world = projector.toWorld(node.position);
895
915
  const color = node.fill ?? node.stroke ?? "#ffffff";
896
916
  let geometry;
897
917
  switch (node.shape) {
898
918
  case "cell":
899
919
  case "rect": {
900
- const w = node.shape === "cell" ? projector.cellSize * 0.95 : node.width ?? projector.cellSize;
901
- const h = node.shape === "cell" ? projector.cellSize * 0.95 : node.height ?? projector.cellSize;
920
+ const w = node.shape === "cell" ? projector.cellSize * 0.95 : (node.width ?? 1) * projector.cellSize;
921
+ const h = node.shape === "cell" ? projector.cellSize * 0.95 : (node.height ?? 1) * projector.cellSize;
902
922
  geometry = /* @__PURE__ */ jsx("planeGeometry", { args: [w, h] });
903
923
  break;
904
924
  }
@@ -927,6 +947,7 @@ function Shape3D({ node, projector }) {
927
947
  ] });
928
948
  }
929
949
  function Text3D({ node, projector }) {
950
+ if (!isValidScenePos(node.position)) return null;
930
951
  const world = projector.toWorld(node.position);
931
952
  return /* @__PURE__ */ jsx(Billboard, { position: [world[0], world[1] + 1.2, world[2]], children: /* @__PURE__ */ jsx(
932
953
  Text,
@@ -979,10 +1000,7 @@ function cn(...inputs) {
979
1000
  return twMerge(clsx(inputs));
980
1001
  }
981
1002
  var DEFAULT_GRID_CONFIG = {
982
- cellSize: 1,
983
- offsetX: 0,
984
- offsetZ: 0
985
- };
1003
+ cellSize: 1};
986
1004
  var Canvas3DHost = forwardRef(
987
1005
  ({
988
1006
  cameraMode = "isometric",
@@ -1006,6 +1024,7 @@ var Canvas3DHost = forwardRef(
1006
1024
  loadingMessage = "Loading 3D Scene...",
1007
1025
  keyMap,
1008
1026
  keyUpMap,
1027
+ pixelsPerUnit,
1009
1028
  children,
1010
1029
  drawables
1011
1030
  }, ref) => {
@@ -1066,21 +1085,22 @@ var Canvas3DHost = forwardRef(
1066
1085
  }
1067
1086
  return { minX, maxX, minZ, maxZ };
1068
1087
  }, [scenePositions]);
1088
+ const cellSize = pixelsPerUnit !== void 0 && pixelsPerUnit > 0 ? 1 / pixelsPerUnit : DEFAULT_GRID_CONFIG.cellSize;
1069
1089
  const cameraTarget = useMemo(
1070
1090
  () => [
1071
- (gridBounds.minX + gridBounds.maxX) / 2,
1091
+ (gridBounds.maxX - gridBounds.minX) / 2 * cellSize,
1072
1092
  0,
1073
- (gridBounds.minZ + gridBounds.maxZ) / 2
1093
+ (gridBounds.maxZ - gridBounds.minZ) / 2 * cellSize
1074
1094
  ],
1075
- [gridBounds]
1095
+ [gridBounds, cellSize]
1076
1096
  );
1077
1097
  const gridConfig = useMemo(
1078
1098
  () => ({
1079
- ...DEFAULT_GRID_CONFIG,
1099
+ cellSize,
1080
1100
  offsetX: -(gridBounds.maxX - gridBounds.minX) / 2,
1081
1101
  offsetZ: -(gridBounds.maxZ - gridBounds.minZ) / 2
1082
1102
  }),
1083
- [gridBounds]
1103
+ [gridBounds, cellSize]
1084
1104
  );
1085
1105
  const drawableProjector = useMemo(
1086
1106
  () => create3DProjector({
@@ -1125,13 +1145,13 @@ var Canvas3DHost = forwardRef(
1125
1145
  }));
1126
1146
  const cameraConfig = useMemo(() => {
1127
1147
  const size = Math.max(
1128
- gridBounds.maxX - gridBounds.minX,
1129
- gridBounds.maxZ - gridBounds.minZ,
1148
+ (gridBounds.maxX - gridBounds.minX) * cellSize,
1149
+ (gridBounds.maxZ - gridBounds.minZ) * cellSize,
1130
1150
  4
1131
1151
  // minimum framing distance so a tiny board isn't zoomed in
1132
1152
  );
1133
- const cx = (gridBounds.minX + gridBounds.maxX) / 2;
1134
- const cz = (gridBounds.minZ + gridBounds.maxZ) / 2;
1153
+ const cx = cameraTarget[0];
1154
+ const cz = cameraTarget[2];
1135
1155
  const d = size * 1;
1136
1156
  switch (cameraMode) {
1137
1157
  case "isometric":
@@ -1144,13 +1164,13 @@ var Canvas3DHost = forwardRef(
1144
1164
  default:
1145
1165
  return { position: [cx + d, d, cz + d], fov: 45 };
1146
1166
  }
1147
- }, [cameraMode, gridBounds]);
1167
+ }, [cameraMode, gridBounds, cellSize, cameraTarget]);
1148
1168
  const followWorld = useMemo(() => {
1149
1169
  if (followTarget) return drawableProjector.toWorld(followTarget);
1150
1170
  return cameraTarget;
1151
1171
  }, [followTarget, drawableProjector, cameraTarget]);
1152
1172
  const followOffset = useMemo(
1153
- () => cameraMode === "chase" ? [0, 4, -7] : [5, 7, 5],
1173
+ () => cameraMode === "chase" ? [0, 4, -7] : [0, 12, 14],
1154
1174
  [cameraMode]
1155
1175
  );
1156
1176
  const handleGroundClick = useCallback((e) => {
@@ -1234,13 +1254,13 @@ var Canvas3DHost = forwardRef(
1234
1254
  Grid,
1235
1255
  {
1236
1256
  args: [
1237
- Math.max(gridBounds.maxX - gridBounds.minX + 2, 10),
1238
- Math.max(gridBounds.maxZ - gridBounds.minZ + 2, 10)
1257
+ Math.max((gridBounds.maxX - gridBounds.minX + 2) * cellSize, 10),
1258
+ Math.max((gridBounds.maxZ - gridBounds.minZ + 2) * cellSize, 10)
1239
1259
  ],
1240
1260
  position: [
1241
- (gridBounds.maxX - gridBounds.minX) / 2 - 0.5,
1261
+ (gridBounds.maxX - gridBounds.minX) / 2 * cellSize - cellSize / 2,
1242
1262
  0,
1243
- (gridBounds.maxZ - gridBounds.minZ) / 2 - 0.5
1263
+ (gridBounds.maxZ - gridBounds.minZ) / 2 * cellSize - cellSize / 2
1244
1264
  ],
1245
1265
  cellSize: 1,
1246
1266
  cellThickness: 1,
@@ -1258,9 +1278,9 @@ var Canvas3DHost = forwardRef(
1258
1278
  {
1259
1279
  rotation: [-Math.PI / 2, 0, 0],
1260
1280
  position: [
1261
- (gridBounds.maxX - gridBounds.minX) / 2 * gridConfig.cellSize,
1281
+ (gridBounds.maxX - gridBounds.minX) / 2 * cellSize,
1262
1282
  0,
1263
- (gridBounds.maxZ - gridBounds.minZ) / 2 * gridConfig.cellSize
1283
+ (gridBounds.maxZ - gridBounds.minZ) / 2 * cellSize
1264
1284
  ],
1265
1285
  onClick: handleGroundClick,
1266
1286
  children: [
@@ -1268,8 +1288,8 @@ var Canvas3DHost = forwardRef(
1268
1288
  "planeGeometry",
1269
1289
  {
1270
1290
  args: [
1271
- (gridBounds.maxX - gridBounds.minX + 4) * gridConfig.cellSize,
1272
- (gridBounds.maxZ - gridBounds.minZ + 4) * gridConfig.cellSize
1291
+ (gridBounds.maxX - gridBounds.minX + 4) * cellSize,
1292
+ (gridBounds.maxZ - gridBounds.minZ + 4) * cellSize
1273
1293
  ]
1274
1294
  }
1275
1295
  ),
@@ -1,7 +1,7 @@
1
- export { C as ContentSegment, D as DEFAULT_CONFIG, a as DomEntityBox, b as DomLayoutData, c as DomOutputsBox, d as DomStateNode, e as DomTransitionLabel, f as DomTransitionPath, E as EntityDefinition, R as RenderOptions, S as StateDefinition, g as StateMachineDefinition, T as TraitSnapshotGetter, h as TransitionDefinition, V as VisualizerConfig, i as bindCanvasCapture, j as bindEventBus, k as bindLastDrawables, l as bindTraitStateGetter, m as clearVerification, n as cn, o as extractOutputsFromTransitions, p as extractStateMachine, q as formatGuard, r as getAllChecks, s as getBridgeHealth, t as getEffectSummary, u as getSnapshot, v as getSummary, w as getTraitSnapshots, x as getTransitions, y as getTransitionsForTrait, z as parseContentSegments, A as parseMarkdownWithCodeBlocks, B as recordServerResponse, F as recordTransition, G as registerCheck, H as registerTraitSnapshot, I as renderStateMachineToDomData, J as renderStateMachineToSvg, K as subscribeToVerification, L as updateAssetStatus, M as updateBridgeHealth, N as updateCheck, O as waitForTransition } from '../cn-BkQrMR01.cjs';
1
+ export { C as ContentSegment, D as DEFAULT_CONFIG, a as DomEntityBox, b as DomLayoutData, c as DomOutputsBox, d as DomStateNode, e as DomTransitionLabel, f as DomTransitionPath, E as EntityDefinition, R as RenderOptions, S as StateDefinition, g as StateMachineDefinition, T as TraitSnapshotGetter, h as TransitionDefinition, V as VisualizerConfig, i as bindCanvasCapture, j as bindEventBus, k as bindLastDrawables, l as bindTraitStateGetter, m as clearVerification, n as cn, o as extractOutputsFromTransitions, p as extractStateMachine, q as formatGuard, r as getAllChecks, s as getBridgeHealth, t as getEffectSummary, u as getSnapshot, v as getSummary, w as getTraitSnapshots, x as getTransitions, y as getTransitionsForTrait, z as parseContentSegments, A as parseMarkdownWithCodeBlocks, B as recordServerResponse, F as recordTransition, G as registerCheck, H as registerTraitSnapshot, I as renderStateMachineToDomData, J as renderStateMachineToSvg, K as subscribeToVerification, L as updateAssetStatus, M as updateBridgeHealth, N as updateCheck, O as waitForTransition } from '../cn-D3H9UzCW.cjs';
2
2
  import { FieldValue, EntityRow, EventPayload } from '@almadar/core';
3
3
  export { AssetLoadStatus, BridgeHealth, CheckStatus, EffectTrace, EventLogEntry, OrbitalVerificationAPI, ServerResponseTrace, TraitStateSnapshot, TransitionTrace, VerificationCheck, VerificationSnapshot, VerificationSummary } from '@almadar/core';
4
- import '../paintDispatch-DR942knx.cjs';
4
+ import '../paintDispatch-BXJgISot.cjs';
5
5
  import 'clsx';
6
6
 
7
7
  /**
@@ -1,7 +1,7 @@
1
- export { C as ContentSegment, D as DEFAULT_CONFIG, a as DomEntityBox, b as DomLayoutData, c as DomOutputsBox, d as DomStateNode, e as DomTransitionLabel, f as DomTransitionPath, E as EntityDefinition, R as RenderOptions, S as StateDefinition, g as StateMachineDefinition, T as TraitSnapshotGetter, h as TransitionDefinition, V as VisualizerConfig, i as bindCanvasCapture, j as bindEventBus, k as bindLastDrawables, l as bindTraitStateGetter, m as clearVerification, n as cn, o as extractOutputsFromTransitions, p as extractStateMachine, q as formatGuard, r as getAllChecks, s as getBridgeHealth, t as getEffectSummary, u as getSnapshot, v as getSummary, w as getTraitSnapshots, x as getTransitions, y as getTransitionsForTrait, z as parseContentSegments, A as parseMarkdownWithCodeBlocks, B as recordServerResponse, F as recordTransition, G as registerCheck, H as registerTraitSnapshot, I as renderStateMachineToDomData, J as renderStateMachineToSvg, K as subscribeToVerification, L as updateAssetStatus, M as updateBridgeHealth, N as updateCheck, O as waitForTransition } from '../cn-CTEFm9PC.js';
1
+ export { C as ContentSegment, D as DEFAULT_CONFIG, a as DomEntityBox, b as DomLayoutData, c as DomOutputsBox, d as DomStateNode, e as DomTransitionLabel, f as DomTransitionPath, E as EntityDefinition, R as RenderOptions, S as StateDefinition, g as StateMachineDefinition, T as TraitSnapshotGetter, h as TransitionDefinition, V as VisualizerConfig, i as bindCanvasCapture, j as bindEventBus, k as bindLastDrawables, l as bindTraitStateGetter, m as clearVerification, n as cn, o as extractOutputsFromTransitions, p as extractStateMachine, q as formatGuard, r as getAllChecks, s as getBridgeHealth, t as getEffectSummary, u as getSnapshot, v as getSummary, w as getTraitSnapshots, x as getTransitions, y as getTransitionsForTrait, z as parseContentSegments, A as parseMarkdownWithCodeBlocks, B as recordServerResponse, F as recordTransition, G as registerCheck, H as registerTraitSnapshot, I as renderStateMachineToDomData, J as renderStateMachineToSvg, K as subscribeToVerification, L as updateAssetStatus, M as updateBridgeHealth, N as updateCheck, O as waitForTransition } from '../cn-Dm0VrLRG.js';
2
2
  import { FieldValue, EntityRow, EventPayload } from '@almadar/core';
3
3
  export { AssetLoadStatus, BridgeHealth, CheckStatus, EffectTrace, EventLogEntry, OrbitalVerificationAPI, ServerResponseTrace, TraitStateSnapshot, TransitionTrace, VerificationCheck, VerificationSnapshot, VerificationSummary } from '@almadar/core';
4
- import '../paintDispatch-DR942knx.js';
4
+ import '../paintDispatch-BXJgISot.js';
5
5
  import 'clsx';
6
6
 
7
7
  /**