@almadar/ui 2.15.7 → 2.15.10
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/components/atoms/ContentSection.d.ts +16 -0
- package/dist/components/atoms/SectionHeader.d.ts +14 -0
- package/dist/components/atoms/StatCard.d.ts +13 -0
- package/dist/components/atoms/index.d.ts +3 -0
- package/dist/components/atoms/svg/SvgBranch.d.ts +12 -0
- package/dist/components/atoms/svg/SvgConnection.d.ts +13 -0
- package/dist/components/atoms/svg/SvgFlow.d.ts +10 -0
- package/dist/components/atoms/svg/SvgGrid.d.ts +14 -0
- package/dist/components/atoms/svg/SvgLobe.d.ts +13 -0
- package/dist/components/atoms/svg/SvgMesh.d.ts +12 -0
- package/dist/components/atoms/svg/SvgMorph.d.ts +11 -0
- package/dist/components/atoms/svg/SvgNode.d.ts +12 -0
- package/dist/components/atoms/svg/SvgPulse.d.ts +12 -0
- package/dist/components/atoms/svg/SvgRing.d.ts +13 -0
- package/dist/components/atoms/svg/SvgShield.d.ts +11 -0
- package/dist/components/atoms/svg/SvgStack.d.ts +13 -0
- package/dist/components/atoms/svg/index.d.ts +12 -0
- package/dist/{chunk-K43H3ZDY.js → components/index.cjs} +24780 -16651
- package/dist/components/index.js +37757 -889
- package/dist/components/molecules/AnimatedCounter.d.ts +18 -0
- package/dist/components/molecules/ArticleSection.d.ts +18 -0
- package/dist/components/molecules/CTABanner.d.ts +31 -0
- package/dist/components/molecules/CaseStudyCard.d.ts +24 -0
- package/dist/components/molecules/CodeExample.d.ts +23 -0
- package/dist/components/molecules/CommunityLinks.d.ts +25 -0
- package/dist/components/molecules/DocBreadcrumb.d.ts +20 -0
- package/dist/components/molecules/DocCodeBlock.d.ts +13 -0
- package/dist/components/molecules/DocPagination.d.ts +14 -0
- package/dist/components/molecules/DocSearch.d.ts +15 -0
- package/dist/components/molecules/DocSidebar.d.ts +24 -0
- package/dist/components/molecules/DocTOC.d.ts +24 -0
- package/dist/components/molecules/FeatureCard.d.ts +26 -0
- package/dist/components/molecules/FeatureGrid.d.ts +19 -0
- package/dist/components/molecules/GradientDivider.d.ts +14 -0
- package/dist/components/molecules/HeroSection.d.ts +36 -0
- package/dist/components/molecules/InstallBox.d.ts +16 -0
- package/dist/components/molecules/MarketingFooter.d.ts +27 -0
- package/dist/components/molecules/PricingCard.d.ts +21 -0
- package/dist/components/molecules/PricingGrid.d.ts +13 -0
- package/dist/components/molecules/PullQuote.d.ts +14 -0
- package/dist/components/molecules/ServiceCatalog.d.ts +19 -0
- package/dist/components/molecules/ShowcaseCard.d.ts +20 -0
- package/dist/components/molecules/SocialProof.d.ts +25 -0
- package/dist/components/molecules/SplitSection.d.ts +21 -0
- package/dist/components/molecules/StatsGrid.d.ts +17 -0
- package/dist/components/molecules/StepFlow.d.ts +20 -0
- package/dist/components/molecules/TagCloud.d.ts +18 -0
- package/dist/components/molecules/TeamCard.d.ts +18 -0
- package/dist/components/molecules/index.d.ts +19 -0
- package/dist/components/molecules/svg/AIGenerates.d.ts +7 -0
- package/dist/components/molecules/svg/ClosedCircuit.d.ts +7 -0
- package/dist/components/molecules/svg/CommunityOwnership.d.ts +7 -0
- package/dist/components/molecules/svg/CompileAnywhere.d.ts +7 -0
- package/dist/components/molecules/svg/ComposableModels.d.ts +7 -0
- package/dist/components/molecules/svg/DescribeProveDeploy.d.ts +7 -0
- package/dist/components/molecules/svg/DomainGrid.d.ts +7 -0
- package/dist/components/molecules/svg/EventBus.d.ts +7 -0
- package/dist/components/molecules/svg/OrbitalUnit.d.ts +7 -0
- package/dist/components/molecules/svg/PlanVerifyRemember.d.ts +7 -0
- package/dist/components/molecules/svg/ProveCorrect.d.ts +7 -0
- package/dist/components/molecules/svg/ServiceLayers.d.ts +7 -0
- package/dist/components/molecules/svg/SharedReality.d.ts +7 -0
- package/dist/components/molecules/svg/StandardLibrary.d.ts +7 -0
- package/dist/components/molecules/svg/StateMachine.d.ts +7 -0
- package/dist/components/molecules/svg/WorldModel.d.ts +7 -0
- package/dist/components/molecules/svg/index.d.ts +16 -0
- package/dist/components/organisms/CaseStudyOrganism.d.ts +19 -0
- package/dist/components/organisms/FeatureGridOrganism.d.ts +20 -0
- package/dist/components/organisms/HeroOrganism.d.ts +18 -0
- package/dist/components/organisms/PricingOrganism.d.ts +19 -0
- package/dist/components/organisms/ShowcaseOrganism.d.ts +20 -0
- package/dist/components/organisms/StatsOrganism.d.ts +17 -0
- package/dist/components/organisms/StepFlowOrganism.d.ts +20 -0
- package/dist/components/organisms/TeamOrganism.d.ts +18 -0
- package/dist/components/organisms/UISlotRenderer.d.ts +1 -0
- package/dist/components/organisms/game/three/index.cjs +2525 -0
- package/dist/components/organisms/game/three/index.js +1795 -50
- package/dist/components/organisms/index.d.ts +9 -0
- package/dist/components/organisms/marketing-types.d.ts +87 -0
- package/dist/components/templates/AboutPageTemplate.d.ts +26 -0
- package/dist/components/templates/DashboardLayout.d.ts +2 -1
- package/dist/components/templates/FeatureDetailPageTemplate.d.ts +27 -0
- package/dist/components/templates/LandingPageTemplate.d.ts +31 -0
- package/dist/components/templates/PricingPageTemplate.d.ts +26 -0
- package/dist/components/templates/index.d.ts +4 -0
- package/dist/context/index.cjs +550 -0
- package/dist/context/index.js +420 -6
- package/dist/docs/index.cjs +4015 -0
- package/dist/docs/index.d.cts +412 -0
- package/dist/docs/index.d.ts +29 -0
- package/dist/docs/index.js +3977 -0
- package/dist/hooks/index.cjs +2606 -0
- package/dist/hooks/index.js +2535 -8
- package/dist/illustrations/index.cjs +3004 -0
- package/dist/illustrations/index.d.cts +261 -0
- package/dist/illustrations/index.d.ts +35 -0
- package/dist/illustrations/index.js +2971 -0
- package/dist/{chunk-XL7WB2O5.js → lib/index.cjs} +454 -274
- package/dist/lib/index.js +1407 -3
- package/dist/locales/index.cjs +340 -0
- package/dist/locales/index.js +105 -2
- package/dist/marketing/index.cjs +4683 -0
- package/dist/marketing/index.d.cts +831 -0
- package/dist/marketing/index.d.ts +62 -0
- package/dist/marketing/index.js +4626 -0
- package/dist/providers/index.cjs +4811 -0
- package/dist/providers/index.js +4765 -11
- package/dist/{chunk-K2D5D3WK.js → renderer/index.cjs} +101 -42
- package/dist/renderer/index.js +1036 -2
- package/dist/runtime/index.cjs +4400 -0
- package/dist/runtime/index.js +3615 -19
- package/dist/{chunk-N7MVUW4R.js → stores/index.cjs} +24 -1
- package/dist/stores/index.js +194 -2
- package/dist/tsup.config.d.ts +2 -1
- package/package.json +27 -12
- package/tailwind-preset.cjs +27 -2
- package/themes/index.css +22 -20
- package/dist/chunk-3HJHHULT.js +0 -93
- package/dist/chunk-3JGAROCW.js +0 -149
- package/dist/chunk-4N3BAPDB.js +0 -1667
- package/dist/chunk-CDIOHSKG.js +0 -661
- package/dist/chunk-DKQN5FVU.js +0 -279
- package/dist/chunk-GF6RQBO7.js +0 -375
- package/dist/chunk-PKBMQBKP.js +0 -5
- package/dist/chunk-QIABKRCN.js +0 -107
- package/dist/chunk-SD3KVCY6.js +0 -1465
- package/dist/chunk-YXZM3WCF.js +0 -222
|
@@ -0,0 +1,2525 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React8 = require('react');
|
|
4
|
+
var fiber = require('@react-three/fiber');
|
|
5
|
+
var THREE6 = require('three');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
var drei = require('@react-three/drei');
|
|
8
|
+
var GLTFLoader = require('three/examples/jsm/loaders/GLTFLoader');
|
|
9
|
+
var OrbitControls_js = require('three/examples/jsm/controls/OrbitControls.js');
|
|
10
|
+
var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
|
|
11
|
+
var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
|
|
12
|
+
|
|
13
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
|
+
|
|
15
|
+
function _interopNamespace(e) {
|
|
16
|
+
if (e && e.__esModule) return e;
|
|
17
|
+
var n = Object.create(null);
|
|
18
|
+
if (e) {
|
|
19
|
+
Object.keys(e).forEach(function (k) {
|
|
20
|
+
if (k !== 'default') {
|
|
21
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
22
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
get: function () { return e[k]; }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
n.default = e;
|
|
30
|
+
return Object.freeze(n);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var React8__default = /*#__PURE__*/_interopDefault(React8);
|
|
34
|
+
var THREE6__namespace = /*#__PURE__*/_interopNamespace(THREE6);
|
|
35
|
+
|
|
36
|
+
var __defProp = Object.defineProperty;
|
|
37
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
38
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
39
|
+
function Scene3D({ background = "#1a1a2e", fog, children }) {
|
|
40
|
+
const { scene } = fiber.useThree();
|
|
41
|
+
const initializedRef = React8.useRef(false);
|
|
42
|
+
React8.useEffect(() => {
|
|
43
|
+
if (initializedRef.current) return;
|
|
44
|
+
initializedRef.current = true;
|
|
45
|
+
if (background.startsWith("#") || background.startsWith("rgb")) {
|
|
46
|
+
scene.background = new THREE6__namespace.Color(background);
|
|
47
|
+
} else {
|
|
48
|
+
const loader = new THREE6__namespace.TextureLoader();
|
|
49
|
+
loader.load(background, (texture) => {
|
|
50
|
+
scene.background = texture;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (fog) {
|
|
54
|
+
scene.fog = new THREE6__namespace.Fog(fog.color, fog.near, fog.far);
|
|
55
|
+
}
|
|
56
|
+
return () => {
|
|
57
|
+
scene.background = null;
|
|
58
|
+
scene.fog = null;
|
|
59
|
+
};
|
|
60
|
+
}, [scene, background, fog]);
|
|
61
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
62
|
+
}
|
|
63
|
+
var Camera3D = React8.forwardRef(
|
|
64
|
+
({
|
|
65
|
+
mode = "isometric",
|
|
66
|
+
position = [10, 10, 10],
|
|
67
|
+
target = [0, 0, 0],
|
|
68
|
+
zoom = 1,
|
|
69
|
+
fov = 45,
|
|
70
|
+
enableOrbit = true,
|
|
71
|
+
minDistance = 2,
|
|
72
|
+
maxDistance = 100,
|
|
73
|
+
onChange
|
|
74
|
+
}, ref) => {
|
|
75
|
+
const { camera, set, viewport } = fiber.useThree();
|
|
76
|
+
const controlsRef = React8.useRef(null);
|
|
77
|
+
const initialPosition = React8.useRef(new THREE6__namespace.Vector3(...position));
|
|
78
|
+
const initialTarget = React8.useRef(new THREE6__namespace.Vector3(...target));
|
|
79
|
+
React8.useEffect(() => {
|
|
80
|
+
let newCamera;
|
|
81
|
+
if (mode === "isometric") {
|
|
82
|
+
const aspect = viewport.aspect;
|
|
83
|
+
const size = 10 / zoom;
|
|
84
|
+
newCamera = new THREE6__namespace.OrthographicCamera(
|
|
85
|
+
-size * aspect,
|
|
86
|
+
size * aspect,
|
|
87
|
+
size,
|
|
88
|
+
-size,
|
|
89
|
+
0.1,
|
|
90
|
+
1e3
|
|
91
|
+
);
|
|
92
|
+
} else {
|
|
93
|
+
newCamera = new THREE6__namespace.PerspectiveCamera(fov, viewport.aspect, 0.1, 1e3);
|
|
94
|
+
}
|
|
95
|
+
newCamera.position.copy(initialPosition.current);
|
|
96
|
+
newCamera.lookAt(initialTarget.current);
|
|
97
|
+
set({ camera: newCamera });
|
|
98
|
+
if (mode === "top-down") {
|
|
99
|
+
newCamera.position.set(0, 20 / zoom, 0);
|
|
100
|
+
newCamera.lookAt(0, 0, 0);
|
|
101
|
+
}
|
|
102
|
+
return () => {
|
|
103
|
+
};
|
|
104
|
+
}, [mode, fov, zoom, viewport.aspect, set]);
|
|
105
|
+
fiber.useFrame(() => {
|
|
106
|
+
if (onChange) {
|
|
107
|
+
onChange(camera);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
React8.useImperativeHandle(ref, () => ({
|
|
111
|
+
getCamera: () => camera,
|
|
112
|
+
setPosition: (x, y, z) => {
|
|
113
|
+
camera.position.set(x, y, z);
|
|
114
|
+
if (controlsRef.current) {
|
|
115
|
+
controlsRef.current.update();
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
lookAt: (x, y, z) => {
|
|
119
|
+
camera.lookAt(x, y, z);
|
|
120
|
+
if (controlsRef.current) {
|
|
121
|
+
controlsRef.current.target.set(x, y, z);
|
|
122
|
+
controlsRef.current.update();
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
reset: () => {
|
|
126
|
+
camera.position.copy(initialPosition.current);
|
|
127
|
+
camera.lookAt(initialTarget.current);
|
|
128
|
+
if (controlsRef.current) {
|
|
129
|
+
controlsRef.current.target.copy(initialTarget.current);
|
|
130
|
+
controlsRef.current.update();
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
getViewBounds: () => {
|
|
134
|
+
const min = new THREE6__namespace.Vector3(-10, -10, -10);
|
|
135
|
+
const max = new THREE6__namespace.Vector3(10, 10, 10);
|
|
136
|
+
return { min, max };
|
|
137
|
+
}
|
|
138
|
+
}));
|
|
139
|
+
const maxPolarAngle = mode === "top-down" ? 0.1 : Math.PI / 2 - 0.1;
|
|
140
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
141
|
+
drei.OrbitControls,
|
|
142
|
+
{
|
|
143
|
+
ref: controlsRef,
|
|
144
|
+
camera,
|
|
145
|
+
enabled: enableOrbit,
|
|
146
|
+
target: initialTarget.current,
|
|
147
|
+
minDistance,
|
|
148
|
+
maxDistance,
|
|
149
|
+
maxPolarAngle,
|
|
150
|
+
enableDamping: true,
|
|
151
|
+
dampingFactor: 0.05
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
Camera3D.displayName = "Camera3D";
|
|
157
|
+
function Lighting3D({
|
|
158
|
+
ambientIntensity = 0.6,
|
|
159
|
+
ambientColor = "#ffffff",
|
|
160
|
+
directionalIntensity = 0.8,
|
|
161
|
+
directionalColor = "#ffffff",
|
|
162
|
+
directionalPosition = [10, 20, 10],
|
|
163
|
+
shadows = true,
|
|
164
|
+
shadowMapSize = 2048,
|
|
165
|
+
shadowCameraSize = 20,
|
|
166
|
+
showHelpers = false
|
|
167
|
+
}) {
|
|
168
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
169
|
+
/* @__PURE__ */ jsxRuntime.jsx("ambientLight", { intensity: ambientIntensity, color: ambientColor }),
|
|
170
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
171
|
+
"directionalLight",
|
|
172
|
+
{
|
|
173
|
+
position: directionalPosition,
|
|
174
|
+
intensity: directionalIntensity,
|
|
175
|
+
color: directionalColor,
|
|
176
|
+
castShadow: shadows,
|
|
177
|
+
"shadow-mapSize": [shadowMapSize, shadowMapSize],
|
|
178
|
+
"shadow-camera-left": -shadowCameraSize,
|
|
179
|
+
"shadow-camera-right": shadowCameraSize,
|
|
180
|
+
"shadow-camera-top": shadowCameraSize,
|
|
181
|
+
"shadow-camera-bottom": -shadowCameraSize,
|
|
182
|
+
"shadow-camera-near": 0.1,
|
|
183
|
+
"shadow-camera-far": 100,
|
|
184
|
+
"shadow-bias": -1e-3
|
|
185
|
+
}
|
|
186
|
+
),
|
|
187
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
188
|
+
"hemisphereLight",
|
|
189
|
+
{
|
|
190
|
+
intensity: 0.3,
|
|
191
|
+
color: "#87ceeb",
|
|
192
|
+
groundColor: "#362d1d"
|
|
193
|
+
}
|
|
194
|
+
),
|
|
195
|
+
showHelpers && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
196
|
+
"directionalLightHelper",
|
|
197
|
+
{
|
|
198
|
+
args: [
|
|
199
|
+
new THREE6__namespace.DirectionalLight(directionalColor, directionalIntensity),
|
|
200
|
+
5
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
) })
|
|
204
|
+
] });
|
|
205
|
+
}
|
|
206
|
+
function Canvas3DLoadingState({
|
|
207
|
+
progress = 0,
|
|
208
|
+
loaded = 0,
|
|
209
|
+
total = 0,
|
|
210
|
+
message = "Loading 3D Scene...",
|
|
211
|
+
details,
|
|
212
|
+
showSpinner = true,
|
|
213
|
+
className
|
|
214
|
+
}) {
|
|
215
|
+
const clampedProgress = Math.max(0, Math.min(100, progress));
|
|
216
|
+
const hasProgress = total > 0;
|
|
217
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `canvas-3d-loading ${className || ""}`, children: [
|
|
218
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "canvas-3d-loading__content", children: [
|
|
219
|
+
showSpinner && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "canvas-3d-loading__spinner", children: [
|
|
220
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "spinner__ring" }),
|
|
221
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "spinner__ring spinner__ring--secondary" })
|
|
222
|
+
] }),
|
|
223
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "canvas-3d-loading__message", children: message }),
|
|
224
|
+
details && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "canvas-3d-loading__details", children: details }),
|
|
225
|
+
hasProgress && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "canvas-3d-loading__progress", children: [
|
|
226
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "progress__bar", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
227
|
+
"div",
|
|
228
|
+
{
|
|
229
|
+
className: "progress__fill",
|
|
230
|
+
style: { width: `${clampedProgress}%` }
|
|
231
|
+
}
|
|
232
|
+
) }),
|
|
233
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "progress__text", children: [
|
|
234
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "progress__percentage", children: [
|
|
235
|
+
clampedProgress,
|
|
236
|
+
"%"
|
|
237
|
+
] }),
|
|
238
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "progress__count", children: [
|
|
239
|
+
"(",
|
|
240
|
+
loaded,
|
|
241
|
+
"/",
|
|
242
|
+
total,
|
|
243
|
+
")"
|
|
244
|
+
] })
|
|
245
|
+
] })
|
|
246
|
+
] })
|
|
247
|
+
] }),
|
|
248
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "canvas-3d-loading__background", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg__grid" }) })
|
|
249
|
+
] });
|
|
250
|
+
}
|
|
251
|
+
var Canvas3DErrorBoundary = class extends React8.Component {
|
|
252
|
+
constructor(props) {
|
|
253
|
+
super(props);
|
|
254
|
+
__publicField(this, "handleReset", () => {
|
|
255
|
+
this.setState({
|
|
256
|
+
hasError: false,
|
|
257
|
+
error: null,
|
|
258
|
+
errorInfo: null
|
|
259
|
+
});
|
|
260
|
+
this.props.onReset?.();
|
|
261
|
+
});
|
|
262
|
+
this.state = {
|
|
263
|
+
hasError: false,
|
|
264
|
+
error: null,
|
|
265
|
+
errorInfo: null
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
static getDerivedStateFromError(error) {
|
|
269
|
+
return {
|
|
270
|
+
hasError: true,
|
|
271
|
+
error,
|
|
272
|
+
errorInfo: null
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
componentDidCatch(error, errorInfo) {
|
|
276
|
+
this.setState({ errorInfo });
|
|
277
|
+
this.props.onError?.(error, errorInfo);
|
|
278
|
+
console.error("[Canvas3DErrorBoundary] Error caught:", error);
|
|
279
|
+
console.error("[Canvas3DErrorBoundary] Component stack:", errorInfo.componentStack);
|
|
280
|
+
}
|
|
281
|
+
render() {
|
|
282
|
+
if (this.state.hasError) {
|
|
283
|
+
if (this.props.fallback) {
|
|
284
|
+
return this.props.fallback;
|
|
285
|
+
}
|
|
286
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "canvas-3d-error", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "canvas-3d-error__content", children: [
|
|
287
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "canvas-3d-error__icon", children: "\u26A0\uFE0F" }),
|
|
288
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "canvas-3d-error__title", children: "3D Scene Error" }),
|
|
289
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "canvas-3d-error__message", children: "Something went wrong while rendering the 3D scene." }),
|
|
290
|
+
this.state.error && /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "canvas-3d-error__details", children: [
|
|
291
|
+
/* @__PURE__ */ jsxRuntime.jsx("summary", { children: "Error Details" }),
|
|
292
|
+
/* @__PURE__ */ jsxRuntime.jsxs("pre", { className: "error__stack", children: [
|
|
293
|
+
this.state.error.message,
|
|
294
|
+
"\n",
|
|
295
|
+
this.state.error.stack
|
|
296
|
+
] }),
|
|
297
|
+
this.state.errorInfo && /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "error__component-stack", children: this.state.errorInfo.componentStack })
|
|
298
|
+
] }),
|
|
299
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "canvas-3d-error__actions", children: [
|
|
300
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
301
|
+
"button",
|
|
302
|
+
{
|
|
303
|
+
className: "error__button error__button--primary",
|
|
304
|
+
onClick: this.handleReset,
|
|
305
|
+
children: "Try Again"
|
|
306
|
+
}
|
|
307
|
+
),
|
|
308
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
309
|
+
"button",
|
|
310
|
+
{
|
|
311
|
+
className: "error__button error__button--secondary",
|
|
312
|
+
onClick: () => window.location.reload(),
|
|
313
|
+
children: "Reload Page"
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
] })
|
|
317
|
+
] }) });
|
|
318
|
+
}
|
|
319
|
+
return this.props.children;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
function detectAssetRoot(modelUrl) {
|
|
323
|
+
const idx = modelUrl.indexOf("/3d/");
|
|
324
|
+
if (idx !== -1) {
|
|
325
|
+
return modelUrl.substring(0, idx + 4);
|
|
326
|
+
}
|
|
327
|
+
return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
|
|
328
|
+
}
|
|
329
|
+
function useGLTFModel(url, resourceBasePath) {
|
|
330
|
+
const [state, setState] = React8.useState({
|
|
331
|
+
model: null,
|
|
332
|
+
isLoading: false,
|
|
333
|
+
error: null
|
|
334
|
+
});
|
|
335
|
+
React8.useEffect(() => {
|
|
336
|
+
if (!url) {
|
|
337
|
+
setState({ model: null, isLoading: false, error: null });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
console.log("[ModelLoader] Loading:", url);
|
|
341
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
342
|
+
const assetRoot = resourceBasePath || detectAssetRoot(url);
|
|
343
|
+
const loader = new GLTFLoader.GLTFLoader();
|
|
344
|
+
loader.setResourcePath(assetRoot);
|
|
345
|
+
loader.load(
|
|
346
|
+
url,
|
|
347
|
+
(gltf) => {
|
|
348
|
+
console.log("[ModelLoader] Loaded:", url);
|
|
349
|
+
setState({
|
|
350
|
+
model: gltf.scene,
|
|
351
|
+
isLoading: false,
|
|
352
|
+
error: null
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
void 0,
|
|
356
|
+
(err) => {
|
|
357
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
358
|
+
console.warn("[ModelLoader] Failed:", url, errorMsg);
|
|
359
|
+
setState({
|
|
360
|
+
model: null,
|
|
361
|
+
isLoading: false,
|
|
362
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
}, [url, resourceBasePath]);
|
|
367
|
+
return state;
|
|
368
|
+
}
|
|
369
|
+
function ModelLoader({
|
|
370
|
+
url,
|
|
371
|
+
position = [0, 0, 0],
|
|
372
|
+
scale = 1,
|
|
373
|
+
rotation = [0, 0, 0],
|
|
374
|
+
isSelected = false,
|
|
375
|
+
isHovered = false,
|
|
376
|
+
onClick,
|
|
377
|
+
onHover,
|
|
378
|
+
fallbackGeometry = "box",
|
|
379
|
+
castShadow = true,
|
|
380
|
+
receiveShadow = true,
|
|
381
|
+
resourceBasePath
|
|
382
|
+
}) {
|
|
383
|
+
const { model: loadedModel, isLoading, error } = useGLTFModel(url, resourceBasePath);
|
|
384
|
+
const model = React8.useMemo(() => {
|
|
385
|
+
if (!loadedModel) return null;
|
|
386
|
+
const cloned = loadedModel.clone();
|
|
387
|
+
cloned.traverse((child) => {
|
|
388
|
+
if (child instanceof THREE6__namespace.Mesh) {
|
|
389
|
+
child.castShadow = castShadow;
|
|
390
|
+
child.receiveShadow = receiveShadow;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
return cloned;
|
|
394
|
+
}, [loadedModel, castShadow, receiveShadow]);
|
|
395
|
+
const scaleArray = React8.useMemo(() => {
|
|
396
|
+
if (typeof scale === "number") {
|
|
397
|
+
return [scale, scale, scale];
|
|
398
|
+
}
|
|
399
|
+
return scale;
|
|
400
|
+
}, [scale]);
|
|
401
|
+
const rotationRad = React8.useMemo(() => {
|
|
402
|
+
return [
|
|
403
|
+
rotation[0] * Math.PI / 180,
|
|
404
|
+
rotation[1] * Math.PI / 180,
|
|
405
|
+
rotation[2] * Math.PI / 180
|
|
406
|
+
];
|
|
407
|
+
}, [rotation]);
|
|
408
|
+
if (isLoading) {
|
|
409
|
+
return /* @__PURE__ */ jsxRuntime.jsx("group", { position, children: /* @__PURE__ */ jsxRuntime.jsxs("mesh", { rotation: [Math.PI / 2, 0, 0], children: [
|
|
410
|
+
/* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.3, 0.35, 16] }),
|
|
411
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#4a90d9", transparent: true, opacity: 0.8 })
|
|
412
|
+
] }) });
|
|
413
|
+
}
|
|
414
|
+
if (error || !model) {
|
|
415
|
+
if (fallbackGeometry === "none") {
|
|
416
|
+
return /* @__PURE__ */ jsxRuntime.jsx("group", { position });
|
|
417
|
+
}
|
|
418
|
+
const fallbackProps = {
|
|
419
|
+
onClick,
|
|
420
|
+
onPointerOver: () => onHover?.(true),
|
|
421
|
+
onPointerOut: () => onHover?.(false)
|
|
422
|
+
};
|
|
423
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("group", { position, children: [
|
|
424
|
+
(isSelected || isHovered) && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.02, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
425
|
+
/* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.6, 0.7, 32] }),
|
|
426
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
427
|
+
"meshBasicMaterial",
|
|
428
|
+
{
|
|
429
|
+
color: isSelected ? 16755200 : 16777215,
|
|
430
|
+
transparent: true,
|
|
431
|
+
opacity: 0.5
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
] }),
|
|
435
|
+
fallbackGeometry === "box" && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { ...fallbackProps, position: [0, 0.5, 0], children: [
|
|
436
|
+
/* @__PURE__ */ jsxRuntime.jsx("boxGeometry", { args: [0.8, 0.8, 0.8] }),
|
|
437
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: error ? 16729156 : 8947848 })
|
|
438
|
+
] }),
|
|
439
|
+
fallbackGeometry === "sphere" && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { ...fallbackProps, position: [0, 0.5, 0], children: [
|
|
440
|
+
/* @__PURE__ */ jsxRuntime.jsx("sphereGeometry", { args: [0.4, 16, 16] }),
|
|
441
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: error ? 16729156 : 8947848 })
|
|
442
|
+
] }),
|
|
443
|
+
fallbackGeometry === "cylinder" && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { ...fallbackProps, position: [0, 0.5, 0], children: [
|
|
444
|
+
/* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.3, 0.3, 0.8, 16] }),
|
|
445
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: error ? 16729156 : 8947848 })
|
|
446
|
+
] })
|
|
447
|
+
] });
|
|
448
|
+
}
|
|
449
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
450
|
+
"group",
|
|
451
|
+
{
|
|
452
|
+
position,
|
|
453
|
+
rotation: rotationRad,
|
|
454
|
+
onClick,
|
|
455
|
+
onPointerOver: () => onHover?.(true),
|
|
456
|
+
onPointerOut: () => onHover?.(false),
|
|
457
|
+
children: [
|
|
458
|
+
(isSelected || isHovered) && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.02, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
459
|
+
/* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.6, 0.7, 32] }),
|
|
460
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
461
|
+
"meshBasicMaterial",
|
|
462
|
+
{
|
|
463
|
+
color: isSelected ? 16755200 : 16777215,
|
|
464
|
+
transparent: true,
|
|
465
|
+
opacity: 0.5
|
|
466
|
+
}
|
|
467
|
+
)
|
|
468
|
+
] }),
|
|
469
|
+
/* @__PURE__ */ jsxRuntime.jsx("primitive", { object: model, scale: scaleArray })
|
|
470
|
+
]
|
|
471
|
+
}
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
function PhysicsObject3D({
|
|
475
|
+
entityId,
|
|
476
|
+
modelUrl,
|
|
477
|
+
initialPosition = [0, 0, 0],
|
|
478
|
+
initialVelocity = [0, 0, 0],
|
|
479
|
+
mass = 1,
|
|
480
|
+
gravity = 9.8,
|
|
481
|
+
groundY = 0,
|
|
482
|
+
scale = 1,
|
|
483
|
+
onPhysicsUpdate,
|
|
484
|
+
onGroundHit,
|
|
485
|
+
onCollision
|
|
486
|
+
}) {
|
|
487
|
+
const groupRef = React8.useRef(null);
|
|
488
|
+
const physicsStateRef = React8.useRef({
|
|
489
|
+
id: entityId,
|
|
490
|
+
x: initialPosition[0],
|
|
491
|
+
y: initialPosition[1],
|
|
492
|
+
z: initialPosition[2],
|
|
493
|
+
vx: initialVelocity[0],
|
|
494
|
+
vy: initialVelocity[1],
|
|
495
|
+
vz: initialVelocity[2],
|
|
496
|
+
rx: 0,
|
|
497
|
+
ry: 0,
|
|
498
|
+
rz: 0,
|
|
499
|
+
isGrounded: false,
|
|
500
|
+
gravity,
|
|
501
|
+
friction: 0.8,
|
|
502
|
+
mass,
|
|
503
|
+
state: "Active"
|
|
504
|
+
});
|
|
505
|
+
const groundHitRef = React8.useRef(false);
|
|
506
|
+
React8.useEffect(() => {
|
|
507
|
+
if (groupRef.current) {
|
|
508
|
+
groupRef.current.position.set(
|
|
509
|
+
initialPosition[0],
|
|
510
|
+
initialPosition[1],
|
|
511
|
+
initialPosition[2]
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}, []);
|
|
515
|
+
fiber.useFrame((state, delta) => {
|
|
516
|
+
const physics = physicsStateRef.current;
|
|
517
|
+
if (physics.state !== "Active") return;
|
|
518
|
+
const dt = Math.min(delta, 0.1);
|
|
519
|
+
if (!physics.isGrounded) {
|
|
520
|
+
physics.vy -= physics.gravity * dt;
|
|
521
|
+
}
|
|
522
|
+
physics.x += physics.vx * dt;
|
|
523
|
+
physics.y += physics.vy * dt;
|
|
524
|
+
physics.z += physics.vz * dt;
|
|
525
|
+
const airResistance = Math.pow(0.99, dt * 60);
|
|
526
|
+
physics.vx *= airResistance;
|
|
527
|
+
physics.vz *= airResistance;
|
|
528
|
+
if (physics.y <= groundY) {
|
|
529
|
+
physics.y = groundY;
|
|
530
|
+
if (!physics.isGrounded) {
|
|
531
|
+
physics.isGrounded = true;
|
|
532
|
+
groundHitRef.current = true;
|
|
533
|
+
physics.vx *= physics.friction;
|
|
534
|
+
physics.vz *= physics.friction;
|
|
535
|
+
onGroundHit?.();
|
|
536
|
+
}
|
|
537
|
+
physics.vy = 0;
|
|
538
|
+
} else {
|
|
539
|
+
physics.isGrounded = false;
|
|
540
|
+
}
|
|
541
|
+
if (groupRef.current) {
|
|
542
|
+
groupRef.current.position.set(physics.x, physics.y, physics.z);
|
|
543
|
+
if (!physics.isGrounded) {
|
|
544
|
+
physics.rx += physics.vz * dt * 0.5;
|
|
545
|
+
physics.rz -= physics.vx * dt * 0.5;
|
|
546
|
+
groupRef.current.rotation.set(physics.rx, physics.ry, physics.rz);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
onPhysicsUpdate?.({ ...physics });
|
|
550
|
+
});
|
|
551
|
+
const scaleArray = typeof scale === "number" ? [scale, scale, scale] : scale;
|
|
552
|
+
return /* @__PURE__ */ jsxRuntime.jsx("group", { ref: groupRef, scale: scaleArray, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
553
|
+
ModelLoader,
|
|
554
|
+
{
|
|
555
|
+
url: modelUrl,
|
|
556
|
+
fallbackGeometry: "box"
|
|
557
|
+
}
|
|
558
|
+
) });
|
|
559
|
+
}
|
|
560
|
+
function usePhysics3DController(entityId) {
|
|
561
|
+
const applyForce = (fx, fy, fz) => {
|
|
562
|
+
console.log(`Apply force to ${entityId}:`, { fx, fy, fz });
|
|
563
|
+
};
|
|
564
|
+
const setVelocity = (vx, vy, vz) => {
|
|
565
|
+
console.log(`Set velocity for ${entityId}:`, { vx, vy, vz });
|
|
566
|
+
};
|
|
567
|
+
const setPosition = (x, y, z) => {
|
|
568
|
+
console.log(`Set position for ${entityId}:`, { x, y, z });
|
|
569
|
+
};
|
|
570
|
+
const jump = (force = 10) => {
|
|
571
|
+
applyForce(0, force, 0);
|
|
572
|
+
};
|
|
573
|
+
return {
|
|
574
|
+
applyForce,
|
|
575
|
+
setVelocity,
|
|
576
|
+
setPosition,
|
|
577
|
+
jump
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function detectAssetRoot2(modelUrl) {
|
|
581
|
+
const idx = modelUrl.indexOf("/3d/");
|
|
582
|
+
if (idx !== -1) {
|
|
583
|
+
return modelUrl.substring(0, idx + 4);
|
|
584
|
+
}
|
|
585
|
+
return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
|
|
586
|
+
}
|
|
587
|
+
function createGLTFLoaderForUrl(url) {
|
|
588
|
+
const loader = new GLTFLoader_js.GLTFLoader();
|
|
589
|
+
loader.setResourcePath(detectAssetRoot2(url));
|
|
590
|
+
return loader;
|
|
591
|
+
}
|
|
592
|
+
var AssetLoader = class {
|
|
593
|
+
constructor() {
|
|
594
|
+
__publicField(this, "objLoader");
|
|
595
|
+
__publicField(this, "textureLoader");
|
|
596
|
+
__publicField(this, "modelCache");
|
|
597
|
+
__publicField(this, "textureCache");
|
|
598
|
+
__publicField(this, "loadingPromises");
|
|
599
|
+
this.objLoader = new OBJLoader_js.OBJLoader();
|
|
600
|
+
this.textureLoader = new THREE6__namespace.TextureLoader();
|
|
601
|
+
this.modelCache = /* @__PURE__ */ new Map();
|
|
602
|
+
this.textureCache = /* @__PURE__ */ new Map();
|
|
603
|
+
this.loadingPromises = /* @__PURE__ */ new Map();
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Load a GLB/GLTF model
|
|
607
|
+
* @param url - URL to the .glb or .gltf file
|
|
608
|
+
* @returns Promise with loaded model scene and animations
|
|
609
|
+
*/
|
|
610
|
+
async loadModel(url) {
|
|
611
|
+
if (this.modelCache.has(url)) {
|
|
612
|
+
return this.modelCache.get(url);
|
|
613
|
+
}
|
|
614
|
+
if (this.loadingPromises.has(url)) {
|
|
615
|
+
return this.loadingPromises.get(url);
|
|
616
|
+
}
|
|
617
|
+
const loader = createGLTFLoaderForUrl(url);
|
|
618
|
+
const loadPromise = loader.loadAsync(url).then((gltf) => {
|
|
619
|
+
const result = {
|
|
620
|
+
scene: gltf.scene,
|
|
621
|
+
animations: gltf.animations || []
|
|
622
|
+
};
|
|
623
|
+
this.modelCache.set(url, result);
|
|
624
|
+
this.loadingPromises.delete(url);
|
|
625
|
+
return result;
|
|
626
|
+
}).catch((error) => {
|
|
627
|
+
this.loadingPromises.delete(url);
|
|
628
|
+
throw new Error(`Failed to load model ${url}: ${error.message}`);
|
|
629
|
+
});
|
|
630
|
+
this.loadingPromises.set(url, loadPromise);
|
|
631
|
+
return loadPromise;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Load an OBJ model (fallback for non-GLB assets)
|
|
635
|
+
* @param url - URL to the .obj file
|
|
636
|
+
* @returns Promise with loaded object group
|
|
637
|
+
*/
|
|
638
|
+
async loadOBJ(url) {
|
|
639
|
+
if (this.modelCache.has(url)) {
|
|
640
|
+
return this.modelCache.get(url).scene;
|
|
641
|
+
}
|
|
642
|
+
if (this.loadingPromises.has(url)) {
|
|
643
|
+
const result = await this.loadingPromises.get(url);
|
|
644
|
+
return result.scene;
|
|
645
|
+
}
|
|
646
|
+
const loadPromise = this.objLoader.loadAsync(url).then((group) => {
|
|
647
|
+
const result = {
|
|
648
|
+
scene: group,
|
|
649
|
+
animations: []
|
|
650
|
+
};
|
|
651
|
+
this.modelCache.set(url, result);
|
|
652
|
+
this.loadingPromises.delete(url);
|
|
653
|
+
return result;
|
|
654
|
+
}).catch((error) => {
|
|
655
|
+
this.loadingPromises.delete(url);
|
|
656
|
+
throw new Error(`Failed to load OBJ ${url}: ${error.message}`);
|
|
657
|
+
});
|
|
658
|
+
this.loadingPromises.set(url, loadPromise);
|
|
659
|
+
return (await loadPromise).scene;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Load a texture
|
|
663
|
+
* @param url - URL to the texture image
|
|
664
|
+
* @returns Promise with loaded texture
|
|
665
|
+
*/
|
|
666
|
+
async loadTexture(url) {
|
|
667
|
+
if (this.textureCache.has(url)) {
|
|
668
|
+
return this.textureCache.get(url);
|
|
669
|
+
}
|
|
670
|
+
if (this.loadingPromises.has(`texture:${url}`)) {
|
|
671
|
+
return this.loadingPromises.get(`texture:${url}`);
|
|
672
|
+
}
|
|
673
|
+
const loadPromise = this.textureLoader.loadAsync(url).then((texture) => {
|
|
674
|
+
texture.colorSpace = THREE6__namespace.SRGBColorSpace;
|
|
675
|
+
this.textureCache.set(url, texture);
|
|
676
|
+
this.loadingPromises.delete(`texture:${url}`);
|
|
677
|
+
return texture;
|
|
678
|
+
}).catch((error) => {
|
|
679
|
+
this.loadingPromises.delete(`texture:${url}`);
|
|
680
|
+
throw new Error(`Failed to load texture ${url}: ${error.message}`);
|
|
681
|
+
});
|
|
682
|
+
this.loadingPromises.set(`texture:${url}`, loadPromise);
|
|
683
|
+
return loadPromise;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Preload multiple assets
|
|
687
|
+
* @param urls - Array of asset URLs to preload
|
|
688
|
+
* @returns Promise that resolves when all assets are loaded
|
|
689
|
+
*/
|
|
690
|
+
async preload(urls) {
|
|
691
|
+
const promises = urls.map((url) => {
|
|
692
|
+
if (url.endsWith(".glb") || url.endsWith(".gltf")) {
|
|
693
|
+
return this.loadModel(url).catch(() => null);
|
|
694
|
+
} else if (url.endsWith(".obj")) {
|
|
695
|
+
return this.loadOBJ(url).catch(() => null);
|
|
696
|
+
} else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
|
|
697
|
+
return this.loadTexture(url).catch(() => null);
|
|
698
|
+
}
|
|
699
|
+
return Promise.resolve(null);
|
|
700
|
+
});
|
|
701
|
+
await Promise.all(promises);
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Check if a model is cached
|
|
705
|
+
* @param url - Model URL
|
|
706
|
+
*/
|
|
707
|
+
hasModel(url) {
|
|
708
|
+
return this.modelCache.has(url);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Check if a texture is cached
|
|
712
|
+
* @param url - Texture URL
|
|
713
|
+
*/
|
|
714
|
+
hasTexture(url) {
|
|
715
|
+
return this.textureCache.has(url);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Get cached model (throws if not cached)
|
|
719
|
+
* @param url - Model URL
|
|
720
|
+
*/
|
|
721
|
+
getModel(url) {
|
|
722
|
+
const model = this.modelCache.get(url);
|
|
723
|
+
if (!model) {
|
|
724
|
+
throw new Error(`Model ${url} not in cache`);
|
|
725
|
+
}
|
|
726
|
+
return model;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Get cached texture (throws if not cached)
|
|
730
|
+
* @param url - Texture URL
|
|
731
|
+
*/
|
|
732
|
+
getTexture(url) {
|
|
733
|
+
const texture = this.textureCache.get(url);
|
|
734
|
+
if (!texture) {
|
|
735
|
+
throw new Error(`Texture ${url} not in cache`);
|
|
736
|
+
}
|
|
737
|
+
return texture;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Clear all caches
|
|
741
|
+
*/
|
|
742
|
+
clearCache() {
|
|
743
|
+
this.textureCache.forEach((texture) => {
|
|
744
|
+
texture.dispose();
|
|
745
|
+
});
|
|
746
|
+
this.modelCache.forEach((model) => {
|
|
747
|
+
model.scene.traverse((child) => {
|
|
748
|
+
if (child instanceof THREE6__namespace.Mesh) {
|
|
749
|
+
child.geometry.dispose();
|
|
750
|
+
if (Array.isArray(child.material)) {
|
|
751
|
+
child.material.forEach((m) => m.dispose());
|
|
752
|
+
} else {
|
|
753
|
+
child.material.dispose();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
this.modelCache.clear();
|
|
759
|
+
this.textureCache.clear();
|
|
760
|
+
this.loadingPromises.clear();
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Get cache statistics
|
|
764
|
+
*/
|
|
765
|
+
getStats() {
|
|
766
|
+
return {
|
|
767
|
+
models: this.modelCache.size,
|
|
768
|
+
textures: this.textureCache.size,
|
|
769
|
+
loading: this.loadingPromises.size
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
var assetLoader = new AssetLoader();
|
|
774
|
+
|
|
775
|
+
// components/organisms/game/three/hooks/useThree.ts
|
|
776
|
+
var DEFAULT_OPTIONS = {
|
|
777
|
+
cameraMode: "isometric",
|
|
778
|
+
cameraPosition: [10, 10, 10],
|
|
779
|
+
backgroundColor: "#1a1a2e",
|
|
780
|
+
shadows: true,
|
|
781
|
+
showGrid: true,
|
|
782
|
+
gridSize: 20,
|
|
783
|
+
assetLoader: new AssetLoader()
|
|
784
|
+
};
|
|
785
|
+
function useThree3(options = {}) {
|
|
786
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
787
|
+
const containerRef = React8.useRef(null);
|
|
788
|
+
const canvasRef = React8.useRef(null);
|
|
789
|
+
const rendererRef = React8.useRef(null);
|
|
790
|
+
const sceneRef = React8.useRef(null);
|
|
791
|
+
const cameraRef = React8.useRef(null);
|
|
792
|
+
const controlsRef = React8.useRef(null);
|
|
793
|
+
const gridHelperRef = React8.useRef(null);
|
|
794
|
+
const rafRef = React8.useRef(0);
|
|
795
|
+
const [isReady, setIsReady] = React8.useState(false);
|
|
796
|
+
const [dimensions, setDimensions] = React8.useState({ width: 0, height: 0 });
|
|
797
|
+
const initialCameraPosition = React8.useMemo(
|
|
798
|
+
() => new THREE6__namespace.Vector3(...opts.cameraPosition),
|
|
799
|
+
[]
|
|
800
|
+
);
|
|
801
|
+
React8.useEffect(() => {
|
|
802
|
+
if (!containerRef.current) return;
|
|
803
|
+
const container = containerRef.current;
|
|
804
|
+
const { clientWidth, clientHeight } = container;
|
|
805
|
+
const scene = new THREE6__namespace.Scene();
|
|
806
|
+
scene.background = new THREE6__namespace.Color(opts.backgroundColor);
|
|
807
|
+
sceneRef.current = scene;
|
|
808
|
+
let camera;
|
|
809
|
+
const aspect = clientWidth / clientHeight;
|
|
810
|
+
if (opts.cameraMode === "isometric") {
|
|
811
|
+
const size = 10;
|
|
812
|
+
camera = new THREE6__namespace.OrthographicCamera(
|
|
813
|
+
-size * aspect,
|
|
814
|
+
size * aspect,
|
|
815
|
+
size,
|
|
816
|
+
-size,
|
|
817
|
+
0.1,
|
|
818
|
+
1e3
|
|
819
|
+
);
|
|
820
|
+
} else {
|
|
821
|
+
camera = new THREE6__namespace.PerspectiveCamera(45, aspect, 0.1, 1e3);
|
|
822
|
+
}
|
|
823
|
+
camera.position.copy(initialCameraPosition);
|
|
824
|
+
cameraRef.current = camera;
|
|
825
|
+
const renderer = new THREE6__namespace.WebGLRenderer({
|
|
826
|
+
antialias: true,
|
|
827
|
+
alpha: true,
|
|
828
|
+
canvas: canvasRef.current || void 0
|
|
829
|
+
});
|
|
830
|
+
renderer.setSize(clientWidth, clientHeight);
|
|
831
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
832
|
+
renderer.shadowMap.enabled = opts.shadows;
|
|
833
|
+
renderer.shadowMap.type = THREE6__namespace.PCFSoftShadowMap;
|
|
834
|
+
rendererRef.current = renderer;
|
|
835
|
+
const controls = new OrbitControls_js.OrbitControls(camera, renderer.domElement);
|
|
836
|
+
controls.enableDamping = true;
|
|
837
|
+
controls.dampingFactor = 0.05;
|
|
838
|
+
controls.minDistance = 2;
|
|
839
|
+
controls.maxDistance = 100;
|
|
840
|
+
controls.maxPolarAngle = Math.PI / 2 - 0.1;
|
|
841
|
+
controlsRef.current = controls;
|
|
842
|
+
const ambientLight = new THREE6__namespace.AmbientLight(16777215, 0.6);
|
|
843
|
+
scene.add(ambientLight);
|
|
844
|
+
const directionalLight = new THREE6__namespace.DirectionalLight(16777215, 0.8);
|
|
845
|
+
directionalLight.position.set(10, 20, 10);
|
|
846
|
+
directionalLight.castShadow = opts.shadows;
|
|
847
|
+
directionalLight.shadow.mapSize.width = 2048;
|
|
848
|
+
directionalLight.shadow.mapSize.height = 2048;
|
|
849
|
+
scene.add(directionalLight);
|
|
850
|
+
if (opts.showGrid) {
|
|
851
|
+
const gridHelper = new THREE6__namespace.GridHelper(
|
|
852
|
+
opts.gridSize,
|
|
853
|
+
opts.gridSize,
|
|
854
|
+
4473924,
|
|
855
|
+
2236962
|
|
856
|
+
);
|
|
857
|
+
scene.add(gridHelper);
|
|
858
|
+
gridHelperRef.current = gridHelper;
|
|
859
|
+
}
|
|
860
|
+
const animate = () => {
|
|
861
|
+
rafRef.current = requestAnimationFrame(animate);
|
|
862
|
+
controls.update();
|
|
863
|
+
renderer.render(scene, camera);
|
|
864
|
+
};
|
|
865
|
+
animate();
|
|
866
|
+
const handleResize = () => {
|
|
867
|
+
const { clientWidth: width, clientHeight: height } = container;
|
|
868
|
+
setDimensions({ width, height });
|
|
869
|
+
if (camera instanceof THREE6__namespace.PerspectiveCamera) {
|
|
870
|
+
camera.aspect = width / height;
|
|
871
|
+
camera.updateProjectionMatrix();
|
|
872
|
+
} else if (camera instanceof THREE6__namespace.OrthographicCamera) {
|
|
873
|
+
const aspect2 = width / height;
|
|
874
|
+
const size = 10;
|
|
875
|
+
camera.left = -size * aspect2;
|
|
876
|
+
camera.right = size * aspect2;
|
|
877
|
+
camera.top = size;
|
|
878
|
+
camera.bottom = -size;
|
|
879
|
+
camera.updateProjectionMatrix();
|
|
880
|
+
}
|
|
881
|
+
renderer.setSize(width, height);
|
|
882
|
+
};
|
|
883
|
+
window.addEventListener("resize", handleResize);
|
|
884
|
+
handleResize();
|
|
885
|
+
setIsReady(true);
|
|
886
|
+
return () => {
|
|
887
|
+
window.removeEventListener("resize", handleResize);
|
|
888
|
+
cancelAnimationFrame(rafRef.current);
|
|
889
|
+
controls.dispose();
|
|
890
|
+
renderer.dispose();
|
|
891
|
+
scene.clear();
|
|
892
|
+
};
|
|
893
|
+
}, []);
|
|
894
|
+
React8.useEffect(() => {
|
|
895
|
+
if (!cameraRef.current || !containerRef.current) return;
|
|
896
|
+
const container = containerRef.current;
|
|
897
|
+
const { clientWidth, clientHeight } = container;
|
|
898
|
+
const aspect = clientWidth / clientHeight;
|
|
899
|
+
const currentPos = cameraRef.current.position.clone();
|
|
900
|
+
let newCamera;
|
|
901
|
+
if (opts.cameraMode === "isometric") {
|
|
902
|
+
const size = 10;
|
|
903
|
+
newCamera = new THREE6__namespace.OrthographicCamera(
|
|
904
|
+
-size * aspect,
|
|
905
|
+
size * aspect,
|
|
906
|
+
size,
|
|
907
|
+
-size,
|
|
908
|
+
0.1,
|
|
909
|
+
1e3
|
|
910
|
+
);
|
|
911
|
+
} else {
|
|
912
|
+
newCamera = new THREE6__namespace.PerspectiveCamera(45, aspect, 0.1, 1e3);
|
|
913
|
+
}
|
|
914
|
+
newCamera.position.copy(currentPos);
|
|
915
|
+
cameraRef.current = newCamera;
|
|
916
|
+
if (controlsRef.current) {
|
|
917
|
+
controlsRef.current.object = newCamera;
|
|
918
|
+
}
|
|
919
|
+
if (rendererRef.current) {
|
|
920
|
+
const animate = () => {
|
|
921
|
+
rafRef.current = requestAnimationFrame(animate);
|
|
922
|
+
controlsRef.current?.update();
|
|
923
|
+
rendererRef.current?.render(sceneRef.current, newCamera);
|
|
924
|
+
};
|
|
925
|
+
cancelAnimationFrame(rafRef.current);
|
|
926
|
+
animate();
|
|
927
|
+
}
|
|
928
|
+
}, [opts.cameraMode]);
|
|
929
|
+
const setCameraPosition = React8.useCallback((x, y, z) => {
|
|
930
|
+
if (cameraRef.current) {
|
|
931
|
+
cameraRef.current.position.set(x, y, z);
|
|
932
|
+
controlsRef.current?.update();
|
|
933
|
+
}
|
|
934
|
+
}, []);
|
|
935
|
+
const lookAt = React8.useCallback((x, y, z) => {
|
|
936
|
+
if (cameraRef.current) {
|
|
937
|
+
cameraRef.current.lookAt(x, y, z);
|
|
938
|
+
controlsRef.current?.target.set(x, y, z);
|
|
939
|
+
controlsRef.current?.update();
|
|
940
|
+
}
|
|
941
|
+
}, []);
|
|
942
|
+
const resetCamera = React8.useCallback(() => {
|
|
943
|
+
if (cameraRef.current) {
|
|
944
|
+
cameraRef.current.position.copy(initialCameraPosition);
|
|
945
|
+
cameraRef.current.lookAt(0, 0, 0);
|
|
946
|
+
if (controlsRef.current) {
|
|
947
|
+
controlsRef.current.target.set(0, 0, 0);
|
|
948
|
+
controlsRef.current.update();
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}, [initialCameraPosition]);
|
|
952
|
+
const fitView = React8.useCallback(
|
|
953
|
+
(bounds) => {
|
|
954
|
+
if (!cameraRef.current) return;
|
|
955
|
+
const centerX = (bounds.minX + bounds.maxX) / 2;
|
|
956
|
+
const centerZ = (bounds.minZ + bounds.maxZ) / 2;
|
|
957
|
+
const width = bounds.maxX - bounds.minX;
|
|
958
|
+
const depth = bounds.maxZ - bounds.minZ;
|
|
959
|
+
const maxDim = Math.max(width, depth);
|
|
960
|
+
const distance = maxDim * 1.5;
|
|
961
|
+
const height = distance * 0.8;
|
|
962
|
+
cameraRef.current.position.set(centerX + distance, height, centerZ + distance);
|
|
963
|
+
lookAt(centerX, 0, centerZ);
|
|
964
|
+
},
|
|
965
|
+
[lookAt]
|
|
966
|
+
);
|
|
967
|
+
return {
|
|
968
|
+
canvasRef,
|
|
969
|
+
renderer: rendererRef.current,
|
|
970
|
+
scene: sceneRef.current,
|
|
971
|
+
camera: cameraRef.current,
|
|
972
|
+
controls: controlsRef.current,
|
|
973
|
+
isReady,
|
|
974
|
+
dimensions,
|
|
975
|
+
setCameraPosition,
|
|
976
|
+
lookAt,
|
|
977
|
+
resetCamera,
|
|
978
|
+
fitView
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
function useAssetLoader(options = {}) {
|
|
982
|
+
const { preloadUrls = [], loader: customLoader } = options;
|
|
983
|
+
const loaderRef = React8.useRef(customLoader || new AssetLoader());
|
|
984
|
+
const [state, setState] = React8.useState({
|
|
985
|
+
isLoading: false,
|
|
986
|
+
progress: 0,
|
|
987
|
+
loaded: 0,
|
|
988
|
+
total: 0,
|
|
989
|
+
errors: []
|
|
990
|
+
});
|
|
991
|
+
React8.useEffect(() => {
|
|
992
|
+
if (preloadUrls.length > 0) {
|
|
993
|
+
preload(preloadUrls);
|
|
994
|
+
}
|
|
995
|
+
}, []);
|
|
996
|
+
const updateProgress = React8.useCallback((loaded, total) => {
|
|
997
|
+
setState((prev) => ({
|
|
998
|
+
...prev,
|
|
999
|
+
loaded,
|
|
1000
|
+
total,
|
|
1001
|
+
progress: total > 0 ? Math.round(loaded / total * 100) : 0
|
|
1002
|
+
}));
|
|
1003
|
+
}, []);
|
|
1004
|
+
const loadModel = React8.useCallback(
|
|
1005
|
+
async (url) => {
|
|
1006
|
+
setState((prev) => ({ ...prev, isLoading: true }));
|
|
1007
|
+
try {
|
|
1008
|
+
const model = await loaderRef.current.loadModel(url);
|
|
1009
|
+
setState((prev) => ({
|
|
1010
|
+
...prev,
|
|
1011
|
+
isLoading: false,
|
|
1012
|
+
loaded: prev.loaded + 1
|
|
1013
|
+
}));
|
|
1014
|
+
return model;
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1017
|
+
setState((prev) => ({
|
|
1018
|
+
...prev,
|
|
1019
|
+
isLoading: false,
|
|
1020
|
+
errors: [...prev.errors, errorMsg]
|
|
1021
|
+
}));
|
|
1022
|
+
throw error;
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
1025
|
+
[]
|
|
1026
|
+
);
|
|
1027
|
+
const loadOBJ = React8.useCallback(
|
|
1028
|
+
async (url) => {
|
|
1029
|
+
setState((prev) => ({ ...prev, isLoading: true }));
|
|
1030
|
+
try {
|
|
1031
|
+
const model = await loaderRef.current.loadOBJ(url);
|
|
1032
|
+
setState((prev) => ({
|
|
1033
|
+
...prev,
|
|
1034
|
+
isLoading: false,
|
|
1035
|
+
loaded: prev.loaded + 1
|
|
1036
|
+
}));
|
|
1037
|
+
return model;
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1040
|
+
setState((prev) => ({
|
|
1041
|
+
...prev,
|
|
1042
|
+
isLoading: false,
|
|
1043
|
+
errors: [...prev.errors, errorMsg]
|
|
1044
|
+
}));
|
|
1045
|
+
throw error;
|
|
1046
|
+
}
|
|
1047
|
+
},
|
|
1048
|
+
[]
|
|
1049
|
+
);
|
|
1050
|
+
const loadTexture = React8.useCallback(
|
|
1051
|
+
async (url) => {
|
|
1052
|
+
setState((prev) => ({ ...prev, isLoading: true }));
|
|
1053
|
+
try {
|
|
1054
|
+
const texture = await loaderRef.current.loadTexture(url);
|
|
1055
|
+
setState((prev) => ({
|
|
1056
|
+
...prev,
|
|
1057
|
+
isLoading: false,
|
|
1058
|
+
loaded: prev.loaded + 1
|
|
1059
|
+
}));
|
|
1060
|
+
return texture;
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1063
|
+
setState((prev) => ({
|
|
1064
|
+
...prev,
|
|
1065
|
+
isLoading: false,
|
|
1066
|
+
errors: [...prev.errors, errorMsg]
|
|
1067
|
+
}));
|
|
1068
|
+
throw error;
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1071
|
+
[]
|
|
1072
|
+
);
|
|
1073
|
+
const preload = React8.useCallback(
|
|
1074
|
+
async (urls) => {
|
|
1075
|
+
setState((prev) => ({
|
|
1076
|
+
...prev,
|
|
1077
|
+
isLoading: true,
|
|
1078
|
+
total: urls.length,
|
|
1079
|
+
loaded: 0,
|
|
1080
|
+
errors: []
|
|
1081
|
+
}));
|
|
1082
|
+
let completed = 0;
|
|
1083
|
+
const errors = [];
|
|
1084
|
+
await Promise.all(
|
|
1085
|
+
urls.map(async (url) => {
|
|
1086
|
+
try {
|
|
1087
|
+
if (url.endsWith(".glb") || url.endsWith(".gltf")) {
|
|
1088
|
+
await loaderRef.current.loadModel(url);
|
|
1089
|
+
} else if (url.endsWith(".obj")) {
|
|
1090
|
+
await loaderRef.current.loadOBJ(url);
|
|
1091
|
+
} else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
|
|
1092
|
+
await loaderRef.current.loadTexture(url);
|
|
1093
|
+
}
|
|
1094
|
+
completed++;
|
|
1095
|
+
updateProgress(completed, urls.length);
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1098
|
+
errors.push(`${url}: ${errorMsg}`);
|
|
1099
|
+
completed++;
|
|
1100
|
+
updateProgress(completed, urls.length);
|
|
1101
|
+
}
|
|
1102
|
+
})
|
|
1103
|
+
);
|
|
1104
|
+
setState((prev) => ({
|
|
1105
|
+
...prev,
|
|
1106
|
+
isLoading: false,
|
|
1107
|
+
errors
|
|
1108
|
+
}));
|
|
1109
|
+
},
|
|
1110
|
+
[updateProgress]
|
|
1111
|
+
);
|
|
1112
|
+
const hasModel = React8.useCallback((url) => {
|
|
1113
|
+
return loaderRef.current.hasModel(url);
|
|
1114
|
+
}, []);
|
|
1115
|
+
const hasTexture = React8.useCallback((url) => {
|
|
1116
|
+
return loaderRef.current.hasTexture(url);
|
|
1117
|
+
}, []);
|
|
1118
|
+
const getModel = React8.useCallback((url) => {
|
|
1119
|
+
try {
|
|
1120
|
+
return loaderRef.current.getModel(url);
|
|
1121
|
+
} catch {
|
|
1122
|
+
return void 0;
|
|
1123
|
+
}
|
|
1124
|
+
}, []);
|
|
1125
|
+
const getTexture = React8.useCallback((url) => {
|
|
1126
|
+
try {
|
|
1127
|
+
return loaderRef.current.getTexture(url);
|
|
1128
|
+
} catch {
|
|
1129
|
+
return void 0;
|
|
1130
|
+
}
|
|
1131
|
+
}, []);
|
|
1132
|
+
const clearCache = React8.useCallback(() => {
|
|
1133
|
+
loaderRef.current.clearCache();
|
|
1134
|
+
setState({
|
|
1135
|
+
isLoading: false,
|
|
1136
|
+
progress: 0,
|
|
1137
|
+
loaded: 0,
|
|
1138
|
+
total: 0,
|
|
1139
|
+
errors: []
|
|
1140
|
+
});
|
|
1141
|
+
}, []);
|
|
1142
|
+
return {
|
|
1143
|
+
...state,
|
|
1144
|
+
loadModel,
|
|
1145
|
+
loadOBJ,
|
|
1146
|
+
loadTexture,
|
|
1147
|
+
preload,
|
|
1148
|
+
hasModel,
|
|
1149
|
+
hasTexture,
|
|
1150
|
+
getModel,
|
|
1151
|
+
getTexture,
|
|
1152
|
+
clearCache
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
function useSceneGraph() {
|
|
1156
|
+
const nodesRef = React8.useRef(/* @__PURE__ */ new Map());
|
|
1157
|
+
const addNode = React8.useCallback((node) => {
|
|
1158
|
+
const existing = nodesRef.current.get(node.id);
|
|
1159
|
+
if (existing) {
|
|
1160
|
+
existing.mesh.removeFromParent();
|
|
1161
|
+
}
|
|
1162
|
+
nodesRef.current.set(node.id, node);
|
|
1163
|
+
}, []);
|
|
1164
|
+
const removeNode = React8.useCallback((id) => {
|
|
1165
|
+
const node = nodesRef.current.get(id);
|
|
1166
|
+
if (node) {
|
|
1167
|
+
node.mesh.removeFromParent();
|
|
1168
|
+
nodesRef.current.delete(id);
|
|
1169
|
+
}
|
|
1170
|
+
}, []);
|
|
1171
|
+
const getNode = React8.useCallback((id) => {
|
|
1172
|
+
return nodesRef.current.get(id);
|
|
1173
|
+
}, []);
|
|
1174
|
+
const updateNodePosition = React8.useCallback(
|
|
1175
|
+
(id, x, y, z) => {
|
|
1176
|
+
const node = nodesRef.current.get(id);
|
|
1177
|
+
if (node) {
|
|
1178
|
+
node.mesh.position.set(x, y, z);
|
|
1179
|
+
node.position = { x, y, z };
|
|
1180
|
+
}
|
|
1181
|
+
},
|
|
1182
|
+
[]
|
|
1183
|
+
);
|
|
1184
|
+
const updateNodeGridPosition = React8.useCallback(
|
|
1185
|
+
(id, gridX, gridZ) => {
|
|
1186
|
+
const node = nodesRef.current.get(id);
|
|
1187
|
+
if (node) {
|
|
1188
|
+
node.gridPosition = { x: gridX, z: gridZ };
|
|
1189
|
+
}
|
|
1190
|
+
},
|
|
1191
|
+
[]
|
|
1192
|
+
);
|
|
1193
|
+
const getNodeAtGrid = React8.useCallback(
|
|
1194
|
+
(x, z, type) => {
|
|
1195
|
+
return Array.from(nodesRef.current.values()).find((node) => {
|
|
1196
|
+
const matchesGrid = node.gridPosition.x === x && node.gridPosition.z === z;
|
|
1197
|
+
return type ? matchesGrid && node.type === type : matchesGrid;
|
|
1198
|
+
});
|
|
1199
|
+
},
|
|
1200
|
+
[]
|
|
1201
|
+
);
|
|
1202
|
+
const getNodesByType = React8.useCallback((type) => {
|
|
1203
|
+
return Array.from(nodesRef.current.values()).filter((node) => node.type === type);
|
|
1204
|
+
}, []);
|
|
1205
|
+
const getNodesInBounds = React8.useCallback(
|
|
1206
|
+
(minX, maxX, minZ, maxZ) => {
|
|
1207
|
+
return Array.from(nodesRef.current.values()).filter((node) => {
|
|
1208
|
+
const { x, z } = node.gridPosition;
|
|
1209
|
+
return x >= minX && x <= maxX && z >= minZ && z <= maxZ;
|
|
1210
|
+
});
|
|
1211
|
+
},
|
|
1212
|
+
[]
|
|
1213
|
+
);
|
|
1214
|
+
const clearNodes = React8.useCallback(() => {
|
|
1215
|
+
nodesRef.current.forEach((node) => {
|
|
1216
|
+
node.mesh.removeFromParent();
|
|
1217
|
+
});
|
|
1218
|
+
nodesRef.current.clear();
|
|
1219
|
+
}, []);
|
|
1220
|
+
const countNodes = React8.useCallback((type) => {
|
|
1221
|
+
if (!type) {
|
|
1222
|
+
return nodesRef.current.size;
|
|
1223
|
+
}
|
|
1224
|
+
return Array.from(nodesRef.current.values()).filter((node) => node.type === type).length;
|
|
1225
|
+
}, []);
|
|
1226
|
+
return {
|
|
1227
|
+
nodesRef,
|
|
1228
|
+
addNode,
|
|
1229
|
+
removeNode,
|
|
1230
|
+
getNode,
|
|
1231
|
+
updateNodePosition,
|
|
1232
|
+
updateNodeGridPosition,
|
|
1233
|
+
getNodeAtGrid,
|
|
1234
|
+
getNodesByType,
|
|
1235
|
+
getNodesInBounds,
|
|
1236
|
+
clearNodes,
|
|
1237
|
+
countNodes
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
function useRaycaster(options) {
|
|
1241
|
+
const { camera, canvas, cellSize = 1, offsetX = 0, offsetZ = 0 } = options;
|
|
1242
|
+
const raycaster = React8.useRef(new THREE6__namespace.Raycaster());
|
|
1243
|
+
const mouse = React8.useRef(new THREE6__namespace.Vector2());
|
|
1244
|
+
const clientToNDC = React8.useCallback(
|
|
1245
|
+
(clientX, clientY) => {
|
|
1246
|
+
if (!canvas) {
|
|
1247
|
+
return { x: 0, y: 0 };
|
|
1248
|
+
}
|
|
1249
|
+
const rect = canvas.getBoundingClientRect();
|
|
1250
|
+
return {
|
|
1251
|
+
x: (clientX - rect.left) / rect.width * 2 - 1,
|
|
1252
|
+
y: -((clientY - rect.top) / rect.height) * 2 + 1
|
|
1253
|
+
};
|
|
1254
|
+
},
|
|
1255
|
+
[canvas]
|
|
1256
|
+
);
|
|
1257
|
+
const isWithinCanvas = React8.useCallback(
|
|
1258
|
+
(clientX, clientY) => {
|
|
1259
|
+
if (!canvas) return false;
|
|
1260
|
+
const rect = canvas.getBoundingClientRect();
|
|
1261
|
+
return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom;
|
|
1262
|
+
},
|
|
1263
|
+
[canvas]
|
|
1264
|
+
);
|
|
1265
|
+
const getIntersection = React8.useCallback(
|
|
1266
|
+
(clientX, clientY, objects) => {
|
|
1267
|
+
if (!camera || !canvas) return null;
|
|
1268
|
+
const ndc = clientToNDC(clientX, clientY);
|
|
1269
|
+
mouse.current.set(ndc.x, ndc.y);
|
|
1270
|
+
raycaster.current.setFromCamera(mouse.current, camera);
|
|
1271
|
+
const intersects = raycaster.current.intersectObjects(objects, true);
|
|
1272
|
+
if (intersects.length > 0) {
|
|
1273
|
+
const hit = intersects[0];
|
|
1274
|
+
return {
|
|
1275
|
+
object: hit.object,
|
|
1276
|
+
point: hit.point,
|
|
1277
|
+
distance: hit.distance,
|
|
1278
|
+
uv: hit.uv,
|
|
1279
|
+
face: hit.face,
|
|
1280
|
+
faceIndex: hit.faceIndex,
|
|
1281
|
+
instanceId: hit.instanceId
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
return null;
|
|
1285
|
+
},
|
|
1286
|
+
[camera, canvas, clientToNDC]
|
|
1287
|
+
);
|
|
1288
|
+
const getAllIntersections = React8.useCallback(
|
|
1289
|
+
(clientX, clientY, objects) => {
|
|
1290
|
+
if (!camera || !canvas) return [];
|
|
1291
|
+
const ndc = clientToNDC(clientX, clientY);
|
|
1292
|
+
mouse.current.set(ndc.x, ndc.y);
|
|
1293
|
+
raycaster.current.setFromCamera(mouse.current, camera);
|
|
1294
|
+
const intersects = raycaster.current.intersectObjects(objects, true);
|
|
1295
|
+
return intersects.map((hit) => ({
|
|
1296
|
+
object: hit.object,
|
|
1297
|
+
point: hit.point,
|
|
1298
|
+
distance: hit.distance,
|
|
1299
|
+
uv: hit.uv,
|
|
1300
|
+
face: hit.face,
|
|
1301
|
+
faceIndex: hit.faceIndex,
|
|
1302
|
+
instanceId: hit.instanceId
|
|
1303
|
+
}));
|
|
1304
|
+
},
|
|
1305
|
+
[camera, canvas, clientToNDC]
|
|
1306
|
+
);
|
|
1307
|
+
const getGridCoordinates = React8.useCallback(
|
|
1308
|
+
(clientX, clientY) => {
|
|
1309
|
+
if (!camera || !canvas) return null;
|
|
1310
|
+
const ndc = clientToNDC(clientX, clientY);
|
|
1311
|
+
mouse.current.set(ndc.x, ndc.y);
|
|
1312
|
+
raycaster.current.setFromCamera(mouse.current, camera);
|
|
1313
|
+
const plane = new THREE6__namespace.Plane(new THREE6__namespace.Vector3(0, 1, 0), 0);
|
|
1314
|
+
const target = new THREE6__namespace.Vector3();
|
|
1315
|
+
const intersection = raycaster.current.ray.intersectPlane(plane, target);
|
|
1316
|
+
if (intersection) {
|
|
1317
|
+
const gridX = Math.round((target.x - offsetX) / cellSize);
|
|
1318
|
+
const gridZ = Math.round((target.z - offsetZ) / cellSize);
|
|
1319
|
+
return { x: gridX, z: gridZ };
|
|
1320
|
+
}
|
|
1321
|
+
return null;
|
|
1322
|
+
},
|
|
1323
|
+
[camera, canvas, cellSize, offsetX, offsetZ, clientToNDC]
|
|
1324
|
+
);
|
|
1325
|
+
const getTileAtPosition = React8.useCallback(
|
|
1326
|
+
(clientX, clientY, scene) => {
|
|
1327
|
+
if (!camera || !canvas) return null;
|
|
1328
|
+
const tileMeshes = [];
|
|
1329
|
+
scene.traverse((obj) => {
|
|
1330
|
+
if (obj.userData.type === "tile" || obj.userData.isTile) {
|
|
1331
|
+
tileMeshes.push(obj);
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
const hit = getIntersection(clientX, clientY, tileMeshes);
|
|
1335
|
+
if (hit) {
|
|
1336
|
+
const gridCoords2 = getGridCoordinates(clientX, clientY);
|
|
1337
|
+
if (gridCoords2) {
|
|
1338
|
+
return {
|
|
1339
|
+
gridX: gridCoords2.x,
|
|
1340
|
+
gridZ: gridCoords2.z,
|
|
1341
|
+
worldPosition: hit.point,
|
|
1342
|
+
objectType: hit.object.userData.type || "tile",
|
|
1343
|
+
objectId: hit.object.userData.id || hit.object.userData.tileId
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
const gridCoords = getGridCoordinates(clientX, clientY);
|
|
1348
|
+
if (gridCoords) {
|
|
1349
|
+
return {
|
|
1350
|
+
gridX: gridCoords.x,
|
|
1351
|
+
gridZ: gridCoords.z,
|
|
1352
|
+
worldPosition: new THREE6__namespace.Vector3(
|
|
1353
|
+
gridCoords.x * cellSize + offsetX,
|
|
1354
|
+
0,
|
|
1355
|
+
gridCoords.z * cellSize + offsetZ
|
|
1356
|
+
)
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
return null;
|
|
1360
|
+
},
|
|
1361
|
+
[camera, canvas, getIntersection, getGridCoordinates, cellSize, offsetX, offsetZ]
|
|
1362
|
+
);
|
|
1363
|
+
return {
|
|
1364
|
+
raycaster,
|
|
1365
|
+
mouse,
|
|
1366
|
+
getIntersection,
|
|
1367
|
+
getAllIntersections,
|
|
1368
|
+
getGridCoordinates,
|
|
1369
|
+
getTileAtPosition,
|
|
1370
|
+
clientToNDC,
|
|
1371
|
+
isWithinCanvas
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
var EventBusContext = React8.createContext(null);
|
|
1375
|
+
|
|
1376
|
+
// hooks/useEventBus.ts
|
|
1377
|
+
function getGlobalEventBus() {
|
|
1378
|
+
if (typeof window !== "undefined") {
|
|
1379
|
+
return window.__kflowEventBus ?? null;
|
|
1380
|
+
}
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
var fallbackListeners = /* @__PURE__ */ new Map();
|
|
1384
|
+
var fallbackAnyListeners = /* @__PURE__ */ new Set();
|
|
1385
|
+
var fallbackEventBus = {
|
|
1386
|
+
emit: (type, payload) => {
|
|
1387
|
+
const event = {
|
|
1388
|
+
type,
|
|
1389
|
+
payload,
|
|
1390
|
+
timestamp: Date.now()
|
|
1391
|
+
};
|
|
1392
|
+
const handlers = fallbackListeners.get(type);
|
|
1393
|
+
if (handlers) {
|
|
1394
|
+
handlers.forEach((handler) => {
|
|
1395
|
+
try {
|
|
1396
|
+
handler(event);
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
console.error(`[EventBus] Error in listener for '${type}':`, error);
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
fallbackAnyListeners.forEach((handler) => {
|
|
1403
|
+
try {
|
|
1404
|
+
handler(event);
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
console.error(`[EventBus] Error in onAny listener for '${type}':`, error);
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
},
|
|
1410
|
+
on: (type, listener) => {
|
|
1411
|
+
if (!fallbackListeners.has(type)) {
|
|
1412
|
+
fallbackListeners.set(type, /* @__PURE__ */ new Set());
|
|
1413
|
+
}
|
|
1414
|
+
fallbackListeners.get(type).add(listener);
|
|
1415
|
+
return () => {
|
|
1416
|
+
const handlers = fallbackListeners.get(type);
|
|
1417
|
+
if (handlers) {
|
|
1418
|
+
handlers.delete(listener);
|
|
1419
|
+
if (handlers.size === 0) {
|
|
1420
|
+
fallbackListeners.delete(type);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
},
|
|
1425
|
+
once: (type, listener) => {
|
|
1426
|
+
const wrappedListener = (event) => {
|
|
1427
|
+
fallbackListeners.get(type)?.delete(wrappedListener);
|
|
1428
|
+
listener(event);
|
|
1429
|
+
};
|
|
1430
|
+
return fallbackEventBus.on(type, wrappedListener);
|
|
1431
|
+
},
|
|
1432
|
+
hasListeners: (type) => {
|
|
1433
|
+
const handlers = fallbackListeners.get(type);
|
|
1434
|
+
return handlers !== void 0 && handlers.size > 0;
|
|
1435
|
+
},
|
|
1436
|
+
onAny: (listener) => {
|
|
1437
|
+
fallbackAnyListeners.add(listener);
|
|
1438
|
+
return () => {
|
|
1439
|
+
fallbackAnyListeners.delete(listener);
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
function useEventBus() {
|
|
1444
|
+
const context = React8.useContext(EventBusContext);
|
|
1445
|
+
return context ?? getGlobalEventBus() ?? fallbackEventBus;
|
|
1446
|
+
}
|
|
1447
|
+
function useEmitEvent() {
|
|
1448
|
+
const eventBus = useEventBus();
|
|
1449
|
+
return React8.useCallback(
|
|
1450
|
+
(type, payload) => {
|
|
1451
|
+
eventBus.emit(type, payload);
|
|
1452
|
+
},
|
|
1453
|
+
[eventBus]
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// components/organisms/game/three/hooks/useGameCanvas3DEvents.ts
|
|
1458
|
+
function useGameCanvas3DEvents(options) {
|
|
1459
|
+
const {
|
|
1460
|
+
tileClickEvent,
|
|
1461
|
+
unitClickEvent,
|
|
1462
|
+
featureClickEvent,
|
|
1463
|
+
canvasClickEvent,
|
|
1464
|
+
tileHoverEvent,
|
|
1465
|
+
tileLeaveEvent,
|
|
1466
|
+
unitAnimationEvent,
|
|
1467
|
+
cameraChangeEvent,
|
|
1468
|
+
onTileClick,
|
|
1469
|
+
onUnitClick,
|
|
1470
|
+
onFeatureClick,
|
|
1471
|
+
onCanvasClick,
|
|
1472
|
+
onTileHover,
|
|
1473
|
+
onUnitAnimation
|
|
1474
|
+
} = options;
|
|
1475
|
+
const emit = useEmitEvent();
|
|
1476
|
+
const optionsRef = React8.useRef(options);
|
|
1477
|
+
optionsRef.current = options;
|
|
1478
|
+
const handleTileClick = React8.useCallback(
|
|
1479
|
+
(tile, event) => {
|
|
1480
|
+
if (tileClickEvent) {
|
|
1481
|
+
emit(tileClickEvent, {
|
|
1482
|
+
tileId: tile.id,
|
|
1483
|
+
x: tile.x,
|
|
1484
|
+
z: tile.z ?? tile.y ?? 0,
|
|
1485
|
+
type: tile.type,
|
|
1486
|
+
terrain: tile.terrain,
|
|
1487
|
+
elevation: tile.elevation
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
optionsRef.current.onTileClick?.(tile, event);
|
|
1491
|
+
},
|
|
1492
|
+
[tileClickEvent, emit]
|
|
1493
|
+
);
|
|
1494
|
+
const handleUnitClick = React8.useCallback(
|
|
1495
|
+
(unit, event) => {
|
|
1496
|
+
if (unitClickEvent) {
|
|
1497
|
+
emit(unitClickEvent, {
|
|
1498
|
+
unitId: unit.id,
|
|
1499
|
+
x: unit.x,
|
|
1500
|
+
z: unit.z ?? unit.y ?? 0,
|
|
1501
|
+
unitType: unit.unitType,
|
|
1502
|
+
name: unit.name,
|
|
1503
|
+
team: unit.team,
|
|
1504
|
+
faction: unit.faction,
|
|
1505
|
+
health: unit.health,
|
|
1506
|
+
maxHealth: unit.maxHealth
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
optionsRef.current.onUnitClick?.(unit, event);
|
|
1510
|
+
},
|
|
1511
|
+
[unitClickEvent, emit]
|
|
1512
|
+
);
|
|
1513
|
+
const handleFeatureClick = React8.useCallback(
|
|
1514
|
+
(feature, event) => {
|
|
1515
|
+
if (featureClickEvent) {
|
|
1516
|
+
emit(featureClickEvent, {
|
|
1517
|
+
featureId: feature.id,
|
|
1518
|
+
x: feature.x,
|
|
1519
|
+
z: feature.z ?? feature.y ?? 0,
|
|
1520
|
+
type: feature.type,
|
|
1521
|
+
elevation: feature.elevation
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
optionsRef.current.onFeatureClick?.(feature, event);
|
|
1525
|
+
},
|
|
1526
|
+
[featureClickEvent, emit]
|
|
1527
|
+
);
|
|
1528
|
+
const handleCanvasClick = React8.useCallback(
|
|
1529
|
+
(event) => {
|
|
1530
|
+
if (canvasClickEvent) {
|
|
1531
|
+
emit(canvasClickEvent, {
|
|
1532
|
+
clientX: event.clientX,
|
|
1533
|
+
clientY: event.clientY,
|
|
1534
|
+
button: event.button
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
optionsRef.current.onCanvasClick?.(event);
|
|
1538
|
+
},
|
|
1539
|
+
[canvasClickEvent, emit]
|
|
1540
|
+
);
|
|
1541
|
+
const handleTileHover = React8.useCallback(
|
|
1542
|
+
(tile, event) => {
|
|
1543
|
+
if (tile) {
|
|
1544
|
+
if (tileHoverEvent) {
|
|
1545
|
+
emit(tileHoverEvent, {
|
|
1546
|
+
tileId: tile.id,
|
|
1547
|
+
x: tile.x,
|
|
1548
|
+
z: tile.z ?? tile.y ?? 0,
|
|
1549
|
+
type: tile.type
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
} else {
|
|
1553
|
+
if (tileLeaveEvent) {
|
|
1554
|
+
emit(tileLeaveEvent, {});
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
optionsRef.current.onTileHover?.(tile, event);
|
|
1558
|
+
},
|
|
1559
|
+
[tileHoverEvent, tileLeaveEvent, emit]
|
|
1560
|
+
);
|
|
1561
|
+
const handleUnitAnimation = React8.useCallback(
|
|
1562
|
+
(unitId, state) => {
|
|
1563
|
+
if (unitAnimationEvent) {
|
|
1564
|
+
emit(unitAnimationEvent, {
|
|
1565
|
+
unitId,
|
|
1566
|
+
state,
|
|
1567
|
+
timestamp: Date.now()
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
optionsRef.current.onUnitAnimation?.(unitId, state);
|
|
1571
|
+
},
|
|
1572
|
+
[unitAnimationEvent, emit]
|
|
1573
|
+
);
|
|
1574
|
+
const handleCameraChange = React8.useCallback(
|
|
1575
|
+
(position) => {
|
|
1576
|
+
if (cameraChangeEvent) {
|
|
1577
|
+
emit(cameraChangeEvent, {
|
|
1578
|
+
position,
|
|
1579
|
+
timestamp: Date.now()
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
},
|
|
1583
|
+
[cameraChangeEvent, emit]
|
|
1584
|
+
);
|
|
1585
|
+
return {
|
|
1586
|
+
handleTileClick,
|
|
1587
|
+
handleUnitClick,
|
|
1588
|
+
handleFeatureClick,
|
|
1589
|
+
handleCanvasClick,
|
|
1590
|
+
handleTileHover,
|
|
1591
|
+
handleUnitAnimation,
|
|
1592
|
+
handleCameraChange
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
var DEFAULT_TERRAIN_COLORS = {
|
|
1596
|
+
grass: "#44aa44",
|
|
1597
|
+
dirt: "#8b7355",
|
|
1598
|
+
sand: "#ddcc88",
|
|
1599
|
+
water: "#4488cc",
|
|
1600
|
+
rock: "#888888",
|
|
1601
|
+
snow: "#eeeeee",
|
|
1602
|
+
forest: "#228b22",
|
|
1603
|
+
desert: "#d4a574",
|
|
1604
|
+
mountain: "#696969",
|
|
1605
|
+
swamp: "#556b2f"
|
|
1606
|
+
};
|
|
1607
|
+
function TileRenderer({
|
|
1608
|
+
tiles,
|
|
1609
|
+
cellSize = 1,
|
|
1610
|
+
offsetX = 0,
|
|
1611
|
+
offsetZ = 0,
|
|
1612
|
+
useInstancing = true,
|
|
1613
|
+
terrainColors = DEFAULT_TERRAIN_COLORS,
|
|
1614
|
+
onTileClick,
|
|
1615
|
+
onTileHover,
|
|
1616
|
+
selectedTileIds = [],
|
|
1617
|
+
validMoves = [],
|
|
1618
|
+
attackTargets = []
|
|
1619
|
+
}) {
|
|
1620
|
+
const meshRef = React8.useRef(null);
|
|
1621
|
+
const geometry = React8.useMemo(() => {
|
|
1622
|
+
return new THREE6__namespace.BoxGeometry(cellSize * 0.95, 0.2, cellSize * 0.95);
|
|
1623
|
+
}, [cellSize]);
|
|
1624
|
+
const material = React8.useMemo(() => {
|
|
1625
|
+
return new THREE6__namespace.MeshStandardMaterial({
|
|
1626
|
+
roughness: 0.8,
|
|
1627
|
+
metalness: 0.1
|
|
1628
|
+
});
|
|
1629
|
+
}, []);
|
|
1630
|
+
const { positions, colors, tileMap } = React8.useMemo(() => {
|
|
1631
|
+
const pos = [];
|
|
1632
|
+
const cols = [];
|
|
1633
|
+
const map = /* @__PURE__ */ new Map();
|
|
1634
|
+
tiles.forEach((tile) => {
|
|
1635
|
+
const x = (tile.x - offsetX) * cellSize;
|
|
1636
|
+
const z = ((tile.z ?? tile.y ?? 0) - offsetZ) * cellSize;
|
|
1637
|
+
const y = (tile.elevation ?? 0) * 0.1;
|
|
1638
|
+
pos.push(new THREE6__namespace.Vector3(x, y, z));
|
|
1639
|
+
const colorHex = terrainColors[tile.type || ""] || terrainColors[tile.terrain || ""] || "#808080";
|
|
1640
|
+
const color = new THREE6__namespace.Color(colorHex);
|
|
1641
|
+
const isValidMove = validMoves.some(
|
|
1642
|
+
(m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
|
|
1643
|
+
);
|
|
1644
|
+
const isAttackTarget = attackTargets.some(
|
|
1645
|
+
(m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
|
|
1646
|
+
);
|
|
1647
|
+
const isSelected = tile.id ? selectedTileIds.includes(tile.id) : false;
|
|
1648
|
+
if (isSelected) {
|
|
1649
|
+
color.addScalar(0.3);
|
|
1650
|
+
} else if (isAttackTarget) {
|
|
1651
|
+
color.setHex(16729156);
|
|
1652
|
+
} else if (isValidMove) {
|
|
1653
|
+
color.setHex(4521796);
|
|
1654
|
+
}
|
|
1655
|
+
cols.push(color);
|
|
1656
|
+
map.set(`${tile.x},${tile.z ?? tile.y ?? 0}`, tile);
|
|
1657
|
+
});
|
|
1658
|
+
return { positions: pos, colors: cols, tileMap: map };
|
|
1659
|
+
}, [tiles, cellSize, offsetX, offsetZ, terrainColors, selectedTileIds, validMoves, attackTargets]);
|
|
1660
|
+
React8.useEffect(() => {
|
|
1661
|
+
if (!meshRef.current || !useInstancing) return;
|
|
1662
|
+
const mesh = meshRef.current;
|
|
1663
|
+
mesh.count = positions.length;
|
|
1664
|
+
const dummy = new THREE6__namespace.Object3D();
|
|
1665
|
+
positions.forEach((pos, i) => {
|
|
1666
|
+
dummy.position.copy(pos);
|
|
1667
|
+
dummy.updateMatrix();
|
|
1668
|
+
mesh.setMatrixAt(i, dummy.matrix);
|
|
1669
|
+
if (mesh.setColorAt) {
|
|
1670
|
+
mesh.setColorAt(i, colors[i]);
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
1674
|
+
if (mesh.instanceColor) {
|
|
1675
|
+
mesh.instanceColor.needsUpdate = true;
|
|
1676
|
+
}
|
|
1677
|
+
}, [positions, colors, useInstancing]);
|
|
1678
|
+
const handlePointerMove = (e) => {
|
|
1679
|
+
if (!onTileHover) return;
|
|
1680
|
+
const instanceId = e.instanceId;
|
|
1681
|
+
if (instanceId !== void 0) {
|
|
1682
|
+
const pos = positions[instanceId];
|
|
1683
|
+
if (pos) {
|
|
1684
|
+
const gridX = Math.round(pos.x / cellSize + offsetX);
|
|
1685
|
+
const gridZ = Math.round(pos.z / cellSize + offsetZ);
|
|
1686
|
+
const tile = tileMap.get(`${gridX},${gridZ}`);
|
|
1687
|
+
if (tile) {
|
|
1688
|
+
onTileHover(tile);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
const handleClick = (e) => {
|
|
1694
|
+
if (!onTileClick) return;
|
|
1695
|
+
const instanceId = e.instanceId;
|
|
1696
|
+
if (instanceId !== void 0) {
|
|
1697
|
+
const pos = positions[instanceId];
|
|
1698
|
+
if (pos) {
|
|
1699
|
+
const gridX = Math.round(pos.x / cellSize + offsetX);
|
|
1700
|
+
const gridZ = Math.round(pos.z / cellSize + offsetZ);
|
|
1701
|
+
const tile = tileMap.get(`${gridX},${gridZ}`);
|
|
1702
|
+
if (tile) {
|
|
1703
|
+
onTileClick(tile);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
const renderIndividualTiles = () => {
|
|
1709
|
+
return tiles.map((tile) => {
|
|
1710
|
+
const x = (tile.x - offsetX) * cellSize;
|
|
1711
|
+
const z = ((tile.z ?? tile.y ?? 0) - offsetZ) * cellSize;
|
|
1712
|
+
const y = (tile.elevation ?? 0) * 0.1;
|
|
1713
|
+
const colorHex = terrainColors[tile.type || ""] || terrainColors[tile.terrain || ""] || "#808080";
|
|
1714
|
+
const isSelected = tile.id ? selectedTileIds.includes(tile.id) : false;
|
|
1715
|
+
const isValidMove = validMoves.some(
|
|
1716
|
+
(m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
|
|
1717
|
+
);
|
|
1718
|
+
const isAttackTarget = attackTargets.some(
|
|
1719
|
+
(m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
|
|
1720
|
+
);
|
|
1721
|
+
let emissive = "#000000";
|
|
1722
|
+
if (isSelected) emissive = "#444444";
|
|
1723
|
+
else if (isAttackTarget) emissive = "#440000";
|
|
1724
|
+
else if (isValidMove) emissive = "#004400";
|
|
1725
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1726
|
+
"mesh",
|
|
1727
|
+
{
|
|
1728
|
+
position: [x, y, z],
|
|
1729
|
+
userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
|
|
1730
|
+
onClick: () => onTileClick?.(tile),
|
|
1731
|
+
onPointerEnter: () => onTileHover?.(tile),
|
|
1732
|
+
onPointerLeave: () => onTileHover?.(null),
|
|
1733
|
+
children: [
|
|
1734
|
+
/* @__PURE__ */ jsxRuntime.jsx("boxGeometry", { args: [cellSize * 0.95, 0.2, cellSize * 0.95] }),
|
|
1735
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1736
|
+
"meshStandardMaterial",
|
|
1737
|
+
{
|
|
1738
|
+
color: colorHex,
|
|
1739
|
+
emissive,
|
|
1740
|
+
roughness: 0.8,
|
|
1741
|
+
metalness: 0.1
|
|
1742
|
+
}
|
|
1743
|
+
)
|
|
1744
|
+
]
|
|
1745
|
+
},
|
|
1746
|
+
tile.id ?? `tile-${tile.x}-${tile.y}`
|
|
1747
|
+
);
|
|
1748
|
+
});
|
|
1749
|
+
};
|
|
1750
|
+
if (useInstancing && tiles.length > 0) {
|
|
1751
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1752
|
+
"instancedMesh",
|
|
1753
|
+
{
|
|
1754
|
+
ref: meshRef,
|
|
1755
|
+
args: [geometry, material, tiles.length],
|
|
1756
|
+
onPointerMove: handlePointerMove,
|
|
1757
|
+
onClick: handleClick
|
|
1758
|
+
}
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
return /* @__PURE__ */ jsxRuntime.jsx("group", { children: renderIndividualTiles() });
|
|
1762
|
+
}
|
|
1763
|
+
function UnitVisual({ unit, position, isSelected, onClick }) {
|
|
1764
|
+
const groupRef = React8.useRef(null);
|
|
1765
|
+
const [animationState, setAnimationState] = React8.useState("idle");
|
|
1766
|
+
const [isHovered, setIsHovered] = React8.useState(false);
|
|
1767
|
+
const teamColor = React8.useMemo(() => {
|
|
1768
|
+
if (unit.faction === "player" || unit.team === "player") return 4491519;
|
|
1769
|
+
if (unit.faction === "enemy" || unit.team === "enemy") return 16729156;
|
|
1770
|
+
if (unit.faction === "neutral" || unit.team === "neutral") return 16777028;
|
|
1771
|
+
return 8947848;
|
|
1772
|
+
}, [unit.faction, unit.team]);
|
|
1773
|
+
fiber.useFrame((state) => {
|
|
1774
|
+
if (groupRef.current && animationState === "idle") {
|
|
1775
|
+
const y = position[1] + Math.sin(state.clock.elapsedTime * 2 + position[0]) * 0.05;
|
|
1776
|
+
groupRef.current.position.y = y;
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
const healthPercent = React8.useMemo(() => {
|
|
1780
|
+
if (unit.health === void 0 || unit.maxHealth === void 0) return 1;
|
|
1781
|
+
return Math.max(0, Math.min(1, unit.health / unit.maxHealth));
|
|
1782
|
+
}, [unit.health, unit.maxHealth]);
|
|
1783
|
+
const healthColor = React8.useMemo(() => {
|
|
1784
|
+
if (healthPercent > 0.5) return "#44aa44";
|
|
1785
|
+
if (healthPercent > 0.25) return "#aaaa44";
|
|
1786
|
+
return "#ff4444";
|
|
1787
|
+
}, [healthPercent]);
|
|
1788
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1789
|
+
"group",
|
|
1790
|
+
{
|
|
1791
|
+
ref: groupRef,
|
|
1792
|
+
position,
|
|
1793
|
+
onClick,
|
|
1794
|
+
onPointerEnter: () => setIsHovered(true),
|
|
1795
|
+
onPointerLeave: () => setIsHovered(false),
|
|
1796
|
+
userData: { type: "unit", unitId: unit.id },
|
|
1797
|
+
children: [
|
|
1798
|
+
isSelected && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.05, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
1799
|
+
/* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
1800
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
|
|
1801
|
+
] }),
|
|
1802
|
+
isHovered && !isSelected && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.05, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
1803
|
+
/* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
1804
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#ffffff", transparent: true, opacity: 0.5 })
|
|
1805
|
+
] }),
|
|
1806
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.1, 0], children: [
|
|
1807
|
+
/* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.25, 0.25, 0.1, 8] }),
|
|
1808
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: teamColor })
|
|
1809
|
+
] }),
|
|
1810
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.5, 0], children: [
|
|
1811
|
+
/* @__PURE__ */ jsxRuntime.jsx("capsuleGeometry", { args: [0.15, 0.5, 4, 8] }),
|
|
1812
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: teamColor })
|
|
1813
|
+
] }),
|
|
1814
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.9, 0], children: [
|
|
1815
|
+
/* @__PURE__ */ jsxRuntime.jsx("sphereGeometry", { args: [0.12, 8, 8] }),
|
|
1816
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: teamColor })
|
|
1817
|
+
] }),
|
|
1818
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 1.3, 0], children: [
|
|
1819
|
+
/* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [0.5, 0.06] }),
|
|
1820
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#333333" })
|
|
1821
|
+
] }),
|
|
1822
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [-0.25 + 0.25 * healthPercent, 1.3, 0.01], children: [
|
|
1823
|
+
/* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [0.5 * healthPercent, 0.04] }),
|
|
1824
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: healthColor })
|
|
1825
|
+
] }),
|
|
1826
|
+
unit.name && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 1.5, 0], children: [
|
|
1827
|
+
/* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [0.4, 0.1] }),
|
|
1828
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#000000", transparent: true, opacity: 0.5 })
|
|
1829
|
+
] })
|
|
1830
|
+
]
|
|
1831
|
+
}
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
function UnitRenderer({
|
|
1835
|
+
units,
|
|
1836
|
+
cellSize = 1,
|
|
1837
|
+
offsetX = 0,
|
|
1838
|
+
offsetZ = 0,
|
|
1839
|
+
selectedUnitId,
|
|
1840
|
+
onUnitClick,
|
|
1841
|
+
onAnimationStateChange,
|
|
1842
|
+
animationSpeed = 1
|
|
1843
|
+
}) {
|
|
1844
|
+
const handleUnitClick = React8__default.default.useCallback(
|
|
1845
|
+
(unit) => {
|
|
1846
|
+
onUnitClick?.(unit);
|
|
1847
|
+
},
|
|
1848
|
+
[onUnitClick]
|
|
1849
|
+
);
|
|
1850
|
+
return /* @__PURE__ */ jsxRuntime.jsx("group", { children: units.map((unit) => {
|
|
1851
|
+
const unitX = unit.x ?? unit.position?.x ?? 0;
|
|
1852
|
+
const unitY = unit.z ?? unit.y ?? unit.position?.y ?? 0;
|
|
1853
|
+
const x = (unitX - offsetX) * cellSize;
|
|
1854
|
+
const z = (unitY - offsetZ) * cellSize;
|
|
1855
|
+
const y = (unit.elevation ?? 0) * 0.1 + 0.5;
|
|
1856
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1857
|
+
UnitVisual,
|
|
1858
|
+
{
|
|
1859
|
+
unit,
|
|
1860
|
+
position: [x, y, z],
|
|
1861
|
+
isSelected: selectedUnitId === unit.id,
|
|
1862
|
+
onClick: () => handleUnitClick(unit)
|
|
1863
|
+
},
|
|
1864
|
+
unit.id
|
|
1865
|
+
);
|
|
1866
|
+
}) });
|
|
1867
|
+
}
|
|
1868
|
+
var DEFAULT_FEATURE_CONFIGS = {
|
|
1869
|
+
tree: { color: 2263842, height: 1.5, scale: 1, geometry: "tree" },
|
|
1870
|
+
rock: { color: 8421504, height: 0.5, scale: 0.8, geometry: "rock" },
|
|
1871
|
+
bush: { color: 3329330, height: 0.4, scale: 0.6, geometry: "bush" },
|
|
1872
|
+
house: { color: 9127187, height: 1.2, scale: 1.2, geometry: "house" },
|
|
1873
|
+
tower: { color: 6908265, height: 2.5, scale: 1, geometry: "tower" },
|
|
1874
|
+
wall: { color: 8421504, height: 1, scale: 1, geometry: "wall" },
|
|
1875
|
+
mountain: { color: 5597999, height: 2, scale: 1.5, geometry: "mountain" },
|
|
1876
|
+
hill: { color: 7048739, height: 0.8, scale: 1.2, geometry: "hill" },
|
|
1877
|
+
water: { color: 4491468, height: 0.1, scale: 1, geometry: "water" },
|
|
1878
|
+
chest: { color: 16766720, height: 0.3, scale: 0.4, geometry: "chest" },
|
|
1879
|
+
sign: { color: 9127187, height: 0.8, scale: 0.3, geometry: "sign" },
|
|
1880
|
+
portal: { color: 10040012, height: 1.5, scale: 1, geometry: "portal" }
|
|
1881
|
+
};
|
|
1882
|
+
function TreeFeature({ height, color }) {
|
|
1883
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1884
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, height * 0.3, 0], children: [
|
|
1885
|
+
/* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.08, 0.1, height * 0.6, 6] }),
|
|
1886
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 9127187 })
|
|
1887
|
+
] }),
|
|
1888
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, height * 0.7, 0], children: [
|
|
1889
|
+
/* @__PURE__ */ jsxRuntime.jsx("coneGeometry", { args: [0.4, height * 0.5, 8] }),
|
|
1890
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
1891
|
+
] }),
|
|
1892
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, height * 0.9, 0], children: [
|
|
1893
|
+
/* @__PURE__ */ jsxRuntime.jsx("coneGeometry", { args: [0.3, height * 0.4, 8] }),
|
|
1894
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
1895
|
+
] }),
|
|
1896
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, height * 1.05, 0], children: [
|
|
1897
|
+
/* @__PURE__ */ jsxRuntime.jsx("coneGeometry", { args: [0.15, height * 0.25, 8] }),
|
|
1898
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
1899
|
+
] })
|
|
1900
|
+
] });
|
|
1901
|
+
}
|
|
1902
|
+
function RockFeature({ height, color }) {
|
|
1903
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, height * 0.4, 0], children: [
|
|
1904
|
+
/* @__PURE__ */ jsxRuntime.jsx("dodecahedronGeometry", { args: [height * 0.5, 0] }),
|
|
1905
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color, roughness: 0.9 })
|
|
1906
|
+
] });
|
|
1907
|
+
}
|
|
1908
|
+
function BushFeature({ height, color }) {
|
|
1909
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1910
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, height * 0.3, 0], children: [
|
|
1911
|
+
/* @__PURE__ */ jsxRuntime.jsx("sphereGeometry", { args: [height * 0.4, 8, 8] }),
|
|
1912
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
1913
|
+
] }),
|
|
1914
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0.1, height * 0.4, 0.1], children: [
|
|
1915
|
+
/* @__PURE__ */ jsxRuntime.jsx("sphereGeometry", { args: [height * 0.25, 8, 8] }),
|
|
1916
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
1917
|
+
] })
|
|
1918
|
+
] });
|
|
1919
|
+
}
|
|
1920
|
+
function HouseFeature({ height, color }) {
|
|
1921
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1922
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, height * 0.4, 0], children: [
|
|
1923
|
+
/* @__PURE__ */ jsxRuntime.jsx("boxGeometry", { args: [0.8, height * 0.8, 0.8] }),
|
|
1924
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 13808780 })
|
|
1925
|
+
] }),
|
|
1926
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, height * 0.9, 0], children: [
|
|
1927
|
+
/* @__PURE__ */ jsxRuntime.jsx("coneGeometry", { args: [0.6, height * 0.4, 4] }),
|
|
1928
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
1929
|
+
] }),
|
|
1930
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, height * 0.25, 0.41], children: [
|
|
1931
|
+
/* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [0.25, height * 0.5] }),
|
|
1932
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 4863784 })
|
|
1933
|
+
] })
|
|
1934
|
+
] });
|
|
1935
|
+
}
|
|
1936
|
+
function TowerFeature({ height, color }) {
|
|
1937
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1938
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, height * 0.5, 0], children: [
|
|
1939
|
+
/* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.3, 0.35, height, 8] }),
|
|
1940
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
1941
|
+
] }),
|
|
1942
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, height + 0.05, 0], children: [
|
|
1943
|
+
/* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.35, 0.35, 0.1, 8] }),
|
|
1944
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
1945
|
+
] })
|
|
1946
|
+
] });
|
|
1947
|
+
}
|
|
1948
|
+
function ChestFeature({ height, color }) {
|
|
1949
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1950
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, height * 0.5, 0], children: [
|
|
1951
|
+
/* @__PURE__ */ jsxRuntime.jsx("boxGeometry", { args: [0.3, height, 0.2] }),
|
|
1952
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color, metalness: 0.6, roughness: 0.3 })
|
|
1953
|
+
] }),
|
|
1954
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, height + 0.05, 0], children: [
|
|
1955
|
+
/* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.15, 0.15, 0.3, 8, 1, false, 0, Math.PI] }),
|
|
1956
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color, metalness: 0.6, roughness: 0.3 })
|
|
1957
|
+
] })
|
|
1958
|
+
] });
|
|
1959
|
+
}
|
|
1960
|
+
function DefaultFeature({ height, color }) {
|
|
1961
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, height * 0.5, 0], children: [
|
|
1962
|
+
/* @__PURE__ */ jsxRuntime.jsx("boxGeometry", { args: [0.5, height, 0.5] }),
|
|
1963
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
1964
|
+
] });
|
|
1965
|
+
}
|
|
1966
|
+
function FeatureVisual({
|
|
1967
|
+
feature,
|
|
1968
|
+
position,
|
|
1969
|
+
isSelected,
|
|
1970
|
+
onClick,
|
|
1971
|
+
onHover
|
|
1972
|
+
}) {
|
|
1973
|
+
const config = DEFAULT_FEATURE_CONFIGS[feature.type] || {
|
|
1974
|
+
color: 8947848,
|
|
1975
|
+
height: 0.5,
|
|
1976
|
+
scale: 1,
|
|
1977
|
+
geometry: "default"
|
|
1978
|
+
};
|
|
1979
|
+
const color = feature.color ? parseInt(feature.color.replace("#", ""), 16) : config.color;
|
|
1980
|
+
const renderGeometry = () => {
|
|
1981
|
+
switch (config.geometry) {
|
|
1982
|
+
case "tree":
|
|
1983
|
+
return /* @__PURE__ */ jsxRuntime.jsx(TreeFeature, { height: config.height, color });
|
|
1984
|
+
case "rock":
|
|
1985
|
+
return /* @__PURE__ */ jsxRuntime.jsx(RockFeature, { height: config.height, color });
|
|
1986
|
+
case "bush":
|
|
1987
|
+
return /* @__PURE__ */ jsxRuntime.jsx(BushFeature, { height: config.height, color });
|
|
1988
|
+
case "house":
|
|
1989
|
+
return /* @__PURE__ */ jsxRuntime.jsx(HouseFeature, { height: config.height, color });
|
|
1990
|
+
case "tower":
|
|
1991
|
+
return /* @__PURE__ */ jsxRuntime.jsx(TowerFeature, { height: config.height, color });
|
|
1992
|
+
case "chest":
|
|
1993
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ChestFeature, { height: config.height, color });
|
|
1994
|
+
default:
|
|
1995
|
+
return /* @__PURE__ */ jsxRuntime.jsx(DefaultFeature, { height: config.height, color });
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1999
|
+
"group",
|
|
2000
|
+
{
|
|
2001
|
+
position,
|
|
2002
|
+
scale: config.scale,
|
|
2003
|
+
onClick,
|
|
2004
|
+
onPointerEnter: () => onHover(true),
|
|
2005
|
+
onPointerLeave: () => onHover(false),
|
|
2006
|
+
userData: { type: "feature", featureId: feature.id, featureType: feature.type },
|
|
2007
|
+
children: [
|
|
2008
|
+
isSelected && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.02, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
2009
|
+
/* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
2010
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
|
|
2011
|
+
] }),
|
|
2012
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.01, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
2013
|
+
/* @__PURE__ */ jsxRuntime.jsx("circleGeometry", { args: [0.35, 16] }),
|
|
2014
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#000000", transparent: true, opacity: 0.2 })
|
|
2015
|
+
] }),
|
|
2016
|
+
renderGeometry()
|
|
2017
|
+
]
|
|
2018
|
+
}
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
function FeatureRenderer({
|
|
2022
|
+
features,
|
|
2023
|
+
cellSize = 1,
|
|
2024
|
+
offsetX = 0,
|
|
2025
|
+
offsetZ = 0,
|
|
2026
|
+
onFeatureClick,
|
|
2027
|
+
onFeatureHover,
|
|
2028
|
+
selectedFeatureIds = [],
|
|
2029
|
+
featureColors
|
|
2030
|
+
}) {
|
|
2031
|
+
return /* @__PURE__ */ jsxRuntime.jsx("group", { children: features.map((feature) => {
|
|
2032
|
+
const x = (feature.x - offsetX) * cellSize;
|
|
2033
|
+
const z = ((feature.z ?? feature.y ?? 0) - offsetZ) * cellSize;
|
|
2034
|
+
const y = (feature.elevation ?? 0) * 0.1;
|
|
2035
|
+
const isSelected = feature.id ? selectedFeatureIds.includes(feature.id) : false;
|
|
2036
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2037
|
+
FeatureVisual,
|
|
2038
|
+
{
|
|
2039
|
+
feature,
|
|
2040
|
+
position: [x, y, z],
|
|
2041
|
+
isSelected,
|
|
2042
|
+
onClick: () => onFeatureClick?.(feature),
|
|
2043
|
+
onHover: (hovered) => onFeatureHover?.(hovered ? feature : null)
|
|
2044
|
+
},
|
|
2045
|
+
feature.id ?? `feature-${feature.x}-${feature.y}`
|
|
2046
|
+
);
|
|
2047
|
+
}) });
|
|
2048
|
+
}
|
|
2049
|
+
function detectAssetRoot3(modelUrl) {
|
|
2050
|
+
const idx = modelUrl.indexOf("/3d/");
|
|
2051
|
+
if (idx !== -1) {
|
|
2052
|
+
return modelUrl.substring(0, idx + 4);
|
|
2053
|
+
}
|
|
2054
|
+
return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
|
|
2055
|
+
}
|
|
2056
|
+
function useGLTFModel2(url) {
|
|
2057
|
+
const [model, setModel] = React8.useState(null);
|
|
2058
|
+
const [isLoading, setIsLoading] = React8.useState(false);
|
|
2059
|
+
const [error, setError] = React8.useState(null);
|
|
2060
|
+
React8.useEffect(() => {
|
|
2061
|
+
if (!url) {
|
|
2062
|
+
setModel(null);
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
setIsLoading(true);
|
|
2066
|
+
setError(null);
|
|
2067
|
+
const assetRoot = detectAssetRoot3(url);
|
|
2068
|
+
const loader = new GLTFLoader.GLTFLoader();
|
|
2069
|
+
loader.setResourcePath(assetRoot);
|
|
2070
|
+
loader.load(
|
|
2071
|
+
url,
|
|
2072
|
+
(gltf) => {
|
|
2073
|
+
setModel(gltf.scene);
|
|
2074
|
+
setIsLoading(false);
|
|
2075
|
+
},
|
|
2076
|
+
void 0,
|
|
2077
|
+
(err) => {
|
|
2078
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2079
|
+
setIsLoading(false);
|
|
2080
|
+
}
|
|
2081
|
+
);
|
|
2082
|
+
}, [url]);
|
|
2083
|
+
return { model, isLoading, error };
|
|
2084
|
+
}
|
|
2085
|
+
function FeatureModel({
|
|
2086
|
+
feature,
|
|
2087
|
+
position,
|
|
2088
|
+
isSelected,
|
|
2089
|
+
onClick,
|
|
2090
|
+
onHover
|
|
2091
|
+
}) {
|
|
2092
|
+
const groupRef = React8.useRef(null);
|
|
2093
|
+
const { model: loadedModel, isLoading } = useGLTFModel2(feature.assetUrl);
|
|
2094
|
+
const model = React8.useMemo(() => {
|
|
2095
|
+
if (!loadedModel) return null;
|
|
2096
|
+
const cloned = loadedModel.clone();
|
|
2097
|
+
cloned.scale.setScalar(0.3);
|
|
2098
|
+
cloned.traverse((child) => {
|
|
2099
|
+
if (child instanceof THREE6__namespace.Mesh) {
|
|
2100
|
+
child.castShadow = true;
|
|
2101
|
+
child.receiveShadow = true;
|
|
2102
|
+
}
|
|
2103
|
+
});
|
|
2104
|
+
return cloned;
|
|
2105
|
+
}, [loadedModel]);
|
|
2106
|
+
fiber.useFrame((state) => {
|
|
2107
|
+
if (groupRef.current) {
|
|
2108
|
+
const featureRotation = feature.rotation;
|
|
2109
|
+
const baseRotation = featureRotation !== void 0 ? featureRotation * Math.PI / 180 - Math.PI / 4 : -Math.PI / 4;
|
|
2110
|
+
const wobble = isSelected ? Math.sin(state.clock.elapsedTime * 2) * 0.1 : 0;
|
|
2111
|
+
groupRef.current.rotation.y = baseRotation + wobble;
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
2114
|
+
if (isLoading) {
|
|
2115
|
+
return /* @__PURE__ */ jsxRuntime.jsx("group", { position, children: /* @__PURE__ */ jsxRuntime.jsxs("mesh", { rotation: [Math.PI / 2, 0, 0], children: [
|
|
2116
|
+
/* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.3, 0.35, 16] }),
|
|
2117
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#4a90d9", transparent: true, opacity: 0.8 })
|
|
2118
|
+
] }) });
|
|
2119
|
+
}
|
|
2120
|
+
if (!model && !feature.assetUrl) {
|
|
2121
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2122
|
+
"group",
|
|
2123
|
+
{
|
|
2124
|
+
position,
|
|
2125
|
+
onClick,
|
|
2126
|
+
onPointerEnter: () => onHover(true),
|
|
2127
|
+
onPointerLeave: () => onHover(false),
|
|
2128
|
+
userData: { type: "feature", featureId: feature.id, featureType: feature.type },
|
|
2129
|
+
children: [
|
|
2130
|
+
isSelected && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.02, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
2131
|
+
/* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
2132
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
|
|
2133
|
+
] }),
|
|
2134
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.5, 0], children: [
|
|
2135
|
+
/* @__PURE__ */ jsxRuntime.jsx("boxGeometry", { args: [0.4, 0.4, 0.4] }),
|
|
2136
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 8947848 })
|
|
2137
|
+
] })
|
|
2138
|
+
]
|
|
2139
|
+
}
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2143
|
+
"group",
|
|
2144
|
+
{
|
|
2145
|
+
ref: groupRef,
|
|
2146
|
+
position,
|
|
2147
|
+
onClick,
|
|
2148
|
+
onPointerEnter: () => onHover(true),
|
|
2149
|
+
onPointerLeave: () => onHover(false),
|
|
2150
|
+
userData: { type: "feature", featureId: feature.id, featureType: feature.type },
|
|
2151
|
+
children: [
|
|
2152
|
+
isSelected && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.02, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
2153
|
+
/* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
2154
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
|
|
2155
|
+
] }),
|
|
2156
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.01, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
2157
|
+
/* @__PURE__ */ jsxRuntime.jsx("circleGeometry", { args: [0.35, 16] }),
|
|
2158
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#000000", transparent: true, opacity: 0.2 })
|
|
2159
|
+
] }),
|
|
2160
|
+
model && /* @__PURE__ */ jsxRuntime.jsx("primitive", { object: model })
|
|
2161
|
+
]
|
|
2162
|
+
}
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
function FeatureRenderer3D({
|
|
2166
|
+
features,
|
|
2167
|
+
cellSize = 1,
|
|
2168
|
+
offsetX = 0,
|
|
2169
|
+
offsetZ = 0,
|
|
2170
|
+
onFeatureClick,
|
|
2171
|
+
onFeatureHover,
|
|
2172
|
+
selectedFeatureIds = []
|
|
2173
|
+
}) {
|
|
2174
|
+
return /* @__PURE__ */ jsxRuntime.jsx("group", { children: features.map((feature) => {
|
|
2175
|
+
const x = (feature.x - offsetX) * cellSize;
|
|
2176
|
+
const z = ((feature.z ?? feature.y ?? 0) - offsetZ) * cellSize;
|
|
2177
|
+
const y = (feature.elevation ?? 0) * 0.1;
|
|
2178
|
+
const isSelected = feature.id ? selectedFeatureIds.includes(feature.id) : false;
|
|
2179
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2180
|
+
FeatureModel,
|
|
2181
|
+
{
|
|
2182
|
+
feature,
|
|
2183
|
+
position: [x, y, z],
|
|
2184
|
+
isSelected,
|
|
2185
|
+
onClick: () => onFeatureClick?.(feature),
|
|
2186
|
+
onHover: (hovered) => onFeatureHover?.(hovered ? feature : null)
|
|
2187
|
+
},
|
|
2188
|
+
feature.id ?? `feature-${feature.x}-${feature.y}`
|
|
2189
|
+
);
|
|
2190
|
+
}) });
|
|
2191
|
+
}
|
|
2192
|
+
function preloadFeatures(urls) {
|
|
2193
|
+
urls.forEach((url) => {
|
|
2194
|
+
if (url) {
|
|
2195
|
+
const loader = new GLTFLoader.GLTFLoader();
|
|
2196
|
+
loader.setResourcePath(detectAssetRoot3(url));
|
|
2197
|
+
loader.load(url, () => {
|
|
2198
|
+
console.log("[FeatureRenderer3D] Preloaded:", url);
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
var DEFAULT_CONFIG = {
|
|
2204
|
+
cellSize: 1,
|
|
2205
|
+
offsetX: 0,
|
|
2206
|
+
offsetZ: 0,
|
|
2207
|
+
elevation: 0
|
|
2208
|
+
};
|
|
2209
|
+
function gridToWorld(gridX, gridZ, config = DEFAULT_CONFIG) {
|
|
2210
|
+
const opts = { ...DEFAULT_CONFIG, ...config };
|
|
2211
|
+
return new THREE6__namespace.Vector3(
|
|
2212
|
+
gridX * opts.cellSize + opts.offsetX,
|
|
2213
|
+
opts.elevation,
|
|
2214
|
+
gridZ * opts.cellSize + opts.offsetZ
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
function worldToGrid(worldX, worldZ, config = DEFAULT_CONFIG) {
|
|
2218
|
+
const opts = { ...DEFAULT_CONFIG, ...config };
|
|
2219
|
+
return {
|
|
2220
|
+
x: Math.round((worldX - opts.offsetX) / opts.cellSize),
|
|
2221
|
+
z: Math.round((worldZ - opts.offsetZ) / opts.cellSize)
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
function raycastToPlane(camera, mouseX, mouseY, planeY = 0) {
|
|
2225
|
+
const raycaster = new THREE6__namespace.Raycaster();
|
|
2226
|
+
const mouse = new THREE6__namespace.Vector2(mouseX, mouseY);
|
|
2227
|
+
raycaster.setFromCamera(mouse, camera);
|
|
2228
|
+
const plane = new THREE6__namespace.Plane(new THREE6__namespace.Vector3(0, 1, 0), -planeY);
|
|
2229
|
+
const target = new THREE6__namespace.Vector3();
|
|
2230
|
+
const intersection = raycaster.ray.intersectPlane(plane, target);
|
|
2231
|
+
return intersection ? target : null;
|
|
2232
|
+
}
|
|
2233
|
+
function raycastToObjects(camera, mouseX, mouseY, objects) {
|
|
2234
|
+
const raycaster = new THREE6__namespace.Raycaster();
|
|
2235
|
+
const mouse = new THREE6__namespace.Vector2(mouseX, mouseY);
|
|
2236
|
+
raycaster.setFromCamera(mouse, camera);
|
|
2237
|
+
const intersects = raycaster.intersectObjects(objects, true);
|
|
2238
|
+
return intersects.length > 0 ? intersects[0] : null;
|
|
2239
|
+
}
|
|
2240
|
+
function gridDistance(a, b) {
|
|
2241
|
+
const dx = b.x - a.x;
|
|
2242
|
+
const dz = b.z - a.z;
|
|
2243
|
+
return Math.sqrt(dx * dx + dz * dz);
|
|
2244
|
+
}
|
|
2245
|
+
function gridManhattanDistance(a, b) {
|
|
2246
|
+
return Math.abs(b.x - a.x) + Math.abs(b.z - a.z);
|
|
2247
|
+
}
|
|
2248
|
+
function getNeighbors(x, z, includeDiagonal = false) {
|
|
2249
|
+
const neighbors = [
|
|
2250
|
+
{ x: x + 1, z },
|
|
2251
|
+
{ x: x - 1, z },
|
|
2252
|
+
{ x, z: z + 1 },
|
|
2253
|
+
{ x, z: z - 1 }
|
|
2254
|
+
];
|
|
2255
|
+
if (includeDiagonal) {
|
|
2256
|
+
neighbors.push(
|
|
2257
|
+
{ x: x + 1, z: z + 1 },
|
|
2258
|
+
{ x: x - 1, z: z - 1 },
|
|
2259
|
+
{ x: x + 1, z: z - 1 },
|
|
2260
|
+
{ x: x - 1, z: z + 1 }
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
return neighbors;
|
|
2264
|
+
}
|
|
2265
|
+
function isInBounds(x, z, bounds) {
|
|
2266
|
+
return x >= bounds.minX && x <= bounds.maxX && z >= bounds.minZ && z <= bounds.maxZ;
|
|
2267
|
+
}
|
|
2268
|
+
function getCellsInRadius(centerX, centerZ, radius) {
|
|
2269
|
+
const cells = [];
|
|
2270
|
+
const radiusSquared = radius * radius;
|
|
2271
|
+
const minX = Math.floor(centerX - radius);
|
|
2272
|
+
const maxX = Math.ceil(centerX + radius);
|
|
2273
|
+
const minZ = Math.floor(centerZ - radius);
|
|
2274
|
+
const maxZ = Math.ceil(centerZ + radius);
|
|
2275
|
+
for (let x = minX; x <= maxX; x++) {
|
|
2276
|
+
for (let z = minZ; z <= maxZ; z++) {
|
|
2277
|
+
const dx = x - centerX;
|
|
2278
|
+
const dz = z - centerZ;
|
|
2279
|
+
if (dx * dx + dz * dz <= radiusSquared) {
|
|
2280
|
+
cells.push({ x, z });
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
return cells;
|
|
2285
|
+
}
|
|
2286
|
+
function createGridHighlight(color = 16776960, opacity = 0.3) {
|
|
2287
|
+
const geometry = new THREE6__namespace.PlaneGeometry(0.95, 0.95);
|
|
2288
|
+
const material = new THREE6__namespace.MeshBasicMaterial({
|
|
2289
|
+
color,
|
|
2290
|
+
transparent: true,
|
|
2291
|
+
opacity,
|
|
2292
|
+
side: THREE6__namespace.DoubleSide
|
|
2293
|
+
});
|
|
2294
|
+
const mesh = new THREE6__namespace.Mesh(geometry, material);
|
|
2295
|
+
mesh.rotation.x = -Math.PI / 2;
|
|
2296
|
+
mesh.position.y = 0.01;
|
|
2297
|
+
return mesh;
|
|
2298
|
+
}
|
|
2299
|
+
function normalizeMouseCoordinates(clientX, clientY, element) {
|
|
2300
|
+
const rect = element.getBoundingClientRect();
|
|
2301
|
+
return {
|
|
2302
|
+
x: (clientX - rect.left) / rect.width * 2 - 1,
|
|
2303
|
+
y: -((clientY - rect.top) / rect.height) * 2 + 1
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
function isInFrustum(position, camera, padding = 0) {
|
|
2307
|
+
const frustum = new THREE6__namespace.Frustum();
|
|
2308
|
+
const projScreenMatrix = new THREE6__namespace.Matrix4();
|
|
2309
|
+
projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
2310
|
+
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
2311
|
+
const sphere = new THREE6__namespace.Sphere(position, padding);
|
|
2312
|
+
return frustum.intersectsSphere(sphere);
|
|
2313
|
+
}
|
|
2314
|
+
function filterByFrustum(positions, camera, padding = 0) {
|
|
2315
|
+
const frustum = new THREE6__namespace.Frustum();
|
|
2316
|
+
const projScreenMatrix = new THREE6__namespace.Matrix4();
|
|
2317
|
+
projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
2318
|
+
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
2319
|
+
return positions.filter((position) => {
|
|
2320
|
+
const sphere = new THREE6__namespace.Sphere(position, padding);
|
|
2321
|
+
return frustum.intersectsSphere(sphere);
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
function getVisibleIndices(positions, camera, padding = 0) {
|
|
2325
|
+
const frustum = new THREE6__namespace.Frustum();
|
|
2326
|
+
const projScreenMatrix = new THREE6__namespace.Matrix4();
|
|
2327
|
+
const visible = /* @__PURE__ */ new Set();
|
|
2328
|
+
projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
2329
|
+
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
2330
|
+
positions.forEach((position, index) => {
|
|
2331
|
+
const sphere = new THREE6__namespace.Sphere(position, padding);
|
|
2332
|
+
if (frustum.intersectsSphere(sphere)) {
|
|
2333
|
+
visible.add(index);
|
|
2334
|
+
}
|
|
2335
|
+
});
|
|
2336
|
+
return visible;
|
|
2337
|
+
}
|
|
2338
|
+
function calculateLODLevel(position, camera, lodLevels) {
|
|
2339
|
+
const distance = position.distanceTo(camera.position);
|
|
2340
|
+
for (let i = 0; i < lodLevels.length; i++) {
|
|
2341
|
+
if (distance < lodLevels[i]) {
|
|
2342
|
+
return i;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
return lodLevels.length;
|
|
2346
|
+
}
|
|
2347
|
+
function updateInstanceLOD(instancedMesh, positions, camera, lodDistances) {
|
|
2348
|
+
const lodIndices = new Uint8Array(positions.length);
|
|
2349
|
+
positions.forEach((position, index) => {
|
|
2350
|
+
lodIndices[index] = calculateLODLevel(position, camera, lodDistances);
|
|
2351
|
+
});
|
|
2352
|
+
return lodIndices;
|
|
2353
|
+
}
|
|
2354
|
+
function cullInstancedMesh(instancedMesh, positions, visibleIndices) {
|
|
2355
|
+
const dummy = new THREE6__namespace.Object3D();
|
|
2356
|
+
let visibleCount = 0;
|
|
2357
|
+
positions.forEach((position, index) => {
|
|
2358
|
+
if (visibleIndices.has(index)) {
|
|
2359
|
+
dummy.position.copy(position);
|
|
2360
|
+
dummy.updateMatrix();
|
|
2361
|
+
instancedMesh.setMatrixAt(visibleCount, dummy.matrix);
|
|
2362
|
+
visibleCount++;
|
|
2363
|
+
}
|
|
2364
|
+
});
|
|
2365
|
+
instancedMesh.count = visibleCount;
|
|
2366
|
+
instancedMesh.instanceMatrix.needsUpdate = true;
|
|
2367
|
+
return visibleCount;
|
|
2368
|
+
}
|
|
2369
|
+
var SpatialHashGrid = class {
|
|
2370
|
+
constructor(cellSize = 10) {
|
|
2371
|
+
__publicField(this, "cellSize");
|
|
2372
|
+
__publicField(this, "cells");
|
|
2373
|
+
__publicField(this, "objectPositions");
|
|
2374
|
+
this.cellSize = cellSize;
|
|
2375
|
+
this.cells = /* @__PURE__ */ new Map();
|
|
2376
|
+
this.objectPositions = /* @__PURE__ */ new Map();
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Get cell key for a position
|
|
2380
|
+
*/
|
|
2381
|
+
getCellKey(x, z) {
|
|
2382
|
+
const cellX = Math.floor(x / this.cellSize);
|
|
2383
|
+
const cellZ = Math.floor(z / this.cellSize);
|
|
2384
|
+
return `${cellX},${cellZ}`;
|
|
2385
|
+
}
|
|
2386
|
+
/**
|
|
2387
|
+
* Insert an object into the grid
|
|
2388
|
+
*/
|
|
2389
|
+
insert(id, position) {
|
|
2390
|
+
const key = this.getCellKey(position.x, position.z);
|
|
2391
|
+
if (!this.cells.has(key)) {
|
|
2392
|
+
this.cells.set(key, /* @__PURE__ */ new Set());
|
|
2393
|
+
}
|
|
2394
|
+
this.cells.get(key).add(id);
|
|
2395
|
+
this.objectPositions.set(id, position.clone());
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* Remove an object from the grid
|
|
2399
|
+
*/
|
|
2400
|
+
remove(id) {
|
|
2401
|
+
const position = this.objectPositions.get(id);
|
|
2402
|
+
if (position) {
|
|
2403
|
+
const key = this.getCellKey(position.x, position.z);
|
|
2404
|
+
this.cells.get(key)?.delete(id);
|
|
2405
|
+
this.objectPositions.delete(id);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Update an object's position
|
|
2410
|
+
*/
|
|
2411
|
+
update(id, newPosition) {
|
|
2412
|
+
this.remove(id);
|
|
2413
|
+
this.insert(id, newPosition);
|
|
2414
|
+
}
|
|
2415
|
+
/**
|
|
2416
|
+
* Query objects within a radius of a position
|
|
2417
|
+
*/
|
|
2418
|
+
queryRadius(center, radius) {
|
|
2419
|
+
const results = [];
|
|
2420
|
+
const radiusSquared = radius * radius;
|
|
2421
|
+
const minCellX = Math.floor((center.x - radius) / this.cellSize);
|
|
2422
|
+
const maxCellX = Math.floor((center.x + radius) / this.cellSize);
|
|
2423
|
+
const minCellZ = Math.floor((center.z - radius) / this.cellSize);
|
|
2424
|
+
const maxCellZ = Math.floor((center.z + radius) / this.cellSize);
|
|
2425
|
+
for (let x = minCellX; x <= maxCellX; x++) {
|
|
2426
|
+
for (let z = minCellZ; z <= maxCellZ; z++) {
|
|
2427
|
+
const key = `${x},${z}`;
|
|
2428
|
+
const cell = this.cells.get(key);
|
|
2429
|
+
if (cell) {
|
|
2430
|
+
cell.forEach((id) => {
|
|
2431
|
+
const position = this.objectPositions.get(id);
|
|
2432
|
+
if (position) {
|
|
2433
|
+
const dx = position.x - center.x;
|
|
2434
|
+
const dz = position.z - center.z;
|
|
2435
|
+
if (dx * dx + dz * dz <= radiusSquared) {
|
|
2436
|
+
results.push(id);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
return results;
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* Query objects within a bounding box
|
|
2447
|
+
*/
|
|
2448
|
+
queryBox(minX, maxX, minZ, maxZ) {
|
|
2449
|
+
const results = [];
|
|
2450
|
+
const minCellX = Math.floor(minX / this.cellSize);
|
|
2451
|
+
const maxCellX = Math.floor(maxX / this.cellSize);
|
|
2452
|
+
const minCellZ = Math.floor(minZ / this.cellSize);
|
|
2453
|
+
const maxCellZ = Math.floor(maxZ / this.cellSize);
|
|
2454
|
+
for (let x = minCellX; x <= maxCellX; x++) {
|
|
2455
|
+
for (let z = minCellZ; z <= maxCellZ; z++) {
|
|
2456
|
+
const key = `${x},${z}`;
|
|
2457
|
+
const cell = this.cells.get(key);
|
|
2458
|
+
if (cell) {
|
|
2459
|
+
cell.forEach((id) => {
|
|
2460
|
+
const position = this.objectPositions.get(id);
|
|
2461
|
+
if (position && position.x >= minX && position.x <= maxX && position.z >= minZ && position.z <= maxZ) {
|
|
2462
|
+
results.push(id);
|
|
2463
|
+
}
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
return results;
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* Clear all objects from the grid
|
|
2472
|
+
*/
|
|
2473
|
+
clear() {
|
|
2474
|
+
this.cells.clear();
|
|
2475
|
+
this.objectPositions.clear();
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Get statistics about the grid
|
|
2479
|
+
*/
|
|
2480
|
+
getStats() {
|
|
2481
|
+
return {
|
|
2482
|
+
objects: this.objectPositions.size,
|
|
2483
|
+
cells: this.cells.size
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
|
|
2488
|
+
exports.AssetLoader = AssetLoader;
|
|
2489
|
+
exports.Camera3D = Camera3D;
|
|
2490
|
+
exports.Canvas3DErrorBoundary = Canvas3DErrorBoundary;
|
|
2491
|
+
exports.Canvas3DLoadingState = Canvas3DLoadingState;
|
|
2492
|
+
exports.FeatureRenderer = FeatureRenderer;
|
|
2493
|
+
exports.FeatureRenderer3D = FeatureRenderer3D;
|
|
2494
|
+
exports.Lighting3D = Lighting3D;
|
|
2495
|
+
exports.ModelLoader = ModelLoader;
|
|
2496
|
+
exports.PhysicsObject3D = PhysicsObject3D;
|
|
2497
|
+
exports.Scene3D = Scene3D;
|
|
2498
|
+
exports.SpatialHashGrid = SpatialHashGrid;
|
|
2499
|
+
exports.TileRenderer = TileRenderer;
|
|
2500
|
+
exports.UnitRenderer = UnitRenderer;
|
|
2501
|
+
exports.assetLoader = assetLoader;
|
|
2502
|
+
exports.calculateLODLevel = calculateLODLevel;
|
|
2503
|
+
exports.createGridHighlight = createGridHighlight;
|
|
2504
|
+
exports.cullInstancedMesh = cullInstancedMesh;
|
|
2505
|
+
exports.filterByFrustum = filterByFrustum;
|
|
2506
|
+
exports.getCellsInRadius = getCellsInRadius;
|
|
2507
|
+
exports.getNeighbors = getNeighbors;
|
|
2508
|
+
exports.getVisibleIndices = getVisibleIndices;
|
|
2509
|
+
exports.gridDistance = gridDistance;
|
|
2510
|
+
exports.gridManhattanDistance = gridManhattanDistance;
|
|
2511
|
+
exports.gridToWorld = gridToWorld;
|
|
2512
|
+
exports.isInBounds = isInBounds;
|
|
2513
|
+
exports.isInFrustum = isInFrustum;
|
|
2514
|
+
exports.normalizeMouseCoordinates = normalizeMouseCoordinates;
|
|
2515
|
+
exports.preloadFeatures = preloadFeatures;
|
|
2516
|
+
exports.raycastToObjects = raycastToObjects;
|
|
2517
|
+
exports.raycastToPlane = raycastToPlane;
|
|
2518
|
+
exports.updateInstanceLOD = updateInstanceLOD;
|
|
2519
|
+
exports.useAssetLoader = useAssetLoader;
|
|
2520
|
+
exports.useGameCanvas3DEvents = useGameCanvas3DEvents;
|
|
2521
|
+
exports.usePhysics3DController = usePhysics3DController;
|
|
2522
|
+
exports.useRaycaster = useRaycaster;
|
|
2523
|
+
exports.useSceneGraph = useSceneGraph;
|
|
2524
|
+
exports.useThree = useThree3;
|
|
2525
|
+
exports.worldToGrid = worldToGrid;
|