Project > 4D

Auto Attach

Attaches model items to TimeLiner tasks by rules, such as task names like L3 Ducts to items on Level 3 in the duct layer, or names containing Slab to a selection set. Previews every task first and attaches in one undo step. Shift+Click loads saved rule templates.

Tab and panelProject > 4D
KindButton
Live whenA model is open

Status

Untested Never executed inside Navisworks. Passed the headless checks only.

Related asks to confirm

No quote is on record yet. These threads look related, but a person still has to open and read them.

Events it listens to

None. It runs only when you click it.

Files

Paths are relative to the Playground.extension folder.

  • Project.tab/4D.panel/Auto_Attach.pushbutton/bundle.yaml
  • Project.tab/4D.panel/Auto_Attach.pushbutton/config.py
  • Project.tab/4D.panel/Auto_Attach.pushbutton/icon.dark.png
  • Project.tab/4D.panel/Auto_Attach.pushbutton/icon.png
  • Project.tab/4D.panel/Auto_Attach.pushbutton/icon.small.dark.png
  • Project.tab/4D.panel/Auto_Attach.pushbutton/icon.small.png
  • Project.tab/4D.panel/Auto_Attach.pushbutton/script.py

Install

  1. Only this tool. In Navisworks, click pyNavis > Playground, search for Auto Attach, tick it and click Install.
  2. The whole extension. Download Playground.extension.zip, unzip it, put the Playground.extension folder in %APPDATA%\pyNavis\extensions\, and click pyNavis > Reload. Auto Attach is on the Project tab, 4D panel.

You need pyNavis 1.3.0 or later.

Tried it? Tell us whether it worked or not. That report is what moves a tool up the status ladder.

Report

Source

The tool's own files, word for word as published in pynavis/Playground. Paths are relative to the Playground.extension folder.

script.py

The script that runs on a click.

Project.tab/4D.panel/Auto_Attach.pushbutton/script.py View on GitHub
# -*- coding: utf-8 -*-
"""Attaches model items to TimeLiner tasks by rules on task names, IDs, types and user fields."""
from pynavis import script, toast

from namo import fourd_attach, fourd_common, fourd_ui, progress, templates, timeliner, ui

TITLE = 'Auto Attach'
TEMPLATES = 'auto_attach'
DEFAULTS = {'rules': fourd_attach.EXAMPLE, 'mode': 'skip', 'leaf_only': True}
MODES = [('Skip tasks that already have items', 'skip'), ('Replace the items they have', 'replace'),
         ('Add to the items they have', 'add')]


def validate_rules(text):
    found, errors = fourd_attach.parse_rules(text)
    if errors:
        return 'Line %d: %s' % errors[0]
    if not found:
        return 'Write at least one rule (lines starting with # are notes).'
    return None


def ask(values):
    fields = [
        {'kind': 'textarea', 'key': 'rules', 'label': 'Rules, one per line', 'rows': 9,
         'default': values.get('rules') or fourd_attach.EXAMPLE, 'validate': validate_rules,
         'hint': 'Task field, test, then => and a set or conditions joined by &. '
                 'The first rule that matches a task wins.'},
        {'kind': 'radio', 'key': 'mode', 'label': 'Tasks that already have items', 'options': MODES,
         'default': values.get('mode', 'skip'), 'fallback': 'skip'},
        {'kind': 'check', 'key': 'leaf_only', 'label': 'Leaf tasks only (not summary tasks)',
         'default': bool(values.get('leaf_only', True))},
        {'kind': 'text', 'key': 'save_as', 'label': 'Save these rules as a template (optional)',
         'placeholder': 'Template name'},
    ]
    return ui.form(TITLE, fields, ok='Preview', width=620)


