Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VYZN: Matrix Widget API: Hiding of one or more elements programmatically #704

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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