Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bldrs",
"version": "1.0.1877",
"version": "1.0.1891",
"main": "src/index.jsx",
"license": "AGPL-3.0",
"homepage": "https://github.com/bldrs-ai/Share",
Expand Down Expand Up @@ -66,7 +66,7 @@
"@babel/plugin-syntax-import-assertions": "7.18.6",
"@babel/preset-env": "7.18.10",
"@babel/preset-react": "7.18.6",
"@bldrs-ai/conway-web-ifc-adapter": "0.23.954-2",
"@bldrs-ai/conway-web-ifc-adapter": "0.23.977-2",
"@bldrs-ai/ifclib": "5.3.3",
"@emotion/react": "11.10.0",
"@emotion/styled": "11.10.0",
Expand Down
1 change: 1 addition & 0 deletions src/Components/NavTree/NavTreeNode.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default function NavTreeNode({
alignItems: 'flex-start', // Align items at the top for multiline labels
backgroundColor: isSelected ? theme.palette.secondary.selected : 'transparent',
cursor: 'pointer',
userSelect: 'none', // Prevent text selection to avoid Google Translate popup
}}
>
{/* Expand/Collapse Icon */}
Expand Down
86 changes: 75 additions & 11 deletions src/Components/NavTree/NavTreePanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,50 @@ export default function NavTreePanel({
const treeData = isNavTree ? rootElement : elementTypesMap

const [visibleNodes, setVisibleNodes] = useState([])
// Cache for materialized children (including geometric parts)
const [materializedChildren, setMaterializedChildren] = useState({})

// Map to store item heights
const itemHeights = useRef({})

// Materialize children when nodes are expanded
useEffect(() => {
if (!isNavTree || !model?.getChildren) {
return
}
setMaterializedChildren((prev) => {
const newMaterialized = {...prev}
for (const nodeId of expandedNodeIds) {
if (!newMaterialized[nodeId]) {
// Start async materialization
const elementID = parseInt(nodeId, 10)
if (Number.isFinite(elementID)) {
model.getChildren(elementID).then((children) => {
setMaterializedChildren((current) => ({...current, [nodeId]: children}))
}).catch((error) => {
console.warn('Failed to materialize children for node:', nodeId, error)
setMaterializedChildren((current) => ({...current, [nodeId]: []}))
})
}
}
}
return newMaterialized
})
}, [expandedNodeIds, isNavTree, model])

// Flatten the tree into a list of visible nodes
useEffect(() => {
const nodes = getVisibleNodes(treeData, expandedNodeIds, isNavTree, model)
const nodes = getVisibleNodes(treeData, expandedNodeIds, isNavTree, model, viewer, materializedChildren)
setVisibleNodes(nodes)
}, [treeData, expandedNodeIds, isNavTree, model])
}, [treeData, expandedNodeIds, isNavTree, model, viewer, materializedChildren])

