<Code Lib>
👍 1 👁 22

Super Pin

by @bimgineer · Jul 9, 2026

Description

Pins elements in Revit, records their position, and warns you if they are ever moved.



## Setup

Place the four files into your pyRevit extension using the folder structure below. Replace `<Your Extension>` and `<Your Panel>` with whatever names your extension and panel use.

<Your Extension>.extension/
|
|-- lib/
|   |-- super_pin_revert.py
|
|-- hooks/
|   |-- doc-changed.py
|
|-- <Any>.tab/
    |-- <Your Panel>.panel/
        |-- Super Pin.pushbutton/
            |-- script.py
            |-- bundle.yaml

After placing all files, reload pyRevit (or restart Revit) to register the hook and button.

> **If your extension already has a `doc-changed.py` hook**, you will need to merge the contents of this one into your existing hook rather than replacing it.

## What Each File Does

| `script.py` | `Super Pin.pushbutton/` | The button. Click to pin + track elements. Shift+Click to remove elements from tracking. Also creates the ExternalEvent that enables automatic move-back. 
| `bundle.yaml` | `Super Pin.pushbutton/` | Optional button metadata (tooltip, title). 
| `doc-changed.py` | `hooks/` | pyRevit hook that fires on every document change. Detects when a tracked element moves and prompts the user to undo, allow, or stop tracking. 
| `super_pin_revert.py` | `lib/` | ExternalEvent handler that moves elements back to their recorded position. Used by both the button and the hook. 

## Sharing Tracking Data Across a Team

By default, each user's tracking data is stored locally at:

%APPDATA%/<FOLDER_NAME>/<USERNAME>/super_pin_locks.json

This means only the user who pinned an element will be warned if it moves. To share tracking data so the whole team is protected, change the `_STORE_DIR` and `_STORE_FILE` variables to a shared network path that every team member can read and write.

> **Important:** You must update these variables in **all three `.py` files**.

### Option A: A shared folder on your server
One shared folder, one JSON file, all documents keyed inside it.

```python
_STORE_DIR = r'\\SERVER\BIM\Super Pin'
_STORE_FILE = os.path.join(_STORE_DIR, 'super_pin_locks.json')
```


### Option B: Next to the Revit model

Puts the lock file in the same folder as the model. Keeps tracking data next to the project it belongs to, but requires the model to be saved to a path everyone can access.

```python
_STORE_DIR = os.path.dirname(doc.PathName) if doc.PathName else <fallback>
_STORE_FILE = os.path.join(_STORE_DIR, 'super_pin_locks.json')
```


### Requirements for shared storage
- Every team member has read/write access to the folder.
- The path is the same for all users (use a UNC path like `\\SERVER\...`, not a mapped drive letter that may differ between machines).
- The `_STORE_DIR` / `_STORE_FILE` values match in all three `.py` files.

## Notes

- Each Revit document gets its own key inside the JSON file (the saved file path, or document title if unsaved).
- The "Undo the move" option requires the ExternalEvent to be created first. This happens automatically the first time you click Super Pin in a session. If you move a tracked element before ever clicking the button, the revert will fall back to asking you to Ctrl+Z.
- This tool is self-contained. It has no dependencies beyond pyRevit and the Revit API.

Code

pyRevit
# -*- coding: utf-8 -*-
"""
 Click to pin + track elements.
 Shift+Click to remove elements from tracking.
 Also creates the ExternalEvent that enables automatic
 move-back, so click it once per Revit session before
 relying on revert.
"""
__title__ = "Super\nPin"
__author__ = "bimgineer/SWE-eml"
min_revit_version: 2025
# TODO: test other Revit versions
__doc__   = ("Pin element(s) and track them. Pre-select or pick elements — "
             "Super Pin pins them in Revit and records their position; "
             "you are warned if one is ever moved.")

import os
import io
import json
import copy

from pyrevit import revit, forms
from Autodesk.Revit.UI.Selection import ObjectType
from Autodesk.Revit.Exceptions import OperationCanceledException
from Autodesk.Revit.DB import ElementId

from System.Collections.Generic import List


uidoc = revit.uidoc
doc   = revit.doc

# ── Storage ──────────────────────────────────────────────────────────────────

_STORE_DIR = os.path.join(
    os.environ.get('APPDATA', os.path.expanduser('~')),
    'FOLDER_NAME',
    os.environ.get('USERNAME', 'user'),
)
_STORE_FILE = os.path.join(_STORE_DIR, 'super_pin_locks.json')


def _doc_key():
    try:
        path = doc.PathName
    except Exception:
        path = None
    return path or doc.Title


def _load():
    """Return {uid: record} for this document."""
    try:
        with io.open(_STORE_FILE, 'r', encoding='utf-8') as fh:
            data = json.load(fh)
    except Exception:
        return {}
    return data.get(_doc_key(), {})


def _load_all():
    try:
        with io.open(_STORE_FILE, 'r', encoding='utf-8') as fh:
            data = json.load(fh)
        if isinstance(data, dict):
            return data
    except Exception:
        pass
    return {}


def _save_all(data):
    if not os.path.isdir(_STORE_DIR):
        os.makedirs(_STORE_DIR)
    tmp = _STORE_FILE + '.tmp'
    with io.open(tmp, 'w', encoding='utf-8') as fh:
        fh.write(json.dumps(data, indent=2, ensure_ascii=False))
    if os.path.isfile(_STORE_FILE):
        os.remove(_STORE_FILE)
    os.rename(tmp, _STORE_FILE)


