Viewpoints panel

Section Nudge

A remote for the section box: faces named the way you see them, moves that follow the screen, a known step per press.

On the ribbonDock panel, Viewpoints panel
NeedsAn open document (with sectioning off it offers to turn it on)

What it does#

Dragging a section handle is imprecise and easy to overshoot. This panel moves the box or one of its faces a known distance per press, every press one undo step, from buttons or from the keyboard.

Adjust: one face at a time#

The six faces sit in an unfolded box and are named the way you see them: Front is the face looking at you, Left is on the screen's left. The names follow the camera, so orbit round the model and Front is still the face in front of you; the small axis label under each name tells you which world axis that currently is. Click a face, then In to keep less of the model or Out to keep more. A tilted plane, a switched-off one, or a second plane facing the same way is listed under the remote by number.

Move: the whole box#

The Move tab slides all six planes together: left, right, up, down on the screen, nearer or farther from you. Two frames to choose from. World axes snaps each move to the world axis nearest the screen direction, like the compass ring round the ViewCube, so the box stays on the building's grid. Screen follows the camera exactly, like the cube itself, so Left is always screen-left even from an odd angle.

The step#

Halve and double buttons, presets in the document's units, and a box to type a value. On an imperial document the box reads and writes feet and inches the way Revit does: 0.5 is half a foot and shows as 0' 6", 0 0 1/8 is an eighth of an inch, and 1' 6", 6 1/2", 18" and 1-6 all work.

Keys and chords#

  • With the panel focused: 0 to 6 pick a target, Tab cycles; Up and Down nudge a face; arrows move the box across the screen and Page Up and Page Down bring it nearer or push it farther; plus and minus change the step; Enter applies a typed step.
  • Over the 3D view, no panel needed: Ctrl+Alt with Up and Down nudge, with Left and Right change the target, with Page Up and Page Down double and halve the step. Each is listed in the Shortcuts editor under its Nudge title, so it can be moved off Ctrl+Alt on a machine where the display driver owns those keys.

Limitations#

  • In Box mode, shrinking a face past the opposite face is refused.
  • The readout follows changes made elsewhere once a second, so a handle dragged in the view shows up after a beat.

Other uses#

  • Fit to selection and Sectioning off sit at the bottom of the panel, so a whole sectioning job stays in one place.
  • Step through a building floor by floor: set the step to the storey height and press Up.

Source#

The tool's own files, word for word as shipped in Viewpoints.panel/Section_Nudge.dockpane. Every pyNavis tool is an ordinary bundle: copy these into your own extension and change what you like.

script.py Runs on a click. View on GitHub
# -*- coding: utf-8 -*-
"""The Section Nudge panel: move the section box or planes from the keyboard.

The panel is a remote: the six faces laid out as an unfolded box and named the
way you see them, Front being the face that looks at you, with the world axis
each faces printed under its name. The camera's right, up and forward are
snapped to world axes (the way the ViewCube reads a view) and the names follow
the camera on the readout timer, so orbit round the model and Front stays the
face in front of you. Two tabs: Adjust holds the
remote and a face is the target; Move is the whole box. Face targets nudge
In and Out; the box moves left, right, up,
down, nearer and farther on the screen: along the nearest world axes (the
compass ring round the ViewCube) or exactly along the screen (the cube
itself), as the World axes / Screen switch says. Every
nudge is one viewpoint write, so Ctrl+Z steps back one nudge. The behaviour
lives in lib/sectionnudge.py, which the six chord bundles beside this panel
share, so a key here and a chord over the view do the same thing.

Reload re-runs this script against fresh controls; the previous run's timer is
stopped through the session holder first, the way the Viewpoint Tracker does.
"""

import clr

import sectionnudge

from pynavis import app, script, section, selection, settings, toast

clr.AddReference('PresentationFramework')
clr.AddReference('PresentationCore')
clr.AddReference('WindowsBase')

from System import TimeSpan
from System.Windows import FontWeights, Thickness, Visibility
from System.Windows.Controls import Control
from System.Windows.Media import BrushConverter
from System.Windows.Controls import Button
from System.Windows.Input import Key
from System.Windows.Threading import DispatcherPriority, DispatcherTimer

# How often the readout and the face names follow the camera and changes made
# outside the panel.
REFRESH_MS = 1000

pane = __pane__
log = script.get_logger()
held = sectionnudge.session()

