Implemented the concept of RenderTree which just extends the functionality of the general purpose WorldTree. Moved all geometry conversions to a seaprate source where no async, no fetches, no special things happen, with the intent of possibly moving this to a webworker in the future

This commit is contained in:
AlexandruPopovici
2022-06-03 16:37:06 +03:00
parent 59ad804227
commit fcea53ddd2
7 changed files with 768 additions and 24 deletions
+2 -2
View File
@@ -14,8 +14,8 @@ if (!container) {
// Viewer setup
const params = DefaultViewerParams
params.environmentSrc =
'https://speckle-xyz-assets.ams3.digitaloceanspaces.com/studio010.hdr'
// params.environmentSrc =
// 'https://speckle-xyz-assets.ams3.digitaloceanspaces.com/studio010.hdr'
// 'http://localhost:3033/sample-hdri.exr'
// TODO: Remove this, just a test of image bundling capabilities!
@@ -0,0 +1,22 @@
import { GeometryData } from './converter/Geometry'
import { SpeckleType } from './converter/GeometryConverter'
export interface NodeRenderData {
speckleType: SpeckleType
geometry: GeometryData
batchId: string
batchIndexStart: number
batchIndexCount: number
}
export class NodeRenderView {
private readonly _renderData: { [id: string]: NodeRenderData } = {}
public get renderData() {
return this._renderData
}
public setData(id: string, data: NodeRenderData) {
this._renderData[id] = data
}
}
+159
View File
@@ -0,0 +1,159 @@
import { Geometry } from './converter/Geometry'
import { GeometryConverter, SpeckleType } from './converter/GeometryConverter'
import ObjectWrapper from './converter/ObjectWrapper'
import { TreeNode } from './converter/WorldTree'
import { NodeRenderData, NodeRenderView } from './NodeRenderView'
export class RenderTree {
private root: TreeNode
public constructor(root: TreeNode) {
this.root = root
}
public buildRenderTree() {
this.root.walk((node: TreeNode): boolean => {
let renderView = null
const geometryData = GeometryConverter.convertNodeToGeometryData(node.model)
if (geometryData) {
const renderData: NodeRenderData = {
speckleType: GeometryConverter.getSpeckleType(node.model),
geometry: geometryData,
batchId: 'n/a',
batchIndexStart: 0,
batchIndexCount: 0
}
renderView = new NodeRenderView()
renderView.setData(node.model.id, renderData)
}
node.model.renderView = renderView
return true
})
}
/**
* TEMPORARY
*/
public getObjectWrappers() {
const objectWrappers = []
this.root.walk((node: TreeNode): boolean => {
const renderView: NodeRenderView = node.model.renderView
if (renderView) {
const plm = Object.keys(renderView.renderData)
const renderData: NodeRenderData = renderView.renderData[plm[0]]
switch (renderData.speckleType) {
case SpeckleType.Pointcloud:
objectWrappers.push(
new ObjectWrapper(
Geometry.makePointCloudGeometry(renderData.geometry),
node.model.raw,
'pointcloud'
)
)
break
case SpeckleType.Brep:
objectWrappers.push(
new ObjectWrapper(
Geometry.makeMeshGeometry(renderData.geometry),
node.model.raw
)
)
break
case SpeckleType.Mesh:
objectWrappers.push(
new ObjectWrapper(
Geometry.makeMeshGeometry(renderData.geometry),
node.model.raw
)
)
break
case SpeckleType.Point:
objectWrappers.push(
new ObjectWrapper(
Geometry.makePointGeometry(renderData.geometry),
node.model.raw,
'point'
)
)
break
case SpeckleType.Line:
objectWrappers.push(
new ObjectWrapper(
Geometry.makeLineGeometry(renderData.geometry),
node.model.raw,
'line'
)
)
break
case SpeckleType.Polyline:
objectWrappers.push(
new ObjectWrapper(
Geometry.makeLineGeometry(renderData.geometry),
node.model.raw,
'line'
)
)
break
case SpeckleType.Box:
objectWrappers.push(
new ObjectWrapper(
Geometry.makeMeshGeometry(renderData.geometry),
node.model.raw
)
)
break
case SpeckleType.Polycurve:
objectWrappers.push(
new ObjectWrapper(
Geometry.makeLineGeometry(renderData.geometry),
node.model.raw,
'line'
)
)
break
case SpeckleType.Curve:
objectWrappers.push(
new ObjectWrapper(
Geometry.makeLineGeometry(renderData.geometry),
node.model.raw,
'line'
)
)
break
case SpeckleType.Circle:
objectWrappers.push(
new ObjectWrapper(
Geometry.makeLineGeometry(renderData.geometry),
node.model.raw,
'line'
)
)
break
case SpeckleType.Arc:
objectWrappers.push(
new ObjectWrapper(
Geometry.makeLineGeometry(renderData.geometry),
node.model.raw,
'line'
)
)
break
case SpeckleType.Ellipse:
objectWrappers.push(
new ObjectWrapper(
Geometry.makeLineGeometry(renderData.geometry),
node.model.raw,
'line'
)
)
break
default:
console.warn(`Skipping geometry conversion for ${renderData.speckleType}`)
return null
}
}
return true
})
return objectWrappers
}
}
@@ -1,6 +1,6 @@
import ObjectLoader from '@speckle/objectloader'
import Converter from './converter/Converter'
// import { WorldTree } from './converter/WorldTree'
import { WorldTree } from './converter/WorldTree'
/**
* Helper wrapper around the ObjectLoader class, with some built in assumptions.
@@ -68,7 +68,7 @@ export default class ViewerObjectLoader {
let total = 0
let viewerLoads = 0
let firstObjectPromise = null
const parsedObjects = [] // Temporary until refactor
let parsedObjects = [] // Temporary until refactor
for await (const obj of this.loader.getObjectIterator()) {
if (this.cancel) {
this.viewer.emit('load-progress', {
@@ -107,7 +107,14 @@ export default class ViewerObjectLoader {
// Geometry.applyWorldTransform(parsedObjects)
// Temporary until refactor
// console.log(WorldTree.getInstance().findAll())
WorldTree.getRenderTree().buildRenderTree()
// console.log(
WorldTree.getInstance().findAll((node) => {
return node.model.renderView !== null
})
// )
parsedObjects = WorldTree.getRenderTree().getObjectWrappers()
for (let k = 0; k < parsedObjects.length; k++) {
await this.converter.asyncPause()
this.viewer.sceneManager.addObject(parsedObjects[k])
@@ -26,9 +26,6 @@ export type ConverterNodeDelegate = (object, node) => Promise<void>
* Warning: HIC SVNT DRACONES.
*/
export default class Coverter {
PolylineToNodebind(arg0: this): ConverterNodeDelegate {
throw new Error('Method not implemented.')
}
private objectLoader
private curveSegmentLength: number
private lastAsyncPause: number
@@ -169,10 +166,10 @@ export default class Coverter {
})
if (node === null) {
WorldTree.getInstance().addNode(childNode, node)
console.warn(`Added root node with id ${obj.id}`)
// console.warn(`Added root node with id ${obj.id}`)
} else {
WorldTree.getInstance().addNode(childNode, node)
console.warn(`Added child node with id ${obj.id} to parent node ${node.model.id}`)
// console.warn(`Added child node with id ${obj.id} to parent node ${node.model.id}`)
}
// Keep track of parents. An object is his own parent, for the simplicity of working with subtrees
@@ -434,9 +431,9 @@ export default class Coverter {
children: []
})
WorldTree.getInstance().addNode(childNode, node)
console.warn(
`Added child node with id ${childNode.model.id} to parent node ${node.model.id}`
)
// console.warn(
// `Added child node with id ${childNode.model.id} to parent node ${node.model.id}`
// )
await this.convertToNode(ref, childNode)
}
@@ -504,6 +501,7 @@ export default class Coverter {
}
private async PolycurveToNode(obj, node) {
node.model.raw.segments = []
for (let i = 0; i < obj.segments.length; i++) {
let element = obj.segments[i]
const nestedNode: TreeNode = WorldTree.getInstance().parse({
@@ -517,6 +515,7 @@ export default class Coverter {
} else if ((element = this.getDisplayValue(element)) !== undefined) {
await this.convertToNode(element, nestedNode)
}
node.model.raw.segments[i] = nestedNode
}
}
@@ -529,6 +528,7 @@ export default class Coverter {
geometry: null,
children: []
})
await this.convertToNode(displayValue, nestedNode)
node.model.raw.displayValue = nestedNode
}
@@ -0,0 +1,545 @@
import {
BoxBufferGeometry,
EllipseCurve,
Line3,
Matrix4,
Vector2,
Vector3
} from 'three'
import { Geometry, GeometryData } from './Geometry'
import MeshTriangulationHelper from './MeshTriangulationHelper'
import { getConversionFactor } from './Units'
import { NodeData } from './WorldTree'
export enum SpeckleType {
View3D = 'View3D',
BlockInstance = 'BlockInstance',
Pointcloud = 'Pointcloud',
Brep = 'Brep',
Mesh = 'Mesh',
Point = 'Point',
Line = 'Line',
Polyline = 'Polyline',
Box = 'Box',
Polycurve = 'Polycurve',
Curve = 'Curve',
Circle = 'Circle',
Arc = 'Arc',
Ellipse = 'Ellipse',
Unknown = 'Unknown'
}
export class GeometryConverter {
public static getSpeckleType(node: NodeData): SpeckleType {
let type = 'Base'
if (node.raw.data)
type = node.raw.data.speckle_type
? node.raw.data.speckle_type.split('.').reverse()[0]
: type
else
type = node.raw.speckle_type
? node.raw.speckle_type.split('.').reverse()[0]
: type
if (type in SpeckleType) return type as SpeckleType
else return SpeckleType.Unknown
}
public static convertNodeToGeometryData(node: NodeData): GeometryData {
const type = GeometryConverter.getSpeckleType(node)
switch (type) {
case SpeckleType.Pointcloud:
return GeometryConverter.PointcloudToGeometryData(node)
case SpeckleType.Brep:
return GeometryConverter.BrepToGeometryData(node)
case SpeckleType.Mesh:
return GeometryConverter.MeshToGeometryData(node)
case SpeckleType.Point:
return GeometryConverter.PointToGeometryData(node)
case SpeckleType.Line:
return GeometryConverter.LineToGeometryData(node)
case SpeckleType.Polyline:
return GeometryConverter.PolylineToGeometryData(node)
case SpeckleType.Box:
return GeometryConverter.BoxToGeometryData(node)
case SpeckleType.Polycurve:
return GeometryConverter.PolycurveToGeometryData(node)
case SpeckleType.Curve:
return GeometryConverter.CurveToGeometryData(node)
case SpeckleType.Circle:
return GeometryConverter.CircleToGeometryData(node)
case SpeckleType.Arc:
return GeometryConverter.ArcToGeometryData(node)
case SpeckleType.Ellipse:
return GeometryConverter.EllipseToGeometryData(node)
default:
console.warn(`Skipping geometry conversion for ${type}`)
return null
}
}
/**
* POINT CLOUD
*/
private static PointcloudToGeometryData(node: NodeData) {
const conversionFactor = getConversionFactor(node.raw.units)
const vertices = node.raw.points
const colorsRaw = node.raw.colors
let colors = null
if (colorsRaw && colorsRaw.length !== 0) {
if (colorsRaw.length !== vertices.length / 3) {
console.warn(
`Mesh (id ${node.raw.id}) colours are mismatched with vertice counts. The number of colours must equal the number of vertices.`
)
}
colors = GeometryConverter.unpackColors(colorsRaw)
}
return {
attributes: {
POSITION: vertices,
COLOR: colors
},
bakeTransform: new Matrix4().makeScale(
conversionFactor,
conversionFactor,
conversionFactor
),
transform: null
} as GeometryData
}
/**
* BREP
*/
private static BrepToGeometryData(node: NodeData) {
return this.MeshToGeometryData(node.raw.displaValue)
}
/**
* MESH
*/
private static MeshToGeometryData(node: NodeData): GeometryData {
if (!node.raw) return
const conversionFactor = getConversionFactor(node.raw.units)
// const buffer = new BufferGeometry()
const indices = []
if (!node.raw.vertices) return
if (!node.raw.faces) return
const vertices = node.raw.vertices
const faces = node.raw.faces
const colorsRaw = node.raw.colors
let colors = null
let k = 0
while (k < faces.length) {
let n = faces[k]
if (n <= 3) n += 3 // 0 -> 3, 1 -> 4
if (n === 3) {
// Triangle face
indices.push(faces[k + 1], faces[k + 2], faces[k + 3])
} else {
// Quad or N-gon face
const triangulation = MeshTriangulationHelper.triangulateFace(
k,
faces,
vertices
)
indices.push(
...triangulation.filter((el) => {
return el !== undefined
})
)
}
k += n + 1
}
if (colorsRaw && colorsRaw.length !== 0) {
if (colorsRaw.length !== vertices.length / 3) {
console.warn(
`Mesh (id ${node.raw.id}) colours are mismatched with vertice counts. The number of colours must equal the number of vertices.`
)
}
colors = GeometryConverter.unpackColors(colorsRaw)
}
return {
attributes: {
POSITION: vertices,
INDEX: indices,
COLOR: colors
},
bakeTransform: new Matrix4().makeScale(
conversionFactor,
conversionFactor,
conversionFactor
),
transform: null
} as GeometryData
}
/**
* POINT
*/
private static PointToGeometryData(node: NodeData): GeometryData {
const conversionFactor = getConversionFactor(node.raw.units)
return {
attributes: {
POSITION: this.PointToFloatArray(node.raw)
},
bakeTransform: new Matrix4().makeScale(
conversionFactor,
conversionFactor,
conversionFactor
),
transform: null
} as GeometryData
}
/**
* LINE
*/
private static LineToGeometryData(node: NodeData): GeometryData {
const conversionFactor = getConversionFactor(node.raw.units)
return {
attributes: {
POSITION: this.PointToFloatArray(node.raw.start).concat(
this.PointToFloatArray(node.raw.end)
)
},
bakeTransform: new Matrix4().makeScale(
conversionFactor,
conversionFactor,
conversionFactor
),
transform: null
} as GeometryData
}
/**
* POLYLINE
*/
private static PolylineToGeometryData(node: NodeData): GeometryData {
const conversionFactor = getConversionFactor(node.raw.units)
if (node.raw.closed)
node.raw.value.push(node.raw.value[0], node.raw.value[1], node.raw.value[2])
return {
attributes: {
POSITION: node.raw.value
},
bakeTransform: new Matrix4().makeScale(
conversionFactor,
conversionFactor,
conversionFactor
),
transform: null
} as GeometryData
}
/**
* BOX
*/
private static BoxToGeometryData(node: NodeData) {
/**
* Right, so we're cheating here a bit. We're using three's box geometry
* to get the vertices and indices. Normally we could(should) do that by hand
* but it's too late in the evenning atm...
*/
const conversionFactor = getConversionFactor(node.raw.units)
const move = this.PointToVector3(node.raw.basePlane.origin)
const width = (node.raw.xSize.end - node.raw.xSize.start) * conversionFactor
const depth = (node.raw.ySize.end - node.raw.ySize.start) * conversionFactor
const height = (node.raw.zSize.end - node.raw.zSize.start) * conversionFactor
const box = new BoxBufferGeometry(width, depth, height, 1, 1, 1)
return {
attributes: {
POSITION: box.attributes.position.array,
INDEX: box.index.array
},
bakeTransform: new Matrix4().setPosition(move),
transform: null
} as GeometryData
}
/**
* POLYCURVE
*/
private static PolycurveToGeometryData(node: NodeData): GeometryData {
const buffers = []
for (let i = 0; i < node.raw.segments.length; i++) {
const element = node.raw.segments[i]
const conv = GeometryConverter.convertNodeToGeometryData(element.model)
buffers.push(conv)
}
return Geometry.mergeGeometryData(buffers)
}
/**
* CURVE
*/
private static CurveToGeometryData(node: NodeData) {
return this.PolylineToGeometryData(node.raw.displayValue.model)
}
/**
* CIRCLE
*/
private static CircleToGeometryData(node: NodeData) {
const conversionFactor = getConversionFactor(node.raw.units)
const curveSegmentLength = 0.1
const points = this.getCircularCurvePoints(
node.raw.plane,
node.raw.radius * conversionFactor,
curveSegmentLength
)
return {
attributes: {
POSITION: this.FlattenVector3Array(points)
},
bakeTransform: null,
transform: null
} as GeometryData
}
/**
* ARC
*/
private static ArcToGeometryData(node: NodeData) {
const origin = new Vector3(
node.raw.plane.origin.x,
node.raw.plane.origin.y,
node.raw.plane.origin.z
)
const startPoint = new Vector3(
node.raw.startPoint.x,
node.raw.startPoint.y,
node.raw.startPoint.z
)
const endPoint = new Vector3(
node.raw.endPoint.x,
node.raw.endPoint.y,
node.raw.endPoint.z
)
const midPoint = new Vector3(
node.raw.midPoint.x,
node.raw.midPoint.y,
node.raw.midPoint.z
)
const chord = new Line3(startPoint, endPoint)
// This the projection of the origin on the chord
const chordCenter = chord.getCenter(new Vector3())
// Direction from the origin to the mid point
const d0 = new Vector3().subVectors(midPoint, origin)
d0.normalize()
// Direction from the origin to it;s projection on the chord
const d1 = new Vector3().subVectors(chordCenter, origin)
d1.normalize()
// If the two above directions point in opposite directions, we need to reverse the arc's winding order
const _clockwise = d0.dot(d1) < 0
// Here we compute arc's orthonormal basis vectors using the origin and the two end points.
const v0 = new Vector3().subVectors(startPoint, origin)
v0.normalize()
const v1 = new Vector3().subVectors(endPoint, origin)
v1.normalize()
const v2 = new Vector3().crossVectors(v0, v1)
v2.normalize()
const v3 = new Vector3().crossVectors(v2, v0)
v3.normalize()
/**
* We clamp the dot value to [-1,1] since that's the domain acos is defined on. Normally dot won't return
* values outside that interval, but due to floating point precision, you sometimes get -1.0000000004, which
* makes acos return NaN
*/
const dot = Math.min(Math.max(v0.dot(v1), -1), 1)
// This is just the angle between the start and end points. Should be same as obj.angleRadians(or something)
const angle = Math.acos(dot)
const radius = node.raw.radius
// We draw the arc in a local un-rotated coordinate system. We rotate it later on via transformation
const curve = new EllipseCurve(
0,
0, // ax, aY
radius,
radius, // xRadius, yRadius
0,
angle, // aStartAngle, aEndAngle
_clockwise, // aClockwise
0 // aRotation
)
// This just samples points along the arc curve
const points = curve.getPoints(50)
const matrix = new Matrix4()
// Scale first, in order for the composition to work correctly
const conversionFactor = getConversionFactor(node.raw.plane.units)
// We determine the orientation of the plane using the three basis vectors computed above
const R = new Matrix4().makeBasis(v0, v3, v2)
// We translate it to the circle's origin (considering the origin's scaling as aswell )
const T = new Matrix4().setPosition(origin.multiplyScalar(conversionFactor))
matrix.multiply(T).multiply(R)
// if (scale) {
const S = new Matrix4().scale(
new Vector3(conversionFactor, conversionFactor, conversionFactor)
)
matrix.multiply(S)
// }
return {
attributes: {
POSITION: this.FlattenVector3Array(points)
},
bakeTransform: matrix,
transform: null
} as GeometryData
}
/**
* ELLIPSE
*/
private static EllipseToGeometryData(node: NodeData) {
const conversionFactor = getConversionFactor(node.raw.units)
const center = new Vector3(
node.raw.plane.origin.x,
node.raw.plane.origin.y,
node.raw.plane.origin.z
).multiplyScalar(conversionFactor)
const xAxis = new Vector3(
node.raw.plane.xdir.x,
node.raw.plane.xdir.y,
node.raw.plane.xdir.z
).normalize()
const yAxis = new Vector3(
node.raw.plane.ydir.x,
node.raw.plane.ydir.y,
node.raw.plane.ydir.z
).normalize()
let resolution = 2 * Math.PI * node.raw.firstRadius * conversionFactor * 10
resolution = parseInt(resolution.toString())
const points = []
for (let index = 0; index <= resolution; index++) {
const t = (index * Math.PI * 2) / resolution
const x = Math.cos(t) * node.raw.firstRadius * conversionFactor
const y = Math.sin(t) * node.raw.secondRadius * conversionFactor
const xMove = new Vector3(xAxis.x * x, xAxis.y * x, xAxis.z * x)
const yMove = new Vector3(yAxis.x * y, yAxis.y * y, yAxis.z * y)
const pt = new Vector3().addVectors(xMove, yMove).add(center)
points.push(pt)
}
return {
attributes: {
POSITION: this.FlattenVector3Array(points)
},
bakeTransform: null,
transform: null
} as GeometryData
}
/**
* UTILS
*/
private static getCircularCurvePoints(
plane,
radius,
startAngle = 0,
endAngle = 2 * Math.PI,
res = 0.1
) {
// Get alignment vectors
const center = this.PointToVector3(plane.origin)
const xAxis = this.PointToVector3(plane.xdir)
const yAxis = this.PointToVector3(plane.ydir)
// Make sure plane axis are unit length!!!!
xAxis.normalize()
yAxis.normalize()
// Determine resolution
let resolution = ((endAngle - startAngle) * radius) / res
resolution = parseInt(resolution.toString())
const points = []
for (let index = 0; index <= resolution; index++) {
const t = startAngle + (index * (endAngle - startAngle)) / resolution
const x = Math.cos(t) * radius
const y = Math.sin(t) * radius
const xMove = new Vector3(xAxis.x * x, xAxis.y * x, xAxis.z * x)
const yMove = new Vector3(yAxis.x * y, yAxis.y * y, yAxis.z * y)
const pt = new Vector3().addVectors(xMove, yMove).add(center)
points.push(pt)
}
return points
}
private static PointToVector3(obj, scale = true) {
const conversionFactor = scale ? getConversionFactor(obj.units) : 1
let v = null
if (obj.value) {
// Old point format based on value list
v = new Vector3(
obj.value[0] * conversionFactor,
obj.value[1] * conversionFactor,
obj.value[2] * conversionFactor
)
} else {
// New point format based on cartesian coords
v = new Vector3(
obj.x * conversionFactor,
obj.y * conversionFactor,
obj.z * conversionFactor
)
}
return v
}
private static PointToFloatArray(obj) {
if (obj.value) {
return [obj.value[0], obj.value[1], obj.value[2]]
} else {
return [obj.x, obj.y, obj.z]
}
}
private static FlattenVector3Array(input: Vector3[] | Vector2[]): number[] {
const output = new Array(input.length * 3)
const vBuff = []
for (let k = 0, l = 0; k < input.length; k++, l += 3) {
input[k].toArray(vBuff)
output[l] = vBuff[0]
output[l + 1] = vBuff[1]
output[l + 2] = vBuff[2] ? vBuff[2] : 0
}
return output
}
private static unpackColors(int32Colors: number[]): number[] {
const colors = new Array<number>(int32Colors.length * 3)
for (let i = 0; i < int32Colors.length; i++) {
const color = int32Colors[i]
const r = (color >> 16) & 0xff
const g = (color >> 8) & 0xff
const b = color & 0xff
colors[i * 3] = r / 255
colors[i * 3 + 1] = g / 255
colors[i * 3 + 2] = b / 255
}
return colors
}
}
@@ -1,17 +1,19 @@
import TreeModel from 'tree-model'
import { GeometryData } from './Geometry'
import { Node } from 'tree-model'
import { NodeRenderView } from '../NodeRenderView'
import { RenderTree } from '../RenderTree'
export type TreeNode = TreeModel.Node<NodeData>
export type SearchPredicate = (node: TreeNode) => boolean
export interface NodeData {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
raw: { [prop: string]: any }
geometry: GeometryData
renderView?: NodeRenderView
}
export class WorldTree {
private static instance: WorldTree
private static renderTreeInstance: RenderTree
private constructor() {
this.tree = new TreeModel()
@@ -25,10 +27,22 @@ export class WorldTree {
return WorldTree.instance
}
private tree: TreeModel
private _root: TreeModel.Node<NodeData>
public static getRenderTree(): RenderTree {
if (!WorldTree.getInstance()._root) {
console.error(`WorldTree not initialised`)
return null
}
if (!WorldTree.renderTreeInstance) {
WorldTree.renderTreeInstance = new RenderTree(WorldTree.getInstance()._root)
}
public get root(): TreeModel.Node<NodeData> {
return WorldTree.renderTreeInstance
}
private tree: TreeModel
private _root: TreeNode
public get root(): TreeNode {
return this._root
}
@@ -44,10 +58,7 @@ export class WorldTree {
parent.addChild(node)
}
public findAll() {
return this.root.all((node: Node<NodeData>) => {
// const type = node.model.raw.speckle_type.split('.').reverse()[0]
return node.model.raw.displayValue !== undefined //type === 'Polyline'
})
public findAll(predicate: SearchPredicate, node?: TreeNode): Array<TreeNode> {
return (node ? node : this.root).all(predicate)
}
}