Bundles

Icons

Four PNGs per bundle, why they are the sizes they are, and the drawing language behind the shipped set.

A bundle with no icon renders as a text-only button. It works, but it looks like a placeholder next to the native Navisworks ribbon, and on a stack row or in a pulldown menu it is nearly unreadable. Icons are the cheapest thing you can do to make a tool look like it belongs.

Icons are plain PNG files sitting beside script.py. There is no manifest entry and no reference in bundle.yaml: the runtime looks for four fixed file names in the bundle folder and uses what it finds.

The four files#

Both .pushbutton and .pulldown folders carry icons. A .stack folder never does, because a stack has no button of its own; each of its children supplies its own art.

FileSlotSize
icon.png Large button, light theme96×96
icon.dark.png Large button, dark theme96×96
icon.small.png Stack rows and pulldown menu rows, light theme32×32
icon.small.dark.png Stack rows and pulldown menu rows, dark theme32×32

The theme is chosen once when the ribbon is built, so a bundle only ever shows two of its four files in a given session. Clicking Reload re-reads the host theme and rebuilds, which is how a tool follows you when you switch Navisworks between light and dark.

Two kinds also read a pressed/on state

A *.toggle reads icon.on.png/icon.on.dark.png and icon.off.png/icon.off.dark.png in place of the plain icon.png pair; see toggle: a button with on/off state. A *.dockpane keeps the ordinary four above for its closed state and additionally reads icon.on.png/icon.on.dark.png, shown on its ribbon toggle while the panel is open; see dockpane: a real Navisworks dock panel. Neither file is required - a dockpane with no on-art just never visibly presses.

The fallback chain#

Only icon.png is genuinely required. The other three fall back to it, and this is the exact code that decides:

text
var large = Find("icon.png");
return (large,
        Find("icon.dark.png")       ?? large,
        Find("icon.small.png")      ?? large,
        Find("icon.small.dark.png") ?? large);
The one required file The three optional variants icon.png 96×96, always read icon.dark.png large button, dark theme icon.small.png stack and menu rows icon.small.dark.png stack and menu rows, dark if missing if missing Every arrow points at icon.png. A missing dark variant does not borrow the small one, and a missing small dark variant does not borrow icon.small.png.
The fallback is a star, not a chain. Each variant is resolved independently against icon.png and nothing else.
There is no chaining between variants

If you ship icon.png and icon.small.png only, the dark theme uses your light 96×96 art in both slots: icon.dark.png falls back to icon.png, and so does icon.small.dark.png. Your carefully simplified small drawing is not used in dark mode at all. Ship all four, or ship exactly one.

No icon.png means no icon at all

If icon.png is absent, all four paths are null even when the other three files exist on disk. The runtime sets ShowImage = false and the button renders as text. A bundle holding only icon.dark.png is a bundle with no icon.

Why 96 and 32#

The sizes are not arbitrary and they are not a style choice. AdWindows, the ribbon framework Navisworks uses, draws Image and LargeImage at their natural WPF size and never scales them to fit the slot. A 96×96 PNG stamped at the usual 96 DPI is 96 logical pixels wide, so before this was handled a large button showed the top-left corner of the art clipped into its 32-pixel slot, and a pulldown menu row showed an enormous image.

The runtime fixes this on load by re-stamping the DPI so the bitmap's logical size matches the slot while its pixel data is untouched:

text
var dpi = 96.0 * image.PixelWidth / logicalSize;

Large buttons load with logicalSize = 32, small rows and menu lists with logicalSize = 16. A 96-pixel image asked to occupy 32 logical pixels is stamped at 288 DPI, so it lays out as 32 units wide but still hands the compositor three times the pixel data. That is what keeps the art crisp at 150% and 200% display scaling instead of being flattened to 32 real pixels.

Each slot is loaded independently from the best available path:

text
largePath = large ?? small;      LargeImage = Load(largePath, 32)
smallPath = small ?? large;      Image      = Load(smallPath, 16)

Pulldowns additionally set ListImageSize = RibbonImageSize.Standard so their menu rows draw the 16-unit slot, which is why the small art matters most inside a pulldown.