def preview(doc, rows, entries, mode, report):
    """[{'entry', 'row', 'found': [items], 'sets': [SavedItem], 'attached': n, 'action'}]."""
    from pynavis import sets
    groups = [e['targets']['conditions'] for e in entries if e.get('targets') and e['targets']['conditions']]
    index = None
    if groups:
        report(0.05, 'Finding every object in the model')
        objects = fourd_attach.model_objects(doc, tick=lambda done, total: report(0.1, 'Finding objects'))
        index = fourd_attach.ObjectIndex(objects, fourd_attach.read_pairs)
        index.prepare(groups, tick=lambda done, total: report(
            0.15 + 0.6 * done / float(total or 1), 'Reading properties of %s objects' % '{:,}'.format(total)))
    found_sets = {}
    out = []
    for number, entry in enumerate(entries):
        report(0.75 + 0.2 * number / float(len(entries) or 1), 'Checking tasks')
        row = rows[entry['index']]
        item = {'entry': entry, 'row': row, 'found': [], 'sets': [], 'missing_sets': [], 'attached': 0}
        if (row.get('attach_kind') or 'none') != 'none':
            current = timeliner.selected_items(row['task'], doc)
            item['attached'] = current.Count if current is not None else 0
            item['has'] = True
        targets = entry.get('targets')
        if targets:
            for path in targets['sets']:
                if path not in found_sets:
                    saved = sets.find(path, doc)
                    found_sets[path] = saved if saved is not None and not saved.IsGroup else None
                if found_sets[path] is None:
                    item['missing_sets'].append(path)
                else:
                    item['sets'].append(found_sets[path])
            if targets['conditions']:
                item['found'] = index.find(targets['conditions'])
        item['count'] = len(item['found']) + sum(sets.items_of(s, doc).Count for s in item['sets'])
        item['action'] = action_of(item, mode)
        out.append(item)
    return out


def action_of(item, mode):
    entry = item['entry']
    if entry['rule'] is None:
        return ('none', 'No rule matched')
    if entry.get('error'):
        return ('error', entry['error'])
    if not item['count']:
        if item['missing_sets']:
            return ('empty', 'No set called %s' % item['missing_sets'][0])
        return ('empty', 'Found nothing')
    if item.get('has') and mode == 'skip':
        return ('skip', 'Skip: has %s' % fourd_common.plural(item['attached'], 'item'))
    verb = {'replace': 'Replace with', 'add': 'Add'}.get(mode, 'Attach') if item.get('has') else 'Attach'
    return ('attach', '%s %s' % (verb, fourd_common.plural(item['count'], 'item')))


def preview_rows(items):
    out = []
    for index, item in enumerate(items):
        row = item['row']
        rule = item['entry']['rule']
        out.append({'cells': [row.get('outline'), row.get('name') or '', 'Line %d' % rule['line'] if rule else '',
                              item['count'], item['attached'], item['action'][1]], 'value': index})
    return out


def apply(doc, items, mode):
    from pynavis import sets
    changes = []
    for item in items:
        if item['action'][0] != 'attach':
            continue
        if item['found'] or mode == 'add' and item.get('has'):
            objects = list(item['found'])
            for saved in item['sets']:
                objects.extend(list(sets.items_of(saved, doc)))
            if mode == 'add' and item.get('has'):
                current = timeliner.selected_items(item['row']['task'], doc)
                if current is not None:
                    objects.extend(list(current))
            target = ('items', objects)
        else:
            target = ('sets', item['sets'])
        changes.append((item['row'], _setter(target, doc)))
    return timeliner.edit_tasks(changes, doc, 'Auto attach')


def _setter(target, doc):
    def edit(values):
        timeliner.set_attachment(values, target, doc)
    return edit


