Compare commits

..

3 Commits

Author SHA1 Message Date
Jonas Holzman 9940a9c8ca ruff format 2025-03-19 17:40:17 +01:00
Jonas Holzman 30ae14f9bb Cleanup init and remove old unused files 2025-03-19 17:36:52 +01:00
Jonas Holzman bc19b0cc6d Apply git template and update README 2025-03-19 17:08:30 +01:00
22 changed files with 670 additions and 711 deletions

View File

@ -0,0 +1,5 @@
${CommitTitle}
${CommitBody}
Pull Request: https://git.autourdeminuit.com/${BaseRepoOwnerName}/${BaseRepoName}/pulls/${PullRequestIndex}

View File

@ -0,0 +1,3 @@
${PullRequestTitle}
Pull Request: https://git.autourdeminuit.com/${BaseRepoOwnerName}/${BaseRepoName}/pulls/${PullRequestIndex}

View File

@ -0,0 +1,22 @@
name: Bug Report
about: Use Help > Report a Bug from the top of Blender to automatically fill out part of this form.
labels:
- "Type/Bug"
body:
- type: textarea
id: body
attributes:
label: "Description"
hide_label: true
value: |
**System Information**
Operating system:
Graphics card:
**Software Version**
Broken: (example: 3.0.0, main, `f1cca3055776`, 2020-12-30)
Worked: (newest version that worked as expected)
**Short description of error**
**Exact steps for others to reproduce the error**

View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -0,0 +1,10 @@
name: Design
about: Create a design task (for developers only)
labels:
- "Type/Design"
body:
- type: textarea
id: body
attributes:
label: "Description"
hide_label: true

View File

@ -0,0 +1,10 @@
name: To Do
about: Create a to do task (for developers only)
labels:
- "Type/To Do"
body:
- type: textarea
id: body
attributes:
label: "Description"
hide_label: true

View File

@ -0,0 +1,8 @@
name: Pull Request
about: Code contribution
body:
- type: textarea
id: body
attributes:
label: "Description"
hide_label: true

137
.gitignore vendored
View File

@ -1,138 +1,17 @@
# Byte-compiled / optimized / DLL files # Python Artifacts
__pycache__ __pycache__
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
# Python virtualenvs
.venv/
.env/
# Editor config files
.idea/
*.code-workspace *.code-workspace
*.vscode *.vscode
test_*
# C extensions # Poetry build artifacts
*.so
# Distribution / packaging
/dist/
/release/
.Python
#build/
develop-eggs/
dist/ dist/
downloads/
eggs/
.eggs/
#lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
_build/
Makefile
make.bat
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
#env/
venv/
#ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/

View File

@ -1,51 +0,0 @@
name: Bug Report
about: File a bug report
title: "[Bug]: "
body:
- id : ticket_type
type: dropdown
attributes:
label: Type
description: Type du ticket
options:
- bug
- feature
- maintenance
- documentation
validation:
required: true
- id : priority
type: dropdown
attributes:
label: Priorité
description: priorité du ticket
options:
- normal
- faible
- élevée
- critique
- id: description
type: textarea
attributes:
label: Description
description: Description du problème rencontré
value: "ça marche pô... \nN'oubliez pas de préciser la task, l'asset ou le shot, et sa version dans le cas d'un bug"
validations:
required: true
- id: path
type: input
attributes:
label: filepath
description: le chemin de la scene s'il existe
- id: log
type: textarea
attributes:
label: Log console
description: Rensigner le crash log s'il existe

View File

@ -1,16 +0,0 @@
## Related ticket
Link to the ticket
## Modifications
Description of what your PR is doing
## How to test it
Description of how to test it
## Risk involved
- LOW
- MID
- HIGH
## Related PR
List of PR involved with this one

View File

@ -1,32 +1,23 @@
# NODE_KIT # Node Kit
> Blender addon to handle some operations on nodes Blender Node Toolkit - Collection of node-related tools (copy-pasting, packing, updating, etc..) all accessible from a simple menu.
Node kit is a blender addon creating a menu to execute operations on nodes like copy/pasting or exporting node_tree ## Development
### Cloning
<!-- TABLE OF CONTENTS --> 1. Create a development directory, such as:
***
<summary>Table of Contents</summary>
<ol>
<li>
<a href="#installation">Installation</a>
</li>
<li>
<a href="#contents">Contents</a>
</li>
</ol>
<!-- INSTALLATION -->
## Installation
1. Create your own local directory in
```sh ```sh
/home/<USER>/dev /home/<USER>/dev
``` ```
2. Create your own local directory in 2. Clone the repository with the following command:
```sh ```sh
git clone ssh://git@git.autourdeminuit.com:222/autour_de_minuit/node_kit.git git clone ssh://git@git.autourdeminuit.com:222/autour_de_minuit/node_kit.git
``` ```
<!-- CONTENTS --> ### Branches
## Contents The main branch is **master**.
It is a **protected branch** that only user with the `Owner` role can push to.
To contribute to this repository, you will have to create you own development branch, and then create a Pull Request. After at least **one validation**, you will be able to merge your changes.
Branches should be named in `kebab-case`, and can be grouped using `/`.
Examples: `bug/fix-crash-on-startup`, `bug/fix-font`, `feature/new-colors`, `theme-logic-refactor`, etc...

View File

@ -1,14 +1,13 @@
bl_info = { bl_info = {
"name": "Node Kit", "name": "Node Kit",
"author": "Florentin Luce", "author": "Florentin Luce, Christophe Seux, Jonas Holzman",
"version": (0, 1), "version": (0, 1),
"blender": (4, 0, 2), "blender": (4, 3, 2),
"category": "Node"} "location": "Node Editor -> Node Kit",
"description": "Collection of node-related tools",
"doc_url": "https://git.autourdeminuit.com/autour_de_minuit/node_kit",
import sys "category": "Node",
import importlib }
from pathlib import Path
from . import ui, operators from . import ui, operators
@ -19,15 +18,7 @@ modules = (
) )
if "bpy" in locals():
import importlib
for mod in modules:
importlib.reload(mod)
def register(): def register():
print('Register Node kit')
for mod in modules: for mod in modules:
mod.register() mod.register()

View File

View File

@ -1,4 +1,3 @@
import bpy import bpy
from mathutils import Color, Vector from mathutils import Color, Vector
@ -9,7 +8,6 @@ class Node:
"""Blender Node abstraction.""" """Blender Node abstraction."""
def __init__(self, bl_node, parent): def __init__(self, bl_node, parent):
self.bl_node = bl_node self.bl_node = bl_node
self.tree = parent self.tree = parent
self.id = hex(id(self.bl_node)) self.id = hex(id(self.bl_node))
@ -67,12 +65,12 @@ class Node:
self._parent = value self._parent = value
# Node id case # Node id case
elif isinstance(value, str) and value.startswith('0x'): elif isinstance(value, str) and value.startswith("0x"):
for node in self.tree.nodes: for node in self.tree.nodes:
if node.id == value: if node.id == value:
self._parent = node self._parent = node
else: else:
print('Cannot find parent') print("Cannot find parent")
# blender node case # blender node case
elif isinstance(value, bpy.types.Node): elif isinstance(value, bpy.types.Node):
@ -94,10 +92,10 @@ class Node:
Returns: Returns:
Node: Node abstract according to the blender node type. Node: Node abstract according to the blender node type.
""" """
if bl_node.bl_idname == 'CompositorNodeRLayers': if bl_node.bl_idname == "CompositorNodeRLayers":
return RenderLayersNode(bl_node, tree) return RenderLayersNode(bl_node, tree)
elif bl_node.bl_idname == 'CompositorNodeValToRGB': elif bl_node.bl_idname == "CompositorNodeValToRGB":
return ColorRampNode(bl_node, tree) return ColorRampNode(bl_node, tree)
else: else:
@ -115,19 +113,23 @@ class Node:
Node: Create abstract node. Node: Create abstract node.
""" """
new_bl_node = tree.bl_node_tree.nodes.new(type=data['bl_idname']) new_bl_node = tree.bl_node_tree.nodes.new(type=data["bl_idname"])
node = cls.from_blender_node(new_bl_node, tree) node = cls.from_blender_node(new_bl_node, tree)
node.id = data['id'] node.id = data["id"]
for p in node.parameters: for p in node.parameters:
setattr(node, p, data[p]) setattr(node, p, data[p])
# set attribute on the blender node only if correct type is retrieve # set attribute on the blender node only if correct type is retrieve
if p not in ('parent', 'scene'): if p not in ("parent", "scene"):
setattr(node.bl_node, p, getattr(node, p)) setattr(node.bl_node, p, getattr(node, p))
node.inputs = [Input.from_dict(ipt_data, node) for ipt_data in data['inputs'].values()] node.inputs = [
node.outputs = [Output.from_dict(opt_data, node) for opt_data in data['outputs'].values()] Input.from_dict(ipt_data, node) for ipt_data in data["inputs"].values()
]
node.outputs = [
Output.from_dict(opt_data, node) for opt_data in data["outputs"].values()
]
return node return node
def dump(self): def dump(self):
@ -138,7 +140,6 @@ class Node:
""" """
for prop_id in self.parameters: for prop_id in self.parameters:
if not hasattr(self, prop_id): if not hasattr(self, prop_id):
continue continue
@ -154,9 +155,9 @@ class Node:
self.data[prop_id] = attr_value self.data[prop_id] = attr_value
self.data['id'] = self.id self.data["id"] = self.id
self.data['inputs'] = {ipt.id: ipt.dump() for ipt in self.inputs} self.data["inputs"] = {ipt.id: ipt.dump() for ipt in self.inputs}
self.data['outputs'] = {opt.id: opt.dump() for opt in self.outputs} self.data["outputs"] = {opt.id: opt.dump() for opt in self.outputs}
return self.data return self.data
@ -195,7 +196,6 @@ class Link:
"""Blender Link abstraction.""" """Blender Link abstraction."""
def __init__(self, bl_link, parent): def __init__(self, bl_link, parent):
self.bl_link = bl_link self.bl_link = bl_link
self.tree = parent self.tree = parent
self.id = hex(id(self.bl_link)) self.id = hex(id(self.bl_link))
@ -206,7 +206,6 @@ class Link:
self.data = {} self.data = {}
def dump(self): def dump(self):
self.data["id"] = self.id
self.data['id'] = self.id
return self.data return self.data

