Merge branch 'feature/initial-viewer-ui-updates' of github.com:specklesystems/speckle-server into feature/initial-viewer-ui-updates
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
<div>
|
||||
<!-- Model Header -->
|
||||
<div
|
||||
class="group flex items-center px-1 py-3 select-none cursor-pointer hover:bg-highlight-1"
|
||||
class="group flex items-center pl-1 pr-2 py-3 select-none cursor-pointer hover:bg-highlight-1"
|
||||
:class="isExpanded ? 'border-b border-outline-3' : ''"
|
||||
@mouseenter="highlightObject"
|
||||
@mouseleave="unhighlightObject"
|
||||
@focusin="highlightObject"
|
||||
|
||||
@@ -3,86 +3,90 @@
|
||||
<template>
|
||||
<div :class="{ 'opacity-60': shouldShowDimmed }">
|
||||
<!-- Header -->
|
||||
<div
|
||||
ref="headerElement"
|
||||
class="group flex items-center justify-between w-full p-1 cursor-pointer"
|
||||
:class="getBackgroundClass"
|
||||
@click.stop="(e:MouseEvent) => setSelection(e)"
|
||||
@mouseenter="highlightObject"
|
||||
@focusin="highlightObject"
|
||||
@mouseleave="unhighlightObject"
|
||||
@focusout="unhighlightObject"
|
||||
>
|
||||
<div class="flex items-center gap-0.5 min-w-0">
|
||||
<div :style="{ width: `${depth * 0.375}rem` }" class="shrink-0"></div>
|
||||
<FormButton
|
||||
v-if="isSingleCollection || isMultipleCollection"
|
||||
size="sm"
|
||||
color="subtle"
|
||||
@click.stop="manualUnfoldToggle"
|
||||
>
|
||||
<IconTriangle
|
||||
class="w-4 h-4 -ml-1.5 -mr-1.5 text-foreground-2"
|
||||
:class="unfold ? 'rotate-90' : ''"
|
||||
/>
|
||||
<span class="sr-only">
|
||||
{{ unfold ? 'Collapse' : 'Expand' }}
|
||||
</span>
|
||||
</FormButton>
|
||||
<div v-else class="w-4 shrink-0"></div>
|
||||
<div
|
||||
class="flex flex-col min-w-0"
|
||||
:class="
|
||||
isHidden || (!isIsolated && stateHasIsolatedObjectsInGeneral)
|
||||
? 'text-foreground-2'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<div class="truncate text-body-2xs">
|
||||
<!-- Note, enforce header from parent if provided (used in the case of root nodes) -->
|
||||
{{ header || headerAndSubheader.header }}
|
||||
</div>
|
||||
<div class="truncate text-body-3xs text-foreground-2">
|
||||
{{ subHeader || headerAndSubheader.subheader }}
|
||||
<div class="rounded-t-sm" :class="unfold ? 'bg-foundation-2' : ''">
|
||||
<div
|
||||
ref="headerElement"
|
||||
class="group flex items-center justify-between w-full p-1 cursor-pointer"
|
||||
:class="getBackgroundClass"
|
||||
@click.stop="(e:MouseEvent) => setSelection(e)"
|
||||
@mouseenter="highlightObject"
|
||||
@focusin="highlightObject"
|
||||
@mouseleave="unhighlightObject"
|
||||
@focusout="unhighlightObject"
|
||||
>
|
||||
<div class="flex items-center gap-0.5 min-w-0">
|
||||
<div :style="{ width: `${depth * 0.375}rem` }" class="shrink-0"></div>
|
||||
<FormButton
|
||||
v-if="isSingleCollection || isMultipleCollection"
|
||||
size="sm"
|
||||
color="subtle"
|
||||
@click.stop="manualUnfoldToggle"
|
||||
>
|
||||
<IconTriangle
|
||||
class="w-4 h-4 -ml-1.5 -mr-1.5 text-foreground-2"
|
||||
:class="unfold ? 'rotate-90' : ''"
|
||||
/>
|
||||
<span class="sr-only">
|
||||
{{ unfold ? 'Collapse' : 'Expand' }}
|
||||
</span>
|
||||
</FormButton>
|
||||
<div v-else class="w-4 shrink-0"></div>
|
||||
<div
|
||||
class="flex flex-col min-w-0"
|
||||
:class="
|
||||
isHidden || (!isIsolated && stateHasIsolatedObjectsInGeneral)
|
||||
? 'text-foreground-2'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<div class="truncate text-body-2xs">
|
||||
<!-- Note, enforce header from parent if provided (used in the case of root nodes) -->
|
||||
{{ header || headerAndSubheader.header }}
|
||||
</div>
|
||||
<div class="truncate text-body-3xs text-foreground-2">
|
||||
{{ subHeader || headerAndSubheader.subheader }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center w-0 group-hover:w-auto overflow-hidden shrink-0">
|
||||
<button
|
||||
v-tippy="getTooltipProps(isHidden ? 'Show' : 'Hide', { placement: 'top' })"
|
||||
class="p-1 rounded-md"
|
||||
:icon-left="isHidden ? IconEyeClosed : IconEye"
|
||||
:class="
|
||||
isHidden || isSelected
|
||||
? 'opacity-100 hover:bg-highlight-1'
|
||||
: 'opacity-0 group-hover:opacity-100 hover:bg-highlight-3'
|
||||
"
|
||||
@click.stop="hideOrShowObject"
|
||||
>
|
||||
<IconEyeClosed v-if="isHidden" class="w-4 h-4" />
|
||||
<IconEye v-else class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
v-tippy="
|
||||
getTooltipProps(isIsolated ? 'Unisolate' : 'Isolate', { placement: 'top' })
|
||||
"
|
||||
class="p-1 rounded-md"
|
||||
:class="
|
||||
isIsolated || isSelected
|
||||
? 'opacity-100 hover:bg-highlight-1'
|
||||
: 'opacity-0 group-hover:opacity-100 hover:bg-highlight-3'
|
||||
"
|
||||
@click.stop="isolateOrUnisolateObject"
|
||||
>
|
||||
<IconViewerUnisolate v-if="isIsolated" class="w-3.5 h-3.5" />
|
||||
<IconViewerIsolate v-else class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div class="flex items-center w-0 group-hover:w-auto overflow-hidden shrink-0">
|
||||
<button
|
||||
v-tippy="getTooltipProps(isHidden ? 'Show' : 'Hide', { placement: 'top' })"
|
||||
class="p-1 rounded-md"
|
||||
:icon-left="isHidden ? IconEyeClosed : IconEye"
|
||||
:class="
|
||||
isHidden || isSelected
|
||||
? 'opacity-100 hover:bg-highlight-1'
|
||||
: 'opacity-0 group-hover:opacity-100 hover:bg-highlight-3'
|
||||
"
|
||||
@click.stop="hideOrShowObject"
|
||||
>
|
||||
<IconEyeClosed v-if="isHidden" class="w-4 h-4" />
|
||||
<IconEye v-else class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
v-tippy="
|
||||
getTooltipProps(isIsolated ? 'Unisolate' : 'Isolate', {
|
||||
placement: 'top'
|
||||
})
|
||||
"
|
||||
class="p-1 rounded-md"
|
||||
:class="
|
||||
isIsolated || isSelected
|
||||
? 'opacity-100 hover:bg-highlight-1'
|
||||
: 'opacity-0 group-hover:opacity-100 hover:bg-highlight-3'
|
||||
"
|
||||
@click.stop="isolateOrUnisolateObject"
|
||||
>
|
||||
<IconViewerUnisolate v-if="isIsolated" class="w-3.5 h-3.5" />
|
||||
<IconViewerIsolate v-else class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Children contents -->
|
||||
<div v-if="unfold" class="bg-foundation-2">
|
||||
<div v-if="unfold">
|
||||
<!-- If we have array collections -->
|
||||
<div v-if="isMultipleCollection">
|
||||
<!-- mul col items -->
|
||||
@@ -338,9 +342,9 @@ const isChildOfSelected = computed(() => {
|
||||
})
|
||||
|
||||
const getBackgroundClass = computed(() => {
|
||||
if (isSelected.value) return 'bg-highlight-3'
|
||||
if (isSelected.value) return 'bg-highlight-3 rounded-sm'
|
||||
if (isChildOfSelected.value) return 'bg-foundation-2'
|
||||
return 'bg-foundation hover:bg-highlight-1'
|
||||
return 'bg-foundation hover:bg-highlight-1 hover:rounded-sm'
|
||||
})
|
||||
|
||||
const highlightObject = () => {
|
||||
|
||||
@@ -186,7 +186,7 @@ export default class Sandbox {
|
||||
this.addStreamControls(url)
|
||||
this.addViewControls()
|
||||
this.addBatches()
|
||||
this.properties = await this.viewer.getObjectProperties()
|
||||
// this.properties = await this.viewer.getObjectProperties()
|
||||
this.batchesParams.totalBvhSize = this.getBVHSize()
|
||||
this.refresh()
|
||||
})
|
||||
|
||||
@@ -135,6 +135,7 @@ const getStream = () => {
|
||||
// prettier-ignore
|
||||
// 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/objects/ee5d160d84090822813bc74188da34a7'
|
||||
|
||||
//large tower
|
||||
//'https://app.speckle.systems/projects/e2a7b596f2/models/ddaf8349f5'
|
||||
@@ -162,6 +163,7 @@ const getStream = () => {
|
||||
// 'https://latest.speckle.systems/streams/3ed8357f29/commits/d10f2af1ce'
|
||||
// AutoCAD NEW
|
||||
// 'https://latest.speckle.systems/streams/3ed8357f29/commits/46905429f6'
|
||||
// 'https://latest.speckle.systems/streams/3ed8357f29/objects/95160b8d593a0ba12dd004d5fe142257'
|
||||
//Blizzard world
|
||||
// 'https://latest.speckle.systems/streams/0c6ad366c4/commits/aa1c393aec'
|
||||
//Car
|
||||
@@ -355,7 +357,6 @@ const getStream = () => {
|
||||
// 'https://app.speckle.systems/streams/25d8a162af/commits/6c842a713c'
|
||||
// 'https://app.speckle.systems/streams/76e3acde68/commits/0ea3d47e6c'
|
||||
// Point cloud
|
||||
// 'https://app.speckle.systems/streams/b920636274/commits/8df6496749'
|
||||
// 'https://multiconsult.speckle.xyz/streams/9721fe797c/objects/ff5d939a8c26bde092152d5b4a0c945d'
|
||||
// 'https://app.speckle.systems/streams/87a2be92c7/objects/803c3c413b133ee9a6631160ccb194c9'
|
||||
// 'https://latest.speckle.systems/streams/1422d91a81/commits/480d88ba68'
|
||||
@@ -543,7 +544,7 @@ const getStream = () => {
|
||||
// 'https://app.speckle.systems/projects/7591c56179/models/82b94108a3'
|
||||
|
||||
// SUPER slow tree build time (LARGE N-GONS TRIANGULATION)
|
||||
// 'https://app.speckle.systems/projects/0edb6ef628/models/ff3d8480bc@cd83d90a2c'
|
||||
// 'https://app.speckle.systems/projects/0edb6ef628/models/87f3fb5e2bd681d731dd048390ae3a8f'
|
||||
|
||||
/* ObjectLoader 2 tests */
|
||||
// `https://latest.speckle.systems/projects/97750296c2/models/767b70fc63@5386a0af02`
|
||||
@@ -601,6 +602,22 @@ const getStream = () => {
|
||||
// 'https://latest.speckle.systems/projects/f28ad5b38a/models/b63ebcd807'
|
||||
// Duplicate display values
|
||||
// 'https://app.speckle.systems/projects/1466fe31c6/models/2eaf0f0571'
|
||||
// MEPS
|
||||
// 'https://app.speckle.systems/projects/f3cee517d4/models/21f128a3ea'
|
||||
// Tower
|
||||
// 'https://app.speckle.systems/projects/e2a7b596f2/models/ddaf8349f5'
|
||||
|
||||
// Barbican
|
||||
// 'https://app.speckle.systems/projects/32baa9291e/models/all'
|
||||
// 'https://app.speckle.systems/streams/32baa9291e/objects/21a3621c0a3e6d2884e1315f02314313'
|
||||
// 'https://app.speckle.systems/projects/5d723f097a/models/c05abd36b5'
|
||||
|
||||
//Guggenheim
|
||||
// 'https://app.speckle.systems/projects/937d78e0a5/models/a48f0274eb'
|
||||
// 'https://app.speckle.systems/projects/937d78e0a5/objects/0e3c61147f3a035a85a3542c7f1c7a43'
|
||||
|
||||
// heatherwick LARGE
|
||||
// 'https://app.speckle.systems/projects/63a3226049/models/bdd4f553a8'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,13 @@ export type SpeckleObject = {
|
||||
applicationId?: string
|
||||
}
|
||||
|
||||
export type DataChunk = {
|
||||
id: string
|
||||
data: number[]
|
||||
references: number
|
||||
processed?: boolean
|
||||
}
|
||||
|
||||
export interface ViewerParams {
|
||||
showStats: boolean
|
||||
environmentSrc: Asset
|
||||
|
||||
@@ -299,9 +299,10 @@ async function runAllModelsQuery(
|
||||
const urls: string[] = []
|
||||
data.project.models.items.forEach(
|
||||
(element: { versions: { items: { referencedObject: string }[] } }) => {
|
||||
urls.push(
|
||||
`${ref.origin}/streams/${ref.projectId}/objects/${element.versions.items[0].referencedObject}`
|
||||
)
|
||||
if (element.versions.items.length)
|
||||
urls.push(
|
||||
`${ref.origin}/streams/${ref.projectId}/objects/${element.versions.items[0].referencedObject}`
|
||||
)
|
||||
}
|
||||
)
|
||||
return urls
|
||||
|
||||
@@ -102,8 +102,16 @@ export class BatchObject {
|
||||
this.pivot_High
|
||||
)
|
||||
}
|
||||
public buildAccelerationStructure(
|
||||
position: Float32Array | Float64Array,
|
||||
indices: Uint16Array | Uint32Array
|
||||
): void
|
||||
public buildAccelerationStructure(bvh: MeshBVH): void
|
||||
|
||||
public buildAccelerationStructure(bvh?: MeshBVH) {
|
||||
public buildAccelerationStructure(
|
||||
positionOrBvh: Float32Array | Float64Array | MeshBVH,
|
||||
indices?: Uint16Array | Uint32Array
|
||||
): void {
|
||||
const transform = new Matrix4().makeTranslation(
|
||||
this._localOrigin.x,
|
||||
this._localOrigin.y,
|
||||
@@ -111,14 +119,15 @@ export class BatchObject {
|
||||
)
|
||||
transform.invert()
|
||||
|
||||
if (!bvh) {
|
||||
const indices: number[] | undefined =
|
||||
this._renderView.renderData.geometry.attributes?.INDEX
|
||||
const position: number[] | undefined =
|
||||
this._renderView.renderData.geometry.attributes?.POSITION
|
||||
let bvh = positionOrBvh
|
||||
|
||||
if (!(bvh instanceof MeshBVH)) {
|
||||
if (!indices) {
|
||||
throw new Error(`Cannot build a BVH with only positions. Need indices too`)
|
||||
}
|
||||
bvh = AccelerationStructure.buildBVH(
|
||||
indices,
|
||||
position,
|
||||
positionOrBvh as Float32Array | Float64Array,
|
||||
DefaultBVHOptions,
|
||||
transform
|
||||
)
|
||||
|
||||
@@ -141,21 +141,23 @@ export default class Batcher {
|
||||
rvs.forEach((nodeRv) => {
|
||||
const geometry = nodeRv.renderData.geometry
|
||||
geometry.instanced = false
|
||||
const attribs = geometry.attributes
|
||||
geometry.attributes = {
|
||||
POSITION: attribs.POSITION.slice(),
|
||||
INDEX: attribs.INDEX.slice(),
|
||||
...(attribs.COLOR && {
|
||||
COLOR: attribs.COLOR.slice()
|
||||
})
|
||||
}
|
||||
/** - I don't particularly like this branch -
|
||||
* All instances should have a transform. But it's the easiest thing we can do
|
||||
* until we figure out the viewer <-> connector object duplication inconsistency
|
||||
*/
|
||||
if (geometry.transform)
|
||||
Geometry.transformGeometryData(geometry, geometry.transform)
|
||||
nodeRv.computeAABB()
|
||||
nodeRv.computeAABB(geometry.transform)
|
||||
/** I don't think we need to duplicate geometry here, now that we're transforming the batch position directly */
|
||||
// const attribs = geometry.attributes
|
||||
// geometry.attributes = {
|
||||
// POSITION: attribs.POSITION.slice(),
|
||||
// INDEX: attribs.INDEX.slice(),
|
||||
// ...(attribs.COLOR && {
|
||||
// COLOR: attribs.COLOR.slice()
|
||||
// })
|
||||
// }
|
||||
// /** - I don't particularly like this branch -
|
||||
// * All instances should have a transform. But it's the easiest thing we can do
|
||||
// * until we figure out the viewer <-> connector object duplication inconsistency
|
||||
// */
|
||||
// if (geometry.transform)
|
||||
// Geometry.transformGeometryData(geometry, geometry.transform)
|
||||
// nodeRv.computeAABB()
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -525,6 +525,21 @@ export class InstancedMeshBatch implements Batch {
|
||||
)
|
||||
const targetInstanceTransformBuffer = this.getCurrentTransformBuffer()
|
||||
|
||||
const positions =
|
||||
this.renderViews[0].renderData.geometry.attributes?.POSITION.getFloat32Array()
|
||||
const indicesLength =
|
||||
this.renderViews[0].renderData.geometry.attributes?.INDEX.length ?? 0
|
||||
const positionsLength =
|
||||
this.renderViews[0].renderData.geometry.attributes?.POSITION.length ?? 0
|
||||
const indices =
|
||||
indicesLength < 65535 && positionsLength < 65535
|
||||
? this.renderViews[0].renderData.geometry.attributes?.INDEX.getUint16Array()
|
||||
: this.renderViews[0].renderData.geometry.attributes?.INDEX.getUint32Array()
|
||||
const colors =
|
||||
this.renderViews[0].renderData.geometry.attributes?.COLOR?.getFloat32Array()
|
||||
const normals =
|
||||
this.renderViews[0].renderData.geometry.attributes?.NORMAL?.getFloat32Array()
|
||||
|
||||
for (let k = 0; k < this.renderViews.length; k++) {
|
||||
/** Catering to typescript
|
||||
* There is no unniverse where an instanced render view does not have a transform
|
||||
@@ -555,13 +570,10 @@ export class InstancedMeshBatch implements Batch {
|
||||
batchObject.localOrigin.z
|
||||
)
|
||||
transform.invert()
|
||||
const indices: number[] | undefined =
|
||||
this.renderViews[k].renderData.geometry.attributes?.INDEX
|
||||
const position: number[] | undefined = this.renderViews[k].renderData.geometry
|
||||
.attributes?.POSITION as number[]
|
||||
|
||||
instanceBVH = AccelerationStructure.buildBVH(
|
||||
indices,
|
||||
position,
|
||||
positions,
|
||||
DefaultBVHOptions,
|
||||
transform
|
||||
)
|
||||
@@ -572,32 +584,13 @@ export class InstancedMeshBatch implements Batch {
|
||||
batchObjects.push(batchObject)
|
||||
}
|
||||
|
||||
const indices: number[] | undefined =
|
||||
this.renderViews[0].renderData.geometry.attributes?.INDEX
|
||||
|
||||
const positions: number[] | undefined =
|
||||
this.renderViews[0].renderData.geometry.attributes?.POSITION
|
||||
|
||||
const colors: number[] | undefined =
|
||||
this.renderViews[0].renderData.geometry.attributes?.COLOR
|
||||
|
||||
const normals: number[] | undefined =
|
||||
this.renderViews[0].renderData.geometry.attributes?.NORMAL
|
||||
|
||||
/** Catering to typescript
|
||||
* There is no unniverse where indices or positions are undefined at this point
|
||||
*/
|
||||
if (!indices || !positions) {
|
||||
throw new Error(`Cannot build batch ${this.id}. Undefined indices or positions`)
|
||||
}
|
||||
this.geometry = this.makeInstancedMeshGeometry(
|
||||
positions.length >= 65535 || indices.length >= 65535
|
||||
? new Uint32Array(indices)
|
||||
: new Uint16Array(indices),
|
||||
new Float64Array(positions),
|
||||
normals ? new Float32Array(normals) : undefined,
|
||||
colors ? new Float32Array(colors) : undefined
|
||||
)
|
||||
this.geometry = this.makeInstancedMeshGeometry(indices, positions, normals, colors)
|
||||
|
||||
this.mesh = new SpeckleInstancedMesh(this.geometry)
|
||||
this.mesh.setBatchObjects(batchObjects)
|
||||
@@ -655,16 +648,12 @@ export class InstancedMeshBatch implements Batch {
|
||||
|
||||
private makeInstancedMeshGeometry(
|
||||
indices: Uint32Array | Uint16Array,
|
||||
position: Float64Array,
|
||||
position: Float32Array,
|
||||
normal?: Float32Array,
|
||||
color?: Float32Array
|
||||
): BufferGeometry {
|
||||
const geometry = new BufferGeometry()
|
||||
if (position) {
|
||||
/** When RTE enabled, we'll be storing the high component of the encoding here,
|
||||
* which considering our current encoding method is actually the original casted
|
||||
* down float32 position!
|
||||
*/
|
||||
geometry.setAttribute('position', new Float32BufferAttribute(position, 3))
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from './Batch.js'
|
||||
import { ObjectLayers } from '../../IViewer.js'
|
||||
import Materials from '../materials/Materials.js'
|
||||
import { ChunkArray } from '../converter/VirtualArray.js'
|
||||
|
||||
export default class LineBatch implements Batch {
|
||||
public id: string
|
||||
@@ -231,6 +232,7 @@ export default class LineBatch implements Batch {
|
||||
|
||||
public buildBatch() {
|
||||
let attributeCount = 0
|
||||
const rvAABB: Box3 = new Box3()
|
||||
const bounds = new Box3()
|
||||
this.renderViews.forEach((val: NodeRenderView) => {
|
||||
if (!val.renderData.geometry.attributes) {
|
||||
@@ -241,14 +243,18 @@ export default class LineBatch implements Batch {
|
||||
: val.renderData.geometry.attributes.POSITION.length
|
||||
bounds.union(val.aabb)
|
||||
})
|
||||
const position = new Float64Array(attributeCount)
|
||||
const needsRTE = Geometry.needsRTE(bounds)
|
||||
|
||||
const position = needsRTE
|
||||
? new Float64Array(attributeCount)
|
||||
: new Float32Array(attributeCount)
|
||||
let offset = 0
|
||||
for (let k = 0; k < this.renderViews.length; k++) {
|
||||
const geometry = this.renderViews[k].renderData.geometry
|
||||
if (!geometry.attributes) {
|
||||
throw new Error(`Cannot build batch ${this.id}. Invalid geometry`)
|
||||
}
|
||||
let points: Array<number>
|
||||
let points: Array<number> | ChunkArray
|
||||
/** We need to make sure the line geometry has a layout of :
|
||||
* start(x,y,z), end(x,y,z), start(x,y,z), end(x,y,z)... etc
|
||||
* Some geometries have that inherent form, some don't
|
||||
@@ -258,19 +264,30 @@ export default class LineBatch implements Batch {
|
||||
points = new Array(2 * length)
|
||||
|
||||
for (let i = 0; i < length; i += 3) {
|
||||
points[2 * i] = geometry.attributes.POSITION[i]
|
||||
points[2 * i + 1] = geometry.attributes.POSITION[i + 1]
|
||||
points[2 * i + 2] = geometry.attributes.POSITION[i + 2]
|
||||
points[2 * i] = geometry.attributes.POSITION.get(i)
|
||||
points[2 * i + 1] = geometry.attributes.POSITION.get(i + 1)
|
||||
points[2 * i + 2] = geometry.attributes.POSITION.get(i + 2)
|
||||
|
||||
points[2 * i + 3] = geometry.attributes.POSITION[i + 3]
|
||||
points[2 * i + 4] = geometry.attributes.POSITION[i + 4]
|
||||
points[2 * i + 5] = geometry.attributes.POSITION[i + 5]
|
||||
points[2 * i + 3] = geometry.attributes.POSITION.get(i + 3)
|
||||
points[2 * i + 4] = geometry.attributes.POSITION.get(i + 4)
|
||||
points[2 * i + 5] = geometry.attributes.POSITION.get(i + 5)
|
||||
}
|
||||
position.set(points, offset)
|
||||
} else {
|
||||
points = geometry.attributes.POSITION
|
||||
geometry.attributes.POSITION.copyToBuffer(position, offset)
|
||||
}
|
||||
|
||||
position.set(points, offset)
|
||||
const positionSubArray = position.subarray(offset, offset + points.length)
|
||||
Geometry.transformArray(positionSubArray, geometry.transform, 0, points.length)
|
||||
/** We re-compute the render view aabb based on transformed geometry
|
||||
* We do this because some transforms like non-uniform scaling can produce incorrect results
|
||||
* if we compute an aabb from original geometry then apply the transform. That's why we compute
|
||||
* an aabb from the transformed geometry here and set it in the rv
|
||||
*/
|
||||
rvAABB.setFromArray(positionSubArray)
|
||||
this.renderViews[k].aabb = rvAABB
|
||||
|
||||
this.renderViews[k].setBatchData(this.id, offset / 6, points.length / 6)
|
||||
|
||||
offset += points.length
|
||||
@@ -319,10 +336,15 @@ export default class LineBatch implements Batch {
|
||||
return material
|
||||
}
|
||||
|
||||
private makeLineGeometry(position: Float64Array): LineSegmentsGeometry {
|
||||
private makeLineGeometry(
|
||||
position: Float64Array | Float32Array
|
||||
): LineSegmentsGeometry {
|
||||
const geometry = new LineSegmentsGeometry()
|
||||
/** This will set the instanceStart and instanceEnd attributes. These will be our high parts */
|
||||
geometry.setPositions(new Float32Array(position))
|
||||
if (position instanceof Float64Array)
|
||||
/** We need to re-allocate because there is no way to cast it down to float32. If we pass in a Float64Array, three.js will do it anyway */
|
||||
geometry.setPositions(new Float32Array(position))
|
||||
else geometry.setPositions(position)
|
||||
|
||||
const buffer = new Float32Array(position.length + position.length / 3)
|
||||
this.colorBuffer = new InstancedInterleavedBuffer(buffer, 8, 1) // rgba, rgba
|
||||
|
||||
@@ -193,6 +193,7 @@ export class MeshBatch extends PrimitiveBatch {
|
||||
public buildBatch(): Promise<void> {
|
||||
let indicesCount = 0
|
||||
let attributeCount = 0
|
||||
const rvAABB: Box3 = new Box3()
|
||||
const bounds: Box3 = new Box3()
|
||||
for (let k = 0; k < this.renderViews.length; k++) {
|
||||
const ervee = this.renderViews[k]
|
||||
@@ -209,11 +210,17 @@ export class MeshBatch extends PrimitiveBatch {
|
||||
attributeCount += ervee.renderData.geometry.attributes.POSITION.length
|
||||
bounds.union(ervee.aabb)
|
||||
}
|
||||
const needsRTE = Geometry.needsRTE(bounds)
|
||||
|
||||
const hasVertexColors =
|
||||
this.renderViews[0].renderData.geometry.attributes?.COLOR !== undefined
|
||||
const indices = new Uint32Array(indicesCount)
|
||||
const position = new Float64Array(attributeCount)
|
||||
const indices =
|
||||
attributeCount >= 65535 || indicesCount >= 65535
|
||||
? new Uint32Array(indicesCount)
|
||||
: new Uint16Array(indicesCount)
|
||||
const position = needsRTE
|
||||
? new Float64Array(attributeCount)
|
||||
: new Float32Array(attributeCount)
|
||||
const color = new Float32Array(hasVertexColors ? attributeCount : 0)
|
||||
color.fill(1)
|
||||
const batchIndices = new Float32Array(attributeCount / 3)
|
||||
@@ -228,49 +235,84 @@ export class MeshBatch extends PrimitiveBatch {
|
||||
/** Catering to typescript
|
||||
* There is no unniverse where indices or positions are undefined at this point
|
||||
*/
|
||||
if (!geometry.attributes || !geometry.attributes.INDEX) {
|
||||
if (!geometry.attributes || !geometry.attributes?.INDEX) {
|
||||
throw new Error(`Cannot build batch ${this.id}. Invalid geometry, or indices`)
|
||||
}
|
||||
indices.set(
|
||||
geometry.attributes.INDEX.map((val) => val + offset / 3),
|
||||
arrayOffset
|
||||
|
||||
geometry.attributes?.INDEX.copyToBuffer(indices, arrayOffset)
|
||||
const indicesSubArray = indices.subarray(
|
||||
arrayOffset,
|
||||
arrayOffset + geometry.attributes?.INDEX.length
|
||||
)
|
||||
position.set(geometry.attributes.POSITION, offset)
|
||||
if (geometry.attributes.COLOR) color.set(geometry.attributes.COLOR, offset)
|
||||
|
||||
geometry.attributes?.POSITION.copyToBuffer(position, offset)
|
||||
const positionSubarray = position.subarray(
|
||||
offset,
|
||||
offset +
|
||||
(this.renderViews[k].renderData.geometry.attributes?.POSITION.length ?? 0)
|
||||
)
|
||||
/** We transform the copied geometry so that we do not alter original chunk data which might be shared */
|
||||
Geometry.transformArray(
|
||||
positionSubarray,
|
||||
geometry.transform,
|
||||
0,
|
||||
geometry.attributes?.POSITION.length
|
||||
)
|
||||
|
||||
if (geometry.attributes.COLOR) {
|
||||
geometry.attributes?.COLOR.copyToBuffer(color, offset)
|
||||
}
|
||||
|
||||
/** We either copy over the provided vertex normals */
|
||||
if (geometry.attributes.NORMAL) {
|
||||
normals.set(geometry.attributes.NORMAL, offset)
|
||||
geometry.attributes?.NORMAL.copyToBuffer(normals, offset)
|
||||
} else {
|
||||
/** Either we compute them ourselves */
|
||||
Geometry.computeVertexNormalsBuffer(
|
||||
Geometry.computeVertexNormalsBufferVirtual(
|
||||
normals.subarray(
|
||||
offset,
|
||||
offset + geometry.attributes.POSITION.length
|
||||
offset +
|
||||
(this.renderViews[k].renderData.geometry.attributes?.POSITION.length ?? 0)
|
||||
) as unknown as number[],
|
||||
geometry.attributes.POSITION,
|
||||
geometry.attributes.INDEX
|
||||
)
|
||||
}
|
||||
batchIndices.fill(
|
||||
k,
|
||||
offset / 3,
|
||||
offset / 3 + geometry.attributes.POSITION.length / 3
|
||||
)
|
||||
batchIndices.fill(k, offset / 3, offset / 3 + geometry.attributes.POSITION.length)
|
||||
this.renderViews[k].setBatchData(
|
||||
this.id,
|
||||
arrayOffset,
|
||||
geometry.attributes.INDEX.length,
|
||||
offset / 3,
|
||||
offset / 3 + geometry.attributes.POSITION.length / 3
|
||||
offset / 3 + geometry.attributes.POSITION.length
|
||||
)
|
||||
|
||||
/** We re-compute the render view aabb based on transformed geometry
|
||||
* We do this because some transforms like non-uniform scaling can produce incorrect results
|
||||
* if we compute an aabb from original geometry then apply the transform. That's why we compute
|
||||
* an aabb from the transformed geometry here and set it in the rv
|
||||
*/
|
||||
rvAABB.setFromArray(positionSubarray)
|
||||
this.renderViews[k].aabb = rvAABB
|
||||
|
||||
const batchObject = new BatchObject(this.renderViews[k], k)
|
||||
batchObject.buildAccelerationStructure()
|
||||
batchObject.buildAccelerationStructure(positionSubarray, indicesSubArray)
|
||||
batchObjects.push(batchObject)
|
||||
|
||||
indices.set(
|
||||
batchObject.accelerationStructure.bvh.geometry.index?.array as number[],
|
||||
arrayOffset
|
||||
)
|
||||
|
||||
/** Re-index the indices inside the batch */
|
||||
for (let i = 0; i < indicesSubArray.length; i++) {
|
||||
indicesSubArray[i] = indicesSubArray[i] + offset / 3
|
||||
}
|
||||
|
||||
offset += geometry.attributes.POSITION.length
|
||||
arrayOffset += geometry.attributes.INDEX.length
|
||||
|
||||
this.renderViews[k].disposeGeometry()
|
||||
}
|
||||
|
||||
const geometry = this.makeMeshGeometry(
|
||||
@@ -280,7 +322,7 @@ export class MeshBatch extends PrimitiveBatch {
|
||||
batchIndices,
|
||||
hasVertexColors ? color : undefined
|
||||
)
|
||||
const needsRTE = Geometry.needsRTE(bounds)
|
||||
|
||||
if (needsRTE) Geometry.updateRTEGeometry(geometry, position)
|
||||
|
||||
this.primitive = new SpeckleMesh(geometry, needsRTE)
|
||||
@@ -296,16 +338,16 @@ export class MeshBatch extends PrimitiveBatch {
|
||||
this.primitive.frustumCulled = false
|
||||
this.primitive.geometry.addGroup(0, this.getCount(), 0)
|
||||
|
||||
batchObjects.forEach((element: BatchObject) => {
|
||||
element.renderView.disposeGeometry()
|
||||
})
|
||||
// batchObjects.forEach((element: BatchObject) => {
|
||||
// element.renderView.disposeGeometry()
|
||||
// })
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
protected makeMeshGeometry(
|
||||
indices: Uint32Array | Uint16Array,
|
||||
position: Float64Array,
|
||||
position: Float64Array | Float32Array,
|
||||
normals: Float32Array,
|
||||
batchIndices: Float32Array,
|
||||
color?: Float32Array
|
||||
@@ -313,10 +355,10 @@ export class MeshBatch extends PrimitiveBatch {
|
||||
const geometry = new BufferGeometry()
|
||||
if (position.length >= 65535 || indices.length >= 65535) {
|
||||
this.indexBuffer0 = new Uint32BufferAttribute(indices, 1)
|
||||
this.indexBuffer1 = new Uint32BufferAttribute(new Uint32Array(indices.length), 1)
|
||||
this.indexBuffer1 = new Uint32BufferAttribute(indices, 1)
|
||||
} else {
|
||||
this.indexBuffer0 = new Uint16BufferAttribute(indices, 1)
|
||||
this.indexBuffer1 = new Uint16BufferAttribute(new Uint16Array(indices.length), 1)
|
||||
this.indexBuffer1 = new Uint16BufferAttribute(indices, 1)
|
||||
}
|
||||
geometry.setIndex(this.indexBuffer0)
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ export class PointBatch extends PrimitiveBatch {
|
||||
|
||||
public buildBatch(): Promise<void> {
|
||||
let attributeCount = 0
|
||||
const rvAABB: Box3 = new Box3()
|
||||
const bounds = new Box3()
|
||||
for (let k = 0; k < this.renderViews.length; k++) {
|
||||
const ervee = this.renderViews[k]
|
||||
@@ -170,9 +171,17 @@ export class PointBatch extends PrimitiveBatch {
|
||||
attributeCount += ervee.renderData.geometry.attributes.POSITION.length
|
||||
bounds.union(ervee.aabb)
|
||||
}
|
||||
const position = new Float64Array(attributeCount)
|
||||
const color = new Float32Array(attributeCount).fill(1)
|
||||
const index = new Int32Array(attributeCount / 3)
|
||||
const needsRTE = Geometry.needsRTE(bounds)
|
||||
const hasVertexColors =
|
||||
this.renderViews[0].renderData.geometry.attributes?.COLOR !== undefined
|
||||
const needsInt32Indices = attributeCount >= 65535
|
||||
const position = needsRTE
|
||||
? new Float64Array(attributeCount)
|
||||
: new Float32Array(attributeCount)
|
||||
const color = new Float32Array(hasVertexColors ? attributeCount : 0).fill(1)
|
||||
const index = needsInt32Indices
|
||||
? new Uint32Array(attributeCount / 3)
|
||||
: new Uint16Array(attributeCount / 3)
|
||||
let offset = 0
|
||||
let indexOffset = 0
|
||||
for (let k = 0; k < this.renderViews.length; k++) {
|
||||
@@ -180,14 +189,39 @@ export class PointBatch extends PrimitiveBatch {
|
||||
if (!geometry.attributes) {
|
||||
throw new Error(`Cannot build batch ${this.id}. Invalid geometry, or indices`)
|
||||
}
|
||||
position.set(geometry.attributes.POSITION, offset)
|
||||
if (geometry.attributes.COLOR) color.set(geometry.attributes.COLOR, offset)
|
||||
|
||||
geometry.attributes?.POSITION.copyToBuffer(position, offset)
|
||||
const positionSubarray = position.subarray(
|
||||
offset,
|
||||
offset +
|
||||
(this.renderViews[k].renderData.geometry.attributes?.POSITION.length ?? 0)
|
||||
)
|
||||
|
||||
Geometry.transformArray(
|
||||
positionSubarray,
|
||||
geometry.transform,
|
||||
0,
|
||||
geometry.attributes?.POSITION.length
|
||||
)
|
||||
if (geometry.attributes.COLOR) {
|
||||
geometry.attributes?.COLOR.copyToBuffer(color, offset)
|
||||
}
|
||||
index.set(
|
||||
new Int32Array(geometry.attributes.POSITION.length / 3).map(
|
||||
(_value, index) => index + indexOffset
|
||||
),
|
||||
(needsInt32Indices
|
||||
? new Uint32Array(geometry.attributes.POSITION.length / 3)
|
||||
: new Uint16Array(geometry.attributes.POSITION.length / 3)
|
||||
).map((_value, index) => index + indexOffset),
|
||||
indexOffset
|
||||
)
|
||||
|
||||
/** We re-compute the render view aabb based on transformed geometry
|
||||
* We do this because some transforms like non-uniform scaling can produce incorrect results
|
||||
* if we compute an aabb from original geometry then apply the transform. That's why we compute
|
||||
* an aabb from the transformed geometry here and set it in the rv
|
||||
*/
|
||||
rvAABB.setFromArray(positionSubarray)
|
||||
this.renderViews[k].aabb = rvAABB
|
||||
|
||||
this.renderViews[k].setBatchData(
|
||||
this.id,
|
||||
offset / 3,
|
||||
@@ -201,7 +235,7 @@ export class PointBatch extends PrimitiveBatch {
|
||||
}
|
||||
const geometry = this.makePointGeometry(index, position, color)
|
||||
|
||||
if (Geometry.needsRTE(bounds)) {
|
||||
if (needsRTE) {
|
||||
Geometry.updateRTEGeometry(geometry, position)
|
||||
if (!this.batchMaterial.defines) this.batchMaterial.defines = {}
|
||||
this.batchMaterial.defines['USE_RTE'] = ' '
|
||||
@@ -221,8 +255,8 @@ export class PointBatch extends PrimitiveBatch {
|
||||
}
|
||||
|
||||
protected makePointGeometry(
|
||||
index: Int32Array,
|
||||
position: Float64Array,
|
||||
index: Uint32Array | Uint16Array,
|
||||
position: Float64Array | Float32Array,
|
||||
color: Float32Array
|
||||
): BufferGeometry {
|
||||
const geometry = new BufferGeometry()
|
||||
|
||||
@@ -144,8 +144,8 @@ export default class TextBatch implements Batch {
|
||||
* - Even if, the **text batch does not use the materials in it's draw groups**, it emulates the behavior as if it would
|
||||
*/
|
||||
public setBatchBuffers(ranges: BatchUpdateRange[]): void {
|
||||
console.warn(' Groups -> ', this.mesh.groups)
|
||||
console.warn(' Ranges -> ', ranges)
|
||||
// console.warn(' Groups -> ', this.mesh.groups)
|
||||
// console.warn(' Ranges -> ', ranges)
|
||||
const splitRanges: BatchUpdateRange[] = []
|
||||
ranges.forEach((range: BatchUpdateRange) => {
|
||||
for (let k = 0; k < range.count; k++) {
|
||||
@@ -321,7 +321,7 @@ export default class TextBatch implements Batch {
|
||||
screenOriented: boolean
|
||||
}
|
||||
const text = new Text()
|
||||
this.renderViews[k].renderData.geometry.bakeTransform?.decompose(
|
||||
this.renderViews[k].renderData.geometry.transform?.decompose(
|
||||
text.position,
|
||||
text.quaternion,
|
||||
text.scale
|
||||
@@ -349,24 +349,23 @@ export default class TextBatch implements Batch {
|
||||
/** We're using visibleBounds for a better fit */
|
||||
const bounds = textRenderInfo.visibleBounds
|
||||
// console.log('bounds -> ', bounds)
|
||||
const vertices = []
|
||||
vertices.push(
|
||||
bounds[0],
|
||||
bounds[3],
|
||||
0,
|
||||
bounds[2],
|
||||
bounds[3],
|
||||
0,
|
||||
bounds[0],
|
||||
bounds[1],
|
||||
0,
|
||||
bounds[2],
|
||||
bounds[1],
|
||||
0
|
||||
)
|
||||
const vertices = new Float32Array(12)
|
||||
vertices[0] = bounds[0]
|
||||
vertices[1] = bounds[3]
|
||||
vertices[2] = 0
|
||||
vertices[3] = bounds[2]
|
||||
vertices[4] = bounds[3]
|
||||
vertices[5] = 0
|
||||
vertices[6] = bounds[0]
|
||||
vertices[7] = bounds[1]
|
||||
vertices[8] = 0
|
||||
vertices[9] = bounds[2]
|
||||
vertices[10] = bounds[1]
|
||||
vertices[11] = 0
|
||||
|
||||
box.setFromArray(vertices)
|
||||
box.applyMatrix4(
|
||||
this.renderViews[k].renderData.geometry.bakeTransform || new Matrix4()
|
||||
this.renderViews[k].renderData.geometry.transform || new Matrix4()
|
||||
)
|
||||
|
||||
needsRTE ||= Geometry.needsRTE(box)
|
||||
@@ -374,7 +373,7 @@ export default class TextBatch implements Batch {
|
||||
const geometry = text.geometry
|
||||
geometry.computeBoundingBox()
|
||||
const textBvh = AccelerationStructure.buildBVH(
|
||||
geometry.index?.array as number[],
|
||||
geometry.index?.array as unknown as Uint16Array | Uint32Array,
|
||||
vertices,
|
||||
DefaultBVHOptions
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ export class TextBatchObject extends BatchObject {
|
||||
|
||||
public constructor(renderView: NodeRenderView, batchIndex: number) {
|
||||
super(renderView, batchIndex)
|
||||
if (renderView.renderData.geometry.bakeTransform)
|
||||
this.textTransform.copy(renderView.renderData.geometry.bakeTransform)
|
||||
if (renderView.renderData.geometry.transform)
|
||||
this.textTransform.copy(renderView.renderData.geometry.transform)
|
||||
/** TO DO: Not sure we should do this */
|
||||
this.transform.copy(this.textTransform)
|
||||
this.transformInv.copy(new Matrix4().copy(this.textTransform).invert())
|
||||
|
||||
@@ -6,18 +6,19 @@ import {
|
||||
Float32BufferAttribute,
|
||||
InstancedInterleavedBuffer,
|
||||
InterleavedBufferAttribute,
|
||||
MathUtils,
|
||||
Matrix4,
|
||||
Vector2,
|
||||
Vector3,
|
||||
Vector4
|
||||
} from 'three'
|
||||
import { type SpeckleObject } from '../../IViewer.js'
|
||||
import { DataChunk, type SpeckleObject } from '../../IViewer.js'
|
||||
import { getRelativeOffset, makePerspectiveProjection } from '../Helpers.js'
|
||||
import earcut from 'earcut'
|
||||
import { ChunkArray } from './VirtualArray.js'
|
||||
|
||||
const vecBuff0: Vector3 = new Vector3()
|
||||
const floatArrayBuff: Float32Array = new Float32Array(16)
|
||||
Vector3
|
||||
|
||||
export enum GeometryAttributes {
|
||||
POSITION = 'POSITION',
|
||||
@@ -28,14 +29,23 @@ export enum GeometryAttributes {
|
||||
INDEX = 'INDEX'
|
||||
}
|
||||
|
||||
type AttributeValue = ChunkArray
|
||||
|
||||
// Required keys
|
||||
type RequiredKeys = GeometryAttributes.POSITION | GeometryAttributes.INDEX
|
||||
|
||||
// Optional keys
|
||||
type OptionalKeys = Exclude<GeometryAttributes, RequiredKeys>
|
||||
|
||||
// Final shape: required + optional keys
|
||||
type GeometryAttributesShape = {
|
||||
[K in RequiredKeys]: AttributeValue
|
||||
} & {
|
||||
[K in OptionalKeys]?: AttributeValue
|
||||
}
|
||||
|
||||
export interface GeometryData {
|
||||
attributes:
|
||||
| ({
|
||||
[GeometryAttributes.POSITION]: number[]
|
||||
} & Partial<
|
||||
Record<Exclude<GeometryAttributes, GeometryAttributes.POSITION>, number[]>
|
||||
>)
|
||||
| null
|
||||
attributes: GeometryAttributesShape | null
|
||||
bakeTransform: Matrix4 | null
|
||||
transform: Matrix4 | null
|
||||
metaData?: SpeckleObject
|
||||
@@ -70,11 +80,7 @@ export class Geometry {
|
||||
|
||||
Geometry.DoubleToHighLowBuffer(doublePositions, position_low, position_high)
|
||||
|
||||
const instanceBufferLow = new InstancedInterleavedBuffer(
|
||||
new Float32Array(position_low),
|
||||
6,
|
||||
1
|
||||
) // xyz, xyz
|
||||
const instanceBufferLow = new InstancedInterleavedBuffer(position_low, 6, 1) // xyz, xyz
|
||||
geometry.setAttribute(
|
||||
'instanceStartLow',
|
||||
new InterleavedBufferAttribute(instanceBufferLow, 3, 0)
|
||||
@@ -87,7 +93,7 @@ export class Geometry {
|
||||
}
|
||||
|
||||
static mergeGeometryAttribute(
|
||||
attributes: (number[] | undefined)[],
|
||||
attributes: AttributeValue[],
|
||||
target: Float32Array | Float64Array
|
||||
): ArrayLike<number> {
|
||||
let offset = 0
|
||||
@@ -96,15 +102,15 @@ export class Geometry {
|
||||
if (!attribute || !target) {
|
||||
throw new Error('Cannot merge geometries. Indices or positions are undefined')
|
||||
}
|
||||
target.set(attribute, offset)
|
||||
attribute.copyToBuffer(target, offset)
|
||||
offset += attribute.length
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
static mergeIndexAttribute(
|
||||
indexAttributes: (number[] | undefined)[],
|
||||
positionAttributes: (number[] | undefined)[]
|
||||
indexAttributes: AttributeValue[],
|
||||
positionAttributes: AttributeValue[]
|
||||
): number[] {
|
||||
let indexOffset = 0
|
||||
const mergedIndex = []
|
||||
@@ -117,7 +123,7 @@ export class Geometry {
|
||||
}
|
||||
|
||||
for (let j = 0; j < index.length; ++j) {
|
||||
mergedIndex.push(index[j] + indexOffset / 3)
|
||||
mergedIndex.push(index.get(j) + indexOffset / 3)
|
||||
}
|
||||
|
||||
indexOffset += positions.length
|
||||
@@ -139,53 +145,59 @@ export class Geometry {
|
||||
Geometry.transformGeometryData(geometries[i], geometries[i].bakeTransform)
|
||||
}
|
||||
|
||||
if (sampleAttributes && sampleAttributes[GeometryAttributes.INDEX]) {
|
||||
const indexAttributes: (number[] | undefined)[] = geometries.map(
|
||||
(item: GeometryData) => {
|
||||
/** Catering to typescript */
|
||||
if (!item.attributes) return
|
||||
return item.attributes[GeometryAttributes.INDEX]
|
||||
}
|
||||
)
|
||||
const positionAttributes: (number[] | undefined)[] = geometries.map((item) => {
|
||||
if (sampleAttributes && sampleAttributes.INDEX) {
|
||||
const indexAttributes = geometries.map((item: GeometryData) => {
|
||||
/** Catering to typescript */
|
||||
if (!item.attributes) return
|
||||
return item.attributes[GeometryAttributes.POSITION]
|
||||
})
|
||||
return item.attributes.INDEX
|
||||
}) as ChunkArray[]
|
||||
const positionAttributes = geometries.map((item) => {
|
||||
/** Catering to typescript */
|
||||
if (!item.attributes) return
|
||||
return item.attributes.POSITION
|
||||
}) as ChunkArray[]
|
||||
/** o_0 Catering to typescript*/
|
||||
if (mergedGeometry.attributes)
|
||||
mergedGeometry.attributes[GeometryAttributes.INDEX] =
|
||||
Geometry.mergeIndexAttribute(indexAttributes, positionAttributes)
|
||||
mergedGeometry.attributes.INDEX = new ChunkArray([
|
||||
{
|
||||
data: Geometry.mergeIndexAttribute(indexAttributes, positionAttributes),
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
for (const k in sampleAttributes) {
|
||||
if (k !== GeometryAttributes.INDEX) {
|
||||
const attributes: (number[] | undefined)[] = geometries.map((item) => {
|
||||
const attributes: ChunkArray[] = geometries.map((item) => {
|
||||
/** Catering to typescript */
|
||||
if (!item.attributes) return
|
||||
return item.attributes[k as GeometryAttributes] as number[]
|
||||
})
|
||||
return item.attributes[k as GeometryAttributes]
|
||||
}) as ChunkArray[]
|
||||
/** Catering to typescript */
|
||||
if (mergedGeometry.attributes)
|
||||
mergedGeometry.attributes[k as GeometryAttributes] =
|
||||
Geometry.mergeGeometryAttribute(
|
||||
attributes,
|
||||
k === GeometryAttributes.POSITION
|
||||
? new Float64Array(
|
||||
attributes.reduce((prev, cur) => {
|
||||
/** Catering to typescript */
|
||||
if (!cur) return 0
|
||||
return prev + cur.length
|
||||
}, 0)
|
||||
)
|
||||
: new Float32Array(
|
||||
attributes.reduce((prev, cur) => {
|
||||
/** Catering to typescript */
|
||||
if (!cur) return 0
|
||||
return prev + cur.length
|
||||
}, 0)
|
||||
)
|
||||
) as number[]
|
||||
if (mergedGeometry.attributes) {
|
||||
const mergedData = Geometry.mergeGeometryAttribute(
|
||||
attributes,
|
||||
k === GeometryAttributes.POSITION
|
||||
? new Float64Array(
|
||||
attributes.reduce((prev, cur) => {
|
||||
/** Catering to typescript */
|
||||
if (!cur) return 0
|
||||
return prev + cur.length
|
||||
}, 0)
|
||||
)
|
||||
: new Float32Array(
|
||||
attributes.reduce((prev, cur) => {
|
||||
/** Catering to typescript */
|
||||
if (!cur) return 0
|
||||
return prev + cur.length
|
||||
}, 0)
|
||||
)
|
||||
) as number[]
|
||||
mergedGeometry.attributes[k as GeometryAttributes] = new ChunkArray([
|
||||
{ data: mergedData, id: MathUtils.generateUUID(), references: 1 }
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,23 +214,76 @@ export class Geometry {
|
||||
if (!geometryData.attributes) return
|
||||
if (!geometryData.attributes.POSITION) return
|
||||
if (!m) return
|
||||
if (Geometry.isMatrix4Identity(m)) return
|
||||
|
||||
const e = m.elements
|
||||
geometryData.attributes.POSITION.chunkArray.forEach((chunk: DataChunk) => {
|
||||
for (let k = 0; k < chunk.data.length; k += 3) {
|
||||
const x = chunk.data[k],
|
||||
y = chunk.data[k + 1],
|
||||
z = chunk.data[k + 2]
|
||||
const w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15])
|
||||
|
||||
for (let k = 0; k < geometryData.attributes.POSITION.length; k += 3) {
|
||||
const x = geometryData.attributes.POSITION[k],
|
||||
y = geometryData.attributes.POSITION[k + 1],
|
||||
z = geometryData.attributes.POSITION[k + 2]
|
||||
chunk.data[k] = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w
|
||||
chunk.data[k + 1] = (e[1] * x + e[5] * y + e[9] * z + e[13]) * w
|
||||
chunk.data[k + 2] = (e[2] * x + e[6] * y + e[10] * z + e[14]) * w
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public static transformArray(
|
||||
array: number[] | Float32Array | Float64Array,
|
||||
m: Matrix4 | null,
|
||||
offset?: number,
|
||||
count?: number
|
||||
) {
|
||||
if (!m) return
|
||||
if (Geometry.isMatrix4Identity(m)) return
|
||||
|
||||
const e = m.elements
|
||||
offset = offset || 0
|
||||
count = count || array.length
|
||||
for (let k = offset; k < offset + count; k += 3) {
|
||||
const x = array[k],
|
||||
y = array[k + 1],
|
||||
z = array[k + 2]
|
||||
const w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15])
|
||||
|
||||
geometryData.attributes.POSITION[k] = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w
|
||||
geometryData.attributes.POSITION[k + 1] =
|
||||
(e[1] * x + e[5] * y + e[9] * z + e[13]) * w
|
||||
geometryData.attributes.POSITION[k + 2] =
|
||||
(e[2] * x + e[6] * y + e[10] * z + e[14]) * w
|
||||
array[k] = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w
|
||||
array[k + 1] = (e[1] * x + e[5] * y + e[9] * z + e[13]) * w
|
||||
array[k + 2] = (e[2] * x + e[6] * y + e[10] * z + e[14]) * w
|
||||
}
|
||||
}
|
||||
|
||||
public static isMatrix4Identity(matrix: Matrix4) {
|
||||
const e = matrix.elements
|
||||
|
||||
// Check all off-diagonal elements first
|
||||
if (
|
||||
e[1] !== 0 ||
|
||||
e[2] !== 0 ||
|
||||
e[3] !== 0 ||
|
||||
e[4] !== 0 ||
|
||||
e[6] !== 0 ||
|
||||
e[7] !== 0 ||
|
||||
e[8] !== 0 ||
|
||||
e[9] !== 0 ||
|
||||
e[11] !== 0 ||
|
||||
e[12] !== 0 ||
|
||||
e[13] !== 0 ||
|
||||
e[14] !== 0
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Now check diagonals
|
||||
if (e[0] !== 1 || e[5] !== 1 || e[10] !== 1 || e[15] !== 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
public static unpackColors(int32Colors: number[]): number[] {
|
||||
const colors = new Array<number>(int32Colors.length * 3)
|
||||
for (let i = 0; i < int32Colors.length; i++) {
|
||||
@@ -431,9 +496,58 @@ export class Geometry {
|
||||
}
|
||||
}
|
||||
|
||||
public static computeVertexNormalsBufferVirtual(
|
||||
buffer: number[],
|
||||
position: ChunkArray,
|
||||
index: ChunkArray
|
||||
) {
|
||||
const pA = new Vector3(),
|
||||
pB = new Vector3(),
|
||||
pC = new Vector3()
|
||||
const nA = new Vector3(),
|
||||
nB = new Vector3(),
|
||||
nC = new Vector3()
|
||||
const cb = new Vector3(),
|
||||
ab = new Vector3()
|
||||
|
||||
// indexed elements
|
||||
for (let i = 0, il = index.length; i < il; i += 3) {
|
||||
const vA = index.get(i + 0)
|
||||
const vB = index.get(i + 1)
|
||||
const vC = index.get(i + 2)
|
||||
pA.set(position.get(vA * 3), position.get(vA * 3 + 1), position.get(vA * 3 + 2))
|
||||
pB.set(position.get(vB * 3), position.get(vB * 3 + 1), position.get(vB * 3 + 2))
|
||||
pC.set(position.get(vC * 3), position.get(vC * 3 + 1), position.get(vC * 3 + 2))
|
||||
|
||||
cb.subVectors(pC, pB)
|
||||
ab.subVectors(pA, pB)
|
||||
cb.cross(ab)
|
||||
|
||||
nA.fromArray(buffer, vA * 3)
|
||||
nB.fromArray(buffer, vB * 3)
|
||||
nC.fromArray(buffer, vC * 3)
|
||||
|
||||
nA.add(cb)
|
||||
nB.add(cb)
|
||||
nC.add(cb)
|
||||
|
||||
buffer[vA * 3] = nA.x
|
||||
buffer[vA * 3 + 1] = nA.y
|
||||
buffer[vA * 3 + 2] = nA.z
|
||||
|
||||
buffer[vB * 3] = nB.x
|
||||
buffer[vB * 3 + 1] = nB.y
|
||||
buffer[vB * 3 + 2] = nB.z
|
||||
|
||||
buffer[vC * 3] = nC.x
|
||||
buffer[vC * 3 + 1] = nC.y
|
||||
buffer[vC * 3 + 2] = nC.z
|
||||
}
|
||||
}
|
||||
|
||||
public static computeVertexNormals(
|
||||
buffer: BufferGeometry,
|
||||
doublePositions: Float64Array
|
||||
positions: Float64Array | Float32Array
|
||||
) {
|
||||
const index = buffer.index
|
||||
const positionAttribute = buffer.getAttribute('position')
|
||||
@@ -470,9 +584,9 @@ export class Geometry {
|
||||
const vB = index.getX(i + 1)
|
||||
const vC = index.getX(i + 2)
|
||||
|
||||
pA.fromArray(doublePositions, vA * 3)
|
||||
pB.fromArray(doublePositions, vB * 3)
|
||||
pC.fromArray(doublePositions, vC * 3)
|
||||
pA.fromArray(positions, vA * 3)
|
||||
pB.fromArray(positions, vB * 3)
|
||||
pC.fromArray(positions, vC * 3)
|
||||
|
||||
cb.subVectors(pC, pB)
|
||||
ab.subVectors(pA, pB)
|
||||
@@ -495,9 +609,9 @@ export class Geometry {
|
||||
|
||||
for (let i = 0, il = positionAttribute.count; i < il; i += 3) {
|
||||
/** This is done blind. Don't think speckle supports non-indexed geometry */
|
||||
pA.fromArray(doublePositions, i * 3)
|
||||
pB.fromArray(doublePositions, i * 3 + 1)
|
||||
pC.fromArray(doublePositions, i * 3 + 2)
|
||||
pA.fromArray(positions, i * 3)
|
||||
pB.fromArray(positions, i * 3 + 1)
|
||||
pC.fromArray(positions, i * 3 + 2)
|
||||
|
||||
cb.subVectors(pC, pB)
|
||||
ab.subVectors(pA, pB)
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { Triangle, Vector3 } from 'three'
|
||||
|
||||
/**
|
||||
* Set of functions to triangulate n-gon faces (i.e. polygon faces with an arbitrary (n) number of vertices).
|
||||
* This class is a JavaScript port of https://github.com/specklesystems/speckle-sharp/blob/main/Objects/Objects/Utils/MeshTriangulationHelper.cs
|
||||
*/
|
||||
const _vec30 = new Vector3()
|
||||
const _vec31 = new Vector3()
|
||||
const _vec32 = new Vector3()
|
||||
const _vec33 = new Vector3()
|
||||
const _normal = new Vector3()
|
||||
const _triangle = new Triangle()
|
||||
|
||||
export default class MeshTriangulationHelper {
|
||||
/**
|
||||
* Calculates the triangulation of the face at given faceIndex.
|
||||
@@ -9,30 +19,39 @@ export default class MeshTriangulationHelper {
|
||||
* @param {Number} faceIndex The index of the face's cardinality indicator `n`
|
||||
* @param {Number[]} faces The list of faces in the mesh
|
||||
* @param {Number[]} vertices The list of vertices in the mesh
|
||||
* @return {Number[]} flat list of triangle faces (without cardinality indicators)
|
||||
* @return {Number} flat list of triangle faces (without cardinality indicators)
|
||||
*/
|
||||
static triangulateFace(faceIndex, faces, vertices) {
|
||||
let n = faces[faceIndex]
|
||||
static triangulateFace(
|
||||
faceIndex,
|
||||
faces,
|
||||
vertices,
|
||||
/** Purists rolling over in their graves because of this */
|
||||
_inout_targetArray,
|
||||
_in_offset
|
||||
) {
|
||||
let n = faces.get(faceIndex)
|
||||
if (n < 3) n += 3 // 0 -> 3, 1 -> 4
|
||||
|
||||
//Converts from relative to absolute index (returns index in mesh.vertices list)
|
||||
/** Why doesn't javascript have a means to inline functions?! */
|
||||
function asIndex(v) {
|
||||
return faceIndex + v + 1
|
||||
}
|
||||
|
||||
//Gets vertex from a relative vert index
|
||||
function V(v) {
|
||||
const index = faces[asIndex(v)] * 3
|
||||
return new Vector3(vertices[index], vertices[index + 1], vertices[index + 2])
|
||||
function V(v, target) {
|
||||
const index = faces.get(asIndex(v)) * 3
|
||||
target.x = vertices.get(index)
|
||||
target.y = vertices.get(index + 1)
|
||||
target.z = vertices.get(index + 2)
|
||||
return target
|
||||
}
|
||||
|
||||
const triangleFaces = Array((n - 2) * 3)
|
||||
|
||||
//Calculate face normal using the Newell Method
|
||||
const faceNormal = new Vector3(0, 0, 0)
|
||||
const faceNormal = _normal
|
||||
for (let ii = n - 1, jj = 0; jj < n; ii = jj, jj++) {
|
||||
const iPos = V(ii)
|
||||
const jPos = V(jj)
|
||||
const iPos = V(ii, _vec30)
|
||||
const jPos = V(jj, _vec31)
|
||||
faceNormal.x += (jPos.y - iPos.y) * (iPos.z + jPos.z) // projection on yz
|
||||
faceNormal.y += (jPos.z - iPos.z) * (iPos.x + jPos.x) // projection on xz
|
||||
faceNormal.z += (jPos.x - iPos.x) * (iPos.y + jPos.y) // projection on xy
|
||||
@@ -40,8 +59,8 @@ export default class MeshTriangulationHelper {
|
||||
faceNormal.normalize()
|
||||
|
||||
//Set up previous and next links to effectively form a double-linked vertex list
|
||||
const prev = Array(n)
|
||||
const next = Array(n)
|
||||
const prev = [] //new Array(n)
|
||||
const next = [] //new Array(n)
|
||||
for (let j = 0; j < n; j++) {
|
||||
prev[j] = j - 1
|
||||
next[j] = j + 1
|
||||
@@ -52,20 +71,25 @@ export default class MeshTriangulationHelper {
|
||||
//Start clipping ears until we are left with a triangle
|
||||
let i = 0
|
||||
let counter = 0
|
||||
let localOffset = 0
|
||||
while (n >= 3) {
|
||||
let isEar = true
|
||||
|
||||
//If we are the last triangle or we have exhausted our vertices, the below statement will be false
|
||||
if (n > 3 && counter < n) {
|
||||
const prevVertex = V(prev[i])
|
||||
const earVertex = V(i)
|
||||
const nextVertex = V(next[i])
|
||||
const prevVertex = V(prev[i], _vec30)
|
||||
const earVertex = V(i, _vec31)
|
||||
const nextVertex = V(next[i], _vec32)
|
||||
|
||||
if (this.triangleIsCCW(faceNormal, prevVertex, earVertex, nextVertex)) {
|
||||
_triangle.a.copy(prevVertex)
|
||||
_triangle.b.copy(earVertex)
|
||||
_triangle.c.copy(nextVertex)
|
||||
|
||||
if (_triangle.isFrontFacing(faceNormal)) {
|
||||
let k = next[next[i]]
|
||||
|
||||
do {
|
||||
if (this.testPointTriangle(V(k), prevVertex, earVertex, nextVertex)) {
|
||||
if (_triangle.containsPoint(V(k, _vec33))) {
|
||||
isEar = false
|
||||
break
|
||||
}
|
||||
@@ -78,10 +102,13 @@ export default class MeshTriangulationHelper {
|
||||
}
|
||||
|
||||
if (isEar) {
|
||||
const a = faces[asIndex(i)]
|
||||
const b = faces[asIndex(next[i])]
|
||||
const c = faces[asIndex(prev[i])]
|
||||
triangleFaces.push(a, b, c)
|
||||
const a = faces.get(asIndex(i))
|
||||
const b = faces.get(asIndex(next[i]))
|
||||
const c = faces.get(asIndex(prev[i]))
|
||||
_inout_targetArray[_in_offset + localOffset] = a
|
||||
_inout_targetArray[_in_offset + localOffset + 1] = b
|
||||
_inout_targetArray[_in_offset + localOffset + 2] = c
|
||||
localOffset += 3
|
||||
|
||||
next[prev[i]] = next[i]
|
||||
prev[next[i]] = prev[i]
|
||||
@@ -94,89 +121,6 @@ export default class MeshTriangulationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
return triangleFaces
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if point v is within the triangle *abc*.
|
||||
* @param {Vector3} v
|
||||
* @param {Vector3} a
|
||||
* @param {Vector3} b
|
||||
* @param {Vector3} c
|
||||
* @returns {boolean} true if v is within triangle.
|
||||
*/
|
||||
static testPointTriangle(v, a, b, c) {
|
||||
function Test(_v, _a, _b) {
|
||||
const crossA = _v.cross(_a)
|
||||
const crossB = _v.cross(_b)
|
||||
const dotWithEpsilon = Number.EPSILON + crossA.dot(crossB)
|
||||
return Math.sign(dotWithEpsilon) !== -1
|
||||
}
|
||||
|
||||
return (
|
||||
Test(b.sub(a), v.sub(a), c.sub(a)) &&
|
||||
Test(c.sub(b), v.sub(b), a.sub(b)) &&
|
||||
Test(a.sub(c), v.sub(c), b.sub(c))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that triangle abc is clockwise with reference to referenceNormal.
|
||||
* @param {Vector3} referenceNormal The normal direction of the face.
|
||||
* @param {Vector3} a
|
||||
* @param {Vector3} b
|
||||
* @param {Vector3} c
|
||||
* @returns {boolean} true if triangle is ccw
|
||||
*/
|
||||
static triangleIsCCW(referenceNormal, a, b, c) {
|
||||
const triangleNormal = c.sub(a).cross(b.sub(a))
|
||||
triangleNormal.normalize()
|
||||
return referenceNormal.dot(triangleNormal) > 0.0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates vector maths operations required for polygon triangulation
|
||||
*/
|
||||
class Vector3 {
|
||||
constructor(x, y, z) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.z = z
|
||||
}
|
||||
|
||||
add(v) {
|
||||
return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z)
|
||||
}
|
||||
|
||||
sub(v) {
|
||||
return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z)
|
||||
}
|
||||
|
||||
mul(n) {
|
||||
return new Vector3(this.x - n, this.y - n, this.z - n)
|
||||
}
|
||||
|
||||
dot(v) {
|
||||
return this.x * v.x + this.y * v.y + this.z * v.z
|
||||
}
|
||||
|
||||
cross(v) {
|
||||
const nx = this.y * v.z - this.z * v.y
|
||||
const ny = this.z * v.x - this.x * v.z
|
||||
const nz = this.x * v.y - this.y * v.x
|
||||
|
||||
return new Vector3(nx, ny, nz)
|
||||
}
|
||||
|
||||
squareSum() {
|
||||
return this.x * this.x + this.y * this.y + this.z * this.z
|
||||
}
|
||||
|
||||
normalize() {
|
||||
const scale = 1.0 / Math.sqrt(this.squareSum())
|
||||
this.x *= scale
|
||||
this.y *= scale
|
||||
this.z *= scale
|
||||
return localOffset
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { TypedArray } from 'type-fest'
|
||||
import { DataChunk } from '../../IViewer.js'
|
||||
import { Box3, MathUtils, Vector3 } from 'three'
|
||||
|
||||
export class VirtualArray {
|
||||
private offsets: number[]
|
||||
|
||||
constructor(public chunks: Array<Array<number>>) {
|
||||
this.updateOffsets()
|
||||
}
|
||||
|
||||
get length() {
|
||||
if (this.chunks.length === 0) return 0
|
||||
const lastChunk = this.chunks[this.chunks.length - 1]
|
||||
return this.offsets[this.offsets.length - 1] + lastChunk.length
|
||||
}
|
||||
|
||||
get(index: number): number {
|
||||
if (this.chunks.length === 1) return this.chunks[0][index]
|
||||
const chunkIndex = this.findChunkIndex(index)
|
||||
const localIndex = index - this.offsets[chunkIndex]
|
||||
return this.chunks[chunkIndex][localIndex]
|
||||
}
|
||||
|
||||
set(index: number, value: number) {
|
||||
if (this.chunks.length === 1) {
|
||||
this.chunks[0][index] = value
|
||||
return
|
||||
}
|
||||
const chunkIndex = this.findChunkIndex(index)
|
||||
const localIndex = index - this.offsets[chunkIndex]
|
||||
this.chunks[chunkIndex][localIndex] = value
|
||||
}
|
||||
|
||||
public findChunkIndex(index: number): number {
|
||||
let low = 0
|
||||
let high = this.offsets.length - 1
|
||||
|
||||
while (low <= high) {
|
||||
const mid = (low + high) >> 1
|
||||
const start = this.offsets[mid]
|
||||
const end = mid + 1 < this.offsets.length ? this.offsets[mid + 1] : this.length
|
||||
if (index >= start && index < end) return mid
|
||||
if (index < start) high = mid - 1
|
||||
else low = mid + 1
|
||||
}
|
||||
|
||||
throw new RangeError('Index out of bounds')
|
||||
}
|
||||
|
||||
public updateOffsets() {
|
||||
this.offsets = []
|
||||
let sum = 0
|
||||
for (const chunk of this.chunks) {
|
||||
this.offsets.push(sum)
|
||||
sum += chunk.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ChunkArray extends VirtualArray {
|
||||
public chunkArray: Array<DataChunk>
|
||||
protected flatArray: TypedArray
|
||||
|
||||
constructor(chunks: Array<DataChunk>) {
|
||||
super(chunks && chunks.map((c: DataChunk) => c.data))
|
||||
this.chunkArray = chunks
|
||||
}
|
||||
|
||||
public slice() {
|
||||
const copiesArray: Array<DataChunk> = []
|
||||
this.chunkArray.forEach((chunk: DataChunk) => {
|
||||
const chunkCopy = new Array<number>(chunk.data.length)
|
||||
for (let k = 0; k < chunk.data.length; k++) {
|
||||
chunkCopy[k] = chunk.data[k]
|
||||
}
|
||||
copiesArray.push({ data: chunkCopy, id: MathUtils.generateUUID(), references: 1 })
|
||||
})
|
||||
return new ChunkArray(copiesArray)
|
||||
}
|
||||
|
||||
public copyToBuffer(buffer: TypedArray, offset: number) {
|
||||
let chunkOffset = 0
|
||||
|
||||
this.chunkArray.forEach((chunk: DataChunk) => {
|
||||
buffer.set(
|
||||
chunk.data as unknown as ArrayLike<number> & ArrayLike<bigint>,
|
||||
offset + chunkOffset
|
||||
)
|
||||
chunkOffset += chunk.data.length
|
||||
})
|
||||
}
|
||||
|
||||
public computeBox3(): Box3 {
|
||||
const box = new Box3()
|
||||
const vec3 = new Vector3()
|
||||
let carry: number[] = [] // to hold x/y if vec3 is split
|
||||
|
||||
for (let c = 0; c < this.chunks.length; c++) {
|
||||
const chunk = this.chunks[c]
|
||||
let i = 0
|
||||
|
||||
// Handle carry-over from previous chunk
|
||||
if (carry.length > 0) {
|
||||
while (carry.length < 3 && i < chunk.length) {
|
||||
carry.push(chunk[i++])
|
||||
}
|
||||
if (carry.length === 3) {
|
||||
vec3.set(carry[0], carry[1], carry[2])
|
||||
box.expandByPoint(vec3)
|
||||
carry = []
|
||||
}
|
||||
}
|
||||
|
||||
// Now read as many full vec3s as possible from this chunk
|
||||
const fullVec3Count = Math.floor((chunk.length - i) / 3)
|
||||
for (let j = 0; j < fullVec3Count; j++) {
|
||||
const x = chunk[i++]
|
||||
const y = chunk[i++]
|
||||
const z = chunk[i++]
|
||||
vec3.set(x, y, z)
|
||||
box.expandByPoint(vec3)
|
||||
}
|
||||
|
||||
// If there's a leftover partial vec3 at the end, save it
|
||||
while (i < chunk.length) {
|
||||
carry.push(chunk[i++])
|
||||
}
|
||||
}
|
||||
|
||||
// Final sanity check
|
||||
if (carry.length !== 0) {
|
||||
console.warn('Virtual position buffer ended with incomplete vec3 data')
|
||||
}
|
||||
|
||||
return box
|
||||
}
|
||||
|
||||
protected getFlatArray<T extends TypedArray>(Type: { new (length: number): T }) {
|
||||
if (!this.flatArray || !(this.flatArray instanceof Type)) {
|
||||
this.flatArray = new Type(this.length)
|
||||
let chunkOffset = 0
|
||||
this.chunks.forEach((chunk: number[]) => {
|
||||
this.flatArray.set(
|
||||
chunk as unknown as ArrayLike<number> & ArrayLike<bigint>,
|
||||
chunkOffset
|
||||
)
|
||||
chunkOffset += chunk.length
|
||||
})
|
||||
}
|
||||
return this.flatArray as T
|
||||
}
|
||||
|
||||
public getFloat32Array(): Float32Array {
|
||||
return this.getFlatArray(Float32Array)
|
||||
}
|
||||
|
||||
public getFloat64Array(): Float64Array {
|
||||
return this.getFlatArray(Float64Array)
|
||||
}
|
||||
|
||||
public getInt16Array(): Int16Array {
|
||||
return this.getFlatArray(Int16Array)
|
||||
}
|
||||
|
||||
public getInt32Array(): Int32Array {
|
||||
return this.getFlatArray(Int32Array)
|
||||
}
|
||||
|
||||
public getUint16Array(): Uint16Array {
|
||||
return this.getFlatArray(Uint16Array)
|
||||
}
|
||||
|
||||
public getUint32Array(): Uint32Array {
|
||||
return this.getFlatArray(Uint32Array)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Quaternion,
|
||||
Raycaster,
|
||||
ReplaceStencilOp,
|
||||
Uint16BufferAttribute,
|
||||
Vector2,
|
||||
Vector3,
|
||||
type Intersection
|
||||
@@ -370,7 +371,7 @@ export class AreaMeasurement extends Measurement {
|
||||
}
|
||||
|
||||
if (!index || index.count !== indices.length) {
|
||||
geometry.setIndex(new BufferAttribute(new Uint16Array(indices), 1))
|
||||
geometry.setIndex(new Uint16BufferAttribute(indices, 1))
|
||||
} else {
|
||||
;(index.array as Uint16Array).set(indices, 0)
|
||||
index.needsUpdate = true
|
||||
|
||||
@@ -303,6 +303,7 @@ export class SectionOutlines extends Extension {
|
||||
private createPlaneOutline(planeId: string): PlaneOutline {
|
||||
const buffer = new Float64Array(SectionOutlines.INITIAL_BUFFER_SIZE)
|
||||
const lineGeometry = new LineSegmentsGeometry()
|
||||
/** We need to re-allocate, otherwise three.js will do it anyway */
|
||||
lineGeometry.setPositions(new Float32Array(buffer))
|
||||
;(
|
||||
lineGeometry.attributes['instanceStart'] as InterleavedBufferAttribute
|
||||
@@ -390,7 +391,7 @@ export class SectionOutlines extends Extension {
|
||||
|
||||
const buffer = new Float32Array(size)
|
||||
outline.renderable.geometry = new LineSegmentsGeometry()
|
||||
outline.renderable.geometry.setPositions(new Float32Array(buffer))
|
||||
outline.renderable.geometry.setPositions(buffer)
|
||||
;(
|
||||
outline.renderable.geometry.attributes[
|
||||
'instanceStart'
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
Color,
|
||||
MeshBasicMaterial,
|
||||
PlaneGeometry,
|
||||
Float32BufferAttribute,
|
||||
Uint16BufferAttribute,
|
||||
Euler
|
||||
} from 'three'
|
||||
import { intersectObjectWithRay, TransformControls } from '../TransformControls.js'
|
||||
@@ -61,7 +63,7 @@ const _vector3 = new Vector3()
|
||||
const _tempEuler = new Euler()
|
||||
const _tempQuaternion = new Quaternion()
|
||||
|
||||
const unitCube = [
|
||||
const unitCube = new Float32Array([
|
||||
-1 * 0.5,
|
||||
-1 * 0.5,
|
||||
-1 * 0.5,
|
||||
@@ -93,12 +95,12 @@ const unitCube = [
|
||||
-1 * 0.5,
|
||||
1 * 0.5,
|
||||
1 * 0.5
|
||||
]
|
||||
])
|
||||
|
||||
const unitCubeIndices: number[] = [
|
||||
const unitCubeIndices: Uint16Array = new Uint16Array([
|
||||
0, 1, 3, 3, 1, 2, 1, 5, 2, 2, 5, 6, 5, 4, 6, 6, 4, 7, 4, 0, 7, 7, 0, 3, 3, 2, 7, 7, 2,
|
||||
6, 4, 5, 0, 0, 5, 1
|
||||
]
|
||||
])
|
||||
|
||||
const unitCubeEdges: number[] = [
|
||||
// Bottom Face
|
||||
@@ -932,11 +934,11 @@ export class SectionTool extends Extension {
|
||||
/** Creates the geometry for the visible outline of the section tool */
|
||||
protected createOutline() {
|
||||
/** We start from the unit cube's edges */
|
||||
const buffer = new Float32Array(unitCubeEdges.slice())
|
||||
const buffer = unitCubeEdges.slice() as unknown as Float32Array
|
||||
|
||||
/** Create the line segments geometry */
|
||||
const lineGeometry = new LineSegmentsGeometry()
|
||||
lineGeometry.setPositions(new Float32Array(buffer))
|
||||
lineGeometry.setPositions(buffer)
|
||||
;(
|
||||
lineGeometry.attributes['instanceStart'] as InterleavedBufferAttribute
|
||||
).data.setUsage(DynamicDrawUsage)
|
||||
@@ -1015,8 +1017,8 @@ export class SectionTool extends Extension {
|
||||
const indexes = unitCubeIndices.slice()
|
||||
|
||||
const g = new BufferGeometry()
|
||||
g.setAttribute('position', new BufferAttribute(new Float32Array(vertices), 3))
|
||||
g.setIndex(indexes)
|
||||
g.setAttribute('position', new Float32BufferAttribute(vertices, 3))
|
||||
g.setIndex(new Uint16BufferAttribute(indexes, 1))
|
||||
g.computeBoundingBox()
|
||||
g.computeVertexNormals()
|
||||
return g
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SpeckleType, type SpeckleObject } from '../../../index.js'
|
||||
import Logger from '../../utils/Logger.js'
|
||||
import { ObjectLoader2 } from '@speckle/objectloader2'
|
||||
import { SpeckleTypeAllRenderables } from '../GeometryConverter.js'
|
||||
import { DataChunk } from '../../../IViewer.js'
|
||||
|
||||
export type ConverterResultDelegate = (count: number) => void
|
||||
export type SpeckleConverterNodeDelegate =
|
||||
@@ -64,7 +65,7 @@ export default class SpeckleConverter {
|
||||
Parameter: null
|
||||
}
|
||||
|
||||
protected readonly IgnoreNodes = ['Parameter']
|
||||
protected readonly IgnoreNodes = ['Parameter', 'RawEncoding']
|
||||
|
||||
constructor(objectLoader: ObjectLoader2, tree: WorldTree) {
|
||||
if (!objectLoader) {
|
||||
@@ -287,24 +288,49 @@ export default class SpeckleConverter {
|
||||
* @param {[type]} arr [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
private async dechunk(arr: Array<{ referencedId: string }>) {
|
||||
if (!arr || arr.length === 0) return arr
|
||||
// Handles pre-chunking objects, or arrs that have not been chunked
|
||||
if (!arr[0].referencedId) return arr
|
||||
private async dechunk(arr: Array<{ referencedId: string }>): Promise<DataChunk[]> {
|
||||
if (!arr || arr.length === 0) {
|
||||
return arr as unknown as DataChunk[]
|
||||
}
|
||||
|
||||
const chunked: unknown[] = []
|
||||
if (Array.isArray(arr[0]) && !arr[0].referencedId) {
|
||||
return arr as unknown as DataChunk[]
|
||||
}
|
||||
// Handles pre-chunking objects, or arrs that have not been chunked
|
||||
if (!arr[0].referencedId) {
|
||||
if (!(arr[0] instanceof Object))
|
||||
return [
|
||||
{
|
||||
data: arr,
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
] as unknown as DataChunk[]
|
||||
else return arr as unknown as DataChunk[]
|
||||
}
|
||||
|
||||
const chunked: DataChunk[] = []
|
||||
for (const ref of arr) {
|
||||
const real: Record<string, unknown> = (await this.objectLoader.getObject({
|
||||
const real: DataChunk = (await this.objectLoader.getObject({
|
||||
id: ref.referencedId
|
||||
})) as unknown as Record<string, number>
|
||||
chunked.push(real.data)
|
||||
})) as unknown as DataChunk
|
||||
if (real.references === undefined) {
|
||||
real.references = 1
|
||||
} else {
|
||||
real.references++
|
||||
}
|
||||
if (typeof real.data[0] !== 'number' || isNaN(real.data[0])) {
|
||||
Logger.error(
|
||||
`Chunk id ${real.id} used for mesh ${ref.referencedId} might not have numeric geometry data. This is not supported!`
|
||||
)
|
||||
}
|
||||
chunked.push(real)
|
||||
// await this.asyncPause()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const dechunked = [].concat(...(chunked as any))
|
||||
// const dechunked = [].concat(...(chunked as any))
|
||||
|
||||
return dechunked
|
||||
return chunked
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -916,14 +942,23 @@ export default class SpeckleConverter {
|
||||
Logger.warn(
|
||||
`Object id ${obj.id} of type ${obj.speckle_type} has no vertex position data and will be ignored`
|
||||
)
|
||||
node.model.raw.vertices = []
|
||||
node.model.raw.faces = []
|
||||
node.model.raw.colors = []
|
||||
node.model.raw.vertexNormals = []
|
||||
return
|
||||
}
|
||||
if (!obj.faces || (obj.faces as Array<number>).length === 0) {
|
||||
Logger.warn(
|
||||
`Object id ${obj.id} of type ${obj.speckle_type} has no face data and will be ignored`
|
||||
)
|
||||
node.model.raw.vertices = []
|
||||
node.model.raw.faces = []
|
||||
node.model.raw.colors = []
|
||||
node.model.raw.vertexNormals = []
|
||||
return
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
node.model.raw.vertices = await this.dechunk(obj.vertices)
|
||||
|
||||
@@ -2,9 +2,11 @@ import { Geometry, type GeometryData } from '../../converter/Geometry.js'
|
||||
import MeshTriangulationHelper from '../../converter/MeshTriangulationHelper.js'
|
||||
import { getConversionFactor } from '../../converter/Units.js'
|
||||
import { type NodeData } from '../../tree/WorldTree.js'
|
||||
import { Box3, EllipseCurve, Matrix4, Vector2, Vector3 } from 'three'
|
||||
import { Box3, EllipseCurve, MathUtils, Matrix4, Vector2, Vector3 } from 'three'
|
||||
import { GeometryConverter, SpeckleType } from '../GeometryConverter.js'
|
||||
import Logger from '../../utils/Logger.js'
|
||||
import { DataChunk } from '../../../IViewer.js'
|
||||
import { ChunkArray } from '../../converter/VirtualArray.js'
|
||||
|
||||
export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
public typeLookupTable: { [type: string]: SpeckleType } = {}
|
||||
@@ -93,9 +95,35 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
node.raw.colors = []
|
||||
break
|
||||
case SpeckleType.Mesh:
|
||||
/** Raw objects will no longer hold references to chunks */
|
||||
node.raw.vertices = []
|
||||
node.raw.faces = []
|
||||
node.raw.colors = []
|
||||
node.raw.normals = []
|
||||
|
||||
// /** We can already delete these because we don't need them after triangulation */
|
||||
// node.raw.faces.forEach((c: DataChunk) => {
|
||||
// c.references--
|
||||
|
||||
// if (!c.references) {
|
||||
// Logger.warn(`Deleting chunk data ${c.id}`)
|
||||
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// //@ts-ignore
|
||||
// delete c.data
|
||||
// }
|
||||
// })
|
||||
|
||||
/** We can already delete this because we've changes the colors to floats in linear space */
|
||||
node.raw.colors.forEach((c: DataChunk) => {
|
||||
c.references--
|
||||
|
||||
if (!c.references) {
|
||||
Logger.warn(`Deleting chunk data ${c.id}`)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
delete c.data
|
||||
}
|
||||
})
|
||||
break
|
||||
case SpeckleType.Point:
|
||||
if (node.raw.value) node.raw.value = []
|
||||
@@ -197,8 +225,10 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
protected PointcloudToGeometryData(node: NodeData): GeometryData | null {
|
||||
const conversionFactor = getConversionFactor(node.raw.units)
|
||||
|
||||
const vertices = node.instanced ? node.raw.points.slice() : node.raw.points
|
||||
const colorsRaw = node.raw.colors
|
||||
const vertices = new ChunkArray(
|
||||
node.instanced ? node.raw.points.slice() : node.raw.points
|
||||
)
|
||||
const colorsRaw = new ChunkArray(node.raw.colors)
|
||||
let colors = null
|
||||
|
||||
if (colorsRaw && colorsRaw.length !== 0) {
|
||||
@@ -215,6 +245,10 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
attributes: {
|
||||
POSITION: vertices,
|
||||
COLOR: colors
|
||||
? new ChunkArray([
|
||||
{ data: colors, id: MathUtils.generateUUID(), references: 1 }
|
||||
])
|
||||
: undefined
|
||||
},
|
||||
bakeTransform: new Matrix4().makeScale(
|
||||
conversionFactor,
|
||||
@@ -254,45 +288,101 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
if (!node.raw) return null
|
||||
|
||||
const conversionFactor = getConversionFactor(node.raw.units)
|
||||
const indices = []
|
||||
|
||||
if (!node.raw.vertices) return null
|
||||
if (!node.raw.faces) return null
|
||||
|
||||
const start = performance.now()
|
||||
const vertices = node.raw.vertices
|
||||
const faces = node.raw.faces
|
||||
const colorsRaw = node.raw.colors
|
||||
|
||||
const vertices = new ChunkArray(node.raw.vertices)
|
||||
const faces = new ChunkArray(node.raw.faces)
|
||||
const colorsRaw = this.chunkArrayHasData(node.raw.colors)
|
||||
? new ChunkArray(node.raw.colors)
|
||||
: undefined
|
||||
let normals = node.raw.vertexNormals
|
||||
? new ChunkArray(node.raw.vertexNormals)
|
||||
: undefined
|
||||
let colors = undefined
|
||||
let k = 0
|
||||
let triangulated = true
|
||||
let triangulatedArraySize = 0
|
||||
while (k < faces.length) {
|
||||
let n = faces[k]
|
||||
const chunkIndex = faces.findChunkIndex(k)
|
||||
if (faces.chunkArray[chunkIndex].processed) {
|
||||
k += faces.chunkArray[chunkIndex].data.length
|
||||
continue
|
||||
}
|
||||
let n = faces.get(k)
|
||||
if (n < 3) n += 3 // 0 -> 3, 1 -> 4
|
||||
k += n + 1
|
||||
|
||||
if (n === 3) {
|
||||
const startP = performance.now()
|
||||
// Triangle face
|
||||
indices.push(faces[k + 1], faces[k + 2], faces[k + 3])
|
||||
this.pushTime += performance.now() - startP
|
||||
triangulatedArraySize += 3
|
||||
continue
|
||||
} else {
|
||||
// Quad or N-gon face
|
||||
const start1 = performance.now()
|
||||
const triangulation = MeshTriangulationHelper.triangulateFace(
|
||||
k,
|
||||
faces,
|
||||
vertices
|
||||
)
|
||||
this.actualTriangulateTime += performance.now() - start1
|
||||
indices.push(
|
||||
...triangulation.filter((el) => {
|
||||
return el !== undefined
|
||||
})
|
||||
)
|
||||
triangulatedArraySize += (n - 2) * 3
|
||||
triangulated = false
|
||||
}
|
||||
|
||||
k += n + 1
|
||||
}
|
||||
|
||||
const indices =
|
||||
triangulatedArraySize >= 65535 || vertices.length >= 65535
|
||||
? new Uint32Array(triangulatedArraySize)
|
||||
: new Uint16Array(triangulatedArraySize)
|
||||
let indicesOffset = 0
|
||||
|
||||
if (triangulated) {
|
||||
/** If already triangulated modfy the faces array in place */
|
||||
faces.chunkArray.forEach((chunk: DataChunk) => {
|
||||
if (chunk.processed) return
|
||||
|
||||
let write = 0
|
||||
for (let read = 0; read < chunk.data.length; read++) {
|
||||
if (read % 4 !== 0) {
|
||||
chunk.data[write++] = chunk.data[read]
|
||||
}
|
||||
}
|
||||
chunk.data.length = write
|
||||
chunk.processed = true
|
||||
})
|
||||
faces.updateOffsets()
|
||||
} else {
|
||||
k = 0
|
||||
while (k < faces.length) {
|
||||
/** We skip to the end of triangulated chunks */
|
||||
const chunkIndex = faces.findChunkIndex(k)
|
||||
if (faces.chunkArray[chunkIndex].processed) {
|
||||
indices.set(faces.chunks[chunkIndex], k)
|
||||
k += faces.chunkArray[chunkIndex].data.length
|
||||
continue
|
||||
}
|
||||
let n = faces.get(k)
|
||||
if (n < 3) n += 3 // 0 -> 3, 1 -> 4
|
||||
if (n === 3) {
|
||||
// Triangle face
|
||||
indices[indicesOffset] = faces.get(k + 1)
|
||||
indices[indicesOffset + 1] = faces.get(k + 2)
|
||||
indices[indicesOffset + 2] = faces.get(k + 3)
|
||||
indicesOffset += 3
|
||||
} else {
|
||||
const start1 = performance.now()
|
||||
const indexCount = MeshTriangulationHelper.triangulateFace(
|
||||
k,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
faces,
|
||||
vertices,
|
||||
indices, // inout
|
||||
indicesOffset // in
|
||||
)
|
||||
indicesOffset += indexCount
|
||||
this.actualTriangulateTime += performance.now() - start1
|
||||
}
|
||||
|
||||
k += n + 1
|
||||
}
|
||||
}
|
||||
|
||||
this.meshTriangulationTime += performance.now() - start
|
||||
|
||||
if (colorsRaw && colorsRaw.length !== 0) {
|
||||
@@ -302,7 +392,13 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
)
|
||||
} else
|
||||
/** We want the colors in linear space */
|
||||
colors = this.unpackColors(colorsRaw, true)
|
||||
colors = new ChunkArray([
|
||||
{
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1,
|
||||
data: this.unpackColors(colorsRaw, true)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
if (normals && normals.length !== 0) {
|
||||
@@ -317,7 +413,15 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: vertices,
|
||||
INDEX: indices,
|
||||
INDEX: triangulated
|
||||
? faces
|
||||
: new ChunkArray([
|
||||
{
|
||||
data: indices as unknown as number[],
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
]),
|
||||
...(colors && { COLOR: colors }),
|
||||
...(normals && { NORMAL: normals })
|
||||
},
|
||||
@@ -368,15 +472,18 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
*/
|
||||
protected PointToGeometryData(node: NodeData): GeometryData | null {
|
||||
const conversionFactor = getConversionFactor(node.raw.units)
|
||||
const points = this.PointToFloatArray(
|
||||
node.raw as { value: Array<number>; units: string } & {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
}
|
||||
)
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: this.PointToFloatArray(
|
||||
node.raw as { value: Array<number>; units: string } & {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
}
|
||||
)
|
||||
POSITION: new ChunkArray([
|
||||
{ data: points, id: MathUtils.generateUUID(), references: 1 }
|
||||
])
|
||||
},
|
||||
bakeTransform: new Matrix4().makeScale(
|
||||
conversionFactor,
|
||||
@@ -394,9 +501,15 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
const conversionFactor = getConversionFactor(node.raw.units)
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: this.PointToFloatArray(node.raw.start).concat(
|
||||
this.PointToFloatArray(node.raw.end)
|
||||
)
|
||||
POSITION: new ChunkArray([
|
||||
{
|
||||
data: this.PointToFloatArray(node.raw.start).concat(
|
||||
this.PointToFloatArray(node.raw.end)
|
||||
),
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
])
|
||||
},
|
||||
bakeTransform: new Matrix4().makeScale(
|
||||
conversionFactor,
|
||||
@@ -412,12 +525,26 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
*/
|
||||
protected PolylineToGeometryData(node: NodeData): GeometryData | null {
|
||||
const conversionFactor = getConversionFactor(node.raw.units)
|
||||
const chunkArray = new ChunkArray(node.raw.value)
|
||||
|
||||
if (node.raw.closed)
|
||||
node.raw.value.push(node.raw.value[0], node.raw.value[1], node.raw.value[2])
|
||||
let outChunk = chunkArray
|
||||
if (node.raw.closed) {
|
||||
const complete = new Float32Array(chunkArray.length + 3)
|
||||
chunkArray.copyToBuffer(complete, 0)
|
||||
complete[chunkArray.length] = complete[0]
|
||||
complete[chunkArray.length + 1] = complete[1]
|
||||
complete[chunkArray.length + 2] = complete[2]
|
||||
outChunk = new ChunkArray([
|
||||
{
|
||||
data: complete as unknown as number[],
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
])
|
||||
}
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: node.raw.value.slice(0)
|
||||
POSITION: outChunk
|
||||
},
|
||||
bakeTransform: new Matrix4().makeScale(
|
||||
conversionFactor,
|
||||
@@ -504,7 +631,9 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: edges
|
||||
POSITION: new ChunkArray([
|
||||
{ data: edges, id: MathUtils.generateUUID(), references: 1 }
|
||||
])
|
||||
},
|
||||
bakeTransform: new Matrix4().copy(T).multiply(R),
|
||||
transform: null
|
||||
@@ -562,7 +691,13 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
)
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: this.FlattenVector3Array(points)
|
||||
POSITION: new ChunkArray([
|
||||
{
|
||||
data: this.FlattenVector3Array(points),
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
])
|
||||
},
|
||||
bakeTransform: null,
|
||||
transform: null
|
||||
@@ -664,7 +799,13 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: this.FlattenVector3Array(points)
|
||||
POSITION: new ChunkArray([
|
||||
{
|
||||
data: this.FlattenVector3Array(points),
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
])
|
||||
},
|
||||
bakeTransform: matrix,
|
||||
transform: null
|
||||
@@ -710,7 +851,13 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: this.FlattenVector3Array(points)
|
||||
POSITION: new ChunkArray([
|
||||
{
|
||||
data: this.FlattenVector3Array(points),
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
])
|
||||
},
|
||||
bakeTransform: null,
|
||||
transform: null
|
||||
@@ -818,10 +965,10 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
return output
|
||||
}
|
||||
|
||||
protected unpackColors(int32Colors: number[], tolinear = false): number[] {
|
||||
protected unpackColors(int32Colors: ChunkArray, tolinear = false): number[] {
|
||||
const colors = new Array<number>(int32Colors.length * 3)
|
||||
for (let i = 0; i < int32Colors.length; i++) {
|
||||
const color = int32Colors[i]
|
||||
const color = int32Colors.get(i)
|
||||
const r = (color >> 16) & 0xff
|
||||
const g = (color >> 8) & 0xff
|
||||
const b = color & 0xff
|
||||
@@ -843,4 +990,9 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
else if (x < 0.04045) return x / 12.92
|
||||
else return Math.pow((x + 0.055) / 1.055, 2.4)
|
||||
}
|
||||
|
||||
/** Connectors send empty chunks ಠ_ಠ */
|
||||
protected chunkArrayHasData(chunks: Array<DataChunk>): boolean {
|
||||
return chunks && chunks.filter((c: DataChunk) => c.data && c.data.length).length > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { SpeckleMaterial } from './SpeckleMaterial.js'
|
||||
import SpecklePointColouredMaterial from './SpecklePointColouredMaterial.js'
|
||||
import { type Asset, AssetType, type MaterialOptions } from '../../IViewer.js'
|
||||
import SpeckleTextColoredMaterial from './SpeckleTextColoredMaterial.js'
|
||||
import { ChunkArray } from '../converter/VirtualArray.js'
|
||||
|
||||
const defaultGradient: Asset = {
|
||||
id: 'defaultGradient',
|
||||
@@ -124,6 +125,10 @@ export default class Materials {
|
||||
if (!materialNode) return null
|
||||
let renderMaterial: RenderMaterial | null = null
|
||||
if (materialNode.model.raw.renderMaterial) {
|
||||
const colorsChunkArray = geometryNode?.model.raw.colors
|
||||
? new ChunkArray(geometryNode?.model.raw.colors)
|
||||
: undefined
|
||||
|
||||
renderMaterial = {
|
||||
id: materialNode.model.raw.renderMaterial.id,
|
||||
color: materialNode.model.raw.renderMaterial.diffuse,
|
||||
@@ -135,9 +140,7 @@ export default class Materials {
|
||||
roughness: materialNode.model.raw.renderMaterial.roughness,
|
||||
metalness: materialNode.model.raw.renderMaterial.metalness,
|
||||
vertexColors:
|
||||
geometryNode &&
|
||||
geometryNode.model.raw.colors &&
|
||||
geometryNode.model.raw.colors.length > 0
|
||||
(geometryNode && colorsChunkArray && colorsChunkArray.length > 0) ?? false
|
||||
}
|
||||
}
|
||||
return renderMaterial
|
||||
|
||||
@@ -82,8 +82,8 @@ export class AccelerationStructure {
|
||||
}
|
||||
|
||||
public static buildBVH(
|
||||
indices: number[] | undefined,
|
||||
position: number[] | undefined,
|
||||
indices: Uint16Array | Uint32Array | undefined,
|
||||
position: Float32Array | Float64Array | undefined,
|
||||
options: BVHOptions = DefaultBVHOptions,
|
||||
transform?: Matrix4
|
||||
): MeshBVH {
|
||||
@@ -92,7 +92,7 @@ export class AccelerationStructure {
|
||||
throw new Error('Cannot build BVH with undefined indices or position!')
|
||||
}
|
||||
|
||||
let bvhPositions = new Float32Array(position)
|
||||
let bvhPositions = position
|
||||
if (transform) {
|
||||
bvhPositions = new Float32Array(position.length)
|
||||
const vecBuff = new Vector3()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Box3,
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
Color,
|
||||
DoubleSide,
|
||||
@@ -24,6 +23,7 @@ import SpeckleBasicMaterial, {
|
||||
import SpeckleTextMaterial from '../materials/SpeckleTextMaterial.js'
|
||||
import { ObjectLayers } from '../../index.js'
|
||||
import Logger from '../utils/Logger.js'
|
||||
import { Uint16BufferAttribute } from 'three'
|
||||
|
||||
const _mat40: Matrix4 = new Matrix4()
|
||||
const _mat41: Matrix4 = new Matrix4()
|
||||
@@ -517,12 +517,9 @@ export class TextLabel extends Text {
|
||||
}
|
||||
|
||||
const geometry = new BufferGeometry()
|
||||
geometry.setIndex(new BufferAttribute(new Uint32Array(indices), 1))
|
||||
geometry.setAttribute(
|
||||
'position',
|
||||
new BufferAttribute(new Float32Array(positions), 3)
|
||||
)
|
||||
geometry.setAttribute('uv', new BufferAttribute(new Float32Array(uvs), 2))
|
||||
geometry.setIndex(new Uint16BufferAttribute(indices, 1))
|
||||
geometry.setAttribute('position', new Float32BufferAttribute(positions, 3))
|
||||
geometry.setAttribute('uv', new Float32BufferAttribute(uvs, 2))
|
||||
geometry.computeBoundingBox()
|
||||
|
||||
return geometry
|
||||
|
||||
@@ -67,24 +67,29 @@ export class TopLevelAccelerationStructure {
|
||||
}
|
||||
|
||||
private buildBVH() {
|
||||
const indices = []
|
||||
const vertices: number[] = new Array<number>(
|
||||
const indices: Uint16Array = new Uint16Array(
|
||||
TopLevelAccelerationStructure.cubeIndices.length * this.batchObjects.length
|
||||
)
|
||||
const vertices: Float32Array = new Float32Array(
|
||||
TopLevelAccelerationStructure.CUBE_VERTS * 3 * this.batchObjects.length
|
||||
)
|
||||
let vertOffset = 0
|
||||
let indexOffset = 0
|
||||
for (let k = 0; k < this.batchObjects.length; k++) {
|
||||
const boxBounds: Box3 = this.batchObjects[k].accelerationStructure.getBoundingBox(
|
||||
new Box3()
|
||||
)
|
||||
this.updateVertArray(boxBounds, vertOffset, vertices)
|
||||
indices.push(
|
||||
...TopLevelAccelerationStructure.cubeIndices.map((val) => val + vertOffset / 3)
|
||||
indices.set(
|
||||
TopLevelAccelerationStructure.cubeIndices.map((val) => val + vertOffset / 3),
|
||||
indexOffset
|
||||
)
|
||||
this.batchObjects[k].tasVertIndexStart = vertOffset / 3
|
||||
this.batchObjects[k].tasVertIndexEnd =
|
||||
vertOffset / 3 + TopLevelAccelerationStructure.CUBE_VERTS
|
||||
|
||||
vertOffset += TopLevelAccelerationStructure.CUBE_VERTS * 3
|
||||
indexOffset += TopLevelAccelerationStructure.cubeIndices.length
|
||||
}
|
||||
this.accelerationStructure = new AccelerationStructure(
|
||||
AccelerationStructure.buildBVH(indices, vertices)
|
||||
@@ -105,7 +110,7 @@ export class TopLevelAccelerationStructure {
|
||||
}
|
||||
}
|
||||
|
||||
private updateVertArray(box: Box3, offset: number, outPositions: number[]) {
|
||||
private updateVertArray(box: Box3, offset: number, outPositions: Float32Array) {
|
||||
outPositions[offset] = box.min.x
|
||||
outPositions[offset + 1] = box.min.y
|
||||
outPositions[offset + 2] = box.max.z
|
||||
@@ -141,7 +146,7 @@ export class TopLevelAccelerationStructure {
|
||||
|
||||
public refit() {
|
||||
const positions = this.accelerationStructure.geometry.attributes.position
|
||||
.array as number[]
|
||||
.array as Float32Array
|
||||
// const boxBuffer: Box3 = new Box3()
|
||||
for (let k = 0; k < this.batchObjects.length; k++) {
|
||||
const start = this.batchObjects[k].tasVertIndexStart
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box3 } from 'three'
|
||||
import { Box3, Matrix4 } from 'three'
|
||||
import { GeometryType } from '../batching/Batch.js'
|
||||
import { GeometryAttributes, type GeometryData } from '../converter/Geometry.js'
|
||||
import Materials, {
|
||||
@@ -7,6 +7,7 @@ import Materials, {
|
||||
type RenderMaterial
|
||||
} from '../materials/Materials.js'
|
||||
import { SpeckleType } from '../loaders/GeometryConverter.js'
|
||||
import { ChunkArray } from '../converter/VirtualArray.js'
|
||||
|
||||
export interface NodeRenderData {
|
||||
id: string
|
||||
@@ -84,6 +85,10 @@ export class NodeRenderView {
|
||||
return this._aabb
|
||||
}
|
||||
|
||||
public set aabb(value: Box3) {
|
||||
this._aabb.copy(value)
|
||||
}
|
||||
|
||||
public get transparent(): boolean {
|
||||
return (
|
||||
(this._renderData.renderMaterial &&
|
||||
@@ -150,14 +155,18 @@ export class NodeRenderView {
|
||||
if (vertEnd !== undefined) this._batchVertexEnd = vertEnd
|
||||
}
|
||||
|
||||
public computeAABB() {
|
||||
public computeAABB(transform?: Matrix4) {
|
||||
if (!this._aabb) this._aabb = new Box3()
|
||||
|
||||
if (
|
||||
this._renderData.geometry.attributes &&
|
||||
this._renderData.geometry.attributes.POSITION.length
|
||||
) {
|
||||
this._aabb.setFromArray(this._renderData.geometry.attributes.POSITION)
|
||||
/** For transformations that contain non-uniform scaling combine with rotation the resulting
|
||||
* aabb is not going to be accurate. We will re-compute and assign it when we build the batches
|
||||
*/
|
||||
this._aabb.copy(this._renderData.geometry.attributes.POSITION.computeBox3())
|
||||
if (transform) this._aabb.applyMatrix4(transform)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +190,9 @@ export class NodeRenderView {
|
||||
|
||||
public disposeGeometry() {
|
||||
for (const attr in this._renderData.geometry.attributes) {
|
||||
this._renderData.geometry.attributes[attr as GeometryAttributes] = []
|
||||
this._renderData.geometry.attributes[attr as GeometryAttributes] = new ChunkArray(
|
||||
[]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { type TreeNode, WorldTree } from './WorldTree.js'
|
||||
import Materials from '../materials/Materials.js'
|
||||
import { type NodeRenderData, NodeRenderView } from './NodeRenderView.js'
|
||||
import { GeometryConverter, SpeckleType } from '../loaders/GeometryConverter.js'
|
||||
import { Geometry } from '../converter/Geometry.js'
|
||||
import Logger from '../utils/Logger.js'
|
||||
|
||||
export class RenderTree {
|
||||
@@ -51,25 +50,29 @@ export class RenderTree {
|
||||
private applyTransforms(node: TreeNode) {
|
||||
if (node.model.renderView) {
|
||||
const transform = this.computeTransform(node)
|
||||
if (node.model.renderView.hasGeometry) {
|
||||
if (node.model.renderView.hasGeometry || node.model.renderView.hasMetadata) {
|
||||
if (node.model.renderView.renderData.geometry.bakeTransform) {
|
||||
transform.multiply(node.model.renderView.renderData.geometry.bakeTransform)
|
||||
}
|
||||
if (
|
||||
node.model.instanced &&
|
||||
node.model.renderView.speckleType === SpeckleType.Mesh
|
||||
)
|
||||
node.model.renderView.renderData.geometry.transform = transform
|
||||
else {
|
||||
Geometry.transformGeometryData(
|
||||
node.model.renderView.renderData.geometry,
|
||||
transform
|
||||
)
|
||||
}
|
||||
node.model.renderView.computeAABB()
|
||||
} else if (node.model.renderView.hasMetadata) {
|
||||
node.model.renderView.renderData.geometry.bakeTransform.premultiply(transform)
|
||||
node.model.renderView.computeAABB()
|
||||
node.model.renderView.renderData.geometry.transform = transform
|
||||
node.model.renderView.computeAABB(!node.model.instanced ? transform : undefined)
|
||||
/** I like that this is gone now! */
|
||||
// if (
|
||||
// node.model.instanced &&
|
||||
// node.model.renderView.speckleType === SpeckleType.Mesh
|
||||
// )
|
||||
// node.model.renderView.renderData.geometry.transform = transform
|
||||
// else {
|
||||
// Geometry.transformGeometryData(
|
||||
// node.model.renderView.renderData.geometry,
|
||||
// transform
|
||||
// )
|
||||
// }
|
||||
// node.model.renderView.computeAABB()
|
||||
// } else if (node.model.renderView.hasMetadata) {
|
||||
// node.model.renderView.renderData.geometry.bakeTransform.premultiply(transform)
|
||||
// node.model.renderView.computeAABB()
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user