Skip to content
8 changes: 8 additions & 0 deletions packages/objectloader2/src/core/objectLoader2.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MemoryCache } from '../deferment/MemoryCache.js'
import { DefermentManager } from '../deferment/defermentManager.js'
import { PropertyManager } from '../properties/PropertyManager.js'
import AggregateQueue from '../queues/aggregateQueue.js'
import AsyncGeneratorQueue from '../queues/asyncGeneratorQueue.js'
import { CustomLogger, take } from '../types/functions.js'
Expand Down Expand Up @@ -31,6 +32,8 @@ export class ObjectLoader2 {
#root?: Item = undefined
#isRootStored = false

#propertyManager: PropertyManager = new PropertyManager()

constructor(options: ObjectLoader2Options) {
this.#rootId = options.rootId
this.#logger = options.logger || ((): void => {})
Expand Down Expand Up @@ -140,6 +143,7 @@ export class ObjectLoader2 {
this.#cacheReader.requestAll(children)
let count = 0
for await (const item of this.#gathered.consume()) {
this.#propertyManager.addBase(item.base!)
yield item.base! //always defined, as we add it to the queue
count++
if (count >= total) {
Expand All @@ -152,6 +156,10 @@ export class ObjectLoader2 {
}
}

get propertyManager(): PropertyManager {
return this.#propertyManager
}

static createFromObjects(objects: Base[]): ObjectLoader2 {
return ObjectLoader2Factory.createFromObjects(objects)
}
Expand Down
7 changes: 7 additions & 0 deletions packages/objectloader2/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export { ObjectLoader2 } from './core/objectLoader2.js'
export { ObjectLoader2Factory } from './core/objectLoader2Factory.js'
export { getFeatureFlag, ObjectLoader2Flags } from './types/functions.js'

export {
PropertyManager,
PropertyInfo,
NumericPropertyInfo,
StringPropertyInfo
} from './properties/PropertyManager.js'
129 changes: 129 additions & 0 deletions packages/objectloader2/src/properties/PropertyManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { flattenBase } from '../types/flatten.js'
import { Base } from '../types/types.js'

type FlattenedBase = { id: string; value: string | number }
type PropertyValues = Record<string, FlattenedBase[]>

export class PropertyManager {
private properties: PropertyInfo[] = []
private propValues: PropertyValues = {}

public addBase(base: Base): void {
const obj = flattenBase(base)
for (const key in obj) {
if (Array.isArray(obj[key])) {
continue
}
if (!this.propValues[key]) {
this.propValues[key] = []
}
this.propValues[key].push({ value: obj[key], id: obj.id as string })
}
}

public finalize(): void {
for (const propKey in this.propValues) {
const propValuesArr = this.propValues[propKey]
const propInfo = {} as PropertyInfo
propInfo.key = propKey

propInfo.type = typeof propValuesArr[0].value === 'string' ? 'string' : 'number'
propInfo.objectCount = propValuesArr.length

// For string based props, keep track of which ids belong to which group
if (propInfo.type === 'string') {
const stringPropInfo = propInfo as StringPropertyInfo
const valueGroups = {} as { [key: string]: string[] }
for (const { value, id } of propValuesArr) {
if (!valueGroups[value]) {
valueGroups[value] = []
}
valueGroups[value].push(id)
}
stringPropInfo.valueGroups = []
for (const key in valueGroups) {
stringPropInfo.valueGroups.push({ value: key, ids: valueGroups[key] })
}

stringPropInfo.valueGroups = stringPropInfo.valueGroups.sort((a, b) =>
a.value.localeCompare(b.value)
)
}
// For numeric props, we keep track of min and max and all the {id, val}s
else if (propInfo.type === 'number') {
const numProp = propInfo as NumericPropertyInfo
numProp.min = Number.MAX_VALUE
numProp.max = Number.MIN_VALUE
for (const { value } of propValuesArr) {
if (typeof value !== 'number') continue // skip non-numeric values
if (value < numProp.min) numProp.min = value
if (value > numProp.max) numProp.max = value
}
numProp.valueGroups = (propValuesArr as unknown as NumberType[]).sort(
(a, b) => a.value - b.value
)
// const sorted = propValuesArr.sort((a, b) => a.value - b.value)
// propInfo.sortedValues = sorted.map(s => s.value)
// propInfo.sortedIds = sorted.map(s => s.value) // tl;dr: not worth it
}
this.properties.push(propInfo)
}
}

public getProperties(): PropertyInfo[] {
this.finalize()
return this.properties
}
}

/**
* PropertyInfo types represent all of the properties that you can filter on in the viewer
*/

export interface PropertyInfo {
/**
* Property identifier, flattened
*/
key: string
/**
* Total number of objects that have this property
*/
objectCount: number
type: 'number' | 'string'
}

export interface NumericPropertyInfo extends PropertyInfo {
type: 'number'
/**
* Absolute min/max values that are available for this property
*/
min: number
max: number
/**
* An array of pairs of object IDs and their actual values for that property
*/
valueGroups: NumberType[]
/**
* User defined/filtered min/max that is bound within min/max above
*/
passMin: number | null
passMax: number | null
}

export interface NumberType {
ids: string[]
value: number
}

export interface StringPropertyInfo extends PropertyInfo {
type: 'string'
/**
* An array of pairs of object IDs and their actual values for that property
*/
valueGroups: StringType[]
}

export interface StringType {
ids: string[]
value: string
}
44 changes: 44 additions & 0 deletions packages/objectloader2/src/types/flatten.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Base } from './types.js'

/**
* Flattens a speckle object. It will ignore arrays, null and undefined valuesm, as well as various 'safe to ignore' speckle properties, such as
* bbox, __closure, __parents, totalChildrenCount.
* @param obj object to flatten
* @returns an object with all its props flattened into `prop.subprop.subsubprop`.
*/
export function flattenBase(obj: Base): Record<string, string | number> {
const flatten = {} as Record<string, string | number>
for (const k in obj) {
if (
k === 'id' ||
k === '__closure' ||
k === '__parents' ||
k === 'bbox' ||
k === 'totalChildrenCount'
) {
continue
}

const v = obj[k]
if (v === null || v === undefined || Array.isArray(v)) {
continue
}
if (v.constructor === Object) {
const flattenProp = flattenBase(v as unknown as Base)
for (const pk in flattenProp) {
flatten[k + '.' + pk] = flattenProp[pk]
}
continue
}
const type = typeof v
if (type === 'string' || type === 'number') {
flatten[k] = v as string | number
} else if (type === 'boolean') {
flatten[k] = v ? 'true' : 'false' // Convert boolean to string for consistency
}
}
if (obj.id) {
flatten.id = obj.id
}
return flatten
}
1 change: 1 addition & 0 deletions packages/objectloader2/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Base {
id: string
speckle_type: string
__closure?: Record<string, number>
[x: string]: unknown
}

export interface Reference {
Expand Down
40 changes: 34 additions & 6 deletions packages/viewer/src/modules/Viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
import { World } from './World.js'
import { type TreeNode, WorldTree } from './tree/WorldTree.js'
import SpeckleRenderer from './SpeckleRenderer.js'
import { type PropertyInfo, PropertyManager } from './filtering/PropertyManager.js'
import type { Query, QueryArgsResultMap } from './queries/Query.js'
import { Queries } from './queries/Queries.js'
import { type Utils } from './Utils.js'
Expand All @@ -30,6 +29,8 @@ import { RenderTree } from './tree/RenderTree.js'
import Logger from './utils/Logger.js'
import Stats from './three/stats.js'
import { TIME_MS } from '@speckle/shared'
import { PropertyInfo, PropertyManager } from './filtering/PropertyManager.js'
import { PropertyInfo as OL2PropertyInfo } from '@speckle/objectloader2'

export class Viewer extends EventEmitter implements IViewer {
/** Container and optional stats element */
Expand All @@ -49,6 +50,7 @@ export class Viewer extends EventEmitter implements IViewer {
/** Misc members */
protected clock: Clock
protected loaders: { [id: string]: Loader } = {}
private properties: Record<string, OL2PropertyInfo[]> = {}

protected extensions: {
[id: string]: Extension
Expand Down Expand Up @@ -155,9 +157,7 @@ export class Viewer extends EventEmitter implements IViewer {
this.speckleRenderer = new SpeckleRenderer(this.tree, this)
this.speckleRenderer.create(this.container)
window.addEventListener('resize', this.resize.bind(this), false)

this.propertyManager = new PropertyManager()

this.propertyManager = new PropertyManager()
this.frame()
this.resize()
}
Expand Down Expand Up @@ -244,11 +244,38 @@ export class Viewer extends EventEmitter implements IViewer {
super.on(eventType, listener)
}

public getObjectProperties(
public async getObjectProperties(
resourceURL: string | null = null,
bypassCache = true
): Promise<PropertyInfo[]> {
return this.propertyManager.getProperties(this.tree, resourceURL, bypassCache)
if (!resourceURL) {
return Promise.resolve([])
}
const ol2Props = this.properties[resourceURL]
if (ol2Props) {
const oldProps = await this.propertyManager.getProperties(this.tree, resourceURL, bypassCache)
oldProps.sort((a, b) => a.key.localeCompare(b.key))
const newProps = ol2Props as unknown as PropertyInfo[]
newProps.sort((a, b) => a.key.localeCompare(b.key))
if (oldProps.length !== newProps.length) {
console.error(
`Property count mismatch for ${resourceURL}: New: ${newProps.length}, Old: ${oldProps.length}`
)
}
for(const oldProp of oldProps) {
const newProp = newProps.find((p) => p.key === oldProp.key)
if (!newProp) {
console.error(`Property ${oldProp.key} not found in New properties`);
}
} for (const newProp of newProps) {
const oldProp = oldProps.find((p) => p.key === newProp.key)
if (!oldProp) {
console.error(`Property ${newProp.key} not found in Old properties`)
}
}
return newProps
}
return Promise.resolve([])
}

public getDataTree(): void {
Expand Down Expand Up @@ -333,6 +360,7 @@ export class Viewer extends EventEmitter implements IViewer {
Logger.log(this.getRenderer().renderingStats)
Logger.log('ASYNC batch build time -> ', performance.now() - t0)
this.requestRender(UpdateFlags.RENDER_RESET | UpdateFlags.SHADOWS)
this.properties[loader.resource] = loader.properties
this.emit(ViewerEvent.LoadComplete, loader.resource)
}

Expand Down
2 changes: 2 additions & 0 deletions packages/viewer/src/modules/loaders/Loader.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PropertyInfo } from '@speckle/objectloader2'
import EventEmitter from '../EventEmitter.js'

export enum LoaderEvent {
Expand All @@ -20,6 +21,7 @@ export abstract class Loader extends EventEmitter {
protected _resource: string
protected _resourceData: unknown

public abstract get properties(): PropertyInfo[]
public abstract get resource(): string
public abstract get finished(): boolean

Expand Down
5 changes: 5 additions & 0 deletions packages/viewer/src/modules/loaders/OBJ/ObjLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ObjConverter } from './ObjConverter.js'
import { ObjGeometryConverter } from './ObjGeometryConverter.js'
import { WorldTree } from '../../../index.js'
import Logger from '../../utils/Logger.js'
import { PropertyInfo } from '@speckle/objectloader2'

export class ObjLoader extends Loader {
protected baseLoader: OBJLoader
Expand All @@ -20,6 +21,10 @@ export class ObjLoader extends Loader {
return this.isFinished
}

public get properties(): PropertyInfo[] {
return []
}

public constructor(targetTree: WorldTree, resource: string, resourceData?: unknown) {
super(resource, resourceData)
this.tree = targetTree
Expand Down
Loading
Loading