292 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			292 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
								 | 
							
								"""
							 | 
						||
| 
								 | 
							
								Generates the logic nodes reference page for Leenkx3D's wiki:
							 | 
						||
| 
								 | 
							
								https://github.com/leenkx3d/leenkx/wiki/reference
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								USAGE:
							 | 
						||
| 
								 | 
							
								    First, generate the node screenshots (1). After that, open a
							 | 
						||
| 
								 | 
							
								    terminal in the folder of this script and execute the following
							 | 
						||
| 
								 | 
							
								    command (Blender must have the Leenkx add-on activated of course):
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    path/to/blender.exe -b -P make_node_reference.py"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    This will create markdown files containing the reference in the
							 | 
						||
| 
								 | 
							
								    `/output` folder relative to this script. You can copy the content
							 | 
						||
| 
								 | 
							
								    from those files into the logic node reference articles. DO NOT
							 | 
						||
| 
								 | 
							
								    commit the generated files to the leenkx_tools repo!
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    Todo: Create a GitHub action to automatically update the reference
							 | 
						||
| 
								 | 
							
								    for each release.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    (1) https://github.com/leenkx3d/leenkx_wiki_images/blob/master/logic_nodes/make_screenshots.py
							 | 
						||
| 
								 | 
							
								        Please also read the usage notes in that file!
							 | 
						||
| 
								 | 
							
								"""
							 | 
						||
| 
								 | 
							
								import ensurepip
							 | 
						||
| 
								 | 
							
								import itertools
							 | 
						||
| 
								 | 
							
								import os
							 | 
						||
| 
								 | 
							
								import subprocess
							 | 
						||
| 
								 | 
							
								import sys
							 | 
						||
| 
								 | 
							
								from typing import List
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import bpy
							 | 
						||
| 
								 | 
							
								from nodeitems_utils import NodeItem
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								from lnx.logicnode import lnx_nodes
							 | 
						||
| 
								 | 
							
								import lnx.props
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								ensurepip.bootstrap()
							 | 
						||
| 
								 | 
							
								os.environ.pop("PIP_REQ_TRACKER", None)
							 | 
						||
| 
								 | 
							
								subprocess.check_output([sys.executable, '-m', 'pip', 'install', '--upgrade', 'markdownmaker'])
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# If pip wants an update, toggle this flag before execution
							 | 
						||
| 
								 | 
							
								UPDATE_PIP = False
							 | 
						||
| 
								 | 
							
								if UPDATE_PIP:
							 | 
						||
| 
								 | 
							
								    subprocess.check_output([sys.executable, '-m', 'pip', 'install', '--upgrade', 'pip'])
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								from markdownmaker.document import Document
							 | 
						||
| 
								 | 
							
								from markdownmaker.markdownmaker import *
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								PY_NODE_DIR = "https://github.com/leenkx3d/leenkx/blob/main/blender/lnx.logicnode/"
							 | 
						||
| 
								 | 
							
								HX_NODE_DIR = "https://github.com/leenkx3d/leenkx/blob/main/Sources/leenkx/logicnode/"
							 | 
						||
| 
								 | 
							
								IMG_DIR = "https://github.com/leenkx3d/leenkx_wiki_images/raw/master/logic_nodes/"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								OUTPUT_DIR = os.path.abspath(__file__)
							 | 
						||
| 
								 | 
							
								OUTPUT_DIR = os.path.join(os.path.dirname(OUTPUT_DIR), "output")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_anchor(text: str) -> str:
							 | 
						||
| 
								 | 
							
								    """Gets the GitHub anchor id for a link."""
							 | 
						||
| 
								 | 
							
								    return "#" + text.lower().replace(" ", "-")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def make_node_link(nodename: str) -> str:
							 | 
						||
| 
								 | 
							
								    """Create a link to a node given by the name of the node"""
							 | 
						||
| 
								 | 
							
								    return Link(label=InlineCode(nodename), url=get_anchor(nodename))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def get_nodetype(typename: str):
							 | 
						||
| 
								 | 
							
								    """Convert the type name to the actual type."""
							 | 
						||
| 
								 | 
							
								    return bpy.types.bpy_struct.bl_rna_get_subclass_py(typename)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def format_desc(description_text: str, *, indented=False) -> str:
							 | 
						||