previous = getattr(held, 'pane_detach', None)
if previous is not None:
    previous()

find = pane.Find
root = find('Root')
step_box = find('StepBox')
tabs = find('Tabs')
adjust_tab, move_tab = find('AdjustTab'), find('MoveTab')
cells = dict((name, find('Cell' + name)) for name in sectionnudge.CELLS)
cell_names = dict((name, find('Cell%sName' % name)) for name in sectionnudge.CELLS)
cell_axes = dict((name, find('Cell%sAxis' % name)) for name in sectionnudge.CELLS)
others_panel = find('Others')
presets_panel = find('Presets')
moves_buttons = {'world': find('MovesWorld'), 'screen': find('MovesScreen')}

scale = [1.0]                   # document units per mm, refreshed with the readout

# The active choice gets the same fill the buttons show on hover, so the
# selected face reads at a glance rather than only by weight; the pair is
# picked for the host theme so it stays visible on dark.
_brush = BrushConverter()
if script.is_dark_theme():
    ACTIVE_FILL, ACTIVE_EDGE = _brush.ConvertFromString('#3A5A7A'), _brush.ConvertFromString('#7FB2E5')
else:
    ACTIVE_FILL, ACTIVE_EDGE = _brush.ConvertFromString('#BEE6FD'), _brush.ConvertFromString('#3C7FB1')


def mark(button, active):
    """Bold and filled when active; back to the theme's own look when not."""
    button.FontWeight = FontWeights.Bold if active else FontWeights.Normal
    if active:
        button.Background = ACTIVE_FILL
        button.BorderBrush = ACTIVE_EDGE
    else:
        button.ClearValue(Control.BackgroundProperty)
        button.ClearValue(Control.BorderBrushProperty)
last_face = [0]                 # the face to come back to when Adjust reopens
syncing = [False]               # refresh is moving the tab, not the user
cell_index = {}                 # cell name -> plane index, from the last refresh
shown_units = [None]            # which units the presets were built for


def safely(work):
    """An exception escaping a WPF handler on the UI thread takes Navisworks
    down with it; a panel is not worth that."""
    try:
        work()
    except Exception as error:
        log.error('Section Nudge: %s' % error)


def small_button(text, action):
    button = Button()
    button.Content = text
    button.Margin = Thickness(1)
    button.Padding = Thickness(6, 2, 6, 2)
    button.Click += lambda sender, args: safely(lambda: act(action))
    return button


def fill_presets(units):
    presets_panel.Children.Clear()
    for label, mm in sectionnudge.presets(units):
        presets_panel.Children.Add(small_button(label, 'step:%r' % mm))
    shown_units[0] = units


def fill_others(indices, labels, target):
    others_panel.Children.Clear()
    for index in indices:
        button = small_button(labels.get(index, sectionnudge.target_name(index)),
                              'target:%d' % index)
        mark(button, index == target)
        others_panel.Children.Add(button)
    others_panel.Visibility = Visibility.Visible if indices else Visibility.Collapsed


def refresh():
    doc = app.get_doc()
    state = sectionnudge.load_state()
    snapshot = sectionnudge.read(doc, note=log.warning)
    frame = sectionnudge.camera_frame(doc)
    units = str(doc.Units)
    scale[0] = sectionnudge.per_mm(doc)
    text = sectionnudge.describe(state, snapshot, units, scale[0], frame)
    placed = sectionnudge.layout(snapshot, frame)
    target = state['target']

    find('Readout').Text = text['readout']
    imperial = units in ('Feet', 'Inches', 'Yards', 'Miles')
    find('StepUnits').Text = '' if imperial else units.lower()
    if not step_box.IsKeyboardFocusWithin:
        step = state['step_mm'] * scale[0]
        step_box.Text = text['step'] if imperial else '%g' % step
    if units != shown_units[0]:
        fill_presets(units)

    is_box = target == 'box'
    if not is_box:
        last_face[0] = target
    syncing[0] = True
    try:
        tabs.SelectedItem = move_tab if is_box else adjust_tab
    finally:
        syncing[0] = False
    for name, button in moves_buttons.items():
        mark(button, name == state['moves'])
    planes = (snapshot or {}).get('planes') or []
    cell_index.clear()
    for name in sectionnudge.CELLS:
        index = placed['cells'][name]
        cell_index[name] = index
        cells[name].IsEnabled = index is not None
        mark(cells[name], index == target)
        if index is None or index >= len(planes):
            cell_axes[name].Text = ''
        else:
            inward = planes[index].get('normal', (0.0, 0.0, 0.0))
            cell_axes[name].Text = sectionnudge.axis_label((-inward[0], -inward[1], -inward[2]))
    fill_others(placed['others'], placed['labels'], target)

    off = snapshot is not None and not snapshot['enabled']
    find('SectionOn').Visibility = Visibility.Visible if off else Visibility.Collapsed
    find('SectionOff').IsEnabled = snapshot is not None and not off


