Skip to content

Commit

Permalink
VYZN: (feature) hide/unhide and temporary isolate IFC elements (#617)
Browse files Browse the repository at this point in the history
* feat: custom viewer api

* subset list to dict

* remove log statement

* fix missing __getIFCViewerAPIMockSingleton

* pickByID new implementation

* fix: remove resetSelection guard to fix key down callback function lost state

* fix: reset selection guard back to fix failing tests and call the from the call back func changed

* subsets revisited

* unhide all hidden elements

* feature: ifc isolator component, incapsulates hide and isolate operations

* fix: ids loaded from meshes to insure having visual props

* fix: remove unused ifc classes imports

* fix: get express ids from model mesh  attributes for better performance

* fix: reset reveal upon unhide

* fix: turn off reveal on unhide all

* feat: hide/unhide icons in nav tree for spatial structures and root aggregators

* fix: failing unit tests

* linter fixes

* fix: reveal hidden elements material's depthTest

* re-evaluate reveal subset if on when hiding again

* fix: ifc viewer highlighter

* remove unused imports

* fix: disable hide icons on temp isolation mode

* reflect isoaltor status to selected elements store

* unit test for hide element by the tree icons

* toggle hide icon e2e test

* fix post processor

* fix failing cadview tests

* fix: placemarks broke the other post processing effects

* fix: remove singleton post-processor

* fix: space typo

* fix: disable lint error

* fix: maintain hidden elements when viewer changes

* fix: align hide icons to right

---------

Signed-off-by: Pablo Mayrgundter <pablo.mayrgundter@gmail.com>
Co-authored-by: Ibrahim Saad <ibrahim.saad@xbim.net>
Co-authored-by: OlegMoshkovich <oleg.mosh@gmail.com>
Co-authored-by: Pablo Mayrgundter <pablo.mayrgundter@gmail.com>
  • Loading branch information
4 people committed Mar 24, 2023
1 parent 9ca69dd commit ca07d4d
Show file tree
Hide file tree
Showing 23 changed files with 793 additions and 122 deletions.
17 changes: 12 additions & 5 deletions __mocks__/web-ifc-viewer.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
jest.mock('three')
jest.mock('../src/Infrastructure/IfcHighlighter')
jest.mock('../src/Infrastructure/IfcIsolator')
jest.mock('../src/Infrastructure/CustomPostProcessor')
const ifcjsMock = jest.createMockFromModule('web-ifc-viewer')


// Not sure why this is required, but otherwise these internal fields
// are not present in the instantiated IfcViewerAPI.
// are not present in the instantiated IfcViewerAPIExtended.
const loadedModel = {
ifcManager: {
getSpatialStructure: jest.fn(),
Expand All @@ -15,6 +17,9 @@ const loadedModel = {
boundingBox: {
getCenter: jest.fn(),
},
attributes: {
expressID: 123,
},
},
}

Expand Down Expand Up @@ -46,7 +51,6 @@ const impl = {
},
},
},
loadIfcUrl: jest.fn(jest.fn(() => loadedModel)),
setWasmPath: jest.fn(),
selector: {
unpickIfcItems: jest.fn(),
Expand Down Expand Up @@ -87,13 +91,16 @@ const impl = {
getRenderer: jest.fn(),
getScene: jest.fn(),
getCamera: jest.fn(),
getClippingPlanes: jest.fn(() => {
return []
}),
},
loadIfcUrl: jest.fn(jest.fn(() => loadedModel)),
getProperties: jest.fn((modelId, eltId) => {
return loadedModel.ifcManager.getProperties(eltId)
}),
setSelection: jest.fn(),
pickIfcItemsByID: jest.fn(),
loadIfcUrl: jest.fn(jest.fn(() => loadedModel)),
}
const constructorMock = ifcjsMock.IfcViewerAPI
constructorMock.mockImplementation(() => impl)
Expand All @@ -102,13 +109,13 @@ constructorMock.mockImplementation(() => impl)
/**
* @return {object} The single mock instance of IfcViewerAPI.
*/
function __getIfcViewerAPIMockSingleton() {
function __getIfcViewerAPIExtendedMockSingleton() {
return impl
}


export {
ifcjsMock as default,
constructorMock as IfcViewerAPI,
__getIfcViewerAPIMockSingleton,
__getIfcViewerAPIExtendedMockSingleton as __getIfcViewerAPIExtendedMockSingleton,
}
15 changes: 15 additions & 0 deletions cypress/e2e/hide-feat/hide-feat.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
describe('Ifc Hide/Unhide E2E test suite', () => {
context('Hide icon toggle', () => {
beforeEach(() => {
cy.setCookie('isFirstTime', 'false')
cy.visit('/')
})

it('should toggle hide icon when clicked', () => {
cy.findByTestId('hide-icon').should('exist')
cy.findByTestId('hide-icon').should('have.attr', 'data-icon', 'eye')
cy.findByTestId('hide-icon').realClick()
cy.findByTestId('hide-icon').should('have.attr', 'data-icon', 'eye-slash')
})
})
})
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bldrs",
"version": "1.0.0-r658",
"version": "1.0.0-r677",
"main": "src/index.jsx",
"license": "MIT",
"homepage": "https://github.com/bldrs-ai/Share",
Expand Down Expand Up @@ -33,6 +33,9 @@
"@bldrs-ai/ifclib": "^5.3.3",
"@emotion/react": "^11.10.0",
"@emotion/styled": "^11.10.0",
"@fortawesome/fontawesome-svg-core": "^6.3.0",
"@fortawesome/free-solid-svg-icons": "^6.3.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@iconscout/react-unicons": "^1.1.6",
"@mui/icons-material": "^5.11.9",
"@mui/lab": "^5.0.0-alpha.95",
Expand Down
2 changes: 2 additions & 0 deletions src/Components/About/AboutControl.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export default function AboutControl() {
const setIsDialogDisplayedLocal = (value) => {
setIsDialogDisplayed(value)
}

// eslint-disable-next-line no-unused-vars
const setIsDialogDisplayedForDialog = () => {
setIsDialogDisplayed(false)
setCookieBoolean({component: 'about', name: 'isFirstTime', value: false})
Expand Down
4 changes: 2 additions & 2 deletions src/Components/CameraControl.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import {act, render, screen, renderHook} from '@testing-library/react'
import useStore from '../store/useStore'
import ShareMock from '../ShareMock'
import {__getIfcViewerAPIMockSingleton} from 'web-ifc-viewer'
import {__getIfcViewerAPIExtendedMockSingleton} from 'web-ifc-viewer'
import CameraControl, {
onHash,
parseHashParams,
Expand All @@ -22,7 +22,7 @@ describe('CameraControl', () => {

it('CameraControl', async () => {
const {result} = renderHook(() => useStore((state) => state))
const viewer = __getIfcViewerAPIMockSingleton()
const viewer = __getIfcViewerAPIExtendedMockSingleton()
await act(() => {
result.current.setViewerStore(viewer)
})
Expand Down
10 changes: 5 additions & 5 deletions src/Components/CutPlaneMenu.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ShareControl from './ShareControl'
import ShareMock from '../ShareMock'
import useStore from '../store/useStore'
import model from '../__mocks__/MockModel.js'
import {__getIfcViewerAPIMockSingleton} from 'web-ifc-viewer'
import {__getIfcViewerAPIExtendedMockSingleton} from 'web-ifc-viewer'


jest.mock('three')
Expand All @@ -32,7 +32,7 @@ describe('CutPlaneMenu', () => {
const {getByTitle, getByText} = render(<ShareMock><CutPlaneMenu/></ShareMock>)
const sectionButton = getByTitle('Section')
const {result} = renderHook(() => useStore((state) => state))
const viewer = __getIfcViewerAPIMockSingleton()
const viewer = __getIfcViewerAPIExtendedMockSingleton()
await act(() => {
result.current.setViewerStore(viewer)
})
Expand All @@ -55,7 +55,7 @@ describe('CutPlaneMenu', () => {
<CutPlaneMenu/>
</ShareMock>)
const {result} = renderHook(() => useStore((state) => state))
const viewer = __getIfcViewerAPIMockSingleton()
const viewer = __getIfcViewerAPIExtendedMockSingleton()
await act(() => {
result.current.setViewerStore(viewer)
})
Expand All @@ -71,7 +71,7 @@ describe('CutPlaneMenu', () => {
</ShareMock>)
const {result} = renderHook(() => useStore((state) => state))
// mock contains one plane
const viewer = __getIfcViewerAPIMockSingleton()
const viewer = __getIfcViewerAPIExtendedMockSingleton()
await act(() => {
result.current.setViewerStore(viewer)
})
Expand All @@ -83,7 +83,7 @@ describe('CutPlaneMenu', () => {

it('Plane Offset is correct', async () => {
const {result} = renderHook(() => useStore((state) => state))
const viewer = __getIfcViewerAPIMockSingleton()
const viewer = __getIfcViewerAPIExtendedMockSingleton()
await act(() => {
result.current.setViewerStore(viewer)
result.current.setModelStore(model)
Expand Down
70 changes: 59 additions & 11 deletions src/Components/NavTree.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import React from 'react'
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import {faEye, faEyeSlash, faGlasses} from '@fortawesome/free-solid-svg-icons'
import clsx from 'clsx'
import PropTypes from 'prop-types'
import {reifyName} from '@bldrs-ai/ifclib'
import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import TreeItem, {useTreeItem} from '@mui/lab/TreeItem'
import useStore from '../store/useStore'
import IfcIsolator from '../Infrastructure/IfcIsolator'


const NavTreePropTypes = {
Expand Down Expand Up @@ -36,8 +40,43 @@ const NavTreePropTypes = {
* The id of the node.
*/
nodeId: PropTypes.string.isRequired,
/**
* Determines if the tree node has a hide icon.
*/
hasHideIcon: PropTypes.bool,
}

/**
* @param {IfcIsolator} The IFC isoaltor
* @param {number} IFC element id
* @return {object} React component
*/
function HideIcon({elementId}) {
const isHidden = useStore((state) => state.hiddenElements[elementId])
const isIsolated = useStore((state) => state.isolatedElements[elementId])
const isTempIsolationModeOn = useStore((state) => state.isTempIsolationModeOn)
const viewer = useStore((state) => state.viewerStore)

const toggleHide = () => {
const toBeHidden = viewer.isolator.flattenChildren(elementId)
if (!isHidden) {
viewer.isolator.hideElementsById(toBeHidden)
} else {
viewer.isolator.unHideElementsById(toBeHidden)
}
}

const iconStyle = {float: 'right', margin: '4px'}
if (isTempIsolationModeOn) {
iconStyle.pointerEvents = 'none'
if (!isIsolated) {
iconStyle.opacity = 0.4
}
}

const icon = isIsolated ? faGlasses : (!isHidden ? faEye : faEyeSlash)
return <FontAwesomeIcon data-testid='hide-icon' style={iconStyle} onClick={toggleHide} icon={icon}/>
}

/**
* @param {object} model IFC model
Expand All @@ -61,6 +100,7 @@ export default function NavTree({
icon: iconProp,
expansionIcon,
displayIcon,
hasHideIcon,
} = props

const {
Expand All @@ -84,7 +124,6 @@ export default function NavTree({
selectWithShiftClickEvents(event.shiftKey, element.expressID)
}


return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
Expand All @@ -103,34 +142,43 @@ export default function NavTree({
>
{icon}
</Box>
<Typography
variant='tree'
onClick={handleSelectionClick}
>
{label}
</Typography>
<div style={{width: '300px'}}>
<Typography
variant='tree'
onClick={handleSelectionClick}
>
{label}
</Typography>
{hasHideIcon &&
<div style={{display: 'contents'}}>
<HideIcon elementId={element.expressID}/>
</div>
}
</div>
</div>
)
})


CustomContent.propTypes = NavTreePropTypes


const CustomTreeItem = (props) => {
return <TreeItem ContentComponent={CustomContent} {...props}/>
}

const viewer = useStore((state) => state.viewerStore)

let i = 0

const hasHideIcon = viewer.isolator.canBeHidden(element.expressID)

let i = 0
// TODO(pablo): Had to add this React.Fragment wrapper to get rid of
// warning about missing a unique key foreach item. Don't really understand it.
return (
<CustomTreeItem
nodeId={element.expressID.toString()}
label={reifyName({properties: model}, element)}
ContentProps={{
hasHideIcon: hasHideIcon,
}}
>
{element.children && element.children.length > 0 ?
element.children.map((child) => {
Expand Down
43 changes: 39 additions & 4 deletions src/Components/NavTree.test.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React from 'react'
import {render, act, fireEvent} from '@testing-library/react'
import {act, render, renderHook, fireEvent} from '@testing-library/react'
import useStore from '../store/useStore'
import ShareMock from '../ShareMock'
import {MockViewer, newMockStringValueElt} from '../utils/IfcMock.test'
import {newMockStringValueElt} from '../utils/IfcMock.test'
import NavTree from './NavTree'
import {IfcViewerAPIExtended} from '../Infrastructure/IfcViewerAPIExtended'
import {actAsyncFlush} from '../utils/tests'


jest.mock('@mui/lab/TreeItem', () => {
Expand All @@ -22,18 +25,50 @@ jest.mock('@mui/lab/TreeItem', () => {
})

describe('NavTree', () => {
test('NavTree for single element', () => {
it('NavTree for single element', async () => {
const testLabel = 'Test node label'
const {result} = renderHook(() => useStore((state) => state))
const viewer = new IfcViewerAPIExtended()
await act(() => {
result.current.setViewerStore(viewer)
})
const {getByText} = render(
<ShareMock>
<NavTree
viewer={new MockViewer}
element={newMockStringValueElt(testLabel)}
/>
</ShareMock>)
await actAsyncFlush()
expect(getByText(testLabel)).toBeInTheDocument()
})

it('Can hide element by eye icon', async () => {
const selectElementsMock = jest.fn()
const testLabel = 'Test node label'
const ifcElementMock = newMockStringValueElt(testLabel)
const {result} = renderHook(() => useStore((state) => state))
const viewer = new IfcViewerAPIExtended()
await act(() => {
result.current.setViewerStore(viewer)
result.current.updateHiddenStatus(1, false)
})
viewer.isolator.canBeHidden.mockReturnValue(true)
viewer.isolator.flattenChildren.mockReturnValue([ifcElementMock.expressID])
const {getByText, getByTestId} = render(
<NavTree
element={ifcElementMock}
pathPrefix={'/share/v/p/index.ifc'}
selectWithShiftClickEvents={selectElementsMock}
/>)
const root = await getByText(testLabel)
const hideIcon = await getByTestId('hide-icon')
expect(root).toBeInTheDocument()
expect(hideIcon).toBeInTheDocument()
fireEvent.click(hideIcon)
expect(viewer.isolator.canBeHidden.mock.calls).toHaveLength(1)
expect(viewer.isolator.hideElementsById).toHaveBeenLastCalledWith([ifcElementMock.expressID])
})

it('should select element on click', async () => {
const selectElementsMock = jest.fn()
const testLabel = 'Test node label'
Expand Down

0 comments on commit ca07d4d

Please sign in to comment.