@needle-tools/engine 4.10.0-beta → 4.10.0-beta.1
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/components.needle.json +1 -1
- package/dist/{needle-engine.bundle-42AmEGfk.umd.cjs → needle-engine.bundle-6so_os_w.umd.cjs} +141 -130
- package/dist/{needle-engine.bundle-Dj6faVbC.js → needle-engine.bundle-Dj2DYdMY.js} +6063 -5884
- package/dist/{needle-engine.bundle-C6zhyLF5.min.js → needle-engine.bundle-Djy6H4lx.min.js} +163 -152
- package/dist/needle-engine.js +258 -257
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/engine/codegen/register_types.js +2 -0
- package/lib/engine/codegen/register_types.js.map +1 -1
- package/lib/engine/engine_camera.d.ts +7 -1
- package/lib/engine/engine_camera.fit.d.ts +1 -1
- package/lib/engine/engine_camera.fit.js +3 -30
- package/lib/engine/engine_camera.fit.js.map +1 -1
- package/lib/engine/engine_camera.js +46 -6
- package/lib/engine/engine_camera.js.map +1 -1
- package/lib/engine/engine_context.d.ts +6 -0
- package/lib/engine/engine_context.js +48 -9
- package/lib/engine/engine_context.js.map +1 -1
- package/lib/engine/webcomponents/logo-element.d.ts +1 -1
- package/lib/engine/webcomponents/logo-element.js +29 -5
- package/lib/engine/webcomponents/logo-element.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu.js +4 -3
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.loading.d.ts +0 -1
- package/lib/engine/webcomponents/needle-engine.loading.js +3 -36
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine-components/OrbitControls.d.ts +4 -1
- package/lib/engine-components/OrbitControls.js +30 -6
- package/lib/engine-components/OrbitControls.js.map +1 -1
- package/lib/engine-components/Renderer.js +4 -1
- package/lib/engine-components/Renderer.js.map +1 -1
- package/lib/engine-components/codegen/components.d.ts +1 -0
- package/lib/engine-components/codegen/components.js +1 -0
- package/lib/engine-components/codegen/components.js.map +1 -1
- package/lib/engine-components/timeline/PlayableDirector.d.ts +7 -0
- package/lib/engine-components/timeline/PlayableDirector.js +7 -0
- package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
- package/lib/engine-components/timeline/TimelineModels.d.ts +9 -1
- package/lib/engine-components/timeline/TimelineTracks.js +4 -2
- package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
- package/lib/engine-components/utils/LookAt.js +5 -1
- package/lib/engine-components/utils/LookAt.js.map +1 -1
- package/lib/engine-components/web/Clickthrough.js +10 -2
- package/lib/engine-components/web/Clickthrough.js.map +1 -1
- package/lib/engine-components/web/ScrollFollow.d.ts +22 -0
- package/lib/engine-components/web/ScrollFollow.js +124 -13
- package/lib/engine-components/web/ScrollFollow.js.map +1 -1
- package/lib/engine-components/web/ViewBox.d.ts +16 -0
- package/lib/engine-components/web/ViewBox.js +183 -0
- package/lib/engine-components/web/ViewBox.js.map +1 -0
- package/lib/engine-components/web/index.d.ts +1 -0
- package/lib/engine-components/web/index.js +1 -0
- package/lib/engine-components/web/index.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/codegen/register_types.ts +2 -0
- package/src/engine/engine_camera.fit.ts +2 -32
- package/src/engine/engine_camera.ts +62 -8
- package/src/engine/engine_context.ts +50 -10
- package/src/engine/webcomponents/logo-element.ts +29 -4
- package/src/engine/webcomponents/needle menu/needle-menu.ts +4 -3
- package/src/engine/webcomponents/needle-engine.loading.ts +32 -32
- package/src/engine-components/OrbitControls.ts +40 -1
- package/src/engine-components/Renderer.ts +4 -1
- package/src/engine-components/codegen/components.ts +1 -0
- package/src/engine-components/timeline/PlayableDirector.ts +9 -0
- package/src/engine-components/timeline/TimelineModels.ts +9 -1
- package/src/engine-components/timeline/TimelineTracks.ts +4 -2
- package/src/engine-components/utils/LookAt.ts +5 -1
- package/src/engine-components/web/Clickthrough.ts +11 -2
- package/src/engine-components/web/ScrollFollow.ts +149 -15
- package/src/engine-components/web/ViewBox.ts +199 -0
- package/src/engine-components/web/index.ts +2 -1
|
@@ -993,11 +993,50 @@ export class OrbitControls extends Behaviour implements ICameraController {
|
|
|
993
993
|
|
|
994
994
|
// Adapted from https://discourse.threejs.org/t/camera-zoom-to-fit-object/936/24
|
|
995
995
|
// Slower but better implementation that takes bones and exact vertex positions into account: https://github.com/google/model-viewer/blob/04e900c5027de8c5306fe1fe9627707f42811b05/packages/model-viewer/src/three-components/ModelScene.ts#L321
|
|
996
|
+
|
|
996
997
|
/**
|
|
997
998
|
* Fits the camera to show the objects provided (defaults to the scene if no objects are passed in)
|
|
999
|
+
* @param options The options for fitting the camera. Use to provide objects to fit to, fit direction and size and other settings.
|
|
998
1000
|
*/
|
|
999
|
-
fitCamera(options?: OrbitFitCameraOptions)
|
|
1001
|
+
fitCamera(options?: OrbitFitCameraOptions);
|
|
1002
|
+
/** @deprecated Use fitCamera(options) */
|
|
1003
|
+
fitCamera(objects?: Object3D | Array<Object3D>, options?: Omit<OrbitFitCameraOptions, "objects">);
|
|
1004
|
+
fitCamera(objectsOrOptions?: Object3D | Array<Object3D> | OrbitFitCameraOptions, options?: OrbitFitCameraOptions): void {
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
let objects: Object3D | Array<Object3D> | undefined = undefined;
|
|
1008
|
+
// If the user passed in an array as first argument
|
|
1009
|
+
if (Array.isArray(objectsOrOptions)) {
|
|
1010
|
+
objects = objectsOrOptions;
|
|
1011
|
+
}
|
|
1012
|
+
// If the user passed in an object as first argument
|
|
1013
|
+
else if (objectsOrOptions && "type" in objectsOrOptions) {
|
|
1014
|
+
objects = objectsOrOptions;
|
|
1015
|
+
}
|
|
1016
|
+
// If the user passed in an object as first argument and options as second argument
|
|
1017
|
+
else if (objectsOrOptions && typeof objectsOrOptions === "object") {
|
|
1018
|
+
if (!(objectsOrOptions instanceof Object3D) && !Array.isArray(objectsOrOptions)) {
|
|
1019
|
+
options = objectsOrOptions;
|
|
1020
|
+
objects = options.objects;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
// Ensure objects are setup correctly
|
|
1024
|
+
if (objects && !Array.isArray(objects)) {
|
|
1025
|
+
objects = [objects];
|
|
1026
|
+
}
|
|
1027
|
+
if (!Array.isArray(objects) || objects && objects.length <= 0) {
|
|
1028
|
+
objects = this.context.scene.children;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Make sure there's anything to fit to
|
|
1032
|
+
if (!Array.isArray(objects) || objects.length <= 0) {
|
|
1033
|
+
console.warn("No objects to fit camera to...");
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
|
|
1000
1038
|
const res = fitCamera({
|
|
1039
|
+
objects: [...objects],
|
|
1001
1040
|
...options,
|
|
1002
1041
|
autoApply: false,
|
|
1003
1042
|
context: this.context,
|
|
@@ -358,8 +358,11 @@ export class Renderer extends Behaviour implements IRenderer {
|
|
|
358
358
|
//@ts-ignore
|
|
359
359
|
get sharedMaterials(): SharedMaterialArray {
|
|
360
360
|
|
|
361
|
+
// @ts-ignore (original materials will be set during deserialization)
|
|
362
|
+
if(this._originalMaterials === undefined) return null;
|
|
363
|
+
|
|
361
364
|
// @ts-ignore during deserialization code might access this property *before* the setter and then create an empty array
|
|
362
|
-
if (
|
|
365
|
+
if (this.__isDeserializing === true) return null;
|
|
363
366
|
|
|
364
367
|
if (!this._sharedMaterials || !this._sharedMaterials.is(this)) {
|
|
365
368
|
if (!this._originalMaterials) this._originalMaterials = [];
|
|
@@ -208,6 +208,7 @@ export { ClickThrough } from "../web/Clickthrough.js";
|
|
|
208
208
|
export { CursorFollow } from "../web/CursorFollow.js";
|
|
209
209
|
export { HoverAnimation } from "../web/HoverAnimation.js";
|
|
210
210
|
export { ScrollFollow } from "../web/ScrollFollow.js";
|
|
211
|
+
export { ViewBox } from "../web/ViewBox.js";
|
|
211
212
|
export { Avatar } from "../webxr/Avatar.js";
|
|
212
213
|
export { XRControllerFollow } from "../webxr/controllers/XRControllerFollow.js";
|
|
213
214
|
export { XRControllerModel } from "../webxr/controllers/XRControllerModel.js";
|
|
@@ -66,10 +66,19 @@ export class PlayableDirector extends Behaviour {
|
|
|
66
66
|
this.createTrackFunctions[type] = fn;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The timeline asset that is played by this director.
|
|
71
|
+
*/
|
|
69
72
|
playableAsset?: Models.TimelineAssetModel;
|
|
73
|
+
|
|
70
74
|
/** Set to true to start playing the timeline when the scene starts */
|
|
71
75
|
@serializable()
|
|
72
76
|
playOnAwake?: boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Determines how the timeline behaves when it reaches the end of its duration.
|
|
80
|
+
* @default DirectorWrapMode.Loop
|
|
81
|
+
*/
|
|
73
82
|
@serializable()
|
|
74
83
|
extrapolationMode: DirectorWrapMode = DirectorWrapMode.Loop;
|
|
75
84
|
|
|
@@ -92,4 +92,12 @@ export declare class SignalMarkerModel extends MarkerModel {
|
|
|
92
92
|
asset: string;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Marker with a name, used for scroll-driven timelines. It is used together with elements in your HTML to define what time in the timeline should be active when the element is in the scroll view.
|
|
97
|
+
*
|
|
98
|
+
* @example Mark html elements to define scroll positions
|
|
99
|
+
* ```html
|
|
100
|
+
* <div data-timeline-marker>...</div>
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export type ScrollMarkerModel = MarkerModel & { name: string };
|
|
@@ -175,8 +175,10 @@ export class AnimationTrackHandler extends TrackHandler {
|
|
|
175
175
|
// which means we want to notify the object that it's not animated anymore
|
|
176
176
|
// and the animator can then take over
|
|
177
177
|
onStateChanged() {
|
|
178
|
-
if (this._animator)
|
|
179
|
-
|
|
178
|
+
if (this._animator) {
|
|
179
|
+
// We can not check the *isPlaying* state here because the timeline might be paused and evaluated by e.g. ScrollFollow
|
|
180
|
+
setObjectAnimated(this._animator.gameObject, this, this.director.enabled && this.director.weight > 0);
|
|
181
|
+
}
|
|
180
182
|
}
|
|
181
183
|
|
|
182
184
|
createHooks(clipModel: Models.AnimationClipModel, clip) {
|
|
@@ -6,6 +6,7 @@ import { type UsdzBehaviour } from "../../engine-components/export/usdz/extensio
|
|
|
6
6
|
import { ActionBuilder, BehaviorModel, TriggerBuilder, USDVec3 } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
|
|
7
7
|
import { USDObject } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
|
|
8
8
|
import { Behaviour } from "../Component.js";
|
|
9
|
+
import { isDevEnvironment } from "../../engine/debug/index.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* LookAt behaviour makes the object look at a target object or the camera.
|
|
@@ -42,7 +43,10 @@ export class LookAt extends Behaviour implements UsdzBehaviour {
|
|
|
42
43
|
/** @internal */
|
|
43
44
|
onBeforeRender(): void {
|
|
44
45
|
let target: Object3D | null | undefined = this.target;
|
|
45
|
-
if (!target)
|
|
46
|
+
if (!target) {
|
|
47
|
+
target = this.context.mainCamera;
|
|
48
|
+
if (isDevEnvironment()) console.warn(`[LookAt] No target set on ${this.name}, using main camera as target.`);
|
|
49
|
+
}
|
|
46
50
|
if (!target) return;
|
|
47
51
|
|
|
48
52
|
let copyTargetRotation = this.copyTargetRotation;
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { NEPointerEvent } from "../../engine/engine_input.js";
|
|
2
2
|
import { onStart } from "../../engine/engine_lifecycle_api.js";
|
|
3
|
+
import { addAttributeChangeCallback } from "../../engine/engine_utils.js";
|
|
3
4
|
import { Behaviour } from "../Component.js";
|
|
4
5
|
|
|
5
6
|
// Automatically add ClickThrough component if "clickthrough" attribute is present on the needle-engine element
|
|
6
7
|
onStart(ctx => {
|
|
7
8
|
const attribute = ctx.domElement.getAttribute("clickthrough");
|
|
8
|
-
if (attribute
|
|
9
|
-
ctx.scene.addComponent(ClickThrough);
|
|
9
|
+
if (clickthroughEnabled(attribute)) {
|
|
10
|
+
const comp = ctx.scene.addComponent(ClickThrough);
|
|
11
|
+
addAttributeChangeCallback(ctx.domElement, "clickthrough", () => {
|
|
12
|
+
const attribute = ctx.domElement.getAttribute("clickthrough");
|
|
13
|
+
comp.enabled = clickthroughEnabled(attribute);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function clickthroughEnabled(val: string | null) {
|
|
18
|
+
return val !== null && val !== "0" && val !== "false";
|
|
10
19
|
}
|
|
11
20
|
});
|
|
12
21
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Box3, Object3D } from "three";
|
|
2
2
|
import { element } from "three/src/nodes/TSL.js";
|
|
3
|
-
import { Context } from "../../engine/engine_context.js";
|
|
4
3
|
|
|
4
|
+
import { Context } from "../../engine/engine_context.js";
|
|
5
5
|
import { Mathf } from "../../engine/engine_math.js";
|
|
6
6
|
import { serializable } from "../../engine/engine_serialization.js";
|
|
7
7
|
import { getBoundingBox } from "../../engine/engine_three_utils.js";
|
|
@@ -150,7 +150,8 @@ export class ScrollFollow extends Behaviour {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
if (this._current_value !== this._appliedValue)
|
|
153
|
+
// if (this._current_value !== this._appliedValue)
|
|
154
|
+
{
|
|
154
155
|
this._appliedValue = this._current_value;
|
|
155
156
|
|
|
156
157
|
let defaultPrevented = false;
|
|
@@ -242,7 +243,8 @@ export class ScrollFollow extends Behaviour {
|
|
|
242
243
|
|
|
243
244
|
if (target instanceof PlayableDirector) {
|
|
244
245
|
this.handleTimelineTarget(target, value);
|
|
245
|
-
if (
|
|
246
|
+
if (target.isPlaying) target.pause();
|
|
247
|
+
target.evaluate();
|
|
246
248
|
}
|
|
247
249
|
else if (target instanceof Animator) {
|
|
248
250
|
target.setFloat("scroll", value);
|
|
@@ -292,18 +294,35 @@ export class ScrollFollow extends Behaviour {
|
|
|
292
294
|
let scrollRegionEnd = 0;
|
|
293
295
|
markersArray.length = 0;
|
|
294
296
|
|
|
295
|
-
|
|
297
|
+
// querySelectorResults.length = 0;
|
|
298
|
+
let markerIndex = 0;
|
|
299
|
+
|
|
300
|
+
// https://scroll-driven-animations.style/tools/view-timeline/ranges
|
|
301
|
+
for (const marker of director.foreachMarker<ScrollMarkerModel & { element?: HTMLElement | null, needsUpdate?: boolean, timeline?: ViewTimeline }>("ScrollMarker")) {
|
|
302
|
+
|
|
303
|
+
const index = markerIndex++;
|
|
296
304
|
|
|
297
305
|
// Get marker elements from DOM
|
|
298
|
-
if (
|
|
306
|
+
if ((marker.element === undefined || marker.needsUpdate === true || /** element is not in DOM anymore? */ (!marker.element?.parentNode))) {
|
|
299
307
|
marker.needsUpdate = false;
|
|
300
308
|
try {
|
|
301
|
-
marker.element =
|
|
302
|
-
if (debug) console.debug("ScrollMarker found on page", marker.element, marker.
|
|
309
|
+
marker.element = tryGetElementsForSelector(index, marker.name) as HTMLElement | null;
|
|
310
|
+
if (debug) console.debug("ScrollMarker found on page", marker.element, marker.name);
|
|
311
|
+
// if (!marker.element) {
|
|
312
|
+
// marker.timeline = undefined;
|
|
313
|
+
// continue;
|
|
314
|
+
// }
|
|
315
|
+
// else {
|
|
316
|
+
// /** @ts-ignore */
|
|
317
|
+
// marker.timeline = new ViewTimeline({
|
|
318
|
+
// subject: marker.element,
|
|
319
|
+
// axis: 'block', // https://drafts.csswg.org/scroll-animations/#scroll-notation
|
|
320
|
+
// });
|
|
321
|
+
// }
|
|
303
322
|
}
|
|
304
323
|
catch (error) {
|
|
305
324
|
marker.element = null;
|
|
306
|
-
console.error("ScrollMarker selector is not valid: " + marker.
|
|
325
|
+
console.error("ScrollMarker selector is not valid: " + marker.name + "\n", error);
|
|
307
326
|
}
|
|
308
327
|
}
|
|
309
328
|
|
|
@@ -331,11 +350,28 @@ export class ScrollFollow extends Behaviour {
|
|
|
331
350
|
weightsArray.length = 0;
|
|
332
351
|
let sum = 0;
|
|
333
352
|
|
|
353
|
+
// 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
354
|
let markerCount = 0;
|
|
335
355
|
for (const marker of markersArray) {
|
|
336
356
|
|
|
337
357
|
if (!marker.element) continue;
|
|
338
358
|
|
|
359
|
+
markerCount += 1;
|
|
360
|
+
|
|
361
|
+
// const timeline = marker.timeline;
|
|
362
|
+
// if (timeline) {
|
|
363
|
+
// const time01 = calculateTimelinePositionNormalized(timeline);
|
|
364
|
+
// if (time01 > 0 && time01 <= 1) {
|
|
365
|
+
// const overlap = calculateTimelinePositionNormalized(timeline!);
|
|
366
|
+
// const weight = overlap;
|
|
367
|
+
// // console.log(marker.element.className, time01)
|
|
368
|
+
// weightsArray.push({ time: marker.time, weight: weight });
|
|
369
|
+
// sum += weight;
|
|
370
|
+
// }
|
|
371
|
+
// }
|
|
372
|
+
// continue;
|
|
373
|
+
// if(this.context.time.frame % 10 === 0) console.log(marker.element?.className, timeline, calculateTimelinePositionNormalized(timeline!));
|
|
374
|
+
|
|
339
375
|
const top = marker.element.offsetTop;
|
|
340
376
|
const height = marker.element.offsetHeight;
|
|
341
377
|
const bottom = top + height;
|
|
@@ -356,7 +392,6 @@ export class ScrollFollow extends Behaviour {
|
|
|
356
392
|
const overlapTop = Math.max(top, currentTop);
|
|
357
393
|
const overlapBottom = Math.min(bottom, currentBottom);
|
|
358
394
|
overlap = Math.max(0, overlapBottom - overlapTop);
|
|
359
|
-
// console.log(marker.element.className, overlap)
|
|
360
395
|
}
|
|
361
396
|
|
|
362
397
|
markerCount += 1;
|
|
@@ -372,25 +407,124 @@ export class ScrollFollow extends Behaviour {
|
|
|
372
407
|
}
|
|
373
408
|
else if (weightsArray.length > 0) {
|
|
374
409
|
// normalize and calculate weighted time
|
|
375
|
-
let time =
|
|
376
|
-
for (const
|
|
377
|
-
const weight =
|
|
410
|
+
let time = 0;
|
|
411
|
+
for (const entry of weightsArray) {
|
|
412
|
+
const weight = entry.weight / Math.max(0.00001, sum);
|
|
413
|
+
// console.log(weight.toFixed(2))
|
|
378
414
|
// lerp time based on weight
|
|
379
|
-
const diff = Math.abs(
|
|
415
|
+
const diff = Math.abs(entry.time - time);
|
|
380
416
|
time += diff * weight;
|
|
381
417
|
}
|
|
382
|
-
|
|
418
|
+
// console.log(time.toFixed(2), [...weightsArray])
|
|
419
|
+
if (this.damping <= 0)
|
|
420
|
+
director.time = time;
|
|
421
|
+
else
|
|
422
|
+
director.time = Mathf.lerp(director.time, time, this.context.time.deltaTime / this.damping);
|
|
383
423
|
}
|
|
384
424
|
}
|
|
385
425
|
|
|
386
426
|
}
|
|
387
427
|
|
|
428
|
+
|
|
429
|
+
|
|
388
430
|
const weightsArray: OverlapInfo[] = [];
|
|
389
|
-
const markersArray: (ScrollMarkerModel & { element?: HTMLElement | null })[] = [];
|
|
431
|
+
const markersArray: (ScrollMarkerModel & { element?: HTMLElement | null, timeline?: ViewTimeline })[] = [];
|
|
390
432
|
|
|
391
433
|
type OverlapInfo = {
|
|
392
434
|
/** Marker time */
|
|
393
435
|
time: number,
|
|
394
436
|
/** Overlap in pixels */
|
|
395
437
|
weight: number,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
// type SelectorCache = {
|
|
442
|
+
// /** The selector used to query the *elements */
|
|
443
|
+
// selector: string,
|
|
444
|
+
// elements: Element[] | null,
|
|
445
|
+
// usedElementCount: number,
|
|
446
|
+
// }
|
|
447
|
+
// const querySelectorResults: Array<SelectorCache> = [];
|
|
448
|
+
|
|
449
|
+
const needleScrollMarkerCacheKey = "data-timeline-marker";
|
|
450
|
+
const needleScrollMarkerIndexCache = new Map<number, Element | null>();
|
|
451
|
+
const needleScrollMarkerNameCache = new Map<string, Element | null>();
|
|
452
|
+
let needsScrollMarkerRefresh = true;
|
|
453
|
+
|
|
454
|
+
function tryGetElementsForSelector(index: number, name: string): Element | null {
|
|
455
|
+
|
|
456
|
+
if (!needsScrollMarkerRefresh) {
|
|
457
|
+
let element = name?.length ? needleScrollMarkerNameCache.get(name) : null;
|
|
458
|
+
if (element) return element;
|
|
459
|
+
element = needleScrollMarkerIndexCache.get(index) || null;
|
|
460
|
+
return element;
|
|
461
|
+
}
|
|
462
|
+
needsScrollMarkerRefresh = false;
|
|
463
|
+
needleScrollMarkerIndexCache.clear();
|
|
464
|
+
const markers = document.querySelectorAll(`[data-timeline-marker]`);
|
|
465
|
+
markers.forEach((m, i) => {
|
|
466
|
+
needleScrollMarkerIndexCache.set(i, m);
|
|
467
|
+
const name = m.getAttribute("data-timeline-marker");
|
|
468
|
+
if (name?.length) needleScrollMarkerNameCache.set(name, m);
|
|
469
|
+
});
|
|
470
|
+
const element = needleScrollMarkerIndexCache.get(index) || null;
|
|
471
|
+
return element;
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
/* e.g.
|
|
475
|
+
<div class="section behind start" data-needle-scroll-marker>
|
|
476
|
+
*/
|
|
477
|
+
// console.log(index, element)
|
|
478
|
+
if (element) return element;
|
|
479
|
+
|
|
480
|
+
// for (const entry of querySelectorResults) {
|
|
481
|
+
// if (entry.selector === selector) {
|
|
482
|
+
// const index = entry.usedElementCount++;
|
|
483
|
+
// return entry.elements && index < entry.elements.length ? entry.elements[index] : null;
|
|
484
|
+
// }
|
|
485
|
+
// }
|
|
486
|
+
// const elements = document.querySelectorAll(selector);
|
|
487
|
+
// querySelectorResults.push({ selector, elements: Array.from(elements), usedElementCount: 1 });
|
|
488
|
+
// if (elements.length > 0) return elements[0];
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
// #region ScrollTimeline
|
|
494
|
+
|
|
495
|
+
function calculateTimelinePositionNormalized(timeline: ViewTimeline) {
|
|
496
|
+
if (!timeline.source) return 0;
|
|
497
|
+
const currentTime = timeline.currentTime;
|
|
498
|
+
const duration = timeline.duration;
|
|
499
|
+
let durationValue = 1;
|
|
500
|
+
if (duration.unit === "seconds") {
|
|
501
|
+
durationValue = duration.value;
|
|
502
|
+
}
|
|
503
|
+
else if (duration.unit === "percent") {
|
|
504
|
+
durationValue = duration.value;
|
|
505
|
+
}
|
|
506
|
+
const t01 = currentTime.unit === "seconds" ? (currentTime.value / durationValue) : (currentTime.value / 100);
|
|
507
|
+
return t01;
|
|
508
|
+
}
|
|
509
|
+
function calculateNormalizedOverlap(timeline: ViewTimeline) {
|
|
510
|
+
if (!timeline.source) return 0;
|
|
511
|
+
const start = timeline.startOffset;
|
|
512
|
+
const end = timeline.endOffset;
|
|
513
|
+
const total = start.value + end.value;
|
|
514
|
+
if (total <= 0) return 1;
|
|
515
|
+
const startNorm = start.value / total;
|
|
516
|
+
const endNorm = end.value / total;
|
|
517
|
+
return 1 - (startNorm + endNorm);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
declare global {
|
|
522
|
+
interface ViewTimeline {
|
|
523
|
+
axis: 'block' | 'inline';
|
|
524
|
+
currentTime: { unit: 'seconds' | 'percent', value: number };
|
|
525
|
+
duration: { unit: 'seconds' | 'percent', value: number };
|
|
526
|
+
source: Element | null;
|
|
527
|
+
startOffset: { unit: 'px', value: number };
|
|
528
|
+
endOffset: { unit: 'px', value: number };
|
|
529
|
+
}
|
|
396
530
|
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { Camera, PerspectiveCamera, Vector2, Vector3 } from "three";
|
|
2
|
+
import { serializable } from "../../engine/engine_serialization_decorator.js";
|
|
3
|
+
import { registerType } from "../../engine/engine_typestore.js";
|
|
4
|
+
import { getTempVector } from "../../engine/engine_three_utils.js";
|
|
5
|
+
import { Behaviour } from "../Component.js";
|
|
6
|
+
import { isDevEnvironment } from "../../engine/debug/debug.js";
|
|
7
|
+
import { getParam } from "../../engine/engine_utils.js";
|
|
8
|
+
|
|
9
|
+
const debugParam = getParam("debugviewbox");
|
|
10
|
+
|
|
11
|
+
@registerType
|
|
12
|
+
export class ViewBox extends Behaviour {
|
|
13
|
+
|
|
14
|
+
static instances: ViewBox[] = [];
|
|
15
|
+
|
|
16
|
+
@serializable()
|
|
17
|
+
referenceFieldOfView: number = 60;
|
|
18
|
+
|
|
19
|
+
@serializable()
|
|
20
|
+
debug: boolean = false;
|
|
21
|
+
|
|
22
|
+
awake() {
|
|
23
|
+
// this.referenceFieldOfView = (this.context.mainCamera as PerspectiveCamera)?.fov || 60;
|
|
24
|
+
// setInterval(()=>{
|
|
25
|
+
// this.enabled = !this.enabled
|
|
26
|
+
// }, 1000)
|
|
27
|
+
}
|
|
28
|
+
onEnable(): void {
|
|
29
|
+
if (debugParam || this.debug || isDevEnvironment()) console.debug("[ViewBox] Using camera fov:", this.referenceFieldOfView);
|
|
30
|
+
ViewBox.instances.push(this);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
onDisable(): void {
|
|
34
|
+
const idx = ViewBox.instances.indexOf(this);
|
|
35
|
+
if (idx !== -1) ViewBox.instances.splice(idx, 1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onBeforeRender() {
|
|
39
|
+
if (this.context.isInXR) return;
|
|
40
|
+
const isActive = ViewBox.instances[ViewBox.instances.length - 1] === this;
|
|
41
|
+
if (!isActive) return;
|
|
42
|
+
|
|
43
|
+
// calculate box size to fit the camera frustrum size at the current position (just scale)
|
|
44
|
+
const camera = this.context.mainCamera;
|
|
45
|
+
if (!camera) return;
|
|
46
|
+
if (!(camera instanceof PerspectiveCamera)) {
|
|
47
|
+
// TODO: support orthographic camera
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (this.referenceFieldOfView === undefined || this.referenceFieldOfView <= 0) {
|
|
52
|
+
if (debugParam || this.debug) console.warn("[ViewBox] No valid referenceFieldOfView set, cannot adjust box size:", this.referenceFieldOfView);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const domRect = this.context.domElement.getBoundingClientRect();
|
|
57
|
+
const domX = domRect.x;
|
|
58
|
+
const domY = domRect.y;
|
|
59
|
+
const domWidth = domRect.width;
|
|
60
|
+
const domHeight = domRect.height;
|
|
61
|
+
|
|
62
|
+
let rectPosX = 0;
|
|
63
|
+
let rectPosY = 0;
|
|
64
|
+
let rectWidth = domWidth;
|
|
65
|
+
let rectHeight = domHeight;
|
|
66
|
+
let diffWidth = 1;
|
|
67
|
+
let diffHeight = 1;
|
|
68
|
+
// use focus rect if available
|
|
69
|
+
const focusRectSize = this.context.focusRectSize;
|
|
70
|
+
if (focusRectSize) {
|
|
71
|
+
// console.log(focusRectSize)
|
|
72
|
+
rectPosX = focusRectSize.x;
|
|
73
|
+
rectPosY = focusRectSize.y;
|
|
74
|
+
rectWidth = focusRectSize.width;
|
|
75
|
+
rectHeight = focusRectSize.height;
|
|
76
|
+
diffWidth = domWidth / rectWidth;
|
|
77
|
+
diffHeight = domHeight / rectHeight;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// const view = camera.view;
|
|
81
|
+
const view = camera.view;
|
|
82
|
+
const zoom = camera.zoom;
|
|
83
|
+
const aspect = camera.aspect;
|
|
84
|
+
const fov = camera.fov;
|
|
85
|
+
camera.view = null;
|
|
86
|
+
camera.zoom = 1;
|
|
87
|
+
// camera.aspect = rectWidth / rectHeight;
|
|
88
|
+
camera.fov = this.referenceFieldOfView;
|
|
89
|
+
camera.updateProjectionMatrix();
|
|
90
|
+
|
|
91
|
+
const boxPosition = this.gameObject.worldPosition;
|
|
92
|
+
const boxScale = this.gameObject.worldScale;
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
// const fov = this.referenceFieldOfView
|
|
97
|
+
const distance = camera.worldPosition.distanceTo(boxPosition);
|
|
98
|
+
const vFOV = this.referenceFieldOfView * Math.PI / 180; // convert vertical fov to radians
|
|
99
|
+
const height = 2 * Math.tan(vFOV / 2) * distance; // visible height
|
|
100
|
+
const width = height * camera.aspect; // visible width
|
|
101
|
+
|
|
102
|
+
const projectedBox = this.projectBoxIntoCamera(boxPosition, boxScale, camera, height * .5);
|
|
103
|
+
const boxWidth = (projectedBox.maxX - projectedBox.minX);
|
|
104
|
+
const boxHeight = (projectedBox.maxY - projectedBox.minY);
|
|
105
|
+
|
|
106
|
+
// TODO: take the rect size different into account
|
|
107
|
+
const scale = this.fit(
|
|
108
|
+
boxWidth * camera.aspect,
|
|
109
|
+
boxHeight,
|
|
110
|
+
width / diffWidth,
|
|
111
|
+
height / diffHeight
|
|
112
|
+
);
|
|
113
|
+
const vec = getTempVector(boxPosition);
|
|
114
|
+
vec.project(camera);
|
|
115
|
+
this.context.focusRectSettings.offsetX = vec.x;
|
|
116
|
+
this.context.focusRectSettings.offsetY = vec.y;
|
|
117
|
+
this.context.focusRectSettings.zoom = scale;
|
|
118
|
+
// if we don't have a focus rect yet, set it to the dom element
|
|
119
|
+
if (!this.context.focusRect) this.context.setCameraFocusRect(this.context.domElement);
|
|
120
|
+
|
|
121
|
+
// Reset values
|
|
122
|
+
camera.view = view;
|
|
123
|
+
camera.zoom = zoom;
|
|
124
|
+
camera.aspect = aspect;
|
|
125
|
+
camera.fov = fov;
|
|
126
|
+
// camera.updateProjectionMatrix();
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
// BACKLOG: some code for box scale of an object (different component)
|
|
130
|
+
// this.gameObject.worldScale = getTempVector(width, height, worldscale.z);
|
|
131
|
+
// this.gameObject.scale.multiplyScalar(.98)
|
|
132
|
+
// const minscale = Math.min(width, height);
|
|
133
|
+
// console.log(width, height);
|
|
134
|
+
// this.gameObject.worldScale = getTempVector(scale, scale, scale);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Cover fit
|
|
140
|
+
*/
|
|
141
|
+
private fit(width1: number, height1: number, width2: number, height2: number) {
|
|
142
|
+
const scaleX = width2 / width1;
|
|
143
|
+
const scaleY = height2 / height1;
|
|
144
|
+
return Math.min(scaleX, scaleY);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
private projectBoxIntoCamera(position: Vector3, scale: Vector3, camera: Camera, diff: number) {
|
|
150
|
+
|
|
151
|
+
const factor = .5 * diff;
|
|
152
|
+
|
|
153
|
+
const corners = [
|
|
154
|
+
getTempVector(-scale.x * factor, -scale.y * factor, -scale.z * factor),
|
|
155
|
+
getTempVector(scale.x * factor, -scale.y * factor, -scale.z * factor),
|
|
156
|
+
getTempVector(-scale.x * factor, scale.y * factor, -scale.z * factor),
|
|
157
|
+
getTempVector(scale.x * factor, scale.y * factor, -scale.z * factor),
|
|
158
|
+
getTempVector(-scale.x * factor, -scale.y * factor, scale.z * factor),
|
|
159
|
+
getTempVector(scale.x * factor, -scale.y * factor, scale.z * factor),
|
|
160
|
+
getTempVector(-scale.x * factor, scale.y * factor, scale.z * factor),
|
|
161
|
+
getTempVector(scale.x * factor, scale.y * factor, scale.z * factor),
|
|
162
|
+
];
|
|
163
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
164
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
165
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
166
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
167
|
+
for (let i = 0; i < corners.length; i++) {
|
|
168
|
+
const c = corners[i];
|
|
169
|
+
c.add(position);
|
|
170
|
+
c.project(camera);
|
|
171
|
+
if (c.x < minX) minX = c.x;
|
|
172
|
+
if (c.x > maxX) maxX = c.x;
|
|
173
|
+
if (c.y < minY) minY = c.y;
|
|
174
|
+
if (c.y > maxY) maxY = c.y;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// if(!this._projectedBoxElement) {
|
|
178
|
+
// this._projectedBoxElement = document.createElement("div");
|
|
179
|
+
// this.context.domElement.appendChild(this._projectedBoxElement);
|
|
180
|
+
// }
|
|
181
|
+
// this._projectedBoxElement.style.position = "fixed";
|
|
182
|
+
// this._projectedBoxElement.style.outline = "10px solid red";
|
|
183
|
+
// this._projectedBoxElement.style.left = ((minX * .5 + .5) * this.context.domWidth) + "px";
|
|
184
|
+
// this._projectedBoxElement.style.top = ((-maxY * .5 + .5) * this.context.domHeight) + "px";
|
|
185
|
+
// this._projectedBoxElement.style.width = ((maxX - minX) * .5 * this.context.domWidth) + "px";
|
|
186
|
+
// this._projectedBoxElement.style.height = ((maxY - minY) * .5 * this.context.domHeight) + "px";
|
|
187
|
+
// this._projectedBoxElement.style.pointerEvents = "none";
|
|
188
|
+
// this._projectedBoxElement.style.zIndex = "1000";
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
return { minX, maxX, minY, maxY };
|
|
192
|
+
|
|
193
|
+
}
|
|
194
|
+
private _projectedBoxElement: HTMLElement | null = null;
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
}
|