@almadar/shell-web 0.0.0-stage → 0.1.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 (274) hide show
  1. package/bundling.d.mts +18 -0
  2. package/bundling.mjs +28 -0
  3. package/dist/ArbitraryClassCompiler-Mzabq-UB.d.ts +100 -0
  4. package/dist/AvlGlyph-JKJOAH23.js +6 -0
  5. package/dist/AvlStateMachine-PI4YGREK.js +8 -0
  6. package/dist/AvlTransitionDetail-ZH7V24UG.js +12 -0
  7. package/dist/AvlTransitionExplainer-ZSRT7L6Y.js +13 -0
  8. package/dist/BehaviorValueFrame-6RUJXT4A.js +29 -0
  9. package/dist/BehaviorValueFrame-SSEMEBTG.css +1756 -0
  10. package/dist/BehaviorView-RFEE5J6E.js +10 -0
  11. package/dist/Canvas3DHost-_Y-5uLM_.d.ts +552 -0
  12. package/dist/EventBusProvider-CVwjkV_2.d.ts +284 -0
  13. package/dist/FileTree-BCFnBEuQ.d.ts +254 -0
  14. package/dist/FlowCanvas-3JRMNJLV.css +1756 -0
  15. package/dist/FlowCanvas-STDC4OWK.js +27 -0
  16. package/dist/GameAudioProvider-Cm9BnvmP.d.ts +93 -0
  17. package/dist/KaTeX_AMS-Regular-CYEKBG2K.woff +0 -0
  18. package/dist/KaTeX_AMS-Regular-JKX5W2C4.ttf +0 -0
  19. package/dist/KaTeX_AMS-Regular-U6PRYMIZ.woff2 +0 -0
  20. package/dist/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2 +0 -0
  21. package/dist/KaTeX_Caligraphic-Bold-WZ3QSGD3.woff +0 -0
  22. package/dist/KaTeX_Caligraphic-Bold-ZTS3R3HK.ttf +0 -0
  23. package/dist/KaTeX_Caligraphic-Regular-3LKEU76G.woff +0 -0
  24. package/dist/KaTeX_Caligraphic-Regular-A7XRTZ5Q.ttf +0 -0
  25. package/dist/KaTeX_Caligraphic-Regular-KX5MEWCF.woff2 +0 -0
  26. package/dist/KaTeX_Fraktur-Bold-2QVFK6NQ.woff2 +0 -0
  27. package/dist/KaTeX_Fraktur-Bold-T4SWXBMT.woff +0 -0
  28. package/dist/KaTeX_Fraktur-Bold-WGHVTYOR.ttf +0 -0
  29. package/dist/KaTeX_Fraktur-Regular-2PEIFJSJ.woff2 +0 -0
  30. package/dist/KaTeX_Fraktur-Regular-5U4OPH2X.ttf +0 -0
  31. package/dist/KaTeX_Fraktur-Regular-PQMHCIK6.woff +0 -0
  32. package/dist/KaTeX_Main-Bold-2GA4IZIN.woff +0 -0
  33. package/dist/KaTeX_Main-Bold-W5FBVCZM.ttf +0 -0
  34. package/dist/KaTeX_Main-Bold-YP5VVQRP.woff2 +0 -0
  35. package/dist/KaTeX_Main-BoldItalic-4P4C7HJH.woff +0 -0
  36. package/dist/KaTeX_Main-BoldItalic-N4V3DX7S.woff2 +0 -0
  37. package/dist/KaTeX_Main-BoldItalic-ODMLBJJQ.ttf +0 -0
  38. package/dist/KaTeX_Main-Italic-I43T2HSR.ttf +0 -0
  39. package/dist/KaTeX_Main-Italic-RELBIK7M.woff2 +0 -0
  40. package/dist/KaTeX_Main-Italic-SASNQFN2.woff +0 -0
  41. package/dist/KaTeX_Main-Regular-ARRPAO67.woff2 +0 -0
  42. package/dist/KaTeX_Main-Regular-P5I74A2A.woff +0 -0
  43. package/dist/KaTeX_Main-Regular-W74P5G27.ttf +0 -0
  44. package/dist/KaTeX_Math-BoldItalic-6EBV3DK5.woff +0 -0
  45. package/dist/KaTeX_Math-BoldItalic-K4WTGH3J.woff2 +0 -0
  46. package/dist/KaTeX_Math-BoldItalic-VB447A4D.ttf +0 -0
  47. package/dist/KaTeX_Math-Italic-6KGCHLFN.woff2 +0 -0
  48. package/dist/KaTeX_Math-Italic-KKK3USB2.woff +0 -0
  49. package/dist/KaTeX_Math-Italic-SON4MRCA.ttf +0 -0
  50. package/dist/KaTeX_SansSerif-Bold-RRNVJFFW.woff2 +0 -0
  51. package/dist/KaTeX_SansSerif-Bold-STQ6RXC7.ttf +0 -0
  52. package/dist/KaTeX_SansSerif-Bold-X5M5EMOD.woff +0 -0
  53. package/dist/KaTeX_SansSerif-Italic-HMPFTM52.woff2 +0 -0
  54. package/dist/KaTeX_SansSerif-Italic-PSN4QKYX.woff +0 -0
  55. package/dist/KaTeX_SansSerif-Italic-WTBAZBGY.ttf +0 -0
  56. package/dist/KaTeX_SansSerif-Regular-2TL3USAE.ttf +0 -0
  57. package/dist/KaTeX_SansSerif-Regular-OQCII6EP.woff +0 -0
  58. package/dist/KaTeX_SansSerif-Regular-XIQ62X4E.woff2 +0 -0
  59. package/dist/KaTeX_Script-Regular-72OLXYNA.ttf +0 -0
  60. package/dist/KaTeX_Script-Regular-A5IFOEBS.woff +0 -0
  61. package/dist/KaTeX_Script-Regular-APUWIHLP.woff2 +0 -0
  62. package/dist/KaTeX_Size1-Regular-4HRHTS65.woff +0 -0
  63. package/dist/KaTeX_Size1-Regular-5LRUTBFT.woff2 +0 -0
  64. package/dist/KaTeX_Size1-Regular-7K6AASVL.ttf +0 -0
  65. package/dist/KaTeX_Size2-Regular-222HN3GT.ttf +0 -0
  66. package/dist/KaTeX_Size2-Regular-K5ZHAIS6.woff +0 -0
  67. package/dist/KaTeX_Size2-Regular-LELKET5D.woff2 +0 -0
  68. package/dist/KaTeX_Size3-Regular-TLFPAHDE.woff +0 -0
  69. package/dist/KaTeX_Size3-Regular-UFCO6WCA.ttf +0 -0
  70. package/dist/KaTeX_Size3-Regular-WQRQ47UD.woff2 +0 -0
  71. package/dist/KaTeX_Size4-Regular-7PGNVPQK.ttf +0 -0
  72. package/dist/KaTeX_Size4-Regular-CDMV7U5C.woff2 +0 -0
  73. package/dist/KaTeX_Size4-Regular-PKMWZHNC.woff +0 -0
  74. package/dist/KaTeX_Typewriter-Regular-3F5K6SQ6.ttf +0 -0
  75. package/dist/KaTeX_Typewriter-Regular-MJMFSK64.woff +0 -0
  76. package/dist/KaTeX_Typewriter-Regular-VBYJ4NRC.woff2 +0 -0
  77. package/dist/KernelEngineContext-BOCQoFwk.d.ts +43 -0
  78. package/dist/LayersPanel-BBQ54IVG.js +7 -0
  79. package/dist/ModuleCard-3TT2X7IW.js +7 -0
  80. package/dist/OrbInspector-K6T6O7GI.js +27 -0
  81. package/dist/OrbInspector-MHLJ6OEE.css +1756 -0
  82. package/dist/Select-mDZIwNqN.d.ts +199 -0
  83. package/dist/ServerBridge-Cm1On51b.d.ts +233 -0
  84. package/dist/ThemeContext-DJmO59xd.d.ts +147 -0
  85. package/dist/TraitValueFrame-2PJGLLVI.js +28 -0
  86. package/dist/TraitValueFrame-4G3KOUBR.css +1756 -0
  87. package/dist/UISlotContext-Dl_ewwCI.d.ts +79 -0
  88. package/dist/UISlotRenderer-JKFSX24W.js +27 -0
  89. package/dist/UISlotRenderer-KVFO6J7R.css +1756 -0
  90. package/dist/UserContext-DqWeZkUZ.d.ts +100 -0
  91. package/dist/auth/browser/index.d.ts +2 -0
  92. package/dist/auth/browser/index.js +1 -0
  93. package/dist/auth/react/index.d.ts +49 -0
  94. package/dist/auth/react/index.js +119 -0
  95. package/dist/avl/index.css +1756 -0
  96. package/dist/avl/index.d.ts +2238 -0
  97. package/dist/avl/index.js +2693 -0
  98. package/dist/avl-3d-context-BIRKWpaL.d.ts +204 -0
  99. package/dist/avl-schema-parser-CtJxJ9KU.d.ts +132 -0
  100. package/dist/avl-zoom-state-CKMVuBFg.d.ts +17 -0
  101. package/dist/chunk-2PY54Z44.js +227 -0
  102. package/dist/chunk-2QOH2HWM.js +95 -0
  103. package/dist/chunk-3BBUBJFF.js +339 -0
  104. package/dist/chunk-42FY4RYA.js +191 -0
  105. package/dist/chunk-5QTYQTPS.js +15 -0
  106. package/dist/chunk-6P3MWXYE.js +440 -0
  107. package/dist/chunk-73SRAB4P.js +1053 -0
  108. package/dist/chunk-A2LR6VQQ.js +349 -0
  109. package/dist/chunk-AILY3XPS.js +562 -0
  110. package/dist/chunk-B4BU77LT.js +432 -0
  111. package/dist/chunk-DFNZUYX4.js +378 -0
  112. package/dist/chunk-DJ433ET5.js +1327 -0
  113. package/dist/chunk-DLZF4PVE.js +24 -0
  114. package/dist/chunk-DYEKAA4L.js +787 -0
  115. package/dist/chunk-DZ42OJQC.js +176 -0
  116. package/dist/chunk-EOWMARUB.js +16 -0
  117. package/dist/chunk-EQAMU2ZC.js +14 -0
  118. package/dist/chunk-FAJDTHWC.js +98 -0
  119. package/dist/chunk-FIW6WN23.js +508 -0
  120. package/dist/chunk-FP3WIKTC.js +303 -0
  121. package/dist/chunk-G7DCAZ22.js +19 -0
  122. package/dist/chunk-GAR5TQ5B.js +199 -0
  123. package/dist/chunk-GKIFONTE.js +131 -0
  124. package/dist/chunk-GYTPOFVC.js +47155 -0
  125. package/dist/chunk-KVQS4GXU.js +328 -0
  126. package/dist/chunk-L2DEKY4J.js +97 -0
  127. package/dist/chunk-L677XGY3.js +701 -0
  128. package/dist/chunk-MBQAMMSE.js +11877 -0
  129. package/dist/chunk-P4LNHAFI.js +1051 -0
  130. package/dist/chunk-P4ZFUQFF.js +93 -0
  131. package/dist/chunk-PC7K4SK7.js +1384 -0
  132. package/dist/chunk-R5O5AUPI.js +573 -0
  133. package/dist/chunk-SGM3FASH.js +672 -0
  134. package/dist/chunk-SJGJJKBY.js +502 -0
  135. package/dist/chunk-TESBIVUA.js +11 -0
  136. package/dist/chunk-TZZBINQY.js +82 -0
  137. package/dist/chunk-U6QRZASX.js +280 -0
  138. package/dist/chunk-VFF5OO47.js +319 -0
  139. package/dist/chunk-YDNJLLG6.js +516 -0
  140. package/dist/chunk-YHMRX6YA.js +455 -0
  141. package/dist/chunk-ZO6MXAS3.js +69 -0
  142. package/dist/components/index.css +1756 -0
  143. package/dist/components/index.d.ts +11093 -0
  144. package/dist/components/index.js +1110 -0
  145. package/dist/context/index.d.ts +9 -0
  146. package/dist/context/index.js +7 -0
  147. package/dist/core.d.ts +2523 -0
  148. package/dist/core.js +2 -0
  149. package/dist/design-classes/index.cjs +429 -0
  150. package/dist/design-classes/index.d.cts +165 -0
  151. package/dist/design-classes/index.d.ts +165 -0
  152. package/dist/design-classes/index.js +397 -0
  153. package/dist/emojiItems-QGSNAABW.js +20 -0
  154. package/dist/fx-CmQjnHmT.d.ts +371 -0
  155. package/dist/hooks/index.d.ts +936 -0
  156. package/dist/hooks/index.js +7 -0
  157. package/dist/index.d.ts +486 -0
  158. package/dist/index.js +38 -0
  159. package/dist/katexPlugin-FA7NX4PE.css +1161 -0
  160. package/dist/katexPlugin-NCDKOOQY.js +2 -0
  161. package/dist/kernel-client-ZH4PSMCN.js +367 -0
  162. package/dist/kernel-wire-B4S8pnP3.d.ts +65 -0
  163. package/dist/lib/index.d.ts +602 -0
  164. package/dist/lib/index.js +1288 -0
  165. package/dist/paintDispatch-ChW5mVuK.d.ts +698 -0
  166. package/dist/parseLessonSegments-GpCeMrb2.d.ts +334 -0
  167. package/dist/perf-CKiBKbkw.d.ts +22 -0
  168. package/dist/perf-Dr28-AgT.d.ts +57 -0
  169. package/dist/prism-AIWGN2X5.js +134 -0
  170. package/dist/providers/index.css +1756 -0
  171. package/dist/providers/index.d.ts +340 -0
  172. package/dist/providers/index.js +30 -0
  173. package/dist/renderer/index.d.ts +467 -0
  174. package/dist/renderer/index.js +3 -0
  175. package/dist/runtime/index.css +1756 -0
  176. package/dist/runtime/index.d.ts +440 -0
  177. package/dist/runtime/index.js +31 -0
  178. package/dist/slot-host-CPzrxLo9.d.ts +48 -0
  179. package/dist/ssr/index.cjs +7631 -0
  180. package/dist/ssr/index.d.cts +487 -0
  181. package/dist/ssr/index.d.ts +487 -0
  182. package/dist/ssr/index.js +7599 -0
  183. package/dist/storage/browser/index.d.ts +1 -0
  184. package/dist/storage/browser/index.js +1 -0
  185. package/dist/test-support.d.ts +35 -0
  186. package/dist/test-support.js +114 -0
  187. package/dist/themeTokens-CRd8AEDF.d.ts +125 -0
  188. package/dist/three/index.css +503 -0
  189. package/dist/three/index.d.ts +1185 -0
  190. package/dist/three/index.js +3896 -0
  191. package/dist/useKeyboardRouter-Dsq553Ur.d.ts +42 -0
  192. package/dist/useUISlots-CiobNFGb.d.ts +211 -0
  193. package/dist/verification-C5pTeMJv.d.ts +66 -0
  194. package/dist/verificationRegistry-CabxhfTv.d.ts +122 -0
  195. package/dist/wrapCallbackForEvent-B9QLbhxX.d.ts +19 -0
  196. package/index.css +199 -0
  197. package/package.json +269 -3
  198. package/src/core/locales/ar.json +1263 -0
  199. package/src/core/locales/en.json +1263 -0
  200. package/src/core/locales/index.ts +54 -0
  201. package/src/core/locales/sl.json +1263 -0
  202. package/src/core/tokens/native-tokens.json +369 -0
  203. package/tailwind-compile.cjs +29 -0
  204. package/tailwind-preset.cjs +991 -0
  205. package/themes/_base.css +451 -0
  206. package/themes/_contract.md +307 -0
  207. package/themes/_fonts-kenney.css +11 -0
  208. package/themes/_fonts.css +12 -0
  209. package/themes/_foundation.css +105 -0
  210. package/themes/_head.css +22 -0
  211. package/themes/_presets.css +67 -0
  212. package/themes/almadar-heritage.css +455 -0
  213. package/themes/almadar-website.css +269 -0
  214. package/themes/almadar.css +269 -0
  215. package/themes/aqua.css +455 -0
  216. package/themes/arabesque.css +459 -0
  217. package/themes/art-deco.css +457 -0
  218. package/themes/art-nouveau.css +459 -0
  219. package/themes/atelier.css +463 -0
  220. package/themes/bauhaus.css +461 -0
  221. package/themes/bloomberg-dense.css +463 -0
  222. package/themes/blueprint.css +457 -0
  223. package/themes/clay.css +449 -0
  224. package/themes/comic.css +453 -0
  225. package/themes/constructivist.css +457 -0
  226. package/themes/corporate.css +449 -0
  227. package/themes/cyberpunk.css +453 -0
  228. package/themes/frutiger-aero.css +458 -0
  229. package/themes/game-adventure.css +201 -0
  230. package/themes/game-rpg.css +203 -0
  231. package/themes/game-sci-fi.css +201 -0
  232. package/themes/game-ui-pack.css +200 -0
  233. package/themes/gazette.css +449 -0
  234. package/themes/glass.css +457 -0
  235. package/themes/gothic.css +453 -0
  236. package/themes/index.css +21 -0
  237. package/themes/ink-wash.css +459 -0
  238. package/themes/kawaii.css +459 -0
  239. package/themes/kflow.css +310 -0
  240. package/themes/kiosk.css +449 -0
  241. package/themes/linear-clean.css +463 -0
  242. package/themes/luxury.css +451 -0
  243. package/themes/mac-classic.css +455 -0
  244. package/themes/material.css +463 -0
  245. package/themes/memphis.css +453 -0
  246. package/themes/mid-century.css +455 -0
  247. package/themes/minimalist.css +449 -0
  248. package/themes/mudcloth.css +459 -0
  249. package/themes/nata.css +310 -0
  250. package/themes/neon.css +453 -0
  251. package/themes/neumorphic.css +451 -0
  252. package/themes/newsprint.css +455 -0
  253. package/themes/notion-editorial.css +463 -0
  254. package/themes/orb.css +465 -0
  255. package/themes/pixel.css +449 -0
  256. package/themes/prism.css +309 -0
  257. package/themes/rawi.css +310 -0
  258. package/themes/retro.css +452 -0
  259. package/themes/risograph.css +463 -0
  260. package/themes/scandi.css +469 -0
  261. package/themes/sketch.css +455 -0
  262. package/themes/solarpunk.css +469 -0
  263. package/themes/steampunk.css +451 -0
  264. package/themes/swiss.css +455 -0
  265. package/themes/terminal.css +466 -0
  266. package/themes/trait-wars.css +487 -0
  267. package/themes/ukiyo-e.css +459 -0
  268. package/themes/vaporwave.css +455 -0
  269. package/themes/victorian.css +453 -0
  270. package/themes/wabi-sabi.css +459 -0
  271. package/themes/win95.css +455 -0
  272. package/themes/wireframe.css +439 -0
  273. package/themes/zine.css +455 -0
  274. package/README.md +0 -3
