diff --git a/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx b/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx index 155c3c74a25..64101a93d34 100644 --- a/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx +++ b/react-components/src/components/Reveal3DResources/Reveal3DResources.tsx @@ -55,7 +55,7 @@ export const Reveal3DResources = ({ .map((options) => ({ ...image360Settings, ...options })); }, [resources, image360Settings]); - useRemoveNonReferencedModels(reveal3DModels, image360CollectionAddOptions, viewer); + useRemoveNonReferencedModels(resources, viewer); const cadModelOptions = useMemo( () => reveal3DModels.filter((model): model is CadModelOptions => model.type === 'cad'), diff --git a/react-components/src/components/Reveal3DResources/useRemoveNonReferencedModels.ts b/react-components/src/components/Reveal3DResources/useRemoveNonReferencedModels.ts index be318a6661a..09e5d23b3aa 100644 --- a/react-components/src/components/Reveal3DResources/useRemoveNonReferencedModels.ts +++ b/react-components/src/components/Reveal3DResources/useRemoveNonReferencedModels.ts @@ -2,18 +2,18 @@ * Copyright 2024 Cognite AS */ import { type Cognite3DViewer, type CogniteModel, type Image360Collection } from '@cognite/reveal'; -import { type AddImage360CollectionOptions, type TypedReveal3DModel } from './types'; +import { type AddResourceOptions } from './types'; import { + is360ImageAddOptions, is360ImageDataModelAddOptions, is360ImageEventsAddOptions, is3dModelOptions } from './typeGuards'; import { useEffect } from 'react'; -import { isSameCadModel, isSamePointCloudModel } from '../../utilities/isSameModel'; +import { isSame3dModel } from '../../utilities/isSameModel'; export function useRemoveNonReferencedModels( - addOptions: TypedReveal3DModel[], - image360CollectionAddOptions: AddImage360CollectionOptions[], + addOptions: AddResourceOptions[], viewer: Cognite3DViewer ): void { useEffect(() => { @@ -23,10 +23,7 @@ export function useRemoveNonReferencedModels( viewer.removeModel(model); }); - const nonReferencedCollections = findNonReferencedCollections( - image360CollectionAddOptions, - viewer - ); + const nonReferencedCollections = findNonReferencedCollections(addOptions, viewer); nonReferencedCollections.forEach((collection) => { viewer.remove360ImageSet(collection); @@ -35,11 +32,11 @@ export function useRemoveNonReferencedModels( } function findNonReferencedModels( - addOptions: TypedReveal3DModel[], + addOptions: AddResourceOptions[], viewer: Cognite3DViewer ): CogniteModel[] { const models = viewer.models; - const addOptionsSet = new Set(addOptions); + const addOptionsSet = new Set(addOptions.filter(is3dModelOptions)); return models.filter((model) => { const correspondingAddOptions = (() => { @@ -48,25 +45,13 @@ function findNonReferencedModels( continue; } - const isCadAndSame = - options.type === 'cad' && - isSameCadModel(options, { - type: 'cad', - modelId: model.modelId, - revisionId: model.revisionId, - transform: model.getModelTransformation() - }); - - const isPointCloudAndSame = - options.type === 'pointcloud' && - isSamePointCloudModel(options, { - type: 'pointcloud', - modelId: model.modelId, - revisionId: model.revisionId, - transform: model.getModelTransformation() - }); - - if (isCadAndSame || isPointCloudAndSame) { + const isSameModel = isSame3dModel(options, { + modelId: model.modelId, + revisionId: model.revisionId, + transform: model.getModelTransformation() + }); + + if (isSameModel) { return options; } } @@ -83,9 +68,11 @@ function findNonReferencedModels( } function findNonReferencedCollections( - image360CollectionAddOptions: AddImage360CollectionOptions[], + addOptions: AddResourceOptions[], viewer: Cognite3DViewer ): Image360Collection[] { + const image360CollectionAddOptions = addOptions.filter(is360ImageAddOptions); + const collections = viewer.get360ImageCollections(); const collectionAddOptionsSet = new Set(image360CollectionAddOptions); diff --git a/react-components/src/hooks/useCalculatePointCloudModelsStyling.tsx b/react-components/src/hooks/useCalculatePointCloudModelsStyling.tsx index 064ea82d8ba..179b63cc2be 100644 --- a/react-components/src/hooks/useCalculatePointCloudModelsStyling.tsx +++ b/react-components/src/hooks/useCalculatePointCloudModelsStyling.tsx @@ -10,7 +10,7 @@ import { import { useMemo } from 'react'; import { type AnnotationIdStylingGroup } from '../components/PointCloudContainer/useApplyPointCloudStyling'; import { useQuery } from '@tanstack/react-query'; -import { isSamePointCloudModel } from '../utilities/isSameModel'; +import { isSame3dModel } from '../utilities/isSameModel'; import { usePointCloudAnnotationMappingsForModels, usePointCloudAnnotationIdsForModels @@ -168,7 +168,7 @@ function groupStyleGroupByModel( return styleGroup.reduce((accumulatedGroups, currentGroup) => { const existingGroupWithModel = accumulatedGroups.find((group) => - isSamePointCloudModel(group.model, currentGroup.model) + isSame3dModel(group.model, currentGroup.model) ); existingGroupWithModel?.styleGroups.push(...currentGroup.styleGroups); return accumulatedGroups; diff --git a/react-components/src/utilities/isSameModel.ts b/react-components/src/utilities/isSameModel.ts index 18c74c6974c..9b015e502f3 100644 --- a/react-components/src/utilities/isSameModel.ts +++ b/react-components/src/utilities/isSameModel.ts @@ -3,8 +3,8 @@ */ import { type GeometryFilter } from '@cognite/reveal'; import { - type PointCloudModelOptions, - type CadModelOptions + type CadModelOptions, + type AddReveal3DModelOptions } from '../components/Reveal3DResources/types'; import { Matrix4 } from 'three'; @@ -43,9 +43,9 @@ function isSameGeometryFilter( ); } -export function isSamePointCloudModel( - model0: PointCloudModelOptions, - model1: PointCloudModelOptions +export function isSame3dModel( + model0: AddReveal3DModelOptions, + model1: AddReveal3DModelOptions ): boolean { return ( model0.modelId === model1.modelId && diff --git a/react-components/tests/unit-tests/components/Reveal3DResources/useRemoveNonReferencedModels.test.ts b/react-components/tests/unit-tests/components/Reveal3DResources/useRemoveNonReferencedModels.test.ts new file mode 100644 index 00000000000..d2af83fdd7c --- /dev/null +++ b/react-components/tests/unit-tests/components/Reveal3DResources/useRemoveNonReferencedModels.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; + +import { Mock } from 'moq.ts'; + +import { renderHook } from '@testing-library/react'; + +import { useRemoveNonReferencedModels } from '../../../../src/components/Reveal3DResources/useRemoveNonReferencedModels'; + +import { + viewerImage360CollectionsMock, + viewerMock, + viewerModelsMock, + viewerRemoveModelsMock +} from '../../fixtures/viewer'; +import { cadMock, cadModelOptions } from '../../fixtures/cadModel'; +import { pointCloudMock, pointCloudModelOptions } from '../../fixtures/pointCloud'; +import { image360Mock, image360Options } from '../../fixtures/image360'; + +describe(useRemoveNonReferencedModels.name, () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + test('does not crash when no models are added', () => { + viewerModelsMock.mockReturnValue([]); + viewerImage360CollectionsMock.mockReturnValue([]); + expect(() => renderHook(() => useRemoveNonReferencedModels([], viewerMock))).not.toThrow(); + }); + + test('removes models when empty ', () => { + viewerModelsMock.mockReturnValue([cadMock]); + viewerImage360CollectionsMock.mockReturnValue([]); + renderHook(() => useRemoveNonReferencedModels([], viewerMock)); + expect(viewerRemoveModelsMock).toHaveBeenCalledOnce(); + }); + + test('does not remove models when in addOptions', () => { + viewerModelsMock.mockReturnValue([pointCloudMock, cadMock]); + viewerImage360CollectionsMock.mockReturnValue([image360Mock]); + renderHook(() => + useRemoveNonReferencedModels( + [cadModelOptions, pointCloudModelOptions, image360Options], + viewerMock + ) + ); + expect(viewerRemoveModelsMock).not.toHaveBeenCalled(); + }); + + test('removes only relevant model', () => { + viewerModelsMock.mockReturnValue([pointCloudMock, cadMock]); + viewerImage360CollectionsMock.mockReturnValue([image360Mock]); + renderHook(() => useRemoveNonReferencedModels([cadModelOptions, image360Options], viewerMock)); + expect(viewerRemoveModelsMock).toHaveBeenCalledWith(pointCloudMock); + }); +}); diff --git a/react-components/tests/unit-tests/components/RevealContainer.test.tsx b/react-components/tests/unit-tests/components/RevealContainer.test.tsx index 490c8f63ea9..bbab6b2a286 100644 --- a/react-components/tests/unit-tests/components/RevealContainer.test.tsx +++ b/react-components/tests/unit-tests/components/RevealContainer.test.tsx @@ -1,7 +1,7 @@ import { describe, expect, test } from 'vitest'; import { render, screen } from '@testing-library/react'; import { RevealCanvas, RevealContext } from '../../../src'; -import { It, Mock } from 'moq.ts'; +import { Mock } from 'moq.ts'; import { type CogniteClient } from '@cognite/sdk'; import { RevealKeepAliveContext } from '../../../src/components/RevealKeepAlive/RevealKeepAliveContext'; import { type FC, useRef } from 'react'; @@ -11,7 +11,7 @@ import { type PointCloudAnnotationCache } from '../../../src/components/CachePro import { type Image360AnnotationCache } from '../../../src/components/CacheProvider/Image360AnnotationCache'; import { type SceneIdentifiers } from '../../../src/components/SceneContainer/sceneTypes'; import { type RevealRenderTarget } from '../../../src/architecture/base/renderTarget/RevealRenderTarget'; -import { Cognite3DViewer } from '@cognite/reveal'; +import { viewerMock } from '../fixtures/viewer'; describe(RevealCanvas.name, () => { test('Mounting reveal container will mount a canvas to the DOM', () => { @@ -22,19 +22,10 @@ describe(RevealCanvas.name, () => { .setup((p) => p.project) .returns('test'); - const domElement = document - .createElement('div') - .appendChild(document.createElement('canvas')); - // This object is not created as a Mock because it seems to interact badly with const renderTargetRef = useRef({ get viewer() { - return new Mock() - .setup((viewer) => viewer.setBackgroundColor(It.IsAny())) - .returns() - .setup((viewer) => viewer.domElement) - .returns(domElement) - .object(); + return viewerMock; }, initialize(): void {} } as unknown as RevealRenderTarget); diff --git a/react-components/tests/unit-tests/fixtures/cadModel.ts b/react-components/tests/unit-tests/fixtures/cadModel.ts new file mode 100644 index 00000000000..6d9c3559164 --- /dev/null +++ b/react-components/tests/unit-tests/fixtures/cadModel.ts @@ -0,0 +1,17 @@ +import { CogniteCadModel } from '@cognite/reveal'; +import { Mock } from 'moq.ts'; +import { Matrix4 } from 'three'; + +export const cadModelOptions = { + modelId: 123, + revisionId: 456 +}; + +export const cadMock = new Mock() + .setup((p) => p.modelId) + .returns(cadModelOptions.modelId) + .setup((p) => p.revisionId) + .returns(cadModelOptions.revisionId) + .setup((p) => p.getModelTransformation()) + .returns(new Matrix4()) + .object(); diff --git a/react-components/tests/unit-tests/fixtures/image360.ts b/react-components/tests/unit-tests/fixtures/image360.ts new file mode 100644 index 00000000000..c03b2be4103 --- /dev/null +++ b/react-components/tests/unit-tests/fixtures/image360.ts @@ -0,0 +1,11 @@ +import { Image360Collection } from '@cognite/reveal'; +import { Mock } from 'moq.ts'; + +export const image360Options = { + siteId: 'siteId' +}; + +export const image360Mock = new Mock() + .setup((p) => p.id) + .returns(image360Options.siteId) + .object(); diff --git a/react-components/tests/unit-tests/fixtures/pointCloud.ts b/react-components/tests/unit-tests/fixtures/pointCloud.ts new file mode 100644 index 00000000000..fa4296b36c6 --- /dev/null +++ b/react-components/tests/unit-tests/fixtures/pointCloud.ts @@ -0,0 +1,17 @@ +import { CognitePointCloudModel } from '@cognite/reveal'; +import { Mock } from 'moq.ts'; +import { Matrix4 } from 'three'; + +export const pointCloudModelOptions = { + modelId: 321, + revisionId: 654 +}; + +export const pointCloudMock = new Mock() + .setup((p) => p.modelId) + .returns(pointCloudModelOptions.modelId) + .setup((p) => p.revisionId) + .returns(pointCloudModelOptions.revisionId) + .setup((p) => p.getModelTransformation()) + .returns(new Matrix4()) + .object(); diff --git a/react-components/tests/unit-tests/fixtures/viewer.ts b/react-components/tests/unit-tests/fixtures/viewer.ts new file mode 100644 index 00000000000..c86f1401729 --- /dev/null +++ b/react-components/tests/unit-tests/fixtures/viewer.ts @@ -0,0 +1,23 @@ +import { vi } from 'vitest'; + +import { Cognite3DViewer, CogniteModel, Image360Collection } from '@cognite/reveal'; +import { Mock, It } from 'moq.ts'; + +const domElement = document.createElement('div').appendChild(document.createElement('canvas')); + +export const viewerModelsMock = vi.fn<[], CogniteModel[]>(); +export const viewerRemoveModelsMock = vi.fn<[CogniteModel], void>(); +export const viewerImage360CollectionsMock = vi.fn<[], Image360Collection[]>(); + +export const viewerMock = new Mock() + .setup((viewer) => viewer.setBackgroundColor(It.IsAny())) + .returns() + .setup((viewer) => viewer.domElement) + .returns(domElement) + .setup((p) => p.models) + .callback(viewerModelsMock) + .setup((p) => p.get360ImageCollections()) + .callback(viewerImage360CollectionsMock) + .setup((p) => p.removeModel) + .returns(viewerRemoveModelsMock) + .object();