def act(action):
    result = sectionnudge.run_action(action)
    if result['problem']:
        toast.warning(result['problem'])
    refresh()


def apply_typed_step():
    typed = sectionnudge.parse_length(step_box.Text, str(app.get_doc().Units))
    if typed is None:
        toast.warning('That is not a distance')
        return
    if typed <= 0:
        toast.warning('The step must be more than zero')
        return
    act('step:%r' % (typed / scale[0] if scale[0] else typed))
    root.Focus()


def wire(name, action):
    find(name).Click += lambda sender, args: safely(lambda: act(action))


def pick_cell(name):
    index = cell_index.get(name)
    if index is not None:
        act('target:%d' % index)


for cell in sectionnudge.CELLS:
    cells[cell].Click += (lambda n: lambda sender, args: safely(lambda: pick_cell(n)))(cell)
wire('NudgeIn', 'in')
wire('NudgeOut', 'out')
wire('MoveLeft', 'left')
wire('MoveRight', 'right')
wire('MoveUp', 'up')
wire('MoveDown', 'down')
wire('MoveNearer', 'nearer')
wire('MoveFarther', 'farther')
wire('MovesWorld', 'moves:world')
wire('MovesScreen', 'moves:screen')
wire('StepHalve', 'halve')
wire('StepDouble', 'double')


def turn_on(sender, args):
    def work():
        sectionnudge.enable(app.get_doc())
        refresh()
    safely(work)


def turn_off(sender, args):
    def work():
        result = section.clear()
        toast.show(result.level, result.message, result.detail)
        refresh()
    safely(work)


def fit(sender, args):
    def work():
        values = settings.load(section.TOOL, section.DEFAULTS)
        result = section.fit_to_selection(selection.get_items(), values)
        toast.show(result.level, result.message, result.detail)
        refresh()
    safely(work)


def on_tab(sender, args):
    # The Move tab is the box target and Adjust is a face, so switching tabs
    # switches targets; refresh moving the tab to match the target must not
    # bounce back through here.
    if syncing[0] or args.Source is not tabs:
        return
    def work():
        if tabs.SelectedItem is move_tab:
            act('target:box')
        else:
            act('target:%d' % last_face[0])
    safely(work)


tabs.SelectionChanged += on_tab
find('SectionOn').Click += turn_on
find('SectionOff').Click += turn_off
find('FitSelection').Click += fit


def on_key(sender, args):
    def work():
        name = args.Key.ToString()
        if step_box.IsKeyboardFocusWithin:
            # Typing a number: only Enter and Escape mean anything here.
            if args.Key == Key.Enter:
                args.Handled = True
                apply_typed_step()
            elif args.Key == Key.Escape:
                args.Handled = True
                root.Focus()
            return
        action = sectionnudge.action_for_key(name, sectionnudge.load_state()['target'])
        if action is not None:
            args.Handled = True
            act(action)
    safely(work)


root.PreviewKeyDown += on_key
root.PreviewMouseDown += lambda sender, args: (
    None if step_box.IsMouseOver else root.Focus())

timer = DispatcherTimer(DispatcherPriority.Background)
timer.Interval = TimeSpan.FromMilliseconds(REFRESH_MS)


def tick(sender, args):
    if pane.Visible:
        safely(refresh)


timer.Tick += tick
timer.Start()


def detach():
    try:
        timer.Stop()
    except Exception:
        pass


