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
29 changes: 29 additions & 0 deletions __mocks__/OPFSWorkerRef.stub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// A small in-place fake worker that becomes "ready" after initializeWorker message
const messageListeners = new Set();

export const workerRef_ = {
script: '/OPFS.Worker.js',
postMessage: jest.fn((msg) => {
// When OPFSService sends the init command, ACK asynchronously
if (msg && msg.command === 'initializeWorker') {
// Use a microtask so no fake-timer juggling is needed
Promise.resolve().then(() => {
const evt = { type: 'message', data: { event: 'workerInitialized', completed: true } };
// Support both patterns: addEventListener and onmessage
workerRef_.onmessage?.(evt);
for (const fn of messageListeners) fn(evt);
});
}
}),
terminate: jest.fn(),

addEventListener: jest.fn((type, fn) => {
if (type === 'message') messageListeners.add(fn);
}),
removeEventListener: jest.fn((type, fn) => {
if (type === 'message') messageListeners.delete(fn);
}),

// Some code uses direct assignment
onmessage: null,
};
Binary file added bldrs-ai-conway-0.22.961.tgz
Binary file not shown.
Binary file added bldrs-ai-conway-0.22.962.tgz
Binary file not shown.
Binary file added bldrs-ai-conway-web-ifc-adapter-0.22.961.tgz
Binary file not shown.
Binary file added bldrs-ai-conway-web-ifc-adapter-0.22.962.tgz
Binary file not shown.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bldrs",
"version": "1.0.1571",
"version": "1.0.1583",
"main": "src/index.jsx",
"license": "AGPL-3.0",
"homepage": "https://github.com/bldrs-ai/Share",
Expand Down Expand Up @@ -71,7 +71,8 @@
"@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.940-2",
"@bldrs-ai/conway": "./bldrs-ai-conway-0.22.962.tgz",
"@bldrs-ai/conway-web-ifc-adapter": "./bldrs-ai-conway-web-ifc-adapter-0.22.962.tgz",
"@bldrs-ai/ifclib": "5.3.3",
"@emotion/react": "11.10.0",
"@emotion/styled": "11.10.0",
Expand Down Expand Up @@ -105,6 +106,7 @@
"matrix-widget-api": "1.13.1",
"msw": "2.10.4",
"normalize.css": "8.0.1",
"pako": "^2.1.0",
"postprocessing": "6.29.3",
"prop-types": "15.8.1",
"react": "18.2.0",
Expand Down
19 changes: 19 additions & 0 deletions public/static/js/ConwayGeomWasmNode.js

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions public/static/js/ConwayGeomWasmNodeMT.js

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions public/static/js/ConwayGeomWasmWeb.js

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions public/static/js/ConwayGeomWasmWebMT.js

Large diffs are not rendered by default.

Binary file added public/static/js/ConwayGeomWasmWebMT.wasm
Binary file not shown.
52 changes: 52 additions & 0 deletions public/static/js/draco/draco_decoder.js

Large diffs are not rendered by default.

Binary file added public/static/js/draco/draco_decoder.wasm
Binary file not shown.
33 changes: 33 additions & 0 deletions public/static/js/draco/draco_encoder.js

Large diffs are not rendered by default.

104 changes: 104 additions & 0 deletions public/static/js/draco/draco_wasm_wrapper.js

Large diffs are not rendered by default.

132 changes: 113 additions & 19 deletions src/Components/CutPlane/CutPlaneMenu.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, {ReactElement, useState, useEffect} from 'react'
import {useLocation} from 'react-router-dom'
import {Vector3} from 'three'
import {Vector3, Box3, Plane} from 'three'
import Menu from '@mui/material/Menu'
import MenuItem from '@mui/material/MenuItem'
import SvgIcon from '@mui/material/SvgIcon'
import Typography from '@mui/material/Typography'
import useStore from '../../store/useStore'
import {GlbClipper} from '../../Infrastructure/GlbClipper'
import debug from '../../utils/debug'
import {addHashParams, getHashParams, getObjectParams, removeParams} from '../../utils/location'
import {floatStrTrim, isNumeric} from '../../utils/strings'
Expand All @@ -18,6 +19,30 @@ import PlanIcon from '../../assets/icons/Plan.svg'
import SectionIcon from '../../assets/icons/Section.svg'


