Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update node documentation #612

Merged
merged 9 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/example_animations.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ To morph between 3 conformations of ATP synthase use ChimeraX commands "open 6n2
::: callout-caution
# Requires MDAnalysis Installed

To follow this particular tutorial, ensure you have first [installed Molecular Nodes properly](installation.md), including the optional MDAnalysis python package.
To follow this particular tutorial, ensure you have first [installed Molecular Nodes properly](installation.qmd), including the optional MDAnalysis python package.
:::

Download the trajectory files from the the [CHARMM-GUI website](https://charmm-gui.org/?doc=archive&lib=covid19):
Expand Down
14 changes: 7 additions & 7 deletions docs/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@
if submenu.description:
file.write(submenu.description)
file.write("\n\n")
for item in submenu.items:
if item.is_break:
for menu_item in submenu.items:
if menu_item.is_break:
continue
if item.backup is not None:
name = item.backup
if menu_item.backup is not None:
name = menu_item.backup
else:
name = item.name
doc = noodlenotes.MenuItemDocumenter(item)
name = menu_item.name
documenter = noodlenotes.MenuItemDocummenter(menu_item)

file.write(doc.as_markdown())
file.write(documenter.as_markdown())
file.write("\n\n")
3 changes: 2 additions & 1 deletion docs/nodes/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ At any point you can view the internals of the node groups that are included wit
With a node selected, you can <kbd>Tab</kbd> to enter a selected node, and use <kbd>Ctrl</kbd> + <kbd>Tab</kbd> to exit the node group.
You can also do the same by right clicking.
Feel free to tweak the internals of node groups, but beware that changing one node group will change that node group for all othe uses of it.
Starting a new Blender session will ensure that the node groups are fresh and 'factor reset'.
Starting a new Blender session will ensure that the node groups are fresh and 'factor reset'.

2 changes: 1 addition & 1 deletion docs/noodlenotes/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .documenter import TreeDocumenter, MenuItemDocumenter
from .documenter import MenuItemDocummenter, Documenter
65 changes: 42 additions & 23 deletions docs/noodlenotes/documenter.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import bpy
import pathlib
import sys

from .interface import InterfaceGroup, InterfaceItem
from . import markdown
from typing import List

TOP_FOLDER = pathlib.Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(TOP_FOLDER))

class TreeDocumenter:
def __init__(
self, tree: bpy.types.NodeTree, description: str = None, image: str = None
) -> None:
from molecularnodes.ui.menu import Menu, MenuItem, CustomItem, Item, Break


class Documenter:
def __init__(self, tree: bpy.types.NodeTree, menu_item: MenuItem = None) -> None:
self.tree = tree
self.items = [InterfaceItem(x) for x in tree.interface.items_tree]
self.inputs = InterfaceGroup([x for x in self.items if x.is_input])
self.outputs = InterfaceGroup([x for x in self.items if x.is_output])
self._description = description
self.image = markdown.Video(image)
self.menu_item = menu_item
self.level = 2

@property
Expand All @@ -24,20 +29,37 @@ def title(self) -> str:
return f"## {self.tree.name.removesuffix('_')}"

def description(self) -> str:
if self._description:
return self._description + "\n\n" + self.tree.description
return self.menu_item.description + "\n\n" + self.tree.description

def videos(self) -> List[str]:
links = self.menu_item.videos

if links is None:
return None

for x in links:
if x is None:
return None

if isinstance(links, str):
links = [links]

if not all([isinstance(x, str) for x in links]):
raise ValueError(f"All url values must be strings: {links=}")

videos = "\n\n".join(
[markdown.Video(x).as_markdown() for x in links if x is not None]
)

return self.tree.description
return "\n\n" + videos + "\n\n"

def collect_items(self):
items = [
self.title(),
self.description(),
self.image.as_markdown(),
"### Inputs",
self.inputs.as_markdown(),
"### Outputs",
self.outputs.as_markdown(),
self.videos(),
self.inputs.as_markdown("Inputs"),
self.outputs.as_markdown("Outputs"),
]
return [item for item in items if item is not None]

Expand All @@ -47,14 +69,11 @@ def as_markdown(self) -> str:
return text


def MenuItemDocumenter(menu_item) -> TreeDocumenter:
if menu_item.backup:
tree = bpy.data.node_groups[menu_item.backup]
else:
tree = bpy.data.node_groups[menu_item.name]
class MenuItemDocummenter(Documenter):
def __init__(self, menu_item: MenuItem) -> None:
super().__init__(tree=menu_item.tree, menu_item=menu_item)

doc = TreeDocumenter(
tree=tree, description=menu_item.description, image=menu_item.video_url
)

return doc
class TreeDocumenter(Documenter):
def __init__(self, tree: bpy.types.NodeTree) -> None:
super().__init__(tree=tree)
40 changes: 27 additions & 13 deletions docs/noodlenotes/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ def type(self) -> str:
return "PANEL"
return "`{}`".format(self.item.socket_type.replace("NodeSocket", ""))

@property
def is_vector(self) -> bool:
return self.type in ["Vector", "Color", "Rotation", "Matrix"]

def __len__(self) -> int:
if self.type == "PANEL":
return 0
elif self.type in ["Vector", "Rotation"]:
return 3
elif self.type in ["Color"]:
return 4
elif self.type == "Matrix":
return 16
else:
return 1