def _add_lock(element, user):
    """Register element as tracked. Returns True if newly added."""
    data = copy.deepcopy(_load_all())
    locks = data.setdefault(_doc_key(), {})
    uid = element.UniqueId
    already = uid in locks
    locks[uid] = {
        'name': _display_name(element),
        'sig': _location_sig(element),
        'user': user,
    }
    _save_all(data)
    return not already


def _remove_lock(uid):
    """Remove a tracked element. Returns True if something was removed."""
    data = copy.deepcopy(_load_all())
    key = _doc_key()
    locks = data.get(key)
    if not locks or uid not in locks:
        return False
    del locks[uid]
    if not locks:
        del data[key]
    _save_all(data)
    return True


# ── Helpers ──────────────────────────────────────────────────────────────────

def _display_name(element):
    try:
        if element.Category:
            return element.Category.Name + ' : ' + element.Name
        return element.Name
    except Exception:
        return 'Element {0}'.format(element.Id)


def _location_sig(element):
    """Compact location signature: [x,y,z] for points, [x1..z2] for curves."""
    try:
        loc = element.Location
    except Exception:
        loc = None
    if loc is not None:
        pt = getattr(loc, 'Point', None)
        if pt is not None:
            return [pt.X, pt.Y, pt.Z]
        curve = getattr(loc, 'Curve', None)
        if curve is not None:
            try:
                p0 = curve.GetEndPoint(0)
                p1 = curve.GetEndPoint(1)
                return [p0.X, p0.Y, p0.Z, p1.X, p1.Y, p1.Z]
            except Exception:
                pass
    for view_arg in (None, doc.ActiveView):
        try:
            bb = element.get_BoundingBox(view_arg)
            if bb is not None:
                mn, mx = bb.Min, bb.Max
                return [(mn.X + mx.X) / 2.0,
                        (mn.Y + mx.Y) / 2.0,
                        (mn.Z + mx.Z) / 2.0]
        except Exception:
            pass
    return None


def _current_selection():
    try:
        return list(uidoc.Selection.GetElementIds())
    except Exception:
        return []


def _restore_selection(ids):
    if ids:
        try:
            uidoc.Selection.SetElementIds(List[ElementId](list(ids)))
        except Exception:
            pass


def _pick_targets(preselected):
    if preselected:
        return [e for e in (doc.GetElement(i) for i in preselected) if e]
    try:
        ref = uidoc.Selection.PickObject(
            ObjectType.Element,
            'Select an element to pin and track.'
        )
    except OperationCanceledException:
        return []
    el = doc.GetElement(ref.ElementId)
    return [el] if el else []


def _username():
    try:
        name = doc.Application.Username
        if name:
            return name
    except Exception:
        pass
    return os.environ.get('USERNAME', 'unknown')


# ── Pin + track ──────────────────────────────────────────────────────────────

def pin_and_track(targets, user):
    pinned = 0
    already = 0
    errors = []
    for el in targets:
        try:
            if not el.Pinned:
                with revit.Transaction('Super Pin'):
                    el.Pinned = True
        except Exception as ex:
            errors.append(_display_name(el))
            continue

        if _add_lock(el, user):
            pinned += 1
        else:
            already += 1

    parts = []
    if pinned:
        parts.append('{0} element(s) pinned and tracked.'.format(pinned))
    if already:
        parts.append('{0} already tracked.'.format(already))
    if errors:
        parts.append('Could not pin: ' + ', '.join(errors))

    forms.alert('\n'.join(parts), title='Super Pin')


# ── Manage (Shift+Click) ────────────────────────────────────────────────────

def manage(original_selection):
    locks = _load()
    if not locks:
        forms.alert('No elements are currently tracked.', title='Super Pin')
        return

    try:
        if original_selection:
            picked = [e for e in (doc.GetElement(i) for i in original_selection)
                      if e]
        else:
            try:
                refs = uidoc.Selection.PickObjects(
                    ObjectType.Element,
                    'Select tracked element(s) to stop protecting.'
                )
            except OperationCanceledException:
                return
            picked = [e for e in (doc.GetElement(r.ElementId) for r in refs)
                      if e]

        tracked = [e for e in picked if e.UniqueId in locks]
        if not tracked:
            forms.alert('None of the selected elements are tracked.',
                        title='Super Pin')
            return

        names = '\n'.join(
            u'  •  {0}'.format(_display_name(e)) for e in tracked)
        choice = forms.alert(
            u'Stop tracking?\n\n{0}'.format(names),
            title='Super Pin',
            options=['Remove from tracking (keep pinned)',
                     'Remove and unpin',
                     'Cancel']
        )
        if not choice or choice == 'Cancel':
            return

        unpin = (choice == 'Remove and unpin')
        removed = 0
        for e in tracked:
            if _remove_lock(e.UniqueId):
                removed += 1
            if unpin:
                try:
                    if e.Pinned:
                        with revit.Transaction('Super Pin — unpin'):
                            e.Pinned = False
                except Exception:
                    pass

        forms.alert('{0} element(s) removed from tracking.'.format(removed),
                    title='Super Pin')
    finally:
        _restore_selection(original_selection)


# ── Entry point ──────────────────────────────────────────────────────────────

def main():
    if doc is None:
        forms.alert('No active document.', title='Super Pin')
        return

    try:
        import super_pin_revert
        super_pin_revert.ensure_event()
    except Exception:
        pass

    original_selection = _current_selection()

    if bool(globals().get('__shiftclick__', False)):
        manage(original_selection)
        return

    targets = _pick_targets(original_selection)
    if not targets:
        _restore_selection(original_selection)
        return

    pin_and_track(targets, _username())


main()
guest@rvtdocs: ~/discuss

Want to join the conversation?

rvtdocs auth

Share & save code Comment on pages Save bookmarks Upvote snippets Gamification

Loading…