entangled-worlds-orbiters-shared 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 (38) hide show
  1. package/README.md +75 -0
  2. package/package.json +49 -0
  3. package/public/config/textures.json +332 -0
  4. package/src/arrow-ui/BooleanController.js +31 -0
  5. package/src/arrow-ui/ColorController.js +108 -0
  6. package/src/arrow-ui/Controller.js +447 -0
  7. package/src/arrow-ui/FunctionController.js +27 -0
  8. package/src/arrow-ui/GUI.js +595 -0
  9. package/src/arrow-ui/NumberController.js +567 -0
  10. package/src/arrow-ui/OptionController.js +64 -0
  11. package/src/arrow-ui/StringController.js +41 -0
  12. package/src/arrow-ui/index.js +13 -0
  13. package/src/arrow-ui/lilGuiLoader.js +10 -0
  14. package/src/arrow-ui/utils/getColorFormat.js +75 -0
  15. package/src/arrow-ui/utils/injectStyles.js +10 -0
  16. package/src/arrow-ui/utils/normalizeColorString.js +29 -0
  17. package/src/config/index.js +8 -0
  18. package/src/config/textureConfig.js +299 -0
  19. package/src/core/index.js +3 -0
  20. package/src/core/lightFresnel.js +64 -0
  21. package/src/core/lights.js +43 -0
  22. package/src/index.js +8 -0
  23. package/src/loaders/bodyTextureLoader.js +39 -0
  24. package/src/loaders/cubeMapLoader.js +264 -0
  25. package/src/loaders/glbLoader.js +71 -0
  26. package/src/loaders/index.js +4 -0
  27. package/src/moons/index.js +3 -0
  28. package/src/moons/moonsGeneration.js +228 -0
  29. package/src/moons/moonsManager.js +64 -0
  30. package/src/utils/dispose.js +20 -0
  31. package/src/utils/index.js +3 -0
  32. package/src/utils/materials.js +31 -0
  33. package/src/world/cloudShell.js +89 -0
  34. package/src/world/index.js +6 -0
  35. package/src/world/worldGLB.js +7 -0
  36. package/src/world/worldManager.js +93 -0
  37. package/src/world/worldNormal.js +25 -0
  38. package/src/world/worldTexture.js +239 -0