View File

@ -8,7 +8,6 @@ class NodeTree:
"""Blender node tree abstraction.""" """Blender node tree abstraction."""
def __init__(self, bl_node_tree): def __init__(self, bl_node_tree):
self.bl_node_tree = bl_node_tree self.bl_node_tree = bl_node_tree
self.data = {} self.data = {}
@ -28,8 +27,12 @@ class NodeTree:
Returns: Returns:
dict: Nodes and links as dict. dict: Nodes and links as dict.
""" """
self.data['nodes'] = {n.id: n.dump() for n in self.nodes if not select_only or (select_only and n.select)} self.data["nodes"] = {
self.data['links'] = [l.id for l in self.links] n.id: n.dump()
for n in self.nodes
if not select_only or (select_only and n.select)
}
self.data["links"] = [l.id for l in self.links]
return self.data return self.data
@ -46,8 +49,7 @@ class NodeTree:
self.data = data self.data = data
for node_id, node_data in self.data['nodes'].items(): for node_id, node_data in self.data["nodes"].items():
new_node = Node.from_dict(node_data, self) new_node = Node.from_dict(node_data, self)
self.nodes.append(new_node) self.nodes.append(new_node)
@ -55,15 +57,15 @@ class NodeTree:
for ipt in new_node.inputs: for ipt in new_node.inputs:
if ipt.is_linked: if ipt.is_linked:
connections.setdefault(ipt.link, {})['input'] = ipt.bl_input connections.setdefault(ipt.link, {})["input"] = ipt.bl_input
for opt in new_node.outputs: for opt in new_node.outputs:
if opt.is_linked: if opt.is_linked:
for link in opt.link: for link in opt.link:
connections.setdefault(link, {})['output'] = opt.bl_output connections.setdefault(link, {})["output"] = opt.bl_output
for link_id in self.data['links']: for link_id in self.data["links"]:
ipt = connections[link_id]['input'] ipt = connections[link_id]["input"]
opt = connections[link_id]['output'] opt = connections[link_id]["output"]
self.bl_node_tree.links.new(ipt, opt) self.bl_node_tree.links.new(ipt, opt)

View File

@ -1,9 +1,5 @@
class Socket: class Socket:
def __init__(self, bl_socket, tree): def __init__(self, bl_socket, tree):
self.tree = tree self.tree = tree
self.bl_socket = bl_socket self.bl_socket = bl_socket
self.data = {} self.data = {}
@ -14,12 +10,11 @@ class Socket:
self._value = None self._value = None
if hasattr(bl_socket, 'default_value'): if hasattr(bl_socket, "default_value"):
self._value = bl_socket.default_value self._value = bl_socket.default_value
@property @property
def value(self): def value(self):
if not isinstance(self._value, (str, int, float, bool)): if not isinstance(self._value, (str, int, float, bool)):
self._value = [v for v in self._value] self._value = [v for v in self._value]
@ -32,16 +27,15 @@ class Socket:
return self._value return self._value
def to_dict(self): def to_dict(self):
self.data['id'] = self.id self.data["id"] = self.id
self.data['value'] = self.value self.data["value"] = self.value
self.data['identifier'] = self.identifier self.data["identifier"] = self.identifier
self.data['is_linked'] = self.is_linked self.data["is_linked"] = self.is_linked
self.data['link'] = self.get_link() self.data["link"] = self.get_link()
return self.data return self.data
class Input(Socket): class Input(Socket):
def __init__(self, bl_input, tree): def __init__(self, bl_input, tree):
super().__init__(bl_input, tree) super().__init__(bl_input, tree)
@ -49,10 +43,8 @@ class Input(Socket):
@classmethod @classmethod
def from_dict(cls, data, node): def from_dict(cls, data, node):
for bl_ipt in node.bl_node.inputs: for bl_ipt in node.bl_node.inputs:
if bl_ipt.identifier != data["identifier"]:
if bl_ipt.identifier != data['identifier']:
continue continue
new_ipt = cls(bl_ipt, node.tree) new_ipt = cls(bl_ipt, node.tree)
@ -63,7 +55,6 @@ class Input(Socket):
return new_ipt return new_ipt
def get_link(self): def get_link(self):
if not self.is_linked: if not self.is_linked:
return None return None
@ -74,7 +65,6 @@ class Input(Socket):
class Output(Socket): class Output(Socket):
def __init__(self, bl_output, tree): def __init__(self, bl_output, tree):
super().__init__(bl_output, tree) super().__init__(bl_output, tree)
@ -82,10 +72,8 @@ class Output(Socket):
@classmethod @classmethod
def from_dict(cls, data, node): def from_dict(cls, data, node):
for bl_opt in node.bl_node.outputs: for bl_opt in node.bl_node.outputs:
if bl_opt.identifier != data["identifier"]:
if bl_opt.identifier != data['identifier']:
continue continue
new_opt = cls(bl_opt, node.tree) new_opt = cls(bl_opt, node.tree)
@ -96,7 +84,6 @@ class Output(Socket):
return new_opt return new_opt
def get_link(self): def get_link(self):
links = [] links = []
if not self.is_linked: if not self.is_linked:

View File

