Federating Speckle Models

We’ve received excellent feature ideas for merging Speckle streams/commits/branches, such as exporting federated models, merging multiple branches, and improving the online viewer. :pray:

References to some of these are at the end of this post. There are more, but they show a spread of concerns for coincidentally showing aggregate items in the viewer through to extending the Git-like functionality enabled with Speckle.

While there is no firm development roadmap for Model Federation on the horizon, I have put this tutorial demonstrating, in part, what is already possible. It is prepared in the style of a Jupyter notebook.

This tutorial is in 3 parts.

  1. Federation by Overlays (this post)
  2. Federation by Brute force merge
  3. Federation by Referencing

It is quite long and covers a few different features, so grab a :coffee: or :beer: and I hope you enjoy the read.


Speckle Federator

This example combines the same 3 commits into a view presented in the embedded viewer. We will be using specklepy as our SDK of choice, but these are all equally possible with .NET

The Setup

from specklepy.api.client import SpeckleClient

client = SpeckleClient(host=HOST_SERVER)  # or whatever your host is
client.authenticate_with_token(ACCESS_TOKEN)  # or whatever your token is

I’ll use the same three commits for each example:

commits = [
    "https://speckle.xyz/streams/7ce9010d71/commits/27a5df66a0",  # scooper bucket
    "https://speckle.xyz/streams/7ce9010d71/commits/f7ef7f5270",  # fixing plate
    "https://speckle.xyz/streams/7ce9010d71/commits/f6052eaa16",  # hinged arm
]

Merge type 1 - Viewer Overlays

It is possible in the Speckle Web frontend to combine commits, and objects to make any scene you wish using the Add Overlays function

image

That will present a dialog for you to select from commits made within the same Project Stream or by Url

In this first option, we can replicate that workflow automagically and generate an overlay URL that combines the 3 commits

\streams\{STREAM_ID}\commits\{COMMIT_ID1}?overlay={COMMIT_ID2},{COMMIT_ID3}

from specklepy.transports.server import ServerTransport
from specklepy.api.wrapper import StreamWrapper

wrappers = [StreamWrapper(commit_url) for commit_url in commits]
transport = ServerTransport(client=client, stream_id=wrappers[0].stream_id)

Assuming we are dealing with only one Project Stream, the wrappers is the specklepy helper for each of the commit Urls in the array, I only need one transport object.

That is pretty much all we need for our embedded viewer for the overlays, all that remains is to add some viewer options

stream_id = wrappers[0].stream_id
commit_ids = [wrapper.commit_id for wrapper in wrappers]

transparency = True
autoload = True
hide_controls = False
hide_sidebar = True
hide_selection_info = True

# the overlay is for all commits after the first in the array
overlay = ",".join(commit_ids[1:])

embed_url = (
f"https://speckle.xyz/embed?stream={stream_id}"
f"&commit={commit_ids[0]}"
f"&overlay={overlay}"
f"&transparent={transparency}"
f"&autoload={autoload}"
f"&hidecontrols={hide_controls}"
f"&hidesidebar={hide_sidebar}"
f"&hideselectioninfo={hide_selection_info}"
)

That last operation is verbose only for legibility here.

To display the viewer in a notebook:

from IPython.display import IFrame

IFrame(embed_url, width=400, height=300)
9 Likes

Merge 2 - Smash them together

The next version of merging is extreme difference from simple overlays. We will extract all the component objects from each commit. This is a bit more involved as we need to load the things from the server and reconstruct a commit.

image

  1. Take the content of the 3 commits and commit that to speckle.
  2. View that commit in the embedded viewer

Firstly, we’ll get the commit objects

commit_objects = [
    client.commit.get(stream_id, commit_id) for commit_id in commit_ids
]

If the 3 URLs given are all commits, then the first name object has a property called referencedObject which is the id of the wrapper object for the commit data.

referenced_objects = [r.referencedObject for r in commit_objects]

We then’ Receive’ each reference object to assemble a large array of all the committed things.

from specklepy.api import operations

commit_data = [
    operations.receive(obj_id=ref_obj, remote_transport=wrap.get_transport())
    for ref_obj, wrap in zip(referenced_objects, wrappers)
]

