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