@ -7,13 +7,12 @@ from copy import copy
from os.path import abspath from os.path import abspath
def get_default(prop): def get_default(prop):
"""Get the default value of a bl property""" """Get the default value of a bl property"""
if getattr(prop, 'is_array', False): if getattr(prop, "is_array", False):
return list(prop.default_array) return list(prop.default_array)
elif hasattr(prop, 'default'): elif hasattr(prop, "default"):
return prop.default return prop.default
@ -28,7 +27,7 @@ def get_dumper(bl_object, fallback=None):
def get_bl_object(data): def get_bl_object(data):
"""Find the bl object for loading data into it depending on the type and the context""" """Find the bl object for loading data into it depending on the type and the context"""
if data.get('_new', {}).get('type') == 'GeometryNodeTree': if data.get("_new", {}).get("type") == "GeometryNodeTree":
return bpy.context.object.modifiers.active.node_group return bpy.context.object.modifiers.active.node_group
@ -71,19 +70,23 @@ def set_attribute(bl_object, attr, value):
class Dumper: class Dumper:
pointers = {} pointers = {}
includes = [] includes = []
excludes = ["rna_type", "bl_rna", 'id_data', 'depsgraph'] excludes = ["rna_type", "bl_rna", "id_data", "depsgraph"]
@classmethod @classmethod
def properties(cls, bl_object): def properties(cls, bl_object):
if cls.includes and not cls.excludes: if cls.includes and not cls.excludes:
return [bl_object.bl_rna.properties[p] for p in cls.includes] return [bl_object.bl_rna.properties[p] for p in cls.includes]
else: else:
return [ p for p in bl_object.bl_rna.properties if not return [
p.identifier.startswith('bl_') and p.identifier not in cls.excludes] p
for p in bl_object.bl_rna.properties
if not p.identifier.startswith("bl_")
and p.identifier not in cls.excludes
]
@classmethod @classmethod
def new(cls, data): def new(cls, data):
print(f'New not implemented for data {data}') print(f"New not implemented for data {data}")
@classmethod @classmethod
def load(cls, data, bl_object=None): def load(cls, data, bl_object=None):
@ -94,33 +97,35 @@ class Dumper:
return return
# pprint(data) # pprint(data)
if bl_pointer := data.get('bl_pointer'): if bl_pointer := data.get("bl_pointer"):
cls.pointers[bl_pointer] = bl_object cls.pointers[bl_pointer] = bl_object
props = cls.properties(bl_object) props = cls.properties(bl_object)
for key, value in sorted(data.items(), key=lambda x: props.index(x[0]) if x[0] in props else 0): for key, value in sorted(
if key.startswith('_') or key not in bl_object.bl_rna.properties: data.items(), key=lambda x: props.index(x[0]) if x[0] in props else 0
):
if key.startswith("_") or key not in bl_object.bl_rna.properties:
continue continue
prop = bl_object.bl_rna.properties[key] prop = bl_object.bl_rna.properties[key]
attr = getattr(bl_object, key) attr = getattr(bl_object, key)
if prop.type == 'COLLECTION': if prop.type == "COLLECTION":
dumper = PropCollection dumper = PropCollection
if hasattr(attr, 'bl_rna'): if hasattr(attr, "bl_rna"):
bl_type = attr.bl_rna.type_recast() bl_type = attr.bl_rna.type_recast()
dumper = get_dumper(bl_type, fallback=PropCollection) dumper = get_dumper(bl_type, fallback=PropCollection)
dumper.load(value, attr) dumper.load(value, attr)
continue continue
elif prop.type == 'POINTER': elif prop.type == "POINTER":
# if key == 'node_tree': # if key == 'node_tree':
# print('--------------') # print('--------------')
# print(bl_object, value) # print(bl_object, value)
# print(cls.pointers) # print(cls.pointers)
if isinstance(value, int): # It's a pointer if isinstance(value, int): # It's a pointer
if value not in cls.pointers: if value not in cls.pointers:
print(bl_object, "not loaded yet", prop) print(bl_object, "not loaded yet", prop)
value = cls.pointers[value] value = cls.pointers[value]
@ -141,9 +146,9 @@ class Dumper:
dumper.load(value, attr) dumper.load(value, attr)
# attr = getattr(bl_object, key) # attr = getattr(bl_object, key)
# if not attr: # if not attr:
cls.pointers[value['bl_pointer']] = attr cls.pointers[value["bl_pointer"]] = attr
if hasattr(attr, 'update'): if hasattr(attr, "update"):
attr.update() attr.update()
value = attr value = attr
@ -152,7 +157,7 @@ class Dumper:
set_attribute(bl_object, key, value) set_attribute(bl_object, key, value)
# Some coll needs a manual update like curve mapping # Some coll needs a manual update like curve mapping
if hasattr(attr, 'update'): if hasattr(attr, "update"):
attr.update() attr.update()
elif not prop.is_readonly: elif not prop.is_readonly:
@ -172,10 +177,9 @@ class Dumper:
data = {"bl_pointer": bl_object.as_pointer()} data = {"bl_pointer": bl_object.as_pointer()}
cls.pointers[bl_object.as_pointer()] = bl_object cls.pointers[bl_object.as_pointer()] = bl_object
for prop in cls.properties(bl_object): for prop in cls.properties(bl_object):
if not hasattr(bl_object, prop.identifier): if not hasattr(bl_object, prop.identifier):
print(f'{bl_object} has no attribute {prop.identifier}') print(f"{bl_object} has no attribute {prop.identifier}")
continue continue
# print(prop.identifier) # print(prop.identifier)
@ -184,7 +188,7 @@ class Dumper:
# Not storing default value # Not storing default value
if prop.identifier not in cls.includes: if prop.identifier not in cls.includes:
if (array := getattr(prop, 'default_array', None)) and value == array: if (array := getattr(prop, "default_array", None)) and value == array:
continue continue
if isinstance(value, (str, int, float)) and value == prop.default: if isinstance(value, (str, int, float)) and value == prop.default:
continue continue
@ -192,10 +196,10 @@ class Dumper:
if getattr(prop, "is_array", False): if getattr(prop, "is_array", False):
value = PropArray.dump(value) value = PropArray.dump(value)
elif prop.type == 'COLLECTION': elif prop.type == "COLLECTION":
value = PropCollection.dump(value) value = PropCollection.dump(value)
elif prop.type == 'POINTER' and value: elif prop.type == "POINTER" and value:
# if prop.identifier == 'image': # if prop.identifier == 'image':
# print(bl_object, cls.pointers) # print(bl_object, cls.pointers)
if value.as_pointer() in cls.pointers: if value.as_pointer() in cls.pointers:
@ -234,7 +238,6 @@ class PropCollection(Dumper):
# Value cannot be None # Value cannot be None
return [v for v in values if v is not None] return [v for v in values if v is not None]
@classmethod @classmethod
def load(cls, values, coll): def load(cls, values, coll):
if not values: if not values:
@ -242,32 +245,34 @@ class PropCollection(Dumper):
dumper = None dumper = None
if not hasattr(coll, 'new'): # Static collection if not hasattr(coll, "new"): # Static collection
for item, value in zip(coll, values): for item, value in zip(coll, values):
dumper = dumper or get_dumper(item) dumper = dumper or get_dumper(item)
dumper.load(value, item) dumper.load(value, item)
return return
new_func = coll.bl_rna.functions['new'] new_func = coll.bl_rna.functions["new"]
for i, value in enumerate(values): for i, value in enumerate(values):
if value.get("_new"):
if value.get('_new'): params = value["_new"]
params = value['_new']
else: else:
params = {k: value.get(k, get_default(v)) for k, v in new_func.parameters.items()[:-1]} params = {
k: value.get(k, get_default(v))
for k, v in new_func.parameters.items()[:-1]
}
# Replace arg pointer with bl object # Replace arg pointer with bl object
valid_pointers = True valid_pointers = True
for param in coll.bl_rna.functions['new'].parameters: for param in coll.bl_rna.functions["new"].parameters:
if param.identifier not in params or param.type != 'POINTER': if param.identifier not in params or param.type != "POINTER":
continue continue
pointer_id = params[param.identifier] pointer_id = params[param.identifier]
if bl_object := cls.pointers.get(pointer_id): if bl_object := cls.pointers.get(pointer_id):
params[param.identifier] = bl_object params[param.identifier] = bl_object
else: else:
print(f'No Pointer found for param {param.identifier} of {coll}') print(f"No Pointer found for param {param.identifier} of {coll}")
valid_pointers = False valid_pointers = False
if not valid_pointers: if not valid_pointers:
@ -276,7 +281,6 @@ class PropCollection(Dumper):
# print(param.identifier, cls.pointers[pointer_id]) # print(param.identifier, cls.pointers[pointer_id])
try: try:
item = coll.new(**params) item = coll.new(**params)
except RuntimeError as e: except RuntimeError as e:
# print(e, coll.data) # print(e, coll.data)
@ -307,7 +311,13 @@ class PropArray(Dumper):
class NodeSocket(Dumper): class NodeSocket(Dumper):
bl_type = bpy.types.NodeSocket bl_type = bpy.types.NodeSocket
excludes = Dumper.excludes + ["node", "links", "display_shape", "rna_type", "link_limit"] excludes = Dumper.excludes + [
"node",
"links",
"display_shape",
"rna_type",
"link_limit",
]
@classmethod @classmethod
def dump(cls, socket): def dump(cls, socket):
@ -339,9 +349,10 @@ class NodeLink(Dumper):
@classmethod @classmethod
def dump(cls, link): def dump(cls, link):
return {"_new": { return {
"_new": {
"input": link.from_socket.as_pointer(), "input": link.from_socket.as_pointer(),
"output": link.to_socket.as_pointer() "output": link.to_socket.as_pointer(),
} }
} }
@ -357,29 +368,26 @@ class NodeTreeInterfaceSocket(Dumper):
data = super().dump(socket) data = super().dump(socket)
# data["_id"] = socket.as_pointer() # data["_id"] = socket.as_pointer()
data['_new'] = {"name": data.get('name', '')} data["_new"] = {"name": data.get("name", "")}
if socket.item_type == 'SOCKET':
data['_new']["in_out"] = socket.in_out
if socket.item_type == "SOCKET":
data["_new"]["in_out"] = socket.in_out
# It's a real panel not the interface root # It's a real panel not the interface root
if socket.parent.parent: if socket.parent.parent:
data['parent'] = socket.parent.as_pointer() data["parent"] = socket.parent.as_pointer()
return data return data
class NodeSockets(PropCollection): class NodeSockets(PropCollection):
@classmethod @classmethod
def load(cls, values, coll): def load(cls, values, coll):
# return # return
node_sockets = [s for s in coll if not s.is_unavailable] node_sockets = [s for s in coll if not s.is_unavailable]
for socket, value in zip(node_sockets, values): for socket, value in zip(node_sockets, values):
cls.pointers[value['bl_pointer']] = socket cls.pointers[value["bl_pointer"]] = socket
Dumper.load(value, socket) Dumper.load(value, socket)
# for k, v in value.items(): # for k, v in value.items():
# if k not in socket.bl_rna.properties: # if k not in socket.bl_rna.properties:
@ -419,7 +427,12 @@ class NodeOutputs(NodeSockets):
class Node(Dumper): class Node(Dumper):
bl_type = bpy.types.Node bl_type = bpy.types.Node
excludes = Dumper.excludes + ["dimensions", "height", "internal_links", "paired_output"] excludes = Dumper.excludes + [
"dimensions",
"height",
"internal_links",
"paired_output",
]
@classmethod @classmethod
def dump(cls, node=None): def dump(cls, node=None):
@ -427,7 +440,9 @@ class Node(Dumper):
data = super().dump(node) data = super().dump(node)
# data["_id"] = node.as_pointer() # data["_id"] = node.as_pointer()
data["_new"] = {"type": node.bl_rna.identifier} # 'node_tree': node.id_data.as_pointer() data["_new"] = {
"type": node.bl_rna.identifier
} # 'node_tree': node.id_data.as_pointer()
if paired_output := getattr(node, "paired_output", None): if paired_output := getattr(node, "paired_output", None):
data["_pair_with_output"] = paired_output.as_pointer() data["_pair_with_output"] = paired_output.as_pointer()
@ -443,13 +458,13 @@ class Node(Dumper):
return return
# cls.pointers[data['bl_pointer']] = node # cls.pointers[data['bl_pointer']] = node
inputs = copy(data.pop('inputs', [])) inputs = copy(data.pop("inputs", []))
outputs = copy(data.pop('outputs', [])) outputs = copy(data.pop("outputs", []))
super().load(data, node) super().load(data, node)
data['inputs'] = inputs data["inputs"] = inputs
data['outputs'] = outputs data["outputs"] = outputs
# Loading input and outputs after the properties # Loading input and outputs after the properties
super().load({"inputs": inputs, "outputs": outputs}, node) super().load({"inputs": inputs, "outputs": outputs}, node)
@ -472,20 +487,19 @@ class NodeTreeInterface(Dumper):
@classmethod @classmethod
def load(cls, data, interface): def load(cls, data, interface):
print("Load Interface")
print('Load Interface') for value in data.get("items_tree", []):
item_type = value.get("item_type", "SOCKET")
for value in data.get('items_tree', []): if item_type == "SOCKET":
item_type = value.get('item_type', 'SOCKET') item = interface.new_socket(**value["_new"])
if item_type == 'SOCKET': elif item_type == "PANEL":
item = interface.new_socket(**value['_new'])
elif item_type == 'PANEL':
# print(value['_new']) # print(value['_new'])
item = interface.new_panel(**value['_new']) item = interface.new_panel(**value["_new"])
NodeTreeInterfaceSocket.load(value, item) NodeTreeInterfaceSocket.load(value, item)
interface.active_index = data.get('active_index', 0) interface.active_index = data.get("active_index", 0)
class Nodes(PropCollection): class Nodes(PropCollection):
@ -497,13 +511,16 @@ class Nodes(PropCollection):
# Pair zone input and output # Pair zone input and output
for node_data in values: for node_data in values:
if paired_output_id := node_data.get('_pair_with_output', None): if paired_output_id := node_data.get("_pair_with_output", None):
node = cls.pointers[node_data['bl_pointer']] node = cls.pointers[node_data["bl_pointer"]]
node.pair_with_output(cls.pointers[paired_output_id]) node.pair_with_output(cls.pointers[paired_output_id])
# print(node, node_data['outputs']) # print(node, node_data['outputs'])
Dumper.load({"inputs": node_data['inputs'], "outputs": node_data['outputs']}, node) Dumper.load(
{"inputs": node_data["inputs"], "outputs": node_data["outputs"]},
node,
)
class NodeTree(Dumper): class NodeTree(Dumper):
@ -513,31 +530,41 @@ class NodeTree(Dumper):
@classmethod @classmethod
def new(cls, data): def new(cls, data):
if link := data.get('_link'): if link := data.get("_link"):
with bpy.data.libraries.load(link['filepath'], link=True) as (data_from, data_to): with bpy.data.libraries.load(link["filepath"], link=True) as (
setattr(data_to, link['data_type'], [link['name']]) data_from,
return getattr(data_to, link['data_type'])[0] data_to,
):
setattr(data_to, link["data_type"], [link["name"]])
return getattr(data_to, link["data_type"])[0]
return bpy.data.node_groups.new(**data["_new"]) return bpy.data.node_groups.new(**data["_new"])
@classmethod @classmethod
def dump(cls, node_tree): def dump(cls, node_tree):
if node_tree.library: if node_tree.library:
data = {'bl_pointer': node_tree.as_pointer()} data = {"bl_pointer": node_tree.as_pointer()}
filepath = abspath(bpy.path.abspath(node_tree.library.filepath, library=node_tree.library.library)) filepath = abspath(
data["_link"] = {"filepath": filepath, "data_type": 'node_groups', 'name': node_tree.name} bpy.path.abspath(
node_tree.library.filepath, library=node_tree.library.library
)
)
data["_link"] = {
"filepath": filepath,
"data_type": "node_groups",
"name": node_tree.name,
}
else: else:
data = super().dump(node_tree) data = super().dump(node_tree)
data["_new"] = {"type": node_tree.bl_rna.identifier, 'name': node_tree.name} data["_new"] = {"type": node_tree.bl_rna.identifier, "name": node_tree.name}
return data return data
class Points(PropCollection): class Points(PropCollection):
@classmethod @classmethod
def load(cls, values, coll): def load(cls, values, coll):
new_func = coll.bl_rna.functions['new'] new_func = coll.bl_rna.functions["new"]
params = {k: get_default(v) + 1.1 for k, v in new_func.parameters.items()[:-1]} params = {k: get_default(v) + 1.1 for k, v in new_func.parameters.items()[:-1]}
# Match the same number of elements in collection # Match the same number of elements in collection
@ -583,7 +610,7 @@ class AOVs(PropCollection):
@classmethod @classmethod
def load(cls, values, coll): def load(cls, values, coll):
for value in values: for value in values:
aov = coll.get(value['name']) aov = coll.get(value["name"])
if not aov: if not aov:
aov = coll.add() aov = coll.add()
@ -595,7 +622,7 @@ class Image(Dumper):
bl_type = bpy.types.Image bl_type = bpy.types.Image
excludes = [] excludes = []
includes = ['name', 'filepath'] includes = ["name", "filepath"]
@classmethod @classmethod
def new(cls, data): def new(cls, data):
@ -605,20 +632,20 @@ class Image(Dumper):
# if image is None: # if image is None:
# image = bpy.data.images.load(data['filepath']) # image = bpy.data.images.load(data['filepath'])
return bpy.data.images.load(data['filepath'], check_existing=True) return bpy.data.images.load(data["filepath"], check_existing=True)
class Material(Dumper): class Material(Dumper):
bl_type = bpy.types.Material bl_type = bpy.types.Material
excludes = Dumper.excludes + ['preview', "original"] excludes = Dumper.excludes + ["preview", "original"]
@classmethod @classmethod
def new(cls, data): def new(cls, data):
material = bpy.data.materials.get(data.get('name', '')) material = bpy.data.materials.get(data.get("name", ""))
if material is None: if material is None:
material = bpy.data.materials.new(data['name']) material = bpy.data.materials.new(data["name"])
return material return material
@ -626,26 +653,25 @@ class Material(Dumper):
class Object(Dumper): class Object(Dumper):
bl_type = bpy.types.Object bl_type = bpy.types.Object
excludes = [] excludes = []
includes = ['name'] includes = ["name"]
@classmethod @classmethod
def new(cls, data): def new(cls, data):
if name := data.get('name'): if name := data.get("name"):
return bpy.data.objects.get(name) return bpy.data.objects.get(name)
class Scene(Dumper): class Scene(Dumper):
bl_type = bpy.types.Scene bl_type = bpy.types.Scene
excludes = [] excludes = []
includes = ['name'] includes = ["name"]
@classmethod @classmethod
def new(cls, data): def new(cls, data):
if scene := bpy.data.scenes.get(data.get('name', '')): if scene := bpy.data.scenes.get(data.get("name", "")):
return scene return scene
return bpy.data.scenes.new(name=data.get('name', '')) return bpy.data.scenes.new(name=data.get("name", ""))
""" """
@classmethod @classmethod
@ -661,14 +687,15 @@ class Scene(Dumper):
} }
""" """
class Collection(Dumper): class Collection(Dumper):
bl_type = bpy.types.Collection bl_type = bpy.types.Collection
includes = ['name'] includes = ["name"]
excludes = [] excludes = []
@classmethod @classmethod
def new(cls, data): def new(cls, data):
if name := data.get('name'): if name := data.get("name"):
return bpy.data.collections.get(name) return bpy.data.collections.get(name)
# @classmethod # @classmethod
@ -682,16 +709,15 @@ class Collection(Dumper):
class CompositorNodeRLayers(Node): class CompositorNodeRLayers(Node):
bl_type = bpy.types.CompositorNodeRLayers bl_type = bpy.types.CompositorNodeRLayers
excludes = Dumper.excludes + ['scene'] excludes = Dumper.excludes + ["scene"]
@classmethod @classmethod
def load(cls, data, node): def load(cls, data, node):
# print('load CompositorNodeRLayers') # print('load CompositorNodeRLayers')
scene_data = data.pop('scene') scene_data = data.pop("scene")
# print(scene_data) # print(scene_data)
layer = data.pop('layer') layer = data.pop("layer")
scene = Scene.new(scene_data) scene = Scene.new(scene_data)
Scene.load(scene_data, scene) Scene.load(scene_data, scene)
@ -705,8 +731,6 @@ class CompositorNodeRLayers(Node):
# print(bpy.) # print(bpy.)
@classmethod @classmethod
def dump(cls, node): def dump(cls, node):
# Add scene and viewlayer passes # Add scene and viewlayer passes
@ -717,22 +741,25 @@ class CompositorNodeRLayers(Node):
view_layer = node.scene.view_layers[node.layer] view_layer = node.scene.view_layers[node.layer]
view_layer_data = ViewLayer.dump(view_layer) view_layer_data = ViewLayer.dump(view_layer)
''' """
view_layer_data = { view_layer_data = {
"name": view_layer.name} "name": view_layer.name}
properties = {p.name: p for p in view_layer.bl_rna.properties} properties = {p.name: p for p in view_layer.bl_rna.properties}
for prop in view_layer.bl_rna: for prop in view_layer.bl_rna:
if prop.identifier.startswith('use_pass'): if prop.identifier.startswith('use_pass'):
view_layer_data[prop.identifier] view_layer_data[prop.identifier]
''' """
# cls.pointers[bl_object.as_pointer()] = bl_object # cls.pointers[bl_object.as_pointer()] = bl_object
data['scene'] = { data["scene"] = {
'bl_pointer': node.scene.as_pointer(), "bl_pointer": node.scene.as_pointer(),
'name': node.scene.name, "name": node.scene.name,
'render' : {'bl_pointer': node.scene.render.as_pointer(), "engine": node.scene.render.engine}, "render": {
'view_layers': [view_layer_data] "bl_pointer": node.scene.render.as_pointer(),
"engine": node.scene.render.engine,
},
"view_layers": [view_layer_data],
} }
return data return data
@ -740,8 +767,15 @@ class CompositorNodeRLayers(Node):
class ViewLayer(Dumper): class ViewLayer(Dumper):
bl_type = bpy.types.ViewLayer bl_type = bpy.types.ViewLayer
excludes = Dumper.excludes + ['freestyle_settings', 'eevee', 'cycles', 'active_layer_collection', excludes = Dumper.excludes + [
'active_aov', 'active_lightgroup_index', 'active_lightgroup'] "freestyle_settings",
"eevee",
"cycles",
"active_layer_collection",
"active_aov",
"active_lightgroup_index",
"active_lightgroup",
]
# includes = ['name'] # includes = ['name']
@ -752,10 +786,10 @@ class ViewLayers(PropCollection):
def load(cls, values, coll): def load(cls, values, coll):
# print('LOAD VIEWLAYERS', values) # print('LOAD VIEWLAYERS', values)
for value in values: for value in values:
view_layer = coll.get(value['name']) view_layer = coll.get(value["name"])
if view_layer is None: if view_layer is None:
view_layer = coll.new(value['name']) view_layer = coll.new(value["name"])
Dumper.load(value, view_layer) Dumper.load(value, view_layer)

View File

@ -1,18 +1,18 @@
import bpy import bpy
import re import re
def clean_name(name): def clean_name(name):
if re.match(r'(.*)\.\d{3}$', name): if re.match(r"(.*)\.\d{3}$", name):
return name[:-4] return name[:-4]
return name return name
def is_node_groups_duplicate(node_groups): def is_node_groups_duplicate(node_groups):
node_group_types = sorted([n.type for n in node_groups[0].nodes]) node_group_types = sorted([n.type for n in node_groups[0].nodes])
return all( sorted([n.type for n in ng.nodes]) == return all(
node_group_types for ng in node_groups[1:]) sorted([n.type for n in ng.nodes]) == node_group_types for ng in node_groups[1:]
)
def remap_node_group_duplicates(nodes=None, force=False): def remap_node_group_duplicates(nodes=None, force=False):
@ -50,11 +50,13 @@ def remap_node_group_duplicates(nodes=None, force=False):
if not is_duplicate and not force: if not is_duplicate and not force:
failed.append((node_group.name, node_groups[0].name)) failed.append((node_group.name, node_groups[0].name))
print(f'Cannot merge Nodegroup {node_group.name} with {node_groups[0].name} they are different') print(
f"Cannot merge Nodegroup {node_group.name} with {node_groups[0].name} they are different"
)
continue continue
merged.append((node_group.name, node_groups[0].name)) merged.append((node_group.name, node_groups[0].name))
print(f'Merge Nodegroup {node_group.name} into {node_groups[0].name}') print(f"Merge Nodegroup {node_group.name} into {node_groups[0].name}")
node_group.user_remap(node_groups[0]) node_group.user_remap(node_groups[0])
bpy.data.node_groups.remove(node_group) bpy.data.node_groups.remove(node_group)

View File

@ -1,5 +1,6 @@
import bpy import bpy
def set_params(src, tgt, mod_to_node=True, org_modifier=None): def set_params(src, tgt, mod_to_node=True, org_modifier=None):
# mod to node: est-ce qu'on copie les valeurs d'un modifier a une node, ou l'inverse # mod to node: est-ce qu'on copie les valeurs d'un modifier a une node, ou l'inverse
if mod_to_node: # syntax for node and modifier are slightly different if mod_to_node: # syntax for node and modifier are slightly different
@ -8,9 +9,9 @@ def set_params(src, tgt, mod_to_node=True, org_modifier=None):
tree = src.node_tree.interface.items_tree tree = src.node_tree.interface.items_tree
for param in tree: for param in tree:
if param.socket_type == 'NodeSocketGeometry': if param.socket_type == "NodeSocketGeometry":
continue continue
if param.in_out == 'OUTPUT': if param.in_out == "OUTPUT":
continue continue
# seulement en extract mode, src est une node donc on check si des parametres sont dans le modifier # seulement en extract mode, src est une node donc on check si des parametres sont dans le modifier
@ -26,19 +27,23 @@ def set_params(src, tgt, mod_to_node=True, org_modifier=None):
else: else:
tgt[identifier] = src.inputs[identifier].default_value tgt[identifier] = src.inputs[identifier].default_value
def set_group_inputs(target, objects, group): def set_group_inputs(target, objects, group):
mod = target.modifiers[0] mod = target.modifiers[0]
node_dct = {} # used for cleanup node_dct = {} # used for cleanup
for key, inp in get_node_inputs(objects).items(): for key, inp in get_node_inputs(objects).items():
# add the socket to the node group / modifier pannel # add the socket to the node group / modifier pannel
sock = group.interface.new_socket(inp["label"],in_out="INPUT",socket_type=inp["socket"]) sock = group.interface.new_socket(
inp["label"], in_out="INPUT", socket_type=inp["socket"]
)
mod[sock.identifier] = inp["data"] mod[sock.identifier] = inp["data"]
# inspect all nodes and add a group input node when that socket is used # inspect all nodes and add a group input node when that socket is used
for node in parse_nodes(objects): for node in parse_nodes(objects):
for param in node.node_tree.interface.items_tree: for param in node.node_tree.interface.items_tree:
nkey = get_input_socket_key(node, param) nkey = get_input_socket_key(node, param)
if not nkey: continue if not nkey:
continue
if nkey == key: if nkey == key:
input_node = add_input_node(group, node, param.identifier, sock) input_node = add_input_node(group, node, param.identifier, sock)
@ -55,10 +60,11 @@ def set_group_inputs(target, objects, group):
# on refait la meme chose pour les object info nodes car leur syntaxe est un peu differente # on refait la meme chose pour les object info nodes car leur syntaxe est un peu differente
for node in parse_nodes(objects, type="OBJECT_INFO"): for node in parse_nodes(objects, type="OBJECT_INFO"):
nkey = get_input_socket_key(node, param) nkey = get_input_socket_key(node, param)
if not nkey: continue if not nkey:
continue
if nkey == key: if nkey == key:
input_node = add_input_node(group, node, 'Object', sock) input_node = add_input_node(group, node, "Object", sock)
node.inputs['Object'].default_value = None node.inputs["Object"].default_value = None
# add to dict for cleanup # add to dict for cleanup
if not node in node_dct.keys(): if not node in node_dct.keys():
@ -72,6 +78,7 @@ def set_group_inputs(target, objects, group):
input_node.location[1] += 50 * offset input_node.location[1] += 50 * offset
hide_sockets(input_node) hide_sockets(input_node)
def get_node_link_value(node, param_name, org_mod): def get_node_link_value(node, param_name, org_mod):
if not org_mod: if not org_mod:
return return
@ -82,20 +89,25 @@ def get_node_link_value(node, param_name, org_mod):
return org_mod[socket_id] return org_mod[socket_id]
def get_geo_socket(node, input=True): def get_geo_socket(node, input=True):
if node.type != "GROUP": if node.type != "GROUP":
return('Geometry') return "Geometry"
for param in node.node_tree.interface.items_tree: for param in node.node_tree.interface.items_tree:
if param.socket_type != 'NodeSocketGeometry': if param.socket_type != "NodeSocketGeometry":
continue continue
if input and param.in_out == 'INPUT' : return param.identifier if input and param.in_out == "INPUT":
if not input and param.in_out == 'OUTPUT' : return param.identifier return param.identifier
if not input and param.in_out == "OUTPUT":
return param.identifier
return None return None
def get_input_socket_key(node, param): def get_input_socket_key(node, param):
if node.type == "GROUP": if node.type == "GROUP":
if param.in_out != 'INPUT': if param.in_out != "INPUT":
return False return False
if not param.socket_type in ['NodeSocketObject','NodeSocketCollection']: if not param.socket_type in ["NodeSocketObject", "NodeSocketCollection"]:
return False return False
tgt = node.inputs[param.identifier].default_value tgt = node.inputs[param.identifier].default_value
@ -104,11 +116,12 @@ def get_input_socket_key(node, param):
return f"{param.socket_type[10:][:3]} {tgt.name}" return f"{param.socket_type[10:][:3]} {tgt.name}"
if node.type == "OBJECT_INFO": if node.type == "OBJECT_INFO":
tgt = node.inputs['Object'].default_value tgt = node.inputs["Object"].default_value
if not tgt: if not tgt:
return False return False
return f"Object {tgt.name}" return f"Object {tgt.name}"
def get_node_inputs(combined_nodes): def get_node_inputs(combined_nodes):
# inputs["Col COL.name"] = {name = COL.name, data = COL, socket = "COLLECTION"} # inputs["Col COL.name"] = {name = COL.name, data = COL, socket = "COLLECTION"}
# inputs["Obj OBJ.name"] = {name = OBJ.name, data = OBJ, socket = "OBJECT"} # inputs["Obj OBJ.name"] = {name = OBJ.name, data = OBJ, socket = "OBJECT"}
@ -119,17 +132,28 @@ def get_node_inputs(combined_nodes):
if not key: if not key:
continue continue
tgt = node.inputs[param.identifier].default_value tgt = node.inputs[param.identifier].default_value
inputs[key] = {'name': tgt.name, 'data': tgt, 'label': param.name , 'socket': param.socket_type} inputs[key] = {
"name": tgt.name,
"data": tgt,
"label": param.name,
"socket": param.socket_type,
}
for node in parse_nodes(combined_nodes, type="OBJECT_INFO"): for node in parse_nodes(combined_nodes, type="OBJECT_INFO"):
key = get_input_socket_key(node, None) key = get_input_socket_key(node, None)
if not key: if not key:
continue continue
tgt = node.inputs['Object'].default_value tgt = node.inputs["Object"].default_value
inputs[key] = {'name': tgt.name, 'data': tgt, 'label': 'Source OB' , 'socket': "NodeSocketObject"} inputs[key] = {
"name": tgt.name,
"data": tgt,
"label": "Source OB",
"socket": "NodeSocketObject",
}
return inputs return inputs
def get_node_bounds(objects, mode=0, x=0, y=0): def get_node_bounds(objects, mode=0, x=0, y=0):
min_x = min_y = 10000000 min_x = min_y = 10000000
max_x = max_y = 0 max_x = max_y = 0
@ -143,7 +167,8 @@ def get_node_bounds(objects, mode=0, x=0, y=0):
min_y = min(co[1], min_y) min_y = min(co[1], min_y)
max_y = max(co[1], max_y) max_y = max(co[1], max_y)
if mode == 0: if mode == 0:
return([max_x+x, (min_y+max_y)/2 ]) return [max_x + x, (min_y + max_y) / 2]
def get_collection(name): def get_collection(name):
scn = bpy.context.scene scn = bpy.context.scene
@ -152,23 +177,29 @@ def get_collection(name):
# look for existing # look for existing
for c in bpy.data.collections: for c in bpy.data.collections:
if c.name == name: col = c if c.name == name:
col = c
# create if needed # create if needed
if not col: col = bpy.data.collections.new(name) if not col:
col = bpy.data.collections.new(name)
# link to scene if needed # link to scene if needed
for c in scn.collection.children_recursive: for c in scn.collection.children_recursive:
if c.name == col.name: link = True if c.name == col.name:
link = True
if not link: if not link:
scn.collection.children.link(col) scn.collection.children.link(col)
return col return col
def get_mod_frames(grp): def get_mod_frames(grp):
frames = [] frames = []
for node in grp.nodes: for node in grp.nodes:
if node.type == "FRAME": frames.append(node) if node.type == "FRAME":
return(frames) frames.append(node)
return frames
def get_frame_childrens(frame): def get_frame_childrens(frame):
childrens = [] childrens = []
@ -183,13 +214,16 @@ def get_frame_childrens(frame):
childrens = [locs[x] for x in entries] childrens = [locs[x] for x in entries]
return childrens return childrens
def parse_nodes(combined_nodes, type="GROUP"): def parse_nodes(combined_nodes, type="GROUP"):
nodes = [] nodes = []
for frame in combined_nodes: for frame in combined_nodes:
for node in frame: for node in frame:
if node.type == type: nodes.append(node) if node.type == type:
nodes.append(node)
return nodes return nodes
def copy_source_ob(ob, col): def copy_source_ob(ob, col):
# est-ce que l'objet a des data ? si oui on cree une copie , # est-ce que l'objet a des data ? si oui on cree une copie ,
# si non on renvois None # si non on renvois None
@ -210,6 +244,7 @@ def copy_source_ob(ob, col):
col.objects.link(new_ob) col.objects.link(new_ob)
return new_ob return new_ob
def hide_sockets(node, collapse=True): def hide_sockets(node, collapse=True):
for socket in node.outputs: for socket in node.outputs:
if not socket.links: if not socket.links:
@ -220,14 +255,15 @@ def hide_sockets(node,collapse = True):
if collapse: if collapse:
node.hide = True node.hide = True
def add_input_node(group, node, param_id, socket): def add_input_node(group, node, param_id, socket):
group_input_node = group.nodes.new('NodeGroupInput') group_input_node = group.nodes.new("NodeGroupInput")
group_input_node.location = node.location group_input_node.location = node.location
group_input_node.location[1] += 70 group_input_node.location[1] += 70
group_input_node.label = socket.name group_input_node.label = socket.name
group.links.new(group_input_node.outputs[socket.identifier], group.links.new(group_input_node.outputs[socket.identifier], node.inputs[param_id])
node.inputs[param_id]) return group_input_node
return(group_input_node)
def add_material_node(ob, group, nodes): def add_material_node(ob, group, nodes):
if not ob.material_slots: if not ob.material_slots:
@ -235,13 +271,14 @@ def add_material_node(ob, group, nodes):
if not ob.material_slots[0].material: if not ob.material_slots[0].material:
return nodes return nodes
last_node = nodes[-1:][0] last_node = nodes[-1:][0]
node = group.nodes.new('GeometryNodeSetMaterial') node = group.nodes.new("GeometryNodeSetMaterial")
node.inputs['Material'].default_value = ob.material_slots[0].material node.inputs["Material"].default_value = ob.material_slots[0].material
node.location = last_node.location node.location = last_node.location
node.location[0] += 300 node.location[0] += 300
nodes.append(node) nodes.append(node)
return nodes return nodes
def join_nodes(group, nodes): def join_nodes(group, nodes):
prev = None prev = None
for i, node in enumerate(nodes): for i, node in enumerate(nodes):
@ -256,23 +293,27 @@ def join_nodes(group, nodes):
group.links.new(prev.outputs[geo_out], node.inputs[geo_in]) group.links.new(prev.outputs[geo_out], node.inputs[geo_in])
prev = node prev = node
def frame_nodes(group, nodes, ob): def frame_nodes(group, nodes, ob):
nd = group.nodes.new('NodeFrame') nd = group.nodes.new("NodeFrame")
# frame = nodes.new(type='NodeFrame') # frame = nodes.new(type='NodeFrame')
for n in nodes: for n in nodes:
n.parent = nd n.parent = nd
nd.label = ob.name nd.label = ob.name
def combine_ob(ob, group, y=0, col=None): def combine_ob(ob, group, y=0, col=None):
nodes = [] nodes = []
# object info node # object info node
nd = group.nodes.new('GeometryNodeObjectInfo') nd = group.nodes.new("GeometryNodeObjectInfo")
nd.location[0] -= 300 nd.location[0] -= 300
nd.location[1] = y * 800 nd.location[1] = y * 800
nd.transform_space = "RELATIVE" nd.transform_space = "RELATIVE"
nd.inputs['Object'].default_value = copy_source_ob(ob, col) # si l'objet contient des data on crée une copie nd.inputs["Object"].default_value = copy_source_ob(
ob, col
) # si l'objet contient des data on crée une copie
nodes.append(nd) nodes.append(nd)
# ob modifiers # ob modifiers
@ -282,7 +323,7 @@ def combine_ob(ob, group, y=0, col=None):
if md.node_group == group: if md.node_group == group:
continue continue
nd = group.nodes.new('GeometryNodeGroup') nd = group.nodes.new("GeometryNodeGroup")
nd.label = md.name nd.label = md.name
nd.width = 230 nd.width = 230
nd.location[0] = x * 300 nd.location[0] = x * 300
@ -296,6 +337,7 @@ def combine_ob(ob, group, y=0, col=None):
frame_nodes(group, nodes, ob) frame_nodes(group, nodes, ob)
return nodes return nodes
def gen_target_ob(group, col=None): def gen_target_ob(group, col=None):
ob = gen_empty_ob(group.name, col=col) ob = gen_empty_ob(group.name, col=col)
mod = ob.modifiers.new(group.name, "NODES") mod = ob.modifiers.new(group.name, "NODES")
@ -303,20 +345,22 @@ def gen_target_ob(group, col=None):
ob.show_name = True ob.show_name = True
bpy.context.view_layer.objects.active = ob bpy.context.view_layer.objects.active = ob
return(ob) return ob
def gen_empty_ob(name, col=None): def gen_empty_ob(name, col=None):
scn = bpy.context.scene scn = bpy.context.scene
ob = bpy.data.objects.new(name, object_data=bpy.data.meshes.new(name)) ob = bpy.data.objects.new(name, object_data=bpy.data.meshes.new(name))
ob.data.materials.append(None) ob.data.materials.append(None)
ob.material_slots[0].link = 'OBJECT' ob.material_slots[0].link = "OBJECT"
if not col: if not col:
scn.collection.objects.link(ob) scn.collection.objects.link(ob)
else: else:
col.objects.link(ob) col.objects.link(ob)
return(ob) return ob
def assign_modifiers(ob, frame, org_modifier): def assign_modifiers(ob, frame, org_modifier):
for node in get_frame_childrens(frame): for node in get_frame_childrens(frame):
@ -328,11 +372,14 @@ def assign_modifiers(ob, frame, org_modifier):
set_params(node, mod, mod_to_node=False, org_modifier=org_modifier) set_params(node, mod, mod_to_node=False, org_modifier=org_modifier)
mod.node_group.interface_update(bpy.context) mod.node_group.interface_update(bpy.context)
def join_branches(objects, group): def join_branches(objects, group):
# join all trees and add an output node # join all trees and add an output node
join = group.nodes.new('GeometryNodeJoinGeometry') join = group.nodes.new("GeometryNodeJoinGeometry")
out = group.nodes.new('NodeGroupOutput') out = group.nodes.new("NodeGroupOutput")
out_sock = group.interface.new_socket("Geometry",in_out="OUTPUT",socket_type="NodeSocketGeometry") out_sock = group.interface.new_socket(
"Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry"
)
loc = get_node_bounds(objects, x=500) loc = get_node_bounds(objects, x=500)
join.location = loc join.location = loc
@ -341,36 +388,42 @@ def join_branches(objects, group):
for ob in objects: for ob in objects:
node = ob[-1:][0] node = ob[-1:][0]
group.links.new(node.outputs[get_geo_socket(node, input=False)], group.links.new(
join.inputs[get_geo_socket(join)]) node.outputs[get_geo_socket(node, input=False)],
join.inputs[get_geo_socket(join)],
)
group.links.new(
join.outputs[get_geo_socket(join, input=False)], out.inputs[out_sock.identifier]
)
group.links.new(join.outputs[get_geo_socket(join, input=False)],
out.inputs[out_sock.identifier])
def gen_extracted_ob(name, frame, col, mod): def gen_extracted_ob(name, frame, col, mod):
ob = None ob = None
for node in get_frame_childrens(frame): for node in get_frame_childrens(frame):
if node.type != "OBJECT_INFO": if node.type != "OBJECT_INFO":
continue continue
target = get_node_link_value(node, 'Object', mod) target = get_node_link_value(node, "Object", mod)
if target: if target:
ob = target.copy() ob = target.copy()
ob.data = ob.data.copy() ob.data = ob.data.copy()
col.objects.link(ob) col.objects.link(ob)
if not ob: ob = gen_empty_ob(name , col = col) if not ob:
ob = gen_empty_ob(name, col=col)
# assign material # assign material
for node in get_frame_childrens(frame): for node in get_frame_childrens(frame):
if node.type != "SET_MATERIAL": if node.type != "SET_MATERIAL":
continue continue
ob.material_slots[0].material = node.inputs['Material'].default_value ob.material_slots[0].material = node.inputs["Material"].default_value
return ob return ob
def combine_objects(objs): def combine_objects(objs):
name = f"NODEGROUP_combined" name = f"NODEGROUP_combined"
col = get_collection(name) col = get_collection(name)
group = bpy.data.node_groups.new(name=name, type='GeometryNodeTree') group = bpy.data.node_groups.new(name=name, type="GeometryNodeTree")
objects = [] objects = []
for y, ob in enumerate(objs): for y, ob in enumerate(objs):
@ -380,6 +433,7 @@ def combine_objects(objs):
set_group_inputs(target, objects, group) set_group_inputs(target, objects, group)
join_branches(objects, group) join_branches(objects, group)
def extract_objects(object): def extract_objects(object):
mod = object.modifiers[0] mod = object.modifiers[0]
grp = mod.node_group grp = mod.node_group
@ -390,6 +444,7 @@ def extract_objects(object):
ob = gen_extracted_ob(name, frame, col, mod) ob = gen_extracted_ob(name, frame, col, mod)
assign_modifiers(ob, frame, mod) assign_modifiers(ob, frame, mod)
# combine_objects(bpy.context.selected_objects) # combine_objects(bpy.context.selected_objects)
# extract_objects(bpy.context.active_object) # extract_objects(bpy.context.active_object)
""" """

View File

@ -1,12 +0,0 @@
import plateform
from pathlib import Path
from os.path import expandvars
def get_cache_dir()
if plateform.system() == 'Linux':
return Path(expandvars('$HOME/.cache/blender'))
elif plateform.system() == 'Darwin':
return Path('/Library/Caches/Blender')
elif plateform.system() == 'Windows':
return Path(expandvars('%USERPROFILE%\AppData\Local\Blender Foundation\Blender'))

View File

@ -21,19 +21,22 @@ from .core.pack_nodes import combine_objects, extract_objects
class NODEKIT_OT_copy(Operator): class NODEKIT_OT_copy(Operator):
bl_idname = 'node_kit.copy_node_tree' bl_idname = "node_kit.copy_node_tree"
bl_label = 'Copy nodes' bl_label = "Copy nodes"
bl_options = {'REGISTER', 'UNDO'} bl_options = {"REGISTER", "UNDO"}
select_only: BoolProperty(default=True) select_only: BoolProperty(default=True)
def execute(self, context): def execute(self, context):
ntree = context.space_data.edit_tree ntree = context.space_data.edit_tree
if self.select_only: if self.select_only:
ntree_data = { ntree_data = {
"nodes" : dump([n for n in ntree.nodes if n.select]) ,#[dump(n) for n in ntree.nodes if n.select], "nodes": dump(
"links" : dump([l for l in ntree.links if l.from_node.select and l.to_node.select]) [n for n in ntree.nodes if n.select]
), # [dump(n) for n in ntree.nodes if n.select],
"links": dump(
[l for l in ntree.links if l.from_node.select and l.to_node.select]
),
} }
else: else:
ntree_data = dump(ntree) ntree_data = dump(ntree)
@ -42,76 +45,91 @@ class NODEKIT_OT_copy(Operator):
context.window_manager.clipboard = json.dumps(ntree_data) context.window_manager.clipboard = json.dumps(ntree_data)
return {'FINISHED'} return {"FINISHED"}
class NODEKIT_OT_paste(Operator): class NODEKIT_OT_paste(Operator):
bl_idname = 'node_kit.paste_node_tree' bl_idname = "node_kit.paste_node_tree"
bl_label = 'Paste nodes' bl_label = "Paste nodes"
def execute(self, context): def execute(self, context):
ntree_data = json.loads(context.window_manager.clipboard) ntree_data = json.loads(context.window_manager.clipboard)
load(ntree_data, context.space_data.edit_tree) load(ntree_data, context.space_data.edit_tree)
return {'FINISHED'} return {"FINISHED"}
class NODEKIT_OT_remap_node_group_duplicates(Operator): class NODEKIT_OT_remap_node_group_duplicates(Operator):
bl_idname = 'node_kit.remap_node_group_duplicates' bl_idname = "node_kit.remap_node_group_duplicates"
bl_label = 'Clean nodes' bl_label = "Clean nodes"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
selection : EnumProperty(items=[(s, s.title(), '') for s in ('ALL', 'SELECTED', 'CURRENT')], default="CURRENT", name='All Nodes') selection: EnumProperty(
force : BoolProperty(default=False, description='Remap nodes even if there are different', name='Force') items=[(s, s.title(), "") for s in ("ALL", "SELECTED", "CURRENT")],
default="CURRENT",
name="All Nodes",
)
force: BoolProperty(
default=False,
description="Remap nodes even if there are different",
name="Force",
)
def invoke(self, context, event): def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
def execute(self, context): def execute(self, context):
nodes = None nodes = None
if self.selection == 'SELECTED': if self.selection == "SELECTED":
nodes = [ n.node_tree for n in context.space_data.edit_tree.nodes nodes = [
if n.type == "GROUP" and n.select] n.node_tree
elif self.selection == 'ACTIVE': for n in context.space_data.edit_tree.nodes
if n.type == "GROUP" and n.select
]
elif self.selection == "ACTIVE":
active_node = context.space_data.edit_tree active_node = context.space_data.edit_tree
nodes = [active_node] nodes = [active_node]
merged, failed = remap_node_group_duplicates(nodes=nodes, force=self.force) merged, failed = remap_node_group_duplicates(nodes=nodes, force=self.force)
if failed and not merged: if failed and not merged:
self.report({"ERROR"}, 'No duplicates remapped, Node Group are differents') self.report({"ERROR"}, "No duplicates remapped, Node Group are differents")
return {"CANCELLED"} return {"CANCELLED"}
self.report({"INFO"}, f'{len(merged)} Node Groups Remapped, {len(failed)} Node Groups failed') self.report(
{"INFO"},
f"{len(merged)} Node Groups Remapped, {len(failed)} Node Groups failed",
)
return {'FINISHED'} return {"FINISHED"}
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.prop(self, "selection", expand=True) layout.prop(self, "selection", expand=True)
layout.prop(self, "force") layout.prop(self, "force")
if self.force and self.selection == 'CURRENT': if self.force and self.selection == "CURRENT":
ntree = context.space_data.edit_tree ntree = context.space_data.edit_tree
layout.label(text=f'Remap node {ntree.name} to others') layout.label(text=f"Remap node {ntree.name} to others")
elif self.force and self.selection == 'SELECTED': elif self.force and self.selection == "SELECTED":
layout.label(text='Selected nodes will override others') layout.label(text="Selected nodes will override others")
elif self.selection == 'SELECTED': elif self.selection == "SELECTED":
layout.label(text='Remap last .*** nodes') layout.label(text="Remap last .*** nodes")
layout.label(text='Ex: Node.001 will override Node') layout.label(text="Ex: Node.001 will override Node")
elif self.selection in ('CURRENT', 'ALL'): elif self.selection in ("CURRENT", "ALL"):
layout.label(text='Remap last .*** nodes') layout.label(text="Remap last .*** nodes")
layout.label(text='Ex: Node.001 will override Node') layout.label(text="Ex: Node.001 will override Node")
class NODEKIT_OT_update_nodes(Operator): class NODEKIT_OT_update_nodes(Operator):
bl_idname = 'node_kit.update_nodes' bl_idname = "node_kit.update_nodes"
bl_label = 'Update node' bl_label = "Update node"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
selection : EnumProperty(items=[(s, s.title(), '') for s in ('ALL', 'SELECTED', 'ACTIVE')], default="ACTIVE", name='All Nodes') selection: EnumProperty(
items=[(s, s.title(), "") for s in ("ALL", "SELECTED", "ACTIVE")],
default="ACTIVE",
name="All Nodes",
)
def invoke(self, context, event): def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
@ -123,16 +141,18 @@ class NODEKIT_OT_update_nodes(Operator):
ntree_name = ntree.name ntree_name = ntree.name
new_ntree = None new_ntree = None
if self.selection == 'SELECTED': if self.selection == "SELECTED":
nodes = [ n.node_tree for n in context.space_data.edit_tree.nodes nodes = [
if n.type == "GROUP" and n.select] n.node_tree
elif self.selection == 'ACTIVE': for n in context.space_data.edit_tree.nodes
if n.type == "GROUP" and n.select
]
elif self.selection == "ACTIVE":
active_node = context.space_data.edit_tree active_node = context.space_data.edit_tree
nodes = [active_node] nodes = [active_node]
else: else:
nodes = list(bpy.data.node_groups) nodes = list(bpy.data.node_groups)
node_names = set(n.name for n in nodes) node_names = set(n.name for n in nodes)
# new_node_groups = [] # new_node_groups = []
@ -142,15 +162,20 @@ class NODEKIT_OT_update_nodes(Operator):
library_path = Path(asset_library.path) library_path = Path(asset_library.path)
blend_files = [fp for fp in library_path.glob("**/*.blend") if fp.is_file()] blend_files = [fp for fp in library_path.glob("**/*.blend") if fp.is_file()]
node_groups = list(bpy.data.node_groups)# Storing original node_geoup to compare with imported node_groups = list(
bpy.data.node_groups
) # Storing original node_geoup to compare with imported
link = (asset_library.import_method == 'LINK') link = asset_library.import_method == "LINK"
for blend_file in blend_files: for blend_file in blend_files:
print(blend_file) print(blend_file)
with bpy.data.libraries.load(str(blend_file), assets_only=True, link=link) as (data_from, data_to): with bpy.data.libraries.load(
str(blend_file), assets_only=True, link=link
) as (data_from, data_to):
print(data_from.node_groups) print(data_from.node_groups)
import_node_groups = [n for n in data_from.node_groups if n in node_names] import_node_groups = [
n for n in data_from.node_groups if n in node_names
]
print("import_node_groups", import_node_groups) print("import_node_groups", import_node_groups)
data_to.node_groups = import_node_groups data_to.node_groups = import_node_groups
@ -171,13 +196,20 @@ class NODEKIT_OT_update_nodes(Operator):
# if new_node_groups: # if new_node_groups:
for new_node_group in new_node_groups: for new_node_group in new_node_groups:
new_node_group_name = new_node_group.library_weak_reference.id_name[2:] new_node_group_name = new_node_group.library_weak_reference.id_name[2:]
local_node_group = next((n for n in bpy.data.node_groups if n.name == new_node_group_name and n != new_node_group), None) local_node_group = next(
(
n
for n in bpy.data.node_groups
if n.name == new_node_group_name and n != new_node_group
),
None,
)
if not local_node_group: if not local_node_group:
print(f'No local node_group {new_node_group_name}') print(f"No local node_group {new_node_group_name}")
continue continue
print(f'Merge node {local_node_group.name} into {new_node_group.name}') print(f"Merge node {local_node_group.name} into {new_node_group.name}")
local_node_group.user_remap(new_node_group) local_node_group.user_remap(new_node_group)
new_node_group.interface_update(context) new_node_group.interface_update(context)
@ -187,7 +219,7 @@ class NODEKIT_OT_update_nodes(Operator):
new_node_group.asset_clear() new_node_group.asset_clear()
# self.report({"INFO"}, f"Node updated from {blend_file}") # self.report({"INFO"}, f"Node updated from {blend_file}")
return {'FINISHED'} return {"FINISHED"}
# else: # else:
# self.report({"ERROR"}, f'No Node Group named "{ntree_name}" in the library') # self.report({"ERROR"}, f'No Node Group named "{ntree_name}" in the library')
@ -199,23 +231,23 @@ class NODEKIT_OT_update_nodes(Operator):
class NODEKIT_OT_pack_nodes(Operator): class NODEKIT_OT_pack_nodes(Operator):
bl_idname = 'node_kit.pack_nodes' bl_idname = "node_kit.pack_nodes"
bl_label = 'Update node' bl_label = "Update node"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
combine_objects(context.selected_objects) combine_objects(context.selected_objects)
return {'FINISHED'} return {"FINISHED"}
class NODEKIT_OT_unpack_nodes(Operator): class NODEKIT_OT_unpack_nodes(Operator):
bl_idname = 'node_kit.unpack_nodes' bl_idname = "node_kit.unpack_nodes"
bl_label = 'Update node' bl_label = "Update node"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def execute(self, context): def execute(self, context):
extract_objects(context.active_object) extract_objects(context.active_object)
return {'FINISHED'} return {"FINISHED"}
classes = ( classes = (
@ -224,7 +256,7 @@ classes = (
NODEKIT_OT_remap_node_group_duplicates, NODEKIT_OT_remap_node_group_duplicates,
NODEKIT_OT_update_nodes, NODEKIT_OT_update_nodes,
NODEKIT_OT_pack_nodes, NODEKIT_OT_pack_nodes,
NODEKIT_OT_unpack_nodes NODEKIT_OT_unpack_nodes,
) )

27
ui.py
View File

@ -15,22 +15,29 @@ class NODEKIT_MT_node_kit(bpy.types.Menu):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.operator('node_kit.copy_node_tree', text='Copy Nodes', icon='COPYDOWN') layout.operator("node_kit.copy_node_tree", text="Copy Nodes", icon="COPYDOWN")
layout.operator('node_kit.paste_node_tree', text='Paste Nodes', icon='PASTEDOWN') layout.operator(
"node_kit.paste_node_tree", text="Paste Nodes", icon="PASTEDOWN"
)
layout.separator() layout.separator()
layout.operator('node_kit.remap_node_group_duplicates', text='Remap Node Groups Duplicates', icon='NODE_INSERT_OFF') layout.operator(
layout.operator('node_kit.update_nodes', text='Update Nodes', icon='IMPORT') "node_kit.remap_node_group_duplicates",
text="Remap Node Groups Duplicates",
icon="NODE_INSERT_OFF",
)
layout.operator("node_kit.update_nodes", text="Update Nodes", icon="IMPORT")
layout.separator() layout.separator()
layout.operator('node_kit.pack_nodes', text='Pack Nodes', icon='PACKAGE') layout.operator("node_kit.pack_nodes", text="Pack Nodes", icon="PACKAGE")
layout.operator('node_kit.unpack_nodes', text='UnPack Nodes', icon='UGLYPACKAGE') layout.operator(
"node_kit.unpack_nodes", text="UnPack Nodes", icon="UGLYPACKAGE"
classes = (
NODEKIT_MT_node_kit,
) )
classes = (NODEKIT_MT_node_kit,)
def draw_menu(self, context): def draw_menu(self, context):
self.layout.menu('NODEKIT_MT_node_kit') self.layout.menu("NODEKIT_MT_node_kit")
def register(): def register():