held.pane_detach = detach
safely(refresh)
pane.xaml The dock panel's content. View on GitHub
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      x:Name="Root" Margin="12" Focusable="True">
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
  </Grid.RowDefinitions>

  <!-- No colours anywhere: the panel follows the Navisworks theme. The active
       target is shown bold, which reads in both themes. -->

  <!-- Row 0: the two jobs. Adjust moves one face in or out; Move slides the
       whole box. The Move tab is the box target, so opening it picks the box
       and opening Adjust picks the last face used. -->
  <TabControl x:Name="Tabs" Grid.Row="0" Padding="4">

    <TabItem x:Name="AdjustTab" Header="Adjust">
      <StackPanel>
        <!-- The remote. The six faces laid out as an unfolded box, named the
             way you see them (Front is the face looking at you), each with the
             world axis it faces under the name. The script fills the names and
             axes from the camera and greys a cell no plane faces. -->
        <Grid>
          <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
          </Grid.ColumnDefinitions>
          <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
          </Grid.RowDefinitions>

          <Button x:Name="CellTop" Grid.Row="0" Grid.Column="1" Margin="1" Padding="0,3,0,3">
            <StackPanel>
              <TextBlock x:Name="CellTopName" Text="Top" TextAlignment="Center"/>
              <TextBlock x:Name="CellTopAxis" Text="" FontSize="10" Opacity="0.6" TextAlignment="Center"/>
            </StackPanel>
          </Button>
          <Button x:Name="CellLeft" Grid.Row="1" Grid.Column="0" Margin="1" Padding="0,3,0,3">
            <StackPanel>
              <TextBlock x:Name="CellLeftName" Text="Left" TextAlignment="Center"/>
              <TextBlock x:Name="CellLeftAxis" Text="" FontSize="10" Opacity="0.6" TextAlignment="Center"/>
            </StackPanel>
          </Button>
          <Button x:Name="CellFront" Grid.Row="1" Grid.Column="1" Margin="1" Padding="0,3,0,3">
            <StackPanel>
              <TextBlock x:Name="CellFrontName" Text="Front" TextAlignment="Center"/>
              <TextBlock x:Name="CellFrontAxis" Text="" FontSize="10" Opacity="0.6" TextAlignment="Center"/>
            </StackPanel>
          </Button>
          <Button x:Name="CellRight" Grid.Row="1" Grid.Column="2" Margin="1" Padding="0,3,0,3">
            <StackPanel>
              <TextBlock x:Name="CellRightName" Text="Right" TextAlignment="Center"/>
              <TextBlock x:Name="CellRightAxis" Text="" FontSize="10" Opacity="0.6" TextAlignment="Center"/>
            </StackPanel>
          </Button>
          <Button x:Name="CellBottom" Grid.Row="2" Grid.Column="1" Margin="1" Padding="0,3,0,3">
            <StackPanel>
              <TextBlock x:Name="CellBottomName" Text="Bottom" TextAlignment="Center"/>
              <TextBlock x:Name="CellBottomAxis" Text="" FontSize="10" Opacity="0.6" TextAlignment="Center"/>
            </StackPanel>
          </Button>
          <Button x:Name="CellBack" Grid.Row="3" Grid.Column="1" Margin="1" Padding="0,3,0,3">
            <StackPanel>
              <TextBlock x:Name="CellBackName" Text="Back" TextAlignment="Center"/>
              <TextBlock x:Name="CellBackAxis" Text="" FontSize="10" Opacity="0.6" TextAlignment="Center"/>
            </StackPanel>
          </Button>
        </Grid>

        <!-- Planes that face no cell (tilted, off, or a second plane the same
             way). The script fills it; hidden when there are none. -->
        <WrapPanel x:Name="Others" Margin="0,4,0,0" Visibility="Collapsed"/>

        <UniformGrid Rows="1" Columns="2" Margin="0,8,0,0">
          <Button x:Name="NudgeIn" Content="In" Margin="1" Padding="0,6,0,6"
                  ToolTip="Move the face in, keeping less of the model (Up)"/>
          <Button x:Name="NudgeOut" Content="Out" Margin="1" Padding="0,6,0,6"
                  ToolTip="Move the face out, keeping more of the model (Down)"/>
        </UniformGrid>
      </StackPanel>
    </TabItem>

    <TabItem x:Name="MoveTab" Header="Move">
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="*"/>
          <ColumnDefinition Width="*"/>
          <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <!-- Which frame the box moves in: the compass ring round the ViewCube
             (world axes, snapped to the view) or the cube itself (the screen). -->
        <Button x:Name="MovesWorld" Grid.Row="0" Grid.Column="0" Content="World axes" Margin="1" Padding="0,3,0,3"
                ToolTip="Moves land on the world axes nearest the screen's, like the compass ring round the ViewCube"/>
        <Button x:Name="MovesScreen" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" Content="Screen" Margin="1" Padding="0,3,0,3"
                ToolTip="Moves follow the screen exactly, like the ViewCube itself, so Left is always screen-left"/>

        <Button x:Name="MoveUp" Grid.Row="1" Grid.Column="1" Content="&#9650;" Margin="1,8,1,1" Padding="0,4,0,4"
                ToolTip="Move the box up the screen (Up)"/>
        <Button x:Name="MoveNearer" Grid.Row="1" Grid.Column="2" Content="Nearer" Margin="1,8,1,1" Padding="0,4,0,4"
                ToolTip="Move the box towards you (Page Up)"/>
        <Button x:Name="MoveLeft" Grid.Row="2" Grid.Column="0" Content="&#9664;" Margin="1" Padding="0,4,0,4"
                ToolTip="Move the box left on the screen (Left)"/>
        <Button x:Name="MoveRight" Grid.Row="2" Grid.Column="2" Content="&#9654;" Margin="1" Padding="0,4,0,4"
                ToolTip="Move the box right on the screen (Right)"/>
        <Button x:Name="MoveDown" Grid.Row="3" Grid.Column="1" Content="&#9660;" Margin="1" Padding="0,4,0,4"
                ToolTip="Move the box down the screen (Down)"/>
        <Button x:Name="MoveFarther" Grid.Row="3" Grid.Column="2" Content="Farther" Margin="1" Padding="0,4,0,4"
                ToolTip="Move the box away from you (Page Down)"/>
      </Grid>
    </TabItem>
  </TabControl>

  <!-- Row 1: where the target is, right under the buttons that move it -->
  <StackPanel Grid.Row="1" Margin="0,6,0,0">
    <TextBlock x:Name="Readout" Text="" FontSize="11" Opacity="0.75" TextWrapping="Wrap"/>
    <Button x:Name="SectionOn" Content="Turn sectioning on" Margin="0,6,0,0"
            Padding="8,3,8,3" HorizontalAlignment="Left" Visibility="Collapsed"/>
  </StackPanel>

  <!-- Row 2: the step, with presets in the document's units -->
  <StackPanel Grid.Row="2" Margin="0,10,0,0">
    <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="Auto"/>
      </Grid.ColumnDefinitions>
      <TextBlock Grid.Column="0" Text="Step" VerticalAlignment="Center" Margin="0,0,6,0"/>
      <TextBox x:Name="StepBox" Grid.Column="1" Text="100" VerticalAlignment="Center"
               Padding="4,2,4,2" MinWidth="60"/>
      <TextBlock x:Name="StepUnits" Grid.Column="2" Text="mm" VerticalAlignment="Center"
                 Margin="6,0,6,0" Opacity="0.75"/>
      <Button x:Name="StepHalve" Grid.Column="3" Content="-" Width="28" Margin="1"
              ToolTip="Halve the step (minus key)"/>
      <Button x:Name="StepDouble" Grid.Column="4" Content="+" Width="28" Margin="1"
              ToolTip="Double the step (plus key)"/>
    </Grid>
    <WrapPanel x:Name="Presets" Margin="0,4,0,0"/>
  </StackPanel>

  <!-- Row 3: the two things you do before and after nudging -->
  <UniformGrid Grid.Row="3" Rows="1" Columns="2" Margin="0,10,0,0">
    <Button x:Name="FitSelection" Content="Fit to selection" Margin="1" Padding="0,4,0,4"
            ToolTip="Fit the section box to the selected objects, then nudge from there"/>
    <Button x:Name="SectionOff" Content="Sectioning off" Margin="1" Padding="0,4,0,4"
            ToolTip="Switch sectioning off, leaving the camera where it is"/>
  </UniformGrid>
</Grid>
bundle.yaml The bundle contract: the keys pyNavis reads for this tool. View on GitHub
title: Section Nudge
tooltip: A remote for the section box. The six faces are named the way you see them, Front being the face looking at you, and the box moves left, right, up, down, nearer and farther on the screen. Pick a face or the whole box, set a step, nudge. Each nudge is one undo step. The same moves are on Ctrl+Alt chords over the 3D view.
keytip: SN