Force delete or purge nested layers

Hi everyone,

I often import 2D or 3D drawings from Vectorworks into Rhino, and every time the layer structure ends up being a bit of a mess with lots of nested layers, duplicates, and empty ones.

When I try to clean the file, I often want to delete certain layers, but Rhino won’t let me because they’re nested inside other layers or have dependencies. Even after moving or deleting the objects, some layers remain locked or can’t be purged.

Is there a way to force delete or purge layers, even if they’re part of a nested hierarchy? Something like a “delete layer and all its sublayers” command would be really helpful.

Thanks in advance for any advice or scripts that could help tidy up these imports.

Hi a2rc -

A nested hierarchy of layers doesn’t prevent deleting layers.
Do you also want to delete objects that are on layers?
-wim

Hi Wim,
thanks for your reply - maybe I wasn’t clear, sorry about that.

Let me give an example: I often get files with lots of different blocks. Inside the block, objects are on layer “XXX”, while the block itself is assigned to layer “YYY”.
When I try to delete layer “XXX”, Rhino shows the usual message saying “There is a block definition named [something] on layer XXX…”.

What I’d like to do is force delete that layer and also remove the objects or block definitions associated with it.
Is that possible in any way?

Hi a2rc -

Answering “Yes to All” to the question of deleting both the object and the layer when deleting a top-level layer will also delete any block instances on all sub-layers. When you then purge the document, all unused block definitions will be deleted. You can then delete any layer that previously contained geometry that was in a block definition.
-wim

Hi Wim,

That’s exactly the issue - since layer “XXX” only exists inside block definitions, I don’t get the usual message like “Layer XXX has 7 objects. Are you sure you want to delete the objects and the layer?” where I could simply confirm and remove everything.

Instead, I get the message “There is a block definition named [something] on layer XXX. Use the BlockManager command to delete this block before trying to delete this layer.”

In practice, this means I would have to manually delete all the blocks that contain that layer, which is often hundreds of them, and the process becomes very time-consuming.

Am I approaching this the wrong way, or is there a faster method to clean up those block-related layers?

you can select all Blockinstances _SelBlockInstance
or from a specific definition _SelBlockInstanceNamed
you can _explode blocks
you can _purge unused block instances.

Of course you can not delete a layer that contains geometry that is part of a block definition.
But you can edit a Block and move the geometry to another layer.

some of the commands above should solve your problem.
if not - please post an example .3dm file that shows the workflow you want to accomplish.

kind regards - tom

If you’re happy utilising the grasshopper interface, you can use the content cache mechanic, along with content filtering and drilling down within block instances and definitions to reassign imported blocks and their geometries away from ‘phantom’ layers, before updating/pushing them back into the rhino file again via content cache.

This is a process I’ve often done myself due to similar workflow issues with vectorworks handling.

Here’s the script I use for this.

# deletes layer by force (deletes objects from blocks)

import Rhino
import rhinoscriptsyntax as rs
import scriptcontext as sc

def unblock(idef):
	#return [p for p in rs.ExplodeBlockInstance(rs.InsertBlock2(idef.Name, rs.XformIdentity()))]
	instance = rs.InsertBlock2(idef.Name, rs.XformIdentity())
	rs.UnselectAllObjects()
	rs.SelectObject(instance)
	rs.Command("_Explode", echo=False)
	return rs.LastCreatedObjects()

def reblock(objList, idef):
	objectList = [sc.doc.Objects.Find(obj) for obj in objList]
	geomList = [obj.Geometry for obj in objectList]
	attrList = [obj.Attributes for obj in objectList]
	if sc.doc.InstanceDefinitions.ModifyGeometry(idef.Index, geomList, attrList):
		rs.DeleteObjects(objList)
		return True
	else:
		return False

def getIndex(layer):
	return sc.doc.Layers.FindByFullPath(layer, True)

def getChildren(layer):
	children = rs.LayerChildren(layer)
	sub = []
	for child in children:
		sub += getChildren(child)
	children += sub
	return children