@@ -0,0 +1,3896 @@
1
+ import { goldenSpiralPositions, forceDirectedPositions, Avl3DOrbitalNode, Avl3DCrossWire, Avl3DEntityCore, fibonacciSpherePositions, Avl3DLabel, Avl3DStateNode, Avl3DTransitionArc, Avl3DExprTree, AVL_3D_COLORS, zoomReducer, initialZoomState, CAMERA_POSITIONS, getBreadcrumbs, Avl3DTooltip } from '../chunk-DJ433ET5.js';
2
+ export { AVL_3D_COLORS, CAMERA_POSITIONS, arcCurve3D, fibonacciSpherePositions, goldenSpiralPositions, orbitRingPositions, selfLoopCurve3D, treeLayout3D } from '../chunk-DJ433ET5.js';
3
+ import { useAvl3DPalette, Avl3DContext } from '../chunk-EOWMARUB.js';
4
+ export { Avl3DContext, useAvl3DConfig } from '../chunk-EOWMARUB.js';
5
+ import { useOnScreen, useCanvasTheme, withSeries, resolveDrawableColors, markColor, applySceneMaterials, collectDrawnItems, buildHitIndex, withPreviewPosition, isValidScenePos, isAnimatedShape, applyShapeAnimation, expandFxLayer, isAnimatedGroup, applyMeshAnimation, spriteAssetState, SPRITE_FALLBACK_FILL, isAnimatedMesh, clampSegments, polyhedronBounds } from '../chunk-PC7K4SK7.js';
6
+ import { parseTransitionLevel, parseTraitLevel, parseOrbitalLevel, parseApplicationLevel } from '../chunk-VFF5OO47.js';
7
+ import { AVL_INK, resolveThemeColor } from '../chunk-DFNZUYX4.js';
8
+ import { keyMapCode, resolveKeyMapEvent } from '../chunk-B4BU77LT.js';
9
+ import '../chunk-DYEKAA4L.js';
10
+ import { useEventBus, useTranslate, cn, useEmitEvent, isAtlasAsset, getAtlas, subRectFor, Box, HStack, Typography, pressableProps, useTheme, useThemeScope } from '../chunk-MBQAMMSE.js';
11
+ import '../chunk-G7DCAZ22.js';
12
+ import React3, { createContext, forwardRef, useRef, useState, useMemo, useEffect, useCallback, useImperativeHandle, Component, useContext, useReducer, useLayoutEffect } from 'react';
13
+ import { Canvas, useThree, useFrame } from '@react-three/fiber';
14
+ import * as THREE5 from 'three';
15
+ import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
16
+ import { Grid, OrbitControls, Billboard, Text, Stars, Sparkles } from '@react-three/drei';
17
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
18
+ import { createLogger } from '@almadar/logger';
19
+ import { Bloom, Vignette, EffectComposer, DepthOfField } from '@react-three/postprocessing';
20
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
21
+ import { clone } from 'three/examples/jsm/utils/SkeletonUtils.js';
22
+ import { OrbitControls as OrbitControls$1 } from 'three/examples/jsm/controls/OrbitControls.js';
23
+ import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
24
+
25
+ function useGameCanvas3DEvents(options) {
26
+ const {
27
+ tileClickEvent,
28
+ unitClickEvent,
29
+ featureClickEvent,
30
+ canvasClickEvent,
31
+ tileHoverEvent,
32
+ tileLeaveEvent,
33
+ unitAnimationEvent,
34
+ cameraChangeEvent,
35
+ onTileClick,
36
+ onUnitClick,
37
+ onFeatureClick,
38
+ onCanvasClick,
39
+ onTileHover,
40
+ onUnitAnimation
41
+ } = options;
42
+ const emit = useEmitEvent();
43
+ const optionsRef = useRef(options);
44
+ optionsRef.current = options;
45
+ const handleTileClick = useCallback(
46
+ (tile, event) => {
47
+ if (tileClickEvent) {
48
+ emit(tileClickEvent, {
49
+ tileId: tile.id,
50
+ x: tile.x,
51
+ z: tile.z ?? tile.y ?? 0,
52
+ type: tile.type,
53
+ terrain: tile.terrain,
54
+ elevation: tile.elevation
55
+ });
56
+ }
57
+ optionsRef.current.onTileClick?.(tile, event);
58
+ },
59
+ [tileClickEvent, emit]
60
+ );
61
+ const handleUnitClick = useCallback(
62
+ (unit, event) => {
63
+ if (unitClickEvent) {
64
+ emit(unitClickEvent, {
65
+ unitId: unit.id,
66
+ x: unit.x,
67
+ z: unit.z ?? unit.y ?? 0,
68
+ unitType: unit.unitType,
69
+ name: unit.name,
70
+ team: unit.team,
71
+ faction: unit.faction,
72
+ health: unit.health,
73
+ maxHealth: unit.maxHealth
74
+ });
75
+ }
76
+ optionsRef.current.onUnitClick?.(unit, event);
77
+ },
78
+ [unitClickEvent, emit]
79
+ );
80
+ const handleFeatureClick = useCallback(
81
+ (feature, event) => {
82
+ if (featureClickEvent) {
83
+ emit(featureClickEvent, {
84
+ featureId: feature.id,
85
+ x: feature.x,
86
+ z: feature.z ?? feature.y ?? 0,
87
+ type: feature.type,
88
+ elevation: feature.elevation
89
+ });
90
+ }
91
+ optionsRef.current.onFeatureClick?.(feature, event);
92
+ },
93
+ [featureClickEvent, emit]
94
+ );
95
+ const handleCanvasClick = useCallback(
96
+ (event) => {
97
+ if (canvasClickEvent) {
98
+ emit(canvasClickEvent, {
99
+ clientX: event.clientX,
100
+ clientY: event.clientY,
101
+ button: event.button
102
+ });
103
+ }
104
+ optionsRef.current.onCanvasClick?.(event);
105
+ },
106
+ [canvasClickEvent, emit]
107
+ );
108
+ const handleTileHover = useCallback(
109
+ (tile, event) => {
110
+ if (tile) {
111
+ if (tileHoverEvent) {
112
+ emit(tileHoverEvent, {
113
+ tileId: tile.id,
114
+ x: tile.x,
115
+ z: tile.z ?? tile.y ?? 0,
116
+ type: tile.type
117
+ });
118
+ }
119
+ } else {
120
+ if (tileLeaveEvent) {
121
+ emit(tileLeaveEvent, {});
122
+ }
123
+ }
124
+ optionsRef.current.onTileHover?.(tile, event);
125
+ },
126
+ [tileHoverEvent, tileLeaveEvent, emit]
127
+ );
128
+ const handleUnitAnimation = useCallback(
129
+ (unitId, state) => {
130
+ if (unitAnimationEvent) {
131
+ emit(unitAnimationEvent, {
132
+ unitId,
133
+ state,
134
+ timestamp: Date.now()
135
+ });
136
+ }
137
+ optionsRef.current.onUnitAnimation?.(unitId, state);
138
+ },
139
+ [unitAnimationEvent, emit]
140
+ );
141
+ const handleCameraChange = useCallback(
142
+ (position) => {
143
+ if (cameraChangeEvent) {
144
+ emit(cameraChangeEvent, {
145
+ position,
146
+ timestamp: Date.now()
147
+ });
148
+ }
149
+ },
150
+ [cameraChangeEvent, emit]
151
+ );
152
+ return {
153
+ handleTileClick,
154
+ handleUnitClick,
155
+ handleFeatureClick,
156
+ handleCanvasClick,
157
+ handleTileHover,
158
+ handleUnitAnimation,
159
+ handleCameraChange
160
+ };
161
+ }
162
+ function Canvas3DLoadingState({
163
+ progress = 0,
164
+ loaded = 0,
165
+ total = 0,
166
+ message: messageProp,
167
+ details,
168
+ showSpinner = true,
169
+ className
170
+ }) {
171
+ const { t, locale } = useTranslate();
172
+ const message = messageProp ?? t("canvas3d.loadingScene");
173
+ const numberFormat = new Intl.NumberFormat(locale);
174
+ const clampedProgress = Math.max(0, Math.min(100, progress));
175
+ const hasProgress = total > 0;
176
+ return /* @__PURE__ */ jsxs("div", { className: `canvas-3d-loading ${className || ""}`, children: [
177
+ /* @__PURE__ */ jsxs("div", { className: "canvas-3d-loading__content", children: [
178
+ showSpinner && /* @__PURE__ */ jsxs("div", { className: "canvas-3d-loading__spinner", children: [
179
+ /* @__PURE__ */ jsx("div", { className: "spinner__ring" }),
180
+ /* @__PURE__ */ jsx("div", { className: "spinner__ring spinner__ring--secondary" })
181
+ ] }),
182
+ /* @__PURE__ */ jsx("div", { className: "canvas-3d-loading__message", children: message }),
183
+ details && /* @__PURE__ */ jsx("div", { className: "canvas-3d-loading__details", children: details }),
184
+ hasProgress && /* @__PURE__ */ jsxs("div", { className: "canvas-3d-loading__progress", children: [
185
+ /* @__PURE__ */ jsx("div", { className: "progress__bar", children: /* @__PURE__ */ jsx(
186
+ "div",
187
+ {
188
+ className: "progress__fill",
189
+ style: { width: `${clampedProgress}%` }
190
+ }
191
+ ) }),
192
+ /* @__PURE__ */ jsxs("div", { className: "progress__text", children: [
193
+ /* @__PURE__ */ jsx("span", { className: "progress__percentage", children: new Intl.NumberFormat(locale, { style: "percent" }).format(clampedProgress / 100) }),
194
+ /* @__PURE__ */ jsxs("span", { className: "progress__count", children: [
195
+ "(",
196
+ numberFormat.format(loaded),
197
+ "/",
198
+ numberFormat.format(total),
199
+ ")"
200
+ ] })
201
+ ] })
202
+ ] })
203
+ ] }),
204
+ /* @__PURE__ */ jsx("div", { className: "canvas-3d-loading__background", children: /* @__PURE__ */ jsx("div", { className: "bg__grid" }) })
205
+ ] });
206
+ }
207
+ var log = createLogger("almadar:ui:game:canvas3d:error-boundary");
208
+ function Canvas3DErrorFallback({ error, errorInfo, onReset }) {
209
+ const { t } = useTranslate();
210
+ return /* @__PURE__ */ jsx("div", { className: "canvas-3d-error", children: /* @__PURE__ */ jsxs("div", { className: "canvas-3d-error__content", children: [
211
+ /* @__PURE__ */ jsx("div", { className: "canvas-3d-error__icon", children: "\u26A0\uFE0F" }),
212
+ /* @__PURE__ */ jsx("h2", { className: "canvas-3d-error__title", children: t("canvas3d.errorTitle") }),
213
+ /* @__PURE__ */ jsx("p", { className: "canvas-3d-error__message", children: t("canvas3d.errorMessage") }),
214
+ error && /* @__PURE__ */ jsxs("details", { className: "canvas-3d-error__details", children: [
215
+ /* @__PURE__ */ jsx("summary", { children: t("canvas3d.errorDetails") }),
216
+ /* @__PURE__ */ jsxs("pre", { className: "error__stack", children: [
217
+ error.message,
218
+ "\n",
219
+ error.stack
220
+ ] }),
221
+ errorInfo && /* @__PURE__ */ jsx("pre", { className: "error__component-stack", children: errorInfo.componentStack })
222
+ ] }),
223
+ /* @__PURE__ */ jsxs("div", { className: "canvas-3d-error__actions", children: [
224
+ /* @__PURE__ */ jsx(
225
+ "button",
226
+ {
227
+ className: "error__button error__button--primary",
228
+ onClick: onReset,
229
+ children: t("common.retry")
230
+ }
231
+ ),
232
+ /* @__PURE__ */ jsx(
233
+ "button",
234
+ {
235
+ className: "error__button error__button--secondary",
236
+ onClick: () => window.location.reload(),
237
+ children: t("canvas3d.reloadPage")
238
+ }
239
+ )
240
+ ] })
241
+ ] }) });
242
+ }
243
+ var Canvas3DErrorBoundary = class extends Component {
244
+ constructor(props) {
245
+ super(props);
246
+ this.state = {
247
+ hasError: false,
248
+ error: null,
249
+ errorInfo: null
250
+ };
251
+ }
252
+ static getDerivedStateFromError(error) {
253
+ return {
254
+ hasError: true,
255
+ error,
256
+ errorInfo: null
257
+ };
258
+ }
259
+ componentDidCatch(error, errorInfo) {
260
+ this.setState({ errorInfo });
261
+ this.props.onError?.(error, errorInfo);
262
+ log.error("Error caught", { error });
263
+ log.error("Component stack", { componentStack: errorInfo.componentStack ?? "<none>" });
264
+ }
265
+ handleReset = () => {
266
+ this.setState({
267
+ hasError: false,
268
+ error: null,
269
+ errorInfo: null
270
+ });
271
+ this.props.onReset?.();
272
+ };
273
+ render() {
274
+ if (this.state.hasError) {
275
+ if (this.props.fallback) {
276
+ return this.props.fallback;
277
+ }
278
+ return /* @__PURE__ */ jsx(
279
+ Canvas3DErrorFallback,
280
+ {
281
+ error: this.state.error,
282
+ errorInfo: this.state.errorInfo,
283
+ onReset: this.handleReset
284
+ }
285
+ );
286
+ }
287
+ return this.props.children;
288
+ }
289
+ };
290
+ function Lighting3D({
291
+ ambientIntensity = 0.6,
292
+ ambientColor = "#ffffff",
293
+ directionalIntensity = 0.8,
294
+ directionalColor = "#ffffff",
295
+ directionalPosition = [10, 20, 10],
296
+ hemisphereIntensity = 0.3,
297
+ hemisphereColor = "#87ceeb",
298
+ hemisphereGroundColor = "#362d1d",
299
+ points,
300
+ shadows = true,
301
+ shadowMapSize = 2048,
302
+ shadowCameraSize = 20,
303
+ shadowBias = -1e-3,
304
+ shadowNormalBias,
305
+ shadowCameraNear = 0.1,
306
+ shadowCameraFar = 100,
307
+ showHelpers = false
308
+ }) {
309
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
310
+ /* @__PURE__ */ jsx("ambientLight", { intensity: ambientIntensity, color: ambientColor }),
311
+ /* @__PURE__ */ jsx(
312
+ "directionalLight",
313
+ {
314
+ position: directionalPosition,
315
+ intensity: directionalIntensity,
316
+ color: directionalColor,
317
+ castShadow: shadows,
318
+ "shadow-mapSize": [shadowMapSize, shadowMapSize],
319
+ "shadow-camera-left": -shadowCameraSize,
320
+ "shadow-camera-right": shadowCameraSize,
321
+ "shadow-camera-top": shadowCameraSize,
322
+ "shadow-camera-bottom": -shadowCameraSize,
323
+ "shadow-camera-near": shadowCameraNear,
324
+ "shadow-camera-far": shadowCameraFar,
325
+ "shadow-bias": shadowBias,
326
+ "shadow-normalBias": shadowNormalBias
327
+ }
328
+ ),
329
+ /* @__PURE__ */ jsx(
330
+ "hemisphereLight",
331
+ {
332
+ intensity: hemisphereIntensity,
333
+ color: hemisphereColor,
334
+ groundColor: hemisphereGroundColor
335
+ }
336
+ ),
337
+ points?.map((p, i) => /* @__PURE__ */ jsx(
338
+ "pointLight",
339
+ {
340
+ position: p.position,
341
+ intensity: p.intensity,
342
+ color: p.color,
343
+ distance: p.distance,
344
+ decay: p.decay
345
+ },
346
+ i
347
+ )),
348
+ showHelpers && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(
349
+ "directionalLightHelper",
350
+ {
351
+ args: [
352
+ new THREE5.DirectionalLight(directionalColor, directionalIntensity),
353
+ 5
354
+ ]
355
+ }
356
+ ) })
357
+ ] });
358
+ }
359
+ function Effects3D({ post }) {
360
+ if (!post || !post.bloom && !post.vignette) return null;
361
+ const passes = [];
362
+ if (post.bloom) {
363
+ passes.push(
364
+ /* @__PURE__ */ jsx(
365
+ Bloom,
366
+ {
367
+ intensity: post.bloom.intensity ?? 1,
368
+ luminanceThreshold: post.bloom.threshold ?? 0.9,
369
+ luminanceSmoothing: post.bloom.smoothing ?? 0.3
370
+ },
371
+ "bloom"
372
+ )
373
+ );
374
+ }
375
+ if (post.vignette) {
376
+ passes.push(
377
+ /* @__PURE__ */ jsx(
378
+ Vignette,
379
+ {
380
+ offset: post.vignette.offset ?? 0.3,
381
+ darkness: post.vignette.darkness ?? 0.6
382
+ },
383
+ "vignette"
384
+ )
385
+ );
386
+ }
387
+ return /* @__PURE__ */ jsx(EffectComposer, { children: passes });
388
+ }
389
+ function CameraController3D({
390
+ onCameraChange
391
+ }) {
392
+ const { camera } = useThree();
393
+ useEffect(() => {
394
+ if (onCameraChange) {
395
+ onCameraChange({
396
+ x: camera.position.x,
397
+ y: camera.position.y,
398
+ z: camera.position.z
399
+ });
400
+ }
401
+ }, [camera.position, onCameraChange]);
402
+ return null;
403
+ }
404
+ function FollowCamera3D({
405
+ target,
406
+ offset
407
+ }) {
408
+ const { camera } = useThree();
409
+ const look = useRef(new THREE5.Vector3(target[0], target[1], target[2]));
410
+ const goal = useRef(new THREE5.Vector3());
411
+ useFrame((_, delta) => {
412
+ const t = Math.min(1, delta * 5);
413
+ goal.current.set(target[0] + offset[0], target[1] + offset[1], target[2] + offset[2]);
414
+ camera.position.lerp(goal.current, t);
415
+ look.current.lerp(goal.current.set(target[0], target[1], target[2]), t);
416
+ camera.lookAt(look.current);
417
+ });
418
+ return null;
419
+ }
420
+
421
+ // src/lib/drawable/projector3d.ts
422
+ function create3DProjector(opts = {}) {
423
+ const cellSize = opts.cellSize ?? 1;
424
+ const offsetX = opts.offsetX ?? 0;
425
+ const offsetZ = opts.offsetZ ?? 0;
426
+ return {
427
+ cellSize,
428
+ toWorld: (pos) => [pos.x * cellSize + offsetX, pos.z ?? 0, pos.y * cellSize + offsetZ]
429
+ };
430
+ }
431
+ var BoneStore = class {
432
+ bones = /* @__PURE__ */ new Map();
433
+ listeners = /* @__PURE__ */ new Set();
434
+ register(name, bone) {
435
+ this.bones.set(name, bone);
436
+ this.notify();
437
+ return () => {
438
+ if (this.bones.get(name) === bone) {
439
+ this.bones.delete(name);
440
+ this.notify();
441
+ }
442
+ };
443
+ }
444
+ get(name) {
445
+ return this.bones.get(name);
446
+ }
447
+ subscribe(listener) {
448
+ this.listeners.add(listener);
449
+ return () => this.listeners.delete(listener);
450
+ }
451
+ notify() {
452
+ for (const l of this.listeners) l();
453
+ }
454
+ };
455
+ var BoneRegistryContext = createContext(null);
456
+ var log2 = createLogger("almadar:ui:game:model-loader");
457
+ var gltfCache = /* @__PURE__ */ new Map();
458
+ function loadGltfCached(url, assetRoot) {
459
+ const key = `${assetRoot}|${url}`;
460
+ let entry = gltfCache.get(key);
461
+ if (!entry) {
462
+ const pending = {
463
+ promise: new Promise((resolveDone) => {
464
+ const loader = new GLTFLoader();
465
+ loader.setResourcePath(assetRoot);
466
+ loader.load(
467
+ url,
468
+ (gltf) => {
469
+ log2.debug("Loaded", { url, clips: gltf.animations.length });
470
+ pending.gltf = { scene: gltf.scene, animations: gltf.animations };
471
+ resolveDone();
472
+ },
473
+ void 0,
474
+ (err) => {
475
+ log2.warn("Failed", { url, error: err instanceof Error ? err : String(err) });
476
+ pending.error = err instanceof Error ? err : new Error(String(err));
477
+ resolveDone();
478
+ }
479
+ );
480
+ })
481
+ };
482
+ entry = pending;
483
+ gltfCache.set(key, entry);
484
+ }
485
+ return entry;
486
+ }
487
+ function detectAssetRoot(modelUrl) {
488
+ const idx = modelUrl.indexOf("/3d/");
489
+ if (idx !== -1) {
490
+ return modelUrl.substring(0, idx + 4);
491
+ }
492
+ return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
493
+ }
494
+ function stateFromEntry(entry) {
495
+ if (!entry) return { model: null, clips: [], isLoading: false, error: null };
496
+ if (entry.gltf) return { model: entry.gltf.scene, clips: entry.gltf.animations, isLoading: false, error: null };
497
+ return { model: null, clips: [], isLoading: !entry.error, error: entry.error ?? null };
498
+ }
499
+ function useGLTFModel(url, resourceBasePath) {
500
+ const [state, setState] = useState(
501
+ () => stateFromEntry(url ? loadGltfCached(url, resourceBasePath || detectAssetRoot(url)) : null)
502
+ );
503
+ useEffect(() => {
504
+ if (!url) {
505
+ setState({ model: null, clips: [], isLoading: false, error: null });
506
+ return;
507
+ }
508
+ const entry = loadGltfCached(url, resourceBasePath || detectAssetRoot(url));
509
+ setState(stateFromEntry(entry));
510
+ if (entry.gltf || entry.error) return;
511
+ let cancelled = false;
512
+ void entry.promise.then(() => {
513
+ if (!cancelled) setState(stateFromEntry(entry));
514
+ });
515
+ return () => {
516
+ cancelled = true;
517
+ };
518
+ }, [url, resourceBasePath]);
519
+ return state;
520
+ }
521
+ function ModelLoader({
522
+ url,
523
+ position = [0, 0, 0],
524
+ scale = 1,
525
+ rotation = [0, 0, 0],
526
+ isSelected = false,
527
+ isHovered = false,
528
+ onClick,
529
+ onHover,
530
+ fallbackGeometry = "box",
531
+ castShadow = true,
532
+ receiveShadow = true,
533
+ resourceBasePath,
534
+ animation,
535
+ tint
536
+ }) {
537
+ const { model: loadedModel, clips, isLoading, error } = useGLTFModel(url, resourceBasePath);
538
+ const model = useMemo(() => {
539
+ if (!loadedModel) return null;
540
+ const cloned = clone(loadedModel);
541
+ cloned.updateMatrixWorld(true);
542
+ const tintColor = tint ? new THREE5.Color(tint) : null;
543
+ cloned.traverse((child) => {
544
+ if (child instanceof THREE5.Mesh) {
545
+ child.castShadow = castShadow;
546
+ child.receiveShadow = receiveShadow;
547
+ if (tintColor && child.material instanceof THREE5.MeshStandardMaterial) {
548
+ const mat = child.material.clone();
549
+ mat.color.multiply(tintColor);
550
+ child.material = mat;
551
+ }
552
+ }
553
+ });
554
+ return cloned;
555
+ }, [loadedModel, castShadow, receiveShadow, tint]);
556
+ const mixer = useMemo(() => model ? new THREE5.AnimationMixer(model) : null, [model]);
557
+ useEffect(() => {
558
+ if (!mixer || !animation || clips.length === 0) return;
559
+ const wanted = animation.toLowerCase();
560
+ const clip = clips.find((c) => c.name === animation) ?? clips.find((c) => c.name.toLowerCase() === wanted);
561
+ if (!clip) return;
562
+ const action = mixer.clipAction(clip);
563
+ action.reset().fadeIn(0.15).play();
564
+ return () => {
565
+ action.fadeOut(0.15);
566
+ };
567
+ }, [mixer, clips, animation]);
568
+ useFrame((_, delta) => {
569
+ mixer?.update(delta);
570
+ });
571
+ const normFactor = useMemo(() => {
572
+ if (!model) return 1;
573
+ const box = new THREE5.Box3().setFromObject(model);
574
+ const size = new THREE5.Vector3();
575
+ box.getSize(size);
576
+ const maxDim = Math.max(size.x, size.y, size.z);
577
+ if (!Number.isFinite(maxDim) || maxDim < 0.05) return 1;
578
+ return 1 / maxDim;
579
+ }, [model]);
580
+ const scaleArray = useMemo(() => {
581
+ const base = typeof scale === "number" ? [scale, scale, scale] : scale;
582
+ return [base[0] * normFactor, base[1] * normFactor, base[2] * normFactor];
583
+ }, [scale, normFactor]);
584
+ const rotationRad = useMemo(() => {
585
+ return [
586
+ rotation[0] * Math.PI / 180,
587
+ rotation[1] * Math.PI / 180,
588
+ rotation[2] * Math.PI / 180
589
+ ];
590
+ }, [rotation]);
591
+ if (isLoading) {
592
+ return /* @__PURE__ */ jsx("group", { position, children: /* @__PURE__ */ jsxs("mesh", { rotation: [Math.PI / 2, 0, 0], children: [
593
+ /* @__PURE__ */ jsx("ringGeometry", { args: [0.3, 0.35, 16] }),
594
+ /* @__PURE__ */ jsx("meshBasicMaterial", { color: "#4a90d9", transparent: true, opacity: 0.8 })
595
+ ] }) });
596
+ }
597
+ if (error || !model) {
598
+ if (fallbackGeometry === "none") {
599
+ return /* @__PURE__ */ jsx("group", { position });
600
+ }
601
+ const fallbackProps = {
602
+ onClick,
603
+ onPointerOver: () => onHover?.(true),
604
+ onPointerOut: () => onHover?.(false)
605
+ };
606
+ return /* @__PURE__ */ jsxs("group", { position, children: [
607
+ (isSelected || isHovered) && /* @__PURE__ */ jsxs("mesh", { position: [0, 0.02, 0], rotation: [-Math.PI / 2, 0, 0], children: [
608
+ /* @__PURE__ */ jsx("ringGeometry", { args: [0.6, 0.7, 32] }),
609
+ /* @__PURE__ */ jsx(
610
+ "meshBasicMaterial",
611
+ {
612
+ color: isSelected ? 16755200 : 16777215,
613
+ transparent: true,
614
+ opacity: 0.5
615
+ }
616
+ )
617
+ ] }),
618
+ fallbackGeometry === "box" && /* @__PURE__ */ jsxs("mesh", { ...fallbackProps, position: [0, 0.5, 0], children: [
619
+ /* @__PURE__ */ jsx("boxGeometry", { args: [0.8, 0.8, 0.8] }),
620
+ /* @__PURE__ */ jsx("meshStandardMaterial", { color: error ? 16729156 : 8947848 })
621
+ ] }),
622
+ fallbackGeometry === "sphere" && /* @__PURE__ */ jsxs("mesh", { ...fallbackProps, position: [0, 0.5, 0], children: [
623
+ /* @__PURE__ */ jsx("sphereGeometry", { args: [0.4, 16, 16] }),
624
+ /* @__PURE__ */ jsx("meshStandardMaterial", { color: error ? 16729156 : 8947848 })
625
+ ] }),
626
+ fallbackGeometry === "cylinder" && /* @__PURE__ */ jsxs("mesh", { ...fallbackProps, position: [0, 0.5, 0], children: [
627
+ /* @__PURE__ */ jsx("cylinderGeometry", { args: [0.3, 0.3, 0.8, 16] }),
628
+ /* @__PURE__ */ jsx("meshStandardMaterial", { color: error ? 16729156 : 8947848 })
629
+ ] })
630
+ ] });
631
+ }
632
+ return /* @__PURE__ */ jsxs(
633
+ "group",
634
+ {
635
+ position,
636
+ rotation: rotationRad,
637
+ onClick,
638
+ onPointerOver: () => onHover?.(true),
639
+ onPointerOut: () => onHover?.(false),
640
+ children: [
641
+ (isSelected || isHovered) && /* @__PURE__ */ jsxs("mesh", { position: [0, 0.02, 0], rotation: [-Math.PI / 2, 0, 0], children: [
642
+ /* @__PURE__ */ jsx("ringGeometry", { args: [0.6, 0.7, 32] }),
643
+ /* @__PURE__ */ jsx(
644
+ "meshBasicMaterial",
645
+ {
646
+ color: isSelected ? 16755200 : 16777215,
647
+ transparent: true,
648
+ opacity: 0.5
649
+ }
650
+ )
651
+ ] }),
652
+ /* @__PURE__ */ jsx("primitive", { object: model, scale: scaleArray })
653
+ ]
654
+ }
655
+ );
656
+ }
657
+ var mesh3dLog = createLogger("almadar:ui:drawable-3d");
658
+ var warnedUnsupported = /* @__PURE__ */ new Set();
659
+ var warnUnsupported3d = (kind) => {
660
+ if (warnedUnsupported.has(kind)) return;
661
+ warnedUnsupported.add(kind);
662
+ mesh3dLog.warn("unsupported drawable kind on the 3D backend \u2014 skipped", { kind });
663
+ };
664
+ var CrossOriginTextureLoader = class extends THREE5.TextureLoader {
665
+ constructor() {
666
+ super();
667
+ this.crossOrigin = "anonymous";
668
+ }
669
+ };
670
+ function useBillboardTexture(url) {
671
+ const [state, setState] = React3.useState({
672
+ texture: null,
673
+ error: false
674
+ });
675
+ React3.useEffect(() => {
676
+ let active = true;
677
+ const loader = new CrossOriginTextureLoader();
678
+ loader.load(
679
+ url,
680
+ (texture) => {
681
+ if (!active) return;
682
+ texture.colorSpace = THREE5.SRGBColorSpace;
683
+ setState({ texture, error: false });
684
+ },
685
+ void 0,
686
+ () => {
687
+ if (!active) return;
688
+ setState({ texture: null, error: true });
689
+ }
690
+ );
691
+ return () => {
692
+ active = false;
693
+ };
694
+ }, [url]);
695
+ return state;
696
+ }
697
+ function useAtlasFrame(asset) {
698
+ const [tick, setTick] = React3.useState(0);
699
+ React3.useEffect(() => {
700
+ if (!isAtlasAsset(asset)) return;
701
+ getAtlas(asset.atlas, () => setTick((t) => t + 1));
702
+ }, [asset, tick]);
703
+ return React3.useMemo(() => {
704
+ if (!isAtlasAsset(asset)) return { frame: null, ready: true };
705
+ const atlas = getAtlas(asset.atlas, () => setTick((t) => t + 1));
706
+ if (!atlas) {
707
+ return { frame: null, ready: false };
708
+ }
709
+ const r = subRectFor(atlas, asset.sprite);
710
+ if (!r) return { frame: null, ready: true };
711
+ return { frame: { x: r.sx, y: r.sy, w: r.sw, h: r.sh }, ready: true };
712
+ }, [asset, tick]);
713
+ }
714
+ function SpriteBillboard({ node, world, cellSize = 1, groupOpacity = 1 }) {
715
+ const { texture, error: textureError } = useBillboardTexture(node.asset.url);
716
+ const { frame: atlasFrame, ready: atlasReady } = useAtlasFrame(node.asset);
717
+ const frame = node.frame ?? atlasFrame;
718
+ const anchor = node.anchor ?? "top-left";
719
+ const size = React3.useMemo(() => {
720
+ const img = texture?.image;
721
+ const imgW = img?.width || 1;
722
+ const imgH = img?.height || 1;
723
+ const aspect = frame ? frame.w / frame.h : imgW / imgH;
724
+ if (anchor === "top-left") {
725
+ const width2 = (node.width ?? 1) * cellSize;
726
+ const height2 = (node.height ?? 1) * cellSize;
727
+ return { width: width2, height: height2, imgW, imgH };
728
+ }
729
+ const height = node.height ?? 1;
730
+ const width = node.width ?? height * aspect;
731
+ return { width: width * cellSize, height: height * cellSize, imgW, imgH };
732
+ }, [texture, frame, node.height, node.width, anchor, cellSize]);
733
+ const groundGeometry = React3.useMemo(() => {
734
+ if (anchor !== "top-left" || !texture || !atlasReady) return null;
735
+ const g = new THREE5.PlaneGeometry(size.width, size.height);
736
+ g.rotateX(-Math.PI / 2);
737
+ return g;
738
+ }, [anchor, texture, atlasReady, size.width, size.height]);
739
+ if (!texture || !atlasReady) {
740
+ if (anchor === "top-left") {
741
+ return /* @__PURE__ */ jsx("group", { position: [world[0] + size.width / 2, 0.02, world[2] + size.height / 2], children: /* @__PURE__ */ jsxs("mesh", { rotation: [-Math.PI / 2, 0, 0], children: [
742
+ /* @__PURE__ */ jsx("planeGeometry", { args: [size.width, size.height] }),
743
+ /* @__PURE__ */ jsx("meshBasicMaterial", { color: textureError ? 16729156 : 8947848, transparent: true, opacity: 0.6, side: THREE5.DoubleSide })
744
+ ] }) });
745
+ }
746
+ return /* @__PURE__ */ jsx("group", { position: [world[0], world[1] + size.height / 2, world[2]], children: /* @__PURE__ */ jsxs("mesh", { children: [
747
+ /* @__PURE__ */ jsx("planeGeometry", { args: [size.width, size.height] }),
748
+ /* @__PURE__ */ jsx("meshBasicMaterial", { color: textureError ? 16729156 : 8947848, transparent: true, opacity: 0.6, side: THREE5.DoubleSide })
749
+ ] }) });
750
+ }
751
+ texture.magFilter = texture.minFilter = THREE5.NearestFilter;
752
+ texture.needsUpdate = true;
753
+ if (frame) {
754
+ texture.repeat.set(frame.w / size.imgW, frame.h / size.imgH);
755
+ texture.offset.set(frame.x / size.imgW, 1 - (frame.y + frame.h) / size.imgH);
756
+ }
757
+ if (anchor === "top-left") {
758
+ return /* @__PURE__ */ jsx("group", { position: [world[0] + size.width / 2, 0.01, world[2] + size.height / 2], children: /* @__PURE__ */ jsx("mesh", { geometry: groundGeometry ?? void 0, children: /* @__PURE__ */ jsx(
759
+ "meshBasicMaterial",
760
+ {
761
+ map: texture,
762
+ transparent: true,
763
+ alphaTest: 0.1,
764
+ side: THREE5.DoubleSide,
765
+ opacity: (node.opacity ?? 1) * groupOpacity
766
+ }
767
+ ) }) });
768
+ }
769
+ return /* @__PURE__ */ jsx(Billboard, { position: [world[0], world[1] + size.height / 2, world[2]], children: /* @__PURE__ */ jsxs("mesh", { children: [
770
+ /* @__PURE__ */ jsx("planeGeometry", { args: [size.width, size.height] }),
771
+ /* @__PURE__ */ jsx(
772
+ "meshBasicMaterial",
773
+ {
774
+ map: texture,
775
+ transparent: true,
776
+ alphaTest: 0.1,
777
+ side: THREE5.DoubleSide,
778
+ opacity: node.opacity ?? 1
779
+ }
780
+ )
781
+ ] }) });
782
+ }
783
+ function Sprite3D({ node, projector, groupOpacity = 1 }) {
784
+ const asset = node.asset;
785
+ const state = spriteAssetState(node);
786
+ if (state === "invalid-position") return null;
787
+ if (state === "asset-unset") {
788
+ const side = (node.width ?? 0.6) * projector.cellSize;
789
+ return /* @__PURE__ */ jsxs("mesh", { position: projector.toWorld(node.position), children: [
790
+ /* @__PURE__ */ jsx("boxGeometry", { args: [side, (node.height ?? 0.6) * projector.cellSize, side] }),
791
+ /* @__PURE__ */ jsx("meshStandardMaterial", { color: SPRITE_FALLBACK_FILL })
792
+ ] });
793
+ }
794
+ if (asset.dimension === "3d") {
795
+ const scale = node.width === void 0 ? 1 : node.height === void 0 ? node.width * projector.cellSize : [
796
+ node.width * projector.cellSize,
797
+ node.height * projector.cellSize,
798
+ node.width * projector.cellSize
799
+ ];
800
+ return /* @__PURE__ */ jsx("group", { position: projector.toWorld(node.position), children: /* @__PURE__ */ jsx(
801
+ ModelLoader,
802
+ {
803
+ url: asset.url,
804
+ scale,
805
+ rotation: [0, node.rotation ?? 0, 0],
806
+ animation: node.animation,
807
+ fallbackGeometry: "box",
808
+ castShadow: true,
809
+ receiveShadow: true
810
+ }
811
+ ) });
812
+ }
813
+ return /* @__PURE__ */ jsx(SpriteBillboard, { node, world: projector.toWorld(node.position), cellSize: projector.cellSize, groupOpacity });
814
+ }
815
+ function Shape3D({ node, projector, groupOpacity = 1 }) {
816
+ const groupRef = React3.useRef(null);
817
+ const materialRef = React3.useRef(null);
818
+ const animated = isAnimatedShape(node);
819
+ const validPos = isValidScenePos(node.position);
820
+ const world = validPos ? projector.toWorld(node.position) : [0, 0, 0];
821
+ useFrame(({ clock }) => {
822
+ if (!animated || !groupRef.current) return;
823
+ const view = applyShapeAnimation(node, clock.elapsedTime * 1e3);
824
+ groupRef.current.position.set(
825
+ world[0] + (view.offsetX ?? 0) * projector.cellSize,
826
+ world[1] + 0.02,
827
+ world[2] + (view.offsetY ?? 0) * projector.cellSize
828
+ );
829
+ groupRef.current.rotation.y = -(view.rotate ?? 0);
830
+ const mat = materialRef.current;
831
+ if (mat) {
832
+ mat.opacity = (view.opacity ?? node.opacity ?? 1) * groupOpacity;
833
+ if (view.fill) mat.color.set(view.fill);
834
+ }
835
+ });
836
+ if (!validPos) return null;
837
+ const color = node.fill ?? node.stroke ?? "#ffffff";
838
+ let geometry;
839
+ switch (node.shape) {
840
+ case "cell":
841
+ case "rect": {
842
+ const w = node.shape === "cell" ? projector.cellSize * 0.95 : (node.width ?? 1) * projector.cellSize;
843
+ const h = node.shape === "cell" ? projector.cellSize * 0.95 : (node.height ?? 1) * projector.cellSize;
844
+ geometry = /* @__PURE__ */ jsx("planeGeometry", { args: [w, h] });
845
+ break;
846
+ }
847
+ case "ellipse": {
848
+ const radiusX = node.radiusX ?? 0.4;
849
+ geometry = node.stroke && !node.fill ? /* @__PURE__ */ jsx("ringGeometry", { args: [Math.max(0, radiusX - 0.08), radiusX, 32] }) : /* @__PURE__ */ jsx("circleGeometry", { args: [radiusX, 32] });
850
+ break;
851
+ }
852
+ case "poly": {
853
+ if (!node.points || node.points.length === 0) return null;
854
+ const s = new THREE5.Shape();
855
+ node.points.forEach((p, i) => {
856
+ if (i === 0) s.moveTo(p.x, p.y);
857
+ else s.lineTo(p.x, p.y);
858
+ });
859
+ s.closePath();
860
+ geometry = /* @__PURE__ */ jsx("shapeGeometry", { args: [s] });
861
+ break;
862
+ }
863
+ case "path": {
864
+ warnUnsupported3d("draw-shape:path");
865
+ return null;
866
+ }
867
+ default:
868
+ return null;
869
+ }
870
+ return /* @__PURE__ */ jsx("group", { ref: groupRef, position: [world[0], world[1] + 0.02, world[2]], children: /* @__PURE__ */ jsxs("mesh", { rotation: [-Math.PI / 2, 0, 0], children: [
871
+ geometry,
872
+ /* @__PURE__ */ jsx("meshBasicMaterial", { ref: materialRef, color, transparent: true, opacity: (node.opacity ?? 1) * groupOpacity, side: THREE5.DoubleSide })
873
+ ] }) });
874
+ }
875
+ function Text3D({ node, projector, groupOpacity = 1 }) {
876
+ if (!isValidScenePos(node.position)) return null;
877
+ const world = projector.toWorld(node.position);
878
+ return /* @__PURE__ */ jsx(Billboard, { position: [world[0], world[1] + 1.2, world[2]], children: /* @__PURE__ */ jsx(
879
+ Text,
880
+ {
881
+ fontSize: 0.32,
882
+ color: node.color,
883
+ anchorX: "center",
884
+ anchorY: "middle",
885
+ outlineWidth: 0.02,
886
+ outlineColor: "#000000",
887
+ fillOpacity: groupOpacity,
888
+ outlineOpacity: groupOpacity,
889
+ children: node.text
890
+ }
891
+ ) });
892
+ }
893
+ var GROUND_ROTATION = [-Math.PI / 2, 0, 0];
894
+ var textureCache = /* @__PURE__ */ new Map();
895
+ function getMeshTexture(url) {
896
+ const cached = textureCache.get(url);
897
+ if (cached) return cached;
898
+ const texture = new THREE5.TextureLoader().load(url);
899
+ texture.flipY = false;
900
+ texture.colorSpace = THREE5.SRGBColorSpace;
901
+ texture.wrapS = THREE5.RepeatWrapping;
902
+ texture.wrapT = THREE5.RepeatWrapping;
903
+ textureCache.set(url, texture);
904
+ return texture;
905
+ }
906
+ function polyhedronGeometry(node, cellSize) {
907
+ const verts = node.vertices;
908
+ const faces = node.faces;
909
+ const bounds = polyhedronBounds(verts);
910
+ if (!verts || !bounds || !faces || faces.length === 0) return null;
911
+ const positions = new Float32Array(verts.length * 3);
912
+ for (let i = 0; i < verts.length; i++) {
913
+ positions[i * 3] = verts[i][0] * cellSize;
914
+ positions[i * 3 + 1] = verts[i][2] * cellSize;
915
+ positions[i * 3 + 2] = verts[i][1] * cellSize;
916
+ }
917
+ const index = [];
918
+ for (const face of faces) {
919
+ if (face.length < 3) continue;
920
+ const [a, b, c] = face;
921
+ if (a === b || b === c || a === c) continue;
922
+ if (![a, b, c].every((i) => Number.isInteger(i) && i >= 0 && i < verts.length)) continue;
923
+ index.push(a, c, b);
924
+ }
925
+ if (index.length === 0) return null;
926
+ const geometry = new THREE5.BufferGeometry();
927
+ geometry.setAttribute("position", new THREE5.BufferAttribute(positions, 3));
928
+ if (node.uvs && node.uvs.length === verts.length) {
929
+ const uv = new Float32Array(verts.length * 2);
930
+ for (let i = 0; i < verts.length; i++) {
931
+ uv[i * 2] = node.uvs[i][0];
932
+ uv[i * 2 + 1] = node.uvs[i][1];
933
+ }
934
+ geometry.setAttribute("uv", new THREE5.BufferAttribute(uv, 2));
935
+ }
936
+ geometry.setIndex(index);
937
+ geometry.computeVertexNormals();
938
+ const skin = node.skin;
939
+ if (skin && skin.indices.length === verts.length && skin.weights.length === verts.length) {
940
+ const skinIndex = new Uint16Array(verts.length * 4);
941
+ const skinWeight = new Float32Array(verts.length * 4);
942
+ for (let i = 0; i < verts.length; i++) {
943
+ for (let k = 0; k < 4; k++) {
944
+ skinIndex[i * 4 + k] = skin.indices[i][k] ?? 0;
945
+ skinWeight[i * 4 + k] = skin.weights[i][k] ?? 0;
946
+ }
947
+ }
948
+ geometry.setAttribute("skinIndex", new THREE5.BufferAttribute(skinIndex, 4));
949
+ geometry.setAttribute("skinWeight", new THREE5.BufferAttribute(skinWeight, 4));
950
+ }
951
+ const size = Math.max(bounds.max[0] - bounds.min[0], bounds.max[1] - bounds.min[1], bounds.max[2] - bounds.min[2]) * cellSize;
952
+ return { geometry, lift: -bounds.min[2] * cellSize, size };
953
+ }
954
+ function meshGeometry(node, cellSize) {
955
+ const seg = clampSegments(node.segments);
956
+ const r = (node.radius ?? 0.4) * cellSize;
957
+ const w = (node.width ?? 1) * cellSize;
958
+ const h = (node.height ?? 1) * cellSize;
959
+ const d = (node.depth ?? node.width ?? 1) * cellSize;
960
+ switch (node.shape) {
961
+ case "box":
962
+ return { element: /* @__PURE__ */ jsx("boxGeometry", { args: [w, h, d] }), lift: h / 2, size: Math.max(w, h, d) };
963
+ case "sphere":
964
+ return { element: /* @__PURE__ */ jsx("sphereGeometry", { args: [r, seg, seg] }), lift: r, size: r * 2 };
965
+ case "capsule":
966
+ return { element: /* @__PURE__ */ jsx("capsuleGeometry", { args: [r, h, Math.max(2, Math.round(seg / 2)), seg] }), lift: h / 2 + r, size: h + r * 2 };
967
+ case "cylinder":
968
+ return {
969
+ element: /* @__PURE__ */ jsx("cylinderGeometry", { args: [(node.radiusTop ?? node.radius ?? 0.4) * cellSize, (node.radiusBottom ?? node.radius ?? 0.4) * cellSize, h, seg] }),
970
+ lift: h / 2,
971
+ size: Math.max(h, r * 2)
972
+ };
973
+ case "cone":
974
+ return { element: /* @__PURE__ */ jsx("coneGeometry", { args: [r, h, seg] }), lift: h / 2, size: Math.max(h, r * 2) };
975
+ case "torus": {
976
+ const tube = (node.tube ?? (node.radius ?? 0.4) / 3) * cellSize;
977
+ return { element: /* @__PURE__ */ jsx("torusGeometry", { args: [r, tube, Math.max(3, Math.round(seg / 2)), seg] }), lift: r + tube, size: (r + tube) * 2 };
978
+ }
979
+ case "plane":
980
+ return { element: /* @__PURE__ */ jsx("planeGeometry", { args: [w, d] }), lift: 0, size: Math.max(w, d), baseRotation: GROUND_ROTATION };
981
+ case "circle":
982
+ return { element: /* @__PURE__ */ jsx("circleGeometry", { args: [r, seg] }), lift: 0, size: r * 2, baseRotation: GROUND_ROTATION };
983
+ case "polyhedron":
984
+ return polyhedronGeometry(node, cellSize);
985
+ default:
986
+ return null;
987
+ }
988
+ }
989
+ var warnedPolyhedronOutline = false;
990
+ var SIDE_MAP = {
991
+ front: THREE5.FrontSide,
992
+ back: THREE5.BackSide,
993
+ double: THREE5.DoubleSide
994
+ };
995
+ function meshMaterial(mat, opacity, ref) {
996
+ const m = mat ?? {};
997
+ const common = {
998
+ ref,
999
+ color: m.color ?? "#ffffff",
1000
+ transparent: opacity < 1,
1001
+ opacity,
1002
+ side: SIDE_MAP[m.side ?? "front"],
1003
+ ...m.map ? { map: getMeshTexture(m.map) } : {}
1004
+ };
1005
+ const emissive = m.emissive ? { emissive: m.emissive, emissiveIntensity: m.emissiveIntensity ?? 1 } : {};
1006
+ switch (m.kind ?? "standard") {
1007
+ case "basic":
1008
+ return /* @__PURE__ */ jsx("meshBasicMaterial", { ...common });
1009
+ case "toon":
1010
+ return /* @__PURE__ */ jsx("meshToonMaterial", { ...common, ...emissive });
1011
+ case "physical":
1012
+ return /* @__PURE__ */ jsx(
1013
+ "meshPhysicalMaterial",
1014
+ {
1015
+ ...common,
1016
+ ...emissive,
1017
+ metalness: m.metalness ?? 0,
1018
+ roughness: m.roughness ?? 0.5,
1019
+ transmission: m.transmission ?? 0,
1020
+ ior: m.ior ?? 1.5,
1021
+ flatShading: m.flatShading ?? false
1022
+ }
1023
+ );
1024
+ case "standard":
1025
+ default:
1026
+ return /* @__PURE__ */ jsx(
1027
+ "meshStandardMaterial",
1028
+ {
1029
+ ...common,
1030
+ ...emissive,
1031
+ metalness: m.metalness ?? 0,
1032
+ roughness: m.roughness ?? 0.5,
1033
+ flatShading: m.flatShading ?? false
1034
+ }
1035
+ );
1036
+ }
1037
+ }
1038
+ function SkinnedPolyhedron3D({
1039
+ node,
1040
+ skin,
1041
+ geometry,
1042
+ cellSize,
1043
+ opacity
1044
+ }) {
1045
+ const boneStore = useContext(BoneRegistryContext);
1046
+ const meshRef = useRef(null);
1047
+ const [registryTick, setRegistryTick] = useState(0);
1048
+ useEffect(() => boneStore?.subscribe(() => setRegistryTick((t) => t + 1)), [boneStore]);
1049
+ useEffect(() => {
1050
+ const mesh = meshRef.current;
1051
+ if (!mesh || !boneStore) return;
1052
+ const bones = [];
1053
+ for (const name of skin.bones) {
1054
+ const bone = boneStore.get(name);
1055
+ if (!bone) return;
1056
+ bones.push(bone);
1057
+ }
1058
+ if (mesh.skeleton && mesh.skeleton.bones.length === bones.length && mesh.skeleton.bones.every((b, i) => b === bones[i])) return;
1059
+ if (skin.inverseBindMatrices.length !== bones.length) return;
1060
+ const inverses = skin.inverseBindMatrices.map((m) => {
1061
+ const mat = new THREE5.Matrix4().fromArray(m);
1062
+ mat.elements[12] *= cellSize;
1063
+ mat.elements[13] *= cellSize;
1064
+ mat.elements[14] *= cellSize;
1065
+ return mat;
1066
+ });
1067
+ mesh.bind(new THREE5.Skeleton(bones, inverses), new THREE5.Matrix4());
1068
+ }, [registryTick, skin, boneStore, geometry, cellSize]);
1069
+ useEffect(() => {
1070
+ const mesh = meshRef.current;
1071
+ return () => mesh?.skeleton?.dispose();
1072
+ }, []);
1073
+ return /* @__PURE__ */ jsx(
1074
+ "skinnedMesh",
1075
+ {
1076
+ ref: meshRef,
1077
+ geometry,
1078
+ frustumCulled: false,
1079
+ castShadow: node.castShadow ?? true,
1080
+ receiveShadow: node.receiveShadow ?? true,
1081
+ children: meshMaterial(node.material, opacity)
1082
+ }
1083
+ );
1084
+ }
1085
+ function Mesh3D({
1086
+ node,
1087
+ projector,
1088
+ groupOpacity = 1
1089
+ }) {
1090
+ const groupRef = useRef(null);
1091
+ const meshRef = useRef(null);
1092
+ const materialRef = useRef(null);
1093
+ const animated = isAnimatedMesh(node);
1094
+ const validPos = isValidScenePos(node.position);
1095
+ const baseWorld = validPos ? projector.toWorld(node.position) : [0, 0, 0];
1096
+ const geo = useMemo(() => meshGeometry(node, projector.cellSize), [node, projector.cellSize]);
1097
+ useEffect(() => {
1098
+ const g = geo?.geometry;
1099
+ return () => g?.dispose();
1100
+ }, [geo]);
1101
+ useFrame(({ clock }) => {
1102
+ if (!animated || !groupRef.current) return;
1103
+ const state = applyMeshAnimation(node, clock.elapsedTime * 1e3);
1104
+ if (!state) return;
1105
+ const [wx, wy, wz] = baseWorld;
1106
+ groupRef.current.position.set(
1107
+ wx + state.offset[0] * projector.cellSize,
1108
+ wy + state.offset[2] * projector.cellSize,
1109
+ wz + state.offset[1] * projector.cellSize
1110
+ );
1111
+ groupRef.current.scale.setScalar(state.scale);
1112
+ if (meshRef.current) {
1113
+ const shapeRot = geo?.baseRotation ?? [0, 0, 0];
1114
+ const base = node.rotation ?? [0, 0, 0];
1115
+ meshRef.current.rotation.set(
1116
+ shapeRot[0] + base[0] + state.rotate[0],
1117
+ shapeRot[1] + base[1] + state.rotate[1],
1118
+ shapeRot[2] + base[2] + state.rotate[2]
1119
+ );
1120
+ }
1121
+ const mat = materialRef.current;
1122
+ if (mat) {
1123
+ const effective = (state.opacity ?? node.opacity ?? 1) * (node.material?.opacity ?? 1) * groupOpacity;
1124
+ mat.opacity = effective;
1125
+ mat.transparent = effective < 1;
1126
+ if (state.color && mat.color) mat.color.set(state.color);
1127
+ if (state.emissive && mat.emissive) mat.emissive.set(state.emissive);
1128
+ if (state.emissiveIntensity !== void 0 && mat.emissiveIntensity !== void 0) {
1129
+ mat.emissiveIntensity = state.emissiveIntensity;
1130
+ }
1131
+ }
1132
+ });
1133
+ if (!validPos || !geo) return null;
1134
+ const lift = (node.pivot ?? "bottom") === "bottom" ? geo.lift : 0;
1135
+ const opacity = (node.opacity ?? 1) * (node.material?.opacity ?? 1) * groupOpacity;
1136
+ if (node.skin && geo.geometry) {
1137
+ return /* @__PURE__ */ jsx("group", { position: baseWorld, children: /* @__PURE__ */ jsx(SkinnedPolyhedron3D, { node, skin: node.skin, geometry: geo.geometry, cellSize: projector.cellSize, opacity }) });
1138
+ }
1139
+ const nodeRotation = node.rotation ?? [0, 0, 0];
1140
+ const shapeRotation = geo.baseRotation ?? [0, 0, 0];
1141
+ const rotation = [
1142
+ shapeRotation[0] + nodeRotation[0],
1143
+ shapeRotation[1] + nodeRotation[1],
1144
+ shapeRotation[2] + nodeRotation[2]
1145
+ ];
1146
+ const outlineScale = geo.size > 0 ? 1 + (node.outline?.width ?? 0.05) * projector.cellSize / geo.size : 1;
1147
+ if (node.outline && geo.geometry && !warnedPolyhedronOutline) {
1148
+ warnedPolyhedronOutline = true;
1149
+ console.warn('[draw-mesh] outline is not yet supported on shape "polyhedron" \u2014 skipped');
1150
+ }
1151
+ const geometryProp = geo.geometry ? { geometry: geo.geometry } : {};
1152
+ return /* @__PURE__ */ jsxs("group", { ref: groupRef, position: baseWorld, children: [
1153
+ /* @__PURE__ */ jsxs(
1154
+ "mesh",
1155
+ {
1156
+ ref: meshRef,
1157
+ ...geometryProp,
1158
+ position: [0, lift, 0],
1159
+ rotation,
1160
+ castShadow: node.castShadow ?? true,
1161
+ receiveShadow: node.receiveShadow ?? true,
1162
+ children: [
1163
+ geo.element,
1164
+ meshMaterial(node.material, opacity, materialRef)
1165
+ ]
1166
+ }
1167
+ ),
1168
+ node.outline && !geo.geometry && /* @__PURE__ */ jsxs("mesh", { position: [0, lift, 0], rotation, scale: outlineScale, children: [
1169
+ geo.element,
1170
+ /* @__PURE__ */ jsx(
1171
+ "meshBasicMaterial",
1172
+ {
1173
+ color: node.outline.color ?? "#101014",
1174
+ side: THREE5.BackSide,
1175
+ transparent: opacity < 1,
1176
+ opacity
1177
+ }
1178
+ )
1179
+ ] })
1180
+ ] });
1181
+ }
1182
+ function Drawable3D({ node, projector, groupOpacity = 1 }) {
1183
+ const rendered = (() => {
1184
+ switch (node.type) {
1185
+ case "draw-sprite":
1186
+ return /* @__PURE__ */ jsx(Sprite3D, { node, projector, groupOpacity });
1187
+ case "draw-shape":
1188
+ return /* @__PURE__ */ jsx(Shape3D, { node, projector, groupOpacity });
1189
+ case "draw-text":
1190
+ return /* @__PURE__ */ jsx(Text3D, { node, projector, groupOpacity });
1191
+ case "draw-mesh":
1192
+ return /* @__PURE__ */ jsx(Mesh3D, { node, projector, groupOpacity });
1193
+ case "draw-sprite-layer":
1194
+ return /* @__PURE__ */ jsx(Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsx(Sprite3D, { node: item, projector, groupOpacity }, i)) });
1195
+ case "draw-shape-layer":
1196
+ return /* @__PURE__ */ jsx(Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsx(Shape3D, { node: item, projector, groupOpacity }, i)) });
1197
+ case "draw-text-layer":
1198
+ return /* @__PURE__ */ jsx(Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsx(Text3D, { node: item, projector, groupOpacity }, i)) });
1199
+ case "draw-group": {
1200
+ if (!isValidScenePos(node.position) || !Array.isArray(node.items)) return null;
1201
+ if (node.clip) warnUnsupported3d("draw-group:clip");
1202
+ return /* @__PURE__ */ jsx(Group3D, { node, projector, groupOpacity });
1203
+ }
1204
+ case "draw-fx-layer":
1205
+ return /* @__PURE__ */ jsx(FxLayer3D, { node, projector, groupOpacity });
1206
+ case "draw-skinned-mesh":
1207
+ warnUnsupported3d("draw-skinned-mesh");
1208
+ return null;
1209
+ }
1210
+ })();
1211
+ return node.id ? /* @__PURE__ */ jsx("group", { userData: { drawableId: node.id }, children: rendered }) : rendered;
1212
+ }
1213
+ function Group3D({
1214
+ node,
1215
+ projector,
1216
+ groupOpacity
1217
+ }) {
1218
+ const ref = useRef(null);
1219
+ const animated = isAnimatedGroup(node);
1220
+ const world = projector.toWorld(node.position);
1221
+ const inner = create3DProjector({ cellSize: projector.cellSize });
1222
+ const opacity = (node.opacity ?? 1) * groupOpacity;
1223
+ const s = node.scale ?? 1;
1224
+ const baseRotation = node.rotation ?? [0, -(node.rotate ?? 0), 0];
1225
+ const parentStore = useContext(BoneRegistryContext);
1226
+ const scopedStore = useMemo(() => node.skeleton ? new BoneStore() : null, [node.skeleton]);
1227
+ const boneStore = scopedStore ?? parentStore;
1228
+ const boneObject = useMemo(() => node.bone ? new THREE5.Bone() : null, [node.bone]);
1229
+ useEffect(() => {
1230
+ if (!node.bone || !boneStore || !boneObject) return;
1231
+ return boneStore.register(node.bone, boneObject);
1232
+ }, [node.bone, boneStore, boneObject]);
1233
+ useFrame(({ clock }) => {
1234
+ if (!animated || !ref.current) return;
1235
+ const state = applyMeshAnimation(node, clock.elapsedTime * 1e3);
1236
+ if (!state) return;
1237
+ const cell = projector.cellSize;
1238
+ ref.current.position.set(
1239
+ world[0] + state.offset[0] * cell,
1240
+ world[1] + state.offset[2] * cell,
1241
+ world[2] + state.offset[1] * cell
1242
+ );
1243
+ ref.current.rotation.set(
1244
+ baseRotation[0] + state.rotate[0],
1245
+ baseRotation[1] + state.rotate[1],
1246
+ baseRotation[2] + state.rotate[2]
1247
+ );
1248
+ ref.current.scale.setScalar(s * state.scale);
1249
+ });
1250
+ const children = /* @__PURE__ */ jsxs("group", { ref, position: world, rotation: baseRotation, scale: [s, s, s], children: [
1251
+ boneObject && /* @__PURE__ */ jsx("primitive", { object: boneObject }),
1252
+ node.items.map((item, i) => /* @__PURE__ */ jsx(Drawable3D, { node: item, projector: inner, groupOpacity: opacity }, i))
1253
+ ] });
1254
+ return scopedStore ? /* @__PURE__ */ jsx(BoneRegistryContext.Provider, { value: scopedStore, children }) : children;
1255
+ }
1256
+ function FxLayer3D({
1257
+ node,
1258
+ projector,
1259
+ groupOpacity
1260
+ }) {
1261
+ const [, setFrame] = useState(0);
1262
+ const live = Array.isArray(node.items) && node.items.length > 0;
1263
+ useFrame(() => {
1264
+ if (live) setFrame((f) => (f + 1) % 1e6);
1265
+ });
1266
+ if (!live) return null;
1267
+ const epochNow = typeof performance !== "undefined" ? performance.timeOrigin + performance.now() : 0;
1268
+ const children = expandFxLayer(node, epochNow, "3d", void 0, void 0);
1269
+ return /* @__PURE__ */ jsx(Fragment, { children: children.map((c, i) => /* @__PURE__ */ jsx(Drawable3D, { node: c, projector, groupOpacity }, i)) });
1270
+ }
1271
+ var DEFAULT_GRID_CONFIG = {
1272
+ cellSize: 1};
1273
+ var EDIT_SELECTION_RADIUS = 0.6;
1274
+ function CameraPose({
1275
+ position,
1276
+ fov,
1277
+ controls
1278
+ }) {
1279
+ const camera = useThree((s) => s.camera);
1280
+ const [px, py, pz] = position;
1281
+ useEffect(() => {
1282
+ camera.position.set(px, py, pz);
1283
+ if (camera.isPerspectiveCamera) {
1284
+ camera.fov = fov;
1285
+ camera.updateProjectionMatrix();
1286
+ }
1287
+ controls.current?.update();
1288
+ }, [camera, px, py, pz, fov, controls]);
1289
+ return null;
1290
+ }
1291
+ function RoomEnvironment3D() {
1292
+ const { gl, scene } = useThree(({ gl: gl2, scene: scene2 }) => ({ gl: gl2, scene: scene2 }));
1293
+ useEffect(() => {
1294
+ const pmremGenerator = new THREE5.PMREMGenerator(gl);
1295
+ const envTexture = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;
1296
+ scene.environment = envTexture;
1297
+ return () => {
1298
+ scene.environment = null;
1299
+ envTexture.dispose();
1300
+ pmremGenerator.dispose();
1301
+ };
1302
+ }, [gl, scene]);
1303
+ return null;
1304
+ }
1305
+ var Canvas3DHost = forwardRef(
1306
+ ({
1307
+ cameraMode = "isometric",
1308
+ followTarget,
1309
+ showGrid = true,
1310
+ orientation = "standard",
1311
+ overlay = "default",
1312
+ shadows = true,
1313
+ backgroundColor,
1314
+ series,
1315
+ className,
1316
+ isLoading: externalLoading,
1317
+ error: externalError,
1318
+ tileClickEvent,
1319
+ unitClickEvent,
1320
+ featureClickEvent,
1321
+ canvasClickEvent,
1322
+ tileHoverEvent,
1323
+ tileLeaveEvent,
1324
+ unitAnimationEvent,
1325
+ cameraChangeEvent,
1326
+ loadingMessage: loadingMessageProp,
1327
+ keyMap,
1328
+ keyUpMap,
1329
+ pixelsPerUnit,
1330
+ fov,
1331
+ azimuth,
1332
+ elevation,
1333
+ zoom,
1334
+ controlsEnabled = true,
1335
+ lighting,
1336
+ post,
1337
+ children,
1338
+ drawables,
1339
+ editable = false,
1340
+ selectedId = null,
1341
+ onSelect,
1342
+ onMove,
1343
+ selectEvent,
1344
+ moveEvent
1345
+ }, ref) => {
1346
+ const containerRef = useRef(null);
1347
+ const onScreen = useOnScreen(containerRef);
1348
+ const controlsRef = useRef(null);
1349
+ const [internalError, setInternalError] = useState(null);
1350
+ const eventBus = useEventBus();
1351
+ const { t } = useTranslate();
1352
+ const loadingMessage = loadingMessageProp ?? t("canvas3d.loadingScene");
1353
+ const keysRef = useRef(/* @__PURE__ */ new Set());
1354
+ const { theme: baseCanvasTheme } = useCanvasTheme(containerRef);
1355
+ const seriesKey = series ? series.join("|") : "";
1356
+ const canvasTheme = useMemo(() => withSeries(baseCanvasTheme, series, containerRef.current), [baseCanvasTheme, seriesKey]);
1357
+ const allDrawables = useMemo(() => {
1358
+ const raw = drawables ?? [];
1359
+ const scope = containerRef.current;
1360
+ if (!canvasTheme || !scope) return raw;
1361
+ const resolved = resolveDrawableColors(raw, (c) => markColor(canvasTheme, c, scope, "ink"));
1362
+ return applySceneMaterials(resolved, canvasTheme);
1363
+ }, [drawables, canvasTheme]);
1364
+ const ground = backgroundColor ?? canvasTheme?.ground ?? "transparent";
1365
+ const selectionColor = canvasTheme?.tones.highlight ?? "currentColor";
1366
+ useEffect(() => {
1367
+ if (!keyMap && !keyUpMap) return;
1368
+ const down = (e) => {
1369
+ const code = keyMapCode(e);
1370
+ if (keysRef.current.has(code)) return;
1371
+ keysRef.current.add(code);
1372
+ const ev = resolveKeyMapEvent(keyMap, e);
1373
+ if (ev) {
1374
+ eventBus.emit(`UI:${ev}`, {});
1375
+ e.preventDefault();
1376
+ }
1377
+ };
1378
+ const up = (e) => {
1379
+ keysRef.current.delete(keyMapCode(e));
1380
+ const ev = resolveKeyMapEvent(keyUpMap, e);
1381
+ if (ev) eventBus.emit(`UI:${ev}`, {});
1382
+ };
1383
+ window.addEventListener("keydown", down);
1384
+ window.addEventListener("keyup", up);
1385
+ return () => {
1386
+ window.removeEventListener("keydown", down);
1387
+ window.removeEventListener("keyup", up);
1388
+ };
1389
+ }, [keyMap, keyUpMap, eventBus]);
1390
+ const eventHandlers = useGameCanvas3DEvents({
1391
+ tileClickEvent,
1392
+ unitClickEvent,
1393
+ featureClickEvent,
1394
+ canvasClickEvent,
1395
+ tileHoverEvent,
1396
+ tileLeaveEvent,
1397
+ unitAnimationEvent,
1398
+ cameraChangeEvent
1399
+ });
1400
+ const drawnItems = useMemo(() => collectDrawnItems(allDrawables), [allDrawables]);
1401
+ const scenePositions = useMemo(() => drawnItems.map((i) => i.pos), [drawnItems]);
1402
+ const hitIndex = useMemo(() => buildHitIndex(drawnItems), [drawnItems]);
1403
+ const gridBounds = useMemo(() => {
1404
+ if (scenePositions.length === 0) {
1405
+ return { minX: 0, maxX: 10, minZ: 0, maxZ: 10 };
1406
+ }
1407
+ let minX = Infinity;
1408
+ let maxX = -Infinity;
1409
+ let minZ = Infinity;
1410
+ let maxZ = -Infinity;
1411
+ for (const p of scenePositions) {
1412
+ if (p.x < minX) minX = p.x;
1413
+ if (p.x > maxX) maxX = p.x;
1414
+ if (p.y < minZ) minZ = p.y;
1415
+ if (p.y > maxZ) maxZ = p.y;
1416
+ }
1417
+ return { minX, maxX, minZ, maxZ };
1418
+ }, [scenePositions]);
1419
+ const cellSize = pixelsPerUnit !== void 0 && pixelsPerUnit > 0 ? 1 / pixelsPerUnit : DEFAULT_GRID_CONFIG.cellSize;
1420
+ const cameraTarget = useMemo(
1421
+ () => [
1422
+ (gridBounds.maxX - gridBounds.minX) / 2 * cellSize,
1423
+ 0,
1424
+ (gridBounds.maxZ - gridBounds.minZ) / 2 * cellSize
1425
+ ],
1426
+ [gridBounds, cellSize]
1427
+ );
1428
+ const gridConfig = useMemo(
1429
+ () => ({
1430
+ cellSize,
1431
+ offsetX: -(gridBounds.maxX - gridBounds.minX) / 2,
1432
+ offsetZ: -(gridBounds.maxZ - gridBounds.minZ) / 2
1433
+ }),
1434
+ [gridBounds, cellSize]
1435
+ );
1436
+ const boneStore = useMemo(() => new BoneStore(), []);
1437
+ const drawableProjector = useMemo(
1438
+ () => create3DProjector({
1439
+ cellSize: gridConfig.cellSize,
1440
+ offsetX: -gridBounds.minX * gridConfig.cellSize,
1441
+ offsetZ: -gridBounds.minZ * gridConfig.cellSize
1442
+ }),
1443
+ [gridBounds, gridConfig]
1444
+ );
1445
+ const editDragRef = useRef(null);
1446
+ const [dragPreview, setDragPreview] = useState(null);
1447
+ const groundPointToScene = useCallback((point) => ({
1448
+ x: Math.round(point.x / gridConfig.cellSize + gridBounds.minX),
1449
+ y: Math.round(point.z / gridConfig.cellSize + gridBounds.minZ)
1450
+ }), [gridConfig, gridBounds]);
1451
+ const paintDrawables = useMemo(
1452
+ () => dragPreview ? withPreviewPosition(allDrawables, dragPreview.id, { x: dragPreview.x, y: dragPreview.y }) : allDrawables,
1453
+ [allDrawables, dragPreview]
1454
+ );
1455
+ const selectedItem = useMemo(
1456
+ () => editable && selectedId != null ? drawnItems.find((it) => it.id === selectedId) : void 0,
1457
+ [editable, selectedId, drawnItems]
1458
+ );
1459
+ const selectionPos = useMemo(() => {
1460
+ if (!selectedItem) return void 0;
1461
+ if (dragPreview && dragPreview.id === selectedItem.id) return { x: dragPreview.x, y: dragPreview.y };
1462
+ return selectedItem.pos;
1463
+ }, [selectedItem, dragPreview]);
1464
+ useImperativeHandle(ref, () => ({
1465
+ getCameraPosition: () => {
1466
+ if (controlsRef.current) {
1467
+ const pos = controlsRef.current.object.position;
1468
+ return new THREE5.Vector3(pos.x, pos.y, pos.z);
1469
+ }
1470
+ return null;
1471
+ },
1472
+ setCameraPosition: (x, y, z) => {
1473
+ if (controlsRef.current) {
1474
+ controlsRef.current.object.position.set(x, y, z);
1475
+ controlsRef.current.update();
1476
+ }
1477
+ },
1478
+ lookAt: (x, y, z) => {
1479
+ if (controlsRef.current) {
1480
+ controlsRef.current.target.set(x, y, z);
1481
+ controlsRef.current.update();
1482
+ }
1483
+ },
1484
+ resetCamera: () => {
1485
+ if (controlsRef.current) {
1486
+ controlsRef.current.reset();
1487
+ }
1488
+ },
1489
+ screenshot: () => {
1490
+ const canvas = containerRef.current?.querySelector("canvas");
1491
+ if (canvas) {
1492
+ return canvas.toDataURL("image/png");
1493
+ }
1494
+ return null;
1495
+ }
1496
+ }));
1497
+ const cameraConfig = useMemo(() => {
1498
+ const size = Math.max(
1499
+ (gridBounds.maxX - gridBounds.minX) * cellSize,
1500
+ (gridBounds.maxZ - gridBounds.minZ) * cellSize,
1501
+ 4
1502
+ // minimum framing distance so a tiny board isn't zoomed in
1503
+ );
1504
+ const cx = cameraTarget[0];
1505
+ const cz = cameraTarget[2];
1506
+ const d = size * 1 / (zoom !== void 0 && zoom > 0 ? zoom : 1);
1507
+ const fovDeg = fov ?? 45;
1508
+ const base = (() => {
1509
+ switch (cameraMode) {
1510
+ case "isometric":
1511
+ return { position: [cx + d, d * 0.8, cz + d], fov: fovDeg };
1512
+ case "top-down":
1513
+ return { position: [cx, d * 2, cz + d * 0.35], fov: fovDeg };
1514
+ case "front":
1515
+ return { position: [cx, d * 0.32, cz + d * 1.15], fov: fovDeg };
1516
+ case "follow":
1517
+ return { position: [cx, d * 0.5, cz + d], fov: fovDeg };
1518
+ case "perspective":
1519
+ default: {
1520
+ if (elevation === void 0) return { position: [cx + d, d, cz + d], fov: fovDeg };
1521
+ const el = Math.min(Math.max(elevation, 0), Math.PI / 2 - 0.1);
1522
+ return { position: [cx + d, d * Math.tan(el), cz + d], fov: fovDeg };
1523
+ }
1524
+ }
1525
+ })();
1526
+ if (!azimuth) return base;
1527
+ const ox = base.position[0] - cx;
1528
+ const oz = base.position[2] - cz;
1529
+ const c = Math.cos(azimuth);
1530
+ const s = Math.sin(azimuth);
1531
+ return { position: [cx + ox * c - oz * s, base.position[1], cz + ox * s + oz * c], fov: base.fov };
1532
+ }, [cameraMode, gridBounds, cellSize, cameraTarget, fov, azimuth, elevation, zoom]);
1533
+ const followWorld = useMemo(() => {
1534
+ if (followTarget) return drawableProjector.toWorld(followTarget);
1535
+ return cameraTarget;
1536
+ }, [followTarget, drawableProjector, cameraTarget]);
1537
+ const followOffset = useMemo(
1538
+ () => cameraMode === "chase" ? [0, 4, -7] : [0, 12, 14],
1539
+ [cameraMode]
1540
+ );
1541
+ const handleGroundClick = useCallback((e) => {
1542
+ if (!tileClickEvent && !unitClickEvent) return;
1543
+ e.stopPropagation();
1544
+ const cell = groundPointToScene(e.point);
1545
+ const hitId = hitIndex.get(`${cell.x},${cell.y}`);
1546
+ if (hitId !== void 0 && unitClickEvent) {
1547
+ eventBus.emit(`UI:${unitClickEvent}`, { unitId: hitId, x: cell.x, z: cell.y });
1548
+ } else if (tileClickEvent) {
1549
+ eventBus.emit(`UI:${tileClickEvent}`, { x: cell.x, z: cell.y });
1550
+ }
1551
+ }, [tileClickEvent, unitClickEvent, groundPointToScene, hitIndex, eventBus]);
1552
+ const handleDrawableClick = useCallback((e) => {
1553
+ if (!unitClickEvent) return;
1554
+ let obj = e.object;
1555
+ while (obj) {
1556
+ const id = obj.userData.drawableId;
1557
+ if (id !== void 0) {
1558
+ e.stopPropagation();
1559
+ eventBus.emit(`UI:${unitClickEvent}`, {
1560
+ unitId: id,
1561
+ x: e.point.x / gridConfig.cellSize + gridBounds.minX,
1562
+ z: e.point.z / gridConfig.cellSize + gridBounds.minZ
1563
+ });
1564
+ return;
1565
+ }
1566
+ obj = obj.parent;
1567
+ }
1568
+ }, [unitClickEvent, gridConfig, gridBounds, eventBus]);
1569
+ const handleEditDrawablePointerDown = useCallback((e) => {
1570
+ if (!editable) return;
1571
+ let obj = e.object;
1572
+ while (obj) {
1573
+ const id = obj.userData.drawableId;
1574
+ if (id !== void 0) {
1575
+ e.stopPropagation();
1576
+ editDragRef.current = {
1577
+ id,
1578
+ startClientX: e.nativeEvent.clientX,
1579
+ startClientY: e.nativeEvent.clientY,
1580
+ moved: false
1581
+ };
1582
+ return;
1583
+ }
1584
+ obj = obj.parent;
1585
+ }
1586
+ }, [editable]);
1587
+ const handleEditGroundPointerMove = useCallback((e) => {
1588
+ const drag = editDragRef.current;
1589
+ if (!editable || !drag) return;
1590
+ const dx = e.nativeEvent.clientX - drag.startClientX;
1591
+ const dy = e.nativeEvent.clientY - drag.startClientY;
1592
+ if (!drag.moved && Math.abs(dx) + Math.abs(dy) <= 5) return;
1593
+ drag.moved = true;
1594
+ const cell = groundPointToScene(e.point);
1595
+ setDragPreview({ id: drag.id, x: cell.x, y: cell.y });
1596
+ }, [editable, groundPointToScene]);
1597
+ const handleEditPointerUp = useCallback((_e) => {
1598
+ if (!editable) return;
1599
+ const drag = editDragRef.current;
1600
+ editDragRef.current = null;
1601
+ if (!drag) {
1602
+ onSelect?.(null);
1603
+ if (selectEvent) eventBus.emit(`UI:${selectEvent}`, { id: null });
1604
+ return;
1605
+ }
1606
+ if (drag.moved) {
1607
+ setDragPreview((preview) => {
1608
+ if (preview && preview.id === drag.id) {
1609
+ onMove?.(drag.id, preview.x, preview.y);
1610
+ if (moveEvent) eventBus.emit(`UI:${moveEvent}`, { id: drag.id, x: preview.x, y: preview.y });
1611
+ }
1612
+ return null;
1613
+ });
1614
+ return;
1615
+ }
1616
+ const next = selectedId === drag.id ? null : drag.id;
1617
+ onSelect?.(next);
1618
+ if (selectEvent) eventBus.emit(`UI:${selectEvent}`, { id: next });
1619
+ }, [editable, selectedId, onMove, moveEvent, onSelect, selectEvent, eventBus]);
1620
+ if (externalLoading) {
1621
+ return /* @__PURE__ */ jsx(
1622
+ Canvas3DLoadingState,
1623
+ {
1624
+ progress: 1,
1625
+ loaded: 1,
1626
+ total: 1,
1627
+ message: loadingMessage,
1628
+ className
1629
+ }
1630
+ );
1631
+ }
1632
+ const displayError = externalError || internalError;
1633
+ if (displayError) {
1634
+ return /* @__PURE__ */ jsx(Canvas3DErrorBoundary, { children: /* @__PURE__ */ jsx("div", { className: "game-canvas-3d game-canvas-3d--error", children: /* @__PURE__ */ jsx("div", { className: "game-canvas-3d__error", children: t("canvas3d.errorWithMessage", { message: displayError }) }) }) });
1635
+ }
1636
+ return /* @__PURE__ */ jsx(
1637
+ Canvas3DErrorBoundary,
1638
+ {
1639
+ onError: (err) => setInternalError(err.message),
1640
+ onReset: () => setInternalError(null),
1641
+ children: /* @__PURE__ */ jsx(
1642
+ "div",
1643
+ {
1644
+ ref: containerRef,
1645
+ className: cn("game-canvas-3d relative w-full overflow-hidden", className),
1646
+ style: { flex: "1 1 0", minHeight: "400px" },
1647
+ "data-orientation": orientation,
1648
+ "data-camera-mode": cameraMode,
1649
+ "data-overlay": overlay,
1650
+ children: /* @__PURE__ */ jsxs(
1651
+ Canvas,
1652
+ {
1653
+ frameloop: onScreen ? "always" : "never",
1654
+ shadows,
1655
+ flat: lighting?.toneMapping === "none",
1656
+ gl: { preserveDrawingBuffer: true },
1657
+ camera: {
1658
+ position: cameraConfig.position,
1659
+ fov: cameraConfig.fov,
1660
+ near: 0.1,
1661
+ far: 1e3
1662
+ },
1663
+ style: { background: ground, position: "absolute", inset: 0 },
1664
+ onClick: (e) => {
1665
+ if (e.target === e.currentTarget) {
1666
+ eventHandlers.handleCanvasClick(e);
1667
+ }
1668
+ },
1669
+ onPointerUp: handleEditPointerUp,
1670
+ children: [
1671
+ /* @__PURE__ */ jsx(CameraController3D, { onCameraChange: eventHandlers.handleCameraChange }),
1672
+ cameraMode !== "follow" && cameraMode !== "chase" && /* @__PURE__ */ jsx(CameraPose, { position: cameraConfig.position, fov: cameraConfig.fov, controls: controlsRef }),
1673
+ (cameraMode === "follow" || cameraMode === "chase") && /* @__PURE__ */ jsx(FollowCamera3D, { target: followWorld, offset: followOffset }),
1674
+ /* @__PURE__ */ jsx(
1675
+ Lighting3D,
1676
+ {
1677
+ shadows,
1678
+ shadowBias: -4e-4,
1679
+ shadowNormalBias: 0.04,
1680
+ shadowCameraSize: 5,
1681
+ shadowCameraNear: 0.5,
1682
+ shadowCameraFar: 500,
1683
+ ambientIntensity: lighting?.ambient?.intensity ?? canvasTheme?.scene.ambient,
1684
+ ambientColor: lighting?.ambient?.color,
1685
+ directionalIntensity: lighting?.directional?.intensity ?? canvasTheme?.scene.key,
1686
+ directionalColor: lighting?.directional?.color ?? canvasTheme?.scene.keyColor,
1687
+ directionalPosition: lighting?.directional?.position,
1688
+ hemisphereIntensity: lighting?.hemisphere?.intensity,
1689
+ hemisphereColor: lighting?.hemisphere?.color,
1690
+ hemisphereGroundColor: lighting?.hemisphere?.groundColor,
1691
+ points: lighting?.points
1692
+ }
1693
+ ),
1694
+ lighting?.environment === "room" && /* @__PURE__ */ jsx(RoomEnvironment3D, {}),
1695
+ showGrid && /* @__PURE__ */ jsx(
1696
+ Grid,
1697
+ {
1698
+ args: [
1699
+ Math.max((gridBounds.maxX - gridBounds.minX + 2) * cellSize, 10),
1700
+ Math.max((gridBounds.maxZ - gridBounds.minZ + 2) * cellSize, 10)
1701
+ ],
1702
+ position: [
1703
+ (gridBounds.maxX - gridBounds.minX) / 2 * cellSize - cellSize / 2,
1704
+ 0,
1705
+ (gridBounds.maxZ - gridBounds.minZ) / 2 * cellSize - cellSize / 2
1706
+ ],
1707
+ cellSize: 1,
1708
+ cellThickness: 1,
1709
+ cellColor: canvasTheme?.tones.grid ?? "currentColor",
1710
+ sectionSize: 5,
1711
+ sectionThickness: 1.5,
1712
+ sectionColor: canvasTheme?.tones.axis ?? "currentColor",
1713
+ fadeDistance: 50,
1714
+ fadeStrength: 1
1715
+ }
1716
+ ),
1717
+ allDrawables.length > 0 && /* @__PURE__ */ jsx(BoneRegistryContext.Provider, { value: boneStore, children: /* @__PURE__ */ jsx(
1718
+ "group",
1719
+ {
1720
+ onClick: !editable && unitClickEvent ? handleDrawableClick : void 0,
1721
+ onPointerDown: editable ? handleEditDrawablePointerDown : void 0,
1722
+ children: paintDrawables.map((node, i) => /* @__PURE__ */ jsx(Drawable3D, { node, projector: drawableProjector }, i))
1723
+ }
1724
+ ) }),
1725
+ selectionPos && /* @__PURE__ */ jsx(
1726
+ Shape3D,
1727
+ {
1728
+ node: {
1729
+ type: "draw-shape",
1730
+ shape: "ellipse",
1731
+ position: selectionPos,
1732
+ radiusX: EDIT_SELECTION_RADIUS,
1733
+ stroke: selectionColor
1734
+ },
1735
+ projector: drawableProjector
1736
+ }
1737
+ ),
1738
+ (tileClickEvent || unitClickEvent || editable) && /* @__PURE__ */ jsxs(
1739
+ "mesh",
1740
+ {
1741
+ rotation: [-Math.PI / 2, 0, 0],
1742
+ position: [
1743
+ (gridBounds.maxX - gridBounds.minX) / 2 * cellSize,
1744
+ 0,
1745
+ (gridBounds.maxZ - gridBounds.minZ) / 2 * cellSize
1746
+ ],
1747
+ onClick: !editable ? handleGroundClick : void 0,
1748
+ onPointerMove: editable ? handleEditGroundPointerMove : void 0,
1749
+ children: [
1750
+ /* @__PURE__ */ jsx(
1751
+ "planeGeometry",
1752
+ {
1753
+ args: [
1754
+ (gridBounds.maxX - gridBounds.minX + 4) * cellSize,
1755
+ (gridBounds.maxZ - gridBounds.minZ + 4) * cellSize
1756
+ ]
1757
+ }
1758
+ ),
1759
+ /* @__PURE__ */ jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })
1760
+ ]
1761
+ }
1762
+ ),
1763
+ post && (post.bloom || post.vignette) ? /* @__PURE__ */ jsx(Effects3D, { post }) : null,
1764
+ /* @__PURE__ */ jsx(
1765
+ OrbitControls,
1766
+ {
1767
+ ref: controlsRef,
1768
+ enabled: controlsEnabled && !editable && cameraMode !== "follow" && cameraMode !== "chase",
1769
+ target: cameraTarget,
1770
+ enableDamping: true,
1771
+ dampingFactor: 0.05,
1772
+ enableZoom: true,
1773
+ enablePan: true,
1774
+ touches: { ONE: THREE5.TOUCH.ROTATE, TWO: THREE5.TOUCH.DOLLY_PAN },
1775
+ minDistance: 2,
1776
+ maxDistance: 100,
1777
+ maxPolarAngle: Math.PI / 2 - 0.1
1778
+ }
1779
+ )
1780
+ ]
1781
+ }
1782
+ )
1783
+ }
1784
+ )
1785
+ }
1786
+ );
1787
+ }
1788
+ );
1789
+ Canvas3DHost.displayName = "Canvas3DHost";
1790
+ function Scene3D({ background = "#1a1a2e", fog, children }) {
1791
+ const { scene } = useThree();
1792
+ const initializedRef = useRef(false);
1793
+ useEffect(() => {
1794
+ if (initializedRef.current) return;
1795
+ initializedRef.current = true;
1796
+ if (background.startsWith("#") || background.startsWith("rgb")) {
1797
+ scene.background = new THREE5.Color(background);
1798
+ } else {
1799
+ const loader = new THREE5.TextureLoader();
1800
+ loader.load(background, (texture) => {
1801
+ scene.background = texture;
1802
+ });
1803
+ }
1804
+ if (fog) {
1805
+ scene.fog = new THREE5.Fog(fog.color, fog.near, fog.far);
1806
+ }
1807
+ return () => {
1808
+ scene.background = null;
1809
+ scene.fog = null;
1810
+ };
1811
+ }, [scene, background, fog]);
1812
+ return /* @__PURE__ */ jsx(Fragment, { children });
1813
+ }
1814
+ var Camera3D = forwardRef(
1815
+ ({
1816
+ mode = "isometric",
1817
+ position = [10, 10, 10],
1818
+ target = [0, 0, 0],
1819
+ zoom = 1,
1820
+ fov = 45,
1821
+ enableOrbit = true,
1822
+ minDistance = 2,
1823
+ maxDistance = 100,
1824
+ onChange
1825
+ }, ref) => {
1826
+ const { camera, set, viewport } = useThree();
1827
+ const controlsRef = useRef(null);
1828
+ const initialPosition = useRef(new THREE5.Vector3(...position));
1829
+ const initialTarget = useRef(new THREE5.Vector3(...target));
1830
+ useEffect(() => {
1831
+ let newCamera;
1832
+ if (mode === "isometric") {
1833
+ const aspect = viewport.aspect;
1834
+ const size = 10 / zoom;
1835
+ newCamera = new THREE5.OrthographicCamera(
1836
+ -size * aspect,
1837
+ size * aspect,
1838
+ size,
1839
+ -size,
1840
+ 0.1,
1841
+ 1e3
1842
+ );
1843
+ } else {
1844
+ newCamera = new THREE5.PerspectiveCamera(fov, viewport.aspect, 0.1, 1e3);
1845
+ }
1846
+ newCamera.position.copy(initialPosition.current);
1847
+ newCamera.lookAt(initialTarget.current.x, initialTarget.current.y, initialTarget.current.z);
1848
+ set({ camera: newCamera });
1849
+ if (mode === "top-down") {
1850
+ newCamera.position.set(0, 20 / zoom, 0);
1851
+ newCamera.lookAt(0, 0, 0);
1852
+ }
1853
+ return () => {
1854
+ };
1855
+ }, [mode, fov, zoom, viewport.aspect, set]);
1856
+ useFrame(() => {
1857
+ if (onChange) {
1858
+ onChange(camera);
1859
+ }
1860
+ });
1861
+ useImperativeHandle(ref, () => ({
1862
+ getCamera: () => camera,
1863
+ setPosition: (x, y, z) => {
1864
+ camera.position.set(x, y, z);
1865
+ if (controlsRef.current) {
1866
+ controlsRef.current.update();
1867
+ }
1868
+ },
1869
+ lookAt: (x, y, z) => {
1870
+ camera.lookAt(x, y, z);
1871
+ if (controlsRef.current) {
1872
+ controlsRef.current.target.set(x, y, z);
1873
+ controlsRef.current.update();
1874
+ }
1875
+ },
1876
+ reset: () => {
1877
+ camera.position.copy(initialPosition.current);
1878
+ camera.lookAt(initialTarget.current.x, initialTarget.current.y, initialTarget.current.z);
1879
+ if (controlsRef.current) {
1880
+ controlsRef.current.target.copy(initialTarget.current);
1881
+ controlsRef.current.update();
1882
+ }
1883
+ },
1884
+ getViewBounds: () => {
1885
+ const min = new THREE5.Vector3(-10, -10, -10);
1886
+ const max = new THREE5.Vector3(10, 10, 10);
1887
+ return { min, max };
1888
+ }
1889
+ }));
1890
+ const maxPolarAngle = mode === "top-down" ? 0.1 : Math.PI / 2 - 0.1;
1891
+ return /* @__PURE__ */ jsx(
1892
+ OrbitControls,
1893
+ {
1894
+ ref: controlsRef,
1895
+ camera,
1896
+ enabled: enableOrbit,
1897
+ target: [initialTarget.current.x, initialTarget.current.y, initialTarget.current.z],
1898
+ minDistance,
1899
+ maxDistance,
1900
+ maxPolarAngle,
1901
+ enableDamping: true,
1902
+ dampingFactor: 0.05
1903
+ }
1904
+ );
1905
+ }
1906
+ );
1907
+ Camera3D.displayName = "Camera3D";
1908
+ function detectAssetRoot2(modelUrl) {
1909
+ const idx = modelUrl.indexOf("/3d/");
1910
+ if (idx !== -1) {
1911
+ return modelUrl.substring(0, idx + 4);
1912
+ }
1913
+ return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
1914
+ }
1915
+ function createGLTFLoaderForUrl(url) {
1916
+ const loader = new GLTFLoader();
1917
+ loader.setResourcePath(detectAssetRoot2(url));
1918
+ return loader;
1919
+ }
1920
+ var AssetLoader = class {
1921
+ objLoader;
1922
+ textureLoader;
1923
+ modelCache;
1924
+ textureCache;
1925
+ loadingPromises;
1926
+ constructor() {
1927
+ this.objLoader = new OBJLoader();
1928
+ this.textureLoader = new THREE5.TextureLoader();
1929
+ this.modelCache = /* @__PURE__ */ new Map();
1930
+ this.textureCache = /* @__PURE__ */ new Map();
1931
+ this.loadingPromises = /* @__PURE__ */ new Map();
1932
+ }
1933
+ /**
1934
+ * Load a GLB/GLTF model
1935
+ * @param url - URL to the .glb or .gltf file
1936
+ * @returns Promise with loaded model scene and animations
1937
+ */
1938
+ async loadModel(url) {
1939
+ if (this.modelCache.has(url)) {
1940
+ return this.modelCache.get(url);
1941
+ }
1942
+ if (this.loadingPromises.has(url)) {
1943
+ return this.loadingPromises.get(url);
1944
+ }
1945
+ const loader = createGLTFLoaderForUrl(url);
1946
+ const loadPromise = loader.loadAsync(url).then((gltf) => {
1947
+ const result = {
1948
+ scene: gltf.scene,
1949
+ animations: gltf.animations || []
1950
+ };
1951
+ this.modelCache.set(url, result);
1952
+ this.loadingPromises.delete(url);
1953
+ return result;
1954
+ }).catch((error) => {
1955
+ this.loadingPromises.delete(url);
1956
+ throw new Error(`Failed to load model ${url}: ${error.message}`);
1957
+ });
1958
+ this.loadingPromises.set(url, loadPromise);
1959
+ return loadPromise;
1960
+ }
1961
+ /**
1962
+ * Load an OBJ model (fallback for non-GLB assets)
1963
+ * @param url - URL to the .obj file
1964
+ * @returns Promise with loaded object group
1965
+ */
1966
+ async loadOBJ(url) {
1967
+ if (this.modelCache.has(url)) {
1968
+ return this.modelCache.get(url).scene;
1969
+ }
1970
+ if (this.loadingPromises.has(url)) {
1971
+ const result = await this.loadingPromises.get(url);
1972
+ return result.scene;
1973
+ }
1974
+ const loadPromise = this.objLoader.loadAsync(url).then((group) => {
1975
+ const result = {
1976
+ scene: group,
1977
+ animations: []
1978
+ };
1979
+ this.modelCache.set(url, result);
1980
+ this.loadingPromises.delete(url);
1981
+ return result;
1982
+ }).catch((error) => {
1983
+ this.loadingPromises.delete(url);
1984
+ throw new Error(`Failed to load OBJ ${url}: ${error.message}`);
1985
+ });
1986
+ this.loadingPromises.set(url, loadPromise);
1987
+ return (await loadPromise).scene;
1988
+ }
1989
+ /**
1990
+ * Load a texture
1991
+ * @param url - URL to the texture image
1992
+ * @returns Promise with loaded texture
1993
+ */
1994
+ async loadTexture(url) {
1995
+ if (this.textureCache.has(url)) {
1996
+ return this.textureCache.get(url);
1997
+ }
1998
+ if (this.loadingPromises.has(`texture:${url}`)) {
1999
+ return this.loadingPromises.get(`texture:${url}`);
2000
+ }
2001
+ const loadPromise = this.textureLoader.loadAsync(url).then((texture) => {
2002
+ texture.colorSpace = THREE5.SRGBColorSpace;
2003
+ this.textureCache.set(url, texture);
2004
+ this.loadingPromises.delete(`texture:${url}`);
2005
+ return texture;
2006
+ }).catch((error) => {
2007
+ this.loadingPromises.delete(`texture:${url}`);
2008
+ throw new Error(`Failed to load texture ${url}: ${error.message}`);
2009
+ });
2010
+ this.loadingPromises.set(`texture:${url}`, loadPromise);
2011
+ return loadPromise;
2012
+ }
2013
+ /**
2014
+ * Preload multiple assets
2015
+ * @param urls - Array of asset URLs to preload
2016
+ * @returns Promise that resolves when all assets are loaded
2017
+ */
2018
+ async preload(urls) {
2019
+ const promises = urls.map((url) => {
2020
+ if (url.endsWith(".glb") || url.endsWith(".gltf")) {
2021
+ return this.loadModel(url).catch(() => null);
2022
+ } else if (url.endsWith(".obj")) {
2023
+ return this.loadOBJ(url).catch(() => null);
2024
+ } else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
2025
+ return this.loadTexture(url).catch(() => null);
2026
+ }
2027
+ return Promise.resolve(null);
2028
+ });
2029
+ await Promise.all(promises);
2030
+ }
2031
+ /**
2032
+ * Check if a model is cached
2033
+ * @param url - Model URL
2034
+ */
2035
+ hasModel(url) {
2036
+ return this.modelCache.has(url);
2037
+ }
2038
+ /**
2039
+ * Check if a texture is cached
2040
+ * @param url - Texture URL
2041
+ */
2042
+ hasTexture(url) {
2043
+ return this.textureCache.has(url);
2044
+ }
2045
+ /**
2046
+ * Get cached model (throws if not cached)
2047
+ * @param url - Model URL
2048
+ */
2049
+ getModel(url) {
2050
+ const model = this.modelCache.get(url);
2051
+ if (!model) {
2052
+ throw new Error(`Model ${url} not in cache`);
2053
+ }
2054
+ return model;
2055
+ }
2056
+ /**
2057
+ * Get cached texture (throws if not cached)
2058
+ * @param url - Texture URL
2059
+ */
2060
+ getTexture(url) {
2061
+ const texture = this.textureCache.get(url);
2062
+ if (!texture) {
2063
+ throw new Error(`Texture ${url} not in cache`);
2064
+ }
2065
+ return texture;
2066
+ }
2067
+ /**
2068
+ * Clear all caches
2069
+ */
2070
+ clearCache() {
2071
+ this.textureCache.forEach((texture) => {
2072
+ texture.dispose();
2073
+ });
2074
+ this.modelCache.forEach((model) => {
2075
+ model.scene.traverse((child) => {
2076
+ if (child instanceof THREE5.Mesh) {
2077
+ child.geometry.dispose();
2078
+ if (Array.isArray(child.material)) {
2079
+ child.material.forEach((m) => m.dispose());
2080
+ } else {
2081
+ child.material.dispose();
2082
+ }
2083
+ }
2084
+ });
2085
+ });
2086
+ this.modelCache.clear();
2087
+ this.textureCache.clear();
2088
+ this.loadingPromises.clear();
2089
+ }
2090
+ /**
2091
+ * Get cache statistics
2092
+ */
2093
+ getStats() {
2094
+ return {
2095
+ models: this.modelCache.size,
2096
+ textures: this.textureCache.size,
2097
+ loading: this.loadingPromises.size
2098
+ };
2099
+ }
2100
+ };
2101
+ var assetLoader = new AssetLoader();
2102
+
2103
+ // src/lib/drawable/three/hooks/useThree.ts
2104
+ var DEFAULT_OPTIONS = {
2105
+ cameraMode: "isometric",
2106
+ cameraPosition: [10, 10, 10],
2107
+ backgroundColor: "#1a1a2e",
2108
+ shadows: true,
2109
+ showGrid: true,
2110
+ gridSize: 20,
2111
+ assetLoader: new AssetLoader()
2112
+ };
2113
+ function useThree5(options = {}) {
2114
+ const opts = { ...DEFAULT_OPTIONS, ...options };
2115
+ const containerRef = useRef(null);
2116
+ const canvasRef = useRef(null);
2117
+ const rendererRef = useRef(null);
2118
+ const sceneRef = useRef(null);
2119
+ const cameraRef = useRef(null);
2120
+ const controlsRef = useRef(null);
2121
+ const gridHelperRef = useRef(null);
2122
+ const rafRef = useRef(0);
2123
+ const [isReady, setIsReady] = useState(false);
2124
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
2125
+ const initialCameraPosition = useMemo(
2126
+ () => new THREE5.Vector3(...opts.cameraPosition),
2127
+ []
2128
+ );
2129
+ useEffect(() => {
2130
+ if (!containerRef.current) return;
2131
+ const container = containerRef.current;
2132
+ const { clientWidth, clientHeight } = container;
2133
+ const scene = new THREE5.Scene();
2134
+ scene.background = new THREE5.Color(opts.backgroundColor);
2135
+ sceneRef.current = scene;
2136
+ let camera;
2137
+ const aspect = clientWidth / clientHeight;
2138
+ if (opts.cameraMode === "isometric") {
2139
+ const size = 10;
2140
+ camera = new THREE5.OrthographicCamera(
2141
+ -size * aspect,
2142
+ size * aspect,
2143
+ size,
2144
+ -size,
2145
+ 0.1,
2146
+ 1e3
2147
+ );
2148
+ } else {
2149
+ camera = new THREE5.PerspectiveCamera(45, aspect, 0.1, 1e3);
2150
+ }
2151
+ camera.position.copy(initialCameraPosition);
2152
+ cameraRef.current = camera;
2153
+ const renderer = new THREE5.WebGLRenderer({
2154
+ antialias: true,
2155
+ alpha: true,
2156
+ canvas: canvasRef.current || void 0
2157
+ });
2158
+ renderer.setSize(clientWidth, clientHeight);
2159
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
2160
+ renderer.shadowMap.enabled = opts.shadows;
2161
+ renderer.shadowMap.type = THREE5.PCFSoftShadowMap;
2162
+ rendererRef.current = renderer;
2163
+ const controls = new OrbitControls$1(camera, renderer.domElement);
2164
+ controls.enableDamping = true;
2165
+ controls.dampingFactor = 0.05;
2166
+ controls.minDistance = 2;
2167
+ controls.maxDistance = 100;
2168
+ controls.maxPolarAngle = Math.PI / 2 - 0.1;
2169
+ controlsRef.current = controls;
2170
+ const ambientLight = new THREE5.AmbientLight(16777215, 0.6);
2171
+ scene.add(ambientLight);
2172
+ const directionalLight = new THREE5.DirectionalLight(16777215, 0.8);
2173
+ directionalLight.position.set(10, 20, 10);
2174
+ directionalLight.castShadow = opts.shadows;
2175
+ directionalLight.shadow.mapSize.width = 2048;
2176
+ directionalLight.shadow.mapSize.height = 2048;
2177
+ scene.add(directionalLight);
2178
+ if (opts.showGrid) {
2179
+ const gridHelper = new THREE5.GridHelper(
2180
+ opts.gridSize,
2181
+ opts.gridSize,
2182
+ 4473924,
2183
+ 2236962
2184
+ );
2185
+ scene.add(gridHelper);
2186
+ gridHelperRef.current = gridHelper;
2187
+ }
2188
+ const animate = () => {
2189
+ rafRef.current = requestAnimationFrame(animate);
2190
+ controls.update();
2191
+ renderer.render(scene, camera);
2192
+ };
2193
+ animate();
2194
+ const handleResize = () => {
2195
+ const { clientWidth: width, clientHeight: height } = container;
2196
+ setDimensions({ width, height });
2197
+ if (camera instanceof THREE5.PerspectiveCamera) {
2198
+ camera.aspect = width / height;
2199
+ camera.updateProjectionMatrix();
2200
+ } else if (camera instanceof THREE5.OrthographicCamera) {
2201
+ const aspect2 = width / height;
2202
+ const size = 10;
2203
+ camera.left = -size * aspect2;
2204
+ camera.right = size * aspect2;
2205
+ camera.top = size;
2206
+ camera.bottom = -size;
2207
+ camera.updateProjectionMatrix();
2208
+ }
2209
+ renderer.setSize(width, height);
2210
+ };
2211
+ window.addEventListener("resize", handleResize);
2212
+ handleResize();
2213
+ setIsReady(true);
2214
+ return () => {
2215
+ window.removeEventListener("resize", handleResize);
2216
+ cancelAnimationFrame(rafRef.current);
2217
+ controls.dispose();
2218
+ renderer.dispose();
2219
+ scene.clear();
2220
+ };
2221
+ }, []);
2222
+ useEffect(() => {
2223
+ if (!cameraRef.current || !containerRef.current) return;
2224
+ const container = containerRef.current;
2225
+ const { clientWidth, clientHeight } = container;
2226
+ const aspect = clientWidth / clientHeight;
2227
+ const currentPos = cameraRef.current.position.clone();
2228
+ let newCamera;
2229
+ if (opts.cameraMode === "isometric") {
2230
+ const size = 10;
2231
+ newCamera = new THREE5.OrthographicCamera(
2232
+ -size * aspect,
2233
+ size * aspect,
2234
+ size,
2235
+ -size,
2236
+ 0.1,
2237
+ 1e3
2238
+ );
2239
+ } else {
2240
+ newCamera = new THREE5.PerspectiveCamera(45, aspect, 0.1, 1e3);
2241
+ }
2242
+ newCamera.position.copy(currentPos);
2243
+ cameraRef.current = newCamera;
2244
+ if (controlsRef.current) {
2245
+ controlsRef.current.object = newCamera;
2246
+ }
2247
+ if (rendererRef.current) {
2248
+ const animate = () => {
2249
+ rafRef.current = requestAnimationFrame(animate);
2250
+ controlsRef.current?.update();
2251
+ rendererRef.current?.render(sceneRef.current, newCamera);
2252
+ };
2253
+ cancelAnimationFrame(rafRef.current);
2254
+ animate();
2255
+ }
2256
+ }, [opts.cameraMode]);
2257
+ const setCameraPosition = useCallback((x, y, z) => {
2258
+ if (cameraRef.current) {
2259
+ cameraRef.current.position.set(x, y, z);
2260
+ controlsRef.current?.update();
2261
+ }
2262
+ }, []);
2263
+ const lookAt = useCallback((x, y, z) => {
2264
+ if (cameraRef.current) {
2265
+ cameraRef.current.lookAt(x, y, z);
2266
+ controlsRef.current?.target.set(x, y, z);
2267
+ controlsRef.current?.update();
2268
+ }
2269
+ }, []);
2270
+ const resetCamera = useCallback(() => {
2271
+ if (cameraRef.current) {
2272
+ cameraRef.current.position.copy(initialCameraPosition);
2273
+ cameraRef.current.lookAt(0, 0, 0);
2274
+ if (controlsRef.current) {
2275
+ controlsRef.current.target.set(0, 0, 0);
2276
+ controlsRef.current.update();
2277
+ }
2278
+ }
2279
+ }, [initialCameraPosition]);
2280
+ const fitView = useCallback(
2281
+ (bounds) => {
2282
+ if (!cameraRef.current) return;
2283
+ const centerX = (bounds.minX + bounds.maxX) / 2;
2284
+ const centerZ = (bounds.minZ + bounds.maxZ) / 2;
2285
+ const width = bounds.maxX - bounds.minX;
2286
+ const depth = bounds.maxZ - bounds.minZ;
2287
+ const maxDim = Math.max(width, depth);
2288
+ const distance = maxDim * 1.5;
2289
+ const height = distance * 0.8;
2290
+ cameraRef.current.position.set(centerX + distance, height, centerZ + distance);
2291
+ lookAt(centerX, 0, centerZ);
2292
+ },
2293
+ [lookAt]
2294
+ );
2295
+ return {
2296
+ canvasRef,
2297
+ renderer: rendererRef.current,
2298
+ scene: sceneRef.current,
2299
+ camera: cameraRef.current,
2300
+ controls: controlsRef.current,
2301
+ isReady,
2302
+ dimensions,
2303
+ setCameraPosition,
2304
+ lookAt,
2305
+ resetCamera,
2306
+ fitView
2307
+ };
2308
+ }
2309
+ function useAssetLoader(options = {}) {
2310
+ const { preloadUrls = [], loader: customLoader } = options;
2311
+ const loaderRef = useRef(customLoader || new AssetLoader());
2312
+ const [state, setState] = useState({
2313
+ isLoading: false,
2314
+ progress: 0,
2315
+ loaded: 0,
2316
+ total: 0,
2317
+ errors: []
2318
+ });
2319
+ useEffect(() => {
2320
+ if (preloadUrls.length > 0) {
2321
+ preload(preloadUrls);
2322
+ }
2323
+ }, []);
2324
+ const updateProgress = useCallback((loaded, total) => {
2325
+ setState((prev) => ({
2326
+ ...prev,
2327
+ loaded,
2328
+ total,
2329
+ progress: total > 0 ? Math.round(loaded / total * 100) : 0
2330
+ }));
2331
+ }, []);
2332
+ const loadModel = useCallback(
2333
+ async (url) => {
2334
+ setState((prev) => ({ ...prev, isLoading: true }));
2335
+ try {
2336
+ const model = await loaderRef.current.loadModel(url);
2337
+ setState((prev) => ({
2338
+ ...prev,
2339
+ isLoading: false,
2340
+ loaded: prev.loaded + 1
2341
+ }));
2342
+ return model;
2343
+ } catch (error) {
2344
+ const errorMsg = error instanceof Error ? error.message : String(error);
2345
+ setState((prev) => ({
2346
+ ...prev,
2347
+ isLoading: false,
2348
+ errors: [...prev.errors, errorMsg]
2349
+ }));
2350
+ throw error;
2351
+ }
2352
+ },
2353
+ []
2354
+ );
2355
+ const loadOBJ = useCallback(
2356
+ async (url) => {
2357
+ setState((prev) => ({ ...prev, isLoading: true }));
2358
+ try {
2359
+ const model = await loaderRef.current.loadOBJ(url);
2360
+ setState((prev) => ({
2361
+ ...prev,
2362
+ isLoading: false,
2363
+ loaded: prev.loaded + 1
2364
+ }));
2365
+ return model;
2366
+ } catch (error) {
2367
+ const errorMsg = error instanceof Error ? error.message : String(error);
2368
+ setState((prev) => ({
2369
+ ...prev,
2370
+ isLoading: false,
2371
+ errors: [...prev.errors, errorMsg]
2372
+ }));
2373
+ throw error;
2374
+ }
2375
+ },
2376
+ []
2377
+ );
2378
+ const loadTexture = useCallback(
2379
+ async (url) => {
2380
+ setState((prev) => ({ ...prev, isLoading: true }));
2381
+ try {
2382
+ const texture = await loaderRef.current.loadTexture(url);
2383
+ setState((prev) => ({
2384
+ ...prev,
2385
+ isLoading: false,
2386
+ loaded: prev.loaded + 1
2387
+ }));
2388
+ return texture;
2389
+ } catch (error) {
2390
+ const errorMsg = error instanceof Error ? error.message : String(error);
2391
+ setState((prev) => ({
2392
+ ...prev,
2393
+ isLoading: false,
2394
+ errors: [...prev.errors, errorMsg]
2395
+ }));
2396
+ throw error;
2397
+ }
2398
+ },
2399
+ []
2400
+ );
2401
+ const preload = useCallback(
2402
+ async (urls) => {
2403
+ setState((prev) => ({
2404
+ ...prev,
2405
+ isLoading: true,
2406
+ total: urls.length,
2407
+ loaded: 0,
2408
+ errors: []
2409
+ }));
2410
+ let completed = 0;
2411
+ const errors = [];
2412
+ await Promise.all(
2413
+ urls.map(async (url) => {
2414
+ try {
2415
+ if (url.endsWith(".glb") || url.endsWith(".gltf")) {
2416
+ await loaderRef.current.loadModel(url);
2417
+ } else if (url.endsWith(".obj")) {
2418
+ await loaderRef.current.loadOBJ(url);
2419
+ } else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
2420
+ await loaderRef.current.loadTexture(url);
2421
+ }
2422
+ completed++;
2423
+ updateProgress(completed, urls.length);
2424
+ } catch (error) {
2425
+ const errorMsg = error instanceof Error ? error.message : String(error);
2426
+ errors.push(`${url}: ${errorMsg}`);
2427
+ completed++;
2428
+ updateProgress(completed, urls.length);
2429
+ }
2430
+ })
2431
+ );
2432
+ setState((prev) => ({
2433
+ ...prev,
2434
+ isLoading: false,
2435
+ errors
2436
+ }));
2437
+ },
2438
+ [updateProgress]
2439
+ );
2440
+ const hasModel = useCallback((url) => {
2441
+ return loaderRef.current.hasModel(url);
2442
+ }, []);
2443
+ const hasTexture = useCallback((url) => {
2444
+ return loaderRef.current.hasTexture(url);
2445
+ }, []);
2446
+ const getModel = useCallback((url) => {
2447
+ try {
2448
+ return loaderRef.current.getModel(url);
2449
+ } catch {
2450
+ return void 0;
2451
+ }
2452
+ }, []);
2453
+ const getTexture = useCallback((url) => {
2454
+ try {
2455
+ return loaderRef.current.getTexture(url);
2456
+ } catch {
2457
+ return void 0;
2458
+ }
2459
+ }, []);
2460
+ const clearCache = useCallback(() => {
2461
+ loaderRef.current.clearCache();
2462
+ setState({
2463
+ isLoading: false,
2464
+ progress: 0,
2465
+ loaded: 0,
2466
+ total: 0,
2467
+ errors: []
2468
+ });
2469
+ }, []);
2470
+ return {
2471
+ ...state,
2472
+ loadModel,
2473
+ loadOBJ,
2474
+ loadTexture,
2475
+ preload,
2476
+ hasModel,
2477
+ hasTexture,
2478
+ getModel,
2479
+ getTexture,
2480
+ clearCache
2481
+ };
2482
+ }
2483
+ function useSceneGraph() {
2484
+ const nodesRef = useRef(/* @__PURE__ */ new Map());
2485
+ const addNode = useCallback((node) => {
2486
+ const existing = nodesRef.current.get(node.id);
2487
+ if (existing) {
2488
+ existing.mesh.removeFromParent();
2489
+ }
2490
+ nodesRef.current.set(node.id, node);
2491
+ }, []);
2492
+ const removeNode = useCallback((id) => {
2493
+ const node = nodesRef.current.get(id);
2494
+ if (node) {
2495
+ node.mesh.removeFromParent();
2496
+ nodesRef.current.delete(id);
2497
+ }
2498
+ }, []);
2499
+ const getNode = useCallback((id) => {
2500
+ return nodesRef.current.get(id);
2501
+ }, []);
2502
+ const updateNodePosition = useCallback(
2503
+ (id, x, y, z) => {
2504
+ const node = nodesRef.current.get(id);
2505
+ if (node) {
2506
+ node.mesh.position.set(x, y, z);
2507
+ node.position = { x, y, z };
2508
+ }
2509
+ },
2510
+ []
2511
+ );
2512
+ const updateNodeGridPosition = useCallback(
2513
+ (id, gridX, gridZ) => {
2514
+ const node = nodesRef.current.get(id);
2515
+ if (node) {
2516
+ node.gridPosition = { x: gridX, z: gridZ };
2517
+ }
2518
+ },
2519
+ []
2520
+ );
2521
+ const getNodeAtGrid = useCallback(
2522
+ (x, z, type) => {
2523
+ return Array.from(nodesRef.current.values()).find((node) => {
2524
+ const matchesGrid = node.gridPosition.x === x && node.gridPosition.z === z;
2525
+ return type ? matchesGrid && node.type === type : matchesGrid;
2526
+ });
2527
+ },
2528
+ []
2529
+ );
2530
+ const getNodesByType = useCallback((type) => {
2531
+ return Array.from(nodesRef.current.values()).filter((node) => node.type === type);
2532
+ }, []);
2533
+ const getNodesInBounds = useCallback(
2534
+ (minX, maxX, minZ, maxZ) => {
2535
+ return Array.from(nodesRef.current.values()).filter((node) => {
2536
+ const { x, z } = node.gridPosition;
2537
+ return x >= minX && x <= maxX && z >= minZ && z <= maxZ;
2538
+ });
2539
+ },
2540
+ []
2541
+ );
2542
+ const clearNodes = useCallback(() => {
2543
+ nodesRef.current.forEach((node) => {
2544
+ node.mesh.removeFromParent();
2545
+ });
2546
+ nodesRef.current.clear();
2547
+ }, []);
2548
+ const countNodes = useCallback((type) => {
2549
+ if (!type) {
2550
+ return nodesRef.current.size;
2551
+ }
2552
+ return Array.from(nodesRef.current.values()).filter((node) => node.type === type).length;
2553
+ }, []);
2554
+ return {
2555
+ nodesRef,
2556
+ addNode,
2557
+ removeNode,
2558
+ getNode,
2559
+ updateNodePosition,
2560
+ updateNodeGridPosition,
2561
+ getNodeAtGrid,
2562
+ getNodesByType,
2563
+ getNodesInBounds,
2564
+ clearNodes,
2565
+ countNodes
2566
+ };
2567
+ }
2568
+ function useRaycaster(options) {
2569
+ const { camera, canvas, cellSize = 1, offsetX = 0, offsetZ = 0 } = options;
2570
+ const raycaster = useRef(new THREE5.Raycaster());
2571
+ const mouse = useRef(new THREE5.Vector2());
2572
+ const clientToNDC = useCallback(
2573
+ (clientX, clientY) => {
2574
+ if (!canvas) {
2575
+ return { x: 0, y: 0 };
2576
+ }
2577
+ const rect = canvas.getBoundingClientRect();
2578
+ return {
2579
+ x: (clientX - rect.left) / rect.width * 2 - 1,
2580
+ y: -((clientY - rect.top) / rect.height) * 2 + 1
2581
+ };
2582
+ },
2583
+ [canvas]
2584
+ );
2585
+ const isWithinCanvas = useCallback(
2586
+ (clientX, clientY) => {
2587
+ if (!canvas) return false;
2588
+ const rect = canvas.getBoundingClientRect();
2589
+ return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom;
2590
+ },
2591
+ [canvas]
2592
+ );
2593
+ const getIntersection = useCallback(
2594
+ (clientX, clientY, objects) => {
2595
+ if (!camera || !canvas) return null;
2596
+ const ndc = clientToNDC(clientX, clientY);
2597
+ mouse.current.set(ndc.x, ndc.y);
2598
+ raycaster.current.setFromCamera(mouse.current, camera);
2599
+ const intersects = raycaster.current.intersectObjects(objects, true);
2600
+ if (intersects.length > 0) {
2601
+ const hit = intersects[0];
2602
+ return {
2603
+ object: hit.object,
2604
+ point: hit.point,
2605
+ distance: hit.distance,
2606
+ uv: hit.uv,
2607
+ face: hit.face,
2608
+ faceIndex: hit.faceIndex,
2609
+ instanceId: hit.instanceId
2610
+ };
2611
+ }
2612
+ return null;
2613
+ },
2614
+ [camera, canvas, clientToNDC]
2615
+ );
2616
+ const getAllIntersections = useCallback(
2617
+ (clientX, clientY, objects) => {
2618
+ if (!camera || !canvas) return [];
2619
+ const ndc = clientToNDC(clientX, clientY);
2620
+ mouse.current.set(ndc.x, ndc.y);
2621
+ raycaster.current.setFromCamera(mouse.current, camera);
2622
+ const intersects = raycaster.current.intersectObjects(objects, true);
2623
+ return intersects.map((hit) => ({
2624
+ object: hit.object,
2625
+ point: hit.point,
2626
+ distance: hit.distance,
2627
+ uv: hit.uv,
2628
+ face: hit.face,
2629
+ faceIndex: hit.faceIndex,
2630
+ instanceId: hit.instanceId
2631
+ }));
2632
+ },
2633
+ [camera, canvas, clientToNDC]
2634
+ );
2635
+ const getGridCoordinates = useCallback(
2636
+ (clientX, clientY) => {
2637
+ if (!camera || !canvas) return null;
2638
+ const ndc = clientToNDC(clientX, clientY);
2639
+ mouse.current.set(ndc.x, ndc.y);
2640
+ raycaster.current.setFromCamera(mouse.current, camera);
2641
+ const plane = new THREE5.Plane(new THREE5.Vector3(0, 1, 0), 0);
2642
+ const target = new THREE5.Vector3();
2643
+ const intersection = raycaster.current.ray.intersectPlane(plane, target);
2644
+ if (intersection) {
2645
+ const gridX = Math.round((target.x - offsetX) / cellSize);
2646
+ const gridZ = Math.round((target.z - offsetZ) / cellSize);
2647
+ return { x: gridX, z: gridZ };
2648
+ }
2649
+ return null;
2650
+ },
2651
+ [camera, canvas, cellSize, offsetX, offsetZ, clientToNDC]
2652
+ );
2653
+ const getTileAtPosition = useCallback(
2654
+ (clientX, clientY, scene) => {
2655
+ if (!camera || !canvas) return null;
2656
+ const tileMeshes = [];
2657
+ scene.traverse((obj) => {
2658
+ if (obj.userData.type === "tile" || obj.userData.isTile) {
2659
+ tileMeshes.push(obj);
2660
+ }
2661
+ });
2662
+ const hit = getIntersection(clientX, clientY, tileMeshes);
2663
+ if (hit) {
2664
+ const gridCoords2 = getGridCoordinates(clientX, clientY);
2665
+ if (gridCoords2) {
2666
+ return {
2667
+ gridX: gridCoords2.x,
2668
+ gridZ: gridCoords2.z,
2669
+ worldPosition: hit.point,
2670
+ objectType: hit.object.userData.type || "tile",
2671
+ objectId: hit.object.userData.id || hit.object.userData.tileId
2672
+ };
2673
+ }
2674
+ }
2675
+ const gridCoords = getGridCoordinates(clientX, clientY);
2676
+ if (gridCoords) {
2677
+ return {
2678
+ gridX: gridCoords.x,
2679
+ gridZ: gridCoords.z,
2680
+ worldPosition: new THREE5.Vector3(
2681
+ gridCoords.x * cellSize + offsetX,
2682
+ 0,
2683
+ gridCoords.z * cellSize + offsetZ
2684
+ )
2685
+ };
2686
+ }
2687
+ return null;
2688
+ },
2689
+ [camera, canvas, getIntersection, getGridCoordinates, cellSize, offsetX, offsetZ]
2690
+ );
2691
+ return {
2692
+ raycaster,
2693
+ mouse,
2694
+ getIntersection,
2695
+ getAllIntersections,
2696
+ getGridCoordinates,
2697
+ getTileAtPosition,
2698
+ clientToNDC,
2699
+ isWithinCanvas
2700
+ };
2701
+ }
2702
+ var DEFAULT_CONFIG = {
2703
+ cellSize: 1,
2704
+ offsetX: 0,
2705
+ offsetZ: 0,
2706
+ elevation: 0
2707
+ };
2708
+ function gridToWorld(gridX, gridZ, config = DEFAULT_CONFIG) {
2709
+ const opts = { ...DEFAULT_CONFIG, ...config };
2710
+ return new THREE5.Vector3(
2711
+ gridX * opts.cellSize + opts.offsetX,
2712
+ opts.elevation,
2713
+ gridZ * opts.cellSize + opts.offsetZ
2714
+ );
2715
+ }
2716
+ function worldToGrid(worldX, worldZ, config = DEFAULT_CONFIG) {
2717
+ const opts = { ...DEFAULT_CONFIG, ...config };
2718
+ return {
2719
+ x: Math.round((worldX - opts.offsetX) / opts.cellSize),
2720
+ z: Math.round((worldZ - opts.offsetZ) / opts.cellSize)
2721
+ };
2722
+ }
2723
+ function raycastToPlane(camera, mouseX, mouseY, planeY = 0) {
2724
+ const raycaster = new THREE5.Raycaster();
2725
+ const mouse = new THREE5.Vector2(mouseX, mouseY);
2726
+ raycaster.setFromCamera(mouse, camera);
2727
+ const plane = new THREE5.Plane(new THREE5.Vector3(0, 1, 0), -planeY);
2728
+ const target = new THREE5.Vector3();
2729
+ const intersection = raycaster.ray.intersectPlane(plane, target);
2730
+ return intersection ? target : null;
2731
+ }
2732
+ function raycastToObjects(camera, mouseX, mouseY, objects) {
2733
+ const raycaster = new THREE5.Raycaster();
2734
+ const mouse = new THREE5.Vector2(mouseX, mouseY);
2735
+ raycaster.setFromCamera(mouse, camera);
2736
+ const intersects = raycaster.intersectObjects(objects, true);
2737
+ return intersects.length > 0 ? intersects[0] : null;
2738
+ }
2739
+ function gridDistance(a, b) {
2740
+ const dx = b.x - a.x;
2741
+ const dz = b.z - a.z;
2742
+ return Math.sqrt(dx * dx + dz * dz);
2743
+ }
2744
+ function gridManhattanDistance(a, b) {
2745
+ return Math.abs(b.x - a.x) + Math.abs(b.z - a.z);
2746
+ }
2747
+ function getNeighbors(x, z, includeDiagonal = false) {
2748
+ const neighbors = [
2749
+ { x: x + 1, z },
2750
+ { x: x - 1, z },
2751
+ { x, z: z + 1 },
2752
+ { x, z: z - 1 }
2753
+ ];
2754
+ if (includeDiagonal) {
2755
+ neighbors.push(
2756
+ { x: x + 1, z: z + 1 },
2757
+ { x: x - 1, z: z - 1 },
2758
+ { x: x + 1, z: z - 1 },
2759
+ { x: x - 1, z: z + 1 }
2760
+ );
2761
+ }
2762
+ return neighbors;
2763
+ }
2764
+ function isInBounds(x, z, bounds) {
2765
+ return x >= bounds.minX && x <= bounds.maxX && z >= bounds.minZ && z <= bounds.maxZ;
2766
+ }
2767
+ function getCellsInRadius(centerX, centerZ, radius) {
2768
+ const cells = [];
2769
+ const radiusSquared = radius * radius;
2770
+ const minX = Math.floor(centerX - radius);
2771
+ const maxX = Math.ceil(centerX + radius);
2772
+ const minZ = Math.floor(centerZ - radius);
2773
+ const maxZ = Math.ceil(centerZ + radius);
2774
+ for (let x = minX; x <= maxX; x++) {
2775
+ for (let z = minZ; z <= maxZ; z++) {
2776
+ const dx = x - centerX;
2777
+ const dz = z - centerZ;
2778
+ if (dx * dx + dz * dz <= radiusSquared) {
2779
+ cells.push({ x, z });
2780
+ }
2781
+ }
2782
+ }
2783
+ return cells;
2784
+ }
2785
+ function createGridHighlight(color = 16776960, opacity = 0.3) {
2786
+ const geometry = new THREE5.PlaneGeometry(0.95, 0.95);
2787
+ const material = new THREE5.MeshBasicMaterial({
2788
+ color,
2789
+ transparent: true,
2790
+ opacity,
2791
+ side: THREE5.DoubleSide
2792
+ });
2793
+ const mesh = new THREE5.Mesh(geometry, material);
2794
+ mesh.rotation.x = -Math.PI / 2;
2795
+ mesh.position.y = 0.01;
2796
+ return mesh;
2797
+ }
2798
+ function normalizeMouseCoordinates(clientX, clientY, element) {
2799
+ const rect = element.getBoundingClientRect();
2800
+ return {
2801
+ x: (clientX - rect.left) / rect.width * 2 - 1,
2802
+ y: -((clientY - rect.top) / rect.height) * 2 + 1
2803
+ };
2804
+ }
2805
+ function isInFrustum(position, camera, padding = 0) {
2806
+ const frustum = new THREE5.Frustum();
2807
+ const projScreenMatrix = new THREE5.Matrix4();
2808
+ projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
2809
+ frustum.setFromProjectionMatrix(projScreenMatrix);
2810
+ const sphere = new THREE5.Sphere(position, padding);
2811
+ return frustum.intersectsSphere(sphere);
2812
+ }
2813
+ function filterByFrustum(positions, camera, padding = 0) {
2814
+ const frustum = new THREE5.Frustum();
2815
+ const projScreenMatrix = new THREE5.Matrix4();
2816
+ projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
2817
+ frustum.setFromProjectionMatrix(projScreenMatrix);
2818
+ return positions.filter((position) => {
2819
+ const sphere = new THREE5.Sphere(position, padding);
2820
+ return frustum.intersectsSphere(sphere);
2821
+ });
2822
+ }
2823
+ function getVisibleIndices(positions, camera, padding = 0) {
2824
+ const frustum = new THREE5.Frustum();
2825
+ const projScreenMatrix = new THREE5.Matrix4();
2826
+ const visible = /* @__PURE__ */ new Set();
2827
+ projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
2828
+ frustum.setFromProjectionMatrix(projScreenMatrix);
2829
+ positions.forEach((position, index) => {
2830
+ const sphere = new THREE5.Sphere(position, padding);
2831
+ if (frustum.intersectsSphere(sphere)) {
2832
+ visible.add(index);
2833
+ }
2834
+ });
2835
+ return visible;
2836
+ }
2837
+ function calculateLODLevel(position, camera, lodLevels) {
2838
+ const distance = position.distanceTo(camera.position);
2839
+ for (let i = 0; i < lodLevels.length; i++) {
2840
+ if (distance < lodLevels[i]) {
2841
+ return i;
2842
+ }
2843
+ }
2844
+ return lodLevels.length;
2845
+ }
2846
+ function updateInstanceLOD(instancedMesh, positions, camera, lodDistances) {
2847
+ const lodIndices = new Uint8Array(positions.length);
2848
+ positions.forEach((position, index) => {
2849
+ lodIndices[index] = calculateLODLevel(position, camera, lodDistances);
2850
+ });
2851
+ return lodIndices;
2852
+ }
2853
+ function cullInstancedMesh(instancedMesh, positions, visibleIndices) {
2854
+ const dummy = new THREE5.Object3D();
2855
+ let visibleCount = 0;
2856
+ positions.forEach((position, index) => {
2857
+ if (visibleIndices.has(index)) {
2858
+ dummy.position.copy(position);
2859
+ dummy.updateMatrix();
2860
+ instancedMesh.setMatrixAt(visibleCount, dummy.matrix);
2861
+ visibleCount++;
2862
+ }
2863
+ });
2864
+ instancedMesh.count = visibleCount;
2865
+ instancedMesh.instanceMatrix.needsUpdate = true;
2866
+ return visibleCount;
2867
+ }
2868
+ var SpatialHashGrid = class {
2869
+ cellSize;
2870
+ cells;
2871
+ objectPositions;
2872
+ constructor(cellSize = 10) {
2873
+ this.cellSize = cellSize;
2874
+ this.cells = /* @__PURE__ */ new Map();
2875
+ this.objectPositions = /* @__PURE__ */ new Map();
2876
+ }
2877
+ /**
2878
+ * Get cell key for a position
2879
+ */
2880
+ getCellKey(x, z) {
2881
+ const cellX = Math.floor(x / this.cellSize);
2882
+ const cellZ = Math.floor(z / this.cellSize);
2883
+ return `${cellX},${cellZ}`;
2884
+ }
2885
+ /**
2886
+ * Insert an object into the grid
2887
+ */
2888
+ insert(id, position) {
2889
+ const key = this.getCellKey(position.x, position.z);
2890
+ if (!this.cells.has(key)) {
2891
+ this.cells.set(key, /* @__PURE__ */ new Set());
2892
+ }
2893
+ this.cells.get(key).add(id);
2894
+ this.objectPositions.set(id, position.clone());
2895
+ }
2896
+ /**
2897
+ * Remove an object from the grid
2898
+ */
2899
+ remove(id) {
2900
+ const position = this.objectPositions.get(id);
2901
+ if (position) {
2902
+ const key = this.getCellKey(position.x, position.z);
2903
+ this.cells.get(key)?.delete(id);
2904
+ this.objectPositions.delete(id);
2905
+ }
2906
+ }
2907
+ /**
2908
+ * Update an object's position
2909
+ */
2910
+ update(id, newPosition) {
2911
+ this.remove(id);
2912
+ this.insert(id, newPosition);
2913
+ }
2914
+ /**
2915
+ * Query objects within a radius of a position
2916
+ */
2917
+ queryRadius(center, radius) {
2918
+ const results = [];
2919
+ const radiusSquared = radius * radius;
2920
+ const minCellX = Math.floor((center.x - radius) / this.cellSize);
2921
+ const maxCellX = Math.floor((center.x + radius) / this.cellSize);
2922
+ const minCellZ = Math.floor((center.z - radius) / this.cellSize);
2923
+ const maxCellZ = Math.floor((center.z + radius) / this.cellSize);
2924
+ for (let x = minCellX; x <= maxCellX; x++) {
2925
+ for (let z = minCellZ; z <= maxCellZ; z++) {
2926
+ const key = `${x},${z}`;
2927
+ const cell = this.cells.get(key);
2928
+ if (cell) {
2929
+ cell.forEach((id) => {
2930
+ const position = this.objectPositions.get(id);
2931
+ if (position) {
2932
+ const dx = position.x - center.x;
2933
+ const dz = position.z - center.z;
2934
+ if (dx * dx + dz * dz <= radiusSquared) {
2935
+ results.push(id);
2936
+ }
2937
+ }
2938
+ });
2939
+ }
2940
+ }
2941
+ }
2942
+ return results;
2943
+ }
2944
+ /**
2945
+ * Query objects within a bounding box
2946
+ */
2947
+ queryBox(minX, maxX, minZ, maxZ) {
2948
+ const results = [];
2949
+ const minCellX = Math.floor(minX / this.cellSize);
2950
+ const maxCellX = Math.floor(maxX / this.cellSize);
2951
+ const minCellZ = Math.floor(minZ / this.cellSize);
2952
+ const maxCellZ = Math.floor(maxZ / this.cellSize);
2953
+ for (let x = minCellX; x <= maxCellX; x++) {
2954
+ for (let z = minCellZ; z <= maxCellZ; z++) {
2955
+ const key = `${x},${z}`;
2956
+ const cell = this.cells.get(key);
2957
+ if (cell) {
2958
+ cell.forEach((id) => {
2959
+ const position = this.objectPositions.get(id);
2960
+ if (position && position.x >= minX && position.x <= maxX && position.z >= minZ && position.z <= maxZ) {
2961
+ results.push(id);
2962
+ }
2963
+ });
2964
+ }
2965
+ }
2966
+ }
2967
+ return results;
2968
+ }
2969
+ /**
2970
+ * Clear all objects from the grid
2971
+ */
2972
+ clear() {
2973
+ this.cells.clear();
2974
+ this.objectPositions.clear();
2975
+ }
2976
+ /**
2977
+ * Get statistics about the grid
2978
+ */
2979
+ getStats() {
2980
+ return {
2981
+ objects: this.objectPositions.size,
2982
+ cells: this.cells.size
2983
+ };
2984
+ }
2985
+ };
2986
+ var Avl3DApplicationScene = ({
2987
+ data,
2988
+ onOrbitalClick
2989
+ }) => {
2990
+ const groupRef = useRef(null);
2991
+ const [, setHoverTrigger] = useState(0);
2992
+ const orbitalPositions = useMemo(() => {
2993
+ const baseRadius = Math.max(4, data.orbitals.length * 1.5);
2994
+ if (data.crossLinks.length === 0) {
2995
+ return goldenSpiralPositions(data.orbitals.length, baseRadius);
2996
+ }
2997
+ const nameToIdx = /* @__PURE__ */ new Map();
2998
+ data.orbitals.forEach((o, i) => nameToIdx.set(o.name, i));
2999
+ const edges = data.crossLinks.map((link) => ({
3000
+ from: nameToIdx.get(link.emitterOrbital) ?? -1,
3001
+ to: nameToIdx.get(link.listenerOrbital) ?? -1
3002
+ })).filter((e) => e.from >= 0 && e.to >= 0);
3003
+ return forceDirectedPositions(data.orbitals.length, edges, baseRadius);
3004
+ }, [data.orbitals, data.crossLinks]);
3005
+ const positionByName = useMemo(() => {
3006
+ const map = /* @__PURE__ */ new Map();
3007
+ data.orbitals.forEach((orb, i) => {
3008
+ const pos = orbitalPositions[i];
3009
+ map.set(orb.name, [pos.x, pos.y, pos.z]);
3010
+ });
3011
+ return map;
3012
+ }, [data.orbitals, orbitalPositions]);
3013
+ useFrame((_, delta) => {
3014
+ if (!groupRef.current) return;
3015
+ groupRef.current.rotation.y += delta * 0.03;
3016
+ });
3017
+ return /* @__PURE__ */ jsxs("group", { ref: groupRef, children: [
3018
+ data.orbitals.map((orbital, i) => {
3019
+ const pos = orbitalPositions[i];
3020
+ return /* @__PURE__ */ jsx(
3021
+ Avl3DOrbitalNode,
3022
+ {
3023
+ name: orbital.name,
3024
+ entityName: orbital.entityName,
3025
+ traitCount: orbital.traitNames.length,
3026
+ pageCount: orbital.pageNames.length,
3027
+ persistence: orbital.persistence,
3028
+ position: [pos.x, pos.y, pos.z],
3029
+ onClick: () => {
3030
+ onOrbitalClick?.(orbital.name);
3031
+ setHoverTrigger((v) => v + 1);
3032
+ }
3033
+ },
3034
+ orbital.name
3035
+ );
3036
+ }),
3037
+ data.crossLinks.map((link, i) => {
3038
+ const fromPos = positionByName.get(link.emitterOrbital);
3039
+ const toPos = positionByName.get(link.listenerOrbital);
3040
+ if (!fromPos || !toPos) return null;
3041
+ return /* @__PURE__ */ jsx(
3042
+ Avl3DCrossWire,
3043
+ {
3044
+ from: fromPos,
3045
+ to: toPos,
3046
+ eventName: link.eventName
3047
+ },
3048
+ `${link.emitterOrbital}-${link.eventName}-${link.listenerOrbital}-${i}`
3049
+ );
3050
+ })
3051
+ ] });
3052
+ };
3053
+ Avl3DApplicationScene.displayName = "Avl3DApplicationScene";
3054
+ function TraitOrbit({
3055
+ name,
3056
+ radius,
3057
+ tilt,
3058
+ speed,
3059
+ highlighted,
3060
+ dimmed,
3061
+ stateCount = 0,
3062
+ eventCount = 0,
3063
+ transitionCount = 0,
3064
+ onClick,
3065
+ onHover
3066
+ }) {
3067
+ const groupRef = useRef(null);
3068
+ const angleRef = useRef(Math.random() * Math.PI * 2);
3069
+ const palette = useAvl3DPalette();
3070
+ const { t } = useTranslate();
3071
+ useFrame((_, delta) => {
3072
+ angleRef.current += delta * speed;
3073
+ if (!groupRef.current) return;
3074
+ const x = radius * Math.cos(angleRef.current);
3075
+ const flatZ = radius * Math.sin(angleRef.current);
3076
+ const y = flatZ * Math.sin(tilt);
3077
+ const z = flatZ * Math.cos(tilt);
3078
+ groupRef.current.position.set(x, y, z);
3079
+ });
3080
+ if (!palette) return null;
3081
+ const opacity = dimmed ? 0.2 : 0.5;
3082
+ const emissiveIntensity = highlighted ? 0.8 : 0.4;
3083
+ return /* @__PURE__ */ jsxs("group", { children: [
3084
+ /* @__PURE__ */ jsxs("mesh", { rotation: [tilt, 0, 0], children: [
3085
+ /* @__PURE__ */ jsx("torusGeometry", { args: [radius, 0.015, 8, 64] }),
3086
+ /* @__PURE__ */ jsx(
3087
+ "meshStandardMaterial",
3088
+ {
3089
+ color: palette.traitOrbit,
3090
+ emissive: palette.traitOrbit,
3091
+ emissiveIntensity,
3092
+ transparent: true,
3093
+ opacity
3094
+ }
3095
+ )
3096
+ ] }),
3097
+ /* @__PURE__ */ jsxs("group", { ref: groupRef, children: [
3098
+ /* @__PURE__ */ jsxs(
3099
+ "mesh",
3100
+ {
3101
+ onClick: (e) => {
3102
+ e.stopPropagation();
3103
+ onClick?.();
3104
+ },
3105
+ onPointerOver: (e) => {
3106
+ e.stopPropagation();
3107
+ onHover?.(true);
3108
+ document.body.style.cursor = "pointer";
3109
+ },
3110
+ onPointerOut: () => {
3111
+ onHover?.(false);
3112
+ document.body.style.cursor = "auto";
3113
+ },
3114
+ scale: highlighted ? 1.3 : 1,
3115
+ children: [
3116
+ /* @__PURE__ */ jsx("sphereGeometry", { args: [0.25, 16, 16] }),
3117
+ /* @__PURE__ */ jsx(
3118
+ "meshStandardMaterial",
3119
+ {
3120
+ color: palette.traitOrbit,
3121
+ emissive: palette.traitOrbit,
3122
+ emissiveIntensity: highlighted ? 1.2 : 0.6,
3123
+ transparent: true,
3124
+ opacity: dimmed ? 0.3 : 0.9
3125
+ }
3126
+ )
3127
+ ]
3128
+ }
3129
+ ),
3130
+ /* @__PURE__ */ jsx(
3131
+ Avl3DLabel,
3132
+ {
3133
+ position: [0, 0.5, 0],
3134
+ text: name,
3135
+ color: dimmed ? AVL_INK.quiet : AVL_INK.text,
3136
+ fontSize: highlighted ? 13 : 11
3137
+ }
3138
+ ),
3139
+ highlighted && /* @__PURE__ */ jsx(
3140
+ Avl3DTooltip,
3141
+ {
3142
+ position: [0.8, 0.8, 0],
3143
+ title: name,
3144
+ accentColor: palette.traitOrbitHighlight,
3145
+ rows: [
3146
+ { label: t("avl3d.states"), value: String(stateCount) },
3147
+ { label: t("avl3d.events"), value: String(eventCount) },
3148
+ { label: t("avl3d.transitions"), value: String(transitionCount) }
3149
+ ]
3150
+ }
3151
+ )
3152
+ ] })
3153
+ ] });
3154
+ }
3155
+ function PagePortal({ name, position }) {
3156
+ const palette = useAvl3DPalette();
3157
+ if (!palette) return null;
3158
+ return /* @__PURE__ */ jsxs("group", { position, children: [
3159
+ /* @__PURE__ */ jsxs("mesh", { rotation: [Math.PI / 2, 0, 0], children: [
3160
+ /* @__PURE__ */ jsx("cylinderGeometry", { args: [0.35, 0.35, 0.12, 6] }),
3161
+ /* @__PURE__ */ jsx(
3162
+ "meshStandardMaterial",
3163
+ {
3164
+ color: palette.pagePortal,
3165
+ emissive: palette.pagePortal,
3166
+ emissiveIntensity: 0.5,
3167
+ transparent: true,
3168
+ opacity: 0.7,
3169
+ roughness: 0.3
3170
+ }
3171
+ )
3172
+ ] }),
3173
+ /* @__PURE__ */ jsxs("mesh", { rotation: [Math.PI / 2, 0, 0], children: [
3174
+ /* @__PURE__ */ jsx("cylinderGeometry", { args: [0.25, 0.25, 0.14, 6] }),
3175
+ /* @__PURE__ */ jsx(
3176
+ "meshStandardMaterial",
3177
+ {
3178
+ color: palette.pagePortal,
3179
+ emissive: palette.pagePortal,
3180
+ emissiveIntensity: 0.8,
3181
+ transparent: true,
3182
+ opacity: 0.2,
3183
+ depthWrite: false
3184
+ }
3185
+ )
3186
+ ] }),
3187
+ /* @__PURE__ */ jsx(
3188
+ Avl3DLabel,
3189
+ {
3190
+ position: [0, -0.5, 0],
3191
+ text: name,
3192
+ color: palette.pagePortal,
3193
+ fontSize: 9
3194
+ }
3195
+ )
3196
+ ] });
3197
+ }
3198
+ var Avl3DOrbitalScene = ({
3199
+ data,
3200
+ onTraitClick,
3201
+ highlightedTrait,
3202
+ onTraitHighlight
3203
+ }) => {
3204
+ const [localHighlight, setLocalHighlight] = useState(null);
3205
+ const activeHighlight = highlightedTrait ?? localHighlight;
3206
+ const traitOrbits = useMemo(() => {
3207
+ return data.traits.map((trait, i) => ({
3208
+ name: trait.name,
3209
+ radius: 3 + i * 1.2,
3210
+ tilt: i * Math.PI / (data.traits.length + 1) - Math.PI / 4,
3211
+ speed: 0.3 - i * 0.04,
3212
+ stateCount: trait.stateCount,
3213
+ eventCount: trait.eventCount,
3214
+ transitionCount: trait.transitionCount
3215
+ }));
3216
+ }, [data.traits]);
3217
+ const pagePositions = useMemo(() => {
3218
+ const outerRadius = 3 + data.traits.length * 1.2 + 1.5;
3219
+ return data.pages.map((_, i) => {
3220
+ const angle = 2 * Math.PI * i / data.pages.length - Math.PI / 2;
3221
+ return [
3222
+ outerRadius * Math.cos(angle),
3223
+ 0,
3224
+ outerRadius * Math.sin(angle)
3225
+ ];
3226
+ });
3227
+ }, [data.pages, data.traits.length]);
3228
+ return /* @__PURE__ */ jsxs("group", { children: [
3229
+ /* @__PURE__ */ jsx(
3230
+ Avl3DEntityCore,
3231
+ {
3232
+ name: data.entity.name,
3233
+ fieldCount: data.entity.fields.length,
3234
+ persistence: data.entity.persistence,
3235
+ position: [0, 0, 0],
3236
+ fields: data.entity.fields.map((f) => f.type)
3237
+ }
3238
+ ),
3239
+ traitOrbits.map((orbit) => /* @__PURE__ */ jsx(
3240
+ TraitOrbit,
3241
+ {
3242
+ name: orbit.name,
3243
+ radius: orbit.radius,
3244
+ tilt: orbit.tilt,
3245
+ speed: orbit.speed,
3246
+ highlighted: activeHighlight === orbit.name,
3247
+ dimmed: activeHighlight !== null && activeHighlight !== orbit.name,
3248
+ stateCount: orbit.stateCount,
3249
+ eventCount: orbit.eventCount,
3250
+ transitionCount: orbit.transitionCount,
3251
+ onClick: () => onTraitClick?.(orbit.name),
3252
+ onHover: (h) => {
3253
+ const value = h ? orbit.name : null;
3254
+ onTraitHighlight?.(value);
3255
+ setLocalHighlight(value);
3256
+ }
3257
+ },
3258
+ orbit.name
3259
+ )),
3260
+ data.pages.map((page, i) => /* @__PURE__ */ jsx(
3261
+ PagePortal,
3262
+ {
3263
+ name: page.name,
3264
+ position: pagePositions[i]
3265
+ },
3266
+ page.name
3267
+ ))
3268
+ ] });
3269
+ };
3270
+ Avl3DOrbitalScene.displayName = "Avl3DOrbitalScene";
3271
+ var Avl3DTraitScene = ({
3272
+ data,
3273
+ onTransitionClick
3274
+ }) => {
3275
+ const statePositions = useMemo(() => {
3276
+ const radius = Math.max(3, data.states.length * 0.8);
3277
+ const positions = fibonacciSpherePositions(data.states.length, radius);
3278
+ const posMap = /* @__PURE__ */ new Map();
3279
+ data.states.forEach((state, i) => {
3280
+ const pos = positions[i];
3281
+ posMap.set(state.name, [pos.x, pos.y, pos.z]);
3282
+ });
3283
+ return posMap;
3284
+ }, [data.states]);
3285
+ const transitionIndices = useMemo(() => {
3286
+ const pairCount = /* @__PURE__ */ new Map();
3287
+ return data.transitions.map((t) => {
3288
+ const key = `${t.from}->${t.to}`;
3289
+ const idx = pairCount.get(key) ?? 0;
3290
+ pairCount.set(key, idx + 1);
3291
+ return idx;
3292
+ });
3293
+ }, [data.transitions]);
3294
+ const transitionCounts = useMemo(() => {
3295
+ const incoming = /* @__PURE__ */ new Map();
3296
+ const outgoing = /* @__PURE__ */ new Map();
3297
+ for (const t of data.transitions) {
3298
+ outgoing.set(t.from, (outgoing.get(t.from) ?? 0) + 1);
3299
+ incoming.set(t.to, (incoming.get(t.to) ?? 0) + 1);
3300
+ }
3301
+ return { incoming, outgoing };
3302
+ }, [data.transitions]);
3303
+ return /* @__PURE__ */ jsxs("group", { children: [
3304
+ /* @__PURE__ */ jsx(
3305
+ Avl3DLabel,
3306
+ {
3307
+ position: [0, 5, 0],
3308
+ text: `${data.name} (${data.linkedEntity})`,
3309
+ color: AVL_INK.text,
3310
+ fontSize: 14
3311
+ }
3312
+ ),
3313
+ data.states.map((state) => {
3314
+ const pos = statePositions.get(state.name);
3315
+ if (!pos) return null;
3316
+ return /* @__PURE__ */ jsx(
3317
+ Avl3DStateNode,
3318
+ {
3319
+ name: state.name,
3320
+ isInitial: state.isInitial ?? void 0,
3321
+ isTerminal: state.isTerminal ?? void 0,
3322
+ position: pos,
3323
+ incomingCount: transitionCounts.incoming.get(state.name) ?? 0,
3324
+ outgoingCount: transitionCounts.outgoing.get(state.name) ?? 0
3325
+ },
3326
+ state.name
3327
+ );
3328
+ }),
3329
+ data.transitions.map((transition, i) => {
3330
+ const fromPos = statePositions.get(transition.from);
3331
+ const toPos = statePositions.get(transition.to);
3332
+ if (!fromPos || !toPos) return null;
3333
+ const isSelf = transition.from === transition.to;
3334
+ return /* @__PURE__ */ jsx(
3335
+ Avl3DTransitionArc,
3336
+ {
3337
+ from: fromPos,
3338
+ to: toPos,
3339
+ event: transition.event,
3340
+ hasGuard: !!transition.guard,
3341
+ effectCount: transition.effects?.length ?? 0,
3342
+ effectTypes: transition.effects?.map((e) => {
3343
+ if (Array.isArray(e)) return String(e[0]);
3344
+ return typeof e === "string" ? e : "unknown";
3345
+ }),
3346
+ index: transitionIndices[i],
3347
+ isSelf,
3348
+ fromState: transition.from,
3349
+ toState: transition.to,
3350
+ onClick: () => onTransitionClick?.(i)
3351
+ },
3352
+ `${transition.from}-${transition.event}-${transition.to}-${i}`
3353
+ );
3354
+ })
3355
+ ] });
3356
+ };
3357
+ Avl3DTraitScene.displayName = "Avl3DTraitScene";
3358
+ var Avl3DTransitionScene = ({
3359
+ data
3360
+ }) => {
3361
+ const palette = useAvl3DPalette();
3362
+ const { t, locale } = useTranslate();
3363
+ if (!palette) return null;
3364
+ return /* @__PURE__ */ jsxs("group", { children: [
3365
+ /* @__PURE__ */ jsx(
3366
+ Avl3DStateNode,
3367
+ {
3368
+ name: data.from,
3369
+ position: [0, 4, 0]
3370
+ }
3371
+ ),
3372
+ /* @__PURE__ */ jsxs("group", { position: [0, 2.2, 0], children: [
3373
+ /* @__PURE__ */ jsxs("mesh", { children: [
3374
+ /* @__PURE__ */ jsx("boxGeometry", { args: [2, 0.6, 0.4] }),
3375
+ /* @__PURE__ */ jsx(
3376
+ "meshStandardMaterial",
3377
+ {
3378
+ color: palette.transitionArc,
3379
+ emissive: palette.transitionArc,
3380
+ emissiveIntensity: 0.5,
3381
+ roughness: 0.3
3382
+ }
3383
+ )
3384
+ ] }),
3385
+ /* @__PURE__ */ jsx(
3386
+ Avl3DLabel,
3387
+ {
3388
+ position: [0, 0, 0.3],
3389
+ text: data.event,
3390
+ color: AVL_INK.text,
3391
+ fontSize: 13
3392
+ }
3393
+ )
3394
+ ] }),
3395
+ /* @__PURE__ */ jsxs("mesh", { position: [0, 3.1, 0], children: [
3396
+ /* @__PURE__ */ jsx("cylinderGeometry", { args: [0.02, 0.02, 1.2, 6] }),
3397
+ /* @__PURE__ */ jsx("meshStandardMaterial", { color: palette.transitionArc, transparent: true, opacity: 0.5 })
3398
+ ] }),
3399
+ data.guard && /* @__PURE__ */ jsxs("group", { position: [0, 1, 0], children: [
3400
+ /* @__PURE__ */ jsxs("mesh", { rotation: [0, 0, Math.PI / 4], children: [
3401
+ /* @__PURE__ */ jsx("boxGeometry", { args: [0.5, 0.5, 0.3] }),
3402
+ /* @__PURE__ */ jsx(
3403
+ "meshStandardMaterial",
3404
+ {
3405
+ color: palette.guardPass,
3406
+ emissive: palette.guardPass,
3407
+ emissiveIntensity: 0.6
3408
+ }
3409
+ )
3410
+ ] }),
3411
+ /* @__PURE__ */ jsxs("mesh", { position: [0, 0.7, 0], children: [
3412
+ /* @__PURE__ */ jsx("cylinderGeometry", { args: [0.02, 0.02, 0.8, 6] }),
3413
+ /* @__PURE__ */ jsx("meshStandardMaterial", { color: palette.transitionArc, transparent: true, opacity: 0.5 })
3414
+ ] }),
3415
+ /* @__PURE__ */ jsx(
3416
+ Avl3DExprTree,
3417
+ {
3418
+ expression: data.guard,
3419
+ position: [3, 0, 0]
3420
+ }
3421
+ ),
3422
+ /* @__PURE__ */ jsx(
3423
+ Avl3DLabel,
3424
+ {
3425
+ position: [1.5, 0.3, 0],
3426
+ text: t("avl.guard").toLocaleLowerCase(locale),
3427
+ color: palette.guardPass,
3428
+ fontSize: 9
3429
+ }
3430
+ )
3431
+ ] }),
3432
+ data.effects.length > 0 && /* @__PURE__ */ jsxs("group", { position: [0, data.guard ? -0.5 : 0.5, 0], children: [
3433
+ /* @__PURE__ */ jsx(
3434
+ Avl3DLabel,
3435
+ {
3436
+ position: [0, 0.4, 0],
3437
+ text: t(data.effects.length === 1 ? "avl3d.effectCountOne" : "avl3d.effectCountOther", { count: data.effects.length }),
3438
+ color: AVL_INK.caution,
3439
+ fontSize: 10
3440
+ }
3441
+ ),
3442
+ data.effects.map((effect, i) => {
3443
+ const xOffset = (i - (data.effects.length - 1) / 2) * 3;
3444
+ return /* @__PURE__ */ jsx(
3445
+ Avl3DExprTree,
3446
+ {
3447
+ expression: effect,
3448
+ position: [xOffset, -1, 0]
3449
+ },
3450
+ i
3451
+ );
3452
+ })
3453
+ ] }),
3454
+ /* @__PURE__ */ jsxs("mesh", { position: [0, data.guard ? -2.5 : -1.5, 0], children: [
3455
+ /* @__PURE__ */ jsx("cylinderGeometry", { args: [0.02, 0.02, 1.2, 6] }),
3456
+ /* @__PURE__ */ jsx("meshStandardMaterial", { color: palette.transitionArc, transparent: true, opacity: 0.5 })
3457
+ ] }),
3458
+ /* @__PURE__ */ jsx(
3459
+ Avl3DStateNode,
3460
+ {
3461
+ name: data.to,
3462
+ position: [0, data.guard ? -3.5 : -2.5, 0]
3463
+ }
3464
+ )
3465
+ ] });
3466
+ };
3467
+ Avl3DTransitionScene.displayName = "Avl3DTransitionScene";
3468
+ var BLOOM_CONFIG = {
3469
+ application: { intensity: 0.4, luminanceThreshold: 0.88, luminanceSmoothing: 0.95 },
3470
+ orbital: { intensity: 0.5, luminanceThreshold: 0.85, luminanceSmoothing: 0.9 },
3471
+ trait: { intensity: 0.3, luminanceThreshold: 0.92, luminanceSmoothing: 0.9 },
3472
+ transition: { intensity: 0.35, luminanceThreshold: 0.9, luminanceSmoothing: 0.9 }
3473
+ };
3474
+ var DOF_CONFIG = {
3475
+ application: null,
3476
+ orbital: { focusDistance: 0.02, focalLength: 0.02, bokehScale: 1.5 },
3477
+ trait: null,
3478
+ transition: null
3479
+ };
3480
+ var SPARKLE_CONFIGS = {
3481
+ application: [
3482
+ { count: 60, size: 1.2, speed: 0.2, opacity: 0.35, scale: 35, colorKey: "sparkle" },
3483
+ { count: 15, size: 1.8, speed: 0.1, opacity: 0.2, scale: 25, colorKey: "sparkleWarm" }
3484
+ ],
3485
+ orbital: [
3486
+ { count: 30, size: 0.8, speed: 0.15, opacity: 0.25, scale: 18, colorKey: "sparkle" },
3487
+ { count: 8, size: 1.2, speed: 0.08, opacity: 0.15, scale: 12, colorKey: "sparkleWarm" }
3488
+ ],
3489
+ trait: [
3490
+ { count: 15, size: 0.6, speed: 0.1, opacity: 0.2, scale: 12, colorKey: "sparkle" }
3491
+ ],
3492
+ transition: [
3493
+ { count: 8, size: 0.4, speed: 0.08, opacity: 0.15, scale: 8, colorKey: "sparkle" }
3494
+ ]
3495
+ };
3496
+ var STARS_CONFIG = {
3497
+ application: { radius: 80, count: 3e3, factor: 5, fade: true },
3498
+ orbital: { radius: 60, count: 1500, factor: 4, fade: true },
3499
+ trait: { radius: 40, count: 800, factor: 3, fade: true },
3500
+ transition: { radius: 30, count: 400, factor: 2, fade: true }
3501
+ };
3502
+ var Avl3DEffects = ({
3503
+ level,
3504
+ enabled = true
3505
+ }) => {
3506
+ const palette = useAvl3DPalette();
3507
+ const starsKey = useMemo(() => `stars-${level}`, [level]);
3508
+ if (!enabled || !palette) return null;
3509
+ const bloom = BLOOM_CONFIG[level];
3510
+ const dof = DOF_CONFIG[level];
3511
+ const sparkles = SPARKLE_CONFIGS[level];
3512
+ const stars = STARS_CONFIG[level];
3513
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3514
+ /* @__PURE__ */ jsx(
3515
+ Stars,
3516
+ {
3517
+ radius: stars.radius,
3518
+ count: stars.count,
3519
+ factor: stars.factor,
3520
+ saturation: 0.1,
3521
+ fade: stars.fade,
3522
+ speed: 0.3
3523
+ },
3524
+ starsKey
3525
+ ),
3526
+ sparkles.map((cfg, i) => /* @__PURE__ */ jsx(
3527
+ Sparkles,
3528
+ {
3529
+ count: cfg.count,
3530
+ size: cfg.size,
3531
+ speed: cfg.speed,
3532
+ opacity: cfg.opacity,
3533
+ scale: cfg.scale,
3534
+ color: palette[cfg.colorKey]
3535
+ },
3536
+ `sparkle-${level}-${i}`
3537
+ )),
3538
+ dof ? /* @__PURE__ */ jsxs(EffectComposer, { children: [
3539
+ /* @__PURE__ */ jsx(
3540
+ Bloom,
3541
+ {
3542
+ intensity: bloom.intensity,
3543
+ luminanceThreshold: bloom.luminanceThreshold,
3544
+ luminanceSmoothing: bloom.luminanceSmoothing,
3545
+ mipmapBlur: true
3546
+ }
3547
+ ),
3548
+ /* @__PURE__ */ jsx(
3549
+ DepthOfField,
3550
+ {
3551
+ focusDistance: dof.focusDistance,
3552
+ focalLength: dof.focalLength,
3553
+ bokehScale: dof.bokehScale
3554
+ }
3555
+ ),
3556
+ /* @__PURE__ */ jsx(Vignette, { eskil: false, offset: 0.2, darkness: 0.35 })
3557
+ ] }) : /* @__PURE__ */ jsxs(EffectComposer, { children: [
3558
+ /* @__PURE__ */ jsx(
3559
+ Bloom,
3560
+ {
3561
+ intensity: bloom.intensity,
3562
+ luminanceThreshold: bloom.luminanceThreshold,
3563
+ luminanceSmoothing: bloom.luminanceSmoothing,
3564
+ mipmapBlur: true
3565
+ }
3566
+ ),
3567
+ /* @__PURE__ */ jsx(Vignette, { eskil: false, offset: 0.2, darkness: 0.35 })
3568
+ ] })
3569
+ ] });
3570
+ };
3571
+ Avl3DEffects.displayName = "Avl3DEffects";
3572
+ function useThemeColors(values, scopeRef) {
3573
+ const { appliedTheme } = useTheme();
3574
+ const scopeKey = JSON.stringify(useThemeScope());
3575
+ const [resolved, setResolved] = useState(null);
3576
+ const valuesKey = JSON.stringify(values);
3577
+ useLayoutEffect(() => {
3578
+ const scope = scopeRef.current;
3579
+ if (!scope) return;
3580
+ const out = {};
3581
+ for (const key of Object.keys(values)) out[key] = resolveThemeColor(values[key], scope);
3582
+ setResolved(out);
3583
+ }, [appliedTheme, scopeKey, valuesKey, scopeRef]);
3584
+ return resolved;
3585
+ }
3586
+ var log3 = createLogger("almadar:ui:avl-3d-viewer");
3587
+ function CameraController({ targetPosition, targetLookAt, animated }) {
3588
+ const { camera } = useThree();
3589
+ const targetPosVec = useRef(new THREE5.Vector3(...targetPosition));
3590
+ const targetLookVec = useRef(new THREE5.Vector3(...targetLookAt));
3591
+ const isAnimating = useRef(false);
3592
+ useEffect(() => {
3593
+ const newTarget = new THREE5.Vector3(...targetPosition);
3594
+ const newLookAt = new THREE5.Vector3(...targetLookAt);
3595
+ if (!newTarget.equals(targetPosVec.current) || !newLookAt.equals(targetLookVec.current)) {
3596
+ targetPosVec.current.copy(newTarget);
3597
+ targetLookVec.current.copy(newLookAt);
3598
+ if (animated) {
3599
+ isAnimating.current = true;
3600
+ } else {
3601
+ camera.position.copy(targetPosVec.current);
3602
+ camera.lookAt(targetLookVec.current.x, targetLookVec.current.y, targetLookVec.current.z);
3603
+ }
3604
+ }
3605
+ }, [targetPosition, targetLookAt, animated, camera]);
3606
+ useFrame((_, delta) => {
3607
+ if (!isAnimating.current) return;
3608
+ const speed = 3;
3609
+ camera.position.x = THREE5.MathUtils.damp(camera.position.x, targetPosVec.current.x, speed, delta);
3610
+ camera.position.y = THREE5.MathUtils.damp(camera.position.y, targetPosVec.current.y, speed, delta);
3611
+ camera.position.z = THREE5.MathUtils.damp(camera.position.z, targetPosVec.current.z, speed, delta);
3612
+ camera.lookAt(targetLookVec.current);
3613
+ const dist = camera.position.distanceTo(targetPosVec.current);
3614
+ if (dist < 0.05) {
3615
+ isAnimating.current = false;
3616
+ }
3617
+ });
3618
+ return null;
3619
+ }
3620
+ function SceneFade({ animating, children }) {
3621
+ const groupRef = useRef(null);
3622
+ const opacityRef = useRef(1);
3623
+ useFrame((_, delta) => {
3624
+ if (!groupRef.current) return;
3625
+ const target = animating ? 0 : 1;
3626
+ opacityRef.current = THREE5.MathUtils.damp(opacityRef.current, target, 5, delta);
3627
+ groupRef.current.visible = opacityRef.current > 0.05;
3628
+ const s = 0.9 + opacityRef.current * 0.1;
3629
+ groupRef.current.scale.setScalar(s);
3630
+ });
3631
+ return /* @__PURE__ */ jsx("group", { ref: groupRef, children });
3632
+ }
3633
+ var Avl3DViewer = ({
3634
+ schema: schemaProp,
3635
+ className,
3636
+ animated = true,
3637
+ initialOrbital,
3638
+ initialTrait,
3639
+ onZoomChange,
3640
+ width = "100%",
3641
+ height = 500,
3642
+ effects = true,
3643
+ modelOverrides = {}
3644
+ }) => {
3645
+ const { t } = useTranslate();
3646
+ const containerRef = useRef(null);
3647
+ const resolvedColors = useThemeColors(AVL_3D_COLORS, containerRef);
3648
+ const palette = useMemo(() => {
3649
+ if (!resolvedColors) return null;
3650
+ const out = {};
3651
+ for (const key of Object.keys(AVL_3D_COLORS)) {
3652
+ const resolved = resolvedColors[key];
3653
+ if (!resolved) {
3654
+ log3.error("avl-3d-palette-key-unresolved", { key, value: AVL_3D_COLORS[key] });
3655
+ return null;
3656
+ }
3657
+ out[key] = resolved.css;
3658
+ }
3659
+ return out;
3660
+ }, [resolvedColors]);
3661
+ const schema = useMemo(() => {
3662
+ if (typeof schemaProp === "string") {
3663
+ try {
3664
+ return JSON.parse(schemaProp);
3665
+ } catch {
3666
+ return { name: t("common.error"), orbitals: [] };
3667
+ }
3668
+ }
3669
+ return schemaProp;
3670
+ }, [schemaProp, t]);
3671
+ const configValue = useMemo(() => ({
3672
+ modelOverrides,
3673
+ effectsEnabled: effects,
3674
+ palette
3675
+ }), [modelOverrides, effects, palette]);
3676
+ const [state, dispatch] = useReducer(zoomReducer, initialZoomState);
3677
+ useEffect(() => {
3678
+ if (initialOrbital) {
3679
+ dispatch({ type: "ZOOM_INTO_ORBITAL", orbital: initialOrbital, targetPosition: { x: 0, y: 0 } });
3680
+ setTimeout(() => dispatch({ type: "ANIMATION_COMPLETE" }), 0);
3681
+ if (initialTrait) {
3682
+ setTimeout(() => {
3683
+ dispatch({ type: "ZOOM_INTO_TRAIT", trait: initialTrait, targetPosition: { x: 0, y: 0 } });
3684
+ setTimeout(() => dispatch({ type: "ANIMATION_COMPLETE" }), 0);
3685
+ }, 10);
3686
+ }
3687
+ }
3688
+ }, [initialOrbital, initialTrait]);
3689
+ useEffect(() => {
3690
+ onZoomChange?.(state.level, {
3691
+ orbital: state.selectedOrbital ?? void 0,
3692
+ trait: state.selectedTrait ?? void 0
3693
+ });
3694
+ }, [state.level, state.selectedOrbital, state.selectedTrait, onZoomChange]);
3695
+ useEffect(() => {
3696
+ if (!state.animating) return;
3697
+ const timer = setTimeout(() => {
3698
+ dispatch({ type: "ANIMATION_COMPLETE" });
3699
+ }, 800);
3700
+ return () => clearTimeout(timer);
3701
+ }, [state.animating]);
3702
+ const handleKeyDown = useCallback((e) => {
3703
+ if (e.key === "Escape") {
3704
+ dispatch({ type: "ZOOM_OUT" });
3705
+ }
3706
+ }, []);
3707
+ const handleOrbitalClick = useCallback((name) => {
3708
+ dispatch({ type: "ZOOM_INTO_ORBITAL", orbital: name, targetPosition: { x: 0, y: 0 } });
3709
+ }, []);
3710
+ const handleTraitClick = useCallback((name) => {
3711
+ dispatch({ type: "ZOOM_INTO_TRAIT", trait: name, targetPosition: { x: 0, y: 0 } });
3712
+ }, []);
3713
+ const [highlightedTrait, setHighlightedTrait] = React3.useState(null);
3714
+ const handleTransitionClick = useCallback((index) => {
3715
+ dispatch({ type: "ZOOM_INTO_TRANSITION", transitionIndex: index, targetPosition: { x: 0, y: 0 } });
3716
+ }, []);
3717
+ const handleBreadcrumbClick = useCallback((targetLevel) => {
3718
+ const levelOrder = ["application", "orbital", "trait", "transition"];
3719
+ const currentIdx = levelOrder.indexOf(state.level);
3720
+ const targetIdx = levelOrder.indexOf(targetLevel);
3721
+ if (targetIdx < currentIdx) {
3722
+ dispatch({ type: "ZOOM_OUT" });
3723
+ }
3724
+ }, [state.level]);
3725
+ const cameraConfig = CAMERA_POSITIONS[state.level];
3726
+ const sceneContent = useMemo(() => {
3727
+ switch (state.level) {
3728
+ case "application": {
3729
+ const data = parseApplicationLevel(schema);
3730
+ return /* @__PURE__ */ jsx(
3731
+ Avl3DApplicationScene,
3732
+ {
3733
+ data,
3734
+ onOrbitalClick: handleOrbitalClick
3735
+ }
3736
+ );
3737
+ }
3738
+ case "orbital": {
3739
+ if (!state.selectedOrbital) return null;
3740
+ const orbData = parseOrbitalLevel(schema, state.selectedOrbital);
3741
+ if (!orbData) return null;
3742
+ return /* @__PURE__ */ jsx(
3743
+ Avl3DOrbitalScene,
3744
+ {
3745
+ data: orbData,
3746
+ onTraitClick: handleTraitClick,
3747
+ highlightedTrait,
3748
+ onTraitHighlight: setHighlightedTrait
3749
+ }
3750
+ );
3751
+ }
3752
+ case "trait": {
3753
+ if (!state.selectedOrbital || !state.selectedTrait) return null;
3754
+ const traitData = parseTraitLevel(schema, state.selectedOrbital, state.selectedTrait);
3755
+ if (!traitData) return null;
3756
+ return /* @__PURE__ */ jsx(
3757
+ Avl3DTraitScene,
3758
+ {
3759
+ data: traitData,
3760
+ onTransitionClick: handleTransitionClick
3761
+ }
3762
+ );
3763
+ }
3764
+ case "transition": {
3765
+ if (!state.selectedOrbital || !state.selectedTrait || state.selectedTransition === null) return null;
3766
+ const transData = parseTransitionLevel(schema, state.selectedOrbital, state.selectedTrait, state.selectedTransition);
3767
+ if (!transData) return null;
3768
+ return /* @__PURE__ */ jsx(
3769
+ Avl3DTransitionScene,
3770
+ {
3771
+ data: transData
3772
+ }
3773
+ );
3774
+ }
3775
+ default:
3776
+ return null;
3777
+ }
3778
+ }, [state.level, state.selectedOrbital, state.selectedTrait, state.selectedTransition, schema, handleOrbitalClick, handleTraitClick, handleTransitionClick, highlightedTrait, setHighlightedTrait]);
3779
+ const breadcrumbs = getBreadcrumbs(state);
3780
+ return /* @__PURE__ */ jsxs(
3781
+ Box,
3782
+ {
3783
+ ref: containerRef,
3784
+ className: `relative ${className ?? ""}`,
3785
+ style: {
3786
+ width,
3787
+ height: typeof height === "number" ? `${height}px` : height,
3788
+ backgroundColor: AVL_3D_COLORS.background,
3789
+ borderRadius: "8px",
3790
+ overflow: "hidden"
3791
+ },
3792
+ onKeyDown: handleKeyDown,
3793
+ tabIndex: 0,
3794
+ children: [
3795
+ /* @__PURE__ */ jsx(
3796
+ HStack,
3797
+ {
3798
+ gap: "xs",
3799
+ align: "center",
3800
+ className: "absolute top-2 left-2 z-10 bg-surface/80 backdrop-blur rounded-md px-3 py-1.5",
3801
+ children: breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxs(React3.Fragment, { children: [
3802
+ i > 0 && /* @__PURE__ */ jsx(Typography, { variant: "small", color: "muted", className: "mx-1", children: "/" }),
3803
+ i < breadcrumbs.length - 1 ? /* @__PURE__ */ jsx(
3804
+ Box,
3805
+ {
3806
+ as: "span",
3807
+ className: "cursor-pointer hover:underline",
3808
+ ...pressableProps(() => handleBreadcrumbClick(crumb.level)),
3809
+ children: /* @__PURE__ */ jsx(Typography, { variant: "small", color: "muted", children: crumb.labelKey ? t(crumb.labelKey, crumb.labelParams) : crumb.label })
3810
+ }
3811
+ ) : /* @__PURE__ */ jsx(Typography, { variant: "small", color: "primary", className: "font-bold", children: crumb.labelKey ? t(crumb.labelKey, crumb.labelParams) : crumb.label })
3812
+ ] }, crumb.level))
3813
+ }
3814
+ ),
3815
+ state.level !== "application" && /* @__PURE__ */ jsx(
3816
+ Typography,
3817
+ {
3818
+ variant: "small",
3819
+ color: "muted",
3820
+ className: "absolute bottom-2 right-2 z-10 bg-surface/60 backdrop-blur rounded px-2 py-1",
3821
+ children: t("avl.pressEscToZoomOut")
3822
+ }
3823
+ ),
3824
+ palette && /* @__PURE__ */ jsx(
3825
+ Canvas,
3826
+ {
3827
+ style: { width: "100%", height: "100%" },
3828
+ camera: {
3829
+ position: cameraConfig.position,
3830
+ fov: 60,
3831
+ near: 0.1,
3832
+ far: 200
3833
+ },
3834
+ onCreated: ({ gl }) => {
3835
+ gl.setClearColor(palette.background);
3836
+ },
3837
+ children: /* @__PURE__ */ jsx(Avl3DContext.Provider, { value: configValue, children: /* @__PURE__ */ jsxs(
3838
+ Scene3D,
3839
+ {
3840
+ background: palette.background,
3841
+ fog: { color: palette.fog, near: 30, far: 80 },
3842
+ children: [
3843
+ /* @__PURE__ */ jsx(
3844
+ Camera3D,
3845
+ {
3846
+ mode: "perspective",
3847
+ position: cameraConfig.position,
3848
+ target: cameraConfig.target,
3849
+ enableOrbit: true,
3850
+ minDistance: 3,
3851
+ maxDistance: 80
3852
+ }
3853
+ ),
3854
+ /* @__PURE__ */ jsx(
3855
+ Lighting3D,
3856
+ {
3857
+ ambientIntensity: 0.35,
3858
+ ambientColor: palette.lightAmbient,
3859
+ directionalIntensity: 0.5,
3860
+ directionalColor: palette.lightDirectional,
3861
+ directionalPosition: [8, 15, 12],
3862
+ shadows: false
3863
+ }
3864
+ ),
3865
+ /* @__PURE__ */ jsx(
3866
+ "pointLight",
3867
+ {
3868
+ position: [0, -10, 0],
3869
+ color: palette.lightFill,
3870
+ intensity: 0.15,
3871
+ distance: 50,
3872
+ decay: 2
3873
+ }
3874
+ ),
3875
+ /* @__PURE__ */ jsx(
3876
+ CameraController,
3877
+ {
3878
+ targetPosition: cameraConfig.position,
3879
+ targetLookAt: cameraConfig.target,
3880
+ animated
3881
+ }
3882
+ ),
3883
+ /* @__PURE__ */ jsx(SceneFade, { animating: state.animating, children: sceneContent }),
3884
+ /* @__PURE__ */ jsx(Avl3DEffects, { level: state.level, enabled: effects })
3885
+ ]
3886
+ }
3887
+ ) })
3888
+ }
3889
+ )
3890
+ ]
3891
+ }
3892
+ );
3893
+ };
3894
+ Avl3DViewer.displayName = "Avl3DViewer";
3895
+
3896
+ export { AssetLoader, Avl3DApplicationScene, Avl3DEffects, Avl3DOrbitalScene, Avl3DTraitScene, Avl3DTransitionScene, Avl3DViewer, Camera3D, Canvas3DErrorBoundary, Canvas3DHost, Canvas3DLoadingState, Canvas3DHost as GameCanvas3D, Lighting3D, ModelLoader, Scene3D, SpatialHashGrid, assetLoader, calculateLODLevel, createGridHighlight, cullInstancedMesh, filterByFrustum, getCellsInRadius, getNeighbors, getVisibleIndices, gridDistance, gridManhattanDistance, gridToWorld, isInBounds, isInFrustum, normalizeMouseCoordinates, raycastToObjects, raycastToPlane, updateInstanceLOD, useAssetLoader, useGameCanvas3DEvents, useRaycaster, useSceneGraph, useThree5 as useThree, worldToGrid };