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.
| File | Slot | Size |
|---|---|---|
icon.png |
Large button, light theme | 96×96 |
icon.dark.png |
Large button, dark theme | 96×96 |
icon.small.png |
Stack rows and pulldown menu rows, light theme | 32×32 |
icon.small.dark.png |
Stack rows and pulldown menu rows, dark theme | 32×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.
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:
var large = Find("icon.png");
return (large,
Find("icon.dark.png") ?? large,
Find("icon.small.png") ?? large,
Find("icon.small.dark.png") ?? large);icon.png and nothing else.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.
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:
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:
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.
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.
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.
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.
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:
| Constant | Value | Relative stroke | Used for |
|---|---|---|---|
WEIGHT_LARGE | 8 | 8 of 96, about 8% | The 96×96 files |
WEIGHT_SMALL | 14 | 14 of 96, about 15% | The 32×32 files |
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.
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 bundleThe contact sheet renders the whole set against light and dark plates at both densities, which is how you review a change without opening Navisworks.
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:
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:
| Helper | What 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:
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.
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.pngexists 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.