entangled-worlds-orbiters-shared 1.0.3 → 1.0.5

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/README.md CHANGED
@@ -16,6 +16,16 @@ A shared library containing core components for 3D world rendering, moons system
16
16
  ```bash
17
17
  npm install entangled-worlds-orbiters-shared
18
18
  ```
19
+ ## Test locally
20
+
21
+ In /entangled-worlds-orbiters-shared:
22
+ npm install
23
+ npm link
24
+
25
+ In /entangled-worlds-preview or /orbiters:
26
+
27
+ npm link entangled-worlds-orbiters-shared
28
+
19
29
 
20
30
  ## Usage
21
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entangled-worlds-orbiters-shared",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Shared library for entangled worlds orbiters - contains core world rendering, moons system, and UI components",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,22 +1,20 @@
1
1
  /**
2
2
  * Texture Configuration Loader
3
- * Loads texture definitions from local config file or remote endpoint
3
+ * Loads texture definitions from remote endpoint or uses passed config
4
4
  */
5
5
  import { resolvePublicUrl } from './env.js';
6
6
 
7
- const DEFAULT_CONFIG_URL = '/config/textures.json';
8
- const FALLBACK_CONFIG_URL = 'https://api.entangled-worlds.com/config/textures.json'; // Future endpoint
9
-
10
7
  let cachedConfig = null;
11
8
  let configPromise = null;
9
+ let lastConfigUrl = null;
12
10
 
13
11
  /**
14
- * Load texture configuration from local file or remote endpoint
15
- * @param {string} configUrl - Override default config URL
12
+ * Load texture configuration from remote endpoint or use passed config
13
+ * @param {string} configUrl - Override config URL
16
14
  * @param {boolean} useCache - Whether to use cached config if available
17
15
  * @returns {Promise<Object>} Texture configuration object
18
16
  */
