Custom TypeScript Speckle Connector

Hi @chrkong

I’ve had a look and there are two issues:

  • All your objects are missing ids. The viewer will ignore any object that doesn’t have an id. As a quick fix you could use MathUtils.generateUUID() from three.js (since you are already importing it). Both the DandasNode and their display values will need ids
  • Some of the display values have null vertex positions:

This is likely to happen because some DandasNodes have undefined DiameterBredde or Terraenkote or Bundkote which inevitably lead to bad cylinder geometries.

Cheers

1 Like

Okay, I’ve added ids to the objects, but now the viewer wont load anything and I get this in the console:
image

The model can be found here: Viewer - dandas.xml - Speckle

Hi @chrkong

You are sending meshes with null values for vertex positions

chafdf

Like I said in the last post, that’s happening because when you are generating the geometries DandasNodes have undefined DiameterBredde or Terraenkote or Bundkote which inevitably lead to bad cylinder geometries.

Additionally, you display values still have no id

Cheers

1 Like

Looks like it is working now :grinning:

Thanks for the help @alex and @jonathon :star_struck:

3 Likes

I have a couple of followup questions:

  1. My geometry is quite far from the origin and in general spread quite thin, which causes the usual problems with rendering and zooming. Is there a way to solve this? Like a local origin or something?

  2. I have added some fields with the @ prefix with the expectation that they would become detached and that I would have to de-reference them, when I pull the data again from the service, which is not the case. Do I have to implement the detachment logic in my ObjectSerializer and is there some guide how to do that?

Link to project

Indeed, the @ decorator is used as a signififier to the serializer/deserializer to detach (and chunk) data. Because you are in the brave uncharted territory of a non-existant TS SDK the examples I have thrown your way from Excel connector may have omitted this feature of its serializer.

I can only point you to the implementations in:

specklepy

and speckle-sharp

Any chance that the TS boss @vwb has such an implementation lying around?

Otherwise, I’ll give it a try on my own :slight_smile:

Ping @oguzhankoral for the sketchup implementation!
(sketchup has/had this!)

Actually, I think I got the detach part sorted.

Add this to the PreserializeObject:

    if (detachable) {
      const serialized = await this.SerializeBaseWithClosures(object, [...closures])
      return new ObjectReference(serialized.id)
    }

And updated PreserializeEachObjectProperty with

    for (const key of objectKeys) {
      const detachable = key.startsWith('@')
      converted.set(
        BaseObjectSerializer.CleanKey(key),
        await this.PreserializeObject(o[key as keyof object], closures, detachable),
      )
    }

Full code below

import { enc, MD5 } from 'crypto-js'
import { IBase, ITransport } from './interfaces.ts'

/**
 * Serializer for Speckle objects written in Typescript
 */
export class BaseObjectSerializer {
  constructor(public transports: ITransport[]) {}

  public async SerializeBase(object: object): Promise<SerializedBase> {
    return await this.SerializeBaseWithClosures(object, [])
  }

  private async SerializeBaseWithClosures(object: object, closures: Array<Map<string, number>>) {
    const thisClosure = new Map<string, number>()
    closures.push(thisClosure)

    const converted = await this.PreserializeEachObjectProperty(object, closures)
    let json = this.SerializeMap(converted)
    const id = this.GetId(json)
    converted.set('id', id)

    this.AddSelfToParentClosures(id, closures)
    if (thisClosure.size > 0) {
      converted.set('__closure', Object.fromEntries(thisClosure))
    }
    converted.set('totalChildrenCount', thisClosure.size)

    json = this.SerializeMap(converted)
    await this.StoreObject(converted)
    return new SerializedBase(id, json)
  }

  private async PreserializeObject(
    object: unknown,
    closures: Array<Map<string, number>>,
    detachable: boolean = false,
  ): Promise<unknown> {
    if (!(object instanceof Object) || object instanceof String) {
      return object
    }

    if (detachable) {
      const serialized = await this.SerializeBaseWithClosures(object, [...closures])
      return new ObjectReference(serialized.id)
    }

    if (object instanceof DataChunk) {
      const serialized = await this.SerializeBaseWithClosures(object, [...closures])
      return new ObjectReference(serialized.id)
    }

    if (object instanceof Array) {
      // chunk array into 5000 by default
      const chunkSize = 5000
      if (object.length > chunkSize) {
        let serializedCount = 0
        const data = new Array<DataChunk>()
        while (serializedCount < object.length) {
          const dataChunkCount = Math.min(chunkSize, object.length - serializedCount)
          data.push(new DataChunk(object.slice(serializedCount, serializedCount + dataChunkCount)))
          serializedCount += dataChunkCount
        }
        return await this.PreserializeObject(data, closures)
      }

      const convertedList = new Array<unknown>()
      for (const element of object) {
        convertedList.push(await this.PreserializeObject(element, closures))
      }
      return convertedList
    }

    if (object instanceof Object) {
      return Object.fromEntries(await this.PreserializeEachObjectProperty(object, closures))
    }

    throw new Error(`Cannot serialize object ${object}`)
  }

  private async PreserializeEachObjectProperty(
    o: object,
    closures: Array<Map<string, number>>,
  ): Promise<Map<string, unknown>> {
    const converted = new Map<string, unknown>()

    const getters = Object.entries(Object.getOwnPropertyDescriptors(Reflect.getPrototypeOf(o)))
      .filter(([key, descriptor]) => typeof descriptor.get === 'function' && key !== '__proto__')
      .map(([key]) => key)

    const objectKeys = new Array<string>()
    objectKeys.push(...Object.keys(o))
    objectKeys.push(...getters)

    for (const key of objectKeys) {
      const detachable = key.startsWith('@')
      converted.set(
        BaseObjectSerializer.CleanKey(key),
        await this.PreserializeObject(o[key as keyof object], closures, detachable),
      )
    }

    return converted
  }

