I had to do some repetitive visualizations and was in need of those tools - maybe those are helpfull for someone.
Detail Layer Management Tools: OLOD, OLON & DLT
Managing per-detail layer visibility in Rhino layouts is tedious — especially when working with overlapping details or complex layer hierarchies. These three Python scripts streamline the most common tasks.
All scripts work with Rhino 8’s per-viewport layer visibility API and are designed for use within layout detail views.
OLOD — OneLayerOffInDetail
What it does: Pick objects inside an active detail → their layers are turned off only in that detail. Global layer visibility is not affected.
How to use:
-
Double-click into a detail on a layout page
-
Run
OLOD -
Select one or more objects (window/crossing selection works)
-
Press Enter — the layers of the selected objects are now hidden in this detail only
Why it’s useful: Native Rhino only offers OneLayerOff (global, single pick) or manually toggling the Detail On column in the Layers panel. OLOD combines object-based picking with per-detail scope, making it much faster to configure detail views — especially with deeply nested layer structures.
# -*- coding: utf-8 -*-
# OneLayerOffInDetail (OLOD) v1
# Pick objects in active layout detail -> turn off their layers in this detail only.
# Global layer visibility is NOT affected.
# Requires: active page view with a double-clicked (active) detail.
import Rhino
import scriptcontext as sc
def main():
# --- Validate: must be in a page view ---
view = sc.doc.Views.ActiveView
if view is None or not isinstance(view, Rhino.Display.RhinoPageView):
print("OLOD: Not in a layout/page view. Switch to a layout first.")
return
page_view = view
vp_id = page_view.ActiveViewportID
# --- Validate: must be inside a detail (not on the page itself) ---
details = page_view.GetDetailViews()
active_detail = None
for d in details:
if d.Viewport.Id == vp_id:
active_detail = d
break
if active_detail is None:
print("OLOD: No active detail. Double-click into a detail first.")
return
detail_name = active_detail.Name if active_detail.Name else "(unnamed)"
print("OLOD: Active detail = '{}'".format(detail_name))
# --- Pick objects ---
go = Rhino.Input.Custom.GetObject()
go.SetCommandPrompt("Select objects to turn off their layers in this detail")
go.GeometryFilter = Rhino.DocObjects.ObjectType.AnyObject
go.SubObjectSelect = False
go.GroupSelect = True
go.GetMultiple(1, 0)
if go.CommandResult() != Rhino.Commands.Result.Success:
print("OLOD: Cancelled.")
return
# --- Collect unique layer indices ---
layer_indices = set()
for i in range(go.ObjectCount):
rh_obj = go.Object(i).Object()
if rh_obj is not None:
layer_indices.add(rh_obj.Attributes.LayerIndex)
if not layer_indices:
print("OLOD: No valid objects selected.")
return
# --- Turn off layers in this detail ---
count = 0
skipped = []
for idx in layer_indices:
# Re-acquire layer (may have been modified in previous iteration)
layer = sc.doc.Layers[idx]
if layer is None:
continue
full_path = layer.FullPath
# Cannot turn off the current layer
if idx == sc.doc.Layers.CurrentLayerIndex:
skipped.append(full_path + " [current layer]")
continue
# Per-viewport visibility only works on globally visible layers
if not layer.IsVisible:
skipped.append(full_path + " [globally off]")
continue
# Already off in this detail?
if not layer.PerViewportIsVisible(vp_id):
skipped.append(full_path + " [already off in detail]")
continue
# Turn off in this detail only
layer.SetPerViewportVisible(vp_id, False)
layer.SetPerViewportPersistentVisibility(vp_id, False)
count += 1
print(" OFF: {}".format(full_path))
# --- Report ---
if skipped:
print(" ---")
for s in skipped:
print(" SKIP: {}".format(s))
print("--- OLOD: {} layer(s) turned off in detail '{}' ---".format(count, detail_name))
sc.doc.Views.Redraw()
if __name__ == "__main__":
main()
OLON — OneLayerOnInDetail (Ghost Overlay)
What it does: The reverse of OLOD — but since hidden objects can’t be picked, OLON uses a “ghost mode”: it temporarily shows all hidden layers as selectable geometry while dimming the currently visible layers as locked context. Pick what you want back, and only those layers are restored.
How to use:
-
Double-click into a detail that has layers turned off
-
Run
OLON -
The view switches to ghost mode:
-
Normal color = previously hidden layers (now selectable)
-
Dimmed/gray = currently visible layers (locked, non-selectable)
-
-
Pick objects from the hidden layers you want to restore
-
Press Enter — original state is restored, then only the picked layers are turned back on
Display Mode requirement: For the ghost effect to work visually, enable “Apply these settings to layers” in your Display Mode under Objects → Locked Objects. Set a lock color (e.g. light gray) so locked layers are clearly distinguishable.
Technical notes:
-
Parent layers of hidden sublayers are excluded from locking (Rhino’s lock inheritance would otherwise block sublayer selection)
-
A custom geometry filter ensures only objects on hidden layers can be picked
-
The entire ghost mode is wrapped in
try/finally— the original state is always restored, even on cancel or error
# -*- coding: utf-8 -*-
# OneLayerOnInDetail (OLON) v3 — Ghost Overlay
# Shows layers hidden in the active detail as selectable geometry,
# dims currently visible layers as locked context (ghost).
# Pick objects from hidden layers to turn them back on.
#
# v3: Ancestor layers of hidden sublayers are excluded from locking
# (Rhino lock inheritance would block sublayer selection).
# Custom geometry filter restricts picks to hidden layers only.
import System
import Rhino
import scriptcontext as sc
def main():
# --- Validate: page view + active detail ---
view = sc.doc.Views.ActiveView
if view is None or not isinstance(view, Rhino.Display.RhinoPageView):
print("OLON: Not in a layout/page view.")
return
page_view = view
vp_id = page_view.ActiveViewportID
details = page_view.GetDetailViews()
active_detail = None
for d in details:
if d.Viewport.Id == vp_id:
active_detail = d
break
if active_detail is None:
print("OLON: No active detail. Double-click into a detail first.")
return
detail_name = active_detail.Name if active_detail.Name else "(unnamed)"
# --- Classify layers: hidden vs visible in this detail ---
hidden_indices = set()
visible_indices = []
for i in range(sc.doc.Layers.Count):
layer = sc.doc.Layers[i]
if layer is None or layer.IsDeleted:
continue
if not layer.IsVisible:
continue
if layer.PerViewportIsVisible(vp_id):
visible_indices.append(i)
else:
hidden_indices.add(i)
if not hidden_indices:
print("OLON: No layers are hidden in this detail. Nothing to restore.")
return
print("OLON: Detail '{}' — {} hidden layer(s):".format(detail_name, len(hidden_indices)))
for i in hidden_indices:
print(" [hidden] {}".format(sc.doc.Layers[i].FullPath))
# --- Find ancestor layers of hidden sublayers ---
# These must NOT be locked, or their hidden children become non-selectable
ancestor_indices = set()
for i in hidden_indices:
layer = sc.doc.Layers[i]
parent_id = layer.ParentLayerId
while parent_id != System.Guid.Empty:
parent_layer = sc.doc.Layers.FindId(parent_id)
if parent_layer is not None and not parent_layer.IsDeleted:
ancestor_indices.add(parent_layer.Index)
parent_id = parent_layer.ParentLayerId
else:
break
if ancestor_indices:
print(" ({} ancestor layer(s) excluded from locking)".format(len(ancestor_indices)))
# --- Snapshot original lock states ---
original_locks = {}
for i in visible_indices:
original_locks[i] = sc.doc.Layers[i].IsLocked
picked_layer_indices = set()
try:
# === ENTER GHOST MODE ===
# 1) Lock visible layers EXCEPT ancestors of hidden sublayers
for i in visible_indices:
if i in ancestor_indices:
continue # Skip — locking would block hidden children
layer = sc.doc.Layers[i]
if not layer.IsLocked:
layer.IsLocked = True
sc.doc.Layers.Modify(layer, i, False)
# 2) Show all hidden layers in this detail
for i in hidden_indices:
layer = sc.doc.Layers[i]
layer.SetPerViewportVisible(vp_id, True)
layer.SetPerViewportPersistentVisibility(vp_id, True)
sc.doc.Layers.Modify(layer, i, False)
sc.doc.Views.Redraw()
print("")
print("=== GHOST MODE ACTIVE ===")
print(" Normal color = hidden layers (selectable)")
print(" Dimmed/gray = visible layers (locked context)")
print(" Pick objects to turn ON their layers. Enter=confirm, Esc=cancel")
print("")
# --- Pick objects with custom filter ---
# Only objects on hidden layers are selectable
def hidden_layer_filter(rh_obj, geo, ci):
return rh_obj.Attributes.LayerIndex in hidden_indices
go = Rhino.Input.Custom.GetObject()
go.SetCommandPrompt("Select objects to turn ON their layers in this detail")
go.GeometryFilter = Rhino.DocObjects.ObjectType.AnyObject
go.SubObjectSelect = False
go.GroupSelect = True
go.SetCustomGeometryFilter(hidden_layer_filter)
go.GetMultiple(1, 0)
if go.CommandResult() == Rhino.Commands.Result.Success:
for i in range(go.ObjectCount):
rh_obj = go.Object(i).Object()
if rh_obj is not None:
picked_layer_indices.add(rh_obj.Attributes.LayerIndex)
finally:
# === RESTORE ORIGINAL STATE (always runs) ===
# Restore lock states
for i, was_locked in original_locks.items():
layer = sc.doc.Layers[i]
if layer is not None:
layer.IsLocked = was_locked
sc.doc.Layers.Modify(layer, i, False)
# Re-hide all temporarily shown layers
for i in hidden_indices:
layer = sc.doc.Layers[i]
if layer is not None:
layer.SetPerViewportVisible(vp_id, False)
layer.SetPerViewportPersistentVisibility(vp_id, False)
sc.doc.Layers.Modify(layer, i, False)
# --- Apply: turn ON only the picked layers ---
if picked_layer_indices:
count = 0
for idx in picked_layer_indices:
layer = sc.doc.Layers[idx]
if layer is not None:
layer.SetPerViewportVisible(vp_id, True)
layer.SetPerViewportPersistentVisibility(vp_id, True)
sc.doc.Layers.Modify(layer, idx, False)
count += 1
print(" ON: {}".format(layer.FullPath))
print("--- OLON: {} layer(s) turned on in detail '{}' ---".format(count, detail_name))
else:
print("OLON: Cancelled or no hidden-layer objects picked. No changes.")
sc.doc.Views.Redraw()
if __name__ == "__main__":
main()
DLT — DetailLayerTransfer
What it does: Copies the complete per-detail layer visibility state from one detail to one or more target details. Push-based: pick source, then pick targets.
How to use:
-
Be on a layout page (not inside a detail — click the layout background to exit)
-
Run
DLT -
Pick the source detail frame
-
Pick one or more target detail frames → Enter
-
All per-detail layer ON/OFF settings from the source are applied to the targets (overwrite)
Why it’s useful: When working with overlapping details on the same layout (e.g. separate details for structure, MEP, furniture on the same plan), each detail needs identical or near-identical layer configurations. Setting this up manually means double or triple the work. DLT copies the entire visibility state in one step.
# -*- coding: utf-8 -*-
# DetailLayerTransfer (DLT) v1
# Push: pick source detail -> pick target detail(s) -> transfer per-viewport layer visibility.
# Must be on layout page level (not inside a detail).
# Overwrites target detail visibility with source state.
import Rhino
import scriptcontext as sc
def detail_filter(rh_obj, geo, ci):
"""Custom geometry filter: only allow picking DetailViewObjects."""
return isinstance(rh_obj, Rhino.DocObjects.DetailViewObject)
def main():
# --- Validate: must be on a page view ---
view = sc.doc.Views.ActiveView
if view is None or not isinstance(view, Rhino.Display.RhinoPageView):
print("DLT: Not in a layout/page view.")
return
page_view = view
vp_id = page_view.ActiveViewportID
# --- Check we're on the page level, not inside a detail ---
details = page_view.GetDetailViews()
inside_detail = False
for d in details:
if d.Viewport.Id == vp_id:
inside_detail = True
break
if inside_detail:
print("DLT: You are inside a detail. Click the layout background to exit the detail first.")
return
if len(details) < 2:
print("DLT: Need at least 2 details on this layout (found {}).".format(len(details)))
return
# --- Pick SOURCE detail ---
go_src = Rhino.Input.Custom.GetObject()
go_src.SetCommandPrompt("Pick SOURCE detail frame")
go_src.SubObjectSelect = False
go_src.SetCustomGeometryFilter(detail_filter)
go_src.GetMultiple(1, 1)
if go_src.CommandResult() != Rhino.Commands.Result.Success:
print("DLT: Cancelled.")
return
src_obj = go_src.Object(0).Object()
if not isinstance(src_obj, Rhino.DocObjects.DetailViewObject):
print("DLT: Selected object is not a detail view.")
return
src_detail = src_obj
src_vp_id = src_detail.Viewport.Id
src_name = src_detail.Name if src_detail.Name else "(unnamed)"
print("DLT: Source = '{}'".format(src_name))
# --- Pick TARGET detail(s) ---
go_tgt = Rhino.Input.Custom.GetObject()
go_tgt.SetCommandPrompt("Pick TARGET detail frame(s) — Enter when done")
go_tgt.SubObjectSelect = False
go_tgt.SetCustomGeometryFilter(detail_filter)
go_tgt.EnablePreSelect(False, True)
go_tgt.DeselectAllBeforePostSelect = True
go_tgt.GetMultiple(1, 0)
if go_tgt.CommandResult() != Rhino.Commands.Result.Success:
print("DLT: Cancelled.")
return
target_details = []
for i in range(go_tgt.ObjectCount):
obj = go_tgt.Object(i).Object()
if not isinstance(obj, Rhino.DocObjects.DetailViewObject):
continue
if obj.Viewport.Id == src_vp_id:
print(" SKIP: Same as source detail")
continue
target_details.append(obj)
if not target_details:
print("DLT: No valid target details selected.")
return
tgt_names = [t.Name if t.Name else "(unnamed)" for t in target_details]
print("DLT: {} target(s): {}".format(len(target_details), ", ".join(tgt_names)))
# --- Read source state and transfer ---
changes = 0
layers_changed = 0
for layer_idx in range(sc.doc.Layers.Count):
layer = sc.doc.Layers[layer_idx]
if layer is None or layer.IsDeleted:
continue
if not layer.IsVisible:
continue # Globally off — no per-detail control
src_visible = layer.PerViewportIsVisible(src_vp_id)
layer_touched = False
for tgt in target_details:
tgt_vp_id = tgt.Viewport.Id
# Re-acquire layer (may have been modified by previous target)
layer = sc.doc.Layers[layer_idx]
tgt_visible = layer.PerViewportIsVisible(tgt_vp_id)
if tgt_visible != src_visible:
layer.SetPerViewportVisible(tgt_vp_id, src_visible)
layer.SetPerViewportPersistentVisibility(tgt_vp_id, src_visible)
sc.doc.Layers.Modify(layer, layer_idx, False)
changes += 1
layer_touched = True
if layer_touched:
layers_changed += 1
# --- Report ---
print("--- DLT: {} layer(s), {} change(s) applied ---".format(layers_changed, changes))
print(" Source: '{}' -> Target(s): {}".format(src_name, ", ".join(tgt_names)))
sc.doc.Views.Redraw()
if __name__ == "__main__":
main()
General Notes
-
All three scripts require globally visible layers to function — Rhino’s per-viewport visibility only works on layers that are globally ON. For layout workflows, it’s recommended to keep layers globally visible and use the Model On column to hide them in model space.
-
Alias suggestions:
OLOD,OLON,DLT -
Tested with Rhino 8 / IronPython 2.7
PS: the text was written with AI - because I am still lazy