rayzee 7.27.1 → 7.28.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.
@@ -1,6 +1,7 @@
1
1
  import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace, RepeatWrapping } from "three";
2
2
  import { TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX, MATERIAL_DATA_LAYOUT, normalizeAttenuationDistance } from '../EngineDefaults.js';
3
3
  import TexturesWorker from './Workers/TexturesWorker.js?worker&inline';
4
+ import { ISSUE_CODES } from '../EngineIssues.js';
4
5
 
5
6
  // Canvas pooling for efficient reuse of canvas elements
6
7
  class CanvasPool {
@@ -460,6 +461,8 @@ export class TextureCreator {
460
461
  this.maxConcurrentWorkers = TEXTURE_CONSTANTS.MAX_CONCURRENT_WORKERS;
461
462
  this.activeWorkers = 0;
462
463
 
464
+ this._issues = options.issues ?? null;
465
+
463
466
  // Longest-edge cap for material-texture arrays. Clamped to the hardware ceiling.
464
467
  this.maxTextureSize = this._clampTextureSize(
465
468
  options.maxTextureSize ?? TEXTURE_CONSTANTS.DEFAULT_MAX_TEXTURE_SIZE
@@ -479,6 +482,17 @@ export class TextureCreator {
479
482
 
480
483
  }
481
484
 
485
+ /** A failed map array leaves those surfaces untextured — complete-looking and wrong. @private */
486
+ _reportTextureFailure( map, error ) {
487
+
488
+ this._issues?.record(
489
+ ISSUE_CODES.TEXTURE_BUILD_FAILED,
490
+ `${map} texture array failed to build — those surfaces render untextured`,
491
+ { map, cause: String( error?.message ?? error ) }
492
+ );
493
+
494
+ }
495
+
482
496
  _clampTextureSize( size ) {
483
497
 
484
498
  const n = Math.floor( Number( size ) || TEXTURE_CONSTANTS.DEFAULT_MAX_TEXTURE_SIZE );
@@ -573,7 +587,11 @@ export class TextureCreator {
573
587
 
574
588
  } catch ( error ) {
575
589
 
576
- console.warn( 'Texture processing failed, trying fallback:', error );
590
+ this._issues?.warn(
591
+ ISSUE_CODES.TEXTURE_PROCESSING_FALLBACK,
592
+ 'worker texture processing failed — retrying on the main thread',
593
+ { cause: String( error?.message ?? error ) }
594
+ );
577
595
  return await this.processOnMainThreadSync( normalized );
578
596
 
579
597
  } finally {
@@ -1114,105 +1132,25 @@ export class TextureCreator {
1114
1132
  // Create texture arrays
1115
1133
  const texturePromises = [];
1116
1134
 
1117
- if ( maps && maps.length > 0 ) {
1118
-
1119
- texturePromises.push(
1120
- this.createTexturesToDataTexture( maps )
1121
- .then( tex => ( { type: 'albedo', texture: tex } ) )
1122
- .catch( error => {
1123
-
1124
- console.warn( 'Failed to create albedo textures:', error );
1125
- return { type: 'albedo', texture: null };
1126
-
1127
- } )
1128
- );
1129
-
1130
- }
1131
-
1132
- if ( normalMaps && normalMaps.length > 0 ) {
1133
-
1134
- texturePromises.push(
1135
- this.createTexturesToDataTexture( normalMaps )
1136
- .then( tex => ( { type: 'normal', texture: tex } ) )
1137
- .catch( error => {
1138
-
1139
- console.warn( 'Failed to create normal textures:', error );
1140
- return { type: 'normal', texture: null };
1141
-
1142
- } )
1143
- );
1144
-
1145
- }
1146
-
1147
- if ( bumpMaps && bumpMaps.length > 0 ) {
1148
-
1149
- texturePromises.push(
1150
- this.createTexturesToDataTexture( bumpMaps )
1151
- .then( tex => ( { type: 'bump', texture: tex } ) )
1152
- .catch( error => {
1153
-
1154
- console.warn( 'Failed to create bump textures:', error );
1155
- return { type: 'bump', texture: null };
1156
-
1157
- } )
1158
- );
1159
-
1160
- }
1135
+ for ( const [ type, list ] of [
1136
+ [ 'albedo', maps ],
1137
+ [ 'normal', normalMaps ],
1138
+ [ 'bump', bumpMaps ],
1139
+ [ 'roughness', roughnessMaps ],
1140
+ [ 'metalness', metalnessMaps ],
1141
+ [ 'emissive', emissiveMaps ],
1142
+ [ 'displacement', displacementMaps ],
1143
+ ] ) {
1161
1144
 
1162
- if ( roughnessMaps && roughnessMaps.length > 0 ) {
1145
+ if ( ! list || list.length === 0 ) continue;
1163
1146
 
1164
1147
  texturePromises.push(
1165
- this.createTexturesToDataTexture( roughnessMaps )
1166
- .then( tex => ( { type: 'roughness', texture: tex } ) )
1148
+ this.createTexturesToDataTexture( list )
1149
+ .then( texture => ( { type, texture } ) )
1167
1150
  .catch( error => {
1168
1151
 
1169
- console.warn( 'Failed to create roughness textures:', error );
1170
- return { type: 'roughness', texture: null };
1171
-
1172
- } )
1173
- );
1174
-
1175
- }
1176
-
1177
- if ( metalnessMaps && metalnessMaps.length > 0 ) {
1178
-
1179
- texturePromises.push(
1180
- this.createTexturesToDataTexture( metalnessMaps )
1181
- .then( tex => ( { type: 'metalness', texture: tex } ) )
1182
- .catch( error => {
1183
-
1184
- console.warn( 'Failed to create metalness textures:', error );
1185
- return { type: 'metalness', texture: null };
1186
-
1187
- } )
1188
- );
1189
-
1190
- }
1191
-
1192
- if ( emissiveMaps && emissiveMaps.length > 0 ) {
1193
-
1194
- texturePromises.push(
1195
- this.createTexturesToDataTexture( emissiveMaps )
1196
- .then( tex => ( { type: 'emissive', texture: tex } ) )
1197
- .catch( error => {
1198
-
1199
- console.warn( 'Failed to create emissive textures:', error );
1200
- return { type: 'emissive', texture: null };
1201
-
1202
- } )
1203
- );
1204
-
1205
- }
1206
-
1207
- if ( displacementMaps && displacementMaps.length > 0 ) {
1208
-
1209
- texturePromises.push(
1210
- this.createTexturesToDataTexture( displacementMaps )
1211
- .then( tex => ( { type: 'displacement', texture: tex } ) )
1212
- .catch( error => {
1213
-
1214
- console.warn( 'Failed to create displacement textures:', error );
1215
- return { type: 'displacement', texture: null };
1152
+ this._reportTextureFailure( type, error );
1153
+ return { type, texture: null };
1216
1154
 
1217
1155
  } )
1218
1156
  );
@@ -1222,6 +1160,10 @@ export class TextureCreator {
1222
1160
  // Wait for all texture arrays to complete
1223
1161
  const textureResults = await Promise.allSettled( texturePromises );
1224
1162
 
1163
+ // allSettled would otherwise swallow a strict host's EngineIssueError.
1164
+ const refused = textureResults.find( ( r ) => r.status === 'rejected' );
1165
+ if ( refused ) throw refused.reason;
1166
+
1225
1167
  // Organize results
1226
1168
  const textures = {};
1227
1169
 
@@ -214,3 +214,42 @@ export function applySaturation( out, saturation ) {
214
214
  out[ 2 ] = luma + ( out[ 2 ] - luma ) * saturation;
215
215
 
216
216
  }
217
+
218
+ /**
219
+ * Linear float RGBA → sRGB bytes, in the output pass's order: exposure, saturation, curve,
220
+ * transfer function. Not interchangeable — moving any step shifts every mid-tone.
221
+ *
222
+ * @param {Float32Array} linear - RGBA, 4 floats per pixel
223
+ * @param {Object} options
224
+ * @param {number} options.exposure - renderer.toneMappingExposure, raw
225
+ * @param {number} options.toneMapping - Three.js ToneMapping constant
226
+ * @param {number} [options.saturation=1]
227
+ * @param {boolean} [options.preserveAlpha=false]
228
+ * @returns {Uint8ClampedArray} RGBA bytes
229
+ */
230
+ export function toneMapToRGBA8( linear, { exposure, toneMapping, saturation = 1, preserveAlpha = false } ) {
231
+
232
+ const curve = TONE_MAP_FNS.get( toneMapping ) ?? TONE_MAP_FNS.get( NoToneMapping );
233
+ const gain = effectiveExposure( exposure, toneMapping );
234
+ const out = new Uint8ClampedArray( linear.length );
235
+ const scratch = [ 0, 0, 0 ];
236
+
237
+ for ( let i = 0; i < linear.length; i += 4 ) {
238
+
239
+ scratch[ 0 ] = linear[ i ] * gain;
240
+ scratch[ 1 ] = linear[ i + 1 ] * gain;
241
+ scratch[ 2 ] = linear[ i + 2 ] * gain;
242
+
243
+ applySaturation( scratch, saturation );
244
+ curve( scratch[ 0 ], scratch[ 1 ], scratch[ 2 ], 1.0, scratch );
245
+
246
+ out[ i ] = linearToSRGB( scratch[ 0 ] ) * 255 + 0.5;
247
+ out[ i + 1 ] = linearToSRGB( scratch[ 1 ] ) * 255 + 0.5;
248
+ out[ i + 2 ] = linearToSRGB( scratch[ 2 ] ) * 255 + 0.5;
249
+ out[ i + 3 ] = preserveAlpha ? linear[ i + 3 ] * 255 + 0.5 : 255;
250
+
251
+ }
252
+
253
+ return out;
254
+
255
+ }
@@ -1,6 +1,7 @@
1
1
  import { EventDispatcher, Color, Vector2, MathUtils } from 'three';
2
2
  import { ENGINE_DEFAULTS } from './EngineDefaults.js';
3
3
  import { EngineEvents } from './EngineEvents.js';
4
+ import { ISSUE_CODES } from './EngineIssues.js';
4
5
 
5
6
  /**
6
7
  * Routing table: maps each setting key to its target stage/handler.
@@ -78,6 +79,14 @@ const SETTING_ROUTES = {
78
79
  * Default keys to extract from ENGINE_DEFAULTS for initializing the values map.
79
80
  * Maps ENGINE_DEFAULTS key → RenderSettings key when they differ.
80
81
  */
82
+ /** Provenance tags for getEffective(). Add-only — hosts branch on them. */
83
+ export const SETTING_SOURCE = Object.freeze( {
84
+ DEFAULT: 'default',
85
+ HOST: 'host',
86
+ SCENE_METADATA: 'scene-metadata',
87
+ MODE_PRESET: 'mode-preset',
88
+ } );
89
+
81
90
  const DEFAULTS_KEY_MAP = {
82
91
  bounces: 'maxBounces',
83
92
  debugMode: 'visMode',
@@ -96,13 +105,23 @@ const DEFAULTS_KEY_MAP = {
96
105
  */
97
106
  export class RenderSettings extends EventDispatcher {
98
107
 
99
- constructor( defaults = ENGINE_DEFAULTS ) {
108
+ /**
109
+ * @param {Object} [defaults]
110
+ * @param {Object} [options]
111
+ * @param {import('./EngineIssues.js').IssueLog} [options.issues] - records unroutable keys
112
+ */
113
+ constructor( defaults = ENGINE_DEFAULTS, { issues = null } = {} ) {
100
114
 
101
115
  super();
102
116
 
117
+ this._issues = issues;
118
+
103
119
  /** @type {Map<string, *>} */
104
120
  this._values = new Map();
105
121
 
122
+ /** @type {Map<string, string>} - see getEffective() */
123
+ this._sources = new Map();
124
+
106
125
  /** @type {import('./Stages/PathTracer.js').PathTracer|null} */
107
126
  this._pathTracer = null;
108
127
 
@@ -272,24 +291,17 @@ export class RenderSettings extends EventDispatcher {
272
291
  * @param {boolean} [options.reset] - Override the route's default reset behavior
273
292
  * @param {boolean} [options.silent] - Suppress the settingChanged event
274
293
  */
275
- set( key, value, { reset, silent } = {} ) {
294
+ set( key, value, { reset, silent, source = SETTING_SOURCE.HOST } = {} ) {
276
295
 
277
- const prev = this._values.get( key );
278
- if ( prev === value ) return;
279
-
280
- this._values.set( key, value );
281
-
282
- const route = SETTING_ROUTES[ key ];
283
- if ( ! route ) return;
284
-
285
- this._applyRoute( route, value, prev );
296
+ const applied = this._applyOne( key, value, source );
297
+ if ( ! applied?.route ) return;
286
298
 
287
- const shouldReset = reset !== undefined ? reset : ( route.reset ?? true );
299
+ const shouldReset = reset !== undefined ? reset : ( applied.route.reset ?? true );
288
300
  if ( shouldReset ) this._resetCallback?.();
289
301
 
290
302
  if ( ! silent ) {
291
303
 
292
- this.dispatchEvent( { type: EngineEvents.SETTING_CHANGED, key, value, prev } );
304
+ this.dispatchEvent( { type: EngineEvents.SETTING_CHANGED, key, value, prev: applied.prev } );
293
305
 
294
306
  }
295
307
 
@@ -301,28 +313,22 @@ export class RenderSettings extends EventDispatcher {
301
313
  * @param {Object} [options]
302
314
  * @param {boolean} [options.silent] - Suppress settingChanged events
303
315
  * @param {boolean} [options.reset] - Override the routes' default reset behavior
316
+ * @param {string} [options.source] - Provenance tag; see SETTING_SOURCE
304
317
  */
305
- setMany( updates, { silent, reset } = {} ) {
318
+ setMany( updates, { silent, reset, source = SETTING_SOURCE.HOST } = {} ) {
306
319
 
307
320
  let needsReset = false;
308
321
 
309
322
  for ( const [ key, value ] of Object.entries( updates ) ) {
310
323
 
311
- const prev = this._values.get( key );
312
- if ( prev === value ) continue;
324
+ const applied = this._applyOne( key, value, source );
325
+ if ( ! applied?.route ) continue;
313
326
 
314
- this._values.set( key, value );
315
-
316
- const route = SETTING_ROUTES[ key ];
317
- if ( ! route ) continue;
318
-
319
- this._applyRoute( route, value, prev );
320
-
321
- if ( route.reset ?? true ) needsReset = true;
327
+ if ( applied.route.reset ?? true ) needsReset = true;
322
328
 
323
329
  if ( ! silent ) {
324
330
 
325
- this.dispatchEvent( { type: EngineEvents.SETTING_CHANGED, key, value, prev } );
331
+ this.dispatchEvent( { type: EngineEvents.SETTING_CHANGED, key, value, prev: applied.prev } );
326
332
 
327
333
  }
328
334
 
@@ -333,12 +339,73 @@ export class RenderSettings extends EventDispatcher {
333
339
 
334
340
  }
335
341
 
342
+ /**
343
+ * Stores one value, tags its provenance and pushes it to its stage.
344
+ * @returns {?{route: ?Object, prev: *}} null when the value did not change
345
+ * @private
346
+ */
347
+ _applyOne( key, value, source ) {
348
+
349
+ const prev = this._values.get( key );
350
+ if ( prev === value ) return null;
351
+
352
+ this._values.set( key, value );
353
+ this._sources.set( key, source );
354
+
355
+ const route = SETTING_ROUTES[ key ];
356
+ if ( ! route ) {
357
+
358
+ this._reportUnknownKey( key );
359
+ return { route: null, prev };
360
+
361
+ }
362
+
363
+ this._applyRoute( route, value, prev );
364
+ return { route, prev };
365
+
366
+ }
367
+
368
+ /** Stored but never applied: a typo becoming a wrong image. @private */
369
+ _reportUnknownKey( key ) {
370
+
371
+ this._issues?.record(
372
+ ISSUE_CODES.SETTING_UNKNOWN_KEY,
373
+ `unknown setting "${key}" — stored but never applied to any stage`,
374
+ { key }
375
+ );
376
+
377
+ }
378
+
336
379
  get( key ) {
337
380
 
338
381
  return this._values.get( key );
339
382
 
340
383
  }
341
384
 
385
+ /**
386
+ * Every live setting with the value in force and who put it there. `routed: false` means
387
+ * stored but reaching no stage — see _reportUnknownKey.
388
+ *
389
+ * @returns {Object<string, {value: *, source: string, routed: boolean}>}
390
+ */
391
+ getEffective() {
392
+
393
+ const out = {};
394
+
395
+ for ( const [ key, value ] of this._values ) {
396
+
397
+ out[ key ] = {
398
+ value,
399
+ source: this._sources.get( key ) ?? SETTING_SOURCE.DEFAULT,
400
+ routed: SETTING_ROUTES[ key ] !== undefined,
401
+ };
402
+
403
+ }
404
+
405
+ return out;
406
+
407
+ }
408
+
342
409
  getAll() {
343
410
 
344
411
  return Object.fromEntries( this._values );
@@ -396,6 +463,7 @@ export class RenderSettings extends EventDispatcher {
396
463
  if ( key in defaults ) {
397
464
 
398
465
  this._values.set( key, defaults[ key ] );
466
+ this._sources.set( key, SETTING_SOURCE.DEFAULT );
399
467
 
400
468
  }
401
469
 
@@ -407,6 +475,7 @@ export class RenderSettings extends EventDispatcher {
407
475
  if ( defaultsKey in defaults ) {
408
476
 
409
477
  this._values.set( settingsKey, defaults[ defaultsKey ] );
478
+ this._sources.set( settingsKey, SETTING_SOURCE.DEFAULT );
410
479
 
411
480
  }
412
481
 
package/src/index.js CHANGED
@@ -9,11 +9,20 @@
9
9
  import './TSL/patches.js';
10
10
 
11
11
  // Main application
12
- export { PathTracerApp } from './PathTracerApp.js';
12
+ export { PathTracerApp, describeAdapter } from './PathTracerApp.js';
13
13
 
14
14
  // Event types
15
15
  export { EngineEvents } from './EngineEvents.js';
16
16
 
17
+ // Headless rendering — the supported entry point for a caller with no screen
18
+ export { renderHeadless, openHeadless, captureHeadless } from './Headless.js';
19
+
20
+ // Degradation contract — what the engine survived instead of failing on
21
+ export { ISSUE_CODES, ISSUE_SEVERITY, IssueLog, EngineIssueError } from './EngineIssues.js';
22
+
23
+ // Settings provenance + viewer-vs-physical tuning
24
+ export { SETTING_SOURCE } from './RenderSettings.js';
25
+
17
26
  // Asset URL / cache namespace overrides (call before constructing PathTracerApp)
18
27
  export { configureAssets, getAssetConfig } from './AssetConfig.js';
19
28
 
@@ -43,6 +52,8 @@ export {
43
52
  PRODUCTION_RENDER_CONFIG,
44
53
  INTERACTIVE_RENDER_CONFIG,
45
54
  MAX_RESERVABLE_RENDER_SIZE,
55
+ RENDER_PROFILES,
56
+ getRenderProfile,
46
57
  } from './EngineDefaults.js';
47
58
 
48
59
  // Settings & managers (for advanced consumers)