  private static disallowedCharacters: string[] = ['.', '/']

  private static CleanKey(originalKey: string): string {
    const newStringChars = []
    for (let i = 0; i < originalKey.length; i++) {
      if (i == 1 && originalKey[i] == '@' && originalKey[0] == '@') {
        continue
      }
      if (this.disallowedCharacters.includes(originalKey[i])) {
        continue
      }

      newStringChars.push(originalKey[i])
    }
    return newStringChars.join('')
  }

  private async StoreObject(object: Map<string, unknown>) {
    for (const transport of this.transports) {
      await transport.SaveObject(object)
    }
  }

  private SerializeMap(map: Map<string, unknown>): string {
    return JSON.stringify(Object.fromEntries(map))
  }

  private GetId(json: string): string {
    return MD5(json).toString(enc.Hex)
  }

  private AddSelfToParentClosures(objectId: string, closureTables: Array<Map<string, number>>) {
    // only go to closureTable length - 1 because the last closure table belongs to the object with the
    // provided id
    const parentClosureTablesCount = closureTables.length - 1

    for (let parentLevel = 0; parentLevel < parentClosureTablesCount; parentLevel++) {
      const childDepth = parentClosureTablesCount - parentLevel
      closureTables[parentLevel].set(objectId, childDepth)
    }
  }
}

export class DataChunk implements IBase {
  public speckle_type = 'Speckle.Core.Models.DataChunk'
  public data: unknown[]

  constructor(data: unknown[] | null) {
    this.data = data ?? []
  }
}

export class ObjectReference implements IBase {
  public speckle_type = 'reference'

  constructor(public referencedId: string) {}
}

export class SerializedBase {
  constructor(
    public id: string,
    public json: string,
  ) {}
}

3 Likes

However, I still feel like something is wrong with my objects and I am tracing it to the collection.

Here is my viewer:

So I have a collection, which looks like it has nothing in it / only includes a single element.

But if I got Dev Mode:

I can see that I got stuff.

Compared to a standard Revit model:

So I would expect that my Dandas Nodes looks like the Site from Revit.
However, Dandas Nodes are called Collection and is a object, while the Site is an Array Collection and is a array.

The code I use to construct my Dandas Nodes object looks like this:

{
    speckle_type: 'Speckle.Core.Models.Collection',
    name: 'Dandas Nodes',
    id: uuidv4(),
    collectionType: 'Dandas Nodes',
    '@elements': nodes
      .filter((node) => ['1', '4', '7'].indexOf(node.KnudeKode) >= 0 && node.FormKode === '1')
      .map((node) => ({
        ...node,
        name: node._Knudenavn,
        id: uuidv5(node._Knudenavn, uuidv5.URL),
        parameters: generateParameters(node),
        speckle_type: 'Objects.BuiltElements.DandasNode',
        displayValue: [generateGeometry(node)],
      })),
  }

What am I missing?

I can’t see how the Dandas Nodes and Site differs in Dev Mode

It was in Sketchup, we do it in Ruby anymore but the Excel connector already has it for a while. speckle-excel/src/plugins/BaseObjectSerializer.ts at 3dad1a0849f7adb5996c97cf63f7c46f366431c1 · specklesystems/speckle-excel · GitHub

1 Like

i think we’re starting to need a link to the raw deal, to understand what’s what… there might be a subtle difference or fuckup somewhere. Your collection of nodes - does it have a closure table?

you’re also getting confused (by us, apologies) with the structure from revit. site in there is essentially a dynamic prop on the root commit object - we’re changing all this in dui3 to be more sane.

rootObj["@site"] = List of things

the closure is on the rootObj. In your case, it needs to be present as well on the “Dandas Nodes” collection…

Here is the link: Viewer - dandas2 - Speckle

okay - basically the __closure table needs to incorporate references to not just the immediate children, but all leaf nodes under the respective object.

from what i see the root collection only has one id in there, which is wrong. There’s some other warning sings in there, ie @elements is supposed to be an array of object references, but i think it’s an object (and the 0, 1, 2, 3 etc. are actually object keys - it has an id property)

tl;dr i’d need to look at the serialisation code. it’s probably not doing what you’re expecting it to do; i won’t have time right now unfortunately :grimacing:

can we get our hands on reproducing this from a repo somewhere?

PS: did the hackaton start early and nobody told me :sweat_smile:

I didn’t change the BaseObjectSerializer besides adding the few lines for handling detachable objects and updating some types. So I don’t know about the closures :person_shrugging:

The @elements property is created as an array (see above), so it must be changed during the serialization.

I can create a reproducible repo for you - give me a minute.

Not part of the hackathon - just plain old ‘building cool things on top of Speckle’ :sweat_smile:

1 Like

Alright got a repo for you to play with: GitHub - ocni-dtu/speckle-custom-connector

1 Like

Viewer - dandas - Speckle :confetti_ball:

tl;dr: the excel-derived serializer that you were using is not correct. I’ve plugged in an older one (from sketchup), and we have achieved success :slight_smile:

I’ll cleanup and push code your way.

(note, i’ve enlarged by 100x your geometry to make it visible easier in the browser)

4 Likes

Awesome @dimitrie, thanks a lot!! :star_struck:

3 Likes
1 Like

A post was split to a new topic: Should I serialize my custom JSON when sending to Speckle