@property
def default(self, round_length: int = 3) -> str:
try:
Expand Down Expand Up @@ -89,19 +105,9 @@ def description(self):
return ""

def max_length(self):
info_to_test = [self.description, self.min, self.max, self.default]
info_to_test = [self.description, self.min, self.max, self.default, self.type]
return max([len(x) for x in info_to_test if x is not None])

def formatted(self):
text = f"Default Value: {self.default}\n"
try:
text += f"Min: {self.min}\n"
text += f"Max: {self.max}\n"
except AttributeError:
pass

return text


class InterfaceGroup:
def __init__(self, items: List[InterfaceItem]) -> None:
Expand Down Expand Up @@ -144,8 +150,16 @@ def body(self) -> str:
def tail(self) -> str:
return '\n\n: {tbl-colwidths="[15, 10, 55, 20]"}\n\n'

def as_markdown(self):
return self.top_line() + self.sep() + self.body() + self.tail() + "\n"
def as_markdown(self, title: str = "", level: int = 3):
body = self.body()
if not body:
return ""
hashes = "#" * level
lines = f"{hashes} {title}\n\n"
for x in [self.top_line(), self.sep(), self.body(), self.tail(), "\n"]:
lines += x

return lines

def __repr__(self) -> str:
return self.as_markdown()
2 changes: 1 addition & 1 deletion docs/noodlenotes/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def __init__(self, url: str, caption: str = "") -> None:

def as_markdown(self) -> str:
if not self.url or self.url == "":
return None
return "![]()"
return f"![{self.caption}]({self.format_url()})"

def format_url(self) -> str:
Expand Down
Binary file modified molecularnodes/assets/MN_data_file_4.2.blend
Binary file not shown.
149 changes: 149 additions & 0 deletions molecularnodes/ui/menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
from typing import List, Union
import bpy


class Item:
def __init__(self) -> None:
self.is_break = False
self.is_custom = False
self.backup: str = None

@classmethod
def menu(
self, layout: bpy.types.UILayout, context: bpy.types.Context = None
) -> None:
pass

@property
def node_name(self) -> str:
if hasattr(self, "backup"):
return self.backup
else:
return self.name

@property
def tree(self) -> bpy.types.GeometryNodeGroup:
if self.name.startswith("mn."):
return bpy.data.node_groups[self.backup]
return bpy.data.node_groups[self.name]


class MenuItem(Item):
def __init__(
self,
name: str,
label: str = None,
description: str = None,
videos: str = None,
backup: str = None,
) -> None:
super().__init__()
self.name = name
self.label = label
if self.label is None:
self.label = self.name
self.description = description
self.videos = videos
self.backup = backup

def short_description(self):
return self.description.split("\n")[0].removesuffix(".")

def menu(
self, layout: bpy.types.UILayout, context: bpy.types.Context = None
) -> None:
if self.label is None:
self.label = self.name

if self.name.startswith("mn."):
layout.operator(self.name)
return None

op = layout.operator("mn.add_custom_node_group", text=self.label)
op.node_label = self.label
op.node_name = self.name
op.node_description = self.description
op.node_link = False


class CustomItem(Item):
def __init__(
self,
label: str,
field: str,
dtype: str,
name: str,
prefix: str,
property_id: str,
description: str,
videos: str = None,
) -> None:
super().__init__()
self.label = label
self.field = field
self.dtype = dtype
self.name = name
self.prefix = prefix
self.property_id = property_id
self.description = description
self.videos = videos
self.is_custom = True

def menu(
self, layout: bpy.types.UILayout, context: bpy.types.Context = None
) -> None:
row = layout.row()
op = row.operator("mn.iswitch_custom", text=self.label)
op.field = self.field
op.dtype = self.dtype
op.prefix = self.prefix
op.node_property = self.property_id
op.node_name = self.label

if self.dtype == "RGBA":
op.description = f"Choose custom colors for {self.label}"
elif self.dtype == "BOOLEAN":
op.description = f"Choose custom selections for {self.label}"
else:
raise ValueError(f"Data type currently not supported: {self.dtype}")
row.enabled = bool(context.active_object.get(self.property_id))


class Break:
def __init__(self, text: str = None) -> None:
super().__init__()
self.is_break = True
self.text = text

def menu(
self, layout: bpy.types.UILayout, context: bpy.types.Context = None
) -> None:
layout.separator()
# optionally we can add a subtitle for the next section of nodes
if self.text and self.text.strip() != "":
layout.label(text=self.text)


class Submenu:
def __init__(self, name, items, title: str = None, description: str = None) -> None:
self.name: str = name
self.items: List[Union[MenuItem, Break, CustomItem]] = items
self.title = title
self.description = description

def node_names(self):
return [item.name for item in self.items if not isinstance(item, Break)]

def menu(self, layout: bpy.types.UILayout, context: bpy.types.Context):
for item in self.items:
item.menu(layout=layout, context=context)


class Menu:
def __init__(self, submenus: List[Submenu]) -> None:
self.submenus = submenus

def get_submenu(self, name: str) -> Submenu:
for sub in self.submenus:
if sub.name == name:
return sub
Loading
Loading