def deleteLayer(layer):
	if rs.CurrentLayer() == layer:
		rs.MessageBox("Cannot delete current layer.", 16+0, "Error")
		return
	layerIndex = getIndex(layer)
	layerChildren = getChildren(layer)
	if rs.CurrentLayer() in layerChildren:
		rs.MessageBox("Cannot delete parent of current layer.", 16+0, "Error")
		return
	allIndices = [layerIndex] + [getIndex(child) for child in layerChildren]
	layerObj = [obj for obj in sc.doc.Objects if obj.Attributes.LayerIndex in allIndices]
	layerBlocks = [block for block in sc.doc.InstanceDefinitions if not block.IsDeleted and any([obj.Attributes.LayerIndex in allIndices for obj in block.GetObjects()])]
	message = "Layer <{}> is used by {} sublayer{}, {} object{}, and {} block definition{}.\n".format(layer, len(layerChildren), "" if len(layerChildren) == 1 else "s", len(layerObj), "" if len(layerObj) == 1 else "s", len(layerBlocks), "" if len(layerBlocks) == 1 else "s")
	message += "Deleting this layer will also delete any objects that use it or its sublayers and remove them from any block definitions.\n"
	message += "Are you sure you want to continue?\n\n"
	message += "To delete, click Yes.\nTo cancel, click No."
	result = rs.MessageBox(message, 48+4, "Objects on Layer")
	if result != 6:
		print("Cancel")
		return
	rs.EnableRedraw(False)
	for lay in [layer]+layerChildren:
		rs.LayerLocked(lay, False)
		rs.LayerVisible(lay, True)
	blocksToDelete = []
	for block in layerBlocks:
		objects = unblock(block)
		remaining = [obj for obj in objects if rs.ObjectLayer(obj) not in [layer]+layerChildren]
		rs.DeleteObjects([obj for obj in objects if rs.ObjectLayer(obj) in [layer]+layerChildren])
		if len(remaining) > 0: reblock(remaining, block)
		else: blocksToDelete.append(block)
	for block in blocksToDelete:
		rs.DeleteBlock(block.Name)
	rs.DeleteObjects([obj for obj in sc.doc.Objects if obj.Attributes.LayerIndex in allIndices])
	sc.doc.Layers.Delete(layerIndex, True)
	rs.EnableRedraw(True)
	print("Done." if not rs.IsLayer(layer) else "Could not delete layer.")

if __name__ == "__main__":
	selectedObjects = rs.SelectedObjects(True, True)
	layer = rs.GetLayer("Select layer to delete")
	if layer and rs.IsLayer(layer):
		print("Deleteing layer <{}>...".format(layer))
		deleteLayer(layer)
		rs.UnselectAllObjects()
		rs.SelectObjects(selectedObjects)
	else:
		print("Layer not selected.")

Thank you Measure!

This almost make the job.
Do you know if there is the possibility of moving all the object to the layer “Default” before deleting the layers?

ctrl a → _properties → set the layer ?

or do you mean all objects that define blocks ?

Indeed Tom, I meant all the objects that define blocks, that are in those layers that I would like to delete afterwards.

I use this merge layer script often… it asks which layer(s) you want get rid of, and which layer you wish all objects to be moved to. It works with objects also inside blocks too.

Not exactly what you’re looking for but perhaps its helpful…

xLayer_merge.py (5.1 KB)

For that I would use this merge-layer script (merge each unwanted layer into the Default layer).

# BJW - Merge one layer into another
import Rhino
import rhinoscriptsyntax as rs
import scriptcontext as sc

def MergeLayer(fromLayer, toLayer):
	fromIndex = sc.doc.Layers.FindByFullPath(fromLayer, True)
	toIndex = sc.doc.Layers.FindByFullPath(toLayer, True)
	count = 0
	for obj in sc.doc.Objects:
		if obj.Attributes.LayerIndex == fromIndex:
			obj.Attributes.LayerIndex = toIndex
			obj.CommitChanges()
			count += 1
	for block in sc.doc.InstanceDefinitions:
		for obj in block.GetObjects():
			if obj.Attributes.LayerIndex == fromIndex:
				obj.Attributes.LayerIndex = toIndex
				obj.CommitChanges()
				count += 1
	print("Moved {} objects to new layer.".format(count))
	if rs.CurrentLayer() == fromLayer:
		rs.CurrentLayer(toLayer)
	if rs.DeleteLayer(fromLayer):
		print("Deleted old layer.")
	else:
		print("Could not delete old layer.")

if __name__ == "__main__":
	fromLayer = rs.GetLayer("Select layer to merge")
	if fromLayer and rs.IsLayer(fromLayer):
		print("Merge layer <{}>...".format(fromLayer))
		toLayer = rs.GetLayer("Select layer to merge into")
		if toLayer and rs.IsLayer(toLayer):
			print("into layer <{}>... merging...".format(toLayer))
			MergeLayer(fromLayer, toLayer)
		else:
			print("Layer not selected.")
	else:
		print("Layer not selected.")

This works wonderfully!

Thanks

This works very well! The script provided by Benjamin is actually a bit faster since it allows you to select multiple layers and move them to the Default layer.

Thank you.

I was thinking about making a similar script but it sounds like benjamin’s knocks it out of the park.

AutoCAD has a similar feature (right click on the layer from within the layer properties dialog). “Merging” the layer retains nested objects inside blocks but just changes their layer. The “laydel” command (in AutoCAD) is playing with fire as it clears ever object on that layer (including nested objects). I use the merge a ton in AutoCAD.