Share your favourite scripts, macros and plug-ins

LiveSun_Rhino8_Project_v0.3 (2).zip (109.0 KB)

Hi all! I made this little plugin called LiveSun! I wanted to create a sun setup that changes with the actual sun throughout the day as I’m working on my architecture models. It updates the sun location every ten minutes.

It comes with three commands.

LiveSunSetup - lets you set you location and timezone and stuff as you typically would

LiveSunOn / LiveSunOff - Turns on the LiveSun

Cloudy - Lets you set the “cloudiness” of the model from a scale of 0.0 to 1.0 - (this looks just ok, and I’d like to work on it a bit more.)

You can also have _LiveSunOn run at startup if you want it to automatically start every time you open Rhino.

Here’s my model in the evening. It’s kind of nice to be surprised by the different ways the “sun” hits it whenever you open it up.

Just unzip the folder in your scripts folder, find the .rhp file in there and drop it into Rhino to set it up :slight_smile: enjoy!

-Paul

dear all

Script for flow along Mesh with LiveSync

https://discourse.mcneel.com/t/script-for-flow-along-mesh-with-livesync/221213

SquishBack 3d LiveSync.py (46.0 KB)

Hi all,

Draw order comes up here regularly, and the thing that always got me is that Rhino gives you four commands to change it and no way to see what any of them actually did.

This started as a personal fix — hatches kept swallowing my massing model — and ended up being something my office now uses, so posting in case it’s useful to anyone else.

Credit where it’s due: the mechanics came almost entirely from @Helvetosaur’s explanations in the Draw Order Overview thread. That display order is a plain integer rather than a three-state thing, that explicitly assigned values override layer order, and that unsupported object types sit permanently at the 0 baseline. His SetObjDrawOrder.py and SetObjDisplayOrderOSD.py already cover much of this ground — this is a different interface on the same idea, not a replacement. The OSD script draws order numbers onto the objects themselves, which mine doesn’t do.

What it does:

— Modeless Eto window, stays open while you work
— Live readout: select anything and it shows the current draw order, or the range and band if the selection is mixed
— Five named tiers (Front / Ahead / Middle / Behind / Back) at +20 / +10 / 0 / −10 / −20, click to assign to the selection
— +1 / −1 nudge buttons for resolving two objects sitting on the same level
— Click a tier with nothing selected and it selects everything in that band instead. Nearest-tier matching, so objects nudged off the exact value are still found

The tier scheme is deliberately built around 0 as the baseline where surfaces, meshes and blocks are stuck. Negative draws behind the model, positive in front. Framing it that way is what finally made the whole thing click for me, and for the colleagues I handed it to.

Tiers, values, colours and tooltips are a single editable list at the top of the file. Band boundaries derive from it automatically, so adding a sixth tier or changing the spacing needs no other edits.

Written and tested on Rhino 7, Windows, IronPython 2.7. There are no Python 2-only idioms left in it, so it should run under Rhino 8/9 CPython too — but I haven’t tested that and would appreciate a report if anyone tries.

Feedback on the Eto side is welcome, it’s my first modeless form. I hope this one helps out a few!

Full code
# DrawOrderPalette.py
# Tier presets, +/-1 nudge, live selection readout, select-by-tier.
#
# Tier buttons do one of two things depending on context:
#   something selected -> assign that tier to the selection
#   nothing selected   -> select everything whose NEAREST tier is that one,
#                         so objects you have nudged off the exact value
#                         still belong to their tier and stay findable

import Rhino
import Rhino.UI
import Eto.Forms as forms
import Eto.Drawing as drawing
import scriptcontext as sc


# ---------------------------------------------------------------------------
# EDIT THIS BLOCK ONLY.
# (name, draw order value, swatch RGB, tooltip)
# Listed front-to-back, so the top row draws on top. 3D volumes and blocks
# are permanently pinned at 0 and cannot be moved - that is the Middle plane.
# Band boundaries are derived automatically, so changing spacing or adding a
# sixth tier needs no other edits.
# ---------------------------------------------------------------------------
PRESETS = [
    ("Front",   20, (192,  57,  43), "Text and dimensions - always on top"),
    ("Ahead",   10, (230, 126,  34), "Linework that must sit over 3D volumes"),
    ("Middle",   0, ( 39, 135,  75), "Default plane - sits with volumes and blocks"),
    ("Behind", -10, ( 46, 134, 171), "Pattern hatches - behind volumes"),
    ("Back",   -20, ( 31,  58, 147), "Solid fills and site base - furthest back"),
]

POLL_SECONDS = 0.3    # raise to 0.5-1.0 if it feels sluggish on a big file
BIG_SELECTION = 500   # ask before selecting more than this many objects
STICKY_KEY = "DrawOrderPalette"

# ascending list of tier values, derived from PRESETS
TIER_VALUES = sorted(set(p[1] for p in PRESETS))
TIER_NAMES = dict((p[1], p[0]) for p in PRESETS)