| 
								 | 
							
								    """Format the raw description string."""
							 | 
						||
| 
								 | 
							
								    out = ""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Indentation for list items is 2 spaces per markdown standard in
							 | 
						||
| 
								 | 
							
								    # this case. For now, sub-lists are indented one level only, more
							 | 
						||
| 
								 | 
							
								    # is not required currently
							 | 
						||
| 
								 | 
							
								    line_start = "  " if indented else ""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Whether the last line was empty (ignore multiple empty lines)
							 | 
						||
| 
								 | 
							
								    last_empty = False
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for line in description_text.splitlines():
							 | 
						||
| 
								 | 
							
								        line = line.strip()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # List item. Explicitly check for space after "-", might be a negative number else
							 | 
						||
| 
								 | 
							
								        if line.startswith("- "):
							 | 
						||
| 
								 | 
							
								            out += "\n" + line_start + line
							 | 
						||
| 
								 | 
							
								            last_empty = False
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        elif line == "":
							 | 
						||
| 
								 | 
							
								            if last_empty:
							 | 
						||
| 
								 | 
							
								                continue
							 | 
						||
| 
								 | 
							
								            out += "\n"
							 | 
						||
| 
								 | 
							
								            last_empty = True
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            if last_empty:
							 | 
						||
| 
								 | 
							
								                out += "\n" + line_start + line  # Create a full empty line above the start of a paragraph
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                out += " " + line  # Combine one paragraph to a single line
							 | 
						||
| 
								 | 
							
								            last_empty = False
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Remove any left-over whitespace at the beginning/end
							 | 
						||
| 
								 | 
							
								    return out.strip()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def generate_node_documentation(doc: Document, nodeitem: NodeItem, category: lnx_nodes.LnxNodeCategory):
							 | 
						||
| 
								 | 
							
								    nodetype = get_nodetype(nodeitem.nodetype)
							 | 
						||
| 
								 | 
							
								    docstring: str = nodetype.__doc__
							 | 
						||
| 
								 | 
							
								    if docstring is not None:
							 | 
						||
| 
								 | 
							
								        doc_parts = docstring.split("@")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Show docstring until the first "@"
							 | 
						||
| 
								 | 
							
								        node_description = doc_parts[0].rstrip("\n")
							 | 
						||
| 
								 | 
							
								        node_description = format_desc(node_description)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        deprecation_note = Optional()
							 | 
						||
| 
								 | 
							
								        doc.add(deprecation_note)
							 | 
						||
| 
								 | 
							
								        doc.add(Paragraph(node_description))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        has_see = False
							 | 
						||
| 
								 | 
							
								        has_inputs = False
							 | 
						||
| 
								 | 
							
								        has_outputs = False
							 | 
						||
| 
								 | 
							
								        has_options = False
							 | 
						||
| 
								 | 
							
								        see_list = []
							 | 
						||
| 
								 | 
							
								        input_list = []
							 | 
						||
| 
								 | 
							
								        output_list = []
							 | 
						||
| 
								 | 
							
								        option_list = []
							 | 
						||
| 
								 | 
							
								        for part in doc_parts:
							 | 
						||
| 
								 | 
							
								            # Reference to other logic nodes
							 | 
						||
| 
								 | 
							
								            if part.startswith("seeNode "):
							 | 
						||
| 
								 | 
							
								                if not has_see:
							 | 
						||
| 
								 | 
							
								                    has_see = True
							 | 
						||
| 
								 | 
							
								                    doc.add(Paragraph(Bold("See also:")))
							 | 
						||
| 
								 | 
							
								                    doc.add(UnorderedList(see_list))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                see_list.append(Italic(make_node_link(part[8:].rstrip())))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            # General references
							 | 
						||
| 
								 | 
							
								            elif part.startswith("see "):
							 | 
						||
| 
								 | 
							
								                if not has_see:
							 | 
						||
| 
								 | 
							
								                    has_see = True
							 | 
						||
| 
								 | 
							
								                    doc.add(Paragraph(Bold("See also:")))
							 | 
						||
| 
								 | 
							
								                    doc.add(UnorderedList(see_list))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                see_list.append(Italic(part[4:].rstrip()))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Add node screenshot
							 | 
						||
| 
								 | 
							
								        image_file = IMG_DIR + category.name.lower() + "/" + nodeitem.nodetype + ".jpg"
							 | 
						||
