@almadar/ui 5.107.0 → 5.108.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/avl/index.cjs +1427 -2828
- package/dist/avl/index.js +525 -1926
- package/dist/components/{game → core}/atoms/GameIcon.d.ts +1 -1
- package/dist/components/core/atoms/index.d.ts +1 -0
- package/dist/components/core/templates/index.d.ts +0 -1
- package/dist/components/game/molecules/index.d.ts +8 -28
- package/dist/components/index.cjs +223 -1691
- package/dist/components/index.js +213 -1671
- package/dist/lib/drawable/projector.d.ts +1 -1
- package/dist/{components/game/three/molecules/GameCanvas3D.d.ts → lib/drawable/three/Canvas3DHost.d.ts} +12 -9
- package/dist/lib/drawable/{Drawable3D.d.ts → three/Drawable3D.d.ts} +2 -2
- package/dist/lib/drawable/three/GameCamera3D.d.ts +16 -0
- package/dist/lib/drawable/three/game3dTheme.d.ts +14 -0
- package/dist/{components/game → lib/drawable}/three/hooks/useGameCanvas3DEvents.d.ts +1 -1
- package/dist/{components/game → lib/drawable}/three/index.cjs +2019 -2020
- package/dist/{components/game → lib/drawable}/three/index.css +3 -3
- package/dist/lib/drawable/three/index.d.ts +39 -0
- package/dist/{components/game → lib/drawable}/three/index.js +1932 -1934
- package/dist/lib/drawable/{mesh3d.d.ts → three/mesh3d.d.ts} +4 -4
- package/dist/{components/game/lib → lib}/puzzleObject.d.ts +11 -2
- package/dist/{components/game/providers → providers}/GameAudioProvider.d.ts +1 -1
- package/dist/providers/index.cjs +1619 -2690
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +827 -1902
- package/dist/runtime/index.cjs +1288 -2689
- package/dist/runtime/index.js +505 -1906
- package/package.json +7 -7
- package/dist/components/game/atoms/ActionButton.d.ts +0 -31
- package/dist/components/game/atoms/ComboCounter.d.ts +0 -20
- package/dist/components/game/atoms/DamageNumber.d.ts +0 -18
- package/dist/components/game/atoms/GameCard.d.ts +0 -34
- package/dist/components/game/atoms/ItemSlot.d.ts +0 -31
- package/dist/components/game/atoms/ResourceCounter.d.ts +0 -26
- package/dist/components/game/atoms/Sprite.d.ts +0 -52
- package/dist/components/game/atoms/StatusEffect.d.ts +0 -25
- package/dist/components/game/atoms/TurnIndicator.d.ts +0 -22
- package/dist/components/game/atoms/WaypointMarker.d.ts +0 -23
- package/dist/components/game/molecules/EventLog.d.ts +0 -31
- package/dist/components/game/molecules/InventoryGrid.d.ts +0 -35
- package/dist/components/game/molecules/ObjectRulePanel.d.ts +0 -32
- package/dist/components/game/molecules/ResourceBar.d.ts +0 -26
- package/dist/components/game/molecules/RuleEditor.d.ts +0 -39
- package/dist/components/game/molecules/VariablePanel.d.ts +0 -28
- package/dist/components/game/templates/GameTemplate.d.ts +0 -35
- package/dist/components/game/three/atoms/SpriteBillboard3D.d.ts +0 -18
- package/dist/components/game/three/game3dTheme.d.ts +0 -55
- package/dist/components/game/three/index.d.ts +0 -12
- package/dist/components/game/three/lib/game3dAssetManifest.d.ts +0 -48
- package/dist/components/game/three/molecules/GameCamera3D.d.ts +0 -35
- package/dist/components/game/three/molecules/index.d.ts +0 -32
- package/dist/components/game/three/patterns.d.ts +0 -16
- /package/dist/{components/game/hooks → hooks}/useCamera.d.ts +0 -0
- /package/dist/{components/game/hooks → hooks}/useGameAudio.d.ts +0 -0
- /package/dist/{components/game/hooks → hooks}/useImageCache.d.ts +0 -0
- /package/dist/{components/game/hooks → hooks}/useUnitSpriteAtlas.d.ts +0 -0
- /package/dist/{components/game/lib → lib}/boardEntity.d.ts +0 -0
- /package/dist/{components/game/three/molecules → lib/drawable/three}/Camera3D.d.ts +0 -0
- /package/dist/{components/game/three/molecules → lib/drawable/three}/Canvas3DErrorBoundary.d.ts +0 -0
- /package/dist/{components/game/three/molecules → lib/drawable/three}/Canvas3DLoadingState.d.ts +0 -0
- /package/dist/{components/game/three/molecules → lib/drawable/three}/Lighting3D.d.ts +0 -0
- /package/dist/{components/game/three/molecules → lib/drawable/three}/ModelLoader.d.ts +0 -0
- /package/dist/{components/game/three/molecules → lib/drawable/three}/Scene3D.d.ts +0 -0
- /package/dist/{components/game → lib/drawable}/three/hooks/useAssetLoader.d.ts +0 -0
- /package/dist/{components/game → lib/drawable}/three/hooks/useRaycaster.d.ts +0 -0
- /package/dist/{components/game → lib/drawable}/three/hooks/useSceneGraph.d.ts +0 -0
- /package/dist/{components/game → lib/drawable}/three/hooks/useThree.d.ts +0 -0
- /package/dist/{components/game → lib/drawable}/three/lib/AssetLoader.d.ts +0 -0
- /package/dist/{components/game → lib/drawable}/three/lib/culling.d.ts +0 -0
- /package/dist/{components/game → lib/drawable}/three/lib/grid3D.d.ts +0 -0
- /package/dist/{components/game/lib → lib}/editorUtils.d.ts +0 -0
- /package/dist/{components/game/lib → lib}/game.d.ts +0 -0
- /package/dist/{components/game/lib/index.d.ts → lib/gameShared.d.ts} +0 -0
- /package/dist/{components/game/lib → lib}/isometric.d.ts +0 -0
- /package/dist/{components/game/lib → lib}/isometricTypes.d.ts +0 -0
- /package/dist/{components/game/lib → lib}/makeAsset.d.ts +0 -0
- /package/dist/{components/game/lib → lib}/mechanics.d.ts +0 -0
- /package/dist/{components/game/lib → lib}/physicsPresets.d.ts +0 -0
- /package/dist/{components/game/lib → lib}/physicsTypes.d.ts +0 -0
- /package/dist/{components/game/lib → lib}/spriteAnimation.d.ts +0 -0
- /package/dist/{components/game/lib → lib}/spriteAnimationTypes.d.ts +0 -0
- /package/dist/{components/game/lib → lib}/spriteSheetConstants.d.ts +0 -0
|
@@ -1,195 +1,307 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import React6, { forwardRef, useRef, useState, useEffect, useMemo, useImperativeHandle, useCallback, createContext, useContext, Component, useReducer } from 'react';
|
|
2
|
+
import { EventBusContext, useTraitScope } from '@almadar/ui/providers';
|
|
3
|
+
import { createLogger } from '@almadar/logger';
|
|
4
|
+
import { Canvas, useThree, useFrame, useLoader } from '@react-three/fiber';
|
|
5
|
+
import * as THREE9 from 'three';
|
|
4
6
|
import { Vector3, QuadraticBezierCurve3, MathUtils, Quaternion } from 'three';
|
|
7
|
+
import { Grid, OrbitControls, Billboard, Text, Stars, Sparkles, Html, RoundedBox } from '@react-three/drei';
|
|
5
8
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
|
-
import { OrbitControls, Grid, Billboard, Text, Stars, Sparkles, Html, RoundedBox } from '@react-three/drei';
|
|
7
|
-
import { createLogger } from '@almadar/logger';
|
|
8
9
|
import { GLTFLoader as GLTFLoader$1 } from 'three/examples/jsm/loaders/GLTFLoader';
|
|
9
10
|
import { clone } from 'three/examples/jsm/utils/SkeletonUtils';
|
|
11
|
+
import { clsx } from 'clsx';
|
|
12
|
+
import { twMerge } from 'tailwind-merge';
|
|
10
13
|
import { OrbitControls as OrbitControls$1 } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
11
14
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
12
15
|
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
|
|
13
|
-
import { EventBusContext, useTraitScope } from '@almadar/ui/providers';
|
|
14
|
-
import { clsx } from 'clsx';
|
|
15
|
-
import { twMerge } from 'tailwind-merge';
|
|
16
16
|
import { EffectComposer, Bloom, DepthOfField, Vignette } from '@react-three/postprocessing';
|
|
17
17
|
import { useTranslate } from '@almadar/ui/hooks';
|
|
18
18
|
|
|
19
19
|
var __defProp = Object.defineProperty;
|
|
20
20
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
21
21
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
var log = createLogger("almadar:eventbus");
|
|
23
|
+
var subLog = createLogger("almadar:eventbus:subscribe");
|
|
24
|
+
var scopeLog = createLogger("almadar:ui:trait-scope");
|
|
25
|
+
function getGlobalEventBus() {
|
|
26
|
+
if (typeof window !== "undefined") {
|
|
27
|
+
return window.__kflowEventBus ?? null;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
var fallbackListeners = /* @__PURE__ */ new Map();
|
|
32
|
+
var fallbackAnyListeners = /* @__PURE__ */ new Set();
|
|
33
|
+
var fallbackEventBus = {
|
|
34
|
+
emit: (type, payload, source) => {
|
|
35
|
+
const event = {
|
|
36
|
+
type,
|
|
37
|
+
payload,
|
|
38
|
+
timestamp: Date.now(),
|
|
39
|
+
source
|
|
40
|
+
};
|
|
41
|
+
const handlers = fallbackListeners.get(type);
|
|
42
|
+
log.debug("emit", { type, payloadKeys: payload ? Object.keys(payload).length : 0, listenerCount: (handlers?.size ?? 0) + fallbackAnyListeners.size });
|
|
43
|
+
if (handlers) {
|
|
44
|
+
handlers.forEach((handler) => {
|
|
45
|
+
try {
|
|
46
|
+
handler(event);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
log.error("Error in listener", { type, error: error instanceof Error ? error : String(error) });
|
|
49
|
+
}
|
|
34
50
|
});
|
|
35
51
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
scene.fog = null;
|
|
42
|
-
};
|
|
43
|
-
}, [scene, background, fog]);
|
|
44
|
-
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
45
|
-
}
|
|
46
|
-
var Camera3D = forwardRef(
|
|
47
|
-
({
|
|
48
|
-
mode = "isometric",
|
|
49
|
-
position = [10, 10, 10],
|
|
50
|
-
target = [0, 0, 0],
|
|
51
|
-
zoom = 1,
|
|
52
|
-
fov = 45,
|
|
53
|
-
enableOrbit = true,
|
|
54
|
-
minDistance = 2,
|
|
55
|
-
maxDistance = 100,
|
|
56
|
-
onChange
|
|
57
|
-
}, ref) => {
|
|
58
|
-
const { camera, set, viewport } = useThree();
|
|
59
|
-
const controlsRef = useRef(null);
|
|
60
|
-
const initialPosition = useRef(new THREE6.Vector3(...position));
|
|
61
|
-
const initialTarget = useRef(new THREE6.Vector3(...target));
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
let newCamera;
|
|
64
|
-
if (mode === "isometric") {
|
|
65
|
-
const aspect = viewport.aspect;
|
|
66
|
-
const size = 10 / zoom;
|
|
67
|
-
newCamera = new THREE6.OrthographicCamera(
|
|
68
|
-
-size * aspect,
|
|
69
|
-
size * aspect,
|
|
70
|
-
size,
|
|
71
|
-
-size,
|
|
72
|
-
0.1,
|
|
73
|
-
1e3
|
|
74
|
-
);
|
|
75
|
-
} else {
|
|
76
|
-
newCamera = new THREE6.PerspectiveCamera(fov, viewport.aspect, 0.1, 1e3);
|
|
77
|
-
}
|
|
78
|
-
newCamera.position.copy(initialPosition.current);
|
|
79
|
-
newCamera.lookAt(initialTarget.current.x, initialTarget.current.y, initialTarget.current.z);
|
|
80
|
-
set({ camera: newCamera });
|
|
81
|
-
if (mode === "top-down") {
|
|
82
|
-
newCamera.position.set(0, 20 / zoom, 0);
|
|
83
|
-
newCamera.lookAt(0, 0, 0);
|
|
84
|
-
}
|
|
85
|
-
return () => {
|
|
86
|
-
};
|
|
87
|
-
}, [mode, fov, zoom, viewport.aspect, set]);
|
|
88
|
-
useFrame(() => {
|
|
89
|
-
if (onChange) {
|
|
90
|
-
onChange(camera);
|
|
52
|
+
fallbackAnyListeners.forEach((handler) => {
|
|
53
|
+
try {
|
|
54
|
+
handler(event);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
log.error("Error in onAny listener", { type, error: error instanceof Error ? error : String(error) });
|
|
91
57
|
}
|
|
92
58
|
});
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
59
|
+
},
|
|
60
|
+
on: (type, listener) => {
|
|
61
|
+
if (!fallbackListeners.has(type)) {
|
|
62
|
+
fallbackListeners.set(type, /* @__PURE__ */ new Set());
|
|
63
|
+
}
|
|
64
|
+
fallbackListeners.get(type).add(listener);
|
|
65
|
+
subLog.debug("subscribe", { type, totalListeners: fallbackListeners.get(type).size });
|
|
66
|
+
return () => {
|
|
67
|
+
const handlers = fallbackListeners.get(type);
|
|
68
|
+
if (handlers) {
|
|
69
|
+
handlers.delete(listener);
|
|
70
|
+
if (handlers.size === 0) {
|
|
71
|
+
fallbackListeners.delete(type);
|
|
99
72
|
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
once: (type, listener) => {
|
|
77
|
+
const wrappedListener = (event) => {
|
|
78
|
+
fallbackListeners.get(type)?.delete(wrappedListener);
|
|
79
|
+
listener(event);
|
|
80
|
+
};
|
|
81
|
+
return fallbackEventBus.on(type, wrappedListener);
|
|
82
|
+
},
|
|
83
|
+
hasListeners: (type) => {
|
|
84
|
+
const handlers = fallbackListeners.get(type);
|
|
85
|
+
return handlers !== void 0 && handlers.size > 0;
|
|
86
|
+
},
|
|
87
|
+
onAny: (listener) => {
|
|
88
|
+
fallbackAnyListeners.add(listener);
|
|
89
|
+
subLog.debug("subscribe:any", { totalAnyListeners: fallbackAnyListeners.size });
|
|
90
|
+
return () => {
|
|
91
|
+
fallbackAnyListeners.delete(listener);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
function useEventBus() {
|
|
96
|
+
const context = useContext(EventBusContext);
|
|
97
|
+
const baseBus = context ?? getGlobalEventBus() ?? fallbackEventBus;
|
|
98
|
+
const scope = useTraitScope();
|
|
99
|
+
return useMemo(() => {
|
|
100
|
+
if (!scope) {
|
|
101
|
+
return {
|
|
102
|
+
...baseBus,
|
|
103
|
+
emit: (type, payload, source) => {
|
|
104
|
+
if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
|
|
105
|
+
scopeLog.warn("emit:bare-key-no-scope", { type });
|
|
106
|
+
}
|
|
107
|
+
baseBus.emit(type, payload, source);
|
|
106
108
|
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
...baseBus,
|
|
113
|
+
emit: (type, payload, source) => {
|
|
114
|
+
if (typeof type === "string" && type.startsWith("UI:")) {
|
|
115
|
+
const tail = type.slice(3);
|
|
116
|
+
const qualified = tail.includes(".") ? type : `UI:${scope.orbital}.${scope.trait}.${tail}`;
|
|
117
|
+
if (qualified !== type) {
|
|
118
|
+
scopeLog.info("emit:qualified", {
|
|
119
|
+
from: type,
|
|
120
|
+
to: qualified,
|
|
121
|
+
scopeOrbital: scope.orbital,
|
|
122
|
+
scopeTrait: scope.trait
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
baseBus.emit(qualified, payload, source);
|
|
126
|
+
return;
|
|
114
127
|
}
|
|
115
|
-
|
|
116
|
-
getViewBounds: () => {
|
|
117
|
-
const min = new THREE6.Vector3(-10, -10, -10);
|
|
118
|
-
const max = new THREE6.Vector3(10, 10, 10);
|
|
119
|
-
return { min, max };
|
|
120
|
-
}
|
|
121
|
-
}));
|
|
122
|
-
const maxPolarAngle = mode === "top-down" ? 0.1 : Math.PI / 2 - 0.1;
|
|
123
|
-
return /* @__PURE__ */ jsx(
|
|
124
|
-
OrbitControls,
|
|
125
|
-
{
|
|
126
|
-
ref: controlsRef,
|
|
127
|
-
camera,
|
|
128
|
-
enabled: enableOrbit,
|
|
129
|
-
target: [initialTarget.current.x, initialTarget.current.y, initialTarget.current.z],
|
|
130
|
-
minDistance,
|
|
131
|
-
maxDistance,
|
|
132
|
-
maxPolarAngle,
|
|
133
|
-
enableDamping: true,
|
|
134
|
-
dampingFactor: 0.05
|
|
128
|
+
baseBus.emit(type, payload, source);
|
|
135
129
|
}
|
|
136
|
-
|
|
130
|
+
};
|
|
131
|
+
}, [baseBus, scope]);
|
|
132
|
+
}
|
|
133
|
+
function useEmitEvent() {
|
|
134
|
+
const eventBus = useEventBus();
|
|
135
|
+
return useCallback(
|
|
136
|
+
(type, payload, source) => {
|
|
137
|
+
eventBus.emit(type, payload, source);
|
|
138
|
+
},
|
|
139
|
+
[eventBus]
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// lib/drawable/hitTest.ts
|
|
144
|
+
function collectDrawnItems(nodes) {
|
|
145
|
+
const out = [];
|
|
146
|
+
for (const n of nodes) {
|
|
147
|
+
switch (n.type) {
|
|
148
|
+
case "draw-sprite":
|
|
149
|
+
case "draw-shape":
|
|
150
|
+
case "draw-text":
|
|
151
|
+
out.push({ pos: n.position, id: n.id });
|
|
152
|
+
break;
|
|
153
|
+
case "draw-sprite-layer":
|
|
154
|
+
case "draw-shape-layer":
|
|
155
|
+
case "draw-text-layer":
|
|
156
|
+
for (const it of n.items) out.push({ pos: it.position, id: it.id });
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
137
159
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
function buildHitIndex(items) {
|
|
163
|
+
const m = /* @__PURE__ */ new Map();
|
|
164
|
+
for (const it of items) {
|
|
165
|
+
if (it.id !== void 0) m.set(`${it.pos.x},${it.pos.y}`, it.id);
|
|
166
|
+
}
|
|
167
|
+
return m;
|
|
168
|
+
}
|
|
169
|
+
function useGameCanvas3DEvents(options) {
|
|
170
|
+
const {
|
|
171
|
+
tileClickEvent,
|
|
172
|
+
unitClickEvent,
|
|
173
|
+
featureClickEvent,
|
|
174
|
+
canvasClickEvent,
|
|
175
|
+
tileHoverEvent,
|
|
176
|
+
tileLeaveEvent,
|
|
177
|
+
unitAnimationEvent,
|
|
178
|
+
cameraChangeEvent,
|
|
179
|
+
onTileClick,
|
|
180
|
+
onUnitClick,
|
|
181
|
+
onFeatureClick,
|
|
182
|
+
onCanvasClick,
|
|
183
|
+
onTileHover,
|
|
184
|
+
onUnitAnimation
|
|
185
|
+
} = options;
|
|
186
|
+
const emit = useEmitEvent();
|
|
187
|
+
const optionsRef = useRef(options);
|
|
188
|
+
optionsRef.current = options;
|
|
189
|
+
const handleTileClick = useCallback(
|
|
190
|
+
(tile, event) => {
|
|
191
|
+
if (tileClickEvent) {
|
|
192
|
+
emit(tileClickEvent, {
|
|
193
|
+
tileId: tile.id,
|
|
194
|
+
x: tile.x,
|
|
195
|
+
z: tile.z ?? tile.y ?? 0,
|
|
196
|
+
type: tile.type,
|
|
197
|
+
terrain: tile.terrain,
|
|
198
|
+
elevation: tile.elevation
|
|
199
|
+
});
|
|
173
200
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
201
|
+
optionsRef.current.onTileClick?.(tile, event);
|
|
202
|
+
},
|
|
203
|
+
[tileClickEvent, emit]
|
|
204
|
+
);
|
|
205
|
+
const handleUnitClick = useCallback(
|
|
206
|
+
(unit, event) => {
|
|
207
|
+
if (unitClickEvent) {
|
|
208
|
+
emit(unitClickEvent, {
|
|
209
|
+
unitId: unit.id,
|
|
210
|
+
x: unit.x,
|
|
211
|
+
z: unit.z ?? unit.y ?? 0,
|
|
212
|
+
unitType: unit.unitType,
|
|
213
|
+
name: unit.name,
|
|
214
|
+
team: unit.team,
|
|
215
|
+
faction: unit.faction,
|
|
216
|
+
health: unit.health,
|
|
217
|
+
maxHealth: unit.maxHealth
|
|
218
|
+
});
|
|
181
219
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
220
|
+
optionsRef.current.onUnitClick?.(unit, event);
|
|
221
|
+
},
|
|
222
|
+
[unitClickEvent, emit]
|
|
223
|
+
);
|
|
224
|
+
const handleFeatureClick = useCallback(
|
|
225
|
+
(feature, event) => {
|
|
226
|
+
if (featureClickEvent) {
|
|
227
|
+
emit(featureClickEvent, {
|
|
228
|
+
featureId: feature.id,
|
|
229
|
+
x: feature.x,
|
|
230
|
+
z: feature.z ?? feature.y ?? 0,
|
|
231
|
+
type: feature.type,
|
|
232
|
+
elevation: feature.elevation
|
|
233
|
+
});
|
|
190
234
|
}
|
|
191
|
-
|
|
192
|
-
|
|
235
|
+
optionsRef.current.onFeatureClick?.(feature, event);
|
|
236
|
+
},
|
|
237
|
+
[featureClickEvent, emit]
|
|
238
|
+
);
|
|
239
|
+
const handleCanvasClick = useCallback(
|
|
240
|
+
(event) => {
|
|
241
|
+
if (canvasClickEvent) {
|
|
242
|
+
emit(canvasClickEvent, {
|
|
243
|
+
clientX: event.clientX,
|
|
244
|
+
clientY: event.clientY,
|
|
245
|
+
button: event.button
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
optionsRef.current.onCanvasClick?.(event);
|
|
249
|
+
},
|
|
250
|
+
[canvasClickEvent, emit]
|
|
251
|
+
);
|
|
252
|
+
const handleTileHover = useCallback(
|
|
253
|
+
(tile, event) => {
|
|
254
|
+
if (tile) {
|
|
255
|
+
if (tileHoverEvent) {
|
|
256
|
+
emit(tileHoverEvent, {
|
|
257
|
+
tileId: tile.id,
|
|
258
|
+
x: tile.x,
|
|
259
|
+
z: tile.z ?? tile.y ?? 0,
|
|
260
|
+
type: tile.type
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
if (tileLeaveEvent) {
|
|
265
|
+
emit(tileLeaveEvent, {});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
optionsRef.current.onTileHover?.(tile, event);
|
|
269
|
+
},
|
|
270
|
+
[tileHoverEvent, tileLeaveEvent, emit]
|
|
271
|
+
);
|
|
272
|
+
const handleUnitAnimation = useCallback(
|
|
273
|
+
(unitId, state) => {
|
|
274
|
+
if (unitAnimationEvent) {
|
|
275
|
+
emit(unitAnimationEvent, {
|
|
276
|
+
unitId,
|
|
277
|
+
state,
|
|
278
|
+
timestamp: Date.now()
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
optionsRef.current.onUnitAnimation?.(unitId, state);
|
|
282
|
+
},
|
|
283
|
+
[unitAnimationEvent, emit]
|
|
284
|
+
);
|
|
285
|
+
const handleCameraChange = useCallback(
|
|
286
|
+
(position) => {
|
|
287
|
+
if (cameraChangeEvent) {
|
|
288
|
+
emit(cameraChangeEvent, {
|
|
289
|
+
position,
|
|
290
|
+
timestamp: Date.now()
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
[cameraChangeEvent, emit]
|
|
295
|
+
);
|
|
296
|
+
return {
|
|
297
|
+
handleTileClick,
|
|
298
|
+
handleUnitClick,
|
|
299
|
+
handleFeatureClick,
|
|
300
|
+
handleCanvasClick,
|
|
301
|
+
handleTileHover,
|
|
302
|
+
handleUnitAnimation,
|
|
303
|
+
handleCameraChange
|
|
304
|
+
};
|
|
193
305
|
}
|
|
194
306
|
function Canvas3DLoadingState({
|
|
195
307
|
progress = 0,
|
|
@@ -236,7 +348,7 @@ function Canvas3DLoadingState({
|
|
|
236
348
|
/* @__PURE__ */ jsx("div", { className: "canvas-3d-loading__background", children: /* @__PURE__ */ jsx("div", { className: "bg__grid" }) })
|
|
237
349
|
] });
|
|
238
350
|
}
|
|
239
|
-
var
|
|
351
|
+
var log2 = createLogger("almadar:ui:game:canvas3d:error-boundary");
|
|
240
352
|
var Canvas3DErrorBoundary = class extends Component {
|
|
241
353
|
constructor(props) {
|
|
242
354
|
super(props);
|
|
@@ -264,8 +376,8 @@ var Canvas3DErrorBoundary = class extends Component {
|
|
|
264
376
|
componentDidCatch(error, errorInfo) {
|
|
265
377
|
this.setState({ errorInfo });
|
|
266
378
|
this.props.onError?.(error, errorInfo);
|
|
267
|
-
|
|
268
|
-
|
|
379
|
+
log2.error("Error caught", { error });
|
|
380
|
+
log2.error("Component stack", { componentStack: errorInfo.componentStack ?? "<none>" });
|
|
269
381
|
}
|
|
270
382
|
render() {
|
|
271
383
|
if (this.state.hasError) {
|
|
@@ -308,7 +420,92 @@ var Canvas3DErrorBoundary = class extends Component {
|
|
|
308
420
|
return this.props.children;
|
|
309
421
|
}
|
|
310
422
|
};
|
|
311
|
-
|
|
423
|
+
function Lighting3D({
|
|
424
|
+
ambientIntensity = 0.6,
|
|
425
|
+
ambientColor = "#ffffff",
|
|
426
|
+
directionalIntensity = 0.8,
|
|
427
|
+
directionalColor = "#ffffff",
|
|
428
|
+
directionalPosition = [10, 20, 10],
|
|
429
|
+
shadows = true,
|
|
430
|
+
shadowMapSize = 2048,
|
|
431
|
+
shadowCameraSize = 20,
|
|
432
|
+
shadowBias = -1e-3,
|
|
433
|
+
shadowNormalBias,
|
|
434
|
+
shadowCameraNear = 0.1,
|
|
435
|
+
shadowCameraFar = 100,
|
|
436
|
+
showHelpers = false
|
|
437
|
+
}) {
|
|
438
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
439
|
+
/* @__PURE__ */ jsx("ambientLight", { intensity: ambientIntensity, color: ambientColor }),
|
|
440
|
+
/* @__PURE__ */ jsx(
|
|
441
|
+
"directionalLight",
|
|
442
|
+
{
|
|
443
|
+
position: directionalPosition,
|
|
444
|
+
intensity: directionalIntensity,
|
|
445
|
+
color: directionalColor,
|
|
446
|
+
castShadow: shadows,
|
|
447
|
+
"shadow-mapSize": [shadowMapSize, shadowMapSize],
|
|
448
|
+
"shadow-camera-left": -shadowCameraSize,
|
|
449
|
+
"shadow-camera-right": shadowCameraSize,
|
|
450
|
+
"shadow-camera-top": shadowCameraSize,
|
|
451
|
+
"shadow-camera-bottom": -shadowCameraSize,
|
|
452
|
+
"shadow-camera-near": shadowCameraNear,
|
|
453
|
+
"shadow-camera-far": shadowCameraFar,
|
|
454
|
+
"shadow-bias": shadowBias,
|
|
455
|
+
"shadow-normalBias": shadowNormalBias
|
|
456
|
+
}
|
|
457
|
+
),
|
|
458
|
+
/* @__PURE__ */ jsx(
|
|
459
|
+
"hemisphereLight",
|
|
460
|
+
{
|
|
461
|
+
intensity: 0.3,
|
|
462
|
+
color: "#87ceeb",
|
|
463
|
+
groundColor: "#362d1d"
|
|
464
|
+
}
|
|
465
|
+
),
|
|
466
|
+
showHelpers && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(
|
|
467
|
+
"directionalLightHelper",
|
|
468
|
+
{
|
|
469
|
+
args: [
|
|
470
|
+
new THREE9.DirectionalLight(directionalColor, directionalIntensity),
|
|
471
|
+
5
|
|
472
|
+
]
|
|
473
|
+
}
|
|
474
|
+
) })
|
|
475
|
+
] });
|
|
476
|
+
}
|
|
477
|
+
function CameraController3D({
|
|
478
|
+
onCameraChange
|
|
479
|
+
}) {
|
|
480
|
+
const { camera } = useThree();
|
|
481
|
+
useEffect(() => {
|
|
482
|
+
if (onCameraChange) {
|
|
483
|
+
onCameraChange({
|
|
484
|
+
x: camera.position.x,
|
|
485
|
+
y: camera.position.y,
|
|
486
|
+
z: camera.position.z
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}, [camera.position, onCameraChange]);
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
function FollowCamera3D({
|
|
493
|
+
target,
|
|
494
|
+
offset
|
|
495
|
+
}) {
|
|
496
|
+
const { camera } = useThree();
|
|
497
|
+
const look = useRef(new THREE9.Vector3(target[0], target[1], target[2]));
|
|
498
|
+
const goal = useRef(new THREE9.Vector3());
|
|
499
|
+
useFrame((_, delta) => {
|
|
500
|
+
const t = Math.min(1, delta * 5);
|
|
501
|
+
goal.current.set(target[0] + offset[0], target[1] + offset[1], target[2] + offset[2]);
|
|
502
|
+
camera.position.lerp(goal.current, t);
|
|
503
|
+
look.current.lerp(goal.current.set(target[0], target[1], target[2]), t);
|
|
504
|
+
camera.lookAt(look.current);
|
|
505
|
+
});
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
var log3 = createLogger("almadar:ui:game:model-loader");
|
|
312
509
|
var gltfCache = /* @__PURE__ */ new Map();
|
|
313
510
|
function loadGltfCached(url, assetRoot) {
|
|
314
511
|
const key = `${assetRoot}|${url}`;
|
|
@@ -321,13 +518,13 @@ function loadGltfCached(url, assetRoot) {
|
|
|
321
518
|
loader.load(
|
|
322
519
|
url,
|
|
323
520
|
(gltf) => {
|
|
324
|
-
|
|
521
|
+
log3.debug("Loaded", { url, clips: gltf.animations.length });
|
|
325
522
|
pending.gltf = { scene: gltf.scene, animations: gltf.animations };
|
|
326
523
|
resolveDone();
|
|
327
524
|
},
|
|
328
525
|
void 0,
|
|
329
526
|
(err) => {
|
|
330
|
-
|
|
527
|
+
log3.warn("Failed", { url, error: err instanceof Error ? err : String(err) });
|
|
331
528
|
pending.error = err instanceof Error ? err : new Error(String(err));
|
|
332
529
|
resolveDone();
|
|
333
530
|
}
|
|
@@ -394,12 +591,12 @@ function ModelLoader({
|
|
|
394
591
|
if (!loadedModel) return null;
|
|
395
592
|
const cloned = clone(loadedModel);
|
|
396
593
|
cloned.updateMatrixWorld(true);
|
|
397
|
-
const tintColor = tint ? new
|
|
594
|
+
const tintColor = tint ? new THREE9.Color(tint) : null;
|
|
398
595
|
cloned.traverse((child) => {
|
|
399
|
-
if (child instanceof
|
|
596
|
+
if (child instanceof THREE9.Mesh) {
|
|
400
597
|
child.castShadow = castShadow;
|
|
401
598
|
child.receiveShadow = receiveShadow;
|
|
402
|
-
if (tintColor && child.material instanceof
|
|
599
|
+
if (tintColor && child.material instanceof THREE9.MeshStandardMaterial) {
|
|
403
600
|
const mat = child.material.clone();
|
|
404
601
|
mat.color.multiply(tintColor);
|
|
405
602
|
child.material = mat;
|
|
@@ -408,7 +605,7 @@ function ModelLoader({
|
|
|
408
605
|
});
|
|
409
606
|
return cloned;
|
|
410
607
|
}, [loadedModel, castShadow, receiveShadow, tint]);
|
|
411
|
-
const mixer = useMemo(() => model ? new
|
|
608
|
+
const mixer = useMemo(() => model ? new THREE9.AnimationMixer(model) : null, [model]);
|
|
412
609
|
useEffect(() => {
|
|
413
610
|
if (!mixer || !animation || clips.length === 0) return;
|
|
414
611
|
const wanted = animation.toLowerCase();
|
|
@@ -425,8 +622,8 @@ function ModelLoader({
|
|
|
425
622
|
});
|
|
426
623
|
const normFactor = useMemo(() => {
|
|
427
624
|
if (!model) return 1;
|
|
428
|
-
const box = new
|
|
429
|
-
const size = new
|
|
625
|
+
const box = new THREE9.Box3().setFromObject(model);
|
|
626
|
+
const size = new THREE9.Vector3();
|
|
430
627
|
box.getSize(size);
|
|
431
628
|
const maxDim = Math.max(size.x, size.y, size.z);
|
|
432
629
|
if (!Number.isFinite(maxDim) || maxDim < 0.05) return 1;
|
|
@@ -509,1576 +706,1377 @@ function ModelLoader({
|
|
|
509
706
|
}
|
|
510
707
|
);
|
|
511
708
|
}
|
|
512
|
-
function
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
return loader;
|
|
523
|
-
}
|
|
524
|
-
var AssetLoader = class {
|
|
525
|
-
constructor() {
|
|
526
|
-
__publicField(this, "objLoader");
|
|
527
|
-
__publicField(this, "textureLoader");
|
|
528
|
-
__publicField(this, "modelCache");
|
|
529
|
-
__publicField(this, "textureCache");
|
|
530
|
-
__publicField(this, "loadingPromises");
|
|
531
|
-
this.objLoader = new OBJLoader();
|
|
532
|
-
this.textureLoader = new THREE6.TextureLoader();
|
|
533
|
-
this.modelCache = /* @__PURE__ */ new Map();
|
|
534
|
-
this.textureCache = /* @__PURE__ */ new Map();
|
|
535
|
-
this.loadingPromises = /* @__PURE__ */ new Map();
|
|
536
|
-
}
|
|
537
|
-
/**
|
|
538
|
-
* Load a GLB/GLTF model
|
|
539
|
-
* @param url - URL to the .glb or .gltf file
|
|
540
|
-
* @returns Promise with loaded model scene and animations
|
|
541
|
-
*/
|
|
542
|
-
async loadModel(url) {
|
|
543
|
-
if (this.modelCache.has(url)) {
|
|
544
|
-
return this.modelCache.get(url);
|
|
545
|
-
}
|
|
546
|
-
if (this.loadingPromises.has(url)) {
|
|
547
|
-
return this.loadingPromises.get(url);
|
|
548
|
-
}
|
|
549
|
-
const loader = createGLTFLoaderForUrl(url);
|
|
550
|
-
const loadPromise = loader.loadAsync(url).then((gltf) => {
|
|
551
|
-
const result = {
|
|
552
|
-
scene: gltf.scene,
|
|
553
|
-
animations: gltf.animations || []
|
|
554
|
-
};
|
|
555
|
-
this.modelCache.set(url, result);
|
|
556
|
-
this.loadingPromises.delete(url);
|
|
557
|
-
return result;
|
|
558
|
-
}).catch((error) => {
|
|
559
|
-
this.loadingPromises.delete(url);
|
|
560
|
-
throw new Error(`Failed to load model ${url}: ${error.message}`);
|
|
561
|
-
});
|
|
562
|
-
this.loadingPromises.set(url, loadPromise);
|
|
563
|
-
return loadPromise;
|
|
709
|
+
function SpriteBillboard({ node, world }) {
|
|
710
|
+
const texture = useLoader(THREE9.TextureLoader, node.asset.url);
|
|
711
|
+
const img = texture.image;
|
|
712
|
+
const imgW = img?.width || 1;
|
|
713
|
+
const imgH = img?.height || 1;
|
|
714
|
+
if (node.frame) {
|
|
715
|
+
texture.repeat.set(node.frame.w / imgW, node.frame.h / imgH);
|
|
716
|
+
texture.offset.set(node.frame.x / imgW, 1 - (node.frame.y + node.frame.h) / imgH);
|
|
717
|
+
texture.magFilter = texture.minFilter = THREE9.NearestFilter;
|
|
718
|
+
texture.needsUpdate = true;
|
|
564
719
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
720
|
+
const aspect = node.frame ? node.frame.w / node.frame.h : imgW / imgH;
|
|
721
|
+
const height = node.height ?? 1;
|
|
722
|
+
const width = node.width ?? height * aspect;
|
|
723
|
+
return /* @__PURE__ */ jsx(Billboard, { position: [world[0], world[1] + height / 2, world[2]], children: /* @__PURE__ */ jsxs("mesh", { children: [
|
|
724
|
+
/* @__PURE__ */ jsx("planeGeometry", { args: [width, height] }),
|
|
725
|
+
/* @__PURE__ */ jsx(
|
|
726
|
+
"meshBasicMaterial",
|
|
727
|
+
{
|
|
728
|
+
map: texture,
|
|
729
|
+
transparent: true,
|
|
730
|
+
alphaTest: 0.1,
|
|
731
|
+
side: THREE9.DoubleSide,
|
|
732
|
+
opacity: node.opacity ?? 1
|
|
733
|
+
}
|
|
734
|
+
)
|
|
735
|
+
] }) });
|
|
736
|
+
}
|
|
737
|
+
function Sprite3D({ node, projector }) {
|
|
738
|
+
if (node.asset.dimension === "3d" && node.asset.url) {
|
|
739
|
+
return /* @__PURE__ */ jsx("group", { position: projector.toWorld(node.position), children: /* @__PURE__ */ jsx(
|
|
740
|
+
ModelLoader,
|
|
741
|
+
{
|
|
742
|
+
url: node.asset.url,
|
|
743
|
+
scale: node.width ?? projector.cellSize,
|
|
744
|
+
rotation: [0, node.rotation ?? 0, 0],
|
|
745
|
+
fallbackGeometry: "box",
|
|
746
|
+
castShadow: true,
|
|
747
|
+
receiveShadow: true
|
|
748
|
+
}
|
|
749
|
+
) });
|
|
592
750
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
751
|
+
if (!node.asset.url) return null;
|
|
752
|
+
return /* @__PURE__ */ jsx(SpriteBillboard, { node, world: projector.toWorld(node.position) });
|
|
753
|
+
}
|
|
754
|
+
function Shape3D({ node, projector }) {
|
|
755
|
+
const world = projector.toWorld(node.position);
|
|
756
|
+
const color = node.fill ?? node.stroke ?? "#ffffff";
|
|
757
|
+
let geometry;
|
|
758
|
+
switch (node.shape) {
|
|
759
|
+
case "cell":
|
|
760
|
+
case "rect": {
|
|
761
|
+
const w = node.shape === "cell" ? projector.cellSize * 0.95 : node.width ?? projector.cellSize;
|
|
762
|
+
const h = node.shape === "cell" ? projector.cellSize * 0.95 : node.height ?? projector.cellSize;
|
|
763
|
+
geometry = /* @__PURE__ */ jsx("planeGeometry", { args: [w, h] });
|
|
764
|
+
break;
|
|
601
765
|
}
|
|
602
|
-
|
|
603
|
-
|
|
766
|
+
case "ellipse": {
|
|
767
|
+
const radiusX = node.radiusX ?? 0.4;
|
|
768
|
+
geometry = node.stroke && !node.fill ? /* @__PURE__ */ jsx("ringGeometry", { args: [Math.max(0, radiusX - 0.08), radiusX, 32] }) : /* @__PURE__ */ jsx("circleGeometry", { args: [radiusX, 32] });
|
|
769
|
+
break;
|
|
604
770
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
return loadPromise;
|
|
616
|
-
}
|
|
617
|
-
/**
|
|
618
|
-
* Preload multiple assets
|
|
619
|
-
* @param urls - Array of asset URLs to preload
|
|
620
|
-
* @returns Promise that resolves when all assets are loaded
|
|
621
|
-
*/
|
|
622
|
-
async preload(urls) {
|
|
623
|
-
const promises = urls.map((url) => {
|
|
624
|
-
if (url.endsWith(".glb") || url.endsWith(".gltf")) {
|
|
625
|
-
return this.loadModel(url).catch(() => null);
|
|
626
|
-
} else if (url.endsWith(".obj")) {
|
|
627
|
-
return this.loadOBJ(url).catch(() => null);
|
|
628
|
-
} else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
|
|
629
|
-
return this.loadTexture(url).catch(() => null);
|
|
630
|
-
}
|
|
631
|
-
return Promise.resolve(null);
|
|
632
|
-
});
|
|
633
|
-
await Promise.all(promises);
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Check if a model is cached
|
|
637
|
-
* @param url - Model URL
|
|
638
|
-
*/
|
|
639
|
-
hasModel(url) {
|
|
640
|
-
return this.modelCache.has(url);
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* Check if a texture is cached
|
|
644
|
-
* @param url - Texture URL
|
|
645
|
-
*/
|
|
646
|
-
hasTexture(url) {
|
|
647
|
-
return this.textureCache.has(url);
|
|
648
|
-
}
|
|
649
|
-
/**
|
|
650
|
-
* Get cached model (throws if not cached)
|
|
651
|
-
* @param url - Model URL
|
|
652
|
-
*/
|
|
653
|
-
getModel(url) {
|
|
654
|
-
const model = this.modelCache.get(url);
|
|
655
|
-
if (!model) {
|
|
656
|
-
throw new Error(`Model ${url} not in cache`);
|
|
771
|
+
case "poly": {
|
|
772
|
+
if (!node.points || node.points.length === 0) return null;
|
|
773
|
+
const s = new THREE9.Shape();
|
|
774
|
+
node.points.forEach((p, i) => {
|
|
775
|
+
if (i === 0) s.moveTo(p.x, p.y);
|
|
776
|
+
else s.lineTo(p.x, p.y);
|
|
777
|
+
});
|
|
778
|
+
s.closePath();
|
|
779
|
+
geometry = /* @__PURE__ */ jsx("shapeGeometry", { args: [s] });
|
|
780
|
+
break;
|
|
657
781
|
}
|
|
658
|
-
|
|
782
|
+
default:
|
|
783
|
+
return null;
|
|
659
784
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
785
|
+
return /* @__PURE__ */ jsxs("mesh", { rotation: [-Math.PI / 2, 0, 0], position: [world[0], world[1] + 0.02, world[2]], children: [
|
|
786
|
+
geometry,
|
|
787
|
+
/* @__PURE__ */ jsx("meshBasicMaterial", { color, transparent: true, opacity: node.opacity ?? 1, side: THREE9.DoubleSide })
|
|
788
|
+
] });
|
|
789
|
+
}
|
|
790
|
+
function Text3D({ node, projector }) {
|
|
791
|
+
const world = projector.toWorld(node.position);
|
|
792
|
+
return /* @__PURE__ */ jsx(Billboard, { position: [world[0], world[1] + 1.2, world[2]], children: /* @__PURE__ */ jsx(
|
|
793
|
+
Text,
|
|
794
|
+
{
|
|
795
|
+
fontSize: 0.32,
|
|
796
|
+
color: node.color,
|
|
797
|
+
anchorX: "center",
|
|
798
|
+
anchorY: "middle",
|
|
799
|
+
outlineWidth: 0.02,
|
|
800
|
+
outlineColor: "#000000",
|
|
801
|
+
children: node.text
|
|
668
802
|
}
|
|
669
|
-
|
|
803
|
+
) });
|
|
804
|
+
}
|
|
805
|
+
function Drawable3D({ node, projector }) {
|
|
806
|
+
switch (node.type) {
|
|
807
|
+
case "draw-sprite":
|
|
808
|
+
return /* @__PURE__ */ jsx(Sprite3D, { node, projector });
|
|
809
|
+
case "draw-shape":
|
|
810
|
+
return /* @__PURE__ */ jsx(Shape3D, { node, projector });
|
|
811
|
+
case "draw-text":
|
|
812
|
+
return /* @__PURE__ */ jsx(Text3D, { node, projector });
|
|
813
|
+
case "draw-sprite-layer":
|
|
814
|
+
return /* @__PURE__ */ jsx(Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsx(Sprite3D, { node: item, projector }, i)) });
|
|
815
|
+
case "draw-shape-layer":
|
|
816
|
+
return /* @__PURE__ */ jsx(Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsx(Shape3D, { node: item, projector }, i)) });
|
|
817
|
+
case "draw-text-layer":
|
|
818
|
+
return /* @__PURE__ */ jsx(Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsx(Text3D, { node: item, projector }, i)) });
|
|
670
819
|
}
|
|
671
|
-
|
|
672
|
-
* Clear all caches
|
|
673
|
-
*/
|
|
674
|
-
clearCache() {
|
|
675
|
-
this.textureCache.forEach((texture) => {
|
|
676
|
-
texture.dispose();
|
|
677
|
-
});
|
|
678
|
-
this.modelCache.forEach((model) => {
|
|
679
|
-
model.scene.traverse((child) => {
|
|
680
|
-
if (child instanceof THREE6.Mesh) {
|
|
681
|
-
child.geometry.dispose();
|
|
682
|
-
if (Array.isArray(child.material)) {
|
|
683
|
-
child.material.forEach((m) => m.dispose());
|
|
684
|
-
} else {
|
|
685
|
-
child.material.dispose();
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
});
|
|
689
|
-
});
|
|
690
|
-
this.modelCache.clear();
|
|
691
|
-
this.textureCache.clear();
|
|
692
|
-
this.loadingPromises.clear();
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* Get cache statistics
|
|
696
|
-
*/
|
|
697
|
-
getStats() {
|
|
698
|
-
return {
|
|
699
|
-
models: this.modelCache.size,
|
|
700
|
-
textures: this.textureCache.size,
|
|
701
|
-
loading: this.loadingPromises.size
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
};
|
|
705
|
-
var assetLoader = new AssetLoader();
|
|
820
|
+
}
|
|
706
821
|
|
|
707
|
-
//
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
shadows: true,
|
|
713
|
-
showGrid: true,
|
|
714
|
-
gridSize: 20,
|
|
715
|
-
assetLoader: new AssetLoader()
|
|
716
|
-
};
|
|
717
|
-
function useThree3(options = {}) {
|
|
718
|
-
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
719
|
-
const containerRef = useRef(null);
|
|
720
|
-
const canvasRef = useRef(null);
|
|
721
|
-
const rendererRef = useRef(null);
|
|
722
|
-
const sceneRef = useRef(null);
|
|
723
|
-
const cameraRef = useRef(null);
|
|
724
|
-
const controlsRef = useRef(null);
|
|
725
|
-
const gridHelperRef = useRef(null);
|
|
726
|
-
const rafRef = useRef(0);
|
|
727
|
-
const [isReady, setIsReady] = useState(false);
|
|
728
|
-
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
|
729
|
-
const initialCameraPosition = useMemo(
|
|
730
|
-
() => new THREE6.Vector3(...opts.cameraPosition),
|
|
731
|
-
[]
|
|
732
|
-
);
|
|
733
|
-
useEffect(() => {
|
|
734
|
-
if (!containerRef.current) return;
|
|
735
|
-
const container = containerRef.current;
|
|
736
|
-
const { clientWidth, clientHeight } = container;
|
|
737
|
-
const scene = new THREE6.Scene();
|
|
738
|
-
scene.background = new THREE6.Color(opts.backgroundColor);
|
|
739
|
-
sceneRef.current = scene;
|
|
740
|
-
let camera;
|
|
741
|
-
const aspect = clientWidth / clientHeight;
|
|
742
|
-
if (opts.cameraMode === "isometric") {
|
|
743
|
-
const size = 10;
|
|
744
|
-
camera = new THREE6.OrthographicCamera(
|
|
745
|
-
-size * aspect,
|
|
746
|
-
size * aspect,
|
|
747
|
-
size,
|
|
748
|
-
-size,
|
|
749
|
-
0.1,
|
|
750
|
-
1e3
|
|
751
|
-
);
|
|
752
|
-
} else {
|
|
753
|
-
camera = new THREE6.PerspectiveCamera(45, aspect, 0.1, 1e3);
|
|
754
|
-
}
|
|
755
|
-
camera.position.copy(initialCameraPosition);
|
|
756
|
-
cameraRef.current = camera;
|
|
757
|
-
const renderer = new THREE6.WebGLRenderer({
|
|
758
|
-
antialias: true,
|
|
759
|
-
alpha: true,
|
|
760
|
-
canvas: canvasRef.current || void 0
|
|
761
|
-
});
|
|
762
|
-
renderer.setSize(clientWidth, clientHeight);
|
|
763
|
-
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
764
|
-
renderer.shadowMap.enabled = opts.shadows;
|
|
765
|
-
renderer.shadowMap.type = THREE6.PCFSoftShadowMap;
|
|
766
|
-
rendererRef.current = renderer;
|
|
767
|
-
const controls = new OrbitControls$1(camera, renderer.domElement);
|
|
768
|
-
controls.enableDamping = true;
|
|
769
|
-
controls.dampingFactor = 0.05;
|
|
770
|
-
controls.minDistance = 2;
|
|
771
|
-
controls.maxDistance = 100;
|
|
772
|
-
controls.maxPolarAngle = Math.PI / 2 - 0.1;
|
|
773
|
-
controlsRef.current = controls;
|
|
774
|
-
const ambientLight = new THREE6.AmbientLight(16777215, 0.6);
|
|
775
|
-
scene.add(ambientLight);
|
|
776
|
-
const directionalLight = new THREE6.DirectionalLight(16777215, 0.8);
|
|
777
|
-
directionalLight.position.set(10, 20, 10);
|
|
778
|
-
directionalLight.castShadow = opts.shadows;
|
|
779
|
-
directionalLight.shadow.mapSize.width = 2048;
|
|
780
|
-
directionalLight.shadow.mapSize.height = 2048;
|
|
781
|
-
scene.add(directionalLight);
|
|
782
|
-
if (opts.showGrid) {
|
|
783
|
-
const gridHelper = new THREE6.GridHelper(
|
|
784
|
-
opts.gridSize,
|
|
785
|
-
opts.gridSize,
|
|
786
|
-
4473924,
|
|
787
|
-
2236962
|
|
788
|
-
);
|
|
789
|
-
scene.add(gridHelper);
|
|
790
|
-
gridHelperRef.current = gridHelper;
|
|
791
|
-
}
|
|
792
|
-
const animate = () => {
|
|
793
|
-
rafRef.current = requestAnimationFrame(animate);
|
|
794
|
-
controls.update();
|
|
795
|
-
renderer.render(scene, camera);
|
|
796
|
-
};
|
|
797
|
-
animate();
|
|
798
|
-
const handleResize = () => {
|
|
799
|
-
const { clientWidth: width, clientHeight: height } = container;
|
|
800
|
-
setDimensions({ width, height });
|
|
801
|
-
if (camera instanceof THREE6.PerspectiveCamera) {
|
|
802
|
-
camera.aspect = width / height;
|
|
803
|
-
camera.updateProjectionMatrix();
|
|
804
|
-
} else if (camera instanceof THREE6.OrthographicCamera) {
|
|
805
|
-
const aspect2 = width / height;
|
|
806
|
-
const size = 10;
|
|
807
|
-
camera.left = -size * aspect2;
|
|
808
|
-
camera.right = size * aspect2;
|
|
809
|
-
camera.top = size;
|
|
810
|
-
camera.bottom = -size;
|
|
811
|
-
camera.updateProjectionMatrix();
|
|
812
|
-
}
|
|
813
|
-
renderer.setSize(width, height);
|
|
814
|
-
};
|
|
815
|
-
window.addEventListener("resize", handleResize);
|
|
816
|
-
handleResize();
|
|
817
|
-
setIsReady(true);
|
|
818
|
-
return () => {
|
|
819
|
-
window.removeEventListener("resize", handleResize);
|
|
820
|
-
cancelAnimationFrame(rafRef.current);
|
|
821
|
-
controls.dispose();
|
|
822
|
-
renderer.dispose();
|
|
823
|
-
scene.clear();
|
|
824
|
-
};
|
|
825
|
-
}, []);
|
|
826
|
-
useEffect(() => {
|
|
827
|
-
if (!cameraRef.current || !containerRef.current) return;
|
|
828
|
-
const container = containerRef.current;
|
|
829
|
-
const { clientWidth, clientHeight } = container;
|
|
830
|
-
const aspect = clientWidth / clientHeight;
|
|
831
|
-
const currentPos = cameraRef.current.position.clone();
|
|
832
|
-
let newCamera;
|
|
833
|
-
if (opts.cameraMode === "isometric") {
|
|
834
|
-
const size = 10;
|
|
835
|
-
newCamera = new THREE6.OrthographicCamera(
|
|
836
|
-
-size * aspect,
|
|
837
|
-
size * aspect,
|
|
838
|
-
size,
|
|
839
|
-
-size,
|
|
840
|
-
0.1,
|
|
841
|
-
1e3
|
|
842
|
-
);
|
|
843
|
-
} else {
|
|
844
|
-
newCamera = new THREE6.PerspectiveCamera(45, aspect, 0.1, 1e3);
|
|
845
|
-
}
|
|
846
|
-
newCamera.position.copy(currentPos);
|
|
847
|
-
cameraRef.current = newCamera;
|
|
848
|
-
if (controlsRef.current) {
|
|
849
|
-
controlsRef.current.object = newCamera;
|
|
850
|
-
}
|
|
851
|
-
if (rendererRef.current) {
|
|
852
|
-
const animate = () => {
|
|
853
|
-
rafRef.current = requestAnimationFrame(animate);
|
|
854
|
-
controlsRef.current?.update();
|
|
855
|
-
rendererRef.current?.render(sceneRef.current, newCamera);
|
|
856
|
-
};
|
|
857
|
-
cancelAnimationFrame(rafRef.current);
|
|
858
|
-
animate();
|
|
859
|
-
}
|
|
860
|
-
}, [opts.cameraMode]);
|
|
861
|
-
const setCameraPosition = useCallback((x, y, z) => {
|
|
862
|
-
if (cameraRef.current) {
|
|
863
|
-
cameraRef.current.position.set(x, y, z);
|
|
864
|
-
controlsRef.current?.update();
|
|
865
|
-
}
|
|
866
|
-
}, []);
|
|
867
|
-
const lookAt = useCallback((x, y, z) => {
|
|
868
|
-
if (cameraRef.current) {
|
|
869
|
-
cameraRef.current.lookAt(x, y, z);
|
|
870
|
-
controlsRef.current?.target.set(x, y, z);
|
|
871
|
-
controlsRef.current?.update();
|
|
872
|
-
}
|
|
873
|
-
}, []);
|
|
874
|
-
const resetCamera = useCallback(() => {
|
|
875
|
-
if (cameraRef.current) {
|
|
876
|
-
cameraRef.current.position.copy(initialCameraPosition);
|
|
877
|
-
cameraRef.current.lookAt(0, 0, 0);
|
|
878
|
-
if (controlsRef.current) {
|
|
879
|
-
controlsRef.current.target.set(0, 0, 0);
|
|
880
|
-
controlsRef.current.update();
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
}, [initialCameraPosition]);
|
|
884
|
-
const fitView = useCallback(
|
|
885
|
-
(bounds) => {
|
|
886
|
-
if (!cameraRef.current) return;
|
|
887
|
-
const centerX = (bounds.minX + bounds.maxX) / 2;
|
|
888
|
-
const centerZ = (bounds.minZ + bounds.maxZ) / 2;
|
|
889
|
-
const width = bounds.maxX - bounds.minX;
|
|
890
|
-
const depth = bounds.maxZ - bounds.minZ;
|
|
891
|
-
const maxDim = Math.max(width, depth);
|
|
892
|
-
const distance = maxDim * 1.5;
|
|
893
|
-
const height = distance * 0.8;
|
|
894
|
-
cameraRef.current.position.set(centerX + distance, height, centerZ + distance);
|
|
895
|
-
lookAt(centerX, 0, centerZ);
|
|
896
|
-
},
|
|
897
|
-
[lookAt]
|
|
898
|
-
);
|
|
822
|
+
// lib/drawable/projector3d.ts
|
|
823
|
+
function create3DProjector(opts = {}) {
|
|
824
|
+
const cellSize = opts.cellSize ?? 1;
|
|
825
|
+
const offsetX = opts.offsetX ?? 0;
|
|
826
|
+
const offsetZ = opts.offsetZ ?? 0;
|
|
899
827
|
return {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
scene: sceneRef.current,
|
|
903
|
-
camera: cameraRef.current,
|
|
904
|
-
controls: controlsRef.current,
|
|
905
|
-
isReady,
|
|
906
|
-
dimensions,
|
|
907
|
-
setCameraPosition,
|
|
908
|
-
lookAt,
|
|
909
|
-
resetCamera,
|
|
910
|
-
fitView
|
|
828
|
+
cellSize,
|
|
829
|
+
toWorld: (pos) => [pos.x * cellSize + offsetX, pos.z ?? 0, pos.y * cellSize + offsetZ]
|
|
911
830
|
};
|
|
912
831
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
832
|
+
|
|
833
|
+
// lib/drawable/three/game3dTheme.ts
|
|
834
|
+
var GRID_COLORS_3D = {
|
|
835
|
+
cell: "#444444",
|
|
836
|
+
section: "#666666"
|
|
837
|
+
};
|
|
838
|
+
var DEFAULT_BACKGROUND_3D = "#1a1a2e";
|
|
839
|
+
function cn(...inputs) {
|
|
840
|
+
return twMerge(clsx(inputs));
|
|
841
|
+
}
|
|
842
|
+
var DEFAULT_GRID_CONFIG = {
|
|
843
|
+
cellSize: 1,
|
|
844
|
+
offsetX: 0,
|
|
845
|
+
offsetZ: 0
|
|
846
|
+
};
|
|
847
|
+
var Canvas3DHost = forwardRef(
|
|
848
|
+
({
|
|
849
|
+
cameraMode = "isometric",
|
|
850
|
+
followTarget,
|
|
851
|
+
showGrid = true,
|
|
852
|
+
orientation = "standard",
|
|
853
|
+
overlay = "default",
|
|
854
|
+
shadows = true,
|
|
855
|
+
backgroundColor = DEFAULT_BACKGROUND_3D,
|
|
856
|
+
className,
|
|
857
|
+
isLoading: externalLoading,
|
|
858
|
+
error: externalError,
|
|
859
|
+
tileClickEvent,
|
|
860
|
+
unitClickEvent,
|
|
861
|
+
featureClickEvent,
|
|
862
|
+
canvasClickEvent,
|
|
863
|
+
tileHoverEvent,
|
|
864
|
+
tileLeaveEvent,
|
|
865
|
+
unitAnimationEvent,
|
|
866
|
+
cameraChangeEvent,
|
|
867
|
+
loadingMessage = "Loading 3D Scene...",
|
|
868
|
+
keyMap,
|
|
869
|
+
keyUpMap,
|
|
870
|
+
children,
|
|
871
|
+
drawables
|
|
872
|
+
}, ref) => {
|
|
873
|
+
const containerRef = useRef(null);
|
|
874
|
+
const controlsRef = useRef(null);
|
|
875
|
+
const [internalError, setInternalError] = useState(null);
|
|
876
|
+
const eventBus = useEventBus();
|
|
877
|
+
const keysRef = useRef(/* @__PURE__ */ new Set());
|
|
878
|
+
useEffect(() => {
|
|
879
|
+
if (!keyMap && !keyUpMap) return;
|
|
880
|
+
const down = (e) => {
|
|
881
|
+
if (keysRef.current.has(e.code)) return;
|
|
882
|
+
keysRef.current.add(e.code);
|
|
883
|
+
const ev = keyMap?.[e.code];
|
|
884
|
+
if (ev) {
|
|
885
|
+
eventBus.emit(`UI:${ev}`, {});
|
|
886
|
+
e.preventDefault();
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
const up = (e) => {
|
|
890
|
+
keysRef.current.delete(e.code);
|
|
891
|
+
const ev = keyUpMap?.[e.code];
|
|
892
|
+
if (ev) eventBus.emit(`UI:${ev}`, {});
|
|
893
|
+
};
|
|
894
|
+
window.addEventListener("keydown", down);
|
|
895
|
+
window.addEventListener("keyup", up);
|
|
896
|
+
return () => {
|
|
897
|
+
window.removeEventListener("keydown", down);
|
|
898
|
+
window.removeEventListener("keyup", up);
|
|
899
|
+
};
|
|
900
|
+
}, [keyMap, keyUpMap, eventBus]);
|
|
901
|
+
const eventHandlers = useGameCanvas3DEvents({
|
|
902
|
+
tileClickEvent,
|
|
903
|
+
unitClickEvent,
|
|
904
|
+
featureClickEvent,
|
|
905
|
+
canvasClickEvent,
|
|
906
|
+
tileHoverEvent,
|
|
907
|
+
tileLeaveEvent,
|
|
908
|
+
unitAnimationEvent,
|
|
909
|
+
cameraChangeEvent
|
|
910
|
+
});
|
|
911
|
+
const drawnItems = useMemo(() => collectDrawnItems(drawables ?? []), [drawables]);
|
|
912
|
+
const scenePositions = useMemo(() => drawnItems.map((i) => i.pos), [drawnItems]);
|
|
913
|
+
const hitIndex = useMemo(() => buildHitIndex(drawnItems), [drawnItems]);
|
|
914
|
+
const gridBounds = useMemo(() => {
|
|
915
|
+
if (scenePositions.length === 0) {
|
|
916
|
+
return { minX: 0, maxX: 10, minZ: 0, maxZ: 10 };
|
|
955
917
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
...prev,
|
|
966
|
-
isLoading: false,
|
|
967
|
-
loaded: prev.loaded + 1
|
|
968
|
-
}));
|
|
969
|
-
return model;
|
|
970
|
-
} catch (error) {
|
|
971
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
972
|
-
setState((prev) => ({
|
|
973
|
-
...prev,
|
|
974
|
-
isLoading: false,
|
|
975
|
-
errors: [...prev.errors, errorMsg]
|
|
976
|
-
}));
|
|
977
|
-
throw error;
|
|
918
|
+
let minX = Infinity;
|
|
919
|
+
let maxX = -Infinity;
|
|
920
|
+
let minZ = Infinity;
|
|
921
|
+
let maxZ = -Infinity;
|
|
922
|
+
for (const p of scenePositions) {
|
|
923
|
+
if (p.x < minX) minX = p.x;
|
|
924
|
+
if (p.x > maxX) maxX = p.x;
|
|
925
|
+
if (p.y < minZ) minZ = p.y;
|
|
926
|
+
if (p.y > maxZ) maxZ = p.y;
|
|
978
927
|
}
|
|
979
|
-
|
|
980
|
-
[]
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
928
|
+
return { minX, maxX, minZ, maxZ };
|
|
929
|
+
}, [scenePositions]);
|
|
930
|
+
const cameraTarget = useMemo(
|
|
931
|
+
() => [
|
|
932
|
+
(gridBounds.minX + gridBounds.maxX) / 2,
|
|
933
|
+
0,
|
|
934
|
+
(gridBounds.minZ + gridBounds.maxZ) / 2
|
|
935
|
+
],
|
|
936
|
+
[gridBounds]
|
|
937
|
+
);
|
|
938
|
+
const gridConfig = useMemo(
|
|
939
|
+
() => ({
|
|
940
|
+
...DEFAULT_GRID_CONFIG,
|
|
941
|
+
offsetX: -(gridBounds.maxX - gridBounds.minX) / 2,
|
|
942
|
+
offsetZ: -(gridBounds.maxZ - gridBounds.minZ) / 2
|
|
943
|
+
}),
|
|
944
|
+
[gridBounds]
|
|
945
|
+
);
|
|
946
|
+
const drawableProjector = useMemo(
|
|
947
|
+
() => create3DProjector({
|
|
948
|
+
cellSize: gridConfig.cellSize,
|
|
949
|
+
offsetX: -gridBounds.minX * gridConfig.cellSize,
|
|
950
|
+
offsetZ: -gridBounds.minZ * gridConfig.cellSize
|
|
951
|
+
}),
|
|
952
|
+
[gridBounds, gridConfig]
|
|
953
|
+
);
|
|
954
|
+
useImperativeHandle(ref, () => ({
|
|
955
|
+
getCameraPosition: () => {
|
|
956
|
+
if (controlsRef.current) {
|
|
957
|
+
const pos = controlsRef.current.object.position;
|
|
958
|
+
return new THREE9.Vector3(pos.x, pos.y, pos.z);
|
|
959
|
+
}
|
|
960
|
+
return null;
|
|
961
|
+
},
|
|
962
|
+
setCameraPosition: (x, y, z) => {
|
|
963
|
+
if (controlsRef.current) {
|
|
964
|
+
controlsRef.current.object.position.set(x, y, z);
|
|
965
|
+
controlsRef.current.update();
|
|
966
|
+
}
|
|
967
|
+
},
|
|
968
|
+
lookAt: (x, y, z) => {
|
|
969
|
+
if (controlsRef.current) {
|
|
970
|
+
controlsRef.current.target.set(x, y, z);
|
|
971
|
+
controlsRef.current.update();
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
resetCamera: () => {
|
|
975
|
+
if (controlsRef.current) {
|
|
976
|
+
controlsRef.current.reset();
|
|
977
|
+
}
|
|
978
|
+
},
|
|
979
|
+
screenshot: () => {
|
|
980
|
+
const canvas = containerRef.current?.querySelector("canvas");
|
|
981
|
+
if (canvas) {
|
|
982
|
+
return canvas.toDataURL("image/png");
|
|
983
|
+
}
|
|
984
|
+
return null;
|
|
1001
985
|
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
isLoading: true,
|
|
1010
|
-
total: urls.length,
|
|
1011
|
-
loaded: 0,
|
|
1012
|
-
errors: []
|
|
1013
|
-
}));
|
|
1014
|
-
let completed = 0;
|
|
1015
|
-
const errors = [];
|
|
1016
|
-
await Promise.all(
|
|
1017
|
-
urls.map(async (url) => {
|
|
1018
|
-
try {
|
|
1019
|
-
if (url.endsWith(".glb") || url.endsWith(".gltf")) {
|
|
1020
|
-
await loaderRef.current.loadModel(url);
|
|
1021
|
-
} else if (url.endsWith(".obj")) {
|
|
1022
|
-
await loaderRef.current.loadOBJ(url);
|
|
1023
|
-
} else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
|
|
1024
|
-
await loaderRef.current.loadTexture(url);
|
|
1025
|
-
}
|
|
1026
|
-
completed++;
|
|
1027
|
-
updateProgress(completed, urls.length);
|
|
1028
|
-
} catch (error) {
|
|
1029
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1030
|
-
errors.push(`${url}: ${errorMsg}`);
|
|
1031
|
-
completed++;
|
|
1032
|
-
updateProgress(completed, urls.length);
|
|
1033
|
-
}
|
|
1034
|
-
})
|
|
986
|
+
}));
|
|
987
|
+
const cameraConfig = useMemo(() => {
|
|
988
|
+
const size = Math.max(
|
|
989
|
+
gridBounds.maxX - gridBounds.minX,
|
|
990
|
+
gridBounds.maxZ - gridBounds.minZ,
|
|
991
|
+
4
|
|
992
|
+
// minimum framing distance so a tiny board isn't zoomed in
|
|
1035
993
|
);
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
}, []);
|
|
1050
|
-
const getModel = useCallback((url) => {
|
|
1051
|
-
try {
|
|
1052
|
-
return loaderRef.current.getModel(url);
|
|
1053
|
-
} catch {
|
|
1054
|
-
return void 0;
|
|
1055
|
-
}
|
|
1056
|
-
}, []);
|
|
1057
|
-
const getTexture = useCallback((url) => {
|
|
1058
|
-
try {
|
|
1059
|
-
return loaderRef.current.getTexture(url);
|
|
1060
|
-
} catch {
|
|
1061
|
-
return void 0;
|
|
1062
|
-
}
|
|
1063
|
-
}, []);
|
|
1064
|
-
const clearCache = useCallback(() => {
|
|
1065
|
-
loaderRef.current.clearCache();
|
|
1066
|
-
setState({
|
|
1067
|
-
isLoading: false,
|
|
1068
|
-
progress: 0,
|
|
1069
|
-
loaded: 0,
|
|
1070
|
-
total: 0,
|
|
1071
|
-
errors: []
|
|
1072
|
-
});
|
|
1073
|
-
}, []);
|
|
1074
|
-
return {
|
|
1075
|
-
...state,
|
|
1076
|
-
loadModel,
|
|
1077
|
-
loadOBJ,
|
|
1078
|
-
loadTexture,
|
|
1079
|
-
preload,
|
|
1080
|
-
hasModel,
|
|
1081
|
-
hasTexture,
|
|
1082
|
-
getModel,
|
|
1083
|
-
getTexture,
|
|
1084
|
-
clearCache
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
|
-
function useSceneGraph() {
|
|
1088
|
-
const nodesRef = useRef(/* @__PURE__ */ new Map());
|
|
1089
|
-
const addNode = useCallback((node) => {
|
|
1090
|
-
const existing = nodesRef.current.get(node.id);
|
|
1091
|
-
if (existing) {
|
|
1092
|
-
existing.mesh.removeFromParent();
|
|
1093
|
-
}
|
|
1094
|
-
nodesRef.current.set(node.id, node);
|
|
1095
|
-
}, []);
|
|
1096
|
-
const removeNode = useCallback((id) => {
|
|
1097
|
-
const node = nodesRef.current.get(id);
|
|
1098
|
-
if (node) {
|
|
1099
|
-
node.mesh.removeFromParent();
|
|
1100
|
-
nodesRef.current.delete(id);
|
|
1101
|
-
}
|
|
1102
|
-
}, []);
|
|
1103
|
-
const getNode = useCallback((id) => {
|
|
1104
|
-
return nodesRef.current.get(id);
|
|
1105
|
-
}, []);
|
|
1106
|
-
const updateNodePosition = useCallback(
|
|
1107
|
-
(id, x, y, z) => {
|
|
1108
|
-
const node = nodesRef.current.get(id);
|
|
1109
|
-
if (node) {
|
|
1110
|
-
node.mesh.position.set(x, y, z);
|
|
1111
|
-
node.position = { x, y, z };
|
|
1112
|
-
}
|
|
1113
|
-
},
|
|
1114
|
-
[]
|
|
1115
|
-
);
|
|
1116
|
-
const updateNodeGridPosition = useCallback(
|
|
1117
|
-
(id, gridX, gridZ) => {
|
|
1118
|
-
const node = nodesRef.current.get(id);
|
|
1119
|
-
if (node) {
|
|
1120
|
-
node.gridPosition = { x: gridX, z: gridZ };
|
|
1121
|
-
}
|
|
1122
|
-
},
|
|
1123
|
-
[]
|
|
1124
|
-
);
|
|
1125
|
-
const getNodeAtGrid = useCallback(
|
|
1126
|
-
(x, z, type) => {
|
|
1127
|
-
return Array.from(nodesRef.current.values()).find((node) => {
|
|
1128
|
-
const matchesGrid = node.gridPosition.x === x && node.gridPosition.z === z;
|
|
1129
|
-
return type ? matchesGrid && node.type === type : matchesGrid;
|
|
1130
|
-
});
|
|
1131
|
-
},
|
|
1132
|
-
[]
|
|
1133
|
-
);
|
|
1134
|
-
const getNodesByType = useCallback((type) => {
|
|
1135
|
-
return Array.from(nodesRef.current.values()).filter((node) => node.type === type);
|
|
1136
|
-
}, []);
|
|
1137
|
-
const getNodesInBounds = useCallback(
|
|
1138
|
-
(minX, maxX, minZ, maxZ) => {
|
|
1139
|
-
return Array.from(nodesRef.current.values()).filter((node) => {
|
|
1140
|
-
const { x, z } = node.gridPosition;
|
|
1141
|
-
return x >= minX && x <= maxX && z >= minZ && z <= maxZ;
|
|
1142
|
-
});
|
|
1143
|
-
},
|
|
1144
|
-
[]
|
|
1145
|
-
);
|
|
1146
|
-
const clearNodes = useCallback(() => {
|
|
1147
|
-
nodesRef.current.forEach((node) => {
|
|
1148
|
-
node.mesh.removeFromParent();
|
|
1149
|
-
});
|
|
1150
|
-
nodesRef.current.clear();
|
|
1151
|
-
}, []);
|
|
1152
|
-
const countNodes = useCallback((type) => {
|
|
1153
|
-
if (!type) {
|
|
1154
|
-
return nodesRef.current.size;
|
|
1155
|
-
}
|
|
1156
|
-
return Array.from(nodesRef.current.values()).filter((node) => node.type === type).length;
|
|
1157
|
-
}, []);
|
|
1158
|
-
return {
|
|
1159
|
-
nodesRef,
|
|
1160
|
-
addNode,
|
|
1161
|
-
removeNode,
|
|
1162
|
-
getNode,
|
|
1163
|
-
updateNodePosition,
|
|
1164
|
-
updateNodeGridPosition,
|
|
1165
|
-
getNodeAtGrid,
|
|
1166
|
-
getNodesByType,
|
|
1167
|
-
getNodesInBounds,
|
|
1168
|
-
clearNodes,
|
|
1169
|
-
countNodes
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
function useRaycaster(options) {
|
|
1173
|
-
const { camera, canvas, cellSize = 1, offsetX = 0, offsetZ = 0 } = options;
|
|
1174
|
-
const raycaster = useRef(new THREE6.Raycaster());
|
|
1175
|
-
const mouse = useRef(new THREE6.Vector2());
|
|
1176
|
-
const clientToNDC = useCallback(
|
|
1177
|
-
(clientX, clientY) => {
|
|
1178
|
-
if (!canvas) {
|
|
1179
|
-
return { x: 0, y: 0 };
|
|
994
|
+
const cx = (gridBounds.minX + gridBounds.maxX) / 2;
|
|
995
|
+
const cz = (gridBounds.minZ + gridBounds.maxZ) / 2;
|
|
996
|
+
const d = size * 1;
|
|
997
|
+
switch (cameraMode) {
|
|
998
|
+
case "isometric":
|
|
999
|
+
return { position: [cx + d, d * 0.8, cz + d], fov: 45 };
|
|
1000
|
+
case "top-down":
|
|
1001
|
+
return { position: [cx, d * 2, cz + d * 0.35], fov: 45 };
|
|
1002
|
+
case "follow":
|
|
1003
|
+
return { position: [cx, d * 0.5, cz + d], fov: 45 };
|
|
1004
|
+
case "perspective":
|
|
1005
|
+
default:
|
|
1006
|
+
return { position: [cx + d, d, cz + d], fov: 45 };
|
|
1180
1007
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1008
|
+
}, [cameraMode, gridBounds]);
|
|
1009
|
+
const followWorld = useMemo(() => {
|
|
1010
|
+
if (followTarget) return drawableProjector.toWorld(followTarget);
|
|
1011
|
+
return cameraTarget;
|
|
1012
|
+
}, [followTarget, drawableProjector, cameraTarget]);
|
|
1013
|
+
const followOffset = useMemo(
|
|
1014
|
+
() => cameraMode === "chase" ? [0, 4, -7] : [5, 7, 5],
|
|
1015
|
+
[cameraMode]
|
|
1016
|
+
);
|
|
1017
|
+
const handleGroundClick = useCallback((e) => {
|
|
1018
|
+
if (!tileClickEvent && !unitClickEvent) return;
|
|
1019
|
+
e.stopPropagation();
|
|
1020
|
+
const cell = {
|
|
1021
|
+
x: Math.round(e.point.x / gridConfig.cellSize + gridBounds.minX),
|
|
1022
|
+
y: Math.round(e.point.z / gridConfig.cellSize + gridBounds.minZ)
|
|
1185
1023
|
};
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
if (!canvas) return false;
|
|
1192
|
-
const rect = canvas.getBoundingClientRect();
|
|
1193
|
-
return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom;
|
|
1194
|
-
},
|
|
1195
|
-
[canvas]
|
|
1196
|
-
);
|
|
1197
|
-
const getIntersection = useCallback(
|
|
1198
|
-
(clientX, clientY, objects) => {
|
|
1199
|
-
if (!camera || !canvas) return null;
|
|
1200
|
-
const ndc = clientToNDC(clientX, clientY);
|
|
1201
|
-
mouse.current.set(ndc.x, ndc.y);
|
|
1202
|
-
raycaster.current.setFromCamera(mouse.current, camera);
|
|
1203
|
-
const intersects = raycaster.current.intersectObjects(objects, true);
|
|
1204
|
-
if (intersects.length > 0) {
|
|
1205
|
-
const hit = intersects[0];
|
|
1206
|
-
return {
|
|
1207
|
-
object: hit.object,
|
|
1208
|
-
point: hit.point,
|
|
1209
|
-
distance: hit.distance,
|
|
1210
|
-
uv: hit.uv,
|
|
1211
|
-
face: hit.face,
|
|
1212
|
-
faceIndex: hit.faceIndex,
|
|
1213
|
-
instanceId: hit.instanceId
|
|
1214
|
-
};
|
|
1215
|
-
}
|
|
1216
|
-
return null;
|
|
1217
|
-
},
|
|
1218
|
-
[camera, canvas, clientToNDC]
|
|
1219
|
-
);
|
|
1220
|
-
const getAllIntersections = useCallback(
|
|
1221
|
-
(clientX, clientY, objects) => {
|
|
1222
|
-
if (!camera || !canvas) return [];
|
|
1223
|
-
const ndc = clientToNDC(clientX, clientY);
|
|
1224
|
-
mouse.current.set(ndc.x, ndc.y);
|
|
1225
|
-
raycaster.current.setFromCamera(mouse.current, camera);
|
|
1226
|
-
const intersects = raycaster.current.intersectObjects(objects, true);
|
|
1227
|
-
return intersects.map((hit) => ({
|
|
1228
|
-
object: hit.object,
|
|
1229
|
-
point: hit.point,
|
|
1230
|
-
distance: hit.distance,
|
|
1231
|
-
uv: hit.uv,
|
|
1232
|
-
face: hit.face,
|
|
1233
|
-
faceIndex: hit.faceIndex,
|
|
1234
|
-
instanceId: hit.instanceId
|
|
1235
|
-
}));
|
|
1236
|
-
},
|
|
1237
|
-
[camera, canvas, clientToNDC]
|
|
1238
|
-
);
|
|
1239
|
-
const getGridCoordinates = useCallback(
|
|
1240
|
-
(clientX, clientY) => {
|
|
1241
|
-
if (!camera || !canvas) return null;
|
|
1242
|
-
const ndc = clientToNDC(clientX, clientY);
|
|
1243
|
-
mouse.current.set(ndc.x, ndc.y);
|
|
1244
|
-
raycaster.current.setFromCamera(mouse.current, camera);
|
|
1245
|
-
const plane = new THREE6.Plane(new THREE6.Vector3(0, 1, 0), 0);
|
|
1246
|
-
const target = new THREE6.Vector3();
|
|
1247
|
-
const intersection = raycaster.current.ray.intersectPlane(plane, target);
|
|
1248
|
-
if (intersection) {
|
|
1249
|
-
const gridX = Math.round((target.x - offsetX) / cellSize);
|
|
1250
|
-
const gridZ = Math.round((target.z - offsetZ) / cellSize);
|
|
1251
|
-
return { x: gridX, z: gridZ };
|
|
1024
|
+
const hitId = hitIndex.get(`${cell.x},${cell.y}`);
|
|
1025
|
+
if (hitId !== void 0 && unitClickEvent) {
|
|
1026
|
+
eventBus.emit(`UI:${unitClickEvent}`, { unitId: hitId, x: cell.x, z: cell.y });
|
|
1027
|
+
} else if (tileClickEvent) {
|
|
1028
|
+
eventBus.emit(`UI:${tileClickEvent}`, { x: cell.x, z: cell.y });
|
|
1252
1029
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
tileMeshes.push(obj);
|
|
1264
|
-
}
|
|
1265
|
-
});
|
|
1266
|
-
const hit = getIntersection(clientX, clientY, tileMeshes);
|
|
1267
|
-
if (hit) {
|
|
1268
|
-
const gridCoords2 = getGridCoordinates(clientX, clientY);
|
|
1269
|
-
if (gridCoords2) {
|
|
1270
|
-
return {
|
|
1271
|
-
gridX: gridCoords2.x,
|
|
1272
|
-
gridZ: gridCoords2.z,
|
|
1273
|
-
worldPosition: hit.point,
|
|
1274
|
-
objectType: hit.object.userData.type || "tile",
|
|
1275
|
-
objectId: hit.object.userData.id || hit.object.userData.tileId
|
|
1276
|
-
};
|
|
1030
|
+
}, [tileClickEvent, unitClickEvent, gridConfig, gridBounds, hitIndex, eventBus]);
|
|
1031
|
+
if (externalLoading) {
|
|
1032
|
+
return /* @__PURE__ */ jsx(
|
|
1033
|
+
Canvas3DLoadingState,
|
|
1034
|
+
{
|
|
1035
|
+
progress: 1,
|
|
1036
|
+
loaded: 1,
|
|
1037
|
+
total: 1,
|
|
1038
|
+
message: loadingMessage,
|
|
1039
|
+
className
|
|
1277
1040
|
}
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
const displayError = externalError || internalError;
|
|
1044
|
+
if (displayError) {
|
|
1045
|
+
return /* @__PURE__ */ jsx(Canvas3DErrorBoundary, { children: /* @__PURE__ */ jsx("div", { className: "game-canvas-3d game-canvas-3d--error", children: /* @__PURE__ */ jsxs("div", { className: "game-canvas-3d__error", children: [
|
|
1046
|
+
"Error: ",
|
|
1047
|
+
displayError
|
|
1048
|
+
] }) }) });
|
|
1049
|
+
}
|
|
1050
|
+
return /* @__PURE__ */ jsx(
|
|
1051
|
+
Canvas3DErrorBoundary,
|
|
1052
|
+
{
|
|
1053
|
+
onError: (err) => setInternalError(err.message),
|
|
1054
|
+
onReset: () => setInternalError(null),
|
|
1055
|
+
children: /* @__PURE__ */ jsx(
|
|
1056
|
+
"div",
|
|
1057
|
+
{
|
|
1058
|
+
ref: containerRef,
|
|
1059
|
+
className: cn("game-canvas-3d relative w-full overflow-hidden", className),
|
|
1060
|
+
style: { flex: "1 1 0", minHeight: "400px" },
|
|
1061
|
+
"data-orientation": orientation,
|
|
1062
|
+
"data-camera-mode": cameraMode,
|
|
1063
|
+
"data-overlay": overlay,
|
|
1064
|
+
children: /* @__PURE__ */ jsxs(
|
|
1065
|
+
Canvas,
|
|
1066
|
+
{
|
|
1067
|
+
shadows,
|
|
1068
|
+
camera: {
|
|
1069
|
+
position: cameraConfig.position,
|
|
1070
|
+
fov: cameraConfig.fov,
|
|
1071
|
+
near: 0.1,
|
|
1072
|
+
far: 1e3
|
|
1073
|
+
},
|
|
1074
|
+
style: { background: backgroundColor, position: "absolute", inset: 0 },
|
|
1075
|
+
onClick: (e) => {
|
|
1076
|
+
if (e.target === e.currentTarget) {
|
|
1077
|
+
eventHandlers.handleCanvasClick(e);
|
|
1078
|
+
}
|
|
1079
|
+
},
|
|
1080
|
+
children: [
|
|
1081
|
+
/* @__PURE__ */ jsx(CameraController3D, { onCameraChange: eventHandlers.handleCameraChange }),
|
|
1082
|
+
(cameraMode === "follow" || cameraMode === "chase") && /* @__PURE__ */ jsx(FollowCamera3D, { target: followWorld, offset: followOffset }),
|
|
1083
|
+
/* @__PURE__ */ jsx(
|
|
1084
|
+
Lighting3D,
|
|
1085
|
+
{
|
|
1086
|
+
shadows,
|
|
1087
|
+
shadowBias: -4e-4,
|
|
1088
|
+
shadowNormalBias: 0.04,
|
|
1089
|
+
shadowCameraSize: 5,
|
|
1090
|
+
shadowCameraNear: 0.5,
|
|
1091
|
+
shadowCameraFar: 500
|
|
1092
|
+
}
|
|
1093
|
+
),
|
|
1094
|
+
showGrid && /* @__PURE__ */ jsx(
|
|
1095
|
+
Grid,
|
|
1096
|
+
{
|
|
1097
|
+
args: [
|
|
1098
|
+
Math.max(gridBounds.maxX - gridBounds.minX + 2, 10),
|
|
1099
|
+
Math.max(gridBounds.maxZ - gridBounds.minZ + 2, 10)
|
|
1100
|
+
],
|
|
1101
|
+
position: [
|
|
1102
|
+
(gridBounds.maxX - gridBounds.minX) / 2 - 0.5,
|
|
1103
|
+
0,
|
|
1104
|
+
(gridBounds.maxZ - gridBounds.minZ) / 2 - 0.5
|
|
1105
|
+
],
|
|
1106
|
+
cellSize: 1,
|
|
1107
|
+
cellThickness: 1,
|
|
1108
|
+
cellColor: GRID_COLORS_3D.cell,
|
|
1109
|
+
sectionSize: 5,
|
|
1110
|
+
sectionThickness: 1.5,
|
|
1111
|
+
sectionColor: GRID_COLORS_3D.section,
|
|
1112
|
+
fadeDistance: 50,
|
|
1113
|
+
fadeStrength: 1
|
|
1114
|
+
}
|
|
1115
|
+
),
|
|
1116
|
+
drawables && drawables.length > 0 && /* @__PURE__ */ jsx("group", { children: drawables.map((node, i) => /* @__PURE__ */ jsx(Drawable3D, { node, projector: drawableProjector }, i)) }),
|
|
1117
|
+
(tileClickEvent || unitClickEvent) && /* @__PURE__ */ jsxs(
|
|
1118
|
+
"mesh",
|
|
1119
|
+
{
|
|
1120
|
+
rotation: [-Math.PI / 2, 0, 0],
|
|
1121
|
+
position: [
|
|
1122
|
+
(gridBounds.maxX - gridBounds.minX) / 2 * gridConfig.cellSize,
|
|
1123
|
+
0,
|
|
1124
|
+
(gridBounds.maxZ - gridBounds.minZ) / 2 * gridConfig.cellSize
|
|
1125
|
+
],
|
|
1126
|
+
onClick: handleGroundClick,
|
|
1127
|
+
children: [
|
|
1128
|
+
/* @__PURE__ */ jsx(
|
|
1129
|
+
"planeGeometry",
|
|
1130
|
+
{
|
|
1131
|
+
args: [
|
|
1132
|
+
(gridBounds.maxX - gridBounds.minX + 4) * gridConfig.cellSize,
|
|
1133
|
+
(gridBounds.maxZ - gridBounds.minZ + 4) * gridConfig.cellSize
|
|
1134
|
+
]
|
|
1135
|
+
}
|
|
1136
|
+
),
|
|
1137
|
+
/* @__PURE__ */ jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })
|
|
1138
|
+
]
|
|
1139
|
+
}
|
|
1140
|
+
),
|
|
1141
|
+
children,
|
|
1142
|
+
/* @__PURE__ */ jsx(
|
|
1143
|
+
OrbitControls,
|
|
1144
|
+
{
|
|
1145
|
+
ref: controlsRef,
|
|
1146
|
+
enabled: cameraMode !== "follow" && cameraMode !== "chase",
|
|
1147
|
+
target: cameraTarget,
|
|
1148
|
+
enableDamping: true,
|
|
1149
|
+
dampingFactor: 0.05,
|
|
1150
|
+
enableZoom: true,
|
|
1151
|
+
enablePan: true,
|
|
1152
|
+
touches: { ONE: THREE9.TOUCH.ROTATE, TWO: THREE9.TOUCH.DOLLY_PAN },
|
|
1153
|
+
minDistance: 2,
|
|
1154
|
+
maxDistance: 100,
|
|
1155
|
+
maxPolarAngle: Math.PI / 2 - 0.1
|
|
1156
|
+
}
|
|
1157
|
+
)
|
|
1158
|
+
]
|
|
1159
|
+
}
|
|
1160
|
+
)
|
|
1161
|
+
}
|
|
1162
|
+
)
|
|
1278
1163
|
}
|
|
1279
|
-
|
|
1280
|
-
if (gridCoords) {
|
|
1281
|
-
return {
|
|
1282
|
-
gridX: gridCoords.x,
|
|
1283
|
-
gridZ: gridCoords.z,
|
|
1284
|
-
worldPosition: new THREE6.Vector3(
|
|
1285
|
-
gridCoords.x * cellSize + offsetX,
|
|
1286
|
-
0,
|
|
1287
|
-
gridCoords.z * cellSize + offsetZ
|
|
1288
|
-
)
|
|
1289
|
-
};
|
|
1290
|
-
}
|
|
1291
|
-
return null;
|
|
1292
|
-
},
|
|
1293
|
-
[camera, canvas, getIntersection, getGridCoordinates, cellSize, offsetX, offsetZ]
|
|
1294
|
-
);
|
|
1295
|
-
return {
|
|
1296
|
-
raycaster,
|
|
1297
|
-
mouse,
|
|
1298
|
-
getIntersection,
|
|
1299
|
-
getAllIntersections,
|
|
1300
|
-
getGridCoordinates,
|
|
1301
|
-
getTileAtPosition,
|
|
1302
|
-
clientToNDC,
|
|
1303
|
-
isWithinCanvas
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
var log3 = createLogger("almadar:eventbus");
|
|
1307
|
-
var subLog = createLogger("almadar:eventbus:subscribe");
|
|
1308
|
-
var scopeLog = createLogger("almadar:ui:trait-scope");
|
|
1309
|
-
function getGlobalEventBus() {
|
|
1310
|
-
if (typeof window !== "undefined") {
|
|
1311
|
-
return window.__kflowEventBus ?? null;
|
|
1164
|
+
);
|
|
1312
1165
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
if (handlers) {
|
|
1328
|
-
handlers.forEach((handler) => {
|
|
1329
|
-
try {
|
|
1330
|
-
handler(event);
|
|
1331
|
-
} catch (error) {
|
|
1332
|
-
log3.error("Error in listener", { type, error: error instanceof Error ? error : String(error) });
|
|
1333
|
-
}
|
|
1166
|
+
);
|
|
1167
|
+
Canvas3DHost.displayName = "Canvas3DHost";
|
|
1168
|
+
function Scene3D({ background = "#1a1a2e", fog, children }) {
|
|
1169
|
+
const { scene } = useThree();
|
|
1170
|
+
const initializedRef = useRef(false);
|
|
1171
|
+
useEffect(() => {
|
|
1172
|
+
if (initializedRef.current) return;
|
|
1173
|
+
initializedRef.current = true;
|
|
1174
|
+
if (background.startsWith("#") || background.startsWith("rgb")) {
|
|
1175
|
+
scene.background = new THREE9.Color(background);
|
|
1176
|
+
} else {
|
|
1177
|
+
const loader = new THREE9.TextureLoader();
|
|
1178
|
+
loader.load(background, (texture) => {
|
|
1179
|
+
scene.background = texture;
|
|
1334
1180
|
});
|
|
1335
1181
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
handler(event);
|
|
1339
|
-
} catch (error) {
|
|
1340
|
-
log3.error("Error in onAny listener", { type, error: error instanceof Error ? error : String(error) });
|
|
1341
|
-
}
|
|
1342
|
-
});
|
|
1343
|
-
},
|
|
1344
|
-
on: (type, listener) => {
|
|
1345
|
-
if (!fallbackListeners.has(type)) {
|
|
1346
|
-
fallbackListeners.set(type, /* @__PURE__ */ new Set());
|
|
1182
|
+
if (fog) {
|
|
1183
|
+
scene.fog = new THREE9.Fog(fog.color, fog.near, fog.far);
|
|
1347
1184
|
}
|
|
1348
|
-
fallbackListeners.get(type).add(listener);
|
|
1349
|
-
subLog.debug("subscribe", { type, totalListeners: fallbackListeners.get(type).size });
|
|
1350
|
-
return () => {
|
|
1351
|
-
const handlers = fallbackListeners.get(type);
|
|
1352
|
-
if (handlers) {
|
|
1353
|
-
handlers.delete(listener);
|
|
1354
|
-
if (handlers.size === 0) {
|
|
1355
|
-
fallbackListeners.delete(type);
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
};
|
|
1359
|
-
},
|
|
1360
|
-
once: (type, listener) => {
|
|
1361
|
-
const wrappedListener = (event) => {
|
|
1362
|
-
fallbackListeners.get(type)?.delete(wrappedListener);
|
|
1363
|
-
listener(event);
|
|
1364
|
-
};
|
|
1365
|
-
return fallbackEventBus.on(type, wrappedListener);
|
|
1366
|
-
},
|
|
1367
|
-
hasListeners: (type) => {
|
|
1368
|
-
const handlers = fallbackListeners.get(type);
|
|
1369
|
-
return handlers !== void 0 && handlers.size > 0;
|
|
1370
|
-
},
|
|
1371
|
-
onAny: (listener) => {
|
|
1372
|
-
fallbackAnyListeners.add(listener);
|
|
1373
|
-
subLog.debug("subscribe:any", { totalAnyListeners: fallbackAnyListeners.size });
|
|
1374
1185
|
return () => {
|
|
1375
|
-
|
|
1186
|
+
scene.background = null;
|
|
1187
|
+
scene.fog = null;
|
|
1376
1188
|
};
|
|
1377
|
-
}
|
|
1378
|
-
};
|
|
1379
|
-
function useEventBus() {
|
|
1380
|
-
const context = useContext(EventBusContext);
|
|
1381
|
-
const baseBus = context ?? getGlobalEventBus() ?? fallbackEventBus;
|
|
1382
|
-
const scope = useTraitScope();
|
|
1383
|
-
return useMemo(() => {
|
|
1384
|
-
if (!scope) {
|
|
1385
|
-
return {
|
|
1386
|
-
...baseBus,
|
|
1387
|
-
emit: (type, payload, source) => {
|
|
1388
|
-
if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
|
|
1389
|
-
scopeLog.warn("emit:bare-key-no-scope", { type });
|
|
1390
|
-
}
|
|
1391
|
-
baseBus.emit(type, payload, source);
|
|
1392
|
-
}
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
return {
|
|
1396
|
-
...baseBus,
|
|
1397
|
-
emit: (type, payload, source) => {
|
|
1398
|
-
if (typeof type === "string" && type.startsWith("UI:")) {
|
|
1399
|
-
const tail = type.slice(3);
|
|
1400
|
-
const qualified = tail.includes(".") ? type : `UI:${scope.orbital}.${scope.trait}.${tail}`;
|
|
1401
|
-
if (qualified !== type) {
|
|
1402
|
-
scopeLog.info("emit:qualified", {
|
|
1403
|
-
from: type,
|
|
1404
|
-
to: qualified,
|
|
1405
|
-
scopeOrbital: scope.orbital,
|
|
1406
|
-
scopeTrait: scope.trait
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
baseBus.emit(qualified, payload, source);
|
|
1410
|
-
return;
|
|
1411
|
-
}
|
|
1412
|
-
baseBus.emit(type, payload, source);
|
|
1413
|
-
}
|
|
1414
|
-
};
|
|
1415
|
-
}, [baseBus, scope]);
|
|
1416
|
-
}
|
|
1417
|
-
function useEmitEvent() {
|
|
1418
|
-
const eventBus = useEventBus();
|
|
1419
|
-
return useCallback(
|
|
1420
|
-
(type, payload, source) => {
|
|
1421
|
-
eventBus.emit(type, payload, source);
|
|
1422
|
-
},
|
|
1423
|
-
[eventBus]
|
|
1424
|
-
);
|
|
1189
|
+
}, [scene, background, fog]);
|
|
1190
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
1425
1191
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
elevation: tile.elevation
|
|
1458
|
-
});
|
|
1459
|
-
}
|
|
1460
|
-
optionsRef.current.onTileClick?.(tile, event);
|
|
1461
|
-
},
|
|
1462
|
-
[tileClickEvent, emit]
|
|
1463
|
-
);
|
|
1464
|
-
const handleUnitClick = useCallback(
|
|
1465
|
-
(unit, event) => {
|
|
1466
|
-
if (unitClickEvent) {
|
|
1467
|
-
emit(unitClickEvent, {
|
|
1468
|
-
unitId: unit.id,
|
|
1469
|
-
x: unit.x,
|
|
1470
|
-
z: unit.z ?? unit.y ?? 0,
|
|
1471
|
-
unitType: unit.unitType,
|
|
1472
|
-
name: unit.name,
|
|
1473
|
-
team: unit.team,
|
|
1474
|
-
faction: unit.faction,
|
|
1475
|
-
health: unit.health,
|
|
1476
|
-
maxHealth: unit.maxHealth
|
|
1477
|
-
});
|
|
1192
|
+
var Camera3D = forwardRef(
|
|
1193
|
+
({
|
|
1194
|
+
mode = "isometric",
|
|
1195
|
+
position = [10, 10, 10],
|
|
1196
|
+
target = [0, 0, 0],
|
|
1197
|
+
zoom = 1,
|
|
1198
|
+
fov = 45,
|
|
1199
|
+
enableOrbit = true,
|
|
1200
|
+
minDistance = 2,
|
|
1201
|
+
maxDistance = 100,
|
|
1202
|
+
onChange
|
|
1203
|
+
}, ref) => {
|
|
1204
|
+
const { camera, set, viewport } = useThree();
|
|
1205
|
+
const controlsRef = useRef(null);
|
|
1206
|
+
const initialPosition = useRef(new THREE9.Vector3(...position));
|
|
1207
|
+
const initialTarget = useRef(new THREE9.Vector3(...target));
|
|
1208
|
+
useEffect(() => {
|
|
1209
|
+
let newCamera;
|
|
1210
|
+
if (mode === "isometric") {
|
|
1211
|
+
const aspect = viewport.aspect;
|
|
1212
|
+
const size = 10 / zoom;
|
|
1213
|
+
newCamera = new THREE9.OrthographicCamera(
|
|
1214
|
+
-size * aspect,
|
|
1215
|
+
size * aspect,
|
|
1216
|
+
size,
|
|
1217
|
+
-size,
|
|
1218
|
+
0.1,
|
|
1219
|
+
1e3
|
|
1220
|
+
);
|
|
1221
|
+
} else {
|
|
1222
|
+
newCamera = new THREE9.PerspectiveCamera(fov, viewport.aspect, 0.1, 1e3);
|
|
1478
1223
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
if (featureClickEvent) {
|
|
1486
|
-
emit(featureClickEvent, {
|
|
1487
|
-
featureId: feature.id,
|
|
1488
|
-
x: feature.x,
|
|
1489
|
-
z: feature.z ?? feature.y ?? 0,
|
|
1490
|
-
type: feature.type,
|
|
1491
|
-
elevation: feature.elevation
|
|
1492
|
-
});
|
|
1224
|
+
newCamera.position.copy(initialPosition.current);
|
|
1225
|
+
newCamera.lookAt(initialTarget.current.x, initialTarget.current.y, initialTarget.current.z);
|
|
1226
|
+
set({ camera: newCamera });
|
|
1227
|
+
if (mode === "top-down") {
|
|
1228
|
+
newCamera.position.set(0, 20 / zoom, 0);
|
|
1229
|
+
newCamera.lookAt(0, 0, 0);
|
|
1493
1230
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
[
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
if (canvasClickEvent) {
|
|
1501
|
-
emit(canvasClickEvent, {
|
|
1502
|
-
clientX: event.clientX,
|
|
1503
|
-
clientY: event.clientY,
|
|
1504
|
-
button: event.button
|
|
1505
|
-
});
|
|
1231
|
+
return () => {
|
|
1232
|
+
};
|
|
1233
|
+
}, [mode, fov, zoom, viewport.aspect, set]);
|
|
1234
|
+
useFrame(() => {
|
|
1235
|
+
if (onChange) {
|
|
1236
|
+
onChange(camera);
|
|
1506
1237
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
if (tileHoverEvent) {
|
|
1515
|
-
emit(tileHoverEvent, {
|
|
1516
|
-
tileId: tile.id,
|
|
1517
|
-
x: tile.x,
|
|
1518
|
-
z: tile.z ?? tile.y ?? 0,
|
|
1519
|
-
type: tile.type
|
|
1520
|
-
});
|
|
1238
|
+
});
|
|
1239
|
+
useImperativeHandle(ref, () => ({
|
|
1240
|
+
getCamera: () => camera,
|
|
1241
|
+
setPosition: (x, y, z) => {
|
|
1242
|
+
camera.position.set(x, y, z);
|
|
1243
|
+
if (controlsRef.current) {
|
|
1244
|
+
controlsRef.current.update();
|
|
1521
1245
|
}
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1246
|
+
},
|
|
1247
|
+
lookAt: (x, y, z) => {
|
|
1248
|
+
camera.lookAt(x, y, z);
|
|
1249
|
+
if (controlsRef.current) {
|
|
1250
|
+
controlsRef.current.target.set(x, y, z);
|
|
1251
|
+
controlsRef.current.update();
|
|
1525
1252
|
}
|
|
1253
|
+
},
|
|
1254
|
+
reset: () => {
|
|
1255
|
+
camera.position.copy(initialPosition.current);
|
|
1256
|
+
camera.lookAt(initialTarget.current.x, initialTarget.current.y, initialTarget.current.z);
|
|
1257
|
+
if (controlsRef.current) {
|
|
1258
|
+
controlsRef.current.target.copy(initialTarget.current);
|
|
1259
|
+
controlsRef.current.update();
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1262
|
+
getViewBounds: () => {
|
|
1263
|
+
const min = new THREE9.Vector3(-10, -10, -10);
|
|
1264
|
+
const max = new THREE9.Vector3(10, 10, 10);
|
|
1265
|
+
return { min, max };
|
|
1526
1266
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
},
|
|
1542
|
-
[unitAnimationEvent, emit]
|
|
1543
|
-
);
|
|
1544
|
-
const handleCameraChange = useCallback(
|
|
1545
|
-
(position) => {
|
|
1546
|
-
if (cameraChangeEvent) {
|
|
1547
|
-
emit(cameraChangeEvent, {
|
|
1548
|
-
position,
|
|
1549
|
-
timestamp: Date.now()
|
|
1550
|
-
});
|
|
1267
|
+
}));
|
|
1268
|
+
const maxPolarAngle = mode === "top-down" ? 0.1 : Math.PI / 2 - 0.1;
|
|
1269
|
+
return /* @__PURE__ */ jsx(
|
|
1270
|
+
OrbitControls,
|
|
1271
|
+
{
|
|
1272
|
+
ref: controlsRef,
|
|
1273
|
+
camera,
|
|
1274
|
+
enabled: enableOrbit,
|
|
1275
|
+
target: [initialTarget.current.x, initialTarget.current.y, initialTarget.current.z],
|
|
1276
|
+
minDistance,
|
|
1277
|
+
maxDistance,
|
|
1278
|
+
maxPolarAngle,
|
|
1279
|
+
enableDamping: true,
|
|
1280
|
+
dampingFactor: 0.05
|
|
1551
1281
|
}
|
|
1552
|
-
|
|
1553
|
-
[cameraChangeEvent, emit]
|
|
1554
|
-
);
|
|
1555
|
-
return {
|
|
1556
|
-
handleTileClick,
|
|
1557
|
-
handleUnitClick,
|
|
1558
|
-
handleFeatureClick,
|
|
1559
|
-
handleCanvasClick,
|
|
1560
|
-
handleTileHover,
|
|
1561
|
-
handleUnitAnimation,
|
|
1562
|
-
handleCameraChange
|
|
1563
|
-
};
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
// lib/drawable/hitTest.ts
|
|
1567
|
-
function collectDrawnItems(nodes) {
|
|
1568
|
-
const out = [];
|
|
1569
|
-
for (const n of nodes) {
|
|
1570
|
-
switch (n.type) {
|
|
1571
|
-
case "draw-sprite":
|
|
1572
|
-
case "draw-shape":
|
|
1573
|
-
case "draw-text":
|
|
1574
|
-
out.push({ pos: n.position, id: n.id });
|
|
1575
|
-
break;
|
|
1576
|
-
case "draw-sprite-layer":
|
|
1577
|
-
case "draw-shape-layer":
|
|
1578
|
-
case "draw-text-layer":
|
|
1579
|
-
for (const it of n.items) out.push({ pos: it.position, id: it.id });
|
|
1580
|
-
break;
|
|
1581
|
-
}
|
|
1282
|
+
);
|
|
1582
1283
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
function
|
|
1586
|
-
const
|
|
1587
|
-
|
|
1588
|
-
|
|
1284
|
+
);
|
|
1285
|
+
Camera3D.displayName = "Camera3D";
|
|
1286
|
+
function detectAssetRoot2(modelUrl) {
|
|
1287
|
+
const idx = modelUrl.indexOf("/3d/");
|
|
1288
|
+
if (idx !== -1) {
|
|
1289
|
+
return modelUrl.substring(0, idx + 4);
|
|
1589
1290
|
}
|
|
1590
|
-
return
|
|
1291
|
+
return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
|
|
1591
1292
|
}
|
|
1592
|
-
function
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
useEffect(() => {
|
|
1597
|
-
if (onCameraChange) {
|
|
1598
|
-
onCameraChange({
|
|
1599
|
-
x: camera.position.x,
|
|
1600
|
-
y: camera.position.y,
|
|
1601
|
-
z: camera.position.z
|
|
1602
|
-
});
|
|
1603
|
-
}
|
|
1604
|
-
}, [camera.position, onCameraChange]);
|
|
1605
|
-
return null;
|
|
1606
|
-
}
|
|
1607
|
-
function FollowCamera3D({
|
|
1608
|
-
target,
|
|
1609
|
-
offset
|
|
1610
|
-
}) {
|
|
1611
|
-
const { camera } = useThree();
|
|
1612
|
-
const look = useRef(new THREE6.Vector3(target[0], target[1], target[2]));
|
|
1613
|
-
const goal = useRef(new THREE6.Vector3());
|
|
1614
|
-
useFrame((_, delta) => {
|
|
1615
|
-
const t = Math.min(1, delta * 5);
|
|
1616
|
-
goal.current.set(target[0] + offset[0], target[1] + offset[1], target[2] + offset[2]);
|
|
1617
|
-
camera.position.lerp(goal.current, t);
|
|
1618
|
-
look.current.lerp(goal.current.set(target[0], target[1], target[2]), t);
|
|
1619
|
-
camera.lookAt(look.current);
|
|
1620
|
-
});
|
|
1621
|
-
return null;
|
|
1293
|
+
function createGLTFLoaderForUrl(url) {
|
|
1294
|
+
const loader = new GLTFLoader();
|
|
1295
|
+
loader.setResourcePath(detectAssetRoot2(url));
|
|
1296
|
+
return loader;
|
|
1622
1297
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1298
|
+
var AssetLoader = class {
|
|
1299
|
+
constructor() {
|
|
1300
|
+
__publicField(this, "objLoader");
|
|
1301
|
+
__publicField(this, "textureLoader");
|
|
1302
|
+
__publicField(this, "modelCache");
|
|
1303
|
+
__publicField(this, "textureCache");
|
|
1304
|
+
__publicField(this, "loadingPromises");
|
|
1305
|
+
this.objLoader = new OBJLoader();
|
|
1306
|
+
this.textureLoader = new THREE9.TextureLoader();
|
|
1307
|
+
this.modelCache = /* @__PURE__ */ new Map();
|
|
1308
|
+
this.textureCache = /* @__PURE__ */ new Map();
|
|
1309
|
+
this.loadingPromises = /* @__PURE__ */ new Map();
|
|
1633
1310
|
}
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
receiveShadow: true
|
|
1662
|
-
}
|
|
1663
|
-
) });
|
|
1311
|
+
/**
|
|
1312
|
+
* Load a GLB/GLTF model
|
|
1313
|
+
* @param url - URL to the .glb or .gltf file
|
|
1314
|
+
* @returns Promise with loaded model scene and animations
|
|
1315
|
+
*/
|
|
1316
|
+
async loadModel(url) {
|
|
1317
|
+
if (this.modelCache.has(url)) {
|
|
1318
|
+
return this.modelCache.get(url);
|
|
1319
|
+
}
|
|
1320
|
+
if (this.loadingPromises.has(url)) {
|
|
1321
|
+
return this.loadingPromises.get(url);
|
|
1322
|
+
}
|
|
1323
|
+
const loader = createGLTFLoaderForUrl(url);
|
|
1324
|
+
const loadPromise = loader.loadAsync(url).then((gltf) => {
|
|
1325
|
+
const result = {
|
|
1326
|
+
scene: gltf.scene,
|
|
1327
|
+
animations: gltf.animations || []
|
|
1328
|
+
};
|
|
1329
|
+
this.modelCache.set(url, result);
|
|
1330
|
+
this.loadingPromises.delete(url);
|
|
1331
|
+
return result;
|
|
1332
|
+
}).catch((error) => {
|
|
1333
|
+
this.loadingPromises.delete(url);
|
|
1334
|
+
throw new Error(`Failed to load model ${url}: ${error.message}`);
|
|
1335
|
+
});
|
|
1336
|
+
this.loadingPromises.set(url, loadPromise);
|
|
1337
|
+
return loadPromise;
|
|
1664
1338
|
}
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
case "cell":
|
|
1674
|
-
case "rect": {
|
|
1675
|
-
const w = node.shape === "cell" ? projector.cellSize * 0.95 : node.width ?? projector.cellSize;
|
|
1676
|
-
const h = node.shape === "cell" ? projector.cellSize * 0.95 : node.height ?? projector.cellSize;
|
|
1677
|
-
geometry = /* @__PURE__ */ jsx("planeGeometry", { args: [w, h] });
|
|
1678
|
-
break;
|
|
1339
|
+
/**
|
|
1340
|
+
* Load an OBJ model (fallback for non-GLB assets)
|
|
1341
|
+
* @param url - URL to the .obj file
|
|
1342
|
+
* @returns Promise with loaded object group
|
|
1343
|
+
*/
|
|
1344
|
+
async loadOBJ(url) {
|
|
1345
|
+
if (this.modelCache.has(url)) {
|
|
1346
|
+
return this.modelCache.get(url).scene;
|
|
1679
1347
|
}
|
|
1680
|
-
|
|
1681
|
-
const
|
|
1682
|
-
|
|
1683
|
-
break;
|
|
1348
|
+
if (this.loadingPromises.has(url)) {
|
|
1349
|
+
const result = await this.loadingPromises.get(url);
|
|
1350
|
+
return result.scene;
|
|
1684
1351
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1352
|
+
const loadPromise = this.objLoader.loadAsync(url).then((group) => {
|
|
1353
|
+
const result = {
|
|
1354
|
+
scene: group,
|
|
1355
|
+
animations: []
|
|
1356
|
+
};
|
|
1357
|
+
this.modelCache.set(url, result);
|
|
1358
|
+
this.loadingPromises.delete(url);
|
|
1359
|
+
return result;
|
|
1360
|
+
}).catch((error) => {
|
|
1361
|
+
this.loadingPromises.delete(url);
|
|
1362
|
+
throw new Error(`Failed to load OBJ ${url}: ${error.message}`);
|
|
1363
|
+
});
|
|
1364
|
+
this.loadingPromises.set(url, loadPromise);
|
|
1365
|
+
return (await loadPromise).scene;
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Load a texture
|
|
1369
|
+
* @param url - URL to the texture image
|
|
1370
|
+
* @returns Promise with loaded texture
|
|
1371
|
+
*/
|
|
1372
|
+
async loadTexture(url) {
|
|
1373
|
+
if (this.textureCache.has(url)) {
|
|
1374
|
+
return this.textureCache.get(url);
|
|
1695
1375
|
}
|
|
1696
|
-
|
|
1697
|
-
return
|
|
1376
|
+
if (this.loadingPromises.has(`texture:${url}`)) {
|
|
1377
|
+
return this.loadingPromises.get(`texture:${url}`);
|
|
1378
|
+
}
|
|
1379
|
+
const loadPromise = this.textureLoader.loadAsync(url).then((texture) => {
|
|
1380
|
+
texture.colorSpace = THREE9.SRGBColorSpace;
|
|
1381
|
+
this.textureCache.set(url, texture);
|
|
1382
|
+
this.loadingPromises.delete(`texture:${url}`);
|
|
1383
|
+
return texture;
|
|
1384
|
+
}).catch((error) => {
|
|
1385
|
+
this.loadingPromises.delete(`texture:${url}`);
|
|
1386
|
+
throw new Error(`Failed to load texture ${url}: ${error.message}`);
|
|
1387
|
+
});
|
|
1388
|
+
this.loadingPromises.set(`texture:${url}`, loadPromise);
|
|
1389
|
+
return loadPromise;
|
|
1698
1390
|
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1391
|
+
/**
|
|
1392
|
+
* Preload multiple assets
|
|
1393
|
+
* @param urls - Array of asset URLs to preload
|
|
1394
|
+
* @returns Promise that resolves when all assets are loaded
|
|
1395
|
+
*/
|
|
1396
|
+
async preload(urls) {
|
|
1397
|
+
const promises = urls.map((url) => {
|
|
1398
|
+
if (url.endsWith(".glb") || url.endsWith(".gltf")) {
|
|
1399
|
+
return this.loadModel(url).catch(() => null);
|
|
1400
|
+
} else if (url.endsWith(".obj")) {
|
|
1401
|
+
return this.loadOBJ(url).catch(() => null);
|
|
1402
|
+
} else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
|
|
1403
|
+
return this.loadTexture(url).catch(() => null);
|
|
1404
|
+
}
|
|
1405
|
+
return Promise.resolve(null);
|
|
1406
|
+
});
|
|
1407
|
+
await Promise.all(promises);
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Check if a model is cached
|
|
1411
|
+
* @param url - Model URL
|
|
1412
|
+
*/
|
|
1413
|
+
hasModel(url) {
|
|
1414
|
+
return this.modelCache.has(url);
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Check if a texture is cached
|
|
1418
|
+
* @param url - Texture URL
|
|
1419
|
+
*/
|
|
1420
|
+
hasTexture(url) {
|
|
1421
|
+
return this.textureCache.has(url);
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Get cached model (throws if not cached)
|
|
1425
|
+
* @param url - Model URL
|
|
1426
|
+
*/
|
|
1427
|
+
getModel(url) {
|
|
1428
|
+
const model = this.modelCache.get(url);
|
|
1429
|
+
if (!model) {
|
|
1430
|
+
throw new Error(`Model ${url} not in cache`);
|
|
1716
1431
|
}
|
|
1717
|
-
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1432
|
+
return model;
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Get cached texture (throws if not cached)
|
|
1436
|
+
* @param url - Texture URL
|
|
1437
|
+
*/
|
|
1438
|
+
getTexture(url) {
|
|
1439
|
+
const texture = this.textureCache.get(url);
|
|
1440
|
+
if (!texture) {
|
|
1441
|
+
throw new Error(`Texture ${url} not in cache`);
|
|
1442
|
+
}
|
|
1443
|
+
return texture;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Clear all caches
|
|
1447
|
+
*/
|
|
1448
|
+
clearCache() {
|
|
1449
|
+
this.textureCache.forEach((texture) => {
|
|
1450
|
+
texture.dispose();
|
|
1451
|
+
});
|
|
1452
|
+
this.modelCache.forEach((model) => {
|
|
1453
|
+
model.scene.traverse((child) => {
|
|
1454
|
+
if (child instanceof THREE9.Mesh) {
|
|
1455
|
+
child.geometry.dispose();
|
|
1456
|
+
if (Array.isArray(child.material)) {
|
|
1457
|
+
child.material.forEach((m) => m.dispose());
|
|
1458
|
+
} else {
|
|
1459
|
+
child.material.dispose();
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
});
|
|
1464
|
+
this.modelCache.clear();
|
|
1465
|
+
this.textureCache.clear();
|
|
1466
|
+
this.loadingPromises.clear();
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Get cache statistics
|
|
1470
|
+
*/
|
|
1471
|
+
getStats() {
|
|
1472
|
+
return {
|
|
1473
|
+
models: this.modelCache.size,
|
|
1474
|
+
textures: this.textureCache.size,
|
|
1475
|
+
loading: this.loadingPromises.size
|
|
1476
|
+
};
|
|
1733
1477
|
}
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
// lib/drawable/projector3d.ts
|
|
1737
|
-
function create3DProjector(opts = {}) {
|
|
1738
|
-
const cellSize = opts.cellSize ?? 1;
|
|
1739
|
-
const offsetX = opts.offsetX ?? 0;
|
|
1740
|
-
const offsetZ = opts.offsetZ ?? 0;
|
|
1741
|
-
return {
|
|
1742
|
-
cellSize,
|
|
1743
|
-
toWorld: (pos) => [pos.x * cellSize + offsetX, pos.z ?? 0, pos.y * cellSize + offsetZ]
|
|
1744
|
-
};
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
// components/game/three/game3dTheme.ts
|
|
1748
|
-
var GRID_COLORS_3D = {
|
|
1749
|
-
cell: "#444444",
|
|
1750
|
-
section: "#666666"
|
|
1751
1478
|
};
|
|
1752
|
-
var
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1479
|
+
var assetLoader = new AssetLoader();
|
|
1480
|
+
|
|
1481
|
+
// lib/drawable/three/hooks/useThree.ts
|
|
1482
|
+
var DEFAULT_OPTIONS = {
|
|
1483
|
+
cameraMode: "isometric",
|
|
1484
|
+
cameraPosition: [10, 10, 10],
|
|
1485
|
+
backgroundColor: "#1a1a2e",
|
|
1486
|
+
shadows: true,
|
|
1487
|
+
showGrid: true,
|
|
1488
|
+
gridSize: 20,
|
|
1489
|
+
assetLoader: new AssetLoader()
|
|
1760
1490
|
};
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
const ev = keyUpMap?.[e.code];
|
|
1806
|
-
if (ev) eventBus.emit(`UI:${ev}`, {});
|
|
1807
|
-
};
|
|
1808
|
-
window.addEventListener("keydown", down);
|
|
1809
|
-
window.addEventListener("keyup", up);
|
|
1810
|
-
return () => {
|
|
1811
|
-
window.removeEventListener("keydown", down);
|
|
1812
|
-
window.removeEventListener("keyup", up);
|
|
1813
|
-
};
|
|
1814
|
-
}, [keyMap, keyUpMap, eventBus]);
|
|
1815
|
-
const eventHandlers = useGameCanvas3DEvents({
|
|
1816
|
-
tileClickEvent,
|
|
1817
|
-
unitClickEvent,
|
|
1818
|
-
featureClickEvent,
|
|
1819
|
-
canvasClickEvent,
|
|
1820
|
-
tileHoverEvent,
|
|
1821
|
-
tileLeaveEvent,
|
|
1822
|
-
unitAnimationEvent,
|
|
1823
|
-
cameraChangeEvent
|
|
1491
|
+
function useThree4(options = {}) {
|
|
1492
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
1493
|
+
const containerRef = useRef(null);
|
|
1494
|
+
const canvasRef = useRef(null);
|
|
1495
|
+
const rendererRef = useRef(null);
|
|
1496
|
+
const sceneRef = useRef(null);
|
|
1497
|
+
const cameraRef = useRef(null);
|
|
1498
|
+
const controlsRef = useRef(null);
|
|
1499
|
+
const gridHelperRef = useRef(null);
|
|
1500
|
+
const rafRef = useRef(0);
|
|
1501
|
+
const [isReady, setIsReady] = useState(false);
|
|
1502
|
+
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
|
1503
|
+
const initialCameraPosition = useMemo(
|
|
1504
|
+
() => new THREE9.Vector3(...opts.cameraPosition),
|
|
1505
|
+
[]
|
|
1506
|
+
);
|
|
1507
|
+
useEffect(() => {
|
|
1508
|
+
if (!containerRef.current) return;
|
|
1509
|
+
const container = containerRef.current;
|
|
1510
|
+
const { clientWidth, clientHeight } = container;
|
|
1511
|
+
const scene = new THREE9.Scene();
|
|
1512
|
+
scene.background = new THREE9.Color(opts.backgroundColor);
|
|
1513
|
+
sceneRef.current = scene;
|
|
1514
|
+
let camera;
|
|
1515
|
+
const aspect = clientWidth / clientHeight;
|
|
1516
|
+
if (opts.cameraMode === "isometric") {
|
|
1517
|
+
const size = 10;
|
|
1518
|
+
camera = new THREE9.OrthographicCamera(
|
|
1519
|
+
-size * aspect,
|
|
1520
|
+
size * aspect,
|
|
1521
|
+
size,
|
|
1522
|
+
-size,
|
|
1523
|
+
0.1,
|
|
1524
|
+
1e3
|
|
1525
|
+
);
|
|
1526
|
+
} else {
|
|
1527
|
+
camera = new THREE9.PerspectiveCamera(45, aspect, 0.1, 1e3);
|
|
1528
|
+
}
|
|
1529
|
+
camera.position.copy(initialCameraPosition);
|
|
1530
|
+
cameraRef.current = camera;
|
|
1531
|
+
const renderer = new THREE9.WebGLRenderer({
|
|
1532
|
+
antialias: true,
|
|
1533
|
+
alpha: true,
|
|
1534
|
+
canvas: canvasRef.current || void 0
|
|
1824
1535
|
});
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1536
|
+
renderer.setSize(clientWidth, clientHeight);
|
|
1537
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
1538
|
+
renderer.shadowMap.enabled = opts.shadows;
|
|
1539
|
+
renderer.shadowMap.type = THREE9.PCFSoftShadowMap;
|
|
1540
|
+
rendererRef.current = renderer;
|
|
1541
|
+
const controls = new OrbitControls$1(camera, renderer.domElement);
|
|
1542
|
+
controls.enableDamping = true;
|
|
1543
|
+
controls.dampingFactor = 0.05;
|
|
1544
|
+
controls.minDistance = 2;
|
|
1545
|
+
controls.maxDistance = 100;
|
|
1546
|
+
controls.maxPolarAngle = Math.PI / 2 - 0.1;
|
|
1547
|
+
controlsRef.current = controls;
|
|
1548
|
+
const ambientLight = new THREE9.AmbientLight(16777215, 0.6);
|
|
1549
|
+
scene.add(ambientLight);
|
|
1550
|
+
const directionalLight = new THREE9.DirectionalLight(16777215, 0.8);
|
|
1551
|
+
directionalLight.position.set(10, 20, 10);
|
|
1552
|
+
directionalLight.castShadow = opts.shadows;
|
|
1553
|
+
directionalLight.shadow.mapSize.width = 2048;
|
|
1554
|
+
directionalLight.shadow.mapSize.height = 2048;
|
|
1555
|
+
scene.add(directionalLight);
|
|
1556
|
+
if (opts.showGrid) {
|
|
1557
|
+
const gridHelper = new THREE9.GridHelper(
|
|
1558
|
+
opts.gridSize,
|
|
1559
|
+
opts.gridSize,
|
|
1560
|
+
4473924,
|
|
1561
|
+
2236962
|
|
1562
|
+
);
|
|
1563
|
+
scene.add(gridHelper);
|
|
1564
|
+
gridHelperRef.current = gridHelper;
|
|
1565
|
+
}
|
|
1566
|
+
const animate = () => {
|
|
1567
|
+
rafRef.current = requestAnimationFrame(animate);
|
|
1568
|
+
controls.update();
|
|
1569
|
+
renderer.render(scene, camera);
|
|
1570
|
+
};
|
|
1571
|
+
animate();
|
|
1572
|
+
const handleResize = () => {
|
|
1573
|
+
const { clientWidth: width, clientHeight: height } = container;
|
|
1574
|
+
setDimensions({ width, height });
|
|
1575
|
+
if (camera instanceof THREE9.PerspectiveCamera) {
|
|
1576
|
+
camera.aspect = width / height;
|
|
1577
|
+
camera.updateProjectionMatrix();
|
|
1578
|
+
} else if (camera instanceof THREE9.OrthographicCamera) {
|
|
1579
|
+
const aspect2 = width / height;
|
|
1580
|
+
const size = 10;
|
|
1581
|
+
camera.left = -size * aspect2;
|
|
1582
|
+
camera.right = size * aspect2;
|
|
1583
|
+
camera.top = size;
|
|
1584
|
+
camera.bottom = -size;
|
|
1585
|
+
camera.updateProjectionMatrix();
|
|
1841
1586
|
}
|
|
1842
|
-
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
const
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
const pos = controlsRef.current.object.position;
|
|
1872
|
-
return new THREE6.Vector3(pos.x, pos.y, pos.z);
|
|
1873
|
-
}
|
|
1874
|
-
return null;
|
|
1875
|
-
},
|
|
1876
|
-
setCameraPosition: (x, y, z) => {
|
|
1877
|
-
if (controlsRef.current) {
|
|
1878
|
-
controlsRef.current.object.position.set(x, y, z);
|
|
1879
|
-
controlsRef.current.update();
|
|
1880
|
-
}
|
|
1881
|
-
},
|
|
1882
|
-
lookAt: (x, y, z) => {
|
|
1883
|
-
if (controlsRef.current) {
|
|
1884
|
-
controlsRef.current.target.set(x, y, z);
|
|
1885
|
-
controlsRef.current.update();
|
|
1886
|
-
}
|
|
1887
|
-
},
|
|
1888
|
-
resetCamera: () => {
|
|
1889
|
-
if (controlsRef.current) {
|
|
1890
|
-
controlsRef.current.reset();
|
|
1891
|
-
}
|
|
1892
|
-
},
|
|
1893
|
-
screenshot: () => {
|
|
1894
|
-
const canvas = containerRef.current?.querySelector("canvas");
|
|
1895
|
-
if (canvas) {
|
|
1896
|
-
return canvas.toDataURL("image/png");
|
|
1897
|
-
}
|
|
1898
|
-
return null;
|
|
1899
|
-
}
|
|
1900
|
-
}));
|
|
1901
|
-
const cameraConfig = useMemo(() => {
|
|
1902
|
-
const size = Math.max(
|
|
1903
|
-
gridBounds.maxX - gridBounds.minX,
|
|
1904
|
-
gridBounds.maxZ - gridBounds.minZ,
|
|
1905
|
-
4
|
|
1906
|
-
// minimum framing distance so a tiny board isn't zoomed in
|
|
1587
|
+
renderer.setSize(width, height);
|
|
1588
|
+
};
|
|
1589
|
+
window.addEventListener("resize", handleResize);
|
|
1590
|
+
handleResize();
|
|
1591
|
+
setIsReady(true);
|
|
1592
|
+
return () => {
|
|
1593
|
+
window.removeEventListener("resize", handleResize);
|
|
1594
|
+
cancelAnimationFrame(rafRef.current);
|
|
1595
|
+
controls.dispose();
|
|
1596
|
+
renderer.dispose();
|
|
1597
|
+
scene.clear();
|
|
1598
|
+
};
|
|
1599
|
+
}, []);
|
|
1600
|
+
useEffect(() => {
|
|
1601
|
+
if (!cameraRef.current || !containerRef.current) return;
|
|
1602
|
+
const container = containerRef.current;
|
|
1603
|
+
const { clientWidth, clientHeight } = container;
|
|
1604
|
+
const aspect = clientWidth / clientHeight;
|
|
1605
|
+
const currentPos = cameraRef.current.position.clone();
|
|
1606
|
+
let newCamera;
|
|
1607
|
+
if (opts.cameraMode === "isometric") {
|
|
1608
|
+
const size = 10;
|
|
1609
|
+
newCamera = new THREE9.OrthographicCamera(
|
|
1610
|
+
-size * aspect,
|
|
1611
|
+
size * aspect,
|
|
1612
|
+
size,
|
|
1613
|
+
-size,
|
|
1614
|
+
0.1,
|
|
1615
|
+
1e3
|
|
1907
1616
|
);
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
}
|
|
1922
|
-
}, [cameraMode, gridBounds]);
|
|
1923
|
-
const followWorld = useMemo(() => {
|
|
1924
|
-
if (followTarget) return drawableProjector.toWorld(followTarget);
|
|
1925
|
-
return cameraTarget;
|
|
1926
|
-
}, [followTarget, drawableProjector, cameraTarget]);
|
|
1927
|
-
const followOffset = useMemo(
|
|
1928
|
-
() => cameraMode === "chase" ? [0, 4, -7] : [5, 7, 5],
|
|
1929
|
-
[cameraMode]
|
|
1930
|
-
);
|
|
1931
|
-
const handleGroundClick = useCallback((e) => {
|
|
1932
|
-
if (!tileClickEvent && !unitClickEvent) return;
|
|
1933
|
-
e.stopPropagation();
|
|
1934
|
-
const cell = {
|
|
1935
|
-
x: Math.round(e.point.x / gridConfig.cellSize + gridBounds.minX),
|
|
1936
|
-
y: Math.round(e.point.z / gridConfig.cellSize + gridBounds.minZ)
|
|
1617
|
+
} else {
|
|
1618
|
+
newCamera = new THREE9.PerspectiveCamera(45, aspect, 0.1, 1e3);
|
|
1619
|
+
}
|
|
1620
|
+
newCamera.position.copy(currentPos);
|
|
1621
|
+
cameraRef.current = newCamera;
|
|
1622
|
+
if (controlsRef.current) {
|
|
1623
|
+
controlsRef.current.object = newCamera;
|
|
1624
|
+
}
|
|
1625
|
+
if (rendererRef.current) {
|
|
1626
|
+
const animate = () => {
|
|
1627
|
+
rafRef.current = requestAnimationFrame(animate);
|
|
1628
|
+
controlsRef.current?.update();
|
|
1629
|
+
rendererRef.current?.render(sceneRef.current, newCamera);
|
|
1937
1630
|
};
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1631
|
+
cancelAnimationFrame(rafRef.current);
|
|
1632
|
+
animate();
|
|
1633
|
+
}
|
|
1634
|
+
}, [opts.cameraMode]);
|
|
1635
|
+
const setCameraPosition = useCallback((x, y, z) => {
|
|
1636
|
+
if (cameraRef.current) {
|
|
1637
|
+
cameraRef.current.position.set(x, y, z);
|
|
1638
|
+
controlsRef.current?.update();
|
|
1639
|
+
}
|
|
1640
|
+
}, []);
|
|
1641
|
+
const lookAt = useCallback((x, y, z) => {
|
|
1642
|
+
if (cameraRef.current) {
|
|
1643
|
+
cameraRef.current.lookAt(x, y, z);
|
|
1644
|
+
controlsRef.current?.target.set(x, y, z);
|
|
1645
|
+
controlsRef.current?.update();
|
|
1646
|
+
}
|
|
1647
|
+
}, []);
|
|
1648
|
+
const resetCamera = useCallback(() => {
|
|
1649
|
+
if (cameraRef.current) {
|
|
1650
|
+
cameraRef.current.position.copy(initialCameraPosition);
|
|
1651
|
+
cameraRef.current.lookAt(0, 0, 0);
|
|
1652
|
+
if (controlsRef.current) {
|
|
1653
|
+
controlsRef.current.target.set(0, 0, 0);
|
|
1654
|
+
controlsRef.current.update();
|
|
1943
1655
|
}
|
|
1944
|
-
}, [tileClickEvent, unitClickEvent, gridConfig, gridBounds, hitIndex, eventBus]);
|
|
1945
|
-
if (externalLoading) {
|
|
1946
|
-
return /* @__PURE__ */ jsx(
|
|
1947
|
-
Canvas3DLoadingState,
|
|
1948
|
-
{
|
|
1949
|
-
progress: 1,
|
|
1950
|
-
loaded: 1,
|
|
1951
|
-
total: 1,
|
|
1952
|
-
message: loadingMessage,
|
|
1953
|
-
className
|
|
1954
|
-
}
|
|
1955
|
-
);
|
|
1956
1656
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1657
|
+
}, [initialCameraPosition]);
|
|
1658
|
+
const fitView = useCallback(
|
|
1659
|
+
(bounds) => {
|
|
1660
|
+
if (!cameraRef.current) return;
|
|
1661
|
+
const centerX = (bounds.minX + bounds.maxX) / 2;
|
|
1662
|
+
const centerZ = (bounds.minZ + bounds.maxZ) / 2;
|
|
1663
|
+
const width = bounds.maxX - bounds.minX;
|
|
1664
|
+
const depth = bounds.maxZ - bounds.minZ;
|
|
1665
|
+
const maxDim = Math.max(width, depth);
|
|
1666
|
+
const distance = maxDim * 1.5;
|
|
1667
|
+
const height = distance * 0.8;
|
|
1668
|
+
cameraRef.current.position.set(centerX + distance, height, centerZ + distance);
|
|
1669
|
+
lookAt(centerX, 0, centerZ);
|
|
1670
|
+
},
|
|
1671
|
+
[lookAt]
|
|
1672
|
+
);
|
|
1673
|
+
return {
|
|
1674
|
+
canvasRef,
|
|
1675
|
+
renderer: rendererRef.current,
|
|
1676
|
+
scene: sceneRef.current,
|
|
1677
|
+
camera: cameraRef.current,
|
|
1678
|
+
controls: controlsRef.current,
|
|
1679
|
+
isReady,
|
|
1680
|
+
dimensions,
|
|
1681
|
+
setCameraPosition,
|
|
1682
|
+
lookAt,
|
|
1683
|
+
resetCamera,
|
|
1684
|
+
fitView
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
function useAssetLoader(options = {}) {
|
|
1688
|
+
const { preloadUrls = [], loader: customLoader } = options;
|
|
1689
|
+
const loaderRef = useRef(customLoader || new AssetLoader());
|
|
1690
|
+
const [state, setState] = useState({
|
|
1691
|
+
isLoading: false,
|
|
1692
|
+
progress: 0,
|
|
1693
|
+
loaded: 0,
|
|
1694
|
+
total: 0,
|
|
1695
|
+
errors: []
|
|
1696
|
+
});
|
|
1697
|
+
useEffect(() => {
|
|
1698
|
+
if (preloadUrls.length > 0) {
|
|
1699
|
+
preload(preloadUrls);
|
|
1963
1700
|
}
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
}
|
|
2071
|
-
)
|
|
2072
|
-
]
|
|
2073
|
-
}
|
|
2074
|
-
)
|
|
1701
|
+
}, []);
|
|
1702
|
+
const updateProgress = useCallback((loaded, total) => {
|
|
1703
|
+
setState((prev) => ({
|
|
1704
|
+
...prev,
|
|
1705
|
+
loaded,
|
|
1706
|
+
total,
|
|
1707
|
+
progress: total > 0 ? Math.round(loaded / total * 100) : 0
|
|
1708
|
+
}));
|
|
1709
|
+
}, []);
|
|
1710
|
+
const loadModel = useCallback(
|
|
1711
|
+
async (url) => {
|
|
1712
|
+
setState((prev) => ({ ...prev, isLoading: true }));
|
|
1713
|
+
try {
|
|
1714
|
+
const model = await loaderRef.current.loadModel(url);
|
|
1715
|
+
setState((prev) => ({
|
|
1716
|
+
...prev,
|
|
1717
|
+
isLoading: false,
|
|
1718
|
+
loaded: prev.loaded + 1
|
|
1719
|
+
}));
|
|
1720
|
+
return model;
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1723
|
+
setState((prev) => ({
|
|
1724
|
+
...prev,
|
|
1725
|
+
isLoading: false,
|
|
1726
|
+
errors: [...prev.errors, errorMsg]
|
|
1727
|
+
}));
|
|
1728
|
+
throw error;
|
|
1729
|
+
}
|
|
1730
|
+
},
|
|
1731
|
+
[]
|
|
1732
|
+
);
|
|
1733
|
+
const loadOBJ = useCallback(
|
|
1734
|
+
async (url) => {
|
|
1735
|
+
setState((prev) => ({ ...prev, isLoading: true }));
|
|
1736
|
+
try {
|
|
1737
|
+
const model = await loaderRef.current.loadOBJ(url);
|
|
1738
|
+
setState((prev) => ({
|
|
1739
|
+
...prev,
|
|
1740
|
+
isLoading: false,
|
|
1741
|
+
loaded: prev.loaded + 1
|
|
1742
|
+
}));
|
|
1743
|
+
return model;
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1746
|
+
setState((prev) => ({
|
|
1747
|
+
...prev,
|
|
1748
|
+
isLoading: false,
|
|
1749
|
+
errors: [...prev.errors, errorMsg]
|
|
1750
|
+
}));
|
|
1751
|
+
throw error;
|
|
1752
|
+
}
|
|
1753
|
+
},
|
|
1754
|
+
[]
|
|
1755
|
+
);
|
|
1756
|
+
const loadTexture = useCallback(
|
|
1757
|
+
async (url) => {
|
|
1758
|
+
setState((prev) => ({ ...prev, isLoading: true }));
|
|
1759
|
+
try {
|
|
1760
|
+
const texture = await loaderRef.current.loadTexture(url);
|
|
1761
|
+
setState((prev) => ({
|
|
1762
|
+
...prev,
|
|
1763
|
+
isLoading: false,
|
|
1764
|
+
loaded: prev.loaded + 1
|
|
1765
|
+
}));
|
|
1766
|
+
return texture;
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1769
|
+
setState((prev) => ({
|
|
1770
|
+
...prev,
|
|
1771
|
+
isLoading: false,
|
|
1772
|
+
errors: [...prev.errors, errorMsg]
|
|
1773
|
+
}));
|
|
1774
|
+
throw error;
|
|
1775
|
+
}
|
|
1776
|
+
},
|
|
1777
|
+
[]
|
|
1778
|
+
);
|
|
1779
|
+
const preload = useCallback(
|
|
1780
|
+
async (urls) => {
|
|
1781
|
+
setState((prev) => ({
|
|
1782
|
+
...prev,
|
|
1783
|
+
isLoading: true,
|
|
1784
|
+
total: urls.length,
|
|
1785
|
+
loaded: 0,
|
|
1786
|
+
errors: []
|
|
1787
|
+
}));
|
|
1788
|
+
let completed = 0;
|
|
1789
|
+
const errors = [];
|
|
1790
|
+
await Promise.all(
|
|
1791
|
+
urls.map(async (url) => {
|
|
1792
|
+
try {
|
|
1793
|
+
if (url.endsWith(".glb") || url.endsWith(".gltf")) {
|
|
1794
|
+
await loaderRef.current.loadModel(url);
|
|
1795
|
+
} else if (url.endsWith(".obj")) {
|
|
1796
|
+
await loaderRef.current.loadOBJ(url);
|
|
1797
|
+
} else if (/\.(png|jpg|jpeg|webp)$/i.test(url)) {
|
|
1798
|
+
await loaderRef.current.loadTexture(url);
|
|
1799
|
+
}
|
|
1800
|
+
completed++;
|
|
1801
|
+
updateProgress(completed, urls.length);
|
|
1802
|
+
} catch (error) {
|
|
1803
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1804
|
+
errors.push(`${url}: ${errorMsg}`);
|
|
1805
|
+
completed++;
|
|
1806
|
+
updateProgress(completed, urls.length);
|
|
2075
1807
|
}
|
|
2076
|
-
)
|
|
1808
|
+
})
|
|
1809
|
+
);
|
|
1810
|
+
setState((prev) => ({
|
|
1811
|
+
...prev,
|
|
1812
|
+
isLoading: false,
|
|
1813
|
+
errors
|
|
1814
|
+
}));
|
|
1815
|
+
},
|
|
1816
|
+
[updateProgress]
|
|
1817
|
+
);
|
|
1818
|
+
const hasModel = useCallback((url) => {
|
|
1819
|
+
return loaderRef.current.hasModel(url);
|
|
1820
|
+
}, []);
|
|
1821
|
+
const hasTexture = useCallback((url) => {
|
|
1822
|
+
return loaderRef.current.hasTexture(url);
|
|
1823
|
+
}, []);
|
|
1824
|
+
const getModel = useCallback((url) => {
|
|
1825
|
+
try {
|
|
1826
|
+
return loaderRef.current.getModel(url);
|
|
1827
|
+
} catch {
|
|
1828
|
+
return void 0;
|
|
1829
|
+
}
|
|
1830
|
+
}, []);
|
|
1831
|
+
const getTexture = useCallback((url) => {
|
|
1832
|
+
try {
|
|
1833
|
+
return loaderRef.current.getTexture(url);
|
|
1834
|
+
} catch {
|
|
1835
|
+
return void 0;
|
|
1836
|
+
}
|
|
1837
|
+
}, []);
|
|
1838
|
+
const clearCache = useCallback(() => {
|
|
1839
|
+
loaderRef.current.clearCache();
|
|
1840
|
+
setState({
|
|
1841
|
+
isLoading: false,
|
|
1842
|
+
progress: 0,
|
|
1843
|
+
loaded: 0,
|
|
1844
|
+
total: 0,
|
|
1845
|
+
errors: []
|
|
1846
|
+
});
|
|
1847
|
+
}, []);
|
|
1848
|
+
return {
|
|
1849
|
+
...state,
|
|
1850
|
+
loadModel,
|
|
1851
|
+
loadOBJ,
|
|
1852
|
+
loadTexture,
|
|
1853
|
+
preload,
|
|
1854
|
+
hasModel,
|
|
1855
|
+
hasTexture,
|
|
1856
|
+
getModel,
|
|
1857
|
+
getTexture,
|
|
1858
|
+
clearCache
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
function useSceneGraph() {
|
|
1862
|
+
const nodesRef = useRef(/* @__PURE__ */ new Map());
|
|
1863
|
+
const addNode = useCallback((node) => {
|
|
1864
|
+
const existing = nodesRef.current.get(node.id);
|
|
1865
|
+
if (existing) {
|
|
1866
|
+
existing.mesh.removeFromParent();
|
|
1867
|
+
}
|
|
1868
|
+
nodesRef.current.set(node.id, node);
|
|
1869
|
+
}, []);
|
|
1870
|
+
const removeNode = useCallback((id) => {
|
|
1871
|
+
const node = nodesRef.current.get(id);
|
|
1872
|
+
if (node) {
|
|
1873
|
+
node.mesh.removeFromParent();
|
|
1874
|
+
nodesRef.current.delete(id);
|
|
1875
|
+
}
|
|
1876
|
+
}, []);
|
|
1877
|
+
const getNode = useCallback((id) => {
|
|
1878
|
+
return nodesRef.current.get(id);
|
|
1879
|
+
}, []);
|
|
1880
|
+
const updateNodePosition = useCallback(
|
|
1881
|
+
(id, x, y, z) => {
|
|
1882
|
+
const node = nodesRef.current.get(id);
|
|
1883
|
+
if (node) {
|
|
1884
|
+
node.mesh.position.set(x, y, z);
|
|
1885
|
+
node.position = { x, y, z };
|
|
1886
|
+
}
|
|
1887
|
+
},
|
|
1888
|
+
[]
|
|
1889
|
+
);
|
|
1890
|
+
const updateNodeGridPosition = useCallback(
|
|
1891
|
+
(id, gridX, gridZ) => {
|
|
1892
|
+
const node = nodesRef.current.get(id);
|
|
1893
|
+
if (node) {
|
|
1894
|
+
node.gridPosition = { x: gridX, z: gridZ };
|
|
1895
|
+
}
|
|
1896
|
+
},
|
|
1897
|
+
[]
|
|
1898
|
+
);
|
|
1899
|
+
const getNodeAtGrid = useCallback(
|
|
1900
|
+
(x, z, type) => {
|
|
1901
|
+
return Array.from(nodesRef.current.values()).find((node) => {
|
|
1902
|
+
const matchesGrid = node.gridPosition.x === x && node.gridPosition.z === z;
|
|
1903
|
+
return type ? matchesGrid && node.type === type : matchesGrid;
|
|
1904
|
+
});
|
|
1905
|
+
},
|
|
1906
|
+
[]
|
|
1907
|
+
);
|
|
1908
|
+
const getNodesByType = useCallback((type) => {
|
|
1909
|
+
return Array.from(nodesRef.current.values()).filter((node) => node.type === type);
|
|
1910
|
+
}, []);
|
|
1911
|
+
const getNodesInBounds = useCallback(
|
|
1912
|
+
(minX, maxX, minZ, maxZ) => {
|
|
1913
|
+
return Array.from(nodesRef.current.values()).filter((node) => {
|
|
1914
|
+
const { x, z } = node.gridPosition;
|
|
1915
|
+
return x >= minX && x <= maxX && z >= minZ && z <= maxZ;
|
|
1916
|
+
});
|
|
1917
|
+
},
|
|
1918
|
+
[]
|
|
1919
|
+
);
|
|
1920
|
+
const clearNodes = useCallback(() => {
|
|
1921
|
+
nodesRef.current.forEach((node) => {
|
|
1922
|
+
node.mesh.removeFromParent();
|
|
1923
|
+
});
|
|
1924
|
+
nodesRef.current.clear();
|
|
1925
|
+
}, []);
|
|
1926
|
+
const countNodes = useCallback((type) => {
|
|
1927
|
+
if (!type) {
|
|
1928
|
+
return nodesRef.current.size;
|
|
1929
|
+
}
|
|
1930
|
+
return Array.from(nodesRef.current.values()).filter((node) => node.type === type).length;
|
|
1931
|
+
}, []);
|
|
1932
|
+
return {
|
|
1933
|
+
nodesRef,
|
|
1934
|
+
addNode,
|
|
1935
|
+
removeNode,
|
|
1936
|
+
getNode,
|
|
1937
|
+
updateNodePosition,
|
|
1938
|
+
updateNodeGridPosition,
|
|
1939
|
+
getNodeAtGrid,
|
|
1940
|
+
getNodesByType,
|
|
1941
|
+
getNodesInBounds,
|
|
1942
|
+
clearNodes,
|
|
1943
|
+
countNodes
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
function useRaycaster(options) {
|
|
1947
|
+
const { camera, canvas, cellSize = 1, offsetX = 0, offsetZ = 0 } = options;
|
|
1948
|
+
const raycaster = useRef(new THREE9.Raycaster());
|
|
1949
|
+
const mouse = useRef(new THREE9.Vector2());
|
|
1950
|
+
const clientToNDC = useCallback(
|
|
1951
|
+
(clientX, clientY) => {
|
|
1952
|
+
if (!canvas) {
|
|
1953
|
+
return { x: 0, y: 0 };
|
|
1954
|
+
}
|
|
1955
|
+
const rect = canvas.getBoundingClientRect();
|
|
1956
|
+
return {
|
|
1957
|
+
x: (clientX - rect.left) / rect.width * 2 - 1,
|
|
1958
|
+
y: -((clientY - rect.top) / rect.height) * 2 + 1
|
|
1959
|
+
};
|
|
1960
|
+
},
|
|
1961
|
+
[canvas]
|
|
1962
|
+
);
|
|
1963
|
+
const isWithinCanvas = useCallback(
|
|
1964
|
+
(clientX, clientY) => {
|
|
1965
|
+
if (!canvas) return false;
|
|
1966
|
+
const rect = canvas.getBoundingClientRect();
|
|
1967
|
+
return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom;
|
|
1968
|
+
},
|
|
1969
|
+
[canvas]
|
|
1970
|
+
);
|
|
1971
|
+
const getIntersection = useCallback(
|
|
1972
|
+
(clientX, clientY, objects) => {
|
|
1973
|
+
if (!camera || !canvas) return null;
|
|
1974
|
+
const ndc = clientToNDC(clientX, clientY);
|
|
1975
|
+
mouse.current.set(ndc.x, ndc.y);
|
|
1976
|
+
raycaster.current.setFromCamera(mouse.current, camera);
|
|
1977
|
+
const intersects = raycaster.current.intersectObjects(objects, true);
|
|
1978
|
+
if (intersects.length > 0) {
|
|
1979
|
+
const hit = intersects[0];
|
|
1980
|
+
return {
|
|
1981
|
+
object: hit.object,
|
|
1982
|
+
point: hit.point,
|
|
1983
|
+
distance: hit.distance,
|
|
1984
|
+
uv: hit.uv,
|
|
1985
|
+
face: hit.face,
|
|
1986
|
+
faceIndex: hit.faceIndex,
|
|
1987
|
+
instanceId: hit.instanceId
|
|
1988
|
+
};
|
|
2077
1989
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
1990
|
+
return null;
|
|
1991
|
+
},
|
|
1992
|
+
[camera, canvas, clientToNDC]
|
|
1993
|
+
);
|
|
1994
|
+
const getAllIntersections = useCallback(
|
|
1995
|
+
(clientX, clientY, objects) => {
|
|
1996
|
+
if (!camera || !canvas) return [];
|
|
1997
|
+
const ndc = clientToNDC(clientX, clientY);
|
|
1998
|
+
mouse.current.set(ndc.x, ndc.y);
|
|
1999
|
+
raycaster.current.setFromCamera(mouse.current, camera);
|
|
2000
|
+
const intersects = raycaster.current.intersectObjects(objects, true);
|
|
2001
|
+
return intersects.map((hit) => ({
|
|
2002
|
+
object: hit.object,
|
|
2003
|
+
point: hit.point,
|
|
2004
|
+
distance: hit.distance,
|
|
2005
|
+
uv: hit.uv,
|
|
2006
|
+
face: hit.face,
|
|
2007
|
+
faceIndex: hit.faceIndex,
|
|
2008
|
+
instanceId: hit.instanceId
|
|
2009
|
+
}));
|
|
2010
|
+
},
|
|
2011
|
+
[camera, canvas, clientToNDC]
|
|
2012
|
+
);
|
|
2013
|
+
const getGridCoordinates = useCallback(
|
|
2014
|
+
(clientX, clientY) => {
|
|
2015
|
+
if (!camera || !canvas) return null;
|
|
2016
|
+
const ndc = clientToNDC(clientX, clientY);
|
|
2017
|
+
mouse.current.set(ndc.x, ndc.y);
|
|
2018
|
+
raycaster.current.setFromCamera(mouse.current, camera);
|
|
2019
|
+
const plane = new THREE9.Plane(new THREE9.Vector3(0, 1, 0), 0);
|
|
2020
|
+
const target = new THREE9.Vector3();
|
|
2021
|
+
const intersection = raycaster.current.ray.intersectPlane(plane, target);
|
|
2022
|
+
if (intersection) {
|
|
2023
|
+
const gridX = Math.round((target.x - offsetX) / cellSize);
|
|
2024
|
+
const gridZ = Math.round((target.z - offsetZ) / cellSize);
|
|
2025
|
+
return { x: gridX, z: gridZ };
|
|
2026
|
+
}
|
|
2027
|
+
return null;
|
|
2028
|
+
},
|
|
2029
|
+
[camera, canvas, cellSize, offsetX, offsetZ, clientToNDC]
|
|
2030
|
+
);
|
|
2031
|
+
const getTileAtPosition = useCallback(
|
|
2032
|
+
(clientX, clientY, scene) => {
|
|
2033
|
+
if (!camera || !canvas) return null;
|
|
2034
|
+
const tileMeshes = [];
|
|
2035
|
+
scene.traverse((obj) => {
|
|
2036
|
+
if (obj.userData.type === "tile" || obj.userData.isTile) {
|
|
2037
|
+
tileMeshes.push(obj);
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
const hit = getIntersection(clientX, clientY, tileMeshes);
|
|
2041
|
+
if (hit) {
|
|
2042
|
+
const gridCoords2 = getGridCoordinates(clientX, clientY);
|
|
2043
|
+
if (gridCoords2) {
|
|
2044
|
+
return {
|
|
2045
|
+
gridX: gridCoords2.x,
|
|
2046
|
+
gridZ: gridCoords2.z,
|
|
2047
|
+
worldPosition: hit.point,
|
|
2048
|
+
objectType: hit.object.userData.type || "tile",
|
|
2049
|
+
objectId: hit.object.userData.id || hit.object.userData.tileId
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
const gridCoords = getGridCoordinates(clientX, clientY);
|
|
2054
|
+
if (gridCoords) {
|
|
2055
|
+
return {
|
|
2056
|
+
gridX: gridCoords.x,
|
|
2057
|
+
gridZ: gridCoords.z,
|
|
2058
|
+
worldPosition: new THREE9.Vector3(
|
|
2059
|
+
gridCoords.x * cellSize + offsetX,
|
|
2060
|
+
0,
|
|
2061
|
+
gridCoords.z * cellSize + offsetZ
|
|
2062
|
+
)
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
return null;
|
|
2066
|
+
},
|
|
2067
|
+
[camera, canvas, getIntersection, getGridCoordinates, cellSize, offsetX, offsetZ]
|
|
2068
|
+
);
|
|
2069
|
+
return {
|
|
2070
|
+
raycaster,
|
|
2071
|
+
mouse,
|
|
2072
|
+
getIntersection,
|
|
2073
|
+
getAllIntersections,
|
|
2074
|
+
getGridCoordinates,
|
|
2075
|
+
getTileAtPosition,
|
|
2076
|
+
clientToNDC,
|
|
2077
|
+
isWithinCanvas
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2082
2080
|
var DEFAULT_CONFIG = {
|
|
2083
2081
|
cellSize: 1,
|
|
2084
2082
|
offsetX: 0,
|
|
@@ -2087,7 +2085,7 @@ var DEFAULT_CONFIG = {
|
|
|
2087
2085
|
};
|
|
2088
2086
|
function gridToWorld(gridX, gridZ, config = DEFAULT_CONFIG) {
|
|
2089
2087
|
const opts = { ...DEFAULT_CONFIG, ...config };
|
|
2090
|
-
return new
|
|
2088
|
+
return new THREE9.Vector3(
|
|
2091
2089
|
gridX * opts.cellSize + opts.offsetX,
|
|
2092
2090
|
opts.elevation,
|
|
2093
2091
|
gridZ * opts.cellSize + opts.offsetZ
|
|
@@ -2101,17 +2099,17 @@ function worldToGrid(worldX, worldZ, config = DEFAULT_CONFIG) {
|
|
|
2101
2099
|
};
|
|
2102
2100
|
}
|
|
2103
2101
|
function raycastToPlane(camera, mouseX, mouseY, planeY = 0) {
|
|
2104
|
-
const raycaster = new
|
|
2105
|
-
const mouse = new
|
|
2102
|
+
const raycaster = new THREE9.Raycaster();
|
|
2103
|
+
const mouse = new THREE9.Vector2(mouseX, mouseY);
|
|
2106
2104
|
raycaster.setFromCamera(mouse, camera);
|
|
2107
|
-
const plane = new
|
|
2108
|
-
const target = new
|
|
2105
|
+
const plane = new THREE9.Plane(new THREE9.Vector3(0, 1, 0), -planeY);
|
|
2106
|
+
const target = new THREE9.Vector3();
|
|
2109
2107
|
const intersection = raycaster.ray.intersectPlane(plane, target);
|
|
2110
2108
|
return intersection ? target : null;
|
|
2111
2109
|
}
|
|
2112
2110
|
function raycastToObjects(camera, mouseX, mouseY, objects) {
|
|
2113
|
-
const raycaster = new
|
|
2114
|
-
const mouse = new
|
|
2111
|
+
const raycaster = new THREE9.Raycaster();
|
|
2112
|
+
const mouse = new THREE9.Vector2(mouseX, mouseY);
|
|
2115
2113
|
raycaster.setFromCamera(mouse, camera);
|
|
2116
2114
|
const intersects = raycaster.intersectObjects(objects, true);
|
|
2117
2115
|
return intersects.length > 0 ? intersects[0] : null;
|
|
@@ -2139,49 +2137,230 @@ function getNeighbors(x, z, includeDiagonal = false) {
|
|
|
2139
2137
|
{ x: x - 1, z: z + 1 }
|
|
2140
2138
|
);
|
|
2141
2139
|
}
|
|
2142
|
-
return neighbors;
|
|
2143
|
-
}
|
|
2144
|
-
function isInBounds(x, z, bounds) {
|
|
2145
|
-
return x >= bounds.minX && x <= bounds.maxX && z >= bounds.minZ && z <= bounds.maxZ;
|
|
2146
|
-
}
|
|
2147
|
-
function getCellsInRadius(centerX, centerZ, radius) {
|
|
2148
|
-
const cells = [];
|
|
2149
|
-
const radiusSquared = radius * radius;
|
|
2150
|
-
const minX = Math.floor(centerX - radius);
|
|
2151
|
-
const maxX = Math.ceil(centerX + radius);
|
|
2152
|
-
const minZ = Math.floor(centerZ - radius);
|
|
2153
|
-
const maxZ = Math.ceil(centerZ + radius);
|
|
2154
|
-
for (let x = minX; x <= maxX; x++) {
|
|
2155
|
-
for (let z = minZ; z <= maxZ; z++) {
|
|
2156
|
-
const dx = x - centerX;
|
|
2157
|
-
const dz = z - centerZ;
|
|
2158
|
-
if (dx * dx + dz * dz <= radiusSquared) {
|
|
2159
|
-
cells.push({ x, z });
|
|
2140
|
+
return neighbors;
|
|
2141
|
+
}
|
|
2142
|
+
function isInBounds(x, z, bounds) {
|
|
2143
|
+
return x >= bounds.minX && x <= bounds.maxX && z >= bounds.minZ && z <= bounds.maxZ;
|
|
2144
|
+
}
|
|
2145
|
+
function getCellsInRadius(centerX, centerZ, radius) {
|
|
2146
|
+
const cells = [];
|
|
2147
|
+
const radiusSquared = radius * radius;
|
|
2148
|
+
const minX = Math.floor(centerX - radius);
|
|
2149
|
+
const maxX = Math.ceil(centerX + radius);
|
|
2150
|
+
const minZ = Math.floor(centerZ - radius);
|
|
2151
|
+
const maxZ = Math.ceil(centerZ + radius);
|
|
2152
|
+
for (let x = minX; x <= maxX; x++) {
|
|
2153
|
+
for (let z = minZ; z <= maxZ; z++) {
|
|
2154
|
+
const dx = x - centerX;
|
|
2155
|
+
const dz = z - centerZ;
|
|
2156
|
+
if (dx * dx + dz * dz <= radiusSquared) {
|
|
2157
|
+
cells.push({ x, z });
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
return cells;
|
|
2162
|
+
}
|
|
2163
|
+
function createGridHighlight(color = 16776960, opacity = 0.3) {
|
|
2164
|
+
const geometry = new THREE9.PlaneGeometry(0.95, 0.95);
|
|
2165
|
+
const material = new THREE9.MeshBasicMaterial({
|
|
2166
|
+
color,
|
|
2167
|
+
transparent: true,
|
|
2168
|
+
opacity,
|
|
2169
|
+
side: THREE9.DoubleSide
|
|
2170
|
+
});
|
|
2171
|
+
const mesh = new THREE9.Mesh(geometry, material);
|
|
2172
|
+
mesh.rotation.x = -Math.PI / 2;
|
|
2173
|
+
mesh.position.y = 0.01;
|
|
2174
|
+
return mesh;
|
|
2175
|
+
}
|
|
2176
|
+
function normalizeMouseCoordinates(clientX, clientY, element) {
|
|
2177
|
+
const rect = element.getBoundingClientRect();
|
|
2178
|
+
return {
|
|
2179
|
+
x: (clientX - rect.left) / rect.width * 2 - 1,
|
|
2180
|
+
y: -((clientY - rect.top) / rect.height) * 2 + 1
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
function isInFrustum(position, camera, padding = 0) {
|
|
2184
|
+
const frustum = new THREE9.Frustum();
|
|
2185
|
+
const projScreenMatrix = new THREE9.Matrix4();
|
|
2186
|
+
projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
2187
|
+
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
2188
|
+
const sphere = new THREE9.Sphere(position, padding);
|
|
2189
|
+
return frustum.intersectsSphere(sphere);
|
|
2190
|
+
}
|
|
2191
|
+
function filterByFrustum(positions, camera, padding = 0) {
|
|
2192
|
+
const frustum = new THREE9.Frustum();
|
|
2193
|
+
const projScreenMatrix = new THREE9.Matrix4();
|
|
2194
|
+
projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
2195
|
+
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
2196
|
+
return positions.filter((position) => {
|
|
2197
|
+
const sphere = new THREE9.Sphere(position, padding);
|
|
2198
|
+
return frustum.intersectsSphere(sphere);
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
function getVisibleIndices(positions, camera, padding = 0) {
|
|
2202
|
+
const frustum = new THREE9.Frustum();
|
|
2203
|
+
const projScreenMatrix = new THREE9.Matrix4();
|
|
2204
|
+
const visible = /* @__PURE__ */ new Set();
|
|
2205
|
+
projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
2206
|
+
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
2207
|
+
positions.forEach((position, index) => {
|
|
2208
|
+
const sphere = new THREE9.Sphere(position, padding);
|
|
2209
|
+
if (frustum.intersectsSphere(sphere)) {
|
|
2210
|
+
visible.add(index);
|
|
2211
|
+
}
|
|
2212
|
+
});
|
|
2213
|
+
return visible;
|
|
2214
|
+
}
|
|
2215
|
+
function calculateLODLevel(position, camera, lodLevels) {
|
|
2216
|
+
const distance = position.distanceTo(camera.position);
|
|
2217
|
+
for (let i = 0; i < lodLevels.length; i++) {
|
|
2218
|
+
if (distance < lodLevels[i]) {
|
|
2219
|
+
return i;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return lodLevels.length;
|
|
2223
|
+
}
|
|
2224
|
+
function updateInstanceLOD(instancedMesh, positions, camera, lodDistances) {
|
|
2225
|
+
const lodIndices = new Uint8Array(positions.length);
|
|
2226
|
+
positions.forEach((position, index) => {
|
|
2227
|
+
lodIndices[index] = calculateLODLevel(position, camera, lodDistances);
|
|
2228
|
+
});
|
|
2229
|
+
return lodIndices;
|
|
2230
|
+
}
|
|
2231
|
+
function cullInstancedMesh(instancedMesh, positions, visibleIndices) {
|
|
2232
|
+
const dummy = new THREE9.Object3D();
|
|
2233
|
+
let visibleCount = 0;
|
|
2234
|
+
positions.forEach((position, index) => {
|
|
2235
|
+
if (visibleIndices.has(index)) {
|
|
2236
|
+
dummy.position.copy(position);
|
|
2237
|
+
dummy.updateMatrix();
|
|
2238
|
+
instancedMesh.setMatrixAt(visibleCount, dummy.matrix);
|
|
2239
|
+
visibleCount++;
|
|
2240
|
+
}
|
|
2241
|
+
});
|
|
2242
|
+
instancedMesh.count = visibleCount;
|
|
2243
|
+
instancedMesh.instanceMatrix.needsUpdate = true;
|
|
2244
|
+
return visibleCount;
|
|
2245
|
+
}
|
|
2246
|
+
var SpatialHashGrid = class {
|
|
2247
|
+
constructor(cellSize = 10) {
|
|
2248
|
+
__publicField(this, "cellSize");
|
|
2249
|
+
__publicField(this, "cells");
|
|
2250
|
+
__publicField(this, "objectPositions");
|
|
2251
|
+
this.cellSize = cellSize;
|
|
2252
|
+
this.cells = /* @__PURE__ */ new Map();
|
|
2253
|
+
this.objectPositions = /* @__PURE__ */ new Map();
|
|
2254
|
+
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Get cell key for a position
|
|
2257
|
+
*/
|
|
2258
|
+
getCellKey(x, z) {
|
|
2259
|
+
const cellX = Math.floor(x / this.cellSize);
|
|
2260
|
+
const cellZ = Math.floor(z / this.cellSize);
|
|
2261
|
+
return `${cellX},${cellZ}`;
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Insert an object into the grid
|
|
2265
|
+
*/
|
|
2266
|
+
insert(id, position) {
|
|
2267
|
+
const key = this.getCellKey(position.x, position.z);
|
|
2268
|
+
if (!this.cells.has(key)) {
|
|
2269
|
+
this.cells.set(key, /* @__PURE__ */ new Set());
|
|
2270
|
+
}
|
|
2271
|
+
this.cells.get(key).add(id);
|
|
2272
|
+
this.objectPositions.set(id, position.clone());
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Remove an object from the grid
|
|
2276
|
+
*/
|
|
2277
|
+
remove(id) {
|
|
2278
|
+
const position = this.objectPositions.get(id);
|
|
2279
|
+
if (position) {
|
|
2280
|
+
const key = this.getCellKey(position.x, position.z);
|
|
2281
|
+
this.cells.get(key)?.delete(id);
|
|
2282
|
+
this.objectPositions.delete(id);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Update an object's position
|
|
2287
|
+
*/
|
|
2288
|
+
update(id, newPosition) {
|
|
2289
|
+
this.remove(id);
|
|
2290
|
+
this.insert(id, newPosition);
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* Query objects within a radius of a position
|
|
2294
|
+
*/
|
|
2295
|
+
queryRadius(center, radius) {
|
|
2296
|
+
const results = [];
|
|
2297
|
+
const radiusSquared = radius * radius;
|
|
2298
|
+
const minCellX = Math.floor((center.x - radius) / this.cellSize);
|
|
2299
|
+
const maxCellX = Math.floor((center.x + radius) / this.cellSize);
|
|
2300
|
+
const minCellZ = Math.floor((center.z - radius) / this.cellSize);
|
|
2301
|
+
const maxCellZ = Math.floor((center.z + radius) / this.cellSize);
|
|
2302
|
+
for (let x = minCellX; x <= maxCellX; x++) {
|
|
2303
|
+
for (let z = minCellZ; z <= maxCellZ; z++) {
|
|
2304
|
+
const key = `${x},${z}`;
|
|
2305
|
+
const cell = this.cells.get(key);
|
|
2306
|
+
if (cell) {
|
|
2307
|
+
cell.forEach((id) => {
|
|
2308
|
+
const position = this.objectPositions.get(id);
|
|
2309
|
+
if (position) {
|
|
2310
|
+
const dx = position.x - center.x;
|
|
2311
|
+
const dz = position.z - center.z;
|
|
2312
|
+
if (dx * dx + dz * dz <= radiusSquared) {
|
|
2313
|
+
results.push(id);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
return results;
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Query objects within a bounding box
|
|
2324
|
+
*/
|
|
2325
|
+
queryBox(minX, maxX, minZ, maxZ) {
|
|
2326
|
+
const results = [];
|
|
2327
|
+
const minCellX = Math.floor(minX / this.cellSize);
|
|
2328
|
+
const maxCellX = Math.floor(maxX / this.cellSize);
|
|
2329
|
+
const minCellZ = Math.floor(minZ / this.cellSize);
|
|
2330
|
+
const maxCellZ = Math.floor(maxZ / this.cellSize);
|
|
2331
|
+
for (let x = minCellX; x <= maxCellX; x++) {
|
|
2332
|
+
for (let z = minCellZ; z <= maxCellZ; z++) {
|
|
2333
|
+
const key = `${x},${z}`;
|
|
2334
|
+
const cell = this.cells.get(key);
|
|
2335
|
+
if (cell) {
|
|
2336
|
+
cell.forEach((id) => {
|
|
2337
|
+
const position = this.objectPositions.get(id);
|
|
2338
|
+
if (position && position.x >= minX && position.x <= maxX && position.z >= minZ && position.z <= maxZ) {
|
|
2339
|
+
results.push(id);
|
|
2340
|
+
}
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2160
2343
|
}
|
|
2161
2344
|
}
|
|
2345
|
+
return results;
|
|
2162
2346
|
}
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
}
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
return {
|
|
2181
|
-
x: (clientX - rect.left) / rect.width * 2 - 1,
|
|
2182
|
-
y: -((clientY - rect.top) / rect.height) * 2 + 1
|
|
2183
|
-
};
|
|
2184
|
-
}
|
|
2347
|
+
/**
|
|
2348
|
+
* Clear all objects from the grid
|
|
2349
|
+
*/
|
|
2350
|
+
clear() {
|
|
2351
|
+
this.cells.clear();
|
|
2352
|
+
this.objectPositions.clear();
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* Get statistics about the grid
|
|
2356
|
+
*/
|
|
2357
|
+
getStats() {
|
|
2358
|
+
return {
|
|
2359
|
+
objects: this.objectPositions.size,
|
|
2360
|
+
cells: this.cells.size
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2185
2364
|
function useTapReveal(options = {}) {
|
|
2186
2365
|
const { onReveal, onDismiss, refs, enabled = true } = options;
|
|
2187
2366
|
const [revealed, setRevealed] = useState(false);
|
|
@@ -2329,7 +2508,7 @@ var positionStyles = {
|
|
|
2329
2508
|
fixed: "fixed",
|
|
2330
2509
|
sticky: "sticky"
|
|
2331
2510
|
};
|
|
2332
|
-
var Box =
|
|
2511
|
+
var Box = React6.forwardRef(
|
|
2333
2512
|
({
|
|
2334
2513
|
padding,
|
|
2335
2514
|
paddingX,
|
|
@@ -2394,7 +2573,7 @@ var Box = React7.forwardRef(
|
|
|
2394
2573
|
onPointerDown?.(e);
|
|
2395
2574
|
}, [hoverEvent, tapReveal, triggerProps, onPointerDown]);
|
|
2396
2575
|
const isClickable = action || onClick;
|
|
2397
|
-
return
|
|
2576
|
+
return React6.createElement(
|
|
2398
2577
|
Component2,
|
|
2399
2578
|
{
|
|
2400
2579
|
ref,
|
|
@@ -2515,7 +2694,7 @@ var Typography = ({
|
|
|
2515
2694
|
}) => {
|
|
2516
2695
|
const variant = variantProp ?? (level ? `h${level}` : "body1");
|
|
2517
2696
|
const Component2 = as || defaultElements[variant];
|
|
2518
|
-
return
|
|
2697
|
+
return React6.createElement(
|
|
2519
2698
|
Component2,
|
|
2520
2699
|
{
|
|
2521
2700
|
id,
|
|
@@ -2588,7 +2767,7 @@ var Stack = ({
|
|
|
2588
2767
|
};
|
|
2589
2768
|
const isHorizontal = direction === "horizontal";
|
|
2590
2769
|
const directionClass = responsive && isHorizontal ? reverse ? "flex-col-reverse md:flex-row-reverse" : "flex-col md:flex-row" : isHorizontal ? reverse ? "flex-row-reverse" : "flex-row" : reverse ? "flex-col-reverse" : "flex-col";
|
|
2591
|
-
return
|
|
2770
|
+
return React6.createElement(
|
|
2592
2771
|
Component2,
|
|
2593
2772
|
{
|
|
2594
2773
|
className: cn(
|
|
@@ -4775,12 +4954,12 @@ function useAvl3DConfig() {
|
|
|
4775
4954
|
}
|
|
4776
4955
|
function CameraController({ targetPosition, targetLookAt, animated }) {
|
|
4777
4956
|
const { camera } = useThree();
|
|
4778
|
-
const targetPosVec = useRef(new
|
|
4779
|
-
const targetLookVec = useRef(new
|
|
4957
|
+
const targetPosVec = useRef(new THREE9.Vector3(...targetPosition));
|
|
4958
|
+
const targetLookVec = useRef(new THREE9.Vector3(...targetLookAt));
|
|
4780
4959
|
const isAnimating = useRef(false);
|
|
4781
4960
|
useEffect(() => {
|
|
4782
|
-
const newTarget = new
|
|
4783
|
-
const newLookAt = new
|
|
4961
|
+
const newTarget = new THREE9.Vector3(...targetPosition);
|
|
4962
|
+
const newLookAt = new THREE9.Vector3(...targetLookAt);
|
|
4784
4963
|
if (!newTarget.equals(targetPosVec.current) || !newLookAt.equals(targetLookVec.current)) {
|
|
4785
4964
|
targetPosVec.current.copy(newTarget);
|
|
4786
4965
|
targetLookVec.current.copy(newLookAt);
|
|
@@ -4795,9 +4974,9 @@ function CameraController({ targetPosition, targetLookAt, animated }) {
|
|
|
4795
4974
|
useFrame((_, delta) => {
|
|
4796
4975
|
if (!isAnimating.current) return;
|
|
4797
4976
|
const speed = 3;
|
|
4798
|
-
camera.position.x =
|
|
4799
|
-
camera.position.y =
|
|
4800
|
-
camera.position.z =
|
|
4977
|
+
camera.position.x = THREE9.MathUtils.damp(camera.position.x, targetPosVec.current.x, speed, delta);
|
|
4978
|
+
camera.position.y = THREE9.MathUtils.damp(camera.position.y, targetPosVec.current.y, speed, delta);
|
|
4979
|
+
camera.position.z = THREE9.MathUtils.damp(camera.position.z, targetPosVec.current.z, speed, delta);
|
|
4801
4980
|
camera.lookAt(targetLookVec.current);
|
|
4802
4981
|
const dist = camera.position.distanceTo(targetPosVec.current);
|
|
4803
4982
|
if (dist < 0.05) {
|
|
@@ -4812,7 +4991,7 @@ function SceneFade({ animating, children }) {
|
|
|
4812
4991
|
useFrame((_, delta) => {
|
|
4813
4992
|
if (!groupRef.current) return;
|
|
4814
4993
|
const target = animating ? 0 : 1;
|
|
4815
|
-
opacityRef.current =
|
|
4994
|
+
opacityRef.current = THREE9.MathUtils.damp(opacityRef.current, target, 5, delta);
|
|
4816
4995
|
groupRef.current.visible = opacityRef.current > 0.05;
|
|
4817
4996
|
const s = 0.9 + opacityRef.current * 0.1;
|
|
4818
4997
|
groupRef.current.scale.setScalar(s);
|
|
@@ -4883,7 +5062,7 @@ var Avl3DViewer = ({
|
|
|
4883
5062
|
const handleTraitClick = useCallback((name) => {
|
|
4884
5063
|
dispatch({ type: "ZOOM_INTO_TRAIT", trait: name, targetPosition: { x: 0, y: 0 } });
|
|
4885
5064
|
}, []);
|
|
4886
|
-
const [highlightedTrait, setHighlightedTrait] =
|
|
5065
|
+
const [highlightedTrait, setHighlightedTrait] = React6.useState(null);
|
|
4887
5066
|
const handleTransitionClick = useCallback((index) => {
|
|
4888
5067
|
dispatch({ type: "ZOOM_INTO_TRANSITION", transitionIndex: index, targetPosition: { x: 0, y: 0 } });
|
|
4889
5068
|
}, []);
|
|
@@ -4970,7 +5149,7 @@ var Avl3DViewer = ({
|
|
|
4970
5149
|
gap: "xs",
|
|
4971
5150
|
align: "center",
|
|
4972
5151
|
className: "absolute top-2 left-2 z-10 bg-surface/80 backdrop-blur rounded-md px-3 py-1.5",
|
|
4973
|
-
children: breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxs(
|
|
5152
|
+
children: breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxs(React6.Fragment, { children: [
|
|
4974
5153
|
i > 0 && /* @__PURE__ */ jsx(Typography, { variant: "small", color: "muted", className: "mx-1", children: "/" }),
|
|
4975
5154
|
i < breadcrumbs.length - 1 ? /* @__PURE__ */ jsx(
|
|
4976
5155
|
Box,
|
|
@@ -5064,186 +5243,5 @@ var Avl3DViewer = ({
|
|
|
5064
5243
|
);
|
|
5065
5244
|
};
|
|
5066
5245
|
Avl3DViewer.displayName = "Avl3DViewer";
|
|
5067
|
-
function isInFrustum(position, camera, padding = 0) {
|
|
5068
|
-
const frustum = new THREE6.Frustum();
|
|
5069
|
-
const projScreenMatrix = new THREE6.Matrix4();
|
|
5070
|
-
projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
5071
|
-
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
5072
|
-
const sphere = new THREE6.Sphere(position, padding);
|
|
5073
|
-
return frustum.intersectsSphere(sphere);
|
|
5074
|
-
}
|
|
5075
|
-
function filterByFrustum(positions, camera, padding = 0) {
|
|
5076
|
-
const frustum = new THREE6.Frustum();
|
|
5077
|
-
const projScreenMatrix = new THREE6.Matrix4();
|
|
5078
|
-
projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
5079
|
-
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
5080
|
-
return positions.filter((position) => {
|
|
5081
|
-
const sphere = new THREE6.Sphere(position, padding);
|
|
5082
|
-
return frustum.intersectsSphere(sphere);
|
|
5083
|
-
});
|
|
5084
|
-
}
|
|
5085
|
-
function getVisibleIndices(positions, camera, padding = 0) {
|
|
5086
|
-
const frustum = new THREE6.Frustum();
|
|
5087
|
-
const projScreenMatrix = new THREE6.Matrix4();
|
|
5088
|
-
const visible = /* @__PURE__ */ new Set();
|
|
5089
|
-
projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
5090
|
-
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
5091
|
-
positions.forEach((position, index) => {
|
|
5092
|
-
const sphere = new THREE6.Sphere(position, padding);
|
|
5093
|
-
if (frustum.intersectsSphere(sphere)) {
|
|
5094
|
-
visible.add(index);
|
|
5095
|
-
}
|
|
5096
|
-
});
|
|
5097
|
-
return visible;
|
|
5098
|
-
}
|
|
5099
|
-
function calculateLODLevel(position, camera, lodLevels) {
|
|
5100
|
-
const distance = position.distanceTo(camera.position);
|
|
5101
|
-
for (let i = 0; i < lodLevels.length; i++) {
|
|
5102
|
-
if (distance < lodLevels[i]) {
|
|
5103
|
-
return i;
|
|
5104
|
-
}
|
|
5105
|
-
}
|
|
5106
|
-
return lodLevels.length;
|
|
5107
|
-
}
|
|
5108
|
-
function updateInstanceLOD(instancedMesh, positions, camera, lodDistances) {
|
|
5109
|
-
const lodIndices = new Uint8Array(positions.length);
|
|
5110
|
-
positions.forEach((position, index) => {
|
|
5111
|
-
lodIndices[index] = calculateLODLevel(position, camera, lodDistances);
|
|
5112
|
-
});
|
|
5113
|
-
return lodIndices;
|
|
5114
|
-
}
|
|
5115
|
-
function cullInstancedMesh(instancedMesh, positions, visibleIndices) {
|
|
5116
|
-
const dummy = new THREE6.Object3D();
|
|
5117
|
-
let visibleCount = 0;
|
|
5118
|
-
positions.forEach((position, index) => {
|
|
5119
|
-
if (visibleIndices.has(index)) {
|
|
5120
|
-
dummy.position.copy(position);
|
|
5121
|
-
dummy.updateMatrix();
|
|
5122
|
-
instancedMesh.setMatrixAt(visibleCount, dummy.matrix);
|
|
5123
|
-
visibleCount++;
|
|
5124
|
-
}
|
|
5125
|
-
});
|
|
5126
|
-
instancedMesh.count = visibleCount;
|
|
5127
|
-
instancedMesh.instanceMatrix.needsUpdate = true;
|
|
5128
|
-
return visibleCount;
|
|
5129
|
-
}
|
|
5130
|
-
var SpatialHashGrid = class {
|
|
5131
|
-
constructor(cellSize = 10) {
|
|
5132
|
-
__publicField(this, "cellSize");
|
|
5133
|
-
__publicField(this, "cells");
|
|
5134
|
-
__publicField(this, "objectPositions");
|
|
5135
|
-
this.cellSize = cellSize;
|
|
5136
|
-
this.cells = /* @__PURE__ */ new Map();
|
|
5137
|
-
this.objectPositions = /* @__PURE__ */ new Map();
|
|
5138
|
-
}
|
|
5139
|
-
/**
|
|
5140
|
-
* Get cell key for a position
|
|
5141
|
-
*/
|
|
5142
|
-
getCellKey(x, z) {
|
|
5143
|
-
const cellX = Math.floor(x / this.cellSize);
|
|
5144
|
-
const cellZ = Math.floor(z / this.cellSize);
|
|
5145
|
-
return `${cellX},${cellZ}`;
|
|
5146
|
-
}
|
|
5147
|
-
/**
|
|
5148
|
-
* Insert an object into the grid
|
|
5149
|
-
*/
|
|
5150
|
-
insert(id, position) {
|
|
5151
|
-
const key = this.getCellKey(position.x, position.z);
|
|
5152
|
-
if (!this.cells.has(key)) {
|
|
5153
|
-
this.cells.set(key, /* @__PURE__ */ new Set());
|
|
5154
|
-
}
|
|
5155
|
-
this.cells.get(key).add(id);
|
|
5156
|
-
this.objectPositions.set(id, position.clone());
|
|
5157
|
-
}
|
|
5158
|
-
/**
|
|
5159
|
-
* Remove an object from the grid
|
|
5160
|
-
*/
|
|
5161
|
-
remove(id) {
|
|
5162
|
-
const position = this.objectPositions.get(id);
|
|
5163
|
-
if (position) {
|
|
5164
|
-
const key = this.getCellKey(position.x, position.z);
|
|
5165
|
-
this.cells.get(key)?.delete(id);
|
|
5166
|
-
this.objectPositions.delete(id);
|
|
5167
|
-
}
|
|
5168
|
-
}
|
|
5169
|
-
/**
|
|
5170
|
-
* Update an object's position
|
|
5171
|
-
*/
|
|
5172
|
-
update(id, newPosition) {
|
|
5173
|
-
this.remove(id);
|
|
5174
|
-
this.insert(id, newPosition);
|
|
5175
|
-
}
|
|
5176
|
-
/**
|
|
5177
|
-
* Query objects within a radius of a position
|
|
5178
|
-
*/
|
|
5179
|
-
queryRadius(center, radius) {
|
|
5180
|
-
const results = [];
|
|
5181
|
-
const radiusSquared = radius * radius;
|
|
5182
|
-
const minCellX = Math.floor((center.x - radius) / this.cellSize);
|
|
5183
|
-
const maxCellX = Math.floor((center.x + radius) / this.cellSize);
|
|
5184
|
-
const minCellZ = Math.floor((center.z - radius) / this.cellSize);
|
|
5185
|
-
const maxCellZ = Math.floor((center.z + radius) / this.cellSize);
|
|
5186
|
-
for (let x = minCellX; x <= maxCellX; x++) {
|
|
5187
|
-
for (let z = minCellZ; z <= maxCellZ; z++) {
|
|
5188
|
-
const key = `${x},${z}`;
|
|
5189
|
-
const cell = this.cells.get(key);
|
|
5190
|
-
if (cell) {
|
|
5191
|
-
cell.forEach((id) => {
|
|
5192
|
-
const position = this.objectPositions.get(id);
|
|
5193
|
-
if (position) {
|
|
5194
|
-
const dx = position.x - center.x;
|
|
5195
|
-
const dz = position.z - center.z;
|
|
5196
|
-
if (dx * dx + dz * dz <= radiusSquared) {
|
|
5197
|
-
results.push(id);
|
|
5198
|
-
}
|
|
5199
|
-
}
|
|
5200
|
-
});
|
|
5201
|
-
}
|
|
5202
|
-
}
|
|
5203
|
-
}
|
|
5204
|
-
return results;
|
|
5205
|
-
}
|
|
5206
|
-
/**
|
|
5207
|
-
* Query objects within a bounding box
|
|
5208
|
-
*/
|
|
5209
|
-
queryBox(minX, maxX, minZ, maxZ) {
|
|
5210
|
-
const results = [];
|
|
5211
|
-
const minCellX = Math.floor(minX / this.cellSize);
|
|
5212
|
-
const maxCellX = Math.floor(maxX / this.cellSize);
|
|
5213
|
-
const minCellZ = Math.floor(minZ / this.cellSize);
|
|
5214
|
-
const maxCellZ = Math.floor(maxZ / this.cellSize);
|
|
5215
|
-
for (let x = minCellX; x <= maxCellX; x++) {
|
|
5216
|
-
for (let z = minCellZ; z <= maxCellZ; z++) {
|
|
5217
|
-
const key = `${x},${z}`;
|
|
5218
|
-
const cell = this.cells.get(key);
|
|
5219
|
-
if (cell) {
|
|
5220
|
-
cell.forEach((id) => {
|
|
5221
|
-
const position = this.objectPositions.get(id);
|
|
5222
|
-
if (position && position.x >= minX && position.x <= maxX && position.z >= minZ && position.z <= maxZ) {
|
|
5223
|
-
results.push(id);
|
|
5224
|
-
}
|
|
5225
|
-
});
|
|
5226
|
-
}
|
|
5227
|
-
}
|
|
5228
|
-
}
|
|
5229
|
-
return results;
|
|
5230
|
-
}
|
|
5231
|
-
/**
|
|
5232
|
-
* Clear all objects from the grid
|
|
5233
|
-
*/
|
|
5234
|
-
clear() {
|
|
5235
|
-
this.cells.clear();
|
|
5236
|
-
this.objectPositions.clear();
|
|
5237
|
-
}
|
|
5238
|
-
/**
|
|
5239
|
-
* Get statistics about the grid
|
|
5240
|
-
*/
|
|
5241
|
-
getStats() {
|
|
5242
|
-
return {
|
|
5243
|
-
objects: this.objectPositions.size,
|
|
5244
|
-
cells: this.cells.size
|
|
5245
|
-
};
|
|
5246
|
-
}
|
|
5247
|
-
};
|
|
5248
5246
|
|
|
5249
|
-
export { AVL_3D_COLORS, AssetLoader, Avl3DApplicationScene, Avl3DContext, Avl3DEffects, Avl3DOrbitalScene, Avl3DTraitScene, Avl3DTransitionScene, Avl3DViewer, CAMERA_POSITIONS, Camera3D, Canvas3DErrorBoundary, Canvas3DLoadingState, GameCanvas3D, Lighting3D, ModelLoader, Scene3D, SpatialHashGrid, arcCurve3D, assetLoader, calculateLODLevel, createGridHighlight, cullInstancedMesh, fibonacciSpherePositions, filterByFrustum, getCellsInRadius, getNeighbors, getVisibleIndices, goldenSpiralPositions, gridDistance, gridManhattanDistance, gridToWorld, isInBounds, isInFrustum, normalizeMouseCoordinates, orbitRingPositions, raycastToObjects, raycastToPlane, selfLoopCurve3D, treeLayout3D, updateInstanceLOD, useAssetLoader, useAvl3DConfig, useGameCanvas3DEvents, useRaycaster, useSceneGraph,
|
|
5247
|
+
export { AVL_3D_COLORS, AssetLoader, Avl3DApplicationScene, Avl3DContext, Avl3DEffects, Avl3DOrbitalScene, Avl3DTraitScene, Avl3DTransitionScene, Avl3DViewer, CAMERA_POSITIONS, Camera3D, Canvas3DErrorBoundary, Canvas3DHost, Canvas3DLoadingState, Canvas3DHost as GameCanvas3D, Lighting3D, ModelLoader, Scene3D, SpatialHashGrid, arcCurve3D, assetLoader, calculateLODLevel, createGridHighlight, cullInstancedMesh, fibonacciSpherePositions, filterByFrustum, getCellsInRadius, getNeighbors, getVisibleIndices, goldenSpiralPositions, gridDistance, gridManhattanDistance, gridToWorld, isInBounds, isInFrustum, normalizeMouseCoordinates, orbitRingPositions, raycastToObjects, raycastToPlane, selfLoopCurve3D, treeLayout3D, updateInstanceLOD, useAssetLoader, useAvl3DConfig, useGameCanvas3DEvents, useRaycaster, useSceneGraph, useThree4 as useThree, worldToGrid };
|