def forget_instance():
    """Drop the stored window reference.
    Written to work under both IronPython 2.7 (Rhino 6/7) and CPython 3
    (Rhino 8/9) - has_key() and .Remove() only exist on the former."""
    try:
        if STICKY_KEY in sc.sticky:
            del sc.sticky[STICKY_KEY]
    except:
        pass


def nearest_tier(order):
    """Which tier value is this draw order closest to?
    Exact midpoints round forward (towards the front)."""
    best = TIER_VALUES[0]
    best_d = abs(order - best)
    for v in TIER_VALUES[1:]:
        d = abs(order - v)
        if d < best_d or (d == best_d and v > best):
            best, best_d = v, d
    return best


# ---------------------------------------------------------------------------
# Object types that actually support draw order.
# Surfaces, meshes, blocks and text dots are excluded - they silently no-op.
# ---------------------------------------------------------------------------
OT = Rhino.DocObjects.ObjectType
SUPPORTED = (OT.Curve, OT.Point, OT.Hatch, OT.Annotation, OT.Detail)


def active_doc():
    # Modeless forms outlive the script, so scriptcontext.doc can go stale.
    return Rhino.RhinoDoc.ActiveDoc


def supported_selection():
    doc = active_doc()
    if doc is None:
        return []
    objs = doc.Objects.GetSelectedObjects(False, False)
    return [o for o in objs if o.ObjectType in SUPPORTED]


def all_supported_objects():
    """Every selectable draw-order-capable object in the document.
    Hidden and locked objects are skipped - they cannot be selected anyway."""
    doc = active_doc()
    if doc is None:
        return []
    s = Rhino.DocObjects.ObjectEnumeratorSettings()
    s.NormalObjects = True
    s.LockedObjects = False
    s.HiddenObjects = False
    s.IncludeLights = False
    s.IncludeGrips = False
    return [o for o in doc.Objects.GetObjectList(s) if o.ObjectType in SUPPORTED]


def write_order(objs, func, undo_label):
    """func(current_order) -> new_order. Applies to every object passed in."""
    doc = active_doc()
    if doc is None or not objs:
        return
    undo = doc.BeginUndoRecord(undo_label)
    try:
        for o in objs:
            attr = o.Attributes.Duplicate()
            attr.DisplayOrder = func(o.Attributes.DisplayOrder)
            doc.Objects.ModifyAttributes(o, attr, True)
    finally:
        doc.EndUndoRecord(undo)
    doc.Views.Redraw()


