@needle-tools/engine 4.10.0-next.f0ec242 → 4.10.0-next.f5ce0ae

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 (122) hide show
  1. package/components.needle.json +1 -1
  2. package/dist/{needle-engine.bundle-BC-0Ex9m.js → needle-engine.bundle-DSzlnhCs.js} +7388 -7113
  3. package/dist/{needle-engine.bundle-dgNq9Vsa.umd.cjs → needle-engine.bundle-VqrrECGF.umd.cjs} +153 -140
  4. package/dist/needle-engine.bundle-pI8o_eru.min.js +1652 -0
  5. package/dist/needle-engine.d.ts +15 -15
  6. package/dist/needle-engine.js +259 -257
  7. package/dist/needle-engine.min.js +1 -1
  8. package/dist/needle-engine.umd.cjs +1 -1
  9. package/dist/vendor-CPuBPspY.umd.cjs +1121 -0
  10. package/dist/vendor-DPCU8cUF.min.js +1121 -0
  11. package/dist/vendor-MBoqSyFm.js +16240 -0
  12. package/lib/engine/codegen/register_types.js +2 -0
  13. package/lib/engine/codegen/register_types.js.map +1 -1
  14. package/lib/engine/engine_camera.d.ts +7 -1
  15. package/lib/engine/engine_camera.js +46 -6
  16. package/lib/engine/engine_camera.js.map +1 -1
  17. package/lib/engine/engine_context.d.ts +6 -0
  18. package/lib/engine/engine_context.js +48 -9
  19. package/lib/engine/engine_context.js.map +1 -1
  20. package/lib/engine/engine_gizmos.d.ts +11 -10
  21. package/lib/engine/engine_gizmos.js +24 -10
  22. package/lib/engine/engine_gizmos.js.map +1 -1
  23. package/lib/engine/engine_license.js +1 -1
  24. package/lib/engine/engine_license.js.map +1 -1
  25. package/lib/engine/engine_lightdata.d.ts +3 -3
  26. package/lib/engine/engine_lightdata.js +10 -10
  27. package/lib/engine/engine_lightdata.js.map +1 -1
  28. package/lib/engine/engine_physics_rapier.js +4 -0
  29. package/lib/engine/engine_physics_rapier.js.map +1 -1
  30. package/lib/engine/engine_scenelighting.d.ts +1 -1
  31. package/lib/engine/engine_scenelighting.js +4 -5
  32. package/lib/engine/engine_scenelighting.js.map +1 -1
  33. package/lib/engine/engine_utils.d.ts +3 -1
  34. package/lib/engine/engine_utils.js +11 -0
  35. package/lib/engine/engine_utils.js.map +1 -1
  36. package/lib/engine/extensions/NEEDLE_lightmaps.js +1 -1
  37. package/lib/engine/extensions/NEEDLE_lightmaps.js.map +1 -1
  38. package/lib/engine/extensions/extension_utils.js +1 -1
  39. package/lib/engine/extensions/extension_utils.js.map +1 -1
  40. package/lib/engine/webcomponents/logo-element.d.ts +1 -1
  41. package/lib/engine/webcomponents/logo-element.js +29 -5
  42. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  43. package/lib/engine/webcomponents/needle menu/needle-menu.js +4 -3
  44. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  45. package/lib/engine/webcomponents/needle-engine.js +22 -0
  46. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  47. package/lib/engine/webcomponents/needle-engine.loading.d.ts +0 -1
  48. package/lib/engine/webcomponents/needle-engine.loading.js +3 -36
  49. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  50. package/lib/engine/xr/NeedleXRController.d.ts +3 -3
  51. package/lib/engine/xr/NeedleXRController.js +28 -0
  52. package/lib/engine/xr/NeedleXRController.js.map +1 -1
  53. package/lib/engine-components/CameraUtils.js +2 -1
  54. package/lib/engine-components/CameraUtils.js.map +1 -1
  55. package/lib/engine-components/Renderer.js +6 -1
  56. package/lib/engine-components/Renderer.js.map +1 -1
  57. package/lib/engine-components/Skybox.js +22 -4
  58. package/lib/engine-components/Skybox.js.map +1 -1
  59. package/lib/engine-components/codegen/components.d.ts +1 -0
  60. package/lib/engine-components/codegen/components.js +1 -0
  61. package/lib/engine-components/codegen/components.js.map +1 -1
  62. package/lib/engine-components/debug/LogStats.d.ts +1 -0
  63. package/lib/engine-components/debug/LogStats.js +1 -0
  64. package/lib/engine-components/debug/LogStats.js.map +1 -1
  65. package/lib/engine-components/timeline/PlayableDirector.d.ts +7 -0
  66. package/lib/engine-components/timeline/PlayableDirector.js +8 -1
  67. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  68. package/lib/engine-components/timeline/TimelineModels.d.ts +9 -1
  69. package/lib/engine-components/timeline/TimelineTracks.d.ts +2 -1
  70. package/lib/engine-components/timeline/TimelineTracks.js +30 -25
  71. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  72. package/lib/engine-components/utils/LookAt.js +5 -1
  73. package/lib/engine-components/utils/LookAt.js.map +1 -1
  74. package/lib/engine-components/web/Clickthrough.js +10 -2
  75. package/lib/engine-components/web/Clickthrough.js.map +1 -1
  76. package/lib/engine-components/web/ScrollFollow.d.ts +23 -0
  77. package/lib/engine-components/web/ScrollFollow.js +168 -42
  78. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  79. package/lib/engine-components/web/ViewBox.d.ts +27 -0
  80. package/lib/engine-components/web/ViewBox.js +242 -0
  81. package/lib/engine-components/web/ViewBox.js.map +1 -0
  82. package/lib/engine-components/web/index.d.ts +1 -0
  83. package/lib/engine-components/web/index.js +1 -0
  84. package/lib/engine-components/web/index.js.map +1 -1
  85. package/lib/engine-components-experimental/Presentation.d.ts +1 -0
  86. package/lib/engine-components-experimental/Presentation.js +1 -0
  87. package/lib/engine-components-experimental/Presentation.js.map +1 -1
  88. package/package.json +2 -1
  89. package/src/engine/codegen/register_types.ts +2 -0
  90. package/src/engine/engine_camera.ts +61 -9
  91. package/src/engine/engine_context.ts +50 -10
  92. package/src/engine/engine_gizmos.ts +37 -23
  93. package/src/engine/engine_license.ts +1 -1
  94. package/src/engine/engine_lightdata.ts +11 -11
  95. package/src/engine/engine_physics_rapier.ts +3 -0
  96. package/src/engine/engine_scenelighting.ts +5 -6
  97. package/src/engine/engine_utils.ts +12 -0
  98. package/src/engine/extensions/NEEDLE_lightmaps.ts +1 -1
  99. package/src/engine/extensions/extension_utils.ts +1 -1
  100. package/src/engine/webcomponents/logo-element.ts +29 -4
  101. package/src/engine/webcomponents/needle menu/needle-menu.ts +4 -3
  102. package/src/engine/webcomponents/needle-engine.loading.ts +32 -32
  103. package/src/engine/webcomponents/needle-engine.ts +33 -6
  104. package/src/engine/xr/NeedleXRController.ts +36 -4
  105. package/src/engine-components/CameraUtils.ts +1 -1
  106. package/src/engine-components/Renderer.ts +6 -1
  107. package/src/engine-components/Skybox.ts +26 -7
  108. package/src/engine-components/codegen/components.ts +1 -0
  109. package/src/engine-components/debug/LogStats.ts +1 -0
  110. package/src/engine-components/timeline/PlayableDirector.ts +10 -1
  111. package/src/engine-components/timeline/TimelineModels.ts +9 -1
  112. package/src/engine-components/timeline/TimelineTracks.ts +30 -25
  113. package/src/engine-components/utils/LookAt.ts +5 -1
  114. package/src/engine-components/web/Clickthrough.ts +11 -2
  115. package/src/engine-components/web/ScrollFollow.ts +204 -51
  116. package/src/engine-components/web/ViewBox.ts +262 -0
  117. package/src/engine-components/web/index.ts +2 -1
  118. package/src/engine-components-experimental/Presentation.ts +1 -0
  119. package/dist/needle-engine.bundle-BSh7dSEx.min.js +0 -1639
  120. package/dist/vendor-D0Yvltn9.umd.cjs +0 -1121
  121. package/dist/vendor-DU8tJyl_.js +0 -14366
  122. package/dist/vendor-JyrX4DVM.min.js +0 -1121
