@preference-sl/pref-viewer 2.13.1-beta.0 → 2.14.0-beta.2
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/package.json +1 -1
- package/src/babylonjs-controller.js +321 -8
- package/src/pref-viewer-3d.js +11 -0
- package/src/pref-viewer.js +16 -1
package/package.json
CHANGED
|
@@ -141,9 +141,11 @@ export default class BabylonJSController {
|
|
|
141
141
|
// Render loop state balances performance with responsiveness.
|
|
142
142
|
render: {
|
|
143
143
|
isLoopRunning: false,
|
|
144
|
+
isIdle: false,
|
|
144
145
|
dirtyFrames: 0,
|
|
145
146
|
continuousUntil: 0,
|
|
146
147
|
lastRenderAt: 0,
|
|
148
|
+
lastActivityAt: 0,
|
|
147
149
|
},
|
|
148
150
|
// Resize state batches frequent ResizeObserver notifications.
|
|
149
151
|
resize: {
|
|
@@ -164,13 +166,259 @@ export default class BabylonJSController {
|
|
|
164
166
|
burstFramesEnhanced: 32, // when AA/SSAO/IBL is enabled, more frames are needed to reach stable output
|
|
165
167
|
interactionMs: 250,
|
|
166
168
|
animationMs: 200,
|
|
167
|
-
idleThrottleMs: 1000 / 15
|
|
169
|
+
idleThrottleMs: 1000 / 8, // Reduced from 15 FPS to 8 FPS for idle - reduces GPU/CPU load
|
|
170
|
+
idleThresholdMs: 3000, // Time before going to true idle (no renders)
|
|
168
171
|
},
|
|
169
172
|
resize: {
|
|
170
173
|
throttleMs: 50, // cap resize work to ~20 Hz while dragging/resizing containers
|
|
171
174
|
},
|
|
175
|
+
// Hardware-adaptive quality settings (applied based on GPU capabilities)
|
|
176
|
+
quality: {
|
|
177
|
+
// Settings for different hardware tiers: 'low', 'medium', 'high'
|
|
178
|
+
// Applied automatically based on GPU detected capabilities
|
|
179
|
+
tier: 'medium', // default, will be updated after hardware detection
|
|
180
|
+
shadowMapSize: 1024, // Standard shadow generator resolution
|
|
181
|
+
iblShadowResolution: 1, // IBL shadow resolution exponent (0=1024, 1=2048, 2=4096)
|
|
182
|
+
iblSampleDirections: 4, // IBL shadow quality
|
|
183
|
+
shadowBlurKernel: 16, // Shadow blur amount
|
|
184
|
+
ssaoEnabled: true, // Screen Space Ambient Occlusion
|
|
185
|
+
},
|
|
172
186
|
};
|
|
173
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Detects hardware capabilities and determines optimal quality tier.
|
|
190
|
+
* Uses WebGL renderer info, device type, and battery status to classify hardware.
|
|
191
|
+
* @private
|
|
192
|
+
* @returns {string} Hardware tier: 'low', 'medium', 'high', or 'ultra'
|
|
193
|
+
*/
|
|
194
|
+
#detectHardwareTier() {
|
|
195
|
+
// First, try to get WebGL info to help identify the device
|
|
196
|
+
const gl = this.#engine?.getRenderingCanvas()?.getContext?.('webgl2') || this.#engine?.getRenderingCanvas()?.getContext?.('webgl');
|
|
197
|
+
|
|
198
|
+
const debugInfo = gl?.getExtension('WEBGL_debug_renderer_info');
|
|
199
|
+
const rendererName = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : '';
|
|
200
|
+
|
|
201
|
+
// If we have WebGL renderer info and it looks like a desktop GPU, skip mobile detection
|
|
202
|
+
// Desktop GPUs typically have "NVIDIA", "AMD", "Intel" in the name
|
|
203
|
+
const isDesktopGPU = /NVIDIA|AMD|Intel|Radeon|GeForce|Quadro/i.test(rendererName);
|
|
204
|
+
|
|
205
|
+
// Also check if it's explicitly a mobile GPU
|
|
206
|
+
const isMobileGPU = /Adreno|Mali|Apple GPU/i.test(rendererName);
|
|
207
|
+
|
|
208
|
+
// If we detect a desktop GPU, go directly to desktop detection
|
|
209
|
+
if (isDesktopGPU && !isMobileGPU) {
|
|
210
|
+
console.log(`[PrefViewer] Desktop GPU detected: ${rendererName}`);
|
|
211
|
+
return this.#detectDesktopTier(gl, rendererName);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// If we detect a mobile GPU, it's definitely mobile
|
|
215
|
+
if (isMobileGPU) {
|
|
216
|
+
console.log(`[PrefViewer] Mobile GPU detected: ${rendererName} - Quality tier: low`);
|
|
217
|
+
return 'low';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// If no WebGL info or unclear, fall back to user agent detection
|
|
221
|
+
const mobileInfo = this.#detectMobileInfo();
|
|
222
|
+
|
|
223
|
+
// Only treat as mobile if we have strong mobile indicators
|
|
224
|
+
if (mobileInfo.isMobile && !mobileInfo.isDesktopLike) {
|
|
225
|
+
if (mobileInfo.isHighEnd) {
|
|
226
|
+
console.log(`[PrefViewer] High-end mobile detected: ${mobileInfo.deviceName} - Quality tier: medium`);
|
|
227
|
+
return 'medium';
|
|
228
|
+
} else {
|
|
229
|
+
console.log(`[PrefViewer] Mobile detected: ${mobileInfo.deviceName} - Quality tier: low`);
|
|
230
|
+
return 'low';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// If still unclear, default to desktop detection
|
|
235
|
+
console.log(`[PrefViewer] Using desktop GPU detection (unknown renderer: ${rendererName || 'none'})`);
|
|
236
|
+
return this.#detectDesktopTier(gl, rendererName);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Detects hardware tier for desktop GPUs based on renderer name and WebGL capabilities.
|
|
241
|
+
* @private
|
|
242
|
+
* @param {WebGLRenderingContext} gl - WebGL context
|
|
243
|
+
* @param {string} rendererName - GPU renderer name
|
|
244
|
+
* @returns {string} Hardware tier
|
|
245
|
+
*/
|
|
246
|
+
#detectDesktopTier(gl, rendererName) {
|
|
247
|
+
// Check WebGL capabilities
|
|
248
|
+
const maxTextureSize = gl?.getParameter(gl.MAX_TEXTURE_SIZE) || 8192;
|
|
249
|
+
|
|
250
|
+
// Detect high-end GPUs
|
|
251
|
+
const isAppleSilicon = /Apple|M1|M2|M3|M4/i.test(rendererName);
|
|
252
|
+
const isNvidiaHighEnd = /RTX|GeForce (3|4|5|6|7|8|9|40|50|60|70|80|90)|RTX A[5-9]|RTX Pro|Quadro/i.test(rendererName);
|
|
253
|
+
const isAmdHighEnd = /RX (6|7|8|9)[0-9]{2,}|RX Vega|Radeon Pro|WX [0-9]/i.test(rendererName);
|
|
254
|
+
const isIntelIntegrated = /Intel.*UHD|Intel.*Iris|Mesa|llvmpipe/i.test(rendererName);
|
|
255
|
+
|
|
256
|
+
let tier = 'high'; // Default to high for desktop
|
|
257
|
+
|
|
258
|
+
if (isIntelIntegrated || maxTextureSize < 8192) {
|
|
259
|
+
tier = 'medium';
|
|
260
|
+
} else if (isAppleSilicon || isNvidiaHighEnd || isAmdHighEnd || maxTextureSize >= 16384) {
|
|
261
|
+
tier = 'ultra';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log(`[PrefViewer] Desktop tier: ${tier} (GPU: ${rendererName || 'unknown'}, MaxTexture: ${maxTextureSize})`);
|
|
265
|
+
return tier;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Detects mobile device type and capabilities.
|
|
270
|
+
* Analyzes user agent, screen specs, and device performance indicators.
|
|
271
|
+
* @private
|
|
272
|
+
* @returns {Object} Mobile device info with tier classification
|
|
273
|
+
*/
|
|
274
|
+
#detectMobileInfo() {
|
|
275
|
+
const userAgent = navigator.userAgent || '';
|
|
276
|
+
const screenWidth = window.screen?.width || 0;
|
|
277
|
+
const screenHeight = window.screen?.height || 0;
|
|
278
|
+
const pixelRatio = window.devicePixelRatio || 1;
|
|
279
|
+
|
|
280
|
+
// Detect device type
|
|
281
|
+
const isIPhone = /iPhone/i.test(userAgent);
|
|
282
|
+
const isIPad = /iPad/i.test(userAgent);
|
|
283
|
+
const isAndroid = /Android/i.test(userAgent);
|
|
284
|
+
const isMobile = /Mobile/i.test(userAgent);
|
|
285
|
+
|
|
286
|
+
// High-end mobile patterns (recent flagship devices)
|
|
287
|
+
const highEndPatterns = [
|
|
288
|
+
/iPhone (1[4-9]|SE)/i, // iPhone 14+
|
|
289
|
+
/iPad Pro/i, // iPad Pro
|
|
290
|
+
/SM-S91[0-9]/i, // Samsung S21+
|
|
291
|
+
/SM-S9[0-9]/i, // Samsung S20+
|
|
292
|
+
/Pixel [7-9]/i, // Pixel 7+
|
|
293
|
+
/OnePlus [8-9|10|11]/i, // OnePlus 8+
|
|
294
|
+
/Xiaomi (1[2-4]|Mi 1[1-4])/i, // Xiaomi 12+
|
|
295
|
+
/Huawei (P50|Mate 4|)/i, // Huawei P50+
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
// Mid-range patterns
|
|
299
|
+
const midRangePatterns = [
|
|
300
|
+
/iPhone (1[0-3]|SE)/i, // iPhone 10-13
|
|
301
|
+
/iPad Air/i, // iPad Air
|
|
302
|
+
/SM-A[0-9]/i, // Samsung A series
|
|
303
|
+
/Redmi Note/i, // Redmi Note
|
|
304
|
+
/Pixel [4-6]/i, // Pixel 4-6
|
|
305
|
+
/Moto G/i, // Moto G
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
const isHighEnd = highEndPatterns.some(p => p.test(userAgent));
|
|
309
|
+
const isMidRange = midRangePatterns.some(p => p.test(userAgent));
|
|
310
|
+
|
|
311
|
+
// Detect tablet ONLY if explicitly in user agent (not just screen size)
|
|
312
|
+
// Many desktops have large screens and would incorrectly be detected as tablets
|
|
313
|
+
const isTablet = /iPad|Android.*Tablet|Nexus 9|Nexus 10/i.test(userAgent);
|
|
314
|
+
|
|
315
|
+
// Detect if mobile (phone form factor) - must have explicit mobile indicators
|
|
316
|
+
const isMobilePhone = /Android.*Mobile|iPhone|iPod/i.test(userAgent);
|
|
317
|
+
|
|
318
|
+
// Check if it looks like a desktop/laptop (has "Windows", "Mac", "Linux" but no mobile indicators)
|
|
319
|
+
const isDesktopLike = /Windows NT|Macintosh|Linux|X11/i.test(userAgent) && !isMobilePhone && !isTablet;
|
|
320
|
+
|
|
321
|
+
// Device name for logging
|
|
322
|
+
let deviceName = 'Unknown';
|
|
323
|
+
if (isIPhone) {
|
|
324
|
+
const match = userAgent.match(/iPhone.*OS (\d+)/);
|
|
325
|
+
deviceName = match ? `iPhone OS ${match[1]}` : 'iPhone';
|
|
326
|
+
} else if (isIPad) {
|
|
327
|
+
deviceName = 'iPad';
|
|
328
|
+
} else if (isAndroid) {
|
|
329
|
+
const match = userAgent.match(/Android.*;.*(.+?)\)/);
|
|
330
|
+
deviceName = match ? match[1].trim() : 'Android';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
isMobile: isMobilePhone && !isDesktopLike,
|
|
335
|
+
isTablet: isTablet && !isDesktopLike,
|
|
336
|
+
isDesktopLike,
|
|
337
|
+
isHighEnd: isHighEnd || (isTablet && pixelRatio >= 2),
|
|
338
|
+
isMidRange: isMidRange,
|
|
339
|
+
isLowEnd: !isHighEnd && !isMidRange,
|
|
340
|
+
deviceName,
|
|
341
|
+
screenWidth,
|
|
342
|
+
screenHeight,
|
|
343
|
+
pixelRatio,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Applies quality settings based on detected hardware tier.
|
|
349
|
+
* @private
|
|
350
|
+
* @param {string} tier - Hardware tier: 'low', 'medium', or 'high'
|
|
351
|
+
*/
|
|
352
|
+
#applyQualitySettings(tier) {
|
|
353
|
+
const qualityPresets = {
|
|
354
|
+
// Mobile low-end (old phones, budget devices)
|
|
355
|
+
low: {
|
|
356
|
+
shadowMapSize: 256, // Very small for speed
|
|
357
|
+
iblShadowResolution: -1, // Disable IBL shadows
|
|
358
|
+
iblSampleDirections: 1, // Minimal sampling
|
|
359
|
+
shadowBlurKernel: 0, // No blur
|
|
360
|
+
ssaoEnabled: false,
|
|
361
|
+
antialiasingEnabled: false,
|
|
362
|
+
hardwareScaling: 2.5, // Aggressive downscaling
|
|
363
|
+
maxModelResolution: 512, // Low-res model loading
|
|
364
|
+
},
|
|
365
|
+
// Mobile high-end / Tablet / Desktop low
|
|
366
|
+
medium: {
|
|
367
|
+
shadowMapSize: 512,
|
|
368
|
+
iblShadowResolution: 0, // 1024x1024
|
|
369
|
+
iblSampleDirections: 2, // Fast sampling
|
|
370
|
+
shadowBlurKernel: 4, // Light blur
|
|
371
|
+
ssaoEnabled: false, // Disable for mobile battery
|
|
372
|
+
antialiasingEnabled: true,
|
|
373
|
+
hardwareScaling: 2.0,
|
|
374
|
+
maxModelResolution: 1024,
|
|
375
|
+
},
|
|
376
|
+
// Desktop mid-range / High-end mobile
|
|
377
|
+
high: {
|
|
378
|
+
shadowMapSize: 1024,
|
|
379
|
+
iblShadowResolution: 0, // 1024x1024 - good balance
|
|
380
|
+
iblSampleDirections: 4, // Quality sampling
|
|
381
|
+
shadowBlurKernel: 16, // Smooth shadows
|
|
382
|
+
ssaoEnabled: true,
|
|
383
|
+
antialiasingEnabled: true,
|
|
384
|
+
hardwareScaling: 1.5,
|
|
385
|
+
maxModelResolution: 2048,
|
|
386
|
+
},
|
|
387
|
+
// Desktop high-end (RTX, Apple Silicon, etc.)
|
|
388
|
+
ultra: {
|
|
389
|
+
shadowMapSize: 2048,
|
|
390
|
+
iblShadowResolution: 1, // 2048x2048
|
|
391
|
+
iblSampleDirections: 8, // Maximum quality
|
|
392
|
+
shadowBlurKernel: 32, // Ultra smooth
|
|
393
|
+
ssaoEnabled: true,
|
|
394
|
+
antialiasingEnabled: true,
|
|
395
|
+
hardwareScaling: 1.0, // Native resolution
|
|
396
|
+
maxModelResolution: 4096,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// Map tier to preset with fallback
|
|
401
|
+
const presetMap = {
|
|
402
|
+
'low': qualityPresets.low,
|
|
403
|
+
'medium': qualityPresets.medium,
|
|
404
|
+
'high': qualityPresets.high,
|
|
405
|
+
'ultra': qualityPresets.ultra,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const preset = presetMap[tier] || qualityPresets.medium;
|
|
409
|
+
|
|
410
|
+
// Apply preset to config
|
|
411
|
+
this.#config.quality = {
|
|
412
|
+
tier,
|
|
413
|
+
...preset,
|
|
414
|
+
// Legacy compatibility
|
|
415
|
+
shadowMapSize: preset.shadowMapSize,
|
|
416
|
+
iblShadowResolution: preset.iblShadowResolution,
|
|
417
|
+
iblSampleDirections: preset.iblSampleDirections,
|
|
418
|
+
shadowBlurKernel: preset.shadowBlurKernel,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
174
422
|
// Promises to track async disable() lifecycle when XR and general teardown may run concurrently; ensures idempotent disable calls are safe and callers can await full teardown completion.
|
|
175
423
|
#disablingPromises = {
|
|
176
424
|
xr: null,
|
|
@@ -463,6 +711,7 @@ export default class BabylonJSController {
|
|
|
463
711
|
* Evaluates whether the renderer should stay in continuous mode.
|
|
464
712
|
* XR always forces continuous rendering; animation/camera motion also extends the
|
|
465
713
|
* continuous deadline window to avoid abrupt stop-start behavior.
|
|
714
|
+
* OPTIMIZATION: Added true idle mode - stops rendering completely after idleThresholdMs of inactivity.
|
|
466
715
|
* @private
|
|
467
716
|
* @param {number} now - Current high-resolution timestamp.
|
|
468
717
|
* @returns {boolean} True when continuous rendering should remain active.
|
|
@@ -470,6 +719,8 @@ export default class BabylonJSController {
|
|
|
470
719
|
#shouldRenderContinuously(now) {
|
|
471
720
|
const inXR = this.#XRExperience?.baseExperience?.state === WebXRState.IN_XR;
|
|
472
721
|
if (inXR) {
|
|
722
|
+
this.#state.render.isIdle = false;
|
|
723
|
+
this.#state.render.lastActivityAt = now;
|
|
473
724
|
return true;
|
|
474
725
|
}
|
|
475
726
|
|
|
@@ -478,18 +729,36 @@ export default class BabylonJSController {
|
|
|
478
729
|
|
|
479
730
|
if (animationRunning) {
|
|
480
731
|
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + this.#config.render.animationMs);
|
|
732
|
+
this.#state.render.isIdle = false;
|
|
733
|
+
this.#state.render.lastActivityAt = now;
|
|
481
734
|
}
|
|
482
735
|
if (cameraInMotion) {
|
|
483
736
|
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + this.#config.render.interactionMs);
|
|
737
|
+
this.#state.render.isIdle = false;
|
|
738
|
+
this.#state.render.lastActivityAt = now;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// OPTIMIZATION: True idle mode - if no activity for idleThresholdMs, stop rendering completely
|
|
742
|
+
const timeSinceActivity = now - this.#state.render.lastActivityAt;
|
|
743
|
+
if (timeSinceActivity > this.#config.render.idleThresholdMs) {
|
|
744
|
+
this.#state.render.isIdle = true;
|
|
484
745
|
}
|
|
485
746
|
|
|
486
|
-
|
|
747
|
+
const shouldRender = animationRunning || cameraInMotion || this.#state.render.continuousUntil > now;
|
|
748
|
+
|
|
749
|
+
// Reset idle flag if we need to render
|
|
750
|
+
if (shouldRender) {
|
|
751
|
+
this.#state.render.isIdle = false;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return shouldRender;
|
|
487
755
|
}
|
|
488
756
|
|
|
489
757
|
/**
|
|
490
758
|
* Render loop callback for Babylon.js.
|
|
491
759
|
* Runs only while scene state is dirty, interactive motion is active, animations are running, or XR is active.
|
|
492
|
-
* It self-stops when the scene becomes idle.
|
|
760
|
+
* It self-stops when the scene becomes idle (true idle mode - no renders at all).
|
|
761
|
+
* OPTIMIZATION: Added true idle mode that completely stops rendering after idleThresholdMs.
|
|
493
762
|
* @private
|
|
494
763
|
* @returns {void}
|
|
495
764
|
*/
|
|
@@ -503,6 +772,12 @@ export default class BabylonJSController {
|
|
|
503
772
|
const continuous = this.#shouldRenderContinuously(now);
|
|
504
773
|
const needsRender = continuous || this.#state.render.dirtyFrames > 0;
|
|
505
774
|
|
|
775
|
+
// OPTIMIZATION: True idle mode - stop rendering completely when idle
|
|
776
|
+
if (this.#state.render.isIdle && !needsRender) {
|
|
777
|
+
this.#stopEngineRenderLoop();
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
506
781
|
if (!needsRender) {
|
|
507
782
|
this.#stopEngineRenderLoop();
|
|
508
783
|
return;
|
|
@@ -1035,9 +1310,10 @@ export default class BabylonJSController {
|
|
|
1035
1310
|
|
|
1036
1311
|
const pipelineName = "PrefViewerIblShadowsRenderPipeline";
|
|
1037
1312
|
|
|
1313
|
+
// Use hardware-adaptive quality settings
|
|
1038
1314
|
const pipelineOptions = {
|
|
1039
|
-
resolutionExp:
|
|
1040
|
-
sampleDirections:
|
|
1315
|
+
resolutionExp: this.#config.quality.iblShadowResolution, // Adaptive: 0=1024, 1=2048, 2=4096
|
|
1316
|
+
sampleDirections: this.#config.quality.iblSampleDirections, // Adaptive: 2=fast, 4=quality
|
|
1041
1317
|
ssShadowsEnabled: true,
|
|
1042
1318
|
shadowRemanence: 0.85,
|
|
1043
1319
|
triPlanarVoxelization: true,
|
|
@@ -1108,6 +1384,7 @@ export default class BabylonJSController {
|
|
|
1108
1384
|
|
|
1109
1385
|
/**
|
|
1110
1386
|
* Configures soft shadows for the built-in directional light used when no HDR environment is present.
|
|
1387
|
+
* Uses hardware-adaptive quality settings.
|
|
1111
1388
|
* @private
|
|
1112
1389
|
* @returns {void}
|
|
1113
1390
|
*/
|
|
@@ -1116,9 +1393,12 @@ export default class BabylonJSController {
|
|
|
1116
1393
|
return;
|
|
1117
1394
|
}
|
|
1118
1395
|
this.#dirLight.autoUpdateExtends = false;
|
|
1119
|
-
|
|
1396
|
+
// Use quality-adaptive shadow map size
|
|
1397
|
+
const shadowMapSize = this.#config.quality.shadowMapSize;
|
|
1398
|
+
const shadowGenerator = new ShadowGenerator(shadowMapSize, this.#dirLight);
|
|
1120
1399
|
shadowGenerator.useBlurExponentialShadowMap = true;
|
|
1121
|
-
|
|
1400
|
+
// Use quality-adaptive blur kernel
|
|
1401
|
+
shadowGenerator.blurKernel = this.#config.quality.shadowBlurKernel;
|
|
1122
1402
|
shadowGenerator.darkness = 0.5;
|
|
1123
1403
|
shadowGenerator.bias = 0.0005;
|
|
1124
1404
|
shadowGenerator.normalBias = 0.02;
|
|
@@ -1499,6 +1779,7 @@ export default class BabylonJSController {
|
|
|
1499
1779
|
|
|
1500
1780
|
/**
|
|
1501
1781
|
* Handles mouse wheel events on the Babylon.js canvas for zooming the camera.
|
|
1782
|
+
* OPTIMIZATION: Tracks activity for idle mode.
|
|
1502
1783
|
* @private
|
|
1503
1784
|
* @param {WheelEvent} event - The mouse wheel event.
|
|
1504
1785
|
* @param {Object} pickInfo - The result of the scene pick operation (not used in this method).
|
|
@@ -1506,6 +1787,12 @@ export default class BabylonJSController {
|
|
|
1506
1787
|
*/
|
|
1507
1788
|
#onMouseWheel(event, pickInfo) {
|
|
1508
1789
|
event.preventDefault();
|
|
1790
|
+
|
|
1791
|
+
// OPTIMIZATION: Mark activity for idle mode
|
|
1792
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1793
|
+
this.#state.render.lastActivityAt = now;
|
|
1794
|
+
this.#state.render.isIdle = false;
|
|
1795
|
+
|
|
1509
1796
|
const camera = this.#scene?.activeCamera;
|
|
1510
1797
|
if (!camera) {
|
|
1511
1798
|
return false;
|
|
@@ -1527,14 +1814,20 @@ export default class BabylonJSController {
|
|
|
1527
1814
|
}
|
|
1528
1815
|
}
|
|
1529
1816
|
|
|
1530
|
-
|
|
1817
|
+
/**
|
|
1531
1818
|
* Handles pointer up events on the Babylon.js scene.
|
|
1819
|
+
* OPTIMIZATION: Tracks activity for idle mode.
|
|
1532
1820
|
* @private
|
|
1533
1821
|
* @param {PointerEvent} event - The pointer up event.
|
|
1534
1822
|
* @param {PickInfo} pickInfo - The result of the scene pick operation.
|
|
1535
1823
|
* @returns {void}
|
|
1536
1824
|
*/
|
|
1537
1825
|
#onPointerUp(event, pickInfo) {
|
|
1826
|
+
// OPTIMIZATION: Mark activity for idle mode
|
|
1827
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1828
|
+
this.#state.render.lastActivityAt = now;
|
|
1829
|
+
this.#state.render.isIdle = false;
|
|
1830
|
+
|
|
1538
1831
|
if (this.#babylonJSAnimationController) {
|
|
1539
1832
|
if (event.button !== 2 || !pickInfo || !pickInfo.pickedMesh) {
|
|
1540
1833
|
this.#babylonJSAnimationController.hideMenu();
|
|
@@ -1548,12 +1841,18 @@ export default class BabylonJSController {
|
|
|
1548
1841
|
|
|
1549
1842
|
/**
|
|
1550
1843
|
* Handles pointer move events on the Babylon.js scene.
|
|
1844
|
+
* OPTIMIZATION: Tracks activity for idle mode.
|
|
1551
1845
|
* @private
|
|
1552
1846
|
* @param {PointerEvent} event - The pointer move event.
|
|
1553
1847
|
* @param {PickInfo|null} pickInfo - The sampled result of the scene pick operation (may be null when sampling skips a raycast).
|
|
1554
1848
|
* @returns {void}
|
|
1555
1849
|
*/
|
|
1556
1850
|
#onPointerMove(event, pickInfo) {
|
|
1851
|
+
// OPTIMIZATION: Mark activity for idle mode
|
|
1852
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1853
|
+
this.#state.render.lastActivityAt = now;
|
|
1854
|
+
this.#state.render.isIdle = false;
|
|
1855
|
+
|
|
1557
1856
|
const camera = this.#scene?.activeCamera;
|
|
1558
1857
|
if (camera && !camera.metadata?.locked) {
|
|
1559
1858
|
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
@@ -2420,6 +2719,20 @@ export default class BabylonJSController {
|
|
|
2420
2719
|
this.#configureDracoCompression();
|
|
2421
2720
|
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
|
|
2422
2721
|
this.#engine.disableUniformBuffers = true;
|
|
2722
|
+
|
|
2723
|
+
// OPTIMIZATION: Detect hardware tier and apply quality settings
|
|
2724
|
+
const detectedTier = this.#detectHardwareTier();
|
|
2725
|
+
this.#applyQualitySettings(detectedTier);
|
|
2726
|
+
|
|
2727
|
+
// OPTIMIZATION: Apply hardware scaling from quality preset
|
|
2728
|
+
const pixelRatio = typeof window !== 'undefined' ? window.devicePixelRatio : 1;
|
|
2729
|
+
const baseScaling = this.#config.quality.hardwareScaling || 1;
|
|
2730
|
+
const scalingLevel = pixelRatio > 1 ? baseScaling * pixelRatio : baseScaling;
|
|
2731
|
+
this.#engine.setHardwareScalingLevel(scalingLevel);
|
|
2732
|
+
|
|
2733
|
+
// OPTIMIZATION: Initialize idle tracking
|
|
2734
|
+
this.#state.render.lastActivityAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2735
|
+
|
|
2423
2736
|
this.#scene = new Scene(this.#engine);
|
|
2424
2737
|
|
|
2425
2738
|
// Activate the rendering of geometry data into a G-buffer, essential for advanced effects like deferred shading,
|
package/src/pref-viewer-3d.js
CHANGED
|
@@ -594,6 +594,11 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
594
594
|
|
|
595
595
|
const loadDetail = await this.#babylonJSController.load(forceReload);
|
|
596
596
|
|
|
597
|
+
// Apply initial render settings if provided in options
|
|
598
|
+
if (config.options?.render) {
|
|
599
|
+
await this.applyRenderSettings(config.options.render);
|
|
600
|
+
}
|
|
601
|
+
|
|
597
602
|
return { ...loadDetail, load: this.#onLoaded() };
|
|
598
603
|
}
|
|
599
604
|
|
|
@@ -622,6 +627,12 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
622
627
|
if (needUpdateIBL) {
|
|
623
628
|
someSetted = someSetted || (await this.#babylonJSController.setIBLOptions());
|
|
624
629
|
}
|
|
630
|
+
|
|
631
|
+
// Apply render settings if provided
|
|
632
|
+
if (options.render) {
|
|
633
|
+
await this.applyRenderSettings(options.render);
|
|
634
|
+
}
|
|
635
|
+
|
|
625
636
|
const detail = this.#onSetOptions();
|
|
626
637
|
return { success: someSetted, detail: detail };
|
|
627
638
|
}
|
package/src/pref-viewer.js
CHANGED
|
@@ -106,7 +106,7 @@ export default class PrefViewer extends HTMLElement {
|
|
|
106
106
|
* @returns {string[]} Array of attribute names to observe.
|
|
107
107
|
*/
|
|
108
108
|
static get observedAttributes() {
|
|
109
|
-
return ["config", "culture", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene"];
|
|
109
|
+
return ["config", "culture", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene", "show-menu"];
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
@@ -174,6 +174,10 @@ export default class PrefViewer extends HTMLElement {
|
|
|
174
174
|
const showScene = value.toLowerCase() === "true";
|
|
175
175
|
showScene ? this.showScene() : this.hideScene();
|
|
176
176
|
break;
|
|
177
|
+
case "show-menu":
|
|
178
|
+
// Recreate menu to apply show/hide setting
|
|
179
|
+
this.#createMenu3D();
|
|
180
|
+
break;
|
|
177
181
|
}
|
|
178
182
|
}
|
|
179
183
|
|
|
@@ -271,6 +275,7 @@ export default class PrefViewer extends HTMLElement {
|
|
|
271
275
|
/**
|
|
272
276
|
* Creates (or recreates) the PrefViewerMenu3D element, wires hover/apply handlers, and syncs it with the 3D component.
|
|
273
277
|
* When a menu already exists it is removed so the DOM stays clean.
|
|
278
|
+
* Respects the "show-menu" attribute to determine if menu should be visible.
|
|
274
279
|
* @private
|
|
275
280
|
* @returns {void}
|
|
276
281
|
*/
|
|
@@ -282,6 +287,16 @@ export default class PrefViewer extends HTMLElement {
|
|
|
282
287
|
this.#menu3D.removeEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
|
|
283
288
|
this.#menu3D.remove();
|
|
284
289
|
}
|
|
290
|
+
|
|
291
|
+
// Check if menu should be shown (default: true if not specified)
|
|
292
|
+
const showMenuAttr = this.getAttribute("show-menu");
|
|
293
|
+
const showMenu = showMenuAttr === null || showMenuAttr === "true";
|
|
294
|
+
|
|
295
|
+
if (!showMenu) {
|
|
296
|
+
this.#menu3D = null;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
285
300
|
this.#menu3D = document.createElement("pref-viewer-menu-3d");
|
|
286
301
|
if (typeof this.#menu3D.setCulture === "function") {
|
|
287
302
|
this.#menu3D.setCulture(this.#culture);
|