Ship square art

Non-square or odd-sized PNGs load without error, but the scale factor is pixelWidth / logicalSize, computed from the width alone. A 96×64 image therefore lays out 32 units wide and 21 units tall, and a 64×64 image is simply smaller than everything beside it. PNG is the contract; other formats are untested and unused.

A file that cannot be decoded logs Could not load icon '<path>' and yields null for that slot, so a corrupt icon.png looks exactly like a missing one.

The drawing language#

The shipped icons follow one rule, and it is worth adopting even if you draw your own set by hand:

Neutral ink draws the thing. A solid accent mark draws the action. Colour therefore always means the same thing: it is the verb. A user scanning the panel reads the grey shape to find what a tool operates on, and the blue mark to find what it does to it.

Memorize, shown at 2x 0,0 96,96 Ink shape: the noun The open tray is the memory register. It is shared by eight Memory glyphs, so the family reads as one family. Accent mark: the verb An arrow pointing into the tray means store. Reverse it and the same drawing means recall. One weight, rounded caps Everything is drawn in a 0..96 space at 4x and downsampled, so edges stay clean without hinting.
The same ink shape plus a different accent mark is a different tool. That is the whole system, and it is why a panel of twenty icons stays legible.

Cohesion comes from reusing marks rather than from a common outline. Two families carry the shipped set: the open tray, which is the memory register and appears in eight Memory glyphs, and the spark, which always means a collision.

tray spark plus dots tilted_box eye memory register a collision add to a list fitted section inspect
The shared marks, drawn at their real proportions. Ink marks name a thing, accent marks name an action, and the same mark always means the same thing.

The palette#

Four colours per theme, and no gradients, shadows or outlines. Ink is a desaturated blue-grey rather than black so it sits at the same visual weight as the native Navisworks glyphs.

Light theme ink #3D4451 accent #0078D4 alert #C42B1C paper #FFFFFF Dark theme ink #E8E8E8 accent #4CC2FF alert #FF5F52 paper #202020 Paper is never painted into the file. Icons are transparent; paper is what they are drawn to sit on.
Alert red is the exception, not a third brand colour. It appears exactly once in the entire shipped set.
One red icon, and only one

Alert red is used on Purge and nowhere else, because Purge is the only destructive tool in the set. The generator says it plainly: alert red appears exactly ONCE in the whole set, on Purge, because it is the only destructive tool. Do not add a second. Red means "this deletes something you cannot get back". Spend it once and it keeps working.

Small icons are separate drawings#

The 32-pixel files are not resized copies of the 96-pixel ones. They are drawn by a different branch of the same function, with fewer elements and a heavier relative stroke, and then rendered down. The generator carries two weights:

ConstantValueRelative strokeUsed for
WEIGHT_LARGE88 of 96, about 8% The 96×96 files
WEIGHT_SMALL1414 of 96, about 15% The 32×32 files
icon.png icon.small.png the same small art, enlarged 96 x 96, stroke 8 The tray has a shelf line, the arrow has a slim shaft. 32 x 32, stroke 14 The shelf line is gone, the tray is wider, the arrow head is fatter. Downscaling the left drawing would leave a 2.7 pixel stroke and a shelf line one pixel from the tray floor.
Left and centre are drawn at true relative scale. At 32 pixels a detail either survives at three pixels or it becomes grey mush, so it is removed rather than shrunk.

The practical rules for a small variant: drop anything under about a fifth of the canvas, thicken every stroke, push the shape closer to the edges, and keep exactly one accent mark. If you cannot say what the tool does with two shapes, the small icon is not the place to try.

The generator#

Every icon in the shipped pyNavis extension is produced by tools/make_icons.py, a dev-time CPython script that needs Pillow. It never ships into Navisworks and is never loaded by an engine, so it is free to use modern syntax.

powershell
python tools/make_icons.py                    # regenerate every bundle's icon files
python tools/make_icons.py --sheet out.png    # regenerate, and write a contact sheet
python tools/make_icons.py --sheet-only --sheet out.png   # only the sheet, touch no bundle

