@plasius/renderer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/CODE_OF_CONDUCT.md +79 -0
  3. package/CONTRIBUTORS.md +27 -0
  4. package/LICENSE +203 -0
  5. package/README.md +70 -0
  6. package/SECURITY.md +17 -0
  7. package/dist/adaptivedpr.d.ts +2 -0
  8. package/dist/adaptivedpr.d.ts.map +1 -0
  9. package/dist/adaptivedpr.js +65 -0
  10. package/dist/camera/cameraRigProfile.d.ts +12 -0
  11. package/dist/camera/cameraRigProfile.d.ts.map +1 -0
  12. package/dist/camera/cameraRigProfile.js +18 -0
  13. package/dist/camera/managedCameraController.d.ts +49 -0
  14. package/dist/camera/managedCameraController.d.ts.map +1 -0
  15. package/dist/camera/managedCameraController.js +271 -0
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +3 -0
  19. package/dist/landscape.d.ts +2 -0
  20. package/dist/landscape.d.ts.map +1 -0
  21. package/dist/landscape.js +120 -0
  22. package/dist/player/player.d.ts +8 -0
  23. package/dist/player/player.d.ts.map +1 -0
  24. package/dist/player/player.js +203 -0
  25. package/dist/player/playerstore.d.ts +205 -0
  26. package/dist/player/playerstore.d.ts.map +1 -0
  27. package/dist/player/playerstore.js +500 -0
  28. package/dist/renderStateProvider.d.ts +57 -0
  29. package/dist/renderStateProvider.d.ts.map +1 -0
  30. package/dist/renderStateProvider.js +50 -0
  31. package/dist/renderer.d.ts +9 -0
  32. package/dist/renderer.d.ts.map +1 -0
  33. package/dist/renderer.js +165 -0
  34. package/dist/scene.d.ts +7 -0
  35. package/dist/scene.d.ts.map +1 -0
  36. package/dist/scene.js +10 -0
  37. package/dist/shaders/fragment/landscapeFragmentShader.js +141 -0
  38. package/dist/shaders/landscapeShader.d.ts +13 -0
  39. package/dist/shaders/landscapeShader.d.ts.map +1 -0
  40. package/dist/shaders/landscapeShader.js +25 -0
  41. package/dist/shaders/vertex/landscapeVertexShader.js +67 -0
  42. package/dist/styles/renderer.module.css +90 -0
  43. package/dist/worldSpaceCompositor.d.ts +50 -0
  44. package/dist/worldSpaceCompositor.d.ts.map +1 -0
  45. package/dist/worldSpaceCompositor.js +159 -0
  46. package/dist/xr/rendererXrBridge.d.ts +12 -0
  47. package/dist/xr/rendererXrBridge.d.ts.map +1 -0
  48. package/dist/xr/rendererXrBridge.js +17 -0
  49. package/docs/adrs/adr-0001-renderer-package-scope.md +21 -0
  50. package/docs/adrs/adr-0002-public-repo-governance.md +24 -0
  51. package/docs/adrs/adr-0003-world-space-compositor-contracts.md +34 -0
  52. package/docs/adrs/adr-template.md +35 -0
  53. package/docs/design/0001-public-package-scope.md +18 -0
  54. package/docs/tdrs/index.md +3 -0
  55. package/docs/tdrs/tdr-0001-renderer-public-package-standards-alignment.md +19 -0
  56. package/legal/CLA-REGISTRY.csv +1 -0
  57. package/legal/CLA.md +22 -0
  58. package/legal/CORPORATE_CLA.md +57 -0
  59. package/legal/INDIVIDUAL_CLA.md +91 -0
  60. package/package.json +117 -0
  61. package/src/adaptivedpr.tsx +74 -0
  62. package/src/camera/cameraRigProfile.ts +29 -0
  63. package/src/camera/managedCameraController.tsx +401 -0
  64. package/src/global.d.ts +10 -0
  65. package/src/index.ts +3 -0
  66. package/src/landscape.tsx +321 -0
  67. package/src/player/player.tsx +257 -0
  68. package/src/player/playerstore.tsx +733 -0
  69. package/src/renderStateProvider.tsx +121 -0
  70. package/src/renderer.tsx +294 -0
  71. package/src/scene.tsx +42 -0
  72. package/src/shaders/fragment/landscapeFragmentShader.d.ts +4 -0
  73. package/src/shaders/fragment/landscapeFragmentShader.js +141 -0
  74. package/src/shaders/landscapeShader.tsx +39 -0
  75. package/src/shaders/vertex/landscapeVertexShader.d.ts +4 -0
  76. package/src/shaders/vertex/landscapeVertexShader.js +67 -0
  77. package/src/styles/renderer.module.css +90 -0
  78. package/src/worldSpaceCompositor.ts +265 -0
  79. package/src/xr/rendererXrBridge.ts +44 -0