@@ -0,0 +1,89 @@
1
+ import * as THREE from 'three';
2
+ import { loadTexture } from '../loaders/bodyTextureLoader.js';
3
+
4
+ // Texture-only clouds material (alpha mask + tint), UV-scrolled
5
+ async function makeTextureMaterial({
6
+ color = 0xffffff,
7
+ opacity = 0.6,
8
+ textureUrl,
9
+ speed = 0.015,
10
+ } = {}) {
11
+ const mat = new THREE.MeshStandardMaterial({
12
+ color,
13
+ transparent: true,
14
+ opacity: 1.0, // drive alpha via alphaMap
15
+ roughness: 1.0,
16
+ metalness: 0.0,
17
+ depthTest: true,
18
+ depthWrite: false,
19
+ side: THREE.FrontSide,
20
+ toneMapped: true,
21
+ });
22
+
23
+ if (textureUrl) {
24
+ const tex = await loadTexture(textureUrl);
25
+ tex.wrapS = THREE.RepeatWrapping;
26
+ tex.wrapT = THREE.RepeatWrapping;
27
+ // Use linear space for alpha masks if available
28
+ tex.colorSpace = THREE.LinearSRGBColorSpace || THREE.SRGBColorSpace;
29
+ tex.repeat.set(1, 1); // fixed tiling
30
+ tex.anisotropy = 2;
31
+ mat.alphaMap = tex;
32
+ mat.opacity = opacity; // material opacity multiplier
33
+ mat.needsUpdate = true;
34
+ } else {
35
+ // No texture -> invisible shell
36
+ mat.opacity = 0.0;
37
+ }
38
+
39
+ // Randomize wind direction per instance (0..360)
40
+ const dirRad = (Math.random() * 360 * Math.PI) / 180;
41
+ const wind = new THREE.Vector2(Math.cos(dirRad), Math.sin(dirRad)).multiplyScalar(speed);
42
+ mat.userData.update = (timeSec) => {
43
+ const t = mat.alphaMap;
44
+ if (t) {
45
+ t.offset.x = wind.x * timeSec;
46
+ t.offset.y = wind.y * timeSec;
47
+ t.needsUpdate = true;
48
+ }
49
+ };
50
+
51
+ return mat;
52
+ }
53
+
54
+ export async function createCloudShell({
55
+ baseGeometry,
56
+ baseRadius = 0.5,
57
+ widthSegments = 64,
58
+ heightSegments = 64,
59
+ textureUrl = '/assets/texture/2k_earth_clouds.jpg',
60
+ color = 0xffffff,
61
+ opacity = 0.6,
62
+ speed = 0.015,
63
+ } = {}) {
64
+ const geom = baseGeometry
65
+ ? baseGeometry.clone()
66
+ : new THREE.SphereGeometry(baseRadius, widthSegments, heightSegments);
67
+
68
+ const material = await makeTextureMaterial({
69
+ color,
70
+ opacity,
71
+ textureUrl,
72
+ speed,
73
+ });
74
+
75
+ const mesh = new THREE.Mesh(geom, material);
76
+ const s = 1.001; // fixed scale
77
+ mesh.scale.set(s, s, s);
78
+ mesh.name = 'worldCloudShell';
79
+ mesh.renderOrder = 998; // after planet, before glow
80
+
81
+ mesh.onBeforeRender = () => {
82
+ const t = performance.now() * 0.001;
83
+ if (material.userData && typeof material.userData.update === 'function') {
84
+ material.userData.update(t);
85
+ }
86
+ };
87
+
88
+ return mesh;
89
+ }
@@ -0,0 +1,6 @@
1
+ // World system exports
2
+ export { WorldManager } from './worldManager.js';
3
+ export { makeSphere, EARTH_RADIUS_UNITS } from './worldTexture.js';
4
+ export { makeNormalSphere } from './worldNormal.js';
5
+ export { makeGLB } from './worldGLB.js';
6
+ export { createCloudShell } from './cloudShell.js';
@@ -0,0 +1,7 @@
1
+ import { loadGLB } from '../loaders/glbLoader.js';
2
+
3
+ export async function makeGLB({ modelUrl, flags, retries = 3 }) {
4
+ if (!modelUrl) throw new Error('modelUrl is required');
5
+ const obj = await loadGLB(modelUrl, { retries, applyFlags: flags });
6
+ return obj;
7
+ }
@@ -0,0 +1,93 @@
1
+ import { disposeObject3D } from '../utils/dispose.js';
2
+ import { makeGLB } from './worldGLB.js';
3
+ import { makeSphere } from './worldTexture.js';
4
+ import { makeNormalSphere } from './worldNormal.js';
5
+
6
+ export class WorldManager {
7
+ constructor(scene, { onLoadStart, onLoadEnd } = {}) {
8
+ this.scene = scene;
9
+ this.current = null;
10
+ this.flags = {};
11
+ this.onLoadStart = onLoadStart;
12
+ this.onLoadEnd = onLoadEnd;
13
+ this.token = 0;
14
+ // Registry for custom world creators
15
+ this.customCreators = new Map();
16
+ }
17
+
18
+ setFlags(flags) {
19
+ console.log('[WorldManager] setFlags:', flags);
20
+ this.flags = { ...this.flags, ...flags };
21
+ }
22
+
23
+ disposeCurrent() {
24
+ if (this.current) {
25
+ console.log('[WorldManager] disposeCurrent:', this.current?.name || this.current);
26
+ disposeObject3D(this.current);
27
+ this.scene.remove(this.current);
28
+ this.current = null;
29
+ }
30
+ }
31
+
32
+ // Register custom world creators for app-specific world types
33
+ registerWorldCreator(mode, creatorFunction) {
34
+ this.customCreators.set(mode, creatorFunction);
35
+ }
36
+
37
+ async setMode(mode, args = {}) {
38
+ const myToken = ++this.token;
39
+ console.log('[WorldManager] setMode request:', { mode, args });
40
+ this.onLoadStart?.();
41
+ try {
42
+ let obj;
43
+
44
+ // Check for custom creators first
45
+ if (this.customCreators.has(mode)) {
46
+ console.log(`[WorldManager] -> custom creator for mode ${mode}`);
47
+ obj = await this.customCreators.get(mode)({ flags: this.flags, ...args });
48
+ } else if (mode === 2) {
49
+ console.log('[WorldManager] -> makeGLB');
50
+ obj = await makeGLB({ modelUrl: args.modelUrl, flags: this.flags });
51
+ } else if (mode === 3) {
52
+ console.log('[WorldManager] -> makeNormalSphere');
53
+ obj = await makeNormalSphere({ flags: this.flags, ...args });
54
+ } else {
55
+ console.log('[WorldManager] -> makeSphere (textured)');
56
+ obj = await makeSphere({ textureUrl: args.textureUrl, flags: this.flags, ...args });
57
+ }
58
+
59
+ if (this.token !== myToken) {
60
+ console.log('[WorldManager] setMode superseded, abandoning result');
61
+ return;
62
+ }
63
+ this.disposeCurrent();
64
+ this.scene.add(obj);
65
+ this.current = obj;
66
+ console.log('[WorldManager] setMode applied. current:', obj?.name || obj);
67
+
68
+ // If the object exposes a frame updater, run a render-time loop
69
+ if (obj && obj.userData && typeof obj.userData.update === 'function' && window.__EW_RENDERER) {
70
+ const updater = obj.userData.update;
71
+ console.log('[WorldManager] update loop started for', obj.name || obj);
72
+ const tick = () => {
73
+ if (this.current === obj) {
74
+ try {
75
+ updater(window.__EW_RENDERER, performance.now() * 0.001);
76
+ } catch (e) {
77
+ console.warn('[WorldManager] update loop error:', e);
78
+ }
79
+ requestAnimationFrame(tick);
80
+ } else {
81
+ console.log('[WorldManager] update loop stopped (object replaced)');
82
+ }
83
+ };
84
+ requestAnimationFrame(tick);
85
+ }
86
+ } catch (e) {
87
+ console.warn('[WorldManager] setMode error:', e);
88
+ throw e;
89
+ } finally {
90
+ if (this.token === myToken) this.onLoadEnd?.();
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,25 @@
1
+ // Normal world (MeshNormalMaterial) implementation
2
+ import * as THREE from 'three';
3
+ import { EARTH_RADIUS_UNITS } from './worldTexture.js';
4
+ import { applyFlagsToMaterial } from '../utils/materials.js';
5
+
6
+ /**
7
+ * Create a simple sphere using THREE.MeshNormalMaterial.
8
+ * Accepts the same basic geometry params as textured sphere so the manager API is consistent.
9
+ */
10
+ export async function makeNormalSphere({
11
+ sphereRadius,
12
+ sphereWidthSegments,
13
+ sphereHeightSegments,
14
+ flags,
15
+ } = {}) {
16
+ const radius = typeof sphereRadius === 'number' ? Math.max(0.0001, sphereRadius) : EARTH_RADIUS_UNITS;
17
+ const widthSegs = Number.isFinite(sphereWidthSegments) ? Math.max(3, Math.floor(sphereWidthSegments)) : 64;
18
+ const heightSegs = Number.isFinite(sphereHeightSegments) ? Math.max(2, Math.floor(sphereHeightSegments)) : 64;
19
+ const geometry = new THREE.SphereGeometry(radius, widthSegs, heightSegs);
20
+ const material = new THREE.MeshNormalMaterial();
21
+ if (flags) applyFlagsToMaterial(material, flags);
22
+ const mesh = new THREE.Mesh(geometry, material);
23
+ mesh.name = 'worldNormalSphere';
24
+ return mesh;
25
+ }
@@ -0,0 +1,239 @@
1
+ import * as THREE from 'three';
2
+ import { loadTexture } from '../loaders/bodyTextureLoader.js';
3
+ import { applyFlagsToMaterial } from '../utils/materials.js';
4
+ import { getLightFresnel } from '../core/lightFresnel.js';
5
+ import { createCloudShell } from './cloudShell.js';
6
+
7
+ export const EARTH_RADIUS_UNITS = 0.5; // scene units
8
+
9
+ /**
10
+ * Simplified detection: width = height -> rectangular, width = 2×height -> equirectangular
11
+ */
12
+ function isLikelyEquirectangular(img) {
13
+ const aspectRatio = img.width / img.height;
14
+
15
+ // Check for 2:1 aspect ratio (equirectangular)
16
+ const is2to1 = Math.abs(aspectRatio - 2.0) < 0.1;
17
+
18
+ // Check for 1:1 aspect ratio (square -> treat as rectangular)
19
+ const is1to1 = Math.abs(aspectRatio - 1.0) < 0.1;
20
+
21
+ if (is1to1) {
22
+ return false; // Square images are rectangular
23
+ }
24
+
25
+ if (is2to1) {
26
+ return true; // 2:1 images are equirectangular
27
+ }
28
+
29
+ // For other aspect ratios, default to rectangular
30
+ return false;
31
+ }
32
+
33
+ export async function makeSphere({
34
+ textureUrl,
35
+ flags,
36
+ uOffset,
37
+ textureOffsetDeg,
38
+ mirrorX,
39
+ mirrorDuplicateX, // optional, still supported for explicit override
40
+ autoDetectEquirectangular = true,
41
+ forceEquirectangular = false,
42
+ forceRectangular = false,
43
+ // Wrapping params
44
+ wrapS = 'repeat',
45
+ wrapT = 'clamp',
46
+ repeatX,
47
+ repeatY,
48
+ rotationDeg,
49
+ center = [0.5, 0.5],
50
+ flipY,
51
+ // NEW: sphere geometry controls (optional)
52
+ sphereRadius,
53
+ sphereWidthSegments,
54
+ sphereHeightSegments,
55
+ // PBR material tuning (optional)
56
+ materialRoughness,
57
+ materialMetalness,
58
+ // NEW: glow controls
59
+ glow = true,
60
+ glowRimHex = 0x66ccff,
61
+ glowFacingHex = 0x000000,
62
+ glowScale = 1.18,
63
+ glowBias = 0.02,
64
+ glowFresnelScale = 1.0,
65
+ glowPower = 3.5,
66
+ // texture clouds controls (simplified)
67
+ cloudsEnabled = false,
68
+ cloudsTextureUrl = '/assets/texture/2k_earth_clouds.jpg',
69
+ cloudsColor = 0xffffff,
70
+ cloudsOpacity = 0.6,
71
+ cloudsSpeed = 0.015,
72
+ } = {}) {
73
+ const radius = typeof sphereRadius === 'number' ? Math.max(0.0001, sphereRadius) : EARTH_RADIUS_UNITS;
74
+ const widthSegs = Number.isFinite(sphereWidthSegments) ? Math.max(3, Math.floor(sphereWidthSegments)) : 64;
75
+ const heightSegs = Number.isFinite(sphereHeightSegments) ? Math.max(2, Math.floor(sphereHeightSegments)) : 64;
76
+ const geometry = new THREE.SphereGeometry(radius, widthSegs, heightSegs);
77
+
78
+ const rough = (typeof materialRoughness === 'number') ? THREE.MathUtils.clamp(materialRoughness, 0, 1) : 0.7;
79
+ const metal = (typeof materialMetalness === 'number') ? THREE.MathUtils.clamp(materialMetalness, 0, 1) : 0.0;
80
+ const material = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: rough, metalness: metal });
81
+
82
+ const WRAP = {
83
+ clamp: THREE.ClampToEdgeWrapping,
84
+ repeat: THREE.RepeatWrapping,
85
+ mirror: THREE.MirroredRepeatWrapping,
86
+ };
87
+
88
+ // Attach a Fresnel glow mesh as a child of the sphere mesh
89
+ function createGlow(rimHex, facingHex) {
90
+ const fresnelMat = getLightFresnel({ rimHex, facingHex });
91
+ // tweak uniforms for stronger rim
92
+ fresnelMat.uniforms.fresnelBias.value = glowBias;
93
+ fresnelMat.uniforms.fresnelScale.value = glowFresnelScale;
94
+ fresnelMat.uniforms.fresnelPower.value = glowPower;
95
+
96
+ const glowGeom = geometry.clone();
97
+ const fresnelMesh = new THREE.Mesh(glowGeom, fresnelMat);
98
+ // Slightly larger to avoid z-fighting and create a halo
99
+ const s = Math.max(glowScale, 1.0);
100
+ fresnelMesh.scale.set(s, s, s);
101
+ fresnelMesh.renderOrder = 999; // render after base mesh
102
+ fresnelMesh.name = 'worldTextureGlow';
103
+ return fresnelMesh;
104
+ }
105
+
106
+ const chosenTexture = textureUrl; // require explicit textureUrl — no implicit default
107
+ if (chosenTexture) {
108
+ try {
109
+ const tex = await loadTexture(chosenTexture);
110
+ tex.colorSpace = THREE.SRGBColorSpace;
111
+ tex.generateMipmaps = true;
112
+ tex.anisotropy = 8;
113
+ if (typeof flipY === 'boolean') tex.flipY = flipY;
114
+
115
+ // Decide mapping approach
116
+ let shouldMirrorDuplicate = mirrorDuplicateX;
117
+ if (tex.image) {
118
+ const img = tex.image;
119
+ let isEquirectangular = false;
120
+
121
+ if (forceEquirectangular) {
122
+ isEquirectangular = true;
123
+ } else if (forceRectangular) {
124
+ isEquirectangular = false;
125
+ } else if (autoDetectEquirectangular && typeof mirrorDuplicateX === 'undefined') {
126
+ const aspectDetection = isLikelyEquirectangular(img);
127
+ isEquirectangular = aspectDetection && Math.abs(img.width / img.height - 2.0) < 0.05;
128
+ } else {
129
+ isEquirectangular = !shouldMirrorDuplicate;
130
+ }
131
+ shouldMirrorDuplicate = !isEquirectangular;
132
+ }
133
+
134
+ // Default to rectangular duplicate if still undefined
135
+ if (typeof shouldMirrorDuplicate === 'undefined') {
136
+ shouldMirrorDuplicate = true;
137
+ }
138
+
139
+ // Seam alignment (offset only)
140
+ let offsetX = 0;
141
+ if (typeof uOffset === 'number') {
142
+ offsetX = ((uOffset % 1) + 1) % 1;
143
+ } else if (typeof textureOffsetDeg === 'number') {
144
+ offsetX = (((textureOffsetDeg / 360) % 1) + 1) % 1;
145
+ }
146
+
147
+ const applyWrapOptions = (t) => {
148
+ t.wrapS = WRAP[wrapS] ?? THREE.RepeatWrapping;
149
+ t.wrapT = WRAP[wrapT] ?? THREE.ClampToEdgeWrapping;
150
+ if (Array.isArray(center) && center.length === 2) t.center.set(center[0], center[1]);
151
+ if (typeof rotationDeg === 'number') t.rotation = (rotationDeg * Math.PI) / 180;
152
+ if (typeof repeatX === 'number' || typeof repeatY === 'number') {
153
+ t.repeat.set(
154
+ typeof repeatX === 'number' ? repeatX : t.repeat.x,
155
+ typeof repeatY === 'number' ? repeatY : t.repeat.y
156
+ );
157
+ }
158
+ t.offset.x = offsetX;
159
+ if (mirrorX) {
160
+ t.repeat.x = -Math.abs(t.repeat.x || 1);
161
+ t.offset.x = (t.offset.x + 1) % 1;
162
+ }
163
+ t.needsUpdate = true;
164
+ };
165
+
166
+ if (shouldMirrorDuplicate && tex.image && typeof document !== 'undefined') {
167
+ // Create a mirrored duplicate without any blending
168
+ const img = tex.image;
169
+ try {
170
+ const canvas = document.createElement('canvas');
171
+ canvas.width = img.width * 2;
172
+ canvas.height = img.height;
173
+ const ctx = canvas.getContext('2d');
174
+
175
+ // Left half: original
176
+ ctx.drawImage(img, 0, 0, img.width, img.height);
177
+ // Right half: horizontally flipped copy
178
+ ctx.save();
179
+ ctx.translate(img.width, 0);
180
+ ctx.scale(-1, 1);
181
+ ctx.drawImage(img, -img.width, 0, img.width, img.height);
182
+ ctx.restore();
183
+
184
+ const processedTex = new THREE.CanvasTexture(canvas);
185
+ processedTex.colorSpace = THREE.SRGBColorSpace;
186
+ processedTex.generateMipmaps = true;
187
+ processedTex.anisotropy = 8;
188
+
189
+ applyWrapOptions(processedTex);
190
+ material.map = processedTex;
191
+ material.needsUpdate = true;
192
+ } catch (e) {
193
+ // Fallback: use mirrored repeat directly
194
+ tex.wrapS = THREE.MirroredRepeatWrapping;
195
+ tex.wrapT = WRAP[wrapT] ?? THREE.ClampToEdgeWrapping;
196
+ tex.repeat.x = Math.max(2, typeof repeatX === 'number' ? repeatX : 2);
197
+ applyWrapOptions(tex);
198
+ material.map = tex;
199
+ material.needsUpdate = true;
200
+ }
201
+ } else {
202
+ // Equirectangular or no canvas: just use original texture without any edge processing
203
+ applyWrapOptions(tex);
204
+ material.map = tex;
205
+ material.needsUpdate = true;
206
+ }
207
+ } catch (e) {
208
+ console.warn('[worldTexture] Failed to load texture:', e);
209
+ }
210
+ }
211
+
212
+ if (flags) applyFlagsToMaterial(material, flags);
213
+ const mesh = new THREE.Mesh(geometry, material);
214
+ mesh.position.set(0, 0, 0);
215
+
216
+ // NEW: add cloud shell before glow
217
+ if (cloudsEnabled) {
218
+ const cloudMesh = await createCloudShell({
219
+ baseGeometry: geometry,
220
+ baseRadius: radius,
221
+ widthSegments: widthSegs,
222
+ heightSegments: heightSegs,
223
+ textureUrl: cloudsTextureUrl,
224
+ color: cloudsColor,
225
+ opacity: cloudsOpacity,
226
+ speed: cloudsSpeed,
227
+ });
228
+ mesh.add(cloudMesh);
229
+ }
230
+
231
+ // Optionally add glow as a child of the mesh so it follows transforms
232
+ if (glow) {
233
+ const glowMesh = createGlow(glowRimHex, glowFacingHex);
234
+ mesh.add(glowMesh);
235
+ }
236
+
237
+ return mesh;
238
+ }
239
+