Views > Viewpoints

Viewpoint Sync

Copies viewpoint folders from one Navisworks file to another through a package: export the folders here, import them there into the folder you choose, skipping, replacing or keeping both when a name exists.

Tab and panelViews > Viewpoints
KindButton
Live whenA model is open

Status

Launched Ran inside Navisworks at least once with no error logged; nobody checked the result yet.

Recorded on 2026-10-05. The evidence is a line in a pyNavis log.

The public ask

No public source is on record for this tool.

Events it listens to

None. It runs only when you click it.

Files

Paths are relative to the Playground.extension folder.

  • Views.tab/Viewpoints.panel/Viewpoint_Sync.pushbutton/bundle.yaml
  • Views.tab/Viewpoints.panel/Viewpoint_Sync.pushbutton/icon.dark.png
  • Views.tab/Viewpoints.panel/Viewpoint_Sync.pushbutton/icon.png
  • Views.tab/Viewpoints.panel/Viewpoint_Sync.pushbutton/icon.small.dark.png
  • Views.tab/Viewpoints.panel/Viewpoint_Sync.pushbutton/icon.small.png
  • Views.tab/Viewpoints.panel/Viewpoint_Sync.pushbutton/script.py

Install

  1. Only this tool. In Navisworks, click pyNavis > Playground, search for Viewpoint Sync, 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. Viewpoint Sync is on the Views tab, Viewpoints 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.

Views.tab/Viewpoints.panel/Viewpoint_Sync.pushbutton/script.py View on GitHub
"""Copies saved-viewpoint folders between Navisworks files through a viewpoint package."""
import os

from namo import progress, ui, vp_common, vp_sync, vpexport
from pynavis import forms, output, script, toast

TITLE = 'Viewpoint Sync'
ACTIONS = ['Export package', 'Import package']
DEFAULTS = {'target': '', 'keep_paths': True, 'policy': 'skip', 'clip': True, 'comments': True}


def export_package(document):
    rows = vp_common.view_rows(document)
    if not rows:
        toast.info('No saved viewpoints', 'Save some viewpoints, then export a package.')
        return
    folders = vp_common.folder_paths(rows)
    table = [{'value': path, 'cells': [vp_common.folder_label(path), count]} for path, count in folders]
    picked = ui.choose('%s: folders to export' % TITLE, table, ['Folder', 'Viewpoints'],
                       checked=[path for path, _c in folders], ok='Next',
                       prompt='A folder takes its subfolders with it. (top level) is the viewpoints '
                              'outside any folder.')
    if picked is None:
        return
    chosen = vp_common.select_rows(rows, {'mode': 'folders', 'folders': picked})
    if not chosen:
        toast.info('Nothing to export', 'The chosen folders hold no viewpoints.')
        return
    path = forms.save_file(filter='Playground viewpoint packages (*.nvp.json)|*.nvp.json',
                           default_name=vp_sync.package_path_default(document), title=TITLE)
    if path is None:
        return
    if not path.lower().endswith('.nvp.json'):
        path = (path[:-5] if path.lower().endswith('.json') else path) + '.nvp.json'
    try:
        vpexport.write_package(path, vpexport.records_from(chosen, document, include_clip=True), document)
    except (IOError, OSError) as error:
        toast.error('Could not write the package', '%s. Check the folder is writable, then try again.' % error)
        return
    toast.success('Exported %s' % vp_common.plural(len(chosen), 'viewpoint'),
                  '%s. Open the other model and choose Import package.' % path)


def import_package(document):
    path = forms.open_file(filter='Viewpoint packages and Navisworks XML (*.nvp.json;*.json;*.xml)|'
                                  '*.nvp.json;*.json;*.xml', title=TITLE)
    if path is None:
        return
    try:
        records, units_from, notes = vpexport.read_file(path)
    except ValueError as error:
        toast.error('Could not read the package', str(error))
        return
    if not records:
        toast.info('The file holds no viewpoints', notes[0] if notes else os.path.basename(path))
        return
    counts = vpexport.folder_counts(records)
    table = [{'value': folder, 'cells': [vp_common.folder_label(folder), count]} for folder, count in counts]
    picked = ui.choose('%s: folders to import' % TITLE, table, ['Folder', 'Viewpoints'],
                       checked=[folder for folder, _c in counts], ok='Next')
    if picked is None:
        return
    chosen = vp_sync.chosen(records, picked)
    if not chosen:
        toast.info('No folders chosen', 'Tick at least one folder, then import the package again.')
        return
    cfg = script.get_config(DEFAULTS)
    units = str(document.Units)
    same = abs(vp_common.unit_factor(units_from, units) - 1.0) < 1e-12
    intro = '%s from %s.' % (vp_common.plural(len(chosen), 'viewpoint'), os.path.basename(path))
    if not same:
        intro += ' The package is in %s and will be converted to %s.' % (units_from.lower(), units.lower())
    values = ui.form(TITLE, vp_sync.import_fields(cfg), intro=intro, ok='Import')
    if values is None:
        return
    cfg.update(values)
    script.save_config(cfg)
    steps = vp_sync.plan(chosen, values['target'], values['policy'], vp_sync.existing_lookup(document),
                         values['keep_paths'])
    counts_now = vp_sync.plan_counts(steps)
    if counts_now['replace'] and not forms.confirm('%s\n\nReplaced viewpoints lose their own camera, section and '
                                                   'comments. One Ctrl+Z undoes the import.'
                                                   % vp_sync.plan_sentence(steps), TITLE):
        return
    outcome = progress.run(TITLE, lambda report: vp_sync.apply(document, steps, units_from, values['clip'],
                                                               values['comments'], report), 'Importing')
    if outcome is progress.CANCELLED:
        return
    results, stopped = outcome
    if results:
        output.set_title('%s: import report' % TITLE)
        output.print_md('## Imported from %s' % os.path.basename(path))
        for note in notes:
            output.print_md(note)
        output.print_table(vp_sync.report_rows(results), ['Folder', 'Name', 'Result'])
    level, message, detail = vp_sync.summary(results, stopped, len(steps))
    toast.show(level, message, detail)


def run():
    document = vp_common.active_doc()
    if not vp_common.has_models(document):
        toast.info('No model open', 'Open or append a model, then run Viewpoint Sync again.')
        return
    action = forms.ask_options(None, ACTIONS, title=TITLE)
    if action == ACTIONS[0]:
        export_package(document)
    elif action == ACTIONS[1]:
        import_package(document)


run()

Library modules this tool uses

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

  • namo.progressGlue between pyNavis' cancellable progress window and Playground's progress callbacks.
  • namo.uiWPF building blocks for Playground tools, themed from the pyNavis design system.
  • namo.vp_commonShared helpers for the Views tools (Viewpoints, Capture and Navigate panels).
  • namo.vp_syncViewpoint Sync: copy saved-viewpoint folders between Navisworks files through a package (namo.vpexport records in a .nvp.json file; a Navisworks viewpoint .xml is read too).
  • namo.vpexportSaved viewpoints out of Navisworks, as plain records, as Navisworks viewpoint exchange XML, and as a Playground package (.nvp.json) that Viewpoint Sync reads back.

bundle.yaml

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

Views.tab/Viewpoints.panel/Viewpoint_Sync.pushbutton/bundle.yaml View on GitHub
title: Viewpoint\nSync
tooltip: Copies viewpoint folders from one Navisworks file to another through a package: export the folders here, import them there into the folder you choose, skipping, replacing or keeping both when a name exists.
context: doc