// Scroll to selected element
// Scroll to selected element (only when selection changes, not when visibleNodes changes)
const prevSelectedRef = useRef(selectedElements[0])
useEffect(() => {
const nodeId = selectedElements[0]
if (nodeId) {
// Only scroll if selection actually changed
if (nodeId && nodeId !== prevSelectedRef.current) {
prevSelectedRef.current = nodeId
const index = visibleNodes.findIndex(
({node}) => node.expressID && node.expressID.toString() === nodeId,
)
Expand Down Expand Up @@ -177,9 +207,11 @@ export default function NavTreePanel({
* @param {Array} expandedNodeIds - IDs of expanded nodes
* @param {boolean} isNavTree - Whether this is a nav tree
* @param {object} model - The model object
* @param {object} viewer - The viewer instance
* @param {object} materializedChildren - Map of nodeId to materialized children
* @return {Array} nodes
*/
function getVisibleNodes(treeData, expandedNodeIds, isNavTree, model) {
function getVisibleNodes(treeData, expandedNodeIds, isNavTree, model, viewer, materializedChildren = {}) {
const visibleNodes = []

/**
Expand All @@ -205,12 +237,41 @@ function getVisibleNodes(treeData, expandedNodeIds, isNavTree, model) {
* @return {object} node
*/
function mapSpatialNode(node) {
const nodeId = node.expressID.toString()
const baseChildren = node.children ? node.children.map(mapSpatialNode) : []

// Merge materialized children (including geometric parts) if available
// Only materialize if baseChildren is empty (for lazy loading geometric parts in IFC)
// For Object3D models, children are already in the tree structure
const materialized = materializedChildren[nodeId] || []
const materializedElements = materialized.map((child) => {
// Preserve all properties from child (including IFC properties for reifyName)
// Don't set label here - let NavTreeNode use reifyName to get proper label
return {
nodeId: child.elementID.toString(),
expressID: child.elementID,
hasChildren: (child.children?.length || 0) > 0,
children: child.children ? child.children.map((c) => ({
nodeId: c.elementID.toString(),
expressID: c.elementID,
hasChildren: false,
children: [],
...c, // Preserve properties for reifyName
})) : [],
...child, // Spread all properties (Name, LongName, etc.) for reifyName to work
}
})

// Only include materialized children if baseChildren is empty (for geometric parts)
// This prevents duplicates when children are already in the tree structure
const allChildren = baseChildren.length > 0 ? baseChildren : materializedElements

return {
nodeId: node.expressID.toString(),
nodeId,
label: reifyName({properties: model}, node),
expressID: node.expressID,
hasChildren: node.children && node.children.length > 0,
children: node.children ? node.children.map(mapSpatialNode) : [],
hasChildren: allChildren.length > 0,
children: allChildren,
}
}

Expand Down Expand Up @@ -257,9 +318,10 @@ const RenderRow = ({index, style, data}) => {
const hasChildren = node.hasChildren
let isSelected = false

if (!hasChildren) {
// For element nodes
isSelected = selectedNodeIds.includes(node.expressID.toString())
// Check if this node is selected by its expressID
if (node.expressID) {
const expressIDStr = node.expressID.toString()
isSelected = selectedNodeIds.includes(expressIDStr)
}

const rowRef = useRef(null)
Expand Down Expand Up @@ -293,7 +355,9 @@ const RenderRow = ({index, style, data}) => {
if (isExpanded) {
setExpandedNodeIds(expandedNodeIds.filter((id) => id !== nodeId))
} else {
// Expand the node first
setExpandedNodeIds([...expandedNodeIds, nodeId])
// Children will be materialized by the useEffect hook
}
}

Expand Down
36 changes: 22 additions & 14 deletions src/Components/Properties/itemProperties.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,26 @@ async function prettyProps(model, propName, propValue, isPset, serial = 0) {
if (propValue.type === 0) {
return null
}
return (
<Row
d1={label}
d2={
await deref(
propValue, model, serial,
// TODO(pablo): there's no 4th param in deref
async (v, mdl, srl) => await createPropertyTable(mdl, v, srl))
}
key={serial}
/>
)
if (!model) {
debug().warn('prettyProps: model is undefined, skipping deref for propName:', propName)
return null
}
try {
const derefValue = await deref(
propValue, model, serial,
// TODO(pablo): there's no 4th param in deref
async (v, mdl, srl) => await createPropertyTable(mdl, v, srl))
return (
<Row
d1={label}
d2={derefValue}
key={serial}
/>
)
} catch (error) {
debug().warn('prettyProps: deref failed for propName:', propName, error.message)
return null
}
}
}
}
Expand Down Expand Up @@ -165,8 +173,8 @@ export async function unpackHelper(model, eltArr, serial, ifcToRowCb) {
throw new Error('Array contains non-reference type')
}
const refId = stoi(p.value)
if (model.getItemProperties) {
const ifcElt = await model.getItemProperties(refId)
if (model.getProperties) {
const ifcElt = await model.getProperties(refId)
ifcToRowCb(ifcElt, rows)
} else {
debug().warn('model has no getProperties method: ', model)
Expand Down
69 changes: 48 additions & 21 deletions src/Containers/CadView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,8 @@ export default function CadView({
assertDefined(m)
debug().log('CadView#onModel', m)
// TODO(pablo): centralize capability check somewhere
if (!m.ifcManager) {
console.warn('CadView#onModel, model without manager:', m)
if (!m.getRootElement) {
console.warn('CadView#onModel, model does not support Model interface:', m)
return
}
window.ondblclick = canvasDoubleClickHandler
Expand All @@ -366,20 +366,26 @@ export default function CadView({
// if we can't read the full model structure.
let rootElt
try {
rootElt = await m.ifcManager.getSpatialStructure(0, true)
rootElt = await m.getRootElement()
} catch (e) {
setAlert('Could not read full model structure. Only model geometry will be available.')
captureException(e, 'Could not read full model structure')
console.error(e)
return
}
debug().log('CadView#onModel: rootElt: ', rootElt)
if (rootElt.expressID === undefined) {
throw new Error('Model has undefined root express ID')

if (rootElt.elementID === undefined && rootElt.expressID === undefined) {
throw new Error('Model has undefined root element ID')
}
// Ensure expressID exists for backward compatibility (should already be set by Model interface)
if (rootElt.expressID === undefined && rootElt.elementID !== undefined) {
rootElt.expressID = rootElt.elementID
}
setupLookupAndParentLinks(rootElt, elementsById)
initSearch(m, rootElt)
const tmpProps = await viewer.getProperties(0, rootElt.expressID)
const elementID = rootElt.elementID ?? rootElt.expressID
const tmpProps = m.getProperties ? await m.getProperties(elementID) : await viewer.getProperties(0, elementID)
const rootProps = tmpProps || {Name: {value: 'Model'}, LongName: {value: 'Model'}}
rootElt.Name = rootProps.Name
rootElt.LongName = rootProps.LongName
Expand Down Expand Up @@ -408,8 +414,10 @@ export default function CadView({
const mesh = picked.object
// TODO(pablo): obsolete? needed this in h3 at some point
viewer.setHighlighted([mesh])
// Get elementID from mesh (expressID on mesh equals elementID for Model interface)
let elementId
if (mesh.expressID !== undefined) {
elementSelection(viewer, elementsById, selectItemsInScene, event.shiftKey, mesh.expressID)
elementId = mesh.expressID
} else {
const geom = mesh.geometry
if (!areDefinedAndNotNull(geom, geom.index)) {
Expand All @@ -418,9 +426,9 @@ export default function CadView({
}
const geoIndex = geom.index.array
const IdAttrName = 'expressID'
const eid = geom.attributes[IdAttrName].getX(geoIndex[3 * picked.faceIndex])
elementSelection(viewer, elementsById, selectItemsInScene, event.shiftKey, eid)
elementId = geom.attributes[IdAttrName].getX(geoIndex[3 * picked.faceIndex])
}
elementSelection(viewer, elementsById, selectItemsInScene, event.shiftKey, elementId)
} catch (e) {
console.error(e)
}
Expand Down Expand Up @@ -529,8 +537,9 @@ export default function CadView({
setSelectedElements(resIds)
// Sets the url to the first selected element path.
if (resultIDs.length > 0 && updateNavigation) {
const firstId = resultIDs.slice(0, 1)
const pathIds = getParentPathIdsForElement(elementsById, parseInt(firstId))
const firstId = parseInt(resultIDs.slice(0, 1)[0])

const pathIds = getParentPathIdsForElement(elementsById, firstId)
const repoFilePath = modelPath.gitpath ? modelPath.getRepoPath() : modelPath.filepath
const enabledFeatures = searchParams.get('feature')
const elementPath = pathIds.join('/')
Expand Down Expand Up @@ -674,17 +683,35 @@ export default function CadView({
if (!Array.isArray(selectedElements) || !viewer) {
return
}
// Update The selection on the scene pick/unpick
const ids = selectedElements.map((id) => parseInt(id))
await viewer.setSelection(0, ids)
// If current selection is not empty
const ids = selectedElements
.map((id) => parseInt(id))
.filter((id) => Number.isFinite(id))
const geometricPartIds = ids.filter((id) => !elementsById[id])
const elementIds = ids.filter((id) => elementsById[id])

try {
if (elementIds.length > 0) {
await viewer.setSelection(0, elementIds)
} else {
await viewer.setSelection(0, [])
}
if (viewer.highlightGeometricParts) {
await viewer.highlightGeometricParts(0, geometricPartIds)
}
} catch (error) {
console.error('Error updating selection:', error.message)
}

if (selectedElements.length > 0) {
// Display the properties of the last one,
const lastId = selectedElements.slice(-1)[0]
const props = await viewer.getProperties(0, Number(lastId))
setSelectedElement(props)
// Update the expanded elements in NavTreePanel
const pathIds = getParentPathIdsForElement(elementsById, parseInt(lastId))
const lastId = parseInt(selectedElements.slice(-1)[0])
try {
const props = await viewer.getProperties(0, lastId)
setSelectedElement(props)
} catch (error) {
debug().log('CadView#selectionEffect: Unable to fetch properties for element:', lastId, error.message)
setSelectedElement(null)
}
const pathIds = getParentPathIdsForElement(elementsById, lastId)
if (pathIds) {
setExpandedElements(pathIds.map((n) => `${n}`))
}
Expand Down
35 changes: 25 additions & 10 deletions src/Containers/selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,52 @@ import {getDescendantExpressIds} from '../utils/TreeUtils'

/**
* Select/Deselect items in the scene using shift+click
* Uses elementID (Model interface abstraction) - elementsById is keyed by elementID
*
* @param {object} viewer
* @param {Map<number,object>} elementsById Express elts by their expressID
* @param {Map<number,object>} elementsById Elements by their elementID
* @param {Function} selectItemsInScene
* @param {boolean} isShiftKeyDown the click event
* @param {number} expressId the express id of the element
* @param {number} elementId the elementID of the element (Model interface)
*/
export function elementSelection(viewer, elementsById, selectItemsInScene, isShiftKeyDown, expressId) {
if (!viewer.isolator.canBePickedInScene(expressId)) {
export function elementSelection(viewer, elementsById, selectItemsInScene, isShiftKeyDown, elementId) {
if (!viewer.isolator.canBePickedInScene(elementId)) {
debug().warn('elementSelection: Element cannot be picked in scene:', elementId)
return
}
const selectedElt = elementsById[expressId]
const selectedElt = elementsById[elementId]
if (!selectedElt) {
debug().error(`selection#getParentPathIdsForElement(${expressId}) missing in table:`, elementsById)
debug().warn('elementSelection: Element not in elementsById, treating as geometric part:', elementId)
// Geometric parts (individual Items) aren't in elementsById, but can still be selected
const selectedInViewer = new Set(viewer.getSelectedIds())
if (isShiftKeyDown) {
if (selectedInViewer.has(elementId)) {
selectedInViewer.delete(elementId)
} else {
selectedInViewer.add(elementId)
}
} else {
selectedInViewer.clear()
selectedInViewer.add(elementId)
}
selectItemsInScene(Array.from(selectedInViewer), true)
return
}
const descendantIds = getDescendantExpressIds(selectedElt)
let updateNav = false
const selectedInViewer = new Set(viewer.getSelectedIds())
if (isShiftKeyDown) {
if (selectedInViewer.has(expressId)) {
if (selectedInViewer.has(elementId)) {
const descendantIdsToRemove = getDescendantExpressIds(selectedElt)
descendantIdsToRemove.forEach((descendantId) => selectedInViewer.delete(descendantId))
selectedInViewer.delete(expressId)
selectedInViewer.delete(elementId)
} else {
selectedInViewer.add(expressId)
selectedInViewer.add(elementId)
descendantIds.forEach((id) => selectedInViewer.add(id))
}
} else {
selectedInViewer.clear()
selectedInViewer.add(expressId)
selectedInViewer.add(elementId)
descendantIds.forEach((descendantId) => selectedInViewer.add(descendantId))
updateNav = true
}
Expand Down
Loading
Loading