@almadar/ui 5.107.0 → 5.109.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.
Files changed (83) hide show
  1. package/dist/avl/index.cjs +1427 -2828
  2. package/dist/avl/index.js +525 -1926
  3. package/dist/components/core/atoms/Box.d.ts +2 -0
  4. package/dist/components/{game → core}/atoms/GameIcon.d.ts +1 -1
  5. package/dist/components/core/atoms/index.d.ts +1 -0
  6. package/dist/components/core/templates/index.d.ts +0 -1
  7. package/dist/components/game/molecules/index.d.ts +8 -28
  8. package/dist/components/index.cjs +223 -1691
  9. package/dist/components/index.js +213 -1671
  10. package/dist/lib/drawable/projector.d.ts +1 -1
  11. package/dist/{components/game/three/molecules/GameCanvas3D.d.ts → lib/drawable/three/Canvas3DHost.d.ts} +12 -9
  12. package/dist/lib/drawable/{Drawable3D.d.ts → three/Drawable3D.d.ts} +2 -2
  13. package/dist/lib/drawable/three/GameCamera3D.d.ts +16 -0
  14. package/dist/lib/drawable/three/game3dTheme.d.ts +14 -0
  15. package/dist/{components/game → lib/drawable}/three/hooks/useGameCanvas3DEvents.d.ts +1 -1
  16. package/dist/{components/game → lib/drawable}/three/index.cjs +2019 -2020
  17. package/dist/{components/game → lib/drawable}/three/index.css +3 -3
  18. package/dist/lib/drawable/three/index.d.ts +39 -0
  19. package/dist/{components/game → lib/drawable}/three/index.js +1932 -1934
  20. package/dist/lib/drawable/{mesh3d.d.ts → three/mesh3d.d.ts} +4 -4
  21. package/dist/{components/game/lib → lib}/puzzleObject.d.ts +11 -2
  22. package/dist/marketing/index.d.cts +2 -0
  23. package/dist/{components/game/providers → providers}/GameAudioProvider.d.ts +1 -1
  24. package/dist/providers/index.cjs +1619 -2690
  25. package/dist/providers/index.d.ts +2 -0
  26. package/dist/providers/index.js +827 -1902
  27. package/dist/runtime/index.cjs +1288 -2689
  28. package/dist/runtime/index.js +505 -1906
  29. package/package.json +7 -7
  30. package/dist/components/game/atoms/ActionButton.d.ts +0 -31
  31. package/dist/components/game/atoms/ComboCounter.d.ts +0 -20
  32. package/dist/components/game/atoms/DamageNumber.d.ts +0 -18
  33. package/dist/components/game/atoms/GameCard.d.ts +0 -34
  34. package/dist/components/game/atoms/ItemSlot.d.ts +0 -31
  35. package/dist/components/game/atoms/ResourceCounter.d.ts +0 -26
  36. package/dist/components/game/atoms/Sprite.d.ts +0 -52
  37. package/dist/components/game/atoms/StatusEffect.d.ts +0 -25
  38. package/dist/components/game/atoms/TurnIndicator.d.ts +0 -22
  39. package/dist/components/game/atoms/WaypointMarker.d.ts +0 -23
  40. package/dist/components/game/molecules/EventLog.d.ts +0 -31
  41. package/dist/components/game/molecules/InventoryGrid.d.ts +0 -35
  42. package/dist/components/game/molecules/ObjectRulePanel.d.ts +0 -32
  43. package/dist/components/game/molecules/ResourceBar.d.ts +0 -26
  44. package/dist/components/game/molecules/RuleEditor.d.ts +0 -39
  45. package/dist/components/game/molecules/VariablePanel.d.ts +0 -28
  46. package/dist/components/game/templates/GameTemplate.d.ts +0 -35
  47. package/dist/components/game/three/atoms/SpriteBillboard3D.d.ts +0 -18
  48. package/dist/components/game/three/game3dTheme.d.ts +0 -55
  49. package/dist/components/game/three/index.d.ts +0 -12
  50. package/dist/components/game/three/lib/game3dAssetManifest.d.ts +0 -48
  51. package/dist/components/game/three/molecules/GameCamera3D.d.ts +0 -35
  52. package/dist/components/game/three/molecules/index.d.ts +0 -32
  53. package/dist/components/game/three/patterns.d.ts +0 -16
  54. /package/dist/{components/game/hooks → hooks}/useCamera.d.ts +0 -0
  55. /package/dist/{components/game/hooks → hooks}/useGameAudio.d.ts +0 -0
  56. /package/dist/{components/game/hooks → hooks}/useImageCache.d.ts +0 -0
  57. /package/dist/{components/game/hooks → hooks}/useUnitSpriteAtlas.d.ts +0 -0
  58. /package/dist/{components/game/lib → lib}/boardEntity.d.ts +0 -0
  59. /package/dist/{components/game/three/molecules → lib/drawable/three}/Camera3D.d.ts +0 -0
  60. /package/dist/{components/game/three/molecules → lib/drawable/three}/Canvas3DErrorBoundary.d.ts +0 -0
  61. /package/dist/{components/game/three/molecules → lib/drawable/three}/Canvas3DLoadingState.d.ts +0 -0
  62. /package/dist/{components/game/three/molecules → lib/drawable/three}/Lighting3D.d.ts +0 -0
  63. /package/dist/{components/game/three/molecules → lib/drawable/three}/ModelLoader.d.ts +0 -0
  64. /package/dist/{components/game/three/molecules → lib/drawable/three}/Scene3D.d.ts +0 -0
  65. /package/dist/{components/game → lib/drawable}/three/hooks/useAssetLoader.d.ts +0 -0
  66. /package/dist/{components/game → lib/drawable}/three/hooks/useRaycaster.d.ts +0 -0
  67. /package/dist/{components/game → lib/drawable}/three/hooks/useSceneGraph.d.ts +0 -0
  68. /package/dist/{components/game → lib/drawable}/three/hooks/useThree.d.ts +0 -0
  69. /package/dist/{components/game → lib/drawable}/three/lib/AssetLoader.d.ts +0 -0
  70. /package/dist/{components/game → lib/drawable}/three/lib/culling.d.ts +0 -0
  71. /package/dist/{components/game → lib/drawable}/three/lib/grid3D.d.ts +0 -0
  72. /package/dist/{components/game/lib → lib}/editorUtils.d.ts +0 -0
  73. /package/dist/{components/game/lib → lib}/game.d.ts +0 -0
  74. /package/dist/{components/game/lib/index.d.ts → lib/gameShared.d.ts} +0 -0
  75. /package/dist/{components/game/lib → lib}/isometric.d.ts +0 -0
  76. /package/dist/{components/game/lib → lib}/isometricTypes.d.ts +0 -0
  77. /package/dist/{components/game/lib → lib}/makeAsset.d.ts +0 -0
  78. /package/dist/{components/game/lib → lib}/mechanics.d.ts +0 -0
  79. /package/dist/{components/game/lib → lib}/physicsPresets.d.ts +0 -0
  80. /package/dist/{components/game/lib → lib}/physicsTypes.d.ts +0 -0
  81. /package/dist/{components/game/lib → lib}/spriteAnimation.d.ts +0 -0
  82. /package/dist/{components/game/lib → lib}/spriteAnimationTypes.d.ts +0 -0
  83. /package/dist/{components/game/lib → lib}/spriteSheetConstants.d.ts +0 -0
@@ -1,19 +1,19 @@
1
1
  'use strict';
2
2
 
3
- var React7 = require('react');
3
+ var React6 = require('react');
4
+ var providers = require('@almadar/ui/providers');
5
+ var logger = require('@almadar/logger');
4
6
  var fiber = require('@react-three/fiber');
5
- var THREE6 = require('three');
6
- var jsxRuntime = require('react/jsx-runtime');
7
+ var THREE9 = require('three');
7
8
  var drei = require('@react-three/drei');
8
- var logger = require('@almadar/logger');
9
+ var jsxRuntime = require('react/jsx-runtime');
9
10
  var GLTFLoader = require('three/examples/jsm/loaders/GLTFLoader');
10
11
  var SkeletonUtils = require('three/examples/jsm/utils/SkeletonUtils');
12
+ var clsx = require('clsx');
13
+ var tailwindMerge = require('tailwind-merge');
11
14
  var OrbitControls_js = require('three/examples/jsm/controls/OrbitControls.js');
12
15
  var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
13
16
  var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
14
- var providers = require('@almadar/ui/providers');
15
- var clsx = require('clsx');
16
- var tailwindMerge = require('tailwind-merge');
17
17
  var postprocessing = require('@react-three/postprocessing');
18
18
  var hooks = require('@almadar/ui/hooks');
19
19
 
@@ -37,187 +37,299 @@ function _interopNamespace(e) {
37
37
  return Object.freeze(n);
38
38
  }
39
39
 
40
- var React7__default = /*#__PURE__*/_interopDefault(React7);
41
- var THREE6__namespace = /*#__PURE__*/_interopNamespace(THREE6);
40
+ var React6__default = /*#__PURE__*/_interopDefault(React6);
41
+ var THREE9__namespace = /*#__PURE__*/_interopNamespace(THREE9);
42
42
 
43
43
  var __defProp = Object.defineProperty;
