From ec78d316bb31e9564adcd9b10dccffb2cbf632ba Mon Sep 17 00:00:00 2001 From: Alexandru Popovici Date: Tue, 7 May 2024 15:18:38 +0300 Subject: [PATCH] Viewer API Improvements (#2072) * Fix some monstrous bugs with index buffer shuffling now that we've changed our approach a bit.~ * Finished with the new material management approach for mesh batches. SelectionExtension now uses this approach and also considers existing material opacities when setting select and hover materials * Updated LineBatch to work with the new material management approach * Implemented the required draw range related changes to the point batch * Text batche now work with the new material management * SpeckleLineMaterial and SpecklePointsMaterial are now SpeckleMaterial as well. Had to rename two properties of SpeckleMaterial due to some typescript named property clash thing, but nothing really changed * Removed eslint-disable clauses in materials where they were no longer needed. Removed unused imports and overrides * Added the RTE define for some materials by default. It can still be overriden if users want to * Stencil outlines is now an toggle-able option for any SpeckleMaterial. Restricted to meshes * Implemented setting desired material for all geometry types via RenderMaterial and DisplayStyle data. SpeckleRenderer now has three overloaded setMaterial function. One for a material instance, one for a filer material and one for RenderData&DisplayStyle. Moved material hash related functionality from NodeRendeView into Materials * Added MaterialOptions which can be used when setting materials based on RenderMaterial/DisplayStyle to toggle various material features like stencilOutlines, pointSize. SelectionExtension now uses data material to set materials, and things are so much more simpler and nicer * Added public method for setting seletion extension options * After some profiling, realized three.js was doing a lot of pointless work each frame so now we're caching materials created from RenderMaterial/DisplayStyle to avoid this. Perf is nice and sharp now * addRenderTreeAsync is now a generator. Handled automatic zooming on viewer loading. Disabled section tool by default * Centralized RTE and shadow RTE buffer in an extended webglrenderer. This avoids re-computing rte data for each material over and over. Also, rte data is now centralized and available to materials * SpeckleMeshes now use a cached material clone as their batch material like any other material they use * Cleaned up Materils. Updated the debug show batches function use the new material manipulation system. So much easier now * Real time measurement exist now as a separate extension. Existing functionality preserved, besides one or two small additions. Renamed the MEASUREMENTS object layer to OVERLAY for a more generic usage. SelectionExtension can now be enabled/disabled. Added an additional overload to the setCameraView method in ICameraController which takes a box3 as target to focus the camera on * Removed viewer related events from input and replaced them everywhere with the proper input event types where required * Fixed two issues with the shadowcatcher. One was a regression introduced after we centralized the RTE data. The other was a super old one one and was essentially causing the shadowcatcher to generate the contact shadows incorrectly because the correct transform textures were not bound (this is three.js being a pain) * WIP on the filtering extension * FilteringExtension is done. Kept the same implementation * Added filter reset function * Removed uniform texture and batch count binding from each material's OnBeforeRender function. Additionallit our material override function from SpeckleMesh now uses our fast copy instead of three's copy function for materials. This decreased CPU overhead each frame by 20+% and also eliminated the ugly call to SpeckleMesh's function for updating the material with the transform texture and batch object count from each material's render callback which was not supposed to be there * Update RenderingStats to measure CPU render time per frame. * Added early and late update functions for extensions. First gets called before the core's update, and the latter afterwards * All speckle materials now use the centralized RTE data. Updated the fasts copy method to copy userdata defined uniforms where needed * Added explosion extension. Additional cleanup * Dirty transforms are marked on a per batch object basis, whenever their transforms gets changed. This automates the transform texture update execution, so we no longer need to manually mark entire batches as dirty * Added getObjects which returns all batch objects in the scene and getObjects which takes an rv an returns only the batch object associated with that rv. Both of these methods are availale on the renderer * Added setters for position, rotiation and scale in batch object * Diff extension is complete. We stuck with creating material instances and using those for coloring since it ensures maximum draw call efficiency. Fixed an issue in MeshBatch where transparent draw groups were not always sorted at the end of the group list leading to incorect opauque object selection during the depth pass * RV batch materials are obtained via the viewer-core API based on the RV itslef. This removed the need to get all batch materials for the differ * Small cleanup * Removed all circular dependencies besides one, like I predicted. The final one will dissapaear on it's own in the near future when we'll gracefull make DataTree obsolete. As a note, the circular dependencies were very shallow, reffering to enums/interfaces/statics declared in specific files. There was no real circular dependency on a class level * Removed last circular dependency just for the sake of completion * SpeckleRendere now has a clipping volume which is used internally to reject picks outside of it, and it's also exposed to the outside world to be used however * Implemented the SpeckleLoader along with it's abstract supertype. Data loading is now done through this loader which handles tree population with raw data as well as render view data. * Working minimal obj loader * Added total node counting and displaying * Viewer's load object now takes a loader of any speckle loader base class and uses that to load, instead of taking urls and tokens. This allows for any kind of loader to work with the vieweer's load function. Moved indexing of obj geometry to the obj geometry convertor. Loaders now take the target world tree instance instead of a viewer instance * Loaders can now load from string and array buffer data where implemented. ObjLoader can now load from a string payload. Sandbox can now load obj files from the UI using a file picker * alex/API2.0-core * Started on #1673. Fixed an issue with walkAsync where the recursive generator would waste too much time idle. * Solution for #1673. Replaced the old async pausing approach with a better version that has true variable wait time, and does not add additional dead wait time. Render tree building is now several times faster * Cut down some more on load time by using a lookup table for determined speckle types. For a very large number of objects, getting the actual speckle type was quite slow. For our reference stream with 1kk objects we cut down around 2 seconds of load time * BoxFromObjects now returns the correctly transformed aabb * Implemented optimisations for batch building step from the loading process. Reduced the step's time by around 50% * Implemented a NodeMap which allows us to search for nodes very very fast. * Added a dynamic pause in the loader which stops the converter from blocking. Paramater object types are not added as nodes anymore. Callback from converter is not without arguments * Replaced some internal walks with the newer and much faster id finding approach * Disable object shallow cloning acrss the speckle converter * Fixed the issues with block instances and revit instances caused by not duplicating speckle objects in the converter * getObjectPropertis is now async and slightly improved the execution time of the flatten function which it uses * Set caching to true by default * Fix for display style hashing * Implemented legacy viewer as a wrapper around the old viewer API * Started testing FE1 with API 2.0. Fixed some legacy issues. Also the camera controller extension now exposes it's underlying controls object for the sake of not messing around with unwatend changes in FE1 * Updated FE1 with API2.0 selection changes * Fixing selection bugs * The viewer now ignores duplicate id nodes * Fixes to object properties population, camera zooming and adding subtrees in core * Fixed some filtering issues. Added UpdateFlags to the viewer's requestRender function * Fixed an issue where section boxes were incorrectly added to the URL * Objects with no id are not given nodes into the tree * Updates to FE2 for API2.0. Also fixed a really really obscure bug in viewer-core where a material an incorrect material was set when reseting the batch to the default material * We now store separate node maps for each model loaded. Each node now holds it's subtree id (as a small number for memory considerations) * Render request after updating the visual diff * Fixed some missing update calls on shadows * Reverted FE1 changes and pinned the viewer library to a specific (latest) version pre-API2.0 * We're adding a viewer node for loaded models as subtree parent. This is because we're no longer spoofing ids and the model parent id needs to be preserved * Fixed an issue where clicking on a comment bubble made the pipeline use accumulation improperly, leading to dark blight * Null check for setMaterial. Fixed another case of dark blight. Hack required by frontend * Fixed the issue with filtering state not propagating in the FE * Updated selection event changes * Fixed an issue where an undefined subtree id would yield an incorrect render tree upon requesting it * Fixed an issue where undefined nodes were returned as valid when searching of ids * NodeRenderView now holds it's subtree id along the speckle id. This allows for precise node matching when looking for specific nodes * Fixed an issue with BlockInstances not instancing underlying meshes from breps * Some fixes to diffing. Some older than API2.0, some new. Render views now have guid composed of their id and subtreeid. * Update node render view id with guid where needed * Unload function now checks for requested resource to unload before trying to unload it * Check for the existence of a batch before applying draw ranges. Inthe FE, extension can temporarily keep dead rvs in their state when switching between streams leading to errors * Fixed an issue for filtering where subtree roots would cause incorrect rv additions to the ghosted rv lists * Unified block instance and revit instance conversion implementation * Separted instanced from non-instanced rvs. Working on InstancedMeshBatch * WIP on instanced types * Implemented visual for box draging * BoxSelection extension beautified and documented along with some small but welcomed changes to the viewer-core * Viewer's getExtension now looks in the prototype chain before returning an undefined extension * Added the extended Selection extension here for possible later use * First iteration on instancing. General idea works. Still WIP on several fronts * Fixed issues related to incorrect transform being calculated for instanced geometry. Fixed an issue with incorrect bounds being calculated * Fixed a few issues with instanced vs non instanced render view gathering * Disabled box selection extension * Fixed some linter errors * Disabled a lint 'error' * Fixed issues with depth rendering and instanced objects and fixed draw range visibility for instanced batches in a minimalistic way * Updated the measurements extension with the visiblity option * Restructed a bit our implementation for the acceleration structure because now the BLAS needs to aggregate an instance of three-mesh-bvh not extend it. So that instanced batch objects can share a single bvh instance -> no redundancy * WIP * Revert "WIP" This reverts commit 20d4bbf6210b0d37b956cc5b41fdb06f29845b4f. * More WIP on trying to make instanced geometry TAS and BAS work properly * Both Tas and Bas intersection testing seem to working fine. Now we need to implemented draw grouping for instanced mesh batches * Added draw group management to InstancedMeshBatch. It works in the same manner as for non instanced batches, but the offset value refers to instances not triangles. I believe it could be simplified, but I'd like to get it up and working first * Draw groups need to hold the index and count from the instanced buffer attribute * Added array shuffling to the instanced mesh batch using the same approach we used for the non instanced one. * Instanced batches now dynamically add InstancedMesh objects based on draw groups and a computed transform buffer * Applying draw range updates for instanced batches now works. Still some issues to handle. It aint much, but it's honest work * Disabled some more RTE until setting visibility for draw ranges is finished * Moved getting opaque, transparent and stencil draw ranges out from Batcher and on to a per-batch basis. Now instanced batches correctly apply draw ranges and get their opaque, transparent and stecil ranges * Fixed an issue with setting the visibility ranges for instanced mesh batches * Instanced attributes for instanced meshes now no longer allocate * Shadow depth material for instances is now set in the speckle renderer * R-enabled RTE globally. Made instancing work with RTE. Made instancing work with both RTE and shadowmapping. * Fixed an issue with materials building up in the mesh batch's cache incorrectly * Implemented gradient indexs attributes for instanced batches. Thismeans, any color ramp based material like the ones we use for filtering now works * Changed the way compound ids are created for instances, so that less memory is required. i.e chrome is not crashing anymore on particular streams * Implemented reseting the draw ranges for instanced mesh batches. We no longer double buffer the gradient index buffer. We just create a new one when shuffling, populate it, then copy it over at the end. We're still double buffering the transforms buffer since that one is larger and we might not want to allocate it each time we shuffle * Removed references of draw groups with ids since we're not doing that anymore. Fixed an issue with mesh batches where the material cache would keep piling materials incorrectly * Several issues with selective transformations on instanced batches fixed. * Added default null materials for instanced meshes both with and without vertex colors * Got rid of the patched InstancedMesh because it was ridiculously slow. Instead we're now computing the scene box using our acceleration structures where available and three.s boundingbox where not available * Minor, yet big regression fix * Fixed regression * Exported some extra types * WIP on instanced balancing * Instanced objects under a certain threshold now get batched together as regular mesh batches * Forgot to update the rvs aabb * Unified instanced and non instanced batch creation. Instances which do not qualify for instanced batching anre now mixed together with the rest of the non instanced batched objects * Added some timing information to instanced batches * Fixed an issue with zooming in on objects not working in the selection extension. Fixed an issue with object picking failing due to fp precision for objects right at the edge of the clipping volume. * Removed logs * Update stream moving via UI * Removed the priority argument from loadObject since it's not needed anymore * Updated LegacyViewer * Fixed an issue with API 2.0 where the legacy form of transforms with only an array of values as the matrix would not work * Updated FE1 viewer package to latest before API 2.0 * Disabled selection when measurement mode is on * Updated ibl params updating * Fixed an issue with measurement text not showing up * Logs * lockfile * Made DataTree obsolete. Removed unused 'input' property from viewer * Fixed circular dependencies * Removed DebugViewer, other small changes * Small changes * Small fixes * Added id to NodeData * Removed unused bounds property in rendertree * Notes to future self + ExtendedExtension is now exported * Removed an unused function from renderer. Removed the parent argumetn from getRenderViewerForNode... since it was legacy and has no meaning anymore. * Removed the instance type check in getRenderViewesForNode method. Should not be needed anymore and it was bad to begin with anyway * Removed some test code * Small changes * Removed pointless bounds calculation. Added note * Added return types * Ingestigating large group operations * Nested nodes from TreeNode are now optional. Small update to node render view. * #1818 Remove the concept of speckle data existing behind a 'data' field * Removed unneeded property * made arguments options in transformTRS * getBoundingBox from acceleration structures have an optional argument now * Several Batch methods no longer take variable numbe of arguments, but arrays now * Added note * Note * Removed unused things * Note and made the raycaster protected in intersections * Added some typings * Replaceds some functions with accessors. Added typings. Renamed stuff * Removed some unused properties. * Typings for measurements * Small typing changes * Fixed some more compile errors * Sources from 'batching' folder are now strict compile compliant * Sources from 'materials' folder are now strict compile compliant * Sources from 'extensions' folder are now strict compile compliant * Sources from 'tree' folder are now strict compile compliant * Viewer interface, implementation, legacy implementation and the exports now comply with the strict compilation flag. Also added the new tsconfig * Sources from 'objects' folder now comply to the strict compilation rules * Sources from 'pipeline' now satisfy the requirements for strict compilation * Sources from 'loaders' folder are now compiling with strict. That was so much fun * Sources from 'query' folder are now compiling with strict * Another round of correction triggered by previous changes * Update the declaration file in the object loader to contain a member that needs to be public * SpeckleRenderer along with the rest of the surces from the root 'modules' folder are not compiled with strict * Completely deprecated DataTree. Updated the dependencies with @types/underscore. Fixed remaining compilation issues * Fixed a failing build on the CI regarding a timeout id * Fixed compile errors from sandbox * Another fix * All EventEmiter child classes now have mapped event handler argument types, so that when attaching to a specific event, the provided handler has the correct types for it's arguments. Implicitly, got rid of the the unknown types in all event handlers. * Disabled verbatimModuleSyntax because it was messing things up. Fixed an issue with an improper import * Fixed frontend-2 linting errors. Also added all the event payload maps to the viewer export * Some good additions but mostly typescript catering * Some more typescript catering, but also something useful. The intersect function is now overloaded so that when you specify only the ObjectLayers.STREAM_CONTENT_MESH object layer in it's layers argument it will always return a ExtendedMeshIntersection which guarantees to have a batchObject, face and it's object is of type SpeckleMesh | SpeckleInstancedMesh. Generally you mostly raycast against meshes and getting a three.js intersection object which has all it's fileds optional led to some very useless defined checks. With this we can avoid all of them * Continued from yesterday, finished with the changes in intersections. Added MeshIntersection which is returned by all bvh intersections. This eliminates the need to check for face, faceIndex or index on intersection results from intersection meshes. Groups from MeshBatch and InstancedMeshBatch are now always DrawRanges(they always were) * Mostly catering to typescript * Removed underscore and all unused dependancies. Fixed remaining lint and build errors * Minor changes * Added the no-non-null assertion rule to the sandbox * getExtension never returns null, but rather throws an exception if requested extension does not exist. Added hasExtension for (theoretical) situations where you want to check for extension existence but don't want to go by try/catch with getExtension. Fixed remaining lint/build errors * getBatches now returns explicit batch types based on the geometry type that you provide * Adding the lockfile * undoing unnecessary FE2 changes * Merged viewer/fe SpeckleObject types * Fixed two small issues * Minor linting issues --------- Co-authored-by: Kristaps Fabians Geikins --- .../components/viewer/explorer/TreeItem.vue | 2 +- .../components/viewer/selection/Object.vue | 2 +- .../components/viewer/settings/Menu.vue | 4 +- .../lib/common/helpers/sceneExplorer.ts | 15 +- .../lib/viewer/composables/setup/postSetup.ts | 3 +- .../frontend-2/lib/viewer/composables/ui.ts | 6 +- .../lib/viewer/composables/viewer.ts | 12 +- packages/objectloader/types/index.d.ts | 2 +- packages/viewer-sandbox/.eslintrc.js | 3 + .../src/Extensions/BoxSelection.ts | 25 +- .../src/Extensions/CameraPlanes.ts | 16 +- .../src/Extensions/ExtendedSelection.ts | 8 +- packages/viewer-sandbox/src/Sandbox.ts | 103 ++--- packages/viewer-sandbox/src/main-multi.ts | 32 +- packages/viewer-sandbox/src/main.ts | 25 +- packages/viewer/.eslintrc.cjs | 3 +- packages/viewer/package.json | 6 +- packages/viewer/src/IViewer.ts | 57 ++- packages/viewer/src/helpers/flatten.ts | 4 +- packages/viewer/src/index.ts | 58 +-- packages/viewer/src/modules/Assets.ts | 22 +- packages/viewer/src/modules/DebugViewer.ts | 8 - packages/viewer/src/modules/Intersections.ts | 125 ++++-- packages/viewer/src/modules/LegacyViewer.ts | 138 ++++--- packages/viewer/src/modules/Shadowcatcher.ts | 17 +- .../viewer/src/modules/SpeckleRenderer.ts | 328 +++++++++------- packages/viewer/src/modules/UrlHelper.ts | 12 +- packages/viewer/src/modules/Viewer.ts | 132 ++++--- packages/viewer/src/modules/batching/Batch.ts | 40 +- .../src/modules/batching/BatchObject.ts | 35 +- .../viewer/src/modules/batching/Batcher.ts | 152 ++++--- .../viewer/src/modules/batching/DrawRanges.ts | 14 +- .../modules/batching/InstancedBatchObject.ts | 13 +- .../modules/batching/InstancedMeshBatch.ts | 172 ++++---- .../viewer/src/modules/batching/LineBatch.ts | 73 ++-- .../viewer/src/modules/batching/MeshBatch.ts | 70 ++-- .../viewer/src/modules/batching/PointBatch.ts | 58 ++- .../src/modules/batching/PrimitiveBatch.ts | 168 ++++---- .../viewer/src/modules/batching/TextBatch.ts | 35 +- .../viewer/src/modules/converter/Geometry.ts | 94 +++-- .../modules/extensions/CameraController.ts | 179 +++++---- .../src/modules/extensions/DiffExtension.ts | 370 ++++++++++-------- .../src/modules/extensions/Extension.ts | 7 +- .../modules/extensions/FilteringExtension.ts | 135 ++++--- .../src/modules/extensions/SectionOutlines.ts | 32 +- .../src/modules/extensions/SectionTool.ts | 86 ++-- .../modules/extensions/SelectionExtension.ts | 121 +++--- .../extensions/measurements/Measurement.ts | 33 +- .../measurements/MeasurementPointGizmo.ts | 65 ++- .../measurements/MeasurementsExtension.ts | 179 +++++---- .../measurements/PerpendicularMeasurement.ts | 65 +-- .../measurements/PointToPointMeasurement.ts | 54 +-- .../src/modules/filtering/PropertyManager.ts | 31 +- packages/viewer/src/modules/input/Input.ts | 61 +-- .../src/modules/loaders/GeometryConverter.ts | 6 +- packages/viewer/src/modules/loaders/Loader.ts | 27 +- .../src/modules/loaders/OBJ/ObjConverter.ts | 33 +- .../loaders/OBJ/ObjGeometryConverter.ts | 14 +- .../src/modules/loaders/OBJ/ObjLoader.ts | 22 +- .../loaders/Speckle/SpeckleConverter.ts | 203 ++++++---- .../Speckle/SpeckleGeometryConverter.ts | 103 +++-- .../modules/loaders/Speckle/SpeckleLoader.ts | 32 +- .../viewer/src/modules/materials/Materials.ts | 117 +++--- .../modules/materials/SpeckleBasicMaterial.ts | 36 +- .../modules/materials/SpeckleDepthMaterial.ts | 34 +- .../materials/SpeckleDisplaceMaterial.ts | 6 +- .../modules/materials/SpeckleGhostMaterial.ts | 6 +- .../modules/materials/SpeckleLineMaterial.ts | 32 +- .../src/modules/materials/SpeckleMaterial.ts | 100 +++-- .../materials/SpeckleNormalMaterial.ts | 29 +- .../materials/SpecklePointColouredMaterial.ts | 123 ++---- .../modules/materials/SpecklePointMaterial.ts | 30 +- .../materials/SpeckleShadowcatcherMaterial.ts | 6 +- .../SpeckleStandardColoredMaterial.ts | 6 +- .../materials/SpeckleStandardMaterial.ts | 37 +- .../modules/materials/SpeckleTextMaterial.ts | 34 +- .../modules/objects/AccelerationStructure.ts | 45 ++- .../objects/RotatablePMREMGenerator.ts | 10 +- .../src/modules/objects/SpeckleCamera.ts | 22 +- .../modules/objects/SpeckleCameraControls.ts | 78 ++-- .../modules/objects/SpeckleInstancedMesh.ts | 123 +++--- .../viewer/src/modules/objects/SpeckleMesh.ts | 125 +++--- .../src/modules/objects/SpeckleRaycaster.ts | 50 ++- .../viewer/src/modules/objects/SpeckleText.ts | 33 +- .../modules/objects/SpeckleWebGLRenderer.ts | 2 +- .../objects/TopLevelAccelerationStructure.ts | 87 ++-- .../src/modules/pipeline/ApplyAOPass.ts | 18 +- .../viewer/src/modules/pipeline/ColorPass.ts | 44 ++- .../src/modules/pipeline/CopyOutputPass.ts | 16 +- .../viewer/src/modules/pipeline/DepthPass.ts | 38 +- .../src/modules/pipeline/DynamicAOPass.ts | 26 +- .../src/modules/pipeline/NormalsPass.ts | 24 +- .../src/modules/pipeline/OverlayPass.ts | 30 +- .../viewer/src/modules/pipeline/Pipeline.ts | 74 ++-- .../src/modules/pipeline/ShadowcatcherPass.ts | 34 +- .../src/modules/pipeline/SpecklePass.ts | 34 +- .../src/modules/pipeline/StaticAOPass.ts | 17 +- .../src/modules/pipeline/StencilMaskPass.ts | 28 +- .../src/modules/pipeline/StencilPass.ts | 29 +- .../queries/IntersectionQuerySolver.ts | 41 +- .../src/modules/queries/PointQuerySolver.ts | 16 +- .../viewer/src/modules/queries/Queries.ts | 2 +- packages/viewer/src/modules/tree/DataTree.ts | 71 ---- packages/viewer/src/modules/tree/NodeMap.ts | 15 +- .../viewer/src/modules/tree/NodeRenderView.ts | 81 ++-- .../viewer/src/modules/tree/RenderTree.ts | 98 ++--- packages/viewer/src/modules/tree/WorldTree.ts | 50 ++- packages/viewer/tsconfig.json | 11 +- yarn.lock | 13 +- 109 files changed, 3405 insertions(+), 2526 deletions(-) delete mode 100644 packages/viewer/src/modules/DebugViewer.ts delete mode 100644 packages/viewer/src/modules/tree/DataTree.ts diff --git a/packages/frontend-2/components/viewer/explorer/TreeItem.vue b/packages/frontend-2/components/viewer/explorer/TreeItem.vue index 0a9f2dcdb5..6978a7fe2b 100644 --- a/packages/frontend-2/components/viewer/explorer/TreeItem.vue +++ b/packages/frontend-2/components/viewer/explorer/TreeItem.vue @@ -181,7 +181,7 @@ const { hideObjects, showObjects, isolateObjects, unIsolateObjects } = const isAtomic = computed(() => props.treeItem.atomic === true) const speckleData = props.treeItem?.raw as SpeckleObject -const rawSpeckleData = props.treeItem?.raw as Record +const rawSpeckleData = props.treeItem?.raw as SpeckleObject const headerAndSubheader = computed(() => { return getHeaderAndSubheaderForSpeckleObject(rawSpeckleData) diff --git a/packages/frontend-2/components/viewer/selection/Object.vue b/packages/frontend-2/components/viewer/selection/Object.vue index 36719ad9d6..552259e4d6 100644 --- a/packages/frontend-2/components/viewer/selection/Object.vue +++ b/packages/frontend-2/components/viewer/selection/Object.vue @@ -77,7 +77,7 @@ class="pl-2" > diff --git a/packages/frontend-2/components/viewer/settings/Menu.vue b/packages/frontend-2/components/viewer/settings/Menu.vue index 3c174ff2f2..fd4b35ccce 100644 --- a/packages/frontend-2/components/viewer/settings/Menu.vue +++ b/packages/frontend-2/components/viewer/settings/Menu.vue @@ -29,8 +29,8 @@ const localViewerSettings = useSynchronizedCookie( const { instance } = useInjectedViewer() const setViewerCameraHandlerControlsMaxPolarAngle = (angle: number) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - instance.getExtension(CameraController).controls.maxPolarAngle = angle + const extension = instance.getExtension(CameraController) + if (extension) extension.controls.maxPolarAngle = angle } const toggleTurntableMode = () => { diff --git a/packages/frontend-2/lib/common/helpers/sceneExplorer.ts b/packages/frontend-2/lib/common/helpers/sceneExplorer.ts index 873f43fb30..aea5d027bf 100644 --- a/packages/frontend-2/lib/common/helpers/sceneExplorer.ts +++ b/packages/frontend-2/lib/common/helpers/sceneExplorer.ts @@ -1,3 +1,5 @@ +import { type SpeckleObject, type SpeckleReference } from '@speckle/viewer' + // Note: minor typing hacks for less squiggly lines in the explorer. // TODO: ask alex re viewer data tree types @@ -10,15 +12,4 @@ export type ExplorerNode = { children: ExplorerNode[] } -export type SpeckleReference = { - referencedId: string -} - -export interface SpeckleObject { - id?: string - elements?: SpeckleReference[] - children?: SpeckleObject[] | SpeckleReference[] - name?: string - speckle_type?: string - [key: string]: unknown -} +export type { SpeckleObject, SpeckleReference } diff --git a/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts b/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts index 714bdf9846..9304b83d25 100644 --- a/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts +++ b/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts @@ -306,13 +306,14 @@ function useViewerCameraIntegration() { const loadCameraDataFromViewer = () => { const controls = instance.getExtension(CameraController).controls + let cameraManuallyChanged = false + const viewerPos = new Vector3() const viewerTarget = new Vector3() controls.getPosition(viewerPos) controls.getTarget(viewerTarget) - let cameraManuallyChanged = false if (!areVectorsLooselyEqual(position.value, viewerPos)) { if (hasInitialLoadFired.value) position.value = viewerPos.clone() cameraManuallyChanged = true diff --git a/packages/frontend-2/lib/viewer/composables/ui.ts b/packages/frontend-2/lib/viewer/composables/ui.ts index 1586dffb46..8b389edbe9 100644 --- a/packages/frontend-2/lib/viewer/composables/ui.ts +++ b/packages/frontend-2/lib/viewer/composables/ui.ts @@ -1,4 +1,4 @@ -import { SpeckleViewer, timeoutAt, type Optional } from '@speckle/shared' +import { SpeckleViewer, timeoutAt } from '@speckle/shared' import type { TreeNode } from '@speckle/viewer' import { CameraController, MeasurementsExtension } from '@speckle/viewer' import type { MeasurementOptions, PropertyInfo } from '@speckle/viewer' @@ -64,14 +64,14 @@ export function useCameraUtilities() { instance.setView(...args) } - let cameraController: Optional = undefined + let cameraController: CameraController | null = null const truck = ( ...args: Parameters['controls']['truck']> ) => { if (!cameraController) { cameraController = instance.getExtension(CameraController) } - cameraController.controls.truck(...args) + cameraController?.controls.truck(...args) } const zoomExtentsOrSelection = () => { diff --git a/packages/frontend-2/lib/viewer/composables/viewer.ts b/packages/frontend-2/lib/viewer/composables/viewer.ts index 52fa313440..4eeb3a4bf7 100644 --- a/packages/frontend-2/lib/viewer/composables/viewer.ts +++ b/packages/frontend-2/lib/viewer/composables/viewer.ts @@ -11,7 +11,7 @@ import { TimeoutError, timeoutAt } from '@speckle/shared' import type { MaybeAsync, Nullable } from '@speckle/shared' import { Vector3 } from 'three' import { areVectorsLooselyEqual } from '~~/lib/viewer/helpers/three' -import { CameraController } from '@speckle/viewer' +import { CameraController, type ViewerEventPayload } from '@speckle/viewer' import type { TreeNode } from '@speckle/viewer' import type { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer' @@ -78,9 +78,9 @@ function getFirstVisibleSelectionHit( } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function useViewerEventListener( - name: ViewerEvent | ViewerEvent[], - listener: (...args: A[]) => MaybeAsync, +export function useViewerEventListener( + name: K | K[], + listener: (args: ViewerEventPayload[K]) => MaybeAsync, options?: Partial<{ state: InitialStateWithRequestAndResponse }> @@ -135,7 +135,8 @@ export function useViewerCameraTracker( } // Only invoke callback if position/target changed in a meaningful way - const controls = instance.getExtension(CameraController).controls + const extension = instance.getExtension(CameraController) + const controls = extension.controls const viewerPos = new Vector3() const viewerTarget = new Vector3() @@ -165,7 +166,6 @@ export function useViewerCameraTracker( onMounted(() => { const extension = instance.getExtension(CameraController) - extension.controls.addEventListener('update', finalCallback) }) diff --git a/packages/objectloader/types/index.d.ts b/packages/objectloader/types/index.d.ts index 828cb5e58a..80c401ed8d 100644 --- a/packages/objectloader/types/index.d.ts +++ b/packages/objectloader/types/index.d.ts @@ -46,6 +46,6 @@ export default class ObjectLoader { ): SpeckleObject | SpeckleObject[] async *getObjectIterator(): Generator - + async getObject(id: string): Promise> dispose(): void } diff --git a/packages/viewer-sandbox/.eslintrc.js b/packages/viewer-sandbox/.eslintrc.js index 5a57524a34..6d9efc2ad8 100644 --- a/packages/viewer-sandbox/.eslintrc.js +++ b/packages/viewer-sandbox/.eslintrc.js @@ -11,6 +11,9 @@ const config = { parserOptions: { sourceType: 'module' }, + rules: { + '@typescript-eslint/no-non-null-assertion': 'error' + }, overrides: [ { files: '*.ts', diff --git a/packages/viewer-sandbox/src/Extensions/BoxSelection.ts b/packages/viewer-sandbox/src/Extensions/BoxSelection.ts index d390115247..ee369b7624 100644 --- a/packages/viewer-sandbox/src/Extensions/BoxSelection.ts +++ b/packages/viewer-sandbox/src/Extensions/BoxSelection.ts @@ -4,13 +4,7 @@ import { ObjectLayers } from '@speckle/viewer' import { NodeRenderView } from '@speckle/viewer' import { SelectionExtension } from '@speckle/viewer' import { BatchObject } from '@speckle/viewer' -import { - Extension, - IViewer, - GeometryType, - MeshBatch, - CameraController -} from '@speckle/viewer' +import { Extension, IViewer, GeometryType, CameraController } from '@speckle/viewer' import { Matrix4, ShaderMaterial, @@ -113,17 +107,18 @@ export class BoxSelection extends Extension { /** Gets the object ids that fall withing the provided selection box */ private getSelectionIds(selectionBox: Box3) { + /** Get the renderer */ + const renderer = this.viewer.getRenderer() /** Get the mesh batches */ - const batches = this.viewer - .getRenderer() - .batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[] - + const batches = renderer.batcher.getBatches(undefined, GeometryType.MESH) /** Compute the clip matrix */ const clipMatrix = new Matrix4() - clipMatrix.multiplyMatrices( - this.viewer.getRenderer().renderingCamera.projectionMatrix, - this.viewer.getRenderer().renderingCamera.matrixWorldInverse - ) + if (renderer.renderingCamera) { + clipMatrix.multiplyMatrices( + renderer.renderingCamera.projectionMatrix, + renderer.renderingCamera.matrixWorldInverse + ) + } /** We're using three-mesh-bvh library for out BVH * Go over each batch and test it against the TAS only. diff --git a/packages/viewer-sandbox/src/Extensions/CameraPlanes.ts b/packages/viewer-sandbox/src/Extensions/CameraPlanes.ts index 7ed3378368..0815712eb8 100644 --- a/packages/viewer-sandbox/src/Extensions/CameraPlanes.ts +++ b/packages/viewer-sandbox/src/Extensions/CameraPlanes.ts @@ -3,7 +3,6 @@ import { Extension, GeometryType, IViewer, - MeshBatch, Vector3 } from '@speckle/viewer' import { PerspectiveCamera } from 'three' @@ -20,9 +19,7 @@ export class CameraPlanes extends Extension { public constructor(viewer: IViewer) { super(viewer) - this.camerController = viewer.getExtension( - CameraController as new () => CameraController - ) + this.camerController = viewer.getExtension(CameraController) as CameraController } public onEarlyUpdate(): void { @@ -30,7 +27,10 @@ export class CameraPlanes extends Extension { } public computePerspectiveCameraPlanes() { - const camera = this.viewer.getRenderer().renderingCamera as PerspectiveCamera + const renderer = this.viewer.getRenderer() + if (!renderer.renderingCamera) return + + const camera = renderer.renderingCamera as PerspectiveCamera const minDist = this.getClosestGeometryDistance(camera) if (minDist === Number.POSITIVE_INFINITY) return @@ -42,7 +42,7 @@ export class CameraPlanes extends Extension { 1 + Math.pow(Math.tan(((fov / 180) * Math.PI) / 2), 2) * (Math.pow(aspect, 2) + 1) ) - this.viewer.getRenderer().renderingCamera.near = nearPlane + renderer.renderingCamera.near = nearPlane console.log(minDist, nearPlane) } @@ -55,11 +55,13 @@ export class CameraPlanes extends Extension { const batches = this.viewer .getRenderer() - .batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[] + .batcher.getBatches(undefined, GeometryType.MESH) let minDist = Number.POSITIVE_INFINITY const minPoint = new Vector3() for (let b = 0; b < batches.length; b++) { const result = batches[b].mesh.TAS.closestPointToPoint(cameraPosition) + if (!result) continue + const planarity = cameraDir.dot( new Vector3().subVectors(result.point, cameraPosition).normalize() ) diff --git a/packages/viewer-sandbox/src/Extensions/ExtendedSelection.ts b/packages/viewer-sandbox/src/Extensions/ExtendedSelection.ts index b49c61b318..6777802a32 100644 --- a/packages/viewer-sandbox/src/Extensions/ExtendedSelection.ts +++ b/packages/viewer-sandbox/src/Extensions/ExtendedSelection.ts @@ -34,10 +34,14 @@ export class ExtendedSelection extends SelectionExtension { } private initGizmo() { + const rendeder = this.viewer.getRenderer() + if (!rendeder.renderingCamera) + throw new Error('Cannot use ExtendedSelection without a rendering camera') + /** Create a new TransformControls gizmo */ this.transformControls = new TransformControls( - this.viewer.getRenderer().renderingCamera, - this.viewer.getRenderer().renderer.domElement + rendeder.renderingCamera, + rendeder.renderer.domElement ) /** The gizmo creates an entire hierarchy of children internally, * and three.js objects do not inherit parent layer values, so diff --git a/packages/viewer-sandbox/src/Sandbox.ts b/packages/viewer-sandbox/src/Sandbox.ts index 1d941b1ca4..fd06d03c94 100644 --- a/packages/viewer-sandbox/src/Sandbox.ts +++ b/packages/viewer-sandbox/src/Sandbox.ts @@ -2,7 +2,7 @@ import { Box3, SectionTool, SpeckleStandardMaterial, TreeNode } from '@speckle/viewer' import { CanonicalView, - DebugViewer, + Viewer, PropertyInfo, SelectionEvent, SunLightConfiguration, @@ -14,7 +14,8 @@ import { DiffExtension, SpeckleLoader, ObjLoader, - UrlHelper + UrlHelper, + LoaderEvent } from '@speckle/viewer' import { FolderApi, Pane } from 'tweakpane' import { DiffResult } from '@speckle/viewer' @@ -25,7 +26,7 @@ import { FilteringExtension } from '@speckle/viewer' import { MeasurementsExtension } from '@speckle/viewer' import { CameraController } from '@speckle/viewer' import { UpdateFlags } from '@speckle/viewer' -import { Viewer, AssetType, Assets } from '@speckle/viewer' +import { AssetType, Assets } from '@speckle/viewer' import Neutral from '../assets/hdri/Neutral.png' import Mild from '../assets/hdri/Mild.png' import Mild2 from '../assets/hdri/Mild2.png' @@ -133,7 +134,7 @@ export default class Sandbox { public constructor( container: HTMLElement, - viewer: DebugViewer, + viewer: Viewer, selectionList: SelectionEvent[] ) { this.viewer = viewer @@ -164,20 +165,17 @@ export default class Sandbox { this.batchesParams.totalBvhSize = this.getBVHSize() this.refresh() }) - viewer.on(ViewerEvent.UnloadComplete, async (url: string) => { - url + viewer.on(ViewerEvent.UnloadComplete, async () => { this.removeViewControls() this.addViewControls() this.properties = await this.viewer.getObjectProperties() }) - viewer.on(ViewerEvent.UnloadAllComplete, async (url: string) => { + viewer.on(ViewerEvent.UnloadAllComplete, async () => { this.removeViewControls() this.addViewControls() this.properties = await this.viewer.getObjectProperties() - // viewer.World.resetWorld() - url }) - viewer.on(ViewerEvent.ObjectClicked, (selectionEvent: SelectionEvent) => { + viewer.on(ViewerEvent.ObjectClicked, (selectionEvent) => { if (selectionEvent && selectionEvent.hits) { const firstHitNode = selectionEvent.hits[0].node if (firstHitNode) { @@ -218,13 +216,14 @@ export default class Sandbox { }) const position = { value: { x: 0, y: 0, z: 0 } } folder.addInput(position, 'value', { label: 'Position' }).on('change', () => { - const rvs = this.viewer - .getWorldTree() - .getRenderTree(url) - .getRenderViewsForNodeId(url) - for (let k = 0; k < rvs.length; k++) { - const object = this.viewer.getRenderer().getObject(rvs[k]) - object.transformTRS(position.value, undefined, undefined, undefined) + const tree = this.viewer.getWorldTree() + const rvs = tree.getRenderTree(url)?.getRenderViewsForNodeId(url) + if (rvs) { + for (let k = 0; k < rvs.length; k++) { + const object = this.viewer.getRenderer().getObject(rvs[k]) + if (object) + object.transformTRS(position.value, undefined, undefined, undefined) + } } this.viewer.requestRender(UpdateFlags.RENDER | UpdateFlags.SHADOWS) this.viewer.getRenderer().updateShadowCatcher() @@ -276,10 +275,7 @@ export default class Sandbox { title: `Object: ${node.model.id}` }) - const rvs = this.viewer - .getWorldTree() - .getRenderTree() - .getRenderViewsForNode(node, node) + const rvs = this.viewer.getWorldTree().getRenderTree().getRenderViewsForNode(node) const objects: BatchObject[] = [] for (let k = 0; k < rvs.length; k++) { const batchObject = this.viewer.getRenderer().getObject(rvs[k]) @@ -344,7 +340,7 @@ export default class Sandbox { .on('change', () => { const unionBox: Box3 = new Box3() objects.forEach((obj: BatchObject) => { - unionBox.union(obj.renderView.aabb) + unionBox.union(obj.renderView.aabb || new Box3()) }) const origin = unionBox.getCenter(new Vector3()) objects.forEach((obj: BatchObject) => { @@ -577,7 +573,7 @@ export default class Sandbox { .on('change', () => { const batches = this.viewer .getRenderer() - .batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[] + .batcher.getBatches(undefined, GeometryType.MESH) batches.forEach((batch: MeshBatch) => { const materials = batch.materials as SpeckleStandardMaterial[] materials.forEach((material: SpeckleStandardMaterial) => { @@ -595,7 +591,7 @@ export default class Sandbox { .on('change', () => { const batches = this.viewer .getRenderer() - .batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[] + .batcher.getBatches(undefined, GeometryType.MESH) batches.forEach((batch: MeshBatch) => { const materials = batch.materials as SpeckleStandardMaterial[] materials.forEach((material: SpeckleStandardMaterial) => { @@ -617,7 +613,7 @@ export default class Sandbox { .on('change', () => { const batches = this.viewer .getRenderer() - .batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[] + .batcher.getBatches(undefined, GeometryType.MESH) batches.forEach((batch: MeshBatch) => { const materials = batch.materials as SpeckleStandardMaterial[] materials.forEach((material: SpeckleStandardMaterial) => { @@ -947,6 +943,13 @@ export default class Sandbox { this.viewer.setLightConfiguration(this.lightParams) }) + const updateShadowcatcher = () => { + const shadowCatcher = this.viewer.getRenderer().shadowcatcher + if (shadowCatcher) { + shadowCatcher.configuration = this.shadowCatcherParams + this.viewer.getRenderer().updateShadowCatcher() + } + } shadowcatcherFolder .addInput(this.shadowCatcherParams, 'textureSize', { label: 'Texture Size', @@ -954,10 +957,8 @@ export default class Sandbox { max: 1024, step: 1 }) - .on('change', (value) => { - value - this.viewer.getRenderer().shadowcatcher.configuration = this.shadowCatcherParams - this.viewer.getRenderer().updateShadowCatcher() + .on('change', () => { + updateShadowcatcher() }) shadowcatcherFolder .addInput(this.shadowCatcherParams, 'weights', { @@ -967,10 +968,8 @@ export default class Sandbox { z: { min: -100, max: 100 }, w: { min: -100, max: 100 } }) - .on('change', (value) => { - value - this.viewer.getRenderer().shadowcatcher.configuration = this.shadowCatcherParams - this.viewer.getRenderer().updateShadowCatcher() + .on('change', () => { + updateShadowcatcher() }) shadowcatcherFolder .addInput(this.shadowCatcherParams, 'blurRadius', { @@ -979,10 +978,8 @@ export default class Sandbox { max: 128, step: 1 }) - .on('change', (value) => { - value - this.viewer.getRenderer().shadowcatcher.configuration = this.shadowCatcherParams - this.viewer.getRenderer().updateShadowCatcher() + .on('change', () => { + updateShadowcatcher() }) shadowcatcherFolder .addInput(this.shadowCatcherParams, 'stdDeviation', { @@ -991,10 +988,8 @@ export default class Sandbox { max: 128, step: 1 }) - .on('change', (value) => { - value - this.viewer.getRenderer().shadowcatcher.configuration = this.shadowCatcherParams - this.viewer.getRenderer().updateShadowCatcher() + .on('change', () => { + updateShadowcatcher() }) shadowcatcherFolder .addInput(this.shadowCatcherParams, 'sigmoidRange', { @@ -1003,10 +998,8 @@ export default class Sandbox { max: 10, step: 0.1 }) - .on('change', (value) => { - value - this.viewer.getRenderer().shadowcatcher.configuration = this.shadowCatcherParams - this.viewer.getRenderer().updateShadowCatcher() + .on('change', () => { + updateShadowcatcher() }) shadowcatcherFolder .addInput(this.shadowCatcherParams, 'sigmoidStrength', { @@ -1015,10 +1008,8 @@ export default class Sandbox { max: 10, step: 0.1 }) - .on('change', (value) => { - value - this.viewer.getRenderer().shadowcatcher.configuration = this.shadowCatcherParams - this.viewer.getRenderer().updateShadowCatcher() + .on('change', () => { + updateShadowcatcher() }) } @@ -1301,9 +1292,19 @@ export default class Sandbox { url, authToken, true, - undefined, - 1 + undefined ) + /** Too spammy */ + // loader.on(LoaderEvent.LoadProgress, (arg: { progress: number; id: string }) => { + // console.warn(arg) + // }) + loader.on(LoaderEvent.LoadCancelled, (resource: string) => { + console.warn(`Resource ${resource} loading was canceled`) + }) + loader.on(LoaderEvent.LoadWarning, (arg: { message: string }) => { + console.error(`Loader warning: ${arg.message}`) + }) + await this.viewer.loadObject(loader, true) } localStorage.setItem('last-load-url', url) diff --git a/packages/viewer-sandbox/src/main-multi.ts b/packages/viewer-sandbox/src/main-multi.ts index 75324f2d24..dd9cc3b433 100644 --- a/packages/viewer-sandbox/src/main-multi.ts +++ b/packages/viewer-sandbox/src/main-multi.ts @@ -2,13 +2,7 @@ import { DefaultViewerParams, SelectionEvent, ViewerEvent, - DebugViewer, - Viewer -} from '@speckle/viewer' - -import './style.css' -import Sandbox from './Sandbox' -import { + Viewer, CameraController, SelectionExtension, SectionTool, @@ -16,15 +10,8 @@ import { MeasurementsExtension } from '@speckle/viewer' -// const container0 = document.querySelector('#renderer0') -// if (!container0) { -// throw new Error("Couldn't find #app container!") -// } - -// const container1 = document.querySelector('#renderer1') -// if (!container1) { -// throw new Error("Couldn't find #app container!") -// } +import './style.css' +import Sandbox from './Sandbox' const createViewer = async (containerName: string, stream: string) => { const container = document.querySelector(containerName) @@ -44,7 +31,7 @@ const createViewer = async (containerName: string, stream: string) => { params.verbose = true const multiSelectList: SelectionEvent[] = [] - const viewer: Viewer = new DebugViewer(container, params) + const viewer: Viewer = new Viewer(container, params) await viewer.init() const cameraController = viewer.createExtension(CameraController) @@ -58,21 +45,12 @@ const createViewer = async (containerName: string, stream: string) => { sectionOutlines // use it measurements // use it - const sandbox = new Sandbox(controlsContainer, viewer as DebugViewer, multiSelectList) + const sandbox = new Sandbox(controlsContainer, viewer, multiSelectList) window.addEventListener('load', () => { viewer.resize() }) - viewer.on( - ViewerEvent.LoadProgress, - (a: { progress: number; id: string; url: string }) => { - if (a.progress >= 1) { - viewer.resize() - } - } - ) - viewer.on(ViewerEvent.LoadComplete, () => { Object.assign(sandbox.sceneParams.worldSize, viewer.World.worldSize) Object.assign(sandbox.sceneParams.worldOrigin, viewer.World.worldOrigin) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 78f7d76a98..7289d139ef 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -4,7 +4,6 @@ import { DefaultViewerParams, SelectionEvent, ViewerEvent, - DebugViewer, Viewer } from '@speckle/viewer' @@ -40,7 +39,7 @@ const createViewer = async (containerName: string, stream: string) => { params.verbose = true const multiSelectList: SelectionEvent[] = [] - const viewer: Viewer = new DebugViewer(container, params) + const viewer: Viewer = new Viewer(container, params) await viewer.init() const cameraController = viewer.createExtension(CameraController) @@ -64,27 +63,15 @@ const createViewer = async (containerName: string, stream: string) => { // rotateCamera // use it // boxSelect // use it - const sandbox = new Sandbox(controlsContainer, viewer as DebugViewer, multiSelectList) + const sandbox = new Sandbox(controlsContainer, viewer, multiSelectList) window.addEventListener('load', () => { viewer.resize() }) - viewer.on( - ViewerEvent.ObjectClicked, - (event: { hits: { node: { model: { id: string } } }[] }) => { - if (event) console.log(event.hits[0].node.model.id) - } - ) - - viewer.on( - ViewerEvent.LoadProgress, - (a: { progress: number; id: string; url: string }) => { - if (a.progress >= 1) { - viewer.resize() - } - } - ) + viewer.on(ViewerEvent.ObjectClicked, (event: SelectionEvent | null) => { + if (event) console.log(event.hits[0].node.model.id) + }) viewer.on(ViewerEvent.LoadComplete, async () => { console.warn(viewer.getRenderer().renderingStats) @@ -121,6 +108,7 @@ const getStream = () => { // 'https://speckle.xyz/streams/da9e320dad/commits/5388ef24b8?c=%5B-7.66134,10.82932,6.41935,-0.07739,-13.88552,1.8697,0,1%5D' // Revit sample house (good for bim-like stuff with many display meshes) 'https://speckle.xyz/streams/da9e320dad/commits/5388ef24b8' + // 'https://latest.speckle.dev/streams/c1faab5c62/commits/ab1a1ab2b6' // 'https://speckle.xyz/streams/da9e320dad/commits/5388ef24b8' // 'https://latest.speckle.dev/streams/58b5648c4d/commits/60371ecb2d' // 'Super' heavy revit shit @@ -347,6 +335,7 @@ const getStream = () => { // 'https://latest.speckle.dev/streams/ee5346d3e1/commits/576310a6d5' // 'https://latest.speckle.dev/streams/ee5346d3e1/commits/489d42ca8c' // 'https://latest.speckle.dev/streams/97750296c2/objects/11a7752e40b4ef0620affc55ce9fdf5a' + // 'https://speckle.xyz/streams/0ed2cdc8eb/commits/350c4e1a4d' // 'https://latest.speckle.dev/streams/92b620fb17/objects/7118603b197c00944f53be650ce721ec' diff --git a/packages/viewer/.eslintrc.cjs b/packages/viewer/.eslintrc.cjs index dbeae6a4a4..8eff2ff389 100644 --- a/packages/viewer/.eslintrc.cjs +++ b/packages/viewer/.eslintrc.cjs @@ -18,7 +18,8 @@ const config = { } }, rules: { - 'no-console': ['warn', { allow: ['warn', 'error'] }] + 'no-console': ['warn', { allow: ['warn', 'error'] }], + '@typescript-eslint/no-non-null-assertion': 'error' }, ignorePatterns: ['dist2', 'example/speckleviewer.web.js'], overrides: [ diff --git a/packages/viewer/package.json b/packages/viewer/package.json index b037623822..33aa79a40b 100644 --- a/packages/viewer/package.json +++ b/packages/viewer/package.json @@ -56,18 +56,15 @@ "@speckle/shared": "workspace:^", "@types/flat": "^5.0.2", "camera-controls": "^1.33.1", - "flat": "^5.0.2", "hold-event": "^0.1.0", "js-logger": "1.6.1", "lodash-es": "^4.17.21", - "rainbowvis.js": "^1.0.1", "string-to-color": "^2.2.2", "three": "^0.140.0", "three-mesh-bvh": "0.5.17", "tree-model": "1.0.7", "troika-three-text": "0.47.2", - "type-fest": "^4.15.0", - "underscore": "1.13.6" + "type-fest": "^4.15.0" }, "devDependencies": { "@babel/core": "^7.18.2", @@ -76,6 +73,7 @@ "@rollup/plugin-babel": "^5.3.1", "@rollup/plugin-image": "^3.0.2", "@types/babel__core": "^7.20.1", + "@types/lodash-es": "4.17.12", "@types/three": "^0.136.0", "@typescript-eslint/eslint-plugin": "^5.39.0", "@typescript-eslint/parser": "^5.39.0", diff --git a/packages/viewer/src/IViewer.ts b/packages/viewer/src/IViewer.ts index 52d7a0840d..e68768a9c8 100644 --- a/packages/viewer/src/IViewer.ts +++ b/packages/viewer/src/IViewer.ts @@ -1,16 +1,31 @@ import { Vector3 } from 'three' +import { type PropertyInfo } from './modules/filtering/PropertyManager' +import type { Query, QueryArgsResultMap } from './modules/queries/Query' +import { type TreeNode, WorldTree } from './modules/tree/WorldTree' +import { type Utils } from './modules/Utils' import defaultHdri from './assets/hdri/Mild-dwab.png' -import { PropertyInfo } from './modules/filtering/PropertyManager' -import { Query, QueryArgsResultMap, QueryResult } from './modules/queries/Query' -import { DataTree } from './modules/tree/DataTree' -import { TreeNode, WorldTree } from './modules/tree/WorldTree' -import { Utils } from './modules/Utils' import { World } from './modules/World' import SpeckleRenderer from './modules/SpeckleRenderer' import { Extension } from './modules/extensions/Extension' -import Input from './modules/input/Input' import { Loader } from './modules/loaders/Loader' import { type Constructor } from 'type-fest' +import type { Vector3Like } from './modules/batching/BatchObject' +import type { FilteringState } from './modules/extensions/FilteringExtension' + +export type SpeckleReference = { + referencedId: string +} + +export type SpeckleObject = { + [k: string]: unknown + speckle_type: string + id: string + elements?: SpeckleReference[] + children?: SpeckleObject[] | SpeckleReference[] + name?: string + referencedId?: string + units?: string +} export interface ViewerParams { showStats: boolean @@ -54,21 +69,29 @@ export const DefaultViewerParams: ViewerParams = { export enum ViewerEvent { ObjectClicked = 'object-clicked', ObjectDoubleClicked = 'object-doubleclicked', - DownloadComplete = 'download-complete', LoadComplete = 'load-complete', - LoadProgress = 'load-progress', UnloadComplete = 'unload-complete', - LoadCancelled = 'load-cancelled', UnloadAllComplete = 'unload-all-complete', Busy = 'busy', FilteringStateSet = 'filtering-state-set', LightConfigUpdated = 'light-config-updated' } +export interface ViewerEventPayload { + [ViewerEvent.ObjectClicked]: SelectionEvent | null + [ViewerEvent.ObjectDoubleClicked]: SelectionEvent | null + [ViewerEvent.LoadComplete]: string + [ViewerEvent.UnloadComplete]: string + [ViewerEvent.UnloadAllComplete]: void + [ViewerEvent.Busy]: boolean + [ViewerEvent.FilteringStateSet]: FilteringState + [ViewerEvent.LightConfigUpdated]: LightConfiguration +} + export type SpeckleView = { name: string id: string - view: Record + view: { origin: Vector3Like; target: Vector3Like } } export type SelectionEvent = { @@ -140,14 +163,16 @@ export enum StencilOutlineType { } export interface IViewer { - get input(): Input get Utils(): Utils get World(): World init(): Promise resize(): void - on(eventType: ViewerEvent, handler: (arg) => void) - requestRender(flags?: number): void + on( + eventType: T, + handler: (arg: ViewerEventPayload[T]) => void + ): void + requestRender(flags?: UpdateFlags): void setLightConfiguration(config: LightConfiguration): void @@ -166,16 +191,14 @@ export interface IViewer { ): Promise /** Data ops */ - getDataTree(): DataTree getWorldTree(): WorldTree - query(query: T): QueryArgsResultMap[T['operation']] - queryAsync(query: Query): Promise + query(query: T): QueryArgsResultMap[T['operation']] | null getRenderer(): SpeckleRenderer getContainer(): HTMLElement createExtension(type: Constructor): T getExtension(type: Constructor): T - + hasExtension(type: Constructor): boolean dispose(): void } diff --git a/packages/viewer/src/helpers/flatten.ts b/packages/viewer/src/helpers/flatten.ts index 819d1e674f..bf1e743b17 100644 --- a/packages/viewer/src/helpers/flatten.ts +++ b/packages/viewer/src/helpers/flatten.ts @@ -4,7 +4,7 @@ * @param obj object to flatten * @returns an object with all its props flattened into `prop.subprop.subsubprop`. */ -const flattenObject = function (obj) { +const flattenObject = function (obj: { [x: string]: unknown; id: unknown }) { const flatten = {} as Record for (const k in obj) { if ( @@ -19,7 +19,7 @@ const flattenObject = function (obj) { const v = obj[k] if (v === null || v === undefined || Array.isArray(v)) continue if (v.constructor === Object) { - const flattenProp = flattenObject(v) + const flattenProp = flattenObject(v as { [x: string]: unknown; id: unknown }) for (const pk in flattenProp) { flatten[k + '.' + pk] = flattenProp[pk] } diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts index ca30e7228e..233f87cc72 100644 --- a/packages/viewer/src/index.ts +++ b/packages/viewer/src/index.ts @@ -3,75 +3,78 @@ import { AssetType, DefaultLightConfiguration, DefaultViewerParams, - IViewer, + type IViewer, ObjectLayers, - SelectionEvent, - SpeckleView, + type SelectionEvent, + type SpeckleObject, + type SpeckleReference, + type SpeckleView, UpdateFlags, ViewerEvent, - ViewerParams + type ViewerParams, + LightConfiguration, + ViewerEventPayload } from './IViewer' -import { +import type { PropertyInfo, StringPropertyInfo, NumericPropertyInfo } from './modules/filtering/PropertyManager' -import { SunLightConfiguration } from './IViewer' -import { DataTree, ObjectPredicate, SpeckleObject } from './modules/tree/DataTree' +import { type SunLightConfiguration } from './IViewer' import { World } from './modules/World' -import { DebugViewer } from './modules/DebugViewer' -import { NodeData, TreeNode, WorldTree } from './modules/tree/WorldTree' -import { +import { type NodeData, type TreeNode, WorldTree } from './modules/tree/WorldTree' +import type { PointQuery, QueryResult, IntersectionQuery, PointQueryResult, IntersectionQueryResult } from './modules/queries/Query' -import { Utils } from './modules/Utils' +import { type Utils } from './modules/Utils' import { BatchObject } from './modules/batching/BatchObject' import { Box3, Vector3 } from 'three' import { - MeasurementOptions, + type MeasurementOptions, MeasurementType, MeasurementsExtension } from './modules/extensions/measurements/MeasurementsExtension' import { Units } from './modules/converter/Units' import { SelectionExtension } from './modules/extensions/SelectionExtension' import { CameraController } from './modules/extensions/CameraController' -import { InlineView } from './modules/extensions/CameraController' -import { CanonicalView } from './modules/extensions/CameraController' -import { CameraEvent } from './modules/objects/SpeckleCamera' -import { SectionTool } from './modules/extensions/SectionTool' +import { type InlineView } from './modules/extensions/CameraController' +import { type CanonicalView } from './modules/extensions/CameraController' +import { CameraEvent, CameraEventPayload } from './modules/objects/SpeckleCamera' +import { SectionTool, SectionToolEventPayload } from './modules/extensions/SectionTool' import { SectionOutlines } from './modules/extensions/SectionOutlines' import { FilteringExtension, - FilteringState + type FilteringState } from './modules/extensions/FilteringExtension' import { Extension } from './modules/extensions/Extension' import { ExplodeExtension } from './modules/extensions/ExplodeExtension' import { DiffExtension, - DiffResult, + type DiffResult, VisualDiffMode } from './modules/extensions/DiffExtension' -import { Loader } from './modules/loaders/Loader' +import { Loader, LoaderEvent } from './modules/loaders/Loader' import { SpeckleLoader } from './modules/loaders/Speckle/SpeckleLoader' import { ObjLoader } from './modules/loaders/OBJ/ObjLoader' import { LegacyViewer } from './modules/LegacyViewer' import { SpeckleType } from './modules/loaders/GeometryConverter' -import Input, { InputEvent } from './modules/input/Input' +import Input, { InputEvent, InputEventPayload } from './modules/input/Input' import { GeometryType } from './modules/batching/Batch' import { MeshBatch } from './modules/batching/MeshBatch' import SpeckleStandardMaterial from './modules/materials/SpeckleStandardMaterial' import SpeckleTextMaterial from './modules/materials/SpeckleTextMaterial' import { SpeckleText } from './modules/objects/SpeckleText' import { NodeRenderView } from './modules/tree/NodeRenderView' +import { type ExtendedIntersection } from './modules/objects/SpeckleRaycaster' +import { SpeckleGeometryConverter } from './modules/loaders/Speckle/SpeckleGeometryConverter' import { Assets } from './modules/Assets' export { Viewer, - DebugViewer, LegacyViewer, DefaultViewerParams, ViewerEvent, @@ -97,6 +100,7 @@ export { Loader, SpeckleLoader, ObjLoader, + LoaderEvent, UpdateFlags, SpeckleType, Input, @@ -108,6 +112,7 @@ export { SpeckleTextMaterial, SpeckleText, NodeRenderView, + SpeckleGeometryConverter, Assets, AssetType } @@ -119,10 +124,10 @@ export type { PropertyInfo, StringPropertyInfo, NumericPropertyInfo, + LightConfiguration, SunLightConfiguration, - DataTree, - ObjectPredicate, SpeckleObject, + SpeckleReference, SpeckleView, CanonicalView, InlineView, @@ -136,7 +141,12 @@ export type { Utils, DiffResult, MeasurementOptions, - FilteringState + FilteringState, + ExtendedIntersection, + ViewerEventPayload, + InputEventPayload, + SectionToolEventPayload, + CameraEventPayload } export * as UrlHelper from './modules/UrlHelper' diff --git a/packages/viewer/src/modules/Assets.ts b/packages/viewer/src/modules/Assets.ts index ac4f483c3a..ff7b635746 100644 --- a/packages/viewer/src/modules/Assets.ts +++ b/packages/viewer/src/modules/Assets.ts @@ -4,20 +4,24 @@ import { TextureLoader, Color, DataTexture, + DataTextureLoader, Matrix4, Euler } from 'three' import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js' import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js' import { FontLoader, Font } from 'three/examples/jsm/loaders/FontLoader.js' -import { Asset, AssetType } from '../IViewer' +import { type Asset, AssetType } from '../IViewer' import Logger from 'js-logger' import { RotatablePMREMGenerator } from './objects/RotatablePMREMGenerator' export class Assets { private static _cache: { [name: string]: Texture | Font } = {} - private static getLoader(src: string, assetType: AssetType): TextureLoader { + private static getLoader( + src: string, + assetType: AssetType + ): TextureLoader | DataTextureLoader | null { if (assetType === undefined) assetType = src.split('.').pop() as AssetType if (!Object.values(AssetType).includes(assetType)) { Logger.warn(`Asset ${src} could not be loaded. Unknown type`) @@ -30,6 +34,8 @@ export class Assets { return new RGBELoader() case AssetType.TEXTURE_8BPP: return new TextureLoader() + default: + return null } } @@ -115,7 +121,7 @@ export class Assets { } public static getFont(asset: Asset | string): Promise { - let srcUrl: string = null + let srcUrl: string | null = null if ((asset).src) { srcUrl = (asset as Asset).src } else { @@ -128,7 +134,7 @@ export class Assets { return new Promise((resolve, reject) => { new FontLoader().load( - srcUrl, + srcUrl as string, (font: Font) => { resolve(font) }, @@ -148,6 +154,14 @@ export class Assets { canvas.height = texture.image.height const context = canvas.getContext('2d') + /** As you can see here https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext#return_value + * The only valid cases where `getContext` returns null are: + * - "contextType doesn't match a possible drawing context" Definetely not the case as we're providing '2d'! + * - "differs from the first contextType requested". It can't since **we're only requesting a context once**! + * - If it returns null outside of these two casese, you have bigger problems than us throwing an exception here + */ + if (!context) throw new Error('Fatal! 2d context could not be retrieved.') + context.drawImage(texture.image, 0, 0) const data = context.getImageData(0, 0, canvas.width, canvas.height) diff --git a/packages/viewer/src/modules/DebugViewer.ts b/packages/viewer/src/modules/DebugViewer.ts deleted file mode 100644 index 2d63377f9e..0000000000 --- a/packages/viewer/src/modules/DebugViewer.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Viewer } from './Viewer' - -export class DebugViewer extends Viewer { - requestRenderShadowmap() { - this.getRenderer().updateDirectLights() - this.requestRender() - } -} diff --git a/packages/viewer/src/modules/Intersections.ts b/packages/viewer/src/modules/Intersections.ts index 1d9d667536..43ddae12f9 100644 --- a/packages/viewer/src/modules/Intersections.ts +++ b/packages/viewer/src/modules/Intersections.ts @@ -1,7 +1,6 @@ import { Box3, Camera, - Intersection, Object3D, Plane, Ray, @@ -12,11 +11,15 @@ import { } from 'three' import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js' -import { SpeckleRaycaster } from './objects/SpeckleRaycaster' +import { + ExtendedIntersection, + ExtendedMeshIntersection, + SpeckleRaycaster +} from './objects/SpeckleRaycaster' import { ObjectLayers } from '../IViewer' export class Intersections { - private raycaster: SpeckleRaycaster + protected raycaster: SpeckleRaycaster private boxBuffer: Box3 = new Box3() private vec0Buffer: Vector4 = new Vector4() private vec1Buffer: Vector4 = new Vector4() @@ -26,12 +29,11 @@ export class Intersections { this.raycaster = new SpeckleRaycaster() this.raycaster.params.Line = { threshold: 0.01 } this.raycaster.params.Points = { threshold: 0.01 } - ;(this.raycaster.params as { Line2? }).Line2 = {} - ;(this.raycaster.params as { Line2? }).Line2.threshold = 1 + this.raycaster.params.Line2 = { threshold: 1 } this.raycaster.onObjectIntersectionTest = this.onObjectIntersection.bind(this) } - private onObjectIntersection(obj: Object3D) { + protected onObjectIntersection(obj: Object3D) { if (obj instanceof LineSegments2) { const box = this.boxBuffer.setFromObject(obj) const min = this.vec0Buffer.set(box.min.x, box.min.y, box.min.z, 1) @@ -62,17 +64,11 @@ export class Intersections { * original line width and how zoomed in the camer is on the line(batch) */ if (!worldSpace) { - if (ssDistance < 1) { - ;(this.raycaster.params as { Line2? }).Line2.threshold = lineWidth * 8 - } else { - ;(this.raycaster.params as { Line2? }).Line2.threshold = lineWidth * 5 - } + this.raycaster.params.Line2.threshold = + ssDistance < 1 ? lineWidth * 8 : lineWidth * 5 } else { - if (ssDistance < 1) { - ;(this.raycaster.params as { Line2? }).Line2.threshold = lineWidth * 2 - } else { - ;(this.raycaster.params as { Line2? }).Line2.threshold = lineWidth - } + this.raycaster.params.Line2.threshold = + ssDistance < 1 ? lineWidth * 2 : lineWidth } } } @@ -81,54 +77,111 @@ export class Intersections { scene: Scene, camera: Camera, point: Vector2, + castLayers: ObjectLayers.STREAM_CONTENT_MESH, + nearest?: boolean, + bounds?: Box3, + firstOnly?: boolean + ): Array | null + public intersect( + scene: Scene, + camera: Camera, + point: Vector2, + castLayers?: Array, + nearest?: boolean, + bounds?: Box3, + firstOnly?: boolean + ): Array | null + + public intersect( + scene: Scene, + camera: Camera, + point: Vector2, + castLayers: Array | ObjectLayers | undefined = undefined, nearest = true, - bounds: Box3 = null, - castLayers: Array = undefined, + bounds?: Box3, firstOnly = false - ): Array { + ): Array | Array | null { this.raycaster.setFromCamera(point, camera) this.raycaster.firstHitOnly = firstOnly - return this.intersectInternal(scene, nearest, bounds, castLayers) + const preserveMask = this.setRaycasterLayers(castLayers) + let result: Array | Array | null + if (castLayers === ObjectLayers.STREAM_CONTENT_MESH) { + result = this.intersectInternal(scene, nearest, bounds) + } else result = this.intersectInternal(scene, nearest, bounds) + this.raycaster.layers.mask = preserveMask + return result } public intersectRay( scene: Scene, camera: Camera, ray: Ray, + castLayers: ObjectLayers.STREAM_CONTENT_MESH, + nearest?: boolean, + bounds?: Box3, + firstOnly?: boolean + ): Array | null + public intersectRay( + scene: Scene, + camera: Camera, + ray: Ray, + castLayers?: Array, + nearest?: boolean, + bounds?: Box3, + firstOnly?: boolean + ): Array | null + + public intersectRay( + scene: Scene, + camera: Camera, + ray: Ray, + castLayers: Array | ObjectLayers | undefined = undefined, nearest = true, - bounds: Box3 = null, - castLayers: Array = undefined, + bounds?: Box3, firstOnly = false - ): Array { + ): Array | Array | null { this.raycaster.camera = camera this.raycaster.set(ray.origin, ray.direction) this.raycaster.firstHitOnly = firstOnly - return this.intersectInternal(scene, nearest, bounds, castLayers) + const preserveMask = this.setRaycasterLayers(castLayers) + let result: Array | Array | null + if (castLayers === ObjectLayers.STREAM_CONTENT_MESH) { + result = this.intersectInternal(scene, nearest, bounds) + } else result = this.intersectInternal(scene, nearest, bounds) + this.raycaster.layers.mask = preserveMask + return result } - private intersectInternal( - scene: Scene, - nearest: boolean, - bounds: Box3, - castLayers: Array - ) { + private setRaycasterLayers( + castLayers: Array | ObjectLayers | undefined + ): number { const preserveMask = this.raycaster.layers.mask - if (castLayers !== undefined) { this.raycaster.layers.disableAll() - castLayers.forEach((layer) => { - this.raycaster.layers.enable(layer) - }) + if (Array.isArray(castLayers)) + castLayers.forEach((layer) => { + this.raycaster.layers.enable(layer) + }) + else { + this.raycaster.layers.enable(castLayers) + } } + return preserveMask + } + + private intersectInternal( + scene: Scene, + nearest?: boolean, + bounds?: Box3 + ): T[] | null { + let results: T[] | null = [] const target = scene.getObjectByName('ContentGroup') - let results = [] if (target) { // const start = performance.now() results = this.raycaster.intersectObjects(target.children) // Logger.warn('Interesct time -> ', performance.now() - start) } - this.raycaster.layers.mask = preserveMask if (results.length === 0) return null if (nearest) diff --git a/packages/viewer/src/modules/LegacyViewer.ts b/packages/viewer/src/modules/LegacyViewer.ts index ad1aa862b2..5aec7f7c60 100644 --- a/packages/viewer/src/modules/LegacyViewer.ts +++ b/packages/viewer/src/modules/LegacyViewer.ts @@ -1,39 +1,48 @@ -import { MathUtils } from 'three' -import { FilteringExtension, FilteringState } from './extensions/FilteringExtension' -import { PolarView } from './extensions/CameraController' -import { InlineView } from './extensions/CameraController' -import { CanonicalView } from './extensions/CameraController' +import { Box3, MathUtils, Vector2 } from 'three' +import { + FilteringExtension, + type FilteringState +} from './extensions/FilteringExtension' +import { + type InlineView, + type PolarView, + type CanonicalView, + CameraController +} from './extensions/CameraController' import { SpeckleType } from './loaders/GeometryConverter' import { Queries } from './queries/Queries' -import { Query, QueryArgsResultMap, QueryResult } from './queries/Query' -import { DataTree, DataTreeBuilder } from './tree/DataTree' +import type { Query, QueryArgsResultMap, QueryResult } from './queries/Query' import { SelectionExtension, - SelectionExtensionOptions + type SelectionExtensionOptions } from './extensions/SelectionExtension' -import { StencilOutlineType } from '../IViewer' import { DefaultViewerParams, - IViewer, - SelectionEvent, - SpeckleView, - SunLightConfiguration, - ViewerParams + type IViewer, + type SelectionEvent, + type SpeckleView, + type SunLightConfiguration, + type ViewerParams, + StencilOutlineType } from '../IViewer' -import { TreeNode, WorldTree } from './tree/WorldTree' import { Viewer } from './Viewer' -import { CameraController } from './extensions/CameraController' import { SectionTool } from './extensions/SectionTool' import { SectionOutlines } from './extensions/SectionOutlines' +import { type TreeNode, WorldTree } from './tree/WorldTree' import { - MeasurementOptions, + type MeasurementOptions, MeasurementsExtension } from './extensions/measurements/MeasurementsExtension' import { ExplodeExtension } from './extensions/ExplodeExtension' -import { DiffExtension, DiffResult, VisualDiffMode } from './extensions/DiffExtension' -import { PropertyInfo } from './filtering/PropertyManager' +import { + DiffExtension, + type DiffResult, + VisualDiffMode +} from './extensions/DiffExtension' +import { type PropertyInfo } from './filtering/PropertyManager' import { BatchObject } from './batching/BatchObject' import { SpeckleLoader } from './loaders/Speckle/SpeckleLoader' +import Logger from 'js-logger' class LegacySelectionExtension extends SelectionExtension { /** FE2 'manually' selects objects pon it's own, so we're disabling the extension's event handler @@ -61,7 +70,7 @@ class HighlightExtension extends SelectionExtension { pointSize: 4 } } - this.setOptions(highlightMaterialData) + this.options = highlightMaterialData } public unselectObjects(ids: Array) { @@ -70,7 +79,8 @@ class HighlightExtension extends SelectionExtension { const nodes = [] for (let k = 0; k < ids.length; k++) { - nodes.push(...this.viewer.getWorldTree().findId(ids[k])) + const foundNodes = this.viewer.getWorldTree().findId(ids[k]) + if (foundNodes) nodes.push(...foundNodes) } this.clearSelection( nodes.filter((node: TreeNode) => { @@ -88,21 +98,20 @@ class HighlightExtension extends SelectionExtension { selection } - protected onPointerMove(e) { + protected onPointerMove(e: Vector2) { e } } export class LegacyViewer extends Viewer { - private cameraController: CameraController = null - private selection: SelectionExtension = null - private sections: SectionTool = null - private sectionOutlines: SectionOutlines = null - private measurements: MeasurementsExtension = null - private filtering: FilteringExtension = null - private explodeExtension: ExplodeExtension = null - private diffExtension: DiffExtension = null - private highlightExtension: HighlightExtension = null + private cameraController: CameraController + private selection: SelectionExtension + private sections: SectionTool + private measurements: MeasurementsExtension + private filtering: FilteringExtension + private explodeExtension: ExplodeExtension + private diffExtension: DiffExtension + private highlightExtension: HighlightExtension public constructor( container: HTMLElement, @@ -112,7 +121,7 @@ export class LegacyViewer extends Viewer { this.cameraController = this.createExtension(CameraController) this.selection = this.createExtension(LegacySelectionExtension) this.sections = this.createExtension(SectionTool) - this.sectionOutlines = this.createExtension(SectionOutlines) + this.createExtension(SectionOutlines) this.measurements = this.createExtension(MeasurementsExtension) this.filtering = this.createExtension(FilteringExtension) this.explodeExtension = this.createExtension(ExplodeExtension) @@ -143,7 +152,7 @@ export class LegacyViewer extends Viewer { if (!box) { box = this.speckleRenderer.sceneBox } - this.sections.setBox(box, offset) + this.sections.setBox(box as Box3, offset) } public getSectionBoxFromObjects(objectIds: string[]) { @@ -155,7 +164,7 @@ export class LegacyViewer extends Viewer { } public getCurrentSectionBox() { - return this.sections.getCurrentBox() + return this.sections.getBox() } public toggleSectionBox() { @@ -178,7 +187,7 @@ export class LegacyViewer extends Viewer { if (!this.filtering.filteringState.selectedObjects) this.filtering.filteringState.selectedObjects = [] this.filtering.filteringState.selectedObjects.push( - ...this.selection.getSelectedObjects().map((obj) => obj.id) + ...this.selection.getSelectedObjects().map((obj) => obj.id as string) ) return Promise.resolve(this.filtering.filteringState) } @@ -192,7 +201,7 @@ export class LegacyViewer extends Viewer { public hideObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = false, ghost = false ): Promise { @@ -211,7 +220,7 @@ export class LegacyViewer extends Viewer { public showObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = false ): Promise { return new Promise((resolve) => { @@ -224,7 +233,7 @@ export class LegacyViewer extends Viewer { public isolateObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = false, ghost = true ): Promise { @@ -243,7 +252,7 @@ export class LegacyViewer extends Viewer { public unIsolateObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = false ): Promise { return new Promise((resolve) => { @@ -294,7 +303,7 @@ export class LegacyViewer extends Viewer { }) } - public resetFilters(): Promise { + public resetFilters(): Promise { return new Promise((resolve) => { const filteringState = this.preserveSelectionFilter(() => { return this.filtering.resetFilters() @@ -303,20 +312,26 @@ export class LegacyViewer extends Viewer { }) } - private preserveSelectionFilter(filterFn: () => FilteringState): FilteringState { - const selectedObjects = this.selection.getSelectedObjects().map((obj) => obj.id) + private preserveSelectionFilter( + filterFn: () => FilteringState | null + ): FilteringState { + const selectedObjects = this.selection + .getSelectedObjects() + .map((obj) => obj.id) as string[] if (selectedObjects.length) this.selection.clearSelection() const filteringState = filterFn() - if (!filteringState.selectedObjects) - filteringState.selectedObjects = selectedObjects + if (filteringState) { + if (!filteringState.selectedObjects) + filteringState.selectedObjects = selectedObjects - this.selection.selectObjects(filteringState.selectedObjects) - return filteringState + this.selection.selectObjects(filteringState.selectedObjects) + } + return filteringState || this.filtering.filteringState } /** TREE */ - public getDataTree(): DataTree { - return DataTreeBuilder.build(this.tree) + public getDataTree(): void { + Logger.error('DataTree is obsolete, please use WorldTree instead') } public getWorldTree(): WorldTree { @@ -333,9 +348,10 @@ export class LegacyViewer extends Viewer { Queries.DefaultIntersectionQuerySolver.setContext(this.speckleRenderer) return Queries.DefaultIntersectionQuerySolver.solve(query) } + return null } - public queryAsync(query: Query): Promise { + public queryAsync(query: Query): Promise | null { //TO DO query return null @@ -391,11 +407,11 @@ export class LegacyViewer extends Viewer { return new Promise((resolve) => { const sectionBoxVisible = this.sections.enabled if (sectionBoxVisible) { - this.sections.displayOff() + this.sections.visible = false } const screenshot = this.speckleRenderer.renderer.domElement.toDataURL('image/png') if (sectionBoxVisible) { - this.sections.displayOn() + this.sections.visible = true } resolve(screenshot) }) @@ -407,11 +423,15 @@ export class LegacyViewer extends Viewer { public getObjects(id: string): BatchObject[] { const nodes = this.tree.findId(id) - const objects = [] - nodes.forEach((node: TreeNode) => { - if (node.model.renderView) - objects.push(this.speckleRenderer.getObject(node.model.renderView)) - }) + const objects: BatchObject[] = [] + if (nodes) { + nodes.forEach((node: TreeNode) => { + if (node.model.renderView) + objects.push( + this.speckleRenderer.getObject(node.model.renderView) as BatchObject + ) + }) + } return objects } @@ -421,7 +441,7 @@ export class LegacyViewer extends Viewer { public async loadObjectAsync( url: string, - token: string = null, + token: string | undefined = undefined, enableCaching = true, zoomToObject = true ) { @@ -442,11 +462,11 @@ export class LegacyViewer extends Viewer { return this.diffExtension.undiff() } - public setDiffTime(diffResult: DiffResult, time: number) { + public setDiffTime(_diffResult: DiffResult, time: number) { this.diffExtension.updateVisualDiff(time) } - public setVisualDiffMode(diffResult: DiffResult, mode: VisualDiffMode) { + public setVisualDiffMode(_diffResult: DiffResult, mode: VisualDiffMode) { this.diffExtension.updateVisualDiff(undefined, mode) } diff --git a/packages/viewer/src/modules/Shadowcatcher.ts b/packages/viewer/src/modules/Shadowcatcher.ts index 15dff06013..dbd1611077 100644 --- a/packages/viewer/src/modules/Shadowcatcher.ts +++ b/packages/viewer/src/modules/Shadowcatcher.ts @@ -12,23 +12,26 @@ import { Scene, Vector2, Vector3, - WebGLRenderer, ZeroFactor } from 'three' import { Geometry } from './converter/Geometry' import SpeckleBasicMaterial from './materials/SpeckleBasicMaterial' import { ShadowcatcherPass } from './pipeline/ShadowcatcherPass' import { ObjectLayers } from '../IViewer' -import { DefaultShadowcatcherConfig, ShadowcatcherConfig } from './ShadowcatcherConfig' +import { + DefaultShadowcatcherConfig, + type ShadowcatcherConfig +} from './ShadowcatcherConfig' +import type { SpeckleWebGLRenderer } from './objects/SpeckleWebGLRenderer' export class Shadowcatcher { public static readonly MESH_NAME = 'Shadowcatcher' public static readonly PLANE_SUBD = 2 public static readonly MAX_TEXTURE_SIZE_SCALE = 0.5 - private planeMesh: Mesh = null + private planeMesh: Mesh private planeSize: Vector2 = new Vector2() - private displayMaterial: SpeckleBasicMaterial = null - public shadowcatcherPass: ShadowcatcherPass = null + private displayMaterial: SpeckleBasicMaterial + public shadowcatcherPass: ShadowcatcherPass private _config: ShadowcatcherConfig = DefaultShadowcatcherConfig public get shadowcatcherMesh() { @@ -73,8 +76,8 @@ export class Shadowcatcher { this.shadowcatcherPass.update(scene) } - public render(renderer: WebGLRenderer) { - this.shadowcatcherPass.render(renderer, null, null) + public render(renderer: SpeckleWebGLRenderer) { + this.shadowcatcherPass.render(renderer) } public bake(worldBox: Box3, maxTexSize: number, force?: boolean) { diff --git a/packages/viewer/src/modules/SpeckleRenderer.ts b/packages/viewer/src/modules/SpeckleRenderer.ts index f9a95a52ad..9ac289a2aa 100644 --- a/packages/viewer/src/modules/SpeckleRenderer.ts +++ b/packages/viewer/src/modules/SpeckleRenderer.ts @@ -7,7 +7,7 @@ import { DirectionalLight, DirectionalLightHelper, Group, - Intersection, + type Intersection, Material, Mesh, Object3D, @@ -19,43 +19,53 @@ import { sRGBEncoding, Texture, Vector3, - VSMShadowMap + VSMShadowMap, + Vector2, + PerspectiveCamera, + OrthographicCamera } from 'three' -import { Batch, BatchUpdateRange, GeometryType } from './batching/Batch' +import { type Batch, type BatchUpdateRange, GeometryType } from './batching/Batch' import Batcher from './batching/Batcher' import { Geometry } from './converter/Geometry' -import Input, { InputEvent, InputOptionsDefault } from './input/Input' +import Input, { InputEvent } from './input/Input' import { Intersections } from './Intersections' import SpeckleDepthMaterial from './materials/SpeckleDepthMaterial' import SpeckleStandardMaterial from './materials/SpeckleStandardMaterial' import { NodeRenderView } from './tree/NodeRenderView' import { Viewer } from './Viewer' -import { TreeNode } from './tree/WorldTree' +import { WorldTree, type TreeNode } from './tree/WorldTree' import { DefaultLightConfiguration, ObjectLayers, - SelectionEvent, - SunLightConfiguration, + type SelectionEvent, + type SunLightConfiguration, ViewerEvent } from '../IViewer' -import { DefaultPipelineOptions, Pipeline, PipelineOptions } from './pipeline/Pipeline' +import { + DefaultPipelineOptions, + Pipeline, + type PipelineOptions +} from './pipeline/Pipeline' import { Shadowcatcher } from './Shadowcatcher' import SpeckleMesh from './objects/SpeckleMesh' -import { ExtendedIntersection } from './objects/SpeckleRaycaster' +import { type ExtendedIntersection } from './objects/SpeckleRaycaster' import { BatchObject } from './batching/BatchObject' -import { CameraEvent, SpeckleCamera } from './objects/SpeckleCamera' +import { CameraEvent, type SpeckleCamera } from './objects/SpeckleCamera' import Materials, { - RenderMaterial, - DisplayStyle, - FilterMaterial + type RenderMaterial, + type DisplayStyle, + type FilterMaterial, + type FilterMaterialOptions } from './materials/Materials' -import { MaterialOptions } from './materials/MaterialOptions' +import { type MaterialOptions } from './materials/MaterialOptions' import { SpeckleMaterial } from './materials/SpeckleMaterial' import { SpeckleWebGLRenderer } from './objects/SpeckleWebGLRenderer' import { SpeckleTypeAllRenderables } from './loaders/GeometryConverter' import SpeckleInstancedMesh from './objects/SpeckleInstancedMesh' import { BaseSpecklePass } from './pipeline/SpecklePass' import { MeshBatch } from './batching/MeshBatch' +import type { Pass } from 'three/examples/jsm/postprocessing/Pass.js' +import { RenderTree } from './tree/RenderTree' export class RenderingStats { private renderTimeAcc = 0 @@ -64,13 +74,13 @@ export class RenderingStats { private renderTimeStart = 0 public renderTime = 0 - public objects: number - public batchCount: number - public drawCalls: number - public trisCount: number - public vertCount: number + public objects: number = 0 + public batchCount: number = 0 + public drawCalls: number = 0 + public trisCount: number = 0 + public vertCount: number = 0 - public batchDetails: Array<{ + public batchDetails!: Array<{ drawCalls: number minDrawCalls: number tris: number @@ -93,32 +103,36 @@ export class RenderingStats { } export default class SpeckleRenderer { - private readonly SHOW_HELPERS = false - private readonly IGNORE_ZERO_OPACITY_OBJECTS = true + protected readonly SHOW_HELPERS = false + protected readonly IGNORE_ZERO_OPACITY_OBJECTS = true public SHOW_BVH = false - private container: HTMLElement - private _renderer: SpeckleWebGLRenderer - private _renderinStats: RenderingStats - public _scene: Scene - private _needsRender: boolean - private rootGroup: Group + + protected _renderer: SpeckleWebGLRenderer + protected _renderinStats: RenderingStats + protected _scene: Scene + protected _needsRender: boolean + protected _intersections: Intersections + protected _shadowcatcher: Shadowcatcher + protected _speckleCamera: SpeckleCamera | null = null + + protected container: HTMLElement + protected rootGroup: Group public batcher: Batcher - private _intersections: Intersections - public input: Input - private sun: DirectionalLight - private sunConfiguration: SunLightConfiguration = DefaultLightConfiguration - private sunTarget: Object3D - public viewer: Viewer // TEMPORARY - public pipeline: Pipeline + protected sun: DirectionalLight + protected sunConfiguration: SunLightConfiguration = DefaultLightConfiguration + protected sunTarget: Object3D + protected tree: WorldTree - private _shadowcatcher: Shadowcatcher = null - private cancel: { [subtreeId: string]: boolean } = {} + protected cancel: { [subtreeId: string]: boolean } = {} - private _speckleCamera: SpeckleCamera = null - private _clippingPlanes: Plane[] = [] - private _clippingVolume: Box3 = new Box3() + protected _clippingPlanes: Plane[] = [] + protected _clippingVolume: Box3 = new Box3() - private _renderOverride: () => void = null + protected _renderOverride: (() => void) | null = null + + public viewer: Viewer // TEMPORARY + public pipeline: Pipeline + public input: Input /******************************** * Renderer and rendering flags */ @@ -136,7 +150,7 @@ export default class SpeckleRenderer { /********************** * Bounds and volumes */ - public get sceneBox() { + public get sceneBox(): Box3 { const bounds: Box3 = new Box3() const batches = this.batcher.getBatches() for (let k = 0; k < batches.length; k++) { @@ -145,11 +159,11 @@ export default class SpeckleRenderer { return bounds } - public get sceneSphere() { + public get sceneSphere(): Sphere { return this.sceneBox.getBoundingSphere(new Sphere()) } - public get sceneCenter() { + public get sceneCenter(): Vector3 { return this.sceneBox.getCenter(new Vector3()) } @@ -176,12 +190,8 @@ export default class SpeckleRenderer { /**************** * Common Objects */ - public get allObjects() { - return this._scene.getObjectByName('ContentGroup') - } - - public subtree(subtreeId: string) { - return this._scene.getObjectByName(subtreeId) + public get allObjects(): Object3D { + return this._scene.getObjectByName('ContentGroup') as Object3D } public get scene() { @@ -191,7 +201,7 @@ export default class SpeckleRenderer { /******** * Lights */ - public get sunLight() { + public get sunLight(): DirectionalLight { return this.sun } @@ -213,7 +223,7 @@ export default class SpeckleRenderer { /******** * Camera */ - public get speckleCamera(): SpeckleCamera { + public get speckleCamera(): SpeckleCamera | null { return this._speckleCamera } @@ -235,7 +245,8 @@ export default class SpeckleRenderer { }) } - public get renderingCamera() { + public get renderingCamera(): PerspectiveCamera | OrthographicCamera | null { + if (!this._speckleCamera) return null return this._speckleCamera.renderingCamera } @@ -249,13 +260,13 @@ export default class SpeckleRenderer { return this.pipeline.pipelineOptions } - public get shadowcatcher() { + public get shadowcatcher(): Shadowcatcher | null { return this._shadowcatcher } /************** * Intersections */ - public get intersections() { + public get intersections(): Intersections { return this._intersections } @@ -294,7 +305,8 @@ export default class SpeckleRenderer { return this._renderinStats } - public constructor(viewer: Viewer /** TEMPORARY */) { + public constructor(tree: WorldTree, viewer: Viewer /** TEMPORARY */) { + this.tree = tree this._renderinStats = new RenderingStats() this._scene = new Scene() this.rootGroup = new Group() @@ -338,7 +350,7 @@ export default class SpeckleRenderer { this.pipeline.configure() this.pipeline.pipelineOptions = DefaultPipelineOptions - this.input = new Input(this._renderer.domElement, InputOptionsDefault) + this.input = new Input(this._renderer.domElement) this.input.on(InputEvent.Click, this.onClick.bind(this)) this.input.on(InputEvent.DoubleClick, this.onDoubleClick.bind(this)) @@ -368,7 +380,9 @@ export default class SpeckleRenderer { ObjectLayers.STREAM_CONTENT_MESH // ObjectLayers.STREAM_CONTENT_LINE ]) - let restoreVisibility, opaque + let restoreVisibility: Record + let opaque: Record + this._shadowcatcher.shadowcatcherPass.onBeforeRender = () => { restoreVisibility = this.batcher.saveVisiblity() opaque = this.batcher.getOpaque() @@ -387,7 +401,7 @@ export default class SpeckleRenderer { } public update(deltaTime: number) { - if (!this._speckleCamera) return + if (!this.renderingCamera) return this.batcher.update(deltaTime) this.renderingCamera.updateMatrixWorld(true) @@ -395,11 +409,11 @@ export default class SpeckleRenderer { this.updateRTEShadows() this.updateTransforms() - this.updateFrustum() + this.updateFrustum(this.renderingCamera) this.pipeline.update(this) - if (this.sunConfiguration.shadowcatcher) { + if (this.sunConfiguration.shadowcatcher && this._shadowcatcher) { this._shadowcatcher.update(this._scene) } } @@ -457,13 +471,12 @@ export default class SpeckleRenderer { private updateRTEShadows() { if (!this.updateRTEShadowBuffers()) return - const meshBatches = this.batcher.getBatches( + const meshBatches: MeshBatch[] = this.batcher.getBatches( undefined, GeometryType.MESH - ) as MeshBatch[] + ) for (let k = 0; k < meshBatches.length; k++) { - const speckleMesh: SpeckleMesh | SpeckleInstancedMesh = meshBatches[k] - .renderObject as SpeckleMesh | SpeckleInstancedMesh + const speckleMesh: SpeckleMesh | SpeckleInstancedMesh = meshBatches[k].mesh /** Shadowmap depth material does not go thorugh the normal flow. * It's onBeforeRender is not getting called That's why we're updating @@ -489,10 +502,12 @@ export default class SpeckleRenderer { } private updateTransforms() { - const meshBatches = this.batcher.getBatches(undefined, GeometryType.MESH) + const meshBatches: MeshBatch[] = this.batcher.getBatches( + undefined, + GeometryType.MESH + ) for (let k = 0; k < meshBatches.length; k++) { - const meshBatch: SpeckleMesh | SpeckleInstancedMesh = meshBatches[k] - .renderObject as SpeckleMesh | SpeckleInstancedMesh + const meshBatch: SpeckleMesh | SpeckleInstancedMesh = meshBatches[k].mesh meshBatch.updateTransformsUniform() meshBatch.traverse((obj: Object3D) => { const depthMaterial: SpeckleDepthMaterial = @@ -504,10 +519,10 @@ export default class SpeckleRenderer { } } - private updateFrustum() { + private updateFrustum(camera: PerspectiveCamera | OrthographicCamera) { const v = new Vector3() const box = this.sceneBox - const camPos = new Vector3().copy(this.renderingCamera.position) + const camPos = new Vector3().copy(camera.position) let d = 0 v.set(box.min.x, box.min.y, box.min.z) // 000 d = Math.max(camPos.distanceTo(v), d) @@ -525,8 +540,8 @@ export default class SpeckleRenderer { d = Math.max(camPos.distanceTo(v), d) v.set(box.max.x, box.max.y, box.max.z) // 111 d = Math.max(camPos.distanceTo(v), d) - this.renderingCamera.far = d * 2 - this.renderingCamera.updateProjectionMatrix() + camera.far = d * 2 + camera.updateProjectionMatrix() } public resetPipeline() { @@ -549,7 +564,7 @@ export default class SpeckleRenderer { // this._needsRender = true this._renderinStats.frameEnd() - if (this.sunConfiguration.shadowcatcher) { + if (this.sunConfiguration.shadowcatcher && this._shadowcatcher) { this._shadowcatcher.render(this._renderer) } // console.log('Get transparent time -> ', InstancedMeshBatch.transparentTime) @@ -562,16 +577,16 @@ export default class SpeckleRenderer { this._needsRender = true } - public async *addRenderTree(subtreeId: string) { - this.cancel[subtreeId] = false + public async *addRenderTree(renderTree: RenderTree) { + this.cancel[renderTree.id] = false const subtreeGroup = new Group() - subtreeGroup.name = subtreeId + subtreeGroup.name = renderTree.id subtreeGroup.layers.set(ObjectLayers.STREAM_CONTENT) this.rootGroup.add(subtreeGroup) const generator = this.batcher.makeBatches( - this.viewer.getWorldTree(), - this.viewer.getWorldTree().getRenderTree(subtreeId), + this.tree, + renderTree, SpeckleTypeAllRenderables ) let currentBatchCount = 0 @@ -591,10 +606,10 @@ export default class SpeckleRenderer { this.updateDirectLights() } - if (this.cancel[subtreeId]) { + if (this.cancel[renderTree.id]) { generator.return() - this.removeRenderTree(subtreeId) - delete this.cancel[subtreeId] + this.removeRenderTree(renderTree.id) + delete this.cancel[renderTree.id] break } currentBatchCount++ @@ -607,8 +622,8 @@ export default class SpeckleRenderer { /** We'll just update the shadowcatcher after all batches are loaded */ this.updateShadowCatcher() this.updateClippingPlanes() - this._speckleCamera.setCameraPlanes(this.sceneBox) - delete this.cancel[subtreeId] + if (this._speckleCamera) this._speckleCamera.setCameraPlanes(this.sceneBox) + delete this.cancel[renderTree.id] } private addBatch(batch: Batch, parent: Object3D) { @@ -638,7 +653,7 @@ export default class SpeckleRenderer { } public removeRenderTree(subtreeId: string) { - this.rootGroup.remove(this.rootGroup.getObjectByName(subtreeId)) + this.rootGroup.remove(this.rootGroup.getObjectByName(subtreeId) as Object3D) this.updateShadowCatcher() const batches = this.batcher.getBatches(subtreeId) @@ -657,16 +672,22 @@ export default class SpeckleRenderer { } } - public setMaterial(rvs: NodeRenderView[], material: Material) + public setMaterial(rvs: NodeRenderView[], material: Material): void public setMaterial( rvs: NodeRenderView[], material: RenderMaterial & DisplayStyle & MaterialOptions - ) - public setMaterial(rvs: NodeRenderView[], material: FilterMaterial) - public setMaterial(rvs: NodeRenderView[], material) { + ): void + public setMaterial(rvs: NodeRenderView[], material: FilterMaterial): void + public setMaterial( + rvs: NodeRenderView[], + material: + | Material + | (RenderMaterial & DisplayStyle & MaterialOptions) + | FilterMaterial + ): void { if (!material) return - const rvMap = {} + const rvMap: { [id: string]: NodeRenderView[] } = {} for (let k = 0; k < rvs.length; k++) { if (!rvs[k].batchId) { continue @@ -699,7 +720,7 @@ export default class SpeckleRenderer { return { offset: value.batchStart, count: value.batchCount, material } }) if (this.batcher.batches[k]) - this.batcher.batches[k].setDrawRanges(...this.flattenDrawRanges(drawRanges)) + this.batcher.batches[k].setDrawRanges(this.flattenDrawRanges(drawRanges)) } } @@ -712,12 +733,17 @@ export default class SpeckleRenderer { return { offset: value.batchStart, count: value.batchCount, - material: this.batcher.materials.getFilterMaterial(value, material), - materialOptions: this.batcher.materials.getFilterMaterialOptions(material) + material: this.batcher.materials.getFilterMaterial( + value, + material + ) as Material, + materialOptions: this.batcher.materials.getFilterMaterialOptions( + material + ) as FilterMaterialOptions } }) if (this.batcher.batches[k]) - this.batcher.batches[k].setDrawRanges(...this.flattenDrawRanges(drawRanges)) + this.batcher.batches[k].setDrawRanges(this.flattenDrawRanges(drawRanges)) } } @@ -737,7 +763,7 @@ export default class SpeckleRenderer { }) if (this.batcher.batches[k]) - this.batcher.batches[k].setDrawRanges(...this.flattenDrawRanges(drawRanges)) + this.batcher.batches[k].setDrawRanges(this.flattenDrawRanges(drawRanges)) } } @@ -791,14 +817,14 @@ export default class SpeckleRenderer { return flatRanges } - public getMaterial(rv: NodeRenderView): Material { + public getMaterial(rv: NodeRenderView): Material | null { if (!rv || !rv.batchId) { return null } return this.batcher.getBatch(rv).getMaterial(rv) } - public getBatchMaterial(rv: NodeRenderView): Material { + public getBatchMaterial(rv: NodeRenderView): Material | null { if (!rv || !rv.batchId) { return null } @@ -818,7 +844,7 @@ export default class SpeckleRenderer { const planes = this._clippingPlanes this.allObjects.traverse((object) => { - const material = (object as unknown as { material }).material + const material = (object as unknown as { material: Material }).material if (!material) return if (!Array.isArray(material)) { material.clippingPlanes = planes @@ -829,13 +855,15 @@ export default class SpeckleRenderer { } }) this.pipeline.updateClippingPlanes(planes) - this._shadowcatcher.updateClippingPlanes(planes) + this._shadowcatcher?.updateClippingPlanes(planes) this.renderer.shadowMap.needsUpdate = true this.resetPipeline() } public updateShadowCatcher() { - this._shadowcatcher.shadowcatcherMesh.visible = this.sunConfiguration.shadowcatcher + if (this.sunConfiguration.shadowcatcher !== undefined) + this._shadowcatcher.shadowcatcherMesh.visible = + this.sunConfiguration.shadowcatcher if (this.sunConfiguration.shadowcatcher) { this._shadowcatcher.bake( this.clippingVolume, @@ -873,14 +901,17 @@ export default class SpeckleRenderer { this.sun.target = this.sunTarget } - public updateDirectLights() { + private updateDirectLights() { const phi = this.sunConfiguration.elevation const theta = this.sunConfiguration.azimuth - const radiusOffset = this.sunConfiguration.radius - this.sun.castShadow = this.sunConfiguration.castShadow - this.sun.intensity = this.sunConfiguration.intensity + const radiusOffset = this.sunConfiguration.radius || 0 + if (this.sunConfiguration.castShadow !== undefined) + this.sun.castShadow = this.sunConfiguration.castShadow + if (this.sunConfiguration.intensity !== undefined) + this.sun.intensity = this.sunConfiguration.intensity this.sun.color = new Color(this.sunConfiguration.color) - this.sun.visible = this.sunConfiguration.enabled + if (this.sunConfiguration.enabled !== undefined) + this.sun.visible = this.sunConfiguration.enabled this.sunTarget.position.copy(this.sceneCenter) const spherical = new Spherical(this.sceneSphere.radius + radiusOffset, phi, theta) @@ -946,7 +977,7 @@ export default class SpeckleRenderer { this.viewer.emit(ViewerEvent.LightConfigUpdated, { ...config }) } - public updateHelpers() { + private updateHelpers() { if (this.SHOW_HELPERS) { ;(this._scene.getObjectByName('CamHelper') as CameraHelper).update() // Thank you prettier, this looks so much better @@ -961,7 +992,7 @@ export default class SpeckleRenderer { public queryHits( results: Array - ): Array<{ node: TreeNode; point: Vector3 }> { + ): Array<{ node: TreeNode; point: Vector3 }> | null { const rvs = [] const points = [] for (let k = 0; k < results.length; k++) { @@ -981,7 +1012,10 @@ export default class SpeckleRenderer { for (let k = 0; k < rvs.length; k++) { const hitId = rvs[k].renderData.id const subtreeId = rvs[k].renderData.subtreeId - const hitNode = this.viewer.getWorldTree().findId(hitId, subtreeId)[0] + const hitNodes = this.tree.findId(hitId, subtreeId) + if (!hitNodes) continue + + const hitNode = hitNodes[0] let parentNode = hitNode while (!parentNode.model.atomic && parentNode.parent) { parentNode = parentNode.parent @@ -993,15 +1027,15 @@ export default class SpeckleRenderer { public queryHitIds( results: Array - ): Array<{ nodeId: string; point: Vector3 }> { + ): Array<{ nodeId: string; point: Vector3 }> | null { const queryResult = [] for (let k = 0; k < results.length; k++) { - let rv = results[k].batchObject?.renderView + let rv: NodeRenderView | null = results[k].batchObject + ?.renderView as NodeRenderView if (!rv) { - rv = this.batcher.getRenderView( - results[k].object.uuid, + const index: number | undefined = results[k].faceIndex !== undefined ? results[k].faceIndex : results[k].index - ) + if (index) rv = this.batcher.getRenderView(results[k].object.uuid, index) } if (rv) { queryResult.push({ nodeId: rv.renderData.id, point: results[k].point }) @@ -1016,42 +1050,47 @@ export default class SpeckleRenderer { return queryResult } + // TO DO: Maybe need a better way public renderViewFromIntersection( intersection: ExtendedIntersection - ): NodeRenderView { + ): NodeRenderView | null { let rv = null if (intersection.batchObject) { rv = intersection.batchObject.renderView const material = (intersection.object as SpeckleMesh).getBatchObjectMaterial( intersection.batchObject ) - if (material.opacity === 0 && this.IGNORE_ZERO_OPACITY_OBJECTS) return null + if (material && material.opacity === 0 && this.IGNORE_ZERO_OPACITY_OBJECTS) + return null } else { - rv = this.batcher.getRenderView( - intersection.object.uuid, + const index = intersection.faceIndex !== undefined ? intersection.faceIndex : intersection.index - ) - if (rv) { - const material = this.batcher.getRenderViewMaterial( - intersection.object.uuid, - intersection.faceIndex !== undefined - ? intersection.faceIndex - : intersection.index - ) - if (material.opacity === 0 && this.IGNORE_ZERO_OPACITY_OBJECTS) return null + if (index) { + rv = this.batcher.getRenderView(intersection.object.uuid, index) + if (rv) { + const material = this.batcher.getRenderViewMaterial( + intersection.object.uuid, + index + ) + if (material && material.opacity === 0 && this.IGNORE_ZERO_OPACITY_OBJECTS) + return null + } } } return rv } - private onClick(e) { - const results: Array = this._intersections.intersect( + private onClick(e: Vector2 & { multiSelect: boolean; event: PointerEvent }) { + if (!this.renderingCamera) return + + const results: Array | null = this._intersections.intersect( this._scene, this.renderingCamera, e, + undefined, true, this.clippingVolume ) @@ -1085,11 +1124,14 @@ export default class SpeckleRenderer { this.viewer.emit(ViewerEvent.ObjectClicked, selectionInfo) } - private onDoubleClick(e) { - const results: Array = this._intersections.intersect( + private onDoubleClick(e: Vector2 & { multiSelect: boolean; event: PointerEvent }) { + if (!this.renderingCamera) return + + const results: Array | null = this._intersections.intersect( this._scene, this.renderingCamera, e, + undefined, true, this.clippingVolume ) @@ -1120,20 +1162,16 @@ export default class SpeckleRenderer { this.viewer.emit(ViewerEvent.ObjectDoubleClicked, selectionInfo) } - public boxFromObjects(objectIds: string[]) { + public boxFromObjects(objectIds: string[]): Box3 { let box = new Box3() const rvs: NodeRenderView[] = [] if (objectIds.length > 0) { for (let k = 0; k < objectIds.length; k++) { - const nodes = this.viewer.getWorldTree().findId(objectIds[k]) - nodes.forEach((node: TreeNode) => { - rvs.push( - ...this.viewer - .getWorldTree() - .getRenderTree() - .getRenderViewsForNode(node, node) - ) - }) + const nodes = this.tree.findId(objectIds[k]) + if (nodes) + nodes.forEach((node: TreeNode) => { + rvs.push(...this.tree.getRenderTree().getRenderViewsForNode(node)) + }) } } else box = this.sceneBox for (let k = 0; k < rvs.length; k++) { @@ -1215,7 +1253,7 @@ export default class SpeckleRenderer { return ids.reverse() } - public getBatchSize(batchId: string) { + public getBatchSize(batchId: string): number { return this.batcher.batches[batchId].renderViews.length } @@ -1225,23 +1263,25 @@ export default class SpeckleRenderer { } public getObjects(): BatchObject[] { - const batches = this.batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[] + const batches = this.batcher.getBatches(undefined, GeometryType.MESH) const meshes = batches.map((batch: MeshBatch) => batch.mesh) const objects = meshes.flatMap((mesh) => mesh.batchObjects) return objects } - public getObject(rv: NodeRenderView): BatchObject { + public getObject(rv: NodeRenderView): BatchObject | null { const batch = this.batcher.getBatch(rv) as MeshBatch if (batch.geometryType !== GeometryType.MESH) { // Logger.error('Render view is not of mesh type. No batch object found') return null } - return batch.mesh.batchObjects.find((value) => value.renderView.guid === rv.guid) + return batch.mesh.batchObjects.find( + (value) => value.renderView.guid === rv.guid + ) as BatchObject } public enableLayers(layers: ObjectLayers[], value: boolean) { - this.pipeline.composer.passes.forEach((pass: BaseSpecklePass) => { + this.pipeline.composer.passes.forEach((pass: Pass) => { if (!(pass instanceof BaseSpecklePass)) return layers.forEach((layer: ObjectLayers) => { pass.enableLayer(layer, value) diff --git a/packages/viewer/src/modules/UrlHelper.ts b/packages/viewer/src/modules/UrlHelper.ts index 4f407b576c..11d675170d 100644 --- a/packages/viewer/src/modules/UrlHelper.ts +++ b/packages/viewer/src/modules/UrlHelper.ts @@ -207,7 +207,9 @@ async function runModelLastVersionQuery( return `${ref.origin}/streams/${ref.projectId}/objects/${data.project.model.versions.items[0].referencedObject}` } catch (e) { Logger.error( - `Could not get object URLs for project ${ref.projectId} and model ${resource.modelId}. Error: ${e.message}` + `Could not get object URLs for project ${ref.projectId} and model ${ + resource.modelId + }. Error: ${e instanceof Error ? e.message : e}` ) } return '' @@ -245,7 +247,9 @@ async function runModelVersionQuery( return `${ref.origin}/streams/${ref.projectId}/objects/${data.project.model.version.referencedObject}` } catch (e) { Logger.error( - `Could not get object URLs for project ${ref.projectId} and model ${resource.modelId}. Error: ${e.message}` + `Could not get object URLs for project ${ref.projectId} and model ${ + resource.modelId + }. Error: ${e instanceof Error ? e.message : e}` ) } return '' @@ -292,7 +296,9 @@ async function runAllModelsQuery( return urls } catch (e) { Logger.error( - `Could not get object URLs for project ${ref.projectId}. Error: ${e.message}` + `Could not get object URLs for project ${ref.projectId}. Error: ${ + e instanceof Error ? e.message : e + }` ) } return [''] diff --git a/packages/viewer/src/modules/Viewer.ts b/packages/viewer/src/modules/Viewer.ts index 2cbcbb5295..5e00cde802 100644 --- a/packages/viewer/src/modules/Viewer.ts +++ b/packages/viewer/src/modules/Viewer.ts @@ -4,31 +4,32 @@ import EventEmitter from './EventEmitter' import { Clock, Texture } from 'three' import { Assets } from './Assets' -import { Optional } from '../helpers/typeHelper' +import { type Optional } from '../helpers/typeHelper' import { DefaultViewerParams, - IViewer, - SpeckleView, - SunLightConfiguration, + type IViewer, + type SpeckleView, + type SunLightConfiguration, UpdateFlags, ViewerEvent, - ViewerParams + type ViewerParams, + type ViewerEventPayload } from '../IViewer' import { World } from './World' -import { TreeNode, WorldTree } from './tree/WorldTree' +import { type TreeNode, WorldTree } from './tree/WorldTree' import SpeckleRenderer from './SpeckleRenderer' -import { PropertyInfo, PropertyManager } from './filtering/PropertyManager' -import { DataTree, DataTreeBuilder } from './tree/DataTree' +import { type PropertyInfo, PropertyManager } from './filtering/PropertyManager' import Logger from 'js-logger' -import { Query, QueryArgsResultMap, QueryResult } from './queries/Query' +import type { Query, QueryArgsResultMap } from './queries/Query' import { Queries } from './queries/Queries' -import { Utils } from './Utils' +import { type Utils } from './Utils' import { Extension } from './extensions/Extension' import Input from './input/Input' import { CameraController } from './extensions/CameraController' import { SpeckleType } from './loaders/GeometryConverter' import { Loader } from './loaders/Loader' import { type Constructor } from 'type-fest' +import { RenderTree } from './tree/RenderTree' export class Viewer extends EventEmitter implements IViewer { /** Container and optional stats element */ @@ -55,7 +56,7 @@ export class Viewer extends EventEmitter implements IViewer { } = {} /** various utils/helpers */ - protected utils: Utils + protected utils: Utils | undefined /** Gets the World object. Currently it's used for info mostly */ public get World(): World { return this.world @@ -87,21 +88,24 @@ export class Viewer extends EventEmitter implements IViewer { } public createExtension(type: Constructor): T { - const extensionsToInject: Array Extension> = - type.prototype.inject + const extensionsToInject: Array< + new (viewer: IViewer, ...args: Extension[]) => Extension + > = type.prototype.inject const injectedExtensions: Array = [] - extensionsToInject.forEach((value: new (viewer: IViewer, ...args) => Extension) => { - if (this.extensions[value.name]) { - injectedExtensions.push(this.extensions[value.name]) - return - } - for (const k in this.extensions) { - const prototypeChain = this.getConstructorChain(this.extensions[k]) - if (prototypeChain.includes(value.name)) { - injectedExtensions.push(this.extensions[k]) + extensionsToInject.forEach( + (value: new (viewer: IViewer, ...args: Extension[]) => Extension) => { + if (this.extensions[value.name]) { + injectedExtensions.push(this.extensions[value.name]) + return + } + for (const k in this.extensions) { + const prototypeChain = this.getConstructorChain(this.extensions[k]) + if (prototypeChain.includes(value.name)) { + injectedExtensions.push(this.extensions[k]) + } } } - }) + ) const extension = new type(this, ...injectedExtensions) this.extensions[type.name] = extension @@ -109,6 +113,18 @@ export class Viewer extends EventEmitter implements IViewer { } public getExtension(type: Constructor): T { + let extension + if ((extension = this.getExtensionInternal(type)) !== null) return extension + + throw new Error(`Could not get Extension of type ${type.name}. Is it created?`) + } + + public hasExtension(type: Constructor): boolean { + const extension = this.getExtensionInternal(type) + return extension ? true : false + } + + private getExtensionInternal(type: Constructor): T | null { if (this.extensions[type.name]) return this.extensions[type.name] as T else { for (const k in this.extensions) { @@ -118,6 +134,7 @@ export class Viewer extends EventEmitter implements IViewer { } } } + return null } public constructor( @@ -139,7 +156,7 @@ export class Viewer extends EventEmitter implements IViewer { this.clock = new Clock() this.inProgressOperations = 0 - this.speckleRenderer = new SpeckleRenderer(this) + this.speckleRenderer = new SpeckleRenderer(this.tree, this) this.speckleRenderer.create(this.container) window.addEventListener('resize', this.resize.bind(this), false) @@ -147,10 +164,6 @@ export class Viewer extends EventEmitter implements IViewer { this.frame() this.resize() - - this.on(ViewerEvent.LoadCancelled, (url: string) => { - Logger.warn(`Cancelled load for ${url}`) - }) } public getContainer() { @@ -170,7 +183,7 @@ export class Viewer extends EventEmitter implements IViewer { }) } - public requestRender(flags: number = UpdateFlags.RENDER) { + public requestRender(flags: UpdateFlags = UpdateFlags.RENDER) { if (flags & UpdateFlags.RENDER) { this.speckleRenderer.needsRender = true this.speckleRenderer.resetPipeline() @@ -225,26 +238,29 @@ export class Viewer extends EventEmitter implements IViewer { } } - public on(eventType: ViewerEvent, listener: (arg) => void): void { + on( + eventType: T, + listener: (arg: ViewerEventPayload[T]) => void + ): void { super.on(eventType, listener) } public getObjectProperties( - resourceURL: string = null, + resourceURL: string | null = null, bypassCache = true ): Promise { return this.propertyManager.getProperties(this.tree, resourceURL, bypassCache) } - public getDataTree(): DataTree { - return DataTreeBuilder.build(this.tree) + public getDataTree(): void { + Logger.error('DataTree has been deprecated! Please use WorldTree') } public getWorldTree(): WorldTree { return this.tree } - public query(query: T): QueryArgsResultMap[T['operation']] { + public query(query: T): QueryArgsResultMap[T['operation']] | null { if (Queries.isPointQuery(query)) { Queries.DefaultPointQuerySolver.setContext(this.speckleRenderer) return Queries.DefaultPointQuerySolver.solve(query) @@ -253,11 +269,7 @@ export class Viewer extends EventEmitter implements IViewer { Queries.DefaultIntersectionQuerySolver.setContext(this.speckleRenderer) return Queries.DefaultIntersectionQuerySolver.solve(query) } - } - public queryAsync(query: Query): Promise { - //TO DO - query return null } @@ -283,11 +295,11 @@ export class Viewer extends EventEmitter implements IViewer { return new Promise((resolve) => { // const sectionBoxVisible = this.sectionBox.display.visible // if (sectionBoxVisible) { - // this.sectionBox.displayOff() + // this.sectionBox.visible = false // } const screenshot = this.speckleRenderer.renderer.domElement.toDataURL('image/png') // if (sectionBoxVisible) { - // this.sectionBox.displayOn() + // this.sectionBox.visible = true // } resolve(screenshot) }) @@ -298,14 +310,20 @@ export class Viewer extends EventEmitter implements IViewer { */ public async loadObject(loader: Loader, zoomToObject = true) { - if (++this.inProgressOperations === 1) - (this as EventEmitter).emit(ViewerEvent.Busy, true) + if (++this.inProgressOperations === 1) this.emit(ViewerEvent.Busy, true) this.loaders[loader.resource] = loader const treeBuilt = await loader.load() if (treeBuilt) { + const renderTree: RenderTree | null = this.tree.getRenderTree(loader.resource) + /** Catering to typescript + * The render tree can't be null, we've just built it + */ + if (!renderTree) { + throw new Error(`Could not get render tree ${loader.resource}`) + } const t0 = performance.now() - for await (const step of this.speckleRenderer.addRenderTree(loader.resource)) { + for await (const step of this.speckleRenderer.addRenderTree(renderTree)) { step if (zoomToObject) { const extension = this.getExtension(CameraController) @@ -324,26 +342,23 @@ export class Viewer extends EventEmitter implements IViewer { if (this.loaders[loader.resource]) this.loaders[loader.resource].dispose() delete this.loaders[loader.resource] - if (--this.inProgressOperations === 0) - (this as EventEmitter).emit(ViewerEvent.Busy, false) + if (--this.inProgressOperations === 0) this.emit(ViewerEvent.Busy, false) } public async cancelLoad(resource: string, unload = false) { this.loaders[resource].cancel() - this.tree.getRenderTree(resource).cancelBuild(resource) + this.tree.getRenderTree(resource)?.cancelBuild() this.speckleRenderer.cancelRenderTree(resource) if (unload) { await this.unloadObject(resource) } else { - if (--this.inProgressOperations === 0) - (this as EventEmitter).emit(ViewerEvent.Busy, false) + if (--this.inProgressOperations === 0) this.emit(ViewerEvent.Busy, false) } } public async unloadObject(resource: string) { try { - if (++this.inProgressOperations === 1) - (this as EventEmitter).emit(ViewerEvent.Busy, true) + if (++this.inProgressOperations === 1) this.emit(ViewerEvent.Busy, true) if (this.tree.findSubtree(resource)) { if (this.loaders[resource]) { await this.cancelLoad(resource, true) @@ -351,38 +366,37 @@ export class Viewer extends EventEmitter implements IViewer { } delete this.loaders[resource] this.speckleRenderer.removeRenderTree(resource) - this.tree.getRenderTree(resource).purge() + this.tree.getRenderTree(resource)?.purge() this.tree.purge(resource) this.requestRender(UpdateFlags.RENDER | UpdateFlags.SHADOWS) } } finally { if (--this.inProgressOperations === 0) { - ;(this as EventEmitter).emit(ViewerEvent.Busy, false) + this.emit(ViewerEvent.Busy, false) Logger.warn(`Removed subtree ${resource}`) - ;(this as EventEmitter).emit(ViewerEvent.UnloadComplete, resource) + this.emit(ViewerEvent.UnloadComplete, resource) } } } public async unloadAll() { try { - if (++this.inProgressOperations === 1) - (this as EventEmitter).emit(ViewerEvent.Busy, true) + if (++this.inProgressOperations === 1) this.emit(ViewerEvent.Busy, true) for (const key of Object.keys(this.loaders)) { if (this.loaders[key]) await this.cancelLoad(key, false) delete this.loaders[key] } - this.tree.root.children.forEach((node) => { + this.tree.root.children.forEach((node: TreeNode) => { this.speckleRenderer.removeRenderTree(node.model.id) - this.tree.getRenderTree().purge() + this.tree.getRenderTree()?.purge() }) this.tree.purge() } finally { if (--this.inProgressOperations === 0) { - ;(this as EventEmitter).emit(ViewerEvent.Busy, false) + this.emit(ViewerEvent.Busy, false) Logger.warn(`Removed all subtrees`) - ;(this as EventEmitter).emit(ViewerEvent.UnloadAllComplete) + this.emit(ViewerEvent.UnloadAllComplete) } } } diff --git a/packages/viewer/src/modules/batching/Batch.ts b/packages/viewer/src/modules/batching/Batch.ts index c880ed9149..f95e2f6396 100644 --- a/packages/viewer/src/modules/batching/Batch.ts +++ b/packages/viewer/src/modules/batching/Batch.ts @@ -1,5 +1,5 @@ import { Box3, Material, Object3D, WebGLRenderer } from 'three' -import { FilterMaterialOptions } from '../materials/Materials' +import { type FilterMaterialOptions } from '../materials/Materials' import { NodeRenderView } from '../tree/NodeRenderView' export enum GeometryType { @@ -10,6 +10,12 @@ export enum GeometryType { TEXT } +export interface DrawGroup { + start: number + count: number + materialIndex: number +} + /** TO DO: Unify point and mesh batch implementations */ export interface Batch { id: string @@ -31,22 +37,22 @@ export interface Batch { getCount(): number setBatchMaterial(material: Material): void - setBatchBuffers(...range: BatchUpdateRange[]): void - setVisibleRange(...range: BatchUpdateRange[]) + setBatchBuffers(range: BatchUpdateRange[]): void + setVisibleRange(range: BatchUpdateRange[]): void getVisibleRange(): BatchUpdateRange - setDrawRanges(...ranges: BatchUpdateRange[]) - resetDrawRanges() - buildBatch() - getRenderView(index: number): NodeRenderView - getMaterialAtIndex(index: number): Material - getMaterial(rv: NodeRenderView): Material + setDrawRanges(ranges: BatchUpdateRange[]): void + resetDrawRanges(): void + buildBatch(): void + getRenderView(index: number): NodeRenderView | null + getMaterialAtIndex(index: number): Material | null + getMaterial(rv: NodeRenderView): Material | null getOpaque(): BatchUpdateRange getTransparent(): BatchUpdateRange getStencil(): BatchUpdateRange getDepth(): BatchUpdateRange - onUpdate(deltaTime: number) - onRender?(renderer: WebGLRenderer) - purge() + onUpdate(deltaTime?: number): void + onRender?(renderer: WebGLRenderer): void + purge(): void } export interface BatchUpdateRange { @@ -65,16 +71,6 @@ export const AllBatchUpdateRange = { offset: 0, count: Infinity } as BatchUpdateRange -export interface DrawGroup { - start: number - count: number - materialIndex?: number -} -export interface DrawGroup { - start: number - count: number - materialIndex?: number -} export const INSTANCE_TRANSFORM_BUFFER_STRIDE = 16 export const INSTANCE_GRADIENT_BUFFER_STRIDE = 1 diff --git a/packages/viewer/src/modules/batching/BatchObject.ts b/packages/viewer/src/modules/batching/BatchObject.ts index db612722cb..bc7c7b4337 100644 --- a/packages/viewer/src/modules/batching/BatchObject.ts +++ b/packages/viewer/src/modules/batching/BatchObject.ts @@ -12,6 +12,8 @@ export type VectorLike = | { x: number; y: number; z?: number; w?: number } | undefined | null +export type Vector3Like = VectorLike & { z: number } +export type Vector4Like = Vector3Like & { w: number } export class BatchObject { protected _renderView: NodeRenderView @@ -21,8 +23,8 @@ export class BatchObject { public transform: Matrix4 public transformInv: Matrix4 - public tasVertIndexStart: number - public tasVertIndexEnd: number + public tasVertIndexStart!: number + public tasVertIndexEnd!: number public quaternion: Quaternion = new Quaternion() public eulerValue: Euler = new Euler() @@ -53,14 +55,13 @@ export class BatchObject { return this._batchIndex } - public get speckleId(): string { - return this._renderView.renderData.id - } - public get aabb(): Box3 { - const box = new Box3().copy(this.renderView.aabb) - box.applyMatrix4(this.transform) - return box + if (this.renderView.aabb) { + const box = new Box3().copy(this.renderView.aabb) + box.applyMatrix4(this.transform) + return box + } + return new Box3() } public get localOrigin(): Vector3 { @@ -117,11 +118,13 @@ export class BatchObject { transform.invert() if (!bvh) { - const indices = this._renderView.renderData.geometry.attributes.INDEX - const position = this._renderView.renderData.geometry.attributes.POSITION + const indices: number[] | undefined = + this._renderView.renderData.geometry.attributes?.INDEX + const position: number[] | undefined = + this._renderView.renderData.geometry.attributes?.POSITION bvh = AccelerationStructure.buildBVH( indices, - new Float32Array(position), + position, DefaultBVHOptions, transform ) @@ -137,10 +140,10 @@ export class BatchObject { } public transformTRS( - translation: VectorLike, - euler: VectorLike, - scale: VectorLike, - pivot: VectorLike + translation: Vector3Like, + euler?: Vector3Like, + scale?: Vector3Like, + pivot?: Vector3Like ) { let T: Matrix4 = BatchObject.matBuff0.identity() let R: Matrix4 = BatchObject.matBuff1.identity() diff --git a/packages/viewer/src/modules/batching/Batcher.ts b/packages/viewer/src/modules/batching/Batcher.ts index 1085c95aff..772ed53793 100644 --- a/packages/viewer/src/modules/batching/Batcher.ts +++ b/packages/viewer/src/modules/batching/Batcher.ts @@ -1,8 +1,17 @@ import { MathUtils } from 'three' import LineBatch from './LineBatch' -import Materials, { FilterMaterialType } from '../materials/Materials' +import Materials, { + FilterMaterialType, + type DisplayStyle, + type RenderMaterial +} from '../materials/Materials' import { NodeRenderView } from '../tree/NodeRenderView' -import { Batch, BatchUpdateRange, GeometryType, NoneBatchUpdateRange } from './Batch' +import { + type Batch, + type BatchUpdateRange, + GeometryType, + NoneBatchUpdateRange +} from './Batch' import { Material, WebGLRenderer } from 'three' import Logger from 'js-logger' import { AsyncPause } from '../World' @@ -10,12 +19,20 @@ import { RenderTree } from '../tree/RenderTree' import TextBatch from './TextBatch' import SpeckleMesh, { TransformStorage } from '../objects/SpeckleMesh' import { SpeckleType } from '../loaders/GeometryConverter' -import { TreeNode, WorldTree } from '../..' +import { type TreeNode, WorldTree } from '../..' import { InstancedMeshBatch } from './InstancedMeshBatch' import { Geometry } from '../converter/Geometry' import { MeshBatch } from './MeshBatch' import { PointBatch } from './PointBatch' +type BatchTypeMap = { + [GeometryType.MESH]: MeshBatch + [GeometryType.LINE]: LineBatch + [GeometryType.POINT]: PointBatch + [GeometryType.POINT_CLOUD]: PointBatch + [GeometryType.TEXT]: TextBatch +} + export default class Batcher { private maxHardwareUniformCount = 0 private floatTextures = false @@ -41,7 +58,6 @@ export default class Batcher { speckleType: SpeckleType[], batchType?: GeometryType ) { - const start = performance.now() let min = Number.MAX_SAFE_INTEGER, max = -1, average = 0, @@ -51,7 +67,6 @@ export default class Batcher { const instancedBatches: { [id: string]: Array } = {} const pause = new AsyncPause() - const startInstancedGathering = performance.now() for (const g in instanceGroups) { pause.tick(100) if (pause.needsWait) { @@ -81,13 +96,10 @@ export default class Batcher { } instancedBatches[vertCount].push(g) } - const instancedGathering = performance.now() - startInstancedGathering - - let deInstancing = 0 - let instanceBuild = 0 for (const v in instancedBatches) { for (let k = 0; k < instancedBatches[v].length; k++) { const nodes = worldTree.findId(instancedBatches[v][k]) + if (!nodes) continue /** Make sure entire instance set is instanced */ let instanced = true nodes.every((node: TreeNode) => (instanced &&= node.model.instanced)) @@ -98,7 +110,6 @@ export default class Batcher { .filter((rv) => rv) if (Number.parseInt(v) < this.minInstancedBatchVertices || !instanced) { - const t0 = performance.now() rvs.forEach((nodeRv) => { const geometry = nodeRv.renderData.geometry geometry.instanced = false @@ -118,28 +129,24 @@ export default class Batcher { Geometry.transformGeometryData(geometry, geometry.transform) nodeRv.computeAABB() }) - deInstancing += performance.now() - t0 continue } - const t1 = performance.now() const materialHash = rvs[0].renderMaterialHash const instancedBatch = await this.buildInstancedBatch( renderTree, rvs, materialHash ) - instanceBuild += performance.now() - t1 + if (!instancedBatch) continue this.batches[instancedBatch.id] = instancedBatch min = Math.min(min, instancedBatch.renderViews.length) max = Math.max(max, instancedBatch.renderViews.length) - average += instancedBatch.renderViews.length batchCount++ yield this.batches[instancedBatch.id] } } - const totalInstanced = performance.now() - start const renderViews = renderTree .getRenderableNodes(...speckleType) @@ -193,6 +200,8 @@ export default class Batcher { batchType ) + if (!batch) continue + this.batches[batch.id] = batch min = Math.min(min, batch.renderViews.length) max = Math.max(max, batch.renderViews.length) @@ -206,10 +215,6 @@ export default class Batcher { average / materialHashes.length }` ) - Logger.warn('Total instanced -> ', totalInstanced) - Logger.warn('Instance gathering -> ', instancedGathering) - Logger.warn('De-instancing -> ', deInstancing) - Logger.warn('Instanced build -> ', instanceBuild) } private splitBatch( @@ -217,19 +222,29 @@ export default class Batcher { vertCount: number ): NodeRenderView[][] { /** We're first splitting based on the batch's max vertex count */ - const vSplit = [] + const vSplit: Array> = [] const vDiv = Math.floor(vertCount / this.maxBatchVertices) if (vDiv > 0) { let count = 0 let index = 0 vSplit.push([]) for (let k = 0; k < renderViews.length; k++) { + /** Catering to typescript. + * RenderViews are prefiltered based on valid geometry before reaching this point + */ + const ervee = renderViews[k] + const nextErvee = renderViews[k + 1] + if (!ervee.renderData.geometry.attributes) { + throw new Error( + `Invalid geometry on render view ${renderViews[k].renderData.id}` + ) + } vSplit[index].push(renderViews[k]) - count += renderViews[k].renderData.geometry.attributes.POSITION.length / 3 + count += ervee.renderData.geometry.attributes.POSITION.length / 3 const nexCount = count + - (renderViews[k + 1] - ? renderViews[k + 1].renderData.geometry.attributes.POSITION.length / 3 + (nextErvee && nextErvee.renderData.geometry.attributes + ? nextErvee.renderData.geometry.attributes.POSITION.length / 3 : 0) if (nexCount >= this.maxBatchVertices && renderViews[k + 1]) { vSplit.push([]) @@ -267,7 +282,7 @@ export default class Batcher { renderTree: RenderTree, renderViews: NodeRenderView[], materialHash: number - ): Promise { + ): Promise { if (!renderViews.length) { /** This is for the case when all renderviews have invalid geometries, and it generally * means there is something wrong with the stream @@ -294,7 +309,7 @@ export default class Batcher { renderViews: NodeRenderView[], materialHash: number, batchType?: GeometryType - ): Promise { + ): Promise { if (!renderViews.length) { /** This is for the case when all renderviews have invalid geometries, and it generally * means there is something wrong with the stream @@ -308,7 +323,8 @@ export default class Batcher { const geometryType = batchType !== undefined ? batchType : renderViews[0].geometryType - let matRef = null + let matRef: RenderMaterial | DisplayStyle | null = + renderViews[0].renderData.renderMaterial if (geometryType === GeometryType.MESH) { matRef = renderViews[0].renderData.renderMaterial @@ -325,7 +341,7 @@ export default class Batcher { const material = this.materials.getMaterial(materialHash, matRef, geometryType) const batchID = MathUtils.generateUUID() - let geometryBatch: Batch = null + let geometryBatch: Batch | null = null switch (geometryType) { case GeometryType.MESH: geometryBatch = new MeshBatch( @@ -365,12 +381,13 @@ export default class Batcher { public render(renderer: WebGLRenderer) { for (const batchId in this.batches) { - if (this.batches[batchId].onRender) this.batches[batchId].onRender(renderer) + const batch = this.batches[batchId] + if (batch.onRender) batch.onRender(renderer) } } public saveVisiblity(): Record { - const visibilityRanges = {} + const visibilityRanges: Record = {} for (const k in this.batches) { const batch: Batch = this.batches[k] visibilityRanges[k] = batch.getVisibleRange() @@ -383,15 +400,15 @@ export default class Batcher { const batch: Batch = this.batches[k] const range = ranges[k] if (!range) { - batch.setVisibleRange(NoneBatchUpdateRange) + batch.setVisibleRange([NoneBatchUpdateRange]) } else { - batch.setVisibleRange(range) + batch.setVisibleRange([range]) } } } public getTransparent(): Record { - const visibilityRanges = {} + const visibilityRanges: Record = {} for (const k in this.batches) { visibilityRanges[k] = this.batches[k].getTransparent() } @@ -399,7 +416,7 @@ export default class Batcher { } public getStencil(): Record { - const visibilityRanges = {} + const visibilityRanges: Record = {} for (const k in this.batches) { visibilityRanges[k] = this.batches[k].getStencil() } @@ -407,7 +424,7 @@ export default class Batcher { } public getOpaque(): Record { - const visibilityRanges = {} + const visibilityRanges: Record = {} for (const k in this.batches) { visibilityRanges[k] = this.batches[k].getOpaque() } @@ -415,7 +432,7 @@ export default class Batcher { } public getDepth(): Record { - const visibilityRanges = {} + const visibilityRanges: Record = {} for (const k in this.batches) { visibilityRanges[k] = this.batches[k].getDepth() } @@ -450,13 +467,38 @@ export default class Batcher { } } - public getBatches(subtreeId?: string, geometryType?: GeometryType) { - return Object.values(this.batches).filter((value: Batch) => { + public getBatches( + subtreeId?: string, + geometryType?: K + ): BatchTypeMap[K][] { + const batches: Batch[] = Object.values(this.batches) + return batches.filter((value: Batch) => { const subtree = subtreeId !== undefined ? value.subtreeId === subtreeId : true const type = - geometryType !== undefined ? value.geometryType === geometryType : true + geometryType !== undefined ? this.isBatchType(value, geometryType) : true return subtree && type - }) + }) as BatchTypeMap[K][] + } + + private isBatchType( + batch: Batch, + geometryType?: K + ): batch is BatchTypeMap[K] { + if (geometryType === undefined) return true + switch (geometryType) { + case GeometryType.MESH: + return batch instanceof MeshBatch + case GeometryType.LINE: + return batch instanceof LineBatch + case GeometryType.POINT: + return batch instanceof PointBatch + case GeometryType.POINT_CLOUD: + return batch instanceof PointBatch + case GeometryType.TEXT: + return batch instanceof TextBatch + default: + return false + } } public getBatch(rv: NodeRenderView) { @@ -490,32 +532,18 @@ export default class Batcher { /** * Used for debuggin only */ - - public async isolateRenderViewBatch(id: string, renderTree: RenderTree) { - const rv = renderTree.getRenderViewForNodeId(id) - for (const k in this.batches) { - if (k !== rv.batchId) { - this.batches[k].setDrawRanges({ - offset: 0, - count: this.batches[k].getCount(), - material: this.materials.getFilterMaterial(this.batches[k].renderViews[0], { - filterType: FilterMaterialType.GHOST - }) - }) - } - } - } - public async isolateBatch(batchId: string) { for (const k in this.batches) { if (k !== batchId) { - this.batches[k].setDrawRanges({ - offset: 0, - count: this.batches[k].getCount(), - material: this.materials.getFilterMaterial(this.batches[k].renderViews[0], { - filterType: FilterMaterialType.GHOST - }) - }) + this.batches[k].setDrawRanges([ + { + offset: 0, + count: this.batches[k].getCount(), + material: this.materials.getFilterMaterial(this.batches[k].renderViews[0], { + filterType: FilterMaterialType.GHOST + }) as Material + } + ]) } } } diff --git a/packages/viewer/src/modules/batching/DrawRanges.ts b/packages/viewer/src/modules/batching/DrawRanges.ts index 219030dedc..17f2979165 100644 --- a/packages/viewer/src/modules/batching/DrawRanges.ts +++ b/packages/viewer/src/modules/batching/DrawRanges.ts @@ -1,6 +1,6 @@ import { Material } from 'three' -import { BatchUpdateRange } from './Batch' -import { DrawGroup } from './Batch' +import { type BatchUpdateRange } from './Batch' +import { type DrawGroup } from './Batch' export class DrawRanges { public integrateRanges( @@ -12,14 +12,14 @@ export class DrawRanges { groups.sort((a, b) => a.start - b.start) ranges.sort((a, b) => a.offset - b.offset) - const edgesForward = {} - const edgesBackwards = {} + const edgesForward: { [key: number]: number } = {} + const edgesBackwards: { [key: number]: number } = {} for (let k = 0, l = groups.length - 1; k < groups.length; k++, l--) { const groupForward = groups[k] const groupBackwards = groups[l] - edgesForward[groupForward.start] = groupForward.materialIndex + edgesForward[groupForward.start] = groupForward.materialIndex as number edgesBackwards[groupBackwards.start + groupBackwards.count] = - groupBackwards.materialIndex + groupBackwards.materialIndex as number } _flatRanges = groups.map((group: DrawGroup) => { @@ -43,7 +43,7 @@ export class DrawRanges { }) _flatRanges = [...new Set(_flatRanges)] - const materialIndex = materials.indexOf(range.material) + const materialIndex = materials.indexOf(range.material as Material) edgesForward[r0] = materialIndex edgesForward[r1] = r1 >= next ? edgesForward[next] : edgesBackwards[next] } diff --git a/packages/viewer/src/modules/batching/InstancedBatchObject.ts b/packages/viewer/src/modules/batching/InstancedBatchObject.ts index ec8b17ef12..f599d27d18 100644 --- a/packages/viewer/src/modules/batching/InstancedBatchObject.ts +++ b/packages/viewer/src/modules/batching/InstancedBatchObject.ts @@ -1,5 +1,5 @@ /* eslint-disable camelcase */ -import { BatchObject, VectorLike } from './BatchObject' +import { BatchObject, type Vector3Like } from './BatchObject' import { Matrix4 } from 'three' import { NodeRenderView } from '../tree/NodeRenderView' @@ -9,17 +9,18 @@ export class InstancedBatchObject extends BatchObject { public constructor(renderView: NodeRenderView, batchIndex: number) { super(renderView, batchIndex) - this.instanceTransform.copy(renderView.renderData.geometry.transform) + if (renderView.renderData.geometry.transform) + this.instanceTransform.copy(renderView.renderData.geometry.transform) this.transform.copy(this.instanceTransform) this.transformInv.copy(new Matrix4().copy(this.instanceTransform).invert()) this.transformDirty = false } public transformTRS( - translation: VectorLike, - euler: VectorLike, - scale: VectorLike, - pivot: VectorLike + translation: Vector3Like, + euler: Vector3Like, + scale: Vector3Like, + pivot: Vector3Like ) { super.transformTRS(translation, euler, scale, pivot) this.transform.multiply(this.instanceTransform) diff --git a/packages/viewer/src/modules/batching/InstancedMeshBatch.ts b/packages/viewer/src/modules/batching/InstancedMeshBatch.ts index 736b7a2715..f283d74e95 100644 --- a/packages/viewer/src/modules/batching/InstancedMeshBatch.ts +++ b/packages/viewer/src/modules/batching/InstancedMeshBatch.ts @@ -14,8 +14,9 @@ import { Geometry } from '../converter/Geometry' import { NodeRenderView } from '../tree/NodeRenderView' import { AllBatchUpdateRange, - Batch, - BatchUpdateRange, + type Batch, + type BatchUpdateRange, + type DrawGroup, GeometryType, INSTANCE_TRANSFORM_BUFFER_STRIDE, NoneBatchUpdateRange @@ -31,7 +32,6 @@ import Logger from 'js-logger' import Materials from '../materials/Materials' import { DrawRanges } from './DrawRanges' import SpeckleStandardColoredMaterial from '../materials/SpeckleStandardColoredMaterial' -import { DrawGroup } from './Batch' export class InstancedMeshBatch implements Batch { public id: string @@ -40,12 +40,12 @@ export class InstancedMeshBatch implements Batch { private geometry: BufferGeometry public batchMaterial: Material public mesh: SpeckleInstancedMesh - private drawRanges: DrawRanges = new DrawRanges() + protected drawRanges: DrawRanges = new DrawRanges() - private instanceTransformBuffer0: Float32Array = null - private instanceTransformBuffer1: Float32Array = null + private instanceTransformBuffer0!: Float32Array + private instanceTransformBuffer1!: Float32Array private transformBufferIndex: number = 0 - private instanceGradientBuffer: Float32Array = null + private instanceGradientBuffer!: Float32Array private needsShuffle = false @@ -67,7 +67,11 @@ export class InstancedMeshBatch implements Batch { } public get triCount(): number { - return (this.geometry.index.count / 3) * this.renderViews.length + /** Catering to typescript + * There is no unniverse where the geometry is non-indexed. We're **explicitly** setting the index at creation time + */ + const indexCount = this.geometry.index ? this.geometry.index.count : 0 + return (indexCount / 3) * this.renderViews.length } public get vertCount(): number { @@ -125,7 +129,7 @@ export class InstancedMeshBatch implements Batch { } /** Note: You can only set visibility on ranges that exist as draw groups! */ - public setVisibleRange(...ranges: BatchUpdateRange[]) { + public setVisibleRange(ranges: BatchUpdateRange[]) { /** Entire batch needs to NOT be drawn */ if (ranges.length === 1 && ranges[0] === NoneBatchUpdateRange) { this.mesh.children.forEach((instance) => (instance.visible = false)) @@ -139,14 +143,15 @@ export class InstancedMeshBatch implements Batch { this.mesh.children.forEach((instance) => (instance.visible = false)) ranges.forEach((range) => { - const instanceIndex = this.groups.indexOf( - this.groups.find( - (group: DrawGroup) => - range.offset === group.start && - range.offset + range.count === group.start + group.count - ) + const foundInstance = this.groups.find( + (group: DrawGroup) => + range.offset === group.start && + range.offset + range.count === group.start + group.count ) - if (instanceIndex !== -1) this.mesh.children[instanceIndex].visible = true + if (foundInstance) { + const instanceIndex = this.groups.indexOf(foundInstance) + if (instanceIndex !== -1) this.mesh.children[instanceIndex].visible = true + } }) } @@ -166,6 +171,7 @@ export class InstancedMeshBatch implements Batch { public getOpaque(): BatchUpdateRange { /** If there is any transparent or hidden group return the update range up to it's offset */ const transparentOrHiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return ( Materials.isTransparent(this.materials[value.materialIndex]) || this.materials[value.materialIndex].visible === false @@ -185,6 +191,7 @@ export class InstancedMeshBatch implements Batch { public getDepth(): BatchUpdateRange { /** If there is any transparent or hidden group return the update range up to it's offset */ const transparentOrHiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return ( Materials.isTransparent(this.materials[value.materialIndex]) || this.materials[value.materialIndex].visible === false || @@ -205,10 +212,12 @@ export class InstancedMeshBatch implements Batch { public getTransparent(): BatchUpdateRange { /** Look for a transparent group */ const transparentGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return Materials.isTransparent(this.materials[value.materialIndex]) }) /** Look for a hidden group */ const hiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return this.materials[value.materialIndex].visible === false }) /** If there is a transparent group return it's range */ @@ -234,6 +243,7 @@ export class InstancedMeshBatch implements Batch { if (this.materials[0].stencilWrite === true) return AllBatchUpdateRange } const stencilGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return this.materials[value.materialIndex].stencilWrite === true }) if (stencilGroup) { @@ -246,28 +256,33 @@ export class InstancedMeshBatch implements Batch { return NoneBatchUpdateRange } - public setBatchBuffers(...range: BatchUpdateRange[]): void { - for (let k = 0; k < range.length; k++) { - if (range[k].materialOptions) { - if (range[k].materialOptions.rampIndex !== undefined) { - const start = range[k].offset + public setBatchBuffers(ranges: BatchUpdateRange[]): void { + for (let k = 0; k < ranges.length; k++) { + const range = ranges[k] + if (range.materialOptions) { + if ( + range.materialOptions.rampIndex !== undefined && + range.materialOptions.rampWidth !== undefined + ) { + const start = ranges[k].offset /** The ramp indices specify the *begining* of each ramp color. When sampling with Nearest filter (since we don't want filtering) * we'll always be sampling right at the edge between texels. Most GPUs will sample consistently, but some won't and we end up with * a ton of artifacts. To avoid this, we are shifting the sampling indices so they're right on the center of each texel, so no inconsistent * sampling can occur. */ - const shiftedIndex = - range[k].materialOptions.rampIndex + - 0.5 / range[k].materialOptions.rampWidth - this.updateGradientIndexBufferData(start / 16, shiftedIndex) + if (range.materialOptions.rampIndex && range.materialOptions.rampWidth) { + const shiftedIndex = + range.materialOptions.rampIndex + 0.5 / range.materialOptions.rampWidth + this.updateGradientIndexBufferData(start / 16, shiftedIndex) + } } /** We need to update the texture here, because each batch uses it's own clone for any material we use on it * because otherwise three.js won't properly update our custom uniforms */ - if (range[k].materialOptions.rampTexture !== undefined) { - if (range[k].material instanceof SpeckleStandardColoredMaterial) { - ;(range[k].material as SpeckleStandardColoredMaterial).setGradientTexture( - range[k].materialOptions.rampTexture + if (range.materialOptions.rampTexture !== undefined) { + if (range.material instanceof SpeckleStandardColoredMaterial) { + ;(range.material as SpeckleStandardColoredMaterial).setGradientTexture( + range.materialOptions.rampTexture ) } } @@ -275,15 +290,15 @@ export class InstancedMeshBatch implements Batch { } } - public setDrawRanges(...ranges: BatchUpdateRange[]) { + public setDrawRanges(ranges: BatchUpdateRange[]) { ranges.forEach((value: BatchUpdateRange) => { if (value.material) { value.material = this.mesh.getCachedMaterial(value.material) } }) - const materials = ranges.map((val) => { - return val.material + const materials: Array = ranges.map((val: BatchUpdateRange) => { + return val.material as Material }) const uniqueMaterials = [...Array.from(new Set(materials.map((value) => value)))] @@ -303,7 +318,7 @@ export class InstancedMeshBatch implements Batch { if (count !== this.renderViews.length * 16) { Logger.error(`Draw groups invalid on ${this.id}`) } - this.setBatchBuffers(...ranges) + this.setBatchBuffers(ranges) this.cleanMaterials() /** We shuffle only when above a certain fragmentation threshold. We don't want to be shuffling every single time */ if (this.drawCalls > this.maxDrawCalls) { @@ -335,7 +350,7 @@ export class InstancedMeshBatch implements Batch { } } - private shuffleDrawGroups() { + private shuffleDrawGroups(): void { const groups = this.groups .sort((a, b) => { return a.start - b.start @@ -354,10 +369,12 @@ export class InstancedMeshBatch implements Batch { return transparentOrder }) - const materialOrder = [] + const materialOrder: Array = [] groups.reduce((previousValue, currentValue) => { - if (previousValue.indexOf(currentValue.materialIndex) === -1) { - previousValue.push(currentValue.materialIndex) + if (currentValue.materialIndex !== undefined) { + if (previousValue.indexOf(currentValue.materialIndex) === -1) { + previousValue.push(currentValue.materialIndex) + } } return previousValue }, materialOrder) @@ -438,17 +455,20 @@ export class InstancedMeshBatch implements Batch { /** Solve hidden groups */ const hiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return this.materials[value.materialIndex].visible === false }) if (hiddenGroup) { - this.setVisibleRange({ - offset: 0, - count: hiddenGroup.start - }) + this.setVisibleRange([ + { + offset: 0, + count: hiddenGroup.start + } + ]) } } - public resetDrawRanges() { + public resetDrawRanges(): void { this.groups.length = 0 this.materials.length = 0 this.groups.push({ @@ -457,7 +477,7 @@ export class InstancedMeshBatch implements Batch { materialIndex: 0 }) this.materials.push(this.batchMaterial) - this.setVisibleRange(AllBatchUpdateRange) + this.setVisibleRange([AllBatchUpdateRange]) this.mesh.updateDrawGroups( this.getCurrentTransformBuffer(), this.getCurrentGradientBuffer() @@ -480,7 +500,7 @@ export class InstancedMeshBatch implements Batch { return this.instanceGradientBuffer } - public buildBatch() { + public buildBatch(): void { const batchObjects = [] let instanceBVH = null this.instanceTransformBuffer0 = new Float32Array( @@ -492,7 +512,17 @@ export class InstancedMeshBatch implements Batch { const targetInstanceTransformBuffer = this.getCurrentTransformBuffer() for (let k = 0; k < this.renderViews.length; k++) { - this.renderViews[k].renderData.geometry.transform.toArray( + /** Catering to typescript + * There is no unniverse where an instanced render view does not have a transform + * It's against it's definition + */ + const ervee = this.renderViews[k] + if (!ervee.renderData.geometry.transform) { + throw new Error( + `Instanced Render view with id ${ervee.renderData.id} has null transform!` + ) + } + ervee.renderData.geometry.transform.toArray( targetInstanceTransformBuffer, k * INSTANCE_TRANSFORM_BUFFER_STRIDE ) @@ -509,11 +539,13 @@ export class InstancedMeshBatch implements Batch { batchObject.localOrigin.z ) transform.invert() - const indices = this.renderViews[k].renderData.geometry.attributes.INDEX - const position = this.renderViews[k].renderData.geometry.attributes.POSITION + const indices: number[] | undefined = + this.renderViews[k].renderData.geometry.attributes?.INDEX + const position: number[] | undefined = this.renderViews[k].renderData.geometry + .attributes?.POSITION as number[] instanceBVH = AccelerationStructure.buildBVH( indices, - new Float32Array(position), + position, DefaultBVHOptions, transform ) @@ -524,25 +556,32 @@ export class InstancedMeshBatch implements Batch { batchObjects.push(batchObject) } - const indices = new Uint32Array( - this.renderViews[0].renderData.geometry.attributes.INDEX - ) - const positions = new Float64Array( - this.renderViews[0].renderData.geometry.attributes.POSITION - ) - const colors = new Float32Array( - this.renderViews[0].renderData.geometry.attributes.COLOR - ) + const indices: number[] | undefined = + this.renderViews[0].renderData.geometry.attributes?.INDEX + + const positions: number[] | undefined = + this.renderViews[0].renderData.geometry.attributes?.POSITION - this.makeInstancedMeshGeometry(indices, positions, colors) + const colors: number[] | undefined = + this.renderViews[0].renderData.geometry.attributes?.COLOR + + /** Catering to typescript + * There is no unniverse where indices or positions are undefined at this point + */ + if (!indices || !positions) { + throw new Error(`Cannot build batch ${this.id}. Undefined indices or positions`) + } + this.makeInstancedMeshGeometry( + positions.length >= 65535 || indices.length >= 65535 + ? new Uint32Array(indices) + : new Uint16Array(indices), + new Float64Array(positions), + colors ? new Float32Array(colors) : undefined + ) this.mesh = new SpeckleInstancedMesh(this.geometry) this.mesh.setBatchObjects(batchObjects) this.mesh.setBatchMaterial(this.batchMaterial) this.mesh.buildTAS() - const bounds = new Box3() - for (let k = 0; k < this.renderViews.length; k++) { - bounds.union(this.renderViews[k].aabb) - } this.geometry.boundingBox = this.mesh.TAS.getBoundingBox(new Box3()) this.geometry.boundingSphere = this.geometry.boundingBox.getBoundingSphere( @@ -564,19 +603,19 @@ export class InstancedMeshBatch implements Batch { ) } - public getRenderView(index: number): NodeRenderView { + public getRenderView(index: number): NodeRenderView | null { index Logger.warn('Deprecated! Use InstancedBatchObject') return null } - public getMaterialAtIndex(index: number): Material { + public getMaterialAtIndex(index: number): Material | null { index Logger.warn('Deprecated! Use InstancedBatchObject') return null } - public getMaterial(rv: NodeRenderView): Material { + public getMaterial(rv: NodeRenderView): Material | null { const group = this.groups.find((value) => { return ( rv.batchStart >= value.start && @@ -629,10 +668,9 @@ export class InstancedMeshBatch implements Batch { data[index] = value } - public purge() { + public purge(): void { this.renderViews.length = 0 this.geometry.dispose() this.batchMaterial.dispose() - this.mesh = null } } diff --git a/packages/viewer/src/modules/batching/LineBatch.ts b/packages/viewer/src/modules/batching/LineBatch.ts index 420ae26834..d911298477 100644 --- a/packages/viewer/src/modules/batching/LineBatch.ts +++ b/packages/viewer/src/modules/batching/LineBatch.ts @@ -1,9 +1,9 @@ import { + Box3, Color, DynamicDrawUsage, InstancedInterleavedBuffer, InterleavedBufferAttribute, - Line, Material, Object3D, Vector4, @@ -16,28 +16,28 @@ import SpeckleLineMaterial from '../materials/SpeckleLineMaterial' import { NodeRenderView } from '../tree/NodeRenderView' import { AllBatchUpdateRange, - Batch, - BatchUpdateRange, + type Batch, + type BatchUpdateRange, + type DrawGroup, GeometryType, NoneBatchUpdateRange } from './Batch' import { ObjectLayers } from '../../IViewer' -import { DrawGroup } from './Batch' import Materials from '../materials/Materials' export default class LineBatch implements Batch { public id: string public subtreeId: string public renderViews: NodeRenderView[] - private geometry: LineSegmentsGeometry + protected geometry: LineSegmentsGeometry public batchMaterial: SpeckleLineMaterial - private mesh: LineSegments2 | Line - public colorBuffer: InstancedInterleavedBuffer + protected mesh: LineSegments2 + public colorBuffer!: InstancedInterleavedBuffer private static readonly vector4Buffer: Vector4 = new Vector4() - public get bounds() { + public get bounds(): Box3 { if (!this.geometry.boundingBox) this.geometry.computeBoundingBox() - return this.geometry.boundingBox + return this.geometry.boundingBox ? this.geometry.boundingBox : new Box3() } public get drawCalls(): number { @@ -65,7 +65,11 @@ export default class LineBatch implements Batch { return 0 } public get lineCount(): number { - return (this.geometry.index.count / 3) * this.geometry['_maxInstanceCount'] + /** Catering to typescript + * There is no unniverse where the geometry is non-indexed. LineSegments2 are **explicitly** indexed + */ + const indexCount = this.geometry.index ? this.geometry.index.count : 0 + return (indexCount / 3) * (this.geometry as never)['_maxInstanceCount'] } public get renderObject(): Object3D { @@ -73,11 +77,11 @@ export default class LineBatch implements Batch { } public get geometryType(): GeometryType { - return this.renderViews[0].geometryType + return GeometryType.LINE } public get materials(): Material[] { - return this.mesh.material as Material[] + return this.mesh.material as unknown as Material[] } public get groups(): DrawGroup[] { @@ -100,7 +104,7 @@ export default class LineBatch implements Batch { renderer.getDrawingBufferSize(this.batchMaterial.resolution) } - public setVisibleRange(...ranges: BatchUpdateRange[]) { + public setVisibleRange(ranges: BatchUpdateRange[]) { if ( ranges.length === 1 && ranges[0].offset === NoneBatchUpdateRange.offset && @@ -163,7 +167,7 @@ export default class LineBatch implements Batch { return NoneBatchUpdateRange } - public setBatchBuffers(...ranges: BatchUpdateRange[]): void { + public setBatchBuffers(ranges: BatchUpdateRange[]): void { const data = this.colorBuffer.array as number[] for (let i = 0; i < ranges.length; i++) { @@ -193,16 +197,18 @@ export default class LineBatch implements Batch { this.geometry.attributes['instanceColorEnd'].needsUpdate = true } - public setDrawRanges(...ranges: BatchUpdateRange[]) { - this.setBatchBuffers(...ranges) + public setDrawRanges(ranges: BatchUpdateRange[]) { + this.setBatchBuffers(ranges) } public resetDrawRanges() { - this.setDrawRanges({ - offset: 0, - count: Infinity, - material: this.batchMaterial - }) + this.setDrawRanges([ + { + offset: 0, + count: Infinity, + material: this.batchMaterial + } + ]) this.mesh.material = this.batchMaterial this.mesh.visible = true this.batchMaterial.transparent = false @@ -210,17 +216,22 @@ export default class LineBatch implements Batch { public buildBatch() { let attributeCount = 0 - this.renderViews.forEach( - (val: NodeRenderView) => - (attributeCount += val.needsSegmentConversion - ? (val.renderData.geometry.attributes.POSITION.length - 3) * 2 - : val.renderData.geometry.attributes.POSITION.length) - ) + this.renderViews.forEach((val: NodeRenderView) => { + if (!val.renderData.geometry.attributes) { + throw new Error(`Cannot build batch ${this.id}. Invalid geometry`) + } + attributeCount += val.needsSegmentConversion + ? (val.renderData.geometry.attributes.POSITION.length - 3) * 2 + : val.renderData.geometry.attributes.POSITION.length + }) const position = new Float64Array(attributeCount) let offset = 0 for (let k = 0; k < this.renderViews.length; k++) { const geometry = this.renderViews[k].renderData.geometry - let points = null + if (!geometry.attributes) { + throw new Error(`Cannot build batch ${this.id}. Invalid geometry`) + } + let points: Array /** We need to make sure the line geometry has a layout of : * start(x,y,z), end(x,y,z), start(x,y,z), end(x,y,z)... etc * Some geometries have that inherent form, some don't @@ -239,7 +250,7 @@ export default class LineBatch implements Batch { points[2 * i + 5] = geometry.attributes.POSITION[i + 5] } } else { - points = geometry.attributes.POSITION + points = geometry.attributes.POSITION as number[] } position.set(points, offset) @@ -259,7 +270,7 @@ export default class LineBatch implements Batch { this.mesh.layers.set(ObjectLayers.STREAM_CONTENT_LINE) } - public getRenderView(index: number): NodeRenderView { + public getRenderView(index: number): NodeRenderView | null { for (let k = 0; k < this.renderViews.length; k++) { if ( index >= this.renderViews[k].batchStart && @@ -270,6 +281,7 @@ export default class LineBatch implements Batch { return this.renderViews[k] } } + return null } public getMaterialAtIndex(index: number): Material { @@ -336,7 +348,6 @@ export default class LineBatch implements Batch { this.renderViews.length = 0 this.geometry.dispose() this.batchMaterial.dispose() - this.mesh = null this.colorBuffer.length = 0 } } diff --git a/packages/viewer/src/modules/batching/MeshBatch.ts b/packages/viewer/src/modules/batching/MeshBatch.ts index e22ae85188..6d4e847f17 100644 --- a/packages/viewer/src/modules/batching/MeshBatch.ts +++ b/packages/viewer/src/modules/batching/MeshBatch.ts @@ -9,37 +9,33 @@ import { DynamicDrawUsage, Sphere } from 'three' -import { GeometryType, BatchUpdateRange } from './Batch' -import { DrawGroup } from './Batch' import { PrimitiveBatch } from './PrimitiveBatch' import SpeckleMesh, { TransformStorage } from '../objects/SpeckleMesh' import Logger from 'js-logger' import { DrawRanges } from './DrawRanges' import { NodeRenderView } from '../tree/NodeRenderView' +import { type BatchUpdateRange, type DrawGroup, GeometryType } from './Batch' import { BatchObject } from './BatchObject' import { Geometry } from '../converter/Geometry' import { ObjectLayers } from '../../IViewer' export class MeshBatch extends PrimitiveBatch { - protected primitive: SpeckleMesh + protected primitive!: SpeckleMesh protected transformStorage: TransformStorage - private indexBuffer0: BufferAttribute - private indexBuffer1: BufferAttribute + private indexBuffer0!: BufferAttribute + private indexBuffer1!: BufferAttribute private indexBufferIndex = 0 - private drawRanges: DrawRanges = new DrawRanges() + protected drawRanges: DrawRanges = new DrawRanges() get bounds(): Box3 { return this.primitive.TAS.getBoundingBox(new Box3()) } get minDrawCalls(): number { - return [ - ...Array.from( - new Set(this.primitive.geometry.groups.map((value) => value.materialIndex)) - ) - ].length + return [...Array.from(new Set(this.groups.map((value) => value.materialIndex)))] + .length } get triCount(): number { @@ -100,6 +96,9 @@ export class MeshBatch extends PrimitiveBatch { end: number, value: number ): { minIndex: number; maxIndex: number } { + if (!this.primitive.geometry.index) { + throw new Error(`Invalid geometry on batch ${this.id}`) + } const index = this.primitive.geometry.index.array as number[] const data = this.gradientIndexBuffer.array as number[] let minVertexIndex = Infinity @@ -122,7 +121,7 @@ export class MeshBatch extends PrimitiveBatch { } } - public setDrawRanges(...ranges: BatchUpdateRange[]) { + public setDrawRanges(ranges: BatchUpdateRange[]) { // const current = this.groups.slice() // const incoming = ranges.slice() ranges.forEach((value: BatchUpdateRange) => { @@ -130,24 +129,22 @@ export class MeshBatch extends PrimitiveBatch { value.material = this.primitive.getCachedMaterial(value.material) } }) - const materials = ranges.map((val) => { - return val.material + const materials: Array = ranges.map((val: BatchUpdateRange) => { + return val.material as Material }) - const uniqueMaterials = [...Array.from(new Set(materials.map((value) => value)))] + const uniqueMaterials: Array = [ + ...Array.from(new Set(materials.map((value: Material) => value))) + ] for (let k = 0; k < uniqueMaterials.length; k++) { if (!this.materials.includes(uniqueMaterials[k])) this.materials.push(uniqueMaterials[k]) } - this.primitive.geometry.groups = this.drawRanges.integrateRanges( - this.groups, - this.materials, - ranges - ) + this.groups = this.drawRanges.integrateRanges(this.groups, this.materials, ranges) let count = 0 - this.primitive.geometry.groups.forEach((value) => (count += value.count)) + this.groups.forEach((value) => (count += value.count)) if (count !== this.getCount()) { // Logger.error('Current -> ', current) // Logger.error('Incoming -> ', incoming) @@ -158,7 +155,7 @@ export class MeshBatch extends PrimitiveBatch { }, ${this.getCount()}, ${this.getCount() - count}` ) } - this.setBatchBuffers(...ranges) + this.setBatchBuffers(ranges) this.cleanMaterials() if (this.drawCalls > this.minDrawCalls + 2) { @@ -196,13 +193,22 @@ export class MeshBatch extends PrimitiveBatch { let indicesCount = 0 let attributeCount = 0 for (let k = 0; k < this.renderViews.length; k++) { - indicesCount += this.renderViews[k].renderData.geometry.attributes.INDEX.length - attributeCount += - this.renderViews[k].renderData.geometry.attributes.POSITION.length + const ervee = this.renderViews[k] + /** Catering to typescript + * There is no unniverse where indices or positions are undefined at this point + */ + if ( + !ervee.renderData.geometry.attributes || + !ervee.renderData.geometry.attributes.INDEX + ) { + throw new Error(`Cannot build batch ${this.id}. Invalid geometry, or indices`) + } + indicesCount += ervee.renderData.geometry.attributes.INDEX.length + attributeCount += ervee.renderData.geometry.attributes.POSITION.length } const hasVertexColors = - this.renderViews[0].renderData.geometry.attributes.COLOR !== undefined + this.renderViews[0].renderData.geometry.attributes?.COLOR !== undefined const indices = new Uint32Array(indicesCount) const position = new Float64Array(attributeCount) const color = new Float32Array(hasVertexColors ? attributeCount : 0) @@ -215,6 +221,12 @@ export class MeshBatch extends PrimitiveBatch { for (let k = 0; k < this.renderViews.length; k++) { const geometry = this.renderViews[k].renderData.geometry + /** Catering to typescript + * There is no unniverse where indices or positions are undefined at this point + */ + if (!geometry.attributes || !geometry.attributes.INDEX) { + throw new Error(`Cannot build batch ${this.id}. Invalid geometry, or indices`) + } indices.set( geometry.attributes.INDEX.map((val) => val + offset / 3), arrayOffset @@ -246,7 +258,7 @@ export class MeshBatch extends PrimitiveBatch { indices, position, batchIndices, - hasVertexColors ? color : null + hasVertexColors ? color : undefined ) this.primitive = new SpeckleMesh(geometry) @@ -310,12 +322,12 @@ export class MeshBatch extends PrimitiveBatch { return geometry } - public getRenderView(index: number): NodeRenderView { + public getRenderView(index: number): NodeRenderView | null { index Logger.warn('Deprecated! Use BatchObject') return null } - public getMaterialAtIndex(index: number): Material { + public getMaterialAtIndex(index: number): Material | null { index Logger.warn('Deprecated! Use BatchObject') return null diff --git a/packages/viewer/src/modules/batching/PointBatch.ts b/packages/viewer/src/modules/batching/PointBatch.ts index ad54faa3f1..60da0ab4eb 100644 --- a/packages/viewer/src/modules/batching/PointBatch.ts +++ b/packages/viewer/src/modules/batching/PointBatch.ts @@ -9,17 +9,16 @@ import { Uint16BufferAttribute, DynamicDrawUsage } from 'three' -import { NodeRenderView } from '../..' -import { GeometryType, BatchUpdateRange } from './Batch' -import { DrawGroup } from './Batch' +import { Geometry } from '../converter/Geometry' +import { NodeRenderView } from '../tree/NodeRenderView' +import { type BatchUpdateRange, type DrawGroup, GeometryType } from './Batch' import { PrimitiveBatch } from './PrimitiveBatch' import { DrawRanges } from './DrawRanges' import Logger from 'js-logger' -import { Geometry } from '../converter/Geometry' import { ObjectLayers } from '../../IViewer' export class PointBatch extends PrimitiveBatch { - protected primitive: Points + protected primitive!: Points protected drawRanges: DrawRanges = new DrawRanges() public get geometryType(): GeometryType { @@ -29,6 +28,8 @@ export class PointBatch extends PrimitiveBatch { if (!this.primitive.geometry.boundingBox) this.primitive.geometry.computeBoundingBox() return this.primitive.geometry.boundingBox + ? this.primitive.geometry.boundingBox + : new Box3() } public get minDrawCalls(): number { @@ -54,9 +55,9 @@ export class PointBatch extends PrimitiveBatch { this.renderViews = renderViews } - public setDrawRanges(...ranges: BatchUpdateRange[]) { - const materials = ranges.map((val) => { - return val.material + public setDrawRanges(ranges: BatchUpdateRange[]) { + const materials: Array = ranges.map((val: BatchUpdateRange) => { + return val.material as Material }) const uniqueMaterials = [...Array.from(new Set(materials.map((value) => value)))] @@ -65,18 +66,14 @@ export class PointBatch extends PrimitiveBatch { this.materials.push(uniqueMaterials[k]) } - this.primitive.geometry.groups = this.drawRanges.integrateRanges( - this.groups, - this.materials, - ranges - ) + this.groups = this.drawRanges.integrateRanges(this.groups, this.materials, ranges) let count = 0 this.groups.forEach((value) => (count += value.count)) if (count !== this.getCount()) { Logger.error(`Draw groups invalid on ${this.id}`) } - this.setBatchBuffers(...ranges) + this.setBatchBuffers(ranges) this.cleanMaterials() if (this.drawCalls > this.minDrawCalls + 2) { @@ -108,10 +105,22 @@ export class PointBatch extends PrimitiveBatch { } protected getCurrentIndexBuffer(): BufferAttribute { + /** Catering to typescript + * There is no unniverse where the geometry is non-indexed. We're **explicitly** setting the index at creation time + */ + if (!this.primitive.geometry.index) { + throw new Error(`Invalid index buffer for batch ${this.id}`) + } return this.primitive.geometry.index } protected getNextIndexBuffer(): BufferAttribute { + /** Catering to typescript + * There is no unniverse where the geometry is non-indexed. We're **explicitly** setting the index at creation time + */ + if (!this.primitive.geometry.index) { + throw new Error(`Invalid index buffer for batch ${this.id}`) + } return new BufferAttribute( (this.primitive.geometry.index.array as Uint16Array | Uint32Array).slice(), this.primitive.geometry.index.itemSize @@ -149,8 +158,14 @@ export class PointBatch extends PrimitiveBatch { public buildBatch(): void { let attributeCount = 0 for (let k = 0; k < this.renderViews.length; k++) { - attributeCount += - this.renderViews[k].renderData.geometry.attributes.POSITION.length + const ervee = this.renderViews[k] + /** Catering to typescript + * There is no unniverse where indices or positions are undefined at this point + */ + if (!ervee.renderData.geometry.attributes) { + throw new Error(`Cannot build batch ${this.id}. Invalid geometry, or indices`) + } + attributeCount += ervee.renderData.geometry.attributes.POSITION.length } const position = new Float64Array(attributeCount) const color = new Float32Array(attributeCount).fill(1) @@ -159,11 +174,14 @@ export class PointBatch extends PrimitiveBatch { let indexOffset = 0 for (let k = 0; k < this.renderViews.length; k++) { const geometry = this.renderViews[k].renderData.geometry + if (!geometry.attributes) { + throw new Error(`Cannot build batch ${this.id}. Invalid geometry, or indices`) + } position.set(geometry.attributes.POSITION, offset) if (geometry.attributes.COLOR) color.set(geometry.attributes.COLOR, offset) index.set( new Int32Array(geometry.attributes.POSITION.length / 3).map( - (value, index) => index + indexOffset + (_value, index) => index + indexOffset ), indexOffset ) @@ -218,7 +236,7 @@ export class PointBatch extends PrimitiveBatch { return geometry } - public getRenderView(index: number): NodeRenderView { + public getRenderView(index: number): NodeRenderView | null { for (let k = 0; k < this.renderViews.length; k++) { if ( index >= this.renderViews[k].batchStart && @@ -227,8 +245,9 @@ export class PointBatch extends PrimitiveBatch { return this.renderViews[k] } } + return null } - public getMaterialAtIndex(index: number): Material { + public getMaterialAtIndex(index: number): Material | null { for (let k = 0; k < this.renderViews.length; k++) { if ( index >= this.renderViews[k].batchStart && @@ -248,5 +267,6 @@ export class PointBatch extends PrimitiveBatch { return this.materials[group.materialIndex] } } + return null } } diff --git a/packages/viewer/src/modules/batching/PrimitiveBatch.ts b/packages/viewer/src/modules/batching/PrimitiveBatch.ts index 5250a8ebff..f1ed9088b8 100644 --- a/packages/viewer/src/modules/batching/PrimitiveBatch.ts +++ b/packages/viewer/src/modules/batching/PrimitiveBatch.ts @@ -2,33 +2,32 @@ import { Material, Object3D, BufferGeometry, BufferAttribute, Box3 } from 'three import { NodeRenderView } from '../..' import { AllBatchUpdateRange, - Batch, - BatchUpdateRange, + type Batch, + type BatchUpdateRange, GeometryType, NoneBatchUpdateRange } from './Batch' -import { DrawGroup } from './Batch' +import { type DrawGroup } from './Batch' import Materials from '../materials/Materials' import SpeckleStandardColoredMaterial from '../materials/SpeckleStandardColoredMaterial' -import Logger from 'js-logger' export abstract class Primitive< TGeometry extends BufferGeometry = BufferGeometry, TMaterial extends Material | Material[] = Material | Material[] > extends Object3D { - geometry: TGeometry - material: TMaterial - visible: boolean + geometry!: TGeometry + material!: TMaterial + visible!: boolean } export abstract class PrimitiveBatch implements Batch { - public id: string - public subtreeId: string - public renderViews: NodeRenderView[] - public batchMaterial: Material + public id!: string + public subtreeId!: string + public renderViews!: NodeRenderView[] + public batchMaterial!: Material protected abstract primitive: Primitive - protected gradientIndexBuffer: BufferAttribute + protected gradientIndexBuffer!: BufferAttribute protected needsShuffle: boolean = false abstract get geometryType(): GeometryType @@ -43,7 +42,17 @@ export abstract class PrimitiveBatch implements Batch { } public get groups(): DrawGroup[] { - return this.primitive.geometry.groups + /** We always write to geomtry.groups via the set accessor + * which takes a DrawGroup[], so geometry.groups will always + * be an array of DrawGroup. + * Not to mention that **all our draw groupd are DrawGroup because + * they always have a materialIndex defined** by design and convention!!! + */ + return this.primitive.geometry.groups as DrawGroup[] + } + + public set groups(value: DrawGroup[]) { + this.primitive.geometry.groups = value } public get renderObject(): Object3D { @@ -59,22 +68,21 @@ export abstract class PrimitiveBatch implements Batch { } public getCount(): number { - return this.primitive.geometry.index.count + return this.primitive.geometry.index?.count || 0 } public setBatchMaterial(material: Material): void { this.batchMaterial = material } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public onUpdate(deltaTime: number) { + public onUpdate() { if (this.needsShuffle) { this.shuffleDrawGroups() this.needsShuffle = false } } - public setVisibleRange(...ranges: BatchUpdateRange[]) { + public setVisibleRange(ranges: BatchUpdateRange[]) { /** Entire batch needs to NOT be drawn */ if (ranges.length === 1 && ranges[0] === NoneBatchUpdateRange) { this.primitive.geometry.setDrawRange(0, 0) @@ -97,17 +105,17 @@ export abstract class PrimitiveBatch implements Batch { maxOffset = Math.max(maxOffset, range.offset) }) + const offset = ranges.find((val) => val.offset === maxOffset) this.primitive.geometry.setDrawRange( minOffset, - maxOffset - minOffset + ranges.find((val) => val.offset === maxOffset).count + maxOffset - minOffset + (offset ? offset.count : 0) ) this.primitive.visible = true } public getVisibleRange(): BatchUpdateRange { /** Entire batch is visible */ - if (this.primitive.geometry.groups.length === 1 && this.primitive.visible) - return AllBatchUpdateRange + if (this.groups.length === 1 && this.primitive.visible) return AllBatchUpdateRange /** Entire batch is hidden */ if (!this.primitive.visible) return NoneBatchUpdateRange /** Parts of the batch are visible */ @@ -120,6 +128,7 @@ export abstract class PrimitiveBatch implements Batch { public getOpaque(): BatchUpdateRange { /** If there is any transparent or hidden group return the update range up to it's offset */ const transparentOrHiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return ( Materials.isTransparent(this.materials[value.materialIndex]) || this.materials[value.materialIndex].visible === false @@ -139,6 +148,7 @@ export abstract class PrimitiveBatch implements Batch { public getDepth(): BatchUpdateRange { /** If there is any transparent or hidden group return the update range up to it's offset */ const transparentOrHiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return ( Materials.isTransparent(this.materials[value.materialIndex]) || this.materials[value.materialIndex].visible === false || @@ -159,10 +169,12 @@ export abstract class PrimitiveBatch implements Batch { public getTransparent(): BatchUpdateRange { /** Look for a transparent group */ const transparentGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return Materials.isTransparent(this.materials[value.materialIndex]) }) /** Look for a hidden group */ const hiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return this.materials[value.materialIndex].visible === false }) /** If there is a transparent group return it's range */ @@ -185,6 +197,7 @@ export abstract class PrimitiveBatch implements Batch { if (this.materials[0].stencilWrite === true) return AllBatchUpdateRange } const stencilGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return this.materials[value.materialIndex].stencilWrite === true }) if (stencilGroup) { @@ -197,39 +210,44 @@ export abstract class PrimitiveBatch implements Batch { return NoneBatchUpdateRange } - public setBatchBuffers(...range: BatchUpdateRange[]): void { + public setBatchBuffers(ranges: BatchUpdateRange[]): void { let minGradientIndex = Infinity let maxGradientIndex = 0 - for (let k = 0; k < range.length; k++) { - if (range[k].materialOptions) { - if (range[k].materialOptions.rampIndex !== undefined) { - const start = range[k].offset - const len = range[k].offset + range[k].count + for (let k = 0; k < ranges.length; k++) { + const range = ranges[k] + if (range.materialOptions) { + if ( + range.materialOptions.rampIndex !== undefined && + range.materialOptions.rampWidth !== undefined + ) { + const start = ranges[k].offset + const len = ranges[k].offset + ranges[k].count /** The ramp indices specify the *begining* of each ramp color. When sampling with Nearest filter (since we don't want filtering) * we'll always be sampling right at the edge between texels. Most GPUs will sample consistently, but some won't and we end up with * a ton of artifacts. To avoid this, we are shifting the sampling indices so they're right on the center of each texel, so no inconsistent * sampling can occur. */ - const shiftedIndex = - range[k].materialOptions.rampIndex + - 0.5 / range[k].materialOptions.rampWidth - const minMaxIndices = this.updateGradientIndexBufferData( - start, - range[k].count === Infinity - ? this.primitive.geometry.attributes['gradientIndex'].array.length - : len, - shiftedIndex - ) - minGradientIndex = Math.min(minGradientIndex, minMaxIndices.minIndex) - maxGradientIndex = Math.max(maxGradientIndex, minMaxIndices.maxIndex) + if (range.materialOptions.rampIndex && range.materialOptions.rampWidth) { + const shiftedIndex = + range.materialOptions.rampIndex + 0.5 / range.materialOptions.rampWidth + const minMaxIndices = this.updateGradientIndexBufferData( + start, + range.count === Infinity + ? this.primitive.geometry.attributes['gradientIndex'].array.length + : len, + shiftedIndex + ) + minGradientIndex = Math.min(minGradientIndex, minMaxIndices.minIndex) + maxGradientIndex = Math.max(maxGradientIndex, minMaxIndices.maxIndex) + } } /** We need to update the texture here, because each batch uses it's own clone for any material we use on it * because otherwise three.js won't properly update our custom uniforms */ - if (range[k].materialOptions.rampTexture !== undefined) { - if (range[k].material instanceof SpeckleStandardColoredMaterial) { - ;(range[k].material as SpeckleStandardColoredMaterial).setGradientTexture( - range[k].materialOptions.rampTexture + if (range.materialOptions.rampTexture !== undefined) { + if (range.material instanceof SpeckleStandardColoredMaterial) { + ;(range.material as SpeckleStandardColoredMaterial).setGradientTexture( + range.materialOptions.rampTexture ) } } @@ -242,7 +260,12 @@ export abstract class PrimitiveBatch implements Batch { protected cleanMaterials() { const materialsInUse = [ ...Array.from( - new Set(this.groups.map((value) => this.materials[value.materialIndex])) + new Set( + this.groups.map((value) => { + if (value.materialIndex === undefined) return undefined + return this.materials[value.materialIndex] + }) + ) ) ] let k = 0 @@ -250,6 +273,7 @@ export abstract class PrimitiveBatch implements Batch { if (!materialsInUse.includes(this.materials[k])) { this.materials.splice(k, 1) this.groups.forEach((value: DrawGroup) => { + if (value.materialIndex === undefined) return if (value.materialIndex > k) value.materialIndex-- }) k = 0 @@ -264,13 +288,15 @@ export abstract class PrimitiveBatch implements Batch { protected abstract shuffleMaterialOrder(a: DrawGroup, b: DrawGroup): number private shuffleDrawGroups() { - const groups = this.primitive.geometry.groups.slice() + const groups = this.groups.slice() groups.sort(this.shuffleMaterialOrder.bind(this)) - const materialOrder = [] + const materialOrder: Array = [] groups.reduce((previousValue, currentValue) => { - if (previousValue.indexOf(currentValue.materialIndex) === -1) { - previousValue.push(currentValue.materialIndex) + if (currentValue.materialIndex !== undefined) { + if (previousValue.indexOf(currentValue.materialIndex) === -1) { + previousValue.push(currentValue.materialIndex) + } } return previousValue }, materialOrder) @@ -332,7 +358,7 @@ export abstract class PrimitiveBatch implements Batch { materialIndex: materialGroup[0].materialIndex }) } - this.primitive.geometry.groups = [] + this.groups = [] for (let i = 0; i < newGroups.length; i++) { this.primitive.geometry.addGroup( newGroups[i].offset, @@ -342,16 +368,22 @@ export abstract class PrimitiveBatch implements Batch { } this.primitive.geometry.setIndex(targetIBO) - this.primitive.geometry.index.needsUpdate = true + /** Catering to typescript + * The line above literally makes sure the index is set. Absurd + */ + if (this.primitive.geometry.index) this.primitive.geometry.index.needsUpdate = true - const hiddenGroup = this.primitive.geometry.groups.find((value) => { - return this.primitive.material[value.materialIndex].visible === false + const hiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false + return this.materials[value.materialIndex].visible === false }) if (hiddenGroup) { - this.setVisibleRange({ - offset: 0, - count: hiddenGroup.start - }) + this.setVisibleRange([ + { + offset: 0, + count: hiddenGroup.start + } + ]) } // console.log('Final -> ', this.id, this.groups.slice()) } @@ -372,7 +404,7 @@ export abstract class PrimitiveBatch implements Batch { this.primitive.geometry.attributes['gradientIndex'].needsUpdate = true } - public abstract setDrawRanges(...ranges: BatchUpdateRange[]) + public abstract setDrawRanges(ranges: BatchUpdateRange[]): void public resetDrawRanges(): void { this.primitive.visible = true @@ -382,29 +414,21 @@ export abstract class PrimitiveBatch implements Batch { } public abstract buildBatch(): void - public abstract getRenderView(index: number): NodeRenderView - public abstract getMaterialAtIndex(index: number): Material - public getMaterial(rv: NodeRenderView): Material { - for (let k = 0; k < this.primitive.geometry.groups.length; k++) { - try { - if ( - rv.batchStart >= this.primitive.geometry.groups[k].start && - rv.batchEnd <= - this.primitive.geometry.groups[k].start + - this.primitive.geometry.groups[k].count - ) { - return this.materials[this.primitive.geometry.groups[k].materialIndex] - } - } catch (e) { - Logger.error('Failed to get material') + public abstract getRenderView(index: number): NodeRenderView | null + public abstract getMaterialAtIndex(index: number): Material | null + public getMaterial(rv: NodeRenderView): Material | null { + for (let k = 0; k < this.groups.length; k++) { + const group = this.groups[k] + if (rv.batchStart >= group.start && rv.batchEnd <= group.start + group.count) { + return this.materials[group.materialIndex] } } + return null } public purge(): void { this.renderViews.length = 0 this.primitive.geometry.dispose() this.batchMaterial.dispose() - this.primitive = null } } diff --git a/packages/viewer/src/modules/batching/TextBatch.ts b/packages/viewer/src/modules/batching/TextBatch.ts index e330ec58c7..c993632432 100644 --- a/packages/viewer/src/modules/batching/TextBatch.ts +++ b/packages/viewer/src/modules/batching/TextBatch.ts @@ -4,23 +4,23 @@ import { Box3, Material, Object3D, WebGLRenderer } from 'three' import { NodeRenderView } from '../tree/NodeRenderView' import { AllBatchUpdateRange, - Batch, - BatchUpdateRange, + type Batch, + type BatchUpdateRange, + type DrawGroup, GeometryType, NoneBatchUpdateRange } from './Batch' import { SpeckleText } from '../objects/SpeckleText' import { ObjectLayers } from '../../IViewer' -import { DrawGroup } from './Batch' import Materials from '../materials/Materials' export default class TextBatch implements Batch { public id: string public subtreeId: string public renderViews: NodeRenderView[] - public batchMaterial: Material - public mesh: SpeckleText + public batchMaterial!: Material + public mesh!: SpeckleText public get bounds(): Box3 { return new Box3().setFromObject(this.mesh) @@ -68,7 +68,7 @@ export default class TextBatch implements Batch { public getCount(): number { return ( this.mesh.textMesh.geometry.index.count + - this.mesh.backgroundMesh?.geometry.index.count + this.mesh.backgroundMesh?.geometry.index?.count ) } @@ -92,7 +92,8 @@ export default class TextBatch implements Batch { renderer } - public setVisibleRange(...ranges: BatchUpdateRange[]) { + public setVisibleRange(ranges: BatchUpdateRange[]) { + ranges // TO DO } @@ -116,11 +117,12 @@ export default class TextBatch implements Batch { return NoneBatchUpdateRange } - public setBatchBuffers(...range: BatchUpdateRange[]): void { + public setBatchBuffers(range: BatchUpdateRange[]): void { + range throw new Error('Method not implemented.') } - public setDrawRanges(...ranges: BatchUpdateRange[]) { + public setDrawRanges(ranges: BatchUpdateRange[]) { this.mesh.textMesh.material = ranges[0].material if (ranges[0].materialOptions && ranges[0].materialOptions.rampIndexColor) { this.mesh.textMesh.material.color.copy(ranges[0].materialOptions.rampIndexColor) @@ -130,11 +132,15 @@ export default class TextBatch implements Batch { public resetDrawRanges() { this.mesh.textMesh.material = this.batchMaterial this.mesh.textMesh.visible = true - // this.geometry.clearGroups() - // this.geometry.setDrawRange(0, Infinity) } public async buildBatch() { + /** Catering to typescript + * There is no unniverse where there is no metadata + */ + if (!this.renderViews[0].renderData.geometry.metaData) { + throw new Error(`Cannot build batch ${this.id}. Metadata`) + } this.mesh = new SpeckleText(this.id, ObjectLayers.STREAM_CONTENT_TEXT) this.mesh.matrixAutoUpdate = false await this.mesh.update( @@ -142,7 +148,8 @@ export default class TextBatch implements Batch { this.renderViews[0].renderData.geometry.metaData ) ) - this.mesh.matrix.copy(this.renderViews[0].renderData.geometry.bakeTransform) + if (this.renderViews[0].renderData.geometry.bakeTransform) + this.mesh.matrix.copy(this.renderViews[0].renderData.geometry.bakeTransform) this.renderViews[0].setBatchData( this.id, 0, @@ -152,14 +159,17 @@ export default class TextBatch implements Batch { } public getRenderView(index: number): NodeRenderView { + index return this.renderViews[0] } public getMaterialAtIndex(index: number): Material { + index return this.batchMaterial } public getMaterial(rv: NodeRenderView): Material { + rv return this.batchMaterial } @@ -167,6 +177,5 @@ export default class TextBatch implements Batch { this.renderViews.length = 0 this.batchMaterial.dispose() this.mesh.geometry.dispose() - this.mesh = null } } diff --git a/packages/viewer/src/modules/converter/Geometry.ts b/packages/viewer/src/modules/converter/Geometry.ts index 907a8d219f..83ebd50f94 100644 --- a/packages/viewer/src/modules/converter/Geometry.ts +++ b/packages/viewer/src/modules/converter/Geometry.ts @@ -8,7 +8,7 @@ import { Matrix4, Vector3 } from 'three' -import { SpeckleObject } from '../tree/DataTree' +import { type SpeckleObject } from '../../IViewer' export enum GeometryAttributes { POSITION = 'POSITION', @@ -20,9 +20,12 @@ export enum GeometryAttributes { } export interface GeometryData { - attributes: Partial> - bakeTransform: Matrix4 - transform: Matrix4 + attributes: + | (Record & + Partial>) + | null + bakeTransform: Matrix4 | null + transform: Matrix4 | null metaData?: SpeckleObject instanced?: boolean } @@ -74,26 +77,33 @@ export class Geometry { } static mergeGeometryAttribute( - attributes: number[][], + attributes: (number[] | undefined)[], target: Float32Array | Float64Array ): ArrayLike { let offset = 0 for (let k = 0; k < attributes.length; k++) { - target.set(attributes[k], offset) - offset += attributes[k].length + const attribute = attributes[k] + if (!attribute || !target) { + throw new Error('Cannot merge geometries. Indices or positions are undefined') + } + target.set(attribute, offset) + offset += attribute.length } return target } static mergeIndexAttribute( - indexAttributes: number[][], - positionAttributes: number[][] + indexAttributes: (number[] | undefined)[], + positionAttributes: (number[] | undefined)[] ): number[] { let indexOffset = 0 const mergedIndex = [] for (let i = 0; i < indexAttributes.length; ++i) { const index = indexAttributes[i] + if (!index || !positionAttributes) { + throw new Error('Cannot merge geometries. Indices or positions are undefined') + } for (let j = 0; j < index.length; ++j) { mergedIndex.push(index[j] + indexOffset) @@ -113,46 +123,74 @@ export class Geometry { } as GeometryData for (let i = 0; i < geometries.length; i++) { - if (geometries[i].bakeTransform) + /** Catering to typescript */ + if (geometries[i].bakeTransform !== null) Geometry.transformGeometryData(geometries[i], geometries[i].bakeTransform) } - if (sampleAttributes[GeometryAttributes.INDEX]) { - const indexAttributes = geometries.map( - (item) => item.attributes[GeometryAttributes.INDEX] - ) - const positionAttributes = geometries.map( - (item) => item.attributes[GeometryAttributes.POSITION] + if (sampleAttributes && sampleAttributes[GeometryAttributes.INDEX]) { + const indexAttributes: (number[] | undefined)[] = geometries.map( + (item: GeometryData) => { + /** Catering to typescript */ + if (!item.attributes) return + return item.attributes[GeometryAttributes.INDEX] + } ) - mergedGeometry.attributes[GeometryAttributes.INDEX] = - Geometry.mergeIndexAttribute(indexAttributes, positionAttributes) + const positionAttributes: (number[] | undefined)[] = geometries.map((item) => { + /** Catering to typescript */ + if (!item.attributes) return + return item.attributes[GeometryAttributes.POSITION] + }) + /** o_0 Catering to typescript*/ + if (mergedGeometry.attributes) + mergedGeometry.attributes[GeometryAttributes.INDEX] = + Geometry.mergeIndexAttribute(indexAttributes, positionAttributes) } for (const k in sampleAttributes) { if (k !== GeometryAttributes.INDEX) { - const attributes = geometries.map((item) => { - return item.attributes[k] + const attributes: (number[] | undefined)[] = geometries.map((item) => { + /** Catering to typescript */ + if (!item.attributes) return + return item.attributes[k as GeometryAttributes] as number[] }) - mergedGeometry.attributes[k] = Geometry.mergeGeometryAttribute( - attributes, - k === GeometryAttributes.POSITION - ? new Float64Array(attributes.reduce((prev, cur) => prev + cur.length, 0)) - : new Float32Array(attributes.reduce((prev, cur) => prev + cur.length, 0)) - ) + /** Catering to typescript */ + if (mergedGeometry.attributes) + mergedGeometry.attributes[k as GeometryAttributes] = + Geometry.mergeGeometryAttribute( + attributes, + k === GeometryAttributes.POSITION + ? new Float64Array( + attributes.reduce((prev, cur) => { + /** Catering to typescript */ + if (!cur) return 0 + return prev + cur.length + }, 0) + ) + : new Float32Array( + attributes.reduce((prev, cur) => { + /** Catering to typescript */ + if (!cur) return 0 + return prev + cur.length + }, 0) + ) + ) as number[] } } geometries.forEach((geometry) => { for (const k in geometry.attributes) { - delete geometry.attributes[k] + delete geometry.attributes[k as GeometryAttributes] } }) return mergedGeometry } - public static transformGeometryData(geometryData: GeometryData, m: Matrix4) { + public static transformGeometryData(geometryData: GeometryData, m: Matrix4 | null) { + if (!geometryData.attributes) return if (!geometryData.attributes.POSITION) return + if (!m) return const e = m.elements diff --git a/packages/viewer/src/modules/extensions/CameraController.ts b/packages/viewer/src/modules/extensions/CameraController.ts index 0fb417d672..ddd3ed1a23 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -4,10 +4,10 @@ import { Extension } from './Extension' import { SpeckleCameraControls } from '../objects/SpeckleCameraControls' import { Box3, OrthographicCamera, PerspectiveCamera, Sphere, Vector3 } from 'three' import { KeyboardKeyHold, HOLD_EVENT_TYPE } from 'hold-event' -import { CameraProjection } from '../objects/SpeckleCamera' -import { CameraEvent, SpeckleCamera } from '../objects/SpeckleCamera' +import { CameraProjection, type CameraEventPayload } from '../objects/SpeckleCamera' +import { CameraEvent, type SpeckleCamera } from '../objects/SpeckleCamera' import Logger from 'js-logger' -import { IViewer, SpeckleView } from '../../IViewer' +import type { IViewer, SpeckleView } from '../../IViewer' export type CanonicalView = | 'front' @@ -33,10 +33,10 @@ export type PolarView = { } export class CameraController extends Extension implements SpeckleCamera { - protected _renderingCamera: PerspectiveCamera | OrthographicCamera = null - protected perspectiveCamera: PerspectiveCamera = null - protected orthographicCamera: OrthographicCamera = null - protected _controls: SpeckleCameraControls = null + protected _renderingCamera!: PerspectiveCamera | OrthographicCamera + protected perspectiveCamera: PerspectiveCamera + protected orthographicCamera: OrthographicCamera + protected _controls: SpeckleCameraControls get renderingCamera(): PerspectiveCamera | OrthographicCamera { return this._renderingCamera @@ -46,15 +46,15 @@ export class CameraController extends Extension implements SpeckleCamera { this._renderingCamera = value } - public get enabled() { + public get enabled(): boolean { return this._controls.enabled } - public set enabled(val) { + public set enabled(val: boolean) { this._controls.enabled = val } - public get fieldOfView() { + public get fieldOfView(): number { return this.perspectiveCamera.fov } @@ -63,11 +63,11 @@ export class CameraController extends Extension implements SpeckleCamera { this.perspectiveCamera.updateProjectionMatrix() } - public get aspect() { + public get aspect(): number { return this.perspectiveCamera.aspect } - public get controls() { + public get controls(): SpeckleCameraControls { return this._controls } @@ -131,14 +131,33 @@ export class CameraController extends Extension implements SpeckleCamera { this.viewer.getRenderer().speckleCamera = this } - setCameraView(objectIds: string[], transition: boolean, fit?: number): void + public on( + eventType: T, + listener: (arg: CameraEventPayload[T]) => void + ): void { + super.on(eventType, listener) + } + + setCameraView( + objectIds: string[] | undefined, + transition: boolean | undefined, + fit?: number + ): void setCameraView( view: CanonicalView | SpeckleView | InlineView | PolarView, - transition: boolean + transition: boolean | undefined, + fit?: number ): void - setCameraView(bounds: Box3, transition: boolean): void + setCameraView(bounds: Box3, transition: boolean | undefined, fit?: number): void setCameraView( - arg0: string[] | CanonicalView | SpeckleView | InlineView | PolarView | Box3, + arg0: + | string[] + | CanonicalView + | SpeckleView + | InlineView + | PolarView + | Box3 + | undefined, arg1 = true, arg2 = 1.2 ): void { @@ -194,19 +213,19 @@ export class CameraController extends Extension implements SpeckleCamera { this.viewer.requestRender() } - public setOrthoCameraOn() { + public setOrthoCameraOn(): void { if (this._renderingCamera === this.orthographicCamera) return this.renderingCamera = this.orthographicCamera this.setupOrthoCamera() this.viewer.requestRender() } - public toggleCameras() { + public toggleCameras(): void { if (this._renderingCamera === this.perspectiveCamera) this.setOrthoCameraOn() else this.setPerspectiveCameraOn() } - protected setupOrthoCamera() { + protected setupOrthoCamera(): void { this._controls.mouseButtons.wheel = CameraControls.ACTION.ZOOM const lineOfSight = new Vector3() @@ -252,11 +271,11 @@ export class CameraController extends Extension implements SpeckleCamera { this.emit(CameraEvent.ProjectionChanged, CameraProjection.PERSPECTIVE) } - public disableRotations() { + public disableRotations(): void { this._controls.mouseButtons.left = CameraControls.ACTION.TRUCK } - public enableRotations() { + public enableRotations(): void { this._controls.mouseButtons.left = CameraControls.ACTION.ROTATE } @@ -269,7 +288,7 @@ export class CameraController extends Extension implements SpeckleCamera { const dKey = new KeyboardKeyHold(KEYCODE.D, 16.666) const isTruckingGroup = new Array(4) - const setTrucking = (index, value) => { + const setTrucking = (index: number, value: boolean) => { isTruckingGroup[index] = value if (isTruckingGroup.every((element) => element === false)) { this._controls.isTrucking = false @@ -277,21 +296,15 @@ export class CameraController extends Extension implements SpeckleCamera { } else this._controls.isTrucking = true } - aKey.addEventListener( - HOLD_EVENT_TYPE.HOLD_START, - function () { - this.controls.dispatchEvent({ type: 'controlstart' }) - }.bind(this) - ) - aKey.addEventListener( - 'holding', - function (event) { - if (this.viewer.mouseOverRenderer === false) return - setTrucking(0, true) - this.controls.truck(-0.01 * event.deltaTime, 0, false) - return - }.bind(this) - ) + aKey.addEventListener(HOLD_EVENT_TYPE.HOLD_START, () => { + this.controls['dispatchEvent']({ type: 'controlstart' }) + }) + aKey.addEventListener('holding', (event) => { + if (!event) return + setTrucking(0, true) + this.controls.truck(-0.01 * event.deltaTime, 0, false) + return + }) aKey.addEventListener( HOLD_EVENT_TYPE.HOLD_END, function () { @@ -299,21 +312,15 @@ export class CameraController extends Extension implements SpeckleCamera { }.bind(this) ) - dKey.addEventListener( - HOLD_EVENT_TYPE.HOLD_START, - function () { - this.controls.dispatchEvent({ type: 'controlstart' }) - }.bind(this) - ) - dKey.addEventListener( - 'holding', - function (event) { - if (this.viewer.mouseOverRenderer === false) return - setTrucking(1, true) - this.controls.truck(0.01 * event.deltaTime, 0, false) - return - }.bind(this) - ) + dKey.addEventListener(HOLD_EVENT_TYPE.HOLD_START, () => { + this.controls['dispatchEvent']({ type: 'controlstart' }) + }) + dKey.addEventListener('holding', (event) => { + if (!event) return + setTrucking(1, true) + this.controls.truck(0.01 * event.deltaTime, 0, false) + return + }) dKey.addEventListener( HOLD_EVENT_TYPE.HOLD_END, function () { @@ -321,21 +328,15 @@ export class CameraController extends Extension implements SpeckleCamera { }.bind(this) ) - wKey.addEventListener( - HOLD_EVENT_TYPE.HOLD_START, - function () { - this.controls.dispatchEvent({ type: 'controlstart' }) - }.bind(this) - ) - wKey.addEventListener( - 'holding', - function (event) { - if (this.viewer.mouseOverRenderer === false) return - setTrucking(2, true) - this.controls.forward(0.01 * event.deltaTime, false) - return - }.bind(this) - ) + wKey.addEventListener(HOLD_EVENT_TYPE.HOLD_START, () => { + this.controls['dispatchEvent']({ type: 'controlstart' }) + }) + wKey.addEventListener('holding', (event) => { + if (!event) return + setTrucking(2, true) + this.controls.forward(0.01 * event.deltaTime, false) + return + }) wKey.addEventListener( HOLD_EVENT_TYPE.HOLD_END, function () { @@ -343,21 +344,15 @@ export class CameraController extends Extension implements SpeckleCamera { }.bind(this) ) - sKey.addEventListener( - HOLD_EVENT_TYPE.HOLD_START, - function () { - this.controls.dispatchEvent({ type: 'controlstart' }) - }.bind(this) - ) - sKey.addEventListener( - 'holding', - function (event) { - if (this.viewer.mouseOverRenderer === false) return - setTrucking(3, true) - this.controls.forward(-0.01 * event.deltaTime, false) - return - }.bind(this) - ) + sKey.addEventListener(HOLD_EVENT_TYPE.HOLD_START, () => { + this.controls['dispatchEvent']({ type: 'controlstart' }) + }) + sKey.addEventListener('holding', (event) => { + if (!event) return + setTrucking(3, true) + this.controls.forward(-0.01 * event.deltaTime, false) + return + }) sKey.addEventListener( HOLD_EVENT_TYPE.HOLD_END, function () { @@ -418,7 +413,7 @@ export class CameraController extends Extension implements SpeckleCamera { // this.viewer.controls.setBoundary( box ) } - private zoomToBox(box, fit = 1.2, transition = true) { + private zoomToBox(box: Box3, fit = 1.2, transition = true) { if (box.max.x === Infinity || box.max.x === -Infinity) { box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)) } @@ -472,8 +467,10 @@ export class CameraController extends Extension implements SpeckleCamera { ) } - private isBox3(view: unknown): view is Box3 { - return view['isBox3'] + private isBox3( + view: CanonicalView | SpeckleView | InlineView | PolarView | Box3 + ): view is Box3 { + return view instanceof Box3 } protected setView( @@ -496,12 +493,12 @@ export class CameraController extends Extension implements SpeckleCamera { private setViewSpeckle(view: SpeckleView, transition = true) { this._controls.setLookAt( - view.view.origin['x'], - view.view.origin['y'], - view.view.origin['z'], - view.view.target['x'], - view.view.target['y'], - view.view.target['z'], + view.view.origin.x, + view.view.origin.y, + view.view.origin.z, + view.view.target.x, + view.view.target.y, + view.view.target.z, transition ) this.enableRotations() diff --git a/packages/viewer/src/modules/extensions/DiffExtension.ts b/packages/viewer/src/modules/extensions/DiffExtension.ts index 46c3b26f07..84d0f8bb2d 100644 --- a/packages/viewer/src/modules/extensions/DiffExtension.ts +++ b/packages/viewer/src/modules/extensions/DiffExtension.ts @@ -1,20 +1,18 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Color, DoubleSide, FrontSide } from 'three' -import { TreeNode, WorldTree } from '../tree/WorldTree' +import { Color, DoubleSide, FrontSide, Material } from 'three' +import { type TreeNode, WorldTree } from '../tree/WorldTree' import Logger from 'js-logger' -import _, { omit } from 'underscore' +import { groupBy } from 'lodash-es' import { GeometryType } from '../batching/Batch' import SpeckleLineMaterial from '../materials/SpeckleLineMaterial' import SpecklePointMaterial from '../materials/SpecklePointMaterial' import SpeckleStandardMaterial from '../materials/SpeckleStandardMaterial' import { NodeRenderView } from '../tree/NodeRenderView' -import { IViewer } from '../../IViewer' +import { type IViewer } from '../../IViewer' import { Extension } from './Extension' import { SpeckleTypeAllRenderables } from '../loaders/GeometryConverter' import { SpeckleLoader } from '../loaders/Speckle/SpeckleLoader' -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type SpeckleObject = Record type SpeckleMaterialType = | SpeckleStandardMaterial | SpecklePointMaterial @@ -26,10 +24,10 @@ export enum VisualDiffMode { } export interface DiffResult { - unchanged: Array - added: Array - removed: Array - modified: Array> + unchanged: Array + added: Array + removed: Array + modified: Array> } interface VisualDiffResult { @@ -49,24 +47,30 @@ export class DiffExtension extends Extension { this._enabled = value } - protected tree: WorldTree = null - private addedMaterialMesh: SpeckleStandardMaterial = null - private changedNewMaterialMesh: SpeckleStandardMaterial = null - private changedOldMaterialMesh: SpeckleStandardMaterial = null - private removedMaterialMesh: SpeckleStandardMaterial = null + protected tree: WorldTree + private addedMaterialMesh: SpeckleStandardMaterial + private changedNewMaterialMesh: SpeckleStandardMaterial + private changedOldMaterialMesh: SpeckleStandardMaterial + private removedMaterialMesh: SpeckleStandardMaterial - private addedMaterialPoint: SpecklePointMaterial = null - private changedNewMaterialPoint: SpecklePointMaterial = null - private changedOldMaterialPoint: SpecklePointMaterial = null - private removedMaterialPoint: SpecklePointMaterial = null + private addedMaterialPoint: SpecklePointMaterial + private changedNewMaterialPoint: SpecklePointMaterial + private changedOldMaterialPoint: SpecklePointMaterial + private removedMaterialPoint: SpecklePointMaterial private addedMaterials: Array = [] private changedOldMaterials: Array = [] private changedNewMaterials: Array = [] private removedMaterials: Array = [] - private _materialGroups = null - private _visualDiff: VisualDiffResult = null + private _materialGroups: + | { + rvs: NodeRenderView[] + material: SpeckleMaterialType + }[] + | null + + private _visualDiff!: VisualDiffResult private _diffTime = -1 private _diffMode: VisualDiffMode = VisualDiffMode.COLORED @@ -237,7 +241,7 @@ export class DiffExtension extends Extension { } /** Currently, the diff does not store the existing materials. We can do that if we need to */ - public async undiff() { + public async undiff(): Promise { const pipelineOptions = this.viewer.getRenderer().pipelineOptions pipelineOptions.depthSide = DoubleSide this.viewer.getRenderer().pipelineOptions = pipelineOptions @@ -253,12 +257,6 @@ export class DiffExtension extends Extension { await Promise.all(unloadPromises) } - private intersection(o1, o2) { - const [k1, k2] = [Object.keys(o1), Object.keys(o2)] - const [first, next] = k1.length > k2.length ? [k2, o1] : [k1, o2] - return first.filter((k) => k in next) - } - private buildIdMaps( rvs: Array, idMap: { [id: string]: { node: TreeNode; applicationId: string } }, @@ -288,93 +286,7 @@ export class DiffExtension extends Extension { return Promise.resolve(diffResult) } - private diffBoolean(urlA: string, urlB: string): Promise { - const start = performance.now() - const diffResult: DiffResult = { - unchanged: [], - added: [], - removed: [], - modified: [] - } - - const renderTreeA = this.tree.getRenderTree(urlA) - const renderTreeB = this.tree.getRenderTree(urlB) - let rvsA = renderTreeA.getRenderableNodes(...SpeckleTypeAllRenderables) - let rvsB = renderTreeB.getRenderableNodes(...SpeckleTypeAllRenderables) - - rvsA = rvsA.map((value) => { - return renderTreeA.getAtomicParent(value) - }) - - rvsB = rvsB.map((value) => { - return renderTreeB.getAtomicParent(value) - }) - - rvsA = [...Array.from(new Set(rvsA))] - rvsB = [...Array.from(new Set(rvsB))] - - const idMapA = {} - const appIdMapA = {} - this.buildIdMaps(rvsA, idMapA, appIdMapA) - - const idMapB = {} - const appIdMapB = {} - this.buildIdMaps(rvsB, idMapB, appIdMapB) - - /** Get the ids which are common between the two maps. This will be objects - * which have not changed - */ - const unchanged: Array = this.intersection(idMapA, idMapB) - /** We remove the unchanged objects from B and end up with changed + added */ - const addedModified = _.omit(idMapB, unchanged) - /** We remove the unchanged objects from A and end up with changed + removed */ - const removedModified = _.omit(idMapA, unchanged) - /** We remove the changed objects from B. An object from B is changed if - * it's application ID exists in A - */ - const added = _.omit(addedModified, function (value, key, object) { - return value.applicationId && appIdMapA[value.applicationId] !== undefined - }) - /** We remove the changed objects from A. An object from A is changed if - * it's application ID exists in B - */ - const removed = _.omit(removedModified, function (value, key, object) { - return value.applicationId && appIdMapB[value.applicationId] !== undefined - }) - /** We remove the removed objects from A, leaving us only changed objects */ - const modifiedRemoved = _.omit(removedModified, Object.keys(removed)) - /** We remove the removed objects from B, leaving us only changed objects */ - const modifiedAdded = _.omit(addedModified, Object.keys(added)) - - /** We fill the arrays from here on out */ - const modifiedOld = Object.values(modifiedRemoved).map( - (value: { node: TreeNode }) => value.node - ) - const modifiedNew = Object.values(modifiedAdded).map( - (value: { node: TreeNode }) => value.node - ) - diffResult.unchanged.push(...unchanged.map((value) => idMapA[value].node)) - diffResult.unchanged.push(...unchanged.map((value) => idMapB[value].node)) - diffResult.removed.push( - ...Object.values(removed).map((value: { node: TreeNode }) => value.node) - ) - diffResult.added.push( - ...Object.values(added).map((value: { node: TreeNode }) => value.node) - ) - - modifiedOld.forEach((value, index) => { - value - diffResult.modified.push([modifiedOld[index], modifiedNew[index]]) - }) - console.warn('Boolean Time -> ', performance.now() - start) - return Promise.resolve(diffResult) - } - private diffIterative(urlA: string, urlB: string): Promise { - const start = performance.now() - const modifiedNew: Array = [] - const modifiedOld: Array = [] - const diffResult: DiffResult = { unchanged: [], added: [], @@ -384,8 +296,18 @@ export class DiffExtension extends Extension { const renderTreeA = this.tree.getRenderTree(urlA) const renderTreeB = this.tree.getRenderTree(urlB) - let rvsA = renderTreeA.getRenderableNodes(...SpeckleTypeAllRenderables) - let rvsB = renderTreeB.getRenderableNodes(...SpeckleTypeAllRenderables) + if (!renderTreeA) { + return Promise.reject( + `Could not make diff. Resource ${urlA} could not be fetched` + ) + } + if (!renderTreeB) { + return Promise.reject( + `Could not make diff. Resource ${urlB} could not be fetched` + ) + } + let rvsA: TreeNode[] = renderTreeA.getRenderableNodes(...SpeckleTypeAllRenderables) + let rvsB: TreeNode[] = renderTreeB.getRenderableNodes(...SpeckleTypeAllRenderables) rvsA = rvsA.map((value) => { return renderTreeA.getAtomicParent(value) @@ -398,12 +320,12 @@ export class DiffExtension extends Extension { rvsA = [...Array.from(new Set(rvsA))] rvsB = [...Array.from(new Set(rvsB))] - const idMapA = {} - const appIdMapA = {} + const idMapA: { [id: string]: { node: TreeNode; applicationId: string } } = {} + const appIdMapA: { [id: string]: TreeNode } = {} this.buildIdMaps(rvsA, idMapA, appIdMapA) - const idMapB = {} - const appIdMapB = {} + const idMapB: { [id: string]: { node: TreeNode; applicationId: string } } = {} + const appIdMapB: { [id: string]: TreeNode } = {} this.buildIdMaps(rvsB, idMapB, appIdMapB) for (let k = 0; k < rvsB.length; k++) { @@ -448,28 +370,27 @@ export class DiffExtension extends Extension { } } - console.warn('Interative Time -> ', performance.now() - start) - return Promise.resolve(diffResult) } - public updateVisualDiff(time?: number, mode?: VisualDiffMode) { - if ( - (mode !== undefined && mode !== this._diffMode) || - this._materialGroups === null - ) { + public updateVisualDiff(time?: number, mode?: VisualDiffMode): void { + if ((mode !== undefined && mode !== this._diffMode) || !this._materialGroups) { this.resetMaterialGroups() - this.buildMaterialGroups(mode) - this._diffMode = mode + /** Catering to typescript */ + if (mode !== undefined) { + this.buildMaterialGroups(mode) + this._diffMode = mode + } } if (time !== undefined && time !== this._diffTime) { this.setDiffTime(time) this._diffTime = time } - this._materialGroups.forEach((value) => { - this.viewer.getRenderer().setMaterial(value.rvs, value.material) - }) + if (this._materialGroups) + this._materialGroups.forEach((value) => { + this.viewer.getRenderer().setMaterial(value.rvs, value.material) + }) this.viewer.requestRender() } @@ -479,7 +400,9 @@ export class DiffExtension extends Extension { this.addedMaterials.forEach((mat) => { mat.opacity = - mat['clampOpacity'] !== undefined ? Math.min(from, mat['clampOpacity']) : from + (mat as never)['clampOpacity'] !== undefined + ? Math.min(from, (mat as never)['clampOpacity']) + : from mat.depthWrite = from < 0.5 ? false : true mat.transparent = mat.opacity < 1 mat.needsCopy = true @@ -487,7 +410,9 @@ export class DiffExtension extends Extension { this.changedOldMaterials.forEach((mat) => { mat.opacity = - mat['clampOpacity'] !== undefined ? Math.min(to, mat['clampOpacity']) : to + (mat as never)['clampOpacity'] !== undefined + ? Math.min(to, (mat as never)['clampOpacity']) + : to mat.depthWrite = to < 0.5 ? false : true mat.transparent = mat.opacity < 1 mat.needsCopy = true @@ -495,7 +420,9 @@ export class DiffExtension extends Extension { this.changedNewMaterials.forEach((mat) => { mat.opacity = - mat['clampOpacity'] !== undefined ? Math.min(from, mat['clampOpacity']) : from + (mat as never)['clampOpacity'] !== undefined + ? Math.min(from, (mat as never)['clampOpacity']) + : from mat.depthWrite = from < 0.5 ? false : true mat.transparent = mat.opacity < 1 mat.needsCopy = true @@ -503,7 +430,9 @@ export class DiffExtension extends Extension { this.removedMaterials.forEach((mat) => { mat.opacity = - mat['clampOpacity'] !== undefined ? Math.min(to, mat['clampOpacity']) : to + (mat as never)['clampOpacity'] !== undefined + ? Math.min(to, (mat as never)['clampOpacity']) + : to mat.depthWrite = to < 0.5 ? false : true mat.transparent = mat.opacity < 1 mat.needsCopy = true @@ -535,31 +464,25 @@ export class DiffExtension extends Extension { const renderTree = this.tree.getRenderTree() const addedRvs = diffResult.added.flatMap((value) => { - return renderTree.getRenderViewsForNode(value as TreeNode, value as TreeNode) + return renderTree.getRenderViewsForNode(value as TreeNode) }) const removedRvs = diffResult.removed.flatMap((value) => { - return renderTree.getRenderViewsForNode(value as TreeNode, value as TreeNode) + return renderTree.getRenderViewsForNode(value as TreeNode) }) const unchangedRvs = diffResult.unchanged.flatMap((value) => { - return renderTree.getRenderViewsForNode(value as TreeNode, value as TreeNode) + return renderTree.getRenderViewsForNode(value as TreeNode) }) const modifiedOldRvs = diffResult.modified .flatMap((value) => { - return renderTree.getRenderViewsForNode( - value[0] as TreeNode, - value[0] as TreeNode - ) + return renderTree.getRenderViewsForNode(value[0] as TreeNode) }) .filter((value) => { return !unchangedRvs.includes(value) && !removedRvs.includes(value) }) const modifiedNewRvs = diffResult.modified .flatMap((value) => { - return renderTree.getRenderViewsForNode( - value[1] as TreeNode, - value[1] as TreeNode - ) + return renderTree.getRenderViewsForNode(value[1] as TreeNode) }) .filter((value) => { return !unchangedRvs.includes(value) && !addedRvs.includes(value) @@ -665,23 +588,42 @@ export class DiffExtension extends Extension { const changedOld = this.getBatchesSubgroups(visualDiffResult.modifiedOld) const changedNew = this.getBatchesSubgroups(visualDiffResult.modifiedNew) const removed = this.getBatchesSubgroups(visualDiffResult.removed) - this.addedMaterials = added.map((value) => value.material) - this.changedOldMaterials = changedOld.map((value) => value.material) - this.changedNewMaterials = changedNew.map((value) => value.material) - this.removedMaterials = removed.map((value) => value.material) + this.addedMaterials = added.map( + (value: { rvs: NodeRenderView[]; material: SpeckleMaterialType }) => + value.material + ) + this.changedOldMaterials = changedOld.map( + (value: { rvs: NodeRenderView[]; material: SpeckleMaterialType }) => + value.material + ) + this.changedNewMaterials = changedNew.map( + (value: { rvs: NodeRenderView[]; material: SpeckleMaterialType }) => + value.material + ) + this.removedMaterials = removed.map( + (value: { rvs: NodeRenderView[]; material: SpeckleMaterialType }) => + value.material + ) return [...added, ...changedOld, ...changedNew, ...removed] } - private getBatchesSubgroups(subgroup: Array) { - const groupBatches = _.groupBy(subgroup, 'batchId') + private getBatchesSubgroups(subgroup: Array): { + rvs: NodeRenderView[] + material: SpeckleMaterialType + }[] { + const groupBatches = groupBy(subgroup, 'batchId') - const materialGroup = [] + const materialGroup: { + rvs: NodeRenderView[] + material: SpeckleMaterialType + }[] = [] for (const k in groupBatches) { - const matClone = this.viewer - .getRenderer() - .getBatchMaterial(groupBatches[k][0]) - .clone() - matClone['clampOpacity'] = matClone.opacity + const matClone: SpeckleMaterialType = ( + this.viewer.getRenderer().getBatchMaterial(groupBatches[k][0]) as Material + ).clone() as SpeckleMaterialType + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(matClone as any)['clampOpacity'] = matClone.opacity matClone.opacity = 0.5 matClone.transparent = true materialGroup.push({ @@ -692,4 +634,114 @@ export class DiffExtension extends Extension { return materialGroup } + + /** Keeping this for reference */ + // private intersection(o1: object, o2: object) { + // const [k1, k2] = [Object.keys(o1), Object.keys(o2)] + // const [first, next] = k1.length > k2.length ? [k2, o1] : [k1, o2] + // return first.filter((k) => k in next) + // } + + // private diffBoolean(urlA: string, urlB: string): Promise { + // const diffResult: DiffResult = { + // unchanged: [], + // added: [], + // removed: [], + // modified: [] + // } + + // const renderTreeA = this.tree!.getRenderTree(urlA) + // const renderTreeB = this.tree!.getRenderTree(urlB) + // if (!renderTreeA) { + // return Promise.reject( + // `Could not make diff. Resource ${urlA} could not be fetched` + // ) + // } + // if (!renderTreeB) { + // return Promise.reject( + // `Could not make diff. Resource ${urlB} could not be fetched` + // ) + // } + // let rvsA: TreeNode[] = renderTreeA.getRenderableNodes(...SpeckleTypeAllRenderables) + // let rvsB: TreeNode[] = renderTreeB.getRenderableNodes(...SpeckleTypeAllRenderables) + + // rvsA = rvsA.map((value: TreeNode) => { + // return renderTreeA.getAtomicParent(value) + // }) + + // rvsB = rvsB.map((value) => { + // return renderTreeB.getAtomicParent(value) + // }) + + // rvsA = [...Array.from(new Set(rvsA))] + // rvsB = [...Array.from(new Set(rvsB))] + + // const idMapA: { [id: string]: { node: TreeNode; applicationId: string } } = {} + // const appIdMapA: { [id: string]: TreeNode } = {} + // this.buildIdMaps(rvsA, idMapA, appIdMapA) + + // const idMapB: { [id: string]: { node: TreeNode; applicationId: string } } = {} + // const appIdMapB: { [id: string]: TreeNode } = {} + // this.buildIdMaps(rvsB, idMapB, appIdMapB) + + // /** Get the ids which are common between the two maps. This will be objects + // * which have not changed + // */ + // const unchanged: Array = this.intersection(idMapA, idMapB) + // /** We remove the unchanged objects from B and end up with changed + added */ + // const addedModified = _.omit(idMapB, unchanged) + // /** We remove the unchanged objects from A and end up with changed + removed */ + // const removedModified = _.omit(idMapA, unchanged) + // /** We remove the changed objects from B. An object from B is changed if + // * it's application ID exists in A + // */ + // const added = _.omit(addedModified, function (value: { applicationId: string }) { + // return ( + // value.applicationId !== undefined && + // appIdMapA[value.applicationId] !== undefined + // ) + // }) + // /** We remove the changed objects from A. An object from A is changed if + // * it's application ID exists in B + // */ + // const removed = _.omit( + // removedModified, + // function (value: { applicationId: string }) { + // return ( + // value.applicationId !== undefined && + // appIdMapB[value.applicationId] !== undefined + // ) + // } + // ) + // /** We remove the removed objects from A, leaving us only changed objects */ + // const modifiedRemoved = _.omit(removedModified, Object.keys(removed)) + // /** We remove the removed objects from B, leaving us only changed objects */ + // const modifiedAdded = _.omit(addedModified, Object.keys(added)) + + // /** We fill the arrays from here on out */ + // const modifiedOld = (Object.values(modifiedRemoved) as { node: TreeNode }[]).map( + // (value: { node: TreeNode }) => value.node + // ) + // const modifiedNew = (Object.values(modifiedAdded) as { node: TreeNode }[]).map( + // (value: { node: TreeNode }) => value.node + // ) + // diffResult.unchanged.push(...unchanged.map((value) => idMapA[value].node)) + // diffResult.unchanged.push(...unchanged.map((value) => idMapB[value].node)) + // diffResult.removed.push( + // ...(Object.values(removed) as { node: TreeNode }[]).map( + // (value: { node: TreeNode }) => value.node + // ) + // ) + // diffResult.added.push( + // ...(Object.values(added) as { node: TreeNode }[]).map( + // (value: { node: TreeNode }) => value.node + // ) + // ) + + // modifiedOld.forEach((value, index) => { + // value + // diffResult.modified.push([modifiedOld[index], modifiedNew[index]]) + // }) + // return Promise.resolve(diffResult) + // } } diff --git a/packages/viewer/src/modules/extensions/Extension.ts b/packages/viewer/src/modules/extensions/Extension.ts index 011b281731..d40c7b6bc4 100644 --- a/packages/viewer/src/modules/extensions/Extension.ts +++ b/packages/viewer/src/modules/extensions/Extension.ts @@ -1,13 +1,14 @@ -import { IViewer } from '../..' +import type { Constructor } from 'type-fest' +import { type IViewer } from '../..' import EventEmitter from '../EventEmitter' export class Extension extends EventEmitter { - public get inject(): Array Extension> { + public get inject(): Array> { return [] } protected viewer: IViewer - protected _enabled: boolean + protected _enabled: boolean = false public get enabled(): boolean { return this._enabled diff --git a/packages/viewer/src/modules/extensions/FilteringExtension.ts b/packages/viewer/src/modules/extensions/FilteringExtension.ts index dcbfd04912..a3755c4cb8 100644 --- a/packages/viewer/src/modules/extensions/FilteringExtension.ts +++ b/packages/viewer/src/modules/extensions/FilteringExtension.ts @@ -6,19 +6,24 @@ import SpeckleRenderer from '../SpeckleRenderer' import { FilterMaterialType } from '../materials/Materials' import { NodeRenderView } from '../tree/NodeRenderView' import { Extension } from './Extension' -import { TreeNode, WorldTree } from '../tree/WorldTree' -import { IViewer, UpdateFlags, ViewerEvent } from '../../IViewer' -import { +import { type TreeNode, WorldTree } from '../tree/WorldTree' +import { type IViewer, UpdateFlags, ViewerEvent } from '../../IViewer' +import type { NumericPropertyInfo, PropertyInfo, StringPropertyInfo } from '../filtering/PropertyManager' +/** TO DO: Should remove selectedObjects entirely*/ export type FilteringState = { selectedObjects?: string[] hiddenObjects?: string[] isolatedObjects?: string[] - colorGroups?: Record[] + colorGroups?: { + value: string + color: string + ids: string[] + }[] userColorGroups?: { ids: string[]; color: string }[] activePropFilterKey?: string passMin?: number | null @@ -28,12 +33,12 @@ export type FilteringState = { export class FilteringExtension extends Extension { public WTI: WorldTree private Renderer: SpeckleRenderer - private StateKey: string = null + private StateKey: string | undefined = undefined private VisibilityState = new VisibilityState() - private ColorStringFilterState = null - private ColorNumericFilterState = null - private UserspaceColorState = new UserspaceColorState() + private ColorStringFilterState: ColorStringFilterState | null = null + private ColorNumericFilterState: ColorNumericFilterState | null = null + private UserspaceColorState: UserspaceColorState | null = new UserspaceColorState() private CurrentFilteringState: FilteringState = {} as FilteringState public get filteringState(): FilteringState { @@ -55,7 +60,7 @@ export class FilteringExtension extends Extension { public hideObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = false, ghost = false ): FilteringState { @@ -70,7 +75,7 @@ export class FilteringExtension extends Extension { public showObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = false ): FilteringState { return this.setVisibilityState( @@ -83,7 +88,7 @@ export class FilteringExtension extends Extension { public isolateObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = true, ghost = true ): FilteringState { @@ -98,7 +103,7 @@ export class FilteringExtension extends Extension { public unIsolateObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = true, ghost = true ): FilteringState { @@ -113,7 +118,7 @@ export class FilteringExtension extends Extension { private setVisibilityState( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, command: Command, includeDescendants = false, ghost = false @@ -143,7 +148,10 @@ export class FilteringExtension extends Extension { } if (command === Command.HIDE || command === Command.ISOLATE) { - const res = objectIds.reduce((acc, curr) => ((acc[curr] = 1), acc), {}) + const res = objectIds.reduce( + (acc: Record, curr: string) => ((acc[curr] = 1), acc), + {} + ) Object.assign(this.VisibilityState.ids, res) } @@ -159,10 +167,10 @@ export class FilteringExtension extends Extension { this.WTI.walk(this.visibilityWalk.bind(this)) if (command === Command.ISOLATE || command === Command.UNISOLATE) { // this.WTI.walk(this.isolationWalk.bind(this)) - const rvMap = {} + const rvMap: Record = {} this.WTI.walk((node: TreeNode) => { if (!node.model.atomic || this.WTI.isRoot(node)) return true - const rvNodes = this.WTI.getRenderTree().getRenderViewNodesForNode(node, node) + const rvNodes = this.WTI.getRenderTree().getRenderViewNodesForNode(node) if (!this.VisibilityState.ids[node.model.raw.id]) { rvNodes.forEach((rvNode: TreeNode) => { rvMap[rvNode.model.id] = rvNode.model.renderView @@ -181,42 +189,28 @@ export class FilteringExtension extends Extension { } private visibilityWalk(node: TreeNode): boolean { - // if (!node.model.atomic) return true if (this.VisibilityState.ids[node.model.id]) { this.VisibilityState.rvs.push( - ...this.WTI.getRenderTree().getRenderViewsForNode(node, node) - ) - } - return true - } - - private isolationWalk(node: TreeNode): boolean { - if (!node.model.atomic || this.WTI.isRoot(node)) return true - const rvs = this.WTI.getRenderTree().getRenderViewsForNode(node, node) - if (!this.VisibilityState.ids[node.model.raw.id]) { - this.VisibilityState.rvs.push(...rvs) - } else { - this.VisibilityState.rvs = this.VisibilityState.rvs.filter( - (rv) => !rvs.includes(rv) + ...this.WTI.getRenderTree().getRenderViewsForNode(node) ) } return true } - public setColorFilter(prop: PropertyInfo, ghost = true) { + public setColorFilter(prop: PropertyInfo, ghost = true): FilteringState { if (prop.type === 'number') { this.ColorStringFilterState = null - this.ColorNumericFilterState = new ColorNumericFilterState() return this.setNumericColorFilter(prop as NumericPropertyInfo, ghost) } if (prop.type === 'string') { this.ColorNumericFilterState = null - this.ColorStringFilterState = new ColorStringFilterState() return this.setStringColorFilter(prop as StringPropertyInfo, ghost) } + return this.filteringState } - private setNumericColorFilter(numProp: NumericPropertyInfo, ghost) { + private setNumericColorFilter(numProp: NumericPropertyInfo, ghost: boolean) { + this.ColorNumericFilterState = new ColorNumericFilterState() this.ColorNumericFilterState.currentProp = numProp const passMin = numProp.passMin || numProp.min @@ -246,7 +240,7 @@ export class FilteringExtension extends Extension { * as in, if there is an id clash (which will happen for instances), the old implementation's indexOf * would return the first value. Here we choose to do the same */ - const matchingIds = {} + const matchingIds: Record = {} for (let k = 0; k < numProp.valueGroups.length; k++) { if (matchingIds[numProp.valueGroups[k].id]) { continue @@ -265,7 +259,7 @@ export class FilteringExtension extends Extension { if (!node.model.atomic || this.WTI.isRoot(node) || this.WTI.isSubtreeRoot(node)) return true - const rvs = this.WTI.getRenderTree().getRenderViewsForNode(node, node) + const rvs = this.WTI.getRenderTree().getRenderViewsForNode(node) const idx = matchingIds[node.model.raw.id] if (!idx) { nonMatchingRvs.push(...rvs) @@ -275,6 +269,7 @@ export class FilteringExtension extends Extension { value: (idx - passMin) / (passMax - passMin) }) } + return true }) this.ColorNumericFilterState.colorGroups = colorGroups @@ -284,21 +279,23 @@ export class FilteringExtension extends Extension { return this.setFilters() } - private setStringColorFilter(stringProp: StringPropertyInfo, ghost) { + private setStringColorFilter(stringProp: StringPropertyInfo, ghost: boolean) { + this.ColorStringFilterState = new ColorStringFilterState() this.ColorStringFilterState.currentProp = stringProp const valueGroupColors: ValueGroupColorItemStringProps[] = [] for (const vg of stringProp.valueGroups) { const col = stc(vg.value) // TODO: smarter way needed. - const entry = { + const entry: ValueGroupColorItemStringProps = { ...vg, color: new Color(col), - rvs: [] + rvs: [], + idMap: {} } /** This is to avoid indexOf inside the walk callback which is ridiculously slow */ - entry['idMap'] = {} + entry.idMap = {} for (let k = 0; k < vg.ids.length; k++) { - entry['idMap'][vg.ids[k]] = 1 + entry.idMap[vg.ids[k]] = 1 } valueGroupColors.push(entry) } @@ -311,17 +308,16 @@ export class FilteringExtension extends Extension { // they are identified as a different category. // 07.05.2023: Attempt on fixing the issue described above. This fixes #1525, but it does // add a bit of overhead. Not 100% sure if it breaks anything else tho' - const nonMatchingMap = {} + const nonMatchingMap: Record = {} this.WTI.walk((node: TreeNode) => { if (!node.model.atomic || this.WTI.isRoot(node) || this.WTI.isSubtreeRoot(node)) { return true } - const vg = valueGroupColors.find((v) => { - return v['idMap'][node.model.raw.id] + const vg = valueGroupColors.find((v: ValueGroupColorItemStringProps) => { + return v.idMap[node.model.raw.id] }) - const rvNodes = this.WTI.getRenderTree().getRenderViewNodesForNode(node, node) - + const rvNodes = this.WTI.getRenderTree().getRenderViewNodesForNode(node) if (!vg) { rvNodes.forEach( (rvNode) => @@ -331,7 +327,7 @@ export class FilteringExtension extends Extension { return true } - const rvs = [] + const rvs: Array = [] rvNodes.forEach((value: TreeNode) => { if (this.WTI.getRenderTree().getAtomicParent(value) === node) { @@ -348,7 +344,10 @@ export class FilteringExtension extends Extension { const nonMatchingRvs: NodeRenderView[] = Object.values(nonMatchingMap) /** Deleting this since we're not going to use it further */ for (const vg of valueGroupColors) { - delete vg['idMap'] + /** Adamant on this one */ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + delete vg.idMap } this.ColorStringFilterState.colorGroups = valueGroupColors this.ColorStringFilterState.rampTexture = rampTexture @@ -366,7 +365,9 @@ export class FilteringExtension extends Extension { return this.filteringState } - public setUserObjectColors(groups: { objectIds: string[]; color: string }[]) { + public setUserObjectColors( + groups: { objectIds: string[]; color: string }[] + ): FilteringState { this.UserspaceColorState = new UserspaceColorState() // Resetting any other filtering color ops as they're not compatible this.ColorNumericFilterState = null @@ -388,7 +389,7 @@ export class FilteringExtension extends Extension { group.nodes.push(...nodes) nodes.forEach((node: TreeNode) => { const rvsNodes = this.WTI.getRenderTree() - .getRenderViewNodesForNode(node, node) + .getRenderViewNodesForNode(node) .map((rvNode) => rvNode.model.renderView) if (rvsNodes) group.rvs.push(...rvsNodes) }) @@ -404,17 +405,17 @@ export class FilteringExtension extends Extension { return this.setFilters() } - public removeUserObjectColors() { + public removeUserObjectColors(): FilteringState { this.UserspaceColorState = null return this.setFilters() } - public resetFilters(): FilteringState { + public resetFilters(): FilteringState | null { this.VisibilityState = new VisibilityState() this.ColorStringFilterState = null this.ColorNumericFilterState = null this.UserspaceColorState = null - this.StateKey = null + this.StateKey = undefined this.Renderer.resetMaterials() this.viewer.requestRender(UpdateFlags.RENDER | UpdateFlags.SHADOWS) return null @@ -441,8 +442,9 @@ export class FilteringExtension extends Extension { color: group.color.getHexString(), ids: group.ids }) - this.CurrentFilteringState.activePropFilterKey = - this.ColorStringFilterState.currentProp.key + if (this.ColorStringFilterState.currentProp) + this.CurrentFilteringState.activePropFilterKey = + this.ColorStringFilterState.currentProp.key } } // Number based colors @@ -550,7 +552,9 @@ export class FilteringExtension extends Extension { if (this.idCache[key] && this.idCache[key].length) return this.idCache[key] for (let k = 0; k < objectIds.length; k++) { - const node = this.WTI.findId(objectIds[k])[0] + const nodes = this.WTI.findId(objectIds[k]) + if (!nodes) continue + const node = nodes[0] const subtree = node.all((node) => { return node.model.raw !== undefined }) @@ -597,16 +601,16 @@ class VisibilityState { } class ColorStringFilterState { - public currentProp: StringPropertyInfo + public currentProp: StringPropertyInfo | null public colorGroups: ValueGroupColorItemStringProps[] public nonMatchingRvs: NodeRenderView[] - public rampTexture: Texture + public rampTexture: Texture | undefined public ghost = true public reset() { this.currentProp = null this.colorGroups = [] this.nonMatchingRvs = [] - this.rampTexture = null + this.rampTexture = undefined } } @@ -615,14 +619,15 @@ type ValueGroupColorItemStringProps = { ids: string[] color: Color rvs: NodeRenderView[] + idMap: Record } class ColorNumericFilterState { - public currentProp: NumericPropertyInfo - public nonMatchingRvs: NodeRenderView[] - public colorGroups: ValueGroupColorItemNumericProps[] + public currentProp!: NumericPropertyInfo + public nonMatchingRvs!: NodeRenderView[] + public colorGroups!: ValueGroupColorItemNumericProps[] public ghost = true - public matchingIds: string[] + public matchingIds!: Record } type ValueGroupColorItemNumericProps = { @@ -637,7 +642,7 @@ class UserspaceColorState { nodes: TreeNode[] rvs: NodeRenderView[] }[] = [] - public rampTexture: Texture + public rampTexture!: Texture public reset() { this.groups = [] } diff --git a/packages/viewer/src/modules/extensions/SectionOutlines.ts b/packages/viewer/src/modules/extensions/SectionOutlines.ts index 2a7abe1994..43d15b3872 100644 --- a/packages/viewer/src/modules/extensions/SectionOutlines.ts +++ b/packages/viewer/src/modules/extensions/SectionOutlines.ts @@ -5,6 +5,7 @@ import { Group, InterleavedBufferAttribute, Line3, + Material, Plane, Vector2, Vector3 @@ -15,7 +16,7 @@ import { Geometry } from '../converter/Geometry' import SpeckleGhostMaterial from '../materials/SpeckleGhostMaterial' import SpeckleLineMaterial from '../materials/SpeckleLineMaterial' import { Extension } from './Extension' -import { IViewer } from '../..' +import { type IViewer } from '../..' import { SectionTool, SectionToolEvent } from './SectionTool' import { GeometryType } from '../batching/Batch' import { ObjectLayers } from '../../IViewer' @@ -43,7 +44,6 @@ export class SectionOutlines extends Extension { private static readonly INITIAL_BUFFER_SIZE = 60000 // Must be a multiple of 6 private tmpVec: Vector3 = new Vector3() - private tmpVec2: Vector3 = new Vector3() private up: Vector3 = new Vector3(0, 1, 0) private down: Vector3 = new Vector3(0, -1, 0) private left: Vector3 = new Vector3(-1, 0, 0) @@ -94,7 +94,7 @@ export class SectionOutlines extends Extension { this.sectionProvider.on(SectionToolEvent.Updated, this.sectionUpdated.bind(this)) } - public getPlaneOutline(planeId: PlaneId) { + private getPlaneOutline(planeId: PlaneId) { return this.planeOutlines[planeId] } @@ -129,6 +129,10 @@ export class SectionOutlines extends Extension { const tempVector4 = new Vector3() const tempLine = new Line3() const planeId = this.getPlaneId(_plane) + if (!planeId) { + Logger.error(`Invalid plane! Aborting section outline update`) + return + } const clipOutline = this.planeOutlines[planeId].renderable let index = 0 let posAttr = ( @@ -154,13 +158,17 @@ export class SectionOutlines extends Extension { const localPlane = plane return localPlane.intersectsBox(box) }, - intersectsTriangle(tri, i, contained, depth, batchObject) { - i - contained - depth + intersectsTriangle(tri, _i, _contained, _depth, batchObject) { + /** Catering to typescript */ + /** We're intersecting the AS for meshes. There will always be a batchObject */ + if (!batchObject) { + throw new Error('Null batch object in AS intersection!') + } // check each triangle edge to see if it intersects with the plane. If so then // add it to the list of segments. - const material = batches[b].mesh.getBatchObjectMaterial(batchObject) + const material = batches[b].mesh.getBatchObjectMaterial( + batchObject + ) as Material if ( material instanceof SpeckleGhostMaterial || material.visible === false || @@ -349,9 +357,7 @@ export class SectionOutlines extends Extension { ) for (let k = 0; k < planes.length; k++) { this.updatePlaneOutline( - this.viewer - .getRenderer() - .batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[], + this.viewer.getRenderer().batcher.getBatches(undefined, GeometryType.MESH), planes[k], outlineOffset ) @@ -374,7 +380,7 @@ export class SectionOutlines extends Extension { Geometry.updateRTEGeometry(outline.renderable.geometry, buffer) } - private getPlaneId(plane: Plane) { + private getPlaneId(plane: Plane): PlaneId | undefined { this.tmpVec.set( Math.round(plane.normal.x), Math.round(plane.normal.y), @@ -386,5 +392,7 @@ export class SectionOutlines extends Extension { if (this.tmpVec.equals(this.down)) return PlaneId.NEGATIVE_Y if (this.tmpVec.equals(this.back)) return PlaneId.NEGATIVE_Z if (this.tmpVec.equals(this.forward)) return PlaneId.POSITIVE_Z + + return undefined } } diff --git a/packages/viewer/src/modules/extensions/SectionTool.ts b/packages/viewer/src/modules/extensions/SectionTool.ts index 561daa9428..7e5ea75858 100644 --- a/packages/viewer/src/modules/extensions/SectionTool.ts +++ b/packages/viewer/src/modules/extensions/SectionTool.ts @@ -13,10 +13,12 @@ import { BufferAttribute, Raycaster, DoubleSide, - SphereGeometry + SphereGeometry, + type Intersection, + Vector2 } from 'three' import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js' -import { IViewer, ObjectLayers } from '../../IViewer' +import { type IViewer, ObjectLayers } from '../../IViewer' import { Extension } from './Extension' import { CameraEvent } from '../objects/SpeckleCamera' import { InputEvent } from '../input/Input' @@ -28,6 +30,12 @@ export enum SectionToolEvent { Updated = 'section-box-changed' } +export interface SectionToolEventPayload { + [SectionToolEvent.DragStart]: void + [SectionToolEvent.DragEnd]: void + [SectionToolEvent.Updated]: Plane[] +} + export class SectionTool extends Extension { public get inject() { return [CameraController] @@ -39,20 +47,20 @@ export class SectionTool extends Extension { protected boxMaterial: MeshStandardMaterial protected boxMesh: Mesh protected boxMeshHelper: Box3Helper - protected boxMeshHelperMaterial: LineBasicMaterial + protected boxMeshHelperMaterial!: LineBasicMaterial protected plane: PlaneGeometry protected hoverPlane: Mesh protected sphere: Mesh protected sidesSimple: { [id: string]: { verts: number[]; axis: string } } - protected currentRange: number[] - protected planes: Plane[] + protected currentRange: number[] | null + protected planes!: Plane[] - protected prevPosition: Vector3 + protected prevPosition: Vector3 | null protected attachedToBox: boolean - protected controls: TransformControls - protected allowSelection: boolean + protected controls!: TransformControls + protected allowSelection!: boolean protected raycaster: Raycaster @@ -68,6 +76,14 @@ export class SectionTool extends Extension { this.viewer.requestRender() } + public get visible(): boolean { + return this.display.visible + } + + public set visible(value: boolean) { + this.display.visible = value + } + constructor(viewer: IViewer, protected cameraProvider: CameraController) { super(viewer) this.viewer = viewer @@ -94,7 +110,7 @@ export class SectionTool extends Extension { this.display.add(this.boxMesh) - this.boxMeshHelper = new Box3Helper(this.boxGeometry.boundingBox) + this.boxMeshHelper = new Box3Helper(this.boxGeometry.boundingBox || new Box3()) this.boxMeshHelper.material = new LineBasicMaterial({ color: 0x0a66ff, opacity: 0.4 @@ -162,15 +178,27 @@ export class SectionTool extends Extension { this.cameraProvider.on(CameraEvent.FrameUpdate, (data: boolean) => { this.allowSelection = !data }) - this.viewer.getRenderer().input.on(InputEvent.Click, this._clickHandler.bind(this)) + this.viewer.getRenderer().input.on(InputEvent.Click, this.clickHandler.bind(this)) this.enabled = false } + public on( + eventType: T, + listener: (arg: SectionToolEventPayload[T]) => void + ): void { + super.on(eventType, listener) + } + private _setupControls() { + const camera = this.viewer.getRenderer().renderingCamera + if (!camera) { + throw new Error('Cannot create SectionTool extension. No rendering camera found') + } + this.controls?.dispose() this.controls?.detach() this.controls = new TransformControls( - this.viewer.getRenderer().renderingCamera, + camera, this.viewer.getRenderer().renderer.domElement ) for (let k = 0; k < this.controls?.children.length; k++) { @@ -203,7 +231,7 @@ export class SectionTool extends Extension { private _draggingChangeHandler() { if (!this.display.visible) return this.boxGeometry.computeBoundingBox() - this.boxMeshHelper.box.copy(this.boxGeometry.boundingBox) + this.boxMeshHelper.box.copy(this.boxGeometry.boundingBox || new Box3()) // Dragging a side / plane if (this.dragging && this.currentRange) { @@ -247,16 +275,16 @@ export class SectionTool extends Extension { this.prevPosition = this.sphere.position.clone() } this.viewer.getRenderer().clippingPlanes = this.planes - this.viewer.getRenderer().clippingVolume = this.getCurrentBox() + this.viewer.getRenderer().clippingVolume = this.getBox() this.emit(SectionToolEvent.Updated, this.planes) this.viewer.requestRender() } - private _clickHandler(args) { + private clickHandler(args: Vector2 & { event: PointerEvent; multiSelect: boolean }) { if (!this.allowSelection || this.dragging) return this.raycaster.setFromCamera(args, this.cameraProvider.renderingCamera) - let intersectedObjects = [] + let intersectedObjects: Array = [] if (this.display.visible) { intersectedObjects = this.raycaster.intersectObject(this.boxMesh) } @@ -272,8 +300,14 @@ export class SectionTool extends Extension { this.hoverPlane.visible = true const side = this.sidesSimple[ - `${intersectedObjects[0].face.a}${intersectedObjects[0].face.b}${intersectedObjects[0].face.c}` + `${intersectedObjects[0].face?.a}${intersectedObjects[0].face?.b}${intersectedObjects[0].face?.c}` ] + /** Catering to typescript + * We're intersection an indexed mesh. There will always be an intersected face + */ + if (!side) { + throw new Error('Cannot determine section side') + } this.controls.showX = side.axis === 'x' this.controls.showY = side.axis === 'y' this.controls.showZ = side.axis === 'z' @@ -413,12 +447,12 @@ export class SectionTool extends Extension { this.controls.showZ = true } - public getCurrentBox() { + public getBox(): Box3 { if (!this.display.visible) return new Box3() - return this.boxGeometry.boundingBox + return this.boxGeometry.boundingBox || new Box3() } - public setBox(targetBox, offset = 0) { + public setBox(targetBox: Box3, offset = 0): void { let box if (targetBox) box = targetBox @@ -479,22 +513,14 @@ export class SectionTool extends Extension { this.boxGeometry.computeBoundingSphere() this._generateOrUpdatePlanes() this._attachControlsToBox() - this.boxMeshHelper.box.copy(this.boxGeometry.boundingBox) + this.boxMeshHelper.box.copy(this.boxGeometry.boundingBox || new Box3()) this.emit(SectionToolEvent.Updated, this.planes) this.viewer.getRenderer().clippingPlanes = this.planes - this.viewer.getRenderer().clippingVolume = this.getCurrentBox() + this.viewer.getRenderer().clippingVolume = this.getBox() this.viewer.requestRender() } - public toggle() { + public toggle(): void { this.enabled = !this._enabled } - - public displayOff() { - this.display.visible = false - } - - public displayOn() { - this.display.visible = true - } } diff --git a/packages/viewer/src/modules/extensions/SelectionExtension.ts b/packages/viewer/src/modules/extensions/SelectionExtension.ts index 025504b33c..5b7624d09d 100644 --- a/packages/viewer/src/modules/extensions/SelectionExtension.ts +++ b/packages/viewer/src/modules/extensions/SelectionExtension.ts @@ -1,20 +1,23 @@ -import { ExtendedIntersection } from '../objects/SpeckleRaycaster' +import { type ExtendedIntersection } from '../objects/SpeckleRaycaster' import { Extension } from './Extension' import { NodeRenderView } from '../tree/NodeRenderView' -import { Material } from 'three' +import { Material, Vector2 } from 'three' import { InputEvent } from '../input/Input' import { MathUtils } from 'three' import { - IViewer, + type IViewer, ObjectLayers, - SelectionEvent, + type SelectionEvent, UpdateFlags, ViewerEvent } from '../../IViewer' -import Materials, { DisplayStyle, RenderMaterial } from '../materials/Materials' +import Materials, { + type DisplayStyle, + type RenderMaterial +} from '../materials/Materials' import { StencilOutlineType } from '../../IViewer' -import { MaterialOptions } from '../materials/MaterialOptions' -import { TreeNode } from '../tree/WorldTree' +import { type MaterialOptions } from '../materials/MaterialOptions' +import { type TreeNode } from '../tree/WorldTree' import { CameraController } from './CameraController' export interface SelectionExtensionOptions { @@ -55,21 +58,23 @@ export class SelectionExtension extends Extension { protected selectedNodes: Array = [] protected selectionRvs: { [id: string]: NodeRenderView } = {} protected selectionMaterials: { [id: string]: Material } = {} - protected options: SelectionExtensionOptions - protected hoverRv: NodeRenderView - protected hoverMaterial: Material - protected selectionMaterialData: RenderMaterial & DisplayStyle & MaterialOptions - protected hoverMaterialData: RenderMaterial & DisplayStyle & MaterialOptions - protected transparentSelectionMaterialData: RenderMaterial & + protected hoverRv!: NodeRenderView | null + protected hoverMaterial!: Material | null + protected selectionMaterialData!: RenderMaterial & DisplayStyle & MaterialOptions + protected hoverMaterialData!: RenderMaterial & DisplayStyle & MaterialOptions + protected transparentSelectionMaterialData!: RenderMaterial & DisplayStyle & MaterialOptions - protected transparentHoverMaterialData: RenderMaterial & + protected transparentHoverMaterialData!: RenderMaterial & + DisplayStyle & + MaterialOptions + protected hiddenSelectionMaterialData!: RenderMaterial & DisplayStyle & MaterialOptions - protected hiddenSelectionMaterialData: RenderMaterial & DisplayStyle & MaterialOptions protected _enabled = true + protected _options!: SelectionExtensionOptions - public get enabled() { + public get enabled(): boolean { return this._enabled } @@ -77,19 +82,12 @@ export class SelectionExtension extends Extension { this._enabled = value } - public constructor(viewer: IViewer, protected cameraProvider: CameraController) { - super(viewer) - this.viewer.on(ViewerEvent.ObjectClicked, this.onObjectClicked.bind(this)) - this.viewer.on(ViewerEvent.ObjectDoubleClicked, this.onObjectDoubleClick.bind(this)) - this.viewer - .getRenderer() - .input.on(InputEvent.PointerMove, this.onPointerMove.bind(this)) - this.setOptions(DefaultSelectionExtensionOptions) + public get options(): SelectionExtensionOptions { + return this._options } - public setOptions(options: SelectionExtensionOptions) { - this.options = options - /** Opaque selection */ + public set options(value: SelectionExtensionOptions) { + this._options = value this.selectionMaterialData = Object.assign({}, this.options.selectionMaterialData) /** Transparent selection */ this.transparentSelectionMaterialData = Object.assign( @@ -114,11 +112,24 @@ export class SelectionExtension extends Extension { this.transparentHoverMaterialData.opacity = 0.5 } - public getSelectedObjects() { + public constructor(viewer: IViewer, protected cameraProvider: CameraController) { + super(viewer) + this.viewer.on(ViewerEvent.ObjectClicked, this.onObjectClicked.bind(this)) + this.viewer.on(ViewerEvent.ObjectDoubleClicked, this.onObjectDoubleClick.bind(this)) + this.viewer + .getRenderer() + .input.on(InputEvent.PointerMove, this.onPointerMove.bind(this)) + this.options = DefaultSelectionExtensionOptions + } + + public getSelectedObjects(): Array> { return this.selectedNodes.map((v) => v.model.raw) } + public getSelectedNodes(): Array { + return this.selectedNodes + } - public selectObjects(ids: Array, multiSelect = false) { + public selectObjects(ids: Array, multiSelect = false): void { if (!this._enabled) return if (!multiSelect) { @@ -126,18 +137,21 @@ export class SelectionExtension extends Extension { } for (let k = 0; k < ids.length; k++) { - this.selectedNodes.push(...this.viewer.getWorldTree().findId(ids[k])) + const foundNodes = this.viewer.getWorldTree().findId(ids[k]) + if (foundNodes) this.selectedNodes.push(...foundNodes) } this.applySelection() } - public unselectObjects(ids: Array) { + /**TO DO: This is redundant */ + public unselectObjects(ids: Array): void { if (!this._enabled) return const nodes = [] for (let k = 0; k < ids.length; k++) { - nodes.push(...this.viewer.getWorldTree().findId(ids[k])) + const foundNodes = this.viewer.getWorldTree().findId(ids[k]) + if (foundNodes) nodes.push(...foundNodes) } this.clearSelection(nodes) } @@ -149,10 +163,10 @@ export class SelectionExtension extends Extension { return } - const rvs = [] + const rvs: Array = [] nodes.forEach((node: TreeNode) => { rvs.push( - ...this.viewer.getWorldTree().getRenderTree().getRenderViewsForNode(node, node) + ...this.viewer.getWorldTree().getRenderTree().getRenderViewsForNode(node) ) }) this.removeSelection(rvs) @@ -162,7 +176,7 @@ export class SelectionExtension extends Extension { ) } - protected onObjectClicked(selection: SelectionEvent) { + protected onObjectClicked(selection: SelectionEvent | null) { if (!this._enabled) return if (!selection) { @@ -177,7 +191,7 @@ export class SelectionExtension extends Extension { this.applySelection() } - protected onObjectDoubleClick(selectionInfo: SelectionEvent) { + protected onObjectDoubleClick(selectionInfo: SelectionEvent | null) { if (!this._enabled) return if (!selectionInfo) { @@ -190,8 +204,10 @@ export class SelectionExtension extends Extension { ) } - protected onPointerMove(e) { + protected onPointerMove(e: Vector2 & { event: Event }) { if (!this._enabled) return + const camera = this.viewer.getRenderer().renderingCamera + if (!camera) return if (!this.options.hoverMaterialData) return const result = @@ -199,16 +215,16 @@ export class SelectionExtension extends Extension { .getRenderer() .intersections.intersect( this.viewer.getRenderer().scene, - this.viewer.getRenderer().renderingCamera, + camera, e, - true, - this.viewer.getRenderer().clippingVolume, [ ObjectLayers.STREAM_CONTENT_MESH, ObjectLayers.STREAM_CONTENT_POINT, ObjectLayers.STREAM_CONTENT_LINE, ObjectLayers.STREAM_CONTENT_TEXT - ] + ], + true, + this.viewer.getRenderer().clippingVolume ) as ExtendedIntersection[]) || [] /* TEMPORARY */ @@ -228,17 +244,21 @@ export class SelectionExtension extends Extension { const rvs = this.viewer .getWorldTree() .getRenderTree() - .getRenderViewsForNode(this.selectedNodes[k], this.selectedNodes[k]) + .getRenderViewsForNode(this.selectedNodes[k]) rvs.forEach((rv: NodeRenderView) => { if (!this.selectionRvs[rv.guid]) this.selectionRvs[rv.guid] = rv - if (!this.selectionMaterials[rv.guid]) - this.selectionMaterials[rv.guid] = this.viewer.getRenderer().getMaterial(rv) + if (!this.selectionMaterials[rv.guid]) { + this.selectionMaterials[rv.guid] = this.viewer + .getRenderer() + .getMaterial(rv) as Material + } }) } const rvs = Object.values(this.selectionRvs) const opaqueRvs = rvs.filter( (value) => + this.selectionMaterials[value.guid] && this.selectionMaterials[value.guid].visible && this.selectionMaterials[value.guid] && !( @@ -248,13 +268,16 @@ export class SelectionExtension extends Extension { ) const transparentRvs = rvs.filter( (value) => + this.selectionMaterials[value.guid] && this.selectionMaterials[value.guid].visible && this.selectionMaterials[value.guid] && this.selectionMaterials[value.guid].transparent && this.selectionMaterials[value.guid].opacity < 1 ) const hiddenRvs = rvs.filter( - (value) => this.selectionMaterials[value.guid].visible === false + (value) => + this.selectionMaterials[value.guid] && + this.selectionMaterials[value.guid].visible === false ) this.viewer.getRenderer().setMaterial(opaqueRvs, this.selectionMaterialData) @@ -268,7 +291,7 @@ export class SelectionExtension extends Extension { protected removeSelection(rvs?: Array) { this.removeHover() - const materialMap = {} + const materialMap: Record = {} rvs = rvs ? rvs : Object.values(this.selectionRvs) rvs.forEach((rv: NodeRenderView) => { const material = this.selectionMaterials[rv.guid] @@ -293,7 +316,7 @@ export class SelectionExtension extends Extension { } } - protected applyHover(renderView: NodeRenderView) { + protected applyHover(renderView: NodeRenderView | null) { this.removeHover() if (!renderView) return @@ -303,7 +326,7 @@ export class SelectionExtension extends Extension { this.removeHover() this.hoverRv = renderView - this.hoverMaterial = this.viewer.getRenderer().getMaterial(this.hoverRv) + this.hoverMaterial = this.viewer.getRenderer().getMaterial(this.hoverRv) as Material this.viewer .getRenderer() .setMaterial( @@ -317,7 +340,7 @@ export class SelectionExtension extends Extension { } protected removeHover() { - if (this.hoverRv) + if (this.hoverRv && this.hoverMaterial) this.viewer.getRenderer().setMaterial([this.hoverRv], this.hoverMaterial) this.hoverRv = null this.hoverMaterial = null diff --git a/packages/viewer/src/modules/extensions/measurements/Measurement.ts b/packages/viewer/src/modules/extensions/measurements/Measurement.ts index 479898f1b2..a4591561ea 100644 --- a/packages/viewer/src/modules/extensions/measurements/Measurement.ts +++ b/packages/viewer/src/modules/extensions/measurements/Measurement.ts @@ -1,6 +1,14 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable @typescript-eslint/no-empty-function */ -import { Box3, Camera, Object3D, Plane, Vector2, Vector3, Vector4 } from 'three' +import { + Box3, + Camera, + Object3D, + Plane, + Raycaster, + Vector2, + Vector3, + Vector4, + type Intersection +} from 'three' export enum MeasurementState { HIDDEN, @@ -14,8 +22,8 @@ export abstract class Measurement extends Object3D { public endPoint: Vector3 = new Vector3() public startNormal: Vector3 = new Vector3() public endNormal: Vector3 = new Vector3() - public startLineLength: number - public endLineLength: number + public startLineLength!: number + public endLineLength!: number public value = 0 public units = 'm' public precision = 2 @@ -31,7 +39,7 @@ export abstract class Measurement extends Object3D { protected static vec2Buff0: Vector2 = new Vector2() protected _state: MeasurementState = MeasurementState.HIDDEN - protected renderingCamera: Camera + protected renderingCamera: Camera | null protected renderingSize: Vector2 = new Vector2() public set state(value: MeasurementState) { @@ -42,18 +50,19 @@ export abstract class Measurement extends Object3D { return this._state } - public set isVisible(value: boolean) {} + public abstract set isVisible(value: boolean) public get bounds(): Box3 { return new Box3().expandByPoint(this.startPoint).expandByPoint(this.endPoint) } - public frameUpdate(camera: Camera, size: Vector2, bounds: Box3) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public frameUpdate(camera: Camera | null, size: Vector2, _bounds: Box3) { this.renderingCamera = camera this.renderingSize.copy(size) } - public update() {} - public raycast(raycaster, intersects) {} - public highlight(value: boolean) {} - public updateClippingPlanes(planes: Plane[]) {} + public abstract update(): void + public abstract raycast(_raycaster: Raycaster, _intersects: Array): void + public abstract highlight(_value: boolean): void + public abstract updateClippingPlanes(_planes: Plane[]): void } diff --git a/packages/viewer/src/modules/extensions/measurements/MeasurementPointGizmo.ts b/packages/viewer/src/modules/extensions/measurements/MeasurementPointGizmo.ts index a9904b31e8..8c3c012c6f 100644 --- a/packages/viewer/src/modules/extensions/measurements/MeasurementPointGizmo.ts +++ b/packages/viewer/src/modules/extensions/measurements/MeasurementPointGizmo.ts @@ -14,8 +14,10 @@ import { PerspectiveCamera, Plane, Quaternion, + Raycaster, Vector2, - Vector3 + Vector3, + type Intersection } from 'three' import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js' import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js' @@ -90,7 +92,10 @@ export class MeasurementPointGizmo extends Group { material.polygonOffset = true material.polygonOffsetFactor = -5 material.polygonOffsetUnits = 5 - material.opacity = this._style.discOpacity + material.opacity = + this._style.discOpacity !== undefined + ? this._style.discOpacity + : DefaultMeasurementPointGizmoStyle.discOpacity material.transparent = material.opacity < 1 return material } @@ -117,8 +122,11 @@ export class MeasurementPointGizmo extends Group { } lineMaterial.linewidth = 2 lineMaterial.worldUnits = false - lineMaterial.resolution = new Vector2(1513, 1306) - lineMaterial.opacity = this._style.lineOpacity + lineMaterial.resolution = new Vector2(256, 256) + lineMaterial.opacity = + this._style.lineOpacity !== undefined + ? this._style.lineOpacity + : DefaultMeasurementPointGizmoStyle.lineOpacity lineMaterial.transparent = lineMaterial.opacity < 1 lineMaterial.depthTest = false return lineMaterial @@ -129,13 +137,18 @@ export class MeasurementPointGizmo extends Group { { color: color ? color : this._style.pointColor }, ['BILLBOARD_FIXED'] ) - material.opacity = this._style.pointOpacity + material.opacity = + this._style.pointOpacity !== undefined + ? this._style.pointOpacity + : DefaultMeasurementPointGizmoStyle.pointOpacity material.transparent = material.opacity < 1 material.color.convertSRGBToLinear() material.toneMapped = false material.depthTest = false material.billboardPixelHeight = - this._style.pointPixelHeight * window.devicePixelRatio + (this._style.pointPixelHeight !== undefined + ? this._style.pointPixelHeight + : DefaultMeasurementPointGizmoStyle.pointPixelHeight) * window.devicePixelRatio material.userData.billboardPos.value.copy(this.point.position) return material } @@ -151,11 +164,16 @@ export class MeasurementPointGizmo extends Group { ) material.toneMapped = false material.color.convertSRGBToLinear() - material.opacity = this._style.textOpacity + material.opacity = + this._style.textOpacity !== undefined + ? this._style.textOpacity + : DefaultMeasurementPointGizmoStyle.textOpacity material.transparent = material.opacity < 1 material.depthTest = false material.billboardPixelHeight = - this._style.textPixelHeight * window.devicePixelRatio + (this._style.textPixelHeight !== undefined + ? this._style.textPixelHeight + : DefaultMeasurementPointGizmoStyle.textPixelHeight) * window.devicePixelRatio material.userData.billboardPos.value.copy(this.text.position) return material.getDerivedMaterial() @@ -169,7 +187,7 @@ export class MeasurementPointGizmo extends Group { const doublePositions = new Float64Array(geometry.attributes.position.array) Geometry.updateRTEGeometry(geometry, doublePositions) - this.disc = new Mesh(geometry, null) + this.disc = new Mesh(geometry, undefined) this.disc.layers.set(ObjectLayers.MEASUREMENTS) const buffer = new Float64Array(18) @@ -181,7 +199,7 @@ export class MeasurementPointGizmo extends Group { Geometry.updateRTEGeometry(lineGeometry, buffer) - this.line = new LineSegments2(lineGeometry, null) + this.line = new LineSegments2(lineGeometry, undefined) this.line.computeLineDistances() this.line.name = `test-mesurements-line` this.line.frustumCulled = false @@ -190,7 +208,7 @@ export class MeasurementPointGizmo extends Group { const sphereGeometry = new CircleGeometry(1, 16) - this.point = new Mesh(sphereGeometry, null) + this.point = new Mesh(sphereGeometry, undefined) this.point.layers.set(ObjectLayers.MEASUREMENTS) this.point.visible = false this.point.renderOrder = 1 @@ -198,7 +216,10 @@ export class MeasurementPointGizmo extends Group { const point2 = new Mesh(sphereGeometry, this.getPointMaterial(0xffffff)) point2.renderOrder = 2 point2.material.billboardPixelHeight = - this._style.pointPixelHeight * window.devicePixelRatio - + (this._style.pointPixelHeight !== undefined + ? this._style.pointPixelHeight + : DefaultMeasurementPointGizmoStyle.pointPixelHeight) * + window.devicePixelRatio - 2 * window.devicePixelRatio point2.layers.set(ObjectLayers.MEASUREMENTS) this.point.add(point2) @@ -211,7 +232,7 @@ export class MeasurementPointGizmo extends Group { this.add(this.line) this.add(this.text) - this.style = style + this.style = style ? style : DefaultMeasurementPointGizmoStyle } public enable(disc: boolean, line: boolean, point: boolean, text: boolean) { @@ -224,7 +245,12 @@ export class MeasurementPointGizmo extends Group { } public frameUpdate(camera: Camera, bounds: Box3) { - if (camera.type === 'PerspectiveCamera' && +this._style.fixedSize > 0) { + if ( + camera.type === 'PerspectiveCamera' && + +(this._style.fixedSize !== undefined + ? this._style.fixedSize + : DefaultMeasurementPointGizmoStyle.fixedSize) > 0 + ) { const cam = camera as PerspectiveCamera const cameraObjectDistance = cam.position.distanceTo(this.disc.position) const worldSize = Math.abs(2 * Math.tan(cam.fov / 2.0) * cameraObjectDistance) @@ -233,7 +259,12 @@ export class MeasurementPointGizmo extends Group { this.disc.scale.set(size, size, size) this.disc.matrixWorldNeedsUpdate = true } - if (camera.type === 'OrthographicCamera' && +this._style.fixedSize > 0) { + if ( + camera.type === 'OrthographicCamera' && + +(this._style.fixedSize !== undefined + ? this._style.fixedSize + : DefaultMeasurementPointGizmoStyle.fixedSize) > 0 + ) { const cam = camera as OrthographicCamera const orthoSize = cam.top - cam.bottom const size = (orthoSize / cam.zoom) * 0.0075 @@ -309,7 +340,7 @@ export class MeasurementPointGizmo extends Group { backgroundPixelHeight: 20 } this.text.setTransform(position, quaternion, scale) - this.text.backgroundMesh.renderOrder = 3 + if (this.text.backgroundMesh) this.text.backgroundMesh.renderOrder = 3 this.text.textMesh.renderOrder = 4 }) } @@ -321,7 +352,7 @@ export class MeasurementPointGizmo extends Group { this.text.textMesh.material = this.getTextMaterial() } - public raycast(raycaster, intersects) { + public raycast(raycaster: Raycaster, intersects: Array) { // this.disc.raycast(raycaster, intersects) this.line.raycast(raycaster, intersects) // this.point.raycast(raycaster, intersects) diff --git a/packages/viewer/src/modules/extensions/measurements/MeasurementsExtension.ts b/packages/viewer/src/modules/extensions/measurements/MeasurementsExtension.ts index d3f7bd7a24..b29e1f06d4 100644 --- a/packages/viewer/src/modules/extensions/measurements/MeasurementsExtension.ts +++ b/packages/viewer/src/modules/extensions/measurements/MeasurementsExtension.ts @@ -1,13 +1,12 @@ import SpeckleRenderer from '../../SpeckleRenderer' -import { IViewer, ObjectLayers } from '../../../IViewer' +import { type IViewer, ObjectLayers } from '../../../IViewer' import { PerpendicularMeasurement } from './PerpendicularMeasurement' import { Plane, Ray, Raycaster, Vector2, Vector3 } from 'three' import { PointToPointMeasurement } from './PointToPointMeasurement' import { Measurement, MeasurementState } from './Measurement' -import { ExtendedIntersection } from '../../objects/SpeckleRaycaster' +import { ExtendedMeshIntersection } from '../../objects/SpeckleRaycaster' import Logger from 'js-logger' -import SpeckleMesh from '../../objects/SpeckleMesh' import SpeckleGhostMaterial from '../../materials/SpeckleGhostMaterial' import { Extension } from '../Extension' import { InputEvent } from '../../input/Input' @@ -39,12 +38,12 @@ export class MeasurementsExtension extends Extension { return [CameraController] } - protected renderer: SpeckleRenderer = null + protected renderer: SpeckleRenderer protected measurements: Measurement[] = [] - protected _activeMeasurement: Measurement = null - protected _selectedMeasurement: Measurement = null - protected raycaster: Raycaster = null + protected _activeMeasurement: Measurement | null = null + protected _selectedMeasurement: Measurement | null = null + protected raycaster: Raycaster protected _options: MeasurementOptions = Object.assign({}, DefaultMeasurementsOptions) private _frameLock = false @@ -60,10 +59,6 @@ export class MeasurementsExtension extends Extension { return this._enabled } - public get visible(): boolean { - return this._options.visible - } - public set enabled(value: boolean) { this._enabled = value if (this._activeMeasurement) { @@ -75,8 +70,8 @@ export class MeasurementsExtension extends Extension { this.renderer.resetPipeline() } - public set paused(value: boolean) { - this._paused = value + public get options(): MeasurementOptions { + return this._options } public set options(options: MeasurementOptions) { @@ -92,11 +87,11 @@ export class MeasurementsExtension extends Extension { this.applyOptions() } - public get selectedMeasurement(): Measurement { + public get selectedMeasurement(): Measurement | null { return this._selectedMeasurement } - public get activeMeasurement(): Measurement { + public get activeMeasurement(): Measurement | null { return this._activeMeasurement } @@ -111,25 +106,22 @@ export class MeasurementsExtension extends Extension { this.renderer.input.on(InputEvent.DoubleClick, this.onPointerDoubleClick.bind(this)) } - public onLateUpdate(deltaTime: number) { - deltaTime + public onLateUpdate() { if (!this._enabled) return + const camera = this.renderer.renderingCamera + if (!camera) return this._frameLock = false this.renderer.renderer.getDrawingBufferSize(this.screenBuff0) if (this._activeMeasurement) this._activeMeasurement.frameUpdate( - this.renderer.renderingCamera, + camera, this.screenBuff0, this.renderer.sceneBox ) this.measurements.forEach((value: Measurement) => { - value.frameUpdate( - this.renderer.renderingCamera, - this.screenBuff0, - this.renderer.sceneBox - ) + value.frameUpdate(camera, this.screenBuff0, this.renderer.sceneBox) }) } @@ -137,28 +129,29 @@ export class MeasurementsExtension extends Extension { this.renderer.renderer.getDrawingBufferSize(this.screenBuff0) } - protected onPointerMove(data) { + protected onPointerMove(data: Vector2 & { event: Event }) { if (!this._enabled || this._paused) return + const camera = this.renderer.renderingCamera + if (!camera) return + if (this._frameLock) { return } - let result = - (this.renderer.intersections.intersect( + let result: ExtendedMeshIntersection[] = + this.renderer.intersections.intersect( this.renderer.scene, - this.renderer.renderingCamera, + camera, data, + ObjectLayers.STREAM_CONTENT_MESH, true, - this.renderer.clippingVolume, - [ObjectLayers.STREAM_CONTENT_MESH] - ) as ExtendedIntersection[]) || [] + this.renderer.clippingVolume + ) || [] - result = result.filter((value: ExtendedIntersection) => { - const material = (value.object as unknown as SpeckleMesh).getBatchObjectMaterial( - value.batchObject - ) - return !(material instanceof SpeckleGhostMaterial) && material.visible + result = result.filter((value: ExtendedMeshIntersection) => { + const material = value.object.getBatchObjectMaterial(value.batchObject) + return material && !(material instanceof SpeckleGhostMaterial) && material.visible }) if (!result.length) { @@ -166,16 +159,21 @@ export class MeasurementsExtension extends Extension { return } - if (!this._activeMeasurement) { - this.startMeasurement() - } - this._activeMeasurement.isVisible = true - + /** Catering to typescript + * There will always be an intersected face. We're casting against indexed meshes only + */ this.pointBuff.copy(result[0].point) this.normalBuff.copy(result[0].face.normal) + if (this._options.vertexSnap) { this.snap(result[0], this.pointBuff, this.normalBuff) } + + if (!this._activeMeasurement) { + this._activeMeasurement = this.startMeasurement() + this._activeMeasurement.isVisible = true + } + if (this._activeMeasurement.state === MeasurementState.DANGLING_START) { this._activeMeasurement.startPoint.copy(this.pointBuff) this._activeMeasurement.startNormal.copy(this.normalBuff) @@ -189,9 +187,12 @@ export class MeasurementsExtension extends Extension { this.renderer.resetPipeline() this._frameLock = true this._sceneHit = true + // console.log('Time -> ', performance.now() - start) } - protected onPointerClick(data) { + protected onPointerClick( + data: { event: PointerEvent; multiSelect: boolean } & Vector2 + ) { if (!this._enabled) return const measurement = this.pickMeasurement(data) @@ -216,7 +217,9 @@ export class MeasurementsExtension extends Extension { } } - protected onPointerDoubleClick(data) { + protected onPointerDoubleClick( + data: Vector2 & { event: PointerEvent; multiSelect: boolean } + ) { const measurement = this.pickMeasurement(data) if (measurement) { this.cameraProvider.setCameraView(measurement.bounds, true) @@ -228,25 +231,24 @@ export class MeasurementsExtension extends Extension { } } - protected autoLazerMeasure(data) { + protected autoLazerMeasure(data: Vector2) { if (!this._activeMeasurement) return + if (!this.renderer.renderingCamera) return this._activeMeasurement.state = MeasurementState.DANGLING_START - let result = - (this.renderer.intersections.intersect( + let result: ExtendedMeshIntersection[] = + this.renderer.intersections.intersect( this.renderer.scene, this.renderer.renderingCamera, data, + ObjectLayers.STREAM_CONTENT_MESH, true, - this.renderer.clippingVolume, - [ObjectLayers.STREAM_CONTENT_MESH] - ) as ExtendedIntersection[]) || [] + this.renderer.clippingVolume + ) || [] result = result.filter((value) => { - const material = (value.object as unknown as SpeckleMesh).getBatchObjectMaterial( - value.batchObject - ) - return !(material instanceof SpeckleGhostMaterial) && material.visible + const material = value.object.getBatchObjectMaterial(value.batchObject) + return material && !(material instanceof SpeckleGhostMaterial) && material.visible }) if (!result.length) return @@ -257,21 +259,19 @@ export class MeasurementsExtension extends Extension { const offsetPoint = new Vector3() .copy(startPoint) .add(new Vector3().copy(startNormal).multiplyScalar(0.000001)) - let perpResult = - (this.renderer.intersections.intersectRay( + let perpResult: ExtendedMeshIntersection[] = + this.renderer.intersections.intersectRay( this.renderer.scene, this.renderer.renderingCamera, new Ray(offsetPoint, startNormal), + ObjectLayers.STREAM_CONTENT_MESH, true, - this.renderer.clippingVolume, - [ObjectLayers.STREAM_CONTENT_MESH] - ) as ExtendedIntersection[]) || [] + this.renderer.clippingVolume + ) || [] - perpResult = perpResult.filter((value) => { - const material = (value.object as unknown as SpeckleMesh).getBatchObjectMaterial( - value.batchObject - ) - return !(material instanceof SpeckleGhostMaterial) && material.visible + perpResult = perpResult.filter((value: ExtendedMeshIntersection) => { + const material = value.object.getBatchObjectMaterial(value.batchObject) + return material && !(material instanceof SpeckleGhostMaterial) && material.visible }) if (!perpResult.length) { @@ -288,29 +288,34 @@ export class MeasurementsExtension extends Extension { this.finishMeasurement() } - protected startMeasurement() { + protected startMeasurement(): Measurement { + let measurement: Measurement if (this._options.type === MeasurementType.PERPENDICULAR) - this._activeMeasurement = new PerpendicularMeasurement() + measurement = new PerpendicularMeasurement() else if (this._options.type === MeasurementType.POINTTOPOINT) - this._activeMeasurement = new PointToPointMeasurement() + measurement = new PointToPointMeasurement() + else throw new Error('Unsupported measurement type!') - this._activeMeasurement.state = MeasurementState.DANGLING_START - this._activeMeasurement.frameUpdate( + measurement.state = MeasurementState.DANGLING_START + measurement.frameUpdate( this.renderer.renderingCamera, this.screenBuff0, this.renderer.sceneBox ) - this.renderer.scene.add(this._activeMeasurement) + this.renderer.scene.add(measurement) + return measurement } protected cancelMeasurement() { - this.renderer.scene.remove(this._activeMeasurement) + if (this._activeMeasurement) this.renderer.scene.remove(this._activeMeasurement) this._activeMeasurement = null this.renderer.needsRender = true this.renderer.resetPipeline() } protected finishMeasurement() { + if (!this._activeMeasurement) return + this._activeMeasurement.state = MeasurementState.COMPLETE this._activeMeasurement.update() if (this._activeMeasurement.value > 0) { @@ -334,7 +339,7 @@ export class MeasurementsExtension extends Extension { } } - public clearMeasurements() { + public clearMeasurements(): void { this.removeMeasurement() this.measurements.forEach((measurement: Measurement) => { this.renderer.scene.remove(measurement) @@ -347,16 +352,20 @@ export class MeasurementsExtension extends Extension { let flashCount = 0 const maxFlashCount = 5 const handle = setInterval(() => { - this._activeMeasurement.highlight(Boolean(flashCount++ % 2)) - if (flashCount >= maxFlashCount) { - clearInterval(handle) + if (this._activeMeasurement) { + this._activeMeasurement.highlight(Boolean(flashCount++ % 2)) + if (flashCount >= maxFlashCount) { + clearInterval(handle) + } + this.renderer.needsRender = true + this.renderer.resetPipeline() } - this.renderer.needsRender = true - this.renderer.resetPipeline() }, 100) } - protected pickMeasurement(data): Measurement { + protected pickMeasurement(data: Vector2): Measurement | null { + if (!this.renderer.renderingCamera) return null + this.measurements.forEach((value) => { value.highlight(false) }) @@ -372,10 +381,12 @@ export class MeasurementsExtension extends Extension { } protected snap( - intersection: ExtendedIntersection, + intersection: ExtendedMeshIntersection, outPoint: Vector3, outNormal: Vector3 ) { + if (!this.renderer.renderingCamera) return + const v0 = intersection.batchObject.accelerationStructure .getVertexAtIndex(intersection.face.a) .project(this.renderer.renderingCamera) @@ -407,7 +418,7 @@ export class MeasurementsExtension extends Extension { } } - protected updateClippingPlanes(planes: Plane[]) { + protected updateClippingPlanes(planes: Plane[]): void { this.measurements.forEach((value) => { value.updateClippingPlanes(planes) }) @@ -417,8 +428,14 @@ export class MeasurementsExtension extends Extension { const all = [this._activeMeasurement, ...this.measurements] all.forEach((value) => { if (value) { - value.units = this._options.units - value.precision = this._options.precision + value.units = + this._options.units !== undefined + ? this._options.units + : DefaultMeasurementsOptions.units + value.precision = + this._options.precision !== undefined + ? this._options.precision + : DefaultMeasurementsOptions.precision value.update() } }) @@ -441,6 +458,6 @@ export class MeasurementsExtension extends Extension { measurement.update() measurement.state = MeasurementState.COMPLETE measurement.update() - this.measurements.push(this._activeMeasurement) + this.measurements.push(measurement) } } diff --git a/packages/viewer/src/modules/extensions/measurements/PerpendicularMeasurement.ts b/packages/viewer/src/modules/extensions/measurements/PerpendicularMeasurement.ts index 7279440bb2..c4a53733bb 100644 --- a/packages/viewer/src/modules/extensions/measurements/PerpendicularMeasurement.ts +++ b/packages/viewer/src/modules/extensions/measurements/PerpendicularMeasurement.ts @@ -1,18 +1,26 @@ -import { Box3, Camera, Plane, Vector2, Vector3 } from 'three' +import { + Box3, + Camera, + Plane, + Raycaster, + Vector2, + Vector3, + type Intersection +} from 'three' import { MeasurementPointGizmo } from './MeasurementPointGizmo' import { getConversionFactor } from '../../converter/Units' import { Measurement, MeasurementState } from './Measurement' import { ObjectLayers } from '../../../IViewer' export class PerpendicularMeasurement extends Measurement { - private startGizmo: MeasurementPointGizmo = null - private endGizmo: MeasurementPointGizmo = null + private startGizmo: MeasurementPointGizmo | null = null + private endGizmo: MeasurementPointGizmo | null = null private midPoint: Vector3 = new Vector3() private normalIndicatorPixelSize = 15 * window.devicePixelRatio public set isVisible(value: boolean) { - this.startGizmo.enable(value, value, value, value) - this.endGizmo.enable(value, value, value, value) + this.startGizmo?.enable(value, value, value, value) + this.endGizmo?.enable(value, value, value, value) } public get bounds(): Box3 { @@ -35,8 +43,8 @@ export class PerpendicularMeasurement extends Measurement { public frameUpdate(camera: Camera, size: Vector2, bounds: Box3) { super.frameUpdate(camera, size, bounds) - this.startGizmo.frameUpdate(camera, bounds) - this.endGizmo.frameUpdate(camera, bounds) + this.startGizmo?.frameUpdate(camera, bounds) + this.endGizmo?.frameUpdate(camera, bounds) /** Not a fan of this but the camera library fails to tell us when zooming happens * so we need to update the screen space normal indicator each frame, otherwise it * won't look correct while zooming @@ -48,10 +56,11 @@ export class PerpendicularMeasurement extends Measurement { public update() { if (isNaN(this.startPoint.length())) return + if (!this.renderingCamera) return - this.startGizmo.updateDisc(this.startPoint, this.startNormal) - this.startGizmo.updatePoint(this.startPoint) - this.endGizmo.updateDisc(this.endPoint, this.endNormal) + this.startGizmo?.updateDisc(this.startPoint, this.startNormal) + this.startGizmo?.updatePoint(this.startPoint) + this.endGizmo?.updateDisc(this.endPoint, this.endNormal) if (this._state === MeasurementState.DANGLING_START) { const startLine0 = Measurement.vec3Buff0.copy(this.startPoint) @@ -98,12 +107,12 @@ export class PerpendicularMeasurement extends Measurement { endNDC .applyMatrix4(this.renderingCamera.projectionMatrixInverse) .applyMatrix4(this.renderingCamera.matrixWorld) - this.startGizmo.updateLine([ + this.startGizmo?.updateLine([ startLine0, Measurement.vec3Buff1.set(endNDC.x, endNDC.y, endNDC.z) ]) - this.endGizmo.enable(false, false, false, false) + this.endGizmo?.enable(false, false, false, false) } if (this._state === MeasurementState.DANGLING_END) { @@ -148,11 +157,11 @@ export class PerpendicularMeasurement extends Measurement { .copy(this.startNormal) .multiplyScalar(this.startLineLength) ) - this.startGizmo.updateLine([startLine0, startLine1]) + this.startGizmo?.updateLine([startLine0, startLine1]) const endLine0 = Measurement.vec3Buff3.copy(this.endPoint) - this.endGizmo.updateLine([ + this.endGizmo?.updateLine([ endLine0, endLine3, endLine3, @@ -160,7 +169,7 @@ export class PerpendicularMeasurement extends Measurement { this.midPoint, endLine0 ]) - this.endGizmo.updatePoint(this.midPoint) + this.endGizmo?.updatePoint(this.midPoint) const textPos = Measurement.vec3Buff0 .copy(this.startPoint) @@ -171,29 +180,29 @@ export class PerpendicularMeasurement extends Measurement { ) this.value = this.midPoint.distanceTo(this.startPoint) - this.startGizmo.updateText( + this.startGizmo?.updateText( `${(this.value * getConversionFactor('m', this.units)).toFixed( this.precision )} ${this.units}`, textPos ) - this.endGizmo.enable(true, true, true, true) + this.endGizmo?.enable(true, true, true, true) } if (this._state === MeasurementState.COMPLETE) { - this.startGizmo.updateText( + this.startGizmo?.updateText( `${(this.value * getConversionFactor('m', this.units)).toFixed( this.precision )} ${this.units}` ) - this.startGizmo.enable(false, true, true, true) - this.endGizmo.enable(false, false, true, false) + this.startGizmo?.enable(false, true, true, true) + this.endGizmo?.enable(false, false, true, false) } } - public raycast(raycaster, intersects) { - const results = [] - this.startGizmo.raycast(raycaster, results) - this.endGizmo.raycast(raycaster, results) + public raycast(raycaster: Raycaster, intersects: Array) { + const results: Array = [] + this.startGizmo?.raycast(raycaster, results) + this.endGizmo?.raycast(raycaster, results) if (results.length) { intersects.push({ distance: results[0].distance, @@ -207,12 +216,12 @@ export class PerpendicularMeasurement extends Measurement { } public highlight(value: boolean) { - this.startGizmo.highlight = value - this.endGizmo.highlight = value + if (this.startGizmo) this.startGizmo.highlight = value + if (this.endGizmo) this.endGizmo.highlight = value } public updateClippingPlanes(planes: Plane[]) { - this.startGizmo.updateClippingPlanes(planes) - this.endGizmo.updateClippingPlanes(planes) + if (this.startGizmo) this.startGizmo.updateClippingPlanes(planes) + if (this.endGizmo) this.endGizmo.updateClippingPlanes(planes) } } diff --git a/packages/viewer/src/modules/extensions/measurements/PointToPointMeasurement.ts b/packages/viewer/src/modules/extensions/measurements/PointToPointMeasurement.ts index 2cdb4a00e6..e0bf6fe63e 100644 --- a/packages/viewer/src/modules/extensions/measurements/PointToPointMeasurement.ts +++ b/packages/viewer/src/modules/extensions/measurements/PointToPointMeasurement.ts @@ -1,16 +1,16 @@ -import { Box3, Camera, Plane, Vector2 } from 'three' +import { Box3, Camera, Plane, Raycaster, Vector2, type Intersection } from 'three' import { MeasurementPointGizmo } from './MeasurementPointGizmo' import { getConversionFactor } from '../../converter/Units' import { Measurement, MeasurementState } from './Measurement' import { ObjectLayers } from '../../../IViewer' export class PointToPointMeasurement extends Measurement { - private startGizmo: MeasurementPointGizmo = null - private endGizmo: MeasurementPointGizmo = null + private startGizmo: MeasurementPointGizmo | null = null + private endGizmo: MeasurementPointGizmo | null = null public set isVisible(value: boolean) { - this.startGizmo.enable(value, value, value, value) - this.endGizmo.enable(value, value, value, value) + this.startGizmo?.enable(value, value, value, value) + this.endGizmo?.enable(value, value, value, value) } public constructor() { @@ -26,14 +26,14 @@ export class PointToPointMeasurement extends Measurement { public frameUpdate(camera: Camera, size: Vector2, bounds: Box3) { super.frameUpdate(camera, size, bounds) - this.startGizmo.frameUpdate(camera, bounds) - this.endGizmo.frameUpdate(camera, bounds) + this.startGizmo?.frameUpdate(camera, bounds) + this.endGizmo?.frameUpdate(camera, bounds) } public update() { - this.startGizmo.updateDisc(this.startPoint, this.startNormal) - this.startGizmo.updatePoint(this.startPoint) - this.endGizmo.updateDisc(this.endPoint, this.endNormal) + this.startGizmo?.updateDisc(this.startPoint, this.startNormal) + this.startGizmo?.updatePoint(this.startPoint) + this.endGizmo?.updateDisc(this.endPoint, this.endNormal) if (this._state === MeasurementState.DANGLING_START) { const startLine0 = Measurement.vec3Buff0.copy(this.startPoint) @@ -44,8 +44,8 @@ export class PointToPointMeasurement extends Measurement { .copy(this.startNormal) .multiplyScalar(this.startLineLength) ) - this.startGizmo.updateLine([startLine0, startLine1]) - this.endGizmo.enable(false, false, false, false) + this.startGizmo?.updateLine([startLine0, startLine1]) + this.endGizmo?.enable(false, false, false, false) } if (this._state === MeasurementState.DANGLING_END) { this.startLineLength = this.startPoint.distanceTo(this.endPoint) @@ -69,20 +69,20 @@ export class PointToPointMeasurement extends Measurement { .multiplyScalar(this.startLineLength * 0.5) ) - this.startGizmo.updateLine([this.startPoint, lineEndPoint]) - this.endGizmo.updatePoint(lineEndPoint) - this.startGizmo.updateText( + this.startGizmo?.updateLine([this.startPoint, lineEndPoint]) + this.endGizmo?.updatePoint(lineEndPoint) + this.startGizmo?.updateText( `${(this.value * getConversionFactor('m', this.units)).toFixed( this.precision )} ${this.units}`, textPos ) - this.endGizmo.enable(true, true, true, true) + this.endGizmo?.enable(true, true, true, true) } if (this._state === MeasurementState.COMPLETE) { - this.startGizmo.enable(false, true, true, true) - this.endGizmo.enable(false, false, true, false) - this.startGizmo.updateText( + this.startGizmo?.enable(false, true, true, true) + this.endGizmo?.enable(false, false, true, false) + this.startGizmo?.updateText( `${(this.value * getConversionFactor('m', this.units)).toFixed( this.precision )} ${this.units}` @@ -90,10 +90,10 @@ export class PointToPointMeasurement extends Measurement { } } - public raycast(raycaster, intersects) { - const results = [] - this.startGizmo.raycast(raycaster, results) - this.endGizmo.raycast(raycaster, results) + public raycast(raycaster: Raycaster, intersects: Array) { + const results: Array = [] + this.startGizmo?.raycast(raycaster, results) + this.endGizmo?.raycast(raycaster, results) if (results.length) { intersects.push({ distance: results[0].distance, @@ -107,12 +107,12 @@ export class PointToPointMeasurement extends Measurement { } public highlight(value: boolean) { - this.startGizmo.highlight = value - this.endGizmo.highlight = value + if (this.startGizmo) this.startGizmo.highlight = value + if (this.endGizmo) this.endGizmo.highlight = value } public updateClippingPlanes(planes: Plane[]) { - this.startGizmo.updateClippingPlanes(planes) - this.endGizmo.updateClippingPlanes(planes) + if (this.startGizmo) this.startGizmo.updateClippingPlanes(planes) + if (this.endGizmo) this.endGizmo.updateClippingPlanes(planes) } } diff --git a/packages/viewer/src/modules/filtering/PropertyManager.ts b/packages/viewer/src/modules/filtering/PropertyManager.ts index 46dfbd5148..d696ca6c15 100644 --- a/packages/viewer/src/modules/filtering/PropertyManager.ts +++ b/packages/viewer/src/modules/filtering/PropertyManager.ts @@ -1,5 +1,5 @@ import flatten from '../../helpers/flatten' -import { TreeNode, WorldTree } from '../tree/WorldTree' +import { type TreeNode, WorldTree } from '../tree/WorldTree' export class PropertyManager { private propCache = {} as Record @@ -12,7 +12,7 @@ export class PropertyManager { */ public async getProperties( tree: WorldTree, - resourceUrl: string = null, + resourceUrl: string | null = null, bypassCache = false ): Promise { let rootNode: TreeNode = tree.root @@ -21,14 +21,16 @@ export class PropertyManager { return this.propCache[resourceUrl ? resourceUrl : rootNode.model.id] if (resourceUrl) { - const actualRoot = rootNode.children.find((n) => n.model.id === resourceUrl) + const actualRoot = rootNode.children.find( + (n: { model: { id: string } }) => n.model.id === resourceUrl + ) if (actualRoot) rootNode = actualRoot else { throw new Error(`Could not find root node for ${resourceUrl} - is it loaded?`) } } - const propValues = {} + const propValues: { [key: string]: unknown } = {} await tree.walkAsync((node: TreeNode) => { if (!node.model.atomic) return true @@ -36,7 +38,7 @@ export class PropertyManager { for (const key in obj) { if (Array.isArray(obj[key])) continue if (!propValues[key]) propValues[key] = [] - propValues[key].push({ value: obj[key], id: obj.id }) + ;(propValues[key] as Array).push({ value: obj[key], id: obj.id }) } return true }, rootNode) @@ -47,20 +49,33 @@ export class PropertyManager { const propValuesArr = propValues[propKey] const propInfo = {} as PropertyInfo propInfo.key = propKey + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore propInfo.type = typeof propValuesArr[0].value === 'string' ? 'string' : 'number' + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore propInfo.objectCount = propValuesArr.length // For string based props, keep track of which ids belong to which group if (propInfo.type === 'string') { const stringPropInfo = propInfo as StringPropertyInfo const valueGroups = {} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore for (const { value, id } of propValuesArr) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore if (!valueGroups[value]) valueGroups[value] = [] + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore valueGroups[value].push(id) } stringPropInfo.valueGroups = [] - for (const key in valueGroups) + for (const key in valueGroups) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore stringPropInfo.valueGroups.push({ value: key, ids: valueGroups[key] }) + } stringPropInfo.valueGroups = stringPropInfo.valueGroups.sort((a, b) => a.value.localeCompare(b.value) @@ -71,10 +86,14 @@ export class PropertyManager { const numProp = propInfo as NumericPropertyInfo numProp.min = Number.MAX_VALUE numProp.max = Number.MIN_VALUE + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore for (const { value } of propValuesArr) { if (value < numProp.min) numProp.min = value if (value > numProp.max) numProp.max = value } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore numProp.valueGroups = propValuesArr.sort((a, b) => a.value - b.value) // const sorted = propValuesArr.sort((a, b) => a.value - b.value) // propInfo.sortedValues = sorted.map(s => s.value) diff --git a/packages/viewer/src/modules/input/Input.ts b/packages/viewer/src/modules/input/Input.ts index 17bf0e5cd1..7b731bcbd6 100644 --- a/packages/viewer/src/modules/input/Input.ts +++ b/packages/viewer/src/modules/input/Input.ts @@ -1,38 +1,39 @@ import { Vector2 } from 'three' import EventEmitter from '../EventEmitter' -export interface InputOptions { - hover: boolean -} - -export const InputOptionsDefault = { - hover: false +export enum InputEvent { + PointerDown = 'pointer-down', + PointerUp = 'pointer-up', + PointerMove = 'pointer-move', + Click = 'click', + DoubleClick = 'double-click', + KeyUp = 'key-up' } -export enum InputEvent { - PointerDown, - PointerUp, - PointerMove, - Click, - DoubleClick, - KeyUp +export interface InputEventPayload { + [InputEvent.PointerDown]: Vector2 & { event: PointerEvent } + [InputEvent.PointerUp]: Vector2 & { event: PointerEvent } + [InputEvent.PointerMove]: Vector2 & { event: PointerEvent } + [InputEvent.Click]: Vector2 & { event: PointerEvent; multiSelect: boolean } + [InputEvent.DoubleClick]: Vector2 & { event: PointerEvent; multiSelect: boolean } + [InputEvent.KeyUp]: KeyboardEvent } +//TO DO: Define proper interface for InputEvent data export default class Input extends EventEmitter { private static readonly MAX_DOUBLE_CLICK_TIMING = 500 - private tapTimeout + private tapTimeout: number = 0 private lastTap = 0 private lastClick = 0 - private touchLocation: Touch + private touchLocation: Touch | undefined private container - constructor(container: HTMLElement, _options: InputOptions) { + constructor(container: HTMLElement) { super() - _options this.container = container // Handle mouseclicks - let mdTime + let mdTime: number this.container.addEventListener('pointerdown', (e) => { e.preventDefault() const loc = this._getNormalisedClickPosition(e) @@ -72,10 +73,14 @@ export default class Input extends EventEmitter { const tapLength = currentTime - this.lastTap clearTimeout(this.tapTimeout) if (tapLength < 500 && tapLength > 0) { - const loc = this._getNormalisedClickPosition(this.touchLocation) - this.emit(InputEvent.DoubleClick, loc) + if (this.touchLocation) { + const loc = this._getNormalisedClickPosition(this.touchLocation) + this.emit(InputEvent.DoubleClick, loc) + } } else { - this.tapTimeout = setTimeout(function () { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + this.tapTimeout = setTimeout(() => { clearTimeout(this.tapTimeout) }, 500) } @@ -85,18 +90,17 @@ export default class Input extends EventEmitter { this.container.addEventListener('dblclick', (e) => { const data = this._getNormalisedClickPosition(e) ;(data as unknown as Record).event = e + if (e.shiftKey) (data as unknown as Record).multiSelect = true this.emit(InputEvent.DoubleClick, data) }) this.container.addEventListener('pointermove', (e) => { const data = this._getNormalisedClickPosition(e) ;(data as unknown as Record).event = e - this.emit('pointer-move', data) this.emit(InputEvent.PointerMove, data) }) document.addEventListener('keyup', (e) => { - this.emit('key-up', e) this.emit(InputEvent.KeyUp, e) }) @@ -112,9 +116,16 @@ export default class Input extends EventEmitter { // }) } - _getNormalisedClickPosition(e) { + public on( + eventType: T, + listener: (arg: InputEventPayload[T]) => void + ): void { + super.on(eventType, listener) + } + + _getNormalisedClickPosition(e: MouseEvent | Touch) { // Reference: https://threejsfundamentals.org/threejs/lessons/threejs-picking.html - const canvas = this.container + const canvas = this.container as HTMLCanvasElement const rect = this.container.getBoundingClientRect() const pos = { diff --git a/packages/viewer/src/modules/loaders/GeometryConverter.ts b/packages/viewer/src/modules/loaders/GeometryConverter.ts index b0f938cac5..e8b77de02a 100644 --- a/packages/viewer/src/modules/loaders/GeometryConverter.ts +++ b/packages/viewer/src/modules/loaders/GeometryConverter.ts @@ -1,5 +1,5 @@ -import { NodeData } from '../..' -import { GeometryData } from '../converter/Geometry' +import { type GeometryData } from '../converter/Geometry' +import type { NodeData } from '../tree/WorldTree' export enum SpeckleType { View3D = 'View3D', @@ -40,6 +40,6 @@ export const SpeckleTypeAllRenderables: SpeckleType[] = [ export abstract class GeometryConverter { public abstract getSpeckleType(node: NodeData): SpeckleType - public abstract convertNodeToGeometryData(node: NodeData): GeometryData + public abstract convertNodeToGeometryData(node: NodeData): GeometryData | null public abstract disposeNodeGeometryData(node: NodeData): void } diff --git a/packages/viewer/src/modules/loaders/Loader.ts b/packages/viewer/src/modules/loaders/Loader.ts index 469fd70443..10f5a429c3 100644 --- a/packages/viewer/src/modules/loaders/Loader.ts +++ b/packages/viewer/src/modules/loaders/Loader.ts @@ -1,26 +1,43 @@ import EventEmitter from '../EventEmitter' export enum LoaderEvent { - LoadComplete = 'load-complete', LoadProgress = 'load-progress', LoadCancelled = 'load-cancelled', LoadWarning = 'load-warning' } +export interface LoaderEventPayload { + [LoaderEvent.LoadProgress]: { progress: number; id: string } + [LoaderEvent.LoadCancelled]: string + [LoaderEvent.LoadWarning]: { message: string } +} + export abstract class Loader extends EventEmitter { protected _resource: string - protected _resourceData: string | ArrayBuffer + protected _resourceData: string | ArrayBuffer | undefined public abstract get resource(): string public abstract get finished(): boolean - protected constructor(resource: string, resourceData: string | ArrayBuffer) { + protected constructor( + resource: string, + resourceData?: string | ArrayBuffer | undefined + ) { super() this._resource = resource this._resourceData = resourceData } + public on( + eventType: T, + listener: (arg: LoaderEventPayload[T]) => void + ): void { + super.on(eventType, listener) + } + public abstract load(): Promise - public abstract cancel() - public abstract dispose() + public abstract cancel(): void + public dispose(): void { + super.dispose() + } } diff --git a/packages/viewer/src/modules/loaders/OBJ/ObjConverter.ts b/packages/viewer/src/modules/loaders/OBJ/ObjConverter.ts index 2682027f53..fbc0738c15 100644 --- a/packages/viewer/src/modules/loaders/OBJ/ObjConverter.ts +++ b/packages/viewer/src/modules/loaders/OBJ/ObjConverter.ts @@ -1,17 +1,18 @@ -import { Group, Mesh, Object3D } from 'three' -import { TreeNode, WorldTree } from '../../tree/WorldTree' -import { - ConverterNodeDelegate, - ConverterResultDelegate -} from '../Speckle/SpeckleConverter' +import { Mesh, Object3D } from 'three' +import { type TreeNode, WorldTree } from '../../tree/WorldTree' +import type { ConverterResultDelegate } from '../Speckle/SpeckleConverter' import Logger from 'js-logger' +export type ObjConverterNodeDelegate = + | ((object: Object3D, node: TreeNode) => Promise) + | null + export class ObjConverter { - private lastAsyncPause: number + private lastAsyncPause: number = 0 private tree: WorldTree private readonly NodeConverterMapping: { - [name: string]: ConverterNodeDelegate + [name: string]: ObjConverterNodeDelegate } = { Group: this.groupToNode.bind(this), Mesh: this.MeshToNode.bind(this) @@ -33,7 +34,7 @@ export class ObjConverter { objectURL: string, object: Object3D, callback: ConverterResultDelegate, - node: TreeNode = null + node: TreeNode | null = null ) { await this.asyncPause() @@ -68,14 +69,15 @@ export class ObjConverter { } } - private directNodeConverterExists(obj) { + private directNodeConverterExists(obj: Object3D) { return obj.type in this.NodeConverterMapping } - private async convertToNode(obj, node) { + private async convertToNode(obj: Object3D, node: TreeNode) { try { if (this.directNodeConverterExists(obj)) { - return await this.NodeConverterMapping[obj.type](obj, node) + const delegate = this.NodeConverterMapping[obj.type] + if (delegate) return await delegate(obj, node) } return null } catch (e) { @@ -84,7 +86,8 @@ export class ObjConverter { } } - private async MeshToNode(obj: Mesh, node) { + private async MeshToNode(_obj: Object3D, node: TreeNode) { + const obj = _obj as Mesh if (!obj) return if ( !obj.geometry.attributes.position || @@ -99,10 +102,12 @@ export class ObjConverter { node.model.raw.vertices = obj.geometry.attributes.position.array node.model.raw.faces = obj.geometry.index?.array node.model.raw.colors = obj.geometry.attributes.color?.array + return Promise.resolve() } - private groupToNode(obj: Group, node) { + private groupToNode(obj: Object3D, node: TreeNode) { obj node + return Promise.resolve() } } diff --git a/packages/viewer/src/modules/loaders/OBJ/ObjGeometryConverter.ts b/packages/viewer/src/modules/loaders/OBJ/ObjGeometryConverter.ts index d4355bf325..d7a77a17bc 100644 --- a/packages/viewer/src/modules/loaders/OBJ/ObjGeometryConverter.ts +++ b/packages/viewer/src/modules/loaders/OBJ/ObjGeometryConverter.ts @@ -1,6 +1,6 @@ import { Matrix4 } from 'three' -import { NodeData } from '../../..' -import { GeometryData } from '../../converter/Geometry' +import { type NodeData } from '../../..' +import { type GeometryData } from '../../converter/Geometry' import { GeometryConverter, SpeckleType } from '../GeometryConverter' import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils' @@ -11,16 +11,20 @@ export class ObjGeometryConverter extends GeometryConverter { return SpeckleType.BlockInstance case 'Mesh': return SpeckleType.Mesh + default: + return SpeckleType.Unknown } } - public convertNodeToGeometryData(node: NodeData): GeometryData { + public convertNodeToGeometryData(node: NodeData): GeometryData | null { const type = this.getSpeckleType(node) switch (type) { case SpeckleType.BlockInstance: return this.BlockInstanceToGeometryData(node) case SpeckleType.Mesh: return this.MeshToGeometryData(node) + default: + return null } } @@ -53,8 +57,8 @@ export class ObjGeometryConverter extends GeometryConverter { /** * MESH */ - private MeshToGeometryData(node: NodeData): GeometryData { - if (!node.raw) return + private MeshToGeometryData(node: NodeData): GeometryData | null { + if (!node.raw) return null const conversionFactor = 1 if (!node.raw.geometry.index || node.raw.geometry.index.array.length === 0) { diff --git a/packages/viewer/src/modules/loaders/OBJ/ObjLoader.ts b/packages/viewer/src/modules/loaders/OBJ/ObjLoader.ts index e67c5207e7..9836c4b1b7 100644 --- a/packages/viewer/src/modules/loaders/OBJ/ObjLoader.ts +++ b/packages/viewer/src/modules/loaders/OBJ/ObjLoader.ts @@ -10,7 +10,7 @@ export class ObjLoader extends Loader { private baseLoader: OBJLoader private converter: ObjConverter private tree: WorldTree - private isFinished: boolean + private isFinished: boolean = false public get resource(): string { return this._resource @@ -66,12 +66,16 @@ export class ObjLoader extends Loader { pload.then(async () => { const t0 = performance.now() - const res = await this.tree - .getRenderTree(this._resource) - .buildRenderTree(new ObjGeometryConverter()) - Logger.log('Tree build time -> ', performance.now() - t0) - this.isFinished = true - resolve(res) + const renderTree = this.tree.getRenderTree(this._resource) + if (renderTree) { + const res = await renderTree.buildRenderTree(new ObjGeometryConverter()) + Logger.log('Tree build time -> ', performance.now() - t0) + this.isFinished = true + resolve(res) + } else { + Logger.error(`Could not get render tree for ${this._resource}`) + reject() + } }) pload.catch(() => { Logger.error(`Could not load ${this._resource}`) @@ -82,9 +86,9 @@ export class ObjLoader extends Loader { public cancel() { this.isFinished = false - throw new Error('Method not implemented.') } + public dispose() { - this.baseLoader = null + super.dispose() } } diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts index 250ceb8981..72dc43ae36 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts @@ -1,18 +1,22 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { MathUtils } from 'three' -import { TreeNode, WorldTree } from '../../tree/WorldTree' +import { type TreeNode, WorldTree } from '../../tree/WorldTree' import Logger from 'js-logger' import { NodeMap } from '../../tree/NodeMap' +import type { SpeckleObject } from '../../..' +import type ObjectLoader from '@speckle/objectloader' export type ConverterResultDelegate = () => Promise -export type ConverterNodeDelegate = (object, node) => Promise +export type SpeckleConverterNodeDelegate = + | ((object: SpeckleObject, node: TreeNode) => Promise) + | null /** * Utility class providing some top level conversion methods. * Warning: HIC SVNT DRACONES. */ export default class SpeckleConverter { - private objectLoader + private objectLoader: ObjectLoader private activePromises: number private maxChildrenPromises: number private spoofIDs = false @@ -21,7 +25,7 @@ export default class SpeckleConverter { private instanceCounter = 0 private readonly NodeConverterMapping: { - [name: string]: ConverterNodeDelegate + [name: string]: SpeckleConverterNodeDelegate } = { View3D: this.View3DToNode.bind(this), BlockInstance: this.BlockInstanceToNode.bind(this), @@ -45,7 +49,7 @@ export default class SpeckleConverter { private readonly IgnoreNodes = ['Parameter'] - constructor(objectLoader: unknown, tree: WorldTree) { + constructor(objectLoader: ObjectLoader, tree: WorldTree) { if (!objectLoader) { Logger.warn( 'Converter initialized without a corresponding object loader. Any objects that include references will throw errors.' @@ -67,9 +71,9 @@ export default class SpeckleConverter { */ public async traverse( objectURL: string, - obj, + obj: SpeckleObject, callback: ConverterResultDelegate, - node: TreeNode = null + node: TreeNode | null = null ) { // Exit on primitives (string, ints, bools, bigints, etc.) if (obj === null || typeof obj !== 'object') return @@ -126,7 +130,7 @@ export default class SpeckleConverter { // If we can convert it, we should invoke the respective conversion routine. if (this.directNodeConverterExists(obj)) { try { - await this.convertToNode(obj.data || obj, childNode) + await this.convertToNode(obj, childNode) await callback() return } catch (e) { @@ -139,7 +143,7 @@ export default class SpeckleConverter { } } - const target = obj + const target: SpeckleObject = obj // Check if the object has a display value of sorts // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -167,7 +171,9 @@ export default class SpeckleConverter { await callback() } catch (e) { Logger.warn( - `(Traversing) Failed to convert obj with id: ${obj.id} — ${e.message}` + `(Traversing) Failed to convert obj with id: ${obj.id} — ${ + (e as never)['message'] + }` ) } } else { @@ -192,7 +198,7 @@ export default class SpeckleConverter { const elements = this.getElementsValue(obj) if (elements) { childrenConversionPromisses.push( - this.traverse(objectURL, elements, callback, childNode) + this.traverse(objectURL, elements as SpeckleObject, callback, childNode) ) this.activePromises += childrenConversionPromisses.length await Promise.all(childrenConversionPromisses) @@ -214,9 +220,19 @@ export default class SpeckleConverter { if (typeof target[prop] !== 'object' || target[prop] === null) continue if (this.activePromises >= this.maxChildrenPromises) { - await this.traverse(objectURL, target[prop], callback, childNode) + await this.traverse( + objectURL, + target[prop] as SpeckleObject, + callback, + childNode + ) } else { - const childPromise = this.traverse(objectURL, target[prop], callback, childNode) + const childPromise = this.traverse( + objectURL, + target[prop] as SpeckleObject, + callback, + childNode + ) childrenConversionPromisses.push(childPromise) } } @@ -225,7 +241,7 @@ export default class SpeckleConverter { this.activePromises -= childrenConversionPromisses.length } - private getNodeId(obj) { + private getNodeId(obj: SpeckleObject): string { if (this.spoofIDs) return MathUtils.generateUUID() return obj.id } @@ -235,19 +251,22 @@ export default class SpeckleConverter { * @param {[type]} arr [description] * @return {[type]} [description] */ - private async dechunk(arr) { + private async dechunk(arr: Array<{ referencedId: string }>) { if (!arr || arr.length === 0) return arr // Handles pre-chunking objects, or arrs that have not been chunked if (!arr[0].referencedId) return arr - const chunked = [] + const chunked: unknown[] = [] for (const ref of arr) { - const real = await this.objectLoader.getObject(ref.referencedId) + const real: Record = await this.objectLoader.getObject( + ref.referencedId + ) chunked.push(real.data) // await this.asyncPause() } - const dechunked = [].concat(...chunked) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const dechunked = [].concat(...(chunked as any)) return dechunked } @@ -257,9 +276,11 @@ export default class SpeckleConverter { * @param {[type]} obj [description] * @return {[type]} [description] */ - private async resolveReference(obj) { + private async resolveReference(obj: SpeckleObject): Promise { if (obj.referencedId) { - const resolvedObj = await this.objectLoader.getObject(obj.referencedId) + const resolvedObj = (await this.objectLoader.getObject( + obj.referencedId + )) as SpeckleObject // this.asyncPause() return resolvedObj } else return obj @@ -270,10 +291,8 @@ export default class SpeckleConverter { * @param {[type]} obj [description] * @return {[type]} [description] */ - private getSpeckleType(obj): string { - let rawType = 'Base' - if (obj.data) rawType = obj.data.speckle_type ? obj.data.speckle_type : 'Base' - else rawType = obj.speckle_type ? obj.speckle_type : 'Base' + private getSpeckleType(obj: SpeckleObject): string { + const rawType = obj.speckle_type ? obj.speckle_type : 'Base' const lookup = this.typeLookupTable[rawType] if (lookup) return lookup @@ -291,11 +310,9 @@ export default class SpeckleConverter { return typeRet } - private getSpeckleTypeChain(obj): string[] { + private getSpeckleTypeChain(obj: SpeckleObject): string[] { let type = ['Base'] - if (obj.data) - type = obj.data.speckle_type ? obj.data.speckle_type.split(':').reverse() : type - else type = obj.speckle_type ? obj.speckle_type.split(':').reverse() : type + type = obj.speckle_type ? obj.speckle_type.split(':').reverse() : type type = type.map((value: string) => { return value.split('.').reverse()[0] }) @@ -303,15 +320,16 @@ export default class SpeckleConverter { return type } - private directNodeConverterExists(obj) { + private directNodeConverterExists(obj: SpeckleObject) { return this.getSpeckleType(obj) in this.NodeConverterMapping } - private async convertToNode(obj, node) { + private async convertToNode(obj: SpeckleObject, node: TreeNode) { if (obj.referencedId) obj = await this.resolveReference(obj) try { if (this.directNodeConverterExists(obj)) { - return await this.NodeConverterMapping[this.getSpeckleType(obj)](obj, node) + const delegate = this.NodeConverterMapping[this.getSpeckleType(obj)] + if (delegate) return await delegate(obj, node) } return null } catch (e) { @@ -320,7 +338,7 @@ export default class SpeckleConverter { } } - private getDisplayValue(obj) { + private getDisplayValue(obj: SpeckleObject) { const displayValue = obj['displayValue'] || obj['@displayValue'] || @@ -340,11 +358,11 @@ export default class SpeckleConverter { return null } - private getElementsValue(obj) { + private getElementsValue(obj: SpeckleObject) { return obj['elements'] || obj['@elements'] } - private getBlockDefinition(obj) { + private getBlockDefinition(obj: SpeckleObject) { return ( obj['@blockDefinition'] || obj['blockDefinition'] || @@ -353,12 +371,12 @@ export default class SpeckleConverter { ) } - private getBlockDefinitionGeometry(obj) { + private getBlockDefinitionGeometry(obj: SpeckleObject) { return obj['@geometry'] || obj['geometry'] } /** We're wasting a few milis here, but it is what it is */ - private getCompoundId(baseId, counter) { + private getCompoundId(baseId: string, counter: number) { const index = baseId.indexOf(NodeMap.COMPOUND_ID_CHAR) if (index === -1) { return baseId + NodeMap.COMPOUND_ID_CHAR + counter @@ -375,8 +393,12 @@ export default class SpeckleConverter { * NODES */ - private async View3DToNode(obj, node) { + private async View3DToNode(obj: SpeckleObject, _node: TreeNode) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore obj.origin.units = obj.units + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore obj.target.units = obj.units } @@ -384,15 +406,19 @@ export default class SpeckleConverter { * It's only looking for 'elements' and 'displayValues' * I think it can be used for RevitInstances as well to replace it's current lookup, but I'm afraid to do it */ - private async displayableLookup(obj, node, instanced) { + private async displayableLookup( + obj: SpeckleObject, + node: TreeNode, + instanced: boolean + ) { if (this.directNodeConverterExists(obj)) { await this.convertToNode(obj, node) } else { const displayValues = this.getDisplayValue(obj) const elements = this.getElementsValue(obj) const entries = [ - ...(displayValues ? displayValues : []), - ...(elements ? elements : []) + ...(displayValues ? (displayValues as SpeckleObject[]) : []), + ...(elements ? (elements as SpeckleObject[]) : []) ] for (const entry of entries) { const value = await this.resolveReference(entry) @@ -411,16 +437,16 @@ export default class SpeckleConverter { } private async parseInstanceDefinitionGeometry( - instanceObj, - defGeometry, - instanceNode + instanceObj: SpeckleObject, + defGeometry: SpeckleObject, + instanceNode: TreeNode ) { const transformNodeId = MathUtils.generateUUID() let transformData = null /** Legacy form of Transform */ if (Array.isArray(instanceObj.transform)) { transformData = this.getEmptyTransformData(transformNodeId) - transformData.units = instanceObj.units + transformData.units = instanceObj.units as string transformData.matrix = instanceObj.transform } else { transformData = instanceObj.transform @@ -447,7 +473,11 @@ export default class SpeckleConverter { await this.displayableLookup(defGeometry, childNode, true) } - private async parseInstanceElement(instanceObj, elementObj, instanceNode) { + private async parseInstanceElement( + _instanceObj: SpeckleObject, + elementObj: SpeckleObject, + instanceNode: TreeNode + ) { const childNode: TreeNode = this.tree.parse({ id: this.getNodeId(elementObj), raw: elementObj, @@ -458,43 +488,49 @@ export default class SpeckleConverter { await this.displayableLookup(elementObj, childNode, false) } - private async BlockInstanceToNode(obj, node) { - const definition = await this.resolveReference(this.getBlockDefinition(obj)) + private async BlockInstanceToNode(obj: SpeckleObject, node: TreeNode) { + const definition: SpeckleObject = await this.resolveReference( + this.getBlockDefinition(obj) as SpeckleObject + ) node.model.raw.definition = definition - for (const def of this.getBlockDefinitionGeometry(definition)) { + for (const def of this.getBlockDefinitionGeometry(definition) as SpeckleObject[]) { const ref = await this.resolveReference(def) await this.parseInstanceDefinitionGeometry(obj, ref, node) } const elements = this.getElementsValue(obj) if (elements) { - for (const element of elements) { + for (const element of elements as SpeckleObject[]) { const elementObj = await this.resolveReference(element) this.parseInstanceElement(obj, elementObj, node) } } } - private async RevitInstanceToNode(obj, node) { - const definition = await this.resolveReference(obj.definition) + private async RevitInstanceToNode(obj: SpeckleObject, node: TreeNode) { + const definition = await this.resolveReference(obj.definition as SpeckleObject) node.model.raw.definition = definition await this.parseInstanceDefinitionGeometry(obj, definition, node) const elements = this.getElementsValue(obj) if (elements) { - for (const element of elements) { + for (const element of elements as SpeckleObject[]) { const elementObj = await this.resolveReference(element) this.parseInstanceElement(obj, elementObj, node) } } } - private async PointcloudToNode(obj, node) { + private async PointcloudToNode(obj: SpeckleObject, node: TreeNode) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore node.model.raw.points = await this.dechunk(obj.points) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore node.model.raw.colors = await this.dechunk(obj.colors) } - private async BrepToNode(obj, node) { + private async BrepToNode(obj: SpeckleObject, node: TreeNode) { try { if (!obj) return @@ -503,7 +539,7 @@ export default class SpeckleConverter { if (Array.isArray(displayValue)) displayValue = displayValue[0] //Just take the first display value for now (not ideal) if (!displayValue) return - const ref = await this.resolveReference(displayValue) + const ref = await this.resolveReference(displayValue as SpeckleObject) const nestedNode: TreeNode = this.tree.parse({ id: node.model.instanced ? this.getCompoundId(ref.id, this.instanceCounter++) @@ -531,32 +567,37 @@ export default class SpeckleConverter { } } - private async MeshToNode(obj, node) { + private async MeshToNode(obj: SpeckleObject, node: TreeNode) { if (!obj) return - if (!obj.vertices || obj.vertices.length === 0) { + if (!obj.vertices || (obj.vertices as Array).length === 0) { Logger.warn( `Object id ${obj.id} of type ${obj.speckle_type} has no vertex position data and will be ignored` ) return } - if (!obj.faces || obj.faces.length === 0) { + if (!obj.faces || (obj.faces as Array).length === 0) { Logger.warn( `Object id ${obj.id} of type ${obj.speckle_type} has no face data and will be ignored` ) return } - + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore node.model.raw.vertices = await this.dechunk(obj.vertices) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore node.model.raw.faces = await this.dechunk(obj.faces) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore node.model.raw.colors = await this.dechunk(obj.colors) } - private async TextToNode(obj, node) { + private async TextToNode(_obj: SpeckleObject, _node: TreeNode) { return } - private async DimensionToNode(obj, node) { - const displayValues = [...this.getDisplayValue(obj)] + private async DimensionToNode(obj: SpeckleObject, node: TreeNode) { + const displayValues = [...(this.getDisplayValue(obj) as SpeckleObject[])] for (const displayValue of displayValues) { const childNode: TreeNode = this.tree.parse({ id: this.getNodeId(displayValue), @@ -598,28 +639,36 @@ export default class SpeckleConverter { await this.convertToNode(textObj, textNode) } - private async PointToNode(obj, node) { + private async PointToNode(_obj: SpeckleObject, _node: TreeNode) { return } - private async LineToNode(obj, node) { + private async LineToNode(_obj: SpeckleObject, _node: TreeNode) { return } - private async PolylineToNode(obj, node) { - node.model.raw.value = await this.dechunk(obj.value) + private async PolylineToNode(obj: SpeckleObject, node: TreeNode) { + node.model.raw.value = await this.dechunk( + obj.value as Array<{ referencedId: string }> + ) } - private async BoxToNode(obj, node) { + private async BoxToNode(_obj: SpeckleObject, _node: TreeNode) { return } - private async PolycurveToNode(obj, node) { + private async PolycurveToNode(obj: SpeckleObject, node: TreeNode) { node.model.nestedNodes = [] - for (let i = 0; i < obj.segments.length; i++) { - let element = obj.segments[i] + for ( + let i = 0; + i < (obj as unknown as { segments: SpeckleObject[] }).segments.length; + i++ + ) { + let element = (obj as unknown as { segments: SpeckleObject[] }).segments[ + i + ] as SpeckleObject /** Not a big fan of this... */ if (!this.directNodeConverterExists(element)) { - element = this.getDisplayValue(element) + element = this.getDisplayValue(element) as SpeckleObject if (element.referencedId) { element = await this.resolveReference(element) } @@ -636,8 +685,8 @@ export default class SpeckleConverter { } } - private async CurveToNode(obj, node) { - let displayValue = this.getDisplayValue(obj) + private async CurveToNode(obj: SpeckleObject, node: TreeNode) { + let displayValue: SpeckleObject = this.getDisplayValue(obj) as SpeckleObject if (!displayValue) { Logger.warn( `Object ${obj.id} of type ${obj.speckle_type} has no display value and will be ignored` @@ -645,7 +694,7 @@ export default class SpeckleConverter { return } node.model.nestedNodes = [] - displayValue = await this.resolveReference(obj.displayValue) + displayValue = await this.resolveReference(obj.displayValue as SpeckleObject) displayValue.units = displayValue.units || obj.units const nestedNode: TreeNode = this.tree.parse({ id: this.getNodeId(displayValue), @@ -658,15 +707,15 @@ export default class SpeckleConverter { node.model.nestedNodes.push(nestedNode) } - private async CircleToNode(obj, node) { + private async CircleToNode(_obj: SpeckleObject, _node: TreeNode) { return } - private async ArcToNode(obj, node) { + private async ArcToNode(_obj: SpeckleObject, _node: TreeNode) { return } - private async EllipseToNode(obj, node) { + private async EllipseToNode(_obj: SpeckleObject, _node: TreeNode) { return } } diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts index af532ef525..82efd25ba0 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts @@ -1,7 +1,7 @@ -import { Geometry, GeometryData } from '../../converter/Geometry' +import { Geometry, type GeometryData } from '../../converter/Geometry' import MeshTriangulationHelper from '../../converter/MeshTriangulationHelper' import { getConversionFactor } from '../../converter/Units' -import { NodeData } from '../../tree/WorldTree' +import { type NodeData } from '../../tree/WorldTree' import { Box3, EllipseCurve, Matrix4, Vector2, Vector3 } from 'three' import Logger from 'js-logger' import { GeometryConverter, SpeckleType } from '../GeometryConverter' @@ -10,25 +10,22 @@ export class SpeckleGeometryConverter extends GeometryConverter { public typeLookupTable: { [type: string]: SpeckleType } = {} public getSpeckleType(node: NodeData): SpeckleType { - let rawType = 'Base' - if (node.raw.data) - rawType = node.raw.data.speckle_type ? node.raw.data.speckle_type : 'Base' - else rawType = node.raw.speckle_type ? node.raw.speckle_type : 'Base' + const rawType = node.raw.speckle_type ? node.raw.speckle_type : 'Base' const lookup = this.typeLookupTable[rawType] if (lookup) { return lookup } - let typeRet = SpeckleType.Unknown - let typeChain = [] + let typeRet: SpeckleType = SpeckleType.Unknown + let typeChain: string[] = [] typeChain = rawType.split(':').reverse() typeChain = typeChain.map((value: string) => { return value.split('.').reverse()[0] }) for (const type of typeChain) { if (type in SpeckleType) { - typeRet = type + typeRet = type as SpeckleType break } } @@ -36,7 +33,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { return typeRet } - public convertNodeToGeometryData(node: NodeData): GeometryData { + public convertNodeToGeometryData(node: NodeData): GeometryData | null { const type = this.getSpeckleType(node) switch (type) { case SpeckleType.BlockInstance: @@ -168,13 +165,13 @@ export class SpeckleGeometryConverter extends GeometryConverter { } /** BLOCK INSTANCE */ - private BlockInstanceToGeometryData(node: NodeData): GeometryData { + private BlockInstanceToGeometryData(node: NodeData): GeometryData | null { node return null } /** REVIT INSTANCE */ - private RevitInstanceToGeometryData(node: NodeData): GeometryData { + private RevitInstanceToGeometryData(node: NodeData): GeometryData | null { node return null } @@ -182,7 +179,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * POINT CLOUD */ - private PointcloudToGeometryData(node: NodeData) { + private PointcloudToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) const vertices = node.instanced ? node.raw.points.slice() : node.raw.points @@ -216,7 +213,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * BREP */ - private BrepToGeometryData(node) { + private BrepToGeometryData(node: NodeData): GeometryData | null { /** Breps don't (currently) have inherent geometryic description in the viewer. They are replaced * by their mesh display values */ @@ -227,14 +224,14 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * MESH */ - private MeshToGeometryData(node: NodeData): GeometryData { - if (!node.raw) return + private MeshToGeometryData(node: NodeData): GeometryData | null { + if (!node.raw) return null const conversionFactor = getConversionFactor(node.raw.units) const indices = [] - if (!node.raw.vertices) return - if (!node.raw.faces) return + if (!node.raw.vertices) return null + if (!node.raw.faces) return null const vertices = node.raw.vertices const faces = node.raw.faces @@ -293,7 +290,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * TEXT */ - private TextToGeometryData(node: NodeData): GeometryData { + private TextToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) const plane = node.raw.plane const position = new Vector3(plane.origin.x, plane.origin.y, plane.origin.z) @@ -316,11 +313,17 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * POINT */ - private PointToGeometryData(node: NodeData): GeometryData { + private PointToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) return { attributes: { - POSITION: this.PointToFloatArray(node.raw) + POSITION: this.PointToFloatArray( + node.raw as { value: Array; units: string } & { + x: number + y: number + z: number + } + ) }, bakeTransform: new Matrix4().makeScale( conversionFactor, @@ -334,7 +337,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * LINE */ - private LineToGeometryData(node: NodeData): GeometryData { + private LineToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) return { attributes: { @@ -354,7 +357,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * POLYLINE */ - private PolylineToGeometryData(node: NodeData): GeometryData { + private PolylineToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) if (node.raw.closed) @@ -375,7 +378,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * BOX */ - private BoxToGeometryData(node: NodeData) { + private BoxToGeometryData(node: NodeData): GeometryData | null { /** * Right, so we're cheating here a bit. We're using three's box geometry * to get the vertices and indices. Normally we could(should) do that by hand @@ -436,25 +439,30 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * POLYCURVE */ - private PolycurveToGeometryData(node: NodeData): GeometryData { + private PolycurveToGeometryData(node: NodeData): GeometryData | null { + if (!node.nestedNodes || node.nestedNodes.length === 0) { + return null + } const buffers = [] + for (let i = 0; i < node.nestedNodes.length; i++) { const element = node.nestedNodes[i].model const conv = this.convertNodeToGeometryData(element) buffers.push(conv) } - return Geometry.mergeGeometryData(buffers) + return Geometry.mergeGeometryData(buffers as GeometryData[]) } /** * CURVE */ - private CurveToGeometryData(node) { - if (node.nestedNodes.length === 0) { + private CurveToGeometryData(node: NodeData): GeometryData | null { + if (!node.nestedNodes || node.nestedNodes.length === 0) { return null } const polylineGeometry = this.PolylineToGeometryData(node.nestedNodes[0].model) + if (!polylineGeometry || !polylineGeometry.attributes) return null return { attributes: { POSITION: polylineGeometry.attributes.POSITION @@ -467,7 +475,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * CIRCLE */ - private CircleToGeometryData(node: NodeData) { + private CircleToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) const curveSegmentLength = 0.1 * conversionFactor const points = this.getCircularCurvePoints( @@ -489,7 +497,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * ARC */ - private ArcToGeometryData(node: NodeData) { + private ArcToGeometryData(node: NodeData): GeometryData | null { const origin = new Vector3( node.raw.plane.origin.x, node.raw.plane.origin.y, @@ -591,7 +599,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * ELLIPSE */ - private EllipseToGeometryData(node: NodeData) { + private EllipseToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) const center = new Vector3( @@ -639,8 +647,24 @@ export class SpeckleGeometryConverter extends GeometryConverter { */ private getCircularCurvePoints( - plane, - radius, + plane: { + xdir: { value: Array; units: string } & { + x: number + y: number + z: number + } + ydir: { value: Array; units: string } & { + x: number + y: number + z: number + } + origin: { value: Array; units: string } & { + x: number + y: number + z: number + } + }, + radius: number, startAngle = 0, endAngle = 2 * Math.PI, res = 0.1 @@ -673,7 +697,10 @@ export class SpeckleGeometryConverter extends GeometryConverter { return points } - private PointToVector3(obj, scale = true) { + private PointToVector3( + obj: { value: Array; units: string } & { x: number; y: number; z: number }, + scale = true + ) { const conversionFactor = scale ? getConversionFactor(obj.units) : 1 let v = null if (obj.value) { @@ -694,7 +721,9 @@ export class SpeckleGeometryConverter extends GeometryConverter { return v } - private PointToFloatArray(obj) { + private PointToFloatArray( + obj: { value: Array; units: string } & { x: number; y: number; z: number } + ) { if (obj.value) { return [obj.value[0], obj.value[1], obj.value[2]] } else { @@ -704,7 +733,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { private FlattenVector3Array(input: Vector3[] | Vector2[]): number[] { const output = new Array(input.length * 3) - const vBuff = [] + const vBuff: Array = [] for (let k = 0, l = 0; k < input.length; k++, l += 3) { input[k].toArray(vBuff) output[l] = vBuff[0] @@ -733,7 +762,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { return colors } - private srgbToLinear(x) { + private srgbToLinear(x: number) { if (x <= 0) return 0 else if (x >= 1) return 1 else if (x < 0.04045) return x / 12.92 diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts index d6a90ca496..85c9c7c76c 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts @@ -3,14 +3,13 @@ import SpeckleConverter from './SpeckleConverter' import { Loader, LoaderEvent } from '../Loader' import ObjectLoader from '@speckle/objectloader' import { SpeckleGeometryConverter } from './SpeckleGeometryConverter' -import { WorldTree } from '../../..' +import { WorldTree, type SpeckleObject } from '../../..' import { AsyncPause } from '../../World' export class SpeckleLoader extends Loader { private loader: ObjectLoader private converter: SpeckleConverter private tree: WorldTree - private priority: number = 1 private isCancelled = false private isFinished = false @@ -25,17 +24,15 @@ export class SpeckleLoader extends Loader { constructor( targetTree: WorldTree, resource: string, - authToken: string, + authToken?: string, enableCaching?: boolean, - resourceData?: string | ArrayBuffer, - priority: number = 1 + resourceData?: string | ArrayBuffer ) { super(resource, resourceData) this.tree = targetTree - this.priority = priority - let token = null + let token = undefined try { - token = authToken || localStorage.getItem('AuthToken') + token = authToken || (localStorage.getItem('AuthToken') as string | undefined) } catch (error) { // Accessing localStorage may throw when executing on sandboxed document, ignore. } @@ -90,15 +87,19 @@ export class SpeckleLoader extends Loader { return Promise.resolve(false) } if (first) { - firstObjectPromise = this.converter.traverse(this._resource, obj, async () => { - viewerLoads++ - pause.tick(100) - if (pause.needsWait) { - await pause.wait(16) + firstObjectPromise = this.converter.traverse( + this._resource, + obj as SpeckleObject, + async () => { + viewerLoads++ + pause.tick(100) + if (pause.needsWait) { + await pause.wait(16) + } } - }) + ) first = false - total = obj.totalChildrenCount + total = obj.totalChildrenCount as number } current++ this.emit(LoaderEvent.LoadProgress, { @@ -147,6 +148,7 @@ export class SpeckleLoader extends Loader { } dispose() { + super.dispose() this.loader.dispose() } } diff --git a/packages/viewer/src/modules/materials/Materials.ts b/packages/viewer/src/modules/materials/Materials.ts index a6a216975f..a3f6ded614 100644 --- a/packages/viewer/src/modules/materials/Materials.ts +++ b/packages/viewer/src/modules/materials/Materials.ts @@ -1,6 +1,6 @@ import { Color, DoubleSide, FrontSide, Material, Texture, Vector2 } from 'three' import { GeometryType } from '../batching/Batch' -import { TreeNode } from '../tree/WorldTree' +import { type TreeNode } from '../tree/WorldTree' import { NodeRenderView } from '../tree/NodeRenderView' import SpeckleLineMaterial from './SpeckleLineMaterial' import SpeckleStandardMaterial from './SpeckleStandardMaterial' @@ -13,7 +13,7 @@ import SpeckleGhostMaterial from './SpeckleGhostMaterial' import SpeckleTextMaterial from './SpeckleTextMaterial' import { SpeckleMaterial } from './SpeckleMaterial' import SpecklePointColouredMaterial from './SpecklePointColouredMaterial' -import { Asset, AssetType, MaterialOptions } from '../../IViewer' +import { type Asset, AssetType, type MaterialOptions } from '../../IViewer' const defaultGradient: Asset = { id: 'defaultGradient', @@ -44,6 +44,7 @@ export enum FilterMaterialType { HIDDEN } +/** TO DO: This still sucks */ export interface FilterMaterial { filterType: FilterMaterialType rampIndex?: number @@ -62,26 +63,26 @@ export default class Materials { public static readonly UNIFORM_VECTORS_USED = 33 public static readonly DEFAULT_ARTIFICIAL_ROUGHNESS = 0.6 /** The inverse of "shininess" */ private readonly materialMap: { [hash: number]: Material } = {} - private meshGhostMaterial: Material = null - private meshGradientMaterial: Material = null - private meshTransparentGradientMaterial: Material = null - private meshColoredMaterial: Material = null - private meshTransparentColoredMaterial: Material = null - private meshHiddenMaterial: Material = null + private meshGhostMaterial: Material + private meshGradientMaterial: Material + private meshTransparentGradientMaterial: Material + private meshColoredMaterial: Material + private meshTransparentColoredMaterial: Material + private meshHiddenMaterial: Material - private lineGhostMaterial: Material = null - private lineColoredMaterial: Material = null - private lineHiddenMaterial: Material = null + private lineGhostMaterial: Material + private lineColoredMaterial: Material + private lineHiddenMaterial: Material - private pointGhostMaterial: Material = null - private pointCloudColouredMaterial: Material = null - private pointCloudGradientMaterial: Material = null + private pointGhostMaterial: Material + private pointCloudColouredMaterial: Material + private pointCloudGradientMaterial: Material - private textGhostMaterial: Material = null - private textColoredMaterial: Material = null - private textHiddenMaterial: Material = null + private textGhostMaterial: Material + private textColoredMaterial: Material + private textHiddenMaterial: Material - private defaultGradientTextureData: ImageData = null + private defaultGradientTextureData!: ImageData private static readonly NullRenderMaterialHash = this.hashCode( GeometryType.MESH.toString() @@ -112,11 +113,11 @@ export default class Materials { ) public static renderMaterialFromNode( - materialNode: TreeNode, - geometryNode: TreeNode - ): RenderMaterial { + materialNode: TreeNode | null, + geometryNode: TreeNode | null + ): RenderMaterial | null { if (!materialNode) return null - let renderMaterial: RenderMaterial = null + let renderMaterial: RenderMaterial | null = null if (materialNode.model.raw.renderMaterial) { renderMaterial = { id: materialNode.model.raw.renderMaterial.id, @@ -128,15 +129,17 @@ export default class Materials { roughness: materialNode.model.raw.renderMaterial.roughness, metalness: materialNode.model.raw.renderMaterial.metalness, vertexColors: - geometryNode.model.raw.colors && geometryNode.model.raw.colors.length > 0 + geometryNode && + geometryNode.model.raw.colors && + geometryNode.model.raw.colors.length > 0 } } return renderMaterial } - public static displayStyleFromNode(node: TreeNode): DisplayStyle { + public static displayStyleFromNode(node: TreeNode | null): DisplayStyle | null { if (!node) return null - let displayStyle: DisplayStyle = null + let displayStyle: DisplayStyle | null = null if (node.model.raw.displayStyle) { /** If there are no units specified, we ignore the line width value */ let lineWeight = node.model.raw.displayStyle.lineweight || 0 @@ -189,21 +192,32 @@ export default class Materials { return plm } - private static hashCode(s: string) { - let h + private static hashCode(s: string): number { + let h = 0 for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0 return h } - public static isMaterialInstance(material): material is Material { + public static isMaterialInstance( + material: Material | FilterMaterial | RenderMaterial | DisplayStyle + ): material is Material { return material instanceof Material } - public static isFilterMaterial(material): material is FilterMaterial { + public static isFilterMaterial( + material: Material | FilterMaterial | RenderMaterial | DisplayStyle + ): material is FilterMaterial { return 'filterType' in material } - public static isRendeMaterial(materialData): materialData is RenderMaterial { + public static isRendeMaterial( + materialData: + | Material + | FilterMaterial + | RenderMaterial + | DisplayStyle + | MaterialOptions + ): materialData is RenderMaterial { return ( 'color' in materialData && 'opacity' in materialData && @@ -213,14 +227,21 @@ export default class Materials { ) } - public static isDisplayStyle(materialData): materialData is DisplayStyle { + public static isDisplayStyle( + materialData: + | Material + | FilterMaterial + | RenderMaterial + | DisplayStyle + | MaterialOptions + ): materialData is DisplayStyle { return 'color' in materialData && 'lineWeight' in materialData } public static getMaterialHash( renderView: NodeRenderView, - materialData?: RenderMaterial | DisplayStyle | MaterialOptions - ) { + materialData?: RenderMaterial | DisplayStyle | MaterialOptions | null + ): number { if (!materialData) { materialData = renderView.renderData.renderMaterial || renderView.renderData.displayStyle @@ -733,7 +754,7 @@ export default class Materials { public getMaterial( hash: number, - material: RenderMaterial | DisplayStyle, + material: RenderMaterial | DisplayStyle | null, type: GeometryType ): Material { let mat @@ -742,7 +763,7 @@ export default class Materials { mat = this.getMeshMaterial(hash, material as RenderMaterial) break case GeometryType.LINE: - mat = this.getLineMaterial(hash, material) + mat = this.getLineMaterial(hash, material as DisplayStyle) break case GeometryType.POINT: mat = this.getPointMaterial(hash, material as RenderMaterial) @@ -751,7 +772,7 @@ export default class Materials { mat = this.getPointCloudMaterial(hash, material as RenderMaterial) break case GeometryType.TEXT: - mat = this.getTextMaterial(hash, material) + mat = this.getTextMaterial(hash, material as DisplayStyle) break } // } @@ -801,7 +822,7 @@ export default class Materials { public getGhostMaterial( renderView: NodeRenderView, filterMaterial?: FilterMaterial - ): Material { + ): Material | null { filterMaterial switch (renderView.geometryType) { case GeometryType.MESH: @@ -820,7 +841,7 @@ export default class Materials { public getGradientMaterial( renderView: NodeRenderView, filterMaterial?: FilterMaterial - ): Material { + ): Material | null { switch (renderView.geometryType) { case GeometryType.MESH: { const material = renderView.transparent @@ -858,7 +879,7 @@ export default class Materials { public getColoredMaterial( renderView: NodeRenderView, filterMaterial?: FilterMaterial - ): Material { + ): Material | null { switch (renderView.geometryType) { case GeometryType.MESH: { const material = renderView.transparent @@ -896,7 +917,7 @@ export default class Materials { public getHiddenMaterial( renderView: NodeRenderView, filterMaterial?: FilterMaterial - ): Material { + ): Material | null { filterMaterial switch (renderView.geometryType) { case GeometryType.MESH: @@ -915,8 +936,8 @@ export default class Materials { public getFilterMaterial( renderView: NodeRenderView, filterMaterial: FilterMaterial - ): Material { - let retMaterial: Material + ): Material | null { + let retMaterial: Material | null = null switch (filterMaterial.filterType) { case FilterMaterialType.GHOST: retMaterial = this.getGhostMaterial(renderView, filterMaterial) @@ -934,7 +955,7 @@ export default class Materials { /** There's a bug in three.js where it checks for the length of the planes without checking if they exist first * It's been allegedly fixed in a later version but until we update we'll just assing an empty array */ - retMaterial.clippingPlanes = [] + if (retMaterial) retMaterial.clippingPlanes = [] return retMaterial } @@ -948,7 +969,7 @@ export default class Materials { public getFilterMaterialOptions( filterMaterial: FilterMaterial - ): FilterMaterialOptions { + ): FilterMaterialOptions | null { switch (filterMaterial.filterType) { case FilterMaterialType.COLORED: return { @@ -973,7 +994,8 @@ export default class Materials { rampIndexColor: filterMaterial.rampIndexColor !== undefined ? filterMaterial.rampIndexColor - : new Color() + : filterMaterial.rampIndex + ? new Color() .setRGB( this.defaultGradientTextureData.data[ Math.floor( @@ -998,7 +1020,8 @@ export default class Materials { 2 ] / 255 ) - .convertSRGBToLinear(), + .convertSRGBToLinear() + : undefined, rampTexture: filterMaterial.rampTexture ? filterMaterial.rampTexture : this.meshGradientMaterial.userData.gradientRamp.value, @@ -1006,6 +1029,8 @@ export default class Materials { ? filterMaterial.rampTexture.image.width : this.meshGradientMaterial.userData.gradientRamp.value.image.width } + default: + return null } } diff --git a/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts b/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts index 725618ace0..22576e17aa 100644 --- a/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts @@ -1,18 +1,28 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable camelcase */ import { speckleBasicVert } from './shaders/speckle-basic-vert' import { speckleBasicFrag } from './shaders/speckle-basic-frag' -import { ShaderLib, Vector3, Material, IUniform, Vector2 } from 'three' +import { + ShaderLib, + Vector3, + Material, + type IUniform, + Vector2, + type MeshBasicMaterialParameters, + Scene, + Camera, + BufferGeometry, + Object3D +} from 'three' import { Matrix4 } from 'three' -import { Geometry } from '../converter/Geometry' -import { ExtendedMeshBasicMaterial, Uniforms } from './SpeckleMaterial' +import { ExtendedMeshBasicMaterial, type Uniforms } from './SpeckleMaterial' +import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { protected static readonly matBuff: Matrix4 = new Matrix4() protected static readonly vecBuff: Vector2 = new Vector2() - private _billboardPixelHeight: number + private _billboardPixelHeight!: number protected get vertexProgram(): string { return speckleBasicVert @@ -43,7 +53,7 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { this._billboardPixelHeight = value } - constructor(parameters, defines = []) { + constructor(parameters: MeshBasicMaterialParameters, defines: string[] = []) { super(parameters) this.init(defines) } @@ -53,7 +63,7 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { return this.constructor.name } - public copy(source) { + public copy(source: Material) { super.copy(source) this.copyFrom(source) return this @@ -69,8 +79,14 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { } /** Called by three.js render loop */ - public onBeforeRender(_this, scene, camera, geometry, object, group) { - if (this.defines['BILLBOARD_FIXED']) { + public onBeforeRender( + _this: SpeckleWebGLRenderer, + _scene: Scene, + camera: Camera, + _geometry: BufferGeometry, + object: Object3D + ) { + if (this.defines && this.defines['BILLBOARD_FIXED']) { const resolution = _this.getDrawingBufferSize(SpeckleBasicMaterial.vecBuff) SpeckleBasicMaterial.vecBuff.set( (this._billboardPixelHeight / resolution.x) * 2, @@ -81,7 +97,7 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { this.userData.invProjection.value.copy(SpeckleBasicMaterial.matBuff) } - if (this.defines['USE_RTE']) { + if (this.defines && this.defines['USE_RTE']) { object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix) this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow) this.userData.uViewer_high.value.copy(_this.RTEBuffers.viewerHigh) diff --git a/packages/viewer/src/modules/materials/SpeckleDepthMaterial.ts b/packages/viewer/src/modules/materials/SpeckleDepthMaterial.ts index be23afb64b..50cbe81982 100644 --- a/packages/viewer/src/modules/materials/SpeckleDepthMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleDepthMaterial.ts @@ -1,17 +1,21 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable camelcase */ import { speckleDepthVert } from './shaders/speckle-depth-vert' import { speckleDepthFrag } from './shaders/speckle-depth-frag' -import { ShaderLib, Vector3, IUniform } from 'three' +import { + BufferGeometry, + Camera, + Object3D, + Scene, + ShaderLib, + Vector3, + type IUniform, + type MeshDepthMaterialParameters +} from 'three' import { Matrix4, Material } from 'three' -import { ExtendedMeshDepthMaterial, Uniforms } from './SpeckleMaterial' +import { ExtendedMeshDepthMaterial, type Uniforms } from './SpeckleMaterial' +import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' class SpeckleDepthMaterial extends ExtendedMeshDepthMaterial { - private static readonly matBuff: Matrix4 = new Matrix4() - private static readonly vecBuff0: Vector3 = new Vector3() - private static readonly vecBuff1: Vector3 = new Vector3() - private static readonly vecBuff2: Vector3 = new Vector3() - protected get vertexProgram(): string { return speckleDepthVert } @@ -37,7 +41,7 @@ class SpeckleDepthMaterial extends ExtendedMeshDepthMaterial { } } - constructor(parameters, defines = []) { + constructor(parameters: MeshDepthMaterialParameters, defines: string[] = []) { super(parameters) this.init(defines) } @@ -47,7 +51,7 @@ class SpeckleDepthMaterial extends ExtendedMeshDepthMaterial { return this.constructor.name } - public copy(source) { + public copy(source: Material) { super.copy(source) this.copyFrom(source) return this @@ -62,8 +66,14 @@ class SpeckleDepthMaterial extends ExtendedMeshDepthMaterial { /** Another note here, this will NOT get called by three when rendering shadowmaps. We update the uniforms manually * inside SpeckleRenderer for shadowmaps */ - onBeforeRender(_this, scene, camera, geometry, object, group) { - if (this.defines['USE_RTE']) { + onBeforeRender( + _this: SpeckleWebGLRenderer, + _scene: Scene, + _camera: Camera, + _geometry: BufferGeometry, + object: Object3D + ) { + if (this.defines && this.defines['USE_RTE']) { object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix) this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow) this.userData.uViewer_high.value.copy(_this.RTEBuffers.viewerHigh) diff --git a/packages/viewer/src/modules/materials/SpeckleDisplaceMaterial.ts b/packages/viewer/src/modules/materials/SpeckleDisplaceMaterial.ts index 933baf12ae..e8da58247d 100644 --- a/packages/viewer/src/modules/materials/SpeckleDisplaceMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleDisplaceMaterial.ts @@ -1,8 +1,8 @@ import { speckleDisplaceVert } from './shaders/speckle-displace.vert' import { speckleDisplaceFrag } from './shaders/speckle-displace-frag' -import { Material, Vector2 } from 'three' +import { Material, Vector2, type MeshBasicMaterialParameters } from 'three' import SpeckleBasicMaterial from './SpeckleBasicMaterial' -import { Uniforms } from './SpeckleMaterial' +import { type Uniforms } from './SpeckleMaterial' class SpeckleDisplaceMaterial extends SpeckleBasicMaterial { protected get vertexProgram(): string { @@ -17,7 +17,7 @@ class SpeckleDisplaceMaterial extends SpeckleBasicMaterial { return { ...super.uniformsDef, size: new Vector2(), displacement: 0 } } - constructor(parameters, defines) { + constructor(parameters: MeshBasicMaterialParameters, defines: string[] = []) { super(parameters, defines) } diff --git a/packages/viewer/src/modules/materials/SpeckleGhostMaterial.ts b/packages/viewer/src/modules/materials/SpeckleGhostMaterial.ts index 1ea55edf04..f7adb27a3e 100644 --- a/packages/viewer/src/modules/materials/SpeckleGhostMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleGhostMaterial.ts @@ -1,6 +1,7 @@ import { speckleGhostVert } from './shaders/speckle-ghost-vert' import { speckleGhostFrag } from './shaders/speckle-ghost-frag' import SpeckleBasicMaterial from './SpeckleBasicMaterial' +import type { MeshBasicMaterialParameters } from 'three' class SpeckleGhostMaterial extends SpeckleBasicMaterial { protected get vertexProgram(): string { @@ -11,7 +12,10 @@ class SpeckleGhostMaterial extends SpeckleBasicMaterial { return speckleGhostFrag } - constructor(parameters, defines = ['USE_RTE']) { + constructor( + parameters: MeshBasicMaterialParameters, + defines: string[] = ['USE_RTE'] + ) { super(parameters, defines) } } diff --git a/packages/viewer/src/modules/materials/SpeckleLineMaterial.ts b/packages/viewer/src/modules/materials/SpeckleLineMaterial.ts index 05a67c9adc..ed2d5259a3 100644 --- a/packages/viewer/src/modules/materials/SpeckleLineMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleLineMaterial.ts @@ -1,10 +1,22 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable camelcase */ import { speckleLineVert } from './shaders/speckle-line-vert' import { speckleLineFrag } from './shaders/speckle-line-frag' -import { ShaderLib, Vector3, IUniform, Material } from 'three' -import { ExtendedLineMaterial, Uniforms } from './SpeckleMaterial' -import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' +import { + ShaderLib, + Vector3, + type IUniform, + Material, + Scene, + Camera, + BufferGeometry, + Object3D +} from 'three' +import { ExtendedLineMaterial, type Uniforms } from './SpeckleMaterial' +import { + LineMaterial, + type LineMaterialParameters +} from 'three/examples/jsm/lines/LineMaterial.js' +import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' class SpeckleLineMaterial extends ExtendedLineMaterial { protected get vertexProgram(): string { @@ -32,7 +44,7 @@ class SpeckleLineMaterial extends ExtendedLineMaterial { this.needsUpdate = true } - constructor(parameters, defines = ['USE_RTE']) { + constructor(parameters: LineMaterialParameters, defines = ['USE_RTE']) { super(parameters) this.init(defines) } @@ -42,7 +54,7 @@ class SpeckleLineMaterial extends ExtendedLineMaterial { return this.constructor.name } - public copy(source) { + public copy(source: Material) { super.copy(source) this.copyFrom(source) return this @@ -56,7 +68,13 @@ class SpeckleLineMaterial extends ExtendedLineMaterial { to.userData.pixelThreshold.value = from.userData.pixelThreshold.value } - onBeforeRender(_this, scene, camera, geometry, object, group) { + onBeforeRender( + _this: SpeckleWebGLRenderer, + _scene: Scene, + _camera: Camera, + _geometry: BufferGeometry, + object: Object3D + ) { object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix) this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow) this.userData.uViewer_high.value.copy(_this.RTEBuffers.viewerHigh) diff --git a/packages/viewer/src/modules/materials/SpeckleMaterial.ts b/packages/viewer/src/modules/materials/SpeckleMaterial.ts index 1fa742294b..3ffb70356a 100644 --- a/packages/viewer/src/modules/materials/SpeckleMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleMaterial.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ -/* eslint-disable camelcase */ import { AlwaysStencilFunc, - IUniform, + type IUniform, Material, MeshBasicMaterial, MeshDepthMaterial, @@ -10,24 +9,43 @@ import { MeshStandardMaterial, PointsMaterial, ReplaceStencilOp, - UniformsUtils + UniformsUtils, + type Shader } from 'three' import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' import { StencilOutlineType } from '../../IViewer' -import { MaterialOptions } from './MaterialOptions' +import { type MaterialOptions } from './MaterialOptions' class SpeckleUserData { + [k: string]: unknown toJSON() { return {} } } + // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Uniforms = Record export class SpeckleMaterial { - protected _internalUniforms - protected _stencilOutline - public needsCopy: boolean + protected _internalUniforms!: Shader + protected _stencilOutline: StencilOutlineType = StencilOutlineType.NONE + public needsCopy: boolean = false + + protected get speckleUserData(): SpeckleUserData { + return (this as unknown as Material).userData + } + + protected set speckleUserData(value: SpeckleUserData) { + ;(this as unknown as Material).userData = value + } + + protected get speckleDefines(): Record | undefined { + return (this as unknown as Material).defines + } + + protected set speckleDefines(value: Record | undefined) { + ;(this as unknown as Material).defines = value + } protected get vertexProgram(): string { return '' @@ -50,55 +68,62 @@ export class SpeckleMaterial { } protected set stencilOutline(value: StencilOutlineType) { - this['stencilWrite'] = value === StencilOutlineType.NONE ? false : true - if (this['stencilWrite']) { - this['stencilWriteMask'] = 0xff - this['stencilRef'] = 0x00 - this['stencilFunc'] = AlwaysStencilFunc - this['stencilZFail'] = ReplaceStencilOp - this['stencilZPass'] = ReplaceStencilOp - this['stencilFail'] = ReplaceStencilOp + this._stencilOutline = value + const thisAsMaterial: Material = this as unknown as Material + thisAsMaterial.stencilWrite = value === StencilOutlineType.NONE ? false : true + if (thisAsMaterial.stencilWrite) { + thisAsMaterial.stencilWriteMask = 0xff + thisAsMaterial.stencilRef = 0x00 + thisAsMaterial.stencilFunc = AlwaysStencilFunc + thisAsMaterial.stencilZFail = ReplaceStencilOp + thisAsMaterial.stencilZPass = ReplaceStencilOp + thisAsMaterial.stencilFail = ReplaceStencilOp if (value === StencilOutlineType.OUTLINE_ONLY) { - this['colorWrite'] = false - this['depthWrite'] = false - this['stencilWrite'] = true + thisAsMaterial.colorWrite = false + thisAsMaterial.depthWrite = false + thisAsMaterial.stencilWrite = true } } } protected set pointSize(value: number) { - this['size'] = value + ;(this as unknown as PointsMaterial).size = value } - protected init(defines = []) { - this['userData'] = new SpeckleUserData() + protected init(defines: Array = []) { + this.speckleUserData = new SpeckleUserData() this.setUniforms(this.uniformsDef) this.setDefines(defines) - this['onBeforeCompile'] = this.onCompile + ;(this as unknown as Material)['onBeforeCompile'] = this.onCompile } protected setUniforms(def: Uniforms) { for (const k in def) { - this['userData'][k] = { + this.speckleUserData[k] = { value: def[k] } } - this['uniforms'] = UniformsUtils.merge([this.baseUniforms, this['userData']]) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(this as any)['uniforms'] = UniformsUtils.merge([ + this.baseUniforms, + this.speckleUserData + ]) } - protected setDefines(defines = []) { + protected setDefines(defines: Array = []) { if (defines) { - this['defines'] = {} + this.speckleDefines = {} for (let k = 0; k < defines.length; k++) { - this['defines'][defines[k]] = ' ' + this.speckleDefines[defines[k]] = ' ' } } } protected copyUniforms(material: Material) { for (const k in material.userData) { - if (this['userData'][k] !== undefined) - this['userData'][k].value = material.userData[k].value + if (this.speckleUserData[k] !== undefined) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.speckleUserData[k] as any).value = material.userData[k].value } } @@ -106,21 +131,23 @@ export class SpeckleMaterial { if (!this._internalUniforms) return for (const k in this.uniformsDef) { - this._internalUniforms.uniforms[k] = this['userData'][k] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this._internalUniforms.uniforms[k] = this.speckleUserData[k] as IUniform } } - protected copyFrom(source) { - this['userData'] = new SpeckleUserData() + protected copyFrom(source: Material) { + this.speckleUserData = new SpeckleUserData() this.setUniforms(this.uniformsDef) this.copyUniforms(source) this.bindUniforms() - Object.assign(this['defines'], source.defines) - if (source.needsCopy) this.needsCopy = source.needsCopy + Object.assign(this.speckleDefines as object, source.defines) + if ((source as unknown as SpeckleMaterial).needsCopy) + this.needsCopy = (source as unknown as SpeckleMaterial).needsCopy } // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected onCompile(shader, renderer) { + protected onCompile(shader: Shader) { this._internalUniforms = shader this.bindUniforms() shader.vertexShader = this.vertexProgram @@ -141,7 +168,7 @@ export class SpeckleMaterial { to.clippingPlanes = from.clippingPlanes to.clipShadows = from.clipShadows to.colorWrite = from.colorWrite - Object.assign(to.defines, from.defines) + Object.assign(to.defines as object, from.defines) to.depthFunc = from.depthFunc to.depthTest = from.depthTest to.depthWrite = from.depthWrite @@ -182,7 +209,6 @@ export class ExtendedMeshNormalMaterial extends MeshNormalMaterial {} export class ExtendedLineMaterial extends LineMaterial {} export class ExtendedPointsMaterial extends PointsMaterial {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ExtendedMeshStandardMaterial extends SpeckleMaterial {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ExtendedMeshBasicMaterial extends SpeckleMaterial {} diff --git a/packages/viewer/src/modules/materials/SpeckleNormalMaterial.ts b/packages/viewer/src/modules/materials/SpeckleNormalMaterial.ts index 76f5c86111..94e302ee7d 100644 --- a/packages/viewer/src/modules/materials/SpeckleNormalMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleNormalMaterial.ts @@ -1,11 +1,20 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable camelcase */ import { speckleNormalVert } from './shaders/speckle-normal-vert' import { speckleNormalFrag } from './shaders/speckle-normal-frag' -import { ShaderLib, Vector3, IUniform } from 'three' +import { + BufferGeometry, + Camera, + Material, + Object3D, + Scene, + ShaderLib, + Vector3, + type IUniform, + type MeshNormalMaterialParameters +} from 'three' import { Matrix4 } from 'three' -import { Geometry } from '../converter/Geometry' -import { ExtendedMeshNormalMaterial, Uniforms } from './SpeckleMaterial' +import { ExtendedMeshNormalMaterial, type Uniforms } from './SpeckleMaterial' +import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' class SpeckleNormalMaterial extends ExtendedMeshNormalMaterial { protected get vertexProgram(): string { @@ -29,7 +38,7 @@ class SpeckleNormalMaterial extends ExtendedMeshNormalMaterial { } } - constructor(parameters, defines = ['USE_RTE']) { + constructor(parameters: MeshNormalMaterialParameters, defines = ['USE_RTE']) { super(parameters) this.init(defines) } @@ -39,14 +48,20 @@ class SpeckleNormalMaterial extends ExtendedMeshNormalMaterial { return this.constructor.name } - public copy(source) { + public copy(source: Material) { super.copy(source) this.copyFrom(source) return this } /** Called by three.js render loop */ - public onBeforeRender(_this, scene, camera, geometry, object, group) { + public onBeforeRender( + _this: SpeckleWebGLRenderer, + _scene: Scene, + _camera: Camera, + _geometry: BufferGeometry, + object: Object3D + ) { object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix) this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow) this.userData.uViewer_high.value.copy(_this.RTEBuffers.viewerHigh) diff --git a/packages/viewer/src/modules/materials/SpecklePointColouredMaterial.ts b/packages/viewer/src/modules/materials/SpecklePointColouredMaterial.ts index f0213a88ad..ba9d21f191 100644 --- a/packages/viewer/src/modules/materials/SpecklePointColouredMaterial.ts +++ b/packages/viewer/src/modules/materials/SpecklePointColouredMaterial.ts @@ -1,67 +1,28 @@ /* eslint-disable camelcase */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { specklePointVert } from './shaders/speckle-point-vert' import { specklePointFrag } from './shaders/speckle-point-frag' -import { - Matrix4, - NearestFilter, - PointsMaterial, - ShaderLib, - Texture, - UniformsUtils, - Vector3 -} from 'three' -import { Geometry } from '../converter/Geometry' +import { Material, NearestFilter, Texture, type PointsMaterialParameters } from 'three' +import type { Uniforms } from './SpeckleMaterial' +import SpecklePointMaterial from './SpecklePointMaterial' -class SpecklePointMaterial extends PointsMaterial { - private static readonly matBuff: Matrix4 = new Matrix4() - private static readonly vecBuff0: Vector3 = new Vector3() - private static readonly vecBuff1: Vector3 = new Vector3() - private static readonly vecBuff2: Vector3 = new Vector3() +class SpecklePointColouredMaterial extends SpecklePointMaterial { + protected get vertexProgram(): string { + return specklePointVert + } - constructor(parameters, defines = []) { - super(parameters) - this.userData.uViewer_high = { - value: new Vector3() - } - this.userData.uViewer_low = { - value: new Vector3() - } - this.userData.gradientRamp = { - value: null - } - ;(this as any).vertProgram = specklePointVert - ;(this as any).fragProgram = specklePointFrag - ;(this as any).uniforms = UniformsUtils.merge([ - ShaderLib.standard.uniforms, - { - uViewer_high: { - value: this.userData.uViewer_high.value - }, - uViewer_low: { - value: this.userData.uViewer_low.value - }, - gradientRamp: { - value: this.userData.gradientRamp.value - } - } - ]) + protected get fragmentProgram(): string { + return specklePointFrag + } - this.onBeforeCompile = function (shader) { - shader.uniforms.uViewer_high = this.userData.uViewer_high - shader.uniforms.uViewer_low = this.userData.uViewer_low - shader.uniforms.gradientRamp = this.userData.gradientRamp - shader.vertexShader = this.vertProgram - shader.fragmentShader = this.fragProgram - } + protected get uniformsDef(): Uniforms { + return { ...super.uniformsDef, gradientRamp: null } + } - if (defines) { - this.defines = { USE_GRADIENT_RAMP: '' } - } - for (let k = 0; k < defines.length; k++) { - this.defines[defines[k]] = ' ' - } + constructor( + parameters: PointsMaterialParameters, + defines: string[] = ['USE_RTE', 'USE_GRADIENT_RAMP'] + ) { + super(parameters, defines) } public setGradientTexture(texture: Texture) { @@ -72,50 +33,10 @@ class SpecklePointMaterial extends PointsMaterial { this.needsUpdate = true } - copy(source) { - super.copy(source) - this.userData = {} - this.userData.uViewer_high = { - value: new Vector3() - } - this.userData.uViewer_low = { - value: new Vector3() - } - this.userData.gradientRamp = { - value: null - } - - this.defines['USE_RTE'] = ' ' - this.defines['USE_GRADIENT_RAMP'] = ' ' - - return this - } - - onBeforeRender(_this, scene, camera, geometry, object, group) { - SpecklePointMaterial.matBuff.copy(camera.matrixWorldInverse) - SpecklePointMaterial.matBuff.elements[12] = 0 - SpecklePointMaterial.matBuff.elements[13] = 0 - SpecklePointMaterial.matBuff.elements[14] = 0 - SpecklePointMaterial.matBuff.multiply(object.matrixWorld) - object.modelViewMatrix.copy(SpecklePointMaterial.matBuff) - - SpecklePointMaterial.vecBuff0.set( - camera.matrixWorld.elements[12], - camera.matrixWorld.elements[13], - camera.matrixWorld.elements[14] - ) - - Geometry.DoubleToHighLowVector( - SpecklePointMaterial.vecBuff0, - SpecklePointMaterial.vecBuff1, - SpecklePointMaterial.vecBuff2 - ) - - this.userData.uViewer_low.value.copy(SpecklePointMaterial.vecBuff1) - this.userData.uViewer_high.value.copy(SpecklePointMaterial.vecBuff2) - - this.needsUpdate = true + public fastCopy(from: Material, to: Material) { + super.fastCopy(from, to) + to.userData.gradientRamp.value = from.userData.gradientRamp.value } } -export default SpecklePointMaterial +export default SpecklePointColouredMaterial diff --git a/packages/viewer/src/modules/materials/SpecklePointMaterial.ts b/packages/viewer/src/modules/materials/SpecklePointMaterial.ts index 50168cb34c..dbb577cd9f 100644 --- a/packages/viewer/src/modules/materials/SpecklePointMaterial.ts +++ b/packages/viewer/src/modules/materials/SpecklePointMaterial.ts @@ -1,10 +1,20 @@ /* eslint-disable camelcase */ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { specklePointVert } from './shaders/speckle-point-vert' import { specklePointFrag } from './shaders/speckle-point-frag' -import { IUniform, Material, Matrix4, PointsMaterial, ShaderLib, Vector3 } from 'three' -import { Geometry } from '../converter/Geometry' -import { ExtendedPointsMaterial, Uniforms } from './SpeckleMaterial' +import { + type IUniform, + Material, + PointsMaterial, + ShaderLib, + Vector3, + type PointsMaterialParameters, + Scene, + Camera, + BufferGeometry, + Object3D +} from 'three' +import { ExtendedPointsMaterial, type Uniforms } from './SpeckleMaterial' +import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' class SpecklePointMaterial extends ExtendedPointsMaterial { protected get vertexProgram(): string { @@ -26,7 +36,7 @@ class SpecklePointMaterial extends ExtendedPointsMaterial { } } - constructor(parameters, defines = ['USE_RTE']) { + constructor(parameters: PointsMaterialParameters, defines = ['USE_RTE']) { super(parameters) this.init(defines) } @@ -36,7 +46,7 @@ class SpecklePointMaterial extends ExtendedPointsMaterial { return this.constructor.name } - public copy(source) { + public copy(source: Material) { super.copy(source) this.copyFrom(source) return this @@ -51,7 +61,13 @@ class SpecklePointMaterial extends ExtendedPointsMaterial { toStandard.sizeAttenuation = fromStandard.sizeAttenuation } - onBeforeRender(_this, scene, camera, geometry, object, group) { + onBeforeRender( + _this: SpeckleWebGLRenderer, + _scene: Scene, + _camera: Camera, + _geometry: BufferGeometry, + object: Object3D + ) { object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix) this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow) this.userData.uViewer_high.value.copy(_this.RTEBuffers.viewerHigh) diff --git a/packages/viewer/src/modules/materials/SpeckleShadowcatcherMaterial.ts b/packages/viewer/src/modules/materials/SpeckleShadowcatcherMaterial.ts index b3a682bd15..cdb7dacbd6 100644 --- a/packages/viewer/src/modules/materials/SpeckleShadowcatcherMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleShadowcatcherMaterial.ts @@ -1,8 +1,8 @@ import { speckleShadowcatcherVert } from './shaders/speckle-shadowcatcher-vert' import { speckleShadowcatcherFrag } from './shaders/speckle-shadowcatche-frag' import SpeckleBasicMaterial from './SpeckleBasicMaterial' -import { Vector4 } from 'three' -import { Uniforms } from './SpeckleMaterial' +import { Vector4, type MeshBasicMaterialParameters } from 'three' +import { type Uniforms } from './SpeckleMaterial' class SpeckleShadowcatcherMaterial extends SpeckleBasicMaterial { protected get vertexProgram(): string { @@ -26,7 +26,7 @@ class SpeckleShadowcatcherMaterial extends SpeckleBasicMaterial { } } - constructor(parameters, defines = []) { + constructor(parameters: MeshBasicMaterialParameters, defines = []) { super(parameters, defines) } } diff --git a/packages/viewer/src/modules/materials/SpeckleStandardColoredMaterial.ts b/packages/viewer/src/modules/materials/SpeckleStandardColoredMaterial.ts index b79cdcfff8..dad53a1b0a 100644 --- a/packages/viewer/src/modules/materials/SpeckleStandardColoredMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleStandardColoredMaterial.ts @@ -1,8 +1,8 @@ import { speckleStandardColoredVert } from './shaders/speckle-standard-colored-vert' import { speckleStandardColoredFrag } from './shaders/speckle-standard-colored-frag' -import { Texture, NearestFilter } from 'three' +import { Texture, NearestFilter, type MeshStandardMaterialParameters } from 'three' import SpeckleStandardMaterial from './SpeckleStandardMaterial' -import { Uniforms } from './SpeckleMaterial' +import { type Uniforms } from './SpeckleMaterial' class SpeckleStandardColoredMaterial extends SpeckleStandardMaterial { protected get vertexProgram(): string { @@ -17,7 +17,7 @@ class SpeckleStandardColoredMaterial extends SpeckleStandardMaterial { return { ...super.uniformsDef, gradientRamp: null } } - constructor(parameters, defines = ['USE_RTE']) { + constructor(parameters: MeshStandardMaterialParameters, defines = ['USE_RTE']) { super(parameters, defines) } diff --git a/packages/viewer/src/modules/materials/SpeckleStandardMaterial.ts b/packages/viewer/src/modules/materials/SpeckleStandardMaterial.ts index 96c8a1d8e7..49936ac208 100644 --- a/packages/viewer/src/modules/materials/SpeckleStandardMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleStandardMaterial.ts @@ -1,10 +1,19 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable camelcase */ import { speckleStandardVert } from './shaders/speckle-standard-vert' import { speckleStandardFrag } from './shaders/speckle-standard-frag' -import { ShaderLib, Vector3, Material, IUniform } from 'three' +import { + ShaderLib, + Vector3, + Material, + type IUniform, + type MeshStandardMaterialParameters, + Scene, + Camera, + BufferGeometry, + Object3D +} from 'three' import { Matrix4 } from 'three' -import { ExtendedMeshStandardMaterial, Uniforms } from './SpeckleMaterial' +import { ExtendedMeshStandardMaterial, type Uniforms } from './SpeckleMaterial' import { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' class SpeckleStandardMaterial extends ExtendedMeshStandardMaterial { @@ -36,7 +45,7 @@ class SpeckleStandardMaterial extends ExtendedMeshStandardMaterial { } } - constructor(parameters, defines = ['USE_RTE']) { + constructor(parameters: MeshStandardMaterialParameters, defines = ['USE_RTE']) { super(parameters) this.init(defines) } @@ -46,11 +55,13 @@ class SpeckleStandardMaterial extends ExtendedMeshStandardMaterial { return this.constructor.name } - public copy(source) { + public copy(source: Material) { super.copy(source) this.copyFrom(source) - this.originalRoughness = source.originalRoughness - this.artificialRoughness = source.artificialRoughness + if (source instanceof SpeckleStandardMaterial) { + this.originalRoughness = source.originalRoughness + this.artificialRoughness = source.artificialRoughness + } return this } @@ -94,16 +105,26 @@ class SpeckleStandardMaterial extends ExtendedMeshStandardMaterial { if (this.originalRoughness === undefined) this.originalRoughness = this.roughness this.artificialRoughness = artificialRougness } + if (this.originalRoughness === undefined || this.artificialRoughness === undefined) + return + const applyRoughness = artificialRougness !== undefined ? Math.min(this.originalRoughness, this.artificialRoughness) : this.originalRoughness + this.roughness = applyRoughness this.needsCopy = true } /** Called by three.js render loop */ - public onBeforeRender(_this: SpeckleWebGLRenderer, scene, camera, geometry, object) { + public onBeforeRender( + _this: SpeckleWebGLRenderer, + _scene: Scene, + _camera: Camera, + _geometry: BufferGeometry, + object: Object3D + ) { if (this.defines['USE_RTE']) { object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix) this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow) diff --git a/packages/viewer/src/modules/materials/SpeckleTextMaterial.ts b/packages/viewer/src/modules/materials/SpeckleTextMaterial.ts index 6dd11a5fa4..9eb3ac2038 100644 --- a/packages/viewer/src/modules/materials/SpeckleTextMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleTextMaterial.ts @@ -2,17 +2,31 @@ /* eslint-disable camelcase */ import { speckleTextVert } from './shaders/speckle-text-vert' import { speckleTextFrag } from './shaders/speckle-text-frag' -import { ShaderLib, Vector3, IUniform, Vector2, Material } from 'three' +import { + ShaderLib, + Vector3, + type IUniform, + Vector2, + Material, + type MeshBasicMaterialParameters, + Scene, + Camera, + BufferGeometry, + Object3D +} from 'three' import { Matrix4 } from 'three' -import { ExtendedMeshBasicMaterial, Uniforms } from './SpeckleMaterial' +import { ExtendedMeshBasicMaterial, type Uniforms } from './SpeckleMaterial' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +//@ts-ignore import { createTextDerivedMaterial } from 'troika-three-text' +import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' class SpeckleTextMaterial extends ExtendedMeshBasicMaterial { protected static readonly matBuff: Matrix4 = new Matrix4() protected static readonly vecBuff: Vector2 = new Vector2() - private _billboardPixelHeight: number + private _billboardPixelHeight!: number protected get vertexProgram(): string { return speckleTextVert @@ -47,7 +61,7 @@ class SpeckleTextMaterial extends ExtendedMeshBasicMaterial { return this._billboardPixelHeight } - constructor(parameters, defines = []) { + constructor(parameters: MeshBasicMaterialParameters, defines: Array = []) { super(parameters) this.init(defines) } @@ -57,7 +71,7 @@ class SpeckleTextMaterial extends ExtendedMeshBasicMaterial { return this.constructor.name } - public copy(source) { + public copy(source: Material) { super.copy(source) this.copyFrom(source) return this @@ -83,8 +97,14 @@ class SpeckleTextMaterial extends ExtendedMeshBasicMaterial { } /** Called by three.js render loop */ - public onBeforeRender(_this, scene, camera, geometry, object, group) { - if (this.defines['BILLBOARD_FIXED']) { + public onBeforeRender( + _this: SpeckleWebGLRenderer, + _scene: Scene, + camera: Camera, + _geometry: BufferGeometry, + _object: Object3D + ) { + if (this.defines && this.defines['BILLBOARD_FIXED']) { const resolution = _this.getDrawingBufferSize(SpeckleTextMaterial.vecBuff) SpeckleTextMaterial.vecBuff.set( (this._billboardPixelHeight / resolution.x) * 2, diff --git a/packages/viewer/src/modules/objects/AccelerationStructure.ts b/packages/viewer/src/modules/objects/AccelerationStructure.ts index d34cb7f18f..a2a8aabac4 100644 --- a/packages/viewer/src/modules/objects/AccelerationStructure.ts +++ b/packages/viewer/src/modules/objects/AccelerationStructure.ts @@ -3,16 +3,13 @@ import { BufferGeometry, Float32BufferAttribute, FrontSide, - Intersection, Material, Matrix4, - Object3D, Ray, Side, Uint16BufferAttribute, Uint32BufferAttribute, - Vector3, - Event + Vector3 } from 'three' import { CENTER, @@ -21,6 +18,7 @@ import { ShapecastIntersection, SplitStrategy } from 'three-mesh-bvh' +import { MeshIntersection } from './SpeckleRaycaster' const SKIP_GENERATION = Symbol('skip tree generation') @@ -31,7 +29,7 @@ export interface BVHOptions { verbose: boolean useSharedArrayBuffer: boolean setBoundingBox: boolean - onProgress: () => void + onProgress?: () => void [SKIP_GENERATION]: boolean } @@ -42,7 +40,6 @@ export const DefaultBVHOptions = { verbose: true, useSharedArrayBuffer: false, setBoundingBox: false, - onProgress: null, [SKIP_GENERATION]: false } @@ -71,10 +68,10 @@ to get the correct values for Vectors, Rays, Boxes, etc export class AccelerationStructure { private static readonly MatBuff: Matrix4 = new Matrix4() private _bvh: MeshBVH - public inputTransform: Matrix4 - public outputTransform: Matrix4 - public inputOriginTransform: Matrix4 - public outputOriginTransfom: Matrix4 + public inputTransform!: Matrix4 + public outputTransform!: Matrix4 + public inputOriginTransform!: Matrix4 + public outputOriginTransfom!: Matrix4 public get geometry() { return this._bvh.geometry @@ -85,12 +82,17 @@ export class AccelerationStructure { } public static buildBVH( - indices: number[], - position: Float32Array, + indices: number[] | undefined, + position: number[] | undefined, options: BVHOptions = DefaultBVHOptions, transform?: Matrix4 ): MeshBVH { - let bvhPositions = position + /** There is no unniverse where you can build a BVH without proper indices or positions */ + if (!indices || !position) { + throw new Error('Cannot build BVH with undefined indices or position!') + } + + let bvhPositions = new Float32Array(position) if (transform) { bvhPositions = new Float32Array(position.length) const vecBuff = new Vector3() @@ -129,21 +131,23 @@ export class AccelerationStructure { public raycast( ray: Ray, materialOrSide: Side | Material | Material[] = FrontSide - ): Intersection>[] { + ): MeshIntersection[] { const res = this._bvh.raycast(this.transformInput(ray), materialOrSide) res.forEach((value) => { value.point = this.transformOutput(value.point) }) - return res + /** The intersection results from raycasting a bvh will always overlap with MeshIntersection because the bvh uses indexed geometry */ + return res as MeshIntersection[] } public raycastFirst( ray: Ray, materialOrSide: Side | Material | Material[] = FrontSide - ): Intersection> { + ): MeshIntersection { const res = this._bvh.raycastFirst(this.transformInput(ray), materialOrSide) res.point = this.transformOutput(res.point) - return res + /** The intersection results from raycasting a bvh will always overlap with MeshIntersection because the bvh uses indexed geometry */ + return res as MeshIntersection } public shapecast( @@ -274,9 +278,10 @@ export class AccelerationStructure { return output.applyMatrix4(AccelerationStructure.MatBuff) as T } - public getBoundingBox(target) { - this._bvh.getBoundingBox(target) - return this.transformOutput(target) + public getBoundingBox(target?: Box3): Box3 { + const _target: Box3 = target ? target : new Box3() + this._bvh.getBoundingBox(_target) + return this.transformOutput(_target) } public getVertexAtIndex(index: number): Vector3 { diff --git a/packages/viewer/src/modules/objects/RotatablePMREMGenerator.ts b/packages/viewer/src/modules/objects/RotatablePMREMGenerator.ts index 9a11f1204a..9d9638f0d1 100644 --- a/packages/viewer/src/modules/objects/RotatablePMREMGenerator.ts +++ b/packages/viewer/src/modules/objects/RotatablePMREMGenerator.ts @@ -1,7 +1,13 @@ -import { Matrix4, NoBlending, PMREMGenerator, ShaderMaterial } from 'three' +import { + Matrix4, + NoBlending, + PMREMGenerator, + ShaderMaterial, + WebGLRenderer +} from 'three' export class RotatablePMREMGenerator extends PMREMGenerator { - constructor(renderer) { + constructor(renderer: WebGLRenderer) { super(renderer) } diff --git a/packages/viewer/src/modules/objects/SpeckleCamera.ts b/packages/viewer/src/modules/objects/SpeckleCamera.ts index e199ceaf8d..39e00cda30 100644 --- a/packages/viewer/src/modules/objects/SpeckleCamera.ts +++ b/packages/viewer/src/modules/objects/SpeckleCamera.ts @@ -1,10 +1,17 @@ import { Box3, OrthographicCamera, PerspectiveCamera } from 'three' export enum CameraEvent { - Stationary, - Dynamic, - FrameUpdate, - ProjectionChanged + Stationary = 'stationary', + Dynamic = 'dynamic', + FrameUpdate = 'frame-update', + ProjectionChanged = 'projection-changed' +} + +export interface CameraEventPayload { + [CameraEvent.Stationary]: void + [CameraEvent.Dynamic]: void + [CameraEvent.FrameUpdate]: boolean + [CameraEvent.ProjectionChanged]: CameraProjection } export interface SpeckleCamera { @@ -12,8 +19,11 @@ export interface SpeckleCamera { get fieldOfView(): number set fieldOfView(value: number) get aspect(): number - on(type: CameraEvent, handler: (...args) => void) - setCameraPlanes(targetVolume: Box3, offsetScale?: number) + on( + eventType: T, + listener: (arg: CameraEventPayload[T]) => void + ): void + setCameraPlanes(targetVolume: Box3, offsetScale?: number): void } export enum CameraProjection { PERSPECTIVE, diff --git a/packages/viewer/src/modules/objects/SpeckleCameraControls.ts b/packages/viewer/src/modules/objects/SpeckleCameraControls.ts index df1e1990d6..1c95ceffb9 100644 --- a/packages/viewer/src/modules/objects/SpeckleCameraControls.ts +++ b/packages/viewer/src/modules/objects/SpeckleCameraControls.ts @@ -1,52 +1,55 @@ import CameraControls from 'camera-controls' -import { MathUtils, PerspectiveCamera, Vector3 } from 'three' +import { MathUtils, OrthographicCamera, PerspectiveCamera, Vector3 } from 'three' -let ACTION -;(function (ACTION) { - ACTION[(ACTION['NONE'] = 0)] = 'NONE' - ACTION[(ACTION['ROTATE'] = 1)] = 'ROTATE' - ACTION[(ACTION['TRUCK'] = 2)] = 'TRUCK' - ACTION[(ACTION['OFFSET'] = 3)] = 'OFFSET' - ACTION[(ACTION['DOLLY'] = 4)] = 'DOLLY' - ACTION[(ACTION['ZOOM'] = 5)] = 'ZOOM' - ACTION[(ACTION['TOUCH_ROTATE'] = 6)] = 'TOUCH_ROTATE' - ACTION[(ACTION['TOUCH_TRUCK'] = 7)] = 'TOUCH_TRUCK' - ACTION[(ACTION['TOUCH_OFFSET'] = 8)] = 'TOUCH_OFFSET' - ACTION[(ACTION['TOUCH_DOLLY'] = 9)] = 'TOUCH_DOLLY' - ACTION[(ACTION['TOUCH_ZOOM'] = 10)] = 'TOUCH_ZOOM' - ACTION[(ACTION['TOUCH_DOLLY_TRUCK'] = 11)] = 'TOUCH_DOLLY_TRUCK' - ACTION[(ACTION['TOUCH_DOLLY_OFFSET'] = 12)] = 'TOUCH_DOLLY_OFFSET' - ACTION[(ACTION['TOUCH_ZOOM_TRUCK'] = 13)] = 'TOUCH_ZOOM_TRUCK' - ACTION[(ACTION['TOUCH_ZOOM_OFFSET'] = 14)] = 'TOUCH_ZOOM_OFFSET' -})(ACTION || (ACTION = {})) -function isPerspectiveCamera(camera) { - return camera.isPerspectiveCamera +enum ACTION { + NONE = 0, + ROTATE = 1, + TRUCK = 2, + OFFSET = 3, + DOLLY = 4, + ZOOM = 5, + TOUCH_ROTATE = 6, + TOUCH_TRUCK = 7, + TOUCH_OFFSET = 8, + TOUCH_DOLLY = 9, + TOUCH_ZOOM = 10, + TOUCH_DOLLY_TRUCK = 11, + TOUCH_DOLLY_OFFSET = 12, + TOUCH_ZOOM_TRUCK = 13, + TOUCH_ZOOM_OFFSET = 14 } -function isOrthographicCamera(camera) { - return camera.isOrthographicCamera + +function isPerspectiveCamera(camera: PerspectiveCamera | OrthographicCamera) { + return (camera as PerspectiveCamera).isPerspectiveCamera +} +function isOrthographicCamera(camera: PerspectiveCamera | OrthographicCamera) { + return (camera as OrthographicCamera).isOrthographicCamera } const EPSILON = 1e-5 -function approxZero(number, error = EPSILON) { +function approxZero(number: number, error = EPSILON) { return Math.abs(number) < error } -function approxEquals(a, b, error = EPSILON) { +function approxEquals(a: number, b: number, error = EPSILON) { return approxZero(a - b, error) } -let _deltaTarget, _deltaOffset, _v3A, _v3B, _v3C -let _xColumn -let _yColumn -let _zColumn +let _deltaTarget: Vector3, + _deltaOffset: Vector3, + _v3A: Vector3, + _v3B: Vector3, + _v3C: Vector3 +let _xColumn: Vector3 +let _yColumn: Vector3 +let _zColumn: Vector3 export class SpeckleCameraControls extends CameraControls { private _didDolly = false private _didDollyLastFrame = false public _isTrucking = false - private _hasRestedLastFrame = false private _didZoom = false - private overrideDollyLerpRatio = 0 - private overrideZoomLerpRatio = 0 + private overrideDollyLerpRatio: number | undefined = 0 + private overrideZoomLerpRatio: number | undefined = 0 static install() { _v3A = new Vector3() @@ -127,11 +130,7 @@ export class SpeckleCameraControls extends CameraControls { * @param enableTransition * @category Methods */ - zoomTo( - zoom: number, - enableTransition = false, - lerpRatio: number = undefined - ): Promise { + zoomTo(zoom: number, enableTransition = false, lerpRatio?: number): Promise { this._zoomEnd = MathUtils.clamp(zoom, this.minZoom, this.maxZoom) this._needsUpdate = true this.overrideZoomLerpRatio = enableTransition ? 0.05 : lerpRatio @@ -153,7 +152,7 @@ export class SpeckleCameraControls extends CameraControls { dollyTo( distance: number, enableTransition = true, - lerpRatio = undefined + lerpRatio?: number ): Promise { const lastRadius = this._sphericalEnd.radius const newRadius = MathUtils.clamp(distance, this.minDistance, this.maxDistance) @@ -193,8 +192,7 @@ export class SpeckleCameraControls extends CameraControls { return this._createOnRestPromise(resolveImmediately) } - update(delta) { - this._hasRestedLastFrame = this._hasRested + update(delta: number) { const dampingFactor = this._state === ACTION.NONE ? this.dampingFactor : this.draggingDampingFactor const lerpRatio = Math.min(dampingFactor * delta * 60, 1) diff --git a/packages/viewer/src/modules/objects/SpeckleInstancedMesh.ts b/packages/viewer/src/modules/objects/SpeckleInstancedMesh.ts index 75418eeac5..8c66d95bb7 100644 --- a/packages/viewer/src/modules/objects/SpeckleInstancedMesh.ts +++ b/packages/viewer/src/modules/objects/SpeckleInstancedMesh.ts @@ -1,5 +1,6 @@ import { BackSide, + BufferAttribute, BufferGeometry, DoubleSide, Group, @@ -7,23 +8,26 @@ import { InstancedMesh, Material, Matrix4, + Object3D, Ray, Raycaster, + SkinnedMesh, Sphere, Triangle, Vector2, - Vector3 + Vector3, + type Intersection } from 'three' import { BatchObject } from '../batching/BatchObject' import Materials from '../materials/Materials' import { TopLevelAccelerationStructure } from './TopLevelAccelerationStructure' +import { ObjectLayers } from '../../IViewer' +import Logger from 'js-logger' import { - DrawGroup, + type DrawGroup, INSTANCE_GRADIENT_BUFFER_STRIDE, INSTANCE_TRANSFORM_BUFFER_STRIDE } from '../batching/Batch' -import { ObjectLayers } from '../../IViewer' -import Logger from 'js-logger' const _inverseMatrix = new Matrix4() const _ray = new Ray() @@ -54,20 +58,20 @@ const tmpInverseMatrix = /* @__PURE__ */ new Matrix4() export default class SpeckleInstancedMesh extends Group { public static MeshBatchNumber = 0 - private tas: TopLevelAccelerationStructure = null - private batchMaterial: Material = null + private tas: TopLevelAccelerationStructure + private batchMaterial: Material | null = null private materialCache: { [id: string]: Material } = {} - private materialStack: Array = [] + private materialStack: Array> = [] private materialCacheLUT: { [id: string]: number } = {} - private _batchObjects: BatchObject[] + private _batchObjects!: BatchObject[] public groups: Array = [] public materials: Material[] = [] - private instanceGeometry: BufferGeometry = null + private instanceGeometry: BufferGeometry | undefined = undefined private instances: InstancedMesh[] = [] - public get TAS() { + public get TAS(): TopLevelAccelerationStructure { return this.tas } @@ -92,8 +96,7 @@ export default class SpeckleInstancedMesh extends Group { } public setOverrideMaterial(material: Material) { - material - const saveMaterials = [] + const saveMaterials: Array = [] for (let k = 0; k < this.instances.length; k++) { saveMaterials.push(this.instances[k].material) } @@ -118,7 +121,11 @@ export default class SpeckleInstancedMesh extends Group { this.materialCacheLUT[clone.id] = material.id cachedMaterial = clone this.updateMaterialTransformsUniform(this.materialCache[material.id]) - } else if (copy || material['needsCopy'] || cachedMaterial['needsCopy']) { + } else if ( + copy || + (material as never)['needsCopy'] || + (cachedMaterial as never)['needsCopy'] + ) { Materials.fastCopy(material, cachedMaterial) } return cachedMaterial @@ -149,7 +156,8 @@ export default class SpeckleInstancedMesh extends Group { this.instances.length = 0 for (let k = 0; k < this.groups.length; k++) { - const material = this.materials[this.groups[k].materialIndex] + const materialIndex = this.groups[k].materialIndex + const material = this.materials[materialIndex] const group = new InstancedMesh(this.instanceGeometry, material, 0) group.instanceMatrix = new InstancedBufferAttribute( transformBuffer.subarray( @@ -231,7 +239,11 @@ export default class SpeckleInstancedMesh extends Group { // converts the given BVH raycast intersection to align with the three.js raycast // structure (include object, world space distance and point). - private convertRaycastIntersect(hit, object, raycaster) { + private convertRaycastIntersect( + hit: Intersection | null, + object: Object3D, + raycaster: Raycaster + ) { if (hit === null) { return null } @@ -247,9 +259,9 @@ export default class SpeckleInstancedMesh extends Group { } } - raycast(raycaster: Raycaster, intersects) { + raycast(raycaster: Raycaster, intersects: Array) { if (this.tas) { - if (this.batchMaterial === undefined) return + if (!this.batchMaterial) return tmpInverseMatrix.copy(this.matrixWorld).invert() ray.copy(raycaster.ray).applyMatrix4(tmpInverseMatrix) @@ -278,12 +290,13 @@ export default class SpeckleInstancedMesh extends Group { const matrixWorld = this.matrixWorld if (material === undefined) return + if (geometry === undefined) return // Checking boundingSphere distance to ray if (geometry.boundingSphere === null) geometry.computeBoundingSphere() - _sphere.copy(geometry.boundingSphere) + _sphere.copy(geometry.boundingSphere || new Sphere()) _sphere.applyMatrix4(matrixWorld) if (raycaster.ray.intersectsSphere(_sphere) === false) return @@ -303,13 +316,13 @@ export default class SpeckleInstancedMesh extends Group { const index = geometry.index /** Stored high component if RTE is being used. Regular positions otherwise */ - const position = geometry.attributes.position + const position = geometry.attributes.position as BufferAttribute /** Stored low component if RTE is being used. undefined otherwise */ - const positionLow = geometry.attributes['position_low'] - const morphPosition = geometry.morphAttributes.position + const positionLow = geometry.attributes['position_low'] as BufferAttribute + const morphPosition = geometry.morphAttributes.position as Array const morphTargetsRelative = geometry.morphTargetsRelative - const uv = geometry.attributes.uv - const uv2 = geometry.attributes.uv2 + const uv = geometry.attributes.uv as BufferAttribute + const uv2 = geometry.attributes.uv2 as BufferAttribute const groups = geometry.groups const drawRange = geometry.drawRange @@ -319,6 +332,10 @@ export default class SpeckleInstancedMesh extends Group { if (Array.isArray(material)) { for (let i = 0, il = groups.length; i < il; i++) { const group = groups[i] + if (!group.materialIndex) { + Logger.error(`Group with no material, skipping!`) + continue + } const groupMaterial = material[group.materialIndex] const start = Math.max(group.start, drawRange.start) @@ -350,7 +367,8 @@ export default class SpeckleInstancedMesh extends Group { if (intersection) { intersection.faceIndex = Math.floor(j / 3) // triangle number in indexed buffer semantics - intersection.face.materialIndex = group.materialIndex + if (intersection.face) + intersection.face.materialIndex = group.materialIndex as number intersects.push(intersection) } } @@ -392,6 +410,10 @@ export default class SpeckleInstancedMesh extends Group { if (Array.isArray(material)) { for (let i = 0, il = groups.length; i < il; i++) { const group = groups[i] + if (!group.materialIndex) { + Logger.error(`Group with no material, skipping!`) + continue + } const groupMaterial = material[group.materialIndex] const start = Math.max(group.start, drawRange.start) @@ -423,7 +445,8 @@ export default class SpeckleInstancedMesh extends Group { if (intersection) { intersection.faceIndex = Math.floor(j / 3) // triangle number in non-indexed buffer semantics - intersection.face.materialIndex = group.materialIndex + if (intersection.face) + intersection.face.materialIndex = group.materialIndex as number intersects.push(intersection) } } @@ -464,7 +487,16 @@ export default class SpeckleInstancedMesh extends Group { } } -function checkIntersection(object, material, raycaster, ray, pA, pB, pC, point) { +function checkIntersection( + object: Object3D, + material: Material, + raycaster: Raycaster, + ray: Ray, + pA: Vector3, + pB: Vector3, + pC: Vector3, + point: Vector3 +): (Intersection & { uv2: Vector2 | undefined }) | null { let intersect if (material.side === BackSide) { @@ -488,7 +520,8 @@ function checkIntersection(object, material, raycaster, ray, pA, pB, pC, point) object, uv: undefined, uv2: undefined, - face: undefined + face: undefined, + faceIndex: undefined } } @@ -496,19 +529,19 @@ function checkIntersection(object, material, raycaster, ray, pA, pB, pC, point) * hold the default `position` attribute values */ function checkBufferGeometryIntersection( - object, - material, - raycaster, - ray, - positionLow, - positionHigh, - morphPosition, - morphTargetsRelative, - uv, - uv2, - a, - b, - c + object: Object3D, + material: Material, + raycaster: Raycaster, + ray: Ray, + positionLow: BufferAttribute, + positionHigh: BufferAttribute, + morphPosition: Array, + morphTargetsRelative: boolean, + uv: BufferAttribute, + uv2: BufferAttribute, + a: number, + b: number, + c: number ) { _vA.fromBufferAttribute(positionHigh, a) _vB.fromBufferAttribute(positionHigh, b) @@ -519,7 +552,7 @@ function checkBufferGeometryIntersection( _vC.add(_vTemp.fromBufferAttribute(positionLow, c)) } - const morphInfluences = object.morphTargetInfluences + const morphInfluences = (object as SkinnedMesh).morphTargetInfluences if (morphPosition && morphInfluences) { _morphA.set(0, 0, 0) @@ -552,10 +585,10 @@ function checkBufferGeometryIntersection( _vC.add(_morphC) } - if (object.isSkinnedMesh) { - object.boneTransform(a, _vA) - object.boneTransform(b, _vB) - object.boneTransform(c, _vC) + if ((object as SkinnedMesh).isSkinnedMesh) { + ;(object as SkinnedMesh).boneTransform(a, _vA) + ;(object as SkinnedMesh).boneTransform(b, _vB) + ;(object as SkinnedMesh).boneTransform(c, _vC) } const intersection = checkIntersection( diff --git a/packages/viewer/src/modules/objects/SpeckleMesh.ts b/packages/viewer/src/modules/objects/SpeckleMesh.ts index d128cbcf17..7bf2565f76 100644 --- a/packages/viewer/src/modules/objects/SpeckleMesh.ts +++ b/packages/viewer/src/modules/objects/SpeckleMesh.ts @@ -1,7 +1,9 @@ import Logger from 'js-logger' import { BackSide, + Box3, Box3Helper, + BufferAttribute, BufferGeometry, Color, DataTexture, @@ -10,13 +12,16 @@ import { Material, Matrix4, Mesh, + Object3D, Ray, Raycaster, RGBAFormat, + SkinnedMesh, Sphere, Triangle, Vector2, - Vector3 + Vector3, + type Intersection } from 'three' import { BatchObject } from '../batching/BatchObject' import Materials from '../materials/Materials' @@ -57,23 +62,23 @@ export enum TransformStorage { export default class SpeckleMesh extends Mesh { public static MeshBatchNumber = 0 - private tas: TopLevelAccelerationStructure = null - private batchMaterial: Material = null + private tas: TopLevelAccelerationStructure + private batchMaterial: Material private materialCache: { [id: string]: Material } = {} private materialStack: Array = [] private materialCacheLUT: { [id: string]: number } = {} - private _batchObjects: BatchObject[] - private transformsBuffer: Float32Array = null - private transformStorage: TransformStorage + private _batchObjects!: BatchObject[] + private transformsBuffer: Float32Array | undefined = undefined + private transformStorage!: TransformStorage - public transformsTextureUniform: DataTexture = null - public transformsArrayUniforms: Matrix4[] = null + public transformsTextureUniform: DataTexture + public transformsArrayUniforms: Matrix4[] | null = null private debugBatchBox = false - private boxHelper: Box3Helper + private boxHelper!: Box3Helper - public get TAS() { + public get TAS(): TopLevelAccelerationStructure { return this.tas } @@ -134,17 +139,23 @@ export default class SpeckleMesh extends Mesh { this.materialCacheLUT[clone.id] = material.id cachedMaterial = clone this.updateMaterialTransformsUniform(this.materialCache[material.id]) - } else if (copy || material['needsCopy'] || cachedMaterial['needsCopy']) { + } else if ( + copy || + (material as never)['needsCopy'] || + (cachedMaterial as never)['needsCopy'] + ) { Materials.fastCopy(material, cachedMaterial) } return cachedMaterial } public restoreMaterial() { - if (this.materialStack.length > 0) this.material = this.materialStack.pop() + if (this.materialStack.length > 0) + this.material = this.materialStack.pop() as Material | Material[] } public updateMaterialTransformsUniform(material: Material) { + if (!material.defines) material.defines = {} material.defines['TRANSFORM_STORAGE'] = this.transformStorage if (this.transformStorage === TransformStorage.VERTEX_TEXTURE) { @@ -165,6 +176,7 @@ export default class SpeckleMesh extends Mesh { } public updateTransformsUniform() { + if (!this.transformsBuffer) return let needsUpdate = false if (this.transformStorage === TransformStorage.VERTEX_TEXTURE) { for (let k = 0; k < this._batchObjects.length; k++) { @@ -196,6 +208,7 @@ export default class SpeckleMesh extends Mesh { } this.transformsTextureUniform.needsUpdate = needsUpdate } else { + if (!this.transformsArrayUniforms) return for (let k = 0; k < this._batchObjects.length; k++) { const batchObject = this._batchObjects[k] if (!(needsUpdate ||= batchObject.transformDirty)) continue @@ -223,12 +236,17 @@ export default class SpeckleMesh extends Mesh { if (this.tas && needsUpdate) { this.tas.refit() this.tas.getBoundingBox(this.tas.bounds) + /** Caterint to typescript + * There is no unniverse where the geomery bounding box/sphere is null at this point + */ + if (!this.geometry.boundingBox) this.geometry.boundingBox = new Box3() this.geometry.boundingBox.copy(this.tas.bounds) + if (!this.geometry.boundingSphere) this.geometry.boundingSphere = new Sphere() this.geometry.boundingBox.getBoundingSphere(this.geometry.boundingSphere) if (!this.boxHelper && this.debugBatchBox) { this.boxHelper = new Box3Helper(this.tas.bounds, new Color(0xff0000)) this.boxHelper.layers.set(ObjectLayers.PROPS) - this.parent.add(this.boxHelper) + if (this.parent) this.parent.add(this.boxHelper) } } } @@ -258,13 +276,17 @@ export default class SpeckleMesh extends Mesh { ) return null } - return this.material[group.materialIndex] + return this.material[group.materialIndex as number] } } // converts the given BVH raycast intersection to align with the three.js raycast // structure (include object, world space distance and point). - private convertRaycastIntersect(hit, object, raycaster) { + private convertRaycastIntersect( + hit: Intersection | null, + object: Object3D, + raycaster: Raycaster + ) { if (hit === null) { return null } @@ -280,7 +302,7 @@ export default class SpeckleMesh extends Mesh { } } - raycast(raycaster: Raycaster, intersects) { + raycast(raycaster: Raycaster, intersects: Array) { if (this.tas) { if (this.batchMaterial === undefined) return @@ -316,7 +338,7 @@ export default class SpeckleMesh extends Mesh { if (geometry.boundingSphere === null) geometry.computeBoundingSphere() - _sphere.copy(geometry.boundingSphere) + _sphere.copy(geometry.boundingSphere || new Sphere()) _sphere.applyMatrix4(matrixWorld) if (raycaster.ray.intersectsSphere(_sphere) === false) return @@ -336,13 +358,13 @@ export default class SpeckleMesh extends Mesh { const index = geometry.index /** Stored high component if RTE is being used. Regular positions otherwise */ - const position = geometry.attributes.position + const position = geometry.attributes.position as BufferAttribute /** Stored low component if RTE is being used. undefined otherwise */ - const positionLow = geometry.attributes['position_low'] - const morphPosition = geometry.morphAttributes.position + const positionLow = geometry.attributes['position_low'] as BufferAttribute + const morphPosition = geometry.morphAttributes.position as Array const morphTargetsRelative = geometry.morphTargetsRelative - const uv = geometry.attributes.uv - const uv2 = geometry.attributes.uv2 + const uv = geometry.attributes.uv as BufferAttribute + const uv2 = geometry.attributes.uv2 as BufferAttribute const groups = geometry.groups const drawRange = geometry.drawRange @@ -352,6 +374,10 @@ export default class SpeckleMesh extends Mesh { if (Array.isArray(material)) { for (let i = 0, il = groups.length; i < il; i++) { const group = groups[i] + if (!group.materialIndex) { + Logger.error(`Group with no material, skipping!`) + continue + } const groupMaterial = material[group.materialIndex] const start = Math.max(group.start, drawRange.start) @@ -383,7 +409,8 @@ export default class SpeckleMesh extends Mesh { if (intersection) { intersection.faceIndex = Math.floor(j / 3) // triangle number in indexed buffer semantics - intersection.face.materialIndex = group.materialIndex + if (intersection.face) + intersection.face.materialIndex = group.materialIndex as number intersects.push(intersection) } } @@ -425,7 +452,7 @@ export default class SpeckleMesh extends Mesh { if (Array.isArray(material)) { for (let i = 0, il = groups.length; i < il; i++) { const group = groups[i] - const groupMaterial = material[group.materialIndex] + const groupMaterial = material[group.materialIndex as number] const start = Math.max(group.start, drawRange.start) const end = Math.min( @@ -456,7 +483,8 @@ export default class SpeckleMesh extends Mesh { if (intersection) { intersection.faceIndex = Math.floor(j / 3) // triangle number in non-indexed buffer semantics - intersection.face.materialIndex = group.materialIndex + if (intersection.face) + intersection.face.materialIndex = group.materialIndex as number intersects.push(intersection) } } @@ -497,7 +525,16 @@ export default class SpeckleMesh extends Mesh { } } -function checkIntersection(object, material, raycaster, ray, pA, pB, pC, point) { +function checkIntersection( + object: Object3D, + material: Material, + raycaster: Raycaster, + ray: Ray, + pA: Vector3, + pB: Vector3, + pC: Vector3, + point: Vector3 +): (Intersection & { uv2: Vector2 | undefined }) | null { let intersect if (material.side === BackSide) { @@ -529,19 +566,19 @@ function checkIntersection(object, material, raycaster, ray, pA, pB, pC, point) * hold the default `position` attribute values */ function checkBufferGeometryIntersection( - object, - material, - raycaster, - ray, - positionLow, - positionHigh, - morphPosition, - morphTargetsRelative, - uv, - uv2, - a, - b, - c + object: Object3D, + material: Material, + raycaster: Raycaster, + ray: Ray, + positionLow: BufferAttribute, + positionHigh: BufferAttribute, + morphPosition: Array, + morphTargetsRelative: boolean, + uv: BufferAttribute, + uv2: BufferAttribute, + a: number, + b: number, + c: number ) { _vA.fromBufferAttribute(positionHigh, a) _vB.fromBufferAttribute(positionHigh, b) @@ -552,7 +589,7 @@ function checkBufferGeometryIntersection( _vC.add(_vTemp.fromBufferAttribute(positionLow, c)) } - const morphInfluences = object.morphTargetInfluences + const morphInfluences = (object as SkinnedMesh).morphTargetInfluences if (morphPosition && morphInfluences) { _morphA.set(0, 0, 0) @@ -585,10 +622,10 @@ function checkBufferGeometryIntersection( _vC.add(_morphC) } - if (object.isSkinnedMesh) { - object.boneTransform(a, _vA) - object.boneTransform(b, _vB) - object.boneTransform(c, _vC) + if ((object as SkinnedMesh).isSkinnedMesh) { + ;(object as SkinnedMesh).boneTransform(a, _vA) + ;(object as SkinnedMesh).boneTransform(b, _vB) + ;(object as SkinnedMesh).boneTransform(c, _vC) } const intersection = checkIntersection( diff --git a/packages/viewer/src/modules/objects/SpeckleRaycaster.ts b/packages/viewer/src/modules/objects/SpeckleRaycaster.ts index c9a223a26a..f94912ab76 100644 --- a/packages/viewer/src/modules/objects/SpeckleRaycaster.ts +++ b/packages/viewer/src/modules/objects/SpeckleRaycaster.ts @@ -1,7 +1,17 @@ -import { Box3, Intersection, Material, Object3D, Raycaster } from 'three' +import { + Box3, + type Intersection, + Object3D, + Raycaster, + Vector3, + RaycasterParameters, + Face +} from 'three' import { ExtendedTriangle, ShapecastIntersection } from 'three-mesh-bvh' import { BatchObject } from '../batching/BatchObject' import { ObjectLayers } from '../../IViewer' +import SpeckleMesh from './SpeckleMesh' +import SpeckleInstancedMesh from './SpeckleInstancedMesh' export type ExtendedShapeCastCallbacks = { intersectsTAS?: ( @@ -45,13 +55,29 @@ export type ExtendedShapeCastCallbacks = { export interface ExtendedIntersection extends Intersection { batchObject?: BatchObject - material?: Material + pointOnLine?: Vector3 + // material?: Material +} + +export interface MeshIntersection extends Intersection { + face: Face + faceIndex: number +} + +export interface ExtendedMeshIntersection extends MeshIntersection { + batchObject: BatchObject + object: SpeckleMesh | SpeckleInstancedMesh +} + +export interface ExtendedRaycasterParameters extends RaycasterParameters { + Line2: { threshold: number } } export class SpeckleRaycaster extends Raycaster { - public onObjectIntersectionTest: (object: Object3D) => void = null + public onObjectIntersectionTest: ((object: Object3D) => void) | null = null + public params: ExtendedRaycasterParameters - constructor(origin?, direction?, near = 0, far = Infinity) { + constructor(origin?: Vector3, direction?: Vector3, near = 0, far = Infinity) { super(origin, direction, near, far) this.layers.disableAll() this.layers.enable(ObjectLayers.STREAM_CONTENT) @@ -61,9 +87,10 @@ export class SpeckleRaycaster extends Raycaster { this.layers.enable(ObjectLayers.STREAM_CONTENT_POINT_CLOUD) // OFF by default this.layers.enable(ObjectLayers.STREAM_CONTENT_POINT) + this.params = { Line2: { threshold: 0 } } } - public intersectObjects(objects, recursive = true, intersects = []) { + public intersectObjects(objects: Array, recursive = true, intersects = []) { for (let i = 0, l = objects.length; i < l; i++) { intersectObject(objects[i], this, intersects, recursive) } @@ -74,11 +101,16 @@ export class SpeckleRaycaster extends Raycaster { } } -function ascSort(a, b) { +function ascSort(a: Intersection, b: Intersection) { return a.distance - b.distance } -function intersectObject(object, raycaster, intersects, recursive) { +function intersectObject( + object: Object3D, + raycaster: SpeckleRaycaster, + intersects: Array, + recursive: boolean +) { if (object.layers.test(raycaster.layers)) { if (raycaster.onObjectIntersectionTest) { raycaster.onObjectIntersectionTest(object) @@ -87,7 +119,9 @@ function intersectObject(object, raycaster, intersects, recursive) { } recursive &&= // eslint-disable-next-line eqeqeq - object.userData.raycastChildren != undefined ? object.raycastChildren : true + object.userData.raycastChildren != undefined + ? object.userData.raycastChildren + : true if (recursive === true) { const children = object.children diff --git a/packages/viewer/src/modules/objects/SpeckleText.ts b/packages/viewer/src/modules/objects/SpeckleText.ts index 6610d60c03..589522f15a 100644 --- a/packages/viewer/src/modules/objects/SpeckleText.ts +++ b/packages/viewer/src/modules/objects/SpeckleText.ts @@ -8,14 +8,17 @@ import { MeshBasicMaterial, PlaneGeometry, Quaternion, + Raycaster, Vector2, Vector3, - Vector4 + Vector4, + type Intersection } from 'three' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +//@ts-ignore import { Text } from 'troika-three-text' -import { SpeckleObject } from '../tree/DataTree' import SpeckleBasicMaterial from '../materials/SpeckleBasicMaterial' -import { ObjectLayers } from '../../IViewer' +import { ObjectLayers, type SpeckleObject } from '../../IViewer' export interface SpeckleTextParams { textValue?: string @@ -26,7 +29,7 @@ export interface SpeckleTextParams { } export interface SpeckleTextStyle { - backgroundColor?: Color + backgroundColor?: Color | null backgroundCornerRadius?: number backgroundPixelHeight?: number textColor?: Color @@ -44,7 +47,7 @@ const DefaultSpeckleTextStyle: SpeckleTextStyle = { export class SpeckleText extends Mesh { private _layer: ObjectLayers = ObjectLayers.NONE private _text: Text = null - private _background: Mesh = null + private _background: Mesh | null = null private _backgroundSize: Vector3 = new Vector3() private _style: SpeckleTextStyle = Object.assign({}, DefaultSpeckleTextStyle) private _resolution: Vector2 = new Vector2() @@ -136,9 +139,11 @@ export class SpeckleText extends Mesh { if (position) { if (this._style.billboard) { this.textMesh.material.userData.billboardPos.value.copy(position) - ;( - this._background.material as SpeckleBasicMaterial - ).userData.billboardPos.value.copy(position) + if (this._background) { + ;( + this._background.material as SpeckleBasicMaterial + ).userData.billboardPos.value.copy(position) + } } this.position.copy(position) } @@ -146,7 +151,7 @@ export class SpeckleText extends Mesh { if (scale) this.scale.copy(scale) } - public raycast(raycaster, intersects) { + public raycast(raycaster: Raycaster, intersects: Array) { const { textRenderInfo, curveRadius } = this.textMesh if (textRenderInfo) { const bounds = textRenderInfo.blockBounds @@ -206,7 +211,7 @@ export class SpeckleText extends Mesh { raycastMesh.matrixWorld = this.textMesh.matrixWorld } raycastMesh.material.side = this.textMesh.material.side - const tempArray = [] + const tempArray: Array = [] raycastMesh.raycast(raycaster, tempArray) for (let i = 0; i < tempArray.length; i++) { tempArray[i].object = this @@ -221,7 +226,7 @@ export class SpeckleText extends Mesh { private updateBackground() { if (!this._style.backgroundColor) { - this.remove(this._background) + if (this._background) this.remove(this._background) this._background = null return } @@ -251,7 +256,9 @@ export class SpeckleText extends Mesh { const color = new Color(this._style.backgroundColor).convertSRGBToLinear() ;(this._background.material as SpeckleBasicMaterial).color = color ;(this._background.material as SpeckleBasicMaterial).billboardPixelHeight = - this._style.backgroundPixelHeight * window.devicePixelRatio + (this._style.backgroundPixelHeight !== undefined + ? this._style.backgroundPixelHeight + : DefaultSpeckleTextStyle.backgroundPixelHeight || 0) * window.devicePixelRatio } /** From https://discourse.threejs.org/t/roundedrectangle-squircle/28645 */ @@ -282,7 +289,7 @@ export class SpeckleText extends Mesh { return geometry - function contour(j) { + function contour(j: number) { qu = Math.trunc((4 * j) / n) + 1 // quadrant qu: 1..4 sgx = qu === 1 || qu === 4 ? 1 : -1 // signum left/right sgy = qu < 3 ? 1 : -1 // signum top / bottom diff --git a/packages/viewer/src/modules/objects/SpeckleWebGLRenderer.ts b/packages/viewer/src/modules/objects/SpeckleWebGLRenderer.ts index 2a08fe2b6d..c5af9c90a4 100644 --- a/packages/viewer/src/modules/objects/SpeckleWebGLRenderer.ts +++ b/packages/viewer/src/modules/objects/SpeckleWebGLRenderer.ts @@ -1,7 +1,7 @@ import { Camera, Matrix4, Vector3, WebGLRenderer } from 'three' import { Geometry } from '../converter/Geometry' export class RTEBuffers { - private _cache: RTEBuffers = null + private _cache: RTEBuffers | undefined viewer: Vector3 = new Vector3() viewerLow: Vector3 = new Vector3() diff --git a/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts b/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts index 6d2b2f08df..69cbb961f7 100644 --- a/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts +++ b/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts @@ -1,19 +1,22 @@ import { Box3, Box3Helper, + BufferAttribute, Color, FrontSide, - Intersection, Material, Matrix4, - Object3D, Ray, Side, Vector3 } from 'three' import { ExtendedTriangle } from 'three-mesh-bvh' import { BatchObject } from '../batching/BatchObject' -import { ExtendedIntersection, ExtendedShapeCastCallbacks } from './SpeckleRaycaster' +import type { + ExtendedMeshIntersection, + ExtendedShapeCastCallbacks, + MeshIntersection +} from './SpeckleRaycaster' import { ObjectLayers } from '../../IViewer' import { AccelerationStructure } from './AccelerationStructure' @@ -55,8 +58,7 @@ export class TopLevelAccelerationStructure { public bounds: Box3 = new Box3(new Vector3(0, 0, 0), new Vector3(0, 0, 0)) public boxHelpers: Box3Helper[] = [] - public accelerationStructure: AccelerationStructure = null - public lastRefitTime = 0 + public accelerationStructure: AccelerationStructure public constructor(batchObjects: BatchObject[]) { this.batchObjects = batchObjects @@ -66,7 +68,7 @@ export class TopLevelAccelerationStructure { private buildBVH() { const indices = [] - const vertices = new Float32Array( + const vertices: number[] = new Array( TopLevelAccelerationStructure.CUBE_VERTS * 3 * this.batchObjects.length ) let vertOffset = 0 @@ -99,7 +101,7 @@ export class TopLevelAccelerationStructure { this.accelerationStructure.outputOriginTransfom = new Matrix4() } - private updateVertArray(box: Box3, offset: number, outPositions: Float32Array) { + private updateVertArray(box: Box3, offset: number, outPositions: number[]) { outPositions[offset] = box.min.x outPositions[offset + 1] = box.min.y outPositions[offset + 2] = box.max.z @@ -134,38 +136,39 @@ export class TopLevelAccelerationStructure { } public refit() { - const start = performance.now() - const positions = this.accelerationStructure.geometry.attributes.position.array + const positions = this.accelerationStructure.geometry.attributes.position + .array as number[] const boxBuffer: Box3 = new Box3() for (let k = 0; k < this.batchObjects.length; k++) { const start = this.batchObjects[k].tasVertIndexStart const basBox = this.batchObjects[k].accelerationStructure.getBoundingBox(boxBuffer) - this.updateVertArray(basBox, start * 3, positions as Float32Array) + this.updateVertArray(basBox, start * 3, positions) if (TopLevelAccelerationStructure.debugBoxes) this.boxHelpers[k].box.copy(basBox) } this.accelerationStructure.bvh.refit() - this.lastRefitTime = performance.now() - start } /* Core Cast Functions */ public raycast( ray: Ray, materialOrSide: Side | Material | Material[] = FrontSide - ): ExtendedIntersection[] { - const res = [] + ): ExtendedMeshIntersection[] { + const res: ExtendedMeshIntersection[] = [] const rayBuff = new Ray() rayBuff.copy(ray) - const tasResults: Intersection[] = this.accelerationStructure.raycast( + const tasResults: MeshIntersection[] = this.accelerationStructure.raycast( rayBuff, materialOrSide ) if (!tasResults.length) return res - tasResults.forEach((tasRes: Intersection) => { - const vertIndex = - this.accelerationStructure.geometry.index.array[tasRes.faceIndex * 3] + /** The index buffer for the bvh's geometry will *never* be undefined as it uses indexed geometry */ + const indexBufferAttribute: BufferAttribute = this.accelerationStructure.geometry + .index as BufferAttribute + tasResults.forEach((tasRes: MeshIntersection) => { + const vertIndex = indexBufferAttribute.array[tasRes.faceIndex * 3] const batchObjectIndex = Math.trunc( vertIndex / TopLevelAccelerationStructure.CUBE_VERTS ) @@ -175,9 +178,13 @@ export class TopLevelAccelerationStructure { materialOrSide ) hits.forEach((hit) => { - ;(hit as ExtendedIntersection).batchObject = this.batchObjects[batchObjectIndex] + /** We're promoting the MeshIntersection to ExtendedMeshIntersection because + * now we know it's corresponding batch object + */ + const extendedHit: ExtendedMeshIntersection = hit as ExtendedMeshIntersection + extendedHit.batchObject = this.batchObjects[batchObjectIndex] + res.push(extendedHit) }) - res.push(...hits) }) return res @@ -186,30 +193,33 @@ export class TopLevelAccelerationStructure { public raycastFirst( ray: Ray, materialOrSide: Side | Material | Material[] = FrontSide - ): ExtendedIntersection { - const res = null + ): ExtendedMeshIntersection | null { const rayBuff = new Ray() rayBuff.copy(ray) - const tasRes: Intersection = this.accelerationStructure.raycastFirst( + const tasRes: MeshIntersection = this.accelerationStructure.raycastFirst( rayBuff, materialOrSide ) - if (!tasRes) return res + if (!tasRes) return null - const vertIndex = - this.accelerationStructure.geometry.index.array[tasRes.faceIndex * 3] + /** The index buffer for the bvh's geometry will *never* be undefined as it uses indexed geometry */ + const indexBufferAttribute: BufferAttribute = this.accelerationStructure.geometry + .index as BufferAttribute + const vertIndex = indexBufferAttribute.array[tasRes.faceIndex * 3] const batchObjectIndex = Math.trunc( vertIndex / TopLevelAccelerationStructure.CUBE_VERTS ) rayBuff.copy(ray) - const hits = this.batchObjects[batchObjectIndex].accelerationStructure.raycast( - rayBuff, - materialOrSide - ) - hits.forEach((hit) => { - ;(hit as ExtendedIntersection).batchObject = this.batchObjects[batchObjectIndex] - }) - res.push(...hits) + const hit: MeshIntersection = this.batchObjects[ + batchObjectIndex + ].accelerationStructure.raycastFirst(rayBuff, materialOrSide) + /** We're promoting the MeshIntersection to ExtendedMeshIntersection because + * now we know it's corresponding batch object + */ + const extendedHit: ExtendedMeshIntersection = hit as ExtendedMeshIntersection + extendedHit.batchObject = this.batchObjects[batchObjectIndex] + + return extendedHit } public shapecast(callbacks: ExtendedShapeCastCallbacks): boolean { @@ -254,12 +264,15 @@ export class TopLevelAccelerationStructure { let ret = false this.accelerationStructure.shapecast({ intersectsBounds: (box, isLeaf, score, depth, nodeIndex) => { - const res = callbacks.intersectsTAS(box, isLeaf, score, depth, nodeIndex) - return res + if (callbacks.intersectsTAS) + return callbacks.intersectsTAS(box, isLeaf, score, depth, nodeIndex) + return false }, intersectsRange: (triangleOffset: number) => { - const vertIndex = - this.accelerationStructure.geometry.index.array[triangleOffset * 3] + /** The index buffer for the bvh's geometry will *never* be undefined as it uses indexed geometry */ + const indexBufferAttribute: BufferAttribute = this.accelerationStructure + .geometry.index as BufferAttribute + const vertIndex = indexBufferAttribute.array[triangleOffset * 3] const batchObjectIndex = Math.trunc( vertIndex / TopLevelAccelerationStructure.CUBE_VERTS ) diff --git a/packages/viewer/src/modules/pipeline/ApplyAOPass.ts b/packages/viewer/src/modules/pipeline/ApplyAOPass.ts index 66243f6ac9..6e2d526f9b 100644 --- a/packages/viewer/src/modules/pipeline/ApplyAOPass.ts +++ b/packages/viewer/src/modules/pipeline/ApplyAOPass.ts @@ -1,22 +1,24 @@ import { AddEquation, - Camera, CustomBlending, DstAlphaFactor, DstColorFactor, NoBlending, + OrthographicCamera, + PerspectiveCamera, Scene, ShaderMaterial, Texture, + WebGLRenderer, ZeroFactor } from 'three' import { FullScreenQuad, Pass } from 'three/examples/jsm/postprocessing/Pass.js' import { speckleApplyAoFrag } from '../materials/shaders/speckle-apply-ao-frag' import { speckleApplyAoVert } from '../materials/shaders/speckle-apply-ao-vert' import { - InputColorTextureUniform, - InputColorInterpolateTextureUniform, - SpeckleProgressivePass, + type InputColorTextureUniform, + type InputColorInterpolateTextureUniform, + type SpeckleProgressivePass, RenderType } from './SpecklePass' @@ -68,7 +70,7 @@ export class ApplySAOPass extends Pass implements SpeckleProgressivePass { return 'APPLYSAO' } - get outputTexture(): Texture { + get outputTexture(): Texture | null { return null } @@ -94,7 +96,7 @@ export class ApplySAOPass extends Pass implements SpeckleProgressivePass { this.materialCopy.needsUpdate = true } - public update(scene: Scene, camera: Camera) { + public update(scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { scene camera this.materialCopy.defines['NUM_FRAMES'] = this.accumulatioFrames @@ -102,9 +104,7 @@ export class ApplySAOPass extends Pass implements SpeckleProgressivePass { this.materialCopy.needsUpdate = true } - render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive*/) { - writeBuffer - readBuffer + render(renderer: WebGLRenderer) { renderer.setRenderTarget(null) const rendereAutoClear = renderer.autoClear renderer.autoClear = false diff --git a/packages/viewer/src/modules/pipeline/ColorPass.ts b/packages/viewer/src/modules/pipeline/ColorPass.ts index c43d06e08f..394fd16f0b 100644 --- a/packages/viewer/src/modules/pipeline/ColorPass.ts +++ b/packages/viewer/src/modules/pipeline/ColorPass.ts @@ -1,19 +1,29 @@ -import { Camera, Color, Material, Scene, Texture } from 'three' -import { BaseSpecklePass, SpecklePass } from './SpecklePass' +import { + Camera, + Color, + Material, + OrthographicCamera, + PerspectiveCamera, + Scene, + Texture, + WebGLRenderTarget, + WebGLRenderer +} from 'three' +import { BaseSpecklePass, type SpecklePass } from './SpecklePass' export class ColorPass extends BaseSpecklePass implements SpecklePass { - private camera: Camera - private scene: Scene - private overrideMaterial: Material = null + private camera: Camera | null = null + private scene: Scene | null = null + private overrideMaterial: Material private _oldClearColor: Color = new Color() - private clearColor: Color = null + private clearColor: Color private clearAlpha = 0 private clearDepth = true - public onBeforeRenderOpauqe: () => void = null - public onAfterRenderOpaque: () => void = null - public onBeforeRenderTransparent: () => void = null - public onAfterRenderTransparent: () => void = null + public onBeforeRenderOpauqe: (() => void) | undefined = undefined + public onAfterRenderOpaque: (() => void) | undefined = undefined + public onBeforeRenderTransparent: (() => void) | undefined = undefined + public onAfterRenderTransparent: (() => void) | undefined = undefined public constructor() { super() @@ -21,20 +31,26 @@ export class ColorPass extends BaseSpecklePass implements SpecklePass { public get displayName(): string { return 'COLOR' } - public get outputTexture(): Texture { + public get outputTexture(): Texture | null { return null } - public update(scene: Scene, camera: Camera) { + public update(scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { this.camera = camera this.scene = scene } - render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) { + render( + renderer: WebGLRenderer, + _writeBuffer: WebGLRenderTarget, + readBuffer: WebGLRenderTarget + ) { + if (!this.camera || !this.scene) return + const oldAutoClear = renderer.autoClear renderer.autoClear = false - let oldClearAlpha, oldOverrideMaterial + let oldClearAlpha, oldOverrideMaterial!: Material | null if (this.overrideMaterial !== undefined) { oldOverrideMaterial = this.scene.overrideMaterial diff --git a/packages/viewer/src/modules/pipeline/CopyOutputPass.ts b/packages/viewer/src/modules/pipeline/CopyOutputPass.ts index 1de9884acd..eea087451e 100644 --- a/packages/viewer/src/modules/pipeline/CopyOutputPass.ts +++ b/packages/viewer/src/modules/pipeline/CopyOutputPass.ts @@ -1,10 +1,16 @@ -import { NoBlending, ShaderMaterial, Texture, UniformsUtils } from 'three' +import { + NoBlending, + ShaderMaterial, + Texture, + UniformsUtils, + WebGLRenderer +} from 'three' import { FullScreenQuad, Pass } from 'three/examples/jsm/postprocessing/Pass.js' import { CopyShader } from 'three/examples/jsm/shaders/CopyShader.js' import { speckleCopyOutputFrag } from '../materials/shaders/speckle-copy-output-frag' import { speckleCopyOutputVert } from '../materials/shaders/speckle-copy-output-vert' import { PipelineOutputType } from './Pipeline' -import { InputColorTextureUniform, SpecklePass } from './SpecklePass' +import type { InputColorTextureUniform, SpecklePass } from './SpecklePass' export class CopyOutputPass extends Pass implements SpecklePass { private fsQuad: FullScreenQuad @@ -40,13 +46,11 @@ export class CopyOutputPass extends Pass implements SpecklePass { return 'COPY-OUTPUT' } - get outputTexture(): Texture { + get outputTexture(): Texture | null { return null } - render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive*/) { - writeBuffer - readBuffer + render(renderer: WebGLRenderer) { renderer.setRenderTarget(null) const rendereAutoClear = renderer.autoClear renderer.autoClear = false diff --git a/packages/viewer/src/modules/pipeline/DepthPass.ts b/packages/viewer/src/modules/pipeline/DepthPass.ts index 26d7bfb516..4f643df593 100644 --- a/packages/viewer/src/modules/pipeline/DepthPass.ts +++ b/packages/viewer/src/modules/pipeline/DepthPass.ts @@ -12,10 +12,11 @@ import { Scene, Side, Texture, - WebGLRenderTarget + WebGLRenderTarget, + WebGLRenderer } from 'three' import SpeckleDepthMaterial from '../materials/SpeckleDepthMaterial' -import { BaseSpecklePass, SpecklePass } from './SpecklePass' +import { BaseSpecklePass, type SpecklePass } from './SpecklePass' export enum DepthType { PERSPECTIVE_DEPTH, @@ -30,15 +31,15 @@ export enum DepthSize { export class DepthPass extends BaseSpecklePass implements SpecklePass { private renderTarget: WebGLRenderTarget private renderTargetHalf: WebGLRenderTarget - private depthMaterial: SpeckleDepthMaterial = null + private depthMaterial: SpeckleDepthMaterial private depthBufferSize: DepthSize = DepthSize.FULL - private scene: Scene - private camera: Camera + private scene: Scene | null + private camera: Camera | null private colorBuffer: Color = new Color() - public onBeforeRender: () => void = null - public onAfterRender: () => void = null + public onBeforeRender: (() => void) | undefined = undefined + public onAfterRender: (() => void) | undefined = undefined get displayName(): string { return 'DEPTH' @@ -58,8 +59,16 @@ export class DepthPass extends BaseSpecklePass implements SpecklePass { public set depthType(value: DepthType) { if (value === DepthType.LINEAR_DEPTH) - this.depthMaterial.defines['LINEAR_DEPTH'] = ' ' - else delete this.depthMaterial.defines['LINEAR_DEPTH'] + if (this.depthMaterial.defines) { + /** Catering to typescript + * SpeckleDepthMaterial always has it's 'defines' defined + */ + this.depthMaterial.defines['LINEAR_DEPTH'] = ' ' + } else { + if (this.depthMaterial.defines) { + delete this.depthMaterial.defines['LINEAR_DEPTH'] + } + } this.depthMaterial.needsUpdate = true } @@ -107,7 +116,7 @@ export class DepthPass extends BaseSpecklePass implements SpecklePass { this.depthMaterial.clippingPlanes = planes } - public update(scene: Scene, camera: Camera) { + public update(scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { this.camera = camera this.scene = scene this.depthMaterial.userData.near.value = ( @@ -119,11 +128,10 @@ export class DepthPass extends BaseSpecklePass implements SpecklePass { this.depthMaterial.needsUpdate = true } - public render(renderer, writeBuffer, readBuffer) { - writeBuffer - readBuffer + public render(renderer: WebGLRenderer) { + if (!this.camera || !this.scene) return - this.onBeforeRender() + if (this.onBeforeRender) this.onBeforeRender() renderer.getClearColor(this.colorBuffer) const originalClearAlpha = renderer.getClearAlpha() const originalAutoClear = renderer.autoClear @@ -154,7 +162,7 @@ export class DepthPass extends BaseSpecklePass implements SpecklePass { renderer.autoClear = originalAutoClear renderer.setClearColor(this.colorBuffer) renderer.setClearAlpha(originalClearAlpha) - this.onAfterRender() + if (this.onAfterRender) this.onAfterRender() } public setSize(width: number, height: number) { diff --git a/packages/viewer/src/modules/pipeline/DynamicAOPass.ts b/packages/viewer/src/modules/pipeline/DynamicAOPass.ts index ba96eff665..ebf9cabdd5 100644 --- a/packages/viewer/src/modules/pipeline/DynamicAOPass.ts +++ b/packages/viewer/src/modules/pipeline/DynamicAOPass.ts @@ -1,5 +1,4 @@ import { - Camera, Color, NoBlending, OrthographicCamera, @@ -9,7 +8,8 @@ import { Texture, UniformsUtils, Vector2, - WebGLRenderTarget + WebGLRenderTarget, + WebGLRenderer } from 'three' import { FullScreenQuad, Pass } from 'three/examples/jsm/postprocessing/Pass.js' import { speckleSaoFrag } from '../materials/shaders/speckle-sao-frag' @@ -17,7 +17,7 @@ import { speckleSaoVert } from '../materials/shaders/speckle-sao-vert' import { SAOShader } from 'three/examples/jsm/shaders/SAOShader.js' import { DepthLimitedBlurShader } from 'three/examples/jsm/shaders/DepthLimitedBlurShader.js' import { BlurShaderUtils } from 'three/examples/jsm/shaders/DepthLimitedBlurShader.js' -import { +import type { InputDepthTextureUniform, InputNormalsTextureUniform, SpecklePass @@ -62,17 +62,17 @@ export const DefaultDynamicAOPassParams = { export class DynamicSAOPass extends Pass implements SpecklePass { private params: DynamicAOPassParams = DefaultDynamicAOPassParams private colorBuffer: Color = new Color() - private saoMaterial: ShaderMaterial = null - private vBlurMaterial: ShaderMaterial = null - private hBlurMaterial: ShaderMaterial = null - private saoRenderTarget: WebGLRenderTarget = null - private blurIntermediateRenderTarget: WebGLRenderTarget = null - private fsQuad: FullScreenQuad = null + private saoMaterial: ShaderMaterial + private vBlurMaterial: ShaderMaterial + private hBlurMaterial: ShaderMaterial + private saoRenderTarget: WebGLRenderTarget + private blurIntermediateRenderTarget: WebGLRenderTarget + private fsQuad: FullScreenQuad private _outputType: DynamicAOOutputType = DynamicAOOutputType.AO_BLURRED private outputScale = 0.5 - private prevStdDev: number - private prevNumSamples: number + private prevStdDev: number = 0 + private prevNumSamples: number = 0 public get displayName(): string { return 'SAO' @@ -163,7 +163,7 @@ export class DynamicSAOPass extends Pass implements SpecklePass { this.hBlurMaterial.needsUpdate = true } - public update(scene: Scene, camera: Camera) { + public update(_scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { if (this._outputType === DynamicAOOutputType.RECONSTRUCTED_NORMALS) { this.saoMaterial.defines['OUTPUT_RECONSTRUCTED_NORMALS'] = '' } else { @@ -260,7 +260,7 @@ export class DynamicSAOPass extends Pass implements SpecklePass { this.hBlurMaterial.needsUpdate = true } - public render(renderer) { + public render(renderer: WebGLRenderer) { // Rendering SAO texture renderer.getClearColor(this.colorBuffer) const originalClearAlpha = renderer.getClearAlpha() diff --git a/packages/viewer/src/modules/pipeline/NormalsPass.ts b/packages/viewer/src/modules/pipeline/NormalsPass.ts index a8dd4ddd53..180bb31cd2 100644 --- a/packages/viewer/src/modules/pipeline/NormalsPass.ts +++ b/packages/viewer/src/modules/pipeline/NormalsPass.ts @@ -4,24 +4,27 @@ import { DoubleSide, Material, NoBlending, + OrthographicCamera, + PerspectiveCamera, Plane, Scene, Texture, - WebGLRenderTarget + WebGLRenderTarget, + WebGLRenderer } from 'three' import SpeckleNormalMaterial from '../materials/SpeckleNormalMaterial' -import { BaseSpecklePass, SpecklePass } from './SpecklePass' +import { BaseSpecklePass, type SpecklePass } from './SpecklePass' export class NormalsPass extends BaseSpecklePass implements SpecklePass { private renderTarget: WebGLRenderTarget - private normalsMaterial: SpeckleNormalMaterial = null + private normalsMaterial: SpeckleNormalMaterial private scene: Scene private camera: Camera private colorBuffer: Color = new Color() - public onBeforeRender: () => void = null - public onAfterRender: () => void = null + public onBeforeRender: (() => void) | undefined = undefined + public onAfterRender: (() => void) | undefined = undefined get displayName(): string { return 'GEOMETRY-NORMALS' @@ -55,16 +58,13 @@ export class NormalsPass extends BaseSpecklePass implements SpecklePass { this.normalsMaterial.clippingPlanes = planes } - public update(scene: Scene, camera: Camera) { + public update(scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { this.camera = camera this.scene = scene } - public render(renderer, writeBuffer, readBuffer) { - writeBuffer - readBuffer - - this.onBeforeRender() + public render(renderer: WebGLRenderer) { + if (this.onBeforeRender) this.onBeforeRender() renderer.getClearColor(this.colorBuffer) const originalClearAlpha = renderer.getClearAlpha() const originalAutoClear = renderer.autoClear @@ -91,7 +91,7 @@ export class NormalsPass extends BaseSpecklePass implements SpecklePass { renderer.autoClear = originalAutoClear renderer.setClearColor(this.colorBuffer) renderer.setClearAlpha(originalClearAlpha) - this.onAfterRender() + if (this.onAfterRender) this.onAfterRender() } public setSize(width: number, height: number) { diff --git a/packages/viewer/src/modules/pipeline/OverlayPass.ts b/packages/viewer/src/modules/pipeline/OverlayPass.ts index b779942884..825df75c2c 100644 --- a/packages/viewer/src/modules/pipeline/OverlayPass.ts +++ b/packages/viewer/src/modules/pipeline/OverlayPass.ts @@ -1,12 +1,20 @@ -import { Camera, Scene, Texture } from 'three' -import { BaseSpecklePass, SpecklePass } from './SpecklePass' +import { + Camera, + OrthographicCamera, + PerspectiveCamera, + Scene, + Texture, + WebGLRenderTarget, + WebGLRenderer +} from 'three' +import { BaseSpecklePass, type SpecklePass } from './SpecklePass' export class OverlayPass extends BaseSpecklePass implements SpecklePass { - private camera: Camera - private scene: Scene + private camera!: Camera + private scene!: Scene - public onBeforeRender: () => void = null - public onAfterRender: () => void = null + public onBeforeRender: (() => void) | undefined = undefined + public onAfterRender: (() => void) | undefined = undefined public constructor() { super() @@ -14,16 +22,20 @@ export class OverlayPass extends BaseSpecklePass implements SpecklePass { public get displayName(): string { return 'OVERLAY' } - public get outputTexture(): Texture { + public get outputTexture(): Texture | null { return null } - public update(scene: Scene, camera: Camera) { + public update(scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { this.camera = camera this.scene = scene } - render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) { + render( + renderer: WebGLRenderer, + _writeBuffer: WebGLRenderTarget, + readBuffer: WebGLRenderTarget + ) { const oldAutoClear = renderer.autoClear renderer.autoClear = false this.applyLayers(this.camera) diff --git a/packages/viewer/src/modules/pipeline/Pipeline.ts b/packages/viewer/src/modules/pipeline/Pipeline.ts index 2783f13e51..8d48202647 100644 --- a/packages/viewer/src/modules/pipeline/Pipeline.ts +++ b/packages/viewer/src/modules/pipeline/Pipeline.ts @@ -1,8 +1,5 @@ import { DoubleSide, Plane, Side, Vector2, WebGLRenderer } from 'three' -import { - EffectComposer, - Pass -} from 'three/examples/jsm/postprocessing/EffectComposer.js' +import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js' import Batcher from '../batching/Batcher' import SpeckleRenderer from '../SpeckleRenderer' import { ApplySAOPass } from './ApplyAOPass' @@ -13,20 +10,21 @@ import { DefaultDynamicAOPassParams, DynamicSAOPass, DynamicAOOutputType, - DynamicAOPassParams, + type DynamicAOPassParams, NormalsType } from './DynamicAOPass' import { DefaultStaticAoPassParams, StaticAOPass, - StaticAoPassParams + type StaticAoPassParams } from './StaticAOPass' -import { RenderType, SpecklePass } from './SpecklePass' +import { BaseSpecklePass, RenderType, type SpecklePass } from './SpecklePass' import { ColorPass } from './ColorPass' import { StencilPass } from './StencilPass' import { StencilMaskPass } from './StencilMaskPass' import { OverlayPass } from './OverlayPass' import { ObjectLayers } from '../../IViewer' +import type { BatchUpdateRange } from '../batching/Batch' export enum PipelineOutputType { DEPTH_RGBA = 0, @@ -61,40 +59,42 @@ export const DefaultPipelineOptions: PipelineOptions = { } export class Pipeline { - private _renderer: WebGLRenderer = null - private _batcher: Batcher = null + private _renderer: WebGLRenderer + private _batcher: Batcher private _pipelineOptions: PipelineOptions = Object.assign({}, DefaultPipelineOptions) private _needsProgressive = false private _resetFrame = false - private _composer: EffectComposer = null - - private depthPass: DepthPass = null - private normalsPass: NormalsPass = null - private stencilPass: StencilPass = null - private renderPass: ColorPass = null - private stencilMaskPass: StencilMaskPass = null - private dynamicAoPass: DynamicSAOPass = null - private applySaoPass: ApplySAOPass = null - private copyOutputPass: CopyOutputPass = null - private staticAoPass: StaticAOPass = null - private overlayPass: OverlayPass = null + private _composer: EffectComposer + + private depthPass: DepthPass + private normalsPass: NormalsPass + private stencilPass: StencilPass + private renderPass: ColorPass + private stencilMaskPass: StencilMaskPass + private dynamicAoPass: DynamicSAOPass + private applySaoPass: ApplySAOPass + private copyOutputPass: CopyOutputPass + private staticAoPass: StaticAOPass + private overlayPass: OverlayPass private drawingSize: Vector2 = new Vector2() private _renderType: RenderType = RenderType.NORMAL private accumulationFrame = 0 - private onBeforePipelineRender = null - private onAfterPipelineRender = null + private onBeforePipelineRender: (() => void) | null = null + private onAfterPipelineRender: (() => void) | null = null public set pipelineOptions(options: Partial) { Object.assign(this._pipelineOptions, options) this.dynamicAoPass.setParams(options.dynamicAoParams) this.staticAoPass.setParams(options.staticAoParams) this.accumulationFrame = 0 - this.depthPass.depthSide = options.depthSide - this.applySaoPass.setAccumulationFrames(options.accumulationFrames) - this.staticAoPass.setAccumulationFrames(options.accumulationFrames) - this.pipelineOutput = options.pipelineOutput + if (options.depthSide) this.depthPass.depthSide = options.depthSide + if (options.accumulationFrames) { + this.applySaoPass.setAccumulationFrames(options.accumulationFrames) + this.staticAoPass.setAccumulationFrames(options.accumulationFrames) + } + if (options.pipelineOutput) this.pipelineOutput = options.pipelineOutput } public get pipelineOptions(): PipelineOptions { @@ -102,7 +102,7 @@ export class Pipeline { } public set pipelineOutput(outputType: PipelineOutputType) { - let pipeline = [] + let pipeline: Array = [] this.clearPipeline() switch (outputType) { case PipelineOutputType.FINAL: @@ -237,8 +237,11 @@ export class Pipeline { this._renderer = renderer this._batcher = batcher this._composer = new EffectComposer(renderer) - this._composer.readBuffer = null - this._composer.writeBuffer = null + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(this._composer as any).readBuffer = null + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(this._composer as any).writeBuffer = null } public configure() { @@ -270,7 +273,10 @@ export class Pipeline { ]) this.stencilMaskPass.setLayers([ObjectLayers.STREAM_CONTENT_MESH]) this.overlayPass.setLayers([ObjectLayers.OVERLAY, ObjectLayers.MEASUREMENTS]) - let restoreVisibility, opaque, stencil, depth + let restoreVisibility: Record, + opaque: Record, + stencil: Record, + depth: Record this.onBeforePipelineRender = () => { restoreVisibility = this._batcher.saveVisiblity() @@ -381,7 +387,7 @@ export class Pipeline { private setPipeline(pipeline: Array) { for (let k = 0; k < pipeline.length; k++) { - this._composer.addPass(pipeline[k] as unknown as Pass) + this._composer.addPass(pipeline[k] as BaseSpecklePass) } } @@ -397,6 +403,8 @@ export class Pipeline { } public update(renderer: SpeckleRenderer) { + if (!renderer.scene || !renderer.renderingCamera) return + this.stencilPass.update(renderer.scene, renderer.renderingCamera) this.renderPass.update(renderer.scene, renderer.renderingCamera) this.stencilMaskPass.update(renderer.scene, renderer.renderingCamera) @@ -413,7 +421,7 @@ export class Pipeline { public render(): boolean { this._renderer.getDrawingBufferSize(this.drawingSize) - if (this.drawingSize.length() === 0) return + if (this.drawingSize.length() === 0) return false if (this.onBeforePipelineRender) this.onBeforePipelineRender() diff --git a/packages/viewer/src/modules/pipeline/ShadowcatcherPass.ts b/packages/viewer/src/modules/pipeline/ShadowcatcherPass.ts index 5e9738b065..be1fd89ebb 100644 --- a/packages/viewer/src/modules/pipeline/ShadowcatcherPass.ts +++ b/packages/viewer/src/modules/pipeline/ShadowcatcherPass.ts @@ -26,9 +26,13 @@ import { } from 'three/examples/jsm/shaders/DepthLimitedBlurShader.js' import SpeckleDepthMaterial from '../materials/SpeckleDepthMaterial' import SpeckleShadowcatcherMaterial from '../materials/SpeckleShadowcatcherMaterial' -import { BaseSpecklePass, SpecklePass } from './SpecklePass' +import { BaseSpecklePass, type SpecklePass } from './SpecklePass' import { ObjectLayers } from '../../IViewer' -import { DefaultShadowcatcherConfig, ShadowcatcherConfig } from '../ShadowcatcherConfig' +import { + DefaultShadowcatcherConfig, + type ShadowcatcherConfig +} from '../ShadowcatcherConfig' +import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' export class ShadowcatcherPass extends BaseSpecklePass implements SpecklePass { private readonly levels: number = 4 @@ -36,23 +40,23 @@ export class ShadowcatcherPass extends BaseSpecklePass implements SpecklePass { private renderTargets: WebGLRenderTarget[] = [] private tempTargets: WebGLRenderTarget[] = [] private outputTarget: WebGLRenderTarget - private camera: OrthographicCamera = null - private scene: Scene = null + private camera: OrthographicCamera + private scene!: Scene private _needsUpdate = false - private fsQuad: FullScreenQuad = null - private blendMaterial: SpeckleShadowcatcherMaterial = null - private depthMaterial: SpeckleDepthMaterial = null - private vBlurMaterial: ShaderMaterial = null - private hBlurMaterial: ShaderMaterial = null + private fsQuad: FullScreenQuad + private blendMaterial: SpeckleShadowcatcherMaterial + private depthMaterial: SpeckleDepthMaterial + private vBlurMaterial: ShaderMaterial + private hBlurMaterial: ShaderMaterial private blurStdDev = DefaultShadowcatcherConfig.stdDeviation private blurRadius = DefaultShadowcatcherConfig.blurRadius private prevBlurStdDev = 0 private prevBlurRadius = 0 - private cameraHelper = null + private cameraHelper!: CameraHelper - public onBeforeRender: () => void = null - public onAfterRender: () => void = null + public onBeforeRender: (() => void) | undefined = undefined + public onAfterRender: (() => void) | undefined = undefined get displayName(): string { return 'Shadowcatcher' @@ -124,7 +128,7 @@ export class ShadowcatcherPass extends BaseSpecklePass implements SpecklePass { public update(scene: Scene) { this.scene = scene if (this._needsUpdate) { - if (this.cameraHelper === null && this.debugCamera) { + if (!this.cameraHelper && this.debugCamera) { this.cameraHelper = new CameraHelper(this.camera) this.cameraHelper.layers.set(ObjectLayers.PROPS) this.scene.add(this.cameraHelper) @@ -180,9 +184,7 @@ export class ShadowcatcherPass extends BaseSpecklePass implements SpecklePass { } } - public render(renderer, writeBuffer, readBuffer) { - writeBuffer - readBuffer + public render(renderer: SpeckleWebGLRenderer) { if (this._needsUpdate) { renderer.RTEBuffers.push() renderer.updateRTEViewModel(this.camera) diff --git a/packages/viewer/src/modules/pipeline/SpecklePass.ts b/packages/viewer/src/modules/pipeline/SpecklePass.ts index feb2ce99ec..cc1430eca7 100644 --- a/packages/viewer/src/modules/pipeline/SpecklePass.ts +++ b/packages/viewer/src/modules/pipeline/SpecklePass.ts @@ -1,4 +1,11 @@ -import { Camera, Plane, Scene, Texture } from 'three' +import { + Camera, + OrthographicCamera, + PerspectiveCamera, + Plane, + Scene, + Texture +} from 'three' import { Pass } from 'three/examples/jsm/postprocessing/Pass.js' import { ObjectLayers } from '../../IViewer' @@ -17,23 +24,26 @@ export interface SpecklePass { onAferRender?: () => void get displayName(): string - get outputTexture(): Texture + get outputTexture(): Texture | null - update?(scene: Scene, camera: Camera) - setTexture?(uName: string, texture: Texture) - setParams?(params: unknown) - setClippingPlanes?(planes: Plane[]) - setLayers?(layers: ObjectLayers[]) + update?( + scene: Scene | null, + camera: PerspectiveCamera | OrthographicCamera | null + ): void + setTexture?(uName: string, texture: Texture): void + setParams?(params: unknown): void + setClippingPlanes?(planes: Plane[]): void + setLayers?(layers: ObjectLayers[]): void } export interface SpeckleProgressivePass extends SpecklePass { - setFrameIndex(index: number) - setAccumulationFrames(frames: number) - setRenderType?(type: RenderType) + setFrameIndex(index: number): void + setAccumulationFrames(frames: number): void + setRenderType?(type: RenderType): void } export abstract class BaseSpecklePass extends Pass implements SpecklePass { - protected layers: ObjectLayers[] = null + protected layers: ObjectLayers[] | null = null protected _enabledLayers: ObjectLayers[] = [] public get enabledLayers(): ObjectLayers[] { @@ -47,7 +57,7 @@ export abstract class BaseSpecklePass extends Pass implements SpecklePass { get displayName(): string { return 'BASE' } - get outputTexture(): Texture { + get outputTexture(): Texture | null { return null } diff --git a/packages/viewer/src/modules/pipeline/StaticAOPass.ts b/packages/viewer/src/modules/pipeline/StaticAOPass.ts index d3471b729d..68ac1a87e2 100644 --- a/packages/viewer/src/modules/pipeline/StaticAOPass.ts +++ b/packages/viewer/src/modules/pipeline/StaticAOPass.ts @@ -1,6 +1,5 @@ import { AddEquation, - Camera, Color, CustomBlending, DataTexture, @@ -29,7 +28,7 @@ import { speckleStaticAoGenerateFrag } from '../materials/shaders/speckle-static import { speckleStaticAoAccumulateVert } from '../materials/shaders/speckle-static-ao-accumulate-vert' import { speckleStaticAoAccumulateFrag } from '../materials/shaders/speckle-static-ao-accumulate-frag' import { SimplexNoise } from 'three/examples/jsm//math/SimplexNoise.js' -import { +import type { InputDepthTextureUniform, InputNormalsTextureUniform, SpeckleProgressivePass @@ -57,8 +56,8 @@ export const DefaultStaticAoPassParams = { } export class StaticAOPass extends Pass implements SpeckleProgressivePass { - public aoMaterial: ShaderMaterial = null - private accumulateMaterial: ShaderMaterial = null + public aoMaterial: ShaderMaterial + private accumulateMaterial: ShaderMaterial private _generationBuffer: WebGLRenderTarget private _accumulationBuffer: WebGLRenderTarget private params: StaticAoPassParams = DefaultStaticAoPassParams @@ -81,7 +80,7 @@ export class StaticAOPass extends Pass implements SpeckleProgressivePass { this.aoMaterial.needsUpdate = true } - public get outputTexture() { + public get outputTexture(): Texture { return this._accumulationBuffer.texture } @@ -164,7 +163,7 @@ export class StaticAOPass extends Pass implements SpeckleProgressivePass { this.accumulationFrames = frames } - public update(scene: Scene, camera: Camera) { + public update(_scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { /** DEFINES */ this.aoMaterial.defines['PERSPECTIVE_CAMERA'] = (camera as PerspectiveCamera) .isPerspectiveCamera @@ -209,9 +208,7 @@ export class StaticAOPass extends Pass implements SpeckleProgressivePass { this.accumulateMaterial.needsUpdate = true } - public render(renderer, writeBuffer, readBuffer) { - writeBuffer - readBuffer + public render(renderer: WebGLRenderer) { // save original state const originalClearColor = new Color() renderer.getClearColor(originalClearColor) @@ -254,7 +251,7 @@ export class StaticAOPass extends Pass implements SpeckleProgressivePass { } private generateSampleKernel(frameIndex: number) { - const kernelSize = this.params.kernelSize + const kernelSize = this.params.kernelSize || 0 this.kernels[frameIndex] = [] for (let i = 0; i < kernelSize; i++) { diff --git a/packages/viewer/src/modules/pipeline/StencilMaskPass.ts b/packages/viewer/src/modules/pipeline/StencilMaskPass.ts index 1d13642e0e..3195395717 100644 --- a/packages/viewer/src/modules/pipeline/StencilMaskPass.ts +++ b/packages/viewer/src/modules/pipeline/StencilMaskPass.ts @@ -4,27 +4,30 @@ import { DoubleSide, EqualStencilFunc, Material, + OrthographicCamera, + PerspectiveCamera, Plane, Scene, Texture, Vector2, + WebGLRenderTarget, WebGLRenderer } from 'three' import SpeckleDisplaceMaterial from '../materials/SpeckleDisplaceMaterial' -import { BaseSpecklePass, SpecklePass } from './SpecklePass' +import { BaseSpecklePass, type SpecklePass } from './SpecklePass' export class StencilMaskPass extends BaseSpecklePass implements SpecklePass { - private camera: Camera - private scene: Scene - private overrideMaterial: Material = null + private camera!: Camera + private scene!: Scene + private overrideMaterial: Material private _oldClearColor: Color = new Color() - private clearColor: Color = null + private clearColor!: Color private clearAlpha = 0 private clearDepth = true private drawBufferSize: Vector2 = new Vector2() - public onBeforeRender: () => void = null - public onAfterRender: () => void = null + public onBeforeRender: (() => void) | undefined = undefined + public onAfterRender: (() => void) | undefined = undefined public constructor() { super() @@ -42,7 +45,7 @@ export class StencilMaskPass extends BaseSpecklePass implements SpecklePass { public get displayName(): string { return 'STENCIL' } - public get outputTexture(): Texture { + public get outputTexture(): Texture | null { return null } @@ -50,7 +53,7 @@ export class StencilMaskPass extends BaseSpecklePass implements SpecklePass { return this.overrideMaterial } - public update(scene: Scene, camera: Camera) { + public update(scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { this.camera = camera this.scene = scene } @@ -61,14 +64,15 @@ export class StencilMaskPass extends BaseSpecklePass implements SpecklePass { render( renderer: WebGLRenderer, - writeBuffer, - readBuffer /*, deltaTime, maskActive */ + _writeBuffer: WebGLRenderTarget, + readBuffer: WebGLRenderTarget ) { if (this.onBeforeRender) this.onBeforeRender() const oldAutoClear = renderer.autoClear renderer.autoClear = false - let oldClearAlpha, oldOverrideMaterial + let oldClearAlpha, + oldOverrideMaterial = null if (this.overrideMaterial !== undefined) { oldOverrideMaterial = this.scene.overrideMaterial diff --git a/packages/viewer/src/modules/pipeline/StencilPass.ts b/packages/viewer/src/modules/pipeline/StencilPass.ts index 4232b5c73f..7bb9bb1c7f 100644 --- a/packages/viewer/src/modules/pipeline/StencilPass.ts +++ b/packages/viewer/src/modules/pipeline/StencilPass.ts @@ -4,27 +4,31 @@ import { Color, DoubleSide, Material, + OrthographicCamera, + PerspectiveCamera, Plane, ReplaceStencilOp, Scene, Texture, - Vector2 + Vector2, + WebGLRenderTarget, + WebGLRenderer } from 'three' import SpeckleDisplaceMaterial from '../materials/SpeckleDisplaceMaterial' -import { BaseSpecklePass, SpecklePass } from './SpecklePass' +import { BaseSpecklePass, type SpecklePass } from './SpecklePass' export class StencilPass extends BaseSpecklePass implements SpecklePass { private camera: Camera private scene: Scene - private overrideMaterial: Material = null + private overrideMaterial: Material private _oldClearColor: Color = new Color() - private clearColor: Color = null + private clearColor: Color private clearAlpha = 0 private clearDepth = true private drawBufferSize: Vector2 = new Vector2() - public onBeforeRender: () => void = null - public onAfterRender: () => void = null + public onBeforeRender: (() => void) | undefined = undefined + public onAfterRender: (() => void) | undefined = undefined public constructor() { super() @@ -46,7 +50,7 @@ export class StencilPass extends BaseSpecklePass implements SpecklePass { public get displayName(): string { return 'STENCIL' } - public get outputTexture(): Texture { + public get outputTexture(): Texture | null { return null } @@ -54,7 +58,7 @@ export class StencilPass extends BaseSpecklePass implements SpecklePass { return this.overrideMaterial } - public update(scene: Scene, camera: Camera) { + public update(scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { this.camera = camera this.scene = scene } @@ -63,12 +67,17 @@ export class StencilPass extends BaseSpecklePass implements SpecklePass { this.overrideMaterial.clippingPlanes = planes } - render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) { + render( + renderer: WebGLRenderer, + _writeBuffer: WebGLRenderTarget, + readBuffer: WebGLRenderTarget + ) { if (this.onBeforeRender) this.onBeforeRender() const oldAutoClear = renderer.autoClear renderer.autoClear = false - let oldClearAlpha, oldOverrideMaterial + let oldClearAlpha, + oldOverrideMaterial = null if (this.overrideMaterial !== undefined) { oldOverrideMaterial = this.scene.overrideMaterial diff --git a/packages/viewer/src/modules/queries/IntersectionQuerySolver.ts b/packages/viewer/src/modules/queries/IntersectionQuerySolver.ts index 83e1a9149b..48d9c19bdc 100644 --- a/packages/viewer/src/modules/queries/IntersectionQuerySolver.ts +++ b/packages/viewer/src/modules/queries/IntersectionQuerySolver.ts @@ -1,20 +1,20 @@ import Logger from 'js-logger' -import { Intersection, Ray, Vector2, Vector3 } from 'three' +import { type Intersection, Ray, Vector2, Vector3 } from 'three' import SpeckleRenderer from '../SpeckleRenderer' -import { IntersectionQuery, IntersectionQueryResult } from './Query' +import type { IntersectionQuery, IntersectionQueryResult } from './Query' import { ObjectLayers } from '../../IViewer' export class IntersectionQuerySolver { private vecBuff0: Vector3 = new Vector3() private vecBuff1: Vector3 = new Vector3() - private renderer: SpeckleRenderer + private renderer!: SpeckleRenderer public setContext(renderer: SpeckleRenderer) { this.renderer = renderer } - public solve(query: IntersectionQuery): IntersectionQueryResult { + public solve(query: IntersectionQuery): IntersectionQueryResult | null { switch (query.operation) { case 'Occlusion': return this.solveOcclusion(query) @@ -22,28 +22,31 @@ export class IntersectionQuerySolver { return this.solvePick(query) default: Logger.error('Malformed query') - break + return null } } private solveOcclusion(query: IntersectionQuery): IntersectionQueryResult { - const target = this.vecBuff0.set(query.point.x, query.point.y, query.point.z) + if (!this.renderer.renderingCamera) return { objects: null } + + const target = this.vecBuff0.set(query.point.x, query.point.y, query.point.z || 0) const dir = this.vecBuff1.copy(target).sub(this.renderer.renderingCamera.position) dir.normalize() const ray = new Ray(this.renderer.renderingCamera.position, dir) - const results: Array = this.renderer.intersections.intersectRay( - this.renderer.scene, - this.renderer.renderingCamera, - ray, - true, - this.renderer.clippingVolume, - [ObjectLayers.STREAM_CONTENT_MESH] - ) + const results: Array | null = + this.renderer.intersections.intersectRay( + this.renderer.scene, + this.renderer.renderingCamera, + ray, + ObjectLayers.STREAM_CONTENT_MESH, + true, + this.renderer.clippingVolume + ) if (!results || results.length === 0) return { objects: null } const hits = this.renderer.queryHitIds(results) if (!hits) return { objects: null } let targetDistance = this.renderer.renderingCamera.position.distanceTo(target) - targetDistance -= query.tolerance + targetDistance -= query.tolerance !== undefined ? query.tolerance : 0 if (targetDistance < results[0].distance) { return { objects: null } @@ -59,16 +62,20 @@ export class IntersectionQuerySolver { } } - private solvePick(query: IntersectionQuery): IntersectionQueryResult { - const results: Array = this.renderer.intersections.intersect( + private solvePick(query: IntersectionQuery): IntersectionQueryResult | null { + if (!this.renderer.renderingCamera) return null + + const results: Array | null = this.renderer.intersections.intersect( this.renderer.scene, this.renderer.renderingCamera, new Vector2(query.point.x, query.point.y), + undefined, true, this.renderer.clippingVolume ) if (!results) return null const hits = this.renderer.queryHits(results) + if (!hits) return null return { objects: hits.map((value) => { return { diff --git a/packages/viewer/src/modules/queries/PointQuerySolver.ts b/packages/viewer/src/modules/queries/PointQuerySolver.ts index 3593377115..2a246926e4 100644 --- a/packages/viewer/src/modules/queries/PointQuerySolver.ts +++ b/packages/viewer/src/modules/queries/PointQuerySolver.ts @@ -1,16 +1,16 @@ import Logger from 'js-logger' import { Vector3 } from 'three' import SpeckleRenderer from '../SpeckleRenderer' -import { PointQuery, PointQueryResult } from './Query' +import type { PointQuery, PointQueryResult } from './Query' export class PointQuerySolver { - private renderer: SpeckleRenderer + private renderer!: SpeckleRenderer public setContext(renderer: SpeckleRenderer) { this.renderer = renderer } - public solve(query: PointQuery): PointQueryResult { + public solve(query: PointQuery): PointQueryResult | null { switch (query.operation) { case 'Project': return this.solveProjection(query) @@ -18,14 +18,15 @@ export class PointQuerySolver { return this.solveUnprojection(query) default: Logger.error('Malformed query') - break + return null } } private solveProjection(query: PointQuery): PointQueryResult { // WORLD const projected = new Vector3(query.point.x, query.point.y, query.point.z) - projected.project(this.renderer.renderingCamera) + if (this.renderer.renderingCamera) projected.project(this.renderer.renderingCamera) + else Logger.error('Could not run query. Camera is null') return { // NDC @@ -38,8 +39,9 @@ export class PointQuerySolver { private solveUnprojection(query: PointQuery): PointQueryResult { // NDC const unprojected = new Vector3(query.point.x, query.point.y, query.point.z) - unprojected.unproject(this.renderer.renderingCamera) - + if (this.renderer.renderingCamera) + unprojected.unproject(this.renderer.renderingCamera) + else Logger.error('Could not run query. Camera is null') return { // WORLD x: unprojected.x, diff --git a/packages/viewer/src/modules/queries/Queries.ts b/packages/viewer/src/modules/queries/Queries.ts index 4f8ad5d101..bab8f0f966 100644 --- a/packages/viewer/src/modules/queries/Queries.ts +++ b/packages/viewer/src/modules/queries/Queries.ts @@ -1,6 +1,6 @@ import { IntersectionQuerySolver } from './IntersectionQuerySolver' import { PointQuerySolver } from './PointQuerySolver' -import { IntersectionQuery, PointQuery, Query } from './Query' +import type { IntersectionQuery, PointQuery, Query } from './Query' export class Queries { public static DefaultPointQuerySolver: PointQuerySolver = new PointQuerySolver() diff --git a/packages/viewer/src/modules/tree/DataTree.ts b/packages/viewer/src/modules/tree/DataTree.ts deleted file mode 100644 index e8d1c114eb..0000000000 --- a/packages/viewer/src/modules/tree/DataTree.ts +++ /dev/null @@ -1,71 +0,0 @@ -import TreeModel from 'tree-model' -import { TreeNode, WorldTree } from './WorldTree' - -export type SpeckleObject = Record -export type ObjectPredicate = (guid: string, obj: SpeckleObject) => boolean - -export interface DataTree { - findFirst(predicate: ObjectPredicate): SpeckleObject - findAll(predicate: ObjectPredicate): SpeckleObject[] - walk(predicate: ObjectPredicate): void -} - -class DataTreeInternal implements DataTree { - tree: TreeModel - root: TreeNode - - public constructor() { - this.tree = new TreeModel() - this.root = this.tree.parse({ guid: WorldTree.ROOT_ID }) - } - public findAll(predicate: ObjectPredicate): SpeckleObject[] { - return this.root - .all((node: TreeNode) => { - if (!node.model.data) return false - return predicate(node.model.guid, node.model.data) - }) - .map((value: TreeNode) => value.model.data) - } - - public findFirst(predicate: ObjectPredicate): SpeckleObject { - return this.root.first((node: TreeNode) => { - if (!node.model.data) return false - return predicate(node.model.guid, node.model.data) - }).model.data - } - - public walk(predicate: ObjectPredicate) { - this.root.walk((node: TreeNode) => { - if (!node.model.data) return true - return predicate(node.model.guid, node.model.data) - }) - } -} - -export class DataTreeBuilder { - public static build(tree: WorldTree): DataTree { - const dataTree = new DataTreeInternal() - let parent = null - tree.root.walk((node: TreeNode) => { - if (!node.parent) { - parent = dataTree.root - return true - } - - parent = dataTree.root.first((localNode) => { - return localNode.model.guid === node.parent.model.id - }) - - const _node: TreeNode = tree.parse({ - guid: node.model.id, - data: node.model.raw, - atomic: node.model.atomic, - children: [] - }) - parent.addChild(_node) - - return true - }, tree.root) - return dataTree as DataTree - } -} diff --git a/packages/viewer/src/modules/tree/NodeMap.ts b/packages/viewer/src/modules/tree/NodeMap.ts index 0579a7e04a..683b19d085 100644 --- a/packages/viewer/src/modules/tree/NodeMap.ts +++ b/packages/viewer/src/modules/tree/NodeMap.ts @@ -1,10 +1,9 @@ import Logger from 'js-logger' -import { TreeNode } from './WorldTree' +import { type TreeNode } from './WorldTree' export class NodeMap { public static readonly COMPOUND_ID_CHAR = '~' - private subtreeRoot: TreeNode private all: { [id: string]: TreeNode } = {} public instances: { [id: string]: { [id: string]: TreeNode } } = {} @@ -13,7 +12,6 @@ export class NodeMap { } public constructor(subtreeRoot: TreeNode) { - this.subtreeRoot = subtreeRoot this.registerNode(subtreeRoot) } @@ -43,7 +41,7 @@ export class NodeMap { return true } - public getNodeById(id: string): TreeNode[] { + public getNodeById(id: string): TreeNode[] | null { if (id.includes(NodeMap.COMPOUND_ID_CHAR)) { const baseId = id.substring(0, id.indexOf(NodeMap.COMPOUND_ID_CHAR)) if (this.instances[baseId]) { @@ -68,7 +66,7 @@ export class NodeMap { return this.all[id] } - public hasId(id: string) { + public hasId(id: string): boolean { if (id.includes(NodeMap.COMPOUND_ID_CHAR)) { const baseId = id.substring(0, id.indexOf(NodeMap.COMPOUND_ID_CHAR)) if (this.instances[baseId]) { @@ -83,9 +81,10 @@ export class NodeMap { if (this.instances[id]) { return true } + return false } - private registerInstance(node: TreeNode) { + private registerInstance(node: TreeNode): void { const baseId = node.model.id.substring( 0, node.model.id.indexOf(NodeMap.COMPOUND_ID_CHAR) @@ -101,7 +100,7 @@ export class NodeMap { } public purge() { - this.all = null - this.instances = null + this.all = {} + this.instances = {} } } diff --git a/packages/viewer/src/modules/tree/NodeRenderView.ts b/packages/viewer/src/modules/tree/NodeRenderView.ts index f5a176e12a..7a4cdb6afc 100644 --- a/packages/viewer/src/modules/tree/NodeRenderView.ts +++ b/packages/viewer/src/modules/tree/NodeRenderView.ts @@ -1,7 +1,10 @@ import { Box3 } from 'three' import { GeometryType } from '../batching/Batch' -import { GeometryData } from '../converter/Geometry' -import Materials, { DisplayStyle, RenderMaterial } from '../materials/Materials' +import { GeometryAttributes, type GeometryData } from '../converter/Geometry' +import Materials, { + type DisplayStyle, + type RenderMaterial +} from '../materials/Materials' import { SpeckleType } from '../loaders/GeometryConverter' export interface NodeRenderData { @@ -9,8 +12,8 @@ export interface NodeRenderData { subtreeId: number speckleType: SpeckleType geometry: GeometryData - renderMaterial: RenderMaterial - displayStyle: DisplayStyle + renderMaterial: RenderMaterial | null + displayStyle: DisplayStyle | null } export class NodeRenderView { @@ -23,22 +26,23 @@ export class NodeRenderView { private readonly _renderData: NodeRenderData private _materialHash: number private _geometryType: GeometryType - private _guid: string = null + private _guid: string | null = null - private _aabb: Box3 = null + private _aabb: Box3 - public get guid() { + /** TO DO: Not sure if we should store it */ + public get guid(): string { if (!this._guid) { this._guid = this._renderData.subtreeId + this._renderData.id } return this._guid } - public get renderData() { + public get renderData(): NodeRenderData { return this._renderData } - public get renderMaterialHash() { + public get renderMaterialHash(): number { return this._materialHash } @@ -50,15 +54,15 @@ export class NodeRenderView { return this._renderData.geometry && this._renderData.geometry.metaData } - public get speckleType() { + public get speckleType(): SpeckleType { return this._renderData.speckleType } - public get geometryType() { + public get geometryType(): GeometryType { return this._geometryType } - public get batchStart() { + public get batchStart(): number { return this._batchIndexStart } @@ -66,33 +70,35 @@ export class NodeRenderView { return this._batchIndexStart + this._batchIndexCount } - public get batchCount() { + public get batchCount(): number { return this._batchIndexCount } - public get batchId() { + public get batchId(): string { return this._batchId } - public get aabb() { + public get aabb(): Box3 { return this._aabb } - public get transparent() { + public get transparent(): boolean { return ( - this._renderData.renderMaterial && this._renderData.renderMaterial.opacity < 1 + (this._renderData.renderMaterial && + this._renderData.renderMaterial.opacity < 1) || + false ) } - public get vertStart() { + public get vertStart(): number { return this._batchVertexStart } - public get vertEnd() { + public get vertEnd(): number { return this._batchVertexEnd } - public get needsSegmentConversion() { + public get needsSegmentConversion(): boolean { return ( this._renderData.speckleType === SpeckleType.Curve || this._renderData.speckleType === SpeckleType.Polyline || @@ -103,15 +109,16 @@ export class NodeRenderView { ) } - public get validGeometry() { + public get validGeometry(): boolean { return ( - this._renderData.geometry.attributes && - this._renderData.geometry.attributes.POSITION && - this._renderData.geometry.attributes.POSITION.length > 0 && - (this._geometryType === GeometryType.MESH - ? this._renderData.geometry.attributes.INDEX && - this._renderData.geometry.attributes.INDEX.length > 0 - : true) + (this._renderData.geometry.attributes && + this._renderData.geometry.attributes.POSITION && + this._renderData.geometry.attributes.POSITION.length > 0 && + (this._geometryType === GeometryType.MESH + ? this._renderData.geometry.attributes.INDEX && + this._renderData.geometry.attributes.INDEX.length > 0 + : true)) || + false ) } @@ -120,11 +127,11 @@ export class NodeRenderView { this._geometryType = this.getGeometryType() this._materialHash = Materials.getMaterialHash(this) - this._batchId - this._batchIndexCount - this._batchIndexStart - this._batchVertexStart - this._batchVertexEnd + this._batchId = '' + this._batchIndexCount = 0 + this._batchIndexStart = -1 + this._batchVertexStart = -1 + this._batchVertexEnd = -1 } public setBatchData( @@ -142,10 +149,12 @@ export class NodeRenderView { } public computeAABB() { - this._aabb = new Box3().setFromArray(this._renderData.geometry.attributes.POSITION) + this._aabb = new Box3() + if (this._renderData.geometry.attributes) + this._aabb.setFromArray(this._renderData.geometry.attributes.POSITION) } - public getGeometryType(): GeometryType { + private getGeometryType(): GeometryType { switch (this._renderData.speckleType) { case SpeckleType.Mesh: return GeometryType.MESH @@ -165,7 +174,7 @@ export class NodeRenderView { public disposeGeometry() { for (const attr in this._renderData.geometry.attributes) { - this._renderData.geometry.attributes[attr] = [] + this._renderData.geometry.attributes[attr as GeometryAttributes] = [] } } } diff --git a/packages/viewer/src/modules/tree/RenderTree.ts b/packages/viewer/src/modules/tree/RenderTree.ts index 0d5f0b79b8..7e9ae365d2 100644 --- a/packages/viewer/src/modules/tree/RenderTree.ts +++ b/packages/viewer/src/modules/tree/RenderTree.ts @@ -1,7 +1,7 @@ -import { Box3, Matrix4 } from 'three' -import { TreeNode, WorldTree } from './WorldTree' +import { Matrix4 } from 'three' +import { type TreeNode, WorldTree } from './WorldTree' import Materials from '../materials/Materials' -import { NodeRenderData, NodeRenderView } from './NodeRenderView' +import { type NodeRenderData, NodeRenderView } from './NodeRenderView' import Logger from 'js-logger' import { GeometryConverter, SpeckleType } from '../loaders/GeometryConverter' import { Geometry } from '../converter/Geometry' @@ -9,13 +9,8 @@ import { Geometry } from '../converter/Geometry' export class RenderTree { private tree: WorldTree private root: TreeNode - private _treeBounds: Box3 = new Box3() private cancel = false - public get treeBounds(): Box3 { - return this._treeBounds - } - public get id(): string { return this.root.model.id } @@ -55,7 +50,6 @@ export class RenderTree { ) } node.model.renderView.computeAABB() - this._treeBounds.union(node.model.renderView.aabb) } else if (node.model.renderView.hasMetadata) { node.model.renderView.renderData.geometry.bakeTransform.premultiply(transform) } @@ -65,8 +59,8 @@ export class RenderTree { private buildRenderNode( node: TreeNode, geometryConverter: GeometryConverter - ): NodeRenderData { - let ret: NodeRenderData = null + ): NodeRenderData | null { + let ret: NodeRenderData | null = null const geometryData = geometryConverter.convertNodeToGeometryData(node.model) if (geometryData) { const renderMaterialNode = this.getRenderMaterialNode(node) @@ -89,7 +83,7 @@ export class RenderTree { return ret } - private getRenderMaterialNode(node: TreeNode): TreeNode { + private getRenderMaterialNode(node: TreeNode): TreeNode | null { if (node.model.raw.renderMaterial) { return node } @@ -99,9 +93,10 @@ export class RenderTree { return ancestors[k] } } + return null } - private getDisplayStyleNode(node: TreeNode): TreeNode { + private getDisplayStyleNode(node: TreeNode): TreeNode | null { if (node.model.raw.displayStyle) { return node } @@ -111,6 +106,7 @@ export class RenderTree { return ancestors[k] } } + return null } public computeTransform(node: TreeNode): Matrix4 { @@ -123,7 +119,10 @@ export class RenderTree { for (let k = 0; k < ancestors.length; k++) { if (ancestors[k].model.renderView) { const renderNode: NodeRenderData = ancestors[k].model.renderView.renderData - if (renderNode.speckleType === SpeckleType.Transform) { + if ( + renderNode.speckleType === SpeckleType.Transform && + renderNode.geometry.transform + ) { transform.premultiply(renderNode.geometry.transform) } } @@ -151,41 +150,26 @@ export class RenderTree { }) } - /** This gets the render views for a particular node/id. - * Currently it doesn't treat Blocks in a special way, but - * we might want to. - */ - public getRenderViewsForNode(node: TreeNode, parent?: TreeNode): NodeRenderView[] { - if ( - node.model.atomic && - node.model.renderView && - node.model.renderView.renderData.speckleType !== SpeckleType.RevitInstance && - node.model.renderView.renderData.speckleType !== SpeckleType.BlockInstance - ) { - return [node.model.renderView] - } - - return (parent ? parent : node.parent) - .all((_node: TreeNode): boolean => { - return ( - _node.model.renderView && - (_node.model.renderView.hasGeometry || _node.model.renderView.hasMetadata) - ) - }) - .map((val: TreeNode) => val.model.renderView) + public getRenderViewsForNode(node: TreeNode): NodeRenderView[] { + return this.getRenderViewNodesForNode(node).map( + (val: TreeNode) => val.model.renderView + ) } - public getRenderViewNodesForNode(node: TreeNode, parent?: TreeNode): TreeNode[] { + public getRenderViewNodesForNode(node: TreeNode): TreeNode[] { if ( node.model.atomic && - node.model.renderView && + node.model.renderView + /** This should not be needed anymore. */ + /*&& node.model.renderView.renderData.speckleType !== SpeckleType.RevitInstance && node.model.renderView.renderData.speckleType !== SpeckleType.BlockInstance + */ ) { return [node] } - return (parent ? parent : node.parent).all((_node: TreeNode): boolean => { + return node.all((_node: TreeNode): boolean => { return ( _node.model.renderView && (_node.model.renderView.hasGeometry || _node.model.renderView.hasMetadata) @@ -193,45 +177,33 @@ export class RenderTree { }) } - public getAtomicParent(node: TreeNode) { - if (node.model.atomic) { - return node - } - return this.tree.getAncestors(node).find((node) => node.model.atomic) - } - - public getRenderViewsForNodeId(id: string): NodeRenderView[] { + public getRenderViewsForNodeId(id: string): NodeRenderView[] | null { const nodes = this.tree.findId(id) if (!nodes) { Logger.warn(`Id ${id} does not exist`) return null } - const ret = [] + const ret: Array = [] nodes.forEach((node: TreeNode) => { - ret.push(...this.getRenderViewsForNode(node, node)) + ret.push(...this.getRenderViewsForNode(node)) }) return ret } - public getRenderViewForNodeId(id: string): NodeRenderView { - const nodes = this.tree.findId(id) - if (!nodes) { - Logger.warn(`Id ${id} does not exist`) - return null - } - if (nodes.length > 1) { - Logger.warn(`Multiple nodes with ${id} found. Returning first only`) + public getAtomicParent(node: TreeNode): TreeNode { + if (node.model.atomic) { + return node } - return nodes[0].model.renderView + /** There will always the root of the tree as the atomic parent for all nodes */ + return this.tree.getAncestors(node).find((node) => node.model.atomic) as TreeNode } - public purge() { - this.tree = null - } + public purge() {} - public cancelBuild(subtreeId: string) { + /** TO DO: Need to purge only if currently building */ + public cancelBuild(): void { this.cancel = true - this.tree.purge(subtreeId) + this.tree.purge(this.id) this.purge() } } diff --git a/packages/viewer/src/modules/tree/WorldTree.ts b/packages/viewer/src/modules/tree/WorldTree.ts index 7c0bd2a990..54b3f0be0a 100644 --- a/packages/viewer/src/modules/tree/WorldTree.ts +++ b/packages/viewer/src/modules/tree/WorldTree.ts @@ -1,4 +1,4 @@ -import TreeModel from 'tree-model' +import TreeModel, { type Model } from 'tree-model' import { NodeRenderView } from './NodeRenderView' import { RenderTree } from './RenderTree' import Logger from 'js-logger' @@ -7,22 +7,22 @@ import { NodeMap } from './NodeMap' export type TreeNode = TreeModel.Node export type SearchPredicate = (node: TreeNode) => boolean -export type AsyncSearchPredicate = (node: TreeNode) => Promise export interface NodeData { + id: string // eslint-disable-next-line @typescript-eslint/no-explicit-any raw: { [prop: string]: any } children: TreeNode[] - nestedNodes: TreeNode[] atomic: boolean + nestedNodes?: TreeNode[] subtreeId?: number - renderView?: NodeRenderView + renderView?: NodeRenderView | null instanced?: boolean } export class WorldTree { private renderTreeInstances: { [id: string]: RenderTree } = {} - public nodeMaps: { [id: string]: NodeMap } = {} + private nodeMaps: { [id: string]: NodeMap } = {} private readonly supressWarnings = true public static readonly ROOT_ID = 'ROOT' private subtreeId: number = 0 @@ -38,7 +38,10 @@ export class WorldTree { }) } - public getRenderTree(subtreeId?: string): RenderTree { + /** The root render tree will always be non-null because it will always contain the root */ + public getRenderTree(): RenderTree + public getRenderTree(subtreeId: string): RenderTree | null + public getRenderTree(subtreeId?: string): RenderTree | null { if (!this._root) { console.error(`WorldTree not initialised`) return null @@ -63,17 +66,17 @@ export class WorldTree { return this._root } - public get nextSubtreeId(): number { + private get nextSubtreeId(): number { return ++this.subtreeId } - public get nodeCount() { + public get nodeCount(): number { let nodeCount = 0 for (const k in this.nodeMaps) nodeCount += this.nodeMaps[k].nodeCount return nodeCount } - public isRoot(node: TreeNode) { + public isRoot(node: TreeNode): boolean { return node === this._root } @@ -81,7 +84,7 @@ export class WorldTree { return node.parent === this._root } - public parse(model) { + public parse(model: Model): TreeNode { return this.tree.parse(model) } @@ -96,7 +99,7 @@ export class WorldTree { this._root.addChild(node) } - public addNode(node: TreeNode, parent: TreeNode) { + public addNode(node: TreeNode, parent: TreeNode | null) { if (parent === null || parent.model.subtreeId === undefined) { Logger.error(`Invalid parent node!`) return @@ -105,7 +108,7 @@ export class WorldTree { if (this.nodeMaps[parent.model.subtreeId]?.addNode(node)) parent.addChild(node) } - public removeNode(node: TreeNode) { + public removeNode(node: TreeNode): void { node.drop() } @@ -116,7 +119,7 @@ export class WorldTree { return (node ? node : this.root).all(predicate) } - public findId(id: string, subtreeId?: number) { + public findId(id: string, subtreeId?: number): TreeNode[] | null { let idNode = null if (subtreeId) { idNode = this.nodeMaps[subtreeId].getNodeById(id) @@ -129,6 +132,7 @@ export class WorldTree { return idNode } + /** TODO: Would rather not have this */ public findSubtree(id: string) { let idNode = null for (const k in this.nodeMaps) { @@ -141,10 +145,11 @@ export class WorldTree { return node.getPath().reverse().slice(1) // We skip the node itself } - public getInstances(subtree: string): { [id: string]: Record } { - return this.nodeMaps[subtree].instances + public getInstances(subtreeId: string): { [id: string]: Record } { + return this.nodeMaps[subtreeId].instances } + /** TO DO: We might want to add boolean as return type here too */ public walk(predicate: SearchPredicate, node?: TreeNode): void { if (!node && !this.supressWarnings) { Logger.warn(`Root will be used for searching. You might not want that`) @@ -162,7 +167,10 @@ export class WorldTree { const pause = new AsyncPause() let success = true - async function depthFirstPreOrderAsync(callback, context) { + async function depthFirstPreOrderAsync( + callback: SearchPredicate, + context: TreeNode + ) { let i, childCount pause.tick(100) if (pause.needsWait) { @@ -183,10 +191,12 @@ export class WorldTree { public purge(subtreeId?: string) { if (subtreeId) { delete this.renderTreeInstances[subtreeId] - const subtreeNode = this.findId(subtreeId)[0] - this.nodeMaps[subtreeNode.model.subtreeId].purge() - delete this.nodeMaps[subtreeNode.model.subtreeId] - this.removeNode(subtreeNode) + const subtreeNode = this.findId(subtreeId) + if (subtreeNode) { + this.nodeMaps[subtreeNode[0].model.subtreeId].purge() + delete this.nodeMaps[subtreeNode[0].model.subtreeId] + this.removeNode(subtreeNode[0]) + } return } diff --git a/packages/viewer/tsconfig.json b/packages/viewer/tsconfig.json index a1cd799893..74f2229df7 100644 --- a/packages/viewer/tsconfig.json +++ b/packages/viewer/tsconfig.json @@ -4,13 +4,16 @@ "lib": ["DOM"], "module": "es2020", "moduleResolution": "Bundler", - "strict": false, + "strict": true, "sourceMap": true, "isolatedModules": true, "esModuleInterop": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noImplicitReturns": false, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + "verbatimModuleSyntax": false, + "strictPropertyInitialization": false /* We are not building a ToDO list app. This option is ridiculous */, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, "skipLibCheck": true, "outDir": "./dist", "allowJs": true, diff --git a/yarn.lock b/yarn.lock index dfb27cd5d8..98bfedad4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14793,6 +14793,7 @@ __metadata: "@speckle/shared": "workspace:^" "@types/babel__core": ^7.20.1 "@types/flat": ^5.0.2 + "@types/lodash-es": 4.17.12 "@types/three": ^0.136.0 "@typescript-eslint/eslint-plugin": ^5.39.0 "@typescript-eslint/parser": ^5.39.0 @@ -14801,13 +14802,11 @@ __metadata: core-js: ^3.21.1 eslint: ^8.11.0 eslint-config-prettier: ^8.5.0 - flat: ^5.0.2 hold-event: ^0.1.0 js-logger: 1.6.1 jsdom: ^24.0.0 lodash-es: ^4.17.21 prettier: ^2.5.1 - rainbowvis.js: ^1.0.1 regenerator-runtime: ^0.13.7 rollup: ^2.70.1 rollup-plugin-delete: ^2.0.0 @@ -14820,7 +14819,6 @@ __metadata: troika-three-text: 0.47.2 type-fest: ^4.15.0 typescript: ^4.5.4 - underscore: 1.13.6 vitest: ^1.4.0 languageName: unknown linkType: soft @@ -17497,6 +17495,15 @@ __metadata: languageName: node linkType: hard +"@types/lodash-es@npm:4.17.12": + version: 4.17.12 + resolution: "@types/lodash-es@npm:4.17.12" + dependencies: + "@types/lodash": "*" + checksum: 990a99e2243bebe9505cb5ad19fbc172beb4a8e00f9075c99fc06c46c2801ffdb40bc2867271cf580d5f48994fc9fb076ec92cd60a20e621603bf22114e5b077 + languageName: node + linkType: hard + "@types/lodash-es@npm:^4.17.6": version: 4.17.6 resolution: "@types/lodash-es@npm:4.17.6"