@almadar/ui 2.13.2 → 2.14.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/chunk-4N3BAPDB.js +1667 -0
- package/dist/{chunk-PERGHHON.js → chunk-IRIGCHP4.js} +2 -12
- package/dist/{chunk-ZW5N4AUU.js → chunk-M7MOIE46.js} +3 -3
- package/dist/{chunk-Y7IHEYYE.js → chunk-QU2X55WH.js} +11 -1
- package/dist/{chunk-77CBR3Z7.js → chunk-SKWPSQHQ.js} +13448 -2279
- package/dist/{chunk-4ZBSL37D.js → chunk-XL7WB2O5.js} +415 -58
- package/dist/components/index.css +508 -0
- package/dist/components/index.js +769 -11187
- package/dist/components/organisms/game/three/index.js +49 -1709
- package/dist/hooks/index.js +2 -2
- package/dist/lib/index.js +1 -3
- package/dist/providers/index.css +599 -0
- package/dist/providers/index.js +5 -4
- package/dist/runtime/index.css +599 -0
- package/dist/runtime/index.js +6 -6
- package/package.json +5 -4
- package/dist/ThemeContext-D9xUORq5.d.ts +0 -105
- package/dist/chunk-42YQ6JVR.js +0 -48
- package/dist/chunk-WCTZ7WZX.js +0 -311
- package/dist/cn-C_ATNPvi.d.ts +0 -332
- package/dist/components/index.d.ts +0 -9788
- package/dist/components/organisms/game/three/index.d.ts +0 -1233
- package/dist/context/index.d.ts +0 -208
- package/dist/event-bus-types-CjJduURa.d.ts +0 -73
- package/dist/hooks/index.d.ts +0 -1221
- package/dist/isometric-ynNHVPZx.d.ts +0 -111
- package/dist/lib/index.d.ts +0 -320
- package/dist/locales/index.d.ts +0 -22
- package/dist/offline-executor-CHr4uAhf.d.ts +0 -401
- package/dist/providers/index.d.ts +0 -465
- package/dist/renderer/index.d.ts +0 -525
- package/dist/runtime/index.d.ts +0 -280
- package/dist/stores/index.d.ts +0 -151
- package/dist/useUISlots-BBjNvQtb.d.ts +0 -85
|
@@ -1,752 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AssetLoader } from '../../../../chunk-4N3BAPDB.js';
|
|
2
|
+
export { AssetLoader, Camera3D, Canvas3DErrorBoundary, Canvas3DLoadingState, FeatureRenderer, FeatureRenderer3D, Lighting3D, ModelLoader, PhysicsObject3D, Scene3D, TileRenderer, UnitRenderer, assetLoader, preloadFeatures, useAssetLoader, useGameCanvas3DEvents, usePhysics3DController } from '../../../../chunk-4N3BAPDB.js';
|
|
3
|
+
import '../../../../chunk-YXZM3WCF.js';
|
|
2
4
|
import { __publicField } from '../../../../chunk-PKBMQBKP.js';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
7
|
-
import { OrbitControls } from '@react-three/drei';
|
|
8
|
-
import { GLTFLoader as GLTFLoader$1 } from 'three/examples/jsm/loaders/GLTFLoader';
|
|
9
|
-
import { OrbitControls as OrbitControls$1 } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
10
|
-
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
11
|
-
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
|
|
5
|
+
import { useRef, useState, useMemo, useEffect, useCallback } from 'react';
|
|
6
|
+
import * as THREE from 'three';
|
|
7
|
+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
12
8
|
|
|
13
|
-
function Scene3D({ background = "#1a1a2e", fog, children }) {
|
|
14
|
-
const { scene } = useThree();
|
|
15
|
-
const initializedRef = useRef(false);
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
if (initializedRef.current) return;
|
|
18
|
-
initializedRef.current = true;
|
|
19
|
-
if (background.startsWith("#") || background.startsWith("rgb")) {
|
|
20
|
-
scene.background = new THREE6.Color(background);
|
|
21
|
-
} else {
|
|
22
|
-
const loader = new THREE6.TextureLoader();
|
|
23
|
-
loader.load(background, (texture) => {
|
|
24
|
-
scene.background = texture;
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
if (fog) {
|
|
28
|
-
scene.fog = new THREE6.Fog(fog.color, fog.near, fog.far);
|
|
29
|
-
}
|
|
30
|
-
return () => {
|
|
31
|
-
scene.background = null;
|
|
32
|
-
scene.fog = null;
|
|
33
|
-
};
|
|
34
|
-
}, [scene, background, fog]);
|
|
35
|
-
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
36
|
-
}
|
|
37
|
-
var Camera3D = forwardRef(
|
|
38
|
-
({
|
|
39
|
-
mode = "isometric",
|
|
40
|
-
position = [10, 10, 10],
|
|
41
|
-
target = [0, 0, 0],
|
|
42
|
-
zoom = 1,
|
|
43
|
-
fov = 45,
|
|
44
|
-
enableOrbit = true,
|
|
45
|
-
minDistance = 2,
|
|
46
|
-
maxDistance = 100,
|
|
47
|
-
onChange
|
|
48
|
-
}, ref) => {
|
|
49
|
-
const { camera, set, viewport } = useThree();
|
|
50
|
-
const controlsRef = useRef(null);
|
|
51
|
-
const initialPosition = useRef(new THREE6.Vector3(...position));
|
|
52
|
-
const initialTarget = useRef(new THREE6.Vector3(...target));
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
let newCamera;
|
|
55
|
-
if (mode === "isometric") {
|
|
56
|
-
const aspect = viewport.aspect;
|
|
57
|
-
const size = 10 / zoom;
|
|
58
|
-
newCamera = new THREE6.OrthographicCamera(
|
|
59
|
-
-size * aspect,
|
|
60
|
-
size * aspect,
|
|
61
|
-
size,
|
|
62
|
-
-size,
|
|
63
|
-
0.1,
|
|
64
|
-
1e3
|
|
65
|
-
);
|
|
66
|
-
} else {
|
|
67
|
-
newCamera = new THREE6.PerspectiveCamera(fov, viewport.aspect, 0.1, 1e3);
|
|
68
|
-
}
|
|
69
|
-
newCamera.position.copy(initialPosition.current);
|
|
70
|
-
newCamera.lookAt(initialTarget.current);
|
|
71
|
-
set({ camera: newCamera });
|
|
72
|
-
if (mode === "top-down") {
|
|
73
|
-
newCamera.position.set(0, 20 / zoom, 0);
|
|
74
|
-
newCamera.lookAt(0, 0, 0);
|
|
75
|
-
}
|
|
76
|
-
return () => {
|
|
77
|
-
};
|
|
78
|
-
}, [mode, fov, zoom, viewport.aspect, set]);
|
|
79
|
-
useFrame(() => {
|
|
80
|
-
if (onChange) {
|
|
81
|
-
onChange(camera);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
useImperativeHandle(ref, () => ({
|
|
85
|
-
getCamera: () => camera,
|
|
86
|
-
setPosition: (x, y, z) => {
|
|
87
|
-
camera.position.set(x, y, z);
|
|
88
|
-
if (controlsRef.current) {
|
|
89
|
-
controlsRef.current.update();
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
lookAt: (x, y, z) => {
|
|
93
|
-
camera.lookAt(x, y, z);
|
|
94
|
-
if (controlsRef.current) {
|
|
95
|
-
controlsRef.current.target.set(x, y, z);
|
|
96
|
-
controlsRef.current.update();
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
reset: () => {
|
|
100
|
-
camera.position.copy(initialPosition.current);
|
|
101
|
-
camera.lookAt(initialTarget.current);
|
|
102
|
-
if (controlsRef.current) {
|
|
103
|
-
controlsRef.current.target.copy(initialTarget.current);
|
|
104
|
-
controlsRef.current.update();
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
getViewBounds: () => {
|
|
108
|
-
const min = new THREE6.Vector3(-10, -10, -10);
|
|
109
|
-
const max = new THREE6.Vector3(10, 10, 10);
|
|
110
|
-
return { min, max };
|
|
111
|
-
}
|
|
112
|
-
}));
|
|
113
|
-
const maxPolarAngle = mode === "top-down" ? 0.1 : Math.PI / 2 - 0.1;
|
|
114
|
-
return /* @__PURE__ */ jsx(
|
|
115
|
-
OrbitControls,
|
|
116
|
-
{
|
|
117
|
-
ref: controlsRef,
|
|
118
|
-
camera,
|
|
119
|
-
enabled: enableOrbit,
|
|
120
|
-
target: initialTarget.current,
|
|
121
|
-
minDistance,
|
|
122
|
-
maxDistance,
|
|
123
|
-
maxPolarAngle,
|
|
124
|
-
enableDamping: true,
|
|
125
|
-
dampingFactor: 0.05
|
|
126
|
-
}
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
);
|
|
130
|
-
Camera3D.displayName = "Camera3D";
|
|
131
|
-
function Lighting3D({
|
|
132
|
-
ambientIntensity = 0.6,
|
|
133
|
-
ambientColor = "#ffffff",
|
|
134
|
-
directionalIntensity = 0.8,
|
|
135
|
-
directionalColor = "#ffffff",
|
|
136
|
-
directionalPosition = [10, 20, 10],
|
|
137
|
-
shadows = true,
|
|
138
|
-
shadowMapSize = 2048,
|
|
139
|
-
shadowCameraSize = 20,
|
|
140
|
-
showHelpers = false
|
|
141
|
-
}) {
|
|
142
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
143
|
-
/* @__PURE__ */ jsx("ambientLight", { intensity: ambientIntensity, color: ambientColor }),
|
|
144
|
-
/* @__PURE__ */ jsx(
|
|
145
|
-
"directionalLight",
|
|
146
|
-
{
|
|
147
|
-
position: directionalPosition,
|
|
148
|
-
intensity: directionalIntensity,
|
|
149
|
-
color: directionalColor,
|
|
150
|
-
castShadow: shadows,
|
|
151
|
-
"shadow-mapSize": [shadowMapSize, shadowMapSize],
|
|
152
|
-
"shadow-camera-left": -shadowCameraSize,
|
|
153
|
-
"shadow-camera-right": shadowCameraSize,
|
|
154
|
-
"shadow-camera-top": shadowCameraSize,
|
|
155
|
-
"shadow-camera-bottom": -shadowCameraSize,
|
|
156
|
-
"shadow-camera-near": 0.1,
|
|
157
|
-
"shadow-camera-far": 100,
|
|
158
|
-
"shadow-bias": -1e-3
|
|
159
|
-
}
|
|
160
|
-
),
|
|
161
|
-
/* @__PURE__ */ jsx(
|
|
162
|
-
"hemisphereLight",
|
|
163
|
-
{
|
|
164
|
-
intensity: 0.3,
|
|
165
|
-
color: "#87ceeb",
|
|
166
|
-
groundColor: "#362d1d"
|
|
167
|
-
}
|
|
168
|
-
),
|
|
169
|
-
showHelpers && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(
|
|
170
|
-
"directionalLightHelper",
|
|
171
|
-
{
|
|
172
|
-
args: [
|
|
173
|
-
new THREE6.DirectionalLight(directionalColor, directionalIntensity),
|
|
174
|
-
5
|
|
175
|
-
]
|
|
176
|
-
}
|
|
177
|
-
) })
|
|
178
|
-
] });
|
|
179
|
-
}
|
|
180
|
-
function Canvas3DLoadingState({
|
|
181
|
-
progress = 0,
|
|
182
|
-
loaded = 0,
|
|
183
|
-
total = 0,
|
|
184
|
-
message = "Loading 3D Scene...",
|
|
185
|
-
details,
|
|
186
|
-
showSpinner = true,
|
|
187
|
-
className
|
|
188
|
-
}) {
|
|
189
|
-
const clampedProgress = Math.max(0, Math.min(100, progress));
|
|
190
|
-
const hasProgress = total > 0;
|
|
191
|
-
return /* @__PURE__ */ jsxs("div", { className: `canvas-3d-loading ${className || ""}`, children: [
|
|
192
|
-
/* @__PURE__ */ jsxs("div", { className: "canvas-3d-loading__content", children: [
|
|
193
|
-
showSpinner && /* @__PURE__ */ jsxs("div", { className: "canvas-3d-loading__spinner", children: [
|
|
194
|
-
/* @__PURE__ */ jsx("div", { className: "spinner__ring" }),
|
|
195
|
-
/* @__PURE__ */ jsx("div", { className: "spinner__ring spinner__ring--secondary" })
|
|
196
|
-
] }),
|
|
197
|
-
/* @__PURE__ */ jsx("div", { className: "canvas-3d-loading__message", children: message }),
|
|
198
|
-
details && /* @__PURE__ */ jsx("div", { className: "canvas-3d-loading__details", children: details }),
|
|
199
|
-
hasProgress && /* @__PURE__ */ jsxs("div", { className: "canvas-3d-loading__progress", children: [
|
|
200
|
-
/* @__PURE__ */ jsx("div", { className: "progress__bar", children: /* @__PURE__ */ jsx(
|
|
201
|
-
"div",
|
|
202
|
-
{
|
|
203
|
-
className: "progress__fill",
|
|
204
|
-
style: { width: `${clampedProgress}%` }
|
|
205
|
-
}
|
|
206
|
-
) }),
|
|
207
|
-
/* @__PURE__ */ jsxs("div", { className: "progress__text", children: [
|
|
208
|
-
/* @__PURE__ */ jsxs("span", { className: "progress__percentage", children: [
|
|
209
|
-
clampedProgress,
|
|
210
|
-
"%"
|
|
211
|
-
] }),
|
|
212
|
-
/* @__PURE__ */ jsxs("span", { className: "progress__count", children: [
|
|
213
|
-
"(",
|
|
214
|
-
loaded,
|
|
215
|
-
"/",
|
|
216
|
-
total,
|
|
217
|
-
")"
|
|
218
|
-
] })
|
|
219
|
-
] })
|
|
220
|
-
] })
|
|
221
|
-
] }),
|
|
222
|
-
/* @__PURE__ */ jsx("div", { className: "canvas-3d-loading__background", children: /* @__PURE__ */ jsx("div", { className: "bg__grid" }) })
|
|
223
|
-
] });
|
|
224
|
-
}
|
|
225
|
-
var Canvas3DErrorBoundary = class extends Component {
|
|
226
|
-
constructor(props) {
|
|
227
|
-
super(props);
|
|
228
|
-
__publicField(this, "handleReset", () => {
|
|
229
|
-
this.setState({
|
|
230
|
-
hasError: false,
|
|
231
|
-
error: null,
|
|
232
|
-
errorInfo: null
|
|
233
|
-
});
|
|
234
|
-
this.props.onReset?.();
|
|
235
|
-
});
|
|
236
|
-
this.state = {
|
|
237
|
-
hasError: false,
|
|
238
|
-
error: null,
|
|
239
|
-
errorInfo: null
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
static getDerivedStateFromError(error) {
|
|
243
|
-
return {
|
|
244
|
-
hasError: true,
|
|
245
|
-
error,
|
|
246
|
-
errorInfo: null
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
componentDidCatch(error, errorInfo) {
|
|
250
|
-
this.setState({ errorInfo });
|
|
251
|
-
this.props.onError?.(error, errorInfo);
|
|
252
|
-
console.error("[Canvas3DErrorBoundary] Error caught:", error);
|
|
253
|
-
console.error("[Canvas3DErrorBoundary] Component stack:", errorInfo.componentStack);
|
|
254
|
-
}
|
|
255
|
-
render() {
|
|
256
|
-
if (this.state.hasError) {
|
|
257
|
-
if (this.props.fallback) {
|
|
258
|
-
return this.props.fallback;
|
|
259
|
-
}
|
|
260
|
-
return /* @__PURE__ */ jsx("div", { className: "canvas-3d-error", children: /* @__PURE__ */ jsxs("div", { className: "canvas-3d-error__content", children: [
|
|
261
|
-
/* @__PURE__ */ jsx("div", { className: "canvas-3d-error__icon", children: "\u26A0\uFE0F" }),
|
|
262
|
-
/* @__PURE__ */ jsx("h2", { className: "canvas-3d-error__title", children: "3D Scene Error" }),
|
|
263
|
-
/* @__PURE__ */ jsx("p", { className: "canvas-3d-error__message", children: "Something went wrong while rendering the 3D scene." }),
|
|
264
|
-
this.state.error && /* @__PURE__ */ jsxs("details", { className: "canvas-3d-error__details", children: [
|
|
265
|
-
/* @__PURE__ */ jsx("summary", { children: "Error Details" }),
|
|
266
|
-
/* @__PURE__ */ jsxs("pre", { className: "error__stack", children: [
|
|
267
|
-
this.state.error.message,
|
|
268
|
-
"\n",
|
|
269
|
-
this.state.error.stack
|
|
270
|
-
] }),
|
|
271
|
-
this.state.errorInfo && /* @__PURE__ */ jsx("pre", { className: "error__component-stack", children: this.state.errorInfo.componentStack })
|
|
272
|
-
] }),
|
|
273
|
-
/* @__PURE__ */ jsxs("div", { className: "canvas-3d-error__actions", children: [
|
|
274
|
-
/* @__PURE__ */ jsx(
|
|
275
|
-
"button",
|
|
276
|
-
{
|
|
277
|
-
className: "error__button error__button--primary",
|
|
278
|
-
onClick: this.handleReset,
|
|
279
|
-
children: "Try Again"
|
|
280
|
-
}
|
|
281
|
-
),
|
|
282
|
-
/* @__PURE__ */ jsx(
|
|
283
|
-
"button",
|
|
284
|
-
{
|
|
285
|
-
className: "error__button error__button--secondary",
|
|
286
|
-
onClick: () => window.location.reload(),
|
|
287
|
-
children: "Reload Page"
|
|
288
|
-
}
|
|
289
|
-
)
|
|
290
|
-
] })
|
|
291
|
-
] }) });
|
|
292
|
-
}
|
|
293
|
-
return this.props.children;
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
function detectAssetRoot(modelUrl) {
|
|
297
|
-
const idx = modelUrl.indexOf("/3d/");
|
|
298
|
-
if (idx !== -1) {
|
|
299
|
-
return modelUrl.substring(0, idx + 4);
|
|
300
|
-
}
|
|
301
|
-
return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
|
|
302
|
-
}
|
|
303
|
-
function useGLTFModel(url, resourceBasePath) {
|
|
304
|
-
const [state, setState] = useState({
|
|
305
|
-
model: null,
|
|
306
|
-
isLoading: false,
|
|
307
|
-
error: null
|
|
308
|
-
});
|
|
309
|
-
useEffect(() => {
|
|
310
|
-
if (!url) {
|
|
311
|
-
setState({ model: null, isLoading: false, error: null });
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
console.log("[ModelLoader] Loading:", url);
|
|
315
|
-
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
316
|
-
const assetRoot = resourceBasePath || detectAssetRoot(url);
|
|
317
|
-
const loader = new GLTFLoader$1();
|
|
318
|
-
loader.setResourcePath(assetRoot);
|
|
319
|
-
loader.load(
|
|
320
|
-
url,
|
|
321
|
-
(gltf) => {
|
|
322
|
-
console.log("[ModelLoader] Loaded:", url);
|
|
323
|
-
setState({
|
|
324
|
-
model: gltf.scene,
|
|
325
|
-
isLoading: false,
|
|
326
|
-
error: null
|
|
327
|
-
});
|
|
328
|
-
},
|
|
329
|
-
void 0,
|
|
330
|
-
(err) => {
|
|
331
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
332
|
-
console.warn("[ModelLoader] Failed:", url, errorMsg);
|
|
333
|
-
setState({
|
|
334
|
-
model: null,
|
|
335
|
-
isLoading: false,
|
|
336
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
);
|
|
340
|
-
}, [url, resourceBasePath]);
|
|
341
|
-
return state;
|
|
342
|
-
}
|
|
343
|
-
function ModelLoader({
|
|
344
|
-
url,
|
|
345
|
-
position = [0, 0, 0],
|
|
346
|
-
scale = 1,
|
|
347
|
-
rotation = [0, 0, 0],
|
|
348
|
-
isSelected = false,
|
|
349
|
-
isHovered = false,
|
|
350
|
-
onClick,
|
|
351
|
-
onHover,
|
|
352
|
-
fallbackGeometry = "box",
|
|
353
|
-
castShadow = true,
|
|
354
|
-
receiveShadow = true,
|
|
355
|
-
resourceBasePath
|
|
356
|
-
}) {
|
|
357
|
-
const { model: loadedModel, isLoading, error } = useGLTFModel(url, resourceBasePath);
|
|
358
|
-
const model = useMemo(() => {
|
|
359
|
-
if (!loadedModel) return null;
|
|
360
|
-
const cloned = loadedModel.clone();
|
|
361
|
-
cloned.traverse((child) => {
|
|
362
|
-
if (child instanceof THREE6.Mesh) {
|
|
363
|
-
child.castShadow = castShadow;
|
|
364
|
-
child.receiveShadow = receiveShadow;
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
return cloned;
|
|
368
|
-
}, [loadedModel, castShadow, receiveShadow]);
|
|
369
|
-
const scaleArray = useMemo(() => {
|
|
370
|
-
if (typeof scale === "number") {
|
|
371
|
-
return [scale, scale, scale];
|
|
372
|
-
}
|
|
373
|
-
return scale;
|
|
374
|
-
}, [scale]);
|
|
375
|
-
const rotationRad = useMemo(() => {
|
|
376
|
-
return [
|
|
377
|
-
rotation[0] * Math.PI / 180,
|
|
378
|
-
rotation[1] * Math.PI / 180,
|
|
379
|
-
rotation[2] * Math.PI / 180
|
|
380
|
-
];
|
|
381
|
-
}, [rotation]);
|
|
382
|
-
if (isLoading) {
|
|
383
|
-
return /* @__PURE__ */ jsx("group", { position, children: /* @__PURE__ */ jsxs("mesh", { rotation: [Math.PI / 2, 0, 0], children: [
|
|
384
|
-
/* @__PURE__ */ jsx("ringGeometry", { args: [0.3, 0.35, 16] }),
|
|
385
|
-
/* @__PURE__ */ jsx("meshBasicMaterial", { color: "#4a90d9", transparent: true, opacity: 0.8 })
|
|
386
|
-
] }) });
|
|
387
|
-
}
|
|
388
|
-
if (error || !model) {
|
|
389
|
-
if (fallbackGeometry === "none") {
|
|
390
|
-
return /* @__PURE__ */ jsx("group", { position });
|
|
391
|
-
}
|
|
392
|
-
const fallbackProps = {
|
|
393
|
-
onClick,
|
|
394
|
-
onPointerOver: () => onHover?.(true),
|
|
395
|
-
onPointerOut: () => onHover?.(false)
|
|
396
|
-
};
|
|
397
|
-
return /* @__PURE__ */ jsxs("group", { position, children: [
|
|
398
|
-
(isSelected || isHovered) && /* @__PURE__ */ jsxs("mesh", { position: [0, 0.02, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
399
|
-
/* @__PURE__ */ jsx("ringGeometry", { args: [0.6, 0.7, 32] }),
|
|
400
|
-
/* @__PURE__ */ jsx(
|
|
401
|
-
"meshBasicMaterial",
|
|
402
|
-
{
|
|
403
|
-
color: isSelected ? 16755200 : 16777215,
|
|
404
|
-
transparent: true,
|
|
405
|
-
opacity: 0.5
|
|
406
|
-
}
|
|
407
|
-
)
|
|
408
|
-
] }),
|
|
409
|
-
fallbackGeometry === "box" && /* @__PURE__ */ jsxs("mesh", { ...fallbackProps, position: [0, 0.5, 0], children: [
|
|
410
|
-
/* @__PURE__ */ jsx("boxGeometry", { args: [0.8, 0.8, 0.8] }),
|
|
411
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color: error ? 16729156 : 8947848 })
|
|
412
|
-
] }),
|
|
413
|
-
fallbackGeometry === "sphere" && /* @__PURE__ */ jsxs("mesh", { ...fallbackProps, position: [0, 0.5, 0], children: [
|
|
414
|
-
/* @__PURE__ */ jsx("sphereGeometry", { args: [0.4, 16, 16] }),
|
|
415
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color: error ? 16729156 : 8947848 })
|
|
416
|
-
] }),
|
|
417
|
-
fallbackGeometry === "cylinder" && /* @__PURE__ */ jsxs("mesh", { ...fallbackProps, position: [0, 0.5, 0], children: [
|
|
418
|
-
/* @__PURE__ */ jsx("cylinderGeometry", { args: [0.3, 0.3, 0.8, 16] }),
|
|
419
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color: error ? 16729156 : 8947848 })
|
|
420
|
-
] })
|
|
421
|
-
] });
|
|
422
|
-
}
|
|
423
|
-
return /* @__PURE__ */ jsxs(
|
|
424
|
-
"group",
|
|
425
|
-
{
|
|
426
|
-
position,
|
|
427
|
-
rotation: rotationRad,
|
|
428
|
-
onClick,
|
|
429
|
-
onPointerOver: () => onHover?.(true),
|
|
430
|
-
onPointerOut: () => onHover?.(false),
|
|
431
|
-
children: [
|
|
432
|
-
(isSelected || isHovered) && /* @__PURE__ */ jsxs("mesh", { position: [0, 0.02, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
433
|
-
/* @__PURE__ */ jsx("ringGeometry", { args: [0.6, 0.7, 32] }),
|
|
434
|
-
/* @__PURE__ */ jsx(
|
|
435
|
-
"meshBasicMaterial",
|
|
436
|
-
{
|
|
437
|
-
color: isSelected ? 16755200 : 16777215,
|
|
438
|
-
transparent: true,
|
|
439
|
-
opacity: 0.5
|
|
440
|
-
}
|
|
441
|
-
)
|
|
442
|
-
] }),
|
|
443
|
-
/* @__PURE__ */ jsx("primitive", { object: model, scale: scaleArray })
|
|
444
|
-
]
|
|
445
|
-
}
|
|
446
|
-
);
|
|
447
|
-
}
|
|
448
|
-
function PhysicsObject3D({
|
|
449
|
-
entityId,
|
|
450
|
-
modelUrl,
|
|
451
|
-
initialPosition = [0, 0, 0],
|
|
452
|
-
initialVelocity = [0, 0, 0],
|
|
453
|
-
mass = 1,
|
|
454
|
-
gravity = 9.8,
|
|
455
|
-
groundY = 0,
|
|
456
|
-
scale = 1,
|
|
457
|
-
onPhysicsUpdate,
|
|
458
|
-
onGroundHit,
|
|
459
|
-
onCollision
|
|
460
|
-
}) {
|
|
461
|
-
const groupRef = useRef(null);
|
|
462
|
-
const physicsStateRef = useRef({
|
|
463
|
-
id: entityId,
|
|
464
|
-
x: initialPosition[0],
|
|
465
|
-
y: initialPosition[1],
|
|
466
|
-
z: initialPosition[2],
|
|
467
|
-
vx: initialVelocity[0],
|
|
468
|
-
vy: initialVelocity[1],
|
|
469
|
-
vz: initialVelocity[2],
|
|
470
|
-
rx: 0,
|
|
471
|
-
ry: 0,
|
|
472
|
-
rz: 0,
|
|
473
|
-
isGrounded: false,
|
|
474
|
-
gravity,
|
|
475
|
-
friction: 0.8,
|
|
476
|
-
mass,
|
|
477
|
-
state: "Active"
|
|
478
|
-
});
|
|
479
|
-
const groundHitRef = useRef(false);
|
|
480
|
-
useEffect(() => {
|
|
481
|
-
if (groupRef.current) {
|
|
482
|
-
groupRef.current.position.set(
|
|
483
|
-
initialPosition[0],
|
|
484
|
-
initialPosition[1],
|
|
485
|
-
initialPosition[2]
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
}, []);
|
|
489
|
-
useFrame((state, delta) => {
|
|
490
|
-
const physics = physicsStateRef.current;
|
|
491
|
-
if (physics.state !== "Active") return;
|
|
492
|
-
const dt = Math.min(delta, 0.1);
|
|
493
|
-
if (!physics.isGrounded) {
|
|
494
|
-
physics.vy -= physics.gravity * dt;
|
|
495
|
-
}
|
|
496
|
-
physics.x += physics.vx * dt;
|
|
497
|
-
physics.y += physics.vy * dt;
|
|
498
|
-
physics.z += physics.vz * dt;
|
|
499
|
-
const airResistance = Math.pow(0.99, dt * 60);
|
|
500
|
-
physics.vx *= airResistance;
|
|
501
|
-
physics.vz *= airResistance;
|
|
502
|
-
if (physics.y <= groundY) {
|
|
503
|
-
physics.y = groundY;
|
|
504
|
-
if (!physics.isGrounded) {
|
|
505
|
-
physics.isGrounded = true;
|
|
506
|
-
groundHitRef.current = true;
|
|
507
|
-
physics.vx *= physics.friction;
|
|
508
|
-
physics.vz *= physics.friction;
|
|
509
|
-
onGroundHit?.();
|
|
510
|
-
}
|
|
511
|
-
physics.vy = 0;
|
|
512
|
-
} else {
|
|
513
|
-
physics.isGrounded = false;
|
|
514
|
-
}
|
|
515
|
-
if (groupRef.current) {
|
|
516
|
-
groupRef.current.position.set(physics.x, physics.y, physics.z);
|
|
517
|
-
if (!physics.isGrounded) {
|
|
518
|
-
physics.rx += physics.vz * dt * 0.5;
|
|
519
|
-
physics.rz -= physics.vx * dt * 0.5;
|
|
520
|
-
groupRef.current.rotation.set(physics.rx, physics.ry, physics.rz);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
onPhysicsUpdate?.({ ...physics });
|
|
524
|
-
});
|
|
525
|
-
const scaleArray = typeof scale === "number" ? [scale, scale, scale] : scale;
|
|
526
|
-
return /* @__PURE__ */ jsx("group", { ref: groupRef, scale: scaleArray, children: /* @__PURE__ */ jsx(
|
|
527
|
-
ModelLoader,
|
|
528
|
-
{
|
|
529
|
-
url: modelUrl,
|
|
530
|
-
fallbackGeometry: "box"
|
|
531
|
-
}
|
|
532
|
-
) });
|
|
533
|
-
}
|
|
534
|
-
function usePhysics3DController(entityId) {
|
|
535
|
-
const applyForce = (fx, fy, fz) => {
|
|
536
|
-
console.log(`Apply force to ${entityId}:`, { fx, fy, fz });
|
|
537
|
-
};
|
|
538
|
-
const setVelocity = (vx, vy, vz) => {
|
|
539
|
-
console.log(`Set velocity for ${entityId}:`, { vx, vy, vz });
|
|
540
|
-
};
|
|
541
|
-
const setPosition = (x, y, z) => {
|
|
542
|
-
console.log(`Set position for ${entityId}:`, { x, y, z });
|
|
543
|
-
};
|
|
544
|
-
const jump = (force = 10) => {
|
|
545
|
-
applyForce(0, force, 0);
|
|
546
|
-
};
|
|
547
|
-
return {
|
|
548
|
-
applyForce,
|
|
549
|
-
setVelocity,
|
|
550
|
-
setPosition,
|
|
551
|
-
jump
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
function detectAssetRoot2(modelUrl) {
|
|
555
|
-
const idx = modelUrl.indexOf("/3d/");
|
|
556
|
-
if (idx !== -1) {
|
|
557
|
-
return modelUrl.substring(0, idx + 4);
|
|
558
|
-
}
|
|
559
|
-
return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
|
|
560
|
-
}
|
|
561
|
-
function createGLTFLoaderForUrl(url) {
|
|
562
|
-
const loader = new GLTFLoader();
|
|
563
|
-
loader.setResourcePath(detectAssetRoot2(url));
|
|
564
|
-
return loader;
|
|
565
|
-
}
|
|
566
|
-
var AssetLoader = class {
|
|
567
|
-
constructor() {
|
|
568
|
-
__publicField(this, "objLoader");
|
|
569
|
-
__publicField(this, "textureLoader");
|
|
570
|
-
__publicField(this, "modelCache");
|
|
571
|
-
__publicField(this, "textureCache");
|
|
572
|
-
__publicField(this, "loadingPromises");
|
|
573
|
-
this.objLoader = new OBJLoader();
|
|
574
|
-
this.textureLoader = new THREE6.TextureLoader();
|
|
575
|
-
this.modelCache = /* @__PURE__ */ new Map();
|
|
576
|
-
this.textureCache = /* @__PURE__ */ new Map();
|
|
577
|
-
this.loadingPromises = /* @__PURE__ */ new Map();
|
|
578
|
-
}
|
|
579
|
-
/**
|
|
580
|
-
* Load a GLB/GLTF model
|
|
581
|
-
* @param url - URL to the .glb or .gltf file
|
|
582
|
-
* @returns Promise with loaded model scene and animations
|
|
583
|
-
*/
|
|
584
|
-
async loadModel(url) {
|
|
585
|
-
if (this.modelCache.has(url)) {
|
|
586
|
-
return this.modelCache.get(url);
|
|
587
|
-
}
|
|
588
|
-
if (this.loadingPromises.has(url)) {
|
|
589
|
-
return this.loadingPromises.get(url);
|
|
590
|
-
}
|
|
591
|
-
const loader = createGLTFLoaderForUrl(url);
|
|
592
|
-
const loadPromise = loader.loadAsync(url).then((gltf) => {
|
|
593
|
-
const result = {
|
|
594
|
-
scene: gltf.scene,
|
|
595
|
-
animations: gltf.animations || []
|
|
596
|
-
};
|
|
597
|
-
this.modelCache.set(url, result);
|
|
598
|
-
this.loadingPromises.delete(url);
|
|
599
|
-
return result;
|
|
600
|
-
}).catch((error) => {
|
|
601
|
-
this.loadingPromises.delete(url);
|
|
602
|
-
throw new Error(`Failed to load model ${url}: ${error.message}`);
|
|
603
|
-
});
|
|
604
|
-
this.loadingPromises.set(url, loadPromise);
|
|
605
|
-
return loadPromise;
|
|
606
|
-
}
|
|
607
|
-
/**
|
|
608
|
-
* Load an OBJ model (fallback for non-GLB assets)
|
|
609
|
-
* @param url - URL to the .obj file
|
|
610
|
-
* @returns Promise with loaded object group
|
|
611
|
-
*/
|
|
612
|
-
async loadOBJ(url) {
|
|
613
|
-
if (this.modelCache.has(url)) {
|
|
614
|
-
return this.modelCache.get(url).scene;
|
|
615
|
-
}
|
|
616
|
-
if (this.loadingPromises.has(url)) {
|
|
617
|
-
const result = await this.loadingPromises.get(url);
|
|
618
|
-
return result.scene;
|
|
619
|
-
}
|
|
620
|
-
const loadPromise = this.objLoader.loadAsync(url).then((group) => {
|
|
621
|
-
const result = {
|
|
622
|
-
scene: group,
|
|
623
|
-
animations: []
|
|
624
|
-
};
|
|
625
|
-
this.modelCache.set(url, result);
|
|
626
|
-
this.loadingPromises.delete(url);
|
|
627
|
-
return result;
|
|
628
|
-
}).catch((error) => {
|
|
629
|
-
this.loadingPromises.delete(url);
|
|
630
|
-
throw new Error(`Failed to load OBJ ${url}: ${error.message}`);
|
|
631
|
-
});
|
|
632
|
-
this.loadingPromises.set(url, loadPromise);
|
|
633
|
-
return (await loadPromise).scene;
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Load a texture
|
|
637
|
-
* @param url - URL to the texture image
|
|
638
|
-
* @returns Promise with loaded texture
|
|
639
|
-
*/
|
|
640
|
-
async loadTexture(url) {
|
|
641
|
-
if (this.textureCache.has(url)) {
|
|
642
|
-
return this.textureCache.get(url);
|
|
643
|
-
}
|
|
644
|
-
if (this.loadingPromises.has(`texture:${url}`)) {
|
|
645
|
-
return this.loadingPromises.get(`texture:${url}`);
|
|
646
|
-
}
|
|
647
|
-
const loadPromise = this.textureLoader.loadAsync(url).then((texture) => {
|
|
648
|
-
texture.colorSpace = THREE6.SRGBColorSpace;
|
|
649
|
-
this.textureCache.set(url, texture);
|
|
650
|
-
this.loadingPromises.delete(`texture:${url}`);
|
|
651
|
-
return texture;
|
|
652
|
-
}).catch((error) => {
|
|
653
|
-
this.loadingPromises.delete(`texture:${url}`);
|
|
654
|
-
throw new Error(`Failed to load texture ${url}: ${error.message}`);
|
|
655
|
-
});
|
|
656
|
-
this.loadingPromises.set(`texture:${url}`, loadPromise);
|
|
657
|
-
return loadPromise;
|
|
658
|
-
}
|
|
659
|
-
/**
|
|
660
|
-
* Preload multiple assets
|
|
661
|
-
* @param urls - Array of asset URLs to preload
|
|
662
|
-
* @returns Promise that resolves when all assets are loaded
|
|
663
|
-
*/
|
|
664
|
-
async preload(urls) {
|
|
665
|
-
const promises = urls.map((url) => {
|
|
666
|
-
if (url.endsWith(".glb") || url.endsWith(".gltf")) {
|
|
667
|
-
return this.loadModel(url).catch(() => null);
|
|
668
|
-
} else if (url.endsWith(".obj")) {
|
|
669
|
-
return this.loadOBJ(url).catch(() => null);
|
|
670
|
-
} else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
|
|
671
|
-
return this.loadTexture(url).catch(() => null);
|
|
672
|
-
}
|
|
673
|
-
return Promise.resolve(null);
|
|
674
|
-
});
|
|
675
|
-
await Promise.all(promises);
|
|
676
|
-
}
|
|
677
|
-
/**
|
|
678
|
-
* Check if a model is cached
|
|
679
|
-
* @param url - Model URL
|
|
680
|
-
*/
|
|
681
|
-
hasModel(url) {
|
|
682
|
-
return this.modelCache.has(url);
|
|
683
|
-
}
|
|
684
|
-
/**
|
|
685
|
-
* Check if a texture is cached
|
|
686
|
-
* @param url - Texture URL
|
|
687
|
-
*/
|
|
688
|
-
hasTexture(url) {
|
|
689
|
-
return this.textureCache.has(url);
|
|
690
|
-
}
|
|
691
|
-
/**
|
|
692
|
-
* Get cached model (throws if not cached)
|
|
693
|
-
* @param url - Model URL
|
|
694
|
-
*/
|
|
695
|
-
getModel(url) {
|
|
696
|
-
const model = this.modelCache.get(url);
|
|
697
|
-
if (!model) {
|
|
698
|
-
throw new Error(`Model ${url} not in cache`);
|
|
699
|
-
}
|
|
700
|
-
return model;
|
|
701
|
-
}
|
|
702
|
-
/**
|
|
703
|
-
* Get cached texture (throws if not cached)
|
|
704
|
-
* @param url - Texture URL
|
|
705
|
-
*/
|
|
706
|
-
getTexture(url) {
|
|
707
|
-
const texture = this.textureCache.get(url);
|
|
708
|
-
if (!texture) {
|
|
709
|
-
throw new Error(`Texture ${url} not in cache`);
|
|
710
|
-
}
|
|
711
|
-
return texture;
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* Clear all caches
|
|
715
|
-
*/
|
|
716
|
-
clearCache() {
|
|
717
|
-
this.textureCache.forEach((texture) => {
|
|
718
|
-
texture.dispose();
|
|
719
|
-
});
|
|
720
|
-
this.modelCache.forEach((model) => {
|
|
721
|
-
model.scene.traverse((child) => {
|
|
722
|
-
if (child instanceof THREE6.Mesh) {
|
|
723
|
-
child.geometry.dispose();
|
|
724
|
-
if (Array.isArray(child.material)) {
|
|
725
|
-
child.material.forEach((m) => m.dispose());
|
|
726
|
-
} else {
|
|
727
|
-
child.material.dispose();
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
});
|
|
731
|
-
});
|
|
732
|
-
this.modelCache.clear();
|
|
733
|
-
this.textureCache.clear();
|
|
734
|
-
this.loadingPromises.clear();
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* Get cache statistics
|
|
738
|
-
*/
|
|
739
|
-
getStats() {
|
|
740
|
-
return {
|
|
741
|
-
models: this.modelCache.size,
|
|
742
|
-
textures: this.textureCache.size,
|
|
743
|
-
loading: this.loadingPromises.size
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
};
|
|
747
|
-
var assetLoader = new AssetLoader();
|
|
748
|
-
|
|
749
|
-
// components/organisms/game/three/hooks/useThree.ts
|
|
750
9
|
var DEFAULT_OPTIONS = {
|
|
751
10
|
cameraMode: "isometric",
|
|
752
11
|
cameraPosition: [10, 10, 10],
|
|
@@ -756,7 +15,7 @@ var DEFAULT_OPTIONS = {
|
|
|
756
15
|
gridSize: 20,
|
|
757
16
|
assetLoader: new AssetLoader()
|
|
758
17
|
};
|
|
759
|
-
function
|
|
18
|
+
function useThree(options = {}) {
|
|
760
19
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
761
20
|
const containerRef = useRef(null);
|
|
762
21
|
const canvasRef = useRef(null);
|
|
@@ -769,21 +28,21 @@ function useThree3(options = {}) {
|
|
|
769
28
|
const [isReady, setIsReady] = useState(false);
|
|
770
29
|
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
|
771
30
|
const initialCameraPosition = useMemo(
|
|
772
|
-
() => new
|
|
31
|
+
() => new THREE.Vector3(...opts.cameraPosition),
|
|
773
32
|
[]
|
|
774
33
|
);
|
|
775
34
|
useEffect(() => {
|
|
776
35
|
if (!containerRef.current) return;
|
|
777
36
|
const container = containerRef.current;
|
|
778
37
|
const { clientWidth, clientHeight } = container;
|
|
779
|
-
const scene = new
|
|
780
|
-
scene.background = new
|
|
38
|
+
const scene = new THREE.Scene();
|
|
39
|
+
scene.background = new THREE.Color(opts.backgroundColor);
|
|
781
40
|
sceneRef.current = scene;
|
|
782
41
|
let camera;
|
|
783
42
|
const aspect = clientWidth / clientHeight;
|
|
784
43
|
if (opts.cameraMode === "isometric") {
|
|
785
44
|
const size = 10;
|
|
786
|
-
camera = new
|
|
45
|
+
camera = new THREE.OrthographicCamera(
|
|
787
46
|
-size * aspect,
|
|
788
47
|
size * aspect,
|
|
789
48
|
size,
|
|
@@ -792,11 +51,11 @@ function useThree3(options = {}) {
|
|
|
792
51
|
1e3
|
|
793
52
|
);
|
|
794
53
|
} else {
|
|
795
|
-
camera = new
|
|
54
|
+
camera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1e3);
|
|
796
55
|
}
|
|
797
56
|
camera.position.copy(initialCameraPosition);
|
|
798
57
|
cameraRef.current = camera;
|
|
799
|
-
const renderer = new
|
|
58
|
+
const renderer = new THREE.WebGLRenderer({
|
|
800
59
|
antialias: true,
|
|
801
60
|
alpha: true,
|
|
802
61
|
canvas: canvasRef.current || void 0
|
|
@@ -804,25 +63,25 @@ function useThree3(options = {}) {
|
|
|
804
63
|
renderer.setSize(clientWidth, clientHeight);
|
|
805
64
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
806
65
|
renderer.shadowMap.enabled = opts.shadows;
|
|
807
|
-
renderer.shadowMap.type =
|
|
66
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
808
67
|
rendererRef.current = renderer;
|
|
809
|
-
const controls = new OrbitControls
|
|
68
|
+
const controls = new OrbitControls(camera, renderer.domElement);
|
|
810
69
|
controls.enableDamping = true;
|
|
811
70
|
controls.dampingFactor = 0.05;
|
|
812
71
|
controls.minDistance = 2;
|
|
813
72
|
controls.maxDistance = 100;
|
|
814
73
|
controls.maxPolarAngle = Math.PI / 2 - 0.1;
|
|
815
74
|
controlsRef.current = controls;
|
|
816
|
-
const ambientLight = new
|
|
75
|
+
const ambientLight = new THREE.AmbientLight(16777215, 0.6);
|
|
817
76
|
scene.add(ambientLight);
|
|
818
|
-
const directionalLight = new
|
|
77
|
+
const directionalLight = new THREE.DirectionalLight(16777215, 0.8);
|
|
819
78
|
directionalLight.position.set(10, 20, 10);
|
|
820
79
|
directionalLight.castShadow = opts.shadows;
|
|
821
80
|
directionalLight.shadow.mapSize.width = 2048;
|
|
822
81
|
directionalLight.shadow.mapSize.height = 2048;
|
|
823
82
|
scene.add(directionalLight);
|
|
824
83
|
if (opts.showGrid) {
|
|
825
|
-
const gridHelper = new
|
|
84
|
+
const gridHelper = new THREE.GridHelper(
|
|
826
85
|
opts.gridSize,
|
|
827
86
|
opts.gridSize,
|
|
828
87
|
4473924,
|
|
@@ -840,10 +99,10 @@ function useThree3(options = {}) {
|
|
|
840
99
|
const handleResize = () => {
|
|
841
100
|
const { clientWidth: width, clientHeight: height } = container;
|
|
842
101
|
setDimensions({ width, height });
|
|
843
|
-
if (camera instanceof
|
|
102
|
+
if (camera instanceof THREE.PerspectiveCamera) {
|
|
844
103
|
camera.aspect = width / height;
|
|
845
104
|
camera.updateProjectionMatrix();
|
|
846
|
-
} else if (camera instanceof
|
|
105
|
+
} else if (camera instanceof THREE.OrthographicCamera) {
|
|
847
106
|
const aspect2 = width / height;
|
|
848
107
|
const size = 10;
|
|
849
108
|
camera.left = -size * aspect2;
|
|
@@ -874,7 +133,7 @@ function useThree3(options = {}) {
|
|
|
874
133
|
let newCamera;
|
|
875
134
|
if (opts.cameraMode === "isometric") {
|
|
876
135
|
const size = 10;
|
|
877
|
-
newCamera = new
|
|
136
|
+
newCamera = new THREE.OrthographicCamera(
|
|
878
137
|
-size * aspect,
|
|
879
138
|
size * aspect,
|
|
880
139
|
size,
|
|
@@ -883,7 +142,7 @@ function useThree3(options = {}) {
|
|
|
883
142
|
1e3
|
|
884
143
|
);
|
|
885
144
|
} else {
|
|
886
|
-
newCamera = new
|
|
145
|
+
newCamera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1e3);
|
|
887
146
|
}
|
|
888
147
|
newCamera.position.copy(currentPos);
|
|
889
148
|
cameraRef.current = newCamera;
|
|
@@ -952,180 +211,6 @@ function useThree3(options = {}) {
|
|
|
952
211
|
fitView
|
|
953
212
|
};
|
|
954
213
|
}
|
|
955
|
-
function useAssetLoader(options = {}) {
|
|
956
|
-
const { preloadUrls = [], loader: customLoader } = options;
|
|
957
|
-
const loaderRef = useRef(customLoader || new AssetLoader());
|
|
958
|
-
const [state, setState] = useState({
|
|
959
|
-
isLoading: false,
|
|
960
|
-
progress: 0,
|
|
961
|
-
loaded: 0,
|
|
962
|
-
total: 0,
|
|
963
|
-
errors: []
|
|
964
|
-
});
|
|
965
|
-
useEffect(() => {
|
|
966
|
-
if (preloadUrls.length > 0) {
|
|
967
|
-
preload(preloadUrls);
|
|
968
|
-
}
|
|
969
|
-
}, []);
|
|
970
|
-
const updateProgress = useCallback((loaded, total) => {
|
|
971
|
-
setState((prev) => ({
|
|
972
|
-
...prev,
|
|
973
|
-
loaded,
|
|
974
|
-
total,
|
|
975
|
-
progress: total > 0 ? Math.round(loaded / total * 100) : 0
|
|
976
|
-
}));
|
|
977
|
-
}, []);
|
|
978
|
-
const loadModel = useCallback(
|
|
979
|
-
async (url) => {
|
|
980
|
-
setState((prev) => ({ ...prev, isLoading: true }));
|
|
981
|
-
try {
|
|
982
|
-
const model = await loaderRef.current.loadModel(url);
|
|
983
|
-
setState((prev) => ({
|
|
984
|
-
...prev,
|
|
985
|
-
isLoading: false,
|
|
986
|
-
loaded: prev.loaded + 1
|
|
987
|
-
}));
|
|
988
|
-
return model;
|
|
989
|
-
} catch (error) {
|
|
990
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
991
|
-
setState((prev) => ({
|
|
992
|
-
...prev,
|
|
993
|
-
isLoading: false,
|
|
994
|
-
errors: [...prev.errors, errorMsg]
|
|
995
|
-
}));
|
|
996
|
-
throw error;
|
|
997
|
-
}
|
|
998
|
-
},
|
|
999
|
-
[]
|
|
1000
|
-
);
|
|
1001
|
-
const loadOBJ = useCallback(
|
|
1002
|
-
async (url) => {
|
|
1003
|
-
setState((prev) => ({ ...prev, isLoading: true }));
|
|
1004
|
-
try {
|
|
1005
|
-
const model = await loaderRef.current.loadOBJ(url);
|
|
1006
|
-
setState((prev) => ({
|
|
1007
|
-
...prev,
|
|
1008
|
-
isLoading: false,
|
|
1009
|
-
loaded: prev.loaded + 1
|
|
1010
|
-
}));
|
|
1011
|
-
return model;
|
|
1012
|
-
} catch (error) {
|
|
1013
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1014
|
-
setState((prev) => ({
|
|
1015
|
-
...prev,
|
|
1016
|
-
isLoading: false,
|
|
1017
|
-
errors: [...prev.errors, errorMsg]
|
|
1018
|
-
}));
|
|
1019
|
-
throw error;
|
|
1020
|
-
}
|
|
1021
|
-
},
|
|
1022
|
-
[]
|
|
1023
|
-
);
|
|
1024
|
-
const loadTexture = useCallback(
|
|
1025
|
-
async (url) => {
|
|
1026
|
-
setState((prev) => ({ ...prev, isLoading: true }));
|
|
1027
|
-
try {
|
|
1028
|
-
const texture = await loaderRef.current.loadTexture(url);
|
|
1029
|
-
setState((prev) => ({
|
|
1030
|
-
...prev,
|
|
1031
|
-
isLoading: false,
|
|
1032
|
-
loaded: prev.loaded + 1
|
|
1033
|
-
}));
|
|
1034
|
-
return texture;
|
|
1035
|
-
} catch (error) {
|
|
1036
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1037
|
-
setState((prev) => ({
|
|
1038
|
-
...prev,
|
|
1039
|
-
isLoading: false,
|
|
1040
|
-
errors: [...prev.errors, errorMsg]
|
|
1041
|
-
}));
|
|
1042
|
-
throw error;
|
|
1043
|
-
}
|
|
1044
|
-
},
|
|
1045
|
-
[]
|
|
1046
|
-
);
|
|
1047
|
-
const preload = useCallback(
|
|
1048
|
-
async (urls) => {
|
|
1049
|
-
setState((prev) => ({
|
|
1050
|
-
...prev,
|
|
1051
|
-
isLoading: true,
|
|
1052
|
-
total: urls.length,
|
|
1053
|
-
loaded: 0,
|
|
1054
|
-
errors: []
|
|
1055
|
-
}));
|
|
1056
|
-
let completed = 0;
|
|
1057
|
-
const errors = [];
|
|
1058
|
-
await Promise.all(
|
|
1059
|
-
urls.map(async (url) => {
|
|
1060
|
-
try {
|
|
1061
|
-
if (url.endsWith(".glb") || url.endsWith(".gltf")) {
|
|
1062
|
-
await loaderRef.current.loadModel(url);
|
|
1063
|
-
} else if (url.endsWith(".obj")) {
|
|
1064
|
-
await loaderRef.current.loadOBJ(url);
|
|
1065
|
-
} else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
|
|
1066
|
-
await loaderRef.current.loadTexture(url);
|
|
1067
|
-
}
|
|
1068
|
-
completed++;
|
|
1069
|
-
updateProgress(completed, urls.length);
|
|
1070
|
-
} catch (error) {
|
|
1071
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1072
|
-
errors.push(`${url}: ${errorMsg}`);
|
|
1073
|
-
completed++;
|
|
1074
|
-
updateProgress(completed, urls.length);
|
|
1075
|
-
}
|
|
1076
|
-
})
|
|
1077
|
-
);
|
|
1078
|
-
setState((prev) => ({
|
|
1079
|
-
...prev,
|
|
1080
|
-
isLoading: false,
|
|
1081
|
-
errors
|
|
1082
|
-
}));
|
|
1083
|
-
},
|
|
1084
|
-
[updateProgress]
|
|
1085
|
-
);
|
|
1086
|
-
const hasModel = useCallback((url) => {
|
|
1087
|
-
return loaderRef.current.hasModel(url);
|
|
1088
|
-
}, []);
|
|
1089
|
-
const hasTexture = useCallback((url) => {
|
|
1090
|
-
return loaderRef.current.hasTexture(url);
|
|
1091
|
-
}, []);
|
|
1092
|
-
const getModel = useCallback((url) => {
|
|
1093
|
-
try {
|
|
1094
|
-
return loaderRef.current.getModel(url);
|
|
1095
|
-
} catch {
|
|
1096
|
-
return void 0;
|
|
1097
|
-
}
|
|
1098
|
-
}, []);
|
|
1099
|
-
const getTexture = useCallback((url) => {
|
|
1100
|
-
try {
|
|
1101
|
-
return loaderRef.current.getTexture(url);
|
|
1102
|
-
} catch {
|
|
1103
|
-
return void 0;
|
|
1104
|
-
}
|
|
1105
|
-
}, []);
|
|
1106
|
-
const clearCache = useCallback(() => {
|
|
1107
|
-
loaderRef.current.clearCache();
|
|
1108
|
-
setState({
|
|
1109
|
-
isLoading: false,
|
|
1110
|
-
progress: 0,
|
|
1111
|
-
loaded: 0,
|
|
1112
|
-
total: 0,
|
|
1113
|
-
errors: []
|
|
1114
|
-
});
|
|
1115
|
-
}, []);
|
|
1116
|
-
return {
|
|
1117
|
-
...state,
|
|
1118
|
-
loadModel,
|
|
1119
|
-
loadOBJ,
|
|
1120
|
-
loadTexture,
|
|
1121
|
-
preload,
|
|
1122
|
-
hasModel,
|
|
1123
|
-
hasTexture,
|
|
1124
|
-
getModel,
|
|
1125
|
-
getTexture,
|
|
1126
|
-
clearCache
|
|
1127
|
-
};
|
|
1128
|
-
}
|
|
1129
214
|
function useSceneGraph() {
|
|
1130
215
|
const nodesRef = useRef(/* @__PURE__ */ new Map());
|
|
1131
216
|
const addNode = useCallback((node) => {
|
|
@@ -1213,8 +298,8 @@ function useSceneGraph() {
|
|
|
1213
298
|
}
|
|
1214
299
|
function useRaycaster(options) {
|
|
1215
300
|
const { camera, canvas, cellSize = 1, offsetX = 0, offsetZ = 0 } = options;
|
|
1216
|
-
const raycaster = useRef(new
|
|
1217
|
-
const mouse = useRef(new
|
|
301
|
+
const raycaster = useRef(new THREE.Raycaster());
|
|
302
|
+
const mouse = useRef(new THREE.Vector2());
|
|
1218
303
|
const clientToNDC = useCallback(
|
|
1219
304
|
(clientX, clientY) => {
|
|
1220
305
|
if (!canvas) {
|
|
@@ -1284,8 +369,8 @@ function useRaycaster(options) {
|
|
|
1284
369
|
const ndc = clientToNDC(clientX, clientY);
|
|
1285
370
|
mouse.current.set(ndc.x, ndc.y);
|
|
1286
371
|
raycaster.current.setFromCamera(mouse.current, camera);
|
|
1287
|
-
const plane = new
|
|
1288
|
-
const target = new
|
|
372
|
+
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
|
373
|
+
const target = new THREE.Vector3();
|
|
1289
374
|
const intersection = raycaster.current.ray.intersectPlane(plane, target);
|
|
1290
375
|
if (intersection) {
|
|
1291
376
|
const gridX = Math.round((target.x - offsetX) / cellSize);
|
|
@@ -1323,7 +408,7 @@ function useRaycaster(options) {
|
|
|
1323
408
|
return {
|
|
1324
409
|
gridX: gridCoords.x,
|
|
1325
410
|
gridZ: gridCoords.z,
|
|
1326
|
-
worldPosition: new
|
|
411
|
+
worldPosition: new THREE.Vector3(
|
|
1327
412
|
gridCoords.x * cellSize + offsetX,
|
|
1328
413
|
0,
|
|
1329
414
|
gridCoords.z * cellSize + offsetZ
|
|
@@ -1345,751 +430,6 @@ function useRaycaster(options) {
|
|
|
1345
430
|
isWithinCanvas
|
|
1346
431
|
};
|
|
1347
432
|
}
|
|
1348
|
-
function useGameCanvas3DEvents(options) {
|
|
1349
|
-
const {
|
|
1350
|
-
tileClickEvent,
|
|
1351
|
-
unitClickEvent,
|
|
1352
|
-
featureClickEvent,
|
|
1353
|
-
canvasClickEvent,
|
|
1354
|
-
tileHoverEvent,
|
|
1355
|
-
tileLeaveEvent,
|
|
1356
|
-
unitAnimationEvent,
|
|
1357
|
-
cameraChangeEvent,
|
|
1358
|
-
onTileClick,
|
|
1359
|
-
onUnitClick,
|
|
1360
|
-
onFeatureClick,
|
|
1361
|
-
onCanvasClick,
|
|
1362
|
-
onTileHover,
|
|
1363
|
-
onUnitAnimation
|
|
1364
|
-
} = options;
|
|
1365
|
-
const emit = useEmitEvent();
|
|
1366
|
-
const optionsRef = useRef(options);
|
|
1367
|
-
optionsRef.current = options;
|
|
1368
|
-
const handleTileClick = useCallback(
|
|
1369
|
-
(tile, event) => {
|
|
1370
|
-
if (tileClickEvent) {
|
|
1371
|
-
emit(tileClickEvent, {
|
|
1372
|
-
tileId: tile.id,
|
|
1373
|
-
x: tile.x,
|
|
1374
|
-
z: tile.z ?? tile.y ?? 0,
|
|
1375
|
-
type: tile.type,
|
|
1376
|
-
terrain: tile.terrain,
|
|
1377
|
-
elevation: tile.elevation
|
|
1378
|
-
});
|
|
1379
|
-
}
|
|
1380
|
-
optionsRef.current.onTileClick?.(tile, event);
|
|
1381
|
-
},
|
|
1382
|
-
[tileClickEvent, emit]
|
|
1383
|
-
);
|
|
1384
|
-
const handleUnitClick = useCallback(
|
|
1385
|
-
(unit, event) => {
|
|
1386
|
-
if (unitClickEvent) {
|
|
1387
|
-
emit(unitClickEvent, {
|
|
1388
|
-
unitId: unit.id,
|
|
1389
|
-
x: unit.x,
|
|
1390
|
-
z: unit.z ?? unit.y ?? 0,
|
|
1391
|
-
unitType: unit.unitType,
|
|
1392
|
-
name: unit.name,
|
|
1393
|
-
team: unit.team,
|
|
1394
|
-
faction: unit.faction,
|
|
1395
|
-
health: unit.health,
|
|
1396
|
-
maxHealth: unit.maxHealth
|
|
1397
|
-
});
|
|
1398
|
-
}
|
|
1399
|
-
optionsRef.current.onUnitClick?.(unit, event);
|
|
1400
|
-
},
|
|
1401
|
-
[unitClickEvent, emit]
|
|
1402
|
-
);
|
|
1403
|
-
const handleFeatureClick = useCallback(
|
|
1404
|
-
(feature, event) => {
|
|
1405
|
-
if (featureClickEvent) {
|
|
1406
|
-
emit(featureClickEvent, {
|
|
1407
|
-
featureId: feature.id,
|
|
1408
|
-
x: feature.x,
|
|
1409
|
-
z: feature.z ?? feature.y ?? 0,
|
|
1410
|
-
type: feature.type,
|
|
1411
|
-
elevation: feature.elevation
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
optionsRef.current.onFeatureClick?.(feature, event);
|
|
1415
|
-
},
|
|
1416
|
-
[featureClickEvent, emit]
|
|
1417
|
-
);
|
|
1418
|
-
const handleCanvasClick = useCallback(
|
|
1419
|
-
(event) => {
|
|
1420
|
-
if (canvasClickEvent) {
|
|
1421
|
-
emit(canvasClickEvent, {
|
|
1422
|
-
clientX: event.clientX,
|
|
1423
|
-
clientY: event.clientY,
|
|
1424
|
-
button: event.button
|
|
1425
|
-
});
|
|
1426
|
-
}
|
|
1427
|
-
optionsRef.current.onCanvasClick?.(event);
|
|
1428
|
-
},
|
|
1429
|
-
[canvasClickEvent, emit]
|
|
1430
|
-
);
|
|
1431
|
-
const handleTileHover = useCallback(
|
|
1432
|
-
(tile, event) => {
|
|
1433
|
-
if (tile) {
|
|
1434
|
-
if (tileHoverEvent) {
|
|
1435
|
-
emit(tileHoverEvent, {
|
|
1436
|
-
tileId: tile.id,
|
|
1437
|
-
x: tile.x,
|
|
1438
|
-
z: tile.z ?? tile.y ?? 0,
|
|
1439
|
-
type: tile.type
|
|
1440
|
-
});
|
|
1441
|
-
}
|
|
1442
|
-
} else {
|
|
1443
|
-
if (tileLeaveEvent) {
|
|
1444
|
-
emit(tileLeaveEvent, {});
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
optionsRef.current.onTileHover?.(tile, event);
|
|
1448
|
-
},
|
|
1449
|
-
[tileHoverEvent, tileLeaveEvent, emit]
|
|
1450
|
-
);
|
|
1451
|
-
const handleUnitAnimation = useCallback(
|
|
1452
|
-
(unitId, state) => {
|
|
1453
|
-
if (unitAnimationEvent) {
|
|
1454
|
-
emit(unitAnimationEvent, {
|
|
1455
|
-
unitId,
|
|
1456
|
-
state,
|
|
1457
|
-
timestamp: Date.now()
|
|
1458
|
-
});
|
|
1459
|
-
}
|
|
1460
|
-
optionsRef.current.onUnitAnimation?.(unitId, state);
|
|
1461
|
-
},
|
|
1462
|
-
[unitAnimationEvent, emit]
|
|
1463
|
-
);
|
|
1464
|
-
const handleCameraChange = useCallback(
|
|
1465
|
-
(position) => {
|
|
1466
|
-
if (cameraChangeEvent) {
|
|
1467
|
-
emit(cameraChangeEvent, {
|
|
1468
|
-
position,
|
|
1469
|
-
timestamp: Date.now()
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
|
-
},
|
|
1473
|
-
[cameraChangeEvent, emit]
|
|
1474
|
-
);
|
|
1475
|
-
return {
|
|
1476
|
-
handleTileClick,
|
|
1477
|
-
handleUnitClick,
|
|
1478
|
-
handleFeatureClick,
|
|
1479
|
-
handleCanvasClick,
|
|
1480
|
-
handleTileHover,
|
|
1481
|
-
handleUnitAnimation,
|
|
1482
|
-
handleCameraChange
|
|
1483
|
-
};
|
|
1484
|
-
}
|
|
1485
|
-
var DEFAULT_TERRAIN_COLORS = {
|
|
1486
|
-
grass: "#44aa44",
|
|
1487
|
-
dirt: "#8b7355",
|
|
1488
|
-
sand: "#ddcc88",
|
|
1489
|
-
water: "#4488cc",
|
|
1490
|
-
rock: "#888888",
|
|
1491
|
-
snow: "#eeeeee",
|
|
1492
|
-
forest: "#228b22",
|
|
1493
|
-
desert: "#d4a574",
|
|
1494
|
-
mountain: "#696969",
|
|
1495
|
-
swamp: "#556b2f"
|
|
1496
|
-
};
|
|
1497
|
-
function TileRenderer({
|
|
1498
|
-
tiles,
|
|
1499
|
-
cellSize = 1,
|
|
1500
|
-
offsetX = 0,
|
|
1501
|
-
offsetZ = 0,
|
|
1502
|
-
useInstancing = true,
|
|
1503
|
-
terrainColors = DEFAULT_TERRAIN_COLORS,
|
|
1504
|
-
onTileClick,
|
|
1505
|
-
onTileHover,
|
|
1506
|
-
selectedTileIds = [],
|
|
1507
|
-
validMoves = [],
|
|
1508
|
-
attackTargets = []
|
|
1509
|
-
}) {
|
|
1510
|
-
const meshRef = useRef(null);
|
|
1511
|
-
const geometry = useMemo(() => {
|
|
1512
|
-
return new THREE6.BoxGeometry(cellSize * 0.95, 0.2, cellSize * 0.95);
|
|
1513
|
-
}, [cellSize]);
|
|
1514
|
-
const material = useMemo(() => {
|
|
1515
|
-
return new THREE6.MeshStandardMaterial({
|
|
1516
|
-
roughness: 0.8,
|
|
1517
|
-
metalness: 0.1
|
|
1518
|
-
});
|
|
1519
|
-
}, []);
|
|
1520
|
-
const { positions, colors, tileMap } = useMemo(() => {
|
|
1521
|
-
const pos = [];
|
|
1522
|
-
const cols = [];
|
|
1523
|
-
const map = /* @__PURE__ */ new Map();
|
|
1524
|
-
tiles.forEach((tile) => {
|
|
1525
|
-
const x = (tile.x - offsetX) * cellSize;
|
|
1526
|
-
const z = ((tile.z ?? tile.y ?? 0) - offsetZ) * cellSize;
|
|
1527
|
-
const y = (tile.elevation ?? 0) * 0.1;
|
|
1528
|
-
pos.push(new THREE6.Vector3(x, y, z));
|
|
1529
|
-
const colorHex = terrainColors[tile.type || ""] || terrainColors[tile.terrain || ""] || "#808080";
|
|
1530
|
-
const color = new THREE6.Color(colorHex);
|
|
1531
|
-
const isValidMove = validMoves.some(
|
|
1532
|
-
(m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
|
|
1533
|
-
);
|
|
1534
|
-
const isAttackTarget = attackTargets.some(
|
|
1535
|
-
(m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
|
|
1536
|
-
);
|
|
1537
|
-
const isSelected = tile.id ? selectedTileIds.includes(tile.id) : false;
|
|
1538
|
-
if (isSelected) {
|
|
1539
|
-
color.addScalar(0.3);
|
|
1540
|
-
} else if (isAttackTarget) {
|
|
1541
|
-
color.setHex(16729156);
|
|
1542
|
-
} else if (isValidMove) {
|
|
1543
|
-
color.setHex(4521796);
|
|
1544
|
-
}
|
|
1545
|
-
cols.push(color);
|
|
1546
|
-
map.set(`${tile.x},${tile.z ?? tile.y ?? 0}`, tile);
|
|
1547
|
-
});
|
|
1548
|
-
return { positions: pos, colors: cols, tileMap: map };
|
|
1549
|
-
}, [tiles, cellSize, offsetX, offsetZ, terrainColors, selectedTileIds, validMoves, attackTargets]);
|
|
1550
|
-
useEffect(() => {
|
|
1551
|
-
if (!meshRef.current || !useInstancing) return;
|
|
1552
|
-
const mesh = meshRef.current;
|
|
1553
|
-
mesh.count = positions.length;
|
|
1554
|
-
const dummy = new THREE6.Object3D();
|
|
1555
|
-
positions.forEach((pos, i) => {
|
|
1556
|
-
dummy.position.copy(pos);
|
|
1557
|
-
dummy.updateMatrix();
|
|
1558
|
-
mesh.setMatrixAt(i, dummy.matrix);
|
|
1559
|
-
if (mesh.setColorAt) {
|
|
1560
|
-
mesh.setColorAt(i, colors[i]);
|
|
1561
|
-
}
|
|
1562
|
-
});
|
|
1563
|
-
mesh.instanceMatrix.needsUpdate = true;
|
|
1564
|
-
if (mesh.instanceColor) {
|
|
1565
|
-
mesh.instanceColor.needsUpdate = true;
|
|
1566
|
-
}
|
|
1567
|
-
}, [positions, colors, useInstancing]);
|
|
1568
|
-
const handlePointerMove = (e) => {
|
|
1569
|
-
if (!onTileHover) return;
|
|
1570
|
-
const instanceId = e.instanceId;
|
|
1571
|
-
if (instanceId !== void 0) {
|
|
1572
|
-
const pos = positions[instanceId];
|
|
1573
|
-
if (pos) {
|
|
1574
|
-
const gridX = Math.round(pos.x / cellSize + offsetX);
|
|
1575
|
-
const gridZ = Math.round(pos.z / cellSize + offsetZ);
|
|
1576
|
-
const tile = tileMap.get(`${gridX},${gridZ}`);
|
|
1577
|
-
if (tile) {
|
|
1578
|
-
onTileHover(tile);
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
};
|
|
1583
|
-
const handleClick = (e) => {
|
|
1584
|
-
if (!onTileClick) return;
|
|
1585
|
-
const instanceId = e.instanceId;
|
|
1586
|
-
if (instanceId !== void 0) {
|
|
1587
|
-
const pos = positions[instanceId];
|
|
1588
|
-
if (pos) {
|
|
1589
|
-
const gridX = Math.round(pos.x / cellSize + offsetX);
|
|
1590
|
-
const gridZ = Math.round(pos.z / cellSize + offsetZ);
|
|
1591
|
-
const tile = tileMap.get(`${gridX},${gridZ}`);
|
|
1592
|
-
if (tile) {
|
|
1593
|
-
onTileClick(tile);
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
};
|
|
1598
|
-
const renderIndividualTiles = () => {
|
|
1599
|
-
return tiles.map((tile) => {
|
|
1600
|
-
const x = (tile.x - offsetX) * cellSize;
|
|
1601
|
-
const z = ((tile.z ?? tile.y ?? 0) - offsetZ) * cellSize;
|
|
1602
|
-
const y = (tile.elevation ?? 0) * 0.1;
|
|
1603
|
-
const colorHex = terrainColors[tile.type || ""] || terrainColors[tile.terrain || ""] || "#808080";
|
|
1604
|
-
const isSelected = tile.id ? selectedTileIds.includes(tile.id) : false;
|
|
1605
|
-
const isValidMove = validMoves.some(
|
|
1606
|
-
(m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
|
|
1607
|
-
);
|
|
1608
|
-
const isAttackTarget = attackTargets.some(
|
|
1609
|
-
(m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
|
|
1610
|
-
);
|
|
1611
|
-
let emissive = "#000000";
|
|
1612
|
-
if (isSelected) emissive = "#444444";
|
|
1613
|
-
else if (isAttackTarget) emissive = "#440000";
|
|
1614
|
-
else if (isValidMove) emissive = "#004400";
|
|
1615
|
-
return /* @__PURE__ */ jsxs(
|
|
1616
|
-
"mesh",
|
|
1617
|
-
{
|
|
1618
|
-
position: [x, y, z],
|
|
1619
|
-
userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
|
|
1620
|
-
onClick: () => onTileClick?.(tile),
|
|
1621
|
-
onPointerEnter: () => onTileHover?.(tile),
|
|
1622
|
-
onPointerLeave: () => onTileHover?.(null),
|
|
1623
|
-
children: [
|
|
1624
|
-
/* @__PURE__ */ jsx("boxGeometry", { args: [cellSize * 0.95, 0.2, cellSize * 0.95] }),
|
|
1625
|
-
/* @__PURE__ */ jsx(
|
|
1626
|
-
"meshStandardMaterial",
|
|
1627
|
-
{
|
|
1628
|
-
color: colorHex,
|
|
1629
|
-
emissive,
|
|
1630
|
-
roughness: 0.8,
|
|
1631
|
-
metalness: 0.1
|
|
1632
|
-
}
|
|
1633
|
-
)
|
|
1634
|
-
]
|
|
1635
|
-
},
|
|
1636
|
-
tile.id ?? `tile-${tile.x}-${tile.y}`
|
|
1637
|
-
);
|
|
1638
|
-
});
|
|
1639
|
-
};
|
|
1640
|
-
if (useInstancing && tiles.length > 0) {
|
|
1641
|
-
return /* @__PURE__ */ jsx(
|
|
1642
|
-
"instancedMesh",
|
|
1643
|
-
{
|
|
1644
|
-
ref: meshRef,
|
|
1645
|
-
args: [geometry, material, tiles.length],
|
|
1646
|
-
onPointerMove: handlePointerMove,
|
|
1647
|
-
onClick: handleClick
|
|
1648
|
-
}
|
|
1649
|
-
);
|
|
1650
|
-
}
|
|
1651
|
-
return /* @__PURE__ */ jsx("group", { children: renderIndividualTiles() });
|
|
1652
|
-
}
|
|
1653
|
-
function UnitVisual({ unit, position, isSelected, onClick }) {
|
|
1654
|
-
const groupRef = useRef(null);
|
|
1655
|
-
const [animationState, setAnimationState] = useState("idle");
|
|
1656
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
1657
|
-
const teamColor = useMemo(() => {
|
|
1658
|
-
if (unit.faction === "player" || unit.team === "player") return 4491519;
|
|
1659
|
-
if (unit.faction === "enemy" || unit.team === "enemy") return 16729156;
|
|
1660
|
-
if (unit.faction === "neutral" || unit.team === "neutral") return 16777028;
|
|
1661
|
-
return 8947848;
|
|
1662
|
-
}, [unit.faction, unit.team]);
|
|
1663
|
-
useFrame((state) => {
|
|
1664
|
-
if (groupRef.current && animationState === "idle") {
|
|
1665
|
-
const y = position[1] + Math.sin(state.clock.elapsedTime * 2 + position[0]) * 0.05;
|
|
1666
|
-
groupRef.current.position.y = y;
|
|
1667
|
-
}
|
|
1668
|
-
});
|
|
1669
|
-
const healthPercent = useMemo(() => {
|
|
1670
|
-
if (unit.health === void 0 || unit.maxHealth === void 0) return 1;
|
|
1671
|
-
return Math.max(0, Math.min(1, unit.health / unit.maxHealth));
|
|
1672
|
-
}, [unit.health, unit.maxHealth]);
|
|
1673
|
-
const healthColor = useMemo(() => {
|
|
1674
|
-
if (healthPercent > 0.5) return "#44aa44";
|
|
1675
|
-
if (healthPercent > 0.25) return "#aaaa44";
|
|
1676
|
-
return "#ff4444";
|
|
1677
|
-
}, [healthPercent]);
|
|
1678
|
-
return /* @__PURE__ */ jsxs(
|
|
1679
|
-
"group",
|
|
1680
|
-
{
|
|
1681
|
-
ref: groupRef,
|
|
1682
|
-
position,
|
|
1683
|
-
onClick,
|
|
1684
|
-
onPointerEnter: () => setIsHovered(true),
|
|
1685
|
-
onPointerLeave: () => setIsHovered(false),
|
|
1686
|
-
userData: { type: "unit", unitId: unit.id },
|
|
1687
|
-
children: [
|
|
1688
|
-
isSelected && /* @__PURE__ */ jsxs("mesh", { position: [0, 0.05, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
1689
|
-
/* @__PURE__ */ jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
1690
|
-
/* @__PURE__ */ jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
|
|
1691
|
-
] }),
|
|
1692
|
-
isHovered && !isSelected && /* @__PURE__ */ jsxs("mesh", { position: [0, 0.05, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
1693
|
-
/* @__PURE__ */ jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
1694
|
-
/* @__PURE__ */ jsx("meshBasicMaterial", { color: "#ffffff", transparent: true, opacity: 0.5 })
|
|
1695
|
-
] }),
|
|
1696
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, 0.1, 0], children: [
|
|
1697
|
-
/* @__PURE__ */ jsx("cylinderGeometry", { args: [0.25, 0.25, 0.1, 8] }),
|
|
1698
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color: teamColor })
|
|
1699
|
-
] }),
|
|
1700
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, 0.5, 0], children: [
|
|
1701
|
-
/* @__PURE__ */ jsx("capsuleGeometry", { args: [0.15, 0.5, 4, 8] }),
|
|
1702
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color: teamColor })
|
|
1703
|
-
] }),
|
|
1704
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, 0.9, 0], children: [
|
|
1705
|
-
/* @__PURE__ */ jsx("sphereGeometry", { args: [0.12, 8, 8] }),
|
|
1706
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color: teamColor })
|
|
1707
|
-
] }),
|
|
1708
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, 1.3, 0], children: [
|
|
1709
|
-
/* @__PURE__ */ jsx("planeGeometry", { args: [0.5, 0.06] }),
|
|
1710
|
-
/* @__PURE__ */ jsx("meshBasicMaterial", { color: "#333333" })
|
|
1711
|
-
] }),
|
|
1712
|
-
/* @__PURE__ */ jsxs("mesh", { position: [-0.25 + 0.25 * healthPercent, 1.3, 0.01], children: [
|
|
1713
|
-
/* @__PURE__ */ jsx("planeGeometry", { args: [0.5 * healthPercent, 0.04] }),
|
|
1714
|
-
/* @__PURE__ */ jsx("meshBasicMaterial", { color: healthColor })
|
|
1715
|
-
] }),
|
|
1716
|
-
unit.name && /* @__PURE__ */ jsxs("mesh", { position: [0, 1.5, 0], children: [
|
|
1717
|
-
/* @__PURE__ */ jsx("planeGeometry", { args: [0.4, 0.1] }),
|
|
1718
|
-
/* @__PURE__ */ jsx("meshBasicMaterial", { color: "#000000", transparent: true, opacity: 0.5 })
|
|
1719
|
-
] })
|
|
1720
|
-
]
|
|
1721
|
-
}
|
|
1722
|
-
);
|
|
1723
|
-
}
|
|
1724
|
-
function UnitRenderer({
|
|
1725
|
-
units,
|
|
1726
|
-
cellSize = 1,
|
|
1727
|
-
offsetX = 0,
|
|
1728
|
-
offsetZ = 0,
|
|
1729
|
-
selectedUnitId,
|
|
1730
|
-
onUnitClick,
|
|
1731
|
-
onAnimationStateChange,
|
|
1732
|
-
animationSpeed = 1
|
|
1733
|
-
}) {
|
|
1734
|
-
const handleUnitClick = React7.useCallback(
|
|
1735
|
-
(unit) => {
|
|
1736
|
-
onUnitClick?.(unit);
|
|
1737
|
-
},
|
|
1738
|
-
[onUnitClick]
|
|
1739
|
-
);
|
|
1740
|
-
return /* @__PURE__ */ jsx("group", { children: units.map((unit) => {
|
|
1741
|
-
const unitX = unit.x ?? unit.position?.x ?? 0;
|
|
1742
|
-
const unitY = unit.z ?? unit.y ?? unit.position?.y ?? 0;
|
|
1743
|
-
const x = (unitX - offsetX) * cellSize;
|
|
1744
|
-
const z = (unitY - offsetZ) * cellSize;
|
|
1745
|
-
const y = (unit.elevation ?? 0) * 0.1 + 0.5;
|
|
1746
|
-
return /* @__PURE__ */ jsx(
|
|
1747
|
-
UnitVisual,
|
|
1748
|
-
{
|
|
1749
|
-
unit,
|
|
1750
|
-
position: [x, y, z],
|
|
1751
|
-
isSelected: selectedUnitId === unit.id,
|
|
1752
|
-
onClick: () => handleUnitClick(unit)
|
|
1753
|
-
},
|
|
1754
|
-
unit.id
|
|
1755
|
-
);
|
|
1756
|
-
}) });
|
|
1757
|
-
}
|
|
1758
|
-
var DEFAULT_FEATURE_CONFIGS = {
|
|
1759
|
-
tree: { color: 2263842, height: 1.5, scale: 1, geometry: "tree" },
|
|
1760
|
-
rock: { color: 8421504, height: 0.5, scale: 0.8, geometry: "rock" },
|
|
1761
|
-
bush: { color: 3329330, height: 0.4, scale: 0.6, geometry: "bush" },
|
|
1762
|
-
house: { color: 9127187, height: 1.2, scale: 1.2, geometry: "house" },
|
|
1763
|
-
tower: { color: 6908265, height: 2.5, scale: 1, geometry: "tower" },
|
|
1764
|
-
wall: { color: 8421504, height: 1, scale: 1, geometry: "wall" },
|
|
1765
|
-
mountain: { color: 5597999, height: 2, scale: 1.5, geometry: "mountain" },
|
|
1766
|
-
hill: { color: 7048739, height: 0.8, scale: 1.2, geometry: "hill" },
|
|
1767
|
-
water: { color: 4491468, height: 0.1, scale: 1, geometry: "water" },
|
|
1768
|
-
chest: { color: 16766720, height: 0.3, scale: 0.4, geometry: "chest" },
|
|
1769
|
-
sign: { color: 9127187, height: 0.8, scale: 0.3, geometry: "sign" },
|
|
1770
|
-
portal: { color: 10040012, height: 1.5, scale: 1, geometry: "portal" }
|
|
1771
|
-
};
|
|
1772
|
-
function TreeFeature({ height, color }) {
|
|
1773
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1774
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, height * 0.3, 0], children: [
|
|
1775
|
-
/* @__PURE__ */ jsx("cylinderGeometry", { args: [0.08, 0.1, height * 0.6, 6] }),
|
|
1776
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color: 9127187 })
|
|
1777
|
-
] }),
|
|
1778
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, height * 0.7, 0], children: [
|
|
1779
|
-
/* @__PURE__ */ jsx("coneGeometry", { args: [0.4, height * 0.5, 8] }),
|
|
1780
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
1781
|
-
] }),
|
|
1782
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, height * 0.9, 0], children: [
|
|
1783
|
-
/* @__PURE__ */ jsx("coneGeometry", { args: [0.3, height * 0.4, 8] }),
|
|
1784
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
1785
|
-
] }),
|
|
1786
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, height * 1.05, 0], children: [
|
|
1787
|
-
/* @__PURE__ */ jsx("coneGeometry", { args: [0.15, height * 0.25, 8] }),
|
|
1788
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
1789
|
-
] })
|
|
1790
|
-
] });
|
|
1791
|
-
}
|
|
1792
|
-
function RockFeature({ height, color }) {
|
|
1793
|
-
return /* @__PURE__ */ jsxs("mesh", { position: [0, height * 0.4, 0], children: [
|
|
1794
|
-
/* @__PURE__ */ jsx("dodecahedronGeometry", { args: [height * 0.5, 0] }),
|
|
1795
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color, roughness: 0.9 })
|
|
1796
|
-
] });
|
|
1797
|
-
}
|
|
1798
|
-
function BushFeature({ height, color }) {
|
|
1799
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1800
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, height * 0.3, 0], children: [
|
|
1801
|
-
/* @__PURE__ */ jsx("sphereGeometry", { args: [height * 0.4, 8, 8] }),
|
|
1802
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
1803
|
-
] }),
|
|
1804
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0.1, height * 0.4, 0.1], children: [
|
|
1805
|
-
/* @__PURE__ */ jsx("sphereGeometry", { args: [height * 0.25, 8, 8] }),
|
|
1806
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
1807
|
-
] })
|
|
1808
|
-
] });
|
|
1809
|
-
}
|
|
1810
|
-
function HouseFeature({ height, color }) {
|
|
1811
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1812
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, height * 0.4, 0], children: [
|
|
1813
|
-
/* @__PURE__ */ jsx("boxGeometry", { args: [0.8, height * 0.8, 0.8] }),
|
|
1814
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color: 13808780 })
|
|
1815
|
-
] }),
|
|
1816
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, height * 0.9, 0], children: [
|
|
1817
|
-
/* @__PURE__ */ jsx("coneGeometry", { args: [0.6, height * 0.4, 4] }),
|
|
1818
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
1819
|
-
] }),
|
|
1820
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, height * 0.25, 0.41], children: [
|
|
1821
|
-
/* @__PURE__ */ jsx("planeGeometry", { args: [0.25, height * 0.5] }),
|
|
1822
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color: 4863784 })
|
|
1823
|
-
] })
|
|
1824
|
-
] });
|
|
1825
|
-
}
|
|
1826
|
-
function TowerFeature({ height, color }) {
|
|
1827
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1828
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, height * 0.5, 0], children: [
|
|
1829
|
-
/* @__PURE__ */ jsx("cylinderGeometry", { args: [0.3, 0.35, height, 8] }),
|
|
1830
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
1831
|
-
] }),
|
|
1832
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, height + 0.05, 0], children: [
|
|
1833
|
-
/* @__PURE__ */ jsx("cylinderGeometry", { args: [0.35, 0.35, 0.1, 8] }),
|
|
1834
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
1835
|
-
] })
|
|
1836
|
-
] });
|
|
1837
|
-
}
|
|
1838
|
-
function ChestFeature({ height, color }) {
|
|
1839
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1840
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, height * 0.5, 0], children: [
|
|
1841
|
-
/* @__PURE__ */ jsx("boxGeometry", { args: [0.3, height, 0.2] }),
|
|
1842
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color, metalness: 0.6, roughness: 0.3 })
|
|
1843
|
-
] }),
|
|
1844
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, height + 0.05, 0], children: [
|
|
1845
|
-
/* @__PURE__ */ jsx("cylinderGeometry", { args: [0.15, 0.15, 0.3, 8, 1, false, 0, Math.PI] }),
|
|
1846
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color, metalness: 0.6, roughness: 0.3 })
|
|
1847
|
-
] })
|
|
1848
|
-
] });
|
|
1849
|
-
}
|
|
1850
|
-
function DefaultFeature({ height, color }) {
|
|
1851
|
-
return /* @__PURE__ */ jsxs("mesh", { position: [0, height * 0.5, 0], children: [
|
|
1852
|
-
/* @__PURE__ */ jsx("boxGeometry", { args: [0.5, height, 0.5] }),
|
|
1853
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color })
|
|
1854
|
-
] });
|
|
1855
|
-
}
|
|
1856
|
-
function FeatureVisual({
|
|
1857
|
-
feature,
|
|
1858
|
-
position,
|
|
1859
|
-
isSelected,
|
|
1860
|
-
onClick,
|
|
1861
|
-
onHover
|
|
1862
|
-
}) {
|
|
1863
|
-
const config = DEFAULT_FEATURE_CONFIGS[feature.type] || {
|
|
1864
|
-
color: 8947848,
|
|
1865
|
-
height: 0.5,
|
|
1866
|
-
scale: 1,
|
|
1867
|
-
geometry: "default"
|
|
1868
|
-
};
|
|
1869
|
-
const color = feature.color ? parseInt(feature.color.replace("#", ""), 16) : config.color;
|
|
1870
|
-
const renderGeometry = () => {
|
|
1871
|
-
switch (config.geometry) {
|
|
1872
|
-
case "tree":
|
|
1873
|
-
return /* @__PURE__ */ jsx(TreeFeature, { height: config.height, color });
|
|
1874
|
-
case "rock":
|
|
1875
|
-
return /* @__PURE__ */ jsx(RockFeature, { height: config.height, color });
|
|
1876
|
-
case "bush":
|
|
1877
|
-
return /* @__PURE__ */ jsx(BushFeature, { height: config.height, color });
|
|
1878
|
-
case "house":
|
|
1879
|
-
return /* @__PURE__ */ jsx(HouseFeature, { height: config.height, color });
|
|
1880
|
-
case "tower":
|
|
1881
|
-
return /* @__PURE__ */ jsx(TowerFeature, { height: config.height, color });
|
|
1882
|
-
case "chest":
|
|
1883
|
-
return /* @__PURE__ */ jsx(ChestFeature, { height: config.height, color });
|
|
1884
|
-
default:
|
|
1885
|
-
return /* @__PURE__ */ jsx(DefaultFeature, { height: config.height, color });
|
|
1886
|
-
}
|
|
1887
|
-
};
|
|
1888
|
-
return /* @__PURE__ */ jsxs(
|
|
1889
|
-
"group",
|
|
1890
|
-
{
|
|
1891
|
-
position,
|
|
1892
|
-
scale: config.scale,
|
|
1893
|
-
onClick,
|
|
1894
|
-
onPointerEnter: () => onHover(true),
|
|
1895
|
-
onPointerLeave: () => onHover(false),
|
|
1896
|
-
userData: { type: "feature", featureId: feature.id, featureType: feature.type },
|
|
1897
|
-
children: [
|
|
1898
|
-
isSelected && /* @__PURE__ */ jsxs("mesh", { position: [0, 0.02, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
1899
|
-
/* @__PURE__ */ jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
1900
|
-
/* @__PURE__ */ jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
|
|
1901
|
-
] }),
|
|
1902
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, 0.01, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
1903
|
-
/* @__PURE__ */ jsx("circleGeometry", { args: [0.35, 16] }),
|
|
1904
|
-
/* @__PURE__ */ jsx("meshBasicMaterial", { color: "#000000", transparent: true, opacity: 0.2 })
|
|
1905
|
-
] }),
|
|
1906
|
-
renderGeometry()
|
|
1907
|
-
]
|
|
1908
|
-
}
|
|
1909
|
-
);
|
|
1910
|
-
}
|
|
1911
|
-
function FeatureRenderer({
|
|
1912
|
-
features,
|
|
1913
|
-
cellSize = 1,
|
|
1914
|
-
offsetX = 0,
|
|
1915
|
-
offsetZ = 0,
|
|
1916
|
-
onFeatureClick,
|
|
1917
|
-
onFeatureHover,
|
|
1918
|
-
selectedFeatureIds = [],
|
|
1919
|
-
featureColors
|
|
1920
|
-
}) {
|
|
1921
|
-
return /* @__PURE__ */ jsx("group", { children: features.map((feature) => {
|
|
1922
|
-
const x = (feature.x - offsetX) * cellSize;
|
|
1923
|
-
const z = ((feature.z ?? feature.y ?? 0) - offsetZ) * cellSize;
|
|
1924
|
-
const y = (feature.elevation ?? 0) * 0.1;
|
|
1925
|
-
const isSelected = feature.id ? selectedFeatureIds.includes(feature.id) : false;
|
|
1926
|
-
return /* @__PURE__ */ jsx(
|
|
1927
|
-
FeatureVisual,
|
|
1928
|
-
{
|
|
1929
|
-
feature,
|
|
1930
|
-
position: [x, y, z],
|
|
1931
|
-
isSelected,
|
|
1932
|
-
onClick: () => onFeatureClick?.(feature),
|
|
1933
|
-
onHover: (hovered) => onFeatureHover?.(hovered ? feature : null)
|
|
1934
|
-
},
|
|
1935
|
-
feature.id ?? `feature-${feature.x}-${feature.y}`
|
|
1936
|
-
);
|
|
1937
|
-
}) });
|
|
1938
|
-
}
|
|
1939
|
-
function detectAssetRoot3(modelUrl) {
|
|
1940
|
-
const idx = modelUrl.indexOf("/3d/");
|
|
1941
|
-
if (idx !== -1) {
|
|
1942
|
-
return modelUrl.substring(0, idx + 4);
|
|
1943
|
-
}
|
|
1944
|
-
return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
|
|
1945
|
-
}
|
|
1946
|
-
function useGLTFModel2(url) {
|
|
1947
|
-
const [model, setModel] = useState(null);
|
|
1948
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
1949
|
-
const [error, setError] = useState(null);
|
|
1950
|
-
useEffect(() => {
|
|
1951
|
-
if (!url) {
|
|
1952
|
-
setModel(null);
|
|
1953
|
-
return;
|
|
1954
|
-
}
|
|
1955
|
-
setIsLoading(true);
|
|
1956
|
-
setError(null);
|
|
1957
|
-
const assetRoot = detectAssetRoot3(url);
|
|
1958
|
-
const loader = new GLTFLoader$1();
|
|
1959
|
-
loader.setResourcePath(assetRoot);
|
|
1960
|
-
loader.load(
|
|
1961
|
-
url,
|
|
1962
|
-
(gltf) => {
|
|
1963
|
-
setModel(gltf.scene);
|
|
1964
|
-
setIsLoading(false);
|
|
1965
|
-
},
|
|
1966
|
-
void 0,
|
|
1967
|
-
(err) => {
|
|
1968
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1969
|
-
setIsLoading(false);
|
|
1970
|
-
}
|
|
1971
|
-
);
|
|
1972
|
-
}, [url]);
|
|
1973
|
-
return { model, isLoading, error };
|
|
1974
|
-
}
|
|
1975
|
-
function FeatureModel({
|
|
1976
|
-
feature,
|
|
1977
|
-
position,
|
|
1978
|
-
isSelected,
|
|
1979
|
-
onClick,
|
|
1980
|
-
onHover
|
|
1981
|
-
}) {
|
|
1982
|
-
const groupRef = useRef(null);
|
|
1983
|
-
const { model: loadedModel, isLoading } = useGLTFModel2(feature.assetUrl);
|
|
1984
|
-
const model = useMemo(() => {
|
|
1985
|
-
if (!loadedModel) return null;
|
|
1986
|
-
const cloned = loadedModel.clone();
|
|
1987
|
-
cloned.scale.setScalar(0.3);
|
|
1988
|
-
cloned.traverse((child) => {
|
|
1989
|
-
if (child instanceof THREE6.Mesh) {
|
|
1990
|
-
child.castShadow = true;
|
|
1991
|
-
child.receiveShadow = true;
|
|
1992
|
-
}
|
|
1993
|
-
});
|
|
1994
|
-
return cloned;
|
|
1995
|
-
}, [loadedModel]);
|
|
1996
|
-
useFrame((state) => {
|
|
1997
|
-
if (groupRef.current) {
|
|
1998
|
-
const featureRotation = feature.rotation;
|
|
1999
|
-
const baseRotation = featureRotation !== void 0 ? featureRotation * Math.PI / 180 - Math.PI / 4 : -Math.PI / 4;
|
|
2000
|
-
const wobble = isSelected ? Math.sin(state.clock.elapsedTime * 2) * 0.1 : 0;
|
|
2001
|
-
groupRef.current.rotation.y = baseRotation + wobble;
|
|
2002
|
-
}
|
|
2003
|
-
});
|
|
2004
|
-
if (isLoading) {
|
|
2005
|
-
return /* @__PURE__ */ jsx("group", { position, children: /* @__PURE__ */ jsxs("mesh", { rotation: [Math.PI / 2, 0, 0], children: [
|
|
2006
|
-
/* @__PURE__ */ jsx("ringGeometry", { args: [0.3, 0.35, 16] }),
|
|
2007
|
-
/* @__PURE__ */ jsx("meshBasicMaterial", { color: "#4a90d9", transparent: true, opacity: 0.8 })
|
|
2008
|
-
] }) });
|
|
2009
|
-
}
|
|
2010
|
-
if (!model && !feature.assetUrl) {
|
|
2011
|
-
return /* @__PURE__ */ jsxs(
|
|
2012
|
-
"group",
|
|
2013
|
-
{
|
|
2014
|
-
position,
|
|
2015
|
-
onClick,
|
|
2016
|
-
onPointerEnter: () => onHover(true),
|
|
2017
|
-
onPointerLeave: () => onHover(false),
|
|
2018
|
-
userData: { type: "feature", featureId: feature.id, featureType: feature.type },
|
|
2019
|
-
children: [
|
|
2020
|
-
isSelected && /* @__PURE__ */ jsxs("mesh", { position: [0, 0.02, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
2021
|
-
/* @__PURE__ */ jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
2022
|
-
/* @__PURE__ */ jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
|
|
2023
|
-
] }),
|
|
2024
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, 0.5, 0], children: [
|
|
2025
|
-
/* @__PURE__ */ jsx("boxGeometry", { args: [0.4, 0.4, 0.4] }),
|
|
2026
|
-
/* @__PURE__ */ jsx("meshStandardMaterial", { color: 8947848 })
|
|
2027
|
-
] })
|
|
2028
|
-
]
|
|
2029
|
-
}
|
|
2030
|
-
);
|
|
2031
|
-
}
|
|
2032
|
-
return /* @__PURE__ */ jsxs(
|
|
2033
|
-
"group",
|
|
2034
|
-
{
|
|
2035
|
-
ref: groupRef,
|
|
2036
|
-
position,
|
|
2037
|
-
onClick,
|
|
2038
|
-
onPointerEnter: () => onHover(true),
|
|
2039
|
-
onPointerLeave: () => onHover(false),
|
|
2040
|
-
userData: { type: "feature", featureId: feature.id, featureType: feature.type },
|
|
2041
|
-
children: [
|
|
2042
|
-
isSelected && /* @__PURE__ */ jsxs("mesh", { position: [0, 0.02, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
2043
|
-
/* @__PURE__ */ jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
2044
|
-
/* @__PURE__ */ jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
|
|
2045
|
-
] }),
|
|
2046
|
-
/* @__PURE__ */ jsxs("mesh", { position: [0, 0.01, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
2047
|
-
/* @__PURE__ */ jsx("circleGeometry", { args: [0.35, 16] }),
|
|
2048
|
-
/* @__PURE__ */ jsx("meshBasicMaterial", { color: "#000000", transparent: true, opacity: 0.2 })
|
|
2049
|
-
] }),
|
|
2050
|
-
model && /* @__PURE__ */ jsx("primitive", { object: model })
|
|
2051
|
-
]
|
|
2052
|
-
}
|
|
2053
|
-
);
|
|
2054
|
-
}
|
|
2055
|
-
function FeatureRenderer3D({
|
|
2056
|
-
features,
|
|
2057
|
-
cellSize = 1,
|
|
2058
|
-
offsetX = 0,
|
|
2059
|
-
offsetZ = 0,
|
|
2060
|
-
onFeatureClick,
|
|
2061
|
-
onFeatureHover,
|
|
2062
|
-
selectedFeatureIds = []
|
|
2063
|
-
}) {
|
|
2064
|
-
return /* @__PURE__ */ jsx("group", { children: features.map((feature) => {
|
|
2065
|
-
const x = (feature.x - offsetX) * cellSize;
|
|
2066
|
-
const z = ((feature.z ?? feature.y ?? 0) - offsetZ) * cellSize;
|
|
2067
|
-
const y = (feature.elevation ?? 0) * 0.1;
|
|
2068
|
-
const isSelected = feature.id ? selectedFeatureIds.includes(feature.id) : false;
|
|
2069
|
-
return /* @__PURE__ */ jsx(
|
|
2070
|
-
FeatureModel,
|
|
2071
|
-
{
|
|
2072
|
-
feature,
|
|
2073
|
-
position: [x, y, z],
|
|
2074
|
-
isSelected,
|
|
2075
|
-
onClick: () => onFeatureClick?.(feature),
|
|
2076
|
-
onHover: (hovered) => onFeatureHover?.(hovered ? feature : null)
|
|
2077
|
-
},
|
|
2078
|
-
feature.id ?? `feature-${feature.x}-${feature.y}`
|
|
2079
|
-
);
|
|
2080
|
-
}) });
|
|
2081
|
-
}
|
|
2082
|
-
function preloadFeatures(urls) {
|
|
2083
|
-
urls.forEach((url) => {
|
|
2084
|
-
if (url) {
|
|
2085
|
-
const loader = new GLTFLoader$1();
|
|
2086
|
-
loader.setResourcePath(detectAssetRoot3(url));
|
|
2087
|
-
loader.load(url, () => {
|
|
2088
|
-
console.log("[FeatureRenderer3D] Preloaded:", url);
|
|
2089
|
-
});
|
|
2090
|
-
}
|
|
2091
|
-
});
|
|
2092
|
-
}
|
|
2093
433
|
var DEFAULT_CONFIG = {
|
|
2094
434
|
cellSize: 1,
|
|
2095
435
|
offsetX: 0,
|
|
@@ -2098,7 +438,7 @@ var DEFAULT_CONFIG = {
|
|
|
2098
438
|
};
|
|
2099
439
|
function gridToWorld(gridX, gridZ, config = DEFAULT_CONFIG) {
|
|
2100
440
|
const opts = { ...DEFAULT_CONFIG, ...config };
|
|
2101
|
-
return new
|
|
441
|
+
return new THREE.Vector3(
|
|
2102
442
|
gridX * opts.cellSize + opts.offsetX,
|
|
2103
443
|
opts.elevation,
|
|
2104
444
|
gridZ * opts.cellSize + opts.offsetZ
|
|
@@ -2112,17 +452,17 @@ function worldToGrid(worldX, worldZ, config = DEFAULT_CONFIG) {
|
|
|
2112
452
|
};
|
|
2113
453
|
}
|
|
2114
454
|
function raycastToPlane(camera, mouseX, mouseY, planeY = 0) {
|
|
2115
|
-
const raycaster = new
|
|
2116
|
-
const mouse = new
|
|
455
|
+
const raycaster = new THREE.Raycaster();
|
|
456
|
+
const mouse = new THREE.Vector2(mouseX, mouseY);
|
|
2117
457
|
raycaster.setFromCamera(mouse, camera);
|
|
2118
|
-
const plane = new
|
|
2119
|
-
const target = new
|
|
458
|
+
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -planeY);
|
|
459
|
+
const target = new THREE.Vector3();
|
|
2120
460
|
const intersection = raycaster.ray.intersectPlane(plane, target);
|
|
2121
461
|
return intersection ? target : null;
|
|
2122
462
|
}
|
|
2123
463
|
function raycastToObjects(camera, mouseX, mouseY, objects) {
|
|
2124
|
-
const raycaster = new
|
|
2125
|
-
const mouse = new
|
|
464
|
+
const raycaster = new THREE.Raycaster();
|
|
465
|
+
const mouse = new THREE.Vector2(mouseX, mouseY);
|
|
2126
466
|
raycaster.setFromCamera(mouse, camera);
|
|
2127
467
|
const intersects = raycaster.intersectObjects(objects, true);
|
|
2128
468
|
return intersects.length > 0 ? intersects[0] : null;
|
|
@@ -2174,14 +514,14 @@ function getCellsInRadius(centerX, centerZ, radius) {
|
|
|
2174
514
|
return cells;
|
|
2175
515
|
}
|
|
2176
516
|
function createGridHighlight(color = 16776960, opacity = 0.3) {
|
|
2177
|
-
const geometry = new
|
|
2178
|
-
const material = new
|
|
517
|
+
const geometry = new THREE.PlaneGeometry(0.95, 0.95);
|
|
518
|
+
const material = new THREE.MeshBasicMaterial({
|
|
2179
519
|
color,
|
|
2180
520
|
transparent: true,
|
|
2181
521
|
opacity,
|
|
2182
|
-
side:
|
|
522
|
+
side: THREE.DoubleSide
|
|
2183
523
|
});
|
|
2184
|
-
const mesh = new
|
|
524
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
2185
525
|
mesh.rotation.x = -Math.PI / 2;
|
|
2186
526
|
mesh.position.y = 0.01;
|
|
2187
527
|
return mesh;
|
|
@@ -2194,31 +534,31 @@ function normalizeMouseCoordinates(clientX, clientY, element) {
|
|
|
2194
534
|
};
|
|
2195
535
|
}
|
|
2196
536
|
function isInFrustum(position, camera, padding = 0) {
|
|
2197
|
-
const frustum = new
|
|
2198
|
-
const projScreenMatrix = new
|
|
537
|
+
const frustum = new THREE.Frustum();
|
|
538
|
+
const projScreenMatrix = new THREE.Matrix4();
|
|
2199
539
|
projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
2200
540
|
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
2201
|
-
const sphere = new
|
|
541
|
+
const sphere = new THREE.Sphere(position, padding);
|
|
2202
542
|
return frustum.intersectsSphere(sphere);
|
|
2203
543
|
}
|
|
2204
544
|
function filterByFrustum(positions, camera, padding = 0) {
|
|
2205
|
-
const frustum = new
|
|
2206
|
-
const projScreenMatrix = new
|
|
545
|
+
const frustum = new THREE.Frustum();
|
|
546
|
+
const projScreenMatrix = new THREE.Matrix4();
|
|
2207
547
|
projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
2208
548
|
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
2209
549
|
return positions.filter((position) => {
|
|
2210
|
-
const sphere = new
|
|
550
|
+
const sphere = new THREE.Sphere(position, padding);
|
|
2211
551
|
return frustum.intersectsSphere(sphere);
|
|
2212
552
|
});
|
|
2213
553
|
}
|
|
2214
554
|
function getVisibleIndices(positions, camera, padding = 0) {
|
|
2215
|
-
const frustum = new
|
|
2216
|
-
const projScreenMatrix = new
|
|
555
|
+
const frustum = new THREE.Frustum();
|
|
556
|
+
const projScreenMatrix = new THREE.Matrix4();
|
|
2217
557
|
const visible = /* @__PURE__ */ new Set();
|
|
2218
558
|
projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
2219
559
|
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
2220
560
|
positions.forEach((position, index) => {
|
|
2221
|
-
const sphere = new
|
|
561
|
+
const sphere = new THREE.Sphere(position, padding);
|
|
2222
562
|
if (frustum.intersectsSphere(sphere)) {
|
|
2223
563
|
visible.add(index);
|
|
2224
564
|
}
|
|
@@ -2242,7 +582,7 @@ function updateInstanceLOD(instancedMesh, positions, camera, lodDistances) {
|
|
|
2242
582
|
return lodIndices;
|
|
2243
583
|
}
|
|
2244
584
|
function cullInstancedMesh(instancedMesh, positions, visibleIndices) {
|
|
2245
|
-
const dummy = new
|
|
585
|
+
const dummy = new THREE.Object3D();
|
|
2246
586
|
let visibleCount = 0;
|
|
2247
587
|
positions.forEach((position, index) => {
|
|
2248
588
|
if (visibleIndices.has(index)) {
|
|
@@ -2375,4 +715,4 @@ var SpatialHashGrid = class {
|
|
|
2375
715
|
}
|
|
2376
716
|
};
|
|
2377
717
|
|
|
2378
|
-
export {
|
|
718
|
+
export { SpatialHashGrid, calculateLODLevel, createGridHighlight, cullInstancedMesh, filterByFrustum, getCellsInRadius, getNeighbors, getVisibleIndices, gridDistance, gridManhattanDistance, gridToWorld, isInBounds, isInFrustum, normalizeMouseCoordinates, raycastToObjects, raycastToPlane, updateInstanceLOD, useRaycaster, useSceneGraph, useThree, worldToGrid };
|