19
- export async function loadTextureConfig(configUrl = DEFAULT_CONFIG_URL, useCache = true) {
17
+ export async function loadTextureConfig(configUrl = null, useCache = true) {
20
18
  if (useCache && cachedConfig) {
21
19
  return cachedConfig;
22
20
  }
@@ -25,11 +23,19 @@ export async function loadTextureConfig(configUrl = DEFAULT_CONFIG_URL, useCache
25
23
  return configPromise;
26
24
  }
27
25
 
26
+ if (configUrl) {
27
+ lastConfigUrl = configUrl;
28
+ }
29
+
30
+ const targetUrl = configUrl || lastConfigUrl;
31
+
28
32
  configPromise = (async () => {
29
33
  try {
30
- // Resolve config URL against public base if it's a relative path
31
- const resolvedConfigUrl = /^https?:\/\//i.test(configUrl) ? configUrl : resolvePublicUrl(configUrl);
32
- const response = await fetch(resolvedConfigUrl);
34
+ if (!targetUrl) {
35
+ throw new Error('No texture config URL provided');
36
+ }
37
+
38
+ const response = await fetch(targetUrl);
33
39
 
34
40
  if (!response.ok) {
35
41
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
@@ -46,19 +52,10 @@ export async function loadTextureConfig(configUrl = DEFAULT_CONFIG_URL, useCache
46
52
  return config;
47
53
  } catch (error) {
48
54
  console.error('[TextureConfig] Failed to load config:', error);
49
-
50
- // If primary URL fails and it's not already the fallback, try fallback
51
- if (configUrl !== FALLBACK_CONFIG_URL) {
52
- try {
53
- return await loadTextureConfig(FALLBACK_CONFIG_URL, false);
54
- } catch (fallbackError) {
55
- console.error('[TextureConfig] Fallback also failed:', fallbackError);
56
- }
57
- }
58
-
59
- // Return minimal fallback configuration
55
+ // Return minimal fallback configuration when loading fails
60
56
  console.warn('[TextureConfig] Using hardcoded fallback config');
61
- return getHardcodedFallback();
57
+ cachedConfig = getHardcodedFallback();
58
+ return cachedConfig;
62
59
  } finally {
63
60
  configPromise = null;
64
61
  }
@@ -267,10 +264,10 @@ function getHardcodedFallback() {
267
264
  * @param {string} configUrl - Override config URL
268
265
  * @returns {Promise<Object>} Fresh configuration object
269
266
  */
270
- export async function reloadTextureConfig(configUrl = DEFAULT_CONFIG_URL) {
267
+ export async function reloadTextureConfig(configUrl = null) {
271
268
  cachedConfig = null;
272
269
  configPromise = null;
273
- return loadTextureConfig(configUrl, false);
270
+ return loadTextureConfig(configUrl || lastConfigUrl, false);
274
271
  }
275
272
 
276
273
  /**
@@ -5,6 +5,25 @@ import { getLightFresnel } from '../core/lightFresnel.js';
5
5
  import { createCloudShell } from './cloudShell.js';
6
6
  import { resolvePublicUrl } from '../config/env.js';
7
7
 
8
+ // Debug tag so you can verify the loaded file version in console
9
+ export const WORLD_TEXTURE_DEBUG_TAG = 'WT:vFlipGeom-r3@2025-10-28T17:22:00Z';
10
+
11
+ // Enable debug if query/localStorage says so; expose tag
12
+ if (typeof window !== 'undefined') {
13
+ const q = (window.location && window.location.search) || '';
14
+ const qsDebug = /(?:[?&])debugTextures=1(?:&|$)/.test(q);
15
+ const lsDebug = (window.localStorage && window.localStorage.DEBUG_TEXTURE_FLIP === '1') || false;
16
+ if (!window.DEBUG_TEXTURE_FLIP && (qsDebug || lsDebug)) {
17
+ window.DEBUG_TEXTURE_FLIP = true;
18
+ }
19
+ window.EW_WT_DEBUG_TAG = WORLD_TEXTURE_DEBUG_TAG;
20
+ if (window.DEBUG_TEXTURE_FLIP) {
21
+ console.info('[worldTexture] loaded', WORLD_TEXTURE_DEBUG_TAG);
22
+ }
23
+ }
24
+
25
+ let WT_LOGGED_FIRST_CALL = false;
26
+
8
27
  export const EARTH_RADIUS_UNITS = 0.5; // scene units
9
28
 
10
29
  /**
@@ -75,6 +94,7 @@ export async function makeSphere({
75
94
  const widthSegs = Number.isFinite(sphereWidthSegments) ? Math.max(3, Math.floor(sphereWidthSegments)) : 64;
76
95
  const heightSegs = Number.isFinite(sphereHeightSegments) ? Math.max(2, Math.floor(sphereHeightSegments)) : 64;
77
96
  const geometry = new THREE.SphereGeometry(radius, widthSegs, heightSegs);
97
+ let meshGeometry = geometry; // may clone/flip UVs for compressed textures
78
98
 
79
99
  const rough = (typeof materialRoughness === 'number') ? THREE.MathUtils.clamp(materialRoughness, 0, 1) : 0.7;
80
100
  const metal = (typeof materialMetalness === 'number') ? THREE.MathUtils.clamp(materialMetalness, 0, 1) : 0.0;
@@ -86,6 +106,48 @@ export async function makeSphere({
86
106
  mirror: THREE.MirroredRepeatWrapping,
87
107
  };
88
108
 
109
+ // For compressed textures (e.g., KTX2) WebGL cannot apply UNPACK_FLIP_Y at upload.
110
+ // To normalize orientation across loaders, emulate vertical flip in UV sampling:
111
+ // v' = 1 - v => repeat.y = -repeat.y, offset.y = 1 - offset.y
112
+ const normalizeCompressedFlip = (t) => {
113
+ if (!t || !t.isCompressedTexture) return;
114
+ const wantFlip = (typeof flipY === 'boolean') ? flipY : true; // match uncompressed default
115
+ if (!wantFlip) return;
116
+ const rY = (t.repeat && typeof t.repeat.y === 'number') ? t.repeat.y : 1;
117
+ const oY = (t.offset && typeof t.offset.y === 'number') ? t.offset.y : 0;
118
+ t.repeat.y = -rY;
119
+ t.offset.y = 1 - oY;
120
+ t.needsUpdate = true;
121
+ };
122
+
123
+ // Emulate vertical flip for compressed textures (e.g., KTX2) using UV transform
124
+ const normalizeCompressedFlipY = (t, desired) => {
125
+ if (!t || !t.isCompressedTexture) return;
126
+ const wantFlip = (typeof desired === 'boolean') ? desired : true; // default to match uncompressed loader
127
+ const rY = t.repeat?.y ?? 1;
128
+ const oY = t.offset?.y ?? 0;
129
+ if (wantFlip) {
130
+ t.repeat.y = -Math.abs(rY);
131
+ t.offset.y = 1 - oY;
132
+ } else {
133
+ t.repeat.y = Math.abs(rY);
134
+ t.offset.y = 0;
135
+ }
136
+ t.needsUpdate = true;
137
+ };
138
+
139
+ // Flip UV.y on a cloned geometry (keeps other layers safe)
140
+ const flipGeometryUVY = (geom) => {
141
+ if (!geom || !geom.attributes || !geom.attributes.uv) return 0;
142
+ const uv = geom.attributes.uv;
143
+ const arr = uv.array;
144
+ for (let i = 1; i < arr.length; i += 2) {
145
+ arr[i] = 1 - arr[i]; // v = 1 - v
146
+ }
147
+ uv.needsUpdate = true;
148
+ return uv.count;
149
+ };
150
+
89
151
  // Attach a Fresnel glow mesh as a child of the sphere mesh
90
152
  function createGlow(rimHex, facingHex) {
91
153
  const fresnelMat = getLightFresnel({ rimHex, facingHex });
@@ -153,6 +215,11 @@ export async function makeSphere({
153
215
  const applyWrapOptions = (t) => {
154
216
  t.wrapS = WRAP[wrapS] ?? THREE.RepeatWrapping;
155
217
  t.wrapT = WRAP[wrapT] ?? THREE.ClampToEdgeWrapping;
218
+ // Compressed textures + NPOT prefer clamp to avoid artifacts
219
+ if (isCompressed) {
220
+ t.wrapS = THREE.ClampToEdgeWrapping;
221
+ t.wrapT = THREE.ClampToEdgeWrapping;
222
+ }
156
223
  if (Array.isArray(center) && center.length === 2) t.center.set(center[0], center[1]);
157
224
  if (typeof rotationDeg === 'number') t.rotation = (rotationDeg * Math.PI) / 180;
158
225
  if (typeof repeatX === 'number' || typeof repeatY === 'number') {
@@ -201,12 +268,23 @@ export async function makeSphere({
201
268
  tex.wrapT = WRAP[wrapT] ?? THREE.ClampToEdgeWrapping;
202
269
  tex.repeat.x = Math.max(2, typeof repeatX === 'number' ? repeatX : 2);
203
270
  applyWrapOptions(tex);
271
+ // If compressed (e.g., KTX2), normalize vertical orientation using sampling transform
272
+ normalizeCompressedFlip(tex);
204
273
  material.map = tex;
205
274
  material.needsUpdate = true;
206
275
  }
207
276
  } else {
208
- // Equirectangular or no canvas: just use original texture without any edge processing
277
+ // Equirectangular or no canvas: just use original texture
209
278
  applyWrapOptions(tex);
279
+ const wantFlip = (typeof flipY === 'boolean') ? flipY : true;
280
+ if (isCompressed && wantFlip) {
281
+ meshGeometry = geometry.clone();
282
+ flipGeometryUVY(meshGeometry);
283
+ } else if (isCompressed) {
284
+ tex.repeat.y = Math.abs(tex.repeat?.y ?? 1);
285
+ tex.offset.y = 0;
286
+ tex.needsUpdate = true;
287
+ }
210
288
  material.map = tex;
211
289
  material.needsUpdate = true;
212
290
  }
@@ -216,7 +294,7 @@ export async function makeSphere({
216
294
  }
217
295
 
218
296
  if (flags) applyFlagsToMaterial(material, flags);
219
- const mesh = new THREE.Mesh(geometry, material);
297
+ const mesh = new THREE.Mesh(meshGeometry, material);
220
298
  mesh.position.set(0, 0, 0);
221
299
 
222
300
  // NEW: add cloud shell before glow