def run():
    if ui.is_runner():
        toast.info('%s needs a person at the screen' % TITLE, 'Run it from the ribbon.')
        return
    doc = fourd_common.active_doc()
    try:
        rows = timeliner.snapshot(doc)
    except RuntimeError as error:
        toast.error('TimeLiner is not available', str(error))
        return
    if not rows:
        toast.info('No TimeLiner tasks', 'Add tasks in TimeLiner, or build them with TimeLiner from Excel.')
        return
    values = script.get_config(DEFAULTS)
    answer = ask(values)
    if answer is None:
        return
    values.update({'rules': answer['rules'], 'mode': answer['mode'], 'leaf_only': bool(answer['leaf_only'])})
    script.save_config(values)
    if answer['save_as'].strip():
        try:
            templates.Templates(TEMPLATES).save(answer['save_as'], {'rules': answer['rules']})
        except (ValueError, IOError) as error:
            toast.warning('Rules not saved as a template', str(error))
    rule_list, _errors = fourd_attach.parse_rules(answer['rules'])
    entries = fourd_attach.plan(rows, rule_list, bool(answer['leaf_only']))
    if not entries:
        toast.info('No tasks to attach to', 'Every task is a summary task; untick Leaf tasks only.')
        return
    items = progress.run(TITLE, lambda report: preview(doc, rows, entries, answer['mode'], report))
    if items is progress.CANCELLED:
        return
    todo = [i for i in items if i['action'][0] == 'attach']
    unmatched = len([i for i in items if i['action'][0] == 'none'])
    empty = len([i for i in items if i['action'][0] in ('empty', 'error')])
    if not todo:
        toast.info('Nothing to attach', '%s matched no rule, %s found nothing.'
                   % (fourd_common.plural(unmatched, 'task'), fourd_common.plural(empty, 'rule match', 'rule matches')))
        return
    status = '%s to attach, %s matched no rule, %s found nothing.' % (
        fourd_common.plural(len(todo), 'task'), '{:,}'.format(unmatched), '{:,}'.format(empty))
    if not fourd_ui.review(TITLE, ['WBS', 'Task', 'Rule', 'Items found', 'Attached now', 'Action'],
                           preview_rows(items), status, 'Attach to %s' % fourd_common.plural(len(todo), 'task')):
        return
    written, errors = apply(doc, items, answer['mode'])
    detail = '%s matched no rule, %s found nothing.' % (fourd_common.plural(unmatched, 'task'), '{:,}'.format(empty))
    if errors:
        toast.warning('Attached items to %s, %d failed' % (fourd_common.plural(written, 'task'), len(errors)),
                      errors[0])
    else:
        toast.success('Attached items to %s' % fourd_common.plural(written, 'task'), detail)


run()

config.py

The Shift+Click action.

Project.tab/4D.panel/Auto_Attach.pushbutton/config.py View on GitHub
# -*- coding: utf-8 -*-
"""Loads a saved Auto Attach rule template as the current rules, or deletes one."""
from pynavis import forms, script, toast

from namo import templates, ui

TITLE = 'Auto Attach templates'


def run():
    if ui.is_runner():
        return
    store = templates.Templates('auto_attach')
    names = store.names()
    if not names:
        toast.info('No saved rule templates', 'In Auto Attach, type a template name under the rules to save them.')
        return
    name = forms.select_from_list(names, title=TITLE, prompt='Pick a rule template.')
    if name is None:
        return
    action = forms.ask_options('What should happen to %s?' % name,
                               ['Use as the current rules', 'Delete the template'], title=TITLE)
    if action is None:
        return
    if action == 'Delete the template':
        if not forms.confirm('Delete the rule template %s for everyone who shares it?' % name, title=TITLE):
            return
        store.delete(name)
        toast.success('Deleted the rule template', name)
        return
    config = store.get(name) or {}
    values = script.get_config({'rules': '', 'mode': 'skip', 'leaf_only': True})
    values['rules'] = config.get('rules', '')
    script.save_config(values)
    toast.success('Loaded the rules of %s' % name, 'Click Auto Attach to preview them.')


run()

Library modules this tool uses

From the shared namo library, which installs with every tool. Each name opens the module on GitHub.

  • namo.fourd_attachAuto Attach rules, and the one-walk object index both Auto Attach and TimeLiner from Excel use to turn conditions into model items.
  • namo.fourd_commonHelpers the 4D, Quantities and Field tools share: scopes, default files, text.
  • namo.fourd_uiWindows of the 4D tools, built on namo.ui.
  • namo.progressGlue between pyNavis' cancellable progress window and Playground's progress callbacks.
  • namo.templatesNamed, saved configurations per tool ("templates"), shared with the team when a shared folder is set. Report Templates and Scheduled Reports read them all.
  • namo.timelinerTimeLiner: every Playground access to Navisworks TimeLiner, plus the pure schedule logic.
  • namo.uiWPF building blocks for Playground tools, themed from the pyNavis design system.

bundle.yaml

The bundle contract: the keys pyNavis reads for this tool.

Project.tab/4D.panel/Auto_Attach.pushbutton/bundle.yaml View on GitHub
title: Auto\nAttach
tooltip: Attaches model items to TimeLiner tasks by rules, such as task names like L3 Ducts to items on Level 3 in the duct layer, or names containing Slab to a selection set. Previews every task first and attaches in one undo step. Shift+Click loads saved rule templates.
context: doc