/**
* Gets the center of a model's bounding box
* Works for both IFC models (with geometry.boundingBox) and GLB models
*
* @param {object} model - The model object
* @return {Vector3} The center point of the model
*/
function getModelCenter(model) {
const modelCenter = new Vector3()

if (model?.geometry?.boundingBox) {
// IFC model with geometry.boundingBox
model.geometry.boundingBox.getCenter(modelCenter)
} else if (model) {
// GLB or other model - compute bounding box
const box = new Box3()
box.setFromObject(model)
box.getCenter(modelCenter)
}

return modelCenter
}


/**
* Menu of three cut planes for the model
*
Expand All @@ -36,6 +61,7 @@ export default function CutPlaneMenu() {
const setIsCutPlaneActive = useStore((state) => state.setIsCutPlaneActive)

const [anchorEl, setAnchorEl] = useState(null)
const [glbClipper, setGlbClipper] = useState(null)

const location = useLocation()

Expand All @@ -48,10 +74,37 @@ export default function CutPlaneMenu() {
setAnchorEl(null)
}

// Initialize GlbClipper when model changes
useEffect(() => {
if (model && viewer) {
const isGlbModel = viewer.IFC.type === 'glb' || viewer.IFC.type === 'gltf'
if (isGlbModel) {
const clipper = new GlbClipper(viewer, model)
setGlbClipper(clipper)
viewer.glbClipper = clipper // Store on viewer for access in other functions
debug().log('CutPlaneMenu: Initialized GlbClipper')

return () => {
clipper.dispose()
setGlbClipper(null)
delete viewer.glbClipper
}
}
}
}, [model, viewer])

useEffect(() => {
const planeHash = getHashParams(location, HASH_PREFIX_CUT_PLANE)
debug().log('CutPlaneMenu#useEffect: planeHash: ', planeHash)
if (planeHash && model && viewer) {
const isGlbModel = viewer.IFC.type === 'glb' || viewer.IFC.type === 'gltf'

// For GLB models, wait for glbClipper to be initialized
if (isGlbModel && !glbClipper) {
debug().log('CutPlaneMenu#useEffect: Waiting for glbClipper to initialize')
return
}

const planes = getPlanes(planeHash)
debug().log('CutPlaneMenu#useEffect: planes: ', planes)
if (planes && planes.length) {
Expand All @@ -62,36 +115,59 @@ export default function CutPlaneMenu() {
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [model])
}, [model, glbClipper])

const togglePlane = ({direction, offset = 0}) => {
setLevelInstance(null)
const modelCenter = new Vector3
model?.geometry.boundingBox.getCenter(modelCenter)
const modelCenter = getModelCenter(model)
setAnchorEl(null)
const {normal, modelCenterOffset} = getPlaneSceneInfo({modelCenter, direction, offset})
debug().log('CutPlaneMenu#togglePlane: normal: ', normal)
debug().log('CutPlaneMenu#togglePlane: modelCenterOffset: ', modelCenterOffset)
debug().log('CutPlaneMenu#togglePlane: ifcPlanes: ', viewer.clipper.planes)

const isGlbModel = viewer.IFC.type === 'glb' || viewer.IFC.type === 'gltf'

if (cutPlanes.findIndex((cutPlane) => cutPlane.direction === direction) > -1) {
debug().log('CutPlaneMenu#togglePlane: found: ', true)
removeParams(HASH_PREFIX_CUT_PLANE, [direction])
removeCutPlaneDirection(direction)
viewer.clipper.deleteAllPlanes()

if (isGlbModel && glbClipper) {
// For GLB: use GlbClipper
glbClipper.deleteAllPlanes()
} else {
// For IFC: use clipper
viewer.clipper.deleteAllPlanes()
}

const restCutPlanes = cutPlanes.filter((cutPlane) => cutPlane.direction !== direction)
restCutPlanes.forEach((restCutPlane) => {
const planeInfo = getPlaneSceneInfo({modelCenter, direction: restCutPlane.direction, offset: restCutPlane.offset})
viewer.clipper.createFromNormalAndCoplanarPoint(planeInfo.normal, planeInfo.modelCenterOffset)

if (isGlbModel && glbClipper) {
// Create GLB clipping plane with controls
glbClipper.createPlane(planeInfo.normal, planeInfo.modelCenterOffset, restCutPlane.direction, restCutPlane.offset)
} else {
viewer.clipper.createFromNormalAndCoplanarPoint(planeInfo.normal, planeInfo.modelCenterOffset)
}
})

if (restCutPlanes.length === 0) {
setIsCutPlaneActive(false)
}
} else {
debug().log('CutPlaneMenu#togglePlane: found: ', false)
addHashParams(window.location, HASH_PREFIX_CUT_PLANE, {[direction]: offset}, true)
addCutPlaneDirection({direction, offset})
viewer.clipper.createFromNormalAndCoplanarPoint(normal, modelCenterOffset)

if (isGlbModel && glbClipper) {
// For GLB: use GlbClipper with drag controls
glbClipper.createPlane(normal, modelCenterOffset, direction, offset)
} else {
// For IFC: use clipper
viewer.clipper.createFromNormalAndCoplanarPoint(normal, modelCenterOffset)
}

setIsCutPlaneActive(true)
}
}
Expand Down Expand Up @@ -181,10 +257,18 @@ export function resetState(viewer, setCutPlaneDirections, setIsCutPlaneActive) {
* @param {object} viewer bounding box
*/
export function removePlanes(viewer) {
viewer?.clipper.deleteAllPlanes()
const clippingPlanes = viewer?.clipper['context'].clippingPlanes
for (const plane of clippingPlanes) {
viewer?.clipper['context'].removeClippingPlane(plane)
const isGlbModel = viewer?.IFC?.type === 'glb' || viewer?.IFC?.type === 'gltf'

if (isGlbModel && viewer.glbClipper) {
// For GLB: use GlbClipper
viewer.glbClipper.deleteAllPlanes()
} else if (!isGlbModel) {
// For IFC: use clipper
viewer?.clipper.deleteAllPlanes()
const clippingPlanes = viewer?.clipper['context'].clippingPlanes
for (const plane of clippingPlanes) {
viewer?.clipper['context'].removeClippingPlane(plane)
}
}
}

Expand All @@ -197,18 +281,25 @@ export function removePlanes(viewer) {
* @return {object} {x: 0, y: 0, ...}
*/
export function getPlanesOffset(viewer, ifcModel) {
if (viewer.clipper.planes.length > 0) {
const isGlbModel = viewer?.IFC?.type === 'glb' || viewer?.IFC?.type === 'gltf'
const planes = isGlbModel && viewer.glbClipper ? viewer.glbClipper.planes : viewer?.clipper?.planes

if (planes && planes.length > 0) {
let planeNormal
let planeAxisCenter
let planeOffsetFromCenter
const planesOffset = {}
const modelCenter = new Vector3
ifcModel?.geometry.boundingBox.getCenter(modelCenter)
const modelCenter = getModelCenter(ifcModel)
debug().log('CutPlaneMenu#getPlanesOffset: modelCenter: ', modelCenter)
viewer.clipper.planes.forEach((plane) => {
for (const [key, value] of Object.entries(plane.plane.normal)) {

planes.forEach((planeData) => {
const plane = isGlbModel ? planeData.plane : planeData.plane
const normal = plane.normal
const constant = plane.constant

for (const [key, value] of Object.entries(normal)) {
if (value !== 0) {
const planeOffsetFromModelBoundary = plane.plane.constant
const planeOffsetFromModelBoundary = constant
planeNormal = key
planeAxisCenter = modelCenter[planeNormal]
planeOffsetFromCenter = planeOffsetFromModelBoundary - planeAxisCenter
Expand All @@ -229,7 +320,10 @@ export function getPlanesOffset(viewer, ifcModel) {
* @param {object} ifcModel
*/
export function addPlanesToHashState(viewer, ifcModel) {
if (viewer.clipper.planes.length > 0) {
const isGlbModel = viewer?.IFC?.type === 'glb' || viewer?.IFC?.type === 'gltf'
const planes = isGlbModel && viewer.glbClipper ? viewer.glbClipper.planes : viewer?.clipper?.planes

if (planes && planes.length > 0) {
const planeInfo = getPlanesOffset(viewer, ifcModel)
debug().log('CutPlaneMenu#addPlaneLocationToUrl: planeInfo: ', planeInfo)
addHashParams(window.location, HASH_PREFIX_CUT_PLANE, planeInfo, true)
Expand Down
4 changes: 4 additions & 0 deletions src/Components/Markers/MarkerControl.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const mockedUseNavigate = jest.fn()
const defaultLocationValue = {pathname: '/index.ifc', search: '', hash: '', state: null, key: 'default'}
// mock createObjectURL
global.URL.createObjectURL = jest.fn(() => '1111111111111111111111111111111111111111')
// In your test file, put this FIRST (hoisted by Jest):
jest.mock('../../OPFS/OPFSService.js', () => ({
initializeWorker: () => null, // fix import.meta
}))

jest.mock('../../OPFS/utils', () => {
const actualUtils = jest.requireActual('../../OPFS/utils')
Expand Down
Loading