Hi,
Can you please tell me how to import block instances correctly?
My task is to load a .3dm model into a RhinoDoc, but before adding the geometry, I need to modify some of the input data (like materials and transformations). I’m currently stuck at the point of importing block instances properly.
When I run my code, doc.InstanceDefinitions.Add(...) seems to add one extra object - I expect 3 block instances, but I get 4. Also, the transformations applied to the instances are incorrect.
Here’s a simplified snippet of what I’m doing:
File3dm model = scene.Floorplan.Model;
foreach (var idef in model.AllInstanceDefinitions)
{
var geometryList = idef.GetObjectIds()
.Select(id => model.Objects.FindId(id).Geometry.Duplicate())
.ToList();
int idef_index = doc.InstanceDefinitions.Add(idef.Name, idef.Description, Point3d.Origin, geometryList);
if (idef_index < 0)
{
RhinoApp.WriteLine("Unable to import block from file.");
}
}
foreach (var obj in model.Objects)
{
var geometry = obj.Geometry.Duplicate();
var attributes = obj.Attributes.Duplicate();
if (geometry is InstanceReferenceGeometry instance)
{
if (scene.Floorplan.Model.AllInstanceDefinitions.FindId(instance.ParentIdefId) is InstanceDefinitionGeometry definition)
{
var guid = doc.Objects.AddInstanceObject(definition.Index, instance.Xform);
}
}
else
{
doc.Objects.Add(geometry, attributes);
}
}
First off importing blocks from a 3dm model can be a bit of headspin – its a lot easier just to use RhinoScript’s insert command - this might be better if you’re importing a whole “model”, once its imported you can manipulate it. If I remember right the _B part imports as block, so potentially easier to manipulate objects since you can operate on the objects within the block, and then you could explode it when you’re finished (if you no longer need it as a block).
var model = fileName
string insertCommand = $"-_Insert _F _L _L \"{model}\" _B 0,0,0 _Enter _Enter";
RhinoApp.RunScript(insertCommand, true);
If you’re set on loading from the model, then you can tweak your code a little bit:
File3dm model = scene.Floorplan.Model;
foreach (var idef in model.AllInstanceDefinitions)
{
var geometryList = idef.GetObjectIds()
.Select(id => model.Objects.FindId(id).Geometry.Duplicate())
.ToList();
var attributesList = idef.GetObjectIds()
.Select(id => model.Objects.FindId(id).Attributes.Duplicate())
.ToList();
int idef_index = doc.InstanceDefinitions.Add(idef.Name, idef.Description, Point3d.Origin, geometryList, attributesList);
if (idef_index < 0)
{
RhinoApp.WriteLine("Unable to import block from file.");
}
foreach (var obj in model.Objects)
{
var geometry = obj.Geometry.Duplicate();
// Check if instance parentIdefId matches the current idef
if (geometry is InstanceReferenceGeometry instance && instance.ParentIdefId == idef.Id)
{
// Only duplicate attributes when we need them
var attributes = obj.Attributes.Duplicate();
// If there is a match, create instance using the the new definition we created
// not the definition from the model we loaded
// also, add the attributes from the obj
doc.Objects.AddInstanceObject(idef_index, instance.Xform, attributes);
}
}
}
// Add everything else
foreach (var obj in model.Objects)
{
var geometry = obj.Geometry.Duplicate();
var attributes = obj.Attributes.Duplicate();
// These objects are used as part of instance definitions
// We probably don't want to add them to the model
// So we skip them
if (attributes.IsInstanceDefinitionObject)
continue;
doc.Objects.Add(geometry, attributes);
}
Key changes:
Add attributes to doc.InstanceDefinitions.Add, this might be unnecessary but probably a good idea to ensure they match from the original model
We now use AddInstanceObject within the foreach loop that creates the InstanceDefinition, and use the index of the definition we added to the current RhinoDoc, not the index of the definition from the loaded model.
Add attributes when using AddInstanceObject, otherwise layers and other attributes won’t transfer over.
When adding “everything else” skip over objects that are used as part of InstanceDefinitions – these aren’t normally part of the model
That said, this won’t work for nested blocks (which you should probably make it work for). Its a little more complicated as you’ll need to creating a mapping between top level definitions and their respective nested definitions…
I have created a python variation of the function that uses an iteration on nested blocks. Still i ran into a different problem regarding the object attributes:
For linetypes with the same name, the linetypeIndex may differ between the active and the imported file. In my setup some imported objects had a wrong linetyp, since the indexes didnt match between the files. I have got a solution for the problem, but there might be more similar problems with the attributes:
def importBlockInstanceFromFile(filePath, blockname):
def myfunction(idef):
objectIds = idef.GetObjectIds()
for objectId in objectIds:
obj = rhino_file.Objects.FindId(objectId)
if obj.ComponentType == DocObjects.ModelComponentType.InstanceDefinition:
myfunction(obj)
else:
name = idef.Name
description = idef.Description
basePoint = Geometry.Point3d()
geometry_list = [rhino_file.Objects.FindId(id_).Geometry.Duplicate() for id_ in idef.GetObjectIds()]
attributes_list = [rhino_file.Objects.FindId(id_).Attributes.Duplicate() for id_ in idef.GetObjectIds()]
for att in attributes_list:
#Find linetype in the imported file
linetypeRef = rhino_file.AllLinetypes.FindIndex(att.LinetypeIndex)
if linetypeRef:
linetypeNameRef = linetypeRef.Name
linetype = RhinoDoc.ActiveDoc.Linetypes.FindIndex(att.LinetypeIndex)
linetype_Name = linetype.Name
if linetypeRef is not linetype_Name:
correctIndex = RhinoDoc.ActiveDoc.Linetypes.FindName(linetypeNameRef).Index
if correctIndex: att.LinetypeIndex=correctIndex
RhinoDoc.ActiveDoc.InstanceDefinitions.Add(name,description,basePoint,geometry_list,attributes_list)
#read the 3dm
rhino_file = FileIO.File3dm().Read(filePath)
#get table of objects in the 3dm file
idef = rhino_file.AllInstanceDefinitions.FindName(blockname)
myfunction(idef)
Does anyone have a better solution to deal with this?
@peter.zock I can’t think of anything clever off the top of my head.
My only thought would be if you want to guarantee that the correct linetype/hatch/group/material is used you could loop through and transfer these to new model from your imported model, and then when importing the block, you would lookup something like the linetype by name, rather than by index.
This is kinda similar to how Rhino handles duplicate block imports, where if there’s a block with the same name in the model (i.e. Block A) it will ask if you want to keep both, and if you agree, it will will rename the imported block to Block A 01. In the same way you could add 01 to all the imported linetypes to ensure there’s no conflict (although this assume that 01 is unique enough that its unlikely to already exist in the model, if you’re importing blocks from different models it would likely only work for the first import… you can make this as sophisticated as you wanted though if that’s a potential problem, using something like GetUnusedLinetypeName). Then when importing your block, when it came to setting the linetype you would know that the correct linetype name is {linetype_Name} + 01. If you ended up using GetUnusedLinetypeName then you would have to keep track of what the original linetype index/name was to map to the new linetype index.
This has the downside that you’ll end up with duplicates of any of the standard linetypes, but you could probably extend this to check if any of the linetypes were the standard Rhino linetypes or even just check if there’s any of the imported linetypes match existing linetypes and only import/add if they’re truly unique and have a clashing index.
Thanks for you reply! I guess one day I will try to make a proper function based on your proposal. For now I have another solution:
def importromFile(filePath):
# Get the starting object runtime serial number
start_sn = DocObjects.RhinoObject.NextRuntimeSerialNumber
#Import file
RhinoDoc.ActiveDoc.Import(filePath)
# Get the ending object runtime serial number
end_sn = DocObjects.RhinoObject.NextRuntimeSerialNumber
#Delete all objects that are not beeing used in a InstanceDefinition
rh_objects=[]
for sn in range(start_sn, end_sn):
rh_obj = sc.doc.Objects.Find(sn)
if rh_obj:
if not rh_obj.IsInstanceDefinitionGeometry:
sc.doc.Objects.Delete(rh_obj,True,True)
else:
rh_objects.append(rh_obj)
return rh_objects
Its more slow, but does manage the mapping under the hood;
Thanks for you reply! I guess one day I will try to make a proper function based on your proposal. For now I have another solution:
def importromFile(filePath):
# Get the starting object runtime serial number
start_sn = DocObjects.RhinoObject.NextRuntimeSerialNumber
#Import file
RhinoDoc.ActiveDoc.Import(filePath)
# Get the ending object runtime serial number
end_sn = DocObjects.RhinoObject.NextRuntimeSerialNumber
#Delete all objects that are not beeing used in a InstanceDefinition
rh_objects=[]
for sn in range(start_sn, end_sn):
rh_obj = sc.doc.Objects.Find(sn)
if rh_obj:
if not rh_obj.IsInstanceDefinitionGeometry:
sc.doc.Objects.Delete(rh_obj,True,True)
else:
rh_objects.append(rh_obj)
return rh_objects
Its more slow, but does manage the mapping under the hood;
Update:
The RhinoDoc.ActiveDoc.Import(filePath) doesnt handle nested blocks very good. For the nested blocks it will create duplicates. So just in case somenone is interested into this, here is my updated version that can handle nested blocks and that will match layers and linetypes:
Still importing stuff this way feels kind of hard
#! python 3
import rhinoscriptsyntax as rs
from Rhino import Geometry, DocObjects, RhinoDoc, FileIO
import scriptcontext as sc
def import_InstanceDefinition_FromFile(filePath):
def reassign_attributes_indexes(attributes_list):
for attributes in attributes_list:
if attributes.LinetypeIndex > -1:
key = importFile.AllLinetypes.FindIndex(attributes.LinetypeIndex).Name
if key in dic_attributes_match.keys():
attributes.LinetypeIndex = dic_attributes_match[key]
if attributes.LayerIndex:
key = importFile.AllLayers.FindIndex(attributes.LayerIndex).Name
if key in dic_attributes_match.keys():
attributes.LayerIndex = dic_attributes_match[key]
def importBlockInstance(idefgeometry):
objectIds = idefgeometry.GetObjectIds()
geometry_list=[]
attributes_list=[]
for objectId in objectIds:
rhobj = importFile.Objects.FindId(objectId) #Get the objects in the InstanceDefinition
if rhobj.Geometry.ObjectType == DocObjects.ObjectType.InstanceReference: #If one of the object is ObjectType.InstanceReference:
#Get the parent InstanceDefinition
parentIdefId = rhobj.Geometry.ParentIdefId
parentIdef = importFile.AllInstanceDefinitions.FindId(parentIdefId)
#Add the parent InstanceDefinition to the active doc if not existing already
if not sc.doc.InstanceDefinitions.Find(parentIdef.Name):
importBlockInstance(parentIdef)
parentIdef_new = sc.doc.ActiveDoc.InstanceDefinitions.Find(parentIdef.Name) #Get the new copy of the parent InstanceDefinition in the doc
geometry_list.append(Geometry.InstanceReferenceGeometry(parentIdef_new.Id, rhobj.Geometry.Xform)) #Get the geometry base
attributes_list.append(rhobj.Attributes.Duplicate()) #Get the attributes
else:
geometry_list.append(rhobj.Geometry.Duplicate())
attributes_list += [rhobj.Attributes.Duplicate()]
#Reassign index
reassign_attributes_indexes(attributes_list)
#Add the InstanceDefinition to the active doc
idefName = idefgeometry.Name
description = idefgeometry.Description
basePoint = Geometry.Point3d()
index = sc.doc.InstanceDefinitions.Add(idefName,description,basePoint,geometry_list,attributes_list)
importFile=FileIO.File3dm.Read(filePath)
if importFile:
dic_attributes_match = {}
for layer in importFile.AllLayers:
index = sc.doc.Layers.FindByFullPath(layer.FullPath,-1)
if index > -1: #If the layer is already in the doc
dic_attributes_match[layer.Name] = index
else: #If the layer is not in the doc it has to be created
newLayerIndex = sc.doc.Layers.Add(layer)
dic_attributes_match[layer.Name] = newLayerIndex
for linetype in importFile.AllLinetypes:
linetype_match = sc.doc.Linetypes.FindName(linetype.Name) #If the linetype was found, the linetype index, >=0, is returned. If the linetype was not found, -1 is returned. Note, the linetype index of -1 denotes the default, or "Continuous" linetype.
if linetype_match.Index > -1: #If the linetype is already in the doc
dic_attributes_match[linetype.Name] = linetype_match.Index
#print('Layer -' + str(linetype.Name) + '- already in the file, New Index: ' + str(linetype_match.Index))
else: #If the layer is not in the doc it has to be created
newLinetypeIndex = sc.doc.Linetypes.Add(linetype)
dic_attributes_match[linetype.Name] = newLinetypeIndex
#print('Layer created. ' + str(linetype.Name) + '; New Index: ' + str(newLinetypeIndex) + ', Index in the imported file: ' + str(linetype.Index))
for idef in importFile.AllInstanceDefinitions:
if not sc.doc.InstanceDefinitions.Find(idef.Name):
importBlockInstance(idef)
if __name__ == "__main__":
import_InstanceDefinition_FromFile(yourFilePath)