For this example, I will create a commit that retains the structure of the three separate. If you were trying to apply any filters or run diffing operations, this is the stage where you could merge the three only to include the latest objects etc. The subject of a future post on this topic

I happen to know there is no overlap, so I’ll proceed.

from specklepy.objects import Base

granular_commit_object = Base(speckle_type="Federation.Granular")
granular_commit_object["@Components"] = commit_data

… and hash them

hash3 = operations.send(base=granular_commit_object , transports=[transport])

Suppose we follow the reasoning from other discussions around Versions, Federations, Assemblies, Exchanges etc. We should store this new commit on a dedicated branch.

def try_get_branch_or_create(client, stream_id, branch_name):
    try:
        client.branch.get(
            stream_id=stream_id, name=branch_name
        ) or client.branch.create(stream_id=stream_id, name=branch_name)
        return client.branch.get(
            stream_id=stream_id, name=branch_name)
    except GraphQLException:
        return client.branch.create(stream_id=stream_id, name=branch_name)

branch = try_get_branch_or_create(client, stream_id, "federated")

And then we create a new commit that contains the resolved objects

commit_id3 = client.commit.create(
    branch_name=branch.name,
    stream_id=stream_id,
    object_id=hash3,
    message="federated commit",
)

Then as with part 1, we generate an embedded viewer URL to demonstrate the federation:

embed_url3 = (f"https://speckle.xyz/embed?stream={stream_id}"
              f"&commit={commit_id3}"
              f"&transparent={transparency}"
              f"&autoload={autoload}"
              f"&hidecontrols={hide_controls}"
              f"&hidesidebar={hide_sidebar}"
              f"&hideselectioninfo={hide_selection_info}")

from IPython.display import IFrame

IFrame(embed_url3, width=400, height=300)

As mentioned, this version could be a jumping-off point for clever logic employed over what objects to include and which not to.

More importantly for Data Management, this complete merge allows for merges across Project Streams!!


6 Likes

Love both approaches!
looking at method 2, I see that the groups created from different commits are numbered with indexes as components.
Can we assign them a ‘Name’ as a property as well?
(e.g. name filled with the name of the stream_name+commit_name elements originate from)

Next in Method 1, am i correct to understand that this ‘overlay’ only allows for the commits within one stream to be merged?

:100: This was the simplest dumbest version they could be as I was keeping the python code to a one-liner :smiley:

Also correct - the frontend UI warns about this.


Wait for part 3! :wink:

2 Likes

Merge 3 - The Subtle One.

This third option will be the longest, and most intricate but demonstrate best how the essence of Speckle works.

Speckle has long been praised for bringing the concept of object-level versioning and immutability to AEC, and rightly so. What is less well-understood until you get into the weeds (or the docs), is how to leverage the mechanics behind the Speckle magic.

Once an object has been sent to Speckle, its uniqueness is the property that matters most to our Connectors. Change any property of the source object, and the next time it is sent, a new Speckle object is created. Both exist; one is the latest and, in all likelihood, manifested in a Version Commit with all its latest partner objects.

However, if that object doesn’t change, then none of the Connectors send it again. A commit may include it in the “latest” set, but it is not sent. Instead, Speckle can use the originally sent object and include a reference to it in its place, known as a ReferenceObject. You can read all about the philosophy behind this in our documentation.

Why mention all of that? Well, we’ll use ReferenceObjects to gather the commits from earlier and show that the commit contains all the reference material.

I’ll reuse some of the objects we defined in Merge 2.

referenced_objects = [
    client.commit.get(stream_id, commit_id).referencedObject
    for commit_id in commit_ids
]

We can create a new Federation class, essentially just adding a name for the collection. (almost what you asked @Dickels112 - you can see we listen)

from specklepy.objects import Base

class Federation(Base, speckle_type="Federation"):
    def __init__(self, **kwargs):
      self["Components"] = []

new_commit_object = Base(speckle_type="Federation")

I use Components to mean the building blocks of the Federation

new_commit_object["Components"] = [
    Base.of_type("reference", referencedId=commit_id)
    for commit_id in referenced_objects
]**strong text**