# ---------------------------------------------------------------------------
class DrawOrderPalette(forms.Form):

    def __init__(self):
        self.Title = "Draw Order"
        self.Padding = drawing.Padding(8)
        self.Resizable = False
        self.Maximizable = False
        self.Minimizable = False
        self.Topmost = True

        self.m_readout = forms.Label(Text="Nothing selected")
        self.m_readout.Font = drawing.Font(drawing.SystemFont.Bold, 9)

        self.m_hint = forms.Label(
            Text="Nothing selected - a tier click selects that tier")
        self.m_hint.TextColor = drawing.Color.FromArgb(120, 120, 120)

        tiers = forms.DynamicLayout()
        tiers.Spacing = drawing.Size(6, 3)
        for name, value, rgb, tip in PRESETS:
            tiers.AddRow(self._swatch(rgb),
                         self._tier_button(name, value, tip),
                         forms.Label(Text=self._fmt(value)))

        minus = forms.Button(Text="-1")
        minus.ToolTip = "Nudge selection one step back"
        minus.Click += self.on_minus
        plus = forms.Button(Text="+1")
        plus.ToolTip = "Nudge selection one step forward"
        plus.Click += self.on_plus

        nudge = forms.DynamicLayout()
        nudge.Spacing = drawing.Size(6, 0)
        nudge.AddRow(minus, plus)

        reset = forms.Button(Text="Reset to 0")
        reset.ToolTip = "Clear the override - back to the Middle plane"
        reset.Click += self.on_reset
        close = forms.Button(Text="Close")
        close.Click += self.on_close_click

        footer = forms.DynamicLayout()
        footer.Spacing = drawing.Size(6, 0)
        footer.AddRow(reset, close)

        main = forms.DynamicLayout()
        main.Spacing = drawing.Size(0, 8)
        main.AddRow(self.m_readout)
        main.AddRow(tiers)
        main.AddRow(nudge)
        main.AddRow(footer)
        main.AddRow(self.m_hint)
        self.Content = main

        self.m_timer = forms.UITimer()
        self.m_timer.Interval = POLL_SECONDS
        self.m_timer.Elapsed += self.on_tick
        self.m_timer.Start()

        self.Closed += self.on_closed

    # -- widgets ------------------------------------------------------------
    def _fmt(self, value):
        # show the sign so the ordering reads at a glance
        return "%+d" % value if value != 0 else "0"

    def _swatch(self, rgb):
        d = forms.Drawable()
        d.Size = drawing.Size(14, 14)
        d.BackgroundColor = drawing.Color.FromArgb(rgb[0], rgb[1], rgb[2])
        return d

    def _tier_button(self, name, value, tip):
        b = forms.Button(Text=name)
        b.Width = 115
        b.ToolTip = tip + "   |   click with nothing selected to select this band"
        # default-arg trick pins the value inside the closure
        b.Click += lambda s, e, v=value: self.on_preset(v)
        return b

    # -- selection ----------------------------------------------------------
    def select_tier(self, value):
        """Selects everything whose nearest tier is this one, not just
        objects sitting on the exact value."""
        doc = active_doc()
        if doc is None:
            return
        matches = [o for o in all_supported_objects()
                   if nearest_tier(o.Attributes.DisplayOrder) == value]

        if not matches:
            self.m_hint.Text = "Nothing in the %s band" % TIER_NAMES[value]
            return

        if len(matches) > BIG_SELECTION:
            answer = forms.MessageBox.Show(
                self,
                "Select %d objects?\n\nLarge selections slow the live readout."
                % len(matches),
                "Draw Order",
                forms.MessageBoxButtons.YesNo,
                forms.MessageBoxType.Question)
            if answer != forms.DialogResult.Yes:
                return

        doc.Objects.UnselectAll()
        for o in matches:
            o.Select(True)
        doc.Views.Redraw()

    # -- handlers -----------------------------------------------------------
    def on_tick(self, sender, e):
        objs = supported_selection()
        if not objs:
            self.m_readout.Text = "Nothing selected"
            self.m_hint.Text = "Nothing selected - a tier click selects that band"
            return
        self.m_hint.Text = "A tier click assigns to the selection"

        orders = [o.Attributes.DisplayOrder for o in objs]
        lo, hi = min(orders), max(orders)

        if lo == hi:
            self.m_readout.Text = "%d object(s)  -  %s  (%s)" % (
                len(objs), self._fmt(lo), TIER_NAMES.get(lo, "off-tier"))
        else:
            bands = set(nearest_tier(v) for v in orders)
            if len(bands) == 1:
                band = list(bands)[0]
                self.m_readout.Text = "%d object(s)  -  %s to %s  (%s band)" % (
                    len(objs), self._fmt(lo), self._fmt(hi), TIER_NAMES[band])
            else:
                self.m_readout.Text = "%d object(s)  -  mixed %s to %s" % (
                    len(objs), self._fmt(lo), self._fmt(hi))

    def on_preset(self, value):
        if not supported_selection():
            self.select_tier(value)          # nothing selected -> select band
        else:
            write_order(supported_selection(),
                        lambda cur: value, "Set draw order")
            self.on_tick(None, None)

    def on_plus(self, sender, e):
        write_order(supported_selection(),
                    lambda cur: cur + 1, "Nudge draw order")
        self.on_tick(None, None)

    def on_minus(self, sender, e):
        write_order(supported_selection(),
                    lambda cur: cur - 1, "Nudge draw order")
        self.on_tick(None, None)

    def on_reset(self, sender, e):
        write_order(supported_selection(),
                    lambda cur: 0, "Reset draw order")
        self.on_tick(None, None)

    def on_close_click(self, sender, e):
        self.Close()

    def on_closed(self, sender, e):
        try:
            self.m_timer.Stop()
        except:
            pass
        forget_instance()


# ---------------------------------------------------------------------------
def main():
    # close an existing instance so you never end up with two
    if STICKY_KEY in sc.sticky:
        try:
            sc.sticky[STICKY_KEY].Close()
        except:
            pass
        forget_instance()

    form = DrawOrderPalette()
    form.Owner = Rhino.UI.RhinoEtoApp.MainWindow
    form.Show()
    # keeping a reference stops the garbage collector eating the window
    sc.sticky[STICKY_KEY] = form


main()

The file is attached
Anton

DrawOrderPalette.py (10.6 KB)

Hi,

Just saw that you mentioned the need of a refined version for the draw order scripts.

Just made a post about this if you’re still looking for something like that :slight_smile:

Best
Anton

Thank you for the mention ! Yes, it will surely be useful :slight_smile: Just tried on R8 and it work flawlessly !

I’ve fixed a number of bugs in the projectobjects script (it would not remeber projection direction correctly, did not recognize subd targets, wouldn’t project planar objects properly in any version of Rhino). But in this version the default is 20x20, so if you like it higher you’ll have to change that. Tested in Rhino 7 and Rhino 9. A limitation in Rhino 7: it does not work well with planar source objects because of the way that Rhino made cages in Rhino 7 (works fine in Rhino 9.)

(edit: updated to support meshes)

ProjectObjects.py (25.5 KB)