| 
								 | 
							
								        doc.add(Image(url=image_file, alt_text=nodeitem.label + " node"))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        for part in doc_parts:
							 | 
						||
| 
								 | 
							
								            # Input sockets
							 | 
						||
| 
								 | 
							
								            if part.startswith("input "):
							 | 
						||
| 
								 | 
							
								                if not has_inputs:
							 | 
						||
| 
								 | 
							
								                    has_inputs = True
							 | 
						||
| 
								 | 
							
								                    doc.add(Paragraph(Bold("Inputs:")))
							 | 
						||
| 
								 | 
							
								                    doc.add(UnorderedList(input_list))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                socket_name, description = part[6:].split(":", 1)
							 | 
						||
| 
								 | 
							
								                description = format_desc(description, indented=True)
							 | 
						||
| 
								 | 
							
								                input_list.append(f"{InlineCode(socket_name)}: {description}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            # Output sockets
							 | 
						||
| 
								 | 
							
								            elif part.startswith("output "):
							 | 
						||
| 
								 | 
							
								                if not has_outputs:
							 | 
						||
| 
								 | 
							
								                    has_outputs = True
							 | 
						||
| 
								 | 
							
								                    doc.add(Paragraph(Bold("Outputs:")))
							 | 
						||
| 
								 | 
							
								                    doc.add(UnorderedList(output_list))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                socket_name, description = part[7:].split(":", 1)
							 | 
						||
| 
								 | 
							
								                description = format_desc(description, indented=True)
							 | 
						||
| 
								 | 
							
								                output_list.append(f"{InlineCode(socket_name)}: {description}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            # Other UI options
							 | 
						||
| 
								 | 
							
								            elif part.startswith("option "):
							 | 
						||
| 
								 | 
							
								                if not has_options:
							 | 
						||
| 
								 | 
							
								                    has_options = True
							 | 
						||
| 
								 | 
							
								                    doc.add(Paragraph(Bold("Options:")))
							 | 
						||
| 
								 | 
							
								                    doc.add(UnorderedList(option_list))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                option_name, description = part[7:].split(":", 1)
							 | 
						||
| 
								 | 
							
								                description = format_desc(description, indented=True)
							 | 
						||
| 
								 | 
							
								                option_list.append(f"{InlineCode(option_name)}: {description}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            elif part.startswith("deprecated "):
							 | 
						||
| 
								 | 
							
								                alternatives, message = part[11:].split(":", 1)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                message = " ".join(message.split()).replace("\n", "")
							 | 
						||
| 
								 | 
							
								                if not message.endswith(".") and not message == "":
							 | 
						||
| 
								 | 
							
								                    message += "."
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                links = []
							 | 
						||
| 
								 | 
							
								                for alternative in alternatives.split(","):
							 | 
						||
| 
								 | 
							
								                    if alternative == "":
							 | 
						||
| 
								 | 
							
								                        continue
							 | 
						||
| 
								 | 
							
								                    links.append(str(make_node_link(alternative)))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                if len(links) > 0:
							 | 
						||
| 
								 | 
							
								                    alternatives = f"Please use the following node(s) instead: {', '.join(links)}."
							 | 
						||
| 
								 | 
							
								                    message = alternatives + " " + message
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                deprecation_note.content = Quote(f"{Bold('DEPRECATED.')} This node is deprecated and will be removed in future versions of Leenkx. {message}")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Link to sources
							 | 
						||
| 
								 | 
							
								        node_file_py = "/".join(nodetype.__module__.split(".")[2:]) + ".py"
							 | 
						||
| 
								 | 
							
								        node_file_hx = nodetype.bl_idname[2:] + ".hx"  # Discard LN prefix
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        pylink = Link(label="Python", url=PY_NODE_DIR + node_file_py)
							 | 
						||
| 
								 | 
							
								        hxlink = Link(label="Haxe", url=HX_NODE_DIR + node_file_hx)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        doc.add(Paragraph(f"{Bold('Sources:')} {pylink} | {hxlink}"))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def build_page(section_name: str = ""):
							 | 
						||
| 
								 | 
							
								    is_mainpage = section_name == ""
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    doc = Document()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    doc.add(Header("Logic Nodes Reference" + ("" if is_mainpage else f": {Italic(section_name.capitalize())} nodes")))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    doc.add(Paragraph(Italic(
							 | 
						||
| 
								 | 
							
								        "This reference was generated automatically. Please do not edit the"
							 | 
						||
| 
								 | 
							
								        " page directly, instead change the docstrings of the nodes in their"
							 | 
						||
| 
								 | 
							
								        f" {Link(label='Python files', url='https://github.com/leenkx3d/leenkx/tree/main/blender/lnx.logicnode')}"
							 | 
						||
| 
								 | 
							
								        f" or the {Link(label='generator script', url='https://github.com/leenkx3d/leenkx_tools/blob/main/mkdocs/make_node_reference.py')}"
							 | 
						||
| 
								 | 
							
								        f" and {Link(label='open a pull request', url='https://github.com/leenkx3d/leenkx/wiki/contribute#creating-a-pull-request')}."
							 | 
						||
| 
								 | 
							
								        " Thank you for contributing!")))
							 | 
						||
| 
								 | 
							
								    doc.add(Paragraph(Italic(f"This reference was built for {Bold(f'Leenkx {lnx.props.lnx_version}')}.")))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    doc.add(HorizontalRule())
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    with HeaderSubLevel(doc):
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Table of contents
							 | 
						||
| 
								 | 
							
								        doc.add(Header("Node Categories"))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        category_items: List[Node] = []
							 | 
						||
| 
								 | 
							
								        for section, section_categories in lnx_nodes.category_items.items():
							 | 
						||
| 
								 | 
							
								            # Ignore empty sections ("default" e.g)
							 | 
						||
| 
								 | 
							
								            if len(section_categories) > 0:
							 | 
						||
| 
								 | 
							
								                section_title = Bold(section.capitalize())
							 | 
						||
| 
								 | 
							
								                if section_name == section:
							 | 
						||
| 
								 | 
							
								                    # Highlight current page
							 | 
						||
| 
								 | 
							
								                    section_title = Italic(section_title)
							 | 
						||
| 
								 | 
							
								                category_items.append(section_title)
							 | 
						||
| 
								 | 
							
								                url = f"https://github.com/leenkx3d/leenkx/wiki/reference_{section}"
							 | 
						||
| 
								 | 
							
								                category_items.append(UnorderedList([Link(c.name, url + get_anchor(c.name)) for c in section_categories]))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        doc.add(UnorderedList(category_items))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Page content
							 | 
						||
| 
								 | 
							
								        if not is_mainpage:
							 | 
						||
| 
								 | 
							
								            for category in lnx_nodes.category_items[section_name]:
							 | 
						||
| 
								 | 
							
								                doc.add(Header(category.name))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                if category.description != "":
							 | 
						||
| 
								 | 
							
								                    doc.add(Paragraph(category.description))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                with HeaderSubLevel(doc):
							 | 
						||
| 
								 | 
							
								                    # Sort nodes alphabetically and discard section order
							 | 
						||
| 
								 | 
							
								                    iterator = itertools.chain(category.get_all_nodes(), category.deprecated_nodes)
							 | 
						||
| 
								 | 
							
								                    for nodeitem in sorted(iterator, key=lambda n: n.label):
							 | 
						||
| 
								 | 
							
								                        doc.add(Header(nodeitem.label))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                        generate_node_documentation(doc, nodeitem, category)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    filename = "reference.md" if is_mainpage else f"reference_{section_name}.md"
							 | 
						||
| 
								 | 
							
								    with open(os.path.join(OUTPUT_DIR, filename), "w") as out_file:
							 | 
						||
| 
								 | 
							
								        out_file.write(doc.write())
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def run():
							 | 
						||
| 
								 | 
							
								    print("Generating documentation...")
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if not os.path.exists(OUTPUT_DIR):
							 | 
						||
| 
								 | 
							
								        os.mkdir(OUTPUT_DIR)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Main page
							 | 
						||
| 
								 | 
							
								    build_page()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Section sub-pages
							 | 
						||
| 
								 | 
							
								    for section_name in lnx_nodes.category_items.keys():
							 | 
						||
| 
								 | 
							
								        if section_name == 'default':
							 | 
						||
| 
								 | 
							
								            continue
							 | 
						||
| 
								 | 
							
								        build_page(section_name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								if __name__ == "__main__":
							 | 
						||
| 
								 | 
							
								    run()
							 |