Skip to content

Commit

Permalink
VYZN: Matrix Widget API: Hiding of one or more elements programmatica…
Browse files Browse the repository at this point in the history
…lly (#704)

* feat: hiding elements programmatically [widget api]

* uncomment cypress tests

---------

Co-authored-by: Ibrahim Saad <ibrahim.saad@xbim.net>
Co-authored-by: Pablo Mayrgundter <pablo.mayrgundter@gmail.com>
  • Loading branch information
3 people committed Apr 11, 2023
1 parent 04a8a22 commit bcf0c24
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 17 deletions.
116 changes: 116 additions & 0 deletions cypress/e2e/integration/bldrs-inside-iframe.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,120 @@ describe('bldrs inside iframe', () => {

cy.get('@iframe').findByRole('dialog', {timeout: 300000}).should('exist')
})

it('should hide element when HideElements-message emitted', () => {
cy.get('@iframe').trigger('keydown', {keyCode: KEYCODE_ESC})
cy.get('#lastMessageReceivedAction').contains(/ModelLoaded/i)
const globalId = '02uD5Qe8H3mek2PYnMWHk1'

// send a hide elements message
cy.get('#txtSendMessageType').clear().type('ai.bldrs-share.HideElements')
const msg = {
globalIds: [globalId],
}
cy.get('#txtSendMessagePayload').clear().type(JSON.stringify(msg), {parseSpecialCharSequences: false})
cy.get('#btnSendMessage').click()

// trying to select the hidden element
cy.get('#txtSendMessageType').clear().type('ai.bldrs-share.SelectElements')
cy.get('#btnSendMessage').click()

// hidden elements can't be selected
cy.get('#lastMessageReceivedAction').should('not.include.text', /SelectionChanged/i)
})

it('should unhide element when HideElements-message emitted', () => {
cy.get('@iframe').trigger('keydown', {keyCode: KEYCODE_ESC})
cy.get('#lastMessageReceivedAction').contains(/ModelLoaded/i)
const globalId = '02uD5Qe8H3mek2PYnMWHk1'

// send a hide elements message
cy.get('#txtSendMessageType').clear().type('ai.bldrs-share.HideElements')
const msg = {
globalIds: [globalId],
}
cy.get('#txtSendMessagePayload').clear().type(JSON.stringify(msg), {parseSpecialCharSequences: false})
cy.get('#btnSendMessage').click()

// Unhide the hidden element
cy.get('#txtSendMessageType').clear().type('ai.bldrs-share.UnhideElements')
cy.get('#btnSendMessage').click()

// Can be selected again
cy.get('#txtSendMessageType').clear().type('ai.bldrs-share.SelectElements')
cy.get('#btnSendMessage').click()
cy.get('#lastMessageReceivedAction').contains(/SelectionChanged/i)

cy.get('#txtLastMsg').should(($txtLastMsg) => {
const lastMsg = JSON.parse($txtLastMsg.val())
assert.equal(lastMsg.api, 'fromWidget')
assert.equal(lastMsg.widgetId, 'bldrs-share')
assert.exists(lastMsg.requestId)
assert.exists(lastMsg.data)
assert.equal(lastMsg.action, 'ai.bldrs-share.SelectionChanged')
assert.equal(lastMsg.data['current'].length, 1)
assert.equal(lastMsg.data['current'][0], globalId)
})
})

it('should unhide all elements when HideElements-message emitted with wildcard', () => {
cy.get('@iframe').trigger('keydown', {keyCode: KEYCODE_ESC})
cy.get('#lastMessageReceivedAction').contains(/ModelLoaded/i)
const globalId = '02uD5Qe8H3mek2PYnMWHk1'

// send a hide elements message
cy.get('#txtSendMessageType').clear().type('ai.bldrs-share.HideElements')
const msg = {
globalIds: [globalId],
}
cy.get('#txtSendMessagePayload').clear().type(JSON.stringify(msg), {parseSpecialCharSequences: false})
cy.get('#btnSendMessage').click()

// Unhide the hidden element
cy.get('#txtSendMessageType').clear().type('ai.bldrs-share.UnhideElements')
const hidemsg = {
globalIds: '*',
}
cy.get('#txtSendMessagePayload').clear().type(JSON.stringify(hidemsg), {parseSpecialCharSequences: false})
cy.get('#btnSendMessage').click()

// Can be selected again
cy.get('#txtSendMessageType').clear().type('ai.bldrs-share.SelectElements')
cy.get('#txtSendMessagePayload').clear().type(JSON.stringify(msg), {parseSpecialCharSequences: false})
cy.get('#btnSendMessage').click()
cy.get('#lastMessageReceivedAction').contains(/SelectionChanged/i)
})

it('should emit HiddenElments message when element is hidden', () => {
const hiddenElementsCount = 10
cy.get('@iframe').trigger('keydown', {keyCode: KEYCODE_ESC})
cy.get('#lastMessageReceivedAction').contains(/ModelLoaded/i)

// send a hide elements message
cy.get('@iframe').findByRole('tree', {label: 'IFC Navigator'}).click()
cy.get('@iframe').findByTestId('hide-icon').should('exist')
cy.get('@iframe').findByTestId('hide-icon').click()

cy.get('#txtLastMsg').should(($txtLastMsg) => {
const response = JSON.parse($txtLastMsg.val())
assert.equal(response.api, 'fromWidget')
assert.equal(response.widgetId, 'bldrs-share')
assert.exists(response.requestId)
assert.exists(response.data)
assert.equal(response.action, 'ai.bldrs-share.HiddenElements')
assert.equal(response.data['current'].length, hiddenElementsCount)
})

cy.get('@iframe').findByTestId('hide-icon').click()

cy.get('#txtLastMsg').should(($txtLastMsg) => {
const msg = JSON.parse($txtLastMsg.val())
assert.equal(msg.api, 'fromWidget')
assert.equal(msg.widgetId, 'bldrs-share')
assert.exists(msg.requestId)
assert.exists(msg.data)
assert.equal(msg.action, 'ai.bldrs-share.HiddenElements')
assert.equal(msg.data['current'].length, 0)
})
})
})
9 changes: 9 additions & 0 deletions cypress/fixtures/bldrs-inside-iframe.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class BldrsWidgetDriver {
*/
const EVENT_CLIENT_SELECTIONCHANGED_ELEMENTS = 'ai.bldrs-share.SelectionChanged'
const EVENT_CLIENT_MODEL_LOADED = 'ai.bldrs-share.ModelLoaded'
const EVENT_CLIENT_HIDDEN_ELEMENTS = 'ai.bldrs-share.HiddenElements'

document.addEventListener("DOMContentLoaded", function(event) {
const container = document.getElementById('bldrs-widget-iframe')
Expand Down Expand Up @@ -123,6 +124,14 @@ ListenToApiAction(EVENT_CLIENT_MODEL_LOADED,
}
)

ListenToApiAction(EVENT_CLIENT_HIDDEN_ELEMENTS,
event=>
{
txtLastMsg.value = JSON.stringify(event.detail??"")
}
)


btnSendMessage.addEventListener('click', () => {
const messageType = txtSendMessageType.value
const messagePayload = JSON.parse(txtSendMessagePayload.value)
Expand Down
16 changes: 8 additions & 8 deletions src/Infrastructure/IfcIsolator.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default class IfcIsolator {
isolationSubset = null
revealedElementsSubset = null
currentSelectionSubsets = []
ids = []
visualElementsIds = []
spatialStructure = {}
hiddenIds = []
isolatedIds = []
Expand Down Expand Up @@ -58,7 +58,7 @@ export default class IfcIsolator {
*/
async setModel(ifcModel) {
this.ifcModel = ifcModel
this.ids = [...new Set(ifcModel.geometry.attributes.expressID.array)]
this.visualElementsIds = [...new Set(ifcModel.geometry.attributes.expressID.array)]
const rootElement = await this.ifcModel.ifcManager.getSpatialStructure(0, false)
this.collectSpatialElementsId(rootElement)
}
Expand Down Expand Up @@ -160,7 +160,7 @@ export default class IfcIsolator {
const hiddenIdsObject = Object.fromEntries(
this.hiddenIds.map((id) => [id, true]))
useStore.setState({hiddenElements: hiddenIdsObject})
const toBeShown = this.ids.filter((el) => !this.hiddenIds.includes(el))
const toBeShown = this.visualElementsIds.filter((el) => !this.hiddenIds.includes(el))
this.initHideOperationsSubset(toBeShown)
useStore.setState({selectedElements: []})
this.viewer.setSelection(0, [], false)
Expand Down Expand Up @@ -191,7 +191,7 @@ export default class IfcIsolator {
} else {
return
}
const toBeShown = this.ids.filter((el) => !this.hiddenIds.includes(el))
const toBeShown = this.visualElementsIds.filter((el) => !this.hiddenIds.includes(el))
this.initHideOperationsSubset(toBeShown)
const selection = useStore.getState().selectedElements.filter((el) => !this.hiddenIds.includes(Number(el)))
useStore.setState({selectedElements: selection})
Expand Down Expand Up @@ -230,7 +230,7 @@ export default class IfcIsolator {
if (this.hiddenIds.length === 0) {
this.unHideAllElements()
} else {
const toBeShown = this.ids.filter((el) => !this.hiddenIds.includes(el))
const toBeShown = this.visualElementsIds.filter((el) => !this.hiddenIds.includes(el))
this.initHideOperationsSubset(toBeShown)
}
const selection = useStore.getState().selectedElements.map((e) => Number(e))
Expand Down Expand Up @@ -274,7 +274,7 @@ export default class IfcIsolator {
} else {
let hidden = this.hiddenIds
if (this.tempIsolationModeOn) {
hidden = hidden.concat(this.ids.filter((e) => !this.isolatedIds.includes(e)))
hidden = hidden.concat(this.visualElementsIds.filter((e) => !this.isolatedIds.includes(e)))
}
if (hidden.length === 0) {
this.context.getScene().remove(this.revealedElementsSubset)
Expand Down Expand Up @@ -313,7 +313,7 @@ export default class IfcIsolator {
* @return {boolean} true if can be hidden, otherwise false
*/
canBeHidden(elementId) {
return this.ids.includes(elementId) || Object.keys(this.spatialStructure).includes(`${elementId}`)
return this.visualElementsIds.includes(elementId) || Object.keys(this.spatialStructure).includes(`${elementId}`)
}

/**
Expand Down Expand Up @@ -366,7 +366,7 @@ export default class IfcIsolator {
this.context.items.pickableIfcModels.pop()
delete this.isolationSubset
if (this.hiddenIds.length > 0) {
const toBeShown = this.ids.filter((el) => !this.hiddenIds.includes( el ))
const toBeShown = this.visualElementsIds.filter((el) => !this.hiddenIds.includes( el ))
this.initHideOperationsSubset(toBeShown, false)
} else {
this.context.getScene().add(this.ifcModel)
Expand Down
7 changes: 7 additions & 0 deletions src/WidgetApi/ApiEventsRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import LoadModelEventHandler from './event-handlers/LoadModelEventHandler'
import SelectElementsEventHandler from './event-handlers/SelectElementsEventHandler'
import UIComponentsVisibilityEventHandler from './event-handlers/UIComponentsVisibilityEventHandler'
import SuppressAboutDialogHandler from './event-handlers/SuppressAboutDialogHandler'
import HideElementsEventHandler from './event-handlers/HideElementsEventHandler'
import UnhideElementsEventHandler from './event-handlers/UnhideElementsEventHandler'

import ElementSelectionChangedEventDispatcher from './event-dispatchers/ElementSelectionChangedEventDispatcher'
import ModelLoadedEventDispatcher from './event-dispatchers/ModelLoadedEventDispatcher'
import HiddenElementsEventDispatcher from './event-dispatchers/HiddenElementsEventDispatcher'

/**
* Api Events are defined here
Expand Down Expand Up @@ -39,6 +43,8 @@ class ApiEventsRegistry {
new SelectElementsEventHandler(this.apiConnection, this.searchIndex),
new UIComponentsVisibilityEventHandler(this.apiConnection),
new SuppressAboutDialogHandler(this.apiConnection),
new HideElementsEventHandler(this.apiConnection, this.searchIndex),
new UnhideElementsEventHandler(this.apiConnection, this.searchIndex),
]
for (const event of events) {
this.apiConnection.on(`action:${event.name}`, event.handler.bind(event))
Expand All @@ -53,6 +59,7 @@ class ApiEventsRegistry {
const events = [
new ElementSelectionChangedEventDispatcher(this.apiConnection, this.searchIndex),
new ModelLoadedEventDispatcher(this.apiConnection),
new HiddenElementsEventDispatcher(this.apiConnection, this.searchIndex),
]
this.apiConnection.requestCapabilities(events.map((e) => e.name))
for (const eventDispatcher of events) {
Expand Down
12 changes: 6 additions & 6 deletions src/WidgetApi/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ export default class Utils {
}

/**
* get ids of selected elements.
* get global ids of elements.
*
* @param {object} state
* @return {string[]} array of GlobalIds.
* @param {string[]} array of express ids
* @return {string[]} array of global ids
*/
getSelectedElementIds(state) {
getElementsGlobalIds(elementsExpressIds) {
const elementIds = []
if (state.selectedElements === null || state.selectedElements.length === 0) {
if (elementsExpressIds === null || elementsExpressIds.length === 0) {
return elementIds
}

for (const expressId of state.selectedElements) {
for (const expressId of elementsExpressIds) {
const globalId = this.searchIndex.getGlobalIdByExpressId(expressId)
if (globalId) {
elementIds.push(globalId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ElementSelectionChangedEventDispatcher extends ApiEventDispatcher {
if (!propertyStateChanged) {
return
}
const currSelectedItemsGlobalIds = this.utils.getSelectedElementIds(state)
const currSelectedItemsGlobalIds = this.utils.getElementsGlobalIds(state.selectedElements)
const noChanges = unsortedArraysAreEqual(currSelectedItemsGlobalIds, lastSelectedElementGlobalIds)
if (noChanges) {
return
Expand Down
56 changes: 56 additions & 0 deletions src/WidgetApi/event-dispatchers/HiddenElementsEventDispatcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import ApiEventDispatcher from './ApiEventDispatcher'
import Utils from '../Utils'
import useStore from '../../store/useStore'
import {unsortedArraysAreEqual} from '../../utils/arrays'

/**
* class HiddenElementsEventDispatcher
*/
class HiddenElementsEventDispatcher extends ApiEventDispatcher {
name = 'ai.bldrs-share.HiddenElements'
utils = null
apiConnection = null

/**
* constructor
*
* @param {object} apiConnection AbstractApiConnection
* @param {object} searchIndex SearchIndex
*/
constructor(apiConnection, searchIndex) {
super()
this.apiConnection = apiConnection
this.utils = new Utils(searchIndex)
}

/**
* initialize dispatcher.
*
*/
initDispatch() {
let lastHiddenElementsGlobalIds = []
useStore.subscribe((state, previousState) => {
const propertyStateChanged = (state.hiddenElements !== previousState.hiddenElements)
if (!propertyStateChanged) {
return
}
const hiddenElmentsExpressIds = []
Object.entries(state.hiddenElements).forEach((entry) => {
const [key, value] = entry
if (value === true) {
hiddenElmentsExpressIds.push(key)
}
})
const currHiddenElementsGlobalIds = this.utils.getElementsGlobalIds(hiddenElmentsExpressIds)
const noChanges = unsortedArraysAreEqual(currHiddenElementsGlobalIds, lastHiddenElementsGlobalIds)
if (noChanges) {
return
}
const eventData = {previous: lastHiddenElementsGlobalIds, current: currHiddenElementsGlobalIds}
this.apiConnection.send(this.name, eventData)
lastHiddenElementsGlobalIds = currHiddenElementsGlobalIds
})
}
}

export default HiddenElementsEventDispatcher
55 changes: 55 additions & 0 deletions src/WidgetApi/event-handlers/HideElementsEventHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import useStore from '../../store/useStore'
import ApiEventHandler from './ApiEventHandler'
/**
* Hide Elements API event handler
*/
class HideElementsEventHandler extends ApiEventHandler {
apiConnection = null
name = 'ai.bldrs-share.HideElements'


/**
* constructor
*
* @param {object} apiConnection AbstractApiConnection
* @param {object} searchIndex SearchIndex
*/
constructor(apiConnection, searchIndex) {
super()
this.apiConnection = apiConnection
this.searchIndex = searchIndex
}

/**
* The handler for this event
*
* @param {object} data the event associated data
* @return {object} the response of the API call
*/
handler(data) {
if (!('globalIds' in data)) {
return this.apiConnection.missingArgumentResponse('globalIds')
}

if (data.globalIds === null) {
return this.apiConnection.invalidOperationResponse('globalIds can\'t be null')
}

const expressIds = []

if (data.globalIds.length) {
for (const globalId of data.globalIds) {
const expressId = this.searchIndex.getExpressIdByGlobalId(globalId)
if (expressId) {
expressIds.push(expressId)
}
}
}

useStore.getState().viewerStore.isolator.hideElementsById(expressIds.map((id) => Number(id)))

return this.apiConnection.successfulResponse({})
}
}

export default HideElementsEventHandler

0 comments on commit bcf0c24

Please sign in to comment.