44
44
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
45
45
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
46
- function Scene3D({ background = "#1a1a2e", fog, children }) {
47
- const { scene } = fiber.useThree();
48
- const initializedRef = React7.useRef(false);
49
- React7.useEffect(() => {
50
- if (initializedRef.current) return;
51
- initializedRef.current = true;
52
- if (background.startsWith("#") || background.startsWith("rgb")) {
53
- scene.background = new THREE6__namespace.Color(background);
54
- } else {
55
- const loader = new THREE6__namespace.TextureLoader();
56
- loader.load(background, (texture) => {
57
- scene.background = texture;
46
+ var log = logger.createLogger("almadar:eventbus");
47
+ var subLog = logger.createLogger("almadar:eventbus:subscribe");
48
+ var scopeLog = logger.createLogger("almadar:ui:trait-scope");
49
+ function getGlobalEventBus() {
50
+ if (typeof window !== "undefined") {
51
+ return window.__kflowEventBus ?? null;
52
+ }
53
+ return null;
54
+ }
55
+ var fallbackListeners = /* @__PURE__ */ new Map();
56
+ var fallbackAnyListeners = /* @__PURE__ */ new Set();
57
+ var fallbackEventBus = {
58
+ emit: (type, payload, source) => {
59
+ const event = {
60
+ type,
61
+ payload,
62
+ timestamp: Date.now(),
63
+ source
64
+ };
65
+ const handlers = fallbackListeners.get(type);
66
+ log.debug("emit", { type, payloadKeys: payload ? Object.keys(payload).length : 0, listenerCount: (handlers?.size ?? 0) + fallbackAnyListeners.size });
67
+ if (handlers) {
68
+ handlers.forEach((handler) => {
69
+ try {
70
+ handler(event);
71
+ } catch (error) {
72
+ log.error("Error in listener", { type, error: error instanceof Error ? error : String(error) });
73
+ }
58
74
  });
59
75
  }
60
- if (fog) {
61
- scene.fog = new THREE6__namespace.Fog(fog.color, fog.near, fog.far);
62
- }
63
- return () => {
64
- scene.background = null;
65
- scene.fog = null;
66
- };
67
- }, [scene, background, fog]);
68
- return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
69
- }
70
- var Camera3D = React7.forwardRef(
71
- ({
72
- mode = "isometric",
73
- position = [10, 10, 10],
74
- target = [0, 0, 0],
75
- zoom = 1,
76
- fov = 45,
77
- enableOrbit = true,
78
- minDistance = 2,
79
- maxDistance = 100,
80
- onChange
81
- }, ref) => {
82
- const { camera, set, viewport } = fiber.useThree();
83
- const controlsRef = React7.useRef(null);
84
- const initialPosition = React7.useRef(new THREE6__namespace.Vector3(...position));
85
- const initialTarget = React7.useRef(new THREE6__namespace.Vector3(...target));
86
- React7.useEffect(() => {
87
- let newCamera;
88
- if (mode === "isometric") {
89
- const aspect = viewport.aspect;
90
- const size = 10 / zoom;
91
- newCamera = new THREE6__namespace.OrthographicCamera(
92
- -size * aspect,
93
- size * aspect,
94
- size,
95
- -size,
96
- 0.1,
97
- 1e3
98
- );
99
- } else {
100
- newCamera = new THREE6__namespace.PerspectiveCamera(fov, viewport.aspect, 0.1, 1e3);
101
- }
102
- newCamera.position.copy(initialPosition.current);
103
- newCamera.lookAt(initialTarget.current.x, initialTarget.current.y, initialTarget.current.z);
104
- set({ camera: newCamera });
105
- if (mode === "top-down") {
106
- newCamera.position.set(0, 20 / zoom, 0);
107
- newCamera.lookAt(0, 0, 0);
108
- }
109
- return () => {
110
- };
111
- }, [mode, fov, zoom, viewport.aspect, set]);
112
- fiber.useFrame(() => {
113
- if (onChange) {
114
- onChange(camera);
76
+ fallbackAnyListeners.forEach((handler) => {
77
+ try {
78
+ handler(event);
79
+ } catch (error) {
80
+ log.error("Error in onAny listener", { type, error: error instanceof Error ? error : String(error) });
115
81
  }
116
82
  });
117
- React7.useImperativeHandle(ref, () => ({
118
- getCamera: () => camera,
119
- setPosition: (x, y, z) => {
120
- camera.position.set(x, y, z);
121
- if (controlsRef.current) {
122
- controlsRef.current.update();
123
- }
124
- },
125
- lookAt: (x, y, z) => {
126
- camera.lookAt(x, y, z);
127
- if (controlsRef.current) {
128
- controlsRef.current.target.set(x, y, z);
129
- controlsRef.current.update();
130
- }
131
- },
132
- reset: () => {
133
- camera.position.copy(initialPosition.current);
134
- camera.lookAt(initialTarget.current.x, initialTarget.current.y, initialTarget.current.z);
135
- if (controlsRef.current) {
136
- controlsRef.current.target.copy(initialTarget.current);
137
- controlsRef.current.update();
83
+ },
84
+ on: (type, listener) => {
85
+ if (!fallbackListeners.has(type)) {
86
+ fallbackListeners.set(type, /* @__PURE__ */ new Set());
87
+ }
88
+ fallbackListeners.get(type).add(listener);
89
+ subLog.debug("subscribe", { type, totalListeners: fallbackListeners.get(type).size });
90
+ return () => {
91
+ const handlers = fallbackListeners.get(type);
92
+ if (handlers) {
93
+ handlers.delete(listener);
94
+ if (handlers.size === 0) {
95
+ fallbackListeners.delete(type);
138
96
  }
139
- },
140
- getViewBounds: () => {
141
- const min = new THREE6__namespace.Vector3(-10, -10, -10);
142
- const max = new THREE6__namespace.Vector3(10, 10, 10);
143
- return { min, max };
144
- }
145
- }));
146
- const maxPolarAngle = mode === "top-down" ? 0.1 : Math.PI / 2 - 0.1;
147
- return /* @__PURE__ */ jsxRuntime.jsx(
148
- drei.OrbitControls,
149
- {
150
- ref: controlsRef,
151
- camera,
152
- enabled: enableOrbit,
153
- target: [initialTarget.current.x, initialTarget.current.y, initialTarget.current.z],
154
- minDistance,
155
- maxDistance,
156
- maxPolarAngle,
157
- enableDamping: true,
158
- dampingFactor: 0.05
159
97
  }
160
- );
98
+ };
99
+ },
100
+ once: (type, listener) => {
101
+ const wrappedListener = (event) => {
102
+ fallbackListeners.get(type)?.delete(wrappedListener);
103
+ listener(event);
104
+ };
105
+ return fallbackEventBus.on(type, wrappedListener);
106
+ },
107
+ hasListeners: (type) => {
108
+ const handlers = fallbackListeners.get(type);
109
+ return handlers !== void 0 && handlers.size > 0;
110
+ },
111
+ onAny: (listener) => {
112
+ fallbackAnyListeners.add(listener);
113
+ subLog.debug("subscribe:any", { totalAnyListeners: fallbackAnyListeners.size });
114
+ return () => {
115
+ fallbackAnyListeners.delete(listener);
116
+ };
161
117
  }
162
- );
163
- Camera3D.displayName = "Camera3D";
164
- function Lighting3D({
165
- ambientIntensity = 0.6,
166
- ambientColor = "#ffffff",
167
- directionalIntensity = 0.8,
168
- directionalColor = "#ffffff",
169
- directionalPosition = [10, 20, 10],
170
- shadows = true,
171
- shadowMapSize = 2048,
172
- shadowCameraSize = 20,
173
- shadowBias = -1e-3,
174
- shadowNormalBias,
175
- shadowCameraNear = 0.1,
176
- shadowCameraFar = 100,
177
- showHelpers = false
178
- }) {
179
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
180
- /* @__PURE__ */ jsxRuntime.jsx("ambientLight", { intensity: ambientIntensity, color: ambientColor }),
181
- /* @__PURE__ */ jsxRuntime.jsx(
182
- "directionalLight",
183
- {
184
- position: directionalPosition,
185
- intensity: directionalIntensity,
186
- color: directionalColor,
187
- castShadow: shadows,
188
- "shadow-mapSize": [shadowMapSize, shadowMapSize],
189
- "shadow-camera-left": -shadowCameraSize,
190
- "shadow-camera-right": shadowCameraSize,
191
- "shadow-camera-top": shadowCameraSize,
192
- "shadow-camera-bottom": -shadowCameraSize,
193
- "shadow-camera-near": shadowCameraNear,
194
- "shadow-camera-far": shadowCameraFar,
195
- "shadow-bias": shadowBias,
196
- "shadow-normalBias": shadowNormalBias
197
- }
198
- ),
199
- /* @__PURE__ */ jsxRuntime.jsx(
200
- "hemisphereLight",
201
- {
202
- intensity: 0.3,
203
- color: "#87ceeb",
204
- groundColor: "#362d1d"
205
- }
206
- ),
207
- showHelpers && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx(
208
- "directionalLightHelper",
209
- {
210
- args: [
211
- new THREE6__namespace.DirectionalLight(directionalColor, directionalIntensity),
212
- 5
213
- ]
118
+ };
119
+ function useEventBus() {
120
+ const context = React6.useContext(providers.EventBusContext);
121
+ const baseBus = context ?? getGlobalEventBus() ?? fallbackEventBus;
122
+ const scope = providers.useTraitScope();
123
+ return React6.useMemo(() => {
124
+ if (!scope) {
125
+ return {
126
+ ...baseBus,
127
+ emit: (type, payload, source) => {
128
+ if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
129
+ scopeLog.warn("emit:bare-key-no-scope", { type });
130
+ }
131
+ baseBus.emit(type, payload, source);
132
+ }
133
+ };
134
+ }
135
+ return {
136
+ ...baseBus,
137
+ emit: (type, payload, source) => {
138
+ if (typeof type === "string" && type.startsWith("UI:")) {
139
+ const tail = type.slice(3);
140
+ const qualified = tail.includes(".") ? type : `UI:${scope.orbital}.${scope.trait}.${tail}`;
141
+ if (qualified !== type) {
142
+ scopeLog.info("emit:qualified", {
143
+ from: type,
144
+ to: qualified,
145
+ scopeOrbital: scope.orbital,
146
+ scopeTrait: scope.trait
147
+ });
148
+ }
149
+ baseBus.emit(qualified, payload, source);
150
+ return;
151
+ }
152
+ baseBus.emit(type, payload, source);
214
153
  }
215
- ) })
216
- ] });
154
+ };
155
+ }, [baseBus, scope]);
217
156
  }
218
- function Canvas3DLoadingState({
219
- progress = 0,
220
- loaded = 0,
157
+ function useEmitEvent() {
158
+ const eventBus = useEventBus();
159
+ return React6.useCallback(
160
+ (type, payload, source) => {
161
+ eventBus.emit(type, payload, source);
162
+ },
163
+ [eventBus]
164
+ );
165
+ }
166
+
167
+ // lib/drawable/hitTest.ts
168
+ function collectDrawnItems(nodes) {
169
+ const out = [];
170
+ for (const n of nodes) {
171
+ switch (n.type) {
172
+ case "draw-sprite":
173
+ case "draw-shape":
174
+ case "draw-text":
175
+ out.push({ pos: n.position, id: n.id });
176
+ break;
177
+ case "draw-sprite-layer":
178
+ case "draw-shape-layer":
179
+ case "draw-text-layer":
180
+ for (const it of n.items) out.push({ pos: it.position, id: it.id });
181
+ break;
182
+ }
183
+ }
184
+ return out;
185
+ }
186
+ function buildHitIndex(items) {
187
+ const m = /* @__PURE__ */ new Map();
188
+ for (const it of items) {
189
+ if (it.id !== void 0) m.set(`${it.pos.x},${it.pos.y}`, it.id);
190
+ }
191
+ return m;
192
+ }
193
+ function useGameCanvas3DEvents(options) {
194
+ const {
195
+ tileClickEvent,
196
+ unitClickEvent,
197
+ featureClickEvent,
198
+ canvasClickEvent,
199
+ tileHoverEvent,
200
+ tileLeaveEvent,
201
+ unitAnimationEvent,
202
+ cameraChangeEvent,
203
+ onTileClick,
204
+ onUnitClick,
205
+ onFeatureClick,
206
+ onCanvasClick,
207
+ onTileHover,
208
+ onUnitAnimation
209
+ } = options;
210
+ const emit = useEmitEvent();
211
+ const optionsRef = React6.useRef(options);
212
+ optionsRef.current = options;
213
+ const handleTileClick = React6.useCallback(
214
+ (tile, event) => {
215
+ if (tileClickEvent) {
216
+ emit(tileClickEvent, {
217
+ tileId: tile.id,
218
+ x: tile.x,
219
+ z: tile.z ?? tile.y ?? 0,
220
+ type: tile.type,
221
+ terrain: tile.terrain,
222
+ elevation: tile.elevation
223
+ });
224
+ }
225
+ optionsRef.current.onTileClick?.(tile, event);
226
+ },
227
+ [tileClickEvent, emit]
228
+ );
229
+ const handleUnitClick = React6.useCallback(
230
+ (unit, event) => {
231
+ if (unitClickEvent) {
232
+ emit(unitClickEvent, {
233
+ unitId: unit.id,
234
+ x: unit.x,
235
+ z: unit.z ?? unit.y ?? 0,
236
+ unitType: unit.unitType,
237
+ name: unit.name,
238
+ team: unit.team,
239
+ faction: unit.faction,
240
+ health: unit.health,
241
+ maxHealth: unit.maxHealth
242
+ });
243
+ }
244
+ optionsRef.current.onUnitClick?.(unit, event);
245
+ },
246
+ [unitClickEvent, emit]
247
+ );
248
+ const handleFeatureClick = React6.useCallback(
249
+ (feature, event) => {
250
+ if (featureClickEvent) {
251
+ emit(featureClickEvent, {
252
+ featureId: feature.id,
253
+ x: feature.x,
254
+ z: feature.z ?? feature.y ?? 0,
255
+ type: feature.type,
256
+ elevation: feature.elevation
257
+ });
258
+ }
259
+ optionsRef.current.onFeatureClick?.(feature, event);
260
+ },
261
+ [featureClickEvent, emit]
262
+ );
263
+ const handleCanvasClick = React6.useCallback(
264
+ (event) => {
265
+ if (canvasClickEvent) {
266
+ emit(canvasClickEvent, {
267
+ clientX: event.clientX,
268
+ clientY: event.clientY,
269
+ button: event.button
270
+ });
271
+ }
272
+ optionsRef.current.onCanvasClick?.(event);
273
+ },
274
+ [canvasClickEvent, emit]
275
+ );
276
+ const handleTileHover = React6.useCallback(
277
+ (tile, event) => {
278
+ if (tile) {
279
+ if (tileHoverEvent) {
280
+ emit(tileHoverEvent, {
281
+ tileId: tile.id,
282
+ x: tile.x,
283
+ z: tile.z ?? tile.y ?? 0,
284
+ type: tile.type
285
+ });
286
+ }
287
+ } else {
288
+ if (tileLeaveEvent) {
289
+ emit(tileLeaveEvent, {});
290
+ }
291
+ }
292
+ optionsRef.current.onTileHover?.(tile, event);
293
+ },
294
+ [tileHoverEvent, tileLeaveEvent, emit]
295
+ );
296
+ const handleUnitAnimation = React6.useCallback(
297
+ (unitId, state) => {
298
+ if (unitAnimationEvent) {
299
+ emit(unitAnimationEvent, {
300
+ unitId,
301
+ state,
302
+ timestamp: Date.now()
303
+ });
304
+ }
305
+ optionsRef.current.onUnitAnimation?.(unitId, state);
306
+ },
307
+ [unitAnimationEvent, emit]
308
+ );
309
+ const handleCameraChange = React6.useCallback(
310
+ (position) => {
311
+ if (cameraChangeEvent) {
312
+ emit(cameraChangeEvent, {
313
+ position,
314
+ timestamp: Date.now()
315
+ });
316
+ }
317
+ },
318
+ [cameraChangeEvent, emit]
319
+ );
320
+ return {
321
+ handleTileClick,
322
+ handleUnitClick,
323
+ handleFeatureClick,
324
+ handleCanvasClick,
325
+ handleTileHover,
326
+ handleUnitAnimation,
327
+ handleCameraChange
328
+ };
329
+ }
330
+ function Canvas3DLoadingState({
331
+ progress = 0,
332
+ loaded = 0,
221
333
  total = 0,
222
334
  message = "Loading 3D Scene...",
223
335
  details,
@@ -260,8 +372,8 @@ function Canvas3DLoadingState({
260
372
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "canvas-3d-loading__background", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg__grid" }) })
261
373
  ] });
262
374
  }
263
- var log = logger.createLogger("almadar:ui:game:canvas3d:error-boundary");
264
- var Canvas3DErrorBoundary = class extends React7.Component {
375
+ var log2 = logger.createLogger("almadar:ui:game:canvas3d:error-boundary");
376
+ var Canvas3DErrorBoundary = class extends React6.Component {
265
377
  constructor(props) {
266
378
  super(props);
267
379
  __publicField(this, "handleReset", () => {
@@ -288,8 +400,8 @@ var Canvas3DErrorBoundary = class extends React7.Component {
288
400
  componentDidCatch(error, errorInfo) {
289
401
  this.setState({ errorInfo });
290
402
  this.props.onError?.(error, errorInfo);
291
- log.error("Error caught", { error });
292
- log.error("Component stack", { componentStack: errorInfo.componentStack ?? "<none>" });
403
+ log2.error("Error caught", { error });
404
+ log2.error("Component stack", { componentStack: errorInfo.componentStack ?? "<none>" });
293
405
  }
294
406
  render() {
295
407
  if (this.state.hasError) {
@@ -332,7 +444,92 @@ var Canvas3DErrorBoundary = class extends React7.Component {
332
444
  return this.props.children;
333
445
  }
334
446
  };
335
- var log2 = logger.createLogger("almadar:ui:game:model-loader");
447
+ function Lighting3D({
448
+ ambientIntensity = 0.6,
449
+ ambientColor = "#ffffff",
450
+ directionalIntensity = 0.8,
451
+ directionalColor = "#ffffff",
452
+ directionalPosition = [10, 20, 10],
453
+ shadows = true,
454
+ shadowMapSize = 2048,
455
+ shadowCameraSize = 20,
456
+ shadowBias = -1e-3,
457
+ shadowNormalBias,
458
+ shadowCameraNear = 0.1,
459
+ shadowCameraFar = 100,
460
+ showHelpers = false
461
+ }) {
462
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
463
+ /* @__PURE__ */ jsxRuntime.jsx("ambientLight", { intensity: ambientIntensity, color: ambientColor }),
464
+ /* @__PURE__ */ jsxRuntime.jsx(
465
+ "directionalLight",
466
+ {
467
+ position: directionalPosition,
468
+ intensity: directionalIntensity,
469
+ color: directionalColor,
470
+ castShadow: shadows,
471
+ "shadow-mapSize": [shadowMapSize, shadowMapSize],
472
+ "shadow-camera-left": -shadowCameraSize,
473
+ "shadow-camera-right": shadowCameraSize,
474
+ "shadow-camera-top": shadowCameraSize,
475
+ "shadow-camera-bottom": -shadowCameraSize,
476
+ "shadow-camera-near": shadowCameraNear,
477
+ "shadow-camera-far": shadowCameraFar,
478
+ "shadow-bias": shadowBias,
479
+ "shadow-normalBias": shadowNormalBias
480
+ }
481
+ ),
482
+ /* @__PURE__ */ jsxRuntime.jsx(
483
+ "hemisphereLight",
484
+ {
485
+ intensity: 0.3,
486
+ color: "#87ceeb",
487
+ groundColor: "#362d1d"
488
+ }
489
+ ),
490
+ showHelpers && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx(
491
+ "directionalLightHelper",
492
+ {
493
+ args: [
494
+ new THREE9__namespace.DirectionalLight(directionalColor, directionalIntensity),
495
+ 5
496
+ ]
497
+ }
498
+ ) })
499
+ ] });
500
+ }
501
+ function CameraController3D({
502
+ onCameraChange
503
+ }) {
504
+ const { camera } = fiber.useThree();
505
+ React6.useEffect(() => {
506
+ if (onCameraChange) {
507
+ onCameraChange({
508
+ x: camera.position.x,
509
+ y: camera.position.y,
510
+ z: camera.position.z
511
+ });
512
+ }
513
+ }, [camera.position, onCameraChange]);
514
+ return null;
515
+ }
516
+ function FollowCamera3D({
517
+ target,
518
+ offset
519
+ }) {
520
+ const { camera } = fiber.useThree();
521
+ const look = React6.useRef(new THREE9__namespace.Vector3(target[0], target[1], target[2]));
522
+ const goal = React6.useRef(new THREE9__namespace.Vector3());
523
+ fiber.useFrame((_, delta) => {
524
+ const t = Math.min(1, delta * 5);
525
+ goal.current.set(target[0] + offset[0], target[1] + offset[1], target[2] + offset[2]);
526
+ camera.position.lerp(goal.current, t);
527
+ look.current.lerp(goal.current.set(target[0], target[1], target[2]), t);
528
+ camera.lookAt(look.current);
529
+ });
530
+ return null;
531
+ }
532
+ var log3 = logger.createLogger("almadar:ui:game:model-loader");
336
533
  var gltfCache = /* @__PURE__ */ new Map();
337
534
  function loadGltfCached(url, assetRoot) {
338
535
  const key = `${assetRoot}|${url}`;
@@ -345,13 +542,13 @@ function loadGltfCached(url, assetRoot) {
345
542
  loader.load(
346
543
  url,
347
544
  (gltf) => {
348
- log2.debug("Loaded", { url, clips: gltf.animations.length });
545
+ log3.debug("Loaded", { url, clips: gltf.animations.length });
349
546
  pending.gltf = { scene: gltf.scene, animations: gltf.animations };
350
547
  resolveDone();
351
548
  },
352
549
  void 0,
353
550
  (err) => {
354
- log2.warn("Failed", { url, error: err instanceof Error ? err : String(err) });
551
+ log3.warn("Failed", { url, error: err instanceof Error ? err : String(err) });
355
552
  pending.error = err instanceof Error ? err : new Error(String(err));
356
553
  resolveDone();
357
554
  }
@@ -376,10 +573,10 @@ function stateFromEntry(entry) {
376
573
  return { model: null, clips: [], isLoading: !entry.error, error: entry.error ?? null };
377
574
  }
378
575
  function useGLTFModel(url, resourceBasePath) {
379
- const [state, setState] = React7.useState(
576
+ const [state, setState] = React6.useState(
380
577
  () => stateFromEntry(url ? loadGltfCached(url, resourceBasePath || detectAssetRoot(url)) : null)
381
578
  );
382
- React7.useEffect(() => {
579
+ React6.useEffect(() => {
383
580
  if (!url) {
384
581
  setState({ model: null, clips: [], isLoading: false, error: null });
385
582
  return;
@@ -414,16 +611,16 @@ function ModelLoader({
414
611
  tint
415
612
  }) {
416
613
  const { model: loadedModel, clips, isLoading, error } = useGLTFModel(url, resourceBasePath);
417
- const model = React7.useMemo(() => {
614
+ const model = React6.useMemo(() => {
418
615
  if (!loadedModel) return null;
419
616
  const cloned = SkeletonUtils.clone(loadedModel);
420
617
  cloned.updateMatrixWorld(true);
421
- const tintColor = tint ? new THREE6__namespace.Color(tint) : null;
618
+ const tintColor = tint ? new THREE9__namespace.Color(tint) : null;
422
619
  cloned.traverse((child) => {
423
- if (child instanceof THREE6__namespace.Mesh) {
620
+ if (child instanceof THREE9__namespace.Mesh) {
424
621
  child.castShadow = castShadow;
425
622
  child.receiveShadow = receiveShadow;
426
- if (tintColor && child.material instanceof THREE6__namespace.MeshStandardMaterial) {
623
+ if (tintColor && child.material instanceof THREE9__namespace.MeshStandardMaterial) {
427
624
  const mat = child.material.clone();
428
625
  mat.color.multiply(tintColor);
429
626
  child.material = mat;
@@ -432,8 +629,8 @@ function ModelLoader({
432
629
  });
433
630
  return cloned;
434
631
  }, [loadedModel, castShadow, receiveShadow, tint]);
435
- const mixer = React7.useMemo(() => model ? new THREE6__namespace.AnimationMixer(model) : null, [model]);
436
- React7.useEffect(() => {
632
+ const mixer = React6.useMemo(() => model ? new THREE9__namespace.AnimationMixer(model) : null, [model]);
633
+ React6.useEffect(() => {
437
634
  if (!mixer || !animation || clips.length === 0) return;
438
635
  const wanted = animation.toLowerCase();
439
636
  const clip = clips.find((c) => c.name === animation) ?? clips.find((c) => c.name.toLowerCase() === wanted);
@@ -447,20 +644,20 @@ function ModelLoader({
447
644
  fiber.useFrame((_, delta) => {
448
645
  mixer?.update(delta);
449
646
  });
450
- const normFactor = React7.useMemo(() => {
647
+ const normFactor = React6.useMemo(() => {
451
648
  if (!model) return 1;
452
- const box = new THREE6__namespace.Box3().setFromObject(model);
453
- const size = new THREE6__namespace.Vector3();
649
+ const box = new THREE9__namespace.Box3().setFromObject(model);
650
+ const size = new THREE9__namespace.Vector3();
454
651
  box.getSize(size);
455
652
  const maxDim = Math.max(size.x, size.y, size.z);
456
653
  if (!Number.isFinite(maxDim) || maxDim < 0.05) return 1;
457
654
  return 1 / maxDim;
458
655
  }, [model]);
459
- const scaleArray = React7.useMemo(() => {
656
+ const scaleArray = React6.useMemo(() => {
460
657
  const base = typeof scale === "number" ? [scale, scale, scale] : scale;
461
658
  return [base[0] * normFactor, base[1] * normFactor, base[2] * normFactor];
462
659
  }, [scale, normFactor]);
463
- const rotationRad = React7.useMemo(() => {
660
+ const rotationRad = React6.useMemo(() => {
464
661
  return [
465
662
  rotation[0] * Math.PI / 180,
466
663
  rotation[1] * Math.PI / 180,
@@ -533,1576 +730,1377 @@ function ModelLoader({
533
730
  }
534
731
  );
535
732
  }
536
- function detectAssetRoot2(modelUrl) {
537
- const idx = modelUrl.indexOf("/3d/");
538
- if (idx !== -1) {
539
- return modelUrl.substring(0, idx + 4);
540
- }
541
- return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
542
- }
543
- function createGLTFLoaderForUrl(url) {
544
- const loader = new GLTFLoader_js.GLTFLoader();
545
- loader.setResourcePath(detectAssetRoot2(url));
546
- return loader;
547
- }
548
- var AssetLoader = class {
549
- constructor() {
550
- __publicField(this, "objLoader");
551
- __publicField(this, "textureLoader");
552
- __publicField(this, "modelCache");
553
- __publicField(this, "textureCache");
554
- __publicField(this, "loadingPromises");
555
- this.objLoader = new OBJLoader_js.OBJLoader();
556
- this.textureLoader = new THREE6__namespace.TextureLoader();
557
- this.modelCache = /* @__PURE__ */ new Map();
558
- this.textureCache = /* @__PURE__ */ new Map();
559
- this.loadingPromises = /* @__PURE__ */ new Map();
560
- }
561
- /**
562
- * Load a GLB/GLTF model
563
- * @param url - URL to the .glb or .gltf file
564
- * @returns Promise with loaded model scene and animations
565
- */
566
- async loadModel(url) {
567
- if (this.modelCache.has(url)) {
568
- return this.modelCache.get(url);
569
- }
570
- if (this.loadingPromises.has(url)) {
571
- return this.loadingPromises.get(url);
572
- }
573
- const loader = createGLTFLoaderForUrl(url);
574
- const loadPromise = loader.loadAsync(url).then((gltf) => {
575
- const result = {
576
- scene: gltf.scene,
577
- animations: gltf.animations || []
578
- };
579
- this.modelCache.set(url, result);
580
- this.loadingPromises.delete(url);
581
- return result;
582
- }).catch((error) => {
583
- this.loadingPromises.delete(url);
584
- throw new Error(`Failed to load model ${url}: ${error.message}`);
585
- });
586
- this.loadingPromises.set(url, loadPromise);
587
- return loadPromise;
733
+ function SpriteBillboard({ node, world }) {
734
+ const texture = fiber.useLoader(THREE9__namespace.TextureLoader, node.asset.url);
735
+ const img = texture.image;
736
+ const imgW = img?.width || 1;
737
+ const imgH = img?.height || 1;
738
+ if (node.frame) {
739
+ texture.repeat.set(node.frame.w / imgW, node.frame.h / imgH);
740
+ texture.offset.set(node.frame.x / imgW, 1 - (node.frame.y + node.frame.h) / imgH);
741
+ texture.magFilter = texture.minFilter = THREE9__namespace.NearestFilter;
742
+ texture.needsUpdate = true;
588
743
  }
589
- /**
590
- * Load an OBJ model (fallback for non-GLB assets)
591
- * @param url - URL to the .obj file
592
- * @returns Promise with loaded object group
593
- */
594
- async loadOBJ(url) {
595
- if (this.modelCache.has(url)) {
596
- return this.modelCache.get(url).scene;
597
- }
598
- if (this.loadingPromises.has(url)) {
599
- const result = await this.loadingPromises.get(url);
600
- return result.scene;
601
- }
602
- const loadPromise = this.objLoader.loadAsync(url).then((group) => {
603
- const result = {
604
- scene: group,
605
- animations: []
606
- };
607
- this.modelCache.set(url, result);
608
- this.loadingPromises.delete(url);
609
- return result;
610
- }).catch((error) => {
611
- this.loadingPromises.delete(url);
612
- throw new Error(`Failed to load OBJ ${url}: ${error.message}`);
613
- });
614
- this.loadingPromises.set(url, loadPromise);
615
- return (await loadPromise).scene;
744
+ const aspect = node.frame ? node.frame.w / node.frame.h : imgW / imgH;
745
+ const height = node.height ?? 1;
746
+ const width = node.width ?? height * aspect;
747
+ return /* @__PURE__ */ jsxRuntime.jsx(drei.Billboard, { position: [world[0], world[1] + height / 2, world[2]], children: /* @__PURE__ */ jsxRuntime.jsxs("mesh", { children: [
748
+ /* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [width, height] }),
749
+ /* @__PURE__ */ jsxRuntime.jsx(
750
+ "meshBasicMaterial",
751
+ {
752
+ map: texture,
753
+ transparent: true,
754
+ alphaTest: 0.1,
755
+ side: THREE9__namespace.DoubleSide,
756
+ opacity: node.opacity ?? 1
757
+ }
758
+ )
759
+ ] }) });
760
+ }
761
+ function Sprite3D({ node, projector }) {
762
+ if (node.asset.dimension === "3d" && node.asset.url) {
763
+ return /* @__PURE__ */ jsxRuntime.jsx("group", { position: projector.toWorld(node.position), children: /* @__PURE__ */ jsxRuntime.jsx(
764
+ ModelLoader,
765
+ {
766
+ url: node.asset.url,
767
+ scale: node.width ?? projector.cellSize,
768
+ rotation: [0, node.rotation ?? 0, 0],
769
+ fallbackGeometry: "box",
770
+ castShadow: true,
771
+ receiveShadow: true
772
+ }
773
+ ) });
616
774
  }
617
- /**
618
- * Load a texture
619
- * @param url - URL to the texture image
620
- * @returns Promise with loaded texture
621
- */
622
- async loadTexture(url) {
623
- if (this.textureCache.has(url)) {
624
- return this.textureCache.get(url);
775
+ if (!node.asset.url) return null;
776
+ return /* @__PURE__ */ jsxRuntime.jsx(SpriteBillboard, { node, world: projector.toWorld(node.position) });
777
+ }
778
+ function Shape3D({ node, projector }) {
779
+ const world = projector.toWorld(node.position);
780
+ const color = node.fill ?? node.stroke ?? "#ffffff";
781
+ let geometry;
782
+ switch (node.shape) {
783
+ case "cell":
784
+ case "rect": {
785
+ const w = node.shape === "cell" ? projector.cellSize * 0.95 : node.width ?? projector.cellSize;
786
+ const h = node.shape === "cell" ? projector.cellSize * 0.95 : node.height ?? projector.cellSize;
787
+ geometry = /* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [w, h] });
788
+ break;
625
789
  }
626
- if (this.loadingPromises.has(`texture:${url}`)) {
627
- return this.loadingPromises.get(`texture:${url}`);
790
+ case "ellipse": {
791
+ const radiusX = node.radiusX ?? 0.4;
792
+ geometry = node.stroke && !node.fill ? /* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [Math.max(0, radiusX - 0.08), radiusX, 32] }) : /* @__PURE__ */ jsxRuntime.jsx("circleGeometry", { args: [radiusX, 32] });
793
+ break;
628
794
  }
629
- const loadPromise = this.textureLoader.loadAsync(url).then((texture) => {
630
- texture.colorSpace = THREE6__namespace.SRGBColorSpace;
631
- this.textureCache.set(url, texture);
632
- this.loadingPromises.delete(`texture:${url}`);
633
- return texture;
634
- }).catch((error) => {
635
- this.loadingPromises.delete(`texture:${url}`);
636
- throw new Error(`Failed to load texture ${url}: ${error.message}`);
637
- });
638
- this.loadingPromises.set(`texture:${url}`, loadPromise);
639
- return loadPromise;
640
- }
641
- /**
642
- * Preload multiple assets
643
- * @param urls - Array of asset URLs to preload
644
- * @returns Promise that resolves when all assets are loaded
645
- */
646
- async preload(urls) {
647
- const promises = urls.map((url) => {
648
- if (url.endsWith(".glb") || url.endsWith(".gltf")) {
649
- return this.loadModel(url).catch(() => null);
650
- } else if (url.endsWith(".obj")) {
651
- return this.loadOBJ(url).catch(() => null);
652
- } else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
653
- return this.loadTexture(url).catch(() => null);
654
- }
655
- return Promise.resolve(null);
656
- });
657
- await Promise.all(promises);
658
- }
659
- /**
660
- * Check if a model is cached
661
- * @param url - Model URL
662
- */
663
- hasModel(url) {
664
- return this.modelCache.has(url);
665
- }
666
- /**
667
- * Check if a texture is cached
668
- * @param url - Texture URL
669
- */
670
- hasTexture(url) {
671
- return this.textureCache.has(url);
672
- }
673
- /**
674
- * Get cached model (throws if not cached)
675
- * @param url - Model URL
676
- */
677
- getModel(url) {
678
- const model = this.modelCache.get(url);
679
- if (!model) {
680
- throw new Error(`Model ${url} not in cache`);
795
+ case "poly": {
796
+ if (!node.points || node.points.length === 0) return null;
797
+ const s = new THREE9__namespace.Shape();
798
+ node.points.forEach((p, i) => {
799
+ if (i === 0) s.moveTo(p.x, p.y);
800
+ else s.lineTo(p.x, p.y);
801
+ });
802
+ s.closePath();
803
+ geometry = /* @__PURE__ */ jsxRuntime.jsx("shapeGeometry", { args: [s] });
804
+ break;
681
805
  }
682
- return model;
806
+ default:
807
+ return null;
683
808
  }
684
- /**
685
- * Get cached texture (throws if not cached)
686
- * @param url - Texture URL
687
- */
688
- getTexture(url) {
689
- const texture = this.textureCache.get(url);
690
- if (!texture) {
691
- throw new Error(`Texture ${url} not in cache`);
809
+ return /* @__PURE__ */ jsxRuntime.jsxs("mesh", { rotation: [-Math.PI / 2, 0, 0], position: [world[0], world[1] + 0.02, world[2]], children: [
810
+ geometry,
811
+ /* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color, transparent: true, opacity: node.opacity ?? 1, side: THREE9__namespace.DoubleSide })
812
+ ] });
813
+ }
814
+ function Text3D({ node, projector }) {
815
+ const world = projector.toWorld(node.position);
816
+ return /* @__PURE__ */ jsxRuntime.jsx(drei.Billboard, { position: [world[0], world[1] + 1.2, world[2]], children: /* @__PURE__ */ jsxRuntime.jsx(
817
+ drei.Text,
818
+ {
819
+ fontSize: 0.32,
820
+ color: node.color,
821
+ anchorX: "center",
822
+ anchorY: "middle",
823
+ outlineWidth: 0.02,
824
+ outlineColor: "#000000",
825
+ children: node.text
692
826
  }
693
- return texture;
827
+ ) });
828
+ }
829
+ function Drawable3D({ node, projector }) {
830
+ switch (node.type) {
831
+ case "draw-sprite":
832
+ return /* @__PURE__ */ jsxRuntime.jsx(Sprite3D, { node, projector });
833
+ case "draw-shape":
834
+ return /* @__PURE__ */ jsxRuntime.jsx(Shape3D, { node, projector });
835
+ case "draw-text":
836
+ return /* @__PURE__ */ jsxRuntime.jsx(Text3D, { node, projector });
837
+ case "draw-sprite-layer":
838
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(Sprite3D, { node: item, projector }, i)) });
839
+ case "draw-shape-layer":
840
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(Shape3D, { node: item, projector }, i)) });
841
+ case "draw-text-layer":
842
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(Text3D, { node: item, projector }, i)) });
694
843
  }
695
- /**
696
- * Clear all caches
697
- */
698
- clearCache() {
699
- this.textureCache.forEach((texture) => {
700
- texture.dispose();
701
- });
702
- this.modelCache.forEach((model) => {
703
- model.scene.traverse((child) => {
704
- if (child instanceof THREE6__namespace.Mesh) {
705
- child.geometry.dispose();
706
- if (Array.isArray(child.material)) {
707
- child.material.forEach((m) => m.dispose());
708
- } else {
709
- child.material.dispose();
710
- }
711
- }
712
- });
713
- });
714
- this.modelCache.clear();
715
- this.textureCache.clear();
716
- this.loadingPromises.clear();
717
- }
718
- /**
719
- * Get cache statistics
720
- */
721
- getStats() {
722
- return {
723
- models: this.modelCache.size,
724
- textures: this.textureCache.size,
725
- loading: this.loadingPromises.size
726
- };
727
- }
728
- };
729
- var assetLoader = new AssetLoader();
844
+ }
730
845
 
731
- // components/game/three/hooks/useThree.ts
732
- var DEFAULT_OPTIONS = {
733
- cameraMode: "isometric",
734
- cameraPosition: [10, 10, 10],
735
- backgroundColor: "#1a1a2e",
736
- shadows: true,
737
- showGrid: true,
738
- gridSize: 20,
739
- assetLoader: new AssetLoader()
846
+ // lib/drawable/projector3d.ts
847
+ function create3DProjector(opts = {}) {
848
+ const cellSize = opts.cellSize ?? 1;
849
+ const offsetX = opts.offsetX ?? 0;
850
+ const offsetZ = opts.offsetZ ?? 0;
851
+ return {
852
+ cellSize,
853
+ toWorld: (pos) => [pos.x * cellSize + offsetX, pos.z ?? 0, pos.y * cellSize + offsetZ]
854
+ };
855
+ }
856
+
857
+ // lib/drawable/three/game3dTheme.ts
858
+ var GRID_COLORS_3D = {
859
+ cell: "#444444",
860
+ section: "#666666"
740
861
  };
741
- function useThree3(options = {}) {
742
- const opts = { ...DEFAULT_OPTIONS, ...options };
743
- const containerRef = React7.useRef(null);
744
- const canvasRef = React7.useRef(null);
745
- const rendererRef = React7.useRef(null);
746
- const sceneRef = React7.useRef(null);
747
- const cameraRef = React7.useRef(null);
748
- const controlsRef = React7.useRef(null);
749
- const gridHelperRef = React7.useRef(null);
750
- const rafRef = React7.useRef(0);
751
- const [isReady, setIsReady] = React7.useState(false);
752
- const [dimensions, setDimensions] = React7.useState({ width: 0, height: 0 });
753
- const initialCameraPosition = React7.useMemo(
754
- () => new THREE6__namespace.Vector3(...opts.cameraPosition),
755
- []
756
- );
757
- React7.useEffect(() => {
758
- if (!containerRef.current) return;
759
- const container = containerRef.current;
760
- const { clientWidth, clientHeight } = container;
761
- const scene = new THREE6__namespace.Scene();
762
- scene.background = new THREE6__namespace.Color(opts.backgroundColor);
763
- sceneRef.current = scene;
764
- let camera;
765
- const aspect = clientWidth / clientHeight;
766
- if (opts.cameraMode === "isometric") {
767
- const size = 10;
768
- camera = new THREE6__namespace.OrthographicCamera(
769
- -size * aspect,
770
- size * aspect,
771
- size,
772
- -size,
773
- 0.1,
774
- 1e3
775
- );
776
- } else {
777
- camera = new THREE6__namespace.PerspectiveCamera(45, aspect, 0.1, 1e3);
778
- }
779
- camera.position.copy(initialCameraPosition);
780
- cameraRef.current = camera;
781
- const renderer = new THREE6__namespace.WebGLRenderer({
782
- antialias: true,
783
- alpha: true,
784
- canvas: canvasRef.current || void 0
862
+ var DEFAULT_BACKGROUND_3D = "#1a1a2e";
863
+ function cn(...inputs) {
864
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
865
+ }
866
+ var DEFAULT_GRID_CONFIG = {
867
+ cellSize: 1,
868
+ offsetX: 0,
869
+ offsetZ: 0
870
+ };
871
+ var Canvas3DHost = React6.forwardRef(
872
+ ({
873
+ cameraMode = "isometric",
874
+ followTarget,
875
+ showGrid = true,
876
+ orientation = "standard",
877
+ overlay = "default",
878
+ shadows = true,
879
+ backgroundColor = DEFAULT_BACKGROUND_3D,
880
+ className,
881
+ isLoading: externalLoading,
882
+ error: externalError,
883
+ tileClickEvent,
884
+ unitClickEvent,
885
+ featureClickEvent,
886
+ canvasClickEvent,
887
+ tileHoverEvent,
888
+ tileLeaveEvent,
889
+ unitAnimationEvent,
890
+ cameraChangeEvent,
891
+ loadingMessage = "Loading 3D Scene...",
892
+ keyMap,
893
+ keyUpMap,
894
+ children,
895
+ drawables
896
+ }, ref) => {
897
+ const containerRef = React6.useRef(null);
898
+ const controlsRef = React6.useRef(null);
899
+ const [internalError, setInternalError] = React6.useState(null);
900
+ const eventBus = useEventBus();
901
+ const keysRef = React6.useRef(/* @__PURE__ */ new Set());
902
+ React6.useEffect(() => {
903
+ if (!keyMap && !keyUpMap) return;
904
+ const down = (e) => {
905
+ if (keysRef.current.has(e.code)) return;
906
+ keysRef.current.add(e.code);
907
+ const ev = keyMap?.[e.code];
908
+ if (ev) {
909
+ eventBus.emit(`UI:${ev}`, {});
910
+ e.preventDefault();
911
+ }
912
+ };
913
+ const up = (e) => {
914
+ keysRef.current.delete(e.code);
915
+ const ev = keyUpMap?.[e.code];
916
+ if (ev) eventBus.emit(`UI:${ev}`, {});
917
+ };
918
+ window.addEventListener("keydown", down);
919
+ window.addEventListener("keyup", up);
920
+ return () => {
921
+ window.removeEventListener("keydown", down);
922
+ window.removeEventListener("keyup", up);
923
+ };
924
+ }, [keyMap, keyUpMap, eventBus]);
925
+ const eventHandlers = useGameCanvas3DEvents({
926
+ tileClickEvent,
927
+ unitClickEvent,
928
+ featureClickEvent,
929
+ canvasClickEvent,
930
+ tileHoverEvent,
931
+ tileLeaveEvent,
932
+ unitAnimationEvent,
933
+ cameraChangeEvent
785
934
  });
786
- renderer.setSize(clientWidth, clientHeight);
787
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
788
- renderer.shadowMap.enabled = opts.shadows;
789
- renderer.shadowMap.type = THREE6__namespace.PCFSoftShadowMap;
790
- rendererRef.current = renderer;
791
- const controls = new OrbitControls_js.OrbitControls(camera, renderer.domElement);
792
- controls.enableDamping = true;
793
- controls.dampingFactor = 0.05;
794
- controls.minDistance = 2;
795
- controls.maxDistance = 100;
796
- controls.maxPolarAngle = Math.PI / 2 - 0.1;
797
- controlsRef.current = controls;
798
- const ambientLight = new THREE6__namespace.AmbientLight(16777215, 0.6);
799
- scene.add(ambientLight);
800
- const directionalLight = new THREE6__namespace.DirectionalLight(16777215, 0.8);
801
- directionalLight.position.set(10, 20, 10);
802
- directionalLight.castShadow = opts.shadows;
803
- directionalLight.shadow.mapSize.width = 2048;
804
- directionalLight.shadow.mapSize.height = 2048;
805
- scene.add(directionalLight);
806
- if (opts.showGrid) {
807
- const gridHelper = new THREE6__namespace.GridHelper(
808
- opts.gridSize,
809
- opts.gridSize,
810
- 4473924,
811
- 2236962
812
- );
813
- scene.add(gridHelper);
814
- gridHelperRef.current = gridHelper;
815
- }
816
- const animate = () => {
817
- rafRef.current = requestAnimationFrame(animate);
818
- controls.update();
819
- renderer.render(scene, camera);
820
- };
821
- animate();
822
- const handleResize = () => {
823
- const { clientWidth: width, clientHeight: height } = container;
824
- setDimensions({ width, height });
825
- if (camera instanceof THREE6__namespace.PerspectiveCamera) {
826
- camera.aspect = width / height;
827
- camera.updateProjectionMatrix();
828
- } else if (camera instanceof THREE6__namespace.OrthographicCamera) {
829
- const aspect2 = width / height;
830
- const size = 10;
831
- camera.left = -size * aspect2;
832
- camera.right = size * aspect2;
833
- camera.top = size;
834
- camera.bottom = -size;
835
- camera.updateProjectionMatrix();
935
+ const drawnItems = React6.useMemo(() => collectDrawnItems(drawables ?? []), [drawables]);
936
+ const scenePositions = React6.useMemo(() => drawnItems.map((i) => i.pos), [drawnItems]);
937
+ const hitIndex = React6.useMemo(() => buildHitIndex(drawnItems), [drawnItems]);
938
+ const gridBounds = React6.useMemo(() => {
939
+ if (scenePositions.length === 0) {
940
+ return { minX: 0, maxX: 10, minZ: 0, maxZ: 10 };
836
941
  }
837
- renderer.setSize(width, height);
838
- };
839
- window.addEventListener("resize", handleResize);
840
- handleResize();
841
- setIsReady(true);
842
- return () => {
843
- window.removeEventListener("resize", handleResize);
844
- cancelAnimationFrame(rafRef.current);
845
- controls.dispose();
846
- renderer.dispose();
847
- scene.clear();
848
- };
849
- }, []);
850
- React7.useEffect(() => {
851
- if (!cameraRef.current || !containerRef.current) return;
852
- const container = containerRef.current;
853
- const { clientWidth, clientHeight } = container;
854
- const aspect = clientWidth / clientHeight;
855
- const currentPos = cameraRef.current.position.clone();
856
- let newCamera;
857
- if (opts.cameraMode === "isometric") {
858
- const size = 10;
859
- newCamera = new THREE6__namespace.OrthographicCamera(
860
- -size * aspect,
861
- size * aspect,
862
- size,
863
- -size,
864
- 0.1,
865
- 1e3
866
- );
867
- } else {
868
- newCamera = new THREE6__namespace.PerspectiveCamera(45, aspect, 0.1, 1e3);
869
- }
870
- newCamera.position.copy(currentPos);
871
- cameraRef.current = newCamera;
872
- if (controlsRef.current) {
873
- controlsRef.current.object = newCamera;
874
- }
875
- if (rendererRef.current) {
876
- const animate = () => {
877
- rafRef.current = requestAnimationFrame(animate);
878
- controlsRef.current?.update();
879
- rendererRef.current?.render(sceneRef.current, newCamera);
880
- };
881
- cancelAnimationFrame(rafRef.current);
882
- animate();
883
- }
884
- }, [opts.cameraMode]);
885
- const setCameraPosition = React7.useCallback((x, y, z) => {
886
- if (cameraRef.current) {
887
- cameraRef.current.position.set(x, y, z);
888
- controlsRef.current?.update();
889
- }
890
- }, []);
891
- const lookAt = React7.useCallback((x, y, z) => {
892
- if (cameraRef.current) {
893
- cameraRef.current.lookAt(x, y, z);
894
- controlsRef.current?.target.set(x, y, z);
895
- controlsRef.current?.update();
896
- }
897
- }, []);
898
- const resetCamera = React7.useCallback(() => {
899
- if (cameraRef.current) {
900
- cameraRef.current.position.copy(initialCameraPosition);
901
- cameraRef.current.lookAt(0, 0, 0);
902
- if (controlsRef.current) {
903
- controlsRef.current.target.set(0, 0, 0);
904
- controlsRef.current.update();
942
+ let minX = Infinity;
943
+ let maxX = -Infinity;
944
+ let minZ = Infinity;
945
+ let maxZ = -Infinity;
946
+ for (const p of scenePositions) {
947
+ if (p.x < minX) minX = p.x;
948
+ if (p.x > maxX) maxX = p.x;
949
+ if (p.y < minZ) minZ = p.y;
950
+ if (p.y > maxZ) maxZ = p.y;
905
951
  }
906
- }
907
- }, [initialCameraPosition]);
908
- const fitView = React7.useCallback(
909
- (bounds) => {
910
- if (!cameraRef.current) return;
911
- const centerX = (bounds.minX + bounds.maxX) / 2;
912
- const centerZ = (bounds.minZ + bounds.maxZ) / 2;
913
- const width = bounds.maxX - bounds.minX;
914
- const depth = bounds.maxZ - bounds.minZ;
915
- const maxDim = Math.max(width, depth);
916
- const distance = maxDim * 1.5;
917
- const height = distance * 0.8;
918
- cameraRef.current.position.set(centerX + distance, height, centerZ + distance);
919
- lookAt(centerX, 0, centerZ);
920
- },
921
- [lookAt]
922
- );
923
- return {
924
- canvasRef,
925
- renderer: rendererRef.current,
926
- scene: sceneRef.current,
927
- camera: cameraRef.current,
928
- controls: controlsRef.current,
929
- isReady,
930
- dimensions,
931
- setCameraPosition,
932
- lookAt,
933
- resetCamera,
934
- fitView
935
- };
936
- }
937
- function useAssetLoader(options = {}) {
938
- const { preloadUrls = [], loader: customLoader } = options;
939
- const loaderRef = React7.useRef(customLoader || new AssetLoader());
940
- const [state, setState] = React7.useState({
941
- isLoading: false,
942
- progress: 0,
943
- loaded: 0,
944
- total: 0,
945
- errors: []
946
- });
947
- React7.useEffect(() => {
948
- if (preloadUrls.length > 0) {
949
- preload(preloadUrls);
950
- }
951
- }, []);
952
- const updateProgress = React7.useCallback((loaded, total) => {
953
- setState((prev) => ({
954
- ...prev,
955
- loaded,
956
- total,
957
- progress: total > 0 ? Math.round(loaded / total * 100) : 0
958
- }));
959
- }, []);
960
- const loadModel = React7.useCallback(
961
- async (url) => {
962
- setState((prev) => ({ ...prev, isLoading: true }));
963
- try {
964
- const model = await loaderRef.current.loadModel(url);
965
- setState((prev) => ({
966
- ...prev,
967
- isLoading: false,
968
- loaded: prev.loaded + 1
969
- }));
970
- return model;
971
- } catch (error) {
972
- const errorMsg = error instanceof Error ? error.message : String(error);
973
- setState((prev) => ({
974
- ...prev,
975
- isLoading: false,
976
- errors: [...prev.errors, errorMsg]
977
- }));
978
- throw error;
952
+ return { minX, maxX, minZ, maxZ };
953
+ }, [scenePositions]);
954
+ const cameraTarget = React6.useMemo(
955
+ () => [
956
+ (gridBounds.minX + gridBounds.maxX) / 2,
957
+ 0,
958
+ (gridBounds.minZ + gridBounds.maxZ) / 2
959
+ ],
960
+ [gridBounds]
961
+ );
962
+ const gridConfig = React6.useMemo(
963
+ () => ({
964
+ ...DEFAULT_GRID_CONFIG,
965
+ offsetX: -(gridBounds.maxX - gridBounds.minX) / 2,
966
+ offsetZ: -(gridBounds.maxZ - gridBounds.minZ) / 2
967
+ }),
968
+ [gridBounds]
969
+ );
970
+ const drawableProjector = React6.useMemo(
971
+ () => create3DProjector({
972
+ cellSize: gridConfig.cellSize,
973
+ offsetX: -gridBounds.minX * gridConfig.cellSize,
974
+ offsetZ: -gridBounds.minZ * gridConfig.cellSize
975
+ }),
976
+ [gridBounds, gridConfig]
977
+ );
978
+ React6.useImperativeHandle(ref, () => ({
979
+ getCameraPosition: () => {
980
+ if (controlsRef.current) {
981
+ const pos = controlsRef.current.object.position;
982
+ return new THREE9__namespace.Vector3(pos.x, pos.y, pos.z);
983
+ }
984
+ return null;
985
+ },
986
+ setCameraPosition: (x, y, z) => {
987
+ if (controlsRef.current) {
988
+ controlsRef.current.object.position.set(x, y, z);
989
+ controlsRef.current.update();
990
+ }
991
+ },
992
+ lookAt: (x, y, z) => {
993
+ if (controlsRef.current) {
994
+ controlsRef.current.target.set(x, y, z);
995
+ controlsRef.current.update();
996
+ }
997
+ },
998
+ resetCamera: () => {
999
+ if (controlsRef.current) {
1000
+ controlsRef.current.reset();
1001
+ }
1002
+ },
1003
+ screenshot: () => {
1004
+ const canvas = containerRef.current?.querySelector("canvas");
1005
+ if (canvas) {
1006
+ return canvas.toDataURL("image/png");
1007
+ }
1008
+ return null;
979
1009
  }
980
- },
981
- []
982
- );
983
- const loadOBJ = React7.useCallback(
984
- async (url) => {
985
- setState((prev) => ({ ...prev, isLoading: true }));
986
- try {
987
- const model = await loaderRef.current.loadOBJ(url);
988
- setState((prev) => ({
989
- ...prev,
990
- isLoading: false,
991
- loaded: prev.loaded + 1
992
- }));
993
- return model;
994
- } catch (error) {
995
- const errorMsg = error instanceof Error ? error.message : String(error);
996
- setState((prev) => ({
997
- ...prev,
998
- isLoading: false,
999
- errors: [...prev.errors, errorMsg]
1000
- }));
1001
- throw error;
1010
+ }));
1011
+ const cameraConfig = React6.useMemo(() => {
1012
+ const size = Math.max(
1013
+ gridBounds.maxX - gridBounds.minX,
1014
+ gridBounds.maxZ - gridBounds.minZ,
1015
+ 4
1016
+ // minimum framing distance so a tiny board isn't zoomed in
1017
+ );
1018
+ const cx = (gridBounds.minX + gridBounds.maxX) / 2;
1019
+ const cz = (gridBounds.minZ + gridBounds.maxZ) / 2;
1020
+ const d = size * 1;
1021
+ switch (cameraMode) {
1022
+ case "isometric":
1023
+ return { position: [cx + d, d * 0.8, cz + d], fov: 45 };
1024
+ case "top-down":
1025
+ return { position: [cx, d * 2, cz + d * 0.35], fov: 45 };
1026
+ case "follow":
1027
+ return { position: [cx, d * 0.5, cz + d], fov: 45 };
1028
+ case "perspective":
1029
+ default:
1030
+ return { position: [cx + d, d, cz + d], fov: 45 };
1002
1031
  }
1003
- },
1004
- []
1005
- );
1006
- const loadTexture = React7.useCallback(
1007
- async (url) => {
1008
- setState((prev) => ({ ...prev, isLoading: true }));
1009
- try {
1010
- const texture = await loaderRef.current.loadTexture(url);
1011
- setState((prev) => ({
1012
- ...prev,
1013
- isLoading: false,
1014
- loaded: prev.loaded + 1
1015
- }));
1016
- return texture;
1017
- } catch (error) {
1018
- const errorMsg = error instanceof Error ? error.message : String(error);
1019
- setState((prev) => ({
1020
- ...prev,
1021
- isLoading: false,
1022
- errors: [...prev.errors, errorMsg]
1023
- }));
1024
- throw error;
1032
+ }, [cameraMode, gridBounds]);
1033
+ const followWorld = React6.useMemo(() => {
1034
+ if (followTarget) return drawableProjector.toWorld(followTarget);
1035
+ return cameraTarget;
1036
+ }, [followTarget, drawableProjector, cameraTarget]);
1037
+ const followOffset = React6.useMemo(
1038
+ () => cameraMode === "chase" ? [0, 4, -7] : [5, 7, 5],
1039
+ [cameraMode]
1040
+ );
1041
+ const handleGroundClick = React6.useCallback((e) => {
1042
+ if (!tileClickEvent && !unitClickEvent) return;
1043
+ e.stopPropagation();
1044
+ const cell = {
1045
+ x: Math.round(e.point.x / gridConfig.cellSize + gridBounds.minX),
1046
+ y: Math.round(e.point.z / gridConfig.cellSize + gridBounds.minZ)
1047
+ };
1048
+ const hitId = hitIndex.get(`${cell.x},${cell.y}`);
1049
+ if (hitId !== void 0 && unitClickEvent) {
1050
+ eventBus.emit(`UI:${unitClickEvent}`, { unitId: hitId, x: cell.x, z: cell.y });
1051
+ } else if (tileClickEvent) {
1052
+ eventBus.emit(`UI:${tileClickEvent}`, { x: cell.x, z: cell.y });
1025
1053
  }
1026
- },
1027
- []
1028
- );
1029
- const preload = React7.useCallback(
1030
- async (urls) => {
1031
- setState((prev) => ({
1032
- ...prev,
1033
- isLoading: true,
1034
- total: urls.length,
1035
- loaded: 0,
1036
- errors: []
1037
- }));
1038
- let completed = 0;
1039
- const errors = [];
1040
- await Promise.all(
1041
- urls.map(async (url) => {
1042
- try {
1043
- if (url.endsWith(".glb") || url.endsWith(".gltf")) {
1044
- await loaderRef.current.loadModel(url);
1045
- } else if (url.endsWith(".obj")) {
1046
- await loaderRef.current.loadOBJ(url);
1047
- } else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
1048
- await loaderRef.current.loadTexture(url);
1049
- }
1050
- completed++;
1051
- updateProgress(completed, urls.length);
1052
- } catch (error) {
1053
- const errorMsg = error instanceof Error ? error.message : String(error);
1054
- errors.push(`${url}: ${errorMsg}`);
1055
- completed++;
1056
- updateProgress(completed, urls.length);
1057
- }
1058
- })
1054
+ }, [tileClickEvent, unitClickEvent, gridConfig, gridBounds, hitIndex, eventBus]);
1055
+ if (externalLoading) {
1056
+ return /* @__PURE__ */ jsxRuntime.jsx(
1057
+ Canvas3DLoadingState,
1058
+ {
1059
+ progress: 1,
1060
+ loaded: 1,
1061
+ total: 1,
1062
+ message: loadingMessage,
1063
+ className
1064
+ }
1059
1065
  );
1060
- setState((prev) => ({
1061
- ...prev,
1062
- isLoading: false,
1063
- errors
1064
- }));
1065
- },
1066
- [updateProgress]
1067
- );
1068
- const hasModel = React7.useCallback((url) => {
1069
- return loaderRef.current.hasModel(url);
1070
- }, []);
1071
- const hasTexture = React7.useCallback((url) => {
1072
- return loaderRef.current.hasTexture(url);
1073
- }, []);
1074
- const getModel = React7.useCallback((url) => {
1075
- try {
1076
- return loaderRef.current.getModel(url);
1077
- } catch {
1078
- return void 0;
1079
- }
1080
- }, []);
1081
- const getTexture = React7.useCallback((url) => {
1082
- try {
1083
- return loaderRef.current.getTexture(url);
1084
- } catch {
1085
- return void 0;
1086
1066
  }
1087
- }, []);
1088
- const clearCache = React7.useCallback(() => {
1089
- loaderRef.current.clearCache();
1090
- setState({
1091
- isLoading: false,
1092
- progress: 0,
1093
- loaded: 0,
1094
- total: 0,
1095
- errors: []
1096
- });
1097
- }, []);
1098
- return {
1099
- ...state,
1100
- loadModel,
1101
- loadOBJ,
1102
- loadTexture,
1103
- preload,
1104
- hasModel,
1105
- hasTexture,
1106
- getModel,
1107
- getTexture,
1108
- clearCache
1109
- };
1110
- }
1111
- function useSceneGraph() {
1112
- const nodesRef = React7.useRef(/* @__PURE__ */ new Map());
1113
- const addNode = React7.useCallback((node) => {
1114
- const existing = nodesRef.current.get(node.id);
1115
- if (existing) {
1116
- existing.mesh.removeFromParent();
1117
- }
1118
- nodesRef.current.set(node.id, node);
1119
- }, []);
1120
- const removeNode = React7.useCallback((id) => {
1121
- const node = nodesRef.current.get(id);
1122
- if (node) {
1123
- node.mesh.removeFromParent();
1124
- nodesRef.current.delete(id);
1067
+ const displayError = externalError || internalError;
1068
+ if (displayError) {
1069
+ return /* @__PURE__ */ jsxRuntime.jsx(Canvas3DErrorBoundary, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "game-canvas-3d game-canvas-3d--error", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "game-canvas-3d__error", children: [
1070
+ "Error: ",
1071
+ displayError
1072
+ ] }) }) });
1125
1073
  }
1126
- }, []);
1127
- const getNode = React7.useCallback((id) => {
1128
- return nodesRef.current.get(id);
1129
- }, []);
1130
- const updateNodePosition = React7.useCallback(
1131
- (id, x, y, z) => {
1132
- const node = nodesRef.current.get(id);
1133
- if (node) {
1134
- node.mesh.position.set(x, y, z);
1135
- node.position = { x, y, z };
1136
- }
1137
- },
1138
- []
1139
- );
1140
- const updateNodeGridPosition = React7.useCallback(
1141
- (id, gridX, gridZ) => {
1142
- const node = nodesRef.current.get(id);
1143
- if (node) {
1144
- node.gridPosition = { x: gridX, z: gridZ };
1074
+ return /* @__PURE__ */ jsxRuntime.jsx(
1075
+ Canvas3DErrorBoundary,
1076
+ {
1077
+ onError: (err) => setInternalError(err.message),
1078
+ onReset: () => setInternalError(null),
1079
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1080
+ "div",
1081
+ {
1082
+ ref: containerRef,
1083
+ className: cn("game-canvas-3d relative w-full overflow-hidden", className),
1084
+ style: { flex: "1 1 0", minHeight: "400px" },
1085
+ "data-orientation": orientation,
1086
+ "data-camera-mode": cameraMode,
1087
+ "data-overlay": overlay,
1088
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
1089
+ fiber.Canvas,
1090
+ {
1091
+ shadows,
1092
+ camera: {
1093
+ position: cameraConfig.position,
1094
+ fov: cameraConfig.fov,
1095
+ near: 0.1,
1096
+ far: 1e3
1097
+ },
1098
+ style: { background: backgroundColor, position: "absolute", inset: 0 },
1099
+ onClick: (e) => {
1100
+ if (e.target === e.currentTarget) {
1101
+ eventHandlers.handleCanvasClick(e);
1102
+ }
1103
+ },
1104
+ children: [
1105
+ /* @__PURE__ */ jsxRuntime.jsx(CameraController3D, { onCameraChange: eventHandlers.handleCameraChange }),
1106
+ (cameraMode === "follow" || cameraMode === "chase") && /* @__PURE__ */ jsxRuntime.jsx(FollowCamera3D, { target: followWorld, offset: followOffset }),
1107
+ /* @__PURE__ */ jsxRuntime.jsx(
1108
+ Lighting3D,
1109
+ {
1110
+ shadows,
1111
+ shadowBias: -4e-4,
1112
+ shadowNormalBias: 0.04,
1113
+ shadowCameraSize: 5,
1114
+ shadowCameraNear: 0.5,
1115
+ shadowCameraFar: 500
1116
+ }
1117
+ ),
1118
+ showGrid && /* @__PURE__ */ jsxRuntime.jsx(
1119
+ drei.Grid,
1120
+ {
1121
+ args: [
1122
+ Math.max(gridBounds.maxX - gridBounds.minX + 2, 10),
1123
+ Math.max(gridBounds.maxZ - gridBounds.minZ + 2, 10)
1124
+ ],
1125
+ position: [
1126
+ (gridBounds.maxX - gridBounds.minX) / 2 - 0.5,
1127
+ 0,
1128
+ (gridBounds.maxZ - gridBounds.minZ) / 2 - 0.5
1129
+ ],
1130
+ cellSize: 1,
1131
+ cellThickness: 1,
1132
+ cellColor: GRID_COLORS_3D.cell,
1133
+ sectionSize: 5,
1134
+ sectionThickness: 1.5,
1135
+ sectionColor: GRID_COLORS_3D.section,
1136
+ fadeDistance: 50,
1137
+ fadeStrength: 1
1138
+ }
1139
+ ),
1140
+ drawables && drawables.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("group", { children: drawables.map((node, i) => /* @__PURE__ */ jsxRuntime.jsx(Drawable3D, { node, projector: drawableProjector }, i)) }),
1141
+ (tileClickEvent || unitClickEvent) && /* @__PURE__ */ jsxRuntime.jsxs(
1142
+ "mesh",
1143
+ {
1144
+ rotation: [-Math.PI / 2, 0, 0],
1145
+ position: [
1146
+ (gridBounds.maxX - gridBounds.minX) / 2 * gridConfig.cellSize,
1147
+ 0,
1148
+ (gridBounds.maxZ - gridBounds.minZ) / 2 * gridConfig.cellSize
1149
+ ],
1150
+ onClick: handleGroundClick,
1151
+ children: [
1152
+ /* @__PURE__ */ jsxRuntime.jsx(
1153
+ "planeGeometry",
1154
+ {
1155
+ args: [
1156
+ (gridBounds.maxX - gridBounds.minX + 4) * gridConfig.cellSize,
1157
+ (gridBounds.maxZ - gridBounds.minZ + 4) * gridConfig.cellSize
1158
+ ]
1159
+ }
1160
+ ),
1161
+ /* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })
1162
+ ]
1163
+ }
1164
+ ),
1165
+ children,
1166
+ /* @__PURE__ */ jsxRuntime.jsx(
1167
+ drei.OrbitControls,
1168
+ {
1169
+ ref: controlsRef,
1170
+ enabled: cameraMode !== "follow" && cameraMode !== "chase",
1171
+ target: cameraTarget,
1172
+ enableDamping: true,
1173
+ dampingFactor: 0.05,
1174
+ enableZoom: true,
1175
+ enablePan: true,
1176
+ touches: { ONE: THREE9__namespace.TOUCH.ROTATE, TWO: THREE9__namespace.TOUCH.DOLLY_PAN },
1177
+ minDistance: 2,
1178
+ maxDistance: 100,
1179
+ maxPolarAngle: Math.PI / 2 - 0.1
1180
+ }
1181
+ )
1182
+ ]
1183
+ }
1184
+ )
1185
+ }
1186
+ )
1145
1187
  }
1146
- },
1147
- []
1148
- );
1149
- const getNodeAtGrid = React7.useCallback(
1150
- (x, z, type) => {
1151
- return Array.from(nodesRef.current.values()).find((node) => {
1152
- const matchesGrid = node.gridPosition.x === x && node.gridPosition.z === z;
1153
- return type ? matchesGrid && node.type === type : matchesGrid;
1154
- });
1155
- },
1156
- []
1157
- );
1158
- const getNodesByType = React7.useCallback((type) => {
1159
- return Array.from(nodesRef.current.values()).filter((node) => node.type === type);
1160
- }, []);
1161
- const getNodesInBounds = React7.useCallback(
1162
- (minX, maxX, minZ, maxZ) => {
1163
- return Array.from(nodesRef.current.values()).filter((node) => {
1164
- const { x, z } = node.gridPosition;
1165
- return x >= minX && x <= maxX && z >= minZ && z <= maxZ;
1188
+ );
1189
+ }
1190
+ );
1191
+ Canvas3DHost.displayName = "Canvas3DHost";
1192
+ function Scene3D({ background = "#1a1a2e", fog, children }) {
1193
+ const { scene } = fiber.useThree();
1194
+ const initializedRef = React6.useRef(false);
1195
+ React6.useEffect(() => {
1196
+ if (initializedRef.current) return;
1197
+ initializedRef.current = true;
1198
+ if (background.startsWith("#") || background.startsWith("rgb")) {
1199
+ scene.background = new THREE9__namespace.Color(background);
1200
+ } else {
1201
+ const loader = new THREE9__namespace.TextureLoader();
1202
+ loader.load(background, (texture) => {
1203
+ scene.background = texture;
1166
1204
  });
1167
- },
1168
- []
1169
- );
1170
- const clearNodes = React7.useCallback(() => {
1171
- nodesRef.current.forEach((node) => {
1172
- node.mesh.removeFromParent();
1173
- });
1174
- nodesRef.current.clear();
1175
- }, []);
1176
- const countNodes = React7.useCallback((type) => {
1177
- if (!type) {
1178
- return nodesRef.current.size;
1179
1205
  }
1180
- return Array.from(nodesRef.current.values()).filter((node) => node.type === type).length;
1181
- }, []);
1182
- return {
1183
- nodesRef,
1184
- addNode,
1185
- removeNode,
1186
- getNode,
1187
- updateNodePosition,
1188
- updateNodeGridPosition,
1189
- getNodeAtGrid,
1190
- getNodesByType,
1191
- getNodesInBounds,
1192
- clearNodes,
1193
- countNodes
1194
- };
1206
+ if (fog) {
1207
+ scene.fog = new THREE9__namespace.Fog(fog.color, fog.near, fog.far);
1208
+ }
1209
+ return () => {
1210
+ scene.background = null;
1211
+ scene.fog = null;
1212
+ };
1213
+ }, [scene, background, fog]);
1214
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
1195
1215
  }
1196
- function useRaycaster(options) {
1197
- const { camera, canvas, cellSize = 1, offsetX = 0, offsetZ = 0 } = options;
1198
- const raycaster = React7.useRef(new THREE6__namespace.Raycaster());
1199
- const mouse = React7.useRef(new THREE6__namespace.Vector2());
1200
- const clientToNDC = React7.useCallback(
1201
- (clientX, clientY) => {
1202
- if (!canvas) {
1203
- return { x: 0, y: 0 };
1204
- }
1205
- const rect = canvas.getBoundingClientRect();
1206
- return {
1207
- x: (clientX - rect.left) / rect.width * 2 - 1,
1208
- y: -((clientY - rect.top) / rect.height) * 2 + 1
1209
- };
1210
- },
1211
- [canvas]
1212
- );
1213
- const isWithinCanvas = React7.useCallback(
1214
- (clientX, clientY) => {
1215
- if (!canvas) return false;
1216
- const rect = canvas.getBoundingClientRect();
1217
- return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom;
1218
- },
1219
- [canvas]
1220
- );
1221
- const getIntersection = React7.useCallback(
1222
- (clientX, clientY, objects) => {
1223
- if (!camera || !canvas) return null;
1224
- const ndc = clientToNDC(clientX, clientY);
1225
- mouse.current.set(ndc.x, ndc.y);
1226
- raycaster.current.setFromCamera(mouse.current, camera);
1227
- const intersects = raycaster.current.intersectObjects(objects, true);
1228
- if (intersects.length > 0) {
1229
- const hit = intersects[0];
1230
- return {
1231
- object: hit.object,
1232
- point: hit.point,
1233
- distance: hit.distance,
1234
- uv: hit.uv,
1235
- face: hit.face,
1236
- faceIndex: hit.faceIndex,
1237
- instanceId: hit.instanceId
1238
- };
1216
+ var Camera3D = React6.forwardRef(
1217
+ ({
1218
+ mode = "isometric",
1219
+ position = [10, 10, 10],
1220
+ target = [0, 0, 0],
1221
+ zoom = 1,
1222
+ fov = 45,
1223
+ enableOrbit = true,
1224
+ minDistance = 2,
1225
+ maxDistance = 100,
1226
+ onChange
1227
+ }, ref) => {
1228
+ const { camera, set, viewport } = fiber.useThree();
1229
+ const controlsRef = React6.useRef(null);
1230
+ const initialPosition = React6.useRef(new THREE9__namespace.Vector3(...position));
1231
+ const initialTarget = React6.useRef(new THREE9__namespace.Vector3(...target));
1232
+ React6.useEffect(() => {
1233
+ let newCamera;
1234
+ if (mode === "isometric") {
1235
+ const aspect = viewport.aspect;
1236
+ const size = 10 / zoom;
1237
+ newCamera = new THREE9__namespace.OrthographicCamera(
1238
+ -size * aspect,
1239
+ size * aspect,
1240
+ size,
1241
+ -size,
1242
+ 0.1,
1243
+ 1e3
1244
+ );
1245
+ } else {
1246
+ newCamera = new THREE9__namespace.PerspectiveCamera(fov, viewport.aspect, 0.1, 1e3);
1239
1247
  }
1240
- return null;
1241
- },
1242
- [camera, canvas, clientToNDC]
1243
- );
1244
- const getAllIntersections = React7.useCallback(
1245
- (clientX, clientY, objects) => {
1246
- if (!camera || !canvas) return [];
1247
- const ndc = clientToNDC(clientX, clientY);
1248
- mouse.current.set(ndc.x, ndc.y);
1249
- raycaster.current.setFromCamera(mouse.current, camera);
1250
- const intersects = raycaster.current.intersectObjects(objects, true);
1251
- return intersects.map((hit) => ({
1252
- object: hit.object,
1253
- point: hit.point,
1254
- distance: hit.distance,
1255
- uv: hit.uv,
1256
- face: hit.face,
1257
- faceIndex: hit.faceIndex,
1258
- instanceId: hit.instanceId
1259
- }));
1260
- },
1261
- [camera, canvas, clientToNDC]
1262
- );
1263
- const getGridCoordinates = React7.useCallback(
1264
- (clientX, clientY) => {
1265
- if (!camera || !canvas) return null;
1266
- const ndc = clientToNDC(clientX, clientY);
1267
- mouse.current.set(ndc.x, ndc.y);
1268
- raycaster.current.setFromCamera(mouse.current, camera);
1269
- const plane = new THREE6__namespace.Plane(new THREE6__namespace.Vector3(0, 1, 0), 0);
1270
- const target = new THREE6__namespace.Vector3();
1271
- const intersection = raycaster.current.ray.intersectPlane(plane, target);
1272
- if (intersection) {
1273
- const gridX = Math.round((target.x - offsetX) / cellSize);
1274
- const gridZ = Math.round((target.z - offsetZ) / cellSize);
1275
- return { x: gridX, z: gridZ };
1248
+ newCamera.position.copy(initialPosition.current);
1249
+ newCamera.lookAt(initialTarget.current.x, initialTarget.current.y, initialTarget.current.z);
1250
+ set({ camera: newCamera });
1251
+ if (mode === "top-down") {
1252
+ newCamera.position.set(0, 20 / zoom, 0);
1253
+ newCamera.lookAt(0, 0, 0);
1276
1254
  }
1277
- return null;
1278
- },
1279
- [camera, canvas, cellSize, offsetX, offsetZ, clientToNDC]
1280
- );
1281
- const getTileAtPosition = React7.useCallback(
1282
- (clientX, clientY, scene) => {
1283
- if (!camera || !canvas) return null;
1284
- const tileMeshes = [];
1285
- scene.traverse((obj) => {
1286
- if (obj.userData.type === "tile" || obj.userData.isTile) {
1287
- tileMeshes.push(obj);
1255
+ return () => {
1256
+ };
1257
+ }, [mode, fov, zoom, viewport.aspect, set]);
1258
+ fiber.useFrame(() => {
1259
+ if (onChange) {
1260
+ onChange(camera);
1261
+ }
1262
+ });
1263
+ React6.useImperativeHandle(ref, () => ({
1264
+ getCamera: () => camera,
1265
+ setPosition: (x, y, z) => {
1266
+ camera.position.set(x, y, z);
1267
+ if (controlsRef.current) {
1268
+ controlsRef.current.update();
1288
1269
  }
1289
- });
1290
- const hit = getIntersection(clientX, clientY, tileMeshes);
1291
- if (hit) {
1292
- const gridCoords2 = getGridCoordinates(clientX, clientY);
1293
- if (gridCoords2) {
1294
- return {
1295
- gridX: gridCoords2.x,
1296
- gridZ: gridCoords2.z,
1297
- worldPosition: hit.point,
1298
- objectType: hit.object.userData.type || "tile",
1299
- objectId: hit.object.userData.id || hit.object.userData.tileId
1300
- };
1270
+ },
1271
+ lookAt: (x, y, z) => {
1272
+ camera.lookAt(x, y, z);
1273
+ if (controlsRef.current) {
1274
+ controlsRef.current.target.set(x, y, z);
1275
+ controlsRef.current.update();
1276
+ }
1277
+ },
1278
+ reset: () => {
1279
+ camera.position.copy(initialPosition.current);
1280
+ camera.lookAt(initialTarget.current.x, initialTarget.current.y, initialTarget.current.z);
1281
+ if (controlsRef.current) {
1282
+ controlsRef.current.target.copy(initialTarget.current);
1283
+ controlsRef.current.update();
1301
1284
  }
1285
+ },
1286
+ getViewBounds: () => {
1287
+ const min = new THREE9__namespace.Vector3(-10, -10, -10);
1288
+ const max = new THREE9__namespace.Vector3(10, 10, 10);
1289
+ return { min, max };
1302
1290
  }
1303
- const gridCoords = getGridCoordinates(clientX, clientY);
1304
- if (gridCoords) {
1305
- return {
1306
- gridX: gridCoords.x,
1307
- gridZ: gridCoords.z,
1308
- worldPosition: new THREE6__namespace.Vector3(
1309
- gridCoords.x * cellSize + offsetX,
1310
- 0,
1311
- gridCoords.z * cellSize + offsetZ
1312
- )
1313
- };
1291
+ }));
1292
+ const maxPolarAngle = mode === "top-down" ? 0.1 : Math.PI / 2 - 0.1;
1293
+ return /* @__PURE__ */ jsxRuntime.jsx(
1294
+ drei.OrbitControls,
1295
+ {
1296
+ ref: controlsRef,
1297
+ camera,
1298
+ enabled: enableOrbit,
1299
+ target: [initialTarget.current.x, initialTarget.current.y, initialTarget.current.z],
1300
+ minDistance,
1301
+ maxDistance,
1302
+ maxPolarAngle,
1303
+ enableDamping: true,
1304
+ dampingFactor: 0.05
1314
1305
  }
1315
- return null;
1316
- },
1317
- [camera, canvas, getIntersection, getGridCoordinates, cellSize, offsetX, offsetZ]
1318
- );
1319
- return {
1320
- raycaster,
1321
- mouse,
1322
- getIntersection,
1323
- getAllIntersections,
1324
- getGridCoordinates,
1325
- getTileAtPosition,
1326
- clientToNDC,
1327
- isWithinCanvas
1328
- };
1329
- }
1330
- var log3 = logger.createLogger("almadar:eventbus");
1331
- var subLog = logger.createLogger("almadar:eventbus:subscribe");
1332
- var scopeLog = logger.createLogger("almadar:ui:trait-scope");
1333
- function getGlobalEventBus() {
1334
- if (typeof window !== "undefined") {
1335
- return window.__kflowEventBus ?? null;
1306
+ );
1336
1307
  }
1337
- return null;
1308
+ );
1309
+ Camera3D.displayName = "Camera3D";
1310
+ function detectAssetRoot2(modelUrl) {
1311
+ const idx = modelUrl.indexOf("/3d/");
1312
+ if (idx !== -1) {
1313
+ return modelUrl.substring(0, idx + 4);
1314
+ }
1315
+ return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
1338
1316
  }
1339
- var fallbackListeners = /* @__PURE__ */ new Map();
1340
- var fallbackAnyListeners = /* @__PURE__ */ new Set();
1341
- var fallbackEventBus = {
1342
- emit: (type, payload, source) => {
1343
- const event = {
1344
- type,
1345
- payload,
1346
- timestamp: Date.now(),
1347
- source
1348
- };
1349
- const handlers = fallbackListeners.get(type);
1350
- log3.debug("emit", { type, payloadKeys: payload ? Object.keys(payload).length : 0, listenerCount: (handlers?.size ?? 0) + fallbackAnyListeners.size });
1351
- if (handlers) {
1352
- handlers.forEach((handler) => {
1353
- try {
1354
- handler(event);
1355
- } catch (error) {
1356
- log3.error("Error in listener", { type, error: error instanceof Error ? error : String(error) });
1357
- }
1358
- });
1317
+ function createGLTFLoaderForUrl(url) {
1318
+ const loader = new GLTFLoader_js.GLTFLoader();
1319
+ loader.setResourcePath(detectAssetRoot2(url));
1320
+ return loader;
1321
+ }
1322
+ var AssetLoader = class {
1323
+ constructor() {
1324
+ __publicField(this, "objLoader");
1325
+ __publicField(this, "textureLoader");
1326
+ __publicField(this, "modelCache");
1327
+ __publicField(this, "textureCache");
1328
+ __publicField(this, "loadingPromises");
1329
+ this.objLoader = new OBJLoader_js.OBJLoader();
1330
+ this.textureLoader = new THREE9__namespace.TextureLoader();
1331
+ this.modelCache = /* @__PURE__ */ new Map();
1332
+ this.textureCache = /* @__PURE__ */ new Map();
1333
+ this.loadingPromises = /* @__PURE__ */ new Map();
1334
+ }
1335
+ /**
1336
+ * Load a GLB/GLTF model
1337
+ * @param url - URL to the .glb or .gltf file
1338
+ * @returns Promise with loaded model scene and animations
1339
+ */
1340
+ async loadModel(url) {
1341
+ if (this.modelCache.has(url)) {
1342
+ return this.modelCache.get(url);
1359
1343
  }
1360
- fallbackAnyListeners.forEach((handler) => {
1361
- try {
1362
- handler(event);
1363
- } catch (error) {
1364
- log3.error("Error in onAny listener", { type, error: error instanceof Error ? error : String(error) });
1365
- }
1366
- });
1367
- },
1368
- on: (type, listener) => {
1369
- if (!fallbackListeners.has(type)) {
1370
- fallbackListeners.set(type, /* @__PURE__ */ new Set());
1344
+ if (this.loadingPromises.has(url)) {
1345
+ return this.loadingPromises.get(url);
1371
1346
  }
1372
- fallbackListeners.get(type).add(listener);
1373
- subLog.debug("subscribe", { type, totalListeners: fallbackListeners.get(type).size });
1374
- return () => {
1375
- const handlers = fallbackListeners.get(type);
1376
- if (handlers) {
1377
- handlers.delete(listener);
1378
- if (handlers.size === 0) {
1379
- fallbackListeners.delete(type);
1380
- }
1381
- }
1382
- };
1383
- },
1384
- once: (type, listener) => {
1385
- const wrappedListener = (event) => {
1386
- fallbackListeners.get(type)?.delete(wrappedListener);
1387
- listener(event);
1388
- };
1389
- return fallbackEventBus.on(type, wrappedListener);
1390
- },
1391
- hasListeners: (type) => {
1392
- const handlers = fallbackListeners.get(type);
1393
- return handlers !== void 0 && handlers.size > 0;
1394
- },
1395
- onAny: (listener) => {
1396
- fallbackAnyListeners.add(listener);
1397
- subLog.debug("subscribe:any", { totalAnyListeners: fallbackAnyListeners.size });
1398
- return () => {
1399
- fallbackAnyListeners.delete(listener);
1400
- };
1347
+ const loader = createGLTFLoaderForUrl(url);
1348
+ const loadPromise = loader.loadAsync(url).then((gltf) => {
1349
+ const result = {
1350
+ scene: gltf.scene,
1351
+ animations: gltf.animations || []
1352
+ };
1353
+ this.modelCache.set(url, result);
1354
+ this.loadingPromises.delete(url);
1355
+ return result;
1356
+ }).catch((error) => {
1357
+ this.loadingPromises.delete(url);
1358
+ throw new Error(`Failed to load model ${url}: ${error.message}`);
1359
+ });
1360
+ this.loadingPromises.set(url, loadPromise);
1361
+ return loadPromise;
1401
1362
  }
1402
- };
1403
- function useEventBus() {
1404
- const context = React7.useContext(providers.EventBusContext);
1405
- const baseBus = context ?? getGlobalEventBus() ?? fallbackEventBus;
1406
- const scope = providers.useTraitScope();
1407
- return React7.useMemo(() => {
1408
- if (!scope) {
1409
- return {
1410
- ...baseBus,
1411
- emit: (type, payload, source) => {
1412
- if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
1413
- scopeLog.warn("emit:bare-key-no-scope", { type });
1414
- }
1415
- baseBus.emit(type, payload, source);
1416
- }
1363
+ /**
1364
+ * Load an OBJ model (fallback for non-GLB assets)
1365
+ * @param url - URL to the .obj file
1366
+ * @returns Promise with loaded object group
1367
+ */
1368
+ async loadOBJ(url) {
1369
+ if (this.modelCache.has(url)) {
1370
+ return this.modelCache.get(url).scene;
1371
+ }
1372
+ if (this.loadingPromises.has(url)) {
1373
+ const result = await this.loadingPromises.get(url);
1374
+ return result.scene;
1375
+ }
1376
+ const loadPromise = this.objLoader.loadAsync(url).then((group) => {
1377
+ const result = {
1378
+ scene: group,
1379
+ animations: []
1417
1380
  };
1381
+ this.modelCache.set(url, result);
1382
+ this.loadingPromises.delete(url);
1383
+ return result;
1384
+ }).catch((error) => {
1385
+ this.loadingPromises.delete(url);
1386
+ throw new Error(`Failed to load OBJ ${url}: ${error.message}`);
1387
+ });
1388
+ this.loadingPromises.set(url, loadPromise);
1389
+ return (await loadPromise).scene;
1390
+ }
1391
+ /**
1392
+ * Load a texture
1393
+ * @param url - URL to the texture image
1394
+ * @returns Promise with loaded texture
1395
+ */
1396
+ async loadTexture(url) {
1397
+ if (this.textureCache.has(url)) {
1398
+ return this.textureCache.get(url);
1418
1399
  }
1419
- return {
1420
- ...baseBus,
1421
- emit: (type, payload, source) => {
1422
- if (typeof type === "string" && type.startsWith("UI:")) {
1423
- const tail = type.slice(3);
1424
- const qualified = tail.includes(".") ? type : `UI:${scope.orbital}.${scope.trait}.${tail}`;
1425
- if (qualified !== type) {
1426
- scopeLog.info("emit:qualified", {
1427
- from: type,
1428
- to: qualified,
1429
- scopeOrbital: scope.orbital,
1430
- scopeTrait: scope.trait
1431
- });
1400
+ if (this.loadingPromises.has(`texture:${url}`)) {
1401
+ return this.loadingPromises.get(`texture:${url}`);
1402
+ }
1403
+ const loadPromise = this.textureLoader.loadAsync(url).then((texture) => {
1404
+ texture.colorSpace = THREE9__namespace.SRGBColorSpace;
1405
+ this.textureCache.set(url, texture);
1406
+ this.loadingPromises.delete(`texture:${url}`);
1407
+ return texture;
1408
+ }).catch((error) => {
1409
+ this.loadingPromises.delete(`texture:${url}`);
1410
+ throw new Error(`Failed to load texture ${url}: ${error.message}`);
1411
+ });
1412
+ this.loadingPromises.set(`texture:${url}`, loadPromise);
1413
+ return loadPromise;
1414
+ }
1415
+ /**
1416
+ * Preload multiple assets
1417
+ * @param urls - Array of asset URLs to preload
1418
+ * @returns Promise that resolves when all assets are loaded
1419
+ */
1420
+ async preload(urls) {
1421
+ const promises = urls.map((url) => {
1422
+ if (url.endsWith(".glb") || url.endsWith(".gltf")) {
1423
+ return this.loadModel(url).catch(() => null);
1424
+ } else if (url.endsWith(".obj")) {
1425
+ return this.loadOBJ(url).catch(() => null);
1426
+ } else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
1427
+ return this.loadTexture(url).catch(() => null);
1428
+ }
1429
+ return Promise.resolve(null);
1430
+ });
1431
+ await Promise.all(promises);
1432
+ }
1433
+ /**
1434
+ * Check if a model is cached
1435
+ * @param url - Model URL
1436
+ */
1437
+ hasModel(url) {
1438
+ return this.modelCache.has(url);
1439
+ }
1440
+ /**
1441
+ * Check if a texture is cached
1442
+ * @param url - Texture URL
1443
+ */
1444
+ hasTexture(url) {
1445
+ return this.textureCache.has(url);
1446
+ }
1447
+ /**
1448
+ * Get cached model (throws if not cached)
1449
+ * @param url - Model URL
1450
+ */
1451
+ getModel(url) {
1452
+ const model = this.modelCache.get(url);
1453
+ if (!model) {
1454
+ throw new Error(`Model ${url} not in cache`);
1455
+ }
1456
+ return model;
1457
+ }
1458
+ /**
1459
+ * Get cached texture (throws if not cached)
1460
+ * @param url - Texture URL
1461
+ */
1462
+ getTexture(url) {
1463
+ const texture = this.textureCache.get(url);
1464
+ if (!texture) {
1465
+ throw new Error(`Texture ${url} not in cache`);
1466
+ }
1467
+ return texture;
1468
+ }
1469
+ /**
1470
+ * Clear all caches
1471
+ */
1472
+ clearCache() {
1473
+ this.textureCache.forEach((texture) => {
1474
+ texture.dispose();
1475
+ });
1476
+ this.modelCache.forEach((model) => {
1477
+ model.scene.traverse((child) => {
1478
+ if (child instanceof THREE9__namespace.Mesh) {
1479
+ child.geometry.dispose();
1480
+ if (Array.isArray(child.material)) {
1481
+ child.material.forEach((m) => m.dispose());
1482
+ } else {
1483
+ child.material.dispose();
1432
1484
  }
1433
- baseBus.emit(qualified, payload, source);
1434
- return;
1435
1485
  }
1436
- baseBus.emit(type, payload, source);
1437
- }
1486
+ });
1487
+ });
1488
+ this.modelCache.clear();
1489
+ this.textureCache.clear();
1490
+ this.loadingPromises.clear();
1491
+ }
1492
+ /**
1493
+ * Get cache statistics
1494
+ */
1495
+ getStats() {
1496
+ return {
1497
+ models: this.modelCache.size,
1498
+ textures: this.textureCache.size,
1499
+ loading: this.loadingPromises.size
1438
1500
  };
1439
- }, [baseBus, scope]);
1440
- }
1441
- function useEmitEvent() {
1442
- const eventBus = useEventBus();
1443
- return React7.useCallback(
1444
- (type, payload, source) => {
1445
- eventBus.emit(type, payload, source);
1446
- },
1447
- [eventBus]
1448
- );
1449
- }
1501
+ }
1502
+ };
1503
+ var assetLoader = new AssetLoader();
1450
1504
 
1451
- // components/game/three/hooks/useGameCanvas3DEvents.ts
1452
- function useGameCanvas3DEvents(options) {
1453
- const {
1454
- tileClickEvent,
1455
- unitClickEvent,
1456
- featureClickEvent,
1457
- canvasClickEvent,
1458
- tileHoverEvent,
1459
- tileLeaveEvent,
1460
- unitAnimationEvent,
1461
- cameraChangeEvent,
1462
- onTileClick,
1463
- onUnitClick,
1464
- onFeatureClick,
1465
- onCanvasClick,
1466
- onTileHover,
1467
- onUnitAnimation
1468
- } = options;
1469
- const emit = useEmitEvent();
1470
- const optionsRef = React7.useRef(options);
1471
- optionsRef.current = options;
1472
- const handleTileClick = React7.useCallback(
1473
- (tile, event) => {
1474
- if (tileClickEvent) {
1475
- emit(tileClickEvent, {
1476
- tileId: tile.id,
1477
- x: tile.x,
1478
- z: tile.z ?? tile.y ?? 0,
1479
- type: tile.type,
1480
- terrain: tile.terrain,
1481
- elevation: tile.elevation
1482
- });
1483
- }
1484
- optionsRef.current.onTileClick?.(tile, event);
1485
- },
1486
- [tileClickEvent, emit]
1505
+ // lib/drawable/three/hooks/useThree.ts
1506
+ var DEFAULT_OPTIONS = {
1507
+ cameraMode: "isometric",
1508
+ cameraPosition: [10, 10, 10],
1509
+ backgroundColor: "#1a1a2e",
1510
+ shadows: true,
1511
+ showGrid: true,
1512
+ gridSize: 20,
1513
+ assetLoader: new AssetLoader()
1514
+ };
1515
+ function useThree4(options = {}) {
1516
+ const opts = { ...DEFAULT_OPTIONS, ...options };
1517
+ const containerRef = React6.useRef(null);
1518
+ const canvasRef = React6.useRef(null);
1519
+ const rendererRef = React6.useRef(null);
1520
+ const sceneRef = React6.useRef(null);
1521
+ const cameraRef = React6.useRef(null);
1522
+ const controlsRef = React6.useRef(null);
1523
+ const gridHelperRef = React6.useRef(null);
1524
+ const rafRef = React6.useRef(0);
1525
+ const [isReady, setIsReady] = React6.useState(false);
1526
+ const [dimensions, setDimensions] = React6.useState({ width: 0, height: 0 });
1527
+ const initialCameraPosition = React6.useMemo(
1528
+ () => new THREE9__namespace.Vector3(...opts.cameraPosition),
1529
+ []
1487
1530
  );
1488
- const handleUnitClick = React7.useCallback(
1489
- (unit, event) => {
1490
- if (unitClickEvent) {
1491
- emit(unitClickEvent, {
1492
- unitId: unit.id,
1493
- x: unit.x,
1494
- z: unit.z ?? unit.y ?? 0,
1495
- unitType: unit.unitType,
1496
- name: unit.name,
1497
- team: unit.team,
1498
- faction: unit.faction,
1499
- health: unit.health,
1500
- maxHealth: unit.maxHealth
1501
- });
1502
- }
1503
- optionsRef.current.onUnitClick?.(unit, event);
1504
- },
1505
- [unitClickEvent, emit]
1506
- );
1507
- const handleFeatureClick = React7.useCallback(
1508
- (feature, event) => {
1509
- if (featureClickEvent) {
1510
- emit(featureClickEvent, {
1511
- featureId: feature.id,
1512
- x: feature.x,
1513
- z: feature.z ?? feature.y ?? 0,
1514
- type: feature.type,
1515
- elevation: feature.elevation
1516
- });
1531
+ React6.useEffect(() => {
1532
+ if (!containerRef.current) return;
1533
+ const container = containerRef.current;
1534
+ const { clientWidth, clientHeight } = container;
1535
+ const scene = new THREE9__namespace.Scene();
1536
+ scene.background = new THREE9__namespace.Color(opts.backgroundColor);
1537
+ sceneRef.current = scene;
1538
+ let camera;
1539
+ const aspect = clientWidth / clientHeight;
1540
+ if (opts.cameraMode === "isometric") {
1541
+ const size = 10;
1542
+ camera = new THREE9__namespace.OrthographicCamera(
1543
+ -size * aspect,
1544
+ size * aspect,
1545
+ size,
1546
+ -size,
1547
+ 0.1,
1548
+ 1e3
1549
+ );
1550
+ } else {
1551
+ camera = new THREE9__namespace.PerspectiveCamera(45, aspect, 0.1, 1e3);
1552
+ }
1553
+ camera.position.copy(initialCameraPosition);
1554
+ cameraRef.current = camera;
1555
+ const renderer = new THREE9__namespace.WebGLRenderer({
1556
+ antialias: true,
1557
+ alpha: true,
1558
+ canvas: canvasRef.current || void 0
1559
+ });
1560
+ renderer.setSize(clientWidth, clientHeight);
1561
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
1562
+ renderer.shadowMap.enabled = opts.shadows;
1563
+ renderer.shadowMap.type = THREE9__namespace.PCFSoftShadowMap;
1564
+ rendererRef.current = renderer;
1565
+ const controls = new OrbitControls_js.OrbitControls(camera, renderer.domElement);
1566
+ controls.enableDamping = true;
1567
+ controls.dampingFactor = 0.05;
1568
+ controls.minDistance = 2;
1569
+ controls.maxDistance = 100;
1570
+ controls.maxPolarAngle = Math.PI / 2 - 0.1;
1571
+ controlsRef.current = controls;
1572
+ const ambientLight = new THREE9__namespace.AmbientLight(16777215, 0.6);
1573
+ scene.add(ambientLight);
1574
+ const directionalLight = new THREE9__namespace.DirectionalLight(16777215, 0.8);
1575
+ directionalLight.position.set(10, 20, 10);
1576
+ directionalLight.castShadow = opts.shadows;
1577
+ directionalLight.shadow.mapSize.width = 2048;
1578
+ directionalLight.shadow.mapSize.height = 2048;
1579
+ scene.add(directionalLight);
1580
+ if (opts.showGrid) {
1581
+ const gridHelper = new THREE9__namespace.GridHelper(
1582
+ opts.gridSize,
1583
+ opts.gridSize,
1584
+ 4473924,
1585
+ 2236962
1586
+ );
1587
+ scene.add(gridHelper);
1588
+ gridHelperRef.current = gridHelper;
1589
+ }
1590
+ const animate = () => {
1591
+ rafRef.current = requestAnimationFrame(animate);
1592
+ controls.update();
1593
+ renderer.render(scene, camera);
1594
+ };
1595
+ animate();
1596
+ const handleResize = () => {
1597
+ const { clientWidth: width, clientHeight: height } = container;
1598
+ setDimensions({ width, height });
1599
+ if (camera instanceof THREE9__namespace.PerspectiveCamera) {
1600
+ camera.aspect = width / height;
1601
+ camera.updateProjectionMatrix();
1602
+ } else if (camera instanceof THREE9__namespace.OrthographicCamera) {
1603
+ const aspect2 = width / height;
1604
+ const size = 10;
1605
+ camera.left = -size * aspect2;
1606
+ camera.right = size * aspect2;
1607
+ camera.top = size;
1608
+ camera.bottom = -size;
1609
+ camera.updateProjectionMatrix();
1517
1610
  }
1518
- optionsRef.current.onFeatureClick?.(feature, event);
1611
+ renderer.setSize(width, height);
1612
+ };
1613
+ window.addEventListener("resize", handleResize);
1614
+ handleResize();
1615
+ setIsReady(true);
1616
+ return () => {
1617
+ window.removeEventListener("resize", handleResize);
1618
+ cancelAnimationFrame(rafRef.current);
1619
+ controls.dispose();
1620
+ renderer.dispose();
1621
+ scene.clear();
1622
+ };
1623
+ }, []);
1624
+ React6.useEffect(() => {
1625
+ if (!cameraRef.current || !containerRef.current) return;
1626
+ const container = containerRef.current;
1627
+ const { clientWidth, clientHeight } = container;
1628
+ const aspect = clientWidth / clientHeight;
1629
+ const currentPos = cameraRef.current.position.clone();
1630
+ let newCamera;
1631
+ if (opts.cameraMode === "isometric") {
1632
+ const size = 10;
1633
+ newCamera = new THREE9__namespace.OrthographicCamera(
1634
+ -size * aspect,
1635
+ size * aspect,
1636
+ size,
1637
+ -size,
1638
+ 0.1,
1639
+ 1e3
1640
+ );
1641
+ } else {
1642
+ newCamera = new THREE9__namespace.PerspectiveCamera(45, aspect, 0.1, 1e3);
1643
+ }
1644
+ newCamera.position.copy(currentPos);
1645
+ cameraRef.current = newCamera;
1646
+ if (controlsRef.current) {
1647
+ controlsRef.current.object = newCamera;
1648
+ }
1649
+ if (rendererRef.current) {
1650
+ const animate = () => {
1651
+ rafRef.current = requestAnimationFrame(animate);
1652
+ controlsRef.current?.update();
1653
+ rendererRef.current?.render(sceneRef.current, newCamera);
1654
+ };
1655
+ cancelAnimationFrame(rafRef.current);
1656
+ animate();
1657
+ }
1658
+ }, [opts.cameraMode]);
1659
+ const setCameraPosition = React6.useCallback((x, y, z) => {
1660
+ if (cameraRef.current) {
1661
+ cameraRef.current.position.set(x, y, z);
1662
+ controlsRef.current?.update();
1663
+ }
1664
+ }, []);
1665
+ const lookAt = React6.useCallback((x, y, z) => {
1666
+ if (cameraRef.current) {
1667
+ cameraRef.current.lookAt(x, y, z);
1668
+ controlsRef.current?.target.set(x, y, z);
1669
+ controlsRef.current?.update();
1670
+ }
1671
+ }, []);
1672
+ const resetCamera = React6.useCallback(() => {
1673
+ if (cameraRef.current) {
1674
+ cameraRef.current.position.copy(initialCameraPosition);
1675
+ cameraRef.current.lookAt(0, 0, 0);
1676
+ if (controlsRef.current) {
1677
+ controlsRef.current.target.set(0, 0, 0);
1678
+ controlsRef.current.update();
1679
+ }
1680
+ }
1681
+ }, [initialCameraPosition]);
1682
+ const fitView = React6.useCallback(
1683
+ (bounds) => {
1684
+ if (!cameraRef.current) return;
1685
+ const centerX = (bounds.minX + bounds.maxX) / 2;
1686
+ const centerZ = (bounds.minZ + bounds.maxZ) / 2;
1687
+ const width = bounds.maxX - bounds.minX;
1688
+ const depth = bounds.maxZ - bounds.minZ;
1689
+ const maxDim = Math.max(width, depth);
1690
+ const distance = maxDim * 1.5;
1691
+ const height = distance * 0.8;
1692
+ cameraRef.current.position.set(centerX + distance, height, centerZ + distance);
1693
+ lookAt(centerX, 0, centerZ);
1519
1694
  },
1520
- [featureClickEvent, emit]
1695
+ [lookAt]
1521
1696
  );
1522
- const handleCanvasClick = React7.useCallback(
1523
- (event) => {
1524
- if (canvasClickEvent) {
1525
- emit(canvasClickEvent, {
1526
- clientX: event.clientX,
1527
- clientY: event.clientY,
1528
- button: event.button
1529
- });
1697
+ return {
1698
+ canvasRef,
1699
+ renderer: rendererRef.current,
1700
+ scene: sceneRef.current,
1701
+ camera: cameraRef.current,
1702
+ controls: controlsRef.current,
1703
+ isReady,
1704
+ dimensions,
1705
+ setCameraPosition,
1706
+ lookAt,
1707
+ resetCamera,
1708
+ fitView
1709
+ };
1710
+ }
1711
+ function useAssetLoader(options = {}) {
1712
+ const { preloadUrls = [], loader: customLoader } = options;
1713
+ const loaderRef = React6.useRef(customLoader || new AssetLoader());
1714
+ const [state, setState] = React6.useState({
1715
+ isLoading: false,
1716
+ progress: 0,
1717
+ loaded: 0,
1718
+ total: 0,
1719
+ errors: []
1720
+ });
1721
+ React6.useEffect(() => {
1722
+ if (preloadUrls.length > 0) {
1723
+ preload(preloadUrls);
1724
+ }
1725
+ }, []);
1726
+ const updateProgress = React6.useCallback((loaded, total) => {
1727
+ setState((prev) => ({
1728
+ ...prev,
1729
+ loaded,
1730
+ total,
1731
+ progress: total > 0 ? Math.round(loaded / total * 100) : 0
1732
+ }));
1733
+ }, []);
1734
+ const loadModel = React6.useCallback(
1735
+ async (url) => {
1736
+ setState((prev) => ({ ...prev, isLoading: true }));
1737
+ try {
1738
+ const model = await loaderRef.current.loadModel(url);
1739
+ setState((prev) => ({
1740
+ ...prev,
1741
+ isLoading: false,
1742
+ loaded: prev.loaded + 1
1743
+ }));
1744
+ return model;
1745
+ } catch (error) {
1746
+ const errorMsg = error instanceof Error ? error.message : String(error);
1747
+ setState((prev) => ({
1748
+ ...prev,
1749
+ isLoading: false,
1750
+ errors: [...prev.errors, errorMsg]
1751
+ }));
1752
+ throw error;
1530
1753
  }
1531
- optionsRef.current.onCanvasClick?.(event);
1532
1754
  },
1533
- [canvasClickEvent, emit]
1755
+ []
1534
1756
  );
1535
- const handleTileHover = React7.useCallback(
1536
- (tile, event) => {
1537
- if (tile) {
1538
- if (tileHoverEvent) {
1539
- emit(tileHoverEvent, {
1540
- tileId: tile.id,
1541
- x: tile.x,
1542
- z: tile.z ?? tile.y ?? 0,
1543
- type: tile.type
1544
- });
1545
- }
1546
- } else {
1547
- if (tileLeaveEvent) {
1548
- emit(tileLeaveEvent, {});
1549
- }
1757
+ const loadOBJ = React6.useCallback(
1758
+ async (url) => {
1759
+ setState((prev) => ({ ...prev, isLoading: true }));
1760
+ try {
1761
+ const model = await loaderRef.current.loadOBJ(url);
1762
+ setState((prev) => ({
1763
+ ...prev,
1764
+ isLoading: false,
1765
+ loaded: prev.loaded + 1
1766
+ }));
1767
+ return model;
1768
+ } catch (error) {
1769
+ const errorMsg = error instanceof Error ? error.message : String(error);
1770
+ setState((prev) => ({
1771
+ ...prev,
1772
+ isLoading: false,
1773
+ errors: [...prev.errors, errorMsg]
1774
+ }));
1775
+ throw error;
1550
1776
  }
1551
- optionsRef.current.onTileHover?.(tile, event);
1552
1777
  },
1553
- [tileHoverEvent, tileLeaveEvent, emit]
1778
+ []
1554
1779
  );
1555
- const handleUnitAnimation = React7.useCallback(
1556
- (unitId, state) => {
1557
- if (unitAnimationEvent) {
1558
- emit(unitAnimationEvent, {
1559
- unitId,
1560
- state,
1561
- timestamp: Date.now()
1562
- });
1780
+ const loadTexture = React6.useCallback(
1781
+ async (url) => {
1782
+ setState((prev) => ({ ...prev, isLoading: true }));
1783
+ try {
1784
+ const texture = await loaderRef.current.loadTexture(url);
1785
+ setState((prev) => ({
1786
+ ...prev,
1787
+ isLoading: false,
1788
+ loaded: prev.loaded + 1
1789
+ }));
1790
+ return texture;
1791
+ } catch (error) {
1792
+ const errorMsg = error instanceof Error ? error.message : String(error);
1793
+ setState((prev) => ({
1794
+ ...prev,
1795
+ isLoading: false,
1796
+ errors: [...prev.errors, errorMsg]
1797
+ }));
1798
+ throw error;
1563
1799
  }
1564
- optionsRef.current.onUnitAnimation?.(unitId, state);
1565
1800
  },
1566
- [unitAnimationEvent, emit]
1801
+ []
1567
1802
  );
1568
- const handleCameraChange = React7.useCallback(
1569
- (position) => {
1570
- if (cameraChangeEvent) {
1571
- emit(cameraChangeEvent, {
1572
- position,
1573
- timestamp: Date.now()
1574
- });
1575
- }
1803
+ const preload = React6.useCallback(
1804
+ async (urls) => {
1805
+ setState((prev) => ({
1806
+ ...prev,
1807
+ isLoading: true,
1808
+ total: urls.length,
1809
+ loaded: 0,
1810
+ errors: []
1811
+ }));
1812
+ let completed = 0;
1813
+ const errors = [];
1814
+ await Promise.all(
1815
+ urls.map(async (url) => {
1816
+ try {
1817
+ if (url.endsWith(".glb") || url.endsWith(".gltf")) {
1818
+ await loaderRef.current.loadModel(url);
1819
+ } else if (url.endsWith(".obj")) {
1820
+ await loaderRef.current.loadOBJ(url);
1821
+ } else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
1822
+ await loaderRef.current.loadTexture(url);
1823
+ }
1824
+ completed++;
1825
+ updateProgress(completed, urls.length);
1826
+ } catch (error) {
1827
+ const errorMsg = error instanceof Error ? error.message : String(error);
1828
+ errors.push(`${url}: ${errorMsg}`);
1829
+ completed++;
1830
+ updateProgress(completed, urls.length);
1831
+ }
1832
+ })
1833
+ );
1834
+ setState((prev) => ({
1835
+ ...prev,
1836
+ isLoading: false,
1837
+ errors
1838
+ }));
1576
1839
  },
1577
- [cameraChangeEvent, emit]
1840
+ [updateProgress]
1578
1841
  );
1842
+ const hasModel = React6.useCallback((url) => {
1843
+ return loaderRef.current.hasModel(url);
1844
+ }, []);
1845
+ const hasTexture = React6.useCallback((url) => {
1846
+ return loaderRef.current.hasTexture(url);
1847
+ }, []);
1848
+ const getModel = React6.useCallback((url) => {
1849
+ try {
1850
+ return loaderRef.current.getModel(url);
1851
+ } catch {
1852
+ return void 0;
1853
+ }
1854
+ }, []);
1855
+ const getTexture = React6.useCallback((url) => {
1856
+ try {
1857
+ return loaderRef.current.getTexture(url);
1858
+ } catch {
1859
+ return void 0;
1860
+ }
1861
+ }, []);
1862
+ const clearCache = React6.useCallback(() => {
1863
+ loaderRef.current.clearCache();
1864
+ setState({
1865
+ isLoading: false,
1866
+ progress: 0,
1867
+ loaded: 0,
1868
+ total: 0,
1869
+ errors: []
1870
+ });
1871
+ }, []);
1579
1872
  return {
1580
- handleTileClick,
1581
- handleUnitClick,
1582
- handleFeatureClick,
1583
- handleCanvasClick,
1584
- handleTileHover,
1585
- handleUnitAnimation,
1586
- handleCameraChange
1873
+ ...state,
1874
+ loadModel,
1875
+ loadOBJ,
1876
+ loadTexture,
1877
+ preload,
1878
+ hasModel,
1879
+ hasTexture,
1880
+ getModel,
1881
+ getTexture,
1882
+ clearCache
1587
1883
  };
1588
1884
  }
1589
-
1590
- // lib/drawable/hitTest.ts
1591
- function collectDrawnItems(nodes) {
1592
- const out = [];
1593
- for (const n of nodes) {
1594
- switch (n.type) {
1595
- case "draw-sprite":
1596
- case "draw-shape":
1597
- case "draw-text":
1598
- out.push({ pos: n.position, id: n.id });
1599
- break;
1600
- case "draw-sprite-layer":
1601
- case "draw-shape-layer":
1602
- case "draw-text-layer":
1603
- for (const it of n.items) out.push({ pos: it.position, id: it.id });
1604
- break;
1885
+ function useSceneGraph() {
1886
+ const nodesRef = React6.useRef(/* @__PURE__ */ new Map());
1887
+ const addNode = React6.useCallback((node) => {
1888
+ const existing = nodesRef.current.get(node.id);
1889
+ if (existing) {
1890
+ existing.mesh.removeFromParent();
1605
1891
  }
1606
- }
1607
- return out;
1608
- }
1609
- function buildHitIndex(items) {
1610
- const m = /* @__PURE__ */ new Map();
1611
- for (const it of items) {
1612
- if (it.id !== void 0) m.set(`${it.pos.x},${it.pos.y}`, it.id);
1613
- }
1614
- return m;
1615
- }
1616
- function CameraController3D({
1617
- onCameraChange
1618
- }) {
1619
- const { camera } = fiber.useThree();
1620
- React7.useEffect(() => {
1621
- if (onCameraChange) {
1622
- onCameraChange({
1623
- x: camera.position.x,
1624
- y: camera.position.y,
1625
- z: camera.position.z
1626
- });
1892
+ nodesRef.current.set(node.id, node);
1893
+ }, []);
1894
+ const removeNode = React6.useCallback((id) => {
1895
+ const node = nodesRef.current.get(id);
1896
+ if (node) {
1897
+ node.mesh.removeFromParent();
1898
+ nodesRef.current.delete(id);
1627
1899
  }
1628
- }, [camera.position, onCameraChange]);
1629
- return null;
1630
- }
1631
- function FollowCamera3D({
1632
- target,
1633
- offset
1634
- }) {
1635
- const { camera } = fiber.useThree();
1636
- const look = React7.useRef(new THREE6__namespace.Vector3(target[0], target[1], target[2]));
1637
- const goal = React7.useRef(new THREE6__namespace.Vector3());
1638
- fiber.useFrame((_, delta) => {
1639
- const t = Math.min(1, delta * 5);
1640
- goal.current.set(target[0] + offset[0], target[1] + offset[1], target[2] + offset[2]);
1641
- camera.position.lerp(goal.current, t);
1642
- look.current.lerp(goal.current.set(target[0], target[1], target[2]), t);
1643
- camera.lookAt(look.current);
1644
- });
1645
- return null;
1646
- }
1647
- function SpriteBillboard({ node, world }) {
1648
- const texture = fiber.useLoader(THREE6__namespace.TextureLoader, node.asset.url);
1649
- const img = texture.image;
1650
- const imgW = img?.width || 1;
1651
- const imgH = img?.height || 1;
1652
- if (node.frame) {
1653
- texture.repeat.set(node.frame.w / imgW, node.frame.h / imgH);
1654
- texture.offset.set(node.frame.x / imgW, 1 - (node.frame.y + node.frame.h) / imgH);
1655
- texture.magFilter = texture.minFilter = THREE6__namespace.NearestFilter;
1656
- texture.needsUpdate = true;
1657
- }
1658
- const aspect = node.frame ? node.frame.w / node.frame.h : imgW / imgH;
1659
- const height = node.height ?? 1;
1660
- const width = node.width ?? height * aspect;
1661
- return /* @__PURE__ */ jsxRuntime.jsx(drei.Billboard, { position: [world[0], world[1] + height / 2, world[2]], children: /* @__PURE__ */ jsxRuntime.jsxs("mesh", { children: [
1662
- /* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [width, height] }),
1663
- /* @__PURE__ */ jsxRuntime.jsx(
1664
- "meshBasicMaterial",
1665
- {
1666
- map: texture,
1667
- transparent: true,
1668
- alphaTest: 0.1,
1669
- side: THREE6__namespace.DoubleSide,
1670
- opacity: node.opacity ?? 1
1900
+ }, []);
1901
+ const getNode = React6.useCallback((id) => {
1902
+ return nodesRef.current.get(id);
1903
+ }, []);
1904
+ const updateNodePosition = React6.useCallback(
1905
+ (id, x, y, z) => {
1906
+ const node = nodesRef.current.get(id);
1907
+ if (node) {
1908
+ node.mesh.position.set(x, y, z);
1909
+ node.position = { x, y, z };
1671
1910
  }
1672
- )
1673
- ] }) });
1674
- }
1675
- function Sprite3D({ node, projector }) {
1676
- if (node.asset.dimension === "3d" && node.asset.url) {
1677
- return /* @__PURE__ */ jsxRuntime.jsx("group", { position: projector.toWorld(node.position), children: /* @__PURE__ */ jsxRuntime.jsx(
1678
- ModelLoader,
1679
- {
1680
- url: node.asset.url,
1681
- scale: node.width ?? projector.cellSize,
1682
- rotation: [0, node.rotation ?? 0, 0],
1683
- fallbackGeometry: "box",
1684
- castShadow: true,
1685
- receiveShadow: true
1911
+ },
1912
+ []
1913
+ );
1914
+ const updateNodeGridPosition = React6.useCallback(
1915
+ (id, gridX, gridZ) => {
1916
+ const node = nodesRef.current.get(id);
1917
+ if (node) {
1918
+ node.gridPosition = { x: gridX, z: gridZ };
1686
1919
  }
1687
- ) });
1688
- }
1689
- if (!node.asset.url) return null;
1690
- return /* @__PURE__ */ jsxRuntime.jsx(SpriteBillboard, { node, world: projector.toWorld(node.position) });
1691
- }
1692
- function Shape3D({ node, projector }) {
1693
- const world = projector.toWorld(node.position);
1694
- const color = node.fill ?? node.stroke ?? "#ffffff";
1695
- let geometry;
1696
- switch (node.shape) {
1697
- case "cell":
1698
- case "rect": {
1699
- const w = node.shape === "cell" ? projector.cellSize * 0.95 : node.width ?? projector.cellSize;
1700
- const h = node.shape === "cell" ? projector.cellSize * 0.95 : node.height ?? projector.cellSize;
1701
- geometry = /* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [w, h] });
1702
- break;
1703
- }
1704
- case "ellipse": {
1705
- const radiusX = node.radiusX ?? 0.4;
1706
- geometry = node.stroke && !node.fill ? /* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [Math.max(0, radiusX - 0.08), radiusX, 32] }) : /* @__PURE__ */ jsxRuntime.jsx("circleGeometry", { args: [radiusX, 32] });
1707
- break;
1708
- }
1709
- case "poly": {
1710
- if (!node.points || node.points.length === 0) return null;
1711
- const s = new THREE6__namespace.Shape();
1712
- node.points.forEach((p, i) => {
1713
- if (i === 0) s.moveTo(p.x, p.y);
1714
- else s.lineTo(p.x, p.y);
1715
- });
1716
- s.closePath();
1717
- geometry = /* @__PURE__ */ jsxRuntime.jsx("shapeGeometry", { args: [s] });
1718
- break;
1719
- }
1720
- default:
1721
- return null;
1722
- }
1723
- return /* @__PURE__ */ jsxRuntime.jsxs("mesh", { rotation: [-Math.PI / 2, 0, 0], position: [world[0], world[1] + 0.02, world[2]], children: [
1724
- geometry,
1725
- /* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color, transparent: true, opacity: node.opacity ?? 1, side: THREE6__namespace.DoubleSide })
1726
- ] });
1727
- }
1728
- function Text3D({ node, projector }) {
1729
- const world = projector.toWorld(node.position);
1730
- return /* @__PURE__ */ jsxRuntime.jsx(drei.Billboard, { position: [world[0], world[1] + 1.2, world[2]], children: /* @__PURE__ */ jsxRuntime.jsx(
1731
- drei.Text,
1732
- {
1733
- fontSize: 0.32,
1734
- color: node.color,
1735
- anchorX: "center",
1736
- anchorY: "middle",
1737
- outlineWidth: 0.02,
1738
- outlineColor: "#000000",
1739
- children: node.text
1920
+ },
1921
+ []
1922
+ );
1923
+ const getNodeAtGrid = React6.useCallback(
1924
+ (x, z, type) => {
1925
+ return Array.from(nodesRef.current.values()).find((node) => {
1926
+ const matchesGrid = node.gridPosition.x === x && node.gridPosition.z === z;
1927
+ return type ? matchesGrid && node.type === type : matchesGrid;
1928
+ });
1929
+ },
1930
+ []
1931
+ );
1932
+ const getNodesByType = React6.useCallback((type) => {
1933
+ return Array.from(nodesRef.current.values()).filter((node) => node.type === type);
1934
+ }, []);
1935
+ const getNodesInBounds = React6.useCallback(
1936
+ (minX, maxX, minZ, maxZ) => {
1937
+ return Array.from(nodesRef.current.values()).filter((node) => {
1938
+ const { x, z } = node.gridPosition;
1939
+ return x >= minX && x <= maxX && z >= minZ && z <= maxZ;
1940
+ });
1941
+ },
1942
+ []
1943
+ );
1944
+ const clearNodes = React6.useCallback(() => {
1945
+ nodesRef.current.forEach((node) => {
1946
+ node.mesh.removeFromParent();
1947
+ });
1948
+ nodesRef.current.clear();
1949
+ }, []);
1950
+ const countNodes = React6.useCallback((type) => {
1951
+ if (!type) {
1952
+ return nodesRef.current.size;
1740
1953
  }
1741
- ) });
1742
- }
1743
- function Drawable3D({ node, projector }) {
1744
- switch (node.type) {
1745
- case "draw-sprite":
1746
- return /* @__PURE__ */ jsxRuntime.jsx(Sprite3D, { node, projector });
1747
- case "draw-shape":
1748
- return /* @__PURE__ */ jsxRuntime.jsx(Shape3D, { node, projector });
1749
- case "draw-text":
1750
- return /* @__PURE__ */ jsxRuntime.jsx(Text3D, { node, projector });
1751
- case "draw-sprite-layer":
1752
- return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(Sprite3D, { node: item, projector }, i)) });
1753
- case "draw-shape-layer":
1754
- return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(Shape3D, { node: item, projector }, i)) });
1755
- case "draw-text-layer":
1756
- return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(Text3D, { node: item, projector }, i)) });
1757
- }
1758
- }
1759
-
1760
- // lib/drawable/projector3d.ts
1761
- function create3DProjector(opts = {}) {
1762
- const cellSize = opts.cellSize ?? 1;
1763
- const offsetX = opts.offsetX ?? 0;
1764
- const offsetZ = opts.offsetZ ?? 0;
1954
+ return Array.from(nodesRef.current.values()).filter((node) => node.type === type).length;
1955
+ }, []);
1765
1956
  return {
1766
- cellSize,
1767
- toWorld: (pos) => [pos.x * cellSize + offsetX, pos.z ?? 0, pos.y * cellSize + offsetZ]
1957
+ nodesRef,
1958
+ addNode,
1959
+ removeNode,
1960
+ getNode,
1961
+ updateNodePosition,
1962
+ updateNodeGridPosition,
1963
+ getNodeAtGrid,
1964
+ getNodesByType,
1965
+ getNodesInBounds,
1966
+ clearNodes,
1967
+ countNodes
1768
1968
  };
1769
1969
  }
1770
-
1771
- // components/game/three/game3dTheme.ts
1772
- var GRID_COLORS_3D = {
1773
- cell: "#444444",
1774
- section: "#666666"
1775
- };
1776
- var DEFAULT_BACKGROUND_3D = "#1a1a2e";
1777
- function cn(...inputs) {
1778
- return tailwindMerge.twMerge(clsx.clsx(inputs));
1779
- }
1780
- var DEFAULT_GRID_CONFIG = {
1781
- cellSize: 1,
1782
- offsetX: 0,
1783
- offsetZ: 0
1784
- };
1785
- var GameCanvas3D = React7.forwardRef(
1786
- ({
1787
- cameraMode = "isometric",
1788
- followTarget,
1789
- showGrid = true,
1790
- orientation = "standard",
1791
- overlay = "default",
1792
- shadows = true,
1793
- backgroundColor = DEFAULT_BACKGROUND_3D,
1794
- className,
1795
- isLoading: externalLoading,
1796
- error: externalError,
1797
- tileClickEvent,
1798
- unitClickEvent,
1799
- featureClickEvent,
1800
- canvasClickEvent,
1801
- tileHoverEvent,
1802
- tileLeaveEvent,
1803
- unitAnimationEvent,
1804
- cameraChangeEvent,
1805
- loadingMessage = "Loading 3D Scene...",
1806
- keyMap,
1807
- keyUpMap,
1808
- children,
1809
- drawables
1810
- }, ref) => {
1811
- const containerRef = React7.useRef(null);
1812
- const controlsRef = React7.useRef(null);
1813
- const [internalError, setInternalError] = React7.useState(null);
1814
- const eventBus = useEventBus();
1815
- const keysRef = React7.useRef(/* @__PURE__ */ new Set());
1816
- React7.useEffect(() => {
1817
- if (!keyMap && !keyUpMap) return;
1818
- const down = (e) => {
1819
- if (keysRef.current.has(e.code)) return;
1820
- keysRef.current.add(e.code);
1821
- const ev = keyMap?.[e.code];
1822
- if (ev) {
1823
- eventBus.emit(`UI:${ev}`, {});
1824
- e.preventDefault();
1825
- }
1826
- };
1827
- const up = (e) => {
1828
- keysRef.current.delete(e.code);
1829
- const ev = keyUpMap?.[e.code];
1830
- if (ev) eventBus.emit(`UI:${ev}`, {});
1831
- };
1832
- window.addEventListener("keydown", down);
1833
- window.addEventListener("keyup", up);
1834
- return () => {
1835
- window.removeEventListener("keydown", down);
1836
- window.removeEventListener("keyup", up);
1837
- };
1838
- }, [keyMap, keyUpMap, eventBus]);
1839
- const eventHandlers = useGameCanvas3DEvents({
1840
- tileClickEvent,
1841
- unitClickEvent,
1842
- featureClickEvent,
1843
- canvasClickEvent,
1844
- tileHoverEvent,
1845
- tileLeaveEvent,
1846
- unitAnimationEvent,
1847
- cameraChangeEvent
1848
- });
1849
- const drawnItems = React7.useMemo(() => collectDrawnItems(drawables ?? []), [drawables]);
1850
- const scenePositions = React7.useMemo(() => drawnItems.map((i) => i.pos), [drawnItems]);
1851
- const hitIndex = React7.useMemo(() => buildHitIndex(drawnItems), [drawnItems]);
1852
- const gridBounds = React7.useMemo(() => {
1853
- if (scenePositions.length === 0) {
1854
- return { minX: 0, maxX: 10, minZ: 0, maxZ: 10 };
1855
- }
1856
- let minX = Infinity;
1857
- let maxX = -Infinity;
1858
- let minZ = Infinity;
1859
- let maxZ = -Infinity;
1860
- for (const p of scenePositions) {
1861
- if (p.x < minX) minX = p.x;
1862
- if (p.x > maxX) maxX = p.x;
1863
- if (p.y < minZ) minZ = p.y;
1864
- if (p.y > maxZ) maxZ = p.y;
1865
- }
1866
- return { minX, maxX, minZ, maxZ };
1867
- }, [scenePositions]);
1868
- const cameraTarget = React7.useMemo(
1869
- () => [
1870
- (gridBounds.minX + gridBounds.maxX) / 2,
1871
- 0,
1872
- (gridBounds.minZ + gridBounds.maxZ) / 2
1873
- ],
1874
- [gridBounds]
1875
- );
1876
- const gridConfig = React7.useMemo(
1877
- () => ({
1878
- ...DEFAULT_GRID_CONFIG,
1879
- offsetX: -(gridBounds.maxX - gridBounds.minX) / 2,
1880
- offsetZ: -(gridBounds.maxZ - gridBounds.minZ) / 2
1881
- }),
1882
- [gridBounds]
1883
- );
1884
- const drawableProjector = React7.useMemo(
1885
- () => create3DProjector({
1886
- cellSize: gridConfig.cellSize,
1887
- offsetX: -gridBounds.minX * gridConfig.cellSize,
1888
- offsetZ: -gridBounds.minZ * gridConfig.cellSize
1889
- }),
1890
- [gridBounds, gridConfig]
1891
- );
1892
- React7.useImperativeHandle(ref, () => ({
1893
- getCameraPosition: () => {
1894
- if (controlsRef.current) {
1895
- const pos = controlsRef.current.object.position;
1896
- return new THREE6__namespace.Vector3(pos.x, pos.y, pos.z);
1897
- }
1898
- return null;
1899
- },
1900
- setCameraPosition: (x, y, z) => {
1901
- if (controlsRef.current) {
1902
- controlsRef.current.object.position.set(x, y, z);
1903
- controlsRef.current.update();
1904
- }
1905
- },
1906
- lookAt: (x, y, z) => {
1907
- if (controlsRef.current) {
1908
- controlsRef.current.target.set(x, y, z);
1909
- controlsRef.current.update();
1910
- }
1911
- },
1912
- resetCamera: () => {
1913
- if (controlsRef.current) {
1914
- controlsRef.current.reset();
1915
- }
1916
- },
1917
- screenshot: () => {
1918
- const canvas = containerRef.current?.querySelector("canvas");
1919
- if (canvas) {
1920
- return canvas.toDataURL("image/png");
1921
- }
1922
- return null;
1923
- }
1924
- }));
1925
- const cameraConfig = React7.useMemo(() => {
1926
- const size = Math.max(
1927
- gridBounds.maxX - gridBounds.minX,
1928
- gridBounds.maxZ - gridBounds.minZ,
1929
- 4
1930
- // minimum framing distance so a tiny board isn't zoomed in
1931
- );
1932
- const cx = (gridBounds.minX + gridBounds.maxX) / 2;
1933
- const cz = (gridBounds.minZ + gridBounds.maxZ) / 2;
1934
- const d = size * 1;
1935
- switch (cameraMode) {
1936
- case "isometric":
1937
- return { position: [cx + d, d * 0.8, cz + d], fov: 45 };
1938
- case "top-down":
1939
- return { position: [cx, d * 2, cz + d * 0.35], fov: 45 };
1940
- case "follow":
1941
- return { position: [cx, d * 0.5, cz + d], fov: 45 };
1942
- case "perspective":
1943
- default:
1944
- return { position: [cx + d, d, cz + d], fov: 45 };
1970
+ function useRaycaster(options) {
1971
+ const { camera, canvas, cellSize = 1, offsetX = 0, offsetZ = 0 } = options;
1972
+ const raycaster = React6.useRef(new THREE9__namespace.Raycaster());
1973
+ const mouse = React6.useRef(new THREE9__namespace.Vector2());
1974
+ const clientToNDC = React6.useCallback(
1975
+ (clientX, clientY) => {
1976
+ if (!canvas) {
1977
+ return { x: 0, y: 0 };
1945
1978
  }
1946
- }, [cameraMode, gridBounds]);
1947
- const followWorld = React7.useMemo(() => {
1948
- if (followTarget) return drawableProjector.toWorld(followTarget);
1949
- return cameraTarget;
1950
- }, [followTarget, drawableProjector, cameraTarget]);
1951
- const followOffset = React7.useMemo(
1952
- () => cameraMode === "chase" ? [0, 4, -7] : [5, 7, 5],
1953
- [cameraMode]
1954
- );
1955
- const handleGroundClick = React7.useCallback((e) => {
1956
- if (!tileClickEvent && !unitClickEvent) return;
1957
- e.stopPropagation();
1958
- const cell = {
1959
- x: Math.round(e.point.x / gridConfig.cellSize + gridBounds.minX),
1960
- y: Math.round(e.point.z / gridConfig.cellSize + gridBounds.minZ)
1979
+ const rect = canvas.getBoundingClientRect();
1980
+ return {
1981
+ x: (clientX - rect.left) / rect.width * 2 - 1,
1982
+ y: -((clientY - rect.top) / rect.height) * 2 + 1
1961
1983
  };
1962
- const hitId = hitIndex.get(`${cell.x},${cell.y}`);
1963
- if (hitId !== void 0 && unitClickEvent) {
1964
- eventBus.emit(`UI:${unitClickEvent}`, { unitId: hitId, x: cell.x, z: cell.y });
1965
- } else if (tileClickEvent) {
1966
- eventBus.emit(`UI:${tileClickEvent}`, { x: cell.x, z: cell.y });
1967
- }
1968
- }, [tileClickEvent, unitClickEvent, gridConfig, gridBounds, hitIndex, eventBus]);
1969
- if (externalLoading) {
1970
- return /* @__PURE__ */ jsxRuntime.jsx(
1971
- Canvas3DLoadingState,
1972
- {
1973
- progress: 1,
1974
- loaded: 1,
1975
- total: 1,
1976
- message: loadingMessage,
1977
- className
1978
- }
1979
- );
1980
- }
1981
- const displayError = externalError || internalError;
1982
- if (displayError) {
1983
- return /* @__PURE__ */ jsxRuntime.jsx(Canvas3DErrorBoundary, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "game-canvas-3d game-canvas-3d--error", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "game-canvas-3d__error", children: [
1984
- "Error: ",
1985
- displayError
1986
- ] }) }) });
1987
- }
1988
- return /* @__PURE__ */ jsxRuntime.jsx(
1989
- Canvas3DErrorBoundary,
1990
- {
1991
- onError: (err) => setInternalError(err.message),
1992
- onReset: () => setInternalError(null),
1993
- children: /* @__PURE__ */ jsxRuntime.jsx(
1994
- "div",
1995
- {
1996
- ref: containerRef,
1997
- className: cn("game-canvas-3d relative w-full overflow-hidden", className),
1998
- style: { flex: "1 1 0", minHeight: "400px" },
1999
- "data-orientation": orientation,
2000
- "data-camera-mode": cameraMode,
2001
- "data-overlay": overlay,
2002
- children: /* @__PURE__ */ jsxRuntime.jsxs(
2003
- fiber.Canvas,
2004
- {
2005
- shadows,
2006
- camera: {
2007
- position: cameraConfig.position,
2008
- fov: cameraConfig.fov,
2009
- near: 0.1,
2010
- far: 1e3
2011
- },
2012
- style: { background: backgroundColor, position: "absolute", inset: 0 },
2013
- onClick: (e) => {
2014
- if (e.target === e.currentTarget) {
2015
- eventHandlers.handleCanvasClick(e);
2016
- }
2017
- },
2018
- children: [
2019
- /* @__PURE__ */ jsxRuntime.jsx(CameraController3D, { onCameraChange: eventHandlers.handleCameraChange }),
2020
- (cameraMode === "follow" || cameraMode === "chase") && /* @__PURE__ */ jsxRuntime.jsx(FollowCamera3D, { target: followWorld, offset: followOffset }),
2021
- /* @__PURE__ */ jsxRuntime.jsx(
2022
- Lighting3D,
2023
- {
2024
- shadows,
2025
- shadowBias: -4e-4,
2026
- shadowNormalBias: 0.04,
2027
- shadowCameraSize: 5,
2028
- shadowCameraNear: 0.5,
2029
- shadowCameraFar: 500
2030
- }
2031
- ),
2032
- showGrid && /* @__PURE__ */ jsxRuntime.jsx(
2033
- drei.Grid,
2034
- {
2035
- args: [
2036
- Math.max(gridBounds.maxX - gridBounds.minX + 2, 10),
2037
- Math.max(gridBounds.maxZ - gridBounds.minZ + 2, 10)
2038
- ],
2039
- position: [
2040
- (gridBounds.maxX - gridBounds.minX) / 2 - 0.5,
2041
- 0,
2042
- (gridBounds.maxZ - gridBounds.minZ) / 2 - 0.5
2043
- ],
2044
- cellSize: 1,
2045
- cellThickness: 1,
2046
- cellColor: GRID_COLORS_3D.cell,
2047
- sectionSize: 5,
2048
- sectionThickness: 1.5,
2049
- sectionColor: GRID_COLORS_3D.section,
2050
- fadeDistance: 50,
2051
- fadeStrength: 1
2052
- }
2053
- ),
2054
- drawables && drawables.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("group", { children: drawables.map((node, i) => /* @__PURE__ */ jsxRuntime.jsx(Drawable3D, { node, projector: drawableProjector }, i)) }),
2055
- (tileClickEvent || unitClickEvent) && /* @__PURE__ */ jsxRuntime.jsxs(
2056
- "mesh",
2057
- {
2058
- rotation: [-Math.PI / 2, 0, 0],
2059
- position: [
2060
- (gridBounds.maxX - gridBounds.minX) / 2 * gridConfig.cellSize,
2061
- 0,
2062
- (gridBounds.maxZ - gridBounds.minZ) / 2 * gridConfig.cellSize
2063
- ],
2064
- onClick: handleGroundClick,
2065
- children: [
2066
- /* @__PURE__ */ jsxRuntime.jsx(
2067
- "planeGeometry",
2068
- {
2069
- args: [
2070
- (gridBounds.maxX - gridBounds.minX + 4) * gridConfig.cellSize,
2071
- (gridBounds.maxZ - gridBounds.minZ + 4) * gridConfig.cellSize
2072
- ]
2073
- }
2074
- ),
2075
- /* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })
2076
- ]
2077
- }
2078
- ),
2079
- children,
2080
- /* @__PURE__ */ jsxRuntime.jsx(
2081
- drei.OrbitControls,
2082
- {
2083
- ref: controlsRef,
2084
- enabled: cameraMode !== "follow" && cameraMode !== "chase",
2085
- target: cameraTarget,
2086
- enableDamping: true,
2087
- dampingFactor: 0.05,
2088
- enableZoom: true,
2089
- enablePan: true,
2090
- touches: { ONE: THREE6__namespace.TOUCH.ROTATE, TWO: THREE6__namespace.TOUCH.DOLLY_PAN },
2091
- minDistance: 2,
2092
- maxDistance: 100,
2093
- maxPolarAngle: Math.PI / 2 - 0.1
2094
- }
2095
- )
2096
- ]
2097
- }
2098
- )
2099
- }
2100
- )
1984
+ },
1985
+ [canvas]
1986
+ );
1987
+ const isWithinCanvas = React6.useCallback(
1988
+ (clientX, clientY) => {
1989
+ if (!canvas) return false;
1990
+ const rect = canvas.getBoundingClientRect();
1991
+ return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom;
1992
+ },
1993
+ [canvas]
1994
+ );
1995
+ const getIntersection = React6.useCallback(
1996
+ (clientX, clientY, objects) => {
1997
+ if (!camera || !canvas) return null;
1998
+ const ndc = clientToNDC(clientX, clientY);
1999
+ mouse.current.set(ndc.x, ndc.y);
2000
+ raycaster.current.setFromCamera(mouse.current, camera);
2001
+ const intersects = raycaster.current.intersectObjects(objects, true);
2002
+ if (intersects.length > 0) {
2003
+ const hit = intersects[0];
2004
+ return {
2005
+ object: hit.object,
2006
+ point: hit.point,
2007
+ distance: hit.distance,
2008
+ uv: hit.uv,
2009
+ face: hit.face,
2010
+ faceIndex: hit.faceIndex,
2011
+ instanceId: hit.instanceId
2012
+ };
2101
2013
  }
2102
- );
2103
- }
2104
- );
2105
- GameCanvas3D.displayName = "GameCanvas3D";
2014
+ return null;
2015
+ },
2016
+ [camera, canvas, clientToNDC]
2017
+ );
2018
+ const getAllIntersections = React6.useCallback(
2019
+ (clientX, clientY, objects) => {
2020
+ if (!camera || !canvas) return [];
2021
+ const ndc = clientToNDC(clientX, clientY);
2022
+ mouse.current.set(ndc.x, ndc.y);
2023
+ raycaster.current.setFromCamera(mouse.current, camera);
2024
+ const intersects = raycaster.current.intersectObjects(objects, true);
2025
+ return intersects.map((hit) => ({
2026
+ object: hit.object,
2027
+ point: hit.point,
2028
+ distance: hit.distance,
2029
+ uv: hit.uv,
2030
+ face: hit.face,
2031
+ faceIndex: hit.faceIndex,
2032
+ instanceId: hit.instanceId
2033
+ }));
2034
+ },
2035
+ [camera, canvas, clientToNDC]
2036
+ );
2037
+ const getGridCoordinates = React6.useCallback(
2038
+ (clientX, clientY) => {
2039
+ if (!camera || !canvas) return null;
2040
+ const ndc = clientToNDC(clientX, clientY);
2041
+ mouse.current.set(ndc.x, ndc.y);
2042
+ raycaster.current.setFromCamera(mouse.current, camera);
2043
+ const plane = new THREE9__namespace.Plane(new THREE9__namespace.Vector3(0, 1, 0), 0);
2044
+ const target = new THREE9__namespace.Vector3();
2045
+ const intersection = raycaster.current.ray.intersectPlane(plane, target);
2046
+ if (intersection) {
2047
+ const gridX = Math.round((target.x - offsetX) / cellSize);
2048
+ const gridZ = Math.round((target.z - offsetZ) / cellSize);
2049
+ return { x: gridX, z: gridZ };
2050
+ }
2051
+ return null;
2052
+ },
2053
+ [camera, canvas, cellSize, offsetX, offsetZ, clientToNDC]
2054
+ );
2055
+ const getTileAtPosition = React6.useCallback(
2056
+ (clientX, clientY, scene) => {
2057
+ if (!camera || !canvas) return null;
2058
+ const tileMeshes = [];
2059
+ scene.traverse((obj) => {
2060
+ if (obj.userData.type === "tile" || obj.userData.isTile) {
2061
+ tileMeshes.push(obj);
2062
+ }
2063
+ });
2064
+ const hit = getIntersection(clientX, clientY, tileMeshes);
2065
+ if (hit) {
2066
+ const gridCoords2 = getGridCoordinates(clientX, clientY);
2067
+ if (gridCoords2) {
2068
+ return {
2069
+ gridX: gridCoords2.x,
2070
+ gridZ: gridCoords2.z,
2071
+ worldPosition: hit.point,
2072
+ objectType: hit.object.userData.type || "tile",
2073
+ objectId: hit.object.userData.id || hit.object.userData.tileId
2074
+ };
2075
+ }
2076
+ }
2077
+ const gridCoords = getGridCoordinates(clientX, clientY);
2078
+ if (gridCoords) {
2079
+ return {
2080
+ gridX: gridCoords.x,
2081
+ gridZ: gridCoords.z,
2082
+ worldPosition: new THREE9__namespace.Vector3(
2083
+ gridCoords.x * cellSize + offsetX,
2084
+ 0,
2085
+ gridCoords.z * cellSize + offsetZ
2086
+ )
2087
+ };
2088
+ }
2089
+ return null;
2090
+ },
2091
+ [camera, canvas, getIntersection, getGridCoordinates, cellSize, offsetX, offsetZ]
2092
+ );
2093
+ return {
2094
+ raycaster,
2095
+ mouse,
2096
+ getIntersection,
2097
+ getAllIntersections,
2098
+ getGridCoordinates,
2099
+ getTileAtPosition,
2100
+ clientToNDC,
2101
+ isWithinCanvas
2102
+ };
2103
+ }
2106
2104
  var DEFAULT_CONFIG = {
2107
2105
  cellSize: 1,
2108
2106
  offsetX: 0,
@@ -2111,7 +2109,7 @@ var DEFAULT_CONFIG = {
2111
2109
  };
2112
2110
  function gridToWorld(gridX, gridZ, config = DEFAULT_CONFIG) {
2113
2111
  const opts = { ...DEFAULT_CONFIG, ...config };
2114
- return new THREE6__namespace.Vector3(
2112
+ return new THREE9__namespace.Vector3(
2115
2113
  gridX * opts.cellSize + opts.offsetX,
2116
2114
  opts.elevation,
2117
2115
  gridZ * opts.cellSize + opts.offsetZ
@@ -2125,17 +2123,17 @@ function worldToGrid(worldX, worldZ, config = DEFAULT_CONFIG) {
2125
2123
  };
2126
2124
  }
2127
2125
  function raycastToPlane(camera, mouseX, mouseY, planeY = 0) {
2128
- const raycaster = new THREE6__namespace.Raycaster();
2129
- const mouse = new THREE6__namespace.Vector2(mouseX, mouseY);
2126
+ const raycaster = new THREE9__namespace.Raycaster();
2127
+ const mouse = new THREE9__namespace.Vector2(mouseX, mouseY);
2130
2128
  raycaster.setFromCamera(mouse, camera);
2131
- const plane = new THREE6__namespace.Plane(new THREE6__namespace.Vector3(0, 1, 0), -planeY);
2132
- const target = new THREE6__namespace.Vector3();
2129
+ const plane = new THREE9__namespace.Plane(new THREE9__namespace.Vector3(0, 1, 0), -planeY);
2130
+ const target = new THREE9__namespace.Vector3();
2133
2131
  const intersection = raycaster.ray.intersectPlane(plane, target);
2134
2132
  return intersection ? target : null;
2135
2133
  }
2136
2134
  function raycastToObjects(camera, mouseX, mouseY, objects) {
2137
- const raycaster = new THREE6__namespace.Raycaster();
2138
- const mouse = new THREE6__namespace.Vector2(mouseX, mouseY);
2135
+ const raycaster = new THREE9__namespace.Raycaster();
2136
+ const mouse = new THREE9__namespace.Vector2(mouseX, mouseY);
2139
2137
  raycaster.setFromCamera(mouse, camera);
2140
2138
  const intersects = raycaster.intersectObjects(objects, true);
2141
2139
  return intersects.length > 0 ? intersects[0] : null;
@@ -2163,69 +2161,250 @@ function getNeighbors(x, z, includeDiagonal = false) {
2163
2161
  { x: x - 1, z: z + 1 }
2164
2162
  );
2165
2163
  }
2166
- return neighbors;
2167
- }
2168
- function isInBounds(x, z, bounds) {
2169
- return x >= bounds.minX && x <= bounds.maxX && z >= bounds.minZ && z <= bounds.maxZ;
2170
- }
2171
- function getCellsInRadius(centerX, centerZ, radius) {
2172
- const cells = [];
2173
- const radiusSquared = radius * radius;
2174
- const minX = Math.floor(centerX - radius);
2175
- const maxX = Math.ceil(centerX + radius);
2176
- const minZ = Math.floor(centerZ - radius);
2177
- const maxZ = Math.ceil(centerZ + radius);
2178
- for (let x = minX; x <= maxX; x++) {
2179
- for (let z = minZ; z <= maxZ; z++) {
2180
- const dx = x - centerX;
2181
- const dz = z - centerZ;
2182
- if (dx * dx + dz * dz <= radiusSquared) {
2183
- cells.push({ x, z });
2164
+ return neighbors;
2165
+ }
2166
+ function isInBounds(x, z, bounds) {
2167
+ return x >= bounds.minX && x <= bounds.maxX && z >= bounds.minZ && z <= bounds.maxZ;
2168
+ }
2169
+ function getCellsInRadius(centerX, centerZ, radius) {
2170
+ const cells = [];
2171
+ const radiusSquared = radius * radius;
2172
+ const minX = Math.floor(centerX - radius);
2173
+ const maxX = Math.ceil(centerX + radius);
2174
+ const minZ = Math.floor(centerZ - radius);
2175
+ const maxZ = Math.ceil(centerZ + radius);
2176
+ for (let x = minX; x <= maxX; x++) {
2177
+ for (let z = minZ; z <= maxZ; z++) {
2178
+ const dx = x - centerX;
2179
+ const dz = z - centerZ;
2180
+ if (dx * dx + dz * dz <= radiusSquared) {
2181
+ cells.push({ x, z });
2182
+ }
2183
+ }
2184
+ }
2185
+ return cells;
2186
+ }
2187
+ function createGridHighlight(color = 16776960, opacity = 0.3) {
2188
+ const geometry = new THREE9__namespace.PlaneGeometry(0.95, 0.95);
2189
+ const material = new THREE9__namespace.MeshBasicMaterial({
2190
+ color,
2191
+ transparent: true,
2192
+ opacity,
2193
+ side: THREE9__namespace.DoubleSide
2194
+ });
2195
+ const mesh = new THREE9__namespace.Mesh(geometry, material);
2196
+ mesh.rotation.x = -Math.PI / 2;
2197
+ mesh.position.y = 0.01;
2198
+ return mesh;
2199
+ }
2200
+ function normalizeMouseCoordinates(clientX, clientY, element) {
2201
+ const rect = element.getBoundingClientRect();
2202
+ return {
2203
+ x: (clientX - rect.left) / rect.width * 2 - 1,
2204
+ y: -((clientY - rect.top) / rect.height) * 2 + 1
2205
+ };
2206
+ }
2207
+ function isInFrustum(position, camera, padding = 0) {
2208
+ const frustum = new THREE9__namespace.Frustum();
2209
+ const projScreenMatrix = new THREE9__namespace.Matrix4();
2210
+ projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
2211
+ frustum.setFromProjectionMatrix(projScreenMatrix);
2212
+ const sphere = new THREE9__namespace.Sphere(position, padding);
2213
+ return frustum.intersectsSphere(sphere);
2214
+ }
2215
+ function filterByFrustum(positions, camera, padding = 0) {
2216
+ const frustum = new THREE9__namespace.Frustum();
2217
+ const projScreenMatrix = new THREE9__namespace.Matrix4();
2218
+ projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
2219
+ frustum.setFromProjectionMatrix(projScreenMatrix);
2220
+ return positions.filter((position) => {
2221
+ const sphere = new THREE9__namespace.Sphere(position, padding);
2222
+ return frustum.intersectsSphere(sphere);
2223
+ });
2224
+ }
2225
+ function getVisibleIndices(positions, camera, padding = 0) {
2226
+ const frustum = new THREE9__namespace.Frustum();
2227
+ const projScreenMatrix = new THREE9__namespace.Matrix4();
2228
+ const visible = /* @__PURE__ */ new Set();
2229
+ projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
2230
+ frustum.setFromProjectionMatrix(projScreenMatrix);
2231
+ positions.forEach((position, index) => {
2232
+ const sphere = new THREE9__namespace.Sphere(position, padding);
2233
+ if (frustum.intersectsSphere(sphere)) {
2234
+ visible.add(index);
2235
+ }
2236
+ });
2237
+ return visible;
2238
+ }
2239
+ function calculateLODLevel(position, camera, lodLevels) {
2240
+ const distance = position.distanceTo(camera.position);
2241
+ for (let i = 0; i < lodLevels.length; i++) {
2242
+ if (distance < lodLevels[i]) {
2243
+ return i;
2244
+ }
2245
+ }
2246
+ return lodLevels.length;
2247
+ }
2248
+ function updateInstanceLOD(instancedMesh, positions, camera, lodDistances) {
2249
+ const lodIndices = new Uint8Array(positions.length);
2250
+ positions.forEach((position, index) => {
2251
+ lodIndices[index] = calculateLODLevel(position, camera, lodDistances);
2252
+ });
2253
+ return lodIndices;
2254
+ }
2255
+ function cullInstancedMesh(instancedMesh, positions, visibleIndices) {
2256
+ const dummy = new THREE9__namespace.Object3D();
2257
+ let visibleCount = 0;
2258
+ positions.forEach((position, index) => {
2259
+ if (visibleIndices.has(index)) {
2260
+ dummy.position.copy(position);
2261
+ dummy.updateMatrix();
2262
+ instancedMesh.setMatrixAt(visibleCount, dummy.matrix);
2263
+ visibleCount++;
2264
+ }
2265
+ });
2266
+ instancedMesh.count = visibleCount;
2267
+ instancedMesh.instanceMatrix.needsUpdate = true;
2268
+ return visibleCount;
2269
+ }
2270
+ var SpatialHashGrid = class {
2271
+ constructor(cellSize = 10) {
2272
+ __publicField(this, "cellSize");
2273
+ __publicField(this, "cells");
2274
+ __publicField(this, "objectPositions");
2275
+ this.cellSize = cellSize;
2276
+ this.cells = /* @__PURE__ */ new Map();
2277
+ this.objectPositions = /* @__PURE__ */ new Map();
2278
+ }
2279
+ /**
2280
+ * Get cell key for a position
2281
+ */
2282
+ getCellKey(x, z) {
2283
+ const cellX = Math.floor(x / this.cellSize);
2284
+ const cellZ = Math.floor(z / this.cellSize);
2285
+ return `${cellX},${cellZ}`;
2286
+ }
2287
+ /**
2288
+ * Insert an object into the grid
2289
+ */
2290
+ insert(id, position) {
2291
+ const key = this.getCellKey(position.x, position.z);
2292
+ if (!this.cells.has(key)) {
2293
+ this.cells.set(key, /* @__PURE__ */ new Set());
2294
+ }
2295
+ this.cells.get(key).add(id);
2296
+ this.objectPositions.set(id, position.clone());
2297
+ }
2298
+ /**
2299
+ * Remove an object from the grid
2300
+ */
2301
+ remove(id) {
2302
+ const position = this.objectPositions.get(id);
2303
+ if (position) {
2304
+ const key = this.getCellKey(position.x, position.z);
2305
+ this.cells.get(key)?.delete(id);
2306
+ this.objectPositions.delete(id);
2307
+ }
2308
+ }
2309
+ /**
2310
+ * Update an object's position
2311
+ */
2312
+ update(id, newPosition) {
2313
+ this.remove(id);
2314
+ this.insert(id, newPosition);
2315
+ }
2316
+ /**
2317
+ * Query objects within a radius of a position
2318
+ */
2319
+ queryRadius(center, radius) {
2320
+ const results = [];
2321
+ const radiusSquared = radius * radius;
2322
+ const minCellX = Math.floor((center.x - radius) / this.cellSize);
2323
+ const maxCellX = Math.floor((center.x + radius) / this.cellSize);
2324
+ const minCellZ = Math.floor((center.z - radius) / this.cellSize);
2325
+ const maxCellZ = Math.floor((center.z + radius) / this.cellSize);
2326
+ for (let x = minCellX; x <= maxCellX; x++) {
2327
+ for (let z = minCellZ; z <= maxCellZ; z++) {
2328
+ const key = `${x},${z}`;
2329
+ const cell = this.cells.get(key);
2330
+ if (cell) {
2331
+ cell.forEach((id) => {
2332
+ const position = this.objectPositions.get(id);
2333
+ if (position) {
2334
+ const dx = position.x - center.x;
2335
+ const dz = position.z - center.z;
2336
+ if (dx * dx + dz * dz <= radiusSquared) {
2337
+ results.push(id);
2338
+ }
2339
+ }
2340
+ });
2341
+ }
2342
+ }
2343
+ }
2344
+ return results;
2345
+ }
2346
+ /**
2347
+ * Query objects within a bounding box
2348
+ */
2349
+ queryBox(minX, maxX, minZ, maxZ) {
2350
+ const results = [];
2351
+ const minCellX = Math.floor(minX / this.cellSize);
2352
+ const maxCellX = Math.floor(maxX / this.cellSize);
2353
+ const minCellZ = Math.floor(minZ / this.cellSize);
2354
+ const maxCellZ = Math.floor(maxZ / this.cellSize);
2355
+ for (let x = minCellX; x <= maxCellX; x++) {
2356
+ for (let z = minCellZ; z <= maxCellZ; z++) {
2357
+ const key = `${x},${z}`;
2358
+ const cell = this.cells.get(key);
2359
+ if (cell) {
2360
+ cell.forEach((id) => {
2361
+ const position = this.objectPositions.get(id);
2362
+ if (position && position.x >= minX && position.x <= maxX && position.z >= minZ && position.z <= maxZ) {
2363
+ results.push(id);
2364
+ }
2365
+ });
2366
+ }
2184
2367
  }
2185
2368
  }
2369
+ return results;
2186
2370
  }
2187
- return cells;
2188
- }
2189
- function createGridHighlight(color = 16776960, opacity = 0.3) {
2190
- const geometry = new THREE6__namespace.PlaneGeometry(0.95, 0.95);
2191
- const material = new THREE6__namespace.MeshBasicMaterial({
2192
- color,
2193
- transparent: true,
2194
- opacity,
2195
- side: THREE6__namespace.DoubleSide
2196
- });
2197
- const mesh = new THREE6__namespace.Mesh(geometry, material);
2198
- mesh.rotation.x = -Math.PI / 2;
2199
- mesh.position.y = 0.01;
2200
- return mesh;
2201
- }
2202
- function normalizeMouseCoordinates(clientX, clientY, element) {
2203
- const rect = element.getBoundingClientRect();
2204
- return {
2205
- x: (clientX - rect.left) / rect.width * 2 - 1,
2206
- y: -((clientY - rect.top) / rect.height) * 2 + 1
2207
- };
2208
- }
2371
+ /**
2372
+ * Clear all objects from the grid
2373
+ */
2374
+ clear() {
2375
+ this.cells.clear();
2376
+ this.objectPositions.clear();
2377
+ }
2378
+ /**
2379
+ * Get statistics about the grid
2380
+ */
2381
+ getStats() {
2382
+ return {
2383
+ objects: this.objectPositions.size,
2384
+ cells: this.cells.size
2385
+ };
2386
+ }
2387
+ };
2209
2388
  function useTapReveal(options = {}) {
2210
2389
  const { onReveal, onDismiss, refs, enabled = true } = options;
2211
- const [revealed, setRevealed] = React7.useState(false);
2212
- const onRevealRef = React7.useRef(onReveal);
2213
- const onDismissRef = React7.useRef(onDismiss);
2390
+ const [revealed, setRevealed] = React6.useState(false);
2391
+ const onRevealRef = React6.useRef(onReveal);
2392
+ const onDismissRef = React6.useRef(onDismiss);
2214
2393
  onRevealRef.current = onReveal;
2215
2394
  onDismissRef.current = onDismiss;
2216
- const reveal = React7.useCallback(() => {
2395
+ const reveal = React6.useCallback(() => {
2217
2396
  setRevealed((wasRevealed) => {
2218
2397
  if (!wasRevealed) onRevealRef.current?.();
2219
2398
  return true;
2220
2399
  });
2221
2400
  }, []);
2222
- const dismiss = React7.useCallback(() => {
2401
+ const dismiss = React6.useCallback(() => {
2223
2402
  setRevealed((wasRevealed) => {
2224
2403
  if (wasRevealed) onDismissRef.current?.();
2225
2404
  return false;
2226
2405
  });
2227
2406
  }, []);
2228
- const onPointerDown = React7.useCallback(
2407
+ const onPointerDown = React6.useCallback(
2229
2408
  (e) => {
2230
2409
  if (!enabled || e.pointerType === "mouse") return;
2231
2410
  if (revealed) dismiss();
@@ -2233,7 +2412,7 @@ function useTapReveal(options = {}) {
2233
2412
  },
2234
2413
  [enabled, revealed, reveal, dismiss]
2235
2414
  );
2236
- React7.useEffect(() => {
2415
+ React6.useEffect(() => {
2237
2416
  if (!revealed || typeof document === "undefined") return;
2238
2417
  const onDocDown = (ev) => {
2239
2418
  const target = ev.target;
@@ -2353,7 +2532,7 @@ var positionStyles = {
2353
2532
  fixed: "fixed",
2354
2533
  sticky: "sticky"
2355
2534
  };
2356
- var Box = React7__default.default.forwardRef(
2535
+ var Box = React6__default.default.forwardRef(
2357
2536
  ({
2358
2537
  padding,
2359
2538
  paddingX,
@@ -2385,20 +2564,20 @@ var Box = React7__default.default.forwardRef(
2385
2564
  ...rest
2386
2565
  }, ref) => {
2387
2566
  const eventBus = useEventBus();
2388
- const handleClick = React7.useCallback((e) => {
2567
+ const handleClick = React6.useCallback((e) => {
2389
2568
  if (action) {
2390
2569
  e.stopPropagation();
2391
2570
  eventBus.emit(`UI:${action}`, actionPayload ?? {});
2392
2571
  }
2393
2572
  onClick?.(e);
2394
2573
  }, [action, actionPayload, eventBus, onClick]);
2395
- const handleMouseEnter = React7.useCallback((e) => {
2574
+ const handleMouseEnter = React6.useCallback((e) => {
2396
2575
  if (hoverEvent) {
2397
2576
  eventBus.emit(`UI:${hoverEvent}`, { hovered: true });
2398
2577
  }
2399
2578
  onMouseEnter?.(e);
2400
2579
  }, [hoverEvent, eventBus, onMouseEnter]);
2401
- const handleMouseLeave = React7.useCallback((e) => {
2580
+ const handleMouseLeave = React6.useCallback((e) => {
2402
2581
  if (hoverEvent) {
2403
2582
  eventBus.emit(`UI:${hoverEvent}`, { hovered: false });
2404
2583
  }
@@ -2406,19 +2585,19 @@ var Box = React7__default.default.forwardRef(
2406
2585
  }, [hoverEvent, eventBus, onMouseLeave]);
2407
2586
  const { triggerProps } = useTapReveal({
2408
2587
  enabled: tapReveal && !!hoverEvent,
2409
- onReveal: React7.useCallback(() => {
2588
+ onReveal: React6.useCallback(() => {
2410
2589
  if (hoverEvent) eventBus.emit(`UI:${hoverEvent}`, { hovered: true });
2411
2590
  }, [hoverEvent, eventBus]),
2412
- onDismiss: React7.useCallback(() => {
2591
+ onDismiss: React6.useCallback(() => {
2413
2592
  if (hoverEvent) eventBus.emit(`UI:${hoverEvent}`, { hovered: false });
2414
2593
  }, [hoverEvent, eventBus])
2415
2594
  });
2416
- const handlePointerDown = React7.useCallback((e) => {
2595
+ const handlePointerDown = React6.useCallback((e) => {
2417
2596
  if (hoverEvent && tapReveal) triggerProps.onPointerDown(e);
2418
2597
  onPointerDown?.(e);
2419
2598
  }, [hoverEvent, tapReveal, triggerProps, onPointerDown]);
2420
2599
  const isClickable = action || onClick;
2421
- return React7__default.default.createElement(
2600
+ return React6__default.default.createElement(
2422
2601
  Component2,
2423
2602
  {
2424
2603
  ref,
@@ -2539,7 +2718,7 @@ var Typography = ({
2539
2718
  }) => {
2540
2719
  const variant = variantProp ?? (level ? `h${level}` : "body1");
2541
2720
  const Component2 = as || defaultElements[variant];
2542
- return React7__default.default.createElement(
2721
+ return React6__default.default.createElement(
2543
2722
  Component2,
2544
2723
  {
2545
2724
  id,
@@ -2612,7 +2791,7 @@ var Stack = ({
2612
2791
  };
2613
2792
  const isHorizontal = direction === "horizontal";
2614
2793
  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";
2615
- return React7__default.default.createElement(
2794
+ return React6__default.default.createElement(
2616
2795
  Component2,
2617
2796
  {
2618
2797
  className: cn(
@@ -3183,25 +3362,25 @@ function orbitRingPositions(count, radius, tilt) {
3183
3362
  return positions;
3184
3363
  }
3185
3364
  function arcCurve3D(from, to, offset) {
3186
- const start = new THREE6.Vector3(...from);
3187
- const end = new THREE6.Vector3(...to);
3188
- const mid = new THREE6.Vector3().addVectors(start, end).multiplyScalar(0.5);
3189
- const dir = new THREE6.Vector3().subVectors(end, start).normalize();
3190
- const up = new THREE6.Vector3(0, 1, 0);
3191
- const perp = new THREE6.Vector3().crossVectors(dir, up).normalize();
3365
+ const start = new THREE9.Vector3(...from);
3366
+ const end = new THREE9.Vector3(...to);
3367
+ const mid = new THREE9.Vector3().addVectors(start, end).multiplyScalar(0.5);
3368
+ const dir = new THREE9.Vector3().subVectors(end, start).normalize();
3369
+ const up = new THREE9.Vector3(0, 1, 0);
3370
+ const perp = new THREE9.Vector3().crossVectors(dir, up).normalize();
3192
3371
  if (perp.length() < 1e-3) {
3193
- perp.crossVectors(dir, new THREE6.Vector3(1, 0, 0)).normalize();
3372
+ perp.crossVectors(dir, new THREE9.Vector3(1, 0, 0)).normalize();
3194
3373
  }
3195
3374
  const control = mid.clone().add(perp.multiplyScalar(offset));
3196
3375
  control.y += Math.abs(offset) * 0.3;
3197
- return new THREE6.QuadraticBezierCurve3(start, control, end);
3376
+ return new THREE9.QuadraticBezierCurve3(start, control, end);
3198
3377
  }
3199
3378
  function selfLoopCurve3D(position, loopRadius) {
3200
- const base = new THREE6.Vector3(...position);
3201
- const start = base.clone().add(new THREE6.Vector3(-loopRadius * 0.3, 0, 0));
3202
- const end = base.clone().add(new THREE6.Vector3(loopRadius * 0.3, 0, 0));
3203
- const control = base.clone().add(new THREE6.Vector3(0, loopRadius, 0));
3204
- return new THREE6.QuadraticBezierCurve3(start, control, end);
3379
+ const base = new THREE9.Vector3(...position);
3380
+ const start = base.clone().add(new THREE9.Vector3(-loopRadius * 0.3, 0, 0));
3381
+ const end = base.clone().add(new THREE9.Vector3(loopRadius * 0.3, 0, 0));
3382
+ const control = base.clone().add(new THREE9.Vector3(0, loopRadius, 0));
3383
+ return new THREE9.QuadraticBezierCurve3(start, control, end);
3205
3384
  }
3206
3385
  function treeLayout3D(node, origin, horizontalSpacing, verticalSpacing = 2) {
3207
3386
  const results = [];
@@ -3359,16 +3538,16 @@ var Avl3DOrbitalNode = ({
3359
3538
  position,
3360
3539
  onClick
3361
3540
  }) => {
3362
- const groupRef = React7.useRef(null);
3363
- const [hovered, setHovered] = React7.useState(false);
3364
- const scale = React7.useMemo(() => 0.6 + Math.min(traitCount, 8) * 0.3, [traitCount]);
3365
- const currentScale = React7.useRef(scale);
3541
+ const groupRef = React6.useRef(null);
3542
+ const [hovered, setHovered] = React6.useState(false);
3543
+ const scale = React6.useMemo(() => 0.6 + Math.min(traitCount, 8) * 0.3, [traitCount]);
3544
+ const currentScale = React6.useRef(scale);
3366
3545
  const targetScale = hovered ? scale * 1.06 : scale;
3367
3546
  fiber.useFrame((_, delta) => {
3368
3547
  if (!groupRef.current) return;
3369
3548
  groupRef.current.rotation.y += delta * (hovered ? 0.4 : 0.2);
3370
3549
  groupRef.current.rotation.x += delta * 0.05;
3371
- currentScale.current = THREE6.MathUtils.damp(currentScale.current, targetScale, 6, delta);
3550
+ currentScale.current = THREE9.MathUtils.damp(currentScale.current, targetScale, 6, delta);
3372
3551
  });
3373
3552
  const baseBrightness = 0.3 + Math.min(pageCount, 5) * 0.05;
3374
3553
  const emissiveIntensity = hovered ? 0.8 : baseBrightness;
@@ -3504,7 +3683,7 @@ var Avl3DCrossWire = ({
3504
3683
  eventName,
3505
3684
  color = AVL_3D_COLORS.crossWire
3506
3685
  }) => {
3507
- const { tubeArgs, midpoint } = React7.useMemo(() => {
3686
+ const { tubeArgs, midpoint } = React6.useMemo(() => {
3508
3687
  const curve = arcCurve3D(from, to, 2);
3509
3688
  const mid = curve.getPoint(0.5);
3510
3689
  return {
@@ -3543,9 +3722,9 @@ var Avl3DApplicationScene = ({
3543
3722
  data,
3544
3723
  onOrbitalClick
3545
3724
  }) => {
3546
- const groupRef = React7.useRef(null);
3547
- const [, setHoverTrigger] = React7.useState(0);
3548
- const orbitalPositions = React7.useMemo(() => {
3725
+ const groupRef = React6.useRef(null);
3726
+ const [, setHoverTrigger] = React6.useState(0);
3727
+ const orbitalPositions = React6.useMemo(() => {
3549
3728
  const baseRadius = Math.max(4, data.orbitals.length * 1.5);
3550
3729
  if (data.crossLinks.length === 0) {
3551
3730
  return goldenSpiralPositions(data.orbitals.length, baseRadius);
@@ -3558,7 +3737,7 @@ var Avl3DApplicationScene = ({
3558
3737
  })).filter((e) => e.from >= 0 && e.to >= 0);
3559
3738
  return forceDirectedPositions(data.orbitals.length, edges, baseRadius);
3560
3739
  }, [data.orbitals, data.crossLinks]);
3561
- const positionByName = React7.useMemo(() => {
3740
+ const positionByName = React6.useMemo(() => {
3562
3741
  const map = /* @__PURE__ */ new Map();
3563
3742
  data.orbitals.forEach((orb, i) => {
3564
3743
  const pos = orbitalPositions[i];
@@ -3641,7 +3820,7 @@ var Avl3DEntityCore = ({
3641
3820
  position,
3642
3821
  fields
3643
3822
  }) => {
3644
- const coreRef = React7.useRef(null);
3823
+ const coreRef = React6.useRef(null);
3645
3824
  fiber.useFrame((_, delta) => {
3646
3825
  if (!coreRef.current) return;
3647
3826
  coreRef.current.rotation.y += delta * 0.3;
@@ -3794,8 +3973,8 @@ function TraitOrbit({
3794
3973
  onClick,
3795
3974
  onHover
3796
3975
  }) {
3797
- const groupRef = React7.useRef(null);
3798
- const angleRef = React7.useRef(Math.random() * Math.PI * 2);
3976
+ const groupRef = React6.useRef(null);
3977
+ const angleRef = React6.useRef(Math.random() * Math.PI * 2);
3799
3978
  fiber.useFrame((_, delta) => {
3800
3979
  angleRef.current += delta * speed;
3801
3980
  if (!groupRef.current) return;
@@ -3926,9 +4105,9 @@ var Avl3DOrbitalScene = ({
3926
4105
  highlightedTrait,
3927
4106
  onTraitHighlight
3928
4107
  }) => {
3929
- const [localHighlight, setLocalHighlight] = React7.useState(null);
4108
+ const [localHighlight, setLocalHighlight] = React6.useState(null);
3930
4109
  const activeHighlight = highlightedTrait ?? localHighlight;
3931
- const traitOrbits = React7.useMemo(() => {
4110
+ const traitOrbits = React6.useMemo(() => {
3932
4111
  return data.traits.map((trait, i) => ({
3933
4112
  name: trait.name,
3934
4113
  radius: 3 + i * 1.2,
@@ -3939,7 +4118,7 @@ var Avl3DOrbitalScene = ({
3939
4118
  transitionCount: trait.transitionCount
3940
4119
  }));
3941
4120
  }, [data.traits]);
3942
- const pagePositions = React7.useMemo(() => {
4121
+ const pagePositions = React6.useMemo(() => {
3943
4122
  const outerRadius = 3 + data.traits.length * 1.2 + 1.5;
3944
4123
  return data.pages.map((_, i) => {
3945
4124
  const angle = 2 * Math.PI * i / data.pages.length - Math.PI / 2;
@@ -4003,14 +4182,14 @@ var Avl3DStateNode = ({
4003
4182
  outgoingCount = 0,
4004
4183
  onClick
4005
4184
  }) => {
4006
- const [hovered, setHovered] = React7.useState(false);
4185
+ const [hovered, setHovered] = React6.useState(false);
4007
4186
  const baseColor = active ? AVL_3D_COLORS.stateActive : AVL_3D_COLORS.stateIdle;
4008
4187
  const emissiveColor = active ? AVL_3D_COLORS.stateActive : AVL_3D_COLORS.stateEdge;
4009
4188
  const emissiveIntensity = active ? 2 : hovered ? 0.8 : 0.3;
4010
4189
  const targetScale = hovered ? 1.08 : 1;
4011
- const currentScale = React7.useRef(1);
4190
+ const currentScale = React6.useRef(1);
4012
4191
  fiber.useFrame((_, delta) => {
4013
- currentScale.current = THREE6.MathUtils.damp(currentScale.current, targetScale, 6, delta);
4192
+ currentScale.current = THREE9.MathUtils.damp(currentScale.current, targetScale, 6, delta);
4014
4193
  });
4015
4194
  const scale = currentScale.current;
4016
4195
  return /* @__PURE__ */ jsxRuntime.jsxs("group", { position, children: [
@@ -4182,17 +4361,17 @@ var Avl3DTransitionArc = ({
4182
4361
  onClick,
4183
4362
  color = AVL_3D_COLORS.transitionArc
4184
4363
  }) => {
4185
- const [hovered, setHovered] = React7.useState(false);
4186
- const { tubeArgs, labelPos, guardPos, arrowPos, arrowQuat, effectPositions } = React7.useMemo(() => {
4364
+ const [hovered, setHovered] = React6.useState(false);
4365
+ const { tubeArgs, labelPos, guardPos, arrowPos, arrowQuat, effectPositions } = React6.useMemo(() => {
4187
4366
  const offset = 1.5 + index * 0.8;
4188
4367
  const curve = isSelf ? selfLoopCurve3D(from, 1.2) : arcCurve3D(from, to, offset);
4189
4368
  const mid = curve.getPoint(0.5);
4190
4369
  const guardPt = curve.getPoint(0.3);
4191
4370
  const arrowPt = curve.getPoint(0.9);
4192
4371
  const arrowTangent = curve.getTangent(0.9);
4193
- const upVec = new THREE6.Vector3(0, 1, 0);
4194
- const tangentVec = new THREE6.Vector3(arrowTangent.x, arrowTangent.y, arrowTangent.z).normalize();
4195
- const quat = new THREE6.Quaternion().setFromUnitVectors(upVec, tangentVec);
4372
+ const upVec = new THREE9.Vector3(0, 1, 0);
4373
+ const tangentVec = new THREE9.Vector3(arrowTangent.x, arrowTangent.y, arrowTangent.z).normalize();
4374
+ const quat = new THREE9.Quaternion().setFromUnitVectors(upVec, tangentVec);
4196
4375
  const effectPositions2 = [];
4197
4376
  for (let ei = 0; ei < 4; ei++) {
4198
4377
  const t = 0.7 + ei * 0.05;
@@ -4316,7 +4495,7 @@ var Avl3DTraitScene = ({
4316
4495
  data,
4317
4496
  onTransitionClick
4318
4497
  }) => {
4319
- const statePositions = React7.useMemo(() => {
4498
+ const statePositions = React6.useMemo(() => {
4320
4499
  const radius = Math.max(3, data.states.length * 0.8);
4321
4500
  const positions = fibonacciSpherePositions(data.states.length, radius);
4322
4501
  const posMap = /* @__PURE__ */ new Map();
@@ -4326,7 +4505,7 @@ var Avl3DTraitScene = ({
4326
4505
  });
4327
4506
  return posMap;
4328
4507
  }, [data.states]);
4329
- const transitionIndices = React7.useMemo(() => {
4508
+ const transitionIndices = React6.useMemo(() => {
4330
4509
  const pairCount = /* @__PURE__ */ new Map();
4331
4510
  return data.transitions.map((t) => {
4332
4511
  const key = `${t.from}->${t.to}`;
@@ -4335,7 +4514,7 @@ var Avl3DTraitScene = ({
4335
4514
  return idx;
4336
4515
  });
4337
4516
  }, [data.transitions]);
4338
- const transitionCounts = React7.useMemo(() => {
4517
+ const transitionCounts = React6.useMemo(() => {
4339
4518
  const incoming = /* @__PURE__ */ new Map();
4340
4519
  const outgoing = /* @__PURE__ */ new Map();
4341
4520
  for (const t of data.transitions) {
@@ -4416,14 +4595,14 @@ function operatorColor(label) {
4416
4595
  return "#4A90D9";
4417
4596
  }
4418
4597
  function edgeGeometry(parent, child) {
4419
- const p = new THREE6.Vector3(parent.x, parent.y, parent.z);
4420
- const c = new THREE6.Vector3(child.x, child.y, child.z);
4421
- const mid = new THREE6.Vector3().addVectors(p, c).multiplyScalar(0.5);
4422
- const dir = new THREE6.Vector3().subVectors(c, p);
4598
+ const p = new THREE9.Vector3(parent.x, parent.y, parent.z);
4599
+ const c = new THREE9.Vector3(child.x, child.y, child.z);
4600
+ const mid = new THREE9.Vector3().addVectors(p, c).multiplyScalar(0.5);
4601
+ const dir = new THREE9.Vector3().subVectors(c, p);
4423
4602
  const length = dir.length();
4424
- const yAxis = new THREE6.Vector3(0, 1, 0);
4603
+ const yAxis = new THREE9.Vector3(0, 1, 0);
4425
4604
  const angle = yAxis.angleTo(dir.normalize());
4426
- const axis = new THREE6.Vector3().crossVectors(yAxis, dir).normalize();
4605
+ const axis = new THREE9.Vector3().crossVectors(yAxis, dir).normalize();
4427
4606
  if (axis.length() < 1e-3) {
4428
4607
  return {
4429
4608
  position: [mid.x, mid.y, mid.z],
@@ -4445,11 +4624,11 @@ var Avl3DExprTree = ({
4445
4624
  expression,
4446
4625
  position
4447
4626
  }) => {
4448
- const layoutResults = React7.useMemo(() => {
4627
+ const layoutResults = React6.useMemo(() => {
4449
4628
  const origin = { x: position[0], y: position[1], z: position[2] };
4450
4629
  return treeLayout3D(expression, origin, 2.5, 1.5);
4451
4630
  }, [expression, position]);
4452
- const edges = React7.useMemo(() => {
4631
+ const edges = React6.useMemo(() => {
4453
4632
  const result = [];
4454
4633
  function collectEdges(node, parentPos) {
4455
4634
  const entry = layoutResults.find((r) => r.node === node);
@@ -4729,7 +4908,7 @@ var Avl3DEffects = ({
4729
4908
  const dof = DOF_CONFIG[level];
4730
4909
  const sparkles = SPARKLE_CONFIGS[level];
4731
4910
  const stars = STARS_CONFIG[level];
4732
- const starsKey = React7.useMemo(() => `stars-${level}`, [level]);
4911
+ const starsKey = React6.useMemo(() => `stars-${level}`, [level]);
4733
4912
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4734
4913
  /* @__PURE__ */ jsxRuntime.jsx(
4735
4914
  drei.Stars,
@@ -4793,18 +4972,18 @@ var DEFAULT_CONFIG2 = {
4793
4972
  modelOverrides: {},
4794
4973
  effectsEnabled: true
4795
4974
  };
4796
- var Avl3DContext = React7.createContext(DEFAULT_CONFIG2);
4975
+ var Avl3DContext = React6.createContext(DEFAULT_CONFIG2);
4797
4976
  function useAvl3DConfig() {
4798
- return React7.useContext(Avl3DContext);
4977
+ return React6.useContext(Avl3DContext);
4799
4978
  }
4800
4979
  function CameraController({ targetPosition, targetLookAt, animated }) {
4801
4980
  const { camera } = fiber.useThree();
4802
- const targetPosVec = React7.useRef(new THREE6__namespace.Vector3(...targetPosition));
4803
- const targetLookVec = React7.useRef(new THREE6__namespace.Vector3(...targetLookAt));
4804
- const isAnimating = React7.useRef(false);
4805
- React7.useEffect(() => {
4806
- const newTarget = new THREE6__namespace.Vector3(...targetPosition);
4807
- const newLookAt = new THREE6__namespace.Vector3(...targetLookAt);
4981
+ const targetPosVec = React6.useRef(new THREE9__namespace.Vector3(...targetPosition));
4982
+ const targetLookVec = React6.useRef(new THREE9__namespace.Vector3(...targetLookAt));
4983
+ const isAnimating = React6.useRef(false);
4984
+ React6.useEffect(() => {
4985
+ const newTarget = new THREE9__namespace.Vector3(...targetPosition);
4986
+ const newLookAt = new THREE9__namespace.Vector3(...targetLookAt);
4808
4987
  if (!newTarget.equals(targetPosVec.current) || !newLookAt.equals(targetLookVec.current)) {
4809
4988
  targetPosVec.current.copy(newTarget);
4810
4989
  targetLookVec.current.copy(newLookAt);
@@ -4819,9 +4998,9 @@ function CameraController({ targetPosition, targetLookAt, animated }) {
4819
4998
  fiber.useFrame((_, delta) => {
4820
4999
  if (!isAnimating.current) return;
4821
5000
  const speed = 3;
4822
- camera.position.x = THREE6__namespace.MathUtils.damp(camera.position.x, targetPosVec.current.x, speed, delta);
4823
- camera.position.y = THREE6__namespace.MathUtils.damp(camera.position.y, targetPosVec.current.y, speed, delta);
4824
- camera.position.z = THREE6__namespace.MathUtils.damp(camera.position.z, targetPosVec.current.z, speed, delta);
5001
+ camera.position.x = THREE9__namespace.MathUtils.damp(camera.position.x, targetPosVec.current.x, speed, delta);
5002
+ camera.position.y = THREE9__namespace.MathUtils.damp(camera.position.y, targetPosVec.current.y, speed, delta);
5003
+ camera.position.z = THREE9__namespace.MathUtils.damp(camera.position.z, targetPosVec.current.z, speed, delta);
4825
5004
  camera.lookAt(targetLookVec.current);
4826
5005
  const dist = camera.position.distanceTo(targetPosVec.current);
4827
5006
  if (dist < 0.05) {
@@ -4831,12 +5010,12 @@ function CameraController({ targetPosition, targetLookAt, animated }) {
4831
5010
  return null;
4832
5011
  }
4833
5012
  function SceneFade({ animating, children }) {
4834
- const groupRef = React7.useRef(null);
4835
- const opacityRef = React7.useRef(1);
5013
+ const groupRef = React6.useRef(null);
5014
+ const opacityRef = React6.useRef(1);
4836
5015
  fiber.useFrame((_, delta) => {
4837
5016
  if (!groupRef.current) return;
4838
5017
  const target = animating ? 0 : 1;
4839
- opacityRef.current = THREE6__namespace.MathUtils.damp(opacityRef.current, target, 5, delta);
5018
+ opacityRef.current = THREE9__namespace.MathUtils.damp(opacityRef.current, target, 5, delta);
4840
5019
  groupRef.current.visible = opacityRef.current > 0.05;
4841
5020
  const s = 0.9 + opacityRef.current * 0.1;
4842
5021
  groupRef.current.scale.setScalar(s);
@@ -4856,7 +5035,7 @@ var Avl3DViewer = ({
4856
5035
  modelOverrides = {}
4857
5036
  }) => {
4858
5037
  const { t } = hooks.useTranslate();
4859
- const schema = React7.useMemo(() => {
5038
+ const schema = React6.useMemo(() => {
4860
5039
  if (typeof schemaProp === "string") {
4861
5040
  try {
4862
5041
  return JSON.parse(schemaProp);
@@ -4866,12 +5045,12 @@ var Avl3DViewer = ({
4866
5045
  }
4867
5046
  return schemaProp;
4868
5047
  }, [schemaProp]);
4869
- const configValue = React7.useMemo(() => ({
5048
+ const configValue = React6.useMemo(() => ({
4870
5049
  modelOverrides,
4871
5050
  effectsEnabled: effects
4872
5051
  }), [modelOverrides, effects]);
4873
- const [state, dispatch] = React7.useReducer(zoomReducer, initialZoomState);
4874
- React7.useEffect(() => {
5052
+ const [state, dispatch] = React6.useReducer(zoomReducer, initialZoomState);
5053
+ React6.useEffect(() => {
4875
5054
  if (initialOrbital) {
4876
5055
  dispatch({ type: "ZOOM_INTO_ORBITAL", orbital: initialOrbital, targetPosition: { x: 0, y: 0 } });
4877
5056
  setTimeout(() => dispatch({ type: "ANIMATION_COMPLETE" }), 0);
@@ -4883,35 +5062,35 @@ var Avl3DViewer = ({
4883
5062
  }
4884
5063
  }
4885
5064
  }, [initialOrbital, initialTrait]);
4886
- React7.useEffect(() => {
5065
+ React6.useEffect(() => {
4887
5066
  onZoomChange?.(state.level, {
4888
5067
  orbital: state.selectedOrbital ?? void 0,
4889
5068
  trait: state.selectedTrait ?? void 0
4890
5069
  });
4891
5070
  }, [state.level, state.selectedOrbital, state.selectedTrait, onZoomChange]);
4892
- React7.useEffect(() => {
5071
+ React6.useEffect(() => {
4893
5072
  if (!state.animating) return;
4894
5073
  const timer = setTimeout(() => {
4895
5074
  dispatch({ type: "ANIMATION_COMPLETE" });
4896
5075
  }, 800);
4897
5076
  return () => clearTimeout(timer);
4898
5077
  }, [state.animating]);
4899
- const handleKeyDown = React7.useCallback((e) => {
5078
+ const handleKeyDown = React6.useCallback((e) => {
4900
5079
  if (e.key === "Escape") {
4901
5080
  dispatch({ type: "ZOOM_OUT" });
4902
5081
  }
4903
5082
  }, []);
4904
- const handleOrbitalClick = React7.useCallback((name) => {
5083
+ const handleOrbitalClick = React6.useCallback((name) => {
4905
5084
  dispatch({ type: "ZOOM_INTO_ORBITAL", orbital: name, targetPosition: { x: 0, y: 0 } });
4906
5085
  }, []);
4907
- const handleTraitClick = React7.useCallback((name) => {
5086
+ const handleTraitClick = React6.useCallback((name) => {
4908
5087
  dispatch({ type: "ZOOM_INTO_TRAIT", trait: name, targetPosition: { x: 0, y: 0 } });
4909
5088
  }, []);
4910
- const [highlightedTrait, setHighlightedTrait] = React7__default.default.useState(null);
4911
- const handleTransitionClick = React7.useCallback((index) => {
5089
+ const [highlightedTrait, setHighlightedTrait] = React6__default.default.useState(null);
5090
+ const handleTransitionClick = React6.useCallback((index) => {
4912
5091
  dispatch({ type: "ZOOM_INTO_TRANSITION", transitionIndex: index, targetPosition: { x: 0, y: 0 } });
4913
5092
  }, []);
4914
- const handleBreadcrumbClick = React7.useCallback((targetLevel) => {
5093
+ const handleBreadcrumbClick = React6.useCallback((targetLevel) => {
4915
5094
  const levelOrder = ["application", "orbital", "trait", "transition"];
4916
5095
  const currentIdx = levelOrder.indexOf(state.level);
4917
5096
  const targetIdx = levelOrder.indexOf(targetLevel);
@@ -4920,7 +5099,7 @@ var Avl3DViewer = ({
4920
5099
  }
4921
5100
  }, [state.level]);
4922
5101
  const cameraConfig = CAMERA_POSITIONS[state.level];
4923
- const sceneContent = React7.useMemo(() => {
5102
+ const sceneContent = React6.useMemo(() => {
4924
5103
  switch (state.level) {
4925
5104
  case "application": {
4926
5105
  const data = parseApplicationLevel(schema);
@@ -4994,7 +5173,7 @@ var Avl3DViewer = ({
4994
5173
  gap: "xs",
4995
5174
  align: "center",
4996
5175
  className: "absolute top-2 left-2 z-10 bg-surface/80 backdrop-blur rounded-md px-3 py-1.5",
4997
- children: breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxRuntime.jsxs(React7__default.default.Fragment, { children: [
5176
+ children: breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxRuntime.jsxs(React6__default.default.Fragment, { children: [
4998
5177
  i > 0 && /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "muted", className: "mx-1", children: "/" }),
4999
5178
  i < breadcrumbs.length - 1 ? /* @__PURE__ */ jsxRuntime.jsx(
5000
5179
  Box,
@@ -5088,187 +5267,6 @@ var Avl3DViewer = ({
5088
5267
  );
5089
5268
  };
5090
5269
  Avl3DViewer.displayName = "Avl3DViewer";
5091
- function isInFrustum(position, camera, padding = 0) {
5092
- const frustum = new THREE6__namespace.Frustum();
5093
- const projScreenMatrix = new THREE6__namespace.Matrix4();
5094
- projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
5095
- frustum.setFromProjectionMatrix(projScreenMatrix);
5096
- const sphere = new THREE6__namespace.Sphere(position, padding);
5097
- return frustum.intersectsSphere(sphere);
5098
- }
5099
- function filterByFrustum(positions, camera, padding = 0) {
5100
- const frustum = new THREE6__namespace.Frustum();
5101
- const projScreenMatrix = new THREE6__namespace.Matrix4();
5102
- projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
5103
- frustum.setFromProjectionMatrix(projScreenMatrix);
5104
- return positions.filter((position) => {
5105
- const sphere = new THREE6__namespace.Sphere(position, padding);
5106
- return frustum.intersectsSphere(sphere);
5107
- });
5108
- }
5109
- function getVisibleIndices(positions, camera, padding = 0) {
5110
- const frustum = new THREE6__namespace.Frustum();
5111
- const projScreenMatrix = new THREE6__namespace.Matrix4();
5112
- const visible = /* @__PURE__ */ new Set();
5113
- projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
5114
- frustum.setFromProjectionMatrix(projScreenMatrix);
5115
- positions.forEach((position, index) => {
5116
- const sphere = new THREE6__namespace.Sphere(position, padding);
5117
- if (frustum.intersectsSphere(sphere)) {
5118
- visible.add(index);
5119
- }
5120
- });
5121
- return visible;
5122
- }
5123
- function calculateLODLevel(position, camera, lodLevels) {
5124
- const distance = position.distanceTo(camera.position);
5125
- for (let i = 0; i < lodLevels.length; i++) {
5126
- if (distance < lodLevels[i]) {
5127
- return i;
5128
- }
5129
- }
5130
- return lodLevels.length;
5131
- }
5132
- function updateInstanceLOD(instancedMesh, positions, camera, lodDistances) {
5133
- const lodIndices = new Uint8Array(positions.length);
5134
- positions.forEach((position, index) => {
5135
- lodIndices[index] = calculateLODLevel(position, camera, lodDistances);
5136
- });
5137
- return lodIndices;
5138
- }
5139
- function cullInstancedMesh(instancedMesh, positions, visibleIndices) {
5140
- const dummy = new THREE6__namespace.Object3D();
5141
- let visibleCount = 0;
5142
- positions.forEach((position, index) => {
5143
- if (visibleIndices.has(index)) {
5144
- dummy.position.copy(position);
5145
- dummy.updateMatrix();
5146
- instancedMesh.setMatrixAt(visibleCount, dummy.matrix);
5147
- visibleCount++;
5148
- }
5149
- });
5150
- instancedMesh.count = visibleCount;
5151
- instancedMesh.instanceMatrix.needsUpdate = true;
5152
- return visibleCount;
5153
- }
5154
- var SpatialHashGrid = class {
5155
- constructor(cellSize = 10) {
5156
- __publicField(this, "cellSize");
5157
- __publicField(this, "cells");
5158
- __publicField(this, "objectPositions");
5159
- this.cellSize = cellSize;
5160
- this.cells = /* @__PURE__ */ new Map();
5161
- this.objectPositions = /* @__PURE__ */ new Map();
5162
- }
5163
- /**
5164
- * Get cell key for a position
5165
- */
5166
- getCellKey(x, z) {
5167
- const cellX = Math.floor(x / this.cellSize);
5168
- const cellZ = Math.floor(z / this.cellSize);
5169
- return `${cellX},${cellZ}`;
5170
- }
5171
- /**
5172
- * Insert an object into the grid
5173
- */
5174
- insert(id, position) {
5175
- const key = this.getCellKey(position.x, position.z);
5176
- if (!this.cells.has(key)) {
5177
- this.cells.set(key, /* @__PURE__ */ new Set());
5178
- }
5179
- this.cells.get(key).add(id);
5180
- this.objectPositions.set(id, position.clone());
5181
- }
5182
- /**
5183
- * Remove an object from the grid
5184
- */
5185
- remove(id) {
5186
- const position = this.objectPositions.get(id);
5187
- if (position) {
5188
- const key = this.getCellKey(position.x, position.z);
5189
- this.cells.get(key)?.delete(id);
5190
- this.objectPositions.delete(id);
5191
- }
5192
- }
5193
- /**
5194
- * Update an object's position
5195
- */
5196
- update(id, newPosition) {
5197
- this.remove(id);
5198
- this.insert(id, newPosition);
5199
- }
5200
- /**
5201
- * Query objects within a radius of a position
5202
- */
5203
- queryRadius(center, radius) {
5204
- const results = [];
5205
- const radiusSquared = radius * radius;
5206
- const minCellX = Math.floor((center.x - radius) / this.cellSize);
5207
- const maxCellX = Math.floor((center.x + radius) / this.cellSize);
5208
- const minCellZ = Math.floor((center.z - radius) / this.cellSize);
5209
- const maxCellZ = Math.floor((center.z + radius) / this.cellSize);
5210
- for (let x = minCellX; x <= maxCellX; x++) {
5211
- for (let z = minCellZ; z <= maxCellZ; z++) {
5212
- const key = `${x},${z}`;
5213
- const cell = this.cells.get(key);
5214
- if (cell) {
5215
- cell.forEach((id) => {
5216
- const position = this.objectPositions.get(id);
5217
- if (position) {
5218
- const dx = position.x - center.x;
5219
- const dz = position.z - center.z;
5220
- if (dx * dx + dz * dz <= radiusSquared) {
5221
- results.push(id);
5222
- }
5223
- }
5224
- });
5225
- }
5226
- }
5227
- }
5228
- return results;
5229
- }
5230
- /**
5231
- * Query objects within a bounding box
5232
- */
5233
- queryBox(minX, maxX, minZ, maxZ) {
5234
- const results = [];
5235
- const minCellX = Math.floor(minX / this.cellSize);
5236
- const maxCellX = Math.floor(maxX / this.cellSize);
5237
- const minCellZ = Math.floor(minZ / this.cellSize);
5238
- const maxCellZ = Math.floor(maxZ / this.cellSize);
5239
- for (let x = minCellX; x <= maxCellX; x++) {
5240
- for (let z = minCellZ; z <= maxCellZ; z++) {
5241
- const key = `${x},${z}`;
5242
- const cell = this.cells.get(key);
5243
- if (cell) {
5244
- cell.forEach((id) => {
5245
- const position = this.objectPositions.get(id);
5246
- if (position && position.x >= minX && position.x <= maxX && position.z >= minZ && position.z <= maxZ) {
5247
- results.push(id);
5248
- }
5249
- });
5250
- }
5251
- }
5252
- }
5253
- return results;
5254
- }
5255
- /**
5256
- * Clear all objects from the grid
5257
- */
5258
- clear() {
5259
- this.cells.clear();
5260
- this.objectPositions.clear();
5261
- }
5262
- /**
5263
- * Get statistics about the grid
5264
- */
5265
- getStats() {
5266
- return {
5267
- objects: this.objectPositions.size,
5268
- cells: this.cells.size
5269
- };
5270
- }
5271
- };
5272
5270
 
5273
5271
  exports.AVL_3D_COLORS = AVL_3D_COLORS;
5274
5272
  exports.AssetLoader = AssetLoader;
@@ -5282,8 +5280,9 @@ exports.Avl3DViewer = Avl3DViewer;
5282
5280
  exports.CAMERA_POSITIONS = CAMERA_POSITIONS;
5283
5281
  exports.Camera3D = Camera3D;
5284
5282
  exports.Canvas3DErrorBoundary = Canvas3DErrorBoundary;
5283
+ exports.Canvas3DHost = Canvas3DHost;
5285
5284
  exports.Canvas3DLoadingState = Canvas3DLoadingState;
5286
- exports.GameCanvas3D = GameCanvas3D;
5285
+ exports.GameCanvas3D = Canvas3DHost;
5287
5286
  exports.Lighting3D = Lighting3D;
5288
5287
  exports.ModelLoader = ModelLoader;
5289
5288
  exports.Scene3D = Scene3D;
@@ -5316,5 +5315,5 @@ exports.useAvl3DConfig = useAvl3DConfig;
5316
5315
  exports.useGameCanvas3DEvents = useGameCanvas3DEvents;
5317
5316
  exports.useRaycaster = useRaycaster;
5318
5317
  exports.useSceneGraph = useSceneGraph;
5319
- exports.useThree = useThree3;
5318
+ exports.useThree = useThree4;
5320
5319
  exports.worldToGrid = worldToGrid;