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