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:
Alexandru Popovici
2025-07-15 14:48:13 +03:00
committed by GitHub
parent ed875f0134
commit f3974dd9d0
38 changed files with 2631 additions and 988 deletions
@@ -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 -35
View File
@@ -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',
+41 -3
View File
@@ -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'
)
+1 -1
View File
@@ -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": {
+2 -2
View File
@@ -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,
+32 -11
View File
@@ -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()
+64 -33
View File
@@ -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 {
+369 -67
View File
@@ -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
)
}
+1
View File
@@ -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
}
}
+31 -1
View File
@@ -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"