Alex/text updates (#5012)
* feat(viewer-lib): Text updates: - Update to latest troika-three-text version - Started working on the new TextBatch whoich will actually batch texts - Augmented BatchedText type from troika - Renamed old SpeckleText to TextLabel * feat)viewer-lib): Copied over the batched version for the speckle text into our text material since troika won't export it * feat(viewer-lib): First draft on text batching * feat(viewer-lib): WIP on TextBatch and SpeckleText * feat(viewer-lib): SpeckleText batch now has working TAS and BAS. Overloaded getBatches to also take an array of geometry types and added GeometryType.TEXT in places where required alongisde MESH * feat(viewer-lib): Text batch has correctly transformed texts * feat(viewer-lib): Patched troika BatchedText to allow per text opacity. Draw ranges for text batches are now functional * feat(viewer-lib): Fixed an issue with the BAS not correctly reporting bounds. Had to override two methods completely in our SpeckleText extension of the BatchedText just so that we don't do stupid things and still get good performance when dealing with a huge number of texts * feat(viewer-lib): Added text batch object count limit. Default is 5k. Implemented proper material caching and cloning inside SpeckleText. Overriden the default updateBounds function so that we don't waste tens of millisecons per frame!!! pointlessly * feat(viewer-lib): Implemented TextBatchObject along with individual text batch object transform manipulation at batch level. * chore(viewer-lib): Updated the pipelines to not render text geometries twice * feat(viewer-lib): Implemented RTE for batched text rendering. As with the rest of the geometry types, RTE is automaic and will only be used when needed * feat(viewer-lib): Integrated remaining text v3 features: alignments and maxWidth * feat(viwer-lib): Implemented billboarding and RTE billboarding for text. * feat(viewer-lib): Text batches now report correct object materials and can be filtered properly * fix(viewer-lib): Some Fixes: - The need for text RTE is now correctly being computed on the right text dimensions - Sequential update ranges now correctly apply materials to all of them * fix(viewer-lib): RTE text box is now correctly transformed. The text batch object only uses the TAS for intersecting since it's BAS is redundant. * feat(viewer-lib): Text batches now correctly use gradient/ramp textures along with sample indices for colored filtering. * feat(viewer-lib): Implemented raycasting for billboarded text batches in the most simple and robust way I was capable of. Lacks TAS speedup but it's a compromise we have to make and one which we probably will never regret * feat(viewer-lib): Good progress on reworking TextLabel, which replaces the old multi purpose SpeckleText, which we use internally for measurements. More precise rendering, no more rogue margins between text and background. Regular billboarding now also works, along with non-billboarded rendering * feat(viewer-lib): Finally a unified billboarding solution in SpeckleBasicMaterial. Supporting both world and screen billboarding; SpeckleTextMaterial now extends SpeckleBasicMaterial; TextLabel now has proper control over size and margins. No more weird offsets. Added background margins to the text params which work in both world and screen space. * feat(viewer-lib): Implemented raycasting for all billboarding types. Spent quite some time on the screen billboarding one because of a stupid mistake * chore(viewer-lib): Added (vibed) type declaration file for troika's Text class and fixed compiler errors for TextLabel * chore(viewer-lib): Renamed SpeckleText to SpeckleBatchedText and fixed all compiler errors. Updated type definition file * feat(viewer-lib): Integrated TextLabel with measurements. Simplified a lot of code * fix(viewer-lib): Some updates and fixes to text and measurements integration - Screen space billboarding now also takes an NDC offset alongside the size. - Added auto margin calculation for TextLabel background so it's always centered regardless of anchor-ing - DPR is automatically factored in for TextLabel - Some changes to sizes and margins for measurements * fix(viewer-lib): Bunch of fixes and tweaks * fix(viewer-lib): Area measurement's area plane no longer overdraws on top of the area value text label via simple stenciling * fix(viewer-lib): Fixed CI build * fix(viewer-lib): Fixed CI build * feat(viewer-lib): Slightly reduces the size and h margin of text gizmos for measurements * fix(viewer-lib): Fixed incorrect text transformation when neither RTE nor billboarded * chore(viewer-lib): Added review suggestions
This commit is contained in:
committed by
GitHub
parent
ed875f0134
commit
f3974dd9d0
@@ -127,7 +127,10 @@ export class BoxSelection extends Extension {
|
||||
/** Get the renderer */
|
||||
const renderer = this.viewer.getRenderer()
|
||||
/** Get the mesh batches */
|
||||
const batches = renderer.batcher.getBatches(undefined, GeometryType.MESH)
|
||||
const batches = renderer.batcher.getBatches(undefined, [
|
||||
GeometryType.MESH,
|
||||
GeometryType.TEXT
|
||||
])
|
||||
/** Compute the clip matrix */
|
||||
const clipMatrix = new Matrix4()
|
||||
if (renderer.renderingCamera) {
|
||||
|
||||
@@ -51,7 +51,7 @@ export class CameraPlanes extends Extension {
|
||||
|
||||
const batches = this.viewer
|
||||
.getRenderer()
|
||||
.batcher.getBatches(undefined, GeometryType.MESH)
|
||||
.batcher.getBatches(undefined, [GeometryType.MESH, GeometryType.TEXT])
|
||||
let minDist = Number.POSITIVE_INFINITY
|
||||
const minPoint = new Vector3()
|
||||
for (let b = 0; b < batches.length; b++) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import {
|
||||
@@ -53,7 +51,7 @@ import Mild2 from '../assets/hdri/Mild2.png'
|
||||
import Sharp from '../assets/hdri/Sharp.png'
|
||||
import Bright from '../assets/hdri/Bright.png'
|
||||
|
||||
import { Euler, Vector3, Box3, Color, LinearFilter } from 'three'
|
||||
import { Euler, Vector3, Box3, LinearFilter } from 'three'
|
||||
import { GeometryType } from '@speckle/viewer'
|
||||
import { MeshBatch } from '@speckle/viewer'
|
||||
import { ObjectLoader2Factory } from '@speckle/objectloader2'
|
||||
@@ -636,38 +634,6 @@ export default class Sandbox {
|
||||
})
|
||||
this.tabs.pages[0].addSeparator()
|
||||
|
||||
const colors = this.tabs.pages[0].addButton({
|
||||
title: `PM's Colors`
|
||||
})
|
||||
colors.on('click', async () => {
|
||||
const colorNodes = this.viewer.getWorldTree().findAll(
|
||||
(node: TreeNode) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
node.model.renderView &&
|
||||
node.model.renderView.renderData.colorMaterial &&
|
||||
node.model.renderView.geometryType === GeometryType.MESH
|
||||
)
|
||||
const colorMap: { [color: number]: Array<string> } = {}
|
||||
for (let k = 0; k < colorNodes.length; k++) {
|
||||
const node = colorNodes[k]
|
||||
|
||||
const color: number = node.model.renderView.renderData.colorMaterial.color
|
||||
if (!colorMap[color]) colorMap[color] = []
|
||||
|
||||
colorMap[color].push(node.model.id)
|
||||
}
|
||||
const colorGroups = []
|
||||
|
||||
for (const color in colorMap) {
|
||||
colorGroups.push({
|
||||
objectIds: colorMap[color],
|
||||
color: '#' + new Color(Number.parseInt(color)).getHexString()
|
||||
})
|
||||
}
|
||||
console.log(colorGroups)
|
||||
this.viewer.getExtension(FilteringExtension).setUserObjectColors(colorGroups)
|
||||
})
|
||||
|
||||
this.tabs.pages[0]
|
||||
.addInput({ dampening: 30 }, 'dampening', {
|
||||
label: 'Dampening',
|
||||
|
||||
@@ -84,6 +84,29 @@ const createViewer = async (containerName: string, _stream: string) => {
|
||||
}
|
||||
})
|
||||
|
||||
// const label = new TextLabel({
|
||||
// text: 'y: 1.00m',
|
||||
// textColor: new Color(0xffffff),
|
||||
// fontSize: 1,
|
||||
// billboard: 'world',
|
||||
// anchorX: 'left',
|
||||
// anchorY: 'middle',
|
||||
// backgroundColor: new Color(0xfb0404),
|
||||
// backgroundMargins: new Vector2(0.75, 0.1),
|
||||
// backgroundCornerRadius: 0.5,
|
||||
// objectLayer: ObjectLayers.OVERLAY
|
||||
// }) as unknown as Mesh
|
||||
// label.rotateX(Math.PI * 0.5)
|
||||
// label.position.set(2.5, 0, 0)
|
||||
// viewer.getRenderer().scene.add(label)
|
||||
|
||||
// const raycaster = new Raycaster()
|
||||
// raycaster.layers.set(ObjectLayers.OVERLAY)
|
||||
// viewer.getRenderer().input.on(InputEvent.Click, (arg) => {
|
||||
// raycaster.setFromCamera(arg, viewer.getRenderer().renderingCamera)
|
||||
// console.log(raycaster.intersectObject(label))
|
||||
// })
|
||||
|
||||
viewer.on(ViewerEvent.UnloadComplete, () => {
|
||||
Object.assign(sandbox.sceneParams.worldSize, viewer.World.worldSize)
|
||||
Object.assign(sandbox.sceneParams.worldOrigin, viewer.World.worldOrigin)
|
||||
@@ -119,8 +142,8 @@ const getStream = () => {
|
||||
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
|
||||
// 'https://latest.speckle.systems/streams/58b5648c4d/commits/60371ecb2d'
|
||||
|
||||
//bad commit! not all items uploaded to server
|
||||
//'https://app.speckle.systems/projects/8e4347e65d/models/39bea37d69'
|
||||
// bad commit! not all items uploaded to server
|
||||
// 'https://app.speckle.systems/projects/8e4347e65d/models/39bea37d69'
|
||||
|
||||
// 'Super' heavy revit shit
|
||||
//'https://app.speckle.systems/streams/e6f9156405/commits/0694d53bb5'
|
||||
@@ -191,7 +214,7 @@ const getStream = () => {
|
||||
// 'https://latest.speckle.systems/streams/85bc4f61c6/commits/8575fe2978'
|
||||
// Alex cubes
|
||||
// 'https://latest.speckle.systems/streams/4658eb53b9/commits/d8ec9cccf7'
|
||||
// Alex more cubes
|
||||
// // Alex more cubes
|
||||
// 'https://latest.speckle.systems/streams/4658eb53b9/commits/31a8d5ff2b'
|
||||
// Tekla
|
||||
// 'https://latest.speckle.systems/streams/caec6d6676/commits/588c731104'
|
||||
@@ -558,9 +581,24 @@ const getStream = () => {
|
||||
// Small (microscopic) building
|
||||
// 'https://app.speckle.systems/projects/26e4c4aab5/models/7d5ff72f5b'
|
||||
|
||||
// Text grid
|
||||
// 'https://app.speckle.systems/projects/dcab71b3de/models/5ff99aa4e1'
|
||||
// Text grid with a LOT of text
|
||||
// 'https://app.speckle.systems/projects/dcab71b3de/models/5f02df011d'
|
||||
|
||||
// Instances with far away transform
|
||||
// 'https://app.speckle.systems/projects/9d0ce16ba8/models/3c079572ea'
|
||||
|
||||
// Far away text screen
|
||||
// 'https://latest.speckle.systems/projects/d46f6cdc80/models/3a67170b04@c6622b474a'
|
||||
// Far away text, world
|
||||
// 'https://latest.speckle.systems/projects/d46f6cdc80/models/3a67170b04@fac9360249'
|
||||
|
||||
// Text test stream
|
||||
// 'https://latest.speckle.systems/projects/109e01c8c0/models/1eee4edbe6'
|
||||
|
||||
// Single billboaded text
|
||||
// 'https://latest.speckle.systems/projects/f28ad5b38a/models/b63ebcd807'
|
||||
// Duplicate display values
|
||||
// 'https://app.speckle.systems/projects/1466fe31c6/models/2eaf0f0571'
|
||||
)
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
"three": "^0.140.0",
|
||||
"three-mesh-bvh": "0.5.17",
|
||||
"tree-model": "1.0.7",
|
||||
"troika-three-text": "0.47.2",
|
||||
"troika-three-text": "0.52.4",
|
||||
"type-fest": "^4.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -78,7 +78,7 @@ import { GeometryType } from './modules/batching/Batch.js'
|
||||
import { MeshBatch } from './modules/batching/MeshBatch.js'
|
||||
import SpeckleStandardMaterial from './modules/materials/SpeckleStandardMaterial.js'
|
||||
import SpeckleTextMaterial from './modules/materials/SpeckleTextMaterial.js'
|
||||
import { SpeckleText } from './modules/objects/SpeckleText.js'
|
||||
import { TextLabel } from './modules/objects/TextLabel.js'
|
||||
import { NodeRenderView } from './modules/tree/NodeRenderView.js'
|
||||
import {
|
||||
CONTAINED,
|
||||
@@ -234,7 +234,7 @@ export {
|
||||
SpeckleStandardMaterial,
|
||||
SpeckleBasicMaterial,
|
||||
SpeckleTextMaterial,
|
||||
SpeckleText,
|
||||
TextLabel,
|
||||
NodeRenderView,
|
||||
SpeckleGeometryConverter,
|
||||
Assets,
|
||||
|
||||
@@ -23,7 +23,12 @@ import {
|
||||
PerspectiveCamera,
|
||||
OrthographicCamera
|
||||
} from 'three'
|
||||
import { type Batch, type BatchUpdateRange, GeometryType } from './batching/Batch.js'
|
||||
import {
|
||||
type Batch,
|
||||
type BatchUpdateRange,
|
||||
GeometryType,
|
||||
isAcceleratedBatchType
|
||||
} from './batching/Batch.js'
|
||||
import Batcher from './batching/Batcher.js'
|
||||
import { Geometry } from './converter/Geometry.js'
|
||||
import Input, { InputEvent } from './input/Input.js'
|
||||
@@ -67,6 +72,8 @@ import Logger from './utils/Logger.js'
|
||||
|
||||
/* TO DO: Not sure where to best import these */
|
||||
import '../type-augmentations/three-extensions.js'
|
||||
import { TextBatch } from '../index.js'
|
||||
import { SpeckleBatchedText } from './objects/SpeckleBatchedText.js'
|
||||
|
||||
export class RenderingStats {
|
||||
private renderTimeAcc = 0
|
||||
@@ -518,13 +525,16 @@ export default class SpeckleRenderer {
|
||||
}
|
||||
|
||||
private updateTransforms() {
|
||||
const meshBatches: MeshBatch[] = this.batcher.getBatches(
|
||||
undefined,
|
||||
GeometryType.MESH
|
||||
)
|
||||
const meshBatches: (MeshBatch | TextBatch)[] = this.batcher.getBatches(undefined, [
|
||||
GeometryType.MESH,
|
||||
GeometryType.TEXT
|
||||
])
|
||||
for (let k = 0; k < meshBatches.length; k++) {
|
||||
const meshBatch: SpeckleMesh | SpeckleInstancedMesh = meshBatches[k].mesh
|
||||
const meshBatch: SpeckleMesh | SpeckleInstancedMesh | SpeckleBatchedText =
|
||||
meshBatches[k].mesh
|
||||
meshBatch.updateTransformsUniform()
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
meshBatch.traverse((obj: Object3D) => {
|
||||
const depthMaterial: SpeckleDepthMaterial =
|
||||
obj.customDepthMaterial as SpeckleDepthMaterial
|
||||
@@ -630,9 +640,16 @@ export default class SpeckleRenderer {
|
||||
let useRTE = false
|
||||
if (
|
||||
batchRenderable instanceof SpeckleMesh ||
|
||||
batchRenderable instanceof SpeckleInstancedMesh
|
||||
batchRenderable instanceof SpeckleInstancedMesh ||
|
||||
batchRenderable instanceof SpeckleBatchedText
|
||||
) {
|
||||
if (batchRenderable.TAS.bvhHelper) parent.add(batchRenderable.TAS.bvhHelper)
|
||||
}
|
||||
|
||||
if (
|
||||
batchRenderable instanceof SpeckleMesh ||
|
||||
batchRenderable instanceof SpeckleInstancedMesh
|
||||
) {
|
||||
useRTE = batchRenderable.needsRTE
|
||||
}
|
||||
if (batch.geometryType === GeometryType.MESH) {
|
||||
@@ -651,6 +668,7 @@ export default class SpeckleRenderer {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.viewer.World.expandWorld(batch.bounds)
|
||||
}
|
||||
|
||||
@@ -1265,15 +1283,18 @@ export default class SpeckleRenderer {
|
||||
}
|
||||
|
||||
public getObjects(): BatchObject[] {
|
||||
const batches = this.batcher.getBatches(undefined, GeometryType.MESH)
|
||||
const meshes = batches.map((batch: MeshBatch) => batch.mesh)
|
||||
const batches = this.batcher.getBatches(undefined, [
|
||||
GeometryType.MESH,
|
||||
GeometryType.TEXT
|
||||
])
|
||||
const meshes = batches.map((batch: MeshBatch | TextBatch) => batch.mesh)
|
||||
const objects = meshes.flatMap((mesh) => mesh.batchObjects)
|
||||
return objects
|
||||
}
|
||||
|
||||
public getObject(rv: NodeRenderView): BatchObject | null {
|
||||
const batch = this.batcher.getBatch(rv) as MeshBatch
|
||||
if (!batch || batch.geometryType !== GeometryType.MESH) {
|
||||
const batch = this.batcher.getBatch(rv)
|
||||
if (!batch || !isAcceleratedBatchType(batch)) {
|
||||
// Logger.error('Render view is not of mesh type. No batch object found')
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Box3, Material, Object3D, WebGLRenderer } from 'three'
|
||||
import { type FilterMaterialOptions } from '../materials/Materials.js'
|
||||
import { NodeRenderView } from '../tree/NodeRenderView.js'
|
||||
import { MeshBatch } from './MeshBatch.js'
|
||||
import { InstancedMeshBatch } from './InstancedMeshBatch.js'
|
||||
import TextBatch from './TextBatch.js'
|
||||
|
||||
export enum GeometryType {
|
||||
MESH,
|
||||
@@ -78,3 +81,13 @@ let BATCH_INDEX_COUNTER = 0
|
||||
export const getNextBatchIndex = () => {
|
||||
return ++BATCH_INDEX_COUNTER
|
||||
}
|
||||
|
||||
export type AcceleratedBatchTypes = MeshBatch | InstancedMeshBatch | TextBatch
|
||||
|
||||
export function isAcceleratedBatchType(batch: Batch): batch is AcceleratedBatchTypes {
|
||||
return (
|
||||
batch &&
|
||||
(batch.geometryType === GeometryType.MESH ||
|
||||
batch.geometryType === GeometryType.TEXT)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ export class BatchObject {
|
||||
public transform: Matrix4
|
||||
public transformInv: Matrix4
|
||||
|
||||
public tasVertIndexStart!: number
|
||||
public tasVertIndexEnd!: number
|
||||
public tasVertIndexStart: number
|
||||
public tasVertIndexEnd: number
|
||||
|
||||
public quaternion: Quaternion = new Quaternion()
|
||||
public eulerValue: Euler = new Euler()
|
||||
|
||||
@@ -40,6 +40,7 @@ export default class Batcher {
|
||||
private maxBatchObjects = 0
|
||||
private maxBatchVertices = 500000
|
||||
private minInstancedBatchVertices = 10000
|
||||
private maxBatchTextObjects = 5000
|
||||
public materials: Materials
|
||||
public batches: { [id: string]: Batch } = {}
|
||||
|
||||
@@ -284,25 +285,35 @@ export default class Batcher {
|
||||
}
|
||||
/** Finally we're splitting again based on the batch's max object count */
|
||||
const geometryType = renderViews[0].geometryType
|
||||
if (geometryType === GeometryType.MESH) {
|
||||
const oSplit = []
|
||||
for (let i = 0; i < vSplit.length; i++) {
|
||||
const objCount = vSplit[i].length
|
||||
const div = Math.floor(objCount / this.maxBatchObjects)
|
||||
const mod = objCount % this.maxBatchObjects
|
||||
let index = 0
|
||||
for (let k = 0; k < div; k++) {
|
||||
oSplit.push(vSplit[i].slice(index, index + this.maxBatchObjects))
|
||||
index += this.maxBatchObjects
|
||||
}
|
||||
if (mod > 0) {
|
||||
oSplit.push(vSplit[i].slice(index, index + mod))
|
||||
}
|
||||
}
|
||||
return oSplit
|
||||
}
|
||||
const maxCount = this.getMaxObjectCount(geometryType)
|
||||
if (!maxCount) return vSplit
|
||||
|
||||
return vSplit
|
||||
const oSplit = []
|
||||
for (let i = 0; i < vSplit.length; i++) {
|
||||
const objCount = vSplit[i].length
|
||||
const div = Math.floor(objCount / maxCount)
|
||||
const mod = objCount % maxCount
|
||||
let index = 0
|
||||
for (let k = 0; k < div; k++) {
|
||||
oSplit.push(vSplit[i].slice(index, index + maxCount))
|
||||
index += maxCount
|
||||
}
|
||||
if (mod > 0) {
|
||||
oSplit.push(vSplit[i].slice(index, index + mod))
|
||||
}
|
||||
}
|
||||
return oSplit
|
||||
}
|
||||
|
||||
private getMaxObjectCount(geometryType: GeometryType) {
|
||||
switch (geometryType) {
|
||||
case GeometryType.MESH:
|
||||
return this.maxBatchObjects
|
||||
case GeometryType.TEXT:
|
||||
return this.maxBatchTextObjects
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
private async buildInstancedBatch(
|
||||
@@ -539,6 +550,15 @@ export default class Batcher {
|
||||
public getBatches<K extends GeometryType>(
|
||||
subtreeId?: string,
|
||||
geometryType?: K
|
||||
): BatchTypeMap[K][]
|
||||
public getBatches<K extends GeometryType>(
|
||||
subtreeId?: string,
|
||||
geometryType?: Array<K>
|
||||
): BatchTypeMap[K][]
|
||||
|
||||
public getBatches<K extends GeometryType>(
|
||||
subtreeId?: string,
|
||||
geometryType?: K | Array<K>
|
||||
): BatchTypeMap[K][] {
|
||||
const batches: Batch[] = Object.values(this.batches)
|
||||
return batches.filter((value: Batch) => {
|
||||
@@ -551,23 +571,34 @@ export default class Batcher {
|
||||
|
||||
private isBatchType<K extends GeometryType>(
|
||||
batch: Batch,
|
||||
geometryType?: K
|
||||
geometryType?: K | Array<K>
|
||||
): batch is BatchTypeMap[K] {
|
||||
if (geometryType === undefined) return true
|
||||
switch (geometryType) {
|
||||
case GeometryType.MESH:
|
||||
return batch instanceof MeshBatch || batch instanceof InstancedMeshBatch
|
||||
case GeometryType.LINE:
|
||||
return batch instanceof LineBatch
|
||||
case GeometryType.POINT:
|
||||
return batch instanceof PointBatch
|
||||
case GeometryType.POINT_CLOUD:
|
||||
return batch instanceof PointBatch
|
||||
case GeometryType.TEXT:
|
||||
return batch instanceof TextBatch
|
||||
default:
|
||||
return false
|
||||
}
|
||||
let isBatchType = false
|
||||
const array = Array.isArray(geometryType) ? geometryType : [geometryType]
|
||||
array.forEach((value: K) => {
|
||||
switch (value) {
|
||||
case GeometryType.MESH:
|
||||
isBatchType ||=
|
||||
batch instanceof MeshBatch || batch instanceof InstancedMeshBatch
|
||||
break
|
||||
case GeometryType.LINE:
|
||||
isBatchType ||= batch instanceof LineBatch
|
||||
break
|
||||
case GeometryType.POINT:
|
||||
isBatchType ||= batch instanceof PointBatch
|
||||
break
|
||||
case GeometryType.POINT_CLOUD:
|
||||
isBatchType ||= batch instanceof PointBatch
|
||||
break
|
||||
case GeometryType.TEXT:
|
||||
isBatchType ||= batch instanceof TextBatch
|
||||
break
|
||||
default:
|
||||
isBatchType = false
|
||||
}
|
||||
})
|
||||
return isBatchType
|
||||
}
|
||||
|
||||
public getBatch(rv: NodeRenderView): Batch | undefined {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Box3, Material, Object3D, WebGLRenderer } from 'three'
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { Box3, Material, Matrix4, Object3D, WebGLRenderer } from 'three'
|
||||
|
||||
import { NodeRenderView } from '../tree/NodeRenderView.js'
|
||||
import {
|
||||
@@ -10,48 +11,63 @@ import {
|
||||
NoneBatchUpdateRange
|
||||
} from './Batch.js'
|
||||
|
||||
import { SpeckleText } from '../objects/SpeckleText.js'
|
||||
import { ObjectLayers } from '../../IViewer.js'
|
||||
import Materials from '../materials/Materials.js'
|
||||
import { SpeckleBatchedText } from '../objects/SpeckleBatchedText.js'
|
||||
//@ts-ignore
|
||||
import { AnchorX, AnchorY, Text } from 'troika-three-text'
|
||||
import {
|
||||
AccelerationStructure,
|
||||
BatchObject,
|
||||
Geometry,
|
||||
ObjectLayers,
|
||||
SpeckleTextMaterial
|
||||
} from '../../index.js'
|
||||
import { DefaultBVHOptions } from '../objects/AccelerationStructure.js'
|
||||
import { TextBatchObject } from './TextBatchObject.js'
|
||||
import { DrawRanges } from './DrawRanges.js'
|
||||
import Logger from '../utils/Logger.js'
|
||||
import SpeckleTextColoredMaterial from '../materials/SpeckleTextColoredMaterial.js'
|
||||
|
||||
const INSTANCE_TEXT_TRIS_COUNT = 2
|
||||
const INSTANCE_TEXT_VERT_COUNT = 4
|
||||
|
||||
export default class TextBatch implements Batch {
|
||||
public id: string
|
||||
public subtreeId: string
|
||||
public renderViews: NodeRenderView[]
|
||||
public batchMaterial!: Material
|
||||
public mesh: SpeckleText
|
||||
public batchMaterial: Material
|
||||
public mesh: SpeckleBatchedText
|
||||
protected drawRanges: DrawRanges = new DrawRanges()
|
||||
|
||||
public get bounds(): Box3 {
|
||||
return new Box3().setFromObject(this.mesh)
|
||||
return this.mesh.TAS.getBoundingBox(new Box3())
|
||||
}
|
||||
|
||||
public get drawCalls(): number {
|
||||
return 1
|
||||
return this.groups.length
|
||||
}
|
||||
|
||||
public get minDrawCalls(): number {
|
||||
return [...Array.from(new Set(this.groups.map((value) => value.materialIndex)))]
|
||||
.length
|
||||
}
|
||||
|
||||
public get maxDrawCalls(): number {
|
||||
return 1
|
||||
}
|
||||
|
||||
public get triCount(): number {
|
||||
return this.getCount()
|
||||
return INSTANCE_TEXT_TRIS_COUNT * this.renderViews.length
|
||||
}
|
||||
|
||||
public get vertCount(): number {
|
||||
return (
|
||||
this.mesh.textMesh.geometry.attributes.position.count +
|
||||
this.mesh.backgroundMesh?.geometry.attributes.position.count
|
||||
)
|
||||
return INSTANCE_TEXT_VERT_COUNT * this.renderViews.length
|
||||
}
|
||||
|
||||
public constructor(id: string, subtreeId: string, renderViews: NodeRenderView[]) {
|
||||
this.id = id
|
||||
this.subtreeId = subtreeId
|
||||
this.renderViews = renderViews
|
||||
}
|
||||
public get pointCount(): number {
|
||||
return 0
|
||||
}
|
||||
|
||||
public get lineCount(): number {
|
||||
return 0
|
||||
}
|
||||
@@ -61,22 +77,25 @@ export default class TextBatch implements Batch {
|
||||
}
|
||||
|
||||
public get renderObject(): Object3D {
|
||||
return this.mesh
|
||||
return this.mesh as unknown as Object3D
|
||||
}
|
||||
|
||||
public getCount(): number {
|
||||
return (
|
||||
this.mesh.textMesh.geometry.index.count +
|
||||
this.mesh.backgroundMesh?.geometry.index?.count
|
||||
)
|
||||
return this.renderViews.length
|
||||
}
|
||||
|
||||
public get materials(): Material[] {
|
||||
return this.mesh.material as Material[]
|
||||
return this.mesh.materials
|
||||
}
|
||||
|
||||
public get groups(): DrawGroup[] {
|
||||
return []
|
||||
public get groups(): Array<DrawGroup> {
|
||||
return this.mesh.groups
|
||||
}
|
||||
|
||||
public constructor(id: string, subtreeId: string, renderViews: NodeRenderView[]) {
|
||||
this.id = id
|
||||
this.subtreeId = subtreeId
|
||||
this.renderViews = renderViews
|
||||
}
|
||||
|
||||
public setBatchMaterial(material: Material) {
|
||||
@@ -116,65 +135,348 @@ export default class TextBatch implements Batch {
|
||||
return NoneBatchUpdateRange
|
||||
}
|
||||
|
||||
public setBatchBuffers(range: BatchUpdateRange[]): void {
|
||||
range
|
||||
throw new Error('Method not implemented.')
|
||||
/** Text batches are mix between how mesh and line batches work.
|
||||
* - They still keep track of various draw groups each with it's material
|
||||
* - However that material is not really being used, bur rather the properies are copied over to the batch fp32 data texture
|
||||
* - For filtering we cheat and use `SpeckleTextColoredMaterial` only to store the gradient/ramp texture + gradient indices for each text in the batch
|
||||
* - The color from the gradient/ramp texture will be used only if the gradient index > 0, otherwise the regular color will be used
|
||||
* - The gradient index is stored in each text object in it's `userData` and written to the 27'th float in the batch data texture, where the shader reads if from
|
||||
* - 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)
|
||||
const splitRanges: BatchUpdateRange[] = []
|
||||
ranges.forEach((range: BatchUpdateRange) => {
|
||||
for (let k = 0; k < range.count; k++) {
|
||||
splitRanges.push({
|
||||
offset: range.offset + k,
|
||||
count: 1,
|
||||
material: range.material,
|
||||
materialOptions: range.materialOptions
|
||||
})
|
||||
}
|
||||
})
|
||||
//@ts-ignore
|
||||
this.mesh._members.forEach((packingInfo, text) => {
|
||||
const range = splitRanges.find((val) => val.offset === packingInfo.index)
|
||||
if (!range) return
|
||||
|
||||
//@ts-ignore
|
||||
text.color = range.material?.color
|
||||
//@ts-ignore
|
||||
text.material.color = range.material?.color
|
||||
//@ts-ignore
|
||||
text.material.opacity = range.material?.visible ? range.material?.opacity : 0
|
||||
|
||||
if (range.material instanceof SpeckleTextColoredMaterial) {
|
||||
// Group has gradient/ramp texture color source
|
||||
if (range.materialOptions) {
|
||||
if (
|
||||
range.materialOptions.rampIndex !== undefined &&
|
||||
range.materialOptions.rampWidth !== undefined
|
||||
) {
|
||||
/** The ramp indices specify the *begining* of each ramp color. When sampling with Nearest filter (since we don't want filtering)
|
||||
* we'll always be sampling right at the edge between texels. Most GPUs will sample consistently, but some won't and we end up with
|
||||
* a ton of artifacts. To avoid this, we are shifting the sampling indices so they're right on the center of each texel, so no inconsistent
|
||||
* sampling can occur.
|
||||
*/
|
||||
const shiftedIndex =
|
||||
range.materialOptions.rampIndex + 0.5 / range.materialOptions.rampWidth
|
||||
/** Update the gradient indices for the individual texts
|
||||
* The colored material is singular, as provided by Materials
|
||||
*/
|
||||
range.material.updateGradientIndexMap(packingInfo.index, shiftedIndex)
|
||||
text.userData.gradientIndex = shiftedIndex
|
||||
}
|
||||
if (range.materialOptions.rampTexture !== undefined) {
|
||||
;(range.material as SpeckleTextMaterial).setGradientTexture(
|
||||
range.materialOptions.rampTexture
|
||||
)
|
||||
this.mesh.setGradientTexture(range.materialOptions.rampTexture)
|
||||
}
|
||||
} else {
|
||||
text.userData.gradientIndex =
|
||||
range.material.gradientIndexMap[packingInfo.index]
|
||||
this.mesh.setGradientTexture(range.material.userData.gradientRamp.value)
|
||||
}
|
||||
} else {
|
||||
// No gradient or ramp color source
|
||||
text.userData.gradientIndex = -1
|
||||
}
|
||||
|
||||
packingInfo.needsUpdate = true
|
||||
})
|
||||
//@ts-ignore
|
||||
this.mesh.dirty = true
|
||||
//@ts-ignore
|
||||
this.mesh.sync()
|
||||
}
|
||||
|
||||
public setDrawRanges(ranges: BatchUpdateRange[]) {
|
||||
this.mesh.textMesh.material = ranges[0].material
|
||||
if (ranges[0].materialOptions && ranges[0].materialOptions.rampIndexColor) {
|
||||
this.mesh.textMesh.material.color.copy(ranges[0].materialOptions.rampIndexColor)
|
||||
const materials: Array<Material> = ranges.map((val: BatchUpdateRange) => {
|
||||
return val.material as Material
|
||||
})
|
||||
const uniqueMaterials = [...Array.from(new Set(materials.map((value) => value)))]
|
||||
|
||||
for (let k = 0; k < uniqueMaterials.length; k++) {
|
||||
if (!this.materials.includes(uniqueMaterials[k]))
|
||||
this.materials.push(uniqueMaterials[k])
|
||||
}
|
||||
|
||||
this.mesh.groups = this.drawRanges.integrateRanges(
|
||||
this.groups,
|
||||
this.materials,
|
||||
ranges
|
||||
)
|
||||
|
||||
let count = 0
|
||||
this.groups.forEach((value) => (count += value.count))
|
||||
if (count !== this.renderViews.length) {
|
||||
Logger.error(`Draw groups invalid on ${this.id}`)
|
||||
}
|
||||
this.setBatchBuffers(ranges)
|
||||
this.cleanMaterials()
|
||||
}
|
||||
|
||||
private cleanMaterials() {
|
||||
const materialsInUse = [
|
||||
...Array.from(
|
||||
new Set(this.groups.map((value) => this.materials[value.materialIndex]))
|
||||
)
|
||||
]
|
||||
let k = 0
|
||||
while (this.materials.length > materialsInUse.length) {
|
||||
if (!materialsInUse.includes(this.materials[k])) {
|
||||
this.materials.splice(k, 1)
|
||||
this.groups.forEach((value: DrawGroup) => {
|
||||
if (value.materialIndex > k) value.materialIndex--
|
||||
})
|
||||
k = 0
|
||||
continue
|
||||
}
|
||||
k++
|
||||
}
|
||||
}
|
||||
|
||||
public resetDrawRanges() {
|
||||
this.mesh.textMesh.material = this.batchMaterial
|
||||
this.mesh.textMesh.visible = true
|
||||
this.groups.length = 0
|
||||
this.materials.length = 0
|
||||
|
||||
this.materials.push(this.batchMaterial)
|
||||
this.setVisibleRange([AllBatchUpdateRange])
|
||||
this.setDrawRanges([
|
||||
{
|
||||
offset: 0,
|
||||
count: this.renderViews.length,
|
||||
material: this.batchMaterial
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
protected alignmentXToAnchorX(value: number): AnchorX {
|
||||
switch (value) {
|
||||
case 0:
|
||||
return 'left'
|
||||
case 1:
|
||||
return 'center'
|
||||
case 2:
|
||||
return 'right'
|
||||
default:
|
||||
return 'center'
|
||||
}
|
||||
}
|
||||
|
||||
protected alignmentYToAnchorY(value: number): AnchorY {
|
||||
switch (value) {
|
||||
case 0:
|
||||
return 'top'
|
||||
case 1:
|
||||
return 'middle'
|
||||
case 2:
|
||||
return 'bottom'
|
||||
default:
|
||||
return 'middle'
|
||||
}
|
||||
}
|
||||
|
||||
public async buildBatch(): Promise<void> {
|
||||
/** Catering to typescript
|
||||
* There is no unniverse where there is no metadata
|
||||
*/
|
||||
if (!this.renderViews[0].renderData.geometry.metaData) {
|
||||
throw new Error(`Cannot build batch ${this.id}. Metadata`)
|
||||
}
|
||||
this.mesh = new SpeckleText(this.id, ObjectLayers.STREAM_CONTENT_TEXT)
|
||||
this.mesh.matrixAutoUpdate = false
|
||||
await this.mesh.update(
|
||||
SpeckleText.SpeckleTextParamsFromMetadata(
|
||||
this.renderViews[0].renderData.geometry.metaData
|
||||
return new Promise((resolve) => {
|
||||
this.mesh = new SpeckleBatchedText()
|
||||
const textMap = new Map()
|
||||
const batchObjects: BatchObject[] = []
|
||||
const textObjects: Text[] = []
|
||||
const box = new Box3()
|
||||
let needsRTE = false
|
||||
let needsBillboard = false
|
||||
let textSynced = this.renderViews.length
|
||||
for (let k = 0; k < this.renderViews.length; k++) {
|
||||
const textMeta = this.renderViews[k].renderData.geometry
|
||||
.metaData as unknown as {
|
||||
value: string
|
||||
height: number
|
||||
maxWidth: number
|
||||
alignmentH: number
|
||||
alignmentV: number
|
||||
screenOriented: boolean
|
||||
}
|
||||
const text = new Text()
|
||||
this.renderViews[k].renderData.geometry.bakeTransform?.decompose(
|
||||
text.position,
|
||||
text.quaternion,
|
||||
text.scale
|
||||
)
|
||||
text.updateMatrixWorld(true)
|
||||
|
||||
if (textMeta) {
|
||||
text.text = textMeta.value
|
||||
text.fontSize = textMeta.height
|
||||
text.maxWidth =
|
||||
textMeta.maxWidth !== null ? textMeta.maxWidth : Number.POSITIVE_INFINITY
|
||||
text.anchorX = this.alignmentXToAnchorX(textMeta.alignmentH)
|
||||
text.anchorY = this.alignmentYToAnchorY(textMeta.alignmentV)
|
||||
}
|
||||
needsBillboard ||= textMeta !== undefined ? textMeta.screenOriented : false
|
||||
|
||||
text.material = new SpeckleTextMaterial({
|
||||
color: 0xff0000 // control color
|
||||
}).getDerivedMaterial()
|
||||
|
||||
textMap.set(text, this.renderViews[k])
|
||||
|
||||
text.sync(() => {
|
||||
const { textRenderInfo } = text
|
||||
/** 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
|
||||
)
|
||||
box.setFromArray(vertices)
|
||||
box.applyMatrix4(
|
||||
this.renderViews[k].renderData.geometry.bakeTransform || new Matrix4()
|
||||
)
|
||||
|
||||
needsRTE ||= Geometry.needsRTE(box)
|
||||
|
||||
const geometry = text.geometry
|
||||
geometry.computeBoundingBox()
|
||||
const textBvh = AccelerationStructure.buildBVH(
|
||||
geometry.index?.array as number[],
|
||||
vertices,
|
||||
DefaultBVHOptions
|
||||
)
|
||||
/** The bounds bug. <Sigh> it needs a refit to report the correct bounds */
|
||||
textBvh.refit()
|
||||
|
||||
const batchObject = new TextBatchObject(this.renderViews[k], k)
|
||||
batchObject.buildAccelerationStructure(textBvh)
|
||||
batchObjects.push(batchObject)
|
||||
textObjects.push(text)
|
||||
//@ts-ignore
|
||||
this.mesh.addText(text)
|
||||
textSynced--
|
||||
if (!textSynced) {
|
||||
if (!this.batchMaterial.defines) this.batchMaterial.defines = {}
|
||||
if (needsRTE) {
|
||||
this.batchMaterial.defines['USE_RTE'] = ' '
|
||||
}
|
||||
if (needsBillboard) this.batchMaterial.defines['BILLBOARD'] = ' '
|
||||
this.mesh.setBatchObjects(batchObjects, textObjects)
|
||||
this.mesh.setBatchMaterial(this.batchMaterial)
|
||||
this.mesh.buildTAS()
|
||||
|
||||
//@ts-ignore
|
||||
this.mesh.uuid = this.id
|
||||
//@ts-ignore
|
||||
this.mesh.layers.set(ObjectLayers.STREAM_CONTENT_TEXT)
|
||||
//@ts-ignore
|
||||
this.mesh.frustumCulled = false
|
||||
|
||||
this.mesh.dirty = true
|
||||
|
||||
this.groups.push({
|
||||
start: 0,
|
||||
count: this.renderViews.length,
|
||||
materialIndex: 0
|
||||
})
|
||||
//@ts-ignore
|
||||
this.mesh.sync(() => {
|
||||
/** We assign the allocated packing info to the text render views as we'll be using the same batch indices for simplicity */
|
||||
//@ts-ignore
|
||||
this.mesh._members.forEach((packingInfo, text) => {
|
||||
textMap.get(text).setBatchData(this.id, packingInfo.index, 1)
|
||||
packingInfo.needsUpdate = true
|
||||
})
|
||||
this.setBatchBuffers([
|
||||
{
|
||||
offset: 0,
|
||||
count: this.renderViews.length,
|
||||
material: this.batchMaterial
|
||||
}
|
||||
])
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public getRenderView(index: number): NodeRenderView | null {
|
||||
index
|
||||
Logger.warn('Deprecated! Use InstancedBatchObject')
|
||||
return null
|
||||
}
|
||||
|
||||
public getMaterialAtIndex(index: number): Material | null {
|
||||
index
|
||||
Logger.warn('Deprecated! Use InstancedBatchObject')
|
||||
return null
|
||||
}
|
||||
|
||||
public getMaterial(rv: NodeRenderView): Material | null {
|
||||
const group = this.groups.find((value) => {
|
||||
return (
|
||||
rv.batchStart >= value.start &&
|
||||
rv.batchStart + rv.batchCount <= value.count + value.start
|
||||
)
|
||||
)
|
||||
if (this.renderViews[0].renderData.geometry.bakeTransform)
|
||||
this.mesh.matrix.copy(this.renderViews[0].renderData.geometry.bakeTransform)
|
||||
this.renderViews[0].setBatchData(
|
||||
this.id,
|
||||
0,
|
||||
this.mesh.textMesh.geometry.index.count / 3
|
||||
)
|
||||
this.mesh.textMesh.material = this.batchMaterial
|
||||
}
|
||||
})
|
||||
if (!group) {
|
||||
Logger.warn(`Could not get material for ${rv.renderData.id}`)
|
||||
return null
|
||||
}
|
||||
return this.materials[group.materialIndex]
|
||||
|
||||
public getRenderView(index: number): NodeRenderView {
|
||||
index
|
||||
return this.renderViews[0]
|
||||
}
|
||||
// /** Just like for lines, this isn't ideal but it's quicker */
|
||||
// const material = this.materials[group.materialIndex].clone() as SpeckleTextMaterial
|
||||
// //@ts-ignore
|
||||
// this.mesh._members.forEach((packingInfo, text) => {
|
||||
// if (group.start === packingInfo.index) {
|
||||
// material.color.copy(text.material.color)
|
||||
// material.opacity = text.material.opacity
|
||||
// }
|
||||
// })
|
||||
|
||||
public getMaterialAtIndex(index: number): Material {
|
||||
index
|
||||
return this.batchMaterial
|
||||
}
|
||||
|
||||
public getMaterial(rv: NodeRenderView): Material {
|
||||
rv
|
||||
return this.batchMaterial
|
||||
// return material
|
||||
}
|
||||
|
||||
public purge() {
|
||||
this.renderViews.length = 0
|
||||
this.batchMaterial.dispose()
|
||||
this.mesh.geometry.dispose()
|
||||
//@ts-ignore
|
||||
this.mesh.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Box3, Matrix4 } from 'three'
|
||||
import { BatchObject, Vector3Like } from './BatchObject.js'
|
||||
import { NodeRenderView } from '../tree/NodeRenderView.js'
|
||||
|
||||
export class TextBatchObject extends BatchObject {
|
||||
public textTransform: Matrix4 = new Matrix4()
|
||||
|
||||
public constructor(renderView: NodeRenderView, batchIndex: number) {
|
||||
super(renderView, batchIndex)
|
||||
if (renderView.renderData.geometry.bakeTransform)
|
||||
this.textTransform.copy(renderView.renderData.geometry.bakeTransform)
|
||||
/** TO DO: Not sure we should do this */
|
||||
this.transform.copy(this.textTransform)
|
||||
this.transformInv.copy(new Matrix4().copy(this.textTransform).invert())
|
||||
this.transformDirty = false
|
||||
}
|
||||
|
||||
public get aabb(): Box3 {
|
||||
return this._accelerationStructure.getBoundingBox(new Box3())
|
||||
}
|
||||
|
||||
public transformTRS(
|
||||
translation: Vector3Like,
|
||||
euler: Vector3Like,
|
||||
scale: Vector3Like,
|
||||
pivot: Vector3Like
|
||||
) {
|
||||
super.transformTRS(translation, euler, scale, pivot)
|
||||
this.transform.multiply(this.textTransform)
|
||||
this.transformInv.copy(this.transform)
|
||||
this.transformInv.invert()
|
||||
}
|
||||
}
|
||||
@@ -478,7 +478,7 @@ export class CameraController extends Extension implements SpeckleCamera {
|
||||
|
||||
const batches = this.viewer
|
||||
.getRenderer()
|
||||
.batcher.getBatches(undefined, GeometryType.MESH)
|
||||
.batcher.getBatches(undefined, [GeometryType.MESH, GeometryType.TEXT])
|
||||
let minDist = Number.POSITIVE_INFINITY
|
||||
for (let b = 0; b < batches.length; b++) {
|
||||
const result = batches[b].mesh.TAS.closestPointToPointHalfplane(
|
||||
|
||||
@@ -232,14 +232,17 @@ export class SmoothOrbitControls extends SpeckleControls {
|
||||
this.setDamperDecayTime(this._options.damperDecay)
|
||||
|
||||
const billboardMaterial = new SpeckleBasicMaterial({ color: 0x047efb }, [
|
||||
'BILLBOARD_FIXED'
|
||||
'BILLBOARD_SCREEN'
|
||||
])
|
||||
billboardMaterial.opacity = 0.75
|
||||
billboardMaterial.transparent = true
|
||||
billboardMaterial.color.convertSRGBToLinear()
|
||||
billboardMaterial.toneMapped = false
|
||||
billboardMaterial.depthTest = false
|
||||
billboardMaterial.billboardPixelHeight = 15 * window.devicePixelRatio
|
||||
billboardMaterial.billboardPixelSize = new Vector2(
|
||||
15 * window.devicePixelRatio,
|
||||
15 * window.devicePixelRatio
|
||||
)
|
||||
|
||||
this.orbitSphere = new Mesh(new SphereGeometry(0.5, 32, 16), billboardMaterial)
|
||||
this.orbitSphere.layers.set(ObjectLayers.OVERLAY)
|
||||
@@ -863,9 +866,7 @@ export class SmoothOrbitControls extends SpeckleControls {
|
||||
this._options.orbitAroundCursor && this.usePivotal
|
||||
? this.pivotPoint
|
||||
: new Vector3().copy(this.origin).applyMatrix4(this._basisTransform)
|
||||
/** TO DO: Revisit and set by writing to it's position */
|
||||
const mat = this.orbitSphere.material as SpeckleBasicMaterial
|
||||
mat.userData.billboardPos.value.copy(spherePos)
|
||||
this.orbitSphere.position.copy(spherePos)
|
||||
|
||||
/** We'd rather have a palpable epsilon for regular sized streams, but also
|
||||
* compute a custom one for microscopic ones
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import {
|
||||
AlwaysStencilFunc,
|
||||
Box3,
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
Camera,
|
||||
DoubleSide,
|
||||
DynamicDrawUsage,
|
||||
KeepStencilOp,
|
||||
Material,
|
||||
Mesh,
|
||||
NotEqualStencilFunc,
|
||||
OrthographicCamera,
|
||||
PerspectiveCamera,
|
||||
Plane,
|
||||
Quaternion,
|
||||
Raycaster,
|
||||
ReplaceStencilOp,
|
||||
Vector2,
|
||||
Vector3,
|
||||
type Intersection
|
||||
@@ -72,8 +76,16 @@ export class AreaMeasurement extends Measurement {
|
||||
super()
|
||||
|
||||
this.type = 'AreaMeasurement'
|
||||
/** We create the initial gizmo */
|
||||
/** We create the initial gizmo which will always display the area value text label*/
|
||||
const gizmo = new MeasurementPointGizmo()
|
||||
/** The gizmo's TextLabel will write `1` to the stencil buffer */
|
||||
gizmo.text.backgroundMaterial.stencilWrite = true
|
||||
gizmo.text.backgroundMaterial.depthWrite = false
|
||||
gizmo.text.backgroundMaterial.depthTest = false
|
||||
gizmo.text.backgroundMaterial.stencilFunc = AlwaysStencilFunc
|
||||
gizmo.text.backgroundMaterial.stencilRef = 1
|
||||
gizmo.text.backgroundMaterial.stencilZPass = ReplaceStencilOp
|
||||
|
||||
gizmo.enable(false, true, true, false)
|
||||
this.pointGizmos.push(gizmo)
|
||||
this.add(this.pointGizmos[0])
|
||||
@@ -136,6 +148,7 @@ export class AreaMeasurement extends Measurement {
|
||||
|
||||
/** Add a new gizmo */
|
||||
const gizmo = new MeasurementPointGizmo()
|
||||
|
||||
gizmo.enable(false, true, true, false)
|
||||
this.pointGizmos.push(gizmo)
|
||||
this.add(gizmo)
|
||||
@@ -310,8 +323,16 @@ export class AreaMeasurement extends Measurement {
|
||||
toneMapped: false
|
||||
})
|
||||
material.color.convertSRGBToLinear()
|
||||
this.fillPolygon = new Mesh(new BufferGeometry(), material)
|
||||
/** The transparent area plane will only draw were the stencil buffer is **NOT** `1`, effectively not overdrawing the text label */
|
||||
material.depthWrite = false
|
||||
material.depthTest = false
|
||||
material.stencilWrite = true
|
||||
material.stencilFunc = NotEqualStencilFunc
|
||||
material.stencilRef = 1
|
||||
material.stencilZPass = KeepStencilOp
|
||||
|
||||
this.fillPolygon = new Mesh(new BufferGeometry(), material)
|
||||
this.fillPolygon.renderOrder = 100
|
||||
this.fillPolygon.frustumCulled = false
|
||||
this.fillPolygon.layers.set(ObjectLayers.MEASUREMENTS)
|
||||
this.add(this.fillPolygon)
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
Camera,
|
||||
CircleGeometry,
|
||||
Color,
|
||||
DoubleSide,
|
||||
DynamicDrawUsage,
|
||||
Group,
|
||||
InterleavedBufferAttribute,
|
||||
@@ -22,11 +21,9 @@ import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js'
|
||||
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js'
|
||||
import { Geometry } from '../../converter/Geometry.js'
|
||||
import SpeckleLineMaterial from '../../materials/SpeckleLineMaterial.js'
|
||||
import { SpeckleText } from '../../objects/SpeckleText.js'
|
||||
import SpeckleTextMaterial from '../../materials/SpeckleTextMaterial.js'
|
||||
import { TextLabel, TextLabelParams } from '../../objects/TextLabel.js'
|
||||
import SpeckleBasicMaterial from '../../materials/SpeckleBasicMaterial.js'
|
||||
import { ObjectLayers } from '../../../IViewer.js'
|
||||
import Logger from '../../utils/Logger.js'
|
||||
|
||||
export interface MeasurementPointGizmoStyle {
|
||||
dashedLine?: boolean
|
||||
@@ -55,19 +52,21 @@ const DefaultMeasurementPointGizmoStyle = {
|
||||
pointOpacity: 1,
|
||||
textColor: 0xffffff,
|
||||
textOpacity: 1,
|
||||
textPixelHeight: 17,
|
||||
textPixelHeight: 11,
|
||||
pointPixelHeight: 5
|
||||
}
|
||||
|
||||
export class MeasurementPointGizmo extends Group {
|
||||
private normalIndicator: LineSegments2
|
||||
public normalIndicator: LineSegments2
|
||||
private normalIndicatorBuffer: Float64Array = new Float64Array(24)
|
||||
private normalIndicatorNormal: Vector3 = new Vector3()
|
||||
private normalIndicatorTangent: Vector3 = new Vector3()
|
||||
private normalIndicatorBitangent: Vector3 = new Vector3()
|
||||
private line: LineSegments2
|
||||
private point: Mesh
|
||||
private text: SpeckleText
|
||||
|
||||
public line: LineSegments2
|
||||
public point: Mesh<CircleGeometry, SpeckleBasicMaterial>
|
||||
public text: TextLabel
|
||||
|
||||
private _style: MeasurementPointGizmoStyle = Object.assign(
|
||||
{},
|
||||
DefaultMeasurementPointGizmoStyle
|
||||
@@ -80,14 +79,10 @@ export class MeasurementPointGizmo extends Group {
|
||||
|
||||
public set highlight(value: boolean) {
|
||||
if (value) {
|
||||
;(this.normalIndicator.material as SpeckleLineMaterial).color = new Color(
|
||||
0xff0000
|
||||
)
|
||||
;(this.line.material as SpeckleLineMaterial).color = new Color(0xff0000)
|
||||
;(this.point.material as SpeckleBasicMaterial).color = new Color(0xff0000)
|
||||
;(this.text.textMesh.material as SpeckleTextMaterial).color.copy(
|
||||
new Color(0xff0000)
|
||||
)
|
||||
this.normalIndicator.material.color = new Color(0xff0000)
|
||||
this.line.material.color = new Color(0xff0000)
|
||||
this.point.material.color = new Color(0xff0000)
|
||||
this.text.material.color.copy(new Color(0xff0000))
|
||||
} else this.updateStyle()
|
||||
}
|
||||
|
||||
@@ -153,7 +148,7 @@ export class MeasurementPointGizmo extends Group {
|
||||
private getPointMaterial(color?: number) {
|
||||
const material = new SpeckleBasicMaterial(
|
||||
{ color: color ? color : this._style.pointColor },
|
||||
['BILLBOARD_FIXED']
|
||||
['BILLBOARD_SCREEN']
|
||||
)
|
||||
material.opacity =
|
||||
this._style.pointOpacity !== undefined
|
||||
@@ -163,40 +158,15 @@ export class MeasurementPointGizmo extends Group {
|
||||
material.color.convertSRGBToLinear()
|
||||
material.toneMapped = false
|
||||
material.depthTest = false
|
||||
material.billboardPixelHeight =
|
||||
const billboardSize =
|
||||
(this._style.pointPixelHeight !== undefined
|
||||
? this._style.pointPixelHeight
|
||||
: DefaultMeasurementPointGizmoStyle.pointPixelHeight) * window.devicePixelRatio
|
||||
material.userData.billboardPos.value.copy(this.point.position)
|
||||
material.billboardPixelSize = new Vector2(billboardSize, billboardSize)
|
||||
|
||||
return material
|
||||
}
|
||||
|
||||
private getTextMaterial() {
|
||||
const material = new SpeckleTextMaterial(
|
||||
{
|
||||
color: this._style.textColor,
|
||||
opacity: 1,
|
||||
side: DoubleSide
|
||||
},
|
||||
['BILLBOARD_FIXED']
|
||||
)
|
||||
material.toneMapped = false
|
||||
material.color.convertSRGBToLinear()
|
||||
material.opacity =
|
||||
this._style.textOpacity !== undefined
|
||||
? this._style.textOpacity
|
||||
: DefaultMeasurementPointGizmoStyle.textOpacity
|
||||
material.transparent = material.opacity < 1
|
||||
material.depthTest = false
|
||||
material.billboardPixelHeight =
|
||||
(this._style.textPixelHeight !== undefined
|
||||
? this._style.textPixelHeight
|
||||
: DefaultMeasurementPointGizmoStyle.textPixelHeight) * window.devicePixelRatio
|
||||
material.userData.billboardPos.value.copy(this.text.position)
|
||||
|
||||
return material.getDerivedMaterial()
|
||||
}
|
||||
|
||||
public constructor(style?: MeasurementPointGizmoStyle) {
|
||||
super()
|
||||
this.layers.set(ObjectLayers.MEASUREMENTS)
|
||||
@@ -235,24 +205,43 @@ export class MeasurementPointGizmo extends Group {
|
||||
|
||||
const sphereGeometry = new CircleGeometry(1, 16)
|
||||
|
||||
this.point = new Mesh(sphereGeometry, undefined)
|
||||
this.point = new Mesh(sphereGeometry)
|
||||
this.point.layers.set(ObjectLayers.MEASUREMENTS)
|
||||
this.point.visible = false
|
||||
this.point.renderOrder = 1
|
||||
|
||||
const point2 = new Mesh(sphereGeometry, this.getPointMaterial(0xffffff))
|
||||
point2.renderOrder = 2
|
||||
point2.material.billboardPixelHeight =
|
||||
const pixelSize =
|
||||
(this._style.pointPixelHeight !== undefined
|
||||
? this._style.pointPixelHeight
|
||||
: DefaultMeasurementPointGizmoStyle.pointPixelHeight) *
|
||||
window.devicePixelRatio -
|
||||
2 * window.devicePixelRatio
|
||||
point2.material.billboardPixelSize = new Vector2(pixelSize, pixelSize)
|
||||
point2.layers.set(ObjectLayers.MEASUREMENTS)
|
||||
this.point.add(point2)
|
||||
|
||||
this.text = new SpeckleText(MathUtils.generateUUID(), ObjectLayers.MEASUREMENTS)
|
||||
this.text.textMesh.material = null
|
||||
this.text = new TextLabel({
|
||||
textColor: new Color(this._style.textColor),
|
||||
fontSize:
|
||||
this._style.textPixelHeight !== undefined
|
||||
? this._style.textPixelHeight
|
||||
: DefaultMeasurementPointGizmoStyle.textPixelHeight,
|
||||
textOpacity:
|
||||
this._style.textOpacity !== undefined
|
||||
? this._style.textOpacity
|
||||
: DefaultMeasurementPointGizmoStyle.textOpacity,
|
||||
billboard: 'screen',
|
||||
anchorX: 'center',
|
||||
anchorY: 'middle',
|
||||
backgroundColor: new Color(0x047efb),
|
||||
backgroundCornerRadius: 0.3,
|
||||
backgroundMargins: new Vector2(30, 10),
|
||||
objectLayer: ObjectLayers.MEASUREMENTS
|
||||
})
|
||||
this.text.material.depthTest = false
|
||||
this.text.depthOffset = -0.1
|
||||
|
||||
this.add(this.point)
|
||||
this.add(this.normalIndicator)
|
||||
@@ -380,12 +369,6 @@ export class MeasurementPointGizmo extends Group {
|
||||
|
||||
public updatePoint(position: Vector3) {
|
||||
this.point.position.copy(position)
|
||||
;(this.point.material as SpeckleBasicMaterial).userData.billboardPos.value.copy(
|
||||
this.point.position
|
||||
)
|
||||
;(
|
||||
(this.point.children[0] as Mesh).material as SpeckleBasicMaterial
|
||||
).userData.billboardPos.value.copy(this.point.position)
|
||||
}
|
||||
|
||||
public updateLine(points: Vector3[]) {
|
||||
@@ -425,33 +408,23 @@ export class MeasurementPointGizmo extends Group {
|
||||
quaternion?: Quaternion,
|
||||
scale?: Vector3
|
||||
): Promise<void> {
|
||||
return this.text
|
||||
.update({
|
||||
textValue: value,
|
||||
height: 1,
|
||||
anchorX: '50%',
|
||||
anchorY: '50%'
|
||||
})
|
||||
.then(() => {
|
||||
this.text.style = {
|
||||
backgroundColor: new Color(0x047efb),
|
||||
billboard: true,
|
||||
backgroundPixelHeight: 20
|
||||
}
|
||||
this.text.setTransform(position, quaternion, scale)
|
||||
if (this.text.backgroundMesh) this.text.backgroundMesh.renderOrder = 3
|
||||
this.text.textMesh.renderOrder = 4
|
||||
})
|
||||
.catch((reason) => {
|
||||
Logger.log(`Could not update text: ${reason}`)
|
||||
})
|
||||
const params = {
|
||||
text: value
|
||||
} as TextLabelParams
|
||||
|
||||
if (position) this.text.position.copy(position)
|
||||
if (quaternion) this.text.quaternion.copy(quaternion)
|
||||
if (scale) this.text.scale.copy(scale)
|
||||
this.text.updateMatrixWorld(true)
|
||||
|
||||
return this.text.updateParams(params)
|
||||
}
|
||||
|
||||
public updateStyle() {
|
||||
this.normalIndicator.material = this.getNormalIndicatorMaterial()
|
||||
this.line.material = this.getLineMaterial()
|
||||
this.point.material = this.getPointMaterial()
|
||||
this.text.textMesh.material = this.getTextMaterial()
|
||||
void this.text.updateParams({ textColor: new Color(this._style.textColor) })
|
||||
}
|
||||
|
||||
public raycast(raycaster: Raycaster, intersects: Array<Intersection>) {
|
||||
|
||||
@@ -2,9 +2,7 @@ import {
|
||||
Box3,
|
||||
Camera,
|
||||
Color,
|
||||
DoubleSide,
|
||||
Material,
|
||||
MathUtils,
|
||||
Matrix4,
|
||||
OrthographicCamera,
|
||||
PerspectiveCamera,
|
||||
@@ -18,8 +16,7 @@ import {
|
||||
import { getConversionFactor } from '../../converter/Units.js'
|
||||
import { Measurement, MeasurementState } from './Measurement.js'
|
||||
import { ObjectLayers } from '../../../IViewer.js'
|
||||
import { SpeckleText } from '../../objects/SpeckleText.js'
|
||||
import SpeckleTextMaterial from '../../materials/SpeckleTextMaterial.js'
|
||||
import { TextLabel } from '../../objects/TextLabel.js'
|
||||
import { MeasurementPointGizmo } from './MeasurementPointGizmo.js'
|
||||
|
||||
const _vec40 = new Vector4()
|
||||
@@ -31,14 +28,14 @@ const _mat41 = new Matrix4()
|
||||
|
||||
export class PointMeasurement extends Measurement {
|
||||
protected gizmo: MeasurementPointGizmo
|
||||
protected xLabel: SpeckleText
|
||||
protected yLabel: SpeckleText
|
||||
protected zLabel: SpeckleText
|
||||
protected xLabel: TextLabel
|
||||
protected yLabel: TextLabel
|
||||
protected zLabel: TextLabel
|
||||
protected xLabelPosition: Vector3 = new Vector3()
|
||||
protected yLabelPosition: Vector3 = new Vector3()
|
||||
protected zLabelPosition: Vector3 = new Vector3()
|
||||
protected readonly pixelsOffX = 50 * window.devicePixelRatio
|
||||
protected readonly pixelsOffY = 27 * window.devicePixelRatio
|
||||
protected readonly pixelsOffY = 25 * window.devicePixelRatio
|
||||
|
||||
public set isVisible(value: boolean) {
|
||||
this.gizmo.visible = value
|
||||
@@ -52,65 +49,55 @@ export class PointMeasurement extends Measurement {
|
||||
this.type = 'PointMeasurement'
|
||||
this.gizmo = new MeasurementPointGizmo()
|
||||
this.add(this.gizmo)
|
||||
this.xLabel = new SpeckleText(MathUtils.generateUUID(), ObjectLayers.MEASUREMENTS)
|
||||
const xLabelMaterial = new SpeckleTextMaterial(
|
||||
{
|
||||
color: 0xffffff,
|
||||
opacity: 1,
|
||||
side: DoubleSide
|
||||
},
|
||||
['USE_RTE', 'BILLBOARD_FIXED']
|
||||
)
|
||||
xLabelMaterial.toneMapped = false
|
||||
xLabelMaterial.color.convertSRGBToLinear()
|
||||
xLabelMaterial.opacity = 1
|
||||
xLabelMaterial.transparent = false
|
||||
xLabelMaterial.depthTest = false
|
||||
xLabelMaterial.billboardPixelHeight = 17 * window.devicePixelRatio
|
||||
xLabelMaterial.userData.billboardPos.value.copy(this.position)
|
||||
|
||||
this.xLabel.textMesh.material = xLabelMaterial.getDerivedMaterial()
|
||||
this.xLabel = new TextLabel({
|
||||
text: 'sample',
|
||||
textColor: new Color(0xffffff),
|
||||
fontSize: 11,
|
||||
billboard: 'screen',
|
||||
anchorX: 'left',
|
||||
anchorY: 'middle',
|
||||
backgroundColor: new Color(0xfb0404),
|
||||
backgroundMargins: new Vector2(30, 10),
|
||||
backgroundCornerRadius: 0.3,
|
||||
objectLayer: ObjectLayers.MEASUREMENTS
|
||||
})
|
||||
this.xLabel.name = 'XLabel'
|
||||
this.xLabel.material.depthTest = false
|
||||
this.add(this.xLabel)
|
||||
|
||||
this.yLabel = new SpeckleText(MathUtils.generateUUID(), ObjectLayers.MEASUREMENTS)
|
||||
const yLabelMaterial = new SpeckleTextMaterial(
|
||||
{
|
||||
color: 0xffffff,
|
||||
opacity: 1,
|
||||
side: DoubleSide
|
||||
},
|
||||
['USE_RTE', 'BILLBOARD_FIXED']
|
||||
)
|
||||
yLabelMaterial.toneMapped = false
|
||||
yLabelMaterial.color.convertSRGBToLinear()
|
||||
yLabelMaterial.opacity = 1
|
||||
yLabelMaterial.transparent = false
|
||||
yLabelMaterial.depthTest = false
|
||||
yLabelMaterial.billboardPixelHeight = 17 * window.devicePixelRatio
|
||||
yLabelMaterial.userData.billboardPos.value.copy(this.position)
|
||||
|
||||
this.yLabel.textMesh.material = yLabelMaterial.getDerivedMaterial()
|
||||
this.yLabel = new TextLabel({
|
||||
text: 'sample',
|
||||
textColor: new Color(0xffffff),
|
||||
fontSize: 11,
|
||||
anchorX: 'left',
|
||||
anchorY: 'middle',
|
||||
billboard: 'screen',
|
||||
backgroundColor: new Color(0x03c903),
|
||||
backgroundMargins: new Vector2(30, 10),
|
||||
backgroundCornerRadius: 0.3,
|
||||
objectLayer: ObjectLayers.MEASUREMENTS
|
||||
})
|
||||
this.yLabel.name = 'YLabel'
|
||||
this.yLabel.material.depthTest = false
|
||||
this.add(this.yLabel)
|
||||
|
||||
this.zLabel = new SpeckleText(MathUtils.generateUUID(), ObjectLayers.MEASUREMENTS)
|
||||
const zLabelMaterial = new SpeckleTextMaterial(
|
||||
{
|
||||
color: 0xffffff,
|
||||
opacity: 1,
|
||||
side: DoubleSide
|
||||
},
|
||||
['USE_RTE', 'BILLBOARD_FIXED']
|
||||
)
|
||||
zLabelMaterial.toneMapped = false
|
||||
zLabelMaterial.color.convertSRGBToLinear()
|
||||
zLabelMaterial.opacity = 1
|
||||
zLabelMaterial.transparent = false
|
||||
zLabelMaterial.depthTest = false
|
||||
zLabelMaterial.billboardPixelHeight = 17 * window.devicePixelRatio
|
||||
zLabelMaterial.userData.billboardPos.value.copy(this.position)
|
||||
|
||||
this.zLabel.textMesh.material = zLabelMaterial.getDerivedMaterial()
|
||||
this.zLabel = new TextLabel({
|
||||
text: 'sample',
|
||||
textColor: new Color(0xffffff),
|
||||
fontSize: 11,
|
||||
billboard: 'screen',
|
||||
anchorX: 'left',
|
||||
anchorY: 'middle',
|
||||
backgroundColor: new Color(0x047efb),
|
||||
backgroundMargins: new Vector2(30, 10),
|
||||
backgroundCornerRadius: 0.3,
|
||||
objectLayer: ObjectLayers.MEASUREMENTS
|
||||
})
|
||||
this.zLabel.name = 'ZLabel'
|
||||
this.zLabel.material.depthTest = false
|
||||
this.add(this.zLabel)
|
||||
|
||||
this.layers.set(ObjectLayers.MEASUREMENTS)
|
||||
}
|
||||
|
||||
@@ -118,9 +105,9 @@ export class PointMeasurement extends Measurement {
|
||||
super.frameUpdate(camera, size, bounds)
|
||||
|
||||
this.updateLabelPositions()
|
||||
this.xLabel.setTransform(this.xLabelPosition)
|
||||
this.yLabel.setTransform(this.yLabelPosition)
|
||||
this.zLabel.setTransform(this.zLabelPosition)
|
||||
this.xLabel.position.copy(this.xLabelPosition)
|
||||
this.yLabel.position.copy(this.yLabelPosition)
|
||||
this.zLabel.position.copy(this.zLabelPosition)
|
||||
this.gizmo.frameUpdate(camera, size)
|
||||
}
|
||||
|
||||
@@ -171,64 +158,26 @@ export class PointMeasurement extends Measurement {
|
||||
}
|
||||
|
||||
public async update(): Promise<void> {
|
||||
const xP = this.xLabel
|
||||
.update({
|
||||
textValue: `x : ${(
|
||||
this.startPoint.x * getConversionFactor('m', this.units)
|
||||
).toFixed(this.precision)} ${this.units}`,
|
||||
height: 1,
|
||||
anchorX: '0%',
|
||||
anchorY: '50%'
|
||||
})
|
||||
.then(() => {
|
||||
this.xLabel.style = {
|
||||
backgroundColor: new Color(0xfb0404),
|
||||
billboard: true,
|
||||
backgroundPixelHeight: 20
|
||||
}
|
||||
this.xLabel.setTransform(this.xLabelPosition)
|
||||
if (this.xLabel.backgroundMesh) this.xLabel.backgroundMesh.renderOrder = 3
|
||||
this.xLabel.textMesh.renderOrder = 4
|
||||
})
|
||||
const yP = this.yLabel
|
||||
.update({
|
||||
textValue: `y : ${(
|
||||
this.startPoint.y * getConversionFactor('m', this.units)
|
||||
).toFixed(this.precision)} ${this.units}`,
|
||||
height: 1,
|
||||
anchorX: '0%',
|
||||
anchorY: '50%'
|
||||
})
|
||||
.then(() => {
|
||||
this.yLabel.style = {
|
||||
backgroundColor: new Color(0x03c903),
|
||||
billboard: true,
|
||||
backgroundPixelHeight: 20
|
||||
}
|
||||
this.yLabel.setTransform(this.yLabelPosition)
|
||||
if (this.yLabel.backgroundMesh) this.yLabel.backgroundMesh.renderOrder = 3
|
||||
this.yLabel.textMesh.renderOrder = 4
|
||||
})
|
||||
this.xLabel.position.copy(this.xLabelPosition)
|
||||
this.yLabel.position.copy(this.yLabelPosition)
|
||||
this.zLabel.position.copy(this.zLabelPosition)
|
||||
const xP = this.xLabel.updateParams({
|
||||
text: `X : ${(this.startPoint.x * getConversionFactor('m', this.units)).toFixed(
|
||||
this.precision
|
||||
)} ${this.units}`
|
||||
})
|
||||
|
||||
const zP = this.zLabel
|
||||
.update({
|
||||
textValue: `z : ${(
|
||||
this.startPoint.z * getConversionFactor('m', this.units)
|
||||
).toFixed(this.precision)} ${this.units}`,
|
||||
height: 1,
|
||||
anchorX: '0%',
|
||||
anchorY: '50%'
|
||||
})
|
||||
.then(() => {
|
||||
this.zLabel.style = {
|
||||
backgroundColor: new Color(0x047efb),
|
||||
billboard: true,
|
||||
backgroundPixelHeight: 20
|
||||
}
|
||||
this.zLabel.setTransform(this.zLabelPosition)
|
||||
if (this.zLabel.backgroundMesh) this.zLabel.backgroundMesh.renderOrder = 3
|
||||
this.zLabel.textMesh.renderOrder = 4
|
||||
})
|
||||
const yP = this.yLabel.updateParams({
|
||||
text: `Y : ${(this.startPoint.y * getConversionFactor('m', this.units)).toFixed(
|
||||
this.precision
|
||||
)} ${this.units}`
|
||||
})
|
||||
|
||||
const zP = this.zLabel.updateParams({
|
||||
text: `Z : ${(this.startPoint.z * getConversionFactor('m', this.units)).toFixed(
|
||||
this.precision
|
||||
)} ${this.units}`
|
||||
})
|
||||
|
||||
this.gizmo.updateNormalIndicator(this.startPoint, this.startNormal)
|
||||
this.gizmo.updatePoint(this.startPoint)
|
||||
|
||||
@@ -336,20 +336,23 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
*/
|
||||
protected TextToGeometryData(node: NodeData): GeometryData | null {
|
||||
const conversionFactor = getConversionFactor(node.raw.units)
|
||||
/** TEMPORARY UNTIL PROPER IMPLEMENTATION FOR TEXT V3 */
|
||||
const plane = node.raw.plane || {
|
||||
origin: node.raw.origin,
|
||||
xdir: new Vector3(1, 0, 0),
|
||||
ydir: new Vector3(0, 1, 0),
|
||||
normal: new Vector3(0, 0, 1)
|
||||
}
|
||||
const billboard = node.raw.screenOriented || false
|
||||
const position = new Vector3(plane.origin.x, plane.origin.y, plane.origin.z)
|
||||
const scale = new Matrix4().makeScale(
|
||||
conversionFactor,
|
||||
conversionFactor,
|
||||
conversionFactor
|
||||
)
|
||||
const mat = new Matrix4().makeBasis(plane.xdir, plane.ydir, plane.normal)
|
||||
/** We ignore rotation if screen oriented */
|
||||
const mat = billboard
|
||||
? new Matrix4()
|
||||
: new Matrix4().makeBasis(plane.xdir, plane.ydir, plane.normal)
|
||||
mat.setPosition(position)
|
||||
mat.premultiply(scale)
|
||||
return {
|
||||
|
||||
@@ -14,6 +14,7 @@ import SpeckleTextMaterial from './SpeckleTextMaterial.js'
|
||||
import { SpeckleMaterial } from './SpeckleMaterial.js'
|
||||
import SpecklePointColouredMaterial from './SpecklePointColouredMaterial.js'
|
||||
import { type Asset, AssetType, type MaterialOptions } from '../../IViewer.js'
|
||||
import SpeckleTextColoredMaterial from './SpeckleTextColoredMaterial.js'
|
||||
|
||||
const defaultGradient: Asset = {
|
||||
id: 'defaultGradient',
|
||||
@@ -83,6 +84,7 @@ export default class Materials {
|
||||
|
||||
private textGhostMaterial: Material
|
||||
private textColoredMaterial: Material
|
||||
private textGradientMaterial: Material
|
||||
private textHiddenMaterial: Material
|
||||
|
||||
private defaultGradientTextureData!: ImageData
|
||||
@@ -314,7 +316,12 @@ export default class Materials {
|
||||
renderView.geometryType.toString() +
|
||||
geometry +
|
||||
mat +
|
||||
(renderView.geometryType === GeometryType.TEXT ? renderView.renderData.id : '') +
|
||||
(renderView.geometryType === GeometryType.TEXT &&
|
||||
renderView.renderData.geometry.metaData?.screenOriented !== undefined
|
||||
? (
|
||||
renderView.renderData.geometry.metaData?.screenOriented as boolean
|
||||
).toString()
|
||||
: '') +
|
||||
(renderView.renderData.geometry.instanced ? 'instanced' : '')
|
||||
return Materials.hashCode(s)
|
||||
}
|
||||
@@ -490,9 +497,9 @@ export default class Materials {
|
||||
|
||||
this.textGhostMaterial = (
|
||||
this.textGhostMaterial as SpeckleTextMaterial
|
||||
).getDerivedMaterial()
|
||||
).getDerivedBatchedMaterial()
|
||||
|
||||
this.textColoredMaterial = new SpeckleTextMaterial({
|
||||
this.textColoredMaterial = new SpeckleTextColoredMaterial({
|
||||
color: 0xffffff,
|
||||
opacity: 1,
|
||||
side: DoubleSide
|
||||
@@ -503,11 +510,35 @@ export default class Materials {
|
||||
? false
|
||||
: true
|
||||
this.textColoredMaterial.toneMapped = false
|
||||
;(this.textColoredMaterial as SpeckleTextMaterial).color.convertSRGBToLinear()
|
||||
;(
|
||||
this.textColoredMaterial as SpeckleTextColoredMaterial
|
||||
).color.convertSRGBToLinear()
|
||||
|
||||
this.textColoredMaterial = (
|
||||
this.textColoredMaterial as SpeckleTextMaterial
|
||||
).getDerivedMaterial()
|
||||
this.textColoredMaterial as SpeckleTextColoredMaterial
|
||||
).getDerivedBatchedMaterial()
|
||||
|
||||
this.textGradientMaterial = new SpeckleTextColoredMaterial({
|
||||
color: 0xffffff,
|
||||
opacity: 1,
|
||||
side: DoubleSide
|
||||
})
|
||||
this.textGradientMaterial.transparent =
|
||||
this.textGradientMaterial.opacity < 1 ? true : false
|
||||
this.textGradientMaterial.depthWrite = this.textGradientMaterial.transparent
|
||||
? false
|
||||
: true
|
||||
this.textGradientMaterial.toneMapped = false
|
||||
;(
|
||||
this.textGradientMaterial as SpeckleTextColoredMaterial
|
||||
).color.convertSRGBToLinear()
|
||||
;(this.textGradientMaterial as SpeckleTextColoredMaterial).setGradientTexture(
|
||||
await Assets.getTexture(defaultGradient)
|
||||
)
|
||||
|
||||
this.textGradientMaterial = (
|
||||
this.textGradientMaterial as SpeckleTextColoredMaterial
|
||||
).getDerivedBatchedMaterial()
|
||||
|
||||
this.textHiddenMaterial = new SpeckleTextMaterial({
|
||||
color: 0xffffff,
|
||||
@@ -520,7 +551,7 @@ export default class Materials {
|
||||
|
||||
this.textHiddenMaterial = (
|
||||
this.textHiddenMaterial as SpeckleTextMaterial
|
||||
).getDerivedMaterial()
|
||||
).getDerivedBatchedMaterial()
|
||||
}
|
||||
|
||||
private async createDefaultNullMaterials() {
|
||||
@@ -796,7 +827,7 @@ export default class Materials {
|
||||
if (!this.materialMap[hash]) {
|
||||
this.materialMap[hash] = this.makeTextMaterial(material as DisplayStyle)
|
||||
}
|
||||
return (this.materialMap[hash] as SpeckleTextMaterial).getDerivedMaterial()
|
||||
return (this.materialMap[hash] as SpeckleTextMaterial).getDerivedBatchedMaterial()
|
||||
}
|
||||
|
||||
public getGhostMaterial(
|
||||
@@ -852,7 +883,12 @@ export default class Materials {
|
||||
return material
|
||||
}
|
||||
case GeometryType.TEXT:
|
||||
return this.textColoredMaterial
|
||||
const material = this.textGradientMaterial
|
||||
if (filterMaterial?.rampTexture)
|
||||
(material as SpeckleStandardColoredMaterial).setGradientTexture(
|
||||
filterMaterial.rampTexture
|
||||
)
|
||||
return material
|
||||
}
|
||||
}
|
||||
|
||||
@@ -890,7 +926,12 @@ export default class Materials {
|
||||
return material
|
||||
}
|
||||
case GeometryType.TEXT:
|
||||
return this.textColoredMaterial
|
||||
const material = this.textColoredMaterial
|
||||
if (filterMaterial?.rampTexture)
|
||||
(material as SpeckleStandardColoredMaterial).setGradientTexture(
|
||||
filterMaterial.rampTexture
|
||||
)
|
||||
return material
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,19 +11,24 @@ import {
|
||||
Scene,
|
||||
Camera,
|
||||
BufferGeometry,
|
||||
Object3D
|
||||
Object3D,
|
||||
Vector4
|
||||
} from 'three'
|
||||
import { Matrix4 } from 'three'
|
||||
|
||||
import { ExtendedMeshBasicMaterial, type Uniforms } from './SpeckleMaterial.js'
|
||||
import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer.js'
|
||||
|
||||
class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial {
|
||||
protected static readonly matBuff: Matrix4 = new Matrix4()
|
||||
protected static readonly vecBuff: Vector2 = new Vector2()
|
||||
const matBuff: Matrix4 = new Matrix4()
|
||||
const vec2Buff0: Vector2 = new Vector2()
|
||||
const vec2Buff1: Vector2 = new Vector2()
|
||||
const vec2Buff2: Vector2 = new Vector2()
|
||||
|
||||
private _billboardPixelHeight: number
|
||||
private _billboardOffset: Vector2 = new Vector2()
|
||||
export type BillboardingType = 'world' | 'screen'
|
||||
|
||||
class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial {
|
||||
protected _billboardPixelSize: Vector2 = new Vector2()
|
||||
protected _billboardPixelOffset: Vector2 = new Vector2()
|
||||
|
||||
protected get vertexProgram(): string {
|
||||
return speckleBasicVert
|
||||
@@ -43,20 +48,26 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial {
|
||||
uViewer_low: new Vector3(),
|
||||
uTransforms: [new Matrix4()],
|
||||
tTransforms: null,
|
||||
billboardPos: new Vector3(),
|
||||
billboardSize: new Vector2(),
|
||||
billboardOffset: new Vector2(),
|
||||
objCount: 1,
|
||||
invProjection: new Matrix4(),
|
||||
objCount: 1
|
||||
billboardPixelOffsetSize: new Vector4()
|
||||
}
|
||||
}
|
||||
|
||||
public set billboardPixelHeight(value: number) {
|
||||
this._billboardPixelHeight = value
|
||||
public get billboardPixelSize(): Vector2 {
|
||||
return this._billboardPixelSize
|
||||
}
|
||||
|
||||
public set billboardOffset(value: Vector2) {
|
||||
this._billboardOffset.copy(value)
|
||||
public set billboardPixelSize(value: Vector2) {
|
||||
this._billboardPixelSize.copy(value)
|
||||
}
|
||||
|
||||
public get billboardPixeOffset(): Vector2 {
|
||||
return this._billboardPixelOffset
|
||||
}
|
||||
|
||||
public set billboardPixelOffset(value: Vector2) {
|
||||
this._billboardPixelOffset.copy(value)
|
||||
}
|
||||
|
||||
constructor(parameters: MeshBasicMaterialParameters, defines: string[] = []) {
|
||||
@@ -80,8 +91,22 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial {
|
||||
const toStandard = to as SpeckleBasicMaterial
|
||||
const fromStandard = from as SpeckleBasicMaterial
|
||||
toStandard.color.copy(fromStandard.color)
|
||||
toStandard.refractionRatio = fromStandard.refractionRatio
|
||||
to.userData.billboardPos.value.copy(from.userData.billboardPos.value)
|
||||
to.userData.billboardPixelOffsetSize.value.copy(
|
||||
from.userData.billboardPixelOffsetSize.value
|
||||
)
|
||||
}
|
||||
|
||||
public setBillboarding(type: BillboardingType | null) {
|
||||
/** Create the define object if not there */
|
||||
if (!this.defines) this.defines = {}
|
||||
/** Clear all billboarding defines */
|
||||
delete this.defines['BILLBOARD_SCREEN']
|
||||
delete this.defines['BILLBOARD']
|
||||
|
||||
if (!type) return
|
||||
|
||||
if (type === 'world') this.defines['BILLBOARD'] = ' '
|
||||
if (type === 'screen') this.defines['BILLBOARD_SCREEN'] = ' '
|
||||
}
|
||||
|
||||
/** Called by three.js render loop */
|
||||
@@ -92,18 +117,33 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial {
|
||||
_geometry: BufferGeometry,
|
||||
object: Object3D
|
||||
) {
|
||||
if (this.defines && this.defines['BILLBOARD_FIXED']) {
|
||||
const resolution = _this.getDrawingBufferSize(SpeckleBasicMaterial.vecBuff)
|
||||
SpeckleBasicMaterial.vecBuff.set(
|
||||
(this._billboardPixelHeight / resolution.x) * 2,
|
||||
(this._billboardPixelHeight / resolution.y) * 2
|
||||
if (
|
||||
this.defines &&
|
||||
(this.defines['BILLBOARD'] || this.defines['BILLBOARD_SCREEN'])
|
||||
) {
|
||||
matBuff.copy(camera.projectionMatrix).invert()
|
||||
this.userData.invProjection.value.copy(matBuff)
|
||||
this.needsUpdate = true
|
||||
}
|
||||
|
||||
if (this.defines && this.defines['BILLBOARD_SCREEN']) {
|
||||
_this.getDrawingBufferSize(vec2Buff0)
|
||||
const billboardPixelOffsetNDC = vec2Buff1.set(
|
||||
this._billboardPixelOffset.x,
|
||||
this._billboardPixelOffset.y
|
||||
)
|
||||
const billboardPixelSizeNDC = vec2Buff2.set(
|
||||
this._billboardPixelSize.x,
|
||||
this._billboardPixelSize.y
|
||||
)
|
||||
billboardPixelOffsetNDC.divide(vec2Buff0)
|
||||
billboardPixelSizeNDC.divide(vec2Buff0)
|
||||
this.userData.billboardPixelOffsetSize.value.set(
|
||||
billboardPixelOffsetNDC.x,
|
||||
billboardPixelOffsetNDC.y,
|
||||
billboardPixelSizeNDC.x,
|
||||
billboardPixelSizeNDC.y
|
||||
)
|
||||
this.userData.billboardSize.value.copy(SpeckleBasicMaterial.vecBuff)
|
||||
this.userData.billboardOffset.value.copy(this._billboardOffset)
|
||||
SpeckleBasicMaterial.matBuff.copy(camera.projectionMatrix).invert()
|
||||
this.userData.invProjection.value.copy(SpeckleBasicMaterial.matBuff)
|
||||
/** TO DO: Revisit and Enable this */
|
||||
// this.userData.billboardPos.value.copy(object.position)
|
||||
this.needsUpdate = true
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import SpeckleTextMaterial from './SpeckleTextMaterial.js'
|
||||
|
||||
class SpeckleTextColoredMaterial extends SpeckleTextMaterial {
|
||||
public gradientIndexMap: { [index: number]: number } = {}
|
||||
|
||||
public updateGradientIndexMap(index: number, value: number) {
|
||||
this.gradientIndexMap[index] = value
|
||||
}
|
||||
}
|
||||
|
||||
export default SpeckleTextColoredMaterial
|
||||
@@ -1,32 +1,22 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable camelcase */
|
||||
import { speckleTextVert } from './shaders/speckle-text-vert.js'
|
||||
import { speckleTextFrag } from './shaders/speckle-text-frag.js'
|
||||
import {
|
||||
ShaderLib,
|
||||
Vector3,
|
||||
type IUniform,
|
||||
Vector2,
|
||||
Material,
|
||||
type MeshBasicMaterialParameters,
|
||||
Scene,
|
||||
Camera,
|
||||
BufferGeometry,
|
||||
Object3D
|
||||
} from 'three'
|
||||
import { Matrix4 } from 'three'
|
||||
import { Vector2, Material, Texture, NearestFilter } from 'three'
|
||||
|
||||
import { ExtendedMeshBasicMaterial, type Uniforms } from './SpeckleMaterial.js'
|
||||
import { type Uniforms } from './SpeckleMaterial.js'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
import { createDerivedMaterial } from 'troika-three-utils'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
import { createTextDerivedMaterial } from 'troika-three-text'
|
||||
import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer.js'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
import { uniformToVarying } from 'troika-three-text/src/BatchedText.js'
|
||||
import SpeckleBasicMaterial from './SpeckleBasicMaterial.js'
|
||||
|
||||
class SpeckleTextMaterial extends ExtendedMeshBasicMaterial {
|
||||
protected static readonly matBuff: Matrix4 = new Matrix4()
|
||||
protected static readonly vecBuff: Vector2 = new Vector2()
|
||||
|
||||
private _billboardPixelHeight: number
|
||||
class SpeckleTextMaterial extends SpeckleBasicMaterial {
|
||||
public setMatrixTexture: (texture: Texture) => void
|
||||
|
||||
protected get vertexProgram(): string {
|
||||
return speckleTextVert
|
||||
@@ -36,34 +26,8 @@ class SpeckleTextMaterial extends ExtendedMeshBasicMaterial {
|
||||
return speckleTextFrag
|
||||
}
|
||||
|
||||
protected get baseUniforms(): { [uniform: string]: IUniform } {
|
||||
return ShaderLib.basic.uniforms
|
||||
}
|
||||
|
||||
protected get uniformsDef(): Uniforms {
|
||||
return {
|
||||
uViewer_high: new Vector3(),
|
||||
uViewer_low: new Vector3(),
|
||||
uTransforms: [new Matrix4()],
|
||||
tTransforms: null,
|
||||
objCount: 1,
|
||||
billboardPos: new Vector3(),
|
||||
billboardSize: new Vector2(),
|
||||
invProjection: new Matrix4()
|
||||
}
|
||||
}
|
||||
|
||||
public set billboardPixelHeight(value: number) {
|
||||
this._billboardPixelHeight = value
|
||||
}
|
||||
|
||||
public get billboardPixelHeight() {
|
||||
return this._billboardPixelHeight
|
||||
}
|
||||
|
||||
constructor(parameters: MeshBasicMaterialParameters, defines: Array<string> = []) {
|
||||
super(parameters)
|
||||
this.init(defines)
|
||||
return { ...super.uniformsDef, gradientRamp: null }
|
||||
}
|
||||
|
||||
/** We need a unique key per program */
|
||||
@@ -71,55 +35,459 @@ class SpeckleTextMaterial extends ExtendedMeshBasicMaterial {
|
||||
return this.constructor.name
|
||||
}
|
||||
|
||||
public copy(source: Material) {
|
||||
super.copy(source)
|
||||
this.copyFrom(source)
|
||||
return this
|
||||
}
|
||||
|
||||
public getDerivedMaterial() {
|
||||
const derived = createTextDerivedMaterial(this)
|
||||
protected copyCustomUniforms(material: Material) {
|
||||
/** We rebind the uniforms */
|
||||
for (const k in this.userData) {
|
||||
derived.uniforms[k] = this.userData[k]
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
material.uniforms[k] = this.userData[k]
|
||||
}
|
||||
|
||||
}
|
||||
public getDerivedMaterial() {
|
||||
const derived = createTextDerivedMaterial(this)
|
||||
this.copyCustomUniforms(derived)
|
||||
return derived
|
||||
}
|
||||
|
||||
/*
|
||||
Data texture packing strategy:
|
||||
|
||||
# Common:
|
||||
0-15: matrix
|
||||
16-19: uTroikaTotalBounds
|
||||
20-23: uTroikaClipRect
|
||||
24: diffuse (color/outlineColor)
|
||||
25: uTroikaFillOpacity (fillOpacity/outlineOpacity)
|
||||
26: uTroikaCurveRadius
|
||||
27: <blank>
|
||||
|
||||
# Main:
|
||||
28: uTroikaStrokeWidth
|
||||
29: uTroikaStrokeColor
|
||||
30: uTroikaStrokeOpacity
|
||||
|
||||
# Outline:
|
||||
28-29: uTroikaPositionOffset
|
||||
30: uTroikaEdgeOffset
|
||||
31: uTroikaBlurRadius
|
||||
*/
|
||||
/** Sadly, troika does not export this for no good reason so we neee to copy it over */
|
||||
public getDerivedBatchedMaterial() {
|
||||
const texUniformName = 'uTroikaMatricesTexture'
|
||||
const texSizeUniformName = 'uTroikaMatricesTextureSize'
|
||||
const memberIndexAttrName = 'aTroikaTextBatchMemberIndex'
|
||||
const floatsPerMember = 32
|
||||
// Due to how vertexTransform gets injected, the matrix transforms must happen
|
||||
// in the base material of TextDerivedMaterial, but other transforms to its
|
||||
// shader must come after, so we sandwich it between two derivations.
|
||||
|
||||
// Transform the vertex position
|
||||
let batchMaterial = createDerivedMaterial(this, {
|
||||
chained: true,
|
||||
uniforms: {
|
||||
[texSizeUniformName]: { value: new Vector2() },
|
||||
[texUniformName]: { value: null }
|
||||
},
|
||||
// language=GLSL
|
||||
vertexDefs: `
|
||||
uniform highp sampler2D ${texUniformName};
|
||||
uniform vec2 ${texSizeUniformName};
|
||||
attribute float ${memberIndexAttrName};
|
||||
|
||||
vec4 troikaBatchTexel(float offset) {
|
||||
offset += ${memberIndexAttrName} * ${floatsPerMember.toFixed(1)} / 4.0;
|
||||
float w = ${texSizeUniformName}.x;
|
||||
vec2 uv = (vec2(mod(offset, w), floor(offset / w)) + 0.5) / ${texSizeUniformName};
|
||||
return texture2D(${texUniformName}, uv);
|
||||
}
|
||||
`,
|
||||
// language=GLSL prefix="void main() {" suffix="}"
|
||||
vertexTransform: `
|
||||
/** We don't need this. We're transforming ourselves in our shader to allow for RTE*/
|
||||
// mat4 matrix = mat4(
|
||||
// troikaBatchTexel(0.0),
|
||||
// troikaBatchTexel(1.0),
|
||||
// troikaBatchTexel(2.0),
|
||||
// troikaBatchTexel(3.0)
|
||||
// );
|
||||
// position.xyz = (matrix * vec4(position, 1.0)).xyz;
|
||||
`
|
||||
})
|
||||
|
||||
// Add the text shaders
|
||||
batchMaterial = createTextDerivedMaterial(batchMaterial)
|
||||
|
||||
// Now make other changes to the derived text shader code
|
||||
batchMaterial = createDerivedMaterial(batchMaterial, {
|
||||
chained: true,
|
||||
uniforms: {
|
||||
uTroikaIsOutline: { value: false }
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
customRewriter(shaders) {
|
||||
// Convert some text shader uniforms to varyings
|
||||
const varyingUniforms = [
|
||||
'uTroikaTotalBounds',
|
||||
'uTroikaClipRect',
|
||||
'uTroikaPositionOffset',
|
||||
'uTroikaEdgeOffset',
|
||||
'uTroikaBlurRadius',
|
||||
'uTroikaStrokeWidth',
|
||||
'uTroikaStrokeColor',
|
||||
'uTroikaStrokeOpacity',
|
||||
'uTroikaFillOpacity',
|
||||
'uTroikaCurveRadius',
|
||||
'diffuse'
|
||||
]
|
||||
varyingUniforms.forEach((uniformName) => {
|
||||
shaders = uniformToVarying(shaders, uniformName)
|
||||
})
|
||||
return shaders
|
||||
},
|
||||
// language=GLSL
|
||||
vertexDefs: `
|
||||
uniform bool uTroikaIsOutline;
|
||||
vec3 troikaFloatToColor(float v) {
|
||||
return mod(floor(vec3(v / 65536.0, v / 256.0, v)), 256.0) / 256.0;
|
||||
}
|
||||
`,
|
||||
// language=GLSL prefix="void main() {" suffix="}"
|
||||
vertexTransform: `
|
||||
uTroikaTotalBounds = troikaBatchTexel(4.0);
|
||||
uTroikaClipRect = troikaBatchTexel(5.0);
|
||||
|
||||
vec4 data = troikaBatchTexel(6.0);
|
||||
diffuse = troikaFloatToColor(data.x);
|
||||
uTroikaFillOpacity = data.y;
|
||||
uTroikaCurveRadius = data.z;
|
||||
|
||||
data = troikaBatchTexel(7.0);
|
||||
if (uTroikaIsOutline) {
|
||||
if (data == vec4(0.0)) { // degenerate if zero outline
|
||||
position = vec3(0.0);
|
||||
} else {
|
||||
uTroikaPositionOffset = data.xy;
|
||||
uTroikaEdgeOffset = data.z;
|
||||
uTroikaBlurRadius = data.w;
|
||||
}
|
||||
} else {
|
||||
uTroikaStrokeWidth = data.x;
|
||||
uTroikaStrokeColor = troikaFloatToColor(data.y);
|
||||
uTroikaStrokeOpacity = data.z;
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
batchMaterial.setMatrixTexture = (texture: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
image: { width: any; height: any }
|
||||
}) => {
|
||||
batchMaterial.uniforms[texUniformName].value = texture
|
||||
batchMaterial.uniforms[texSizeUniformName].value.set(
|
||||
texture.image.width,
|
||||
texture.image.height
|
||||
)
|
||||
}
|
||||
this.copyCustomUniforms(batchMaterial)
|
||||
;(batchMaterial.defines ??= {})['BATCHED_TEXT'] = ' '
|
||||
return batchMaterial
|
||||
}
|
||||
|
||||
public fastCopy(from: Material, to: Material) {
|
||||
super.fastCopy(from, to)
|
||||
const toStandard = to as SpeckleTextMaterial
|
||||
const fromStandard = from as SpeckleTextMaterial
|
||||
toStandard.color.copy(fromStandard.color)
|
||||
toStandard.refractionRatio = fromStandard.refractionRatio
|
||||
to.userData.billboardPos.value.copy(from.userData.billboardPos.value)
|
||||
to.userData.gradientRamp.value = from.userData.gradientRamp.value
|
||||
}
|
||||
|
||||
/** Called by three.js render loop */
|
||||
public onBeforeRender(
|
||||
_this: SpeckleWebGLRenderer,
|
||||
_scene: Scene,
|
||||
camera: Camera,
|
||||
_geometry: BufferGeometry,
|
||||
_object: Object3D
|
||||
) {
|
||||
if (this.defines && this.defines['BILLBOARD_FIXED']) {
|
||||
const resolution = _this.getDrawingBufferSize(SpeckleTextMaterial.vecBuff)
|
||||
SpeckleTextMaterial.vecBuff.set(
|
||||
(this._billboardPixelHeight / resolution.x) * 2,
|
||||
(this._billboardPixelHeight / resolution.y) * 2
|
||||
)
|
||||
this.userData.billboardSize.value.copy(SpeckleTextMaterial.vecBuff)
|
||||
SpeckleTextMaterial.matBuff.copy(camera.projectionMatrix).invert()
|
||||
this.userData.invProjection.value.copy(SpeckleTextMaterial.matBuff)
|
||||
}
|
||||
/** TO ENABLE */
|
||||
// object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix)
|
||||
// this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow)
|
||||
// this.userData.uViewer_high.value.copy(_this.RTEBuffers.viewerHigh)
|
||||
public setGradientTexture(texture: Texture) {
|
||||
this.userData.gradientRamp.value = texture
|
||||
this.userData.gradientRamp.value.generateMipmaps = false
|
||||
this.userData.gradientRamp.value.minFilter = NearestFilter
|
||||
this.userData.gradientRamp.value.magFilter = NearestFilter
|
||||
this.needsUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
export default SpeckleTextMaterial
|
||||
|
||||
// const matBuff: Matrix4 = new Matrix4()
|
||||
// const vec2Buff: Vector2 = new Vector2()
|
||||
|
||||
// export interface SpeckleTextMaterialParameters extends MeshBasicMaterialParameters {
|
||||
// billboardPixelHeight?: number
|
||||
// }
|
||||
|
||||
// export type BillboardingType = 'world' | 'screen'
|
||||
|
||||
// class SpeckleTextMaterial extends ExtendedMeshBasicMaterial {
|
||||
// private _billboardPixelHeight: number
|
||||
|
||||
// protected get vertexProgram(): string {
|
||||
// return speckleTextVert
|
||||
// }
|
||||
|
||||
// protected get fragmentProgram(): string {
|
||||
// return speckleTextFrag
|
||||
// }
|
||||
|
||||
// protected get baseUniforms(): { [uniform: string]: IUniform } {
|
||||
// return ShaderLib.basic.uniforms
|
||||
// }
|
||||
|
||||
// protected get uniformsDef(): Uniforms {
|
||||
// return {
|
||||
// uViewer_high: new Vector3(),
|
||||
// uViewer_low: new Vector3(),
|
||||
// invProjection: new Matrix4(),
|
||||
// billboardPixelHeight: 0,
|
||||
// screenSize: new Vector2(),
|
||||
// gradientRamp: null
|
||||
// }
|
||||
// }
|
||||
|
||||
// public get billboardPixelHeight() {
|
||||
// return this._billboardPixelHeight
|
||||
// }
|
||||
|
||||
// public set billboardPixelHeight(value: number) {
|
||||
// this._billboardPixelHeight = value
|
||||
// }
|
||||
|
||||
// constructor(parameters: SpeckleTextMaterialParameters, defines: Array<string> = []) {
|
||||
// super(parameters)
|
||||
// this.init(defines)
|
||||
// }
|
||||
|
||||
// /** We need a unique key per program */
|
||||
// public customProgramCacheKey() {
|
||||
// return this.constructor.name
|
||||
// }
|
||||
|
||||
// public copy(source: Material) {
|
||||
// super.copy(source)
|
||||
// this.copyFrom(source)
|
||||
// return this
|
||||
// }
|
||||
|
||||
// protected copyCustomUniforms(material: Material) {
|
||||
// /** We rebind the uniforms */
|
||||
// for (const k in this.userData) {
|
||||
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// //@ts-ignore
|
||||
// material.uniforms[k] = this.userData[k]
|
||||
// }
|
||||
// }
|
||||
// public getDerivedMaterial() {
|
||||
// const derived = createTextDerivedMaterial(this)
|
||||
// this.copyCustomUniforms(derived)
|
||||
// return derived
|
||||
// }
|
||||
|
||||
// /*
|
||||
// Data texture packing strategy:
|
||||
|
||||
// # Common:
|
||||
// 0-15: matrix
|
||||
// 16-19: uTroikaTotalBounds
|
||||
// 20-23: uTroikaClipRect
|
||||
// 24: diffuse (color/outlineColor)
|
||||
// 25: uTroikaFillOpacity (fillOpacity/outlineOpacity)
|
||||
// 26: uTroikaCurveRadius
|
||||
// 27: <blank>
|
||||
|
||||
// # Main:
|
||||
// 28: uTroikaStrokeWidth
|
||||
// 29: uTroikaStrokeColor
|
||||
// 30: uTroikaStrokeOpacity
|
||||
|
||||
// # Outline:
|
||||
// 28-29: uTroikaPositionOffset
|
||||
// 30: uTroikaEdgeOffset
|
||||
// 31: uTroikaBlurRadius
|
||||
// */
|
||||
// /** Sadly, troika does not export this for no good reason so we neee to copy it over */
|
||||
// public getDerivedBatchedMaterial() {
|
||||
// const texUniformName = 'uTroikaMatricesTexture'
|
||||
// const texSizeUniformName = 'uTroikaMatricesTextureSize'
|
||||
// const memberIndexAttrName = 'aTroikaTextBatchMemberIndex'
|
||||
// const floatsPerMember = 32
|
||||
// // Due to how vertexTransform gets injected, the matrix transforms must happen
|
||||
// // in the base material of TextDerivedMaterial, but other transforms to its
|
||||
// // shader must come after, so we sandwich it between two derivations.
|
||||
|
||||
// // Transform the vertex position
|
||||
// let batchMaterial = createDerivedMaterial(this, {
|
||||
// chained: true,
|
||||
// uniforms: {
|
||||
// [texSizeUniformName]: { value: new Vector2() },
|
||||
// [texUniformName]: { value: null }
|
||||
// },
|
||||
// // language=GLSL
|
||||
// vertexDefs: `
|
||||
// uniform highp sampler2D ${texUniformName};
|
||||
// uniform vec2 ${texSizeUniformName};
|
||||
// attribute float ${memberIndexAttrName};
|
||||
|
||||
// vec4 troikaBatchTexel(float offset) {
|
||||
// offset += ${memberIndexAttrName} * ${floatsPerMember.toFixed(1)} / 4.0;
|
||||
// float w = ${texSizeUniformName}.x;
|
||||
// vec2 uv = (vec2(mod(offset, w), floor(offset / w)) + 0.5) / ${texSizeUniformName};
|
||||
// return texture2D(${texUniformName}, uv);
|
||||
// }
|
||||
// `,
|
||||
// // language=GLSL prefix="void main() {" suffix="}"
|
||||
// vertexTransform: `
|
||||
// /** We don't need this. We're transforming ourselves in our shader to allow for RTE*/
|
||||
// // mat4 matrix = mat4(
|
||||
// // troikaBatchTexel(0.0),
|
||||
// // troikaBatchTexel(1.0),
|
||||
// // troikaBatchTexel(2.0),
|
||||
// // troikaBatchTexel(3.0)
|
||||
// // );
|
||||
// // position.xyz = (matrix * vec4(position, 1.0)).xyz;
|
||||
// `
|
||||
// })
|
||||
|
||||
// // Add the text shaders
|
||||
// batchMaterial = createTextDerivedMaterial(batchMaterial)
|
||||
|
||||
// // Now make other changes to the derived text shader code
|
||||
// batchMaterial = createDerivedMaterial(batchMaterial, {
|
||||
// chained: true,
|
||||
// uniforms: {
|
||||
// uTroikaIsOutline: { value: false }
|
||||
// },
|
||||
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// //@ts-ignore
|
||||
// customRewriter(shaders) {
|
||||
// // Convert some text shader uniforms to varyings
|
||||
// const varyingUniforms = [
|
||||
// 'uTroikaTotalBounds',
|
||||
// 'uTroikaClipRect',
|
||||
// 'uTroikaPositionOffset',
|
||||
// 'uTroikaEdgeOffset',
|
||||
// 'uTroikaBlurRadius',
|
||||
// 'uTroikaStrokeWidth',
|
||||
// 'uTroikaStrokeColor',
|
||||
// 'uTroikaStrokeOpacity',
|
||||
// 'uTroikaFillOpacity',
|
||||
// 'uTroikaCurveRadius',
|
||||
// 'diffuse'
|
||||
// ]
|
||||
// varyingUniforms.forEach((uniformName) => {
|
||||
// shaders = uniformToVarying(shaders, uniformName)
|
||||
// })
|
||||
// return shaders
|
||||
// },
|
||||
// // language=GLSL
|
||||
// vertexDefs: `
|
||||
// uniform bool uTroikaIsOutline;
|
||||
// vec3 troikaFloatToColor(float v) {
|
||||
// return mod(floor(vec3(v / 65536.0, v / 256.0, v)), 256.0) / 256.0;
|
||||
// }
|
||||
// `,
|
||||
// // language=GLSL prefix="void main() {" suffix="}"
|
||||
// vertexTransform: `
|
||||
// uTroikaTotalBounds = troikaBatchTexel(4.0);
|
||||
// uTroikaClipRect = troikaBatchTexel(5.0);
|
||||
|
||||
// vec4 data = troikaBatchTexel(6.0);
|
||||
// diffuse = troikaFloatToColor(data.x);
|
||||
// uTroikaFillOpacity = data.y;
|
||||
// uTroikaCurveRadius = data.z;
|
||||
|
||||
// data = troikaBatchTexel(7.0);
|
||||
// if (uTroikaIsOutline) {
|
||||
// if (data == vec4(0.0)) { // degenerate if zero outline
|
||||
// position = vec3(0.0);
|
||||
// } else {
|
||||
// uTroikaPositionOffset = data.xy;
|
||||
// uTroikaEdgeOffset = data.z;
|
||||
// uTroikaBlurRadius = data.w;
|
||||
// }
|
||||
// } else {
|
||||
// uTroikaStrokeWidth = data.x;
|
||||
// uTroikaStrokeColor = troikaFloatToColor(data.y);
|
||||
// uTroikaStrokeOpacity = data.z;
|
||||
// }
|
||||
// `
|
||||
// })
|
||||
|
||||
// batchMaterial.setMatrixTexture = (texture: {
|
||||
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// image: { width: any; height: any }
|
||||
// }) => {
|
||||
// batchMaterial.uniforms[texUniformName].value = texture
|
||||
// batchMaterial.uniforms[texSizeUniformName].value.set(
|
||||
// texture.image.width,
|
||||
// texture.image.height
|
||||
// )
|
||||
// }
|
||||
// this.copyCustomUniforms(batchMaterial)
|
||||
// ;(batchMaterial.defines ??= {})['BATCHED_TEXT'] = ' '
|
||||
// return batchMaterial
|
||||
// }
|
||||
|
||||
// public fastCopy(from: Material, to: Material) {
|
||||
// super.fastCopy(from, to)
|
||||
// const toStandard = to as SpeckleTextMaterial
|
||||
// const fromStandard = from as SpeckleTextMaterial
|
||||
// toStandard.color.copy(fromStandard.color)
|
||||
// toStandard.refractionRatio = fromStandard.refractionRatio
|
||||
// to.userData.gradientRamp.value = from.userData.gradientRamp.value
|
||||
// }
|
||||
|
||||
// public setGradientTexture(texture: Texture) {
|
||||
// this.userData.gradientRamp.value = texture
|
||||
// this.userData.gradientRamp.value.generateMipmaps = false
|
||||
// this.userData.gradientRamp.value.minFilter = NearestFilter
|
||||
// this.userData.gradientRamp.value.magFilter = NearestFilter
|
||||
// this.needsUpdate = true
|
||||
// }
|
||||
|
||||
// public setBillboarding(type: BillboardingType | null) {
|
||||
// /** Create the define object if not there */
|
||||
// if (!this.defines) this.defines = {}
|
||||
// /** Clear all billboarding defines */
|
||||
// delete this.defines['BILLBOARD_SCREEN']
|
||||
// delete this.defines['BILLBOARD']
|
||||
|
||||
// if (!type) return
|
||||
|
||||
// if (type === 'world') this.defines['BILLBOARD'] = ' '
|
||||
// if (type === 'screen') this.defines['BILLBOARD_SCREEN'] = ' '
|
||||
// }
|
||||
|
||||
// /** Called by three.js render loop */
|
||||
// public onBeforeRender(
|
||||
// _this: SpeckleWebGLRenderer,
|
||||
// _scene: Scene,
|
||||
// camera: Camera,
|
||||
// _geometry: BufferGeometry,
|
||||
// _object: Object3D
|
||||
// ) {
|
||||
// if (
|
||||
// this.defines &&
|
||||
// (this.defines['BILLBOARD'] || this.defines['BILLBOARD_SCREEN'])
|
||||
// ) {
|
||||
// matBuff.copy(camera.projectionMatrix).invert()
|
||||
// this.userData.invProjection.value.copy(matBuff)
|
||||
// this.needsUpdate = true
|
||||
// }
|
||||
|
||||
// if (this.defines && this.defines['BILLBOARD_SCREEN']) {
|
||||
// this.userData.billboardPixelHeight.value = this.billboardPixelHeight
|
||||
// this.userData.screenSize.value.copy(_this.getDrawingBufferSize(vec2Buff))
|
||||
// this.needsUpdate = true
|
||||
// }
|
||||
|
||||
// if (this.defines && this.defines['USE_RTE']) {
|
||||
// _object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix)
|
||||
// this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow)
|
||||
// this.userData.uViewer_high.value.copy(_this.RTEBuffers.viewerHigh)
|
||||
// this.needsUpdate = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// export default SpeckleTextMaterial
|
||||
|
||||
@@ -131,13 +131,12 @@ export const speckleBasicVert = /* glsl */ `
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(BILLBOARD) || defined(BILLBOARD_FIXED)
|
||||
uniform vec3 billboardPos;
|
||||
#if defined(BILLBOARD) || defined(BILLBOARD_SCREEN)
|
||||
uniform mat4 invProjection;
|
||||
#endif
|
||||
#ifdef BILLBOARD_FIXED
|
||||
uniform vec2 billboardSize;
|
||||
uniform vec2 billboardOffset;
|
||||
|
||||
#ifdef BILLBOARD_SCREEN
|
||||
uniform vec4 billboardPixelOffsetSize;
|
||||
#endif
|
||||
|
||||
void main() {
|
||||
@@ -192,12 +191,12 @@ void main() {
|
||||
|
||||
#if defined(BILLBOARD)
|
||||
float div = 1.;
|
||||
gl_Position = projectionMatrix * (viewMatrix * vec4(billboardPos, 1.0) + vec4(position.x, position.y, 0., 0.0));
|
||||
#elif defined(BILLBOARD_FIXED)
|
||||
gl_Position = projectionMatrix * (viewMatrix * vec4(billboardPos, 1.0));
|
||||
gl_Position = projectionMatrix * (viewMatrix * vec4(modelMatrix[3].xyz, 1.0) + vec4(position.x, position.y, 0., 0.0));
|
||||
#elif defined(BILLBOARD_SCREEN)
|
||||
gl_Position = projectionMatrix * (viewMatrix * vec4(modelMatrix[3].xyz, 1.0));
|
||||
float div = gl_Position.w;
|
||||
gl_Position /= gl_Position.w;
|
||||
gl_Position.xy += (position.xy + billboardOffset) * billboardSize;
|
||||
gl_Position.xy += position.xy * billboardPixelOffsetSize.zw * 2. + billboardPixelOffsetSize.xy * 2.;
|
||||
#else
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
#endif
|
||||
@@ -206,7 +205,7 @@ void main() {
|
||||
#include <logdepthbuf_vertex>
|
||||
// #include <clipping_planes_vertex> COMMENTED CHUNK
|
||||
#if NUM_CLIPPING_PLANES > 0
|
||||
#if defined(BILLBOARD) || defined(BILLBOARD_FIXED)
|
||||
#if defined(BILLBOARD) || defined(BILLBOARD_SCREEN)
|
||||
vec4 movelViewProjection = gl_Position * div;
|
||||
vClipPosition = - (invProjection * movelViewProjection).xyz;
|
||||
#else
|
||||
|
||||
@@ -21,9 +21,20 @@ uniform float opacity;
|
||||
#include <specularmap_pars_fragment>
|
||||
#include <logdepthbuf_pars_fragment>
|
||||
#include <clipping_planes_pars_fragment>
|
||||
|
||||
#ifdef BATCHED_TEXT
|
||||
uniform sampler2D gradientRamp;
|
||||
varying float vGradientIndex;
|
||||
#endif
|
||||
|
||||
void main() {
|
||||
#include <clipping_planes_fragment>
|
||||
vec4 diffuseColor = vec4( diffuse, opacity );
|
||||
vec4 diffuseColor_RGB = vec4(diffuse, opacity);
|
||||
vec4 diffuseColor = diffuseColor_RGB;
|
||||
#ifdef BATCHED_TEXT
|
||||
vec4 diffuseColor_Tex = vec4( texture2D(gradientRamp, vec2(vGradientIndex, 0.)).rgb, opacity );
|
||||
diffuseColor = mix(diffuseColor_RGB, diffuseColor_Tex, float(vGradientIndex > 0.));
|
||||
#endif
|
||||
#include <logdepthbuf_fragment>
|
||||
#include <map_fragment>
|
||||
#include <color_fragment>
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
export const speckleTextVert = /* glsl */ `
|
||||
#include <common>
|
||||
#ifdef USE_RTE
|
||||
// The high component is stored as the default 'position' attribute buffer
|
||||
attribute vec3 position_low;
|
||||
uniform vec3 uViewer_high;
|
||||
uniform vec3 uViewer_low;
|
||||
#endif
|
||||
|
||||
#ifdef TRANSFORM_STORAGE
|
||||
attribute float objIndex;
|
||||
|
||||
#if TRANSFORM_STORAGE == 0
|
||||
#if __VERSION__ == 300
|
||||
#define TRANSFORM_STRIDE 4
|
||||
#else
|
||||
#define TRANSFORM_STRIDE 4.
|
||||
#endif
|
||||
uniform sampler2D tTransforms;
|
||||
uniform float objCount;
|
||||
#elif TRANSFORM_STORAGE == 1
|
||||
uniform mat4 uTransforms[OBJ_COUNT];
|
||||
#endif
|
||||
#if defined(BILLBOARD) || defined(BILLBOARD_SCREEN)
|
||||
uniform mat4 invProjection;
|
||||
#endif
|
||||
|
||||
#ifdef BILLBOARD_SCREEN
|
||||
uniform vec4 billboardPixelOffsetSize;
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
#include <uv_pars_vertex>
|
||||
#include <uv2_pars_vertex>
|
||||
#include <envmap_pars_vertex>
|
||||
@@ -56,87 +48,9 @@ export const speckleTextVert = /* glsl */ `
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef TRANSFORM_STORAGE
|
||||
void objectTransform(out vec4 quaternion, out vec4 pivotLow, out vec4 pivotHigh, out vec4 translation, out vec4 scale){
|
||||
#if TRANSFORM_STORAGE == 0
|
||||
#if __VERSION__ == 300
|
||||
ivec2 uv = ivec2(int(objIndex) * TRANSFORM_STRIDE, 0);
|
||||
vec4 v0 = texelFetch( tTransforms, uv, 0 );
|
||||
vec4 v1 = texelFetch( tTransforms, uv + ivec2(1, 0), 0);
|
||||
vec4 v2 = texelFetch( tTransforms, uv + ivec2(2, 0), 0);
|
||||
vec4 v3 = texelFetch( tTransforms, uv + ivec2(3, 0), 0);
|
||||
quaternion = v0;
|
||||
pivotLow = vec4(v1.xyz, 1.);
|
||||
pivotHigh = vec4(v2.xyz, 1.);
|
||||
translation = vec4(v3.xyz, 1.);
|
||||
scale = vec4(v1.w, v2.w, v3.w, 1.);
|
||||
#else
|
||||
float size = objCount * TRANSFORM_STRIDE;
|
||||
vec2 cUv = vec2(0.5/size, 0.5);
|
||||
vec2 dUv = vec2(1./size, 0.);
|
||||
|
||||
vec2 uv = vec2((objIndex * TRANSFORM_STRIDE)/size + cUv.x, cUv.y);
|
||||
vec4 v0 = texture2D( tTransforms, uv);
|
||||
vec4 v1 = texture2D( tTransforms, uv + dUv);
|
||||
vec4 v2 = texture2D( tTransforms, uv + 2. * dUv);
|
||||
vec4 v3 = texture2D( tTransforms, uv + 3. * dUv);
|
||||
quaternion = v0;
|
||||
pivotLow = vec4(v1.xyz, 1.);
|
||||
pivotHigh = vec4(v2.xyz, 1.);
|
||||
translation = vec4(v3.xyz, 1.);
|
||||
scale = vec4(v1.w, v2.w, v3.w, 1.);
|
||||
#endif
|
||||
#elif TRANSFORM_STORAGE == 1
|
||||
mat4 tMatrix = uTransforms[int(objIndex)];
|
||||
quaternion = tMatrix[0];
|
||||
pivotLow = vec4(tMatrix[1].xyz, 1.);
|
||||
pivotHigh = vec4(tMatrix[2].xyz, 1.);
|
||||
translation = vec4(tMatrix[3].xyz, 1.);
|
||||
scale = vec4(tMatrix[1][3], tMatrix[2][3], tMatrix[3][3], 1.);
|
||||
#endif
|
||||
}
|
||||
|
||||
vec3 rotate_vertex_position(vec3 position, vec4 quat)
|
||||
{
|
||||
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
|
||||
}
|
||||
|
||||
/** Another workaround for Apple's stupid compiler */
|
||||
vec4 safeMul(vec4 a, vec4 b) {
|
||||
// Prevents constant folding and optimization
|
||||
return (a + vec4(0.0)) * (b + vec4(1.0)) - a * vec4(1.0);
|
||||
}
|
||||
|
||||
highp vec3 rotate_scaled_vertex_position_delta(highp vec4 v0, highp vec4 v1, highp vec4 scale, highp vec4 quat)
|
||||
{
|
||||
/** !!! WORKAROUND FOR Intel IrisXe CARDS !!! */
|
||||
/** The code below will not produce correct results in intel IrisXE integrated GPUs.
|
||||
* The geometry will turn mangled, albeit stable
|
||||
* I can't know for sure what is going on, but rotating the difference seems to
|
||||
* force the result into a lower precision?
|
||||
*/
|
||||
// highp vec4 position = v0 - v1;
|
||||
// return position.xyz + 2.0 * cross(quat.xyz, cross(quat.xyz, position.xyz) + quat.w * position.xyz);
|
||||
|
||||
/** Subtracting the rotated vectors works. */
|
||||
return rotate_vertex_position(safeMul(v0, scale).xyz, quat) - rotate_vertex_position(safeMul(v1, scale).xyz, quat) ;
|
||||
|
||||
/** An alternate workaround is
|
||||
* highp vec3 position = (v0.xyz * (1. + 1e-7)) - (v1.xyz * (1. + 1e-7));
|
||||
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
|
||||
|
||||
However I'm not such a fan of the (1. + 1e-7) part
|
||||
*/
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(BILLBOARD) || defined(BILLBOARD_FIXED)
|
||||
uniform vec3 billboardPos;
|
||||
uniform mat4 invProjection;
|
||||
#endif
|
||||
#ifdef BILLBOARD_FIXED
|
||||
uniform vec2 billboardSize;
|
||||
#ifdef BATCHED_TEXT
|
||||
varying float vGradientIndex;
|
||||
#endif
|
||||
|
||||
void main() {
|
||||
@@ -154,62 +68,64 @@ void main() {
|
||||
#include <begin_vertex>
|
||||
#include <morphtarget_vertex>
|
||||
#include <skinning_vertex>
|
||||
// #include <project_vertex> COMMENTED CHUNK
|
||||
#ifdef TRANSFORM_STORAGE
|
||||
vec4 tQuaternion, tPivotLow, tPivotHigh, tTranslation, tScale;
|
||||
objectTransform(tQuaternion, tPivotLow, tPivotHigh, tTranslation, tScale);
|
||||
#endif
|
||||
#ifdef USE_RTE
|
||||
vec4 position_lowT = vec4(position_low, 1.);
|
||||
vec4 position_highT = vec4(position, 1.);
|
||||
const vec3 ZERO3 = vec3(0., 0., 0.);
|
||||
|
||||
highp vec4 rteLocalPosition = computeRelativePosition(position_lowT.xyz, position_highT.xyz, uViewer_low, uViewer_high);
|
||||
#ifdef TRANSFORM_STORAGE
|
||||
highp vec4 rtePivot = computeRelativePosition(tPivotLow.xyz, tPivotHigh.xyz, uViewer_low, uViewer_high);
|
||||
rteLocalPosition.xyz = rotate_scaled_vertex_position_delta(rteLocalPosition, rtePivot, tScale, tQuaternion) + rtePivot.xyz + tTranslation.xyz;
|
||||
#endif
|
||||
#ifdef USE_INSTANCING
|
||||
vec4 instancePivot = computeRelativePosition(ZERO3, ZERO3, uViewer_low, uViewer_high);
|
||||
rteLocalPosition.xyz = (mat3(instanceMatrix) * (rteLocalPosition - instancePivot).xyz) + instancePivot.xyz + instanceMatrix[3].xyz;
|
||||
#endif
|
||||
vec4 mvPosition;
|
||||
mat4 matrix;
|
||||
|
||||
#ifdef BATCHED_TEXT
|
||||
matrix = mat4(
|
||||
troikaBatchTexel(0.0),
|
||||
troikaBatchTexel(1.0),
|
||||
troikaBatchTexel(2.0),
|
||||
troikaBatchTexel(3.0)
|
||||
);
|
||||
#else
|
||||
matrix = modelMatrix;
|
||||
#endif
|
||||
|
||||
#ifdef USE_RTE
|
||||
vec4 mvPosition = rteLocalPosition;
|
||||
#else
|
||||
vec4 mvPosition = vec4( transformed, 1.0 );
|
||||
#ifdef TRANSFORM_STORAGE
|
||||
mvPosition.xyz = rotate_scaled_vertex_position_delta(mvPosition, tPivotHigh, tScale, tQuaternion) + tPivotHigh.xyz + tTranslation.xyz;
|
||||
#endif
|
||||
#ifdef USE_INSTANCING
|
||||
mvPosition = instanceMatrix * mvPosition;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
mvPosition = modelViewMatrix * mvPosition;
|
||||
|
||||
#if defined(BILLBOARD)
|
||||
float div = 1.;
|
||||
gl_Position = projectionMatrix * (viewMatrix * vec4(billboardPos, 1.0) + vec4(position.x, position.y, 0., 0.0));
|
||||
#elif defined(BILLBOARD_FIXED)
|
||||
gl_Position = projectionMatrix * (viewMatrix * vec4(billboardPos, 1.0));
|
||||
float div = gl_Position.w;
|
||||
gl_Position /= gl_Position.w;
|
||||
gl_Position.xy += position.xy * billboardSize;
|
||||
#else
|
||||
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.);
|
||||
#endif
|
||||
#include <logdepthbuf_vertex>
|
||||
// #include <clipping_planes_vertex> COMMENTED CHUNK
|
||||
#if NUM_CLIPPING_PLANES > 0
|
||||
#if defined(BILLBOARD) || defined(BILLBOARD_FIXED)
|
||||
vec4 movelViewProjection = gl_Position * div;
|
||||
vClipPosition = - (invProjection * movelViewProjection).xyz;
|
||||
/* We store the high part normally as the translation component */
|
||||
vec3 translationHigh = matrix[3].xyz;
|
||||
/** We store the low part of the translation in row4 ofthe matrix */
|
||||
vec3 translationLow = vec3(matrix[0][3], matrix[1][3], matrix[2][3]);
|
||||
highp vec4 rteTranslation = computeRelativePosition(translationLow, translationHigh, uViewer_low, uViewer_high);
|
||||
#if defined(BILLBOARD)
|
||||
mvPosition = (modelViewMatrix * rteTranslation + vec4(position.x, position.y, 0., 0.0));
|
||||
#else
|
||||
vClipPosition = - mvPosition.xyz;
|
||||
mvPosition = vec4(mat3(matrix) * transformed + rteTranslation.xyz, 1.);
|
||||
mvPosition = modelViewMatrix * mvPosition;
|
||||
#endif
|
||||
#else
|
||||
#if defined(BILLBOARD) || defined(BILLBOARD_SCREEN)
|
||||
vec3 billboardPosition = matrix[3].xyz;
|
||||
#if defined(BILLBOARD_SCREEN)
|
||||
mvPosition = projectionMatrix * (viewMatrix * vec4(billboardPosition, 1.0));
|
||||
float div = mvPosition.w;
|
||||
mvPosition /= mvPosition.w;
|
||||
// Pixel values are computed like so
|
||||
// windowX = ((ndc.x + 1) / 2) * width;
|
||||
// windowY = ((ndc.y + 1) / 2) * height;
|
||||
// That's why we multiply by 2.
|
||||
mvPosition.xy += position.xy * billboardPixelOffsetSize.zw * 2. + billboardPixelOffsetSize.xy * 2.;
|
||||
/** Back to view space for convenience */
|
||||
mvPosition *= div;
|
||||
mvPosition = invProjection * mvPosition;
|
||||
#else
|
||||
mvPosition = (viewMatrix * vec4(billboardPosition, 1.) + vec4(position.x, position.y, 0., 0.0));
|
||||
#endif
|
||||
#else
|
||||
mvPosition = viewMatrix * matrix * vec4(transformed, 1.);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef BATCHED_TEXT
|
||||
vGradientIndex = troikaBatchTexel(6.).w;
|
||||
#endif
|
||||
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
|
||||
#include <logdepthbuf_vertex>
|
||||
#include <clipping_planes_vertex>
|
||||
#include <worldpos_vertex>
|
||||
#include <envmap_vertex>
|
||||
#include <fog_vertex>
|
||||
|
||||
@@ -68,10 +68,10 @@ to get the correct values for Vectors, Rays, Boxes, etc
|
||||
export class AccelerationStructure {
|
||||
private static readonly MatBuff: Matrix4 = new Matrix4()
|
||||
private _bvh: MeshBVH
|
||||
public inputTransform!: Matrix4
|
||||
public outputTransform!: Matrix4
|
||||
public inputOriginTransform!: Matrix4
|
||||
public outputOriginTransfom!: Matrix4
|
||||
public inputTransform: Matrix4
|
||||
public outputTransform: Matrix4
|
||||
public inputOriginTransform: Matrix4
|
||||
public outputOriginTransfom: Matrix4
|
||||
|
||||
public get geometry() {
|
||||
return this._bvh.geometry
|
||||
|
||||
@@ -0,0 +1,605 @@
|
||||
import { Text } from 'troika-three-text'
|
||||
import { BatchedText } from 'troika-three-text'
|
||||
import { TopLevelAccelerationStructure } from './TopLevelAccelerationStructure.js'
|
||||
import {
|
||||
Box3,
|
||||
BufferGeometry,
|
||||
Camera,
|
||||
Color,
|
||||
DataTexture,
|
||||
Float32BufferAttribute,
|
||||
FloatType,
|
||||
Int16BufferAttribute,
|
||||
Intersection,
|
||||
Material,
|
||||
Matrix4,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
Object3D,
|
||||
Ray,
|
||||
Raycaster,
|
||||
RGBAFormat,
|
||||
Scene,
|
||||
Sphere,
|
||||
Texture,
|
||||
Vector3
|
||||
} from 'three'
|
||||
import { BatchObject } from '../batching/BatchObject.js'
|
||||
import { ExtendedMeshIntersection, SpeckleRaycaster } from './SpeckleRaycaster.js'
|
||||
import { DrawGroup } from '../batching/Batch.js'
|
||||
import Logger from '../utils/Logger.js'
|
||||
import Materials from '../materials/Materials.js'
|
||||
import SpeckleTextMaterial from '../materials/SpeckleTextMaterial.js'
|
||||
import { Geometry } from '../converter/Geometry.js'
|
||||
import { TextBatchObject } from '../batching/TextBatchObject.js'
|
||||
import { ObjectLayers, SpeckleWebGLRenderer } from '../../index.js'
|
||||
|
||||
const ray = /* @__PURE__ */ new Ray()
|
||||
const tmpInverseMatrix = /* @__PURE__ */ new Matrix4()
|
||||
const vecBuff0 = /* @__PURE__ */ new Vector3()
|
||||
const vecBuff1 = /* @__PURE__ */ new Vector3()
|
||||
const vecBuff2 = /* @__PURE__ */ new Vector3()
|
||||
const matBuff0 = /* @__PURE__ */ new Matrix4()
|
||||
const matBuff1 = /* @__PURE__ */ new Matrix4()
|
||||
const matBuff2 = /* @__PURE__ */ new Matrix4()
|
||||
|
||||
export class SpeckleBatchedText extends BatchedText {
|
||||
declare material: SpeckleTextMaterial
|
||||
|
||||
private tas: TopLevelAccelerationStructure
|
||||
private _batchMaterial: SpeckleTextMaterial
|
||||
private _batchObjects: BatchObject[]
|
||||
private _textObjects: { [id: string]: Text } = {}
|
||||
private _dirty: boolean = false
|
||||
|
||||
public groups: Array<DrawGroup> = []
|
||||
public materials: Material[] = []
|
||||
|
||||
private materialCache: { [id: string]: Material } = {}
|
||||
private materialCacheLUT: { [id: string]: number } = {}
|
||||
|
||||
private readonly DEBUG_BILLBOARDS = false
|
||||
private debugMeshes: Mesh[] = []
|
||||
|
||||
public get TAS(): TopLevelAccelerationStructure {
|
||||
return this.tas
|
||||
}
|
||||
|
||||
public get batchObjects(): BatchObject[] {
|
||||
return this._batchObjects
|
||||
}
|
||||
|
||||
public get batchMaterial(): Material {
|
||||
return this._batchMaterial
|
||||
}
|
||||
|
||||
public set dirty(value: boolean) {
|
||||
this._dirty = value
|
||||
}
|
||||
|
||||
public get isBillboarded() {
|
||||
return (
|
||||
this._batchMaterial &&
|
||||
this._batchMaterial.defines &&
|
||||
this._batchMaterial.defines['BILLBOARD']
|
||||
)
|
||||
}
|
||||
|
||||
public setBatchMaterial(material: Material) {
|
||||
if (!(material instanceof SpeckleTextMaterial)) {
|
||||
Logger.error(
|
||||
`SpeckleBatchedText requires a SpeckleTextMaterial. Found ${material.constructor.name}`
|
||||
)
|
||||
return
|
||||
}
|
||||
this._batchMaterial = this.getCachedMaterial(material) as SpeckleTextMaterial
|
||||
this.material = this._batchMaterial
|
||||
this.materials.push(this._batchMaterial)
|
||||
}
|
||||
|
||||
public setBatchObjects(batchObjects: BatchObject[], textObjects: Text[]) {
|
||||
this._batchObjects = batchObjects
|
||||
for (let k = 0; k < batchObjects.length; k++) {
|
||||
const id = batchObjects[k].renderView.renderData.id
|
||||
this._textObjects[id] = textObjects[k]
|
||||
}
|
||||
}
|
||||
|
||||
private lookupMaterial(material: Material) {
|
||||
return (
|
||||
this.materialCache[material.id] ||
|
||||
this.materialCache[this.materialCacheLUT[material.id]]
|
||||
)
|
||||
}
|
||||
|
||||
public getCachedMaterial(material: Material, copy = false): Material {
|
||||
let cachedMaterial = this.lookupMaterial(material)
|
||||
if (!cachedMaterial) {
|
||||
const clone = new SpeckleTextMaterial({})
|
||||
.copy(material)
|
||||
.getDerivedBatchedMaterial()
|
||||
this.materialCache[material.id] = clone
|
||||
this.materialCacheLUT[clone.id] = material.id
|
||||
cachedMaterial = clone
|
||||
} else if (
|
||||
copy ||
|
||||
(material as never)['needsCopy'] ||
|
||||
(cachedMaterial as never)['needsCopy']
|
||||
) {
|
||||
Materials.fastCopy(material, cachedMaterial)
|
||||
}
|
||||
return cachedMaterial
|
||||
}
|
||||
|
||||
public buildTAS() {
|
||||
this.tas = new TopLevelAccelerationStructure(this.batchObjects)
|
||||
/** We do a refit here, because for some reason the bvh library incorrectly computes the total bvh bounds at creation,
|
||||
* so we force a refit in order to get the proper bounds value out of it
|
||||
*/
|
||||
this.tas.refit()
|
||||
|
||||
/** Copy computed bounds over so that three.js doesn't freak out */
|
||||
this.geometry.boundingBox = this.TAS.getBoundingBox(new Box3())
|
||||
this.geometry.boundingSphere = this.geometry.boundingBox.getBoundingSphere(
|
||||
new Sphere()
|
||||
)
|
||||
}
|
||||
|
||||
/** This could be made faster. BUT, as this point in time it's not worth the effort */
|
||||
public updateTransformsUniform() {
|
||||
let needsUpdate = false
|
||||
for (let k = 0; k < this._batchObjects.length; k++) {
|
||||
const batchObject = this._batchObjects[k]
|
||||
if (!(needsUpdate ||= batchObject.transformDirty)) continue
|
||||
const textObject = this._textObjects[batchObject.renderView.renderData.id]
|
||||
batchObject.transform.decompose(
|
||||
textObject.position,
|
||||
textObject.quaternion,
|
||||
textObject.scale
|
||||
)
|
||||
textObject.updateMatrix()
|
||||
// Matrix
|
||||
const matrix = textObject.matrix.elements
|
||||
const texture =
|
||||
this._dataTextures[
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
textObject.material.isTextOutlineMaterial ? 'outline' : 'main'
|
||||
]
|
||||
const packingInfo = this._members.get(textObject)
|
||||
if (packingInfo) {
|
||||
const startIndex = packingInfo.index * 32
|
||||
for (let i = 0; i < 16; i++) {
|
||||
this.setTexData(texture, startIndex + i, matrix[i])
|
||||
}
|
||||
batchObject.transformDirty = false
|
||||
}
|
||||
}
|
||||
if (this.tas && needsUpdate) {
|
||||
this.tas.refit()
|
||||
this.tas.getBoundingBox(this.tas.bounds)
|
||||
}
|
||||
}
|
||||
|
||||
public updateMaterialTransformsUniform(material: Material) {
|
||||
material
|
||||
}
|
||||
|
||||
public setGradientTexture(texture: Texture) {
|
||||
this._batchMaterial.setGradientTexture(texture)
|
||||
}
|
||||
|
||||
public getBatchObjectMaterial(batchObject: BatchObject) {
|
||||
const rv = batchObject.renderView
|
||||
const group = this.groups.find((value) => {
|
||||
return (
|
||||
rv.batchStart >= value.start &&
|
||||
rv.batchStart + rv.batchCount <= value.count + value.start
|
||||
)
|
||||
})
|
||||
if (!group) {
|
||||
Logger.warn(`Could not get material for ${batchObject.renderView.renderData.id}`)
|
||||
return null
|
||||
}
|
||||
return this.materials[group.materialIndex]
|
||||
}
|
||||
|
||||
// converts the given BVH raycast intersection to align with the three.js raycast
|
||||
// structure (include object, world space distance and point).
|
||||
private convertRaycastIntersect(
|
||||
hit: Intersection | null,
|
||||
object: Object3D,
|
||||
raycaster: Raycaster
|
||||
) {
|
||||
if (hit === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
hit.point.applyMatrix4(object.matrixWorld)
|
||||
hit.distance = hit.point.distanceTo(raycaster.ray.origin)
|
||||
hit.object = object
|
||||
|
||||
if (hit.distance < raycaster.near || hit.distance > raycaster.far) {
|
||||
return null
|
||||
} else {
|
||||
return hit
|
||||
}
|
||||
}
|
||||
|
||||
private initDebugBox() {
|
||||
const debugBox = new Mesh(
|
||||
new BufferGeometry(),
|
||||
new MeshBasicMaterial({ wireframe: true, color: 0xff0000 })
|
||||
)
|
||||
debugBox.geometry.setAttribute(
|
||||
'position',
|
||||
new Float32BufferAttribute(new Array(12), 3)
|
||||
)
|
||||
debugBox.geometry.setIndex(
|
||||
// prettier-ignore
|
||||
new Int16BufferAttribute(
|
||||
[
|
||||
0, 1, 2, // First triangle: bottom-left → bottom-right → top-right
|
||||
0, 2, 3 // Second triangle: bottom-left → top-right → top-left
|
||||
],
|
||||
1
|
||||
)
|
||||
)
|
||||
debugBox.layers.set(ObjectLayers.OVERLAY)
|
||||
this.parent?.add(debugBox)
|
||||
return debugBox
|
||||
}
|
||||
|
||||
/** Debug purposes only */
|
||||
onBeforeRender(
|
||||
renderer: SpeckleWebGLRenderer,
|
||||
scene: Scene,
|
||||
camera: Camera,
|
||||
geometry: BufferGeometry,
|
||||
material: Material,
|
||||
group: unknown
|
||||
) {
|
||||
super.onBeforeRender(renderer, scene, camera, geometry, material, group)
|
||||
if (this.DEBUG_BILLBOARDS && this.isBillboarded) {
|
||||
const vertices = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]
|
||||
for (let k = 0; k < this._batchObjects.length; k++) {
|
||||
if (!this.debugMeshes[k]) {
|
||||
this.debugMeshes[k] = this.initDebugBox()
|
||||
}
|
||||
const textMatrix = (this._batchObjects[k] as TextBatchObject).textTransform
|
||||
|
||||
const billboardPos = vecBuff0.set(
|
||||
textMatrix.elements[12],
|
||||
textMatrix.elements[13],
|
||||
textMatrix.elements[14]
|
||||
)
|
||||
|
||||
const box = new Box3().copy(this._batchObjects[k].aabb)
|
||||
const min = vecBuff1.copy(box.min)
|
||||
const max = vecBuff2.copy(box.max)
|
||||
vertices[0].set(min.x, min.y, 0)
|
||||
vertices[1].set(max.x, min.y, 0)
|
||||
vertices[2].set(max.x, max.y, 0)
|
||||
vertices[3].set(min.x, max.y, 0)
|
||||
|
||||
const billboardMat = matBuff0.makeTranslation(
|
||||
billboardPos.x,
|
||||
billboardPos.y,
|
||||
billboardPos.z
|
||||
)
|
||||
|
||||
billboardMat.multiply(matBuff1.extractRotation(camera.matrixWorld))
|
||||
// TO DO: This is out of place. Probably happening because acceleration structure has the text transform as input transform
|
||||
billboardMat.multiply(matBuff2.copy(textMatrix).invert())
|
||||
|
||||
for (let i = 0; i < vertices.length; i++) {
|
||||
const debugVertex = vecBuff2.copy(vertices[i])
|
||||
debugVertex.applyMatrix4(billboardMat)
|
||||
|
||||
this.debugMeshes[k].geometry.attributes.position.setXYZ(
|
||||
i,
|
||||
debugVertex.x,
|
||||
debugVertex.y,
|
||||
debugVertex.z
|
||||
)
|
||||
}
|
||||
this.debugMeshes[k].geometry.attributes.position.needsUpdate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
raycast(raycaster: SpeckleRaycaster, intersects: Array<Intersection>) {
|
||||
/** We bypass the TAS for billboarded text batches, otherwise we would need to refit it each frame */
|
||||
if (this.isBillboarded) {
|
||||
const rayBuff = new Ray()
|
||||
for (let k = 0; k < this._batchObjects.length; k++) {
|
||||
const textMatrix = (this._batchObjects[k] as TextBatchObject).textTransform
|
||||
/** The billboard position is the text object's position stored in it's world matrix */
|
||||
const billboardPos = vecBuff0.set(
|
||||
textMatrix.elements[12],
|
||||
textMatrix.elements[13],
|
||||
textMatrix.elements[14]
|
||||
)
|
||||
/** We compute the matrix that billboards the text */
|
||||
const billboardMat = matBuff0.makeTranslation(
|
||||
billboardPos.x,
|
||||
billboardPos.y,
|
||||
billboardPos.z
|
||||
)
|
||||
billboardMat.multiply(matBuff1.extractRotation(raycaster.camera.matrixWorld))
|
||||
billboardMat.multiply(matBuff2.copy(textMatrix).invert())
|
||||
/** We invert it in order to apply to the ray instead of the geometry */
|
||||
const invBillboardMat = matBuff0.copy(billboardMat).invert()
|
||||
rayBuff.copy(raycaster.ray)
|
||||
rayBuff.applyMatrix4(invBillboardMat)
|
||||
|
||||
/** Regular intersecting from here on out on a per batch object level */
|
||||
if (raycaster.firstHitOnly === true) {
|
||||
const hit = this.convertRaycastIntersect(
|
||||
this._batchObjects[k].accelerationStructure.raycastFirst(
|
||||
rayBuff,
|
||||
this._batchMaterial
|
||||
),
|
||||
this as unknown as Object3D,
|
||||
raycaster
|
||||
) as ExtendedMeshIntersection
|
||||
if (hit) {
|
||||
hit.batchObject = this._batchObjects[k]
|
||||
intersects.push(hit)
|
||||
break // We break here as we only want the first hit
|
||||
}
|
||||
} else {
|
||||
const hits = this._batchObjects[k].accelerationStructure.raycast(
|
||||
rayBuff,
|
||||
this._batchMaterial
|
||||
)
|
||||
for (let i = 0, l = hits.length; i < l; i++) {
|
||||
const hit = this.convertRaycastIntersect(
|
||||
hits[i],
|
||||
this as unknown as Object3D,
|
||||
raycaster
|
||||
) as ExtendedMeshIntersection
|
||||
if (hit) {
|
||||
hit.batchObject = this._batchObjects[k]
|
||||
intersects.push(hit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.tas) {
|
||||
if (this._batchMaterial === undefined) return
|
||||
|
||||
tmpInverseMatrix.copy(this.matrixWorld).invert()
|
||||
ray.copy(raycaster.ray).applyMatrix4(tmpInverseMatrix)
|
||||
/** Texts are all quads. Intersecting their BAS is redundant */
|
||||
const tasOnly = raycaster.intersectTASOnly || true
|
||||
|
||||
if (raycaster.firstHitOnly === true) {
|
||||
const hit = this.convertRaycastIntersect(
|
||||
this.tas.raycastFirst(ray, tasOnly, this._batchMaterial),
|
||||
this as unknown as Object3D,
|
||||
raycaster
|
||||
)
|
||||
if (hit) {
|
||||
intersects.push(hit)
|
||||
}
|
||||
} else {
|
||||
const hits = this.tas.raycast(ray, tasOnly, this._batchMaterial)
|
||||
for (let i = 0, l = hits.length; i < l; i++) {
|
||||
const hit = this.convertRaycastIntersect(
|
||||
hits[i],
|
||||
this as unknown as Object3D,
|
||||
raycaster
|
||||
)
|
||||
if (hit) {
|
||||
intersects.push(hit)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.raycast(raycaster, intersects)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the batched geometry bounds to hold all members
|
||||
*/
|
||||
updateBounds() {
|
||||
if (!this._dirty) return
|
||||
// Update member local matrices and the overall bounds
|
||||
const tempBox3 = new Box3()
|
||||
const bbox = (this.geometry.boundingBox ?? new Box3()).makeEmpty()
|
||||
this._members.forEach((_, text) => {
|
||||
if (text.matrixAutoUpdate) text.updateMatrix() // ignore world matrix
|
||||
tempBox3.copy(text.geometry.boundingBox ?? new Box3()).applyMatrix4(text.matrix)
|
||||
bbox.union(tempBox3)
|
||||
})
|
||||
bbox.getBoundingSphere(this.geometry.boundingSphere ?? new Sphere())
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Text} text
|
||||
*/
|
||||
addText(text: Text) {
|
||||
if (!this._members.has(text)) {
|
||||
this._members.set(text, {
|
||||
index: -1,
|
||||
glyphCount: -1,
|
||||
dirty: true,
|
||||
needsUpdate: true
|
||||
})
|
||||
text.addEventListener('synccomplete', this._onMemberSynced)
|
||||
}
|
||||
}
|
||||
|
||||
private setTexData(texture: DataTexture, index: number, value: number) {
|
||||
const texData = texture.image.data
|
||||
if (value !== texData[index]) {
|
||||
texData[index] = value
|
||||
texture.needsUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Data texture packing strategy:
|
||||
|
||||
# Common:
|
||||
0-15: matrix
|
||||
16-19: uTroikaTotalBounds
|
||||
20-23: uTroikaClipRect
|
||||
24: diffuse (color/outlineColor)
|
||||
25: uTroikaFillOpacity (fillOpacity/outlineOpacity)
|
||||
26: uTroikaCurveRadius
|
||||
27: <blank>
|
||||
|
||||
# Main:
|
||||
28: uTroikaStrokeWidth
|
||||
29: uTroikaStrokeColor
|
||||
30: uTroikaStrokeOpacity
|
||||
|
||||
# Outline:
|
||||
28-29: uTroikaPositionOffset
|
||||
30: uTroikaEdgeOffset
|
||||
31: uTroikaBlurRadius
|
||||
*/
|
||||
|
||||
/**
|
||||
* @override
|
||||
* Patched version that allows:
|
||||
* - Individual text opacities
|
||||
* - Coordinate inside gradient/ramp texture <27>
|
||||
*/
|
||||
_prepareForRender(material: SpeckleTextMaterial) {
|
||||
if (!this._dirty) return
|
||||
|
||||
this._dirty = false
|
||||
|
||||
const floatsPerMember = 32
|
||||
const tempColor = new Color()
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
const isOutline = material.isTextOutlineMaterial
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
material.uniforms.uTroikaIsOutline.value = isOutline
|
||||
|
||||
// Resize the texture to fit in powers of 2
|
||||
let texture = this._dataTextures[isOutline ? 'outline' : 'main']
|
||||
const dataLength = Math.pow(
|
||||
2,
|
||||
Math.ceil(Math.log2(this._members.size * floatsPerMember))
|
||||
)
|
||||
if (!texture || dataLength !== texture.image.data.length) {
|
||||
// console.log(`resizing: ${dataLength}`);
|
||||
if (texture) texture.dispose()
|
||||
const width = Math.min(dataLength / 4, 1024)
|
||||
texture = this._dataTextures[isOutline ? 'outline' : 'main'] = new DataTexture(
|
||||
new Float32Array(dataLength),
|
||||
width,
|
||||
dataLength / 4 / width,
|
||||
RGBAFormat,
|
||||
FloatType
|
||||
)
|
||||
}
|
||||
|
||||
this._members.forEach((packingInfo, text) => {
|
||||
if (packingInfo.index > -1 && packingInfo.needsUpdate) {
|
||||
packingInfo.needsUpdate = false
|
||||
const startIndex = packingInfo.index * floatsPerMember
|
||||
|
||||
// Matrix
|
||||
const matrix = text.matrix.elements
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
this.setTexData(texture, startIndex + i, matrix[i])
|
||||
}
|
||||
if (material.defines && material.defines['USE_RTE'] !== undefined) {
|
||||
const translation = new Vector3(matrix[12], matrix[13], matrix[14])
|
||||
const translationLow = new Vector3()
|
||||
const translationHigh = new Vector3()
|
||||
Geometry.DoubleToHighLowVector(translation, translationLow, translationHigh)
|
||||
this.setTexData(texture, startIndex + 3, translationLow.x)
|
||||
this.setTexData(texture, startIndex + 7, translationLow.y)
|
||||
this.setTexData(texture, startIndex + 11, translationLow.z)
|
||||
}
|
||||
// Let the member populate the uniforms, since that does all the appropriate
|
||||
// logic and handling of defaults, and we'll just grab the results from there
|
||||
text._prepareForRender(material)
|
||||
const {
|
||||
uTroikaTotalBounds,
|
||||
uTroikaClipRect,
|
||||
uTroikaPositionOffset,
|
||||
uTroikaEdgeOffset,
|
||||
uTroikaBlurRadius,
|
||||
uTroikaStrokeWidth,
|
||||
uTroikaStrokeColor,
|
||||
uTroikaStrokeOpacity,
|
||||
uTroikaFillOpacity,
|
||||
uTroikaCurveRadius
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
} = material.uniforms
|
||||
|
||||
// Total bounds for uv
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.setTexData(
|
||||
texture,
|
||||
startIndex + 16 + i,
|
||||
uTroikaTotalBounds.value.getComponent(i)
|
||||
)
|
||||
}
|
||||
|
||||
// Clip rect
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.setTexData(
|
||||
texture,
|
||||
startIndex + 20 + i,
|
||||
uTroikaClipRect.value.getComponent(i)
|
||||
)
|
||||
}
|
||||
|
||||
// Color
|
||||
let color = isOutline ? text.outlineColor || 0 : text.color
|
||||
if (color === null) color = this.color
|
||||
if (color === null) color = this.material.color
|
||||
if (color === null) color = 0xffffff
|
||||
this.setTexData(texture, startIndex + 24, tempColor.set(color).getHex())
|
||||
|
||||
// Fill opacity / outline opacity
|
||||
this.setTexData(
|
||||
texture,
|
||||
startIndex + 25,
|
||||
(text.material as Material).opacity ?? uTroikaFillOpacity.value
|
||||
)
|
||||
|
||||
// Curve radius
|
||||
this.setTexData(texture, startIndex + 26, uTroikaCurveRadius.value)
|
||||
// Billboard height
|
||||
this.setTexData(texture, startIndex + 27, text.userData.gradientIndex)
|
||||
|
||||
if (isOutline) {
|
||||
// Outline properties
|
||||
this.setTexData(texture, startIndex + 28, uTroikaPositionOffset.value.x)
|
||||
this.setTexData(texture, startIndex + 29, uTroikaPositionOffset.value.y)
|
||||
this.setTexData(texture, startIndex + 30, uTroikaEdgeOffset.value)
|
||||
this.setTexData(texture, startIndex + 31, uTroikaBlurRadius.value)
|
||||
} else {
|
||||
// Stroke properties
|
||||
this.setTexData(texture, startIndex + 28, uTroikaStrokeWidth.value)
|
||||
this.setTexData(
|
||||
texture,
|
||||
startIndex + 29,
|
||||
tempColor.set(uTroikaStrokeColor.value).getHex()
|
||||
)
|
||||
this.setTexData(texture, startIndex + 30, uTroikaStrokeOpacity.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
material.setMatrixTexture(texture)
|
||||
|
||||
// For the non-member-specific uniforms:
|
||||
Text.prototype._prepareForRender.call(this, material)
|
||||
}
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
import {
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
Color,
|
||||
DoubleSide,
|
||||
Matrix4,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
PlaneGeometry,
|
||||
Quaternion,
|
||||
Raycaster,
|
||||
Vector2,
|
||||
Vector3,
|
||||
Vector4,
|
||||
type Intersection
|
||||
} from 'three'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
import { Text } from 'troika-three-text'
|
||||
import SpeckleBasicMaterial from '../materials/SpeckleBasicMaterial.js'
|
||||
import { ObjectLayers, type SpeckleObject } from '../../IViewer.js'
|
||||
|
||||
export interface SpeckleTextParams {
|
||||
textValue?: string
|
||||
richTextValue?: string
|
||||
height?: number
|
||||
anchorX?: string
|
||||
anchorY?: string
|
||||
}
|
||||
|
||||
export interface SpeckleTextStyle {
|
||||
backgroundColor?: Color | null
|
||||
backgroundCornerRadius?: number
|
||||
backgroundPixelHeight?: number
|
||||
textColor?: Color
|
||||
billboard?: boolean
|
||||
}
|
||||
|
||||
const DefaultSpeckleTextStyle: SpeckleTextStyle = {
|
||||
backgroundColor: null,
|
||||
backgroundCornerRadius: 1,
|
||||
backgroundPixelHeight: 50,
|
||||
textColor: new Color(0xffffff),
|
||||
billboard: false
|
||||
}
|
||||
|
||||
/** TO DO: This is weird because in the billboarded scenario, background size is
|
||||
* specified in pixels inside the style, yet we still rely on the actual world space
|
||||
* background rectangle to be a factor larger than the text itself
|
||||
* This needs to be looked and probably eliminated, but it does not currently break functionality
|
||||
*/
|
||||
const BACKGROUND_OVERSIZE = 1.2
|
||||
|
||||
export class SpeckleText extends Mesh {
|
||||
private _layer: ObjectLayers = ObjectLayers.NONE
|
||||
private _text: Text = null
|
||||
private _background: Mesh | null = null
|
||||
private _backgroundSize: Vector3 = new Vector3()
|
||||
private _style: SpeckleTextStyle = Object.assign({}, DefaultSpeckleTextStyle)
|
||||
private _resolution: Vector2 = new Vector2()
|
||||
|
||||
private defaultMaterial = /*#__PURE__*/ new MeshBasicMaterial({
|
||||
color: 0xffffff,
|
||||
side: DoubleSide,
|
||||
transparent: true
|
||||
})
|
||||
private getFlatRaycastMesh = () => {
|
||||
const mesh = new Mesh(new PlaneGeometry(1, 1), this.defaultMaterial)
|
||||
this.getFlatRaycastMesh = () => mesh
|
||||
return mesh
|
||||
}
|
||||
private getCurvedRaycastMesh = () => {
|
||||
const mesh = new Mesh(new PlaneGeometry(1, 1, 32, 1), this.defaultMaterial)
|
||||
this.getCurvedRaycastMesh = () => mesh
|
||||
return mesh
|
||||
}
|
||||
|
||||
public static SpeckleTextParamsFromMetadata(metadata: SpeckleObject) {
|
||||
return {
|
||||
textValue: metadata.value ? metadata.value : 'N/A',
|
||||
height: metadata.height
|
||||
} as SpeckleTextParams
|
||||
}
|
||||
|
||||
public get textMesh() {
|
||||
return this._text
|
||||
}
|
||||
|
||||
public get backgroundMesh() {
|
||||
return this._background
|
||||
}
|
||||
|
||||
public set style(value: SpeckleTextStyle) {
|
||||
Object.assign(this._style, value)
|
||||
this.updateStyle()
|
||||
}
|
||||
|
||||
public constructor(uuid: string, layer: ObjectLayers) {
|
||||
super()
|
||||
this.uuid = uuid
|
||||
this._layer = layer
|
||||
this._text = new Text()
|
||||
this._text.depthOffset = -0.1
|
||||
this._text.raycast = () => {
|
||||
/** We're erasing the child's raycast so we don't raycast twice
|
||||
* Not the best approach but until we figure out text batching it will have to suffice
|
||||
*/
|
||||
}
|
||||
this.layers.set(this._layer)
|
||||
this._text.layers.set(this._layer)
|
||||
this.add(this._text)
|
||||
|
||||
this.onBeforeRender = (renderer) => {
|
||||
renderer.getDrawingBufferSize(this._resolution)
|
||||
}
|
||||
/** Otherwise three.js is inconsistent in calling our 'onBeforeRender' */
|
||||
this.frustumCulled = false
|
||||
}
|
||||
|
||||
public async update(params: SpeckleTextParams, updateFinished?: () => void) {
|
||||
return new Promise<void>((resolve) => {
|
||||
if (params.textValue) {
|
||||
this._text.text = params.textValue
|
||||
}
|
||||
if (params.richTextValue) {
|
||||
//TO DO
|
||||
}
|
||||
if (params.height) {
|
||||
this._text.fontSize = params.height
|
||||
}
|
||||
this._text.anchorX = params.anchorX
|
||||
this._text.anchorY = params.anchorY
|
||||
if (this._text._needsSync) {
|
||||
this._text.sync(() => {
|
||||
resolve()
|
||||
if (updateFinished) updateFinished()
|
||||
})
|
||||
} else {
|
||||
resolve()
|
||||
if (updateFinished) updateFinished()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public setTransform(position?: Vector3, quaternion?: Quaternion, scale?: Vector3) {
|
||||
if (position) {
|
||||
if (this._style.billboard) {
|
||||
this.textMesh.material.userData.billboardPos.value.copy(position)
|
||||
if (this._background) {
|
||||
const textSize = this.textMesh.geometry.boundingBox.getSize(new Vector3())
|
||||
const textCenter = this.textMesh.geometry.boundingBox.getCenter(new Vector3())
|
||||
const offset = new Vector3()
|
||||
.copy(textCenter)
|
||||
.multiplyScalar(BACKGROUND_OVERSIZE)
|
||||
const sizeOffset = new Vector3()
|
||||
.copy(textSize)
|
||||
.multiplyScalar(BACKGROUND_OVERSIZE)
|
||||
.sub(textSize)
|
||||
offset.x +=
|
||||
textCenter.x < 0 ? sizeOffset.x : textCenter.x > 0 ? -sizeOffset.x : 0
|
||||
offset.y +=
|
||||
textCenter.y < 0 ? sizeOffset.y : textCenter.y > 0 ? -sizeOffset.y : 0
|
||||
;(this._background.material as SpeckleBasicMaterial).billboardOffset =
|
||||
new Vector2(offset.x, offset.y)
|
||||
;(
|
||||
this._background.material as SpeckleBasicMaterial
|
||||
).userData.billboardPos.value.copy(position)
|
||||
}
|
||||
}
|
||||
this.position.copy(position)
|
||||
}
|
||||
if (quaternion) this.quaternion.copy(quaternion)
|
||||
if (scale) this.scale.copy(scale)
|
||||
}
|
||||
|
||||
public raycast(raycaster: Raycaster, intersects: Array<Intersection>) {
|
||||
const { textRenderInfo, curveRadius } = this.textMesh
|
||||
if (textRenderInfo) {
|
||||
const bounds = textRenderInfo.blockBounds
|
||||
const raycastMesh = curveRadius
|
||||
? this.getCurvedRaycastMesh()
|
||||
: this.getFlatRaycastMesh()
|
||||
const geom = raycastMesh.geometry
|
||||
const { position, uv } = geom.attributes
|
||||
for (let i = 0; i < uv.count; i++) {
|
||||
let x = bounds[0] + uv.getX(i) * (bounds[2] - bounds[0])
|
||||
const y = bounds[1] + uv.getY(i) * (bounds[3] - bounds[1])
|
||||
let z = 0
|
||||
if (curveRadius) {
|
||||
z = curveRadius - Math.cos(x / curveRadius) * curveRadius
|
||||
x = Math.sin(x / curveRadius) * curveRadius
|
||||
}
|
||||
if (this.textMesh.material.defines['BILLBOARD_FIXED']) {
|
||||
if (this._resolution.length() === 0) return
|
||||
const backgroundSizeIncrease = this._background ? BACKGROUND_OVERSIZE : 1
|
||||
const billboardSize = new Vector2().set(
|
||||
(this.textMesh.material.billboardPixelHeight / this._resolution.x) *
|
||||
2 *
|
||||
backgroundSizeIncrease,
|
||||
(this.textMesh.material.billboardPixelHeight / this._resolution.y) *
|
||||
2 *
|
||||
backgroundSizeIncrease
|
||||
)
|
||||
|
||||
const invProjection = new Matrix4()
|
||||
.copy(raycaster.camera.projectionMatrix)
|
||||
.invert()
|
||||
const invView = new Matrix4()
|
||||
.copy(raycaster.camera.matrixWorldInverse)
|
||||
.invert()
|
||||
|
||||
const clip = new Vector4(
|
||||
this.position.x,
|
||||
this.position.y,
|
||||
this.position.z,
|
||||
1.0
|
||||
)
|
||||
.applyMatrix4(raycaster.camera.matrixWorldInverse)
|
||||
.applyMatrix4(raycaster.camera.projectionMatrix)
|
||||
const pDiv = clip.w
|
||||
clip.multiplyScalar(1 / pDiv)
|
||||
clip.add(new Vector4(x * billboardSize.x, y * billboardSize.y, 0, 0))
|
||||
clip.multiplyScalar(pDiv)
|
||||
clip.applyMatrix4(invProjection)
|
||||
clip.applyMatrix4(invView)
|
||||
position.setXYZ(i, clip.x, clip.y, clip.z)
|
||||
} else {
|
||||
position.setXYZ(i, x, y, z)
|
||||
}
|
||||
}
|
||||
if (this.textMesh.material.defines['BILLBOARD_FIXED']) {
|
||||
geom.computeBoundingBox()
|
||||
geom.computeBoundingSphere()
|
||||
raycastMesh.matrixWorld.identity()
|
||||
} else {
|
||||
geom.boundingSphere = this.textMesh.geometry.boundingSphere
|
||||
geom.boundingBox = this.textMesh.geometry.boundingBox
|
||||
raycastMesh.matrixWorld = this.textMesh.matrixWorld
|
||||
}
|
||||
raycastMesh.material.side = this.textMesh.material.side
|
||||
const tempArray: Array<Intersection> = []
|
||||
raycastMesh.raycast(raycaster, tempArray)
|
||||
for (let i = 0; i < tempArray.length; i++) {
|
||||
tempArray[i].object = this
|
||||
intersects.push(tempArray[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateStyle() {
|
||||
this.updateBackground()
|
||||
}
|
||||
|
||||
private updateBackground() {
|
||||
if (!this._style.backgroundColor) {
|
||||
if (this._background) this.remove(this._background)
|
||||
this._background = null
|
||||
return
|
||||
}
|
||||
|
||||
this._text.geometry.computeBoundingBox()
|
||||
const sizeBox = this._text.geometry.boundingBox.getSize(new Vector3())
|
||||
const sizeDelta = sizeBox.distanceTo(this._backgroundSize)
|
||||
let geometry = this._background?.geometry
|
||||
if (sizeDelta > 0.1) {
|
||||
/** BACKGROUND_OVERSIZE should not be required for billboarded backgrounds. Weird */
|
||||
geometry = this.RectangleRounded(
|
||||
sizeBox.x * BACKGROUND_OVERSIZE,
|
||||
sizeBox.y * BACKGROUND_OVERSIZE,
|
||||
0.5,
|
||||
5
|
||||
)
|
||||
geometry.computeBoundingBox()
|
||||
this._backgroundSize.copy(sizeBox)
|
||||
if (this._background) this._background.geometry = geometry
|
||||
}
|
||||
if (this._background === null) {
|
||||
const material = new SpeckleBasicMaterial({}, ['BILLBOARD_FIXED'])
|
||||
material.toneMapped = false
|
||||
material.side = DoubleSide
|
||||
material.depthTest = false
|
||||
|
||||
this._background = new Mesh(geometry, material)
|
||||
this._background.layers.set(this._layer)
|
||||
this._background.frustumCulled = false
|
||||
this._background.renderOrder = 1
|
||||
this.add(this._background)
|
||||
}
|
||||
const color = new Color(this._style.backgroundColor).convertSRGBToLinear()
|
||||
;(this._background.material as SpeckleBasicMaterial).color = color
|
||||
;(this._background.material as SpeckleBasicMaterial).billboardPixelHeight =
|
||||
(this._style.backgroundPixelHeight !== undefined
|
||||
? this._style.backgroundPixelHeight
|
||||
: DefaultSpeckleTextStyle.backgroundPixelHeight || 0) * window.devicePixelRatio
|
||||
}
|
||||
|
||||
/** From https://discourse.threejs.org/t/roundedrectangle-squircle/28645 */
|
||||
// width, height, radiusCorner, smoothness
|
||||
private RectangleRounded(w: number, h: number, r: number, s: number) {
|
||||
// width, height, radiusCorner, smoothness
|
||||
|
||||
const pi2 = Math.PI * 2
|
||||
const n = (s + 1) * 4 // number of segments
|
||||
const indices = []
|
||||
const positions = []
|
||||
const uvs = []
|
||||
let qu, sgx, sgy, x, y
|
||||
|
||||
for (let j = 1; j < n; j++) indices.push(0, j, j + 1) // 0 is center
|
||||
indices.push(0, n, 1)
|
||||
positions.push(0, 0, 0) // rectangle center
|
||||
uvs.push(0.5, 0.5)
|
||||
for (let j = 0; j < n; j++) contour(j)
|
||||
|
||||
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.computeBoundingBox()
|
||||
return geometry
|
||||
|
||||
function contour(j: number) {
|
||||
qu = Math.trunc((4 * j) / n) + 1 // quadrant qu: 1..4
|
||||
sgx = qu === 1 || qu === 4 ? 1 : -1 // signum left/right
|
||||
sgy = qu < 3 ? 1 : -1 // signum top / bottom
|
||||
x = sgx * (w / 2 - r) + r * Math.cos((pi2 * (j - qu + 1)) / (n - 4)) // corner center + circle
|
||||
y = sgy * (h / 2 - r) + r * Math.sin((pi2 * (j - qu + 1)) / (n - 4))
|
||||
|
||||
positions.push(x, y, 0)
|
||||
uvs.push(0.5 + x / w, 0.5 + y / h)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
import {
|
||||
Box3,
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
Color,
|
||||
DoubleSide,
|
||||
Float32BufferAttribute,
|
||||
FrontSide,
|
||||
Int16BufferAttribute,
|
||||
Material,
|
||||
Matrix4,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
Raycaster,
|
||||
Vector2,
|
||||
Vector3,
|
||||
Vector4,
|
||||
type Intersection
|
||||
} from 'three'
|
||||
import { AnchorX, AnchorY, Text } from 'troika-three-text'
|
||||
import SpeckleBasicMaterial, {
|
||||
BillboardingType
|
||||
} from '../materials/SpeckleBasicMaterial.js'
|
||||
import SpeckleTextMaterial from '../materials/SpeckleTextMaterial.js'
|
||||
import { ObjectLayers } from '../../index.js'
|
||||
import Logger from '../utils/Logger.js'
|
||||
|
||||
const _mat40: Matrix4 = new Matrix4()
|
||||
const _mat41: Matrix4 = new Matrix4()
|
||||
const _box3: Box3 = new Box3()
|
||||
const _vec3: Vector3 = new Vector3()
|
||||
const _vec4: Vector4 = new Vector4()
|
||||
const quadVerts = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]
|
||||
|
||||
export interface TextLabelParams {
|
||||
text?: string
|
||||
fontSize?: number
|
||||
maxWidth?: number
|
||||
anchorX?: AnchorX
|
||||
anchorY?: AnchorY
|
||||
billboard?: BillboardingType | null
|
||||
backgroundColor?: Color | null
|
||||
backgroundCornerRadius?: number
|
||||
backgroundMargins?: Vector2
|
||||
textColor?: Color
|
||||
textOpacity?: number
|
||||
objectLayer?: ObjectLayers
|
||||
}
|
||||
|
||||
/** Screen */
|
||||
export const DefaultTextLabelParams: Required<TextLabelParams> = {
|
||||
text: 'Test Text',
|
||||
fontSize: 40,
|
||||
maxWidth: Number.POSITIVE_INFINITY,
|
||||
anchorX: 'left',
|
||||
anchorY: 'middle',
|
||||
billboard: 'screen',
|
||||
backgroundColor: new Color(0xff0000),
|
||||
backgroundCornerRadius: 0.5,
|
||||
backgroundMargins: new Vector2(50, 10),
|
||||
textColor: new Color(0x00ffff),
|
||||
textOpacity: 1,
|
||||
objectLayer: ObjectLayers.OVERLAY
|
||||
}
|
||||
|
||||
// /** World Billboard*/
|
||||
// export const DefaultTextLabelParams: Required<TextLabelParams> = {
|
||||
// text: 'Test Text',
|
||||
// fontSize: 1,
|
||||
// maxWidth: Number.POSITIVE_INFINITY,
|
||||
// anchorX: 'left',
|
||||
// anchorY: 'middle',
|
||||
// billboard: 'world',
|
||||
// backgroundColor: new Color(0xff0000),
|
||||
// backgroundCornerRadius: 0.5,
|
||||
// backgroundMargins: new Vector2(0.75, 0.1),
|
||||
// textColor: new Color(0x00ffff),
|
||||
// textOpacity: 1,
|
||||
// objectLayer: ObjectLayers.OVERLAY
|
||||
// }
|
||||
|
||||
// /** World */
|
||||
// export const DefaultTextLabelParams: Required<TextLabelParams> = {
|
||||
// text: 'Test Text',
|
||||
// fontSize: 1,
|
||||
// maxWidth: Number.POSITIVE_INFINITY,
|
||||
// anchorX: 'center',
|
||||
// anchorY: 'middle',
|
||||
// billboard: null,
|
||||
// backgroundColor: new Color(0xff0000),
|
||||
// backgroundCornerRadius: 0.5,
|
||||
// backgroundMargins: new Vector2(0.75, 0.1),
|
||||
// textColor: new Color(0x00ffff),
|
||||
// textOpacity: 1,
|
||||
// objectLayer: ObjectLayers.OVERLAY
|
||||
// }
|
||||
|
||||
export class TextLabel extends Text {
|
||||
/** Needs a raycast to start rendering */
|
||||
private readonly DEBUG_BILLBOARDS = false
|
||||
|
||||
declare material: SpeckleTextMaterial
|
||||
|
||||
private _background: Mesh
|
||||
private _backgroundMaterial: SpeckleBasicMaterial
|
||||
private _params: Required<TextLabelParams> = Object.assign({}, DefaultTextLabelParams)
|
||||
private _textBounds: Box3 = new Box3()
|
||||
private _collisionMesh: Mesh
|
||||
public get textMesh() {
|
||||
return this
|
||||
}
|
||||
|
||||
public get backgroundMesh() {
|
||||
return this._background
|
||||
}
|
||||
|
||||
public get textBounds(): Box3 {
|
||||
return this._textBounds
|
||||
}
|
||||
|
||||
public get backgroundMaterial(): SpeckleBasicMaterial {
|
||||
return this._backgroundMaterial
|
||||
}
|
||||
|
||||
public constructor(params: TextLabelParams = DefaultTextLabelParams) {
|
||||
super()
|
||||
this.depthOffset = -0.1
|
||||
|
||||
this.material = new SpeckleTextMaterial({}).getDerivedMaterial()
|
||||
this.material.toneMapped = false
|
||||
|
||||
this._backgroundMaterial = new SpeckleBasicMaterial({})
|
||||
this._backgroundMaterial.toneMapped = false
|
||||
|
||||
this._background = new Mesh(undefined, this._backgroundMaterial)
|
||||
/** Otherwise three.js looses it's shit when rendering it billboarded */
|
||||
this._background.frustumCulled = false
|
||||
/** No raycasting for the background */
|
||||
this._background.raycast = () => {}
|
||||
|
||||
const geometry = new BufferGeometry()
|
||||
geometry.setAttribute(
|
||||
'position',
|
||||
new Float32BufferAttribute(new Array(12).fill(0), 3)
|
||||
)
|
||||
geometry.setIndex(
|
||||
// prettier-ignore
|
||||
new Int16BufferAttribute(
|
||||
[
|
||||
0, 1, 2, // First triangle: bottom-left → bottom-right → top-right
|
||||
0, 2, 3 // Second triangle: bottom-left → top-right → top-left
|
||||
],
|
||||
1
|
||||
)
|
||||
)
|
||||
this._collisionMesh = new Mesh(
|
||||
geometry,
|
||||
new MeshBasicMaterial({ color: 0x00ff00, wireframe: true })
|
||||
)
|
||||
this._collisionMesh.name = 'TextLabel_Collision_Mesh'
|
||||
this._collisionMesh.renderOrder = 1
|
||||
this._collisionMesh.visible = this.DEBUG_BILLBOARDS
|
||||
this.add(this._collisionMesh)
|
||||
|
||||
this.updateParams(params).then().catch
|
||||
}
|
||||
|
||||
public async updateParams(params: TextLabelParams, onUpdateComplete?: () => void) {
|
||||
return new Promise<void>((resolve) => {
|
||||
if (this.material && !(this.material instanceof SpeckleTextMaterial)) {
|
||||
const mat: Material = this.material
|
||||
Logger.error(
|
||||
`TextLabel requires a SpeckleTextMaterial instance. Found ${mat.constructor.name}`
|
||||
)
|
||||
}
|
||||
|
||||
/** Automatically scale with DPR */
|
||||
const transformedParams = Object.assign({}, params)
|
||||
if (params.billboard === 'screen') {
|
||||
if (transformedParams.backgroundMargins)
|
||||
transformedParams.backgroundMargins.multiplyScalar(window.devicePixelRatio)
|
||||
if (transformedParams.fontSize) {
|
||||
transformedParams.fontSize *= window.devicePixelRatio
|
||||
}
|
||||
this.material.side = FrontSide
|
||||
this._backgroundMaterial.side = FrontSide
|
||||
} else {
|
||||
this.material.side = DoubleSide
|
||||
this._backgroundMaterial.side = DoubleSide
|
||||
}
|
||||
|
||||
if (transformedParams.text) this.text = transformedParams.text
|
||||
if (transformedParams.fontSize) this.fontSize = transformedParams.fontSize
|
||||
if (transformedParams.anchorX) this.anchorX = transformedParams.anchorX
|
||||
if (transformedParams.anchorY) this.anchorY = transformedParams.anchorY
|
||||
if (transformedParams.maxWidth) this.maxWidth = transformedParams.maxWidth
|
||||
|
||||
if (transformedParams.textColor !== undefined) {
|
||||
this.material.color.copy(transformedParams.textColor)
|
||||
this.material.color.convertSRGBToLinear()
|
||||
}
|
||||
if (transformedParams.textOpacity !== undefined)
|
||||
this.material.opacity = transformedParams.textOpacity
|
||||
|
||||
if (transformedParams.objectLayer !== undefined) {
|
||||
this.layers.set(transformedParams.objectLayer)
|
||||
this._collisionMesh.layers.set(transformedParams.objectLayer)
|
||||
this._background.layers.set(transformedParams.objectLayer)
|
||||
}
|
||||
|
||||
this.material.needsUpdate = true
|
||||
Object.assign(this._params, transformedParams)
|
||||
|
||||
if (this._needsSync) {
|
||||
this.sync(() => {
|
||||
this.textBoundsToBox(this._textBounds)
|
||||
this.updateBackground()
|
||||
this.updateBillboarding()
|
||||
|
||||
if (onUpdateComplete) onUpdateComplete()
|
||||
resolve()
|
||||
})
|
||||
} else {
|
||||
if (onUpdateComplete) onUpdateComplete()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public raycast(raycaster: Raycaster, intersects: Array<Intersection>) {
|
||||
/** No billboarding, default raycasting works fine */
|
||||
if (!this._params.billboard) {
|
||||
super.raycast(raycaster, intersects)
|
||||
return
|
||||
}
|
||||
/** If we have a billboard, we need to update the collision mesh */
|
||||
const textMatrix = this.matrixWorld
|
||||
const textMatrixInv = _mat40.copy(textMatrix).invert()
|
||||
|
||||
const billboardPos = new Vector3().set(
|
||||
textMatrix.elements[12],
|
||||
textMatrix.elements[13],
|
||||
textMatrix.elements[14]
|
||||
)
|
||||
|
||||
/** World space billboarding */
|
||||
if (this._params.billboard === 'world') {
|
||||
const box = new Box3().copy(
|
||||
this._params.backgroundColor !== null
|
||||
? (this._background.geometry.boundingBox as Box3)
|
||||
: this._textBounds
|
||||
)
|
||||
const min = new Vector3().copy(box.min)
|
||||
const max = new Vector3().copy(box.max)
|
||||
quadVerts[0].set(min.x, min.y, 0)
|
||||
quadVerts[1].set(max.x, min.y, 0)
|
||||
quadVerts[2].set(max.x, max.y, 0)
|
||||
quadVerts[3].set(min.x, max.y, 0)
|
||||
|
||||
const cameraRotationMatrix = _mat41.extractRotation(raycaster.camera.matrixWorld)
|
||||
|
||||
const billboardMat = new Matrix4().makeTranslation(
|
||||
billboardPos.x,
|
||||
billboardPos.y,
|
||||
billboardPos.z
|
||||
)
|
||||
billboardMat.premultiply(textMatrixInv)
|
||||
billboardMat.multiply(cameraRotationMatrix)
|
||||
|
||||
for (let i = 0; i < quadVerts.length; i++) {
|
||||
quadVerts[i].applyMatrix4(billboardMat)
|
||||
|
||||
this._collisionMesh.geometry.attributes.position.setXYZ(
|
||||
i,
|
||||
quadVerts[i].x,
|
||||
quadVerts[i].y,
|
||||
quadVerts[i].z
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Screen space billboarding */
|
||||
if (this._params.billboard === 'screen') {
|
||||
const box = new Box3().copy(this._textBounds)
|
||||
if (box.getSize(new Vector3()).length() === 0) return
|
||||
if (box.isInfiniteBox()) return
|
||||
|
||||
const min = new Vector3().copy(box.min)
|
||||
const max = new Vector3().copy(box.max)
|
||||
quadVerts[0].set(min.x, min.y, 0)
|
||||
quadVerts[1].set(max.x, min.y, 0)
|
||||
quadVerts[2].set(max.x, max.y, 0)
|
||||
quadVerts[3].set(min.x, max.y, 0)
|
||||
|
||||
const billboardSize =
|
||||
this._params.backgroundColor !== null
|
||||
? this._backgroundMaterial.userData.billboardPixelOffsetSize.value
|
||||
: this._backgroundMaterial.userData.billboardPixelOffsetSize.value
|
||||
const invProjection = raycaster.camera.projectionMatrixInverse
|
||||
const invView = raycaster.camera.matrixWorld
|
||||
|
||||
const clip = new Vector4(billboardPos.x, billboardPos.y, billboardPos.z, 1.0)
|
||||
.applyMatrix4(raycaster.camera.matrixWorldInverse)
|
||||
.applyMatrix4(raycaster.camera.projectionMatrix)
|
||||
const pDiv = clip.w
|
||||
clip.multiplyScalar(1 / pDiv)
|
||||
|
||||
for (let i = 0; i < quadVerts.length; i++) {
|
||||
_vec3.copy(quadVerts[i])
|
||||
_vec3.multiply(new Vector3(billboardSize.z * 2, billboardSize.w * 2, 0))
|
||||
_vec3.add(new Vector3(billboardSize.x * 2, billboardSize.y * 2, 0))
|
||||
_vec4.set(clip.x, clip.y, clip.z, 1)
|
||||
_vec4.add(new Vector4(_vec3.x, _vec3.y, 0, 0))
|
||||
_vec4.multiplyScalar(pDiv)
|
||||
_vec4.applyMatrix4(invProjection)
|
||||
_vec4.applyMatrix4(invView)
|
||||
_vec4.applyMatrix4(textMatrixInv)
|
||||
|
||||
this._collisionMesh.geometry.attributes.position.setXYZ(
|
||||
i,
|
||||
_vec4.x,
|
||||
_vec4.y,
|
||||
_vec4.z
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this._collisionMesh.geometry.attributes.position.needsUpdate = true
|
||||
this._collisionMesh.geometry.computeBoundingBox()
|
||||
this._collisionMesh.geometry.computeBoundingSphere()
|
||||
|
||||
/** No need to manually call. _collisionMesh is a child and will get automatically raycasted */
|
||||
this._collisionMesh.raycast(raycaster, intersects)
|
||||
// super.raycast(raycaster, intersects)
|
||||
}
|
||||
|
||||
/** Gets the current bounds reported by troika taking `fontSize` into account */
|
||||
private textBoundsToBox(target: Box3 = new Box3()): Box3 {
|
||||
const { textRenderInfo } = this
|
||||
/** visibleBounds generally is a better fit, *however* it reports faulty on some glyphs and messes up the text size */
|
||||
const bounds = textRenderInfo.visibleBounds
|
||||
|
||||
const vertices = []
|
||||
vertices.push(
|
||||
bounds[0],
|
||||
bounds[3],
|
||||
0,
|
||||
bounds[2],
|
||||
bounds[3],
|
||||
0,
|
||||
bounds[0],
|
||||
bounds[1],
|
||||
0,
|
||||
bounds[2],
|
||||
bounds[1],
|
||||
0
|
||||
)
|
||||
target.setFromArray(vertices)
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
/** Text's blockBounds, the one we're working with bounds-wise is not a unit quad
|
||||
When using BILLBOARD_SCREEN we store the desired pixel size in the text's `fontSize` property
|
||||
This makes troika compute a large text since it thinks our pixels are world units.
|
||||
So we divide the text bounds by the font size to get the size of the unit text bounds, or
|
||||
another way of putting it, to compute the text bounds value as if fontSize = 1
|
||||
From the unit box, we get it's size and compute a world->pixel ratio which we send to the shader
|
||||
*/
|
||||
private updateBillboarding() {
|
||||
this.material.setBillboarding(this._params.billboard)
|
||||
this._backgroundMaterial.setBillboarding(this._params.billboard)
|
||||
|
||||
if (this._params.billboard === 'screen') {
|
||||
/** Get the current bounds */
|
||||
const bounds = _box3.copy(this._textBounds)
|
||||
/** The fontSize is the pixel value so we normalize */
|
||||
bounds.min.divideScalar(this.fontSize)
|
||||
bounds.max.divideScalar(this.fontSize)
|
||||
/** This is the size of the quad for the particular text value */
|
||||
let unitSize = bounds.getSize(_vec3)
|
||||
/** We need to keep aspect ratio for text */
|
||||
this.material.billboardPixelSize = new Vector2(1 / unitSize.y, 1 / unitSize.y)
|
||||
/** Same thing for background */
|
||||
if (!this._background.geometry.boundingBox)
|
||||
this._background.geometry.computeBoundingBox()
|
||||
const bgBounds = new Box3().copy(this._background.geometry.boundingBox as Box3)
|
||||
bgBounds.min.divideScalar(this.fontSize)
|
||||
bgBounds.max.divideScalar(this.fontSize)
|
||||
unitSize = bgBounds.getSize(_vec3)
|
||||
|
||||
const margins = new Vector2(
|
||||
this._params.backgroundMargins?.x ?? 0,
|
||||
this._params.backgroundMargins?.y ?? 0
|
||||
)
|
||||
this._backgroundMaterial.billboardPixelSize = new Vector2(
|
||||
1 / unitSize.y + (margins.x * (1 / unitSize.x)) / this.fontSize,
|
||||
1 / unitSize.y + (margins.y * (1 / unitSize.y)) / this.fontSize
|
||||
)
|
||||
|
||||
const billboardPixelOffset = new Vector2(0, 0)
|
||||
switch (this.anchorX) {
|
||||
case 'left':
|
||||
billboardPixelOffset.x = -margins.x * 0.5
|
||||
break
|
||||
case 'right':
|
||||
billboardPixelOffset.x = margins.x * 0.5
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
switch (this.anchorY) {
|
||||
case 'top':
|
||||
billboardPixelOffset.y = -margins.y * 0.5
|
||||
break
|
||||
case 'bottom':
|
||||
billboardPixelOffset.x = margins.y * 0.5
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
this._backgroundMaterial.billboardPixelOffset = billboardPixelOffset
|
||||
}
|
||||
}
|
||||
|
||||
private updateBackground() {
|
||||
if (!this._params.backgroundColor) {
|
||||
if (this._background) {
|
||||
this._background.geometry.dispose()
|
||||
this.remove(this._background)
|
||||
}
|
||||
return
|
||||
} else if (!this._background.parent) {
|
||||
this.add(this._background)
|
||||
}
|
||||
|
||||
const box = _box3.copy(this._textBounds)
|
||||
const offset = box.getCenter(new Vector3())
|
||||
const boxSize = box.getSize(new Vector3())
|
||||
const radius = this.fontSize * (this._params.backgroundCornerRadius ?? 0)
|
||||
const margins =
|
||||
this._params.billboard !== 'screen'
|
||||
? this._params.backgroundMargins ?? new Vector2()
|
||||
: new Vector2()
|
||||
|
||||
if (!box.isInfiniteBox()) {
|
||||
const geometry = this.RectangleRounded(
|
||||
offset,
|
||||
boxSize.x + margins.x,
|
||||
boxSize.y + margins.y,
|
||||
radius,
|
||||
5
|
||||
)
|
||||
geometry.computeBoundingBox()
|
||||
geometry.computeBoundingSphere()
|
||||
this._background.geometry = geometry
|
||||
}
|
||||
|
||||
const color = new Color(this._params.backgroundColor).convertSRGBToLinear()
|
||||
;(this._background.material as SpeckleBasicMaterial).color = color
|
||||
}
|
||||
|
||||
/** Improved version of https://discourse.threejs.org/t/roundedrectangle-squircle/28645 by way of the vibe */
|
||||
private RectangleRounded(
|
||||
offset: Vector3,
|
||||
w: number,
|
||||
h: number,
|
||||
r: number,
|
||||
s: number,
|
||||
inset = false
|
||||
): BufferGeometry {
|
||||
const positions: number[] = []
|
||||
const uvs: number[] = []
|
||||
const indices: number[] = []
|
||||
|
||||
if (inset) {
|
||||
let maxInset = 0
|
||||
for (let i = 0; i <= s; i++) {
|
||||
const angle = (Math.PI / 2) * (i / (s + 1))
|
||||
const x = r * Math.cos(angle)
|
||||
const inset = r - x
|
||||
if (inset > maxInset) maxInset = inset
|
||||
}
|
||||
w += 2 * maxInset
|
||||
}
|
||||
const radius = Math.min(r, w / 2, h / 2)
|
||||
const segmentsPerCorner = s + 1
|
||||
const pointsPerCorner = segmentsPerCorner + 1
|
||||
const totalPoints = pointsPerCorner * 4
|
||||
|
||||
positions.push(offset.x, offset.y, 0)
|
||||
uvs.push(0.5, 0.5)
|
||||
|
||||
const corners = [
|
||||
{ cx: w / 2 - radius, cy: h / 2 - radius, angleStart: 0 },
|
||||
{ cx: -w / 2 + radius, cy: h / 2 - radius, angleStart: Math.PI / 2 },
|
||||
{ cx: -w / 2 + radius, cy: -h / 2 + radius, angleStart: Math.PI },
|
||||
{ cx: w / 2 - radius, cy: -h / 2 + radius, angleStart: (3 * Math.PI) / 2 }
|
||||
]
|
||||
|
||||
for (let corner = 0; corner < 4; corner++) {
|
||||
const { cx, cy, angleStart } = corners[corner]
|
||||
for (let i = 0; i <= segmentsPerCorner; i++) {
|
||||
const angle = angleStart + (Math.PI / 2) * (i / segmentsPerCorner)
|
||||
const x = cx + radius * Math.cos(angle)
|
||||
const y = cy + radius * Math.sin(angle)
|
||||
|
||||
positions.push(offset.x + x, offset.y + y, 0)
|
||||
uvs.push(0.5 + x / w, 0.5 + y / h)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 1; i <= totalPoints; i++) {
|
||||
const next = i < totalPoints ? i + 1 : 1
|
||||
indices.push(0, i, next)
|
||||
}
|
||||
|
||||
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.computeBoundingBox()
|
||||
|
||||
return geometry
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,6 @@ export class ArcticViewPipeline extends ProgressivePipeline {
|
||||
ObjectLayers.STREAM_CONTENT_LINE,
|
||||
ObjectLayers.STREAM_CONTENT_POINT,
|
||||
ObjectLayers.STREAM_CONTENT_POINT_CLOUD,
|
||||
ObjectLayers.STREAM_CONTENT_TEXT,
|
||||
ObjectLayers.PROPS
|
||||
])
|
||||
viewportPass.setVisibility(ObjectVisibility.OPAQUE)
|
||||
|
||||
@@ -48,7 +48,6 @@ export class DefaultPipeline extends ProgressivePipeline {
|
||||
ObjectLayers.STREAM_CONTENT_LINE,
|
||||
ObjectLayers.STREAM_CONTENT_POINT,
|
||||
ObjectLayers.STREAM_CONTENT_POINT_CLOUD,
|
||||
ObjectLayers.STREAM_CONTENT_TEXT,
|
||||
ObjectLayers.PROPS
|
||||
])
|
||||
opaqueColorPass.setVisibility(ObjectVisibility.OPAQUE)
|
||||
|
||||
@@ -25,7 +25,6 @@ export class SolidViewPipeline extends ProgressivePipeline {
|
||||
ObjectLayers.STREAM_CONTENT_LINE,
|
||||
ObjectLayers.STREAM_CONTENT_POINT,
|
||||
ObjectLayers.STREAM_CONTENT_POINT_CLOUD,
|
||||
ObjectLayers.STREAM_CONTENT_TEXT,
|
||||
ObjectLayers.PROPS
|
||||
])
|
||||
viewportPass.setVisibility(ObjectVisibility.OPAQUE)
|
||||
|
||||
@@ -42,7 +42,6 @@ export class TAAPipeline extends ProgressivePipeline {
|
||||
ObjectLayers.STREAM_CONTENT_LINE,
|
||||
ObjectLayers.STREAM_CONTENT_POINT,
|
||||
ObjectLayers.STREAM_CONTENT_POINT_CLOUD,
|
||||
ObjectLayers.STREAM_CONTENT_TEXT,
|
||||
ObjectLayers.PROPS
|
||||
])
|
||||
opaqueColorPass.setVisibility(ObjectVisibility.OPAQUE)
|
||||
|
||||
@@ -69,6 +69,7 @@ export class RenderTree {
|
||||
node.model.renderView.computeAABB()
|
||||
} else if (node.model.renderView.hasMetadata) {
|
||||
node.model.renderView.renderData.geometry.bakeTransform.premultiply(transform)
|
||||
node.model.renderView.computeAABB()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,3 +150,14 @@ Box3.prototype.intersectOBB = function (obb: OBB): OBB | null {
|
||||
// Step 9: Return the resulting OBB
|
||||
return new OBB(worldCentroid, halfSize, obb.rotation.clone())
|
||||
}
|
||||
|
||||
Box3.prototype.isInfiniteBox = function (): boolean {
|
||||
return (
|
||||
this.min.x === -Infinity ||
|
||||
this.min.y === -Infinity ||
|
||||
this.min.z === -Infinity ||
|
||||
this.max.x === Infinity ||
|
||||
this.max.y === Infinity ||
|
||||
this.max.z === Infinity
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ declare module 'three' {
|
||||
interface Box3 {
|
||||
intersectOBB(obb: OBB): OBB | null
|
||||
fromOBB(obb: OBB): Box3
|
||||
isInfiniteBox(): boolean
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
declare module 'troika-three-text' {
|
||||
import { Mesh, Material, DataTexture, Color } from 'three'
|
||||
|
||||
export type AnchorY = 'middle' | 'top' | 'bottom'
|
||||
export type AnchorX = 'center' | 'left' | 'right' | 'justify'
|
||||
|
||||
export class Text extends Mesh {
|
||||
text: string
|
||||
fontSize: number
|
||||
font: string
|
||||
color: string | number | Color
|
||||
anchorX: AnchorX
|
||||
anchorY: AnchorY
|
||||
maxWidth?: number
|
||||
outlineWidth?: number
|
||||
outlineColor?: string | number | Color
|
||||
outlineOpacity?: number
|
||||
fillOpacity?: number
|
||||
letterSpacing?: number
|
||||
lineHeight?: number
|
||||
curveRadius?: number
|
||||
depthOffset?: number
|
||||
direction?: 'ltr' | 'rtl'
|
||||
_needsSync: boolean
|
||||
get textRenderInfo()
|
||||
|
||||
constructor()
|
||||
|
||||
clone(recursive?: boolean): this
|
||||
copy(source: this, recursive?: boolean): this
|
||||
dispose(): void
|
||||
sync(callback?: () => void): void
|
||||
raycast(...args: unknown[]): void
|
||||
onBeforeRender(...args: unknown[]): void
|
||||
onAfterRender(...args: unknown[]): void
|
||||
localPositionToTextCoords(...args: unknown[]): void
|
||||
worldPositionToTextCoords(...args: unknown[]): void
|
||||
_prepareForRender(material: Material)
|
||||
|
||||
static DefaultMatrixAutoUpdate: boolean
|
||||
static DefaultMatrixWorldAutoUpdate: boolean
|
||||
}
|
||||
|
||||
export class BatchedText extends Text {
|
||||
_members: Map<
|
||||
Text,
|
||||
{
|
||||
index: -1
|
||||
glyphCount: -1
|
||||
dirty: true
|
||||
needsUpdate?: boolean
|
||||
}
|
||||
>
|
||||
addText(text: Text): void
|
||||
_dataTextures: Record<'outline' | 'main', DataTexture>
|
||||
_onMemberSynced: (e) => void
|
||||
removeText(text: Text): void
|
||||
createDerivedMaterial(baseMaterial: Material): Material
|
||||
updateMatrixWorld(force: boolean): void
|
||||
updateBounds(): void
|
||||
sync(callback?: () => void): void
|
||||
copy(source: BatchedText): BatchedText
|
||||
dispose(): void
|
||||
}
|
||||
}
|
||||
@@ -16502,7 +16502,7 @@ __metadata:
|
||||
three: "npm:^0.140.0"
|
||||
three-mesh-bvh: "npm:0.5.17"
|
||||
tree-model: "npm:1.0.7"
|
||||
troika-three-text: "npm:0.47.2"
|
||||
troika-three-text: "npm:0.52.4"
|
||||
type-fest: "npm:^4.15.0"
|
||||
typescript: "npm:^4.5.4"
|
||||
vitest: "npm:^1.4.0"
|
||||
@@ -47218,6 +47218,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"troika-three-text@npm:0.52.4":
|
||||
version: 0.52.4
|
||||
resolution: "troika-three-text@npm:0.52.4"
|
||||
dependencies:
|
||||
bidi-js: "npm:^1.0.2"
|
||||
troika-three-utils: "npm:^0.52.4"
|
||||
troika-worker-utils: "npm:^0.52.0"
|
||||
webgl-sdf-generator: "npm:1.1.1"
|
||||
peerDependencies:
|
||||
three: ">=0.125.0"
|
||||
checksum: 10/bbc0aaaed657b30240b69034543ac71451590e0b7403ae9eadc6b0891b791434185e687c6db545c23433fea0bfe93d67def3e1d120ba73dae74fe08aea95e8ca
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"troika-three-utils@npm:^0.47.2":
|
||||
version: 0.47.2
|
||||
resolution: "troika-three-utils@npm:0.47.2"
|
||||
@@ -47227,6 +47241,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"troika-three-utils@npm:^0.52.4":
|
||||
version: 0.52.4
|
||||
resolution: "troika-three-utils@npm:0.52.4"
|
||||
peerDependencies:
|
||||
three: ">=0.125.0"
|
||||
checksum: 10/cd2382b50584fdbec86c6ab9ac771c777cc937b9f23f40cb3f2fa3f401ba438ffea822171f84fddc7d6537798e4fc1cc8c2f5fe81b04b3e1e6e7bb6c2f228f5c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"troika-worker-utils@npm:^0.47.2":
|
||||
version: 0.47.2
|
||||
resolution: "troika-worker-utils@npm:0.47.2"
|
||||
@@ -47234,6 +47257,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"troika-worker-utils@npm:^0.52.0":
|
||||
version: 0.52.0
|
||||
resolution: "troika-worker-utils@npm:0.52.0"
|
||||
checksum: 10/7b58418a201611f0e350534c6ab6c4fcc0121d6ed3fdaf74b04b8d873e87341122e4f344d48c144d37cce73263d9948db9a7ab61141d90eed3652ebada4b56e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"true-myth@npm:^8.5.0":
|
||||
version: 8.5.0
|
||||
resolution: "true-myth@npm:8.5.0"
|
||||
|
||||
Reference in New Issue
Block a user