The contact sheet renders the whole set against light and dark plates at both densities, which is how you review a change without opening Navisworks.

Never hand-edit an icon

The rule is in the module docstring and it is not negotiable: Never hand-edit an icon. Change the drawing here and re-run. A retouched PNG is silently reverted by the next person who runs the generator, and the retouch is invisible in the diff that matters.

A glyph is one function taking a drawing context. The c.small flag selects the simplified branch, so both densities live side by side and cannot drift apart:

tools/make_icons.py
def ic_memorize(c):
    if c.small:
        small_tray(c)
        c.arrow_v(48, 6, 44, color=c.t['accent'], w=15, head=24)
    else:
        tray(c)
        c.line([(30, 74), (66, 74)], w=6)
        c.arrow_v(48, 10, 42, color=c.t['accent'], w=9, head=17)

The context works in the 0..96 space regardless of the output size, and supplies the primitives:

HelperWhat it draws
Ctx The canvas itself, in 0..96 coordinates, with line, circle, disc, rrect, poly and arrow_v.
tray, small_tray The open-top memory register, so "into" and "out of" both read.
spark The three-armed collision mark, always in accent.
dots Three ink discs: a list or a set of items.
plus An accent cross: add.
tilted_box A section box square to the objects rather than to the world; the tilt is the tool.
edge_mark A short accent line standing just outside one edge of a tilted box: the plane that edge represents.
eye Two arcs meeting at the corners, pupil left free for whatever the tool inspects.

Bundles are registered in the ICONS dictionary, keyed by the bundle path relative to extensions\pyNavis.extension\pyNavis.tab:

tools/make_icons.py
ICONS = {
    'Tools.panel/Smart_Clash_Grouper.pushbutton': ic_grouper,
    'Memory.panel/01_Memorize.pushbutton': ic_memorize,
    'Memory.panel/03_Set.stack/01_Add.pushbutton': ic_add,
    'Memory.panel/05_Memory.pulldown': ic_memory_menu,
}

Two smaller tables carry the stateful art. A *.toggle registers in TOGGLES with an (off drawing, on drawing) pair and gets icon.off(.dark).png and icon.on(.dark).png at 96 instead of a plain icon.png, plus small files rendered from the off state. A *.dockpane keeps its ordinary ICONS-style entry, drawn as the closed state, and adds one entry in DOCKPANE_ON for the on art: icon.on(.dark).png at 96 only, because a dockpane button never renders at the small size.

Create the folder first

write_all() hard-fails with a SystemExit listing every registered bundle whose folder does not exist, and writes nothing at all. Add the bundle folder before you add the ICONS entry, or the whole run aborts.

If you are not using the generator#

You do not have to. The generator is how the shipped set is made, not a requirement of the runtime. Any 96×96 and 32×32 PNGs with the four fixed names work, whatever produced them: Figma, Illustrator, Inkscape, an icon font export, or a screenshot if you insist.

What is worth borrowing is the visual language, because your extension sits on the same ribbon as the shipped one:

  • Transparent background. Never bake in white or grey; the ribbon background changes with the theme.
  • One accent colour, used for the verb. Two accents in one glyph and the eye has nothing to land on.
  • Match the ink weight. Roughly 8% of the canvas at 96, roughly 15% at 32. A hairline icon beside the shipped set reads as broken rather than as delicate.
  • Draw the small size separately. Export at 32 from a simplified artboard, not from the 96 one.
  • Check both themes. A dark grey glyph is invisible on a dark ribbon. Swapping ink to a near-white is usually the whole dark variant.

Checklist#

  • icon.png exists and is 96×96. Without it there is no icon at all.
  • All four files present, or exactly one. Partial sets fall back in ways you did not intend.
  • Square, transparent PNG.
  • The small pair is a simplified drawing, not a resize.
  • The dark pair is checked against a dark ribbon, not against a white page.
  • Accent colour marks the action, and red is reserved for destruction.
  • Reload after adding or changing a file; icons are read at ribbon build time.