This was incredibly simple, and for the most part, we are done. We define a commit as a Federation and add ReferenceObjects as its Components. Regarding Speckle data, that commit, if sent, “contains” the objects in the three reference commits.

Merge 3b - The Gotcha

For the Viewer to resolve this, commit it will require the “closure table” for each reference object. These closures are used as a shortcut to handle processable things. Essentially, we provide the Viewer with a telephone directory (remember them( of all the child objects.

This doesn’t come for free, but we can add a custom operation to our script to get this from the server.

Ideally, we’d check the localTransport first to see if we have the closure table already, but we’ll get it by querying the server for brevity.

We’ll make a straight GraphQL query of the commit. Below is a helper function that will return for a given object_id

from gql import gql, Client
from gql.transport.requests import RequestsHTTPTransport


def get_closures(wrapper, object_id):

    # define a graphQL client
    client = Client(
        transport=RequestsHTTPTransport(
            url=f"{wrapper._account.serverInfo.url}/graphql", verify=True, retries=3
        )
    )

    # define the query
    query = gql(
        """ query Object($stream_id: String!, $object_id: String!) { 
            stream(id: $stream_id) { 
              object(id: $object_id) { 
                data 
              }
            }
          } """
    )
    params = {"stream_id": wrapper.stream_id, "object_id": object_id}

    # Execute the query and profit.
    return client.execute(query, variable_values=params)["stream"]["object"]["data"][
        "__closure"
    ]

To describe what this query asks for, the given Stream and Object (for which we mean the commit objects) return the data property. Commit objects don’t typically contain much data, but one property they possess is the __closure table from the Connector that made the commit in the first place. If we commit our Federation object as it is, the specklepy SDK won’t create that for us.

So, The new_commit_object will need the __closure table from each commit we are merging. We can use the get_closures function we created earlier to get this.

At this point, we could refactor to always using Lists rather than numbered variables, but for now, we’ll add the closures to the new commit object.

closures = {
    k: v
    for d in [get_closures(wrappers[0], obj_id) for obj_id in referenced_objects]
    for k, v in d.items()
}
closures.update(dict.fromkeys(referenced_objects, 1))

new_commit_object["__closure"] = closures

I will reuse the helper function from Merge 2 to check if a ‘Federation’ branch exists and, if not, create it.

branch = try_get_branch_or_create(client, stream_id, "federated-by-reference")

As before we can hash the commit object to add objects to Speckle Server

hash_2 = operations.send(base=new_commit_object, transports=[transport])

All done… ?
…No! This doesn’t work as the default specklepy traversal strips props with the __ prefix, nor does it resolve the closure for Reference Objects. So we’ll need to add a custom operation to the server to fix this.

from typing import Any, Dict, List, Optional, Tuple
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from uuid import uuid4
import hashlib
import re
from enum import Enum
from specklepy.objects.base import Base, DataChunk
import ujson

PRIMITIVES = (int, float, str, bool)


def traverse_base(
    serializer: BaseObjectSerializer, base: Base, closures: Dict[str, Any] = {}
):
    if serializer.write_transports:
        for wt in serializer.write_transports:
            wt.begin_write()

    if not serializer.detach_lineage:
        serializer.detach_lineage = [True]

        serializer.lineage.append(uuid4().hex)
        object_builder = {"id": "", "speckle_type": "Base", "totalChildrenCount": 0}
        object_builder.update(speckle_type=base.speckle_type)
        obj, props = base, base.get_serializable_attributes()

        while props:
            prop = props.pop(0)
            value = getattr(obj, prop, None)
            chunkable = False
            detach = False

            # skip props marked to be ignored with "__" or "_"
            if prop.startswith(("__", "_")):
                continue

            # don't prepopulate id as this will mess up hashing
            if prop == "id":
                continue

            # only bother with chunking and detaching if there is a write transport
            if serializer.write_transports:
                dynamic_chunk_match = prop.startswith("@") and re.match(
                    r"^@\((\d*)\)", prop
                )
                if dynamic_chunk_match:
                    chunk_size = dynamic_chunk_match.groups()[0]
                    serializer._chunkable[prop] = (
                        int(chunk_size) if chunk_size else base._chunk_size_default
                    )

                chunkable = prop in base._chunkable
                detach = bool(
                    prop.startswith("@") or prop in base._detachable or chunkable
                )

            # 1. handle None and primitives (ints, floats, strings, and bools)
            if value is None or isinstance(value, PRIMITIVES):
                object_builder[prop] = value
                continue

            # NOTE: for dynamic props, this won't be re-serialised as an enum but as an int
            if isinstance(value, Enum):
                object_builder[prop] = value.value
                continue

            # 2. handle Base objects
            elif isinstance(value, Base):
                child_obj = serializer.traverse_value(value, detach=detach)
                if detach and serializer.write_transports:
                    ref_id = child_obj["id"]
                    object_builder[prop] = serializer.detach_helper(ref_id=ref_id)
                else:
                    object_builder[prop] = child_obj

            # 3. handle chunkable props
            elif chunkable and serializer.write_transports:
                chunks = []
                max_size = base._chunkable[prop]
                chunk = DataChunk()
                for count, item in enumerate(value):
                    if count and count % max_size == 0:
                        chunks.append(chunk)
                        chunk = DataChunk()
                    chunk.data.append(item)
                chunks.append(chunk)

                chunk_refs = []
                for c in chunks:
                    serializer.detach_lineage.append(detach)
                    ref_id, _ = serializer._traverse_base(c)
                    ref_obj = serializer.detach_helper(ref_id=ref_id)
                    chunk_refs.append(ref_obj)
                object_builder[prop] = chunk_refs

            # 4. handle all other cases
            else:
                child_obj = serializer.traverse_value(value, detach)
                object_builder[prop] = child_obj

            closure = {}
            # add closures & children count to the object
            detached = serializer.detach_lineage.pop()
            if serializer.lineage[-1] in serializer.family_tree:
                closure = {
                    ref: depth - len(serializer.detach_lineage)
                    for ref, depth in serializer.family_tree[
                        serializer.lineage[-1]
                    ].items()
                }

            ############ ADDING OUR MAGIC HERE #################################
            closure.update(closures)

            object_builder["totalChildrenCount"] = len(closure)

            obj_id = hashlib.sha256(ujson.dumps(object_builder).encode()).hexdigest()[
                :32
            ]

            object_builder["id"] = obj_id
            if closure:
                object_builder["__closure"] = serializer.closure_table[obj_id] = closure

            # write detached or root objects to transports
            if detached and serializer.write_transports:
                for t in serializer.write_transports:
                    t.save_object(
                        id=obj_id, serialized_object=ujson.dumps(object_builder)
                    )

            del serializer.lineage[-1]

            if serializer.write_transports:
                for wt in serializer.write_transports:
                    wt.end_write()

            return obj_id, object_builder

WOW. What was that? It is a modified form of the traverse_base method of the BaseObjectSerializer in specklepy. Ordinarily you don’t need to worry about the Base

The version above extracts the function from the serializer class and add the ability to pass in custom closures (because, by default, it won’t make any for a purely referenceObject commit.

We can use that modified method by injecting the closures and the standard BaseObjectSerializer class.

serializer = BaseObjectSerializer(write_transports=[transport])

obj_id, serialized_object = traverse_base(serializer, new_commit_object, closures)

It isn’t necessary, but I have returned the serialized_object for inspection purposes print()ing it shows wat we have achieved

{'id': '5e9ac0017b74034997dbe5fa45714a90',
 'speckle_type': 'Base',
 'totalChildrenCount': 482,
 'Components': [{'id': '8ca84c1c0447b4caaed8b622dad90263',
   'speckle_type': 'reference',
   'totalChildrenCount': 0,
   'applicationId': None,
   'referencedId': 'f048873d78d8833e1a2c0d7c2391a9bb',
   'units': None},
  {'id': 'e4b7f1ace651fa8a899d4860a0572af6',
   'speckle_type': 'reference',
   'totalChildrenCount': 0,
   'applicationId': None,
   'referencedId': 'de61f36d6a4c6b9713e445ab4d801ea9',
   'units': None},
  {'id': '5d1c1e466dd4df7ae76c7c9183b4317f',
   'speckle_type': 'reference',
   'totalChildrenCount': 0,
   'applicationId': None,
   'referencedId': '90f505f7625cd121e99af6e81a1a1013',
   'units': None}],
 '__closure': {'0042e47be89ba7af3cd0344012dd44fb': 6,
  '0225bdfc617ae2e2cfa3182e5f319026': 8,
  '03ab601e5a6e7743dbada875bd634a3d': 3,
  '04849987174c213dcfba897757bcf4b4': 6,
  '04b68bc41ce7aa7e58e088e997193684': 5,
  '062f59e346ab9ba7f59d60a46b4e421a': 4,
  '085d6f93043117211d14fbf9d5443b6a': 6,
  '09514b6698a1bd2eb1416cf67ffd0f7a': 6,

... SNIP 100s of object ids... 

  'de61f36d6a4c6b9713e445ab4d801ea9': 1,
  '90f505f7625cd121e99af6e81a1a1013': 1}}

There’s that telephone directory. The Speckle viewer loves it :heart:

We can race to the end now:

commit_id2 = client.commit.create(
    branch_name=branch.name,
    stream_id=stream_id,
    object_id=obj_id,
    message="federated commit",
)

Once again we build the embed URL and display it.

embed_url2 = f"https://speckle.xyz/embed?stream={stream_id}&commit={commit_id2}&transparent={transparency}&autoload={autoload}&hidecontrols={hide_controls}&hidesidebar={hide_sidebar}&hideselectioninfo={hide_selection_info}"

from IPython.display import IFrame

IFrame(embed_url2, width=400, height=300)

Wrapping up.

This federation is quite simple, quite clunky and doesn’t de-dupe at all, as it does not even examine the individual commits’ content.

To do anything approaching this, we need to revisit Merge 2:

  • load the child members of each commit
  • have a strategy for de-duping
  • have a strategy for merging
  • have a strategy for filtering
  • have a strategy for handling any other conflicts
6 Likes

This tutorial demonstrated creating a federated commit in Speckle using Python and the SpecklePy library. We explored three different methods to merge commits: the simple method, the object-level big-bucket method, and the reference object method.

We also demonstrate a little of how the essence of Speckle works, where once an object is sent to Speckle, its uniqueness is the property that matters most to the Connectors. If the thing doesn’t change, none of the Connectors sends it again. Instead, Speckle can use the originally sent object and include a reference to it in its place, known as a ReferenceObject.

The first method covers mimicking actions you can perform on the web front end using overlays.

In the third method, we leveraged the mechanics behind the Speckle magic by creating a new Federation class, essentially a collection of ReferenceObjects. We used a helper function to get the __closure table for each reference object, a directory of all the child objects used to handle processable things. We then used a modified traverse_base method to inject the closures and create a commit object that contains all the objects in the reference commits.

In the middle, the second method is probably the jumping-off point for exploring more sophisticated assemblies. While gathering the data is a brute force, the results are potentially powerful once you wrap strategies to it. Crucially for some interested in large-scale Data Management solutions, it allows for merges across Project Streams.

7 Likes

Very cool approaches Jonathon, each having their used-case.
I think a combination can be great, where:

  • the merging 1 (overlay method) can be used as a sort of checking/reviewing tool within the stream, whether the ‘shared’ commits are complete/not duplicate
    it would be very powerfull if this functionality can be used cross-streams as well

  • the merging 2/3 are great uses to actually federate them where we can merge commits:

    • Coming from other streams as ‘linked models’
      • This approach allows you to keep ownership of only the information you are owner of.
    • We can create rules (only commits with a certain status/commits in a given branch) can be merged
    • We can do diffs based on the origin of the commit and an older version of it
2 Likes

Each of the three will have its use cases - but as you say, none is “feature complete” beyond simple merging.

For the first version (Federating Speckle Models), I played around a bit, and created a very basic Streamlit+specklepy app (copy-paste is possible)
Just make sure your speckleToken is correct (can also be filled in while running the app)
I have some issues with the “autoload” of the iframe, but pressing play on it also does the trick

#--------------------------
#IMPORT LIBRARIES
#import streamlit
import streamlit as st
#specklepy libraries
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_account_from_token
#--------------------------

#--------------------------
#PAGE CONFIG
st.set_page_config(
    page_title="Speckle Merge type 1 - Viewer Overlays",
    page_icon="📊"
)
#--------------------------
#--------------------------
#CONTAINERS
header = st.container()
input = st.container()
viewer = st.container()
report = st.container()
graphs = st.container()
#--------------------------

#--------------------------
#Definitions
#create a definition that generates an iframe from commit id

#--------------------------
#viewerparameters
transparency = True
autoload = True
hide_controls = False
hide_sidebar = True
hide_selection_info = True
#--------------------------
#single commit viewer, ?why is the autoload not working
def commit2viewer( stream, commitId, height=600) -> str:
    stream_id=stream.id
    embed_src = (
    f"https://{speckleServer}"
    f"/embed?stream={stream_id}"
    f"&commit={commitId}"
    f"&transparent={transparency}"
    f"&autoload={autoload}"
    f"&hidecontrols={hide_controls}"
    f"&hidesidebar={hide_sidebar}"
    f"&hideselectioninfo={hide_selection_info}"
    )
    
    #print(embed_src)
    return st.components.v1.iframe(src=embed_src, height=height)
#--------------------------

#overlay function.. ?why is the autoload not working?
def commit2viewerOverlay(stream, commitIds, height=600) -> str:
    overlay = ",".join(commitIds[1:])
    stream_id=stream.id
    embed_src = (
    f"https://{speckleServer}"
    f"/embed?stream={stream_id}"
    f"&commit={commitIds[0]}"
    f"&overlay={overlay}"
    f"&transparent={transparency}"
    f"&autoload={autoload}"
    f"&hidecontrols={hide_controls}"
    f"&hidesidebar={hide_sidebar}"
    f"&hideselectioninfo={hide_selection_info}"
    )
    
    #print(embed_src)
    return st.components.v1.iframe(src=embed_src, height=height)
#--------------------------

#--------------------------
#HEADER
#Page Header
with header:
    st.title("Speckle Stream Activity App📈")
#About info
with header.expander("About this app🔽", expanded=True):
    st.markdown(
        """This app is just an example of stream overlay options which you can copy/paste yourself.
        Keep in mind the Speckle Token!!
        """
    )
#--------------------------

with input:
    st.subheader("Inputs")
    branchList=list()
    #-------
    #Columns for inputs
    serverCol, tokenCol = st.columns([1,3])
    #-------
	#User Input boxes
    speckleServer = serverCol.text_input("Server URL", "speckle.xyz", help="Speckle server to connect.")
    speckleToken = tokenCol.text_input("Speckle token", "087fea753d12f91a6f692c8ea087c1bf4112e93ed7", help="If you don't know how to get your token, take a look at this [link](<https://speckle.guide/dev/tokens.html>)👈")
    #-------
    #CLIENT
    client = SpeckleClient(host=speckleServer)
    #Get account from Token
    account = get_account_from_token(speckleToken, speckleServer)
    #Authenticate
    client.authenticate_with_account(account)
   
    #-------
    #Streams List👇
    streams = client.stream.list()
    #Get Stream Names
    streamNames = [s.name for s in streams]
    streamDict=[{s.name:s} for s in streams] #create dict of streams (better than the stream.search)
    
    #Checkmark for federating more than one stream
    sName = st.selectbox(label="Select your stream", options=streamNames, help="Select your stream from the dropdown")
    overlayChck=st.checkbox(label="Check this box to overlay multiple commits within the stream", value=True)

    #SELECTED STREAM ✅
    stream=None
    try:
        stream=streamDict[sName]
    except:
        stream=client.stream.search(sName)[0]
        
    #print("!!!!!!!!!!!!!" + stream.name)
    #Stream Branches 
    branches = client.branch.list(stream.id)
    #Stream Commits 🏹
    commits = client.commit.list(stream.id, limit=100)
    
    commitDict=dict()
    commitDict={str(c.branchName +"\ ("+c.id+"), date: " + str((c.createdAt).strftime("%Y-%m-%d (%H:%M): ") + c.authorName +" "+ c.message)):c.id for c in commits}
    commitNames=commitDict.keys()

    #switch selectionelements based on checkmark
    if overlayChck:
        commitList=st.multiselect("Select commits on stream", options=commitNames)
        selectedCommits=[commitDict[c] for c in commitList]
    else:
        commitList=st.selectbox(label="commits",options=commitNames)
        selectedCommits=commitDict[commitList]

    #-------

#--------------------------
#VIEWER👁‍🗨
with viewer:
    #calls different iFrame viewers based on checkmark
    if not overlayChck:
        st.subheader("Only the one selected commit👇")
        try:
            commit2viewer(stream, selectedCommits)
        except:
            st.text("Failed to load latest commit on stream")
    else:
        st.subheader("Overlay of selected commits👇")
        try:
            commit2viewerOverlay(stream, selectedCommits)
        except:
            st.text("Failed to load latest commit on stream, select more than 1 commit")
#--------------------------


3 Likes

:heavy_heart_exclamation: :trophy: @jonathon what a great post to be bringing to the community. I also appreciate the info on closures, I’ve been curious to know how those work and it seems to be somewhat similar to OSM nodes. I have some thoughts on this functionality from designer :triangular_ruler: and developer :keyboard: perspective.

designers being git-ish

I think this is one of the most challenging parts for designers trying to adopt Speckle into project teams and I think it’s because design teams approach their modeling and 3d stuff with a wholistic mentality. Por ejemplo, when designers Reviting away their buildings they will be asked to sync their models so their team has their most recent updates. This sync doesn’t require the user to select what they want to send up, they just click the please work button and sit patiently awaiting their fate and hopefully their whole model is shared with their team.

When I’ve worked with those same teams to onboard Speckle for their models they default to pushing everything in their model up to main. Which is a :tada: moment for new teams, but it doesn’t really create a clean way to leverage the power of referencing objects. I usually try to push for teams separate their model into branches or commits and that’s when I notice a big disconnection with the team’s understanding and how they would modify their work to be broken into components. I’ve created some examples to show how a stream could be setup, usually with branches like arch\footprint, landscape\planting-p1, or context\topo\. I’ll also use the web viewer to overlay the different commits into one view and that starts to make a bit more sense with the teams, giving them a bit more understanding… Not sure where I was going with this. :man_facepalming: I feel like this was a long way of me saying “federated models / referenced objects will be a great tool for designers”


referencing objects

This is a bit of a tangent from the post, so feel free to skip it and just tell me you thought it was “interesting”. Seeing the federation diagrams above reminds me of an idea I had a while back for improving my custom speckle objects by changing objects that had a direct relationship on geometry to be a string that reference the commitId.

//  what I first used to keep track of geometry objects 
List<object> contextGeometry;
// changing the field to be a list of commit ids to pull down
List<string> references ;

I’m not sure what made me go with using the commit id vs the referenceId on the object. I did think this was pretty clever at first. It made my objects much lighter, removed any dependencies my custom converter had on the standard speckle one, and it just made me feel like a :superhero:, I even wrote up a lil pr. I though it would be helpful to have a standard object that developers could use that to compose different commits in a stream(s). But, I’m not really sure how beneficial this feature would end up. The way I designed this system would require converters and connectors to handle the logic for layering in multiple commits.

Then @AlanRynne reached out and pointed me to the ReferenceObject, which ended up being exactly what I needed (thanks again Alan!). I swapped in this object type and saw it just, work. :slight_smile: I think with features like this and tutorials that help demonstrate this type of functionality will make the dev experience a much more enjoyable one.

5 Likes

Hello,

I’m looking at method 1 for simplicity for the moment and the lack of an available webserver with python. My plan is to merge several branches to one for the viewer in the UI.

Would it be possible to generate a link based on the top commit of a branch instead of a commit with an ID?

Best regards,
Michael

While it is possible to address the latest commit on a branch for a single model, sadly, the overlays method does require commit IDs to work.

Stick around for when the FE2 Project::Modell::Versions API comes into force, as new notations exist for assembling federations. FE2 doesn’t yet have an embedded viewer, though.