Hi @henkon, apologies for the late reply! No worries about the long post
Some of your remarks sound like potential bugs from our end. Specifically:
Either those two windows are not hosted anywhere, or we’re fumbling something up.
This one too. “All” should really mean all
This is something that was requested in this thread - so it should make it in our todos.
This is defintively something we should enable and/or write a tutorial about, as it’s not that straightforward, especially because we have two APIs right now through which you can do it (you probably noticed that ) - one is the GraphQL way, which you have tried out, and that supports queries, and the other one is the REST way, which we use for sending and receiving (and that doesn’t support queries).
In .NET, To resolve the “not fun” part of the dynamics and such, i would actually forego the graphql way, and go for receiving the objects the classic way the connectors do and writing a traversal function to isolate what object categories you need. Would you need some help with that? Someone from the team could give it a shot!
I’m recommending this way mostly because we have https://github.com/specklesystems/speckle-server/issues/340 in the backlog since a while, and we plan to tackle it sooner or later! (will add support for querying objects on the REST endpoint too).
Hi @dimitrie
thanks for the reply, I didn’t find it late at all.
For the windows issue:
I did not check in Revit if there are two un-hosted windows, could be. The two windows reported in speckle on-line has no geometry.
For the issue with the doors:
Great. I have only tested the basic example model from Revit, but seems like a bug maybe
For the issue with wall structure:
Great, it sounds good, as it is important
For the “how to treat responses from graphql”, I will look into going in the non-graphql way then. I will try to find documentation on the REST API instead and see how that works out. If I need help, I will let you know by replying on the thread. Maybe I will find some good examples as well. If you have some examples/resources from your knowledge available, I would welcome them of course Happy weekend!
Hi @henkon, just had some fun today hacking a quick Flatten function + some LINQ filtering. I’ve posted the full spiel in our docs right over here (we try to document valid questions and responses!)
Long story short, add this extension method somewhere to your project:
public static class Extensions
{
// Flattens a base object into all its constituent parts.
public static IEnumerable<Base> Flatten(this Base obj)
{
yield return obj;
var props = obj.GetDynamicMemberNames();
foreach (var prop in props)
{
var value = obj[prop];
if (value == null) continue;
if (value is Base b)
{
var nested = b.Flatten();
foreach (var child in nested) yield return child;
}
if (value is IDictionary dict)
{
foreach (var dictValue in dict.Values)
{
if (dictValue is Base lb)
{
foreach (var lbChild in lb.Flatten()) yield return lbChild;
}
}
}
if (value is IEnumerable enumerable)
{
foreach (var listValue in enumerable)
{
if (listValue is Base lb)
{
foreach (var lbChild in lb.Flatten()) yield return lbChild;
}
}
}
}
}
}
Afterwards, you’ll be able to do things such as:
var data = Helpers.Receive("https://speckle.xyz/streams/0d3cb7cb52/commits/681cdd572c").Result;
var flatData = data.Flatten().ToList();
var windows = flatData.FindAll(obj => (string)obj["category"] == "Windows");
var timberWalls = flatData.FindAll(obj => obj is Objects.BuiltElements.Revit.RevitWall wall && wall.type == "Wall - Timber Clad");
var rooms = flatData.FindAll(obj => obj is Objects.BuiltElements.Room);
var levels = flatData.FindAll(obj => obj is Objects.BuiltElements.Level).Cast<Objects.BuiltElements.Level>().GroupBy(level => level.name).Select(g => g.First()).ToList();
Console.WriteLine($"Found {windows.Count} windows.");
Console.WriteLine($"Found {timberWalls.Count} timber walls.");
Console.WriteLine($"Found {rooms.Count} rooms.");
Console.WriteLine($"Found {levels.Count} levels.");
var elementsByLevel = flatData.FindAll(obj => obj["level"] != null).GroupBy(obj => ((Base)obj["level"])["name"]);
foreach(var grouping in elementsByLevel) {
Console.WriteLine($"On level {grouping.Key} there are {grouping.Count()} elements.");
}
Hi again @dimitrie, I really appreciate the help. I tried to replicate your joy from yesterday. hehe, I hit a snag testing it in a console app, but at least got it working from a windows forms app. Thanks
@dimitrie may I challenge you to return all parameters for a window?
First, LINQ is not my strong side yet, and secondly I get in the same trouble as I did with dynamics when trying. I am unable to get the parameters without accessing them by named index, and I do not know them by name up front
Ah, that’s easy. You can get all dynamic prop names from a base object using GetDynamicMembers(), then you can simply iterate through them and access the object via [] notation.
Here’s a snippet i cooked up just now:
var myWindowParams = ((Base)windows[0])["parameters"] as Base;
var propNames = myWindowParams.GetDynamicMembers();
foreach(var prop in propNames)
{
var param = myWindowParams[prop] as Objects.BuiltElements.Revit.Parameter;
Console.WriteLine($"{prop}: {param.value} (Name: {param.name}, Unit: {param.applicationUnit})");
}
Edit: note, i’m doing this for a single window only. Output below. Note, some params come with nulls or empty values, so you should always act defensively there.
hehe, many thanks @dimitrie , I have tried a few combinations that is so close to your snippet, as this was how I imagined it. But didn’t manage to cast things correctly.
Yes, i feel you. We’re trying to allow for full flexibility in a strongly typed language, and sometimes the compromises feel s bit off. As we progress, i hope we will have better patterns emerge and/or document things properly
This is somehow related to my issue, so I just drop my question here @dimitrie.
I’ve got a stream created in Grasshopper with Revit elements in it. Now I try to use the Flatten extension method described above to get out the objects in my @data. This does give back a list with the @data in it again. Double (or tripple) flattening does not do the job. I noticed my stream structure is quite different from the one in the Flattening docs, but I am a bit lost on how to receive objects and use them further in the code. I’m using C# with the Revit API.
Grasshopper 2.5.2 (2.6 gave other issues described here by someone else)
Revit 2.6
so you’re trying to flatten a Grasshopper commit in Revit?
As far as I’m aware, the flatten function should work as expected in 2.5.2. As for 2.6.0, we’ve modified the structure of Grasshopper’s data trees to be a Base object instead of nested lists, but it should still be compatible with the flatten function.
I’m going to do some tests and i’ll get back to you.
Thanks for the response: Speckle, I was unable to receive this stream in my plugin.
Before we used the @SpeckleSchema detached property to actually map the elements to it’s C# object, that seems to not be possible anymore. Then I started looking what would be the right way and I bumped into that Flatten functionality which seemed quite useful for us.
What we are trying to do is get the family name and type from the SpeckleRevit object, and change it in the Speckle stream through a UI if that’s what the user want. Then we update the stream. If there is a more easy way to that then the mapping to the SpeckleRevitObj I would be keen to hear that.
So I’ve had a chat with @dimitrie, and it turns out the Flatten function provided in the docs is more intended as an example (and was designed to work with Revit commits mostly).
I’ve played around a bit and came up with a new Flatten function I’d love you to try. This one no longer takes in a Base object, but rather a generic object, and returns a Enumerable<Base> just like the previous one.
public static class Extensions
{
public static IEnumerable<Base> Flatten(object obj, HashSet<string> existingCache = null)
{
var cache = existingCache ?? new HashSet<string>();
switch (obj)
{
case Base @base:
{
if (cache.Contains(@base.id)) break;
cache.Add(@base.id);
yield return @base;
foreach (var prop in @base.GetDynamicMemberNames())
foreach (var child in Flatten(@base[prop], cache))
yield return child;
break;
}
case IDictionary dict:
foreach (var dictValue in dict.Values)
foreach (var child in Flatten(dictValue, cache))
yield return child;
break;
case IEnumerable enumerable:
foreach (var listValue in enumerable)
foreach (var child in Flatten(listValue, cache))
yield return child;
break;
}
}
}
then just use it like this
var obj = Speckle.Core.Api.Helpers.Receive("YOUR_STREAM_URL").Result;
var flat = Extensions.Flatten(obj).ToList();
I tested it on your commit and some other more complex ones with some success. I’m also preventing duplicate objects from being added to the flat list multiple times.
As for this, not sure what you’re referring to, I’m not aware of any changes in the behaviour of the speckleSchema tags atm, could you give us a bit more detail about this? Feel free to open a different thread as it seems non-related to this directly
Anyway, let me know if the new function works as expected!
var commit = await AppState.Client.CommitGet(AppState.Stream.id, AppState.Stream.commit.id);
var serverTransport = new ServerTransport(AppState.Client.Account, AppState.Stream.id);
var res = await Operations.Receive(commit.referencedObject, serverTransport);
var properties = res.GetDynamicMembers().ToList();
var objList = new List<Base>() { };
var objects = properties.SelectMany(prop =>
{
var listOfLists = res[prop] as List<object>;
return listOfLists?.Select(l => l) ?? new List<object>();
}).ToList().FirstOrDefault() as List<object>;
var schemaObjects = objects.OfType<dynamic>()
.Select(l => l["@SpeckleSchema"] as dynamic).Where(lb => lb != null)
.ToList();
foreach (var schema in schemaObjects)
{
switch (schema)
{
case RevitBeam revitBeam:
objList.Add(revitBeam);
break;
case RevitColumn column:
objList.Add(column);
break;
case RevitWall wall:
objList.Add(wall);
break;
case RevitFloor floor:
objList.Add(floor);
break;
case Beam beam:
objList.Add(beam);
break;
case Column column:
objList.Add(column);
break;
case Wall wall:
objList.Add(wall);
break;
case Floor floor:
objList.Add(floor);
break;
default:
break;
}
}
This is a snippet from the previous codebase, we used this stream from testing: Speckle.
In this way we could easily access the objects properties later in the flow of the programme.
I’ll give the new flatten code a try tomorrow, thanks for creating it!
I just gave it a try, and it is doing the flattening indeed :), also tried different streams and all worked.
But what I’m currently struggling with is casting the Base objects to the right SpeckleRevit object:
var rooms = flatData.FindAll(obj => obj is Objects.BuiltElements.Room);
Trying above for RevitBeam did not work as the object is of type Base, while in the stream you can see that the property “speckle_type” is of RevitBeam.
Then I tried to cast the Base object to the RevitBeam in this way, Revit just froze:
var flat = Extensions.Flatten(res).ToList();
var beams = flat.Where(lb => lb.applicationId != null);
beams = beams.Where(obj => obj.applicationId.Contains("RevitBeam")).Cast<RevitBeam>().ToList();
But that’s also crashing.
Maybe some relevant background:
var obj = Speckle.Core.Api.Helpers.Receive("YOUR_STREAM_URL").Result;
var flat = Extensions.Flatten(obj).ToList();
I was not able to use the Helpers.Receive to receive the stream, so I did it like this:
var commit = await AppState.Client.CommitGet(AppState.Stream.id, AppState.Stream.commit.id);
var serverTransport = new ServerTransport(AppState.Client.Account, AppState.Stream.id);
var res = await Operations.Receive(commit.referencedObject, serverTransport);
I think there may be a bit of a misconception here. When I receive them, all the objects are properly instantiated as an instance of their specific class:
I think what’s happening here is that your script/project/plugin does not have access to the Objects.dll, hence you cannot cast that into a more specific class.
As an example, take the Revit connector:
It is a Revit plugin that is in charge of Send/Receive operations mainly but knows nothing about the objects it receives.
It also has a Converter Project that knows everything about the available speckle objects, as well as the Revit objects, so it acts as a “bridge” between the 2.
This was done to allow for extending the object model, or even replacing it for a different one alltogether.
What I’d do in your case
Depending on where you’re running this:
If its a Grasshopper/Dynamo script, you can just reference our Objects.dll from the Kits folder (%appdata%/speckle/kits/objects) and it should work alright.
If its a Revit or GH plugin (or any other plugin), you could do the same as above to ensure you’re always running the same version of objects,
If your project is designed to work “standalone”, in theory you could just use our Objects nuget package, but you’d have to deal with upgrading the versions as we do, or you might face some nasty errors.
As for not using the Helpers, its fine. I think what is throwing your code off is the final .Result at the end, which some platforms like and others don’t But what you’re doing looks fine.
As for filtering, what you’re doing seems correct:
Nice! Thanks for cross referencing, I hadn’t joined the dots by myself!
So I think the solution for this would be do the latter, and call Assembly.LoadFrom to ensure the Objects.dll is actually loaded, if you’re not doing this already If you are… then we may need to look deeper into this.
The way this currently works is that the Kit manager will inspect the Objects.dll for available types, but it won’t actually load the assembly until its needed, i.e. until a conversion is performed.
So you can’t really count on objects being loaded really, as you’ve already discovered but they will get loaded at some point…
I just messaged Dim to see if we could add some mechanism on our side to make your life easier, and rely on the KitManager to do this for you (and us). But for now, I think the solution that you found is good enough as long as you reference the Objects.dll in the Kits folder which you were already doing.
Anyway, I’m intrigued about the solution, but also how we could make your life as a developer a bit easier Do keep the questions coming! And if you have any suggestions we’d be happy to hear them too.
I actually dived into the RevitSpeckle plugin to investigate how the objects are being mapped and I noticed you were calling the Kit manager over there as well. I tried to implement this one in my code also, but without success (also did not try super hard ).
I will do some testing tomorrow, maybe it will work if I first receive some objects with the RevitSpeckle plugin. This won’t be a solution ofcourse, but it will give more insight in the problem.
I’ll expect to drop more questions later. Thanks a for the support!
Monday mornings aren’t that bad actually, I’ve found the issue
Here some description:
Mapping plugin (the one I develop now) installed in Revit with reference to Objects.dll 2.5.2 in Appdata folder
Updated Speckle to 2.6 so the Objects.dll is updated to 2.6
Did not update the installed Mapping plugin
Solution
Found out that Revit Speckle is also not able to launch
Deleted the installed Mapping plugin
I’ll update the references later and install it again, will confirm it here if the issue is solved
Speckle Revit now works again, and running my updated code through the addin manager also gives the expected results, the objects are now available in the right type.
As you can see from this issue we are very dependend on the updates of Speckle. If our plugin is finished, we probably have to constantly update it paralelly to the updates of Speckle which is a bit annoying. I’m not sure if there are is any clever solutions for that one (I also have limited C# and Revit plugin knowledge, I mostly develop full stack with Python / Vue), that would really help. I have been thinking about incorporating it in the Speckle manager before, would be really nice if that’s possible, but then this will still be an issue.