Fix duplicate elements and robust GlobalId lookup
Some checks are pending
CI / backend-lint-and-test (push) Waiting to run
Some checks are pending
CI / backend-lint-and-test (push) Waiting to run
- Remove IfcWallStandardCase from parser (covered by IfcWall as parent type) - Add seen_global_ids set to prevent duplicate element insertion - Use scalars().first() instead of scalar_one_or_none() for resilient lookup - Re-parse required: clears and rebuilds element data without duplicates
This commit is contained in:
parent
ca0a52c0c1
commit
eb2c662d0e
3 changed files with 118 additions and 58 deletions
|
|
@ -65,7 +65,7 @@ async def get_element_by_global_id(
|
|||
.where(Element.global_id == global_id)
|
||||
)
|
||||
result = await db.execute(query)
|
||||
element = result.scalar_one_or_none()
|
||||
element = result.scalars().first()
|
||||
|
||||
if not element:
|
||||
raise HTTPException(status_code=404, detail="Element not found")
|
||||
|
|
|
|||
|
|
@ -42,13 +42,18 @@ async def parse_ifc_file(
|
|||
|
||||
# Extract elements
|
||||
element_types = [
|
||||
"IfcWall", "IfcWallStandardCase", "IfcDoor", "IfcWindow", "IfcSlab",
|
||||
"IfcWall", "IfcDoor", "IfcWindow", "IfcSlab",
|
||||
"IfcColumn", "IfcBeam", "IfcStair", "IfcRoof",
|
||||
"IfcSpace", "IfcFurnishingElement", "IfcBuildingElementProxy",
|
||||
]
|
||||
|
||||
seen_global_ids = set()
|
||||
|
||||
for ifc_type in element_types:
|
||||
for ifc_element in ifc.by_type(ifc_type):
|
||||
if ifc_element.GlobalId in seen_global_ids:
|
||||
continue
|
||||
seen_global_ids.add(ifc_element.GlobalId)
|
||||
storey = _get_storey(ifc_element)
|
||||
|
||||
element = Element(
|
||||
|
|
|
|||
|
|
@ -7,10 +7,13 @@ export function useViewer() {
|
|||
let model = null
|
||||
let raycaster = new THREE.Raycaster()
|
||||
let mouse = new THREE.Vector2()
|
||||
let selectedMesh = null
|
||||
let selectedMeshes = []
|
||||
let originalMaterials = new Map()
|
||||
let onElementClick = null
|
||||
|
||||
// Map: GlobalId -> array of meshes belonging to that element
|
||||
let globalIdMeshMap = new Map()
|
||||
|
||||
const highlightMaterial = new THREE.MeshStandardMaterial({
|
||||
color: 0x00aaff,
|
||||
emissive: 0x003366,
|
||||
|
|
@ -21,11 +24,9 @@ export function useViewer() {
|
|||
function init(container, onClick) {
|
||||
onElementClick = onClick
|
||||
|
||||
// Scene
|
||||
scene = new THREE.Scene()
|
||||
scene.background = new THREE.Color(0x1a1a2e)
|
||||
|
||||
// Camera
|
||||
camera = new THREE.PerspectiveCamera(
|
||||
60,
|
||||
container.clientWidth / container.clientHeight,
|
||||
|
|
@ -34,14 +35,12 @@ export function useViewer() {
|
|||
)
|
||||
camera.position.set(15, 15, 15)
|
||||
|
||||
// Renderer
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||
renderer.setSize(container.clientWidth, container.clientHeight)
|
||||
renderer.setPixelRatio(window.devicePixelRatio)
|
||||
renderer.shadowMap.enabled = true
|
||||
container.appendChild(renderer.domElement)
|
||||
|
||||
// Controls
|
||||
controls = new OrbitControls(camera, renderer.domElement)
|
||||
controls.enableDamping = true
|
||||
controls.dampingFactor = 0.05
|
||||
|
|
@ -51,7 +50,6 @@ export function useViewer() {
|
|||
controls.target.set(0, 3, 0)
|
||||
controls.update()
|
||||
|
||||
// Lights
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
|
||||
scene.add(ambientLight)
|
||||
|
||||
|
|
@ -64,18 +62,13 @@ export function useViewer() {
|
|||
dirLight2.position.set(-10, 20, -10)
|
||||
scene.add(dirLight2)
|
||||
|
||||
// Grid
|
||||
const grid = new THREE.GridHelper(50, 50, 0x333355, 0x222244)
|
||||
scene.add(grid)
|
||||
|
||||
// Click handler
|
||||
renderer.domElement.addEventListener('click', onCanvasClick)
|
||||
renderer.domElement.addEventListener('dblclick', onCanvasDoubleClick)
|
||||
|
||||
// Resize handler
|
||||
window.addEventListener('resize', () => onResize(container))
|
||||
|
||||
// Start render loop
|
||||
animate()
|
||||
}
|
||||
|
||||
|
|
@ -85,6 +78,7 @@ export function useViewer() {
|
|||
scene.remove(model)
|
||||
model = null
|
||||
originalMaterials.clear()
|
||||
globalIdMeshMap.clear()
|
||||
}
|
||||
|
||||
const loader = new GLTFLoader()
|
||||
|
|
@ -93,14 +87,8 @@ export function useViewer() {
|
|||
(gltf) => {
|
||||
model = gltf.scene
|
||||
|
||||
// Store original materials for highlight/unhighlight
|
||||
model.traverse((child) => {
|
||||
if (child.isMesh) {
|
||||
originalMaterials.set(child, child.material.clone())
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
}
|
||||
})
|
||||
// Build GlobalId -> meshes map
|
||||
buildGlobalIdMap(model)
|
||||
|
||||
scene.add(model)
|
||||
fitCameraToModel()
|
||||
|
|
@ -114,6 +102,88 @@ export function useViewer() {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the scene graph and build a map from GlobalId to all meshes
|
||||
* belonging to that element. IFC GlobalIds are 22 characters long.
|
||||
*
|
||||
* The glb tree typically looks like:
|
||||
* Node "2XPyKWY018sA1ygZKgQPtU" <- GlobalId on a group node
|
||||
* ├── Mesh (frame)
|
||||
* ├── Mesh (panel)
|
||||
* └── Mesh (handle)
|
||||
*
|
||||
* We also handle cases where the GlobalId is directly on a mesh.
|
||||
*/
|
||||
function buildGlobalIdMap(root) {
|
||||
globalIdMeshMap.clear()
|
||||
|
||||
root.traverse((node) => {
|
||||
const globalId = findGlobalId(node)
|
||||
if (!globalId) return
|
||||
|
||||
// Collect all meshes under this node
|
||||
const meshes = []
|
||||
if (node.isMesh) {
|
||||
meshes.push(node)
|
||||
originalMaterials.set(node, node.material.clone())
|
||||
node.castShadow = true
|
||||
node.receiveShadow = true
|
||||
} else {
|
||||
node.traverse((child) => {
|
||||
if (child.isMesh) {
|
||||
meshes.push(child)
|
||||
if (!originalMaterials.has(child)) {
|
||||
originalMaterials.set(child, child.material.clone())
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (meshes.length > 0) {
|
||||
// A GlobalId might appear at multiple levels; merge meshes
|
||||
if (globalIdMeshMap.has(globalId)) {
|
||||
const existing = globalIdMeshMap.get(globalId)
|
||||
for (const m of meshes) {
|
||||
if (!existing.includes(m)) existing.push(m)
|
||||
}
|
||||
} else {
|
||||
globalIdMeshMap.set(globalId, meshes)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node's name looks like an IFC GlobalId.
|
||||
* IFC GlobalIds are exactly 22 characters of base64-like encoding.
|
||||
*/
|
||||
function isGlobalId(name) {
|
||||
if (!name || name.length !== 22) return false
|
||||
return /^[0-9A-Za-z_$]+$/.test(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the GlobalId for a node — either its own name or a parent's name.
|
||||
*/
|
||||
function findGlobalId(node) {
|
||||
if (isGlobalId(node.name)) return node.name
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a mesh, find the GlobalId by walking up the parent chain.
|
||||
*/
|
||||
function findGlobalIdForMesh(mesh) {
|
||||
let current = mesh
|
||||
while (current) {
|
||||
if (isGlobalId(current.name)) return current.name
|
||||
current = current.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function fitCameraToModel() {
|
||||
if (!model) return
|
||||
|
||||
|
|
@ -150,21 +220,11 @@ export function useViewer() {
|
|||
|
||||
if (intersects.length > 0) {
|
||||
const mesh = intersects[0].object
|
||||
highlightElement(mesh)
|
||||
const globalId = findGlobalIdForMesh(mesh)
|
||||
|
||||
// Walk up to find the named parent (GlobalId)
|
||||
let globalId = null
|
||||
let current = mesh
|
||||
while (current) {
|
||||
if (current.name && current.name.length >= 22) {
|
||||
globalId = current.name
|
||||
break
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
|
||||
if (globalId && onElementClick) {
|
||||
onElementClick(globalId)
|
||||
if (globalId) {
|
||||
highlightByGlobalId(globalId)
|
||||
if (onElementClick) onElementClick(globalId)
|
||||
}
|
||||
} else {
|
||||
clearHighlight()
|
||||
|
|
@ -197,17 +257,14 @@ export function useViewer() {
|
|||
function animatePivot(newTarget) {
|
||||
const startTarget = controls.target.clone()
|
||||
const startCamera = camera.position.clone()
|
||||
|
||||
// Keep the same camera-to-target offset vector
|
||||
const offset = startCamera.clone().sub(startTarget)
|
||||
|
||||
const duration = 300 // ms
|
||||
const duration = 300
|
||||
const startTime = performance.now()
|
||||
|
||||
function step(now) {
|
||||
const elapsed = now - startTime
|
||||
const t = Math.min(elapsed / duration, 1)
|
||||
// Ease-out cubic
|
||||
const ease = 1 - Math.pow(1 - t, 3)
|
||||
|
||||
controls.target.lerpVectors(startTarget, newTarget, ease)
|
||||
|
|
@ -222,32 +279,30 @@ export function useViewer() {
|
|||
requestAnimationFrame(step)
|
||||
}
|
||||
|
||||
function highlightElement(mesh) {
|
||||
clearHighlight()
|
||||
selectedMesh = mesh
|
||||
mesh.material = highlightMaterial
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight all meshes belonging to an IFC element by its GlobalId.
|
||||
* This is the single source of truth for highlighting — used by both
|
||||
* 3D click and sidebar click.
|
||||
*/
|
||||
function highlightByGlobalId(globalId) {
|
||||
if (!model) return
|
||||
clearHighlight()
|
||||
|
||||
model.traverse((child) => {
|
||||
if (child.name === globalId && child.isMesh) {
|
||||
highlightElement(child)
|
||||
} else if (child.name === globalId) {
|
||||
child.traverse((sub) => {
|
||||
if (sub.isMesh) highlightElement(sub)
|
||||
})
|
||||
}
|
||||
})
|
||||
const meshes = globalIdMeshMap.get(globalId)
|
||||
if (!meshes || meshes.length === 0) return
|
||||
|
||||
for (const mesh of meshes) {
|
||||
mesh.material = highlightMaterial
|
||||
selectedMeshes.push(mesh)
|
||||
}
|
||||
}
|
||||
|
||||
function clearHighlight() {
|
||||
if (selectedMesh && originalMaterials.has(selectedMesh)) {
|
||||
selectedMesh.material = originalMaterials.get(selectedMesh)
|
||||
selectedMesh = null
|
||||
for (const mesh of selectedMeshes) {
|
||||
if (originalMaterials.has(mesh)) {
|
||||
mesh.material = originalMaterials.get(mesh)
|
||||
}
|
||||
}
|
||||
selectedMeshes = []
|
||||
}
|
||||
|
||||
function onResize(container) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue