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

Fixed TAS Shapecast #3661

Merged
merged 4 commits into from
Dec 10, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 31 additions & 11 deletions packages/viewer-sandbox/src/Extensions/BoxSelection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { InputEvent } from '@speckle/viewer'
import { CONTAINED, InputEvent } from '@speckle/viewer'
import { ObjectLayers } from '@speckle/viewer'
import { NodeRenderView } from '@speckle/viewer'
import { SelectionExtension } from '@speckle/viewer'
import { BatchObject } from '@speckle/viewer'
import { Extension, IViewer, GeometryType, CameraController } from '@speckle/viewer'
Expand All @@ -27,8 +26,9 @@ export class BoxSelection extends Extension {

private dragging = false
private frameLock = false
private _realTimeSelection = true

private idsToSelect: Array<string> | null = []
private idsToSelect: Set<string> | null = new Set()

get enabled(): boolean {
return this._enabled
Expand All @@ -37,6 +37,10 @@ export class BoxSelection extends Extension {
this._enabled = value
}

set realtimeSelection(value: boolean) {
this._realTimeSelection = value
}

public constructor(viewer: IViewer, private cameraController: CameraController) {
super(viewer)
/** Get the SelectionExtension. We'll need it to remotely enable/disable it */
Expand All @@ -53,10 +57,10 @@ export class BoxSelection extends Extension {
}

public onEarlyUpdate() {
if (this.idsToSelect) {
if (this.idsToSelect?.size) {
/** Send the ids to the selection extension to be selected */
this.selectionExtension.clearSelection()
this.selectionExtension.selectObjects(this.idsToSelect, true)
this.selectionExtension.selectObjects(Array.from(this.idsToSelect), true)
this.idsToSelect = null
this.viewer.requestRender()
}
Expand All @@ -72,14 +76,22 @@ export class BoxSelection extends Extension {
}
}

private onPointerUp() {
private onPointerUp(e: Vector2 & { event: PointerEvent }) {
/** Re-enable the camera controller */
this.cameraController.enabled = true
/** Hide the selection box */
this.dragBoxMaterial.uniforms.transform.value = new Matrix4().makeScale(0, 0, 0)
this.dragBoxMaterial.needsUpdate = true

this.dragging = false

if (!this._realTimeSelection && e.event.altKey) {
/** Get the ids of objects that fall withing the selection box */
this.idsToSelect = this.getSelectionIds(this.ndcBox)
}

this.ndcBox.makeEmpty()

this.viewer.requestRender()
}

Expand All @@ -101,9 +113,13 @@ export class BoxSelection extends Extension {
this.ndcBox.max.set(1, 1, 0)
this.ndcBox.applyMatrix4(ndcTransform)

/** Get the ids of objects that fall withing the selection box */
this.idsToSelect = this.getSelectionIds(this.ndcBox)
if (this._realTimeSelection) {
/** Get the ids of objects that fall withing the selection box */
this.idsToSelect = this.getSelectionIds(this.ndcBox)
}

this.frameLock = true
this.viewer.requestRender()
}

/** Gets the object ids that fall withing the provided selection box */
Expand All @@ -124,13 +140,16 @@ export class BoxSelection extends Extension {
/** We're using three-mesh-bvh library for out BVH
* Go over each batch and test it against the TAS only.
**/
const selectionRvs: Array<NodeRenderView> = []
const selection: Set<string> = new Set()
for (let b = 0; b < batches.length; b++) {
batches[b].mesh.TAS.shapecast({
/** This is the callback from the TAS's bounds internal nodes */
intersectsTAS: (box: Box3) => {
/** We continue traversion only if the selection box intersects an internal node */
const ndcBox = this.worldBoxToNDC(box, clipMatrix)
if (selectionBox.containsBox(ndcBox)) {
return CONTAINED
}
const ret = selectionBox.intersectsBox(ndcBox)
return ret
},
Expand All @@ -140,7 +159,8 @@ export class BoxSelection extends Extension {
const ndcBox = this.worldBoxToNDC(objectBox, clipMatrix)
/** We consider an object selected only it's NDC AABB is contained in the selection box */
if (selectionBox.containsBox(ndcBox))
selectionRvs.push(batchObject.renderView)
selection.add(batchObject.renderView.renderData.id)
/** We always return false here because we don't want to continue intersecting batch object triangles. */
return false
},
/** This is the callback from the BAS bounds internal nodes */
Expand All @@ -153,7 +173,7 @@ export class BoxSelection extends Extension {
}
})
}
return selectionRvs.map((rv: NodeRenderView) => rv.renderData.id)
return selection
}

/** Buffers for reading/writing */
Expand Down
10 changes: 7 additions & 3 deletions packages/viewer-sandbox/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { SectionTool } from '@speckle/viewer'
import { SectionOutlines } from '@speckle/viewer'
import { ViewModesKeys } from './Extensions/ViewModesKeys'
import { BoxSelection } from './Extensions/BoxSelection'

const createViewer = async (containerName: string, stream: string) => {
const container = document.querySelector<HTMLElement>(containerName)
Expand Down Expand Up @@ -53,7 +54,8 @@ const createViewer = async (containerName: string, stream: string) => {
const diff = viewer.createExtension(DiffExtension)
viewer.createExtension(ViewModes)
viewer.createExtension(ViewModesKeys)
// const boxSelect = viewer.createExtension(BoxSelection)
const boxSelect = viewer.createExtension(BoxSelection)
boxSelect.realtimeSelection = false
// const rotateCamera = viewer.createExtension(RotateCamera)
cameraController // use it
selection // use it
Expand Down Expand Up @@ -108,12 +110,12 @@ const getStream = () => {
// prettier-ignore
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8?c=%5B-7.66134,10.82932,6.41935,-0.07739,-13.88552,1.8697,0,1%5D'
// Revit sample house (good for bim-like stuff with many display meshes)
'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
// 'https://latest.speckle.systems/streams/c1faab5c62/commits/ab1a1ab2b6'
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
// 'https://latest.speckle.systems/streams/58b5648c4d/commits/60371ecb2d'
// 'Super' heavy revit shit
// 'https://app.speckle.systems/streams/e6f9156405/commits/0694d53bb5'
'https://app.speckle.systems/streams/e6f9156405/commits/0694d53bb5'
// IFC building (good for a tree based structure)
// 'https://latest.speckle.systems/streams/92b620fb17/commits/2ebd336223'
// IFC story, a subtree of the above
Expand Down Expand Up @@ -450,6 +452,8 @@ const getStream = () => {

// Perfectly flat
// 'https://app.speckle.systems/projects/344f803f81/models/5582ab673e'

// 'https://speckle.xyz/streams/27e89d0ad6/commits/5ed4b74252'
)
}

Expand Down
16 changes: 14 additions & 2 deletions packages/viewer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ import SpeckleStandardMaterial from './modules/materials/SpeckleStandardMaterial
import SpeckleTextMaterial from './modules/materials/SpeckleTextMaterial.js'
import { SpeckleText } from './modules/objects/SpeckleText.js'
import { NodeRenderView } from './modules/tree/NodeRenderView.js'
import { type ExtendedIntersection } from './modules/objects/SpeckleRaycaster.js'
import {
CONTAINED,
INTERSECTED,
NOT_INTERSECTED,
type ExtendedIntersection
} from './modules/objects/SpeckleRaycaster.js'
import { SpeckleGeometryConverter } from './modules/loaders/Speckle/SpeckleGeometryConverter.js'
import { Assets } from './modules/Assets.js'
import { InstancedBatchObject } from './modules/batching/InstancedBatchObject.js'
Expand Down Expand Up @@ -124,6 +129,8 @@ import {
FilterMaterialOptions,
FilterMaterialType
} from './modules/materials/Materials.js'
import { AccelerationStructure } from './modules/objects/AccelerationStructure.js'
import { TopLevelAccelerationStructure } from './modules/objects/TopLevelAccelerationStructure.js'

export {
Viewer,
Expand Down Expand Up @@ -165,6 +172,8 @@ export {
LineBatch,
PointBatch,
TextBatch,
AccelerationStructure,
TopLevelAccelerationStructure,
SpeckleStandardMaterial,
SpeckleBasicMaterial,
SpeckleTextMaterial,
Expand Down Expand Up @@ -209,7 +218,10 @@ export {
ViewMode,
FilterMaterial,
FilterMaterialType,
FilterMaterialOptions
FilterMaterialOptions,
NOT_INTERSECTED,
INTERSECTED,
CONTAINED
}

export type {
Expand Down
5 changes: 4 additions & 1 deletion packages/viewer/src/modules/objects/SpeckleRaycaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { ObjectLayers } from '../../IViewer.js'
import SpeckleMesh from './SpeckleMesh.js'
import SpeckleInstancedMesh from './SpeckleInstancedMesh.js'

export const NOT_INTERSECTED: ShapecastIntersection = 0
export const INTERSECTED: ShapecastIntersection = 1
export const CONTAINED: ShapecastIntersection = 2
export type ExtendedShapeCastCallbacks = {
intersectsTAS?: (
box: Box3,
Expand All @@ -21,7 +24,7 @@ export type ExtendedShapeCastCallbacks = {
depth: number,
nodeIndex: number
) => ShapecastIntersection | boolean
intersectTASRange?: (batchObject: BatchObject) => ShapecastIntersection | boolean
intersectTASRange?: (batchObjects: BatchObject) => ShapecastIntersection | boolean
intersectsBounds: (
box: Box3,
isLeaf: boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
Side,
Vector3
} from 'three'
import { MeshBVHVisualizer } from 'three-mesh-bvh'
import { MeshBVHVisualizer, ShapecastIntersection } from 'three-mesh-bvh'
import { BatchObject } from '../batching/BatchObject.js'
import { ExtendedTriangle, HitPointInfo } from 'three-mesh-bvh'
import type {
Expand Down Expand Up @@ -294,28 +294,47 @@ export class TopLevelAccelerationStructure {
}

let ret = false
/** We only call intersectTASRange once for each batch object. */
const visitedObjects: { [id: string]: boolean | ShapecastIntersection } = {}
this.accelerationStructure.shapecast({
intersectsBounds: (box, isLeaf, score, depth, nodeIndex) => {
if (callbacks.intersectsTAS)
if (callbacks.intersectsTAS) {
return callbacks.intersectsTAS(box, isLeaf, score, depth, nodeIndex)
}
return false
},
intersectsRange: (triangleOffset: number) => {
intersectsRange: (triangleOffset: number, triangleCount: number) => {
/** The index buffer for the bvh's geometry will *never* be undefined as it uses indexed geometry */
const indexBufferAttribute: BufferAttribute = this.accelerationStructure
.geometry.index as BufferAttribute
const vertIndex = indexBufferAttribute.array[triangleOffset * 3]
const batchObjectIndex = Math.trunc(
vertIndex / TopLevelAccelerationStructure.CUBE_VERTS
)
if (callbacks.intersectTASRange) {
const ret = callbacks.intersectTASRange(this.batchObjects[batchObjectIndex])
if (!ret) return false
const batchObjects = new Set<BatchObject>()
for (let k = 0; k < triangleCount; k++) {
const indexBufferAttribute: BufferAttribute = this.accelerationStructure
.geometry.index as BufferAttribute
const vertIndex = indexBufferAttribute.array[triangleOffset * 3 + k * 3]
const batchObjectIndex = Math.trunc(
vertIndex / TopLevelAccelerationStructure.CUBE_VERTS
)
const batchObject = this.batchObjects[batchObjectIndex]
if (callbacks.intersectTASRange) {
if (visitedObjects[batchObject.renderView.renderData.id] !== undefined)
continue

const ret = callbacks.intersectTASRange(batchObject)
visitedObjects[batchObject.renderView.renderData.id] = ret
if (ret) batchObjects.add(batchObject)
} else {
batchObjects.add(batchObject)
}
}
/** No batch object selected, stop here */
if (!batchObjects.size) return false

for (const batchObject of batchObjects) {
ret ||= batchObject.accelerationStructure.shapecast(
wrapCallbacks(batchObject)
)
}
ret ||= this.batchObjects[batchObjectIndex].accelerationStructure.shapecast(
wrapCallbacks(this.batchObjects[batchObjectIndex])
)

/** We never test agains the TAS triangles because there is no point. Traversal stops here */
return false
}
})
Expand Down
Loading