@@ -1,10 +1,12 @@
1
+ // For firefox ViewTimeline support
2
+ import "scroll-timeline-polyfill/dist/scroll-timeline.js";
3
+
1
4
  import { Box3, Object3D } from "three";
2
- import { element } from "three/src/nodes/TSL.js";
3
5
 
4
- import { Context } from "../../engine/engine_context.js";
6
+ import { isDevEnvironment } from "../../engine/debug/debug.js";
5
7
  import { Mathf } from "../../engine/engine_math.js";
6
8
  import { serializable } from "../../engine/engine_serialization.js";
7
- import { getBoundingBox } from "../../engine/engine_three_utils.js";
9
+ import { getBoundingBox, setVisibleInCustomShadowRendering } from "../../engine/engine_three_utils.js";
8
10
  import { getParam } from "../../engine/engine_utils.js";
9
11
  import { Animation } from "../Animation.js";
10
12
  import { Animator } from "../Animator.js";
@@ -150,7 +152,8 @@ export class ScrollFollow extends Behaviour {
150
152
  }
151
153
  }
152
154
 
153
- if (this._current_value !== this._appliedValue) {
155
+ // if (this._current_value !== this._appliedValue)
156
+ {
154
157
  this._appliedValue = this._current_value;
155
158
 
156
159
  let defaultPrevented = false;
@@ -172,8 +175,8 @@ export class ScrollFollow extends Behaviour {
172
175
 
173
176
  const value = this.invert ? 1 - this._current_value : this._current_value;
174
177
 
175
- const height = this._rangeEndValue - this._rangeStartValue;
176
- const pixelValue = this._rangeStartValue + value * height;
178
+ // const height = this._rangeEndValue - this._rangeStartValue;
179
+ // const pixelValue = this._rangeStartValue + value * height;
177
180
 
178
181
  // apply scroll to target(s)
179
182
  if (Array.isArray(this.target)) {
@@ -184,7 +187,7 @@ export class ScrollFollow extends Behaviour {
184
187
  }
185
188
 
186
189
  if (debug && this.context.time.frame % 30 === 0) {
187
- console.debug(`[ScrollFollow] ${this._current_value.toFixed(5)} — ${(this._target_value * 100).toFixed(0)}%`);
190
+ console.debug(`[ScrollFollow] ${this._current_value.toFixed(5)} — ${(this._target_value * 100).toFixed(0)}%, targets [${Array.isArray(this.target) ? this.target.length : 1}]`);
188
191
  }
189
192
  }
190
193
  }
@@ -242,7 +245,8 @@ export class ScrollFollow extends Behaviour {
242
245
 
243
246
  if (target instanceof PlayableDirector) {
244
247
  this.handleTimelineTarget(target, value);
245
- if (!target.isPlaying) target.evaluate();
248
+ if (target.isPlaying) target.pause();
249
+ target.evaluate();
246
250
  }
247
251
  else if (target instanceof Animator) {
248
252
  target.setFloat("scroll", value);
@@ -292,18 +296,37 @@ export class ScrollFollow extends Behaviour {
292
296
  let scrollRegionEnd = 0;
293
297
  markersArray.length = 0;
294
298
 
295
- for (const marker of director.foreachMarker<ScrollMarkerModel & { element?: HTMLElement | null, needsUpdate?: boolean }>("ScrollMarker")) {
299
+ // querySelectorResults.length = 0;
300
+ let markerIndex = 0;
301
+
302
+ // https://scroll-driven-animations.style/tools/view-timeline/ranges
303
+ for (const marker of director.foreachMarker<ScrollMarkerModel & { element?: HTMLElement | null, needsUpdate?: boolean, timeline?: ViewTimeline }>("ScrollMarker")) {
304
+
305
+ const index = markerIndex++;
296
306
 
297
307
  // Get marker elements from DOM
298
- if (marker.selector?.length && (marker.element === undefined || marker.needsUpdate === true || /** element is not in DOM anymore? */ (!marker.element?.parentNode))) {
308
+ if ((marker.element === undefined || marker.needsUpdate === true || /** element is not in DOM anymore? */ (marker.element && !marker.element?.parentNode))) {
299
309
  marker.needsUpdate = false;
300
310
  try {
301
- marker.element = document.querySelector<HTMLElement>(marker.selector) || null;
302
- if (debug) console.debug("ScrollMarker found on page", marker.element, marker.selector);
311
+ // TODO: with this it's currently not possible to remap markers from HTML. For example if I have two sections and I want to now use the marker["center"] multiple times to stay at that marker for a longer time
312
+ marker.element = tryGetElementsForSelector(index, marker.name) as HTMLElement | null;
313
+ if (debug) console.debug(`ScrollMarker #${index} "${marker.name}" (${marker.time.toFixed(2)}) found`, marker.element);
314
+ if (!marker.element) {
315
+ marker.timeline = undefined;
316
+ if (debug || isDevEnvironment()) console.warn(`No HTML element found for ScrollMarker: ${marker.name} (index ${index})`);
317
+ continue;
318
+ }
319
+ else {
320
+ /** @ts-ignore */
321
+ marker.timeline = new ViewTimeline({
322
+ subject: marker.element,
323
+ axis: 'block', // https://drafts.csswg.org/scroll-animations/#scroll-notation
324
+ });
325
+ }
303
326
  }
304
327
  catch (error) {
305
328
  marker.element = null;
306
- console.error("ScrollMarker selector is not valid: " + marker.selector + "\n", error);
329
+ console.error("ScrollMarker selector is not valid: " + marker.name + "\n", error);
307
330
  }
308
331
  }
309
332
 
@@ -330,65 +353,195 @@ export class ScrollFollow extends Behaviour {
330
353
 
331
354
  weightsArray.length = 0;
332
355
  let sum = 0;
356
+ const oneFrameTime = 1 / 60;
333
357
 
358
+ // We keep a separate count here in case there are some markers that could not be resolved so point to *invalid* elements - the timeline should fallback to 0-1 scroll behaviour then
334
359
  let markerCount = 0;
335
- for (const marker of markersArray) {
336
-
360
+ for (let i = 0; i < markersArray.length; i++) {
361
+ const marker = markersArray[i];
337
362
  if (!marker.element) continue;
338
-
339
- const top = marker.element.offsetTop;
340
- const height = marker.element.offsetHeight;
341
- const bottom = top + height;
342
- let overlap = 0;
343
-
344
- // TODO: if we have two marker sections where no HTML overlaps (vor example because some large section is between them) we probably want to still virtually interpolate between them slowly in that region
345
-
346
- if (bottom < currentTop) {
347
- // marker is above scroll region
348
- overlap = 0;
349
- }
350
- else if (top > currentBottom) {
351
- // marker is below scroll region
352
- overlap = 0;
353
- }
354
- else {
355
- // calculate overlap in pixels
356
- const overlapTop = Math.max(top, currentTop);
357
- const overlapBottom = Math.min(bottom, currentBottom);
358
- overlap = Math.max(0, overlapBottom - overlapTop);
359
- // console.log(marker.element.className, overlap)
360
- }
361
-
362
- if (overlap > 0) {
363
- weightsArray.push({ time: marker.time, weight: overlap });
364
- sum += overlap;
363
+ const nextMarker = markersArray[i + 1];
364
+
365
+ const nextTime = nextMarker
366
+ ? (nextMarker.time - oneFrameTime)
367
+ : duration;
368
+
369
+ markerCount += 1;
370
+
371
+ const timeline = marker.timeline;
372
+ if (timeline) {
373
+ const time01 = calculateTimelinePositionNormalized(timeline);
374
+ // remap 0-1 to 0 - 1 - 0 (full weight at center)
375
+ const weight = 1 - Math.abs(time01 - 0.5) * 2;
376
+ const name = marker.name || `marker${i}`;
377
+ if (time01 > 0 && time01 <= 1) {
378
+ const lerpTime = marker.time + (nextTime - marker.time) * time01;
379
+ weightsArray.push({ name, time: lerpTime, weight: weight });
380
+ sum += weight;
381
+ }
382
+ // Before the first marker is reached
383
+ else if (i === 0 && time01 <= 0) {
384
+ weightsArray.push({ name, time: 0, weight: 1 });
385
+ sum += 1;
386
+ }
387
+ // After the last marker is reached
388
+ else if (i === markersArray.length - 1 && time01 >= 1) {
389
+ weightsArray.push({ name, time: duration, weight: 1 });
390
+ sum += 1;
391
+ }
365
392
  }
393
+ continue;
394
+ // if(this.context.time.frame % 10 === 0) console.log(marker.element?.className, timeline, calculateTimelinePositionNormalized(timeline!));
395
+
396
+ // const top = marker.element.offsetTop - this._scrollContainerHeight;
397
+ // const height = marker.element.offsetHeight + this._scrollContainerHeight;
398
+ // const bottom = top + height;
399
+ // let overlap = 0;
400
+
401
+ // // TODO: if we have two marker sections where no HTML overlaps (vor example because some large section is between them) we probably want to still virtually interpolate between them slowly in that region
402
+
403
+ // if (bottom < currentTop) {
404
+ // // marker is above scroll region
405
+ // overlap = 0;
406
+ // }
407
+ // else if (top > currentBottom) {
408
+ // // marker is below scroll region
409
+ // overlap = 0;
410
+ // }
411
+ // else {
412
+ // // calculate overlap in pixels
413
+ // const overlapTop = Math.max(top, currentTop);
414
+ // const overlapBottom = Math.min(bottom, currentBottom);
415
+ // const height = Math.max(1, currentBottom - currentTop);
416
+ // overlap = Math.max(0, overlapBottom - overlapTop);
417
+ // }
418
+
419
+ // // if(this.context.time.frame % 20 === 0) console.log(overlap)
420
+
421
+ // if (overlap > 0) {
422
+ // weightsArray.push({ time: marker.time, weight: overlap });
423
+ // sum += overlap;
424
+ // }
366
425
  }
367
426
 
368
- if (weightsArray.length <= 0 && markersArray.length <= 0) {
427
+ if (weightsArray.length <= 0 && markerCount <= 0) {
369
428
  director.time = value * duration;
370
429
  }
371
430
  else if (weightsArray.length > 0) {
372
431
  // normalize and calculate weighted time
373
- let time = weightsArray[0].time;
374
- for (const o of weightsArray) {
375
- const weight = o.weight / Math.max(0.00001, sum);
376
- // lerp time based on weight
377
- const diff = Math.abs(o.time - time);
378
- time += diff * weight;
432
+ let time = weightsArray[0].time; // fallback to first time
433
+ if (weightsArray.length > 1) {
434
+ for (const entry of weightsArray) {
435
+ const weight = entry.weight / Math.max(0.00001, sum);
436
+ // console.log(weight.toFixed(2))
437
+ // lerp time based on weight
438
+ const diff = Math.abs(entry.time - time);
439
+ time += diff * weight;
440
+ }
441
+ }
442
+ if (this.damping <= 0) {
443
+ director.time = time;
444
+ }
445
+ else {
446
+ director.time = Mathf.lerp(director.time, time, this.context.time.deltaTime / this.damping);
447
+ }
448
+
449
+ if (debug && this.context.time.frame % 30 === 0) {
450
+ console.log(`[ScrollFollow ] Timeline ${director.name}: ${time.toFixed(3)}`, weightsArray.map(w => `[${w.name} ${(w.weight * 100).toFixed(0)}%]`).join(", "));
379
451
  }
380
- director.time = time;
381
452
  }
382
453
  }
383
454
 
384
455
  }
385
456
 
457
+
458
+
386
459
  const weightsArray: OverlapInfo[] = [];
387
- const markersArray: (ScrollMarkerModel & { element?: HTMLElement | null })[] = [];
460
+ const markersArray: Array<ScrollMarkerModel & {
461
+ element?: HTMLElement | null,
462
+ timeline?: ViewTimeline,
463
+ }> = [];
388
464
 
389
465
  type OverlapInfo = {
466
+ name: string,
390
467
  /** Marker time */
391
468
  time: number,
392
469
  /** Overlap in pixels */
393
470
  weight: number,
471
+ }
472
+
473
+
474
+ // type SelectorCache = {
475
+ // /** The selector used to query the *elements */
476
+ // selector: string,
477
+ // elements: Element[] | null,
478
+ // usedElementCount: number,
479
+ // }
480
+ // const querySelectorResults: Array<SelectorCache> = [];
481
+
482
+ const needleScrollMarkerIndexCache = new Map<number, Element | null>();
483
+ const needleScrollMarkerNameCache = new Map<string, Element | null>();
484
+ let needsScrollMarkerRefresh = true;
485
+
486
+ function tryGetElementsForSelector(index: number, name: string, _cycle: number = 0): Element | null {
487
+
488
+ if (!needsScrollMarkerRefresh) {
489
+ if (name?.length) {
490
+ const element = needleScrollMarkerNameCache.get(name) || null;
491
+ if (element) return element;
492
+ // const isNumber = !isNaN(Number(name));
493
+ // if (!isNumber) {
494
+ // }
495
+ }
496
+ const element = needleScrollMarkerIndexCache.get(index) || null;
497
+ const value = element?.getAttribute("data-timeline-marker");
498
+ // if (value?.length) {
499
+ // if (cycle === 0) {
500
+ // // if the HTML marker we found by index does define a different marker name we try to find the correct HTML element by name
501
+ // return tryGetElementsForSelector(index, value, 1);
502
+ // }
503
+ // if (isDevEnvironment()) console.warn(`ScrollMarker name mismatch: expected "${name}", got "${value}"`);
504
+ // }
505
+ return element;
506
+ }
507
+ needsScrollMarkerRefresh = false;
508
+ needleScrollMarkerIndexCache.clear();
509
+ const markers = document.querySelectorAll(`[data-timeline-marker]`);
510
+ markers.forEach((m, i) => {
511
+ needleScrollMarkerIndexCache.set(i, m);
512
+ const name = m.getAttribute("data-timeline-marker");
513
+ if (name?.length) needleScrollMarkerNameCache.set(name, m);
514
+ });
515
+ needsScrollMarkerRefresh = false;
516
+ return tryGetElementsForSelector(index, name);
517
+ }
518
+
519
+
520
+ // #region ScrollTimeline
521
+
522
+ function calculateTimelinePositionNormalized(timeline: ViewTimeline) {
523
+ if (!timeline.source) return 0;
524
+ const currentTime = timeline.currentTime;
525
+ const duration = timeline.duration;
526
+ let durationValue = 1;
527
+ if (duration.unit === "seconds") {
528
+ durationValue = duration.value;
529
+ }
530
+ else if (duration.unit === "percent") {
531
+ durationValue = duration.value;
532
+ }
533
+ const t01 = currentTime.unit === "seconds" ? (currentTime.value / durationValue) : (currentTime.value / 100);
534
+ return t01;
535
+ }
536
+
537
+
538
+ declare global {
539
+ interface ViewTimeline {
540
+ axis: 'block' | 'inline';
541
+ currentTime: { unit: 'seconds' | 'percent', value: number };
542
+ duration: { unit: 'seconds' | 'percent', value: number };
543
+ source: Element | null;
544
+ startOffset: { unit: 'px', value: number };
545
+ endOffset: { unit: 'px', value: number };
546
+ }
394
547
  }
@@ -0,0 +1,262 @@
1
+ import { Camera, PerspectiveCamera, Quaternion, Scene, Vector2, Vector3 } from "three";
2
+
3
+ import { isDevEnvironment } from "../../engine/debug/debug.js";
4
+ import { Gizmos } from "../../engine/engine_gizmos.js";
5
+ import { serializable } from "../../engine/engine_serialization_decorator.js";
6
+ import { getTempVector } from "../../engine/engine_three_utils.js";
7
+ import { registerType } from "../../engine/engine_typestore.js";
8
+ import { getParam } from "../../engine/engine_utils.js";
9
+ import { RGBAColor } from "../../engine/js-extensions/RGBAColor.js";
10
+ import { Behaviour } from "../Component.js";
11
+
12
+
13
+ const debugParam = getParam("debugviewbox");
14
+ const disabledGizmoColor = new RGBAColor(.5, .5, .5, .5);
15
+
16
+ /**
17
+ * This component can be used to automatically fit a certain box area into the camera view - no matter your screen size or aspect ratio.
18
+ *
19
+ * Add the ViewBox to an object into your scene
20
+ */
21
+ @registerType
22
+ export class ViewBox extends Behaviour {
23
+
24
+ static readonly instances: ViewBox[] = [];
25
+
26
+ /**
27
+ * The reference field of view is used to calculate the box size. This should usually be the same as your camera's fov.
28
+ * @default undefined (meaning it will use the camera fov on the first frame)
29
+ */
30
+ @serializable()
31
+ referenceFieldOfView: number | undefined = undefined;
32
+
33
+ /**
34
+ * Enable debug logs and rendering for this component instance
35
+ */
36
+ @serializable()
37
+ debug: boolean = false;
38
+
39
+ onEnable(): void {
40
+ if (debugParam || this.debug || isDevEnvironment()) console.debug("[ViewBox] Using camera fov:", this.referenceFieldOfView);
41
+ // register instance
42
+ ViewBox.instances.push(this);
43
+ }
44
+
45
+ onDisable(): void {
46
+ if (debugParam || this.debug) console.debug("[ViewBox] Disabled");
47
+ // unregister instance
48
+ const idx = ViewBox.instances.indexOf(this);
49
+ if (idx !== -1) ViewBox.instances.splice(idx, 1);
50
+ this._projectedBoxElement?.remove();
51
+ }
52
+
53
+ onBeforeRender(): void {
54
+ if (this.context.isInXR) return;
55
+ if (this.destroyed) return;
56
+ const isActive = ViewBox.instances[ViewBox.instances.length - 1] === this;
57
+ if (!isActive) {
58
+ if (debugParam || this.debug) {
59
+ Gizmos.DrawWireBox(this.gameObject.worldPosition, this.gameObject.worldScale, disabledGizmoColor);
60
+ }
61
+ return;
62
+ }
63
+ if (debugParam || this.debug) Gizmos.DrawWireBox(this.gameObject.worldPosition, this.gameObject.worldScale, 0xdddd00, 0, true, this.gameObject.worldQuaternion);
64
+
65
+ // calculate box size to fit the camera frustrum size at the current position (just scale)
66
+ const camera = this.context.mainCamera;
67
+ if (!camera) return;
68
+ if (!(camera instanceof PerspectiveCamera)) {
69
+ // TODO: support orthographic camera
70
+ return;
71
+ }
72
+
73
+ if (this.referenceFieldOfView === undefined) {
74
+ this.referenceFieldOfView = camera.fov;
75
+ }
76
+
77
+ if (this.referenceFieldOfView === undefined || this.referenceFieldOfView <= 0) {
78
+ if (debugParam || this.debug) console.warn("[ViewBox] No valid referenceFieldOfView set, cannot adjust box size:", this.referenceFieldOfView);
79
+ return;
80
+ }
81
+
82
+ const domWidth = this.context.domWidth;
83
+ const domHeight = this.context.domHeight;
84
+
85
+ let rectPosX = 0;
86
+ let rectPosY = 0;
87
+ let rectWidth = domWidth;
88
+ let rectHeight = domHeight;
89
+ let diffWidth = 1;
90
+ let diffHeight = 1;
91
+ // use focus rect if available
92
+ const focusRectSize = this.context.focusRectSize;
93
+ if (focusRectSize) {
94
+ // console.log(focusRectSize)
95
+ rectPosX = focusRectSize.x;
96
+ rectPosY = focusRectSize.y;
97
+ rectWidth = focusRectSize.width;
98
+ rectHeight = focusRectSize.height;
99
+ diffWidth = domWidth / rectWidth;
100
+ diffHeight = domHeight / rectHeight;
101
+ }
102
+
103
+
104
+ const view = camera.view;
105
+ const zoom = camera.zoom;
106
+ const aspect = camera.aspect;
107
+ const fov = camera.fov;
108
+ camera.view = null;
109
+ camera.zoom = 1;
110
+ camera.fov = this.referenceFieldOfView;
111
+ camera.updateProjectionMatrix();
112
+
113
+
114
+ const boxPosition = this.gameObject.worldPosition;
115
+ const boxScale = this.gameObject.worldScale;
116
+
117
+ const cameraPosition = camera.worldPosition;
118
+ const distance = cameraPosition.distanceTo(boxPosition);
119
+
120
+
121
+ // #region camera fixes
122
+ // If the camera is inside the box, move it out
123
+ const boxSizeMax = Math.max(boxScale.x, boxScale.y, boxScale.z);
124
+ const direction = getTempVector(cameraPosition).sub(boxPosition);
125
+ if (distance < boxSizeMax) {
126
+ // move camera out of bounds
127
+ if (this.debug || debugParam) console.warn("[ViewBox] Moving camera out of bounds", distance, "<", boxSizeMax);
128
+ const positionDirection = getTempVector(direction);
129
+ positionDirection.y *= .00000001; // stay on horizontal plane mostly
130
+ positionDirection.normalize();
131
+ const lengthToMove = (boxSizeMax - distance);
132
+ const newPosition = cameraPosition.add(positionDirection.multiplyScalar(lengthToMove));
133
+ camera.worldPosition = newPosition.lerp(cameraPosition, 1 - this.context.time.deltaTime);
134
+ }
135
+
136
+ // Ensure the camera looks at the ViewBox
137
+ // TOOD: smooth lookat over multiple frames if we have multiple viewboxes
138
+ // const dot = direction.normalize().dot(camera.worldForward);
139
+ // if (dot < .9) {
140
+ // console.log(dot);
141
+ // const targetRotation = direction;
142
+ // const rotation = getTempQuaternion();
143
+ // rotation.setFromUnitVectors(camera.worldForward.multiplyScalar(-1), targetRotation);
144
+ // camera.worldQuaternion = rotation;
145
+ // camera.updateMatrixWorld();
146
+ // }
147
+ const boxPositionInCameraSpace = getTempVector(boxPosition);
148
+ camera.worldToLocal(boxPositionInCameraSpace);
149
+ camera.lookAt(boxPosition);
150
+ camera.updateMatrixWorld();
151
+
152
+
153
+ // #region calculate fit
154
+ const vFOV = this.referenceFieldOfView * Math.PI / 180; // convert vertical fov to radians
155
+ const height = 2 * Math.tan(vFOV / 2) * distance; // visible height
156
+ const width = height * camera.aspect; // visible width
157
+
158
+ const projectedBox = this.projectBoxIntoCamera(camera, 1);
159
+ // return
160
+ const boxWidth = (projectedBox.maxX - projectedBox.minX);
161
+ const boxHeight = (projectedBox.maxY - projectedBox.minY);
162
+
163
+ const scale = this.fit(
164
+ boxWidth * camera.aspect,
165
+ boxHeight,
166
+ width / diffWidth,
167
+ height / diffHeight
168
+ );
169
+ // console.log({ scale, width, height, boxWidth: boxWidth * camera.aspect, boxHeight, diffWidth, diffHeight, aspect: camera.aspect, distance })
170
+ // this.context.focusRectSettings.zoom = 1.39;
171
+ // if (!this.context.focusRect) this.context.setCameraFocusRect(this.context.domElement);
172
+ // return
173
+ const vec = getTempVector(boxPosition);
174
+ vec.project(camera);
175
+ this.context.focusRectSettings.offsetX = vec.x;
176
+ this.context.focusRectSettings.offsetY = vec.y;
177
+ this.context.focusRectSettings.zoom = scale / (height * .5);
178
+ // if we don't have a focus rect yet, set it to the dom element
179
+ if (!this.context.focusRect) this.context.setCameraFocusRect(this.context.domElement);
180
+
181
+ // Reset values
182
+ camera.view = view;
183
+ camera.zoom = zoom;
184
+ camera.aspect = aspect;
185
+ camera.fov = fov;
186
+ // camera.updateProjectionMatrix();
187
+
188
+
189
+ // BACKLOG: some code for box scale of an object (different component)
190
+ // this.gameObject.worldScale = getTempVector(width, height, worldscale.z);
191
+ // this.gameObject.scale.multiplyScalar(.98)
192
+ // const minscale = Math.min(width, height);
193
+ // console.log(width, height);
194
+ // this.gameObject.worldScale = getTempVector(scale, scale, scale);
195
+ }
196
+
197
+
198
+ /**
199
+ * Cover fit
200
+ */
201
+ private fit(width1: number, height1: number, width2: number, height2: number) {
202
+ const scaleX = width2 / width1;
203
+ const scaleY = height2 / height1;
204
+ return Math.min(scaleX, scaleY);
205
+ }
206
+
207
+
208
+
209
+ private projectBoxIntoCamera(camera: Camera, _factor: number) {
210
+ const factor = .5 * _factor;
211
+
212
+ const corners = [
213
+ getTempVector(-factor, -factor, -factor),
214
+ getTempVector(factor, -factor, -factor),
215
+ getTempVector(-factor, factor, -factor),
216
+ getTempVector(factor, factor, -factor),
217
+ getTempVector(-factor, -factor, factor),
218
+ getTempVector(factor, -factor, factor),
219
+ getTempVector(-factor, factor, factor),
220
+ getTempVector(factor, factor, factor),
221
+ ];
222
+ let minX = Number.POSITIVE_INFINITY;
223
+ let maxX = Number.NEGATIVE_INFINITY;
224
+ let minY = Number.POSITIVE_INFINITY;
225
+ let maxY = Number.NEGATIVE_INFINITY;
226
+ for (let i = 0; i < corners.length; i++) {
227
+ const c = corners[i];
228
+ c.applyMatrix4(this.gameObject.matrixWorld);
229
+ c.project(camera);
230
+ if (c.x < minX) minX = c.x;
231
+ if (c.x > maxX) maxX = c.x;
232
+ if (c.y < minY) minY = c.y;
233
+ if (c.y > maxY) maxY = c.y;
234
+ }
235
+
236
+ if (debugParam) {
237
+ if (!this._projectedBoxElement) {
238
+ this._projectedBoxElement = document.createElement("div");
239
+ }
240
+ if (this._projectedBoxElement.parentElement !== this.context.domElement)
241
+ this.context.domElement.appendChild(this._projectedBoxElement);
242
+ this._projectedBoxElement.style.position = "fixed";
243
+ // dotted but with larger gaps
244
+ this._projectedBoxElement.style.outline = "2px dashed rgba(255,0,0,.5)";
245
+ this._projectedBoxElement.style.left = ((minX * .5 + .5) * this.context.domWidth) + "px";
246
+ this._projectedBoxElement.style.top = ((-maxY * .5 + .5) * this.context.domHeight) + "px";
247
+ this._projectedBoxElement.style.width = ((maxX - minX) * .5 * this.context.domWidth) + "px";
248
+ this._projectedBoxElement.style.height = ((maxY - minY) * .5 * this.context.domHeight) + "px";
249
+ this._projectedBoxElement.style.pointerEvents = "none";
250
+ this._projectedBoxElement.style.zIndex = "1000";
251
+ }
252
+
253
+
254
+ return { minX, maxX, minY, maxY };
255
+
256
+ }
257
+ private _projectedBoxElement: HTMLElement | null = null;
258
+
259
+
260
+
261
+
262
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./Clickthrough.js";
2
2
  export * from "./CursorFollow.js";
3
3
  export * from "./HoverAnimation.js";
4
- export * from "./ScrollFollow.js";
4
+ export * from "./ScrollFollow.js";
5
+ export * from "./ViewBox.js";
@@ -1,6 +1,7 @@
1
1
  import type { KeyCode } from "../engine/engine_input.js";
2
2
  import { Behaviour } from "../engine-components/Component.js";
3
3
 
4
+ /** @internal */
4
5
  export class PresentationMode extends Behaviour {
5
6
 
6
7
  toggleKey: KeyCode = "KeyP";