@@ -0,0 +1,321 @@
1
+ import * as THREE from "three";
2
+ import { useMemo } from "react";
3
+ import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js";
4
+ import { RigidBody } from "@react-three/rapier";
5
+
6
+ const HEX_RADIUS = 66;
7
+ //const TILE_RESOLUTION = 2048;
8
+ const AREA_WIDTH = 2000;
9
+ const AREA_HEIGHT = 2000;
10
+
11
+ // Generate a single high-resolution hexagon as BufferGeometry
12
+ function generateHexTile(radius: number): THREE.BufferGeometry {
13
+ const geom = new THREE.BufferGeometry();
14
+ const vertices: number[] = [];
15
+ const indices: number[] = [];
16
+
17
+ const segments = 30; // More segments for smoother curve
18
+ const angleStep = (Math.PI * 2) / segments;
19
+
20
+ // Center point
21
+ vertices.push(0, 0, 0);
22
+
23
+ // Outer ring vertices
24
+ for (let i = 0; i <= segments; i++) {
25
+ const angle = angleStep * i;
26
+ const x = radius * Math.cos(angle);
27
+ const z = -radius * Math.sin(angle); // flip Z axis for pointy top upward
28
+ vertices.push(x, 0, z);
29
+ }
30
+
31
+ // Create triangle fan indices
32
+ for (let i = 1; i <= segments; i++) {
33
+ indices.push(0, i, i + 1);
34
+ }
35
+
36
+ geom.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
37
+ geom.setIndex(indices);
38
+ geom.computeVertexNormals();
39
+ return geom;
40
+ }
41
+
42
+ // Axial to world coordinates (pointy-topped hex)
43
+ function axialToWorld(q: number, r: number, radius: number): [number, number] {
44
+ const x = ((radius * 3) / 2) * q;
45
+ const z = radius * Math.sqrt(3) * (r + q / 2);
46
+ return [x, z];
47
+ }
48
+
49
+ // Simple seeded noise for per-tile deformation
50
+ function seededNoise(x: number, z: number, seed = 42): number {
51
+ const s = Math.sin(x * 12.9898 + z * 78.233 + seed) * 43758.5453;
52
+ return (s - Math.floor(s)) * 2 - 1;
53
+ }
54
+
55
+ // Deform only the interior (non-edge) vertices of a hex tile
56
+ function deformHexInterior(
57
+ geom: THREE.BufferGeometry,
58
+ radius: number,
59
+ seed = 42
60
+ ): void {
61
+ const pos = geom.attributes.position as THREE.BufferAttribute;
62
+ const vertexCount = pos.count;
63
+
64
+ for (let i = 0; i < vertexCount; i++) {
65
+ const x = pos.getX(i);
66
+ const z = pos.getZ(i);
67
+ const r = Math.sqrt(x * x + z * z);
68
+
69
+ // Only deform vertices inside the hex edge (leave edge vertices flat)
70
+ if (r < radius * 0.98) {
71
+ // Blend multiple nearby noise samples for smoother curvature
72
+ const n = (dx: number, dz: number) => seededNoise(x + dx, z + dz, seed);
73
+ const smoothY =
74
+ (n(0, 0) * 4 +
75
+ n(-1, 0) +
76
+ n(1, 0) +
77
+ n(0, -1) +
78
+ n(0, 1) +
79
+ n(-1, -1) +
80
+ n(1, 1) +
81
+ n(-1, 1) +
82
+ n(1, -1)) /
83
+ 12;
84
+ pos.setY(i, smoothY);
85
+ }
86
+ }
87
+
88
+ pos.needsUpdate = true;
89
+ geom.computeVertexNormals();
90
+ }
91
+
92
+ function generateHexDepthMap(
93
+ cols: number,
94
+ rows: number,
95
+ seed = 42
96
+ ): number[][] {
97
+ const map: number[][] = [];
98
+ for (let q = 0; q < cols; q++) {
99
+ const row: number[] = [];
100
+ for (let r = 0; r < rows; r++) {
101
+ const value = Math.sin((q + seed) * 0.3) * Math.cos((r + seed) * 0.3);
102
+ row.push(value);
103
+ }
104
+ map.push(row);
105
+ }
106
+ return map;
107
+ }
108
+
109
+ export function Landscape() {
110
+ const geometry = useMemo(() => {
111
+ const radius = HEX_RADIUS;
112
+ const tiles: THREE.BufferGeometry[] = [];
113
+
114
+ const spacingX = (3 / 2) * radius;
115
+ const spacingZ = Math.sqrt(3) * radius;
116
+ const cols = Math.ceil(AREA_WIDTH / spacingX);
117
+ const rows = Math.ceil(AREA_HEIGHT / spacingZ);
118
+
119
+ const offset = Math.floor(cols / 2);
120
+ const depthMap = generateHexDepthMap(cols, rows);
121
+ const elevationScale = 20;
122
+
123
+ const baseHex = generateHexTile(radius);
124
+
125
+ for (let q = -Math.floor(cols / 2); q < Math.ceil(cols / 2); q++) {
126
+ for (let r = -Math.floor(rows / 2); r < Math.ceil(rows / 2); r++) {
127
+ const hex = baseHex.clone();
128
+
129
+ const dq = q + offset;
130
+ const dr = r + offset;
131
+ const elevation = depthMap?.[dq]?.[dr] ?? 0;
132
+
133
+ deformHexInterior(
134
+ hex,
135
+ radius,
136
+ 42 + q * 1000 + r + Math.floor(elevation * 100)
137
+ );
138
+
139
+ const [x, z] = axialToWorld(q, r, radius);
140
+ hex.translate(x, elevation * elevationScale, z);
141
+
142
+ tiles.push(hex);
143
+ }
144
+ }
145
+
146
+ const merged = BufferGeometryUtils.mergeGeometries(tiles, false);
147
+ // Remove normals before merging vertices to ensure proper welding
148
+ if (merged.attributes.normal) {
149
+ merged.deleteAttribute("normal");
150
+ }
151
+ const stitched = BufferGeometryUtils.mergeVertices(merged, 1e-2);
152
+ stitched.computeVertexNormals();
153
+ return stitched;
154
+ }, []);
155
+
156
+ return (
157
+ <RigidBody type="fixed" colliders="trimesh">
158
+ <mesh geometry={geometry} castShadow receiveShadow>
159
+ <landscapeShaderMaterial color="#77aa66" />
160
+ </mesh>
161
+ </RigidBody>
162
+ );
163
+ }
164
+
165
+ /*
166
+
167
+ import * as THREE from "three";
168
+ import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js";
169
+ import { useFrame } from "@react-three/fiber";
170
+ import { useRef, useMemo } from "react";
171
+ import { Mesh } from "three";
172
+
173
+ import "./shaders/landscapeShader.js";
174
+
175
+ // Deterministic, coherent angle function for seamless tile rotation
176
+ function coherentAngle(x: number, y: number, scale: number = 0.2) {
177
+ const seed = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
178
+ const noise = seed - Math.floor(seed);
179
+ return (noise - 0.5) * scale;
180
+ }
181
+
182
+ function generateHexTile(
183
+ edgeLength: number,
184
+ resolution: number
185
+ ): THREE.BufferGeometry {
186
+ const radius = edgeLength;
187
+ const height = Math.sqrt(3) * radius;
188
+ const width = 2 * radius;
189
+
190
+ const geom = new THREE.PlaneGeometry(width, height, resolution, resolution);
191
+
192
+ // Mask out corners beyond hexagon bounds
193
+ const pos = geom.attributes.position;
194
+ const indicesToKeep = [];
195
+
196
+ for (let i = 0; i < pos.count; i++) {
197
+ const x = pos.getX(i);
198
+ const y = pos.getY(i); // plane lies flat, so this is along Z in world
199
+ const px = Math.abs(x);
200
+ const py = Math.abs(y);
201
+ const angle = Math.atan2(py, px);
202
+
203
+ const maxX = radius;
204
+ const maxY = Math.tan(Math.PI / 6) * (maxX - px);
205
+ if (py < height / 2 && py < maxY + 1e-2) {
206
+ indicesToKeep.push(i);
207
+ }
208
+ }
209
+
210
+ // Build new geometry with only the allowed vertices
211
+ const indexAttr = [];
212
+ for (let i = 0; i < geom.index!.count; i += 3) {
213
+ const a = geom.index!.getX(i);
214
+ const b = geom.index!.getX(i + 1);
215
+ const c = geom.index!.getX(i + 2);
216
+ if (
217
+ indicesToKeep.includes(a) &&
218
+ indicesToKeep.includes(b) &&
219
+ indicesToKeep.includes(c)
220
+ ) {
221
+ indexAttr.push(a, b, c);
222
+ }
223
+ }
224
+
225
+ const trimmed = geom.clone();
226
+ trimmed.setIndex(indexAttr);
227
+ trimmed.computeVertexNormals();
228
+ return trimmed;
229
+ }
230
+
231
+ let persistedGeometry: THREE.BufferGeometry | undefined = undefined;
232
+
233
+ interface LandscapeProps {
234
+ width?: number;
235
+ height?: number;
236
+ resolution?: number;
237
+ children?: React.ReactNode;
238
+ }
239
+
240
+ export function Landscape({
241
+ width = 100,
242
+ height = 100,
243
+ resolution = 64,
244
+ children,
245
+ }: LandscapeProps) {
246
+ const ref = useRef<Mesh>(null);
247
+ const materialRef = useRef<any>(null);
248
+
249
+ useFrame(({ clock }) => {
250
+ if (materialRef.current) {
251
+ materialRef.current.uniforms.time.value = clock.getElapsedTime();
252
+ }
253
+ });
254
+
255
+ const radius = 6.6;
256
+ const hexWidth = 2 * radius;
257
+ const hexHeight = Math.sqrt(3) * radius;
258
+
259
+ const spacingX = (3 / 2) * radius;
260
+ const spacingZ = hexHeight;
261
+
262
+ const cols = Math.ceil(width / spacingX);
263
+ const rows = Math.ceil(height / spacingZ);
264
+
265
+ const geometry = useMemo(() => {
266
+ if (persistedGeometry !== undefined) return persistedGeometry;
267
+
268
+ const geometries: THREE.BufferGeometry[] = [];
269
+
270
+ for (let x = 0; x < cols; x++) {
271
+ for (let y = 0; y < rows; y++) {
272
+ const overlap = 0.1;
273
+ // Hex grid layout: pointy-top
274
+ const hexHeight = Math.sqrt(3) * radius;
275
+ const q = x;
276
+ const r = y;
277
+ const posX = radius * 1.5 * q;
278
+ const posZ = Math.sqrt(3) * radius * (r + q / 2);
279
+
280
+ const dx = x - cols / 2 + 0.5;
281
+ const dy = y - rows / 2 + 0.5;
282
+ const distance = Math.sqrt(dx * dx + dy * dy);
283
+ const angleFactor = distance / Math.max(cols, rows);
284
+
285
+ const rotX = coherentAngle(x, y) * angleFactor;
286
+ const rotY = coherentAngle(y, x) * angleFactor;
287
+
288
+ const baseGeom = generateHexTile(radius, resolution);
289
+ baseGeom.rotateX(-Math.PI / 2); // flat base
290
+ baseGeom.rotateX(rotX);
291
+ baseGeom.rotateZ(rotY);
292
+ baseGeom.translate(posX - width / 2, 0, posZ - height / 2);
293
+
294
+ geometries.push(baseGeom);
295
+ }
296
+ }
297
+
298
+ persistedGeometry = BufferGeometryUtils.mergeGeometries(geometries, false);
299
+
300
+ // Post-process: stitch vertices along tile edges to ensure seamless normals
301
+ if (persistedGeometry) {
302
+ const tolerance = 1e-3; // merge close vertices
303
+ const merged = BufferGeometryUtils.mergeVertices(
304
+ persistedGeometry,
305
+ tolerance
306
+ );
307
+ persistedGeometry.dispose(); // clean up old geometry
308
+ persistedGeometry = merged;
309
+ }
310
+
311
+ return persistedGeometry;
312
+ }, [cols, rows, radius, width, height, resolution]);
313
+
314
+ //<landscapeShaderMaterial ref={materialRef} />
315
+ return (
316
+ <mesh ref={ref} geometry={geometry} receiveShadow>
317
+ {children}
318
+ </mesh>
319
+ );
320
+ }
321
+ */
@@ -0,0 +1,257 @@
1
+ import React, { useEffect, useRef, type PropsWithChildren } from "react";
2
+ import type { CameraManager, Vec3 } from "@plasius/gpu-camera";
3
+ import { PlayerStore } from "./playerstore.js";
4
+
5
+ function length3(value: Vec3): number {
6
+ return Math.hypot(value[0], value[1], value[2]);
7
+ }
8
+
9
+ function normalize3(value: Vec3, fallback: Vec3): Vec3 {
10
+ const len = length3(value);
11
+ if (len <= Number.EPSILON) {
12
+ return [...fallback];
13
+ }
14
+ return [value[0] / len, value[1] / len, value[2] / len];
15
+ }
16
+
17
+ function add3(a: Vec3, b: Vec3): Vec3 {
18
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
19
+ }
20
+
21
+ function sub3(a: Vec3, b: Vec3): Vec3 {
22
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
23
+ }
24
+
25
+ function cross3(a: Vec3, b: Vec3): Vec3 {
26
+ return [
27
+ a[1] * b[2] - a[2] * b[1],
28
+ a[2] * b[0] - a[0] * b[2],
29
+ a[0] * b[1] - a[1] * b[0],
30
+ ];
31
+ }
32
+
33
+ function Player({
34
+ cameraManager,
35
+ cameraId,
36
+ children,
37
+ }: PropsWithChildren<{ cameraManager: CameraManager; cameraId: string }>) {
38
+ // Local refs for movement-relevant stats (used by the rAF ticker)
39
+ const strRef = useRef(1);
40
+ const dexRef = useRef(1);
41
+ const endRef = useRef(1);
42
+ const movingKeys = useRef<Set<string>>(new Set());
43
+ const movingRef = useRef(false);
44
+ const runLoadRef = useRef(0); // seconds of continuous run load
45
+ const lastTickRef = useRef(performance.now());
46
+
47
+ // Subscribe via inline selector (useSyncExternalStore under the hood)
48
+ const physEff = PlayerStore.useSelector((s) => {
49
+ const phys = s?.attributesBase?.physical ?? {
50
+ strength: 1,
51
+ dexterity: 1,
52
+ endurance: 1,
53
+ };
54
+ const g = s?.attributesGear ?? {};
55
+ const e = s?.attributesEffects ?? {};
56
+
57
+ let strength =
58
+ (phys.strength ?? 1) + (g.strength ?? 0) + (e.strength ?? 0);
59
+ let dexterity =
60
+ (phys.dexterity ?? 1) + (g.dexterity ?? 0) + (e.dexterity ?? 0);
61
+ let endurance =
62
+ (phys.endurance ?? 1) + (g.endurance ?? 0) + (e.endurance ?? 0);
63
+
64
+ // Add equipped item modifiers
65
+ if (s?.equipment && s?.items) {
66
+ for (const slot of Object.keys(s.equipment)) {
67
+ const itemId = (s.equipment as any)[slot];
68
+ const item = s.items[itemId];
69
+ const m = item?.modifiers as
70
+ | Partial<Record<"strength" | "dexterity" | "endurance", number>>
71
+ | undefined;
72
+ if (m) {
73
+ strength += m.strength ?? 0;
74
+ dexterity += m.dexterity ?? 0;
75
+ endurance += m.endurance ?? 0;
76
+ }
77
+ }
78
+ }
79
+
80
+ // Add status effect attribute modifiers
81
+ if (s?.effects) {
82
+ for (const eff of Object.values(s.effects) as any[]) {
83
+ const mods = eff?.modifiers?.attributes as
84
+ | Partial<Record<"strength" | "dexterity" | "endurance", number>>
85
+ | undefined;
86
+ if (mods) {
87
+ strength += mods.strength ?? 0;
88
+ dexterity += mods.dexterity ?? 0;
89
+ endurance += mods.endurance ?? 0;
90
+ }
91
+ }
92
+ }
93
+
94
+ return { strength, dexterity, endurance };
95
+ });
96
+
97
+ // Keep refs in sync for rAF without causing extra renders
98
+ useEffect(() => {
99
+ strRef.current = physEff.strength;
100
+ dexRef.current = physEff.dexterity;
101
+ endRef.current = physEff.endurance;
102
+ }, [physEff.strength, physEff.dexterity, physEff.endurance]);
103
+
104
+ useEffect(() => {
105
+ window.focus();
106
+ }, []);
107
+
108
+ // Fatigue/exhaustion ticker
109
+ useEffect(() => {
110
+ let raf = 0;
111
+ const tick = () => {
112
+ const now = performance.now();
113
+ const dt = (now - lastTickRef.current) / 1000;
114
+ lastTickRef.current = now;
115
+
116
+ const ln = (x: number) => Math.log(1 + Math.max(0, x) / 25);
117
+ const endurance = endRef.current;
118
+ const recovery = 1.5 + ln(endurance); // recover faster with Endurance
119
+
120
+ if (movingRef.current) {
121
+ runLoadRef.current = Math.max(0, runLoadRef.current + dt);
122
+ } else {
123
+ runLoadRef.current = Math.max(0, runLoadRef.current - dt * recovery);
124
+ }
125
+
126
+ raf = requestAnimationFrame(tick);
127
+ };
128
+
129
+ lastTickRef.current = performance.now();
130
+ raf = requestAnimationFrame(tick);
131
+ return () => cancelAnimationFrame(raf);
132
+ }, []);
133
+
134
+ // Movement handlers
135
+ useEffect(() => {
136
+ const ln = (x: number) => Math.log(1 + Math.max(0, x) / 25);
137
+
138
+ const baseSpeed = () => {
139
+ const dex = dexRef.current;
140
+ const str = strRef.current;
141
+ const dexBoost = 0.25 * ln(dex);
142
+ const strBoost = 0.1 * ln(str);
143
+ return 1 + dexBoost + strBoost;
144
+ };
145
+
146
+ const currentSpeed = () => {
147
+ const endurance = endRef.current;
148
+ const cap = 4 + 2 * ln(endurance);
149
+ const load = runLoadRef.current;
150
+ if (load <= cap) return baseSpeed();
151
+ const over = Math.min(1, (load - cap) / cap);
152
+ const fatigue = 1 - 0.5 * over; // down to 50%
153
+ return baseSpeed() * fatigue;
154
+ };
155
+
156
+ const handleKeyDown = (event: KeyboardEvent) => {
157
+ const cameraState = cameraManager.getCamera(cameraId);
158
+ if (!cameraState) return;
159
+
160
+ const position = [...cameraState.transform.position] as Vec3;
161
+ const target = [...cameraState.transform.target] as Vec3;
162
+ const up = normalize3(cameraState.transform.up ?? [0, 1, 0], [0, 1, 0]);
163
+
164
+ const forward = normalize3(sub3(target, position), [0, 0, -1]);
165
+ const forwardY = forward[1];
166
+ const planarForward = normalize3([forward[0], 0, forward[2]], [0, 0, -1]);
167
+ const right = normalize3(cross3(planarForward, up), [1, 0, 0]);
168
+
169
+ const moveKeys = new Set(["w", "a", "s", "d", "q", "e"]);
170
+ if (moveKeys.has(event.key)) {
171
+ movingKeys.current.add(event.key);
172
+ movingRef.current = true;
173
+ }
174
+
175
+ switch (event.key) {
176
+ case "w":
177
+ position[0] += planarForward[0] * currentSpeed();
178
+ position[1] += planarForward[1] * currentSpeed();
179
+ position[2] += planarForward[2] * currentSpeed();
180
+ break;
181
+ case "s":
182
+ position[0] -= planarForward[0] * currentSpeed();
183
+ position[1] -= planarForward[1] * currentSpeed();
184
+ position[2] -= planarForward[2] * currentSpeed();
185
+ break;
186
+ case "a":
187
+ position[0] -= right[0] * currentSpeed();
188
+ position[1] -= right[1] * currentSpeed();
189
+ position[2] -= right[2] * currentSpeed();
190
+ break;
191
+ case "d":
192
+ position[0] += right[0] * currentSpeed();
193
+ position[1] += right[1] * currentSpeed();
194
+ position[2] += right[2] * currentSpeed();
195
+ break;
196
+ case "q":
197
+ position[1] += currentSpeed();
198
+ break;
199
+ case "e":
200
+ position[1] -= currentSpeed();
201
+ break;
202
+ default:
203
+ break;
204
+ }
205
+
206
+ const nextTarget = add3(position, [
207
+ planarForward[0],
208
+ forwardY,
209
+ planarForward[2],
210
+ ]);
211
+
212
+ cameraManager.applyControl(
213
+ cameraId,
214
+ {
215
+ type: "set-look-at",
216
+ position,
217
+ target: nextTarget,
218
+ up,
219
+ },
220
+ { makeActive: true }
221
+ );
222
+ };
223
+
224
+ const handleKeyUp = (event: KeyboardEvent) => {
225
+ const moveKeys = new Set(["w", "a", "s", "d", "q", "e"]);
226
+ if (moveKeys.has(event.key)) {
227
+ movingKeys.current.delete(event.key);
228
+ if (movingKeys.current.size === 0) movingRef.current = false;
229
+ }
230
+ };
231
+
232
+ window.addEventListener("keydown", handleKeyDown);
233
+ window.addEventListener("keyup", handleKeyUp);
234
+ return () => {
235
+ window.removeEventListener("keydown", handleKeyDown);
236
+ window.removeEventListener("keyup", handleKeyUp);
237
+ };
238
+ }, [cameraManager, cameraId]);
239
+
240
+ return <>{children}</>;
241
+ }
242
+
243
+ function WrappedPlayer({
244
+ cameraManager,
245
+ cameraId,
246
+ children,
247
+ }: PropsWithChildren<{ cameraManager: CameraManager; cameraId: string }>) {
248
+ return (
249
+ <PlayerStore.Provider>
250
+ <Player cameraManager={cameraManager} cameraId={cameraId}>
251
+ {children}
252
+ </Player>
253
+ </PlayerStore.Provider>
254
+ );
255
+ }
256
+
257
+ export { WrappedPlayer as Player };