diff --git a/.gitignore b/.gitignore index 1698cee3..03363a73 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,11 @@ desktop.ini .zed .idea .project + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ +/tests/testAssetDownloads diff --git a/.npmignore b/.npmignore index c7e90d7e..a510f369 100644 --- a/.npmignore +++ b/.npmignore @@ -12,6 +12,8 @@ documentation/ .gitattributes .gitmodules .eslintrc.json +dist/main.js +dist/index.html # files types to ignore rollup.config.js diff --git a/.prettierignore b/.prettierignore index 26a3c207..4a599ffe 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ -source/libs \ No newline at end of file +source/libs +**/*.md diff --git a/API.md b/API.md index cbd2bf65..f0d56285 100644 --- a/API.md +++ b/API.md @@ -7,12 +7,18 @@
GltfState

GltfState containing a state for visualization in GltfView

+
AnimationTimer
+

AnimationTimer class to control animation playback.

+
ResourceLoader

ResourceLoader can be used to load resources for the GltfState that are then used to display the loaded data with GltfView

UserCamera
+
GraphController
+

A controller for managing KHR_interactivity graphs in a glTF scene.

+
@@ -112,9 +118,31 @@ GltfState containing a state for visualization in GltfView * [.animationIndices](#GltfState+animationIndices) * [.animationTimer](#GltfState+animationTimer) * [.variant](#GltfState+variant) + * [.graphController](#GltfState+graphController) + * [.selectionCallback](#GltfState+selectionCallback) + * [.hoverCallback](#GltfState+hoverCallback) + * [.triggerSelection](#GltfState+triggerSelection) + * [.enableHover](#GltfState+enableHover) * [.renderingParameters](#GltfState+renderingParameters) * [.morphing](#GltfState+renderingParameters.morphing) * [.skinning](#GltfState+renderingParameters.skinning) + * [.enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + * [.KHR_materials_clearcoat](#GltfState+renderingParameters.enabledExtensions.KHR_materials_clearcoat) + * [.KHR_materials_sheen](#GltfState+renderingParameters.enabledExtensions.KHR_materials_sheen) + * [.KHR_materials_transmission](#GltfState+renderingParameters.enabledExtensions.KHR_materials_transmission) + * [.KHR_materials_volume](#GltfState+renderingParameters.enabledExtensions.KHR_materials_volume) + * [.KHR_materials_volume_scatter](#GltfState+renderingParameters.enabledExtensions.KHR_materials_volume_scatter) + * [.KHR_materials_ior](#GltfState+renderingParameters.enabledExtensions.KHR_materials_ior) + * [.KHR_materials_specular](#GltfState+renderingParameters.enabledExtensions.KHR_materials_specular) + * [.KHR_materials_iridescence](#GltfState+renderingParameters.enabledExtensions.KHR_materials_iridescence) + * [.KHR_materials_diffuse_transmission](#GltfState+renderingParameters.enabledExtensions.KHR_materials_diffuse_transmission) + * [.KHR_materials_anisotropy](#GltfState+renderingParameters.enabledExtensions.KHR_materials_anisotropy) + * [.KHR_materials_dispersion](#GltfState+renderingParameters.enabledExtensions.KHR_materials_dispersion) + * [.KHR_materials_emissive_strength](#GltfState+renderingParameters.enabledExtensions.KHR_materials_emissive_strength) + * [.KHR_interactivity](#GltfState+renderingParameters.enabledExtensions.KHR_interactivity) + * [.KHR_node_hoverability](#GltfState+renderingParameters.enabledExtensions.KHR_node_hoverability) + * [.KHR_node_selectability](#GltfState+renderingParameters.enabledExtensions.KHR_node_selectability) + * [.KHR_node_visibility](#GltfState+renderingParameters.enabledExtensions.KHR_node_visibility) * [.clearColor](#GltfState+renderingParameters.clearColor) * [.exposure](#GltfState+renderingParameters.exposure) * [.usePunctual](#GltfState+renderingParameters.usePunctual) @@ -143,6 +171,7 @@ GltfState containing a state for visualization in GltfView * [.GEOMETRYNORMAL](#GltfState.DebugOutput.generic.GEOMETRYNORMAL) * [.TANGENT](#GltfState.DebugOutput.generic.TANGENT) * [.BITANGENT](#GltfState.DebugOutput.generic.BITANGENT) + * [.TANGENTW](#GltfState.DebugOutput.generic.TANGENTW) * [.WORLDSPACENORMAL](#GltfState.DebugOutput.generic.WORLDSPACENORMAL) * [.ALPHA](#GltfState.DebugOutput.generic.ALPHA) * [.OCCLUSION](#GltfState.DebugOutput.generic.OCCLUSION) @@ -233,6 +262,40 @@ animation timer allows to control the animation time ### gltfState.variant KHR_materials_variants +**Kind**: instance property of [GltfState](#GltfState) + + +### gltfState.graphController +the graph controller allows selecting and playing graphs from KHR_interactivity + +**Kind**: instance property of [GltfState](#GltfState) + + +### gltfState.selectionCallback +callback for selection: (selectionInfo : { +node, +position, +rayOrigin, +controller }) => {} + +**Kind**: instance property of [GltfState](#GltfState) + + +### gltfState.hoverCallback +callback for hovering: (hoverInfo : { node, controller }) => {} + +**Kind**: instance property of [GltfState](#GltfState) + + +### gltfState.triggerSelection +If the renderer should compute selection information in the next frame. Is automatically reset after the frame is rendered + +**Kind**: instance property of [GltfState](#GltfState) + + +### gltfState.enableHover +If the renderer should compute hover information in the next frame. + **Kind**: instance property of [GltfState](#GltfState) @@ -244,6 +307,23 @@ parameters used to configure the rendering * [.renderingParameters](#GltfState+renderingParameters) * [.morphing](#GltfState+renderingParameters.morphing) * [.skinning](#GltfState+renderingParameters.skinning) + * [.enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + * [.KHR_materials_clearcoat](#GltfState+renderingParameters.enabledExtensions.KHR_materials_clearcoat) + * [.KHR_materials_sheen](#GltfState+renderingParameters.enabledExtensions.KHR_materials_sheen) + * [.KHR_materials_transmission](#GltfState+renderingParameters.enabledExtensions.KHR_materials_transmission) + * [.KHR_materials_volume](#GltfState+renderingParameters.enabledExtensions.KHR_materials_volume) + * [.KHR_materials_volume_scatter](#GltfState+renderingParameters.enabledExtensions.KHR_materials_volume_scatter) + * [.KHR_materials_ior](#GltfState+renderingParameters.enabledExtensions.KHR_materials_ior) + * [.KHR_materials_specular](#GltfState+renderingParameters.enabledExtensions.KHR_materials_specular) + * [.KHR_materials_iridescence](#GltfState+renderingParameters.enabledExtensions.KHR_materials_iridescence) + * [.KHR_materials_diffuse_transmission](#GltfState+renderingParameters.enabledExtensions.KHR_materials_diffuse_transmission) + * [.KHR_materials_anisotropy](#GltfState+renderingParameters.enabledExtensions.KHR_materials_anisotropy) + * [.KHR_materials_dispersion](#GltfState+renderingParameters.enabledExtensions.KHR_materials_dispersion) + * [.KHR_materials_emissive_strength](#GltfState+renderingParameters.enabledExtensions.KHR_materials_emissive_strength) + * [.KHR_interactivity](#GltfState+renderingParameters.enabledExtensions.KHR_interactivity) + * [.KHR_node_hoverability](#GltfState+renderingParameters.enabledExtensions.KHR_node_hoverability) + * [.KHR_node_selectability](#GltfState+renderingParameters.enabledExtensions.KHR_node_selectability) + * [.KHR_node_visibility](#GltfState+renderingParameters.enabledExtensions.KHR_node_visibility) * [.clearColor](#GltfState+renderingParameters.clearColor) * [.exposure](#GltfState+renderingParameters.exposure) * [.usePunctual](#GltfState+renderingParameters.usePunctual) @@ -269,6 +349,127 @@ morphing between vertices skin / skeleton **Kind**: static property of [renderingParameters](#GltfState+renderingParameters) + + +#### renderingParameters.enabledExtensions +enabled extensions + +**Kind**: static property of [renderingParameters](#GltfState+renderingParameters) + +* [.enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + * [.KHR_materials_clearcoat](#GltfState+renderingParameters.enabledExtensions.KHR_materials_clearcoat) + * [.KHR_materials_sheen](#GltfState+renderingParameters.enabledExtensions.KHR_materials_sheen) + * [.KHR_materials_transmission](#GltfState+renderingParameters.enabledExtensions.KHR_materials_transmission) + * [.KHR_materials_volume](#GltfState+renderingParameters.enabledExtensions.KHR_materials_volume) + * [.KHR_materials_volume_scatter](#GltfState+renderingParameters.enabledExtensions.KHR_materials_volume_scatter) + * [.KHR_materials_ior](#GltfState+renderingParameters.enabledExtensions.KHR_materials_ior) + * [.KHR_materials_specular](#GltfState+renderingParameters.enabledExtensions.KHR_materials_specular) + * [.KHR_materials_iridescence](#GltfState+renderingParameters.enabledExtensions.KHR_materials_iridescence) + * [.KHR_materials_diffuse_transmission](#GltfState+renderingParameters.enabledExtensions.KHR_materials_diffuse_transmission) + * [.KHR_materials_anisotropy](#GltfState+renderingParameters.enabledExtensions.KHR_materials_anisotropy) + * [.KHR_materials_dispersion](#GltfState+renderingParameters.enabledExtensions.KHR_materials_dispersion) + * [.KHR_materials_emissive_strength](#GltfState+renderingParameters.enabledExtensions.KHR_materials_emissive_strength) + * [.KHR_interactivity](#GltfState+renderingParameters.enabledExtensions.KHR_interactivity) + * [.KHR_node_hoverability](#GltfState+renderingParameters.enabledExtensions.KHR_node_hoverability) + * [.KHR_node_selectability](#GltfState+renderingParameters.enabledExtensions.KHR_node_selectability) + * [.KHR_node_visibility](#GltfState+renderingParameters.enabledExtensions.KHR_node_visibility) + + + +##### enabledExtensions.KHR\_materials\_clearcoat +KHR_materials_clearcoat adds a clear coat layer on top of the glTF base material + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_materials\_sheen +KHR_materials_sheen adds a sheen layer on top of the glTF base material + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_materials\_transmission +KHR_materials_transmission adds physical-based transparency + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_materials\_volume +KHR_materials_volume adds support for volumetric materials. Used together with KHR_materials_transmission and KHR_materials_diffuse_transmission + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_materials\_volume\_scatter +KHR_materials_volume_scatter allows the simulation of scattering light inside a volume. Used together with KHR_materials_volume + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_materials\_ior +KHR_materials_ior makes the index of refraction configurable + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_materials\_specular +KHR_materials_specular allows configuring specular color (f0 color) and amount of specular reflection + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_materials\_iridescence +KHR_materials_iridescence adds a thin-film iridescence effect + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_materials\_diffuse\_transmission +KHR_materials_diffuse_transmission allows light to pass diffusely through the material + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_materials\_anisotropy +KHR_materials_anisotropy defines microfacet grooves in the surface, stretching the specular reflection on the surface + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_materials\_dispersion +KHR_materials_dispersion defines configuring the strength of the angular separation of colors (chromatic abberation) + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_materials\_emissive\_strength +KHR_materials_emissive_strength enables emissive factors larger than 1.0 + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_interactivity +KHR_interactivity enables execution of a behavior graph + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_node\_hoverability +KHR_node_hoverability enables hovering over nodes + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_node\_selectability +KHR_node_selectability enables selecting nodes + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) + + +##### enabledExtensions.KHR\_node\_visibility +KHR_node_visibility enables controlling the visibility of nodes + +**Kind**: static property of [enabledExtensions](#GltfState+renderingParameters.enabledExtensions) #### renderingParameters.clearColor @@ -408,6 +609,7 @@ such as "NORMAL" * [.GEOMETRYNORMAL](#GltfState.DebugOutput.generic.GEOMETRYNORMAL) * [.TANGENT](#GltfState.DebugOutput.generic.TANGENT) * [.BITANGENT](#GltfState.DebugOutput.generic.BITANGENT) + * [.TANGENTW](#GltfState.DebugOutput.generic.TANGENTW) * [.WORLDSPACENORMAL](#GltfState.DebugOutput.generic.WORLDSPACENORMAL) * [.ALPHA](#GltfState.DebugOutput.generic.ALPHA) * [.OCCLUSION](#GltfState.DebugOutput.generic.OCCLUSION) @@ -459,6 +661,7 @@ generic debug outputs * [.GEOMETRYNORMAL](#GltfState.DebugOutput.generic.GEOMETRYNORMAL) * [.TANGENT](#GltfState.DebugOutput.generic.TANGENT) * [.BITANGENT](#GltfState.DebugOutput.generic.BITANGENT) + * [.TANGENTW](#GltfState.DebugOutput.generic.TANGENTW) * [.WORLDSPACENORMAL](#GltfState.DebugOutput.generic.WORLDSPACENORMAL) * [.ALPHA](#GltfState.DebugOutput.generic.ALPHA) * [.OCCLUSION](#GltfState.DebugOutput.generic.OCCLUSION) @@ -499,6 +702,12 @@ output the tangent from the TBN ##### generic.BITANGENT output the bitangent from the TBN +**Kind**: static property of [generic](#GltfState.DebugOutput.generic) + + +##### generic.TANGENTW +output the tangent w from the TBN (black corresponds to -1; white to 1 + **Kind**: static property of [generic](#GltfState.DebugOutput.generic) @@ -722,6 +931,69 @@ output the anisotropic strength output final direction as defined by the anisotropyTexture and rotation **Kind**: static property of [anisotropy](#GltfState.DebugOutput.anisotropy) + + +## AnimationTimer +AnimationTimer class to control animation playback. + +**Kind**: global class + +* [AnimationTimer](#AnimationTimer) + * [.start()](#AnimationTimer+start) + * [.pause()](#AnimationTimer+pause) + * [.unpause()](#AnimationTimer+unpause) + * [.toggle()](#AnimationTimer+toggle) + * [.reset()](#AnimationTimer+reset) + * [.setFixedTime(timeInSec)](#AnimationTimer+setFixedTime) + * [.elapsedSec()](#AnimationTimer+elapsedSec) + + + +### animationTimer.start() +Start the animation timer and all animations + +**Kind**: instance method of [AnimationTimer](#AnimationTimer) + + +### animationTimer.pause() +Pause all animations + +**Kind**: instance method of [AnimationTimer](#AnimationTimer) + + +### animationTimer.unpause() +Unpause all animations + +**Kind**: instance method of [AnimationTimer](#AnimationTimer) + + +### animationTimer.toggle() +Toggle the animation playback state + +**Kind**: instance method of [AnimationTimer](#AnimationTimer) + + +### animationTimer.reset() +Reset the animation timer. If animations were playing, they will be restarted. + +**Kind**: instance method of [AnimationTimer](#AnimationTimer) + + +### animationTimer.setFixedTime(timeInSec) +Plays all animations starting from the specified time + +**Kind**: instance method of [AnimationTimer](#AnimationTimer) + +| Param | Type | Description | +| --- | --- | --- | +| timeInSec | number | The time in seconds to set the animation timer to | + + + +### animationTimer.elapsedSec() +Get the elapsed time in seconds + +**Kind**: instance method of [AnimationTimer](#AnimationTimer) ## ResourceLoader @@ -992,3 +1264,100 @@ Fit view to updated canvas size without changing rotation if distance is incorre | gltf | Gltf | | sceneIndex | number | + + +## GraphController +A controller for managing KHR_interactivity graphs in a glTF scene. + +**Kind**: global class + +* [GraphController](#GraphController) + * [.initializeGraphs(state)](#GraphController+initializeGraphs) + * [.loadGraph(graphIndex)](#GraphController+loadGraph) ⇒ Array + * [.stopGraphEngine()](#GraphController+stopGraphEngine) + * [.pauseGraph()](#GraphController+pauseGraph) + * [.resumeGraph()](#GraphController+resumeGraph) + * [.resetGraph()](#GraphController+resetGraph) + * [.dispatchEvent(eventName, data)](#GraphController+dispatchEvent) + * [.addCustomEventListener(eventName, callback)](#GraphController+addCustomEventListener) + * [.clearCustomEventListeners()](#GraphController+clearCustomEventListeners) + + + +### graphController.initializeGraphs(state) +Initialize the graph controller with the given state. +This needs to be called every time a glTF assets is loaded. + +**Kind**: instance method of [GraphController](#GraphController) + +| Param | Type | Description | +| --- | --- | --- | +| state | [GltfState](#GltfState) | The state of the application. | + + + +### graphController.loadGraph(graphIndex) ⇒ Array +Loads the specified graph. Resets the engine. Starts playing if this.playing is true. + +**Kind**: instance method of [GraphController](#GraphController) +**Returns**: Array - An array of custom events defined in the graph. + +| Param | Type | +| --- | --- | +| graphIndex | number | + + + +### graphController.stopGraphEngine() +Stops the graph engine. + +**Kind**: instance method of [GraphController](#GraphController) + + +### graphController.pauseGraph() +Pauses the currently playing graph. + +**Kind**: instance method of [GraphController](#GraphController) + + +### graphController.resumeGraph() +Resumes the currently paused graph. + +**Kind**: instance method of [GraphController](#GraphController) + + +### graphController.resetGraph() +Resets the current graph. + +**Kind**: instance method of [GraphController](#GraphController) + + +### graphController.dispatchEvent(eventName, data) +Dispatches an event to the behavior engine. + +**Kind**: instance method of [GraphController](#GraphController) + +| Param | Type | +| --- | --- | +| eventName | string | +| data | \* | + + + +### graphController.addCustomEventListener(eventName, callback) +Adds a custom event listener to the decorator. +Khronos test assets use test/onStart, test/onFail and test/onSuccess. + +**Kind**: instance method of [GraphController](#GraphController) + +| Param | Type | +| --- | --- | +| eventName | string | +| callback | function | + + + +### graphController.clearCustomEventListeners() +Clears all custom event listeners from the decorator. + +**Kind**: instance method of [GraphController](#GraphController) diff --git a/README.md b/README.md index 6de47b75..c6c6c457 100644 --- a/README.md +++ b/README.md @@ -9,27 +9,38 @@ Try out the [glTF Sample Viewer](https://github.khronos.org/glTF-Sample-Viewer-R ## Table of Contents - [Khronos glTF Sample Renderer](#khronos-gltf-sample-renderer) - - [Table of Contents](#table-of-contents) - - [Credits](#credits) - - [Features](#features) - - [API](#api) - - [GltfView](#gltfview) - - [GltfState](#gltfstate) - - [ResourceLoader](#resourceloader) - - [Render Fidelity Tools](#render-fidelity-tools) + - [Table of Contents](#table-of-contents) + - [Credits](#credits) + - [Features](#features) + - [API](#api) + - [GltfView](#gltfview) + - [GltfState](#gltfstate) + - [GraphController](#graphcontroller) + - [AnimationTimer](#animationtimer) + - [ResourceLoader](#resourceloader) + - [Render Fidelity Tools](#render-fidelity-tools) + - [Development](#development) - [Formatting](#formatting) - - [Visual Studio Code](#visual-studio-code) + - [Visual Studio Code](#visual-studio-code) + - [Testing](#testing) ## Credits Developed and refactored by [UX3D](https://www.ux3d.io/). Supported by the [Khronos Group](https://www.khronos.org/) and by [Google](https://www.google.com/) for the glTF Draco mesh compression import. Formerly hosted together with the example frontend at the [glTF Sample Viewer](https://github.com/KhronosGroup/glTF-Sample-Viewer) repository. Original code based on the concluded [glTF-WebGL-PBR](https://github.com/KhronosGroup/glTF-Sample-Viewer/tree/glTF-WebGL-PBR) project. Previously supported by [Facebook](https://www.facebook.com/) for animations, skinning and morphing. +For KHR_interactivity, the behavior engine of the [glTF-InteractivityGraph-AuthoringTool](https://github.com/KhronosGroup/glTF-InteractivityGraph-AuthoringTool) is used. ## Features - [x] glTF 2.0 +- [KHR_accessor_float64](https://github.com/KhronosGroup/glTF/pull/2397) + - [x] Animations + - [x] KHR_animation_pointer + - [ ] Mesh Attributes not supported since WebGL2 only supports 32 bit + - [ ] Skins not supported since WebGL2 only supports 32 bit - [x] [KHR_animation_pointer](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_animation_pointer) - [x] [KHR_draco_mesh_compression](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_draco_mesh_compression) +- [x] [KHR_interactivity](https://github.com/KhronosGroup/glTF/pull/2293) - [x] [KHR_lights_punctual](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_lights_punctual) - [x] [KHR_materials_anisotropy](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_anisotropy) - [x] [KHR_materials_clearcoat](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_clearcoat) @@ -49,6 +60,9 @@ Formerly hosted together with the example frontend at the [glTF Sample Viewer](h - [x] For dense volumes using KHR_materials_diffuse_transmission - [ ] For sparse volumes using KHR_materials_transmission - [x] [KHR_mesh_quantization](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_mesh_quantization) +- [x] [KHR_node_hoverability](https://github.com/KhronosGroup/glTF/pull/2426) +- [x] [KHR_node_selectability](https://github.com/KhronosGroup/glTF/pull/2422) +- [x] [KHR_node_visibility](https://github.com/KhronosGroup/glTF/pull/2410) - [x] [KHR_texture_basisu](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_basisu) - [x] [KHR_texture_transform](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_transform) - [x] [KHR_xmp_json_ld](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_xmp_json_ld) @@ -94,6 +108,21 @@ state.animationTimer.start(); The state is passed to the `view.renderFrame` function to specify the content that should be rendered. +#### GraphController + +The GltfState contains an instance of the GraphController which can be used to load and execute `KHR_interactivity` graphs. One can also send custom events to the graph or subscribe to custom event via callbacks. + +In the GltfState you can define an array of selection and hover points. Each element of the array represents one controller. If `triggerSelection` is set to `true`, the renderer will return the picking result of the clicked position via `selectionCallback`. The interactivity engine will be notified as well, if `KHR_node_selectability` is used in the current glTF. + +If `enableHover` is set to `true`, the renderer will return the picking result of the hovered position via `hoverCallback`. The interactivity engine receives hover results independent of `enableHover` based on the `hoverPositions` array. `enableHover` enables the use of custom hover handling independent of `KHR_interactivity` and is set to `false` by default. + +To make sure that `KHR_interactivity` always behaves correctly together with `KHR_node_selectability` and `KHR_node_hoverability`, update the values in the `hoverPositions` and `selectionPositions` arrays and trigger selections via `triggerSelection`. Currently, only one controller is supported. All entries except the first one of each array are ignored. Arrays are used to enable multiple controllers in the future without breaking the API. + +#### AnimationTimer + +The GltfState contains an instance of the AnimationTimer, which is used to play, pause and reset animations. It needs to be started to enable animations. +The `KHR_interactivity` extension controls animations if present. Therefore, the GraphController uses the time of the AnimationTimer to control animations. The GraphController is paused and resumed independently from the AnimationTimer, thus if an interactivity graph is paused, currently playing animations will continue playing if the AnimationTimer is not paused as well. + ### ResourceLoader The ResourceLoader can be used to load external resources and make them available to the renderer. @@ -106,7 +135,21 @@ state.gltf = await resourceLoader.loadGltf("path/to/some.gltf"); The glTF Sample Renderer is integrated into Google's [render fidelity tools](https://github.com/google/model-viewer/tree/master/packages/render-fidelity-tools). The render fidelity tools allow the comparison of different renderers. To run the project follow the instructions [here](https://github.com/google/model-viewer/blob/master/README.md) and [here](https://github.com/google/model-viewer/blob/master/packages/render-fidelity-tools/README.md). For information on how the glTF Sample Renderer was integrated see the [pull request on Github](https://github.com/google/model-viewer/pull/1962). -## Formatting +## Development + +After cloning this repository, run + +``` +npm install +``` + +to install all dependencies. To test and view your changes on a canvas, it is recommended to clone [glTF Sample Viewer](https://github.com/KhronosGroup/glTF-Sample-Viewer) which uses this renderer as a submodule. + +`npm run build` will build the npm package and put the bundled code into the `dist` directory. + +`npm run build_docs` will regenerate the [API documentation](API.md). + +### Formatting This repository uses [Prettier](https://prettier.io/) for code formatting and [ESLint](https://eslint.org/) for linting. @@ -126,3 +169,27 @@ There are extensions for both Prettier and ESLint in Visual Studio Code. They ca - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) You are encouraged to run Prettier and ESLint on your code before committing. + +### Testing + +glTF-Sample-Render uses [Playwright](https://playwright.dev/) for testing.\ +Currently, only `KHR_interactivity` tests are implemented. + +To run the tests run + +``` +npm run test +``` + +Playwright creates a new browser instance for each test. It can run on Chrome, Safari, Firefox and emulated mobile browsers. After all tests were run, a browser window with a summary will open. The `tests/testApp` directory contains a minimal frontend to be able to start a testing server. The server is started automatically. For debugging the test server you can also start it manually by running + +``` +npm run testApp +``` + +Tests are defined in the `tests` directory by files with the `.spec.ts` ending.\ +The interactivity tests download all test assets from the [glTF-Test-Assets-Interactivity repository](https://github.com/KhronosGroup/glTF-Test-Assets-Interactivity), loads each test file and listens on the `test/onStart`, `test/onSuccess` and `test/onFailed` events to determine if an interactivity test passes or not. `test/onStart` returns the needed execution time in seconds. + +You can also run more complex Playwright commands such as `npx playwright test --ui` or `npx playwright test --project chromium`. For more information check https://playwright.dev/docs/running-tests + +One can also use the [Playwright extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) to run the test more easily with advanced parameters, run tests only selectively or debug tests by adding breakpoints. diff --git a/eslint.config.js b/eslint.config.js index 95d627ae..8209406d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,7 +23,12 @@ export default [ semi: "warn", "no-extra-semi": "warn", "no-undef": "warn", - "no-unused-vars": "warn", + "no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_" + } + ], "no-empty": "warn", "no-redeclare": "warn", "no-prototype-builtins": "warn", diff --git a/package-lock.json b/package-lock.json index a361f1a7..f3c77c40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.1.0", "license": "Apache-2.0", "dependencies": { + "@khronosgroup/gltf-interactivity-sample-engine": "file:../../glTF-InteractivityGraph-AuthoringTool/src/BasicBehaveEngine", "fast-png": "^6.2.0", "gl-matrix": "^3.2.1", "globals": "^15.5.0", @@ -16,46 +17,60 @@ "json-ptr": "^3.1.0" }, "devDependencies": { + "@playwright/test": "^1.56.0", "@rollup/plugin-commonjs": "^26.0.1", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-wasm": "^6.2.2", + "@types/node": "^24.7.2", "concurrently": "^8.2.2", "eslint": "^9.5.0", "eslint-config-prettier": "^10.1.8", - "jsdoc-to-markdown": "^8.0.1", + "jsdoc-to-markdown": "^9.1.3", "prettier": "3.6.2", "rollup": "^4.23.0", "rollup-plugin-copy": "^3.5.0", "rollup-plugin-glslify": "^1.3.1", "rollup-plugin-license": "^3.5.2", - "serve": "^14.2.4" + "serve": "^14.2.5" + } + }, + "../../glTF-InteractivityGraph-AuthoringTool/src/BasicBehaveEngine": { + "name": "@khronosgroup/gltf-interactivity-sample-engine", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "gl-matrix": "^3.4.3" + }, + "devDependencies": { + "@types/node": "^24.1.0", + "typescript": "^5.8.3" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", - "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "dependencies": { - "@babel/types": "^7.25.6" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -74,14 +89,13 @@ } }, "node_modules/@babel/types": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", - "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -100,16 +114,19 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -313,9 +330,9 @@ "dev": true }, "node_modules/@jsdoc/salty": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", - "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz", + "integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==", "dev": true, "dependencies": { "lodash": "^4.17.21" @@ -324,6 +341,10 @@ "node": ">=v12.0.0" } }, + "node_modules/@khronosgroup/gltf-interactivity-sample-engine": { + "resolved": "../../glTF-InteractivityGraph-AuthoringTool/src/BasicBehaveEngine", + "link": true + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -369,6 +390,21 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0.tgz", + "integrity": "sha512-Tzh95Twig7hUwwNe381/K3PggZBZblKUe2wv25oIpzWLr6Z0m4KgV1ZVIjnR6GM9ANEqjZD7XsZEa6JL/7YEgg==", + "dev": true, + "dependencies": { + "playwright": "1.56.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/plugin-commonjs": { "version": "26.0.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.3.tgz", @@ -728,12 +764,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.7.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", - "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", + "version": "24.7.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", + "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", "dev": true, "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~7.14.0" } }, "node_modules/@types/pako": { @@ -753,19 +789,6 @@ "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", "dev": true }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -832,27 +855,6 @@ "node": ">=8" } }, - "node_modules/ansi-escape-sequences": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-4.1.0.tgz", - "integrity": "sha512-dzW9kHxH011uBsidTXd14JXgzye/YLb2LzeKZ4bsgl/Knwx8AtbSFkkGxagdNOoh0DlqHCmfiEjWKBaqjOanVw==", - "dev": true, - "dependencies": { - "array-back": "^3.0.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ansi-escape-sequences/node_modules/array-back": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", - "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1030,26 +1032,23 @@ } }, "node_modules/cache-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cache-point/-/cache-point-2.0.0.tgz", - "integrity": "sha512-4gkeHlFpSKgm3vm2gJN5sPqfmijYRFYCQ6tv5cLw0xVmT6r1z1vd4FNnpuOREco3cBs1G709sZ72LdgddKvL5w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cache-point/-/cache-point-3.0.1.tgz", + "integrity": "sha512-itTIMLEKbh6Dw5DruXbxAgcyLnh/oPGVLBfTPqBOftASxHe8bAeXy7JkO4F0LvHqht7XqP5O/09h5UcHS2w0FA==", "dev": true, "dependencies": { - "array-back": "^4.0.1", - "fs-then-native": "^2.0.0", - "mkdirp2": "^1.0.4" + "array-back": "^6.2.2" }, "engines": { - "node": ">=8" - } - }, - "node_modules/cache-point/node_modules/array-back": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", - "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", - "dev": true, - "engines": { - "node": ">=8" + "node": ">=12.17" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } } }, "node_modules/callsites": { @@ -1208,19 +1207,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/collect-all": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/collect-all/-/collect-all-1.0.4.tgz", - "integrity": "sha512-RKZhRwJtJEP5FWul+gkSMEnaK6H3AGPTTWOiRimCcs+rc/OmQE3Yhy1Q7A7KsdkG3ZXVdZq68Y6ONSdvkeEcKA==", - "dev": true, - "dependencies": { - "stream-connect": "^1.0.2", - "stream-via": "^1.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1246,91 +1232,41 @@ "dev": true }, "node_modules/command-line-args": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", - "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz", + "integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==", "dev": true, "dependencies": { - "array-back": "^3.1.0", - "find-replace": "^3.0.0", + "array-back": "^6.2.2", + "find-replace": "^5.0.2", "lodash.camelcase": "^4.3.0", - "typical": "^4.0.0" + "typical": "^7.2.0" }, "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/command-line-args/node_modules/array-back": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", - "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/command-line-args/node_modules/typical": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", - "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/command-line-tool": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/command-line-tool/-/command-line-tool-0.8.0.tgz", - "integrity": "sha512-Xw18HVx/QzQV3Sc5k1vy3kgtOeGmsKIqwtFFoyjI4bbcpSgnw2CWVULvtakyw4s6fhyAdI6soQQhXc2OzJy62g==", - "dev": true, - "dependencies": { - "ansi-escape-sequences": "^4.0.0", - "array-back": "^2.0.0", - "command-line-args": "^5.0.0", - "command-line-usage": "^4.1.0", - "typical": "^2.6.1" + "node": ">=12.20" }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/command-line-tool/node_modules/array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "dev": true, - "dependencies": { - "typical": "^2.6.1" + "peerDependencies": { + "@75lb/nature": "latest" }, - "engines": { - "node": ">=4" + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } } }, "node_modules/command-line-usage": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-4.1.0.tgz", - "integrity": "sha512-MxS8Ad995KpdAC0Jopo/ovGIroV/m0KHwzKfXxKag6FHOkGsH8/lv5yjgablcRxCJJC0oJeUMuO/gmaq+Wq46g==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz", + "integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==", "dev": true, "dependencies": { - "ansi-escape-sequences": "^4.0.0", - "array-back": "^2.0.0", - "table-layout": "^0.4.2", - "typical": "^2.6.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/command-line-usage/node_modules/array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "dev": true, - "dependencies": { - "typical": "^2.6.1" + "array-back": "^6.2.2", + "chalk-template": "^0.4.0", + "table-layout": "^4.1.0", + "typical": "^7.1.1" }, "engines": { - "node": ">=4" + "node": ">=12.20.0" } }, "node_modules/commander": { @@ -1346,12 +1282,12 @@ "dev": true }, "node_modules/common-sequence": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/common-sequence/-/common-sequence-2.0.2.tgz", - "integrity": "sha512-jAg09gkdkrDO9EWTdXfv80WWH3yeZl5oT69fGfedBNS9pXUKYInVJ1bJ+/ht2+Moeei48TmSbQDYMc8EOx9G0g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-sequence/-/common-sequence-3.0.0.tgz", + "integrity": "sha512-g/CgSYk93y+a1IKm50tKl7kaT/OjjTYVQlEbUlt/49ZLV1mcKpUU7iyDiqTAeLdb4QDtQfq3ako8y8v//fzrWQ==", "dev": true, "engines": { - "node": ">=8" + "node": ">=12.17" } }, "node_modules/commondir": { @@ -1373,23 +1309,32 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/compression/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1405,12 +1350,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1506,6 +1445,15 @@ "node": ">= 8" } }, + "node_modules/current-module-paths": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/current-module-paths/-/current-module-paths-1.1.2.tgz", + "integrity": "sha512-H4s4arcLx/ugbu1XkkgSvcUZax0L6tXUqnppGniQb8l5VjUKGHoayXE5RiriiPhYDd+kjZnaok1Uig13PKtKYQ==", + "dev": true, + "engines": { + "node": ">=12.17" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -1576,26 +1524,29 @@ } }, "node_modules/dmd": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/dmd/-/dmd-6.2.3.tgz", - "integrity": "sha512-SIEkjrG7cZ9GWZQYk/mH+mWtcRPly/3ibVuXO/tP/MFoWz6KiRK77tSMq6YQBPl7RljPtXPQ/JhxbNuCdi1bNw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/dmd/-/dmd-7.1.1.tgz", + "integrity": "sha512-Ap2HP6iuOek7eShReDLr9jluNJm9RMZESlt29H/Xs1qrVMkcS9X6m5h1mBC56WMxNiSo0wvjGICmZlYUSFjwZQ==", "dev": true, "dependencies": { "array-back": "^6.2.2", - "cache-point": "^2.0.0", - "common-sequence": "^2.0.2", - "file-set": "^4.0.2", + "cache-point": "^3.0.0", + "common-sequence": "^3.0.0", + "file-set": "^5.2.2", "handlebars": "^4.7.8", "marked": "^4.3.0", - "object-get": "^2.1.1", - "reduce-flatten": "^3.0.1", - "reduce-unique": "^2.0.1", - "reduce-without": "^1.0.1", - "test-value": "^3.0.0", - "walk-back": "^5.1.0" + "walk-back": "^5.1.1" }, "engines": { - "node": ">=12" + "node": ">=12.17" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } } }, "node_modules/duplexify": { @@ -1780,9 +1731,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2010,46 +1961,24 @@ } }, "node_modules/file-set": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/file-set/-/file-set-4.0.2.tgz", - "integrity": "sha512-fuxEgzk4L8waGXaAkd8cMr73Pm0FxOVkn8hztzUW7BAHhOGH90viQNXbiOsnecCWmfInqU6YmAMwxRMdKETceQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/file-set/-/file-set-5.3.0.tgz", + "integrity": "sha512-FKCxdjLX0J6zqTWdT0RXIxNF/n7MyXXnsSUp0syLEOCKdexvPZ02lNNv2a+gpK9E3hzUYF3+eFZe32ci7goNUg==", "dev": true, "dependencies": { - "array-back": "^5.0.0", - "glob": "^7.1.6" + "array-back": "^6.2.2", + "fast-glob": "^3.3.2" }, "engines": { - "node": ">=10" - } - }, - "node_modules/file-set/node_modules/array-back": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-5.0.0.tgz", - "integrity": "sha512-kgVWwJReZWmVuWOQKEOohXKJX+nD02JAZ54D1RRWlv8L0NebauKAaFxACKzB74RTclt1+WNz5KHaLRDAPZbDEw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/file-set/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "node": ">=12.17" }, - "engines": { - "node": "*" + "peerDependencies": { + "@75lb/nature": "latest" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } } }, "node_modules/fill-range": { @@ -2065,24 +1994,20 @@ } }, "node_modules/find-replace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", - "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", - "dev": true, - "dependencies": { - "array-back": "^3.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/find-replace/node_modules/array-back": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", - "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", + "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", "dev": true, "engines": { - "node": ">=6" + "node": ">=14" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } } }, "node_modules/find-up": { @@ -2160,15 +2085,6 @@ "node": ">=6 <7 || >=8" } }, - "node_modules/fs-then-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fs-then-native/-/fs-then-native-2.0.0.tgz", - "integrity": "sha512-X712jAOaWXkemQCAmWeg5rOT2i+KOpWz1Z/txk/cW0qlOu2oQ9H61vc5w3X/iyuUEfq/OyaFJ78/cZAQD1/bgA==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2844,9 +2760,9 @@ } }, "node_modules/jsdoc": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.3.tgz", - "integrity": "sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", + "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", "dev": true, "dependencies": { "@babel/parser": "^7.20.15", @@ -2873,76 +2789,73 @@ } }, "node_modules/jsdoc-api": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/jsdoc-api/-/jsdoc-api-8.1.1.tgz", - "integrity": "sha512-yas9E4h8NHp1CTEZiU/DPNAvLoUcip+Hl8Xi1RBYzHqSrgsF+mImAZNtwymrXvgbrgl4bNGBU9syulM0JzFeHQ==", + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/jsdoc-api/-/jsdoc-api-9.3.5.tgz", + "integrity": "sha512-TQwh1jA8xtCkIbVwm/XA3vDRAa5JjydyKx1cC413Sh3WohDFxcMdwKSvn4LOsq2xWyAmOU/VnSChTQf6EF0R8g==", "dev": true, "dependencies": { "array-back": "^6.2.2", - "cache-point": "^2.0.0", - "collect-all": "^1.0.4", - "file-set": "^4.0.2", - "fs-then-native": "^2.0.0", - "jsdoc": "^4.0.3", + "cache-point": "^3.0.1", + "current-module-paths": "^1.1.2", + "file-set": "^5.3.0", + "jsdoc": "^4.0.4", "object-to-spawn-args": "^2.0.1", - "temp-path": "^1.0.0", - "walk-back": "^5.1.0" + "walk-back": "^5.1.1" }, "engines": { "node": ">=12.17" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } } }, "node_modules/jsdoc-parse": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/jsdoc-parse/-/jsdoc-parse-6.2.4.tgz", - "integrity": "sha512-MQA+lCe3ioZd0uGbyB3nDCDZcKgKC7m/Ivt0LgKZdUoOlMJxUWJQ3WI6GeyHp9ouznKaCjlp7CU9sw5k46yZTw==", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/jsdoc-parse/-/jsdoc-parse-6.2.5.tgz", + "integrity": "sha512-8JaSNjPLr2IuEY4Das1KM6Z4oLHZYUnjRrr27hKSa78Cj0i5Lur3DzNnCkz+DfrKBDoljGMoWOiBVQbtUZJBPw==", "dev": true, "dependencies": { "array-back": "^6.2.2", "find-replace": "^5.0.1", - "lodash.omit": "^4.5.0", "sort-array": "^5.0.0" }, "engines": { "node": ">=12" } }, - "node_modules/jsdoc-parse/node_modules/find-replace": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", - "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", - "dev": true, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@75lb/nature": "latest" - }, - "peerDependenciesMeta": { - "@75lb/nature": { - "optional": true - } - } - }, "node_modules/jsdoc-to-markdown": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/jsdoc-to-markdown/-/jsdoc-to-markdown-8.0.3.tgz", - "integrity": "sha512-JGYYd5xygnQt1DIxH+HUI+X/ynL8qWihzIF0n15NSCNtM6MplzawURRcaLI2WkiS2hIjRIgsphCOfM7FkaWiNg==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/jsdoc-to-markdown/-/jsdoc-to-markdown-9.1.3.tgz", + "integrity": "sha512-i9wi+6WHX0WKziv0ar88T8h7OmxA0LWdQaV23nY6uQyKvdUPzVt0o6YAaOceFuKRF5Rvlju5w/KnZBfdpDAlnw==", "dev": true, "dependencies": { "array-back": "^6.2.2", - "command-line-tool": "^0.8.0", + "command-line-args": "^6.0.1", + "command-line-usage": "^7.0.3", "config-master": "^3.1.0", - "dmd": "^6.2.3", - "jsdoc-api": "^8.1.1", - "jsdoc-parse": "^6.2.1", - "walk-back": "^5.1.0" + "dmd": "^7.1.1", + "jsdoc-api": "^9.3.5", + "jsdoc-parse": "^6.2.5", + "walk-back": "^5.1.1" }, "bin": { "jsdoc2md": "bin/cli.js" }, "engines": { "node": ">=12.17" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } } }, "node_modules/jsdoc/node_modules/escape-string-regexp": { @@ -3059,18 +2972,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lodash.omit": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", - "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==", - "dev": true - }, - "node_modules/lodash.padend": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", - "integrity": "sha512-sOQs2aqGpbl27tmCS1QNZA09Uqp01ZzWfDUoD+xzTii0E7dSQfRKcRetFwa+uXaxaqL+TKm7CgD2JdKP7aZBSw==", - "dev": true - }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -3178,30 +3079,9 @@ } }, "node_modules/mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "engines": { "node": ">= 0.6" @@ -3258,12 +3138,6 @@ "node": ">=10" } }, - "node_modules/mkdirp2": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/mkdirp2/-/mkdirp2-1.0.5.tgz", - "integrity": "sha512-xOE9xbICroUDmG1ye2h4bZ8WBie9EGmACaco8K8cx6RlkJJrxGIqjGqztAI+NMhexXBcdGbSEzI6N3EJPevxZw==", - "dev": true - }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -3292,9 +3166,9 @@ "dev": true }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, "engines": { "node": ">= 0.6" @@ -3318,12 +3192,6 @@ "node": ">=8" } }, - "node_modules/object-get": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object-get/-/object-get-2.1.1.tgz", - "integrity": "sha512-7n4IpLMzGGcLEMiQKsNR7vCe+N5E9LORFrtNUVy4sO3dj9a3HedZCxEL2T7QuLhcHN1NBuBsMOKaOsAYI9IIvg==", - "dev": true - }, "node_modules/object-to-spawn-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object-to-spawn-args/-/object-to-spawn-args-2.0.1.tgz", @@ -3334,9 +3202,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, "engines": { "node": ">= 0.8" @@ -3530,6 +3398,50 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz", + "integrity": "sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==", + "dev": true, + "dependencies": { + "playwright-core": "1.56.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0.tgz", + "integrity": "sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3650,61 +3562,6 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, - "node_modules/reduce-flatten": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-3.0.1.tgz", - "integrity": "sha512-bYo+97BmUUOzg09XwfkwALt4PQH1M5L0wzKerBt6WLm3Fhdd43mMS89HiT1B9pJIqko/6lWx3OnV4J9f2Kqp5Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/reduce-unique": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/reduce-unique/-/reduce-unique-2.0.1.tgz", - "integrity": "sha512-x4jH/8L1eyZGR785WY+ePtyMNhycl1N2XOLxhCbzZFaqF4AXjLzqSxa2UHgJ2ZVR/HHyPOvl1L7xRnW8ye5MdA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/reduce-without": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/reduce-without/-/reduce-without-1.0.1.tgz", - "integrity": "sha512-zQv5y/cf85sxvdrKPlfcRzlDn/OqKFThNimYmsS3flmkioKvkUGn2Qg9cJVoQiEvdxFGLE0MQER/9fZ9sUqdxg==", - "dev": true, - "dependencies": { - "test-value": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/reduce-without/node_modules/array-back": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw==", - "dev": true, - "dependencies": { - "typical": "^2.6.0" - }, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/reduce-without/node_modules/test-value": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz", - "integrity": "sha512-+1epbAxtKeXttkGFMTX9H42oqzOTufR1ceCF+GYA5aOmvaPq9wd4PUS8329fn2RRLGNeUkgRLnVpycjx8DsO2w==", - "dev": true, - "dependencies": { - "array-back": "^1.0.3", - "typical": "^2.6.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/registry-auth-token": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", @@ -3969,9 +3826,9 @@ ] }, "node_modules/serve": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz", - "integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.5.tgz", + "integrity": "sha512-Qn/qMkzCcMFVPb60E/hQy+iRLpiU8PamOfOSYoAHmmF+fFFmpPpqa6Oci2iWYpTdOUM3VF+TINud7CfbQnsZbA==", "dev": true, "dependencies": { "@zeit/schemas": "2.36.0", @@ -3981,7 +3838,7 @@ "chalk": "5.0.1", "chalk-template": "0.4.0", "clipboardy": "3.0.0", - "compression": "1.7.4", + "compression": "1.8.1", "is-port-reachable": "4.0.0", "serve-handler": "6.1.6", "update-check": "1.5.4" @@ -4121,9 +3978,9 @@ } }, "node_modules/sort-array": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/sort-array/-/sort-array-5.0.0.tgz", - "integrity": "sha512-Sg9MzajSGprcSrMIxsXyNT0e0JB47RJRfJspC+7co4Z5BdNsNl8FmWI+lXEpyKq+vkMG6pHgAhqyCO+bkDTfFQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/sort-array/-/sort-array-5.1.1.tgz", + "integrity": "sha512-EltS7AIsNlAFIM9cayrgKrM6XP94ATWwXP4LCL4IQbvbYhELSt2hZTrixg+AaQwnWFs/JGJgqU3rxMcNNWxGAA==", "dev": true, "dependencies": { "array-back": "^6.2.2", @@ -4141,15 +3998,6 @@ } } }, - "node_modules/sort-array/node_modules/typical": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-7.2.0.tgz", - "integrity": "sha512-W1+HdVRUl8fS3MZ9ogD51GOb46xMmhAZzR0WPw5jcgIZQJVvkddYzAl4YTU6g5w33Y1iRQLdIi2/1jhi2RNL0g==", - "dev": true, - "engines": { - "node": ">=12.17" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4242,46 +4090,12 @@ "escodegen": "^2.1.0" } }, - "node_modules/stream-connect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-connect/-/stream-connect-1.0.2.tgz", - "integrity": "sha512-68Kl+79cE0RGKemKkhxTSg8+6AGrqBt+cbZAXevg2iJ6Y3zX4JhA/sZeGzLpxW9cXhmqAcE7KnJCisUmIUfnFQ==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "dependencies": { - "array-back": "^1.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stream-connect/node_modules/array-back": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw==", - "dev": true, - "dependencies": { - "typical": "^2.6.0" - }, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/stream-shift": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "dev": true }, - "node_modules/stream-via": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/stream-via/-/stream-via-1.0.4.tgz", - "integrity": "sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -4436,62 +4250,16 @@ } }, "node_modules/table-layout": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-0.4.5.tgz", - "integrity": "sha512-zTvf0mcggrGeTe/2jJ6ECkJHAQPIYEwDoqsiqBjI24mvRmQbInK5jq33fyypaCBxX08hMkfmdOqj6haT33EqWw==", - "dev": true, - "dependencies": { - "array-back": "^2.0.0", - "deep-extend": "~0.6.0", - "lodash.padend": "^4.6.1", - "typical": "^2.6.1", - "wordwrapjs": "^3.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/table-layout/node_modules/array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "dev": true, - "dependencies": { - "typical": "^2.6.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/temp-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/temp-path/-/temp-path-1.0.0.tgz", - "integrity": "sha512-TvmyH7kC6ZVTYkqCODjJIbgvu0FKiwQpZ4D1aknE7xpcDf/qEOB8KZEK5ef2pfbVoiBhNWs3yx4y+ESMtNYmlg==", - "dev": true - }, - "node_modules/test-value": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/test-value/-/test-value-3.0.0.tgz", - "integrity": "sha512-sVACdAWcZkSU9x7AOmJo5TqE+GyNJknHaHsMrR6ZnhjVlVN9Yx6FjHrsKZ3BjIpPCT68zYesPWkakrNupwfOTQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", "dev": true, "dependencies": { - "array-back": "^2.0.0", - "typical": "^2.6.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/test-value/node_modules/array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "dev": true, - "dependencies": { - "typical": "^2.6.1" + "array-back": "^6.2.2", + "wordwrapjs": "^5.1.0" }, "engines": { - "node": ">=4" + "node": ">=12.17" } }, "node_modules/text-table": { @@ -4505,18 +4273,9 @@ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, "node_modules/to-regex-range": { @@ -4577,10 +4336,13 @@ "dev": true }, "node_modules/typical": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", - "integrity": "sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg==", - "dev": true + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "dev": true, + "engines": { + "node": ">=12.17" + } }, "node_modules/uc.micro": { "version": "2.1.0", @@ -4608,9 +4370,9 @@ "dev": true }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", "dev": true }, "node_modules/universalify": { @@ -4720,25 +4482,12 @@ "dev": true }, "node_modules/wordwrapjs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-3.0.0.tgz", - "integrity": "sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw==", - "dev": true, - "dependencies": { - "reduce-flatten": "^1.0.1", - "typical": "^2.6.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/wordwrapjs/node_modules/reduce-flatten": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", - "integrity": "sha512-j5WfFJfc9CoXv/WbwVLHq74i/hdTUpy+iNC534LxczMRP67vJeK3V9JOdnL0N1cIRbn9mYhE2yVjvvKXDxvNXQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.1.tgz", + "integrity": "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=12.17" } }, "node_modules/wrap-ansi": { @@ -4927,24 +4676,24 @@ }, "dependencies": { "@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true }, "@babel/parser": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", - "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "requires": { - "@babel/types": "^7.25.6" + "@babel/types": "^7.28.5" } }, "@babel/runtime": { @@ -4954,14 +4703,13 @@ "dev": true }, "@babel/types": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", - "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" } }, "@choojs/findup": { @@ -4974,12 +4722,12 @@ } }, "@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "requires": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "dependencies": { "eslint-visitor-keys": { @@ -5121,14 +4869,22 @@ "dev": true }, "@jsdoc/salty": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", - "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz", + "integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==", "dev": true, "requires": { "lodash": "^4.17.21" } }, + "@khronosgroup/gltf-interactivity-sample-engine": { + "version": "file:../../glTF-InteractivityGraph-AuthoringTool/src/BasicBehaveEngine", + "requires": { + "@types/node": "^24.1.0", + "gl-matrix": "^3.4.3", + "typescript": "^5.8.3" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5162,6 +4918,15 @@ "dev": true, "optional": true }, + "@playwright/test": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0.tgz", + "integrity": "sha512-Tzh95Twig7hUwwNe381/K3PggZBZblKUe2wv25oIpzWLr6Z0m4KgV1ZVIjnR6GM9ANEqjZD7XsZEa6JL/7YEgg==", + "dev": true, + "requires": { + "playwright": "1.56.0" + } + }, "@rollup/plugin-commonjs": { "version": "26.0.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.3.tgz", @@ -5381,12 +5146,12 @@ "dev": true }, "@types/node": { - "version": "22.7.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", - "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", + "version": "24.7.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", + "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", "dev": true, "requires": { - "undici-types": "~6.19.2" + "undici-types": "~7.14.0" } }, "@types/pako": { @@ -5406,16 +5171,6 @@ "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", "dev": true }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, "acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -5469,23 +5224,6 @@ } } }, - "ansi-escape-sequences": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-4.1.0.tgz", - "integrity": "sha512-dzW9kHxH011uBsidTXd14JXgzye/YLb2LzeKZ4bsgl/Knwx8AtbSFkkGxagdNOoh0DlqHCmfiEjWKBaqjOanVw==", - "dev": true, - "requires": { - "array-back": "^3.0.1" - }, - "dependencies": { - "array-back": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", - "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", - "dev": true - } - } - }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -5615,22 +5353,12 @@ "dev": true }, "cache-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cache-point/-/cache-point-2.0.0.tgz", - "integrity": "sha512-4gkeHlFpSKgm3vm2gJN5sPqfmijYRFYCQ6tv5cLw0xVmT6r1z1vd4FNnpuOREco3cBs1G709sZ72LdgddKvL5w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cache-point/-/cache-point-3.0.1.tgz", + "integrity": "sha512-itTIMLEKbh6Dw5DruXbxAgcyLnh/oPGVLBfTPqBOftASxHe8bAeXy7JkO4F0LvHqht7XqP5O/09h5UcHS2w0FA==", "dev": true, "requires": { - "array-back": "^4.0.1", - "fs-then-native": "^2.0.0", - "mkdirp2": "^1.0.4" - }, - "dependencies": { - "array-back": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", - "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", - "dev": true - } + "array-back": "^6.2.2" } }, "callsites": { @@ -5742,16 +5470,6 @@ } } }, - "collect-all": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/collect-all/-/collect-all-1.0.4.tgz", - "integrity": "sha512-RKZhRwJtJEP5FWul+gkSMEnaK6H3AGPTTWOiRimCcs+rc/OmQE3Yhy1Q7A7KsdkG3ZXVdZq68Y6ONSdvkeEcKA==", - "dev": true, - "requires": { - "stream-connect": "^1.0.2", - "stream-via": "^1.0.4" - } - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5774,76 +5492,27 @@ "dev": true }, "command-line-args": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", - "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz", + "integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==", "dev": true, "requires": { - "array-back": "^3.1.0", - "find-replace": "^3.0.0", + "array-back": "^6.2.2", + "find-replace": "^5.0.2", "lodash.camelcase": "^4.3.0", - "typical": "^4.0.0" - }, - "dependencies": { - "array-back": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", - "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", - "dev": true - }, - "typical": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", - "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", - "dev": true - } - } - }, - "command-line-tool": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/command-line-tool/-/command-line-tool-0.8.0.tgz", - "integrity": "sha512-Xw18HVx/QzQV3Sc5k1vy3kgtOeGmsKIqwtFFoyjI4bbcpSgnw2CWVULvtakyw4s6fhyAdI6soQQhXc2OzJy62g==", - "dev": true, - "requires": { - "ansi-escape-sequences": "^4.0.0", - "array-back": "^2.0.0", - "command-line-args": "^5.0.0", - "command-line-usage": "^4.1.0", - "typical": "^2.6.1" - }, - "dependencies": { - "array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "dev": true, - "requires": { - "typical": "^2.6.1" - } - } + "typical": "^7.2.0" } }, "command-line-usage": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-4.1.0.tgz", - "integrity": "sha512-MxS8Ad995KpdAC0Jopo/ovGIroV/m0KHwzKfXxKag6FHOkGsH8/lv5yjgablcRxCJJC0oJeUMuO/gmaq+Wq46g==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz", + "integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==", "dev": true, "requires": { - "ansi-escape-sequences": "^4.0.0", - "array-back": "^2.0.0", - "table-layout": "^0.4.2", - "typical": "^2.6.1" - }, - "dependencies": { - "array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "dev": true, - "requires": { - "typical": "^2.6.1" - } - } + "array-back": "^6.2.2", + "chalk-template": "^0.4.0", + "table-layout": "^4.1.0", + "typical": "^7.1.1" } }, "commander": { @@ -5859,9 +5528,9 @@ "dev": true }, "common-sequence": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/common-sequence/-/common-sequence-2.0.2.tgz", - "integrity": "sha512-jAg09gkdkrDO9EWTdXfv80WWH3yeZl5oT69fGfedBNS9pXUKYInVJ1bJ+/ht2+Moeei48TmSbQDYMc8EOx9G0g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-sequence/-/common-sequence-3.0.0.tgz", + "integrity": "sha512-g/CgSYk93y+a1IKm50tKl7kaT/OjjTYVQlEbUlt/49ZLV1mcKpUU7iyDiqTAeLdb4QDtQfq3ako8y8v//fzrWQ==", "dev": true }, "commondir": { @@ -5880,20 +5549,26 @@ } }, "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "dependencies": { + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -5908,12 +5583,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true } } }, @@ -5992,6 +5661,12 @@ "which": "^2.0.1" } }, + "current-module-paths": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/current-module-paths/-/current-module-paths-1.1.2.tgz", + "integrity": "sha512-H4s4arcLx/ugbu1XkkgSvcUZax0L6tXUqnppGniQb8l5VjUKGHoayXE5RiriiPhYDd+kjZnaok1Uig13PKtKYQ==", + "dev": true + }, "date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -6038,23 +5713,18 @@ } }, "dmd": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/dmd/-/dmd-6.2.3.tgz", - "integrity": "sha512-SIEkjrG7cZ9GWZQYk/mH+mWtcRPly/3ibVuXO/tP/MFoWz6KiRK77tSMq6YQBPl7RljPtXPQ/JhxbNuCdi1bNw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/dmd/-/dmd-7.1.1.tgz", + "integrity": "sha512-Ap2HP6iuOek7eShReDLr9jluNJm9RMZESlt29H/Xs1qrVMkcS9X6m5h1mBC56WMxNiSo0wvjGICmZlYUSFjwZQ==", "dev": true, "requires": { "array-back": "^6.2.2", - "cache-point": "^2.0.0", - "common-sequence": "^2.0.2", - "file-set": "^4.0.2", + "cache-point": "^3.0.0", + "common-sequence": "^3.0.0", + "file-set": "^5.2.2", "handlebars": "^4.7.8", "marked": "^4.3.0", - "object-get": "^2.1.1", - "reduce-flatten": "^3.0.1", - "reduce-unique": "^2.0.1", - "reduce-without": "^1.0.1", - "test-value": "^3.0.0", - "walk-back": "^5.1.0" + "walk-back": "^5.1.1" } }, "duplexify": { @@ -6183,9 +5853,9 @@ } }, "eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true }, "espree": { @@ -6361,35 +6031,13 @@ } }, "file-set": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/file-set/-/file-set-4.0.2.tgz", - "integrity": "sha512-fuxEgzk4L8waGXaAkd8cMr73Pm0FxOVkn8hztzUW7BAHhOGH90viQNXbiOsnecCWmfInqU6YmAMwxRMdKETceQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/file-set/-/file-set-5.3.0.tgz", + "integrity": "sha512-FKCxdjLX0J6zqTWdT0RXIxNF/n7MyXXnsSUp0syLEOCKdexvPZ02lNNv2a+gpK9E3hzUYF3+eFZe32ci7goNUg==", "dev": true, "requires": { - "array-back": "^5.0.0", - "glob": "^7.1.6" - }, - "dependencies": { - "array-back": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-5.0.0.tgz", - "integrity": "sha512-kgVWwJReZWmVuWOQKEOohXKJX+nD02JAZ54D1RRWlv8L0NebauKAaFxACKzB74RTclt1+WNz5KHaLRDAPZbDEw==", - "dev": true - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } + "array-back": "^6.2.2", + "fast-glob": "^3.3.2" } }, "fill-range": { @@ -6402,21 +6050,11 @@ } }, "find-replace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", - "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", + "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", "dev": true, - "requires": { - "array-back": "^3.0.1" - }, - "dependencies": { - "array-back": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", - "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", - "dev": true - } - } + "requires": {} }, "find-up": { "version": "5.0.0", @@ -6475,12 +6113,6 @@ "universalify": "^0.1.0" } }, - "fs-then-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fs-then-native/-/fs-then-native-2.0.0.tgz", - "integrity": "sha512-X712jAOaWXkemQCAmWeg5rOT2i+KOpWz1Z/txk/cW0qlOu2oQ9H61vc5w3X/iyuUEfq/OyaFJ78/cZAQD1/bgA==", - "dev": true - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7022,9 +6654,9 @@ } }, "jsdoc": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.3.tgz", - "integrity": "sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", + "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", "dev": true, "requires": { "@babel/parser": "^7.20.15", @@ -7053,56 +6685,45 @@ } }, "jsdoc-api": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/jsdoc-api/-/jsdoc-api-8.1.1.tgz", - "integrity": "sha512-yas9E4h8NHp1CTEZiU/DPNAvLoUcip+Hl8Xi1RBYzHqSrgsF+mImAZNtwymrXvgbrgl4bNGBU9syulM0JzFeHQ==", + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/jsdoc-api/-/jsdoc-api-9.3.5.tgz", + "integrity": "sha512-TQwh1jA8xtCkIbVwm/XA3vDRAa5JjydyKx1cC413Sh3WohDFxcMdwKSvn4LOsq2xWyAmOU/VnSChTQf6EF0R8g==", "dev": true, "requires": { "array-back": "^6.2.2", - "cache-point": "^2.0.0", - "collect-all": "^1.0.4", - "file-set": "^4.0.2", - "fs-then-native": "^2.0.0", - "jsdoc": "^4.0.3", + "cache-point": "^3.0.1", + "current-module-paths": "^1.1.2", + "file-set": "^5.3.0", + "jsdoc": "^4.0.4", "object-to-spawn-args": "^2.0.1", - "temp-path": "^1.0.0", - "walk-back": "^5.1.0" + "walk-back": "^5.1.1" } }, "jsdoc-parse": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/jsdoc-parse/-/jsdoc-parse-6.2.4.tgz", - "integrity": "sha512-MQA+lCe3ioZd0uGbyB3nDCDZcKgKC7m/Ivt0LgKZdUoOlMJxUWJQ3WI6GeyHp9ouznKaCjlp7CU9sw5k46yZTw==", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/jsdoc-parse/-/jsdoc-parse-6.2.5.tgz", + "integrity": "sha512-8JaSNjPLr2IuEY4Das1KM6Z4oLHZYUnjRrr27hKSa78Cj0i5Lur3DzNnCkz+DfrKBDoljGMoWOiBVQbtUZJBPw==", "dev": true, "requires": { "array-back": "^6.2.2", "find-replace": "^5.0.1", - "lodash.omit": "^4.5.0", "sort-array": "^5.0.0" - }, - "dependencies": { - "find-replace": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", - "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", - "dev": true, - "requires": {} - } } }, "jsdoc-to-markdown": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/jsdoc-to-markdown/-/jsdoc-to-markdown-8.0.3.tgz", - "integrity": "sha512-JGYYd5xygnQt1DIxH+HUI+X/ynL8qWihzIF0n15NSCNtM6MplzawURRcaLI2WkiS2hIjRIgsphCOfM7FkaWiNg==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/jsdoc-to-markdown/-/jsdoc-to-markdown-9.1.3.tgz", + "integrity": "sha512-i9wi+6WHX0WKziv0ar88T8h7OmxA0LWdQaV23nY6uQyKvdUPzVt0o6YAaOceFuKRF5Rvlju5w/KnZBfdpDAlnw==", "dev": true, "requires": { "array-back": "^6.2.2", - "command-line-tool": "^0.8.0", + "command-line-args": "^6.0.1", + "command-line-usage": "^7.0.3", "config-master": "^3.1.0", - "dmd": "^6.2.3", - "jsdoc-api": "^8.1.1", - "jsdoc-parse": "^6.2.1", - "walk-back": "^5.1.0" + "dmd": "^7.1.1", + "jsdoc-api": "^9.3.5", + "jsdoc-parse": "^6.2.5", + "walk-back": "^5.1.1" } }, "json-buffer": { @@ -7201,18 +6822,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "lodash.omit": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", - "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==", - "dev": true - }, - "lodash.padend": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", - "integrity": "sha512-sOQs2aqGpbl27tmCS1QNZA09Uqp01ZzWfDUoD+xzTii0E7dSQfRKcRetFwa+uXaxaqL+TKm7CgD2JdKP7aZBSw==", - "dev": true - }, "lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -7304,28 +6913,11 @@ } }, "mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "requires": { - "mime-db": "1.52.0" - }, - "dependencies": { - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true - } - } - }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -7359,12 +6951,6 @@ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true }, - "mkdirp2": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/mkdirp2/-/mkdirp2-1.0.5.tgz", - "integrity": "sha512-xOE9xbICroUDmG1ye2h4bZ8WBie9EGmACaco8K8cx6RlkJJrxGIqjGqztAI+NMhexXBcdGbSEzI6N3EJPevxZw==", - "dev": true - }, "moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -7390,9 +6976,9 @@ "dev": true }, "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true }, "neo-async": { @@ -7410,12 +6996,6 @@ "path-key": "^3.0.0" } }, - "object-get": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object-get/-/object-get-2.1.1.tgz", - "integrity": "sha512-7n4IpLMzGGcLEMiQKsNR7vCe+N5E9LORFrtNUVy4sO3dj9a3HedZCxEL2T7QuLhcHN1NBuBsMOKaOsAYI9IIvg==", - "dev": true - }, "object-to-spawn-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object-to-spawn-args/-/object-to-spawn-args-2.0.1.tgz", @@ -7423,9 +7003,9 @@ "dev": true }, "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true }, "once": { @@ -7562,6 +7142,31 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "playwright": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz", + "integrity": "sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.56.0" + }, + "dependencies": { + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + } + } + }, + "playwright-core": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0.tgz", + "integrity": "sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==", + "dev": true + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7647,48 +7252,6 @@ } } }, - "reduce-flatten": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-3.0.1.tgz", - "integrity": "sha512-bYo+97BmUUOzg09XwfkwALt4PQH1M5L0wzKerBt6WLm3Fhdd43mMS89HiT1B9pJIqko/6lWx3OnV4J9f2Kqp5Q==", - "dev": true - }, - "reduce-unique": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/reduce-unique/-/reduce-unique-2.0.1.tgz", - "integrity": "sha512-x4jH/8L1eyZGR785WY+ePtyMNhycl1N2XOLxhCbzZFaqF4AXjLzqSxa2UHgJ2ZVR/HHyPOvl1L7xRnW8ye5MdA==", - "dev": true - }, - "reduce-without": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/reduce-without/-/reduce-without-1.0.1.tgz", - "integrity": "sha512-zQv5y/cf85sxvdrKPlfcRzlDn/OqKFThNimYmsS3flmkioKvkUGn2Qg9cJVoQiEvdxFGLE0MQER/9fZ9sUqdxg==", - "dev": true, - "requires": { - "test-value": "^2.0.0" - }, - "dependencies": { - "array-back": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw==", - "dev": true, - "requires": { - "typical": "^2.6.0" - } - }, - "test-value": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz", - "integrity": "sha512-+1epbAxtKeXttkGFMTX9H42oqzOTufR1ceCF+GYA5aOmvaPq9wd4PUS8329fn2RRLGNeUkgRLnVpycjx8DsO2w==", - "dev": true, - "requires": { - "array-back": "^1.0.3", - "typical": "^2.6.0" - } - } - } - }, "registry-auth-token": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", @@ -7876,9 +7439,9 @@ "dev": true }, "serve": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz", - "integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.5.tgz", + "integrity": "sha512-Qn/qMkzCcMFVPb60E/hQy+iRLpiU8PamOfOSYoAHmmF+fFFmpPpqa6Oci2iWYpTdOUM3VF+TINud7CfbQnsZbA==", "dev": true, "requires": { "@zeit/schemas": "2.36.0", @@ -7888,7 +7451,7 @@ "chalk": "5.0.1", "chalk-template": "0.4.0", "clipboardy": "3.0.0", - "compression": "1.7.4", + "compression": "1.8.1", "is-port-reachable": "4.0.0", "serve-handler": "6.1.6", "update-check": "1.5.4" @@ -7992,21 +7555,13 @@ "dev": true }, "sort-array": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/sort-array/-/sort-array-5.0.0.tgz", - "integrity": "sha512-Sg9MzajSGprcSrMIxsXyNT0e0JB47RJRfJspC+7co4Z5BdNsNl8FmWI+lXEpyKq+vkMG6pHgAhqyCO+bkDTfFQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/sort-array/-/sort-array-5.1.1.tgz", + "integrity": "sha512-EltS7AIsNlAFIM9cayrgKrM6XP94ATWwXP4LCL4IQbvbYhELSt2hZTrixg+AaQwnWFs/JGJgqU3rxMcNNWxGAA==", "dev": true, "requires": { "array-back": "^6.2.2", "typical": "^7.1.1" - }, - "dependencies": { - "typical": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-7.2.0.tgz", - "integrity": "sha512-W1+HdVRUl8fS3MZ9ogD51GOb46xMmhAZzR0WPw5jcgIZQJVvkddYzAl4YTU6g5w33Y1iRQLdIi2/1jhi2RNL0g==", - "dev": true - } } }, "source-map": { @@ -8095,38 +7650,12 @@ "escodegen": "^2.1.0" } }, - "stream-connect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-connect/-/stream-connect-1.0.2.tgz", - "integrity": "sha512-68Kl+79cE0RGKemKkhxTSg8+6AGrqBt+cbZAXevg2iJ6Y3zX4JhA/sZeGzLpxW9cXhmqAcE7KnJCisUmIUfnFQ==", - "dev": true, - "requires": { - "array-back": "^1.0.2" - }, - "dependencies": { - "array-back": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw==", - "dev": true, - "requires": { - "typical": "^2.6.0" - } - } - } - }, "stream-shift": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "dev": true }, - "stream-via": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/stream-via/-/stream-via-1.0.4.tgz", - "integrity": "sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==", - "dev": true - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -8237,54 +7766,13 @@ "dev": true }, "table-layout": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-0.4.5.tgz", - "integrity": "sha512-zTvf0mcggrGeTe/2jJ6ECkJHAQPIYEwDoqsiqBjI24mvRmQbInK5jq33fyypaCBxX08hMkfmdOqj6haT33EqWw==", - "dev": true, - "requires": { - "array-back": "^2.0.0", - "deep-extend": "~0.6.0", - "lodash.padend": "^4.6.1", - "typical": "^2.6.1", - "wordwrapjs": "^3.0.0" - }, - "dependencies": { - "array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "dev": true, - "requires": { - "typical": "^2.6.1" - } - } - } - }, - "temp-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/temp-path/-/temp-path-1.0.0.tgz", - "integrity": "sha512-TvmyH7kC6ZVTYkqCODjJIbgvu0FKiwQpZ4D1aknE7xpcDf/qEOB8KZEK5ef2pfbVoiBhNWs3yx4y+ESMtNYmlg==", - "dev": true - }, - "test-value": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/test-value/-/test-value-3.0.0.tgz", - "integrity": "sha512-sVACdAWcZkSU9x7AOmJo5TqE+GyNJknHaHsMrR6ZnhjVlVN9Yx6FjHrsKZ3BjIpPCT68zYesPWkakrNupwfOTQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", "dev": true, "requires": { - "array-back": "^2.0.0", - "typical": "^2.6.1" - }, - "dependencies": { - "array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "dev": true, - "requires": { - "typical": "^2.6.1" - } - } + "array-back": "^6.2.2", + "wordwrapjs": "^5.1.0" } }, "text-table": { @@ -8303,12 +7791,6 @@ "xtend": "~4.0.1" } }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8352,9 +7834,9 @@ "dev": true }, "typical": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", - "integrity": "sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", "dev": true }, "uc.micro": { @@ -8377,9 +7859,9 @@ "dev": true }, "undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", "dev": true }, "universalify": { @@ -8464,22 +7946,10 @@ "dev": true }, "wordwrapjs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-3.0.0.tgz", - "integrity": "sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw==", - "dev": true, - "requires": { - "reduce-flatten": "^1.0.1", - "typical": "^2.6.1" - }, - "dependencies": { - "reduce-flatten": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", - "integrity": "sha512-j5WfFJfc9CoXv/WbwVLHq74i/hdTUpy+iNC534LxczMRP67vJeK3V9JOdnL0N1cIRbn9mYhE2yVjvvKXDxvNXQ==", - "dev": true - } - } + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.1.tgz", + "integrity": "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==", + "dev": true }, "wrap-ansi": { "version": "8.1.0", diff --git a/package.json b/package.json index 7b8283f7..bf8f40c3 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,13 @@ "scripts": { "build": "rollup -c", "watch": "rollup -cw", + "testApp": "npm run build & npx serve ./dist", + "test": "npx playwright test", "prepublishOnly": "npm run build && npm run build_docs", - "build_docs": "jsdoc2md source/gltf-sample-renderer.js source/GltfView/gltf_view.js source/GltfState/gltf_state.js source/ResourceLoader/resource_loader.js source/gltf/user_camera.js > API.md", - "test": "echo \"Error: no test specified\" && exit 1", + "build_docs": "jsdoc2md source/gltf-sample-renderer.js source/GltfView/gltf_view.js source/GltfState/gltf_state.js source/GltfState/animation_timer.js source/ResourceLoader/resource_loader.js source/gltf/user_camera.js source/gltf/interactivity.js > API.md", "lint": "eslint source/**/*.js", "lint:fix": "eslint --fix source/**/*.js", - "prettier": "npx prettier source/**/*.js --check", + "prettier": "npx prettier source/**/*.js tests/**/*.js tests/**/*.ts --check", "prettier:fix": "npm run prettier -- --write", "format": "npm run prettier:fix && npm run lint:fix" }, @@ -29,6 +30,7 @@ "author": "Khronos Group Inc.", "license": "Apache-2.0", "dependencies": { + "@khronosgroup/gltf-interactivity-sample-engine": "file:../../glTF-InteractivityGraph-AuthoringTool/src/BasicBehaveEngine", "fast-png": "^6.2.0", "gl-matrix": "^3.2.1", "globals": "^15.5.0", @@ -36,22 +38,24 @@ "json-ptr": "^3.1.0" }, "devDependencies": { + "@playwright/test": "^1.56.0", "@rollup/plugin-commonjs": "^26.0.1", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-wasm": "^6.2.2", + "@types/node": "^24.7.2", "concurrently": "^8.2.2", "eslint": "^9.5.0", "eslint-config-prettier": "^10.1.8", - "jsdoc-to-markdown": "^8.0.1", + "jsdoc-to-markdown": "^9.1.3", "prettier": "3.6.2", "rollup": "^4.23.0", "rollup-plugin-copy": "^3.5.0", "rollup-plugin-glslify": "^1.3.1", "rollup-plugin-license": "^3.5.2", - "serve": "^14.2.4" + "serve": "^14.2.5" }, "bugs": { "url": "https://github.com/KhronosGroup/glTF-Sample-Renderer/issues" }, "homepage": "https://github.com/KhronosGroup/glTF-Sample-Renderer/#readme" -} +} \ No newline at end of file diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 00000000..8e3e35d2 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,95 @@ +// @ts-check +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: "./tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('')`. */ + baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "download", + testMatch: "**/downloadAssets.spec.ts", + use: { downloadFolder: "interactivity", testRepoURL: "https://raw.githubusercontent.com/KhronosGroup/glTF-Test-Assets-Interactivity/refs/heads/main/Tests/Interactivity/test-index.json" }, + }, + { + name: "download math", + testMatch: "**/downloadAssets.spec.ts", + use: { downloadFolder: "interactivity_math", testRepoURL: "https://raw.githubusercontent.com/KhronosGroup/glTF-Test-Assets-Interactivity/refs/heads/main/Tests/Interactivity/mathtests-index.json" }, + }, + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + dependencies: ["download", "download math"], + testIgnore: "**/downloadAssets.spec.ts", + }, + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + dependencies: ["download"], + testIgnore: "**/downloadAssets.spec.ts", + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + dependencies: ["download"], + testIgnore: "**/downloadAssets.spec.ts", + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run testApp', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/rollup.config.js b/rollup.config.js index 2203a1f3..61ea8a4f 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -27,7 +27,7 @@ export default { resolve({ browser: true, preferBuiltins: false, - dedupe: ['gl-matrix', 'jpeg-js', 'fast-png'] + dedupe: ['gl-matrix', 'jpeg-js', 'fast-png', '@khronosgroup/khr_interactivity_authoring_engine'] }), copy({ targets: [ @@ -38,7 +38,8 @@ export default { "assets/images/lut_sheen_E.png", ], dest: "dist/assets" }, - { src: ["source/libs/*", "!source/libs/hdrpng.js"], dest: "dist/libs" } + { src: ["source/libs/*", "!source/libs/hdrpng.js"], dest: "dist/libs" }, + { src: "tests/testApp/*", dest: "dist"} ] }), commonjs(), diff --git a/source/GltfState/animation_timer.js b/source/GltfState/animation_timer.js new file mode 100644 index 00000000..ff62f054 --- /dev/null +++ b/source/GltfState/animation_timer.js @@ -0,0 +1,69 @@ +/** + * AnimationTimer class to control animation playback. + */ +class AnimationTimer { + constructor() { + this.startTime = 0; + this.paused = true; + this.fixedTime = null; + this.pausedTime = 0; + } + + /** Start the animation timer and all animations */ + start() { + this.startTime = performance.now(); + this.paused = false; + } + + /** Pause all animations */ + pause() { + this.pausedTime = performance.now() - this.startTime; + this.paused = true; + } + + /** Unpause all animations */ + unpause() { + this.startTime += performance.now() - this.startTime - this.pausedTime; + this.paused = false; + } + + /** Toggle the animation playback state */ + toggle() { + if (this.paused) { + this.unpause(); + } else { + this.pause(); + } + } + + /** Reset the animation timer. If animations were playing, they will be restarted. */ + reset() { + if (!this.paused) { + // Animation is running. + this.startTime = performance.now(); + } else { + this.startTime = 0; + } + this.pausedTime = 0; + } + + /** + * Plays all animations starting from the specified time + * @param {number} timeInSec The time in seconds to set the animation timer to + */ + setFixedTime(timeInSec) { + this.paused = false; + this.fixedTime = timeInSec; + } + + /** Get the elapsed time in seconds */ + elapsedSec() { + if (this.paused) { + return this.pausedTime / 1000; + } else { + return this.fixedTime || (performance.now() - this.startTime) / 1000; + } + } +} + +export { AnimationTimer }; diff --git a/source/GltfState/gltf_state.js b/source/GltfState/gltf_state.js index d4b76038..696ea300 100644 --- a/source/GltfState/gltf_state.js +++ b/source/GltfState/gltf_state.js @@ -1,5 +1,6 @@ +import { GraphController } from "../gltf/interactivity.js"; import { UserCamera } from "../gltf/user_camera.js"; -import { AnimationTimer } from "../gltf/utils.js"; +import { AnimationTimer } from "./animation_timer.js"; /** * GltfState containing a state for visualization in GltfView @@ -31,6 +32,30 @@ class GltfState { /** KHR_materials_variants */ this.variant = undefined; + /** the graph controller allows selecting and playing graphs from KHR_interactivity */ + this.graphController = new GraphController(); + + /** callback for selection: (selectionInfo : { + * node, + * position, + * rayOrigin, + * controller }) => {} */ + this.selectionCallback = undefined; + + /** callback for hovering: (hoverInfo : { node, controller }) => {} */ + this.hoverCallback = undefined; + + /** If the renderer should compute selection information in the next frame. Is automatically reset after the frame is rendered */ + this.triggerSelection = false; + /** If the renderer should compute hover information in the next frame. */ + this.enableHover = false; + + /* Array of screen positions for selection. Currently only one is supported. */ + this.selectionPositions = [{ x: undefined, y: undefined }]; + + /* Array of screen positions for hovering. Currently only one is supported. */ + this.hoverPositions = [{ x: undefined, y: undefined }]; + /** parameters used to configure the rendering */ this.renderingParameters = { /** morphing between vertices */ @@ -38,16 +63,17 @@ class GltfState { /** skin / skeleton */ skinning: true, + /** enabled extensions */ enabledExtensions: { - /** KHR_materials_clearcoat */ + /** KHR_materials_clearcoat adds a clear coat layer on top of the glTF base material */ KHR_materials_clearcoat: true, - /** KHR_materials_sheen */ + /** KHR_materials_sheen adds a sheen layer on top of the glTF base material */ KHR_materials_sheen: true, - /** KHR_materials_transmission */ + /** KHR_materials_transmission adds physical-based transparency */ KHR_materials_transmission: true, - /** KHR_materials_volume */ + /** KHR_materials_volume adds support for volumetric materials. Used together with KHR_materials_transmission and KHR_materials_diffuse_transmission */ KHR_materials_volume: true, - /** KHR_materials_volume_scatter */ + /** KHR_materials_volume_scatter allows the simulation of scattering light inside a volume. Used together with KHR_materials_volume */ KHR_materials_volume_scatter: true, /** KHR_materials_ior makes the index of refraction configurable */ KHR_materials_ior: true, @@ -55,12 +81,22 @@ class GltfState { KHR_materials_specular: true, /** KHR_materials_iridescence adds a thin-film iridescence effect */ KHR_materials_iridescence: true, + /** KHR_materials_diffuse_transmission allows light to pass diffusely through the material */ KHR_materials_diffuse_transmission: true, /** KHR_materials_anisotropy defines microfacet grooves in the surface, stretching the specular reflection on the surface */ KHR_materials_anisotropy: true, - /** KHR_materials_dispersion defines configuring the strength of the angular separation of colors (chromatic abberation)*/ + /** KHR_materials_dispersion defines configuring the strength of the angular separation of colors (chromatic abberation) */ KHR_materials_dispersion: true, - KHR_materials_emissive_strength: true + /** KHR_materials_emissive_strength enables emissive factors larger than 1.0 */ + KHR_materials_emissive_strength: true, + /** KHR_interactivity enables execution of a behavior graph */ + KHR_interactivity: true, + /** KHR_node_hoverability enables hovering over nodes */ + KHR_node_hoverability: true, + /** KHR_node_selectability enables selecting nodes */ + KHR_node_selectability: true, + /** KHR_node_visibility enables controlling the visibility of nodes */ + KHR_node_visibility: true }, /** clear color expressed as list of ints in the range [0, 255] */ clearColor: [58, 64, 74, 255], diff --git a/source/GltfView/gltf_view.js b/source/GltfView/gltf_view.js index 69f4f75e..6ed85c12 100644 --- a/source/GltfView/gltf_view.js +++ b/source/GltfView/gltf_view.js @@ -74,6 +74,10 @@ class GltfView { return; } + if (state.graphController?.playing) { + state.graphController.simulateTick(); + } + scene.applyTransformHierarchy(state.gltf); this.renderer.drawScene(state, scene); @@ -100,7 +104,10 @@ class GltfView { transparentMaterialsCount: 0 }; } - const nodes = scene.gatherNodes(state.gltf); + const nodes = scene.gatherNodes( + state.gltf, + state.renderingParameters.enabledExtensions + ).nodes; const activeMeshes = nodes .filter((node) => node.mesh !== undefined) .map((node) => state.gltf.meshes[node.mesh]); @@ -159,31 +166,48 @@ class GltfView { } _animate(state) { - if (state.gltf === undefined) { + if (state.gltf === undefined || state.gltf.animations === undefined) { return; } - - if (state.gltf.animations !== undefined && state.animationIndices !== undefined) { - const disabledAnimations = state.gltf.animations.filter((anim, index) => { + let disabledAnimations = []; + let enabledAnimations = []; + + if ( + state.gltf?.extensions?.KHR_interactivity !== undefined && + state.renderingParameters.enabledExtensions.KHR_interactivity + ) { + if (state.graphController.playing) { + for (const animation of state.gltf.animations) { + if (animation.createdTimestamp !== undefined) { + enabledAnimations.push(animation); + } + } + } + } else if (state.animationIndices !== undefined) { + disabledAnimations = state.gltf.animations.filter((anim, index) => { return false === state.animationIndices.includes(index); }); - - for (const disabledAnimation of disabledAnimations) { - disabledAnimation.advance(state.gltf, undefined); - } - - const t = state.animationTimer.elapsedSec(); - - const animations = state.animationIndices + enabledAnimations = state.animationIndices .map((index) => { return state.gltf.animations[index]; }) .filter((animation) => animation !== undefined); - - for (const animation of animations) { - animation.advance(state.gltf, t); + for (const animation of enabledAnimations) { + if (animation.createdTimestamp !== undefined) { + animation.reset(); + } } } + + for (const disabledAnimation of disabledAnimations) { + disabledAnimation.advance(state.gltf, undefined); + } + + const t = state.animationTimer.elapsedSec(); + + for (const animation of enabledAnimations) { + animation.advance(state.gltf, t); + } } } diff --git a/source/Renderer/renderer.js b/source/Renderer/renderer.js index ca0cbc51..4c41ccdf 100644 --- a/source/Renderer/renderer.js +++ b/source/Renderer/renderer.js @@ -1,10 +1,12 @@ -import { mat4, mat3, vec3, quat } from "gl-matrix"; +import { mat4, mat3, vec3, quat, vec4 } from "gl-matrix"; import { ShaderCache } from "./shader_cache.js"; import { GltfState } from "../GltfState/gltf_state.js"; import { gltfWebGl, GL } from "./webgl.js"; import { EnvironmentRenderer } from "./environment_renderer.js"; import pbrShader from "./shaders/pbr.frag"; +import pickingShader from "./shaders/picking.frag"; +import pickingVertShader from "./shaders/picking.vert"; import brdfShader from "./shaders/brdf.glsl"; import iridescenceShader from "./shaders/iridescence.glsl"; import materialInfoShader from "./shaders/material_info.glsl"; @@ -38,6 +40,11 @@ class gltfRenderer { this.opaqueRenderTexture = 0; this.opaqueFramebuffer = 0; this.opaqueDepthTexture = 0; + this.pickingIDTexture = 0; + this.pickingPositionTexture = 0; + this.pickingDepthTexture = 0; + this.hoverIDTexture = 0; + this.hoverDepthTexture = 0; this.opaqueFramebufferWidth = 1024; this.opaqueFramebufferHeight = 1024; @@ -48,6 +55,8 @@ class gltfRenderer { const shaderSources = new Map(); shaderSources.set("primitive.vert", primitiveShader); shaderSources.set("pbr.frag", pbrShader); + shaderSources.set("picking.frag", pickingShader); + shaderSources.set("picking.vert", pickingVertShader); shaderSources.set("material_info.glsl", materialInfoShader); shaderSources.set("brdf.glsl", brdfShader); shaderSources.set("iridescence.glsl", iridescenceShader); @@ -152,7 +161,50 @@ class gltfRenderer { context.framebufferTexture2D(context.FRAMEBUFFER, context.DEPTH_ATTACHMENT, context.TEXTURE_2D, this.scatterDepthTexture, 0); context.drawBuffers([context.COLOR_ATTACHMENT0]); + this.pickingIDTexture = context.createTexture(); + context.bindTexture(context.TEXTURE_2D, this.pickingIDTexture); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MIN_FILTER, context.NEAREST); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_S, context.CLAMP_TO_EDGE); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_T, context.CLAMP_TO_EDGE); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MAG_FILTER, context.NEAREST); + context.texImage2D(context.TEXTURE_2D, 0, context.R32UI, 1, 1, 0, context.RED_INTEGER, context.UNSIGNED_INT, null); + context.bindTexture(context.TEXTURE_2D, null); + + this.pickingPositionTexture = context.createTexture(); + context.bindTexture(context.TEXTURE_2D, this.pickingPositionTexture); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MIN_FILTER, context.NEAREST); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_S, context.CLAMP_TO_EDGE); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_T, context.CLAMP_TO_EDGE); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MAG_FILTER, context.NEAREST); + context.texImage2D(context.TEXTURE_2D, 0, context.R32UI, 1, 1, 0, context.RED_INTEGER, context.UNSIGNED_INT, null); + context.bindTexture(context.TEXTURE_2D, null); + + this.pickingDepthTexture = context.createTexture(); + context.bindTexture(context.TEXTURE_2D, this.pickingDepthTexture); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MIN_FILTER, context.NEAREST); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_S, context.CLAMP_TO_EDGE); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_T, context.CLAMP_TO_EDGE); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MAG_FILTER, context.NEAREST); + context.texImage2D( context.TEXTURE_2D, 0, context.DEPTH_COMPONENT24, 1, 1, 0, context.DEPTH_COMPONENT, context.UNSIGNED_INT, null); + context.bindTexture(context.TEXTURE_2D, null); + + this.hoverIDTexture = context.createTexture(); + context.bindTexture(context.TEXTURE_2D, this.hoverIDTexture); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MIN_FILTER, context.NEAREST); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_S, context.CLAMP_TO_EDGE); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_T, context.CLAMP_TO_EDGE); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MAG_FILTER, context.NEAREST); + context.texImage2D(context.TEXTURE_2D, 0, context.R32UI, 1, 1, 0, context.RED_INTEGER, context.UNSIGNED_INT, null); + context.bindTexture(context.TEXTURE_2D, null); + this.hoverDepthTexture = context.createTexture(); + context.bindTexture(context.TEXTURE_2D, this.hoverDepthTexture); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MIN_FILTER, context.NEAREST); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_S, context.CLAMP_TO_EDGE); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_T, context.CLAMP_TO_EDGE); + context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MAG_FILTER, context.NEAREST); + context.texImage2D( context.TEXTURE_2D, 0, context.DEPTH_COMPONENT24, 1, 1, 0, context.DEPTH_COMPONENT, context.UNSIGNED_INT, null); + context.bindTexture(context.TEXTURE_2D, null); this.colorRenderBuffer = context.createRenderbuffer(); context.bindRenderbuffer(context.RENDERBUFFER, this.colorRenderBuffer); @@ -165,6 +217,18 @@ class gltfRenderer { context.DEPTH_COMPONENT24, this.opaqueFramebufferWidth, this.opaqueFramebufferHeight); + + this.pickingFramebuffer = context.createFramebuffer(); + context.bindFramebuffer(context.FRAMEBUFFER, this.pickingFramebuffer); + context.framebufferTexture2D(context.FRAMEBUFFER, context.COLOR_ATTACHMENT0, context.TEXTURE_2D, this.pickingIDTexture, 0); + context.framebufferTexture2D(context.FRAMEBUFFER, context.DEPTH_ATTACHMENT, context.TEXTURE_2D, this.pickingDepthTexture, 0); + context.framebufferTexture2D(context.FRAMEBUFFER, context.COLOR_ATTACHMENT1, context.TEXTURE_2D, this.pickingPositionTexture, 0); + context.drawBuffers([context.COLOR_ATTACHMENT0, context.COLOR_ATTACHMENT1]); + + this.hoverFramebuffer = context.createFramebuffer(); + context.bindFramebuffer(context.FRAMEBUFFER, this.hoverFramebuffer); + context.framebufferTexture2D(context.FRAMEBUFFER, context.COLOR_ATTACHMENT0, context.TEXTURE_2D, this.hoverIDTexture, 0); + context.framebufferTexture2D(context.FRAMEBUFFER, context.DEPTH_ATTACHMENT, context.TEXTURE_2D, this.hoverDepthTexture, 0); this.samples = samples; @@ -272,18 +336,58 @@ class gltfRenderer { this.webGl.context.clearColor(...clearColor); this.webGl.context.clear(GL.COLOR_BUFFER_BIT | GL.DEPTH_BUFFER_BIT); this.webGl.context.bindFramebuffer(this.webGl.context.FRAMEBUFFER, null); + this.webGl.context.bindFramebuffer(this.webGl.context.FRAMEBUFFER, this.pickingFramebuffer); + this.webGl.context.clearBufferuiv(GL.COLOR, 0, new Uint32Array([0, 0, 0, 0])); + this.webGl.context.clearBufferuiv(GL.COLOR, 1, new Uint32Array([0, 0, 0, 0])); + this.webGl.context.clearBufferfv(GL.DEPTH, 0, new Float32Array([1.0])); + this.webGl.context.bindFramebuffer(this.webGl.context.FRAMEBUFFER, null); + this.webGl.context.bindFramebuffer(this.webGl.context.FRAMEBUFFER, this.hoverFramebuffer); + this.webGl.context.clearBufferuiv(GL.COLOR, 0, new Uint32Array([0, 0, 0, 0])); + this.webGl.context.clearBufferfv(GL.DEPTH, 0, new Float32Array([1.0])); + this.webGl.context.bindFramebuffer(this.webGl.context.FRAMEBUFFER, null); } prepareScene(state, scene) { - this.nodes = scene.gatherNodes(state.gltf); + const newNodes = scene.gatherNodes(state.gltf, state.renderingParameters.enabledExtensions); + this.selectionDrawables = newNodes.selectableNodes + .filter((node) => node.mesh !== undefined) + .reduce( + (accumulator, node) => + accumulator.concat( + state.gltf.meshes[node.mesh].primitives.map((primitive, index) => { + return { node: node, primitive: primitive, primitiveIndex: index }; + }) + ), + [] + ); + this.hoverDrawables = newNodes.hoverableNodes + .filter((node) => node.mesh !== undefined) + .reduce( + (accumulator, node) => + accumulator.concat( + state.gltf.meshes[node.mesh].primitives.map((primitive, index) => { + return { node: node, primitive: primitive, primitiveIndex: index }; + }) + ), + [] + ); + + // check if nodes have changed since previous frame to avoid unnecessary updates + if ( + newNodes.nodes.length === this.nodes?.length && + newNodes.nodes.every((element, i) => element === this.nodes[i]) + ) { + return; + } + this.nodes = newNodes.nodes; // collect drawables by essentially zipping primitives (for geometry and material) // and nodes for the transform const drawables = this.nodes .filter((node) => node.mesh !== undefined) .reduce( - (acc, node) => - acc.concat( + (accumulator, node) => + accumulator.concat( state.gltf.meshes[node.mesh].primitives.map((primitive, index) => { return { node: node, primitive: primitive, primitiveIndex: index }; }) @@ -291,6 +395,7 @@ class gltfRenderer { [] ) .filter(({ primitive }) => primitive.material !== undefined); + this.drawables = drawables; // opaque drawables don't need sorting this.opaqueDrawables = drawables.filter( @@ -355,10 +460,7 @@ class gltfRenderer { // render complete gltf scene with given camera drawScene(state, scene) { - if (this.preparedScene !== scene) { - this.prepareScene(state, scene); - this.preparedScene = scene; - } + this.prepareScene(state, scene); let currentCamera = undefined; @@ -405,7 +507,7 @@ class gltfRenderer { this.viewMatrix = currentCamera.getViewMatrix(state.gltf); this.currentCameraPosition = currentCamera.getPosition(state.gltf); - this.visibleLights = this.getVisibleLights(state.gltf, scene.nodes); + this.visibleLights = this.getVisibleLights(state.gltf, this.nodes); if ( this.visibleLights.length === 0 && !state.renderingParameters.useIBL && @@ -483,6 +585,82 @@ class gltfRenderer { return; } + let pickingProjection = undefined; + let pickingViewProjection = mat4.create(); + + let pickingX = state.selectionPositions[0].x; + let pickingY = state.selectionPositions[0].y; + + // Draw a 1x1 texture for picking + if (state.triggerSelection && pickingX !== undefined && pickingY !== undefined) { + pickingProjection = currentCamera.getProjectionMatrixForPixel( + pickingX - aspectOffsetX, + this.currentHeight - pickingY - aspectOffsetY, + aspectWidth, + aspectHeight + ); + mat4.multiply(pickingViewProjection, pickingProjection, this.viewMatrix); + this.webGl.context.bindFramebuffer( + this.webGl.context.FRAMEBUFFER, + this.pickingFramebuffer + ); + this.webGl.context.viewport(0, 0, 1, 1); + + for (const drawable of this.selectionDrawables) { + let renderpassConfiguration = {}; + renderpassConfiguration.picking = true; + this.drawPrimitive( + state, + renderpassConfiguration, + drawable.primitive, + drawable.node, + pickingViewProjection + ); + } + } + + pickingX = state.hoverPositions[0].x; + pickingY = state.hoverPositions[0].y; + + const needsHover = state.graphController.needsHover(); + const calcHoverInfo = + (state.enableHover || needsHover) && pickingX !== undefined && pickingY !== undefined; + + // Draw a 1x1 texture for hover + if (calcHoverInfo) { + // We do not need to recalculate the picking projection matrix if selection and hover use the same position + if ( + pickingProjection === undefined || + pickingX !== state.selectionPositions[0].x || + pickingY !== state.selectionPositions[0].y + ) { + pickingProjection = currentCamera.getProjectionMatrixForPixel( + pickingX - aspectOffsetX, + this.currentHeight - pickingY - aspectOffsetY, + aspectWidth, + aspectHeight + ); + mat4.multiply(pickingViewProjection, pickingProjection, this.viewMatrix); + } + this.webGl.context.bindFramebuffer( + this.webGl.context.FRAMEBUFFER, + this.hoverFramebuffer + ); + this.webGl.context.viewport(0, 0, 1, 1); + + for (const drawable of this.hoverDrawables) { + let renderpassConfiguration = {}; + renderpassConfiguration.picking = true; + this.drawPrimitive( + state, + renderpassConfiguration, + drawable.primitive, + drawable.node, + pickingViewProjection + ); + } + } + // If any transmissive drawables are present, render all opaque and transparent drawables into a separate framebuffer. if (this.transmissionDrawables.length > 0) { // Render transmission sample texture @@ -662,6 +840,156 @@ class gltfRenderer { this.viewProjectionMatrix ); } + + // Handle selection + if (state.triggerSelection) { + this.webGl.context.bindFramebuffer( + this.webGl.context.FRAMEBUFFER, + this.pickingFramebuffer + ); + this.webGl.context.viewport(0, 0, 1, 1); + state.triggerSelection = false; + this.webGl.context.readBuffer(this.webGl.context.COLOR_ATTACHMENT0); + + // Read pixel under controller (e.g. mouse cursor), which contains the picking ID + const pixels = new Uint32Array(1); + this.webGl.context.readPixels( + 0, + 0, + 1, + 1, + this.webGl.context.RED_INTEGER, + this.webGl.context.UNSIGNED_INT, + pixels + ); + + // Compute ray origin in world space. This is the near plane position of the current pixel. + pickingX = state.selectionPositions[0].x; + pickingY = state.selectionPositions[0].y; + const x = pickingX - aspectOffsetX; + const y = this.currentHeight - pickingY - aspectOffsetY; + const nearPlane = currentCamera.getNearPlaneForPixel( + x + 0.5, + y + 0.5, + aspectWidth, + aspectHeight + ); + const zNear = currentCamera.perspective?.znear + ? currentCamera.perspective.znear + : currentCamera.orthographic.znear; + let rayOrigin = vec3.fromValues(nearPlane.left, nearPlane.bottom, -zNear); + vec3.transformMat4(rayOrigin, rayOrigin, currentCamera.getTransformMatrix(state.gltf)); + + let pickingResult = { + node: undefined, + position: undefined, + rayOrigin: rayOrigin, + controller: 0 + }; + + // Search for node with matching picking ID + let found = false; + for (const node of state.gltf.nodes) { + if (node.pickingColor === pixels[0]) { + found = true; + pickingResult.node = node; + break; + } + } + + // If a node was found, we need to calculate the ray intersection position + if (found) { + // WebGL does not allow reading from depth buffer + this.webGl.context.readBuffer(this.webGl.context.COLOR_ATTACHMENT1); + const position = new Uint32Array(1); + this.webGl.context.readPixels( + 0, + 0, + 1, + 1, + this.webGl.context.RED_INTEGER, + this.webGl.context.UNSIGNED_INT, + position + ); + + // Transform uint to float [-1, 1] in clip space + const z = (position[0] / 4294967295) * 2.0 - 1.0; + + // Get view space position + const clipSpacePosition = vec4.fromValues(0, 0, z, 1); + vec4.transformMat4( + clipSpacePosition, + clipSpacePosition, + mat4.invert(mat4.create(), pickingProjection) + ); + + // Divide by w to get normalized device coordinates + vec4.divide( + clipSpacePosition, + clipSpacePosition, + vec4.fromValues( + clipSpacePosition[3], + clipSpacePosition[3], + clipSpacePosition[3], + clipSpacePosition[3] + ) + ); + const worldPos = vec4.transformMat4( + vec4.create(), + clipSpacePosition, + mat4.invert(mat4.create(), this.viewMatrix) + ); + pickingResult.position = vec3.fromValues(worldPos[0], worldPos[1], worldPos[2]); + } + + // Send picking result to Interactivity engine + state.graphController.receiveSelection(pickingResult); + + if (state.selectionCallback) { + state.selectionCallback(pickingResult); + } + } + + if (calcHoverInfo) { + this.webGl.context.bindFramebuffer( + this.webGl.context.FRAMEBUFFER, + this.hoverFramebuffer + ); + this.webGl.context.viewport(0, 0, 1, 1); + this.webGl.context.readBuffer(this.webGl.context.COLOR_ATTACHMENT0); + + // Read pixel under controller (e.g. mouse cursor), which contains the picking ID + const pixels = new Uint32Array(1); + this.webGl.context.readPixels( + 0, + 0, + 1, + 1, + this.webGl.context.RED_INTEGER, + this.webGl.context.UNSIGNED_INT, + pixels + ); + + let pickingResult = { + node: undefined, + controller: 0 + }; + + // Search for node with matching picking ID + for (const node of state.gltf.nodes) { + if (node.pickingColor === pixels[0]) { + pickingResult.node = node; + break; + } + } + + // Send picking result to Interactivity engine + state.graphController.receiveHover(pickingResult); + + if (state.enableHover && state.hoverCallback) { + state.hoverCallback(pickingResult); + } + } } // vertices with given material @@ -671,9 +999,9 @@ class gltfRenderer { if (primitive.skip) return; let material; - if(primitive.mappings !== undefined && state.variant != "default") + if(primitive.mappings !== undefined && state.variant != "default" && state.gltf.extensions?.KHR_materials_variants.variants !== undefined) { - const names = state.gltf.variants.map(obj => obj.name); + const names = state.gltf.extensions.KHR_materials_variants.variants.map(obj => obj.name); const idx = names.indexOf(state.variant); let materialIdx = primitive.material; primitive.mappings.forEach(element => { @@ -730,15 +1058,18 @@ class gltfRenderer { this.pushFragParameterDefines(fragDefines, state); + const vertexShader = renderpassConfiguration.picking ? "picking.vert" : "primitive.vert"; let fragmentShader = "pbr.frag"; if (material.type === "SG") { fragmentShader = "specular_glossiness.frag"; } else if (renderpassConfiguration.scatter) { fragmentShader = "scatter.frag"; + } else if (renderpassConfiguration.picking) { + fragmentShader = "picking.frag"; } const fragmentHash = this.shaderCache.selectShader(fragmentShader, fragDefines); - const vertexHash = this.shaderCache.selectShader("primitive.vert", vertDefines); + const vertexHash = this.shaderCache.selectShader(vertexShader, vertDefines); if (fragmentHash && vertexHash) { @@ -752,7 +1083,7 @@ class gltfRenderer { this.webGl.context.useProgram(this.shader.program); - if (state.renderingParameters.usePunctual) + if (state.renderingParameters.usePunctual && !renderpassConfiguration.picking) { this.applyLights(); } @@ -767,7 +1098,9 @@ class gltfRenderer { this.shader.updateUniform("u_NormalMatrix", node.normalMatrix, false); this.shader.updateUniform("u_Exposure", state.renderingParameters.exposure, false); this.shader.updateUniform("u_Camera", this.currentCameraPosition, false); - + if (renderpassConfiguration.picking) { + this.shader.updateUniform("u_PickingColor", node.pickingColor, false); + } this.updateAnimationUniforms(state, node, primitive); @@ -780,7 +1113,7 @@ class gltfRenderer { this.webGl.context.frontFace(GL.CCW); } - if (material.doubleSided) + if (material.doubleSided || renderpassConfiguration.picking) { this.webGl.context.disable(GL.CULL_FACE); } @@ -789,7 +1122,7 @@ class gltfRenderer { this.webGl.context.enable(GL.CULL_FACE); } - if (material.alphaMode === 'BLEND') + if (material.alphaMode === 'BLEND' && !renderpassConfiguration.picking) { this.webGl.context.enable(GL.BLEND); this.webGl.context.blendFuncSeparate(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA); @@ -813,6 +1146,9 @@ class gltfRenderer { let vertexCount = 0; for (const attribute of primitive.glAttributes) { + if (renderpassConfiguration.picking && (attribute.attribute !== "POSITION" || attribute.attribute.startsWith("JOINTS") || attribute.attribute.startsWith("WEIGHTS"))) { + continue; + } const gltfAccessor = state.gltf.accessors[attribute.accessor]; vertexCount = gltfAccessor.count; @@ -965,51 +1301,53 @@ class gltfRenderer { textureIndex++; } - let textureCount = textureIndex; - - textureCount = this.applyEnvironmentMap(state, textureCount); - - - if (state.environment !== undefined) - { - this.webGl.setTexture(this.shader.getUniformLocation("u_SheenELUT"), state.environment, state.environment.sheenELUT, textureCount++); - } - - if (material.hasVolumeScatter && sampledTextures?.scatterSampleTexture !== undefined) - { - this.webGl.context.activeTexture(GL.TEXTURE0 + textureCount); - this.webGl.context.bindTexture(this.webGl.context.TEXTURE_2D, sampledTextures.scatterSampleTexture); - this.webGl.context.uniform1i(this.shader.getUniformLocation("u_ScatterFramebufferSampler"), textureCount); - textureCount++; - - this.webGl.context.activeTexture(GL.TEXTURE0 + textureCount); - this.webGl.context.bindTexture(this.webGl.context.TEXTURE_2D, sampledTextures.scatterDepthSampleTexture); - this.webGl.context.uniform1i(this.shader.getUniformLocation("u_ScatterDepthFramebufferSampler"), textureCount); - textureCount++; - - this.webGl.context.uniform1f(this.shader.getUniformLocation("u_MinRadius"), gltfMaterial.scatterMinRadius); - this.webGl.context.uniform2i(this.shader.getUniformLocation("u_FramebufferSize"), renderpassConfiguration.frameBufferSize[0], renderpassConfiguration.frameBufferSize[1]); - this.webGl.context.uniformMatrix4fv(this.shader.getUniformLocation("u_ProjectionMatrix"),false, this.projMatrix); - - this.shader.updateUniformArray("u_ScatterSamples", gltfMaterial.scatterSamples); - } + if (!renderpassConfiguration.picking) { + let textureCount = textureIndex; - if(sampledTextures?.transmissionSampleTexture !== undefined && - state.environment && - state.renderingParameters.enabledExtensions.KHR_materials_transmission) - { - this.webGl.context.activeTexture(GL.TEXTURE0 + textureCount); - this.webGl.context.bindTexture(this.webGl.context.TEXTURE_2D, this.opaqueRenderTexture); - this.webGl.context.uniform1i(this.shader.getUniformLocation("u_TransmissionFramebufferSampler"), textureCount); - textureCount++; + textureCount = this.applyEnvironmentMap(state, textureCount); - this.webGl.context.uniform2i(this.shader.getUniformLocation("u_TransmissionFramebufferSize"), this.opaqueFramebufferWidth, this.opaqueFramebufferHeight); - this.webGl.context.uniformMatrix4fv(this.shader.getUniformLocation("u_ModelMatrix"),false, node.worldTransform); - this.webGl.context.uniformMatrix4fv(this.shader.getUniformLocation("u_ViewMatrix"),false, this.viewMatrix); - this.webGl.context.uniformMatrix4fv(this.shader.getUniformLocation("u_ProjectionMatrix"),false, this.projMatrix); - } + if (state.environment !== undefined) + { + this.webGl.setTexture(this.shader.getUniformLocation("u_SheenELUT"), state.environment, state.environment.sheenELUT, textureCount++); + } + if (material.hasVolumeScatter && sampledTextures?.scatterSampleTexture !== undefined) + { + this.webGl.context.activeTexture(GL.TEXTURE0 + textureCount); + this.webGl.context.bindTexture(this.webGl.context.TEXTURE_2D, sampledTextures.scatterSampleTexture); + this.webGl.context.uniform1i(this.shader.getUniformLocation("u_ScatterFramebufferSampler"), textureCount); + textureCount++; + + this.webGl.context.activeTexture(GL.TEXTURE0 + textureCount); + this.webGl.context.bindTexture(this.webGl.context.TEXTURE_2D, sampledTextures.scatterDepthSampleTexture); + this.webGl.context.uniform1i(this.shader.getUniformLocation("u_ScatterDepthFramebufferSampler"), textureCount); + textureCount++; + + this.webGl.context.uniform1f(this.shader.getUniformLocation("u_MinRadius"), gltfMaterial.scatterMinRadius); + this.webGl.context.uniform2i(this.shader.getUniformLocation("u_FramebufferSize"), renderpassConfiguration.frameBufferSize[0], renderpassConfiguration.frameBufferSize[1]); + this.webGl.context.uniformMatrix4fv(this.shader.getUniformLocation("u_ProjectionMatrix"),false, this.projMatrix); + + this.shader.updateUniformArray("u_ScatterSamples", gltfMaterial.scatterSamples); + } + + if(sampledTextures?.transmissionSampleTexture !== undefined && + state.environment && + state.renderingParameters.enabledExtensions.KHR_materials_transmission) + { + this.webGl.context.activeTexture(GL.TEXTURE0 + textureCount); + this.webGl.context.bindTexture(this.webGl.context.TEXTURE_2D, this.opaqueRenderTexture); + this.webGl.context.uniform1i(this.shader.getUniformLocation("u_TransmissionFramebufferSampler"), textureCount); + textureCount++; + + this.webGl.context.uniform2i(this.shader.getUniformLocation("u_TransmissionFramebufferSize"), this.opaqueFramebufferWidth, this.opaqueFramebufferHeight); + + this.webGl.context.uniformMatrix4fv(this.shader.getUniformLocation("u_ModelMatrix"),false, node.worldTransform); + this.webGl.context.uniformMatrix4fv(this.shader.getUniformLocation("u_ViewMatrix"),false, this.viewMatrix); + this.webGl.context.uniformMatrix4fv(this.shader.getUniformLocation("u_ProjectionMatrix"),false, this.projMatrix); + } + } + if (drawIndexed) { const indexAccessor = state.gltf.accessors[primitive.indices]; @@ -1030,6 +1368,9 @@ class gltfRenderer { for (const attribute of primitive.glAttributes) { + if (renderpassConfiguration.picking && (attribute.attribute !== "POSITION" || attribute.attribute.startsWith("JOINTS") || attribute.attribute.startsWith("WEIGHTS"))) { + continue; + } const location = this.shader.getAttributeLocation(attribute.name); if (location === null) { @@ -1054,18 +1395,12 @@ class gltfRenderer { getVisibleLights(gltf, nodes) { let nodeLights = []; - for (const nodeIndex of nodes) { - const node = gltf.nodes[nodeIndex]; - - if (node.children !== undefined) { - nodeLights = nodeLights.concat(this.getVisibleLights(gltf, node.children)); - } - + for (const node of nodes) { const lightIndex = node.extensions?.KHR_lights_punctual?.light; if (lightIndex === undefined) { continue; } - const light = gltf.lights[lightIndex]; + const light = gltf.extensions?.KHR_lights_punctual?.lights[lightIndex]; nodeLights.push([node, light]); } diff --git a/source/Renderer/shader.js b/source/Renderer/shader.js index f33ff1a7..d57bd54a 100644 --- a/source/Renderer/shader.js +++ b/source/Renderer/shader.js @@ -164,6 +164,29 @@ class gltfShader { this.gl.context.uniform4iv(uniform.loc, value); break; + case GL.UNSIGNED_INT: { + if ( + Array.isArray(value) || + value instanceof Uint32Array || + value instanceof Int32Array + ) { + this.gl.context.uniform1uiv(uniform.loc, value); + } else { + this.gl.context.uniform1ui(uniform.loc, value); + } + break; + } + + case GL.UNSIGNED_INT_VEC2: + this.gl.context.uniform2uiv(uniform.loc, value); + break; + case GL.UNSIGNED_INT_VEC3: + this.gl.context.uniform3uiv(uniform.loc, value); + break; + case GL.UNSIGNED_INT_VEC4: + this.gl.context.uniform4uiv(uniform.loc, value); + break; + case GL.FLOAT_MAT2: this.gl.context.uniformMatrix2fv(uniform.loc, false, value); break; diff --git a/source/Renderer/shaders/picking.frag b/source/Renderer/shaders/picking.frag new file mode 100644 index 00000000..b7e358b9 --- /dev/null +++ b/source/Renderer/shaders/picking.frag @@ -0,0 +1,11 @@ +precision highp float; + +layout(location = 0) out uint id_color; +layout(location = 1) out uint position; + +uniform uint u_PickingColor; + +void main() { + id_color = u_PickingColor; + position = uint(gl_FragCoord.z * 4294967295.0); // mapping [0, 1] to uint +} diff --git a/source/Renderer/shaders/picking.vert b/source/Renderer/shaders/picking.vert new file mode 100644 index 00000000..7939c0ad --- /dev/null +++ b/source/Renderer/shaders/picking.vert @@ -0,0 +1,34 @@ +#include + + +uniform mat4 u_ViewProjectionMatrix; +uniform mat4 u_ModelMatrix; +uniform mat4 u_NormalMatrix; + + +in vec3 a_position; + + +vec4 getPosition() +{ + vec4 pos = vec4(a_position, 1.0); + +#ifdef USE_MORPHING + pos += getTargetPosition(gl_VertexID); +#endif + +#ifdef USE_SKINNING + pos = getSkinningMatrix() * pos; +#endif + + return pos; +} + + +void main() +{ + gl_PointSize = 1.0f; + vec4 pos = u_ModelMatrix * getPosition(); + + gl_Position = u_ViewProjectionMatrix * pos; +} diff --git a/source/Renderer/webgl.js b/source/Renderer/webgl.js index 33032da8..a0d1abfa 100644 --- a/source/Renderer/webgl.js +++ b/source/Renderer/webgl.js @@ -170,6 +170,9 @@ class gltfWebGl { } if (gltfAccessor.glBuffer === undefined) { + if (gltfAccessor.componentType === 5130) { + throw new Error("64-bit float attributes are not supported in WebGL2"); + } gltfAccessor.glBuffer = this.context.createBuffer(); let data = gltfAccessor.getTypedView(gltf); diff --git a/source/gltf/accessor.js b/source/gltf/accessor.js index 54271a05..b350e9bf 100644 --- a/source/gltf/accessor.js +++ b/source/gltf/accessor.js @@ -81,6 +81,9 @@ class gltfAccessor extends GltfObject { case GL.FLOAT: this.typedView = new Float32Array(buffer.buffer, byteOffset, arrayLength); break; + case 5130: // KHR_accessor_float64 + this.typedView = new Float64Array(buffer.buffer, byteOffset, arrayLength); + break; } } else { this.typedView = this.createView(); @@ -162,6 +165,10 @@ class gltfAccessor extends GltfObject { this.filteredView = new Float32Array(arrayLength); func = "getFloat32"; break; + case 5130: // KHR_accessor_float64 + this.filteredView = new Float64Array(arrayLength); + func = "getFloat64"; + break; } for (let i = 0; i < arrayLength; ++i) { @@ -317,6 +324,13 @@ class gltfAccessor extends GltfObject { valuesArrayLength ); break; + case 5130: // KHR_accessor_float64 + valuesTypedView = new Float64Array( + valuesBuffer.buffer, + valuesByteOffset, + valuesArrayLength + ); + break; } // Overwrite values. @@ -361,6 +375,8 @@ class gltfAccessor extends GltfObject { case GL.UNSIGNED_INT: case GL.FLOAT: return 4; + case 5130: // KHR_accessor_float64 + return 8; default: return 0; } diff --git a/source/gltf/animation.js b/source/gltf/animation.js index 0fcc62ff..ff225aff 100644 --- a/source/gltf/animation.js +++ b/source/gltf/animation.js @@ -14,9 +14,19 @@ class gltfAnimation extends GltfObject { this.samplers = []; this.name = ""; + // For KHR_interactivity + this.createdTimestamp = undefined; // Time in seconds after graph creation when the animation was created. Computed via animation timer. + this.startTime = 0; + this.stopTime = undefined; + this.endTime = Infinity; + this.speed = 1.0; + this.endCallback = undefined; // Callback to call when the animation ends. + this.stopCallback = undefined; // Callback to call when the animation stops. + // not gltf this.interpolators = []; - this.maxTime = 0; + this.maxTime = NaN; + this.minTime = NaN; this.disjointAnimations = []; this.errors = []; @@ -39,21 +49,103 @@ class gltfAnimation extends GltfObject { } } - // advance the animation, if totalTime is undefined, the animation is deactivated - advance(gltf, totalTime) { - if (this.channels === undefined) { - return; - } + reset() { + this.createdTimestamp = undefined; + this.startTime = 0; + this.stopTime = undefined; + this.endTime = Infinity; + this.speed = 1.0; + this.endCallback = undefined; + this.stopCallback = undefined; + } - if (this.maxTime == 0) { + computeMinMaxTime(gltf) { + if (isNaN(this.maxTime) || isNaN(this.minTime)) { + this.maxTime = -Infinity; + this.minTime = Infinity; for (let i = 0; i < this.channels.length; ++i) { const channel = this.channels[i]; const sampler = this.samplers[channel.sampler]; - const input = gltf.accessors[sampler.input].getDeinterlacedView(gltf); - const max = input[input.length - 1]; + const input = gltf.accessors[sampler.input]; + if ( + input.max === undefined || + input.min === undefined || + input.max.length !== 1 || + input.min.length !== 1 + ) { + console.error("Invalid input accessor for animation channel:", channel); + this.minTime = undefined; + this.maxTime = undefined; + return; + } + const max = input.max[0]; + const min = input.min[0]; if (max > this.maxTime) { this.maxTime = max; } + if (min < this.minTime) { + this.minTime = min; + } + } + } + if (this.minTime > this.maxTime || this.minTime < 0 || this.maxTime < 0) { + console.error("Invalid min/max time for animation with index:", this.gltfObjectIndex); + this.minTime = undefined; + this.maxTime = undefined; + } + } + + // advance the animation, if totalTime is undefined, the animation is deactivated + advance(gltf, totalTime) { + if (this.channels === undefined) { + return; + } + + this.computeMinMaxTime(gltf); + + if (this.maxTime === undefined || this.minTime === undefined) { + return; + } + + let stopAnimation = false; + let endAnimation = false; + let elapsedTime = totalTime; + let reverse = false; + + // createdTimestamp is only used for KHR_interactivity + if (this.createdTimestamp !== undefined) { + elapsedTime = totalTime - this.createdTimestamp; + elapsedTime *= this.speed; + if (this.startTime > this.endTime) { + elapsedTime *= -1; + reverse = true; + } + elapsedTime += this.startTime; + if (this.startTime === this.endTime) { + elapsedTime = this.startTime; + endAnimation = true; + } else if (this.stopTime !== undefined) { + // Check if stopTime is reached + if ( + (this.startTime < this.endTime && + elapsedTime >= this.stopTime && + this.stopTime >= this.startTime && + this.stopTime < this.endTime) || + (this.startTime > this.endTime && + elapsedTime <= this.stopTime && + this.stopTime <= this.startTime && + this.stopTime > this.endTime) + ) { + elapsedTime = this.stopTime; + stopAnimation = true; + } + } else if ( + // Check if endTime is reached + (this.startTime < this.endTime && elapsedTime >= this.endTime) || + (this.startTime > this.endTime && elapsedTime <= this.endTime) + ) { + elapsedTime = this.endTime; + endAnimation = true; } } @@ -85,21 +177,28 @@ class gltfAnimation extends GltfObject { break; } + // Search for the animated property if (property != null) { - if (property.startsWith("/extensions/KHR_lights_punctual/")) { - const suffix = property.substring("/extensions/KHR_lights_punctual/".length); - property = "/" + suffix; - } let jsonPointer = JsonPointer.create(property); let parentObject = jsonPointer.parent(gltf); + if (parentObject === undefined) { + if (!this.errors.includes(property)) { + console.warn(`Cannot find property ${property}`); + this.errors.push(property); + } + continue; + } let back = jsonPointer.path.at(-1); let animatedArrayElement = undefined; + + // Check if we are animating an array element e.g. weights if (Array.isArray(parentObject)) { animatedArrayElement = Number(back); jsonPointer = JsonPointer.create(jsonPointer.path.slice(0, -1)); parentObject = jsonPointer.parent(gltf); back = jsonPointer.path.at(-1); } + let animatedProperty = undefined; if ( parentObject.animatedPropertyObjects && @@ -117,6 +216,8 @@ class gltfAnimation extends GltfObject { } continue; } + + // glTF value is not defined and does not have a default value if (animatedProperty.restValue === undefined) { continue; } @@ -126,18 +227,22 @@ class gltfAnimation extends GltfObject { stride = animatedProperty.restValue[animatedArrayElement]?.length ?? 1; } - const interpolant = interpolator.interpolate( + let interpolant = interpolator.interpolate( gltf, channel, sampler, - totalTime, + elapsedTime, stride, - this.maxTime + this.maxTime, + reverse ); if (interpolant === undefined) { animatedProperty.rest(); continue; } + if (typeof animatedProperty.value() === "boolean") { + interpolant = interpolant[0] !== 0; + } // The interpolator will always return a `Float32Array`, even if the animated value is a scalar. // For the renderer it's not a problem because uploading a single-element array is the same as uploading a scalar to a uniform. // However, it becomes a problem if we use the animated value for further computation and assume is stays a scalar. @@ -159,6 +264,19 @@ class gltfAnimation extends GltfObject { } } } + + // Handle end/stop of animation in interactivity + if (stopAnimation) { + this.createdTimestamp = undefined; + this.stopCallback?.(); + this.reset(); + return; + } + if (endAnimation) { + this.createdTimestamp = undefined; + this.endCallback?.(); + this.reset(); + } } } diff --git a/source/gltf/camera.js b/source/gltf/camera.js index 1290f5f6..e08389e6 100644 --- a/source/gltf/camera.js +++ b/source/gltf/camera.js @@ -75,6 +75,62 @@ class gltfCamera extends GltfObject { return projection; } + getNearPlaneForPixel(x, y, width, height) { + let subRight, subTop, subLeft, subBottom; + if (this.type === "perspective") { + const aspectRatio = this.perspective.aspectRatio ?? width / height; + const top = Math.tan(this.perspective.yfov / 2) * this.perspective.znear; + const bottom = -top; + const left = bottom * aspectRatio; + const right = top * aspectRatio; + const computedWidth = Math.abs(right - left); + const computedHeight = Math.abs(top - bottom); + + const subWidth = computedWidth / width; + const subHeight = computedHeight / height; + subLeft = left + x * subWidth; + subBottom = bottom + y * subHeight; + subRight = subLeft + subWidth; + subTop = subBottom + subHeight; + } else if (this.type === "orthographic") { + subLeft = -this.orthographic.xmag + ((2 * this.orthographic.xmag) / width) * x; + subRight = subLeft + (2 * this.orthographic.xmag) / width; + subBottom = -this.orthographic.ymag + ((2 * this.orthographic.ymag) / height) * y; + subTop = subBottom + (2 * this.orthographic.ymag) / height; + } + return { left: subLeft, bottom: subBottom, right: subRight, top: subTop }; + } + + getProjectionMatrixForPixel(x, y, width, height) { + const projection = mat4.create(); + + const nearPlane = this.getNearPlaneForPixel(x, y, width, height); + + if (this.type === "perspective") { + mat4.frustum( + projection, + nearPlane.left, + nearPlane.right, + nearPlane.bottom, + nearPlane.top, + this.perspective.znear, + this.perspective.zfar + ); + } else if (this.type === "orthographic") { + mat4.ortho( + projection, + nearPlane.left, + nearPlane.right, + nearPlane.bottom, + nearPlane.top, + this.orthographic.znear, + this.orthographic.zfar + ); + } + + return projection; + } + getViewMatrix(gltf) { let result = mat4.create(); mat4.invert(result, this.getTransformMatrix(gltf)); diff --git a/source/gltf/gltf.js b/source/gltf/gltf.js index 3e564ceb..dee7008b 100644 --- a/source/gltf/gltf.js +++ b/source/gltf/gltf.js @@ -4,7 +4,6 @@ import { gltfBufferView } from "./buffer_view.js"; import { gltfCamera } from "./camera.js"; import { gltfImage } from "./image.js"; import { gltfLight } from "./light.js"; -import { ImageBasedLight } from "./image_based_light.js"; import { gltfMaterial } from "./material.js"; import { gltfMesh } from "./mesh.js"; import { gltfNode } from "./node.js"; @@ -17,10 +16,13 @@ import { GltfObject } from "./gltf_object.js"; import { gltfAnimation } from "./animation.js"; import { gltfSkin } from "./skin.js"; import { gltfVariant } from "./variant.js"; +import { gltfGraph } from "./interactivity.js"; const allowedExtensions = [ + "KHR_accessor_float64", "KHR_animation_pointer", "KHR_draco_mesh_compression", + "KHR_interactivity", "KHR_lights_image_based", "KHR_lights_punctual", "KHR_materials_anisotropy", @@ -39,6 +41,9 @@ const allowedExtensions = [ "KHR_materials_volume", "KHR_materials_volume_scatter", "KHR_mesh_quantization", + "KHR_node_hoverability", + "KHR_node_selectability", + "KHR_node_visibility", "KHR_texture_basisu", "KHR_texture_transform", "KHR_xmp_json_ld", @@ -48,6 +53,16 @@ const allowedExtensions = [ class glTF extends GltfObject { static animatedProperties = []; + static readOnlyAnimatedProperties = [ + "animations", + "cameras", + // "materials", materials.length need to be handled manually due to the default material + "meshes", + "nodes", + "scene", + "scenes", + "skins" + ]; constructor(file) { super(); this.asset = undefined; @@ -56,7 +71,6 @@ class glTF extends GltfObject { this.scene = undefined; // the default scene to show. this.scenes = []; this.cameras = []; - this.lights = []; this.imageBasedLights = []; this.textures = []; this.images = []; @@ -97,19 +111,35 @@ class glTF extends GltfObject { this.scenes = objectsFromJsons(json.scenes, gltfScene); this.textures = objectsFromJsons(json.textures, gltfTexture); this.nodes = objectsFromJsons(json.nodes, gltfNode); - this.lights = objectsFromJsons(getJsonLightsFromExtensions(json.extensions), gltfLight); - this.imageBasedLights = objectsFromJsons( - getJsonIBLsFromExtensions(json.extensions), - ImageBasedLight - ); this.images = objectsFromJsons(json.images, gltfImage); this.animations = objectsFromJsons(json.animations, gltfAnimation); this.skins = objectsFromJsons(json.skins, gltfSkin); - this.variants = objectsFromJsons( - getJsonVariantsFromExtension(json.extensions), - gltfVariant - ); - this.variants = enforceVariantsUniqueness(this.variants); + + if (json.extensions?.KHR_lights_punctual !== undefined) { + this.extensions.KHR_lights_punctual = new GltfObject([]); + this.extensions.KHR_lights_punctual.lights = objectsFromJsons( + json.extensions.KHR_lights_punctual.lights, + gltfLight + ); + } + if (json.extensions?.KHR_materials_variants !== undefined) { + this.extensions.KHR_materials_variants = new GltfObject([]); + this.extensions.KHR_materials_variants.variants = objectsFromJsons( + json.extensions.KHR_materials_variants?.variants, + gltfVariant + ); + this.extensions.KHR_materials_variants.variants = enforceVariantsUniqueness( + this.extensions.KHR_materials_variants.variants + ); + } + if (json.extensions?.KHR_interactivity !== undefined) { + this.extensions.KHR_interactivity = new GltfObject([]); + this.extensions.KHR_interactivity.graphs = objectsFromJsons( + json.extensions.KHR_interactivity?.graphs, + gltfGraph + ); + this.extensions.KHR_interactivity.graph = json.extensions.KHR_interactivity?.graph ?? 0; + } this.materials.push(gltfMaterial.createDefault()); this.samplers.push(gltfSampler.createDefault()); @@ -123,6 +153,26 @@ class glTF extends GltfObject { } this.computeDisjointAnimations(); + this.addNodeMetaInformation(); + } + + // Adds parent and scene information to each node + addNodeMetaInformation() { + function recurseNodes(gltf, nodeIndex, scene, parent) { + const node = gltf.nodes[nodeIndex]; + node.scene = scene; + node.parentNode = parent; + + // recurse into children + for (const child of node.children) { + recurseNodes(gltf, child, scene, node); + } + } + for (const scene of this.scenes) { + for (const nodeIndex of scene.nodes) { + recurseNodes(this, nodeIndex, scene, undefined); + } + } } // Computes indices of animations which are disjoint and can be played simultaneously. @@ -204,36 +254,6 @@ class glTF extends GltfObject { } } -function getJsonLightsFromExtensions(extensions) { - if (extensions === undefined) { - return []; - } - if (extensions.KHR_lights_punctual === undefined) { - return []; - } - return extensions.KHR_lights_punctual.lights; -} - -function getJsonIBLsFromExtensions(extensions) { - if (extensions === undefined) { - return []; - } - if (extensions.KHR_lights_image_based === undefined) { - return []; - } - return extensions.KHR_lights_image_based.imageBasedLights; -} - -function getJsonVariantsFromExtension(extensions) { - if (extensions === undefined) { - return []; - } - if (extensions.KHR_materials_variants === undefined) { - return []; - } - return extensions.KHR_materials_variants.variants; -} - function enforceVariantsUniqueness(variants) { for (let i = 0; i < variants.length; i++) { const name = variants[i].name; @@ -264,5 +284,6 @@ export { GltfObject, gltfAnimation, gltfSkin, - gltfVariant + gltfVariant, + gltfGraph }; diff --git a/source/gltf/gltf_object.js b/source/gltf/gltf_object.js index 80e47404..e16dfcf6 100644 --- a/source/gltf/gltf_object.js +++ b/source/gltf/gltf_object.js @@ -3,10 +3,14 @@ import { initGlForMembers, fromKeys } from "./utils"; // base class for all gltf objects class GltfObject { - constructor() { + constructor(animatedProperties = undefined) { this.extensions = undefined; this.extras = undefined; + this.gltfObjectIndex = undefined; this.animatedPropertyObjects = {}; + if (animatedProperties !== undefined) { + this.constructor.animatedProperties = animatedProperties; + } if (this.constructor.animatedProperties === undefined) { throw new Error("animatedProperties is not defined for " + this.constructor.name); } @@ -24,6 +28,7 @@ class GltfObject { } static animatedProperties = undefined; + static readOnlyAnimatedProperties = []; // If an array property is defined here, the length can be queried fromJson(json) { fromKeys(this, json); diff --git a/source/gltf/image_based_light.js b/source/gltf/image_based_light.js deleted file mode 100644 index ec3808d8..00000000 --- a/source/gltf/image_based_light.js +++ /dev/null @@ -1,60 +0,0 @@ -import { jsToGl } from "./utils.js"; -import { GltfObject } from "./gltf_object.js"; -import { GL } from "../Renderer/webgl"; - -// https://github.com/KhronosGroup/glTF/blob/khr_ktx2_ibl/extensions/2.0/Khronos/KHR_lights_image_based/schema/imageBasedLight.schema.json - -class ImageBasedLight extends GltfObject { - static animatedProperties = []; - constructor() { - super(); - this.rotation = jsToGl([0, 0, 0, 1]); - this.brightnessFactor = 1; - this.brightnessOffset = 0; - this.specularEnvironmentTexture = undefined; - this.diffuseEnvironmentTexture = undefined; - this.sheenEnvironmentTexture = undefined; - - // non-gltf - this.levelCount = 1; - } - - fromJson(jsonIBL) { - super.fromJson(jsonIBL); - - if (jsonIBL.extensions !== undefined) { - this.fromJsonExtensions(jsonIBL.extensions); - } - } - - fromJsonExtensions(extensions) { - if (extensions.KHR_materials_sheen !== undefined) { - this.sheenEnvironmentTexture = extensions.KHR_materials_sheen.sheenEnvironmentTexture; - } - } - - initGl(gltf) { - if (this.diffuseEnvironmentTexture !== undefined) { - const textureObject = gltf.textures[this.diffuseEnvironmentTexture]; - textureObject.type = GL.TEXTURE_CUBE_MAP; - } - if (this.specularEnvironmentTexture !== undefined) { - const textureObject = gltf.textures[this.specularEnvironmentTexture]; - textureObject.type = GL.TEXTURE_CUBE_MAP; - - const imageObject = gltf.images[textureObject.source]; - this.levelCount = imageObject.image.levelCount; - } - if (this.sheenEnvironmentTexture !== undefined) { - const textureObject = gltf.textures[this.sheenEnvironmentTexture]; - textureObject.type = GL.TEXTURE_CUBE_MAP; - - const imageObject = gltf.images[textureObject.source]; - if (this.levelCount !== imageObject.image.levelCount) { - console.error("Specular and sheen do not have same level count"); - } - } - } -} - -export { ImageBasedLight }; diff --git a/source/gltf/interactivity.js b/source/gltf/interactivity.js new file mode 100644 index 00000000..e1903435 --- /dev/null +++ b/source/gltf/interactivity.js @@ -0,0 +1,854 @@ +import { GltfObject } from "./gltf_object"; +import * as interactivity from "@khronosgroup/gltf-interactivity-sample-engine"; + +class gltfGraph extends GltfObject { + static animatedProperties = []; + + constructor() { + super(); + this.hasHoverEvent = false; + } + + fromJson(json) { + super.fromJson(json); + for (const declaration of json.declarations) { + if (declaration.op === "event/onHoverIn" || declaration.op === "event/onHoverOut") { + this.hasHoverEvent = true; + break; + } + } + } +} + +/** + * A controller for managing KHR_interactivity graphs in a glTF scene. + */ +class GraphController { + constructor(fps = 60, debug = false) { + this.fps = fps; + this.debug = debug; + this.graphIndex = undefined; + this.playing = false; + this.customEvents = []; + this.eventBus = new interactivity.DOMEventBus(); + this.engine = new interactivity.BasicBehaveEngine(this.fps, this.eventBus); + this.decorator = new SampleViewerDecorator(this.engine, this.debug); + this.eventSubscriptions = new Map(); + } + + needsHover() { + if (this.graphIndex === undefined || !this.playing) { + return false; + } + if ( + this.state?.renderingParameters?.enabledExtensions?.KHR_interactivity !== true || + this.state?.renderingParameters?.enabledExtensions?.KHR_node_hoverability !== true + ) { + return false; + } + return ( + this.state?.gltf?.extensions?.KHR_interactivity?.graphs[this.graphIndex] + ?.hasHoverEvent === true + ); + } + + receiveSelection(pickingResult) { + if (this.graphIndex !== undefined) { + this.decorator.receiveSelection(pickingResult); + } + } + + receiveHover(pickingResult) { + if (this.graphIndex !== undefined) { + this.decorator.receiveHover(pickingResult); + } + } + + /** + * Initialize the graph controller with the given state. + * This needs to be called every time a glTF assets is loaded. + * @param {GltfState} state - The state of the application. + */ + initializeGraphs(state) { + this.decorator.pauseEventQueue(); + this.engine.clearEventList(); + this.engine.clearPointerInterpolation(); + this.engine.clearVariableInterpolation(); + this.engine.clearScheduledDelays(); + this.engine.clearValueEvaluationCache(); + this.state = state; + this.playing = false; + this.graphIndex = undefined; + this.eventBus = new interactivity.DOMEventBus(); + this.engine = new interactivity.BasicBehaveEngine(this.fps, this.eventBus); + this.decorator = new SampleViewerDecorator(this.engine, this.debug); + this.decorator.setState(state); + for (const [eventName, callback] of this.eventSubscriptions) { + this.decorator.addCustomEventListener(eventName, callback); + } + } + + /** + * Loads the specified graph. Resets the engine. Starts playing if this.playing is true. + * @param {number} graphIndex + * @return {Array} An array of custom events defined in the graph. + */ + loadGraph(graphIndex) { + this.decorator.resetGraph(); + try { + this.customEvents = this.decorator.loadGraph(graphIndex); + this.graphIndex = graphIndex; + } catch (error) { + console.error("Error loading graph:", error); + } + return this.customEvents; + } + + /** + * Stops the graph engine. + */ + stopGraphEngine() { + if (this.graphIndex === undefined) { + return; + } + this.graphIndex = undefined; + this.playing = false; + this.decorator.pauseEventQueue(); + this.decorator.resetGraph(); + } + + /** + * Pauses the currently playing graph. + */ + pauseGraph() { + if (this.graphIndex === undefined || !this.playing) { + return; + } + this.decorator.pauseEventQueue(); + this.playing = false; + } + + /** + * Resumes the currently paused graph. + */ + resumeGraph() { + if (this.graphIndex === undefined || this.playing) { + return; + } + this.playing = true; + } + + /** + * Resets the current graph. + */ + resetGraph() { + if (this.graphIndex === undefined) { + return; + } + this.loadGraph(this.graphIndex); + } + + simulateTick() { + if (this.graphIndex === undefined) { + return; + } + this.decorator.executeEventQueueTick(); + } + + /** + * Dispatches an event to the behavior engine. + * @param {string} eventName + * @param {*} data + */ + dispatchEvent(eventName, data) { + if (this.graphIndex !== undefined) { + const dataCopy = JSON.parse(JSON.stringify(data)); + this.decorator.dispatchCustomEvent(eventName, dataCopy); + } + } + + /** + * Adds a custom event listener to the decorator. + * Khronos test assets use test/onStart, test/onFail and test/onSuccess. + * @param {string} eventName + * @param {function(CustomEvent)} callback + */ + addCustomEventListener(eventName, callback) { + this.eventSubscriptions.set(eventName, callback); + this.decorator.addCustomEventListener(eventName, callback); + } + + /** + * Clears all custom event listeners from the decorator. + */ + clearCustomEventListeners() { + this.eventSubscriptions.clear(); + this.decorator.clearCustomEventListeners(); + } +} + +class SampleViewerDecorator extends interactivity.ADecorator { + constructor(behaveEngine, debug = false) { + super(behaveEngine); + this.behaveEngine = behaveEngine; + this.world = undefined; + this.lastHoverNodeIndex = undefined; + + if (debug) { + this.behaveEngine.processNodeStarted = this.processNodeStarted; + this.behaveEngine.processAddingNodeToQueue = this.processAddingNodeToQueue; + this.behaveEngine.processExecutingNextNode = this.processExecutingNextNode; + } + this.behaveEngine.getWorld = this.getWorld; + + this.behaveEngine.stopAnimation = this.stopAnimation; + this.behaveEngine.stopAnimationAt = this.stopAnimationAt; + this.behaveEngine.startAnimation = this.startAnimation; + this.behaveEngine.getParentNodeIndex = this.getParentNodeIndex; + + this.registerBehaveEngineNode("animation/stop", interactivity.AnimationStop); + this.registerBehaveEngineNode("animation/start", interactivity.AnimationStart); + this.registerBehaveEngineNode("animation/stopAt", interactivity.AnimationStopAt); + + this.registerBehaveEngineNode("event/onSelect", interactivity.OnSelect); + this.registerBehaveEngineNode("event/onHoverIn", interactivity.OnHoverIn); + this.registerBehaveEngineNode("event/onHoverOut", interactivity.OnHoverOut); + } + + dispatchCustomEvent(eventName, data) { + // KHR_INTERACTIVITY prefix is used in the interactivity engine + this.behaveEngine.dispatchCustomEvent(`KHR_INTERACTIVITY:${eventName}`, data); + } + + addCustomEventListener(eventName, callback) { + // KHR_INTERACTIVITY prefix is used in the interactivity engine + this.behaveEngine.addCustomEventListener(`KHR_INTERACTIVITY:${eventName}`, callback); + } + + convertArrayToMatrix(array, width) { + const matrix = []; + for (let i = 0; i < array.length; i += width) { + matrix.push(array.slice(i, i + width)); + } + return matrix; + } + + setState(state) { + this.resetGraph(); + this.world = state; + this.behaveEngine.world = state; + this.registerKnownPointers(); + } + + receiveSelection(pickingResult) { + if (pickingResult.node) { + this.select( + pickingResult.node?.gltfObjectIndex, + pickingResult.controller, + pickingResult.position, + pickingResult.rayOrigin + ); + } + } + + receiveHover(pickingResult) { + this.hoverOn(pickingResult.node?.gltfObjectIndex, pickingResult.controller); + } + + getParentNodeIndex(nodeIndex) { + if (this.world === undefined || this.world.gltf === undefined) { + return undefined; + } + const node = this.world.gltf.nodes[nodeIndex]; + if (node === undefined || node.parentNode === undefined) { + return undefined; + } + return node.parentNode.gltfObjectIndex; + } + + loadGraph(graphIndex) { + const graphArray = this.world?.gltf?.extensions?.KHR_interactivity?.graphs; + if (graphArray && graphArray.length > graphIndex) { + const graphCopy = JSON.parse(JSON.stringify(graphArray[graphIndex])); + let events = graphCopy.events ?? []; + events = events.filter((event) => event.id !== undefined); + events = JSON.parse(JSON.stringify(events)); // Deep copy to avoid mutation + for (const event of events) { + for (const value of Object.values(event.values)) { + value.type = graphCopy.types[value.type].signature; + } + } + this.behaveEngine.loadBehaveGraph(graphCopy, false); + return events; + } + throw new Error(`Graph with index ${graphIndex} does not exist.`); + } + + resetGraph() { + this.pauseEventQueue(); + this.behaveEngine.loadBehaveGraph({ + nodes: [], + types: [], + events: [], + declarations: [], + variables: [] + }); + if (this.world === undefined) { + return; + } + for (const animation of this.world.gltf.animations) { + animation.reset(); + } + + this.behaveEngine.clearEventList(); + this.behaveEngine.clearPointerInterpolation(); + this.behaveEngine.clearVariableInterpolation(); + this.behaveEngine.clearScheduledDelays(); + this.behaveEngine.clearValueEvaluationCache(); + + const resetAnimatedProperty = (path, propertyName, parent, readOnly) => { + if (readOnly) { + return; + } + parent.animatedPropertyObjects[propertyName].rest(); + }; + this.recurseAllAnimatedProperties(this.world.gltf, resetAnimatedProperty); + } + + processNodeStarted(node) { + console.log("Node started:", node); + } + + processAddingNodeToQueue(flow) { + console.log("Adding node to queue:", flow); + } + + processExecutingNextNode(flow) { + console.log("Executing next node:", flow); + } + + getTypeFromValue(value) { + if (typeof value === "number") { + return "float"; + } + if (typeof value === "boolean") { + return "bool"; + } + if (value.length === 2) { + return "float2"; + } + if (value.length === 3) { + return "float3"; + } + if (value.length === 4) { + return "float4"; + } + if (value.length === 16) { + return "float4x4"; + } + return undefined; + } + + getDefaultValueFromType(type) { + switch (type) { + case "int": + return 0; + case "float": + return NaN; + case "bool": + return false; + case "float2": + return [NaN, NaN]; + case "float3": + return [NaN, NaN, NaN]; + case "float4": + case "float2x2": + return [NaN, NaN, NaN, NaN]; + case "float3x3": + // prettier-ignore + return [ + NaN, NaN, NaN, + NaN, NaN, NaN, + NaN, NaN, NaN]; + case "float4x4": + // prettier-ignore + return [ + NaN, NaN, NaN, NaN, + NaN, NaN, NaN, NaN, + NaN, NaN, NaN, NaN, + NaN, NaN, NaN, NaN + ]; + } + return undefined; + } + + traversePath(path, type, value = undefined) { + const pathPieces = path.split("/"); + pathPieces.shift(); // Remove first empty piece from split + const lastPiece = pathPieces[pathPieces.length - 1]; + if (value !== undefined) { + pathPieces.pop(); + } + let currentNode = this.world.gltf; + + for (let i = 0; i < pathPieces.length; i++) { + if (Array.isArray(currentNode)) { + const index = parseInt(pathPieces[i]); + if (isNaN(index) || index < 0 || index >= currentNode.length) { + return undefined; // Invalid index + } + currentNode = currentNode[index]; + continue; + } + const pathPiece = pathPieces[i]; + if (currentNode[pathPiece] !== undefined) { + currentNode = currentNode[pathPiece]; + } else { + return undefined; + } + } + if ( + type === "float2" || + type === "float3" || + type === "float4" || + type === "float2x2" || + type === "float3x3" || + type === "float4x4" + ) { + if (value !== undefined) { + value = value.slice(0); //clone array + } else { + currentNode = currentNode.slice(0); //clone array + } + } + + if (value !== undefined) { + currentNode.animatedPropertyObjects[lastPiece].animate(value); + } + return currentNode; + } + + recurseAllAnimatedProperties(gltfObject, callable, currentPath = "") { + if (gltfObject === undefined || !(gltfObject instanceof GltfObject)) { + return; + } + + // Call for all animated properties of this gltfObject + for (const property of gltfObject.constructor.animatedProperties) { + if (gltfObject[property] === undefined) { + continue; + } + callable(currentPath, property, gltfObject, false); + } + + // Call for all read-only animated properties of this gltfObject + for (const property of gltfObject.constructor.readOnlyAnimatedProperties) { + if (gltfObject[property] === undefined) { + continue; + } + callable(currentPath, property, gltfObject, true); + } + + // Recurse into all GltfObject + for (const key in gltfObject) { + if (gltfObject[key] instanceof GltfObject) { + this.recurseAllAnimatedProperties( + gltfObject[key], + callable, + currentPath + "/" + key + ); + } else if (Array.isArray(gltfObject[key])) { + if (gltfObject[key].length === 0 || !(gltfObject[key][0] instanceof GltfObject)) { + continue; + } + for (let i = 0; i < gltfObject[key].length; i++) { + this.recurseAllAnimatedProperties( + gltfObject[key][i], + callable, + currentPath + "/" + key + "/" + i + ); + } + } + } + + // Recurse into all extensions + for (const extensionName in gltfObject.extensions) { + const extension = gltfObject.extensions[extensionName]; + if (extension instanceof GltfObject) { + this.recurseAllAnimatedProperties( + extension, + callable, + currentPath + "/extensions/" + extensionName + ); + } + } + } + + registerKnownPointers() { + // The engine is checking if a path is valid so we do not need to handle this here + if (this.world === undefined) { + return; + } + const registerFunction = (currentPath, propertyName, parent, readOnly) => { + let jsonPtr = currentPath + "/" + propertyName; + let type = this.getTypeFromValue(parent[propertyName]); + if (readOnly) { + if (type === "float") { + // All read-only number properties are currently integers + type = "int"; + } + + // If the property is an array, read-only pointers return the length of the array + if (Array.isArray(parent[propertyName])) { + jsonPtr += ".length"; + type = "int"; + this.registerJsonPointer( + jsonPtr, + (path) => { + const fixedPath = path.slice(0, -7); // Remove ".length" + const result = this.traversePath(fixedPath, type); + if (result === undefined) { + return [0]; + } + return [result.length]; + }, + (_path, _value) => {}, + type, + true + ); + return; + } + + this.registerJsonPointer( + jsonPtr, + (path) => { + let result = this.traversePath(path, type); + if (result === undefined) { + result = this.getDefaultValueFromType(type); + } + if (type === "bool" || type === "int" || type === "float") { + result = [result]; + } + return result; + }, + (_path, _value) => {}, + type, + true + ); + return; + } + + if (type === undefined) { + return; + } + + // Register getter and setter for the property + this.registerJsonPointer( + jsonPtr, + (path) => { + let result = this.traversePath(path, type); + if (result === undefined) { + result = this.getDefaultValueFromType(type); + } + if (type === "bool" || type === "int" || type === "float") { + result = [result]; + } + return result; + }, + (path, value) => { + this.traversePath(path, type, value); + }, + type, + false + ); + }; + this.recurseAllAnimatedProperties(this.world.gltf, registerFunction); + + // Special pointers that need to be handled manually + + this.registerJsonPointer( + `/extensions/KHR_lights_punctual/lights.length`, + (_path) => { + const lights = this.world.gltf.extensions?.KHR_lights_punctual?.lights; + if (lights === undefined) { + return [0]; + } + return [lights.length]; + }, + (_path, _value) => {}, + "int", + true + ); + + this.registerJsonPointer( + `/materials.length`, + (_path) => { + // Return the number of materials excluding the default material + return [this.world.gltf.materials.length - 1]; + }, + (_path, _value) => {}, + "int", + true + ); + + const nodeCount = this.world.gltf.nodes.length; + this.registerJsonPointer( + `/nodes/${nodeCount}/children/${nodeCount}`, + (path) => { + return [this.traversePath(path, "int")]; + }, + (_path, _value) => {}, + "int", + true + ); + + // Returns the currently computed global matrix of the node + this.registerJsonPointer( + `/nodes/${nodeCount}/globalMatrix`, + (path) => { + const pathParts = path.split("/"); + const nodeIndex = parseInt(pathParts[2]); + const node = this.world.gltf.nodes[nodeIndex]; + node.scene.applyTransformHierarchy(this.world.gltf); + return node.worldTransform.slice(0); + }, + (_path, _value) => {}, + "float4x4", + true + ); + + // Returns the currently computed local matrix of the node + this.registerJsonPointer( + `/nodes/${nodeCount}/matrix`, + (path) => { + const pathParts = path.split("/"); + const nodeIndex = parseInt(pathParts[2]); + const node = this.world.gltf.nodes[nodeIndex]; + return node.getLocalTransform(); + }, + (_path, _value) => {}, + "float4x4", + true + ); + + // Returns the parent node index of the node + this.registerJsonPointer( + `/nodes/${nodeCount}/parent`, + (path) => { + const pathParts = path.split("/"); + const nodeIndex = parseInt(pathParts[2]); + const node = this.world.gltf.nodes[nodeIndex]; + return [node.parentNode?.gltfObjectIndex]; + }, + (_path, _value) => {}, + "int", + true + ); + + // Pointer to indices + + this.registerJsonPointer( + `/nodes/${nodeCount}/extensions/KHR_lights_punctual/light`, + (path) => { + return [this.traversePath(path, "int")]; + }, + (_path, _value) => {}, + "int", + true + ); + + const sceneCount = this.world.gltf.scenes.length; + this.registerJsonPointer( + `/scenes/${sceneCount}/nodes/${nodeCount}`, + (path) => { + return [this.traversePath(path, "int")]; + }, + (_path, _value) => {}, + "int", + true + ); + + const skinCount = this.world.gltf.skins.length; + this.registerJsonPointer( + `/skins/${skinCount}/joints/${nodeCount}`, + (path) => { + return [this.traversePath(path, "int")]; + }, + (_path, _value) => {}, + "int", + true + ); + + // Pointer for animation control + + const animationCount = this.world.gltf.animations.length; + this.registerJsonPointer( + `/animations/${animationCount}/extensions/KHR_interactivity/isPlaying`, + (path) => { + const pathParts = path.split("/"); + const animationIndex = parseInt(pathParts[2]); + const animation = this.world.gltf.animations[animationIndex]; + return [animation.createdTimestamp !== undefined]; + }, + (_path, _value) => {}, + "bool", + true + ); + this.registerJsonPointer( + `/animations/${animationCount}/extensions/KHR_interactivity/minTime`, + (path) => { + const pathParts = path.split("/"); + const animationIndex = parseInt(pathParts[2]); + const animation = this.world.gltf.animations[animationIndex]; + animation.computeMinMaxTime(this.world.gltf); + return [animation.minTime]; + }, + (_path, _value) => {}, + "float", + true + ); + this.registerJsonPointer( + `/animations/${animationCount}/extensions/KHR_interactivity/maxTime`, + (path) => { + const pathParts = path.split("/"); + const animationIndex = parseInt(pathParts[2]); + const animation = this.world.gltf.animations[animationIndex]; + animation.computeMinMaxTime(this.world.gltf); + return [animation.maxTime]; + }, + (_path, _value) => {}, + "float", + true + ); + + // The playhead returns a number between 0 and maxTime + this.registerJsonPointer( + `/animations/${animationCount}/extensions/KHR_interactivity/playhead`, + (path) => { + const pathParts = path.split("/"); + const animationIndex = parseInt(pathParts[2]); + const animation = this.world.gltf.animations[animationIndex]; + if (animation.interpolators.length === 0) { + return [NaN]; + } + return [animation.interpolators[0].prevT]; + }, + (_path, _value) => {}, + "float", + true + ); + + // The virtual playhead return the current time on the infinite timeline. Can be negative or larger than maxTime + this.registerJsonPointer( + `/animations/${animationCount}/extensions/KHR_interactivity/virtualPlayhead`, + (path) => { + const pathParts = path.split("/"); + const animationIndex = parseInt(pathParts[2]); + const animation = this.world.gltf.animations[animationIndex]; + if (animation.interpolators.length === 0) { + return [NaN]; + } + return [animation.interpolators[0].prevRequestedT]; + }, + (_path, _value) => {}, + "float", + true + ); + + // Pointer for the active camera + + this.registerJsonPointer( + `/extensions/KHR_interactivity/activeCamera/rotation`, + (_path) => { + let activeCamera = this.world.userCamera; + if (this.world.cameraNodeIndex !== undefined) { + if ( + this.world.cameraNodeIndex < 0 || + this.world.cameraNodeIndex >= this.world.gltf.nodes.length + ) { + return [NaN, NaN, NaN, NaN]; + } + const cameraIndex = this.world.gltf.nodes[this.world.cameraNodeIndex].camera; + if (cameraIndex === undefined) { + return [NaN, NaN, NaN, NaN]; + } + activeCamera = this.world.gltf.cameras[cameraIndex]; + activeCamera.setNode(this.world.gltf, this.world.cameraNodeIndex); + } + return activeCamera.getRotation(this.world.gltf).slice(0); + }, + (_path, _value) => { + //no-op + }, + "float4", + true + ); + + this.registerJsonPointer( + `/extensions/KHR_interactivity/activeCamera/position`, + (_path) => { + let activeCamera = this.world.userCamera; + if (this.world.cameraNodeIndex !== undefined) { + if ( + this.world.cameraNodeIndex < 0 || + this.world.cameraNodeIndex >= this.world.gltf.nodes.length + ) { + return [NaN, NaN, NaN]; + } + const cameraIndex = this.world.gltf.nodes[this.world.cameraNodeIndex].camera; + if (cameraIndex === undefined) { + return [NaN, NaN, NaN]; + } + activeCamera = this.world.gltf.cameras[cameraIndex]; + activeCamera.setNode(this.world.gltf, this.world.cameraNodeIndex); + } + return activeCamera.getPosition(this.world.gltf).slice(0); + }, + (_path, _value) => { + //no-op + }, + "float3", + true + ); + } + + registerJsonPointer(jsonPtr, getterCallback, setterCallback, typeName, readOnly) { + this.behaveEngine.registerJsonPointer( + jsonPtr, + getterCallback, + setterCallback, + typeName, + readOnly + ); + } + + getWorld() { + return this.world?.gltf; + } + + stopAnimation(animationIndex) { + const animation = this.world.gltf.animations[animationIndex]; + animation.reset(); + } + + stopAnimationAt(animationIndex, stopTime, callback) { + const animation = this.world.gltf.animations[animationIndex]; + if (animation.createdTimestamp === undefined) { + return; + } + animation.stopTime = stopTime; + animation.stopCallback = callback; + } + + startAnimation(animationIndex, startTime, endTime, speed, callback) { + const animation = this.world.gltf.animations[animationIndex]; + animation.createdTimestamp = undefined; + animation.startTime = startTime; + animation.endTime = endTime; + animation.speed = speed; + animation.endCallback = callback; + animation.createdTimestamp = this.world.animationTimer.elapsedSec(); + } +} + +export { gltfGraph, GraphController }; diff --git a/source/gltf/interpolator.js b/source/gltf/interpolator.js index 76f24798..56b9a115 100644 --- a/source/gltf/interpolator.js +++ b/source/gltf/interpolator.js @@ -7,9 +7,13 @@ class gltfInterpolator { constructor() { this.prevKey = 0; this.prevT = 0.0; + this.prevRequestedT = 0.0; } slerpQuat(q1, q2, t) { + if (q1 instanceof Float64Array || q2 instanceof Float64Array) { + glMatrix.setMatrixArrayType(Float64Array); + } const qn1 = quat.create(); const qn2 = quat.create(); @@ -21,26 +25,34 @@ class gltfInterpolator { quat.slerp(quatResult, qn1, qn2, t); quat.normalize(quatResult, quatResult); + glMatrix.setMatrixArrayType(Float32Array); + return quatResult; } step(prevKey, output, stride) { + if (output instanceof Float64Array) { + glMatrix.setMatrixArrayType(Float64Array); + } const result = new glMatrix.ARRAY_TYPE(stride); for (let i = 0; i < stride; ++i) { result[i] = output[prevKey * stride + i]; } - + glMatrix.setMatrixArrayType(Float32Array); return result; } linear(prevKey, nextKey, output, t, stride) { + if (output instanceof Float64Array) { + glMatrix.setMatrixArrayType(Float64Array); + } const result = new glMatrix.ARRAY_TYPE(stride); for (let i = 0; i < stride; ++i) { result[i] = output[prevKey * stride + i] * (1 - t) + output[nextKey * stride + i] * t; } - + glMatrix.setMatrixArrayType(Float32Array); return result; } @@ -53,6 +65,9 @@ class gltfInterpolator { const V = 1 * stride; const B = 2 * stride; + if (output instanceof Float64Array) { + glMatrix.setMatrixArrayType(Float64Array); + } const result = new glMatrix.ARRAY_TYPE(stride); const tSq = t ** 2; const tCub = t ** 3; @@ -72,6 +87,8 @@ class gltfInterpolator { (tCub - tSq) * a; } + glMatrix.setMatrixArrayType(Float32Array); + return result; } @@ -79,7 +96,7 @@ class gltfInterpolator { this.prevKey = 0; } - interpolate(gltf, channel, sampler, t, stride, maxTime) { + interpolate(gltf, channel, sampler, t, stride, maxTime, reverse) { if (t === undefined) { return undefined; } @@ -87,36 +104,63 @@ class gltfInterpolator { const input = gltf.accessors[sampler.input].getNormalizedDeinterlacedView(gltf); const output = gltf.accessors[sampler.output].getNormalizedDeinterlacedView(gltf); + this.prevRequestedT = t; + if (output.length === stride) { // no interpolation for single keyFrame animations - return jsToGlSlice(output, 0, stride); + if (output instanceof Float64Array) { + glMatrix.setMatrixArrayType(Float64Array); + } + const result = jsToGlSlice(output, 0, stride); + glMatrix.setMatrixArrayType(Float32Array); + return result; } // Wrap t around, so the animation loops. // Make sure that t is never earlier than the first keyframe and never later then the last keyframe. + const isNegative = t < 0; + const isZero = t === 0; t = t % maxTime; + if (isNegative || (t === 0 && !isZero)) { + t += maxTime; + } t = clamp(t, input[0], input[input.length - 1]); - if (this.prevT > t) { + if (this.prevT > t && !reverse) { this.prevKey = 0; } + if (reverse && this.prevT < t) { + this.prevKey = input.length - 1; + } + this.prevT = t; // Find next keyframe: min{ t of input | t > prevKey } let nextKey = null; - for (let i = this.prevKey; i < input.length; ++i) { - if (t <= input[i]) { - nextKey = clamp(i, 1, input.length - 1); - break; + // We need to search backwards for reversed animations + if (reverse) { + for (let i = this.prevKey; i >= 0; --i) { + if (t >= input[i]) { + nextKey = i; + break; + } + } + this.prevKey = clamp(nextKey + 1, nextKey, input.length - 1); + } else { + for (let i = this.prevKey; i < input.length; ++i) { + if (t <= input[i]) { + nextKey = clamp(i, 1, input.length - 1); + break; + } } + this.prevKey = clamp(nextKey - 1, 0, nextKey); } - this.prevKey = clamp(nextKey - 1, 0, nextKey); - const keyDelta = input[nextKey] - input[this.prevKey]; + const keyDelta = Math.abs(input[nextKey] - input[this.prevKey]); // Normalize t: [t0, t1] -> [0, 1] - const tn = (t - input[this.prevKey]) / keyDelta; + const tn = Math.abs(t - input[this.prevKey]) / keyDelta; if (channel.target.path === InterpolationPath.ROTATION) { if (InterpolationModes.CUBICSPLINE === sampler.interpolation) { @@ -149,6 +193,9 @@ class gltfInterpolator { const y = output[4 * index + 1]; const z = output[4 * index + 2]; const w = output[4 * index + 3]; + if (output instanceof Float64Array) { + return new Float64Array([x, y, z, w]); + } return quat.fromValues(x, y, z, w); } } diff --git a/source/gltf/light.js b/source/gltf/light.js index 829a1d4b..9e79091c 100644 --- a/source/gltf/light.js +++ b/source/gltf/light.js @@ -8,7 +8,7 @@ class gltfLight extends GltfObject { super(); this.name = undefined; this.type = "directional"; - this.color = [1, 1, 1]; + this.color = vec3.fromValues(1, 1, 1); this.intensity = 1; this.range = -1; this.spot = new gltfLightSpot(); diff --git a/source/gltf/material.js b/source/gltf/material.js index 60d350d2..0b693f5e 100644 --- a/source/gltf/material.js +++ b/source/gltf/material.js @@ -5,6 +5,7 @@ import { GltfObject } from "./gltf_object.js"; class gltfMaterial extends GltfObject { static animatedProperties = ["alphaCutoff", "emissiveFactor"]; + static readOnlyAnimatedProperties = ["doubleSided"]; static scatterSampleCount = 55; static scatterSamples = undefined; static scatterMinRadius = 1.0; diff --git a/source/gltf/mesh.js b/source/gltf/mesh.js index 5c42f986..1f4b1759 100644 --- a/source/gltf/mesh.js +++ b/source/gltf/mesh.js @@ -4,6 +4,7 @@ import { GltfObject } from "./gltf_object.js"; class gltfMesh extends GltfObject { static animatedProperties = ["weights"]; + static readOnlyAnimatedProperties = ["weights", "primitives"]; constructor() { super(); this.primitives = []; diff --git a/source/gltf/node.js b/source/gltf/node.js index 792d717a..7d984300 100644 --- a/source/gltf/node.js +++ b/source/gltf/node.js @@ -1,6 +1,7 @@ import { mat4, quat, vec3 } from "gl-matrix"; import { jsToGl, jsToGlSlice } from "./utils.js"; import { GltfObject } from "./gltf_object.js"; +import { GL } from "../Renderer/webgl.js"; // contain: // transform @@ -8,6 +9,8 @@ import { GltfObject } from "./gltf_object.js"; class gltfNode extends GltfObject { static animatedProperties = ["rotation", "scale", "translation", "weights"]; + static readOnlyAnimatedProperties = ["camera", "children", "mesh", "skin", "weights"]; + static currentPickingColor = 1; constructor() { super(); this.camera = undefined; @@ -29,10 +32,17 @@ class gltfNode extends GltfObject { this.light = undefined; this.instanceMatrices = undefined; this.instanceWorldTransforms = undefined; + this.pickingColor = undefined; + this.parentNode = undefined; + this.scene = undefined; } // eslint-disable-next-line no-unused-vars initGl(gltf, webGlContext) { + if (this.mesh !== undefined) { + this.pickingColor = gltfNode.currentPickingColor; + gltfNode.currentPickingColor += 1; + } if (this.extensions?.EXT_mesh_gpu_instancing?.attributes !== undefined) { const firstAccessor = Object.values( this.extensions?.EXT_mesh_gpu_instancing?.attributes @@ -42,17 +52,37 @@ class gltfNode extends GltfObject { this.extensions?.EXT_mesh_gpu_instancing?.attributes?.TRANSLATION; let translationData = undefined; if (translationAccessor !== undefined) { - translationData = gltf.accessors[translationAccessor].getDeinterlacedView(gltf); + if (translationAccessor.componentType === GL.FLOAT) { + translationData = gltf.accessors[translationAccessor].getDeinterlacedView(gltf); + } else { + console.warn("EXT_mesh_gpu_instancing translation accessor must be a float"); + } } const rotationAccessor = this.extensions?.EXT_mesh_gpu_instancing?.attributes?.ROTATION; let rotationData = undefined; if (rotationAccessor !== undefined) { - rotationData = gltf.accessors[rotationAccessor].getDeinterlacedView(gltf); + if ( + rotationAccessor.componentType === GL.FLOAT || + (rotationAccessor.normalized && + (rotationAccessor.componentType === GL.BYTE || + rotationAccessor.componentType === GL.SHORT)) + ) { + rotationData = + gltf.accessors[rotationAccessor].getNormalizedDeinterlacedView(gltf); + } else { + console.warn( + "EXT_mesh_gpu_instancing rotation accessor must be a float, byte normalized, or short normalized" + ); + } } const scaleAccessor = this.extensions?.EXT_mesh_gpu_instancing?.attributes?.SCALE; let scaleData = undefined; if (scaleAccessor !== undefined) { - scaleData = gltf.accessors[scaleAccessor].getDeinterlacedView(gltf); + if (scaleAccessor.componentType === GL.FLOAT) { + scaleData = gltf.accessors[scaleAccessor].getDeinterlacedView(gltf); + } else { + console.warn("EXT_mesh_gpu_instancing scale accessor must be a float"); + } } this.instanceMatrices = []; for (let i = 0; i < count; i++) { @@ -75,6 +105,22 @@ class gltfNode extends GltfObject { if (jsonNode.matrix !== undefined) { this.applyMatrix(jsonNode.matrix); } + if (jsonNode.extensions?.KHR_node_visibility !== undefined) { + this.extensions.KHR_node_visibility = new KHR_node_visibility(); + this.extensions.KHR_node_visibility.fromJson(jsonNode.extensions.KHR_node_visibility); + } + if (jsonNode.extensions?.KHR_node_selectability !== undefined) { + this.extensions.KHR_node_selectability = new KHR_node_selectability(); + this.extensions.KHR_node_selectability.fromJson( + jsonNode.extensions.KHR_node_selectability + ); + } + if (jsonNode.extensions?.KHR_node_hoverability !== undefined) { + this.extensions.KHR_node_hoverability = new KHR_node_hoverability(); + this.extensions.KHR_node_hoverability.fromJson( + jsonNode.extensions.KHR_node_hoverability + ); + } } getWeights(gltf) { @@ -113,4 +159,28 @@ class gltfNode extends GltfObject { } } +class KHR_node_visibility extends GltfObject { + static animatedProperties = ["visible"]; + constructor() { + super(); + this.visible = true; + } +} + +class KHR_node_selectability extends GltfObject { + static animatedProperties = ["selectable"]; + constructor() { + super(); + this.selectable = true; + } +} + +class KHR_node_hoverability extends GltfObject { + static animatedProperties = ["hoverable"]; + constructor() { + super(); + this.hoverable = true; + } +} + export { gltfNode }; diff --git a/source/gltf/primitive.js b/source/gltf/primitive.js index 4ea1937e..f1bcb405 100644 --- a/source/gltf/primitive.js +++ b/source/gltf/primitive.js @@ -14,6 +14,7 @@ import { generateTangents } from "../libs/mikktspace.js"; class gltfPrimitive extends GltfObject { static animatedProperties = []; + static readOnlyAnimatedProperties = ["material"]; constructor() { super(); this.attributes = {}; @@ -71,8 +72,8 @@ class gltfPrimitive extends GltfObject { // Generate tangents with Mikktspace which needs normals and texcoords as inputs for triangles if ( this.attributes.TANGENT === undefined && - this.attributes.NORMAL && - this.attributes.TEXCOORD_0 && + this.attributes.NORMAL !== undefined && + this.attributes.TEXCOORD_0 !== undefined && this.mode > 3 ) { console.info("Generating tangents using the MikkTSpace algorithm."); @@ -326,6 +327,10 @@ class gltfPrimitive extends GltfObject { const positionsAccessor = gltf.accessors[this.attributes.POSITION]; const positions = positionsAccessor.getNormalizedTypedView(gltf); + if (positions instanceof Float64Array) { + throw new Error("64-bit float attributes are not supported in WebGL2"); + } + if (this.indices !== undefined) { // Primitive has indices. @@ -949,9 +954,39 @@ class gltfPrimitive extends GltfObject { return; } - const positions = gltf.accessors[this.attributes.POSITION].getTypedView(gltf); - const normals = gltf.accessors[this.attributes.NORMAL].getTypedView(gltf); - const texcoords = gltf.accessors[this.attributes.TEXCOORD_0].getTypedView(gltf); + let positions = + gltf.accessors[this.attributes.POSITION].getNormalizedDeinterlacedView(gltf); + const normals = gltf.accessors[this.attributes.NORMAL].getNormalizedDeinterlacedView(gltf); + let texcoords = + gltf.accessors[this.attributes.TEXCOORD_0].getNormalizedDeinterlacedView(gltf); + + if (positions instanceof Float64Array) { + console.warn( + "Cannot generate tangents: WebGL2 does not support 64-bit float attributes." + ); + return; + } else if (positions instanceof Float32Array === false) { + positions = new Float32Array(positions); + } + + if (normals instanceof Float64Array) { + console.warn( + "Cannot generate tangents: WebGL2 does not support 64-bit float attributes." + ); + return; + } else if (normals instanceof Float32Array === false) { + console.warn("Cannot generate tangents: Normal attribute in wrong format"); + return; + } + + if (texcoords instanceof Float64Array) { + console.warn( + "Cannot generate tangents: WebGL2 does not support 64-bit float attributes." + ); + return; + } else if (texcoords instanceof Float32Array === false) { + texcoords = new Float32Array(texcoords); + } const tangents = generateTangents(positions, normals, texcoords); diff --git a/source/gltf/scene.js b/source/gltf/scene.js index 3e056f56..4966be10 100644 --- a/source/gltf/scene.js +++ b/source/gltf/scene.js @@ -3,22 +3,15 @@ import { GltfObject } from "./gltf_object"; class gltfScene extends GltfObject { static animatedProperties = []; + static readOnlyAnimatedProperties = ["nodes"]; constructor(nodes = [], name = undefined) { super(); this.nodes = nodes; this.name = name; - - // non gltf - this.imageBasedLight = undefined; } initGl(gltf, webGlContext) { super.initGl(gltf, webGlContext); - - if (this.extensions !== undefined && this.extensions.KHR_lights_image_based !== undefined) { - const index = this.extensions.KHR_lights_image_based.imageBasedLight; - this.imageBasedLight = gltf.imageBasedLights[index]; - } } applyTransformHierarchy(gltf, rootTransform = mat4.create()) { @@ -59,24 +52,53 @@ class gltfScene extends GltfObject { } } - gatherNodes(gltf) { + gatherNodes(gltf, enabledExtensions) { const nodes = []; + const selectableNodes = []; + const hoverableNodes = []; - function gatherNode(nodeIndex) { + function gatherNode(nodeIndex, visible, selectable, hoverable) { const node = gltf.nodes[nodeIndex]; - nodes.push(node); + if ( + !enabledExtensions.KHR_node_visibility || + (node.extensions?.KHR_node_visibility?.visible !== false && visible) + ) { + nodes.push(node); + } else { + visible = false; + } + if ( + !enabledExtensions.KHR_node_selectability || + (node.extensions?.KHR_node_selectability?.selectable !== false && selectable) + ) { + selectableNodes.push(node); + } else { + selectable = false; + } + if ( + !enabledExtensions.KHR_node_hoverability || + (node.extensions?.KHR_node_hoverability?.hoverable !== false && hoverable) + ) { + hoverableNodes.push(node); + } else { + hoverable = false; + } // recurse into children for (const child of node.children) { - gatherNode(child); + gatherNode(child, visible, selectable, hoverable); } } for (const node of this.nodes) { - gatherNode(node); + gatherNode(node, true, true, true); } - return nodes; + return { + nodes: nodes, + selectableNodes: selectableNodes, + hoverableNodes: hoverableNodes + }; } includesNode(gltf, nodeIndex) { diff --git a/source/gltf/skin.js b/source/gltf/skin.js index aab9da3c..4c044348 100644 --- a/source/gltf/skin.js +++ b/source/gltf/skin.js @@ -10,6 +10,7 @@ import { gltfSampler } from "./sampler.js"; class gltfSkin extends GltfObject { static animatedProperties = []; + static readOnlyAnimatedProperties = ["joints", "skeleton"]; constructor() { super(); @@ -68,9 +69,16 @@ class gltfSkin extends GltfObject { } computeJoints(gltf, webGlContext) { - let ibmAccessor = null; + let ibmAccessorData = null; if (this.inverseBindMatrices !== undefined) { - ibmAccessor = gltf.accessors[this.inverseBindMatrices].getDeinterlacedView(gltf); + const ibmAccessor = gltf.accessors[this.inverseBindMatrices]; + if (ibmAccessor.componentType === GL.FLOAT) { + ibmAccessorData = ibmAccessor.getDeinterlacedView(gltf); + } else { + console.warn( + "EXT_mesh_gpu_instancing inverseBindMatrices accessor must be a float" + ); + } } this.jointMatrices = []; @@ -85,8 +93,8 @@ class gltfSkin extends GltfObject { let jointMatrix = mat4.clone(node.worldTransform); - if (ibmAccessor !== null) { - let ibm = jsToGlSlice(ibmAccessor, i * 16, 16); + if (ibmAccessorData !== null) { + let ibm = jsToGlSlice(ibmAccessorData, i * 16, 16); mat4.mul(jointMatrix, jointMatrix, ibm); } diff --git a/source/gltf/texture.js b/source/gltf/texture.js index e2723344..2d5b4129 100644 --- a/source/gltf/texture.js +++ b/source/gltf/texture.js @@ -3,6 +3,7 @@ import { fromKeys, initGlForMembers } from "./utils.js"; import { GL } from "../Renderer/webgl.js"; import { GltfObject } from "./gltf_object.js"; +import { vec2 } from "gl-matrix"; class gltfTexture extends GltfObject { static animatedProperties = []; @@ -100,8 +101,8 @@ class KHR_texture_transform extends GltfObject { static animatedProperties = ["offset", "scale", "rotation"]; constructor() { super(); - this.offset = [0, 0]; - this.scale = [1, 1]; + this.offset = vec2.fromValues(0, 0); + this.scale = vec2.fromValues(1, 1); this.rotation = 0; } } diff --git a/source/gltf/utils.js b/source/gltf/utils.js index 4015e27e..e8232c84 100644 --- a/source/gltf/utils.js +++ b/source/gltf/utils.js @@ -49,8 +49,10 @@ function objectsFromJsons(jsonObjects, GltfType) { } const objects = []; - for (const jsonObject of jsonObjects) { - objects.push(objectFromJson(jsonObject, GltfType)); + for (const [index, jsonObject] of jsonObjects.entries()) { + const object = objectFromJson(jsonObject, GltfType); + object.gltfObjectIndex = index; + objects.push(object); } return objects; } @@ -151,72 +153,17 @@ class Timer { } start() { - this.startTime = new Date().getTime() / 1000; + this.startTime = performance.now() / 1000; this.endTime = undefined; this.seconds = undefined; } stop() { - this.endTime = new Date().getTime() / 1000; + this.endTime = performance.now() / 1000; this.seconds = this.endTime - this.startTime; } } -class AnimationTimer { - constructor() { - this.startTime = 0; - this.paused = true; - this.fixedTime = null; - this.pausedTime = 0; - } - - elapsedSec() { - if (this.paused) { - return this.pausedTime / 1000; - } else { - return this.fixedTime || (new Date().getTime() - this.startTime) / 1000; - } - } - - toggle() { - if (this.paused) { - this.unpause(); - } else { - this.pause(); - } - } - - start() { - this.startTime = new Date().getTime(); - this.paused = false; - } - - pause() { - this.pausedTime = new Date().getTime() - this.startTime; - this.paused = true; - } - - unpause() { - this.startTime += new Date().getTime() - this.startTime - this.pausedTime; - this.paused = false; - } - - reset() { - if (!this.paused) { - // Animation is running. - this.startTime = new Date().getTime(); - } else { - this.startTime = 0; - } - this.pausedTime = 0; - } - - setFixedTime(timeInSec) { - this.paused = false; - this.fixedTime = timeInSec; - } -} - export { jsToGl, jsToGlSlice, @@ -236,6 +183,5 @@ export { combinePaths, UniformStruct, Timer, - AnimationTimer, initGlForMembers }; diff --git a/tests/baseTestConfig.ts b/tests/baseTestConfig.ts new file mode 100644 index 00000000..b20e7524 --- /dev/null +++ b/tests/baseTestConfig.ts @@ -0,0 +1,11 @@ +import { test as base } from "@playwright/test"; + +export type TestOptions = { + testRepoURL: string; + downloadFolder: string; +}; + +export const test = base.extend({ + testRepoURL: ["test", { option: true }], + downloadFolder: ["testAssetDownload", { option: true }] +}); diff --git a/tests/downloadAssets.spec.ts b/tests/downloadAssets.spec.ts new file mode 100644 index 00000000..0f3f1ba8 --- /dev/null +++ b/tests/downloadAssets.spec.ts @@ -0,0 +1,42 @@ +import { expect } from "@playwright/test"; +import { test } from "./baseTestConfig"; +import fs from "fs"; +import { dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +test("download assets", async ({ testRepoURL, downloadFolder }) => { + if ( + fs.existsSync(`${__dirname}/testAssetDownloads/${downloadFolder}`) && + process.env.REDOWNLOAD_ASSETS !== "true" + ) { + console.log( + `Assets already downloaded in testAssetDownloads/${downloadFolder}, skipping download. Set REDOWNLOAD_ASSETS=true to force re-download.` + ); + return; + } + console.log(`Downloading assets to testAssetDownloads/${downloadFolder}`); + const response = await fetch(testRepoURL); + expect(response.ok).toBeTruthy(); + const data = await response.json(); + const parentUrl = testRepoURL.substring(0, testRepoURL.lastIndexOf("/")); + for (const asset of data) { + const path = `${asset.name}/glTF-Binary/${asset.variants?.["glTF-Binary"]}`; + const assetResponse = await fetch(`${parentUrl}/${path}`); + console.log(`Downloading ${parentUrl}/${path}`); + expect(assetResponse.ok).toBeTruthy(); + const arrayBuffer = await assetResponse.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + fs.mkdirSync( + `${__dirname}/testAssetDownloads/${downloadFolder}/${asset.name}/glTF-Binary`, + { recursive: true } + ); + fs.writeFileSync(`${__dirname}/testAssetDownloads/${downloadFolder}/${path}`, buffer); + asset.path = `${__dirname}/testAssetDownloads/${downloadFolder}/${path}`; + } + fs.writeFileSync( + `${__dirname}/testAssetDownloads/${downloadFolder}/test-index.json`, + JSON.stringify(data) + ); +}); diff --git a/tests/interactivityTests.spec.ts b/tests/interactivityTests.spec.ts new file mode 100644 index 00000000..c1cb1f12 --- /dev/null +++ b/tests/interactivityTests.spec.ts @@ -0,0 +1,113 @@ +import { expect } from "@playwright/test"; +import { test } from "./baseTestConfig"; +import { ResourceLoader } from "../source/ResourceLoader/resource_loader"; +import { GltfState } from "../source/GltfState/gltf_state"; +import { GltfView } from "../source/GltfView/gltf_view"; +import fs from "fs"; +import { dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +declare global { + interface Window { + resourceLoader: ResourceLoader; + state: GltfState; + view: GltfView; + TEST_TIME: number; + TEST_RESULT: boolean; + passTestData: (input: number | boolean) => void; + } +} + +const directories = fs.readdirSync(`${__dirname}/testAssetDownloads`); +for (const dir of directories) { + const configFile = `${__dirname}/testAssetDownloads/${dir}/test-index.json`; + if (!fs.existsSync(configFile)) { + continue; + } + const fileContents = fs.readFileSync(configFile, "utf-8"); + const testAssets = JSON.parse(fileContents); + for (const asset of testAssets) { + if (asset.path === undefined) { + continue; + } + const path = asset.path; + const file = new Uint8Array(fs.readFileSync(path)); + const testName = path.substring(path.lastIndexOf("/testAssetDownloads/") + 19); + createInteractivityTest(testName, file); + } +} + +function createInteractivityTest(name: string, file: Uint8Array) { + test(`Testing asset ${name}`, async ({ page }) => { + await page.goto(""); + let testDuration: number | undefined = undefined; + let testResult: boolean | undefined = undefined; + const fun = (input: number | boolean) => { + if (typeof input === "number") { + testDuration = input; + } else if (typeof input === "boolean") { + testResult = input; + } + }; + await page.exposeFunction("passTestData", fun); + let success = false; + try { + success = await page.evaluate(async (file) => { + const resourceLoader = window.resourceLoader as ResourceLoader; + const state = window.state as GltfState; + const glTF = await resourceLoader.loadGltf(file.buffer); + state.gltf = glTF; + const defaultScene = state.gltf.scene; + state.sceneIndex = defaultScene === undefined ? 0 : defaultScene; + state.cameraNodeIndex = undefined; + state.graphController.addCustomEventListener("test/onStart", (event) => { + window.passTestData(event.detail.expectedDuration); + window.TEST_TIME = event.detail.expectedDuration; + }); + state.graphController.addCustomEventListener("test/onSuccess", () => { + window.passTestData(true); + window.TEST_RESULT = true; + }); + state.graphController.addCustomEventListener("test/onFailed", () => { + window.passTestData(false); + window.TEST_RESULT = false; + }); + state.animationTimer.start(); + if (state.gltf?.extensions?.KHR_interactivity?.graphs !== undefined) { + state.graphController.initializeGraphs(state); + const graphIndex = state.gltf.extensions.KHR_interactivity.graph ?? 0; + state.graphController.loadGraph(graphIndex); + state.graphController.resumeGraph(); + } else { + state.graphController.stopGraphEngine(); + } + return true; + }, file); + } catch (error) { + console.log(await page.consoleMessages()); + throw error; + } + expect(success).toBeTruthy(); + await page.waitForFunction( + () => { + return window.TEST_TIME !== undefined; + }, + { timeout: 2000 } + ); + if (testDuration! > 0) { + console.log("Test duration (s): ", testDuration); + } + await page.waitForFunction( + () => { + return window.TEST_RESULT !== undefined; + }, + { timeout: testDuration! * 1000 + 1000 } + ); + if (testResult === false) { + console.log(await page.consoleMessages()); + } + expect(testResult).toBe(true); + }); +} diff --git a/tests/testApp/index.html b/tests/testApp/index.html new file mode 100644 index 00000000..d36b625c --- /dev/null +++ b/tests/testApp/index.html @@ -0,0 +1,20 @@ + + + + + glTF Sample Renderer Test Canvas + + + + + + + + + No Canvas! + + + + + + diff --git a/tests/testApp/main.js b/tests/testApp/main.js new file mode 100644 index 00000000..a9454d6e --- /dev/null +++ b/tests/testApp/main.js @@ -0,0 +1,19 @@ +import { GltfView } from "./gltf-viewer.module.js"; + +const canvas = document.getElementById("canvas"); +const context = canvas.getContext("webgl2", { antialias: true }); +const view = new GltfView(context); +const resourceLoader = view.createResourceLoader(); +const state = view.createState(); + +const update = () => { + view.renderFrame(state, canvas.width, canvas.height); + window.requestAnimationFrame(update); +}; + +// After this start executing animation loop. +window.requestAnimationFrame(update); + +globalThis.resourceLoader = resourceLoader; +